From b4af03c308a63a52e5ee2b5d01202c36e4aa5233 Mon Sep 17 00:00:00 2001 From: RuiqiZhou Date: Thu, 16 Mar 2023 12:06:17 +0800 Subject: [PATCH 1/6] [!] fix interop testcase: antiamplication, multiplexing, zerortt, recovery, keyupdate --- src/tls/xqc_crypto.c | 4 ++-- src/transport/xqc_conn.c | 17 ++++++++++------- src/transport/xqc_engine.c | 2 ++ src/transport/xqc_packet.h | 2 +- src/transport/xqc_send_ctl.c | 4 ++-- src/transport/xqc_send_ctl.h | 2 +- 6 files changed, 18 insertions(+), 13 deletions(-) diff --git a/src/tls/xqc_crypto.c b/src/tls/xqc_crypto.c index 02b5300fb..9cd6f18be 100644 --- a/src/tls/xqc_crypto.c +++ b/src/tls/xqc_crypto.c @@ -661,9 +661,9 @@ xqc_crypto_derive_updated_keys(xqc_crypto_t *crypto, xqc_key_type_t type) /* update application traffic secret */ static uint8_t LABEL[] = "quic ku"; - uint8_t dest_buf[INITIAL_SECRET_MAX_LEN]; + uint8_t dest_buf[XQC_MAX_KNP_LEN]; - ret = xqc_hkdf_expand_label(dest_buf, INITIAL_SECRET_MAX_LEN, + ret = xqc_hkdf_expand_label(dest_buf, current_ckm->secret.len, current_ckm->secret.base, current_ckm->secret.len, LABEL, xqc_lengthof(LABEL), &crypto->md); if (ret != XQC_OK) { diff --git a/src/transport/xqc_conn.c b/src/transport/xqc_conn.c index d0ecb4aa5..ffbc2a328 100644 --- a/src/transport/xqc_conn.c +++ b/src/transport/xqc_conn.c @@ -229,11 +229,11 @@ xqc_conn_init_trans_settings(xqc_connection_t *conn) xqc_conn_set_default_settings(rs); /* set local default setting values */ - ls->max_streams_bidi = 1024; + ls->max_streams_bidi = 128; ls->max_stream_data_bidi_remote = 16 * 1024 * 1024; ls->max_stream_data_bidi_local = 16 * 1024 * 1024; - ls->max_streams_uni = 1024; + ls->max_streams_uni = 128; ls->max_stream_data_uni = 16 * 1024 * 1024; /* max_data is the sum of stream_data on all uni and bidi streams */ @@ -257,11 +257,14 @@ xqc_conn_init_flow_ctl(xqc_connection_t *conn) { xqc_conn_flow_ctl_t *flow_ctl = &conn->conn_flow_ctl; xqc_trans_settings_t * settings = & conn->local_settings; - flow_ctl->fc_max_data_can_send = settings->max_data; /* replace with the value specified by peer after handshake */ + + /* TODO: send params are inited to be zero, until zerortt inited or handshake done */ + flow_ctl->fc_max_data_can_send = 1024 * 1024; /* replace with the value specified by peer after handshake */ + flow_ctl->fc_max_streams_bidi_can_send = 16; /* replace with the value specified by peer after handshake */ + flow_ctl->fc_max_streams_uni_can_send = 16; /* replace with the value specified by peer after handshake */ + flow_ctl->fc_max_data_can_recv = settings->max_data; - flow_ctl->fc_max_streams_bidi_can_send = settings->max_streams_bidi; /* replace with the value specified by peer after handshake */ flow_ctl->fc_max_streams_bidi_can_recv = settings->max_streams_bidi; - flow_ctl->fc_max_streams_uni_can_send = settings->max_streams_uni; /* replace with the value specified by peer after handshake */ flow_ctl->fc_max_streams_uni_can_recv = settings->max_streams_uni; flow_ctl->fc_data_sent = 0; flow_ctl->fc_data_recved = 0; @@ -1067,7 +1070,7 @@ xqc_on_packets_send_burst(xqc_connection_t *conn, xqc_path_ctx_t *path, ssize_t xqc_pacing_on_packet_sent(&send_ctl->ctl_pacing, packet_out->po_used_size); } - xqc_send_ctl_on_packet_sent(send_ctl, pn_ctl, packet_out, now); + xqc_send_ctl_on_packet_sent(send_ctl, pn_ctl, packet_out, now, sent); xqc_path_send_buffer_remove(path, packet_out); if (XQC_IS_ACK_ELICITING(packet_out->po_frame_types)) { xqc_send_queue_insert_unacked(packet_out, @@ -1523,7 +1526,7 @@ xqc_send_packet_with_pn(xqc_connection_t *conn, xqc_path_ctx_t *path, xqc_packet /* deliver packet to send control */ xqc_pn_ctl_t *pn_ctl = xqc_get_pn_ctl(conn, path); pn_ctl->ctl_packet_number[packet_out->po_pkt.pkt_pns]++; - xqc_send_ctl_on_packet_sent(path->path_send_ctl, pn_ctl, packet_out, now); + xqc_send_ctl_on_packet_sent(path->path_send_ctl, pn_ctl, packet_out, now, sent); return sent; } diff --git a/src/transport/xqc_engine.c b/src/transport/xqc_engine.c index b4029227b..5616cb757 100644 --- a/src/transport/xqc_engine.c +++ b/src/transport/xqc_engine.c @@ -717,10 +717,12 @@ xqc_engine_process_conn(xqc_connection_t *conn, xqc_usec_t now) } XQC_CHECK_IMMEDIATE_CLOSE(); +#if 0 ret = xqc_conn_try_add_new_conn_id(conn, 0); if (ret) { xqc_log(conn->log, XQC_LOG_ERROR, "|xqc_conn_try_add_new_conn_id error|"); } +#endif /* for multi-path */ if ((conn->conn_flag & XQC_CONN_FLAG_NEW_CID_RECEIVED) diff --git a/src/transport/xqc_packet.h b/src/transport/xqc_packet.h index 2181932a3..d688ac702 100644 --- a/src/transport/xqc_packet.h +++ b/src/transport/xqc_packet.h @@ -14,7 +14,7 @@ #define XQC_MSS (XQC_QUIC_MIN_MSS + XQC_ACK_SPACE) /* max 0rtt packet count sent by client */ -#define XQC_PACKET_0RTT_MAX_COUNT 30 +#define XQC_PACKET_0RTT_MAX_COUNT 64 /* max 0rtt packet count buffered by server before Initial packet received */ #define XQC_UNDECRYPT_0RTT_MAX_BEFORE_INIT 10 /* max buffered packet count */ diff --git a/src/transport/xqc_send_ctl.c b/src/transport/xqc_send_ctl.c index 7adf85c5d..c6f7a4490 100644 --- a/src/transport/xqc_send_ctl.c +++ b/src/transport/xqc_send_ctl.c @@ -559,7 +559,7 @@ xqc_send_ctl_update_cwnd_limited(xqc_send_ctl_t *send_ctl) * OnPacketSent */ void -xqc_send_ctl_on_packet_sent(xqc_send_ctl_t *send_ctl, xqc_pn_ctl_t *pn_ctl, xqc_packet_out_t *packet_out, xqc_usec_t now) +xqc_send_ctl_on_packet_sent(xqc_send_ctl_t *send_ctl, xqc_pn_ctl_t *pn_ctl, xqc_packet_out_t *packet_out, xqc_usec_t now, ssize_t sent) { xqc_pkt_num_space_t pns = packet_out->po_pkt.pkt_pns; @@ -603,7 +603,7 @@ xqc_send_ctl_on_packet_sent(xqc_send_ctl_t *send_ctl, xqc_pn_ctl_t *pn_ctl, xqc_ pn_ctl->ctl_largest_sent[pns] = packet_out->po_pkt.pkt_num; } - send_ctl->ctl_bytes_send += packet_out->po_used_size; + send_ctl->ctl_bytes_send += sent; if (packet_out->po_largest_ack > 0) { xqc_ack_sent_record_add(&pn_ctl->ack_sent_record[pns], packet_out, send_ctl->ctl_srtt, now); diff --git a/src/transport/xqc_send_ctl.h b/src/transport/xqc_send_ctl.h index 87b5da97e..1475d0344 100644 --- a/src/transport/xqc_send_ctl.h +++ b/src/transport/xqc_send_ctl.h @@ -203,7 +203,7 @@ void xqc_send_ctl_decrease_inflight(xqc_connection_t *conn, xqc_packet_out_t *pa void xqc_send_ctl_on_pns_discard(xqc_send_ctl_t *send_ctl, xqc_pkt_num_space_t pns); -void xqc_send_ctl_on_packet_sent(xqc_send_ctl_t *send_ctl, xqc_pn_ctl_t *pn_ctl, xqc_packet_out_t *packet_out, xqc_usec_t now); +void xqc_send_ctl_on_packet_sent(xqc_send_ctl_t *send_ctl, xqc_pn_ctl_t *pn_ctl, xqc_packet_out_t *packet_out, xqc_usec_t now, ssize_t sent); int xqc_send_ctl_on_ack_received_spns(xqc_connection_t *conn, xqc_ack_info_t *const ack_info, xqc_usec_t ack_recv_time); From 66224a8fc44d6aca4702b5fdc0e19de292bbaa5e Mon Sep 17 00:00:00 2001 From: zhaowu Date: Mon, 12 Jun 2023 15:10:46 +0800 Subject: [PATCH 2/6] [!] fix cid limit check error --- src/transport/xqc_cid.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/transport/xqc_cid.c b/src/transport/xqc_cid.c index e9f29f0db..12b9a3cf4 100644 --- a/src/transport/xqc_cid.c +++ b/src/transport/xqc_cid.c @@ -160,7 +160,7 @@ xqc_destroy_cid_set(xqc_cid_set_t *cid_set) xqc_int_t xqc_cid_set_insert_cid(xqc_cid_set_t *cid_set, xqc_cid_t *cid, xqc_cid_state_t state, uint64_t limit) { - if (cid_set->unused_cnt + cid_set->used_cnt >= limit) { + if (cid_set->unused_cnt + cid_set->used_cnt > limit) { return -XQC_EACTIVE_CID_LIMIT; } From f49b64d55e3c2e424ed1e3f125b7af9770d9a3f8 Mon Sep 17 00:00:00 2001 From: Huanhuan Zheng <140379011+cherylsy@users.noreply.github.com> Date: Fri, 20 Oct 2023 14:38:01 +0800 Subject: [PATCH 3/6] [+] Add new case test & Merge master change into interop branch (#347) * [+] QUIC datagram (RFC9221), sending datagrams and bytestreams on http3 connections (non-standard extensions), and bugfix (#307) *[!] fix vulnerabilities on parsing STREAM and CRYPTO frame; * [+] update contributor * [!] fix test case errors on ubuntu latest * [+] Export SSL handler of HTTP/3 connection for ssl_protocol, ssl_cipher, etc. (#310) * [+] Export SSL handler of HTTP/3 connection for ssl_protocol, ssl_cipher, etc. * [!] fix format of function return * [+] support standard stateless reset (#313) * [+] update multipath to draft-04; (#317) * [+] support pmtu; * fix complie error: specified bound depends on the length of the source argument (#312) * fix complie error: specified bound depends on the length of the source argument * fix stringop-overflow complie error * fix stringop-overflow complie error * Doc/rfc9114 (#223) * Update draft-ietf-quic-http-34-zh.md * Update draft-ietf-quic-http-34-zh.md * Update draft-ietf-quic-http-34-zh.md * Create rfc9114-http3-zh.md * [=] add usage specifications for test_client and test_server (#318) * [+] support multipath interop (#320) * [+] add no_crypto and serrial options for mpquic interop (#321) * Update doc (and comment) multipath-[01|02] => 04 (#326) * demo: fix typo (xuqic -> xquic) (#328) * [+] add .gitignore (#329) * fix: use off_t instead of int for filesize (#332) * [!] fix http3 deadloop and segmentation fault, fix stream compose error (#338) * [+]Enable multipath-05 etc. (#340) * [=] fix README-zh-CN doc link (#341) * [!] fix interop testcase: antiamplication, multiplexing, zerortt, recovery, keyupdate * [~]Fix error during rebase * [+]Add new testcase in run endpoint --------- Co-authored-by: Wu Zhao Co-authored-by: lianglli Co-authored-by: Bobo Co-authored-by: robinhzp <9974622@qq.com> Co-authored-by: Alexis La Goutte Co-authored-by: Yanmei Liu Co-authored-by: Marcel Co-authored-by: RuiqiZhou --- .github/workflows/build.yml | 2 +- .github/workflows/codeql-analysis.yml | 2 +- .gitignore | 2 + CMakeLists.txt | 69 +- CONTRIBUTING.md | 4 + README.md | 11 +- cmake/CMakeLists.txt | 63 +- demo/common.h | 6 +- demo/demo_client.c | 921 ++- demo/demo_server.c | 204 +- demo/xqc_hq.h | 4 + demo/xqc_hq_request.c | 59 +- docs/API.md | 14 + docs/docs-zh/CONTRIBUTING-zh.md | 2 + docs/docs-zh/README-zh.md | 4 +- .../translation/draft-ietf-quic-http-34-zh.md | 164 +- docs/translation/rfc9000-transport-zh.md | 7080 ++++++++--------- docs/translation/rfc9114-http3-zh.md | 1298 +++ include/xquic/xqc_configure.h | 9 + include/xquic/xqc_errno.h | 16 +- include/xquic/xqc_http3.h | 358 +- include/xquic/xquic.h | 529 +- include/xquic/xquic_typedef.h | 33 + interop/run_endpoint.sh | 14 +- scripts/case_test.sh | 2764 ++++++- scripts/xquic.lds | 38 +- scripts/xquic_test.sh | 2 +- src/common/xqc_log.c | 17 +- src/common/xqc_log.h | 1 + src/common/xqc_log_event_callback.c | 23 +- src/common/xqc_log_event_callback.h | 2 + src/common/xqc_str.h | 15 + src/congestion_control/xqc_cubic.c | 15 +- src/congestion_control/xqc_cubic.h | 1 + src/congestion_control/xqc_unlimited_cc.c | 74 + src/congestion_control/xqc_unlimited_cc.h | 13 + src/http3/frame/xqc_h3_frame.c | 44 + src/http3/frame/xqc_h3_frame.h | 17 +- src/http3/frame/xqc_h3_frame_defs.h | 24 +- src/http3/xqc_h3_conn.c | 27 +- src/http3/xqc_h3_conn.h | 36 +- src/http3/xqc_h3_ctx.c | 26 +- src/http3/xqc_h3_defs.c | 7 + src/http3/xqc_h3_defs.h | 9 +- src/http3/xqc_h3_ext_bytestream.c | 725 ++ src/http3/xqc_h3_ext_bytestream.h | 58 + src/http3/xqc_h3_ext_dgram.c | 120 + src/http3/xqc_h3_ext_dgram.h | 12 + src/http3/xqc_h3_request.c | 116 +- src/http3/xqc_h3_request.h | 7 +- src/http3/xqc_h3_stream.c | 558 +- src/http3/xqc_h3_stream.h | 19 +- src/tls/babassl/xqc_crypto.c | 4 +- src/tls/boringssl/xqc_crypto.c | 4 +- src/tls/xqc_tls.c | 27 +- src/tls/xqc_tls.h | 8 + .../reinjection_control/xqc_reinj_deadline.c | 99 + .../reinjection_control/xqc_reinj_deadline.h | 18 + .../reinjection_control/xqc_reinj_default.c | 89 + .../reinjection_control/xqc_reinj_default.h | 18 + .../reinjection_control/xqc_reinj_dgram.c | 86 + .../reinjection_control/xqc_reinj_dgram.h | 18 + .../reinjection_control/xqc_reinj_xlink.c | 80 - .../reinjection_control/xqc_reinj_xlink.h | 17 - .../scheduler/xqc_scheduler_backup.c | 308 +- .../scheduler/xqc_scheduler_interop.c | 115 + .../scheduler/xqc_scheduler_interop.h | 13 + .../scheduler/xqc_scheduler_minrtt.c | 22 +- .../scheduler/xqc_scheduler_minrtt.h | 4 - src/transport/scheduler/xqc_scheduler_rap.c | 112 + src/transport/scheduler/xqc_scheduler_rap.h | 19 + src/transport/xqc_cid.c | 10 + src/transport/xqc_cid.h | 3 + src/transport/xqc_client.c | 12 +- src/transport/xqc_conn.c | 1949 ++++- src/transport/xqc_conn.h | 161 +- src/transport/xqc_datagram.c | 491 ++ src/transport/xqc_datagram.h | 39 + src/transport/xqc_defs.h | 10 +- src/transport/xqc_engine.c | 363 +- src/transport/xqc_engine.h | 1 + src/transport/xqc_frame.c | 385 +- src/transport/xqc_frame.h | 6 +- src/transport/xqc_frame_parser.c | 448 +- src/transport/xqc_frame_parser.h | 35 +- src/transport/xqc_multipath.c | 620 +- src/transport/xqc_multipath.h | 74 +- src/transport/xqc_pacing.c | 2 +- src/transport/xqc_packet.c | 2 +- src/transport/xqc_packet.h | 4 +- src/transport/xqc_packet_out.c | 638 +- src/transport/xqc_packet_out.h | 53 +- src/transport/xqc_packet_parser.c | 159 +- src/transport/xqc_packet_parser.h | 15 +- src/transport/xqc_recv_record.c | 28 +- src/transport/xqc_recv_record.h | 6 +- src/transport/xqc_reinjection.c | 121 +- src/transport/xqc_reinjection.h | 20 +- src/transport/xqc_send_ctl.c | 561 +- src/transport/xqc_send_ctl.h | 67 +- src/transport/xqc_send_queue.c | 63 +- src/transport/xqc_send_queue.h | 11 +- src/transport/xqc_stream.c | 304 +- src/transport/xqc_stream.h | 14 +- src/transport/xqc_timer.c | 114 +- src/transport/xqc_timer.h | 119 + src/transport/xqc_transport_params.c | 106 +- src/transport/xqc_transport_params.h | 23 +- src/transport/xqc_utils.c | 27 +- src/transport/xqc_utils.h | 9 +- tests/CMakeLists.txt | 3 +- tests/test_client.c | 1744 +++- tests/test_server.c | 884 +- tests/unittest/main.c | 7 +- tests/unittest/xqc_datagram_test.c | 49 + tests/unittest/xqc_datagram_test.h | 10 + tests/unittest/xqc_engine_test.c | 15 + tests/unittest/xqc_h3_ext_test.c | 87 + tests/unittest/xqc_h3_ext_test.h | 10 + tests/unittest/xqc_h3_test.c | 2 +- tests/unittest/xqc_process_frame_test.c | 6 - tests/unittest/xqc_reno_test.c | 2 +- tests/unittest/xqc_sent_record_test.c | 107 - tests/unittest/xqc_sent_record_test.h | 12 - xqc_build.sh | 20 +- xqc_configure.h.in | 8 +- 126 files changed, 19966 insertions(+), 6772 deletions(-) create mode 100644 docs/translation/rfc9114-http3-zh.md create mode 100644 include/xquic/xqc_configure.h create mode 100644 src/congestion_control/xqc_unlimited_cc.c create mode 100644 src/congestion_control/xqc_unlimited_cc.h create mode 100644 src/http3/xqc_h3_ext_bytestream.c create mode 100644 src/http3/xqc_h3_ext_bytestream.h create mode 100644 src/http3/xqc_h3_ext_dgram.c create mode 100644 src/http3/xqc_h3_ext_dgram.h create mode 100644 src/transport/reinjection_control/xqc_reinj_deadline.c create mode 100644 src/transport/reinjection_control/xqc_reinj_deadline.h create mode 100644 src/transport/reinjection_control/xqc_reinj_default.c create mode 100644 src/transport/reinjection_control/xqc_reinj_default.h create mode 100644 src/transport/reinjection_control/xqc_reinj_dgram.c create mode 100644 src/transport/reinjection_control/xqc_reinj_dgram.h delete mode 100644 src/transport/reinjection_control/xqc_reinj_xlink.c delete mode 100644 src/transport/reinjection_control/xqc_reinj_xlink.h create mode 100644 src/transport/scheduler/xqc_scheduler_interop.c create mode 100644 src/transport/scheduler/xqc_scheduler_interop.h create mode 100644 src/transport/scheduler/xqc_scheduler_rap.c create mode 100644 src/transport/scheduler/xqc_scheduler_rap.h create mode 100644 src/transport/xqc_datagram.c create mode 100644 src/transport/xqc_datagram.h create mode 100644 tests/unittest/xqc_datagram_test.c create mode 100644 tests/unittest/xqc_datagram_test.h create mode 100644 tests/unittest/xqc_h3_ext_test.c create mode 100644 tests/unittest/xqc_h3_ext_test.h delete mode 100644 tests/unittest/xqc_sent_record_test.c delete mode 100644 tests/unittest/xqc_sent_record_test.h diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 71cd089cb..0b9665755 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -61,7 +61,7 @@ jobs: SSL_LIB_PATH_STR="${PWD}/third_party/boringssl/build/ssl/libssl.a;${PWD}/third_party/boringssl/build/crypto/libcrypto.a" mkdir -p build cd build - cmake -DGCOV=on -DCMAKE_BUILD_TYPE=Debug -DXQC_ENABLE_TESTING=1 -DXQC_SUPPORT_SENDMMSG_BUILD=1 -DXQC_ENABLE_EVENT_LOG=1 -DXQC_ENABLE_BBR2=1 -DXQC_DISABLE_RENO=0 -DSSL_TYPE=${SSL_TYPE_STR} -DSSL_PATH=${SSL_PATH_STR} -DSSL_INC_PATH=${SSL_INC_PATH_STR} -DSSL_LIB_PATH=${SSL_LIB_PATH_STR} .. + cmake -DGCOV=on -DCMAKE_BUILD_TYPE=Debug -DXQC_ENABLE_TESTING=1 -DXQC_SUPPORT_SENDMMSG_BUILD=1 -DXQC_ENABLE_EVENT_LOG=1 -DXQC_ENABLE_BBR2=1 -DXQC_ENABLE_RENO=1 -DSSL_TYPE=${SSL_TYPE_STR} -DSSL_PATH=${SSL_PATH_STR} -DSSL_INC_PATH=${SSL_INC_PATH_STR} -DSSL_LIB_PATH=${SSL_LIB_PATH_STR} .. make -j - name: Test diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 158ba07b2..6445f8af7 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -60,7 +60,7 @@ jobs: SSL_LIB_PATH_STR="${PWD}/third_party/boringssl/build/ssl/libssl.a;${PWD}/third_party/boringssl/build/crypto/libcrypto.a" mkdir -p build cd build - cmake -DGCOV=on -DCMAKE_BUILD_TYPE=Debug -DXQC_ENABLE_TESTING=1 -DXQC_SUPPORT_SENDMMSG_BUILD=1 -DXQC_ENABLE_EVENT_LOG=1 -DXQC_ENABLE_BBR2=1 -DXQC_DISABLE_RENO=0 -DSSL_TYPE=${SSL_TYPE_STR} -DSSL_PATH=${SSL_PATH_STR} -DSSL_INC_PATH=${SSL_INC_PATH_STR} -DSSL_LIB_PATH=${SSL_LIB_PATH_STR} .. + cmake -DGCOV=on -DCMAKE_BUILD_TYPE=Debug -DXQC_ENABLE_TESTING=1 -DXQC_SUPPORT_SENDMMSG_BUILD=1 -DXQC_ENABLE_EVENT_LOG=1 -DXQC_ENABLE_BBR2=1 -DXQC_ENABLE_RENO=1 -DSSL_TYPE=${SSL_TYPE_STR} -DSSL_PATH=${SSL_PATH_STR} -DSSL_INC_PATH=${SSL_INC_PATH_STR} -DSSL_LIB_PATH=${SSL_LIB_PATH_STR} .. make -j - name: Perform CodeQL Analysis diff --git a/.gitignore b/.gitignore index 9a830f3e3..27a483fe7 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,5 @@ build/ bss/ *.swp third_party/ +.idea/ +cmake-build-debug/ diff --git a/CMakeLists.txt b/CMakeLists.txt index d675d9c12..304b635d3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,7 +7,12 @@ set (xquic_VERSION_MAJOR 0) set (xquic_VERSION_MINOR 1) set(CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake") -# build type +option (XQC_ENABLE_BBR2 "enable bbr2" ON) +option (XQC_ENABLE_COPA "enable copa" ON) +option (XQC_ENABLE_RENO "enable reno" ON) +option (XQC_ENABLE_UNLIMITED "enable unlimited cc" ON) +option (XQC_ENABLE_MP_INTEROP "enable MPQUIC interop" ON) + if(NOT CMAKE_BUILD_TYPE) set(CMAKE_BUILD_TYPE Release) endif() @@ -74,6 +79,9 @@ if(XQC_COMPAT_DUPLICATE) set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DXQC_COMPAT_DUPLICATE") endif() +if(XQC_COMPAT_GENERATE_SR_PKT) + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DXQC_COMPAT_GENERATE_SR_PKT") +endif() # print only error log if(XQC_ONLY_ERROR_LOG) @@ -116,8 +124,8 @@ endif() # configure file configure_file ( - xqc_configure.h.in - xqc_configure.h + "${CMAKE_CURRENT_SOURCE_DIR}/xqc_configure.h.in" + "${CMAKE_CURRENT_SOURCE_DIR}/include/xquic/xqc_configure.h" ) include_directories( @@ -127,8 +135,6 @@ include_directories( "${CMAKE_CURRENT_BINARY_DIR}/include" ) - - # http3/qpack source set( HTTP3_SOURCES @@ -148,6 +154,8 @@ set( "src/http3/qpack/xqc_ins.c" "src/http3/qpack/stable/xqc_stable.c" "src/http3/qpack/dtable/xqc_dtable.c" + "src/http3/xqc_h3_ext_dgram.c" + "src/http3/xqc_h3_ext_bytestream.c" ) # Transport source @@ -160,6 +168,7 @@ set( "src/transport/xqc_packet_parser.c" "src/transport/xqc_frame_parser.c" "src/transport/xqc_stream.c" + "src/transport/xqc_datagram.c" "src/transport/xqc_packet_out.c" "src/transport/xqc_packet_in.c" "src/transport/xqc_send_ctl.c" @@ -175,12 +184,23 @@ set( "src/transport/xqc_quic_lb.c" "src/transport/xqc_timer.c" "src/transport/xqc_reinjection.c" - "src/transport/reinjection_control/xqc_reinj_xlink.c" + "src/transport/reinjection_control/xqc_reinj_default.c" + "src/transport/reinjection_control/xqc_reinj_deadline.c" + "src/transport/reinjection_control/xqc_reinj_dgram.c" "src/transport/scheduler/xqc_scheduler_minrtt.c" "src/transport/scheduler/xqc_scheduler_common.c" "src/transport/scheduler/xqc_scheduler_backup.c" + "src/transport/scheduler/xqc_scheduler_rap.c" ) +if(XQC_ENABLE_MP_INTEROP) + set( + TRANSPORT_SOURCES + ${TRANSPORT_SOURCES} + "src/transport/scheduler/xqc_scheduler_interop.c" + ) +endif() + # TLS source set ( TLS_SOURCE @@ -237,9 +257,7 @@ set( "src/congestion_control/xqc_sample.c" ) -if(XQC_DISABLE_RENO) - set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DXQC_DISABLE_RENO ") -else() +if(XQC_ENABLE_RENO) set( CONGESTION_CONTROL_SOURCES ${CONGESTION_CONTROL_SOURCES} @@ -248,18 +266,33 @@ else() endif() if(XQC_ENABLE_BBR2) - set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DXQC_ENABLE_BBR2 ") set( CONGESTION_CONTROL_SOURCES - ${CONGESTION_CONTROL_SOURCES} "src/congestion_control/xqc_bbr2.c" ) endif() +if(XQC_ENABLE_COPA) + set( + CONGESTION_CONTROL_SOURCES + ${CONGESTION_CONTROL_SOURCES} + "src/congestion_control/xqc_copa.c" + ) +endif() + +if(XQC_ENABLE_UNLIMITED) + set( + CONGESTION_CONTROL_SOURCES + ${CONGESTION_CONTROL_SOURCES} + "src/congestion_control/xqc_unlimited_cc.c" + ) +endif() + + # xquic source set ( - XQC_SOURCE + XQC_SOURCE ${HTTP3_SOURCES} ${TRANSPORT_SOURCES} ${TLS_SOURCE} @@ -383,13 +416,13 @@ if (XQC_ENABLE_TESTING) endif() if(PLATFORM STREQUAL "mac32") - target_link_libraries(test_server xquic -lm ${CMAKE_CURRENT_SOURCE_DIR}/../libevent32/lib/libevent.dylib) - target_link_libraries(test_client xquic -lm ${CMAKE_CURRENT_SOURCE_DIR}/../libevent32/lib/libevent.dylib) + target_link_libraries(test_server xquic-static ${SSL_LIB_PATH} -ldl -lpthread -lm ${CMAKE_CURRENT_SOURCE_DIR}/../libevent32/lib/libevent.dylib) + target_link_libraries(test_client xquic-static ${SSL_LIB_PATH} -ldl -lpthread -lm ${CMAKE_CURRENT_SOURCE_DIR}/../libevent32/lib/libevent.dylib) target_link_libraries(demo_server xquic -lm ${CMAKE_CURRENT_SOURCE_DIR}/../libevent32/lib/libevent.dylib) target_link_libraries(demo_client xquic -lm ${CMAKE_CURRENT_SOURCE_DIR}/../libevent32/lib/libevent.dylib) elseif(PLATFORM STREQUAL "mac") - target_link_libraries(test_server xquic -lm -L/usr/local/lib -levent) - target_link_libraries(test_client xquic -lm -L/usr/local/lib -levent) + target_link_libraries(test_server xquic-static ${SSL_LIB_PATH} -ldl -lpthread -lm -L/usr/local/lib -levent) + target_link_libraries(test_client xquic-static ${SSL_LIB_PATH} -ldl -lpthread -lm -L/usr/local/lib -levent) target_link_libraries(demo_server xquic -lm -L/usr/local/lib -levent) target_link_libraries(demo_client xquic -lm -L/usr/local/lib -levent) elseif(CMAKE_SYSTEM_NAME MATCHES "Windows") @@ -398,8 +431,8 @@ if (XQC_ENABLE_TESTING) target_link_libraries(demo_server xquic ${EVENT_LIB_PATH} -lm) target_link_libraries(demo_client xquic ${EVENT_LIB_PATH} -lm) else() - target_link_libraries(test_server xquic -levent -lm) - target_link_libraries(test_client xquic -levent -lm) + target_link_libraries(test_server xquic-static ${SSL_LIB_PATH} -ldl -lpthread -levent -lm) + target_link_libraries(test_client xquic-static ${SSL_LIB_PATH} -ldl -lpthread -levent -lm) target_link_libraries(demo_server xquic -levent -lm) target_link_libraries(demo_client xquic -levent -lm) endif() diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 40cd59b57..a6186e2e9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -203,6 +203,8 @@ In no particular order, thanks to these excellent individuals who contributed co * 曾柯(毅丝) * 徐盟欣(象谦) * Bai Shi(白石) +* 周瑞琪(凼凼) +* 李亮(海苍) * @chinsyo * @L1MeN9Yu * @flx413 @@ -217,5 +219,7 @@ In no particular order, thanks to these excellent individuals who contributed co * @yunwei37 * @keengo99 * @ruanshanshan +* @alagoutte +* @MPK1 This list will be continuously updated. Contributions are welcome! diff --git a/README.md b/README.md index 83b2e69a9..a6fba543a 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ [![Codacy Badge](https://app.codacy.com/project/badge/Coverage/485e758edd98409bb7a51cbb803838c4)](https://www.codacy.com/gh/alibaba/xquic/dashboard?utm_source=github.com&utm_medium=referral&utm_content=alibaba/xquic&utm_campaign=Badge_Coverage) ![Platforms](https://img.shields.io/badge/platform-Android%20%7C%20iOS%20%7C%20Linux%20%7C%20macOS-blue) -> [简体中文文档 README-zh-CN](./docs/docs-zh/README-zh.md) +> [简体中文文档 README-zh-CN](https://github.com/alibaba/xquic/blob/main/docs/docs-zh/README-zh.md) ## Introduction @@ -30,7 +30,8 @@ XQUIC Library released by Alibaba is … [![](https://img.shields.io/static/v1?label=draft-13&message=QUIC-LB&color=9cf)](https://tools.ietf.org/html/draft-ietf-quic-load-balancers-13) -[![](https://img.shields.io/static/v1?label=draft-01&message=Multipath-QUIC&color=9cf)](https://tools.ietf.org/html/draft-ietf-quic-multipath-01) +[![](https://img.shields.io/static/v1?label=draft-04&message=Multipath-QUIC&color=9cf)](https://tools.ietf.org/html/draft-ietf-quic-multipath-04) +[![](https://img.shields.io/static/v1?label=draft-05&message=Multipath-QUIC&color=9cf)](https://tools.ietf.org/html/draft-ietf-quic-multipath-05) #### Standardized Features @@ -40,7 +41,7 @@ XQUIC Library released by Alibaba is … #### Not Yet Standardized Features -* [Multipath QUIC](https://tools.ietf.org/html/draft-ietf-quic-multipath-02) +* [Multipath QUIC](https://tools.ietf.org/html/draft-ietf-quic-multipath-04) * [QUIC-LB](https://tools.ietf.org/html/draft-ietf-quic-load-balancers-13) @@ -89,7 +90,7 @@ cd ../.. # build XQUIC with BoringSSL git submodule update --init --recursive mkdir -p build; cd build -cmake -DGCOV=on -DCMAKE_BUILD_TYPE=Debug -DXQC_ENABLE_TESTING=1 -DXQC_SUPPORT_SENDMMSG_BUILD=1 -DXQC_ENABLE_EVENT_LOG=1 -DXQC_ENABLE_BBR2=1 -DXQC_DISABLE_RENO=0 -DSSL_TYPE=${SSL_TYPE_STR} -DSSL_PATH=${SSL_PATH_STR} -DSSL_INC_PATH=${SSL_INC_PATH_STR} -DSSL_LIB_PATH=${SSL_LIB_PATH_STR} .. +cmake -DGCOV=on -DCMAKE_BUILD_TYPE=Debug -DXQC_ENABLE_TESTING=1 -DXQC_SUPPORT_SENDMMSG_BUILD=1 -DXQC_ENABLE_EVENT_LOG=1 -DXQC_ENABLE_BBR2=1 -DXQC_ENABLE_RENO=1 -DSSL_TYPE=${SSL_TYPE_STR} -DSSL_PATH=${SSL_PATH_STR} -DSSL_INC_PATH=${SSL_INC_PATH_STR} -DSSL_LIB_PATH=${SSL_LIB_PATH_STR} .. make -j ``` @@ -114,7 +115,7 @@ cd - # build XQUIC with BabaSSL git submodule update --init --recursive mkdir -p build; cd build -cmake -DGCOV=on -DCMAKE_BUILD_TYPE=Debug -DXQC_ENABLE_TESTING=1 -DXQC_SUPPORT_SENDMMSG_BUILD=1 -DXQC_ENABLE_EVENT_LOG=1 -DXQC_ENABLE_BBR2=1 -DXQC_DISABLE_RENO=0 -DSSL_TYPE=${SSL_TYPE_STR} -DSSL_PATH=${SSL_PATH_STR} -DSSL_INC_PATH=${SSL_INC_PATH_STR} -DSSL_LIB_PATH=${SSL_LIB_PATH_STR} .. +cmake -DGCOV=on -DCMAKE_BUILD_TYPE=Debug -DXQC_ENABLE_TESTING=1 -DXQC_SUPPORT_SENDMMSG_BUILD=1 -DXQC_ENABLE_EVENT_LOG=1 -DXQC_ENABLE_BBR2=1 -DXQC_ENABLE_RENO=1 -DSSL_TYPE=${SSL_TYPE_STR} -DSSL_PATH=${SSL_PATH_STR} -DSSL_INC_PATH=${SSL_INC_PATH_STR} -DSSL_LIB_PATH=${SSL_LIB_PATH_STR} .. make -j ``` diff --git a/cmake/CMakeLists.txt b/cmake/CMakeLists.txt index 4e6b54fa2..27e951a88 100644 --- a/cmake/CMakeLists.txt +++ b/cmake/CMakeLists.txt @@ -63,6 +63,10 @@ if (XQC_DISABLE_LOG) set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DXQC_DISABLE_LOG") endif() +if(XQC_COMPAT_GENERATE_SR_PKT) + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DXQC_COMPAT_GENERATE_SR_PKT") +endif() + if (XQC_ONLY_ERROR_LOG) set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DXQC_ONLY_ERROR_LOG") endif() @@ -121,7 +125,11 @@ else() endif() -configure_file (xqc_configure.h.in include/xquic/xqc_configure.h @ONLY) +configure_file ( + "${CMAKE_CURRENT_SOURCE_DIR}/xqc_configure.h.in" + "${CMAKE_CURRENT_SOURCE_DIR}/include/xquic/xqc_configure.h" + @ONLY +) include_directories( include/ @@ -149,6 +157,8 @@ set( "src/http3/qpack/xqc_ins.c" "src/http3/qpack/stable/xqc_stable.c" "src/http3/qpack/dtable/xqc_dtable.c" + "src/http3/xqc_h3_ext_dgram.c" + "src/http3/xqc_h3_ext_bytestream.c" ) # Transport source @@ -161,6 +171,7 @@ set( "src/transport/xqc_packet_parser.c" "src/transport/xqc_frame_parser.c" "src/transport/xqc_stream.c" + "src/transport/xqc_datagram.c" "src/transport/xqc_packet_out.c" "src/transport/xqc_packet_in.c" "src/transport/xqc_send_ctl.c" @@ -176,12 +187,23 @@ set( "src/transport/xqc_quic_lb.c" "src/transport/xqc_timer.c" "src/transport/xqc_reinjection.c" - "src/transport/reinjection_control/xqc_reinj_xlink.c" + "src/transport/reinjection_control/xqc_reinj_default.c" + "src/transport/reinjection_control/xqc_reinj_deadline.c" + "src/transport/reinjection_control/xqc_reinj_dgram.c" "src/transport/scheduler/xqc_scheduler_minrtt.c" "src/transport/scheduler/xqc_scheduler_common.c" "src/transport/scheduler/xqc_scheduler_backup.c" + "src/transport/scheduler/xqc_scheduler_rap.c" ) +if(XQC_ENABLE_MP_INTEROP) + set( + TRANSPORT_SOURCES + ${TRANSPORT_SOURCES} + "src/transport/scheduler/xqc_scheduler_interop.c" + ) +endif() + # TLS source set ( TLS_SOURCE @@ -238,9 +260,7 @@ set( "src/congestion_control/xqc_sample.c" ) -if (XQC_DISABLE_RENO) - set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DXQC_DISABLE_RENO") -else() +if(XQC_ENABLE_RENO) set( CONGESTION_CONTROL_SOURCES ${CONGESTION_CONTROL_SOURCES} @@ -248,8 +268,7 @@ else() ) endif() -if (XQC_ENABLE_BBR2) - set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DXQC_ENABLE_BBR2") +if(XQC_ENABLE_BBR2) set( CONGESTION_CONTROL_SOURCES ${CONGESTION_CONTROL_SOURCES} @@ -257,6 +276,35 @@ if (XQC_ENABLE_BBR2) ) endif() +if(XQC_ENABLE_COPA) + set( + CONGESTION_CONTROL_SOURCES + ${CONGESTION_CONTROL_SOURCES} + "src/congestion_control/xqc_copa.c" + ) +endif() + +if(XQC_ENABLE_UNLIMITED) + set( + CONGESTION_CONTROL_SOURCES + ${CONGESTION_CONTROL_SOURCES} + "src/congestion_control/xqc_unlimited_cc.c" + ) +endif() + + +if(XQC_ENABLE_TH3) + set(XQC_ENABLE_TUNNEL 1) + set( + TH3_SOURCES + "tunnel/tunnel_h3/th3_ctx.c" + "tunnel/tunnel_h3/th3_vconn.c" + ) +endif() + + ) +endif() + if (XQC_NO_SHARED) set(XQC_BINARY_TYPE STATIC) endif() @@ -298,3 +346,4 @@ if (XQC_BUILD_SAMPLE) target_link_libraries(test_client xquic event) target_link_libraries(test_server xquic event) endif() + diff --git a/demo/common.h b/demo/common.h index 6ced07f35..a6fd22808 100644 --- a/demo/common.h +++ b/demo/common.h @@ -12,10 +12,10 @@ #define CIPHER_SUIT_LEN 256 #define TLS_GROUPS_LEN 64 -#define PATH_LEN 512 -#define RESOURCE_LEN 256 +#define PATH_LEN 1024 +#define RESOURCE_LEN 1024 #define AUTHORITY_LEN 128 -#define URL_LEN 512 +#define URL_LEN 1024 /* the congestion control types */ typedef enum cc_type_s { diff --git a/demo/demo_client.c b/demo/demo_client.c index ab956df28..64b59c2d8 100644 --- a/demo/demo_client.c +++ b/demo/demo_client.c @@ -11,6 +11,8 @@ #include #include #include +#include +#include #include #include #include @@ -27,6 +29,7 @@ #define XQC_PACKET_TMP_BUF_LEN 1600 #define MAX_BUF_SIZE (100*1024*1024) #define XQC_INTEROP_TLS_GROUPS "X25519:P-256:P-384:P-521" +#define MAX_PATH_CNT 2 typedef enum xqc_demo_cli_alpn_type_s { @@ -108,6 +111,14 @@ typedef struct xqc_demo_cli_net_config_s { xqc_demo_cli_task_mode_t mode; + char iflist[MAX_PATH_CNT][128]; /* list of interfaces */ + int ifcnt; + + int multipath; + + uint8_t rebind_p0; + uint8_t rebind_p1; + } xqc_demo_cli_net_config_t; /** @@ -119,9 +130,9 @@ typedef struct xqc_demo_cli_net_config_s { */ /* definition for quic */ -#define MAX_SESSION_TICKET_LEN 2048 /* session ticket len */ -#define MAX_TRANSPORT_PARAMS_LEN 2048 /* transport parameter len */ -#define XQC_MAX_TOKEN_LEN 256 /* token len */ +#define MAX_SESSION_TICKET_LEN 8192 /* session ticket len */ +#define MAX_TRANSPORT_PARAMS_LEN 8192 /* transport parameter len */ +#define XQC_MAX_TOKEN_LEN 8192 /* token len */ #define SESSION_TICKET_FILE "session_ticket" #define TRANSPORT_PARAMS_FILE "transport_params" @@ -145,6 +156,21 @@ typedef struct xqc_demo_cli_quic_config_s { uint8_t use_0rtt; /* 0-rtt switch, default turned off */ uint64_t keyupdate_pkt_threshold; /* packet limit of a single 1-rtt key, 0 for unlimited */ + uint8_t mp_ack_on_any_path; + + char mp_sched[32]; + uint8_t mp_backup; + + uint64_t close_path; + + uint8_t no_encryption; + + uint64_t recv_rate; + + uint32_t reinjection; + + uint8_t mp_version; + } xqc_demo_cli_quic_config_t; @@ -208,6 +234,20 @@ typedef struct xqc_demo_cli_requests_s { char urls[MAX_REQUEST_CNT * MAX_REQUEST_LEN]; int request_cnt; /* requests cnt in urls */ xqc_demo_cli_request_t reqs[MAX_REQUEST_CNT]; + + /* do not save responses to files */ + int dummy_mode; + + /* delay X us to start reqs */ + uint64_t req_start_delay; + + uint64_t idle_gap; + + /* serial requests */ + uint8_t serial; + + int throttled_req; + } xqc_demo_cli_requests_t; @@ -313,26 +353,121 @@ typedef struct xqc_demo_cli_ctx_s { xqc_demo_cli_task_ctx_t task_ctx; } xqc_demo_cli_ctx_t; +typedef struct xqc_demo_cli_user_path_s { + + uint64_t path_id; + uint8_t is_active; -typedef struct xqc_demo_cli_user_conn_s { int fd; - xqc_cid_t cid; - xqc_hq_conn_t *hqc_handle; + int rebind_fd; struct sockaddr_in6 local_addr; socklen_t local_addrlen; - + struct sockaddr_in6 peer_addr; + socklen_t peer_addrlen; + struct event *ev_socket; + struct event *ev_rebind_socket; struct event *ev_timeout; - xqc_demo_cli_ctx_t *ctx; uint64_t last_sock_op_time; - xqc_demo_cli_task_t *task; + + xqc_demo_cli_user_conn_t *user_conn; + +} xqc_demo_cli_user_path_t; + + +typedef struct xqc_demo_cli_user_conn_s { + + xqc_cid_t cid; + xqc_hq_conn_t *hqc_handle; + + xqc_demo_cli_user_path_t paths[MAX_PATH_CNT]; + int active_path_cnt; + int total_path_cnt; + + struct event *ev_delay_req; + struct event *ev_idle_restart; + struct event *ev_close_path; + struct event *ev_rebinding_p0; + struct event *ev_rebinding_p1; + + xqc_demo_cli_ctx_t *ctx; + xqc_demo_cli_task_t *task; } xqc_demo_cli_user_conn_t; +static void +xqc_demo_cli_delayed_idle_restart(int fd, short what, void *arg); + void xqc_demo_cli_continue_send_reqs(xqc_demo_cli_user_conn_t *user_conn); +void +xqc_demo_cli_send_requests(xqc_demo_cli_user_conn_t *user_conn, + xqc_demo_cli_client_args_t *args, + xqc_demo_cli_request_t *reqs, int req_cnt); + + +int xqc_demo_cli_init_user_path(xqc_demo_cli_user_conn_t *user_conn, + int path_seq, uint64_t path_id); + +int +xqc_demo_cli_close_task(xqc_demo_cli_task_t *task) +{ + xqc_demo_cli_user_conn_t *user_conn = task->user_conn; + int i; + + for (i = 0; i < user_conn->total_path_cnt; i++) { + if (user_conn->paths[i].is_active) { + user_conn->paths[i].is_active = 0; + user_conn->active_path_cnt--; + /* remove event handle */ + if (user_conn->paths[i].ev_socket) { + event_del(user_conn->paths[i].ev_socket); + } + + event_del(user_conn->paths[i].ev_timeout); + /* close socket */ + close(user_conn->paths[i].fd); + + if (user_conn->paths[i].ev_rebind_socket) { + event_del(user_conn->paths[i].ev_rebind_socket); + } + + if (user_conn->paths[i].rebind_fd != -1) { + close(user_conn->paths[i].rebind_fd); + } + } + } + + if (user_conn->ev_delay_req) { + event_del(user_conn->ev_delay_req); + user_conn->ev_delay_req = NULL; + } + + if (user_conn->ev_idle_restart) { + event_del(user_conn->ev_idle_restart); + user_conn->ev_idle_restart = NULL; + } + + if (user_conn->ev_close_path) { + event_del(user_conn->ev_close_path); + user_conn->ev_close_path = NULL; + } + + if (user_conn->ev_rebinding_p0) { + event_del(user_conn->ev_rebinding_p0); + user_conn->ev_rebinding_p0 = NULL; + } + + if (user_conn->ev_rebinding_p1) { + event_del(user_conn->ev_rebinding_p1); + user_conn->ev_rebinding_p1 = NULL; + } + + return 0; +} + /** * [return] 1: all req suc, task finished, 0: still got req underway @@ -341,30 +476,33 @@ void xqc_demo_cli_on_stream_fin(xqc_demo_cli_user_stream_t *user_stream) { xqc_demo_cli_task_ctx_t *ctx = &user_stream->user_conn->ctx->task_ctx; + xqc_demo_cli_user_conn_t *user_conn = user_stream->user_conn; + xqc_demo_cli_ctx_t *conn_ctx = user_conn->ctx; int task_idx = user_stream->user_conn->task->task_idx; - /* all reqs are finished, finish the task */ + /* all reqs are finished, close the connection */ if (++ctx->schedule.schedule_info[task_idx].req_fin_cnt == ctx->tasks[task_idx].req_cnt) { ctx->schedule.schedule_info[task_idx].fin_flag = 1; + /* close xquic conn */ + if (conn_ctx->args->quic_cfg.alpn_type == ALPN_H3) { + xqc_h3_conn_close(conn_ctx->engine, &user_conn->cid); + + } else { + xqc_hq_conn_close(conn_ctx->engine, user_conn->hqc_handle, &user_conn->cid); + } } printf("task[%d], fin_cnt: %d, fin_flag: %d\n", task_idx, ctx->schedule.schedule_info[task_idx].req_fin_cnt, ctx->schedule.schedule_info[task_idx].fin_flag); - - /* TODO: fix MAX_STREAMS */ - if (ctx->schedule.schedule_info[task_idx].req_create_cnt - < ctx->tasks[task_idx].user_conn->task->req_cnt) - { - xqc_demo_cli_continue_send_reqs(ctx->tasks[task_idx].user_conn); - } } /* directly finish a task */ void xqc_demo_cli_on_task_finish(xqc_demo_cli_ctx_t *ctx, xqc_demo_cli_task_t *task) { + xqc_demo_cli_close_task(task); ctx->task_ctx.schedule.schedule_info[task->task_idx].status = TASK_STATUS_FINISHED; printf("task finished, total task_req_cnt: %d, req_fin_cnt: %d, req_sent_cnt: %d, " @@ -449,9 +587,11 @@ xqc_demo_cli_write_log_file(xqc_log_level_t lvl, const void *buf, size_t size, v int xqc_demo_cli_open_keylog_file(xqc_demo_cli_ctx_t *ctx) { - ctx->keylog_fd = open(ctx->args->env_cfg.key_out_path, (O_WRONLY | O_APPEND | O_CREAT), 0644); - if (ctx->keylog_fd <= 0) { - return -1; + if (ctx->args->env_cfg.key_output_flag) { + ctx->keylog_fd = open(ctx->args->env_cfg.key_out_path, (O_WRONLY | O_APPEND | O_CREAT), 0644); + if (ctx->keylog_fd <= 0) { + return -1; + } } return 0; @@ -469,9 +609,14 @@ xqc_demo_cli_close_keylog_file(xqc_demo_cli_ctx_t *ctx) } void -xqc_demo_cli_keylog_cb(const char *line, void *engine_user_data) +xqc_demo_cli_keylog_cb(const xqc_cid_t *scid, const char *line, void *engine_user_data) { xqc_demo_cli_ctx_t *ctx = (xqc_demo_cli_ctx_t*)engine_user_data; + + if (ctx->args->env_cfg.key_output_flag == 0) { + return; + } + if (ctx->keylog_fd <= 0) { printf("write keys error!\n"); return; @@ -563,29 +708,55 @@ xqc_demo_cli_read_token(unsigned char *token, unsigned token_len) ssize_t -xqc_demo_cli_write_socket(const unsigned char *buf, size_t size, const struct sockaddr *peer_addr, - socklen_t peer_addrlen, void *conn_user_data) +xqc_demo_cli_write_socket_ex(uint64_t path_id, const unsigned char *buf, size_t size, + const struct sockaddr *peer_addr, socklen_t peer_addrlen, void *conn_user_data) { xqc_demo_cli_user_conn_t *user_conn = (xqc_demo_cli_user_conn_t *)conn_user_data; ssize_t res = 0; + xqc_demo_cli_user_path_t *user_path = NULL; + int i; + + for (i = 0; i < user_conn->total_path_cnt; i++) { + if (user_conn->paths[i].is_active + && user_conn->paths[i].path_id == path_id) + { + user_path = &user_conn->paths[i]; + } + } + + if (user_path == NULL) { + // printf("path %"PRIu64" is not avaliable!\n", path_id); + return XQC_SOCKET_ERROR; + } + + // printf("path %"PRIu64" with fd:%d\n", path_id, user_path->fd); + do { errno = 0; - res = sendto(user_conn->fd, buf, size, 0, peer_addr, peer_addrlen); + res = sendto(user_path->fd, buf, size, 0, peer_addr, peer_addrlen); if (res < 0) { printf("xqc_demo_cli_write_socket err %zd %s, fd: %d, buf: %p, size: %zu, " - "server_addr: %s\n", res, strerror(errno), user_conn->fd, buf, size, + "server_addr: %s\n", res, strerror(errno), user_path->fd, buf, size, user_conn->ctx->args->net_cfg.server_addr); if (errno == EAGAIN) { res = XQC_SOCKET_EAGAIN; } } - user_conn->last_sock_op_time = xqc_demo_now(); + user_path->last_sock_op_time = xqc_demo_now(); } while ((res < 0) && (errno == EINTR)); return res; } +ssize_t +xqc_demo_cli_write_socket(const unsigned char *buf, size_t size, const struct sockaddr *peer_addr, + socklen_t peer_addrlen, void *conn_user_data) +{ + return xqc_demo_cli_write_socket_ex(0, buf, size, peer_addr, peer_addrlen, conn_user_data); +} + + #if defined(XQC_SUPPORT_SENDMMSG) ssize_t xqc_demo_cli_write_mmsg(void *conn_user_data, struct iovec *msg_iov, unsigned int vlen, @@ -594,7 +765,7 @@ xqc_demo_cli_write_mmsg(void *conn_user_data, struct iovec *msg_iov, unsigned in const int MAX_SEG = 128; xqc_demo_cli_user_conn_t *user_conn = (xqc_demo_cli_user_conn_t *)conn_user_data; ssize_t res = 0; - int fd = user_conn->fd; + int fd = user_conn->paths[0].fd; struct mmsghdr mmsg[MAX_SEG]; memset(&mmsg, 0, sizeof(mmsg)); for (int i = 0; i < vlen; i++) { @@ -624,6 +795,81 @@ xqc_demo_cli_conn_update_cid_notify(xqc_connection_t *conn, const xqc_cid_t *ret memcpy(&user_conn->cid, new_cid, sizeof(*new_cid)); } +void +xqc_demo_cli_conn_create_path(const xqc_cid_t *cid, void *conn_user_data) +{ + xqc_demo_cli_user_conn_t *user_conn = conn_user_data; + xqc_demo_cli_ctx_t *ctx = user_conn->ctx; + uint64_t path_id; + int ret; + int backup = 0; + if (user_conn->total_path_cnt < ctx->args->net_cfg.ifcnt + && user_conn->total_path_cnt < MAX_PATH_CNT) + { + + if (user_conn->total_path_cnt == 1 && ctx->args->quic_cfg.mp_backup) { + backup = 1; + } + + ret = xqc_conn_create_path(ctx->engine, &(user_conn->cid), &path_id, backup); + if (ret < 0) { + printf("not support mp, xqc_conn_create_path err = %d\n", ret); + return; + } + + if (backup == 1) { + printf("Init No.%d path (id = %"PRIu64") to STANDBY state\n", 1, path_id); + } + + ret = xqc_demo_cli_init_user_path(user_conn, user_conn->total_path_cnt, path_id); + if (ret < 0) { + xqc_conn_close_path(ctx->engine, &(user_conn->cid), path_id); + return; + } + + if (user_conn->total_path_cnt == 2 && ctx->args->quic_cfg.mp_backup) { + printf("set No.%d path (id = %"PRIu64") to STANDBY state\n", 1, path_id); + xqc_conn_mark_path_standby(ctx->engine, &(user_conn->cid), path_id); + } + + } +} + +void +xqc_demo_cli_path_removed(const xqc_cid_t *scid, uint64_t path_id, + void *conn_user_data) +{ + xqc_demo_cli_user_conn_t *user_conn = conn_user_data; + int i; + for (i = 0; i < user_conn->total_path_cnt; i++) { + if (user_conn->paths[i].is_active + && user_conn->paths[i].path_id == path_id) + { + user_conn->paths[i].is_active = 0; + user_conn->active_path_cnt--; + /* remove event handle */ + if (user_conn->paths[i].ev_socket) { + event_del(user_conn->paths[i].ev_socket); + } + event_del(user_conn->paths[i].ev_timeout); + /* close socket */ + close(user_conn->paths[i].fd); + + if (user_conn->paths[i].ev_rebind_socket) { + event_del(user_conn->paths[i].ev_rebind_socket); + } + + if (user_conn->paths[i].rebind_fd != -1) { + close(user_conn->paths[i].rebind_fd); + } + + printf("No.%d path removed id = %"PRIu64"\n", i, path_id); + } + } + +} + + /****************************************************************************** * start of hq callback functions * @@ -729,12 +975,14 @@ xqc_demo_cli_hq_req_read_notify(xqc_hq_request_t *hqr, void *req_user_data) return 0; } - int nwrite = fwrite(buff, 1, read, user_stream->recv_body_fp); - if (nwrite != read) { - printf("fwrite error\n"); - return -1; + if (user_stream->recv_body_fp) { + int nwrite = fwrite(buff, 1, read, user_stream->recv_body_fp); + if (nwrite != read) { + printf("fwrite error\n"); + return -1; + } + fflush(user_stream->recv_body_fp); } - fflush(user_stream->recv_body_fp); read_sum += read; user_stream->recv_body_len += read; @@ -750,8 +998,10 @@ xqc_demo_cli_hq_req_read_notify(xqc_hq_request_t *hqr, void *req_user_data) user_stream, user_stream->file_name, user_stream->send_body_len, user_stream->recv_body_len); /* close file */ - fclose(user_stream->recv_body_fp); - user_stream->recv_body_fp = NULL; + if (user_stream->recv_body_fp) { + fclose(user_stream->recv_body_fp); + user_stream->recv_body_fp = NULL; + } // xqc_demo_cli_on_stream_fin(user_stream); } @@ -767,6 +1017,12 @@ xqc_demo_cli_hq_req_close_notify(xqc_hq_request_t *hqr, void *req_user_data) xqc_demo_cli_user_stream_t *user_stream = (xqc_demo_cli_user_stream_t *)req_user_data; + /* print stats */ + xqc_request_stats_t stats = xqc_hq_request_get_stats(hqr); + + printf("\033[33m[HQ-req] send_bytes:%zu, recv_bytes:%zu, path_info:%s\n\033[0m", + stats.send_body_size, stats.recv_body_size, stats.stream_info); + /* task schedule */ xqc_demo_cli_on_stream_fin(user_stream); @@ -821,6 +1077,9 @@ xqc_demo_cli_h3_request_read_notify(xqc_h3_request_t *h3_request, xqc_request_no DEBUG; unsigned char fin = 0; xqc_demo_cli_user_stream_t *user_stream = (xqc_demo_cli_user_stream_t *) user_data; + xqc_demo_cli_task_ctx_t *ctx = &user_stream->user_conn->ctx->task_ctx; + xqc_demo_cli_user_conn_t *user_conn = user_stream->user_conn; + uint32_t task_idx = user_conn->task->task_idx; // printf("xqc_demo_cli_h3_request_read_notify, h3_request: %p, user_stream: %p\n", h3_request, user_stream); if (flag & XQC_REQ_NOTIFY_READ_HEADER) { xqc_http_headers_t *headers; @@ -862,11 +1121,13 @@ xqc_demo_cli_h3_request_read_notify(xqc_h3_request_t *h3_request, xqc_request_no return 0; } - if (fwrite(buff, 1, read, user_stream->recv_body_fp) != read) { - printf("fwrite error\n"); - return -1; + if (user_stream->recv_body_fp) { + if (fwrite(buff, 1, read, user_stream->recv_body_fp) != read) { + printf("fwrite error\n"); + return -1; + } + fflush(user_stream->recv_body_fp); } - fflush(user_stream->recv_body_fp); read_sum += read; user_stream->recv_body_len += read; @@ -886,8 +1147,32 @@ xqc_demo_cli_h3_request_read_notify(xqc_h3_request_t *h3_request, xqc_request_no now_us - user_stream->start_time, (stats.send_body_size + stats.recv_body_size) * 1000 / (now_us - user_stream->start_time), stats.send_body_size, stats.recv_body_size); - + + if (user_stream->recv_body_fp) { + fclose(user_stream->recv_body_fp); + user_stream->recv_body_fp = NULL; + } // xqc_demo_cli_on_stream_fin(user_stream); + task_idx = user_stream->user_conn->task->task_idx; + if (ctx->schedule.schedule_info[task_idx].req_create_cnt + < ctx->tasks[task_idx].user_conn->task->req_cnt) + { + if (user_conn->ctx->args->req_cfg.idle_gap) { + + user_conn->ev_idle_restart = event_new(user_conn->ctx->eb, -1, 0, + xqc_demo_cli_delayed_idle_restart, + user_conn); + struct timeval tv = { + .tv_sec = user_conn->ctx->args->req_cfg.idle_gap / 1000, + .tv_usec = (user_conn->ctx->args->req_cfg.idle_gap % 1000) * 1000, + }; + event_add(user_conn->ev_idle_restart, &tv); + + } else { + xqc_demo_cli_continue_send_reqs(ctx->tasks[task_idx].user_conn); + } + + } } return 0; @@ -915,8 +1200,15 @@ xqc_demo_cli_h3_request_close_notify(xqc_h3_request_t *h3_request, void *user_da xqc_request_stats_t stats; stats = xqc_h3_request_get_stats(h3_request); printf("send_body_size:%zu, recv_body_size:%zu, send_header_size:%zu, recv_header_size:%zu, " - "recv_fin:%d, err:%d\n", stats.send_body_size, stats.recv_body_size, - stats.send_header_size, stats.recv_header_size, user_stream->recv_fin, stats.stream_err); + "recv_fin:%d, err:%d, rate_limit:%"PRIu64", mp_state:%d, avail_send_weight:%.3lf, avail_recv_weight:%.3lf\n", + stats.send_body_size, stats.recv_body_size, stats.send_header_size, stats.recv_header_size, + user_stream->recv_fin, stats.stream_err, stats.rate_limit, stats.mp_state, + stats.mp_default_path_send_weight, stats.mp_default_path_recv_weight); + + printf("\033[33m[H3-req] send_bytes:%zu, recv_bytes:%zu, path_info:%s\n\033[0m", + stats.send_body_size + stats.send_header_size, + stats.recv_body_size + stats.recv_header_size, + stats.stream_info); free(user_stream); return 0; @@ -928,14 +1220,14 @@ xqc_demo_cli_h3_request_close_notify(xqc_h3_request_t *h3_request, void *user_da ******************************************************************************/ void -xqc_demo_cli_socket_write_handler(xqc_demo_cli_user_conn_t *user_conn) +xqc_demo_cli_socket_write_handler(xqc_demo_cli_user_conn_t *user_conn, int fd) { DEBUG; // xqc_conn_continue_send(user_conn->ctx->engine, &user_conn->cid); } void -xqc_demo_cli_socket_read_handler(xqc_demo_cli_user_conn_t *user_conn) +xqc_demo_cli_socket_read_handler(xqc_demo_cli_user_conn_t *user_conn, int fd) { DEBUG; ssize_t recv_size = 0; @@ -943,8 +1235,24 @@ xqc_demo_cli_socket_read_handler(xqc_demo_cli_user_conn_t *user_conn) struct sockaddr addr; socklen_t addr_len = 0; unsigned char packet_buf[XQC_PACKET_TMP_BUF_LEN]; + int i; + xqc_demo_cli_user_path_t *user_path = NULL; + for (i = 0; i < user_conn->total_path_cnt; i++) { + if (user_conn->paths[i].is_active + && user_conn->paths[i].fd == fd) + { + user_path = &user_conn->paths[i]; + } + } + + if (user_path == NULL) { + return; + } + + // printf("socket read: path%"PRIu64" fd:%d\n", user_path->path_id, user_path->fd); + do { - recv_size = recvfrom(user_conn->fd, packet_buf, sizeof(packet_buf), 0, + recv_size = recvfrom(user_path->fd, packet_buf, sizeof(packet_buf), 0, (struct sockaddr *)&addr, &addr_len); if (recv_size < 0 && errno == EAGAIN) { break; @@ -954,20 +1262,29 @@ xqc_demo_cli_socket_read_handler(xqc_demo_cli_user_conn_t *user_conn) break; } - user_conn->local_addrlen = sizeof(struct sockaddr_in6); - xqc_int_t ret = getsockname(user_conn->fd, (struct sockaddr*)&user_conn->local_addr, - &user_conn->local_addrlen); + user_path->local_addrlen = sizeof(struct sockaddr_in6); + xqc_int_t ret = getsockname(user_path->fd, (struct sockaddr*)&user_path->local_addr, + &user_path->local_addrlen); if (ret != 0) { printf("getsockname error, errno: %d\n", errno); } recv_sum += recv_size; uint64_t recv_time = xqc_demo_now(); - user_conn->last_sock_op_time = recv_time; + user_path->last_sock_op_time = recv_time; +#ifdef XQC_NO_PID_PACKET_PROCESS + if (xqc_engine_packet_process(user_conn->ctx->engine, packet_buf, recv_size, + (struct sockaddr *)(&user_path->local_addr), + user_path->local_addrlen, (struct sockaddr *)(&addr), + addr_len, (xqc_msec_t)recv_time, + user_conn) != XQC_OK) +#else if (xqc_engine_packet_process(user_conn->ctx->engine, packet_buf, recv_size, - (struct sockaddr *)(&user_conn->local_addr), - user_conn->local_addrlen, (struct sockaddr *)(&addr), - addr_len, (xqc_msec_t)recv_time, user_conn) != XQC_OK) + (struct sockaddr *)(&user_path->local_addr), + user_path->local_addrlen, (struct sockaddr *)(&addr), + addr_len, user_path->path_id, (xqc_msec_t)recv_time, + user_conn) != XQC_OK) +#endif { return; } @@ -985,9 +1302,11 @@ xqc_demo_cli_socket_event_callback(int fd, short what, void *arg) xqc_demo_cli_user_conn_t *user_conn = (xqc_demo_cli_user_conn_t *) arg; if (what & EV_WRITE) { - xqc_demo_cli_socket_write_handler(user_conn); + xqc_demo_cli_socket_write_handler(user_conn, fd); + } else if (what & EV_READ) { - xqc_demo_cli_socket_read_handler(user_conn); + xqc_demo_cli_socket_read_handler(user_conn, fd); + } else { printf("event callback: what=%d\n", what); exit(1); @@ -1012,32 +1331,134 @@ static void xqc_demo_cli_idle_callback(int fd, short what, void *arg) { int rc = 0; - xqc_demo_cli_user_conn_t *user_conn = (xqc_demo_cli_user_conn_t *) arg; - if (xqc_demo_now() - user_conn->last_sock_op_time < (uint64_t)user_conn->ctx->args->net_cfg.conn_timeout * 1000000) { + xqc_demo_cli_user_path_t *user_path = (xqc_demo_cli_user_path_t*) arg; + xqc_demo_cli_user_conn_t *user_conn = user_path->user_conn; + + if (xqc_demo_now() - user_path->last_sock_op_time < (uint64_t)user_conn->ctx->args->net_cfg.conn_timeout * 1000000) { struct timeval tv; tv.tv_sec = user_conn->ctx->args->net_cfg.conn_timeout; tv.tv_usec = 0; - event_add(user_conn->ev_timeout, &tv); + event_add(user_path->ev_timeout, &tv); } else { - if (user_conn->ctx->args->quic_cfg.alpn_type == ALPN_H3) { - rc = xqc_h3_conn_close(user_conn->ctx->engine, &user_conn->cid); + if (user_conn->active_path_cnt > 1) { + /* close path first */ + rc = xqc_conn_close_path(user_conn->ctx->engine, &user_conn->cid, user_path->path_id); - } else { - rc = xqc_hq_conn_close(user_conn->ctx->engine, user_conn->hqc_handle, &user_conn->cid); - } + } + /* if there is only one path, we close the connection */ + if (user_conn->active_path_cnt <= 1 || rc == -XQC_EMP_NO_ACTIVE_PATH) + { + if (user_conn->ctx->args->quic_cfg.alpn_type == ALPN_H3) { + rc = xqc_h3_conn_close(user_conn->ctx->engine, &user_conn->cid); + + } else { + rc = xqc_hq_conn_close(user_conn->ctx->engine, user_conn->hqc_handle, &user_conn->cid); + } + + if (user_conn->ev_delay_req) { + event_del(user_conn->ev_delay_req); + user_conn->ev_delay_req = NULL; + } + + if (user_conn->ev_idle_restart) { + event_del(user_conn->ev_idle_restart); + user_conn->ev_idle_restart = NULL; + } + + if (user_conn->ev_close_path) { + event_del(user_conn->ev_close_path); + user_conn->ev_close_path = NULL; + } + if (user_conn->ev_rebinding_p0) { + event_del(user_conn->ev_rebinding_p0); + user_conn->ev_rebinding_p0 = NULL; + } + + if (user_conn->ev_rebinding_p1) { + event_del(user_conn->ev_rebinding_p1); + user_conn->ev_rebinding_p1 = NULL; + } + + printf("socket idle timeout, task failed, total task_cnt: %d, req_fin_cnt: %d, req_sent_cnt: %d, req_create_cnt: %d\n", + user_conn->ctx->task_ctx.tasks[user_conn->task->task_idx].req_cnt, + user_conn->ctx->task_ctx.schedule.schedule_info[user_conn->task->task_idx].req_fin_cnt, + user_conn->ctx->task_ctx.schedule.schedule_info[user_conn->task->task_idx].req_sent_cnt, + user_conn->ctx->task_ctx.schedule.schedule_info[user_conn->task->task_idx].req_create_cnt); + xqc_demo_cli_on_task_fail(user_conn->ctx, user_conn->task); + } + if (rc) { - printf("close conn error\n"); + printf("close path or conn error, path_id %"PRIu64"\n", user_path->path_id); return; } + } +} + +static void +xqc_demo_cli_delayed_req_start(int fd, short what, void *arg) +{ + xqc_demo_cli_user_conn_t *user_conn = (xqc_demo_cli_user_conn_t *) arg; + int req_cnt = user_conn->task->req_cnt; + if (user_conn->ctx->args->req_cfg.serial) { + req_cnt = req_cnt > 1 ? 1 : req_cnt; + } + xqc_demo_cli_send_requests(user_conn, user_conn->ctx->args, + user_conn->task->reqs, req_cnt); +} + +static void +xqc_demo_cli_delayed_idle_restart(int fd, short what, void *arg) +{ + xqc_demo_cli_user_conn_t *user_conn = (xqc_demo_cli_user_conn_t *)arg; + xqc_demo_cli_task_ctx_t *ctx = &user_conn->ctx->task_ctx; + int task_idx = user_conn->task->task_idx; + xqc_demo_cli_continue_send_reqs(ctx->tasks[task_idx].user_conn); +} + +static void +xqc_demo_cli_close_path_timeout(int fd, short what, void *arg) +{ + xqc_demo_cli_user_conn_t *user_conn = (xqc_demo_cli_user_conn_t *) arg; + if (user_conn->active_path_cnt > 1) + { + xqc_conn_close_path(user_conn->ctx->engine, &(user_conn->cid), user_conn->paths[1].path_id); + } +} + +static void +xqc_demo_cli_rebind_path0(int fd, short what, void *arg) +{ + xqc_demo_cli_user_conn_t *user_conn = (xqc_demo_cli_user_conn_t *) arg; + if (user_conn->paths[0].is_active) { + // change fd + int temp = user_conn->paths[0].fd; + user_conn->paths[0].fd = user_conn->paths[0].rebind_fd; + user_conn->paths[0].rebind_fd = user_conn->paths[0].fd; + + //stop read from the old socket + event_del(user_conn->paths[0].ev_socket); + user_conn->paths[0].ev_socket = NULL; + + xqc_h3_conn_send_ping(user_conn->ctx->engine, &user_conn->cid, NULL); + } +} + +static void +xqc_demo_cli_rebind_path1(int fd, short what, void *arg) +{ + xqc_demo_cli_user_conn_t *user_conn = (xqc_demo_cli_user_conn_t *) arg; + if (user_conn->paths[1].is_active) { + // change fd + int temp = user_conn->paths[1].fd; + user_conn->paths[1].fd = user_conn->paths[1].rebind_fd; + user_conn->paths[1].rebind_fd = user_conn->paths[1].fd; - printf("socket idle timeout, task failed, total task_cnt: %d, req_fin_cnt: %d, req_sent_cnt: %d, req_create_cnt: %d\n", - user_conn->ctx->task_ctx.tasks[user_conn->task->task_idx].req_cnt, - user_conn->ctx->task_ctx.schedule.schedule_info[user_conn->task->task_idx].req_fin_cnt, - user_conn->ctx->task_ctx.schedule.schedule_info[user_conn->task->task_idx].req_sent_cnt, - user_conn->ctx->task_ctx.schedule.schedule_info[user_conn->task->task_idx].req_create_cnt); - xqc_demo_cli_on_task_fail(user_conn->ctx, user_conn->task); + event_del(user_conn->paths[1].ev_socket); + user_conn->paths[1].ev_socket = NULL; + + xqc_h3_conn_send_ping(user_conn->ctx->engine, &user_conn->cid, NULL); } } @@ -1110,17 +1531,32 @@ xqc_demo_cli_init_conneciton_settings(xqc_conn_settings_t* settings, break; case CC_TYPE_CUBIC: - cong_ctrl = xqc_reno_cb; + cong_ctrl = xqc_cubic_cb; break; +#ifdef XQC_ENABLE_RENO case CC_TYPE_RENO: - cong_ctrl = xqc_cubic_cb; + cong_ctrl = xqc_reno_cb; break; +#endif default: break; } + xqc_scheduler_callback_t sched = xqc_minrtt_scheduler_cb; + if (strncmp(args->quic_cfg.mp_sched, "minrtt", strlen("minrtt")) == 0) { + sched = xqc_minrtt_scheduler_cb; + + } if (strncmp(args->quic_cfg.mp_sched, "backup", strlen("backup")) == 0) { + sched = xqc_backup_scheduler_cb; + + } else { +#ifdef XQC_ENABLE_MP_INTEROP + sched = xqc_interop_scheduler_cb; +#endif + } + memset(settings, 0, sizeof(xqc_conn_settings_t)); settings->pacing_on = args->net_cfg.pacing; settings->cong_ctrl_callback = cong_ctrl; @@ -1130,6 +1566,19 @@ xqc_demo_cli_init_conneciton_settings(xqc_conn_settings_t* settings, settings->proto_version = XQC_VERSION_V1; settings->spurious_loss_detect_on = 1; settings->keyupdate_pkt_threshold = args->quic_cfg.keyupdate_pkt_threshold; + settings->enable_multipath = args->net_cfg.multipath; + settings->mp_ack_on_any_path = args->quic_cfg.mp_ack_on_any_path; + settings->scheduler_callback = sched; + settings->recv_rate_bytes_per_sec = args->quic_cfg.recv_rate; + settings->mp_enable_reinjection = args->quic_cfg.reinjection; + settings->reinj_ctl_callback = xqc_deadline_reinj_ctl_cb; + settings->standby_path_probe_timeout = 1000; + settings->multipath_version = args->quic_cfg.mp_version; + settings->mp_ping_on = 1; + if (args->req_cfg.throttled_req != -1) { + settings->enable_stream_rate_limit = 1; + settings->recv_rate_bytes_per_sec = 0; + } } /* set client args to default values */ @@ -1147,11 +1596,17 @@ xqc_demo_cli_init_args(xqc_demo_cli_client_args_t *args) args->env_cfg.log_level = XQC_LOG_DEBUG; strncpy(args->env_cfg.log_path, LOG_PATH, sizeof(args->env_cfg.log_path)); strncpy(args->env_cfg.out_file_dir, OUT_DIR, sizeof(args->env_cfg.out_file_dir)); + strncpy(args->env_cfg.key_out_path, KEY_PATH, sizeof(args->env_cfg.out_file_dir)); /* quic cfg */ args->quic_cfg.alpn_type = ALPN_HQ; strncpy(args->quic_cfg.alpn, "hq-interop", sizeof(args->quic_cfg.alpn)); args->quic_cfg.keyupdate_pkt_threshold = UINT64_MAX; + /* default 04 */ + args->quic_cfg.mp_version = XQC_MULTIPATH_04; + + args->req_cfg.throttled_req = -1; + } void @@ -1244,20 +1699,36 @@ xqc_demo_cli_usage(int argc, char *argv[]) " -A alpn selection: h3/hq\n" " -D save request body directory\n" " -l Log level. e:error d:debug.\n" - " -L xuqic log directory.\n" + " -L xquic log directory.\n" " -U Url. \n" " -k key out path\n" " -K Client's life circle time\n" " -u key update packet threshold\n" + " -d do not save responses to files\n" + " -M enable multipath\n" + " -i interface to create a path. For instance, we can use '-i lo -i lo' to create two paths via lo.\n" + " -w waiting N ms to start the first request.\n" + " -P enable MPQUIC to return ACK_MPs on any paths.\n" + " -s multipath scheduler (interop, minrtt, backup), default: interop\n" + " -b set the second path as a backup path\n" + " -Z close one path after X ms\n" + " -N No encryption (default disabled)\n" + " -Q Send requests one by one (default disabled)\n" + " -T Throttle recving rate (Bps)\n" + " -R Reinjection (1,2,4) \n" + " -V Multipath Version (4,5)\n" + " -I Idle interval between requests (ms)\n" + " -n Throttling the {1,2,...}xn-th requests\n" + " -e NAT rebinding on path 0\n" + " -E NAT rebinding on path 1\n" , prog); } - void xqc_demo_cli_parse_args(int argc, char *argv[], xqc_demo_cli_client_args_t *args) { int ch = 0; - while ((ch = getopt(argc, argv, "a:p:c:Ct:S:0m:A:D:l:L:k:K:U:u:")) != -1) { + while ((ch = getopt(argc, argv, "a:p:c:Ct:S:0m:A:D:l:L:k:K:U:u:dMi:w:Ps:bZ:NQT:R:V:I:n:eE")) != -1) { switch (ch) { /* server ip */ case 'a': @@ -1389,6 +1860,96 @@ xqc_demo_cli_parse_args(int argc, char *argv[], xqc_demo_cli_client_args_t *args args->quic_cfg.keyupdate_pkt_threshold = atoi(optarg); break; + case 'd': + printf("option dummy mode on\n"); + args->req_cfg.dummy_mode = 1; + break; + + case 'M': + printf("option multipath on\n"); + args->net_cfg.multipath = 1; + break; + + case 'i': + printf("option adding interface: %s\n", optarg); + if (args->net_cfg.ifcnt < MAX_PATH_CNT) { + strncpy(args->net_cfg.iflist[args->net_cfg.ifcnt++], optarg, strlen(optarg)); + } else { + printf("too many interfaces (two at most)!\n"); + exit(0); + } + break; + + case 'w': + printf("option first req delay: %s\n", optarg); + args->req_cfg.req_start_delay = atoi(optarg); + break; + + case 'P': + printf("option ACK_MP on any path on\n"); + args->quic_cfg.mp_ack_on_any_path = 1; + break; + + case 's': + printf("option scheduler: %s\n", optarg); + strncpy(args->quic_cfg.mp_sched, optarg, 32); + break; + + case 'b': + printf("option backup path on\n"); + args->quic_cfg.mp_backup = 1; + break; + + case 'Z': + printf("option close a path after %s ms\n", optarg); + args->quic_cfg.close_path = atoi(optarg); + break; + + case 'N': + printf("option no encryption on\n"); + args->quic_cfg.no_encryption = 1; + break; + + case 'Q': + printf("option serial requests on\n"); + args->req_cfg.serial = 1; + break; + + case 'T': + printf("option recv rate limit: %s\n", optarg); + args->quic_cfg.recv_rate = atoi(optarg); + break; + + case 'R': + printf("option reinjection: %s\n", optarg); + args->quic_cfg.reinjection = atoi(optarg); + break; + + case 'V': + printf("option multipath version: %s\n", optarg); + args->quic_cfg.mp_version = atoi(optarg); + break; + + case 'I': + printf("option idle gap: %s\n", optarg); + args->req_cfg.idle_gap = atoi(optarg); + break; + + case 'n': + printf("option throttled reqs: %s\n", optarg); + args->req_cfg.throttled_req = atoi(optarg); + break; + + case 'e': + printf("option rebinding path0 after 2s\n"); + args->net_cfg.rebind_p0 = 1; + break; + + case 'E': + printf("option rebinding path1 after 3s\n"); + args->net_cfg.rebind_p1 = 1; + break; + default: printf("other option :%c\n", ch); xqc_demo_cli_usage(argc, argv); @@ -1469,8 +2030,24 @@ int xqc_demo_cli_send_h3_req(xqc_demo_cli_user_conn_t *user_conn, xqc_demo_cli_user_stream_t *user_stream, xqc_demo_cli_request_t *req) { + xqc_stream_settings_t settings = { .recv_rate_bytes_per_sec = 0 }; + int task_idx = user_conn->task->task_idx; + int req_create_cnt = user_conn->ctx->task_ctx.schedule.schedule_info[task_idx].req_create_cnt; + if (user_conn->ctx->args->req_cfg.throttled_req != -1) { + if (req_create_cnt == user_conn->ctx->args->req_cfg.throttled_req) { + settings.recv_rate_bytes_per_sec = user_conn->ctx->args->quic_cfg.recv_rate; + } + + if (req_create_cnt != 0 + && user_conn->ctx->args->req_cfg.throttled_req != 0 + && (req_create_cnt % user_conn->ctx->args->req_cfg.throttled_req) == 0) + { + settings.recv_rate_bytes_per_sec = user_conn->ctx->args->quic_cfg.recv_rate; + } + } + user_stream->h3_request = xqc_h3_request_create(user_conn->ctx->engine, &user_conn->cid, - user_stream); + &settings, user_stream); if (user_stream->h3_request == NULL) { printf("xqc_h3_request_create error\n"); return -1; @@ -1493,6 +2070,13 @@ xqc_demo_cli_open_file(xqc_demo_cli_user_stream_t *user_stream, const char *save { char file_path[512] = {0}; snprintf(file_path, sizeof(file_path), "%s%s", save_path, req_path); + + if (user_stream->user_conn->ctx->args->req_cfg.dummy_mode) { + printf("dummy mode: do not open file[%s]\n", file_path); + user_stream->recv_body_fp = NULL; + return; + } + user_stream->recv_body_fp = fopen(file_path, "wb"); if (NULL == user_stream->recv_body_fp) { printf("open file[%s] error\n", file_path); @@ -1550,6 +2134,9 @@ xqc_demo_cli_continue_send_reqs(xqc_demo_cli_user_conn_t *user_conn) int task_idx = user_conn->task->task_idx; int req_create_cnt = ctx->task_ctx.schedule.schedule_info[task_idx].req_create_cnt; int req_cnt = user_conn->task->req_cnt - req_create_cnt; + if (ctx->args->req_cfg.serial) { + req_cnt = req_cnt > 1 ? 1 : req_cnt; + } if (req_cnt > 0) { xqc_demo_cli_request_t *reqs = user_conn->task->reqs + req_create_cnt; xqc_demo_cli_send_requests(user_conn, ctx->args, reqs, req_cnt); @@ -1585,9 +2172,9 @@ xqc_demo_cli_h3_conn_close_notify(xqc_h3_conn_t *h3_conn, const xqc_cid_t *cid, xqc_demo_cli_user_conn_t *user_conn = (xqc_demo_cli_user_conn_t *)user_data; xqc_conn_stats_t stats = xqc_conn_get_stats(user_conn->ctx->engine, cid); printf("send_count:%u, lost_count:%u, tlp_count:%u, recv_count:%u, srtt:%"PRIu64" " - "early_data_flag:%d, conn_err:%d, ack_info:%s\n", stats.send_count, stats.lost_count, + "early_data_flag:%d, conn_err:%d, ack_info:%s conn_info:%s\n", stats.send_count, stats.lost_count, stats.tlp_count, stats.recv_count, stats.srtt, stats.early_data_flag, stats.conn_err, - stats.ack_info); + stats.ack_info, stats.conn_info); xqc_demo_cli_on_task_finish(user_conn->ctx, user_conn->task); free(user_conn); @@ -1638,10 +2225,13 @@ xqc_demo_cli_init_callback(xqc_engine_callback_t *cb, xqc_transport_callbacks_t static xqc_transport_callbacks_t tcb = { .write_socket = xqc_demo_cli_write_socket, + .write_socket_ex = xqc_demo_cli_write_socket_ex, .save_token = xqc_demo_cli_save_token, /* save token */ .save_session_cb = xqc_demo_cli_save_session_cb, .save_tp_cb = xqc_demo_cli_save_tp_cb, .conn_update_cid_notify = xqc_demo_cli_conn_update_cid_notify, + .ready_to_create_path_notify = xqc_demo_cli_conn_create_path, + .path_removed_notify = xqc_demo_cli_path_removed, }; *cb = callback; @@ -1765,7 +2355,7 @@ xqc_demo_cli_init_xquic_connection(xqc_demo_cli_user_conn_t *user_conn, if (args->quic_cfg.alpn_type == ALPN_H3) { const xqc_cid_t *cid = xqc_h3_connect(user_conn->ctx->engine, &conn_settings, - args->quic_cfg.token, args->quic_cfg.token_len, args->net_cfg.host, 0, &conn_ssl_config, + args->quic_cfg.token, args->quic_cfg.token_len, args->net_cfg.host, args->quic_cfg.no_encryption, &conn_ssl_config, (struct sockaddr*)&args->net_cfg.addr, args->net_cfg.addr_len, user_conn); if (cid == NULL) { return -1; @@ -1775,7 +2365,7 @@ xqc_demo_cli_init_xquic_connection(xqc_demo_cli_user_conn_t *user_conn, } else { user_conn->hqc_handle = xqc_hq_connect(user_conn->ctx->engine, &conn_settings, - args->quic_cfg.token, args->quic_cfg.token_len, args->net_cfg.host, 0, &conn_ssl_config, + args->quic_cfg.token, args->quic_cfg.token_len, args->net_cfg.host, args->quic_cfg.no_encryption, &conn_ssl_config, (struct sockaddr*)&args->net_cfg.addr, args->net_cfg.addr_len, user_conn); if (user_conn->hqc_handle == NULL) { return -1; @@ -1809,8 +2399,59 @@ xqc_demo_cli_start(xqc_demo_cli_user_conn_t *user_conn, xqc_demo_cli_client_args } #endif - /* TODO: fix MAX_STREAMS bug */ - xqc_demo_cli_send_requests(user_conn, args, reqs, req_cnt); + if (args->quic_cfg.close_path) { + user_conn->ev_close_path = event_new(user_conn->ctx->eb, -1, 0, + xqc_demo_cli_close_path_timeout, + user_conn); + struct timeval tv = { + .tv_sec = args->quic_cfg.close_path / 1000, + .tv_usec = (args->quic_cfg.close_path % 1000) * 1000, + }; + event_add(user_conn->ev_close_path, &tv); + } + + if (args->net_cfg.rebind_p0) { + user_conn->ev_rebinding_p0 = event_new(user_conn->ctx->eb, -1, 0, + xqc_demo_cli_rebind_path0, + user_conn); + struct timeval tv = { + .tv_sec = 2, + .tv_usec = 0, + }; + event_add(user_conn->ev_rebinding_p0, &tv); + } + + if (args->net_cfg.rebind_p1) { + user_conn->ev_rebinding_p1 = event_new(user_conn->ctx->eb, -1, 0, + xqc_demo_cli_rebind_path1, + user_conn); + struct timeval tv = { + .tv_sec = 3, + .tv_usec = 0, + }; + event_add(user_conn->ev_rebinding_p1, &tv); + } + + if (args->req_cfg.req_start_delay) { + user_conn->ev_delay_req = event_new(user_conn->ctx->eb, -1, 0, + xqc_demo_cli_delayed_req_start, + user_conn); + struct timeval tv = { + .tv_sec = args->req_cfg.req_start_delay / 1000, + .tv_usec = (args->req_cfg.req_start_delay % 1000) * 1000, + }; + event_add(user_conn->ev_delay_req, &tv); + + } else { + /* TODO: fix MAX_STREAMS bug */ + if (args->req_cfg.serial) { + xqc_demo_cli_send_requests(user_conn, args, reqs, req_cnt > 1 ? 1 : req_cnt); + + } else { + xqc_demo_cli_send_requests(user_conn, args, reqs, req_cnt); + } + + } } void @@ -1862,8 +2503,15 @@ xqc_demo_cli_get_idle_waiting_task(xqc_demo_cli_ctx_t *ctx) static int -xqc_demo_cli_create_socket(xqc_demo_cli_user_conn_t *user_conn, xqc_demo_cli_net_config_t* cfg) +xqc_demo_cli_create_socket(xqc_demo_cli_user_path_t *user_path, + xqc_demo_cli_net_config_t* cfg, int path_seq) { + + if (cfg->ifcnt && path_seq >= cfg->ifcnt) { + printf("too many sockets (ifcnt:%d)\n", cfg->ifcnt); + return -1; + } + int size; int fd = 0; int ret; @@ -1890,7 +2538,22 @@ xqc_demo_cli_create_socket(xqc_demo_cli_user_conn_t *user_conn, xqc_demo_cli_net goto err; } - user_conn->last_sock_op_time = xqc_demo_now(); + if (cfg->ifcnt) { + struct ifreq ifr; + memset(&ifr, 0x00, sizeof(ifr)); + strncpy(ifr.ifr_name, cfg->iflist[path_seq], sizeof(ifr.ifr_name) - 1); + +#if !defined(__APPLE__) + printf("fd: %d. bind to nic: %s\n", fd, cfg->iflist[path_seq]); + if (setsockopt(fd, SOL_SOCKET, SO_BINDTODEVICE, (char *)&ifr, sizeof(ifr)) < 0) { + printf("bind to nic error: %d, try use sudo\n", errno); + goto err; + } +#endif + + } + + user_path->last_sock_op_time = xqc_demo_now(); return fd; @@ -1899,65 +2562,90 @@ xqc_demo_cli_create_socket(xqc_demo_cli_user_conn_t *user_conn, xqc_demo_cli_net return -1; } -/* create one connection, send multi reqs in multi streams */ -int -xqc_demo_cli_handle_task(xqc_demo_cli_ctx_t *ctx, xqc_demo_cli_task_t *task) +int +xqc_demo_cli_init_user_path(xqc_demo_cli_user_conn_t *user_conn, int path_seq, uint64_t path_id) { - DEBUG; - /* create socket and connection callback user data */ - xqc_demo_cli_user_conn_t *user_conn = calloc(1, sizeof(xqc_demo_cli_user_conn_t)); - user_conn->ctx = ctx; - user_conn->task = task; - // printf(".. user_conn: %p, ctx: %p\n", user_conn, ctx); - user_conn->fd = xqc_demo_cli_create_socket(user_conn, &ctx->args->net_cfg); - if (user_conn->fd < 0) { + xqc_demo_cli_ctx_t *ctx = user_conn->ctx; + xqc_demo_cli_user_path_t *user_path = &user_conn->paths[path_seq]; + + /* create the initial path */ + user_path->fd = xqc_demo_cli_create_socket(user_path, &ctx->args->net_cfg, path_seq); + if (user_path->fd < 0) { printf("xqc_create_socket error\n"); return -1; } + user_path->rebind_fd = -1; + user_path->ev_rebind_socket = NULL; + + if (ctx->args->net_cfg.rebind_p0 && path_seq == 0) { + user_path->rebind_fd = xqc_demo_cli_create_socket(user_path, &ctx->args->net_cfg, path_seq); + if (user_path->rebind_fd < 0) { + printf("xqc_create_rebind_socket error\n"); + return -1; + } + } + + if (ctx->args->net_cfg.rebind_p1 && path_seq == 1) { + user_path->rebind_fd = xqc_demo_cli_create_socket(user_path, &ctx->args->net_cfg, path_seq); + if (user_path->rebind_fd < 0) { + printf("xqc_create_rebind_socket error\n"); + return -1; + } + } + /* socket event */ - user_conn->ev_socket = event_new(ctx->eb, user_conn->fd, EV_READ | EV_PERSIST, + user_path->ev_socket = event_new(ctx->eb, user_path->fd, EV_READ | EV_PERSIST, xqc_demo_cli_socket_event_callback, user_conn); - event_add(user_conn->ev_socket, NULL); + event_add(user_path->ev_socket, NULL); + + if (user_path->rebind_fd != -1) { + user_path->ev_rebind_socket = event_new(ctx->eb, user_path->rebind_fd, EV_READ | EV_PERSIST, + xqc_demo_cli_socket_event_callback, user_conn); + event_add(user_path->ev_rebind_socket, NULL); + } /* xquic timer */ - user_conn->ev_timeout = event_new(ctx->eb, -1, 0, xqc_demo_cli_idle_callback, user_conn); + user_path->ev_timeout = event_new(ctx->eb, -1, 0, xqc_demo_cli_idle_callback, user_path); struct timeval tv; tv.tv_sec = ctx->args->net_cfg.conn_timeout; tv.tv_usec = 0; - event_add(user_conn->ev_timeout, &tv); + event_add(user_path->ev_timeout, &tv); - /* start client */ - xqc_demo_cli_start(user_conn, ctx->args, task->reqs, task->req_cnt); + user_conn->active_path_cnt++; + user_conn->total_path_cnt++; + user_path->is_active = 1; + user_path->user_conn = user_conn; + user_path->path_id = path_id; + + printf("No.%d path created id = %"PRIu64"\n", user_conn->total_path_cnt - 1, path_id); - task->user_conn = user_conn; return 0; } +/* create one connection, send multi reqs in multi streams */ int -xqc_demo_cli_close_task(xqc_demo_cli_ctx_t *ctx, xqc_demo_cli_task_t *task) +xqc_demo_cli_handle_task(xqc_demo_cli_ctx_t *ctx, xqc_demo_cli_task_t *task) { - xqc_demo_cli_user_conn_t *user_conn = task->user_conn; - - /* close xquic conn */ - if (ctx->args->quic_cfg.alpn_type == ALPN_H3) { - xqc_h3_conn_close(ctx->engine, &user_conn->cid); + DEBUG; + /* create socket and connection callback user data */ + xqc_demo_cli_user_conn_t *user_conn = calloc(1, sizeof(xqc_demo_cli_user_conn_t)); + user_conn->ctx = ctx; + user_conn->task = task; - } else { - xqc_hq_conn_close(ctx->engine, user_conn->hqc_handle, &user_conn->cid); + /* init the first path */ + if (xqc_demo_cli_init_user_path(user_conn, 0, 0)) { + return -1; } - /* remove task event handle */ - event_del(user_conn->ev_socket); - event_del(user_conn->ev_timeout); + /* start client */ + xqc_demo_cli_start(user_conn, ctx->args, task->reqs, task->req_cnt); - /* close socket */ - close(user_conn->fd); + task->user_conn = user_conn; return 0; } - static struct timeval tv_task_schedule = {0, 100}; /* @@ -1975,11 +2663,6 @@ xqc_demo_cli_task_schedule_callback(int fd, short what, void *arg) int idle_waiting_task_id = -1; for (size_t i = 0; i < ctx->task_ctx.task_cnt; i++) { - /* if task finished, close task */ - if (ctx->task_ctx.schedule.schedule_info[i].fin_flag) { - xqc_demo_cli_close_task(ctx, ctx->task_ctx.tasks + i); - ctx->task_ctx.schedule.schedule_info[i].fin_flag = 0; - } /* check if all tasks are finished */ if (ctx->task_ctx.schedule.schedule_info[i].status <= TASK_STATUS_RUNNING) { diff --git a/demo/demo_server.c b/demo/demo_server.c index f5143467e..372a4f8d6 100644 --- a/demo/demo_server.c +++ b/demo/demo_server.c @@ -80,6 +80,21 @@ typedef struct xqc_demo_svr_quic_config_s { /* retry */ int retry_on; + + /* dummy mode */ + int dummy_mode; + + /* multipath */ + int multipath; + + /* ack on any path */ + int mp_ack_on_any_path; + + /* scheduler */ + char mp_sched[32]; + + uint32_t reinjection; + } xqc_demo_svr_quic_config_t; @@ -160,7 +175,7 @@ typedef struct xqc_demo_svr_ctx_s { typedef struct xqc_demo_svr_user_conn_s { - struct event *ev_timeout; + struct event *ev_timeout; struct sockaddr_in6 peer_addr; socklen_t peer_addrlen; xqc_cid_t cid; @@ -169,9 +184,9 @@ typedef struct xqc_demo_svr_user_conn_s { typedef struct xqc_demo_svr_resource_s { FILE *fp; - int total_len; /* total len of file */ - int total_offset; /* total sent offset of file */ - char *buf; /* send buf */ + off_t total_len; /* total len of file */ + off_t total_offset; /* total sent offset of file */ + char *buf; /* send buf */ int buf_size; /* send buf size */ int buf_len; /* send buf len */ int buf_offset; /* send buf offset */ @@ -276,6 +291,7 @@ int xqc_demo_svr_open_keylog_file(xqc_demo_svr_ctx_t *ctx) { ctx->keylog_fd = open(ctx->args->env_cfg.key_out_path, (O_WRONLY | O_APPEND | O_CREAT), 0644); + printf("%s %d\n", ctx->args->env_cfg.key_out_path, ctx->keylog_fd); if (ctx->keylog_fd <= 0) { return -1; } @@ -295,7 +311,7 @@ xqc_demo_svr_close_keylog_file(xqc_demo_svr_ctx_t *ctx) } void -xqc_demo_svr_keylog_cb(const char *line, void *eng_user_data) +xqc_demo_svr_keylog_cb(const xqc_cid_t *scid, const char *line, void *eng_user_data) { xqc_demo_svr_ctx_t *ctx = (xqc_demo_svr_ctx_t*)eng_user_data; if (ctx->keylog_fd <= 0) { @@ -340,8 +356,8 @@ void xqc_demo_svr_conn_update_cid_notify(xqc_connection_t *conn, const xqc_cid_t *retire_cid, const xqc_cid_t *new_cid, void *user_data) { - xqc_demo_svr_user_conn_t *user_conn = (xqc_demo_svr_user_conn_t *)user_data; - memcpy(&user_conn->cid, new_cid, sizeof(*new_cid)); + // xqc_demo_svr_user_conn_t *user_conn = (xqc_demo_svr_user_conn_t *)user_data; + // memcpy(&user_conn->cid, new_cid, sizeof(*new_cid)); } /****************************************************************************** @@ -396,8 +412,14 @@ xqc_demo_svr_hq_conn_close_notify(xqc_hq_conn_t *conn, const xqc_cid_t *cid, voi } xqc_demo_svr_user_conn_t *user_conn = (xqc_demo_svr_user_conn_t*)conn_user_data; + xqc_conn_stats_t stats = xqc_conn_get_stats(user_conn->ctx->engine, cid); + printf("send_count:%u, lost_count:%u, tlp_count:%u, recv_count:%u, srtt:%"PRIu64" " + "early_data_flag:%d, conn_err:%d, ack_info:%s, path_info:%s\n", stats.send_count, + stats.lost_count, stats.tlp_count, stats.recv_count, stats.srtt, + stats.early_data_flag, stats.conn_err, stats.ack_info, stats.conn_info); free(user_conn); user_conn = NULL; + return 0; } @@ -465,10 +487,19 @@ xqc_demo_svr_hq_send_file(xqc_hq_request_t *hqr, xqc_demo_svr_user_stream_t *use } else { /* prev buf sent, read new buf and send */ res->buf_offset = 0; - res->buf_len = fread(res->buf, 1, res->buf_size, res->fp); - if (res->buf_len <= 0) { - return -1; + + if (!svr_ctx.args->quic_cfg.dummy_mode) { + res->buf_len = fread(res->buf, 1, res->buf_size, res->fp); + if (res->buf_len <= 0) { + return -1; + } + + } else { + res->buf_len = res->total_len - res->total_offset; + res->buf_len = res->buf_len > res->buf_size ? res->buf_size : res->buf_len; + memset(res->buf, 'D', res->buf_len); } + send_buf = res->buf; send_len = res->buf_len; } @@ -499,14 +530,27 @@ xqc_demo_svr_handle_hq_request(xqc_demo_svr_user_stream_t *user_stream, xqc_hq_r { int ret = 0; - /* format file path */ - char file_path[PATH_LEN] = {0}; - snprintf(file_path, sizeof(file_path), "%s%s", svr_ctx.args->env_cfg.source_file_dir, resource); - user_stream->res.fp = fopen(file_path, "rb"); - if (NULL == user_stream->res.fp) { - printf("error open file [%s]\n", file_path); - goto handle_error; + if (!svr_ctx.args->quic_cfg.dummy_mode) { + /* format file path */ + char file_path[PATH_LEN] = {0}; + snprintf(file_path, sizeof(file_path), "%s%s", svr_ctx.args->env_cfg.source_file_dir, resource); + user_stream->res.fp = fopen(file_path, "rb"); + if (NULL == user_stream->res.fp) { + printf("error open file [%s]\n", file_path); + goto handle_error; + } + /* get total len */ + fseek(user_stream->res.fp, 0, SEEK_END); + user_stream->res.total_len = ftello(user_stream->res.fp); + fseek(user_stream->res.fp, 0, SEEK_SET); + + } else { + user_stream->res.total_len = atoi(resource + 1); + if (user_stream->res.total_len == 0) { + user_stream->res.total_len = 1; + } } + // printf("open file[%s] suc, user_conn: %p\n", file_path, user_stream->conn); /* create buf */ @@ -517,10 +561,6 @@ xqc_demo_svr_handle_hq_request(xqc_demo_svr_user_stream_t *user_stream, xqc_hq_r } user_stream->res.buf_size = READ_FILE_BUF_LEN; - /* get total len */ - fseek(user_stream->res.fp, 0, SEEK_END); - user_stream->res.total_len = ftell(user_stream->res.fp); - fseek(user_stream->res.fp, 0, SEEK_SET); /* begin to send file */ ret = xqc_demo_svr_hq_send_file(hqr, user_stream); @@ -616,9 +656,9 @@ xqc_demo_svr_h3_conn_close_notify(xqc_h3_conn_t *h3_conn, const xqc_cid_t *cid, xqc_demo_svr_user_conn_t *user_conn = (xqc_demo_svr_user_conn_t*)conn_user_data; xqc_conn_stats_t stats = xqc_conn_get_stats(user_conn->ctx->engine, cid); printf("send_count:%u, lost_count:%u, tlp_count:%u, recv_count:%u, srtt:%"PRIu64" " - "early_data_flag:%d, conn_err:%d, ack_info:%s\n", stats.send_count, + "early_data_flag:%d, conn_err:%d, ack_info:%s, path_info:%s\n", stats.send_count, stats.lost_count, stats.tlp_count, stats.recv_count, stats.srtt, - stats.early_data_flag, stats.conn_err, stats.ack_info); + stats.early_data_flag, stats.conn_err, stats.ack_info, stats.conn_info); free(user_conn); user_conn = NULL; @@ -713,10 +753,18 @@ xqc_demo_svr_send_body(xqc_demo_svr_user_stream_t *user_stream) } else { /* prev buf sent, read new buf and send */ res->buf_offset = 0; - res->buf_len = fread(res->buf, 1, res->buf_size, res->fp); - if (res->buf_len <= 0) { - return -1; + if (!svr_ctx.args->quic_cfg.dummy_mode) { + res->buf_len = fread(res->buf, 1, res->buf_size, res->fp); + if (res->buf_len <= 0) { + return -1; + } + + } else { + res->buf_len = res->total_len - res->total_offset; + res->buf_len = res->buf_len > res->buf_size ? res->buf_size : res->buf_len; + memset(res->buf, 'D', res->buf_len); } + send_buf = res->buf; send_len = res->buf_len; } @@ -773,15 +821,27 @@ xqc_demo_svr_handle_h3_request(xqc_demo_svr_user_stream_t *user_stream, rsp_hdrs.headers = rsp_hdr; rsp_hdrs.count = sizeof(rsp_hdr) / sizeof(rsp_hdr[0]); - /* format file path */ - char file_path[PATH_LEN] = {0}; - snprintf(file_path, sizeof(file_path), "%s%s", - svr_ctx.args->env_cfg.source_file_dir, user_stream->recv_buf); - user_stream->res.fp = fopen(file_path, "rb"); - if (NULL == user_stream->res.fp) { - printf("error open file [%s]\n", file_path); - xqc_demo_svr_set_rsp_header_value_int(&rsp_hdrs, H3_HDR_STATUS, 404); - goto h3_handle_error; + if (!svr_ctx.args->quic_cfg.dummy_mode) { + /* format file path */ + char file_path[PATH_LEN] = {0}; + snprintf(file_path, sizeof(file_path), "%s%s", + svr_ctx.args->env_cfg.source_file_dir, user_stream->recv_buf); + user_stream->res.fp = fopen(file_path, "rb"); + if (NULL == user_stream->res.fp) { + printf("error open file [%s]\n", file_path); + xqc_demo_svr_set_rsp_header_value_int(&rsp_hdrs, H3_HDR_STATUS, 404); + goto h3_handle_error; + } + /* get total len */ + fseek(user_stream->res.fp, 0, SEEK_END); + user_stream->res.total_len = ftello(user_stream->res.fp); + fseek(user_stream->res.fp, 0, SEEK_SET); + + } else { + user_stream->res.total_len = atoi(user_stream->recv_buf + 1); + if (user_stream->res.total_len == 0) { + user_stream->res.total_len = 1; + } } /* create buf */ @@ -793,10 +853,6 @@ xqc_demo_svr_handle_h3_request(xqc_demo_svr_user_stream_t *user_stream, } user_stream->res.buf_size = READ_FILE_BUF_LEN; - /* get total len */ - fseek(user_stream->res.fp, 0, SEEK_END); - user_stream->res.total_len = ftell(user_stream->res.fp); - fseek(user_stream->res.fp, 0, SEEK_SET); xqc_demo_svr_set_rsp_header_value_int(&rsp_hdrs, H3_HDR_CONTENT_LENGTH, user_stream->res.total_len); @@ -928,6 +984,12 @@ xqc_demo_svr_write_socket(const unsigned char *buf, size_t size, const struct so return res; } +ssize_t +xqc_demo_svr_write_socket_ex(uint64_t path_id, const unsigned char *buf, size_t size, + const struct sockaddr *peer_addr,socklen_t peer_addrlen, void *conn_user_data) +{ + return xqc_demo_svr_write_socket(buf, size, peer_addr, peer_addrlen, conn_user_data); +} void xqc_demo_svr_socket_write_handler(xqc_demo_svr_ctx_t *ctx, int fd) @@ -961,10 +1023,17 @@ xqc_demo_svr_socket_read_handler(xqc_demo_svr_ctx_t *ctx, int fd) recv_sum += recv_size; uint64_t recv_time = xqc_demo_now(); +#ifdef XQC_NO_PID_PACKET_PROCESS xqc_int_t ret = xqc_engine_packet_process(ctx->engine, packet_buf, recv_size, (struct sockaddr *)(&ctx->local_addr), ctx->local_addrlen, (struct sockaddr *)(&peer_addr), peer_addrlen, (xqc_usec_t)recv_time, ctx); +#else + xqc_int_t ret = xqc_engine_packet_process(ctx->engine, packet_buf, recv_size, + (struct sockaddr *)(&ctx->local_addr), ctx->local_addrlen, + (struct sockaddr *)(&peer_addr), peer_addrlen, XQC_UNKNOWN_PATH_ID, + (xqc_usec_t)recv_time, ctx); +#endif if (ret != XQC_OK) { printf("server_read_handler: packet process err, ret: %d\n", ret); return; @@ -1100,10 +1169,15 @@ xqc_demo_svr_usage(int argc, char *argv[]) " -c Congestion Control Algorithm. r:reno b:bbr c:cubic\n" " -C Pacing on.\n" " -l Log level. e:error d:debug.\n" - " -L xuqic log directory.\n" + " -L xquic log directory.\n" " -6 IPv6\n" " -k Key output file path\n" " -r retry\n" + " -d do not read responses from files\n" + " -M enable MPQUIC.\n" + " -P enable MPQUIC to return ACK_MPs on any paths.\n" + " -s multipath scheduler (interop, minrtt, backup), default: interop\n" + " -R Reinjection (1,2,4) \n" , prog); } @@ -1136,6 +1210,7 @@ xqc_demo_svr_init_args(xqc_demo_svr_args_t *args) args->env_cfg.log_level = XQC_LOG_DEBUG; strncpy(args->env_cfg.log_path, LOG_PATH, TLS_GROUPS_LEN - 1); strncpy(args->env_cfg.source_file_dir, SOURCE_DIR, RESOURCE_LEN - 1); + strncpy(args->env_cfg.key_out_path, KEY_PATH, PATH_LEN - 1); strncpy(args->env_cfg.priv_key_path, PRIV_KEY_PATH, PATH_LEN - 1); strncpy(args->env_cfg.cert_pem_path, CERT_PEM_PATH, PATH_LEN - 1); } @@ -1144,7 +1219,7 @@ void xqc_demo_svr_parse_args(int argc, char *argv[], xqc_demo_svr_args_t *args) { int ch = 0; - while ((ch = getopt(argc, argv, "p:c:CD:l:L:6k:r")) != -1) { + while ((ch = getopt(argc, argv, "p:c:CD:l:L:6k:rdMPs:R:")) != -1) { switch (ch) { /* listen port */ case 'p': @@ -1214,6 +1289,31 @@ xqc_demo_svr_parse_args(int argc, char *argv[], xqc_demo_svr_args_t *args) args->quic_cfg.retry_on = 1; break; + case 'd': + printf("option dummpy mode on\n"); + args->quic_cfg.dummy_mode = 1; + break; + + case 'M': + printf("option multipath enabled\n"); + args->quic_cfg.multipath = 1; + break; + + case 'P': + printf("option ACK_MP on any path enabled\n"); + args->quic_cfg.mp_ack_on_any_path = 1; + break; + + case 's': + printf("option scheduler: %s\n", optarg); + strncpy(args->quic_cfg.mp_sched, optarg, 32); + break; + + case 'R': + printf("option reinjection: %s\n", optarg); + args->quic_cfg.reinjection = atoi(optarg); + break; + default: printf("other option :%c\n", ch); xqc_demo_svr_usage(argc, argv); @@ -1239,6 +1339,7 @@ xqc_demo_svr_init_callback(xqc_engine_callback_t *cb, xqc_transport_callbacks_t static xqc_transport_callbacks_t tcb = { .server_accept = xqc_demo_svr_accept, .write_socket = xqc_demo_svr_write_socket, + .write_socket_ex = xqc_demo_svr_write_socket_ex, .conn_update_cid_notify = xqc_demo_svr_conn_update_cid_notify, }; @@ -1287,13 +1388,28 @@ xqc_demo_svr_init_conn_settings(xqc_demo_svr_args_t *args) case CC_TYPE_CUBIC: ccc = xqc_cubic_cb; break; +#ifdef XQC_ENABLE_RENO case CC_TYPE_RENO: ccc = xqc_reno_cb; break; +#endif default: break; } + xqc_scheduler_callback_t sched; + if (strncmp(args->quic_cfg.mp_sched, "minrtt", strlen("minrtt")) == 0) { + sched = xqc_minrtt_scheduler_cb; + + } if (strncmp(args->quic_cfg.mp_sched, "backup", strlen("backup")) == 0) { + sched = xqc_backup_scheduler_cb; + + } else { +#ifdef XQC_ENABLE_MP_INTEROP + sched = xqc_interop_scheduler_cb; +#endif + } + /* init connection settings */ xqc_conn_settings_t conn_settings = { .pacing_on = args->net_cfg.pacing, @@ -1304,6 +1420,12 @@ xqc_demo_svr_init_conn_settings(xqc_demo_svr_args_t *args) }, .spurious_loss_detect_on = 1, .init_idle_time_out = 60000, + .enable_multipath = args->quic_cfg.multipath, + .mp_ack_on_any_path = args->quic_cfg.mp_ack_on_any_path, + .scheduler_callback = sched, + .reinj_ctl_callback = xqc_deadline_reinj_ctl_cb, + .mp_enable_reinjection = args->quic_cfg.reinjection, + .standby_path_probe_timeout = 1000, }; xqc_server_set_conn_settings(&conn_settings); diff --git a/demo/xqc_hq.h b/demo/xqc_hq.h index 93e41e09b..6b302d712 100644 --- a/demo/xqc_hq.h +++ b/demo/xqc_hq.h @@ -7,6 +7,7 @@ #include #include +#include #include @@ -144,5 +145,8 @@ xqc_hq_request_recv_rsp(xqc_hq_request_t *hqr, char *res_buf, size_t buf_sz, uin xqc_int_t xqc_hq_request_close(xqc_hq_request_t *hqr); +xqc_request_stats_t +xqc_hq_request_get_stats(xqc_hq_request_t *hqr); + #endif diff --git a/demo/xqc_hq_request.c b/demo/xqc_hq_request.c index d30ec9c4e..4c655ee4b 100644 --- a/demo/xqc_hq_request.c +++ b/demo/xqc_hq_request.c @@ -85,7 +85,7 @@ xqc_hq_request_create(xqc_engine_t *engine, xqc_hq_conn_t *hqc, const xqc_cid_t } /* create stream, make hqr the user_data of xqc_stream_t */ - stream = xqc_stream_create(engine, cid, hqr); + stream = xqc_stream_create(engine, cid, NULL, hqr); if (NULL == stream) { PRINT_LOG("create transport-level stream error"); goto fail; @@ -315,7 +315,11 @@ xqc_hq_request_recv_req(xqc_hq_request_t *hqr, char *res_buf, size_t buf_sz, uin ssize_t xqc_hq_request_recv_rsp(xqc_hq_request_t *hqr, char *res_buf, size_t buf_sz, uint8_t *fin) { - return xqc_stream_recv(hqr->stream, res_buf, buf_sz, fin); + ssize_t ret = xqc_stream_recv(hqr->stream, res_buf, buf_sz, fin); + if (ret > 0) { + hqr->recv_cnt += ret; + } + return ret; } void @@ -324,6 +328,57 @@ xqc_hq_request_set_user_data(xqc_hq_request_t *hqr, void *user_data) hqr->user_data = user_data; } +xqc_request_stats_t +xqc_hq_request_get_stats(xqc_hq_request_t *hqr) +{ + xqc_request_stats_t stats; + xqc_memzero(&stats, sizeof(stats)); + + xqc_stream_t *stream = hqr->stream; + uint64_t conn_err = hqr->stream->stream_conn->conn_err; + + stats.recv_body_size = hqr->recv_cnt; + stats.send_body_size = hqr->sent_cnt; + stats.stream_err = conn_err != 0 ? conn_err : hqr->stream->stream_err; + + char *buff = stats.stream_info; + size_t buff_size = XQC_STREAM_INFO_LEN; + size_t cursor = 0, ret = 0; + int i; + + for (int i = 0; i < XQC_MAX_PATHS_COUNT; ++i) { + if ((stream->paths_info[i].path_send_bytes > 0) + || (stream->paths_info[i].path_recv_bytes > 0)) + { + + ret = snprintf(buff + cursor, buff_size - cursor, + "%"PRIu64"-%"PRIu64"-%"PRIu64"-%"PRIu64"-%"PRIu64"#", + stream->paths_info[i].path_id, + stream->paths_info[i].path_pkt_send_count, + stream->paths_info[i].path_pkt_recv_count, + stream->paths_info[i].path_send_bytes, + stream->paths_info[i].path_recv_bytes); + cursor += ret; + + if (cursor >= buff_size) { + goto full; + } + } + } + +full: + cursor = xqc_min(cursor, buff_size); + for (i = cursor - 1; i >= 0; i--) { + if (buff[i] == '-' || buff[i] == '#') { + buff[i] = '\0'; + break; + } + } + buff[buff_size - 1] = '\0'; + + return stats; +} + int xqc_hq_stream_create_notify(xqc_stream_t *stream, void *strm_user_data) diff --git a/docs/API.md b/docs/API.md index 31346b329..2281b818d 100644 --- a/docs/API.md +++ b/docs/API.md @@ -464,6 +464,13 @@ xqc_int_t xqc_conn_get_errno(xqc_connection_t *conn); Get error code of specified connection. +#### xqc_conn_get_ssl +``` +void *xqc_conn_get_ssl(xqc_connection_t *conn); +``` +Get ssl handler of specified connection. + + #### xqc_conn_set_transport_user_data ``` void xqc_conn_set_transport_user_data(xqc_connection_t *conn, void *user_data); @@ -839,6 +846,13 @@ xqc_int_t xqc_h3_conn_get_errno(xqc_h3_conn_t *h3c); Get connection error code. +#### xqc_h3_conn_get_ssl +``` +void *xqc_h3_conn_get_ssl(xqc_h3_conn_t *h3c); +``` +Get ssl handler of http3 connection. + + #### xqc_h3_conn_set_user_data ``` void xqc_h3_conn_set_user_data(xqc_h3_conn_t *h3c, void *user_data); diff --git a/docs/docs-zh/CONTRIBUTING-zh.md b/docs/docs-zh/CONTRIBUTING-zh.md index 53ff2fbc7..aa6dba11a 100644 --- a/docs/docs-zh/CONTRIBUTING-zh.md +++ b/docs/docs-zh/CONTRIBUTING-zh.md @@ -202,6 +202,8 @@ XQUIC开发团队:大淘宝平台技术团队、达摩院XG实验室以及为 * 曾柯(毅丝) * 徐盟欣(象谦) * Bai Shi(白石) +* 周瑞琪(凼凼) +* 李亮(海苍) * @chinsyo * @L1MeN9Yu * @flx413 diff --git a/docs/docs-zh/README-zh.md b/docs/docs-zh/README-zh.md index a4a3a54d8..bf8393313 100644 --- a/docs/docs-zh/README-zh.md +++ b/docs/docs-zh/README-zh.md @@ -45,7 +45,7 @@ cd - # 使用 BabaSSL 编译 XQUIC git submodule update --init --recursive mkdir -p build; cd build -cmake -DGCOV=on -DCMAKE_BUILD_TYPE=Debug -DXQC_ENABLE_TESTING=1 -DXQC_SUPPORT_SENDMMSG_BUILD=1 -DXQC_ENABLE_EVENT_LOG=1 -DXQC_ENABLE_BBR2=1 -DXQC_DISABLE_RENO=0 -DSSL_TYPE=${SSL_TYPE_STR} -DSSL_PATH=${SSL_PATH_STR} -DSSL_INC_PATH=${SSL_INC_PATH_STR} -DSSL_LIB_PATH=${SSL_LIB_PATH_STR} .. +cmake -DGCOV=on -DCMAKE_BUILD_TYPE=Debug -DXQC_ENABLE_TESTING=1 -DXQC_SUPPORT_SENDMMSG_BUILD=1 -DXQC_ENABLE_EVENT_LOG=1 -DXQC_ENABLE_BBR2=1 -DXQC_ENABLE_RENO=1 -DSSL_TYPE=${SSL_TYPE_STR} -DSSL_PATH=${SSL_PATH_STR} -DSSL_INC_PATH=${SSL_INC_PATH_STR} -DSSL_LIB_PATH=${SSL_LIB_PATH_STR} .. make -j ``` @@ -72,7 +72,7 @@ cd ../.. # 使用 BoringSSL 编译 XQUIC git submodule update --init --recursive mkdir -p build; cd build -cmake -DGCOV=on -DCMAKE_BUILD_TYPE=Debug -DXQC_ENABLE_TESTING=1 -DXQC_SUPPORT_SENDMMSG_BUILD=1 -DXQC_ENABLE_EVENT_LOG=1 -DXQC_ENABLE_BBR2=1 -DXQC_DISABLE_RENO=0 -DSSL_TYPE=${SSL_TYPE_STR} -DSSL_PATH=${SSL_PATH_STR} -DSSL_INC_PATH=${SSL_INC_PATH_STR} -DSSL_LIB_PATH=${SSL_LIB_PATH_STR} .. +cmake -DGCOV=on -DCMAKE_BUILD_TYPE=Debug -DXQC_ENABLE_TESTING=1 -DXQC_SUPPORT_SENDMMSG_BUILD=1 -DXQC_ENABLE_EVENT_LOG=1 -DXQC_ENABLE_BBR2=1 -DXQC_ENABLE_RENO=1 -DSSL_TYPE=${SSL_TYPE_STR} -DSSL_PATH=${SSL_PATH_STR} -DSSL_INC_PATH=${SSL_INC_PATH_STR} -DSSL_LIB_PATH=${SSL_LIB_PATH_STR} .. make -j ``` diff --git a/docs/translation/draft-ietf-quic-http-34-zh.md b/docs/translation/draft-ietf-quic-http-34-zh.md index 41985471d..253253f30 100644 --- a/docs/translation/draft-ietf-quic-http-34-zh.md +++ b/docs/translation/draft-ietf-quic-http-34-zh.md @@ -34,9 +34,7 @@ HTTP/3提供了通过QUIC传输协议来传输HTTP语义的功能,和类似HTT QUIC提供了协议协商,基于流的多路复用,以及流控制。3.1节中描述了如何发现HTTP/3终端。 在每条流中,HTTP/3通信的基本单元是帧(frame,参考7.2节)。出于不同目的,定义了每个帧的类型。 -比如,HEADERS和DATA帧是HTTP请求和响应的基础(参考4.1节)。 -Frames that apply to the entire connection are conveyed on a dedicated control stream. -适用于整个连接的帧在专用的控制流上传输。 +比如,HTTP请求和响应由HEADERS和DATA帧承载(参考4.1节),适用于整个连接的帧在专用的控制流上传输。 [QUIC-TRANSPORT]的第2章中,描述了使用QUIC流的抽象概念来实现请求的多路复用。 每个请求-响应对预设了一个QUIC流。每个流之间都是独立的,因此一个发生阻塞、丢包的流不会影响其他流的进展。 @@ -84,11 +82,9 @@ QPACK使用了隔离的单向流来修改和追踪字段表状态,而编码后 当引用[QUIC-TRANSPORT]中的帧时,帧的名字之前会添加引语“QUIC”。比如,“QUIC CONNECTION_CLOSE 帧”。 没有这个引语的引用(帧)指7.2节中定义的帧。 - HTTP/3 connection: A QUIC connection where the negotiated - application protocol is HTTP/3. HTTP/3连接(HTTP/3 connection):一个QUIC连接,其协商的应用协议是HTTP/3。 -对端(peer):一个终端。在讨论某个终端时,"对端 "指的是与主要讨论对象相距较远的终端。 +对端(peer):一个终端。在讨论某个终端时,"对端"指的是与当前讨论对象相距较远的终端。 接收端(receiver):一个正在接收帧的终端。 @@ -96,7 +92,7 @@ HTTP/3连接(HTTP/3 connection):一个QUIC连接,其协商的应用协 服务端:接受一个HTTP/3连接的端点。 服务端接收HTTP请求,并发送HTTP响应。 -流(stream):QUIC传输提供的一个双向或者单向的字节流。一个HTTP/3连接内的所有流都可以被认为是 "HTTP/3流",但HTTP/3内定义了多种流类型。 +流(stream):QUIC传输提供的一个双向或者单向的字节流。一个HTTP/3连接内的所有流都可以被认为是"HTTP/3流",但HTTP/3内定义了多种流类型。 流错误(stream error):个别HTTP/3流上的应用级(application-level)错误。 @@ -113,7 +109,7 @@ HTTP/3连接(HTTP/3 connection):一个QUIC连接,其协商的应用协 HTTP依赖了权威响应的概念:对于指定目标资源,(在目标URI中识别的)源服务端组织响应消息的时候,认为对请求最合适的那个响应。 在 [SEMANTICS] 的4.3节中讨论了给一个HTTP URI设置权威的服务端的事项。 -“Https”方案将权限和证书结合起来,其中客户端认为(通过URI的权威模块识别的)主机的证书是可信的。 +“Https”模式将权限和证书结合起来,其中客户端认为(通过URI的权威模块识别的)主机的证书是可信的。 在收到TLS握手的服务器证书后,客户端必须使用[SEMANTICS]第4.3.4节中描述的过程来验证该证书是否与URI的源服务器相匹配。 如果证书不能对URI的源服务器进行验证,客户端必须不考虑(MUST NOT)该服务器对该源的权威性。 客户端可以(MAY)使用带“https”的URI来访问资源:首先将主机标识解析为IP地址, @@ -123,7 +119,7 @@ HTTP依赖了权威响应的概念:对于指定目标资源,(在目标URI 连通性问题(例如,阻塞UDP)可能导致QUIC建连失败;这种情况下客户端应当(SHOULD)尝试使用基于TCP的HTTP版本。 -服务端可以(MAY)在任何UDP端口上开启HTTP/3服务;另外一种服务通告的方式总是包含了一个显式端口,以及包含和方案相关的显式端口或者默认端口的URI。 +服务端可以(MAY)在任何UDP端口上开启HTTP/3服务;另外一种服务通告的方式总是包含了一个显式端口,以及包含和模式相关的显式端口或者默认端口的URI。 ### 3.1.1. HTTP Alternative Services @@ -134,13 +130,13 @@ HTTP依赖了权威响应的概念:对于指定目标资源,(在目标URI ### 3.1.2. Other Schemes -尽管HTTP独立于传输协议,“http”方案将 权限 和 在权限模块认证的任何主机的指定端口上接受TCP连接的能力结合起来。 +尽管HTTP独立于传输协议,“http”模式将 权限 和 在权限模块认证的任何主机的指定端口上接受TCP连接的能力结合起来。 由于HTTP/3不使用TCP,因此它无法直接访问权威服务端上由“http”URI识别的资源。 然而,诸如[ALTSVR]的协议扩展允许权威服务端来识别其他的权威但可以通过HTTP/3访问的服务。 -在对不是“https”方案的源发送请求之前,客户端必须(MUST)保证服务端愿意为该方案提供服务。 -对于 "http 方案"的源,[RFC8164]中描述了一种实验性的方法来实现这一目标。 -将来可能会为各种方案定义其他机制。 +在对不是“https”模式的源发送请求之前,客户端必须(MUST)保证服务端愿意为该模式提供服务。 +对于 "http 模式"的源,[RFC8164]中描述了一种实验性的方法来实现这一目标。 +将来可能会为各种模式定义其他机制。 ## 3.2 Connection Establishment @@ -155,7 +151,7 @@ QUIC连接的建立如 [QUIC-TRANSPORT] 中所述。在建立连接的过程中 ## 3.3. Connection Reuse -HTTP/3连接可用于多个请求。为了达到最好的性能,如果客户端一般不会关闭连接,除非客户端认为不再需要继续和服务端进行通信(比如,用户离开指定web页),或者知道服务端关闭了连接。 +HTTP/3连接可用于多个请求。为了达到最好的性能,客户端一般不会主动关闭连接,除非客户端认为不再需要继续和服务端进行通信(比如,用户离开指定web页),或者知道服务端已经关闭了连接。 一旦建立上和服务端的连接,该连接可以(MAY)用于请求其他URI,只要服务端是经过认证的。 要将现有连接用于新的源,客户端必须通过 [SEMANTICS] 第4.3.4节中所述的过程,来验证服务端为新的源服务端提供的证书。 这意味着客户端将需要保留服务端证书以及验证该证书所需的任何其他信息,不这样做的客户端将无法为其他的源重用该连接。 @@ -173,21 +169,21 @@ HTTP/3连接可用于多个请求。为了达到最好的性能,如果客户 ## 4.1. HTTP Message Exchanges 客户端在客户端发起的双向QUIC流上发送HTTP请求。客户端必须(MUST)在一个指定的流上发送单个请求。 -服务端在和请求相同的流上发送0到多个临时的HTTP响应,然后再发单个最终的HTTP响应,详见下文。关于临时和最终HTTP响应的描述,请参见[SEMANTICS]的第15章。 +服务端在和请求相同的流上发送0到多个中间HTTP响应,然后再发单个最终HTTP响应,详见下文。关于中间和最终HTTP响应的描述,请参见[SEMANTICS]的第15章。 推送的响应在一个服务端发起的单向QUIC流上进行发送;参考6.2.2小节。 -和标准的响应方式一样,服务端发送0到多个临时的HTTP响应,然后发送单个最终的HTTP响应。 +和标准的响应方式一样,服务端发送0到多个中间HTTP响应,然后发送单个最终HTTP响应。 在4.4节中对推送进行了更加详细的描述。 -在指定的流上,收到多个请求,或者在一个最终的HTTP响应之后再收到一个HTTP响应,必须(MUST)把这种情况当成是异常。 +在指定的流上,收到多个请求,或者在一个最终HTTP响应之后再收到一个HTTP响应,必须(MUST)把这种情况当成是异常。 一个HTTP消息(请求或响应)由以下内容组成: -1. 头部字段区:作为单个HEADERS帧发送(参考7.2.2小节)。 +1. 头部段:作为单个HEADERS帧发送(参考7.2.2小节)。 1. (可选项)载荷:如果有载荷,就作为一列DATA帧发送(参考7.2.1小节), -1. (可选项)尾部字段区:如果有这个字段,则以单个HEADERS帧的形式发送。 +1. (可选项)尾部段:如果有这个字段,则以单个HEADERS帧的形式发送。 -头部字段区和尾部字段区在 [SEMANTICS] 的6.3和6.5节中进行了说明;载荷在 [SEMANTICS] 的6.4节中进行了说明。 +头部段和尾部段在 [SEMANTICS] 的6.3和6.5节中进行了说明;载荷在 [SEMANTICS] 的6.4节中进行了说明。 如果收到一列无效的帧,必须(MUST)把这种情况当成是H3_FRAME_UNEXPECTED类型的连接错误(第8章)。实际上,出现在HEADERS帧之前的DATA帧,或者出现在尾部HEADERS帧之后的HEADERS或DATA帧,都是无效的。其他帧类型(尤其是未知帧类型)可能会受其自身规则的约束,请参阅第9节。 @@ -198,19 +194,18 @@ HTTP/3连接可用于多个请求。为了达到最好的性能,如果客户 HEADERS和PUSH_PROMISE帧可能引用QPACK动态表的更新。虽然这些更新不是消息交换的直接部分,但是他们必须在消息可以被销毁前被接收并处理,详情参考4.1.1. -Transfer codings (see Section 6.1 of [HTTP11]) are not defined for HTTP/3; the Transfer-Encoding header field MUST NOT be used. -未为HTTP/3定义传输编码(请参见[HTTP11]的6.1节),不得(MUST NOT)使用Transfer-Encoding标头字段。 +未为HTTP/3定义传输编码(请参见[HTTP11]的6.1节),因此不得(MUST NOT)使用Transfer-Encoding标头字段。 -当且只当同一个请求的最终响应之前有一个或多个信息响应(1xx,参考 [SEMANTICS] 第15.2节),响应才可以(MAY)包括多个消息。临时响应不包含载荷体或尾部。 +当且只当同一个请求的最终响应之前有一个或多个信息响应(1xx,参考 [SEMANTICS] 第15.2节),响应才可以(MAY)包括多个消息。中间响应不包含载荷体或尾部。 -一个HTTP请求/响应交换完全占用了客户端发起的双向QUIC流。在发送请求之后,客户端必须(MUST)关闭流的发送。除非使用了CONNECT方法(参考4.2节),客户端必须不能(MUST NOT)根据收到的响应来关闭流。在发送最终响应之后,服务端必须(MUST)关闭流的发送。此刻,QUIC流就完全关闭了。 +一个HTTP请求/响应交换完全占用了客户端发起的双向QUIC流。在发送请求之后,客户端必须(MUST)关闭流的发送。除非使用了CONNECT方法(参考4.2节),客户端不得(MUST NOT)根据收到的响应来关闭流。在发送最终响应之后,服务端必须(MUST)关闭流的发送。此刻,QUIC流就完全关闭了。 当关闭了一条流,这意味着最终HTTP消息(final HTTP message)结束了。因为一些消息过大,只要收到消息足够多的内容来进行处理,终端应当(SHOULD)开始处理并发HTTP消息。如果尚未收到足够多的消息来生成一个完整的响应,但是客户端发起的流关闭了连接,服务端应当(SHOULD)以H3_REQUEST_INCOMPLETE错误码终止响应(见第8章)。 如果响应不依赖请求的任何部分,即使请求还没有发送和接收,服务端可以在客户端发送一整个请求之前发送一个完整的响应。 当服务端不需要接收请求的剩余部分,它可以(MAY)终止读取请求流,发送一个完整的响应,并干净地关闭流的发送部分。 当要求客户端停止在请求流上发送时,应当(SHOULD)使用H3_NO_ERROR错误码。 -尽管客户端总是可以因为其他原因而丢弃响应,但是在客户在请求突然终止之后,它必须不能(MUST NOT)丢弃完整的响应。 +尽管客户端总是可以因为其他原因而丢弃响应,但是在客户在请求突然终止之后,它不得(MUST NOT)丢弃完整的响应。 如果服务端发送了部分或者完整的响应,但是没有终止读取请求,客户端应当(SHOULD)继续发送请求的实体,并正常关闭流。 ### 4.1.1. Field Formatting and Compression @@ -224,10 +219,10 @@ HTTP消息以 一系列被称为HTTP字段的 key-value对的形式 携带了元 与HTTP/2一样,HTTP/3不使用Connection头部字段来表示连接特定的字段; 在这个协议中,连接特定元数据通过其他方式传输。 -终端必须不能(MUST NOT)生成包含连接特定字段的HTTP/3字段区。 +终端不得(MUST NOT)生成包含连接特定字段的HTTP/3段。 任何包含连接特定字段的消息必须(MUST)被当成异常(4.1.3小节)。 -唯一的例外是TE头部字段,它可以(MAY)出现在HTTP/3请求头部中;当它出现时,除了“trailiers”,它必须不能(MUST NOT)包含任何其他值。 +唯一的例外是TE头部字段,它可以(MAY)出现在HTTP/3请求头部中;当它出现时,除了“trailiers”,它不得(MUST NOT)包含任何其他值。 将HTTP/1.x消息转换为HTTP/3的中间适配器必须删除[SEMANTICS]第7.6.1节中讨论的特定连接头部字段,否则它们的消息将被其他HTTP/3终端视为非法的(第4.1.3节)。 @@ -236,41 +231,41 @@ HTTP消息以 一系列被称为HTTP字段的 key-value对的形式 携带了元 和HTTP/2一样,HTTP/3采用了一系列以 ’:’(ASCII码 0x3a) 字符开始的伪头部字段。 这些为头部字段携带了目标URI,请求的方法,以及响应的状态码。 -伪头部字段不是HTTP字段。除了本文中定义的,终端必须不能(MUST NOT)生成其他的伪头部字段; -然而,一个扩展可以协商修改此限制(参见第9节)。 +伪头部字段不是HTTP字段。除了本文中定义的,终端不得(MUST NOT)生成其他的伪头部字段; +不过,一个扩展可以协商修改此限制(参见第9节)。 伪头部字段只在它们定义的上下文中有效。 -为请求定义的伪头部字段必须不能(MUST NOT)出现在响应中;为响应定义的伪头部字段必须不能(MUST NOT)出现在请求中。 -伪头部字段必须不能(MUST NOT)出现在尾部字段。 +为请求定义的伪头部字段不得(MUST NOT)出现在响应中;为响应定义的伪头部字段不得(MUST NOT)出现在请求中。 +伪头部字段不得(MUST NOT)出现在尾部字段。 终端必须(MUST)把包含未定义或者无效伪头部字段的请求或响应当成是异常(参考4.1.3)。 -所有伪头部字段必须(MUST)出现在头部字段区的常规头部字段之前。 -任何请求或响应中,如果头部字段区中,伪头部字段出现在常规头部字段之后,必须(MUST)认为这是一种异常。(4.1.3小节) +所有伪头部字段必须(MUST)出现在头部段的常规头部字段之前。 +任何请求或响应中,如果头部段中,伪头部字段出现在常规头部字段之后,必须(MUST)认为这是一种异常。(4.1.3小节) 请求的伪头部字段定义如下: ":method":包含了HTTP方法([SEMANTICS]第9章)。 -":scheme":包含了目标URI的方案部分([URI]第3.1小节)。 -":scheme”不限于”http”和”https”方案的URI。一个代理或者网管可以将请求转换成非HTTP的方案,从而来使HTTP与非HTTP服务进行交互。 -关于使用 "https "以外的方案的指导,请参见3.1.2节。 +":scheme":包含了目标URI的模式部分([URI]第3.1小节)。 +":scheme”不限于”http”和”https”模式的URI。一个代理或者网管可以将请求转换成非HTTP的模式,从而来使HTTP与非HTTP服务进行交互。 +关于使用 "https "以外的模式的指导,请参见3.1.2节。 -":authority”:包含了目标URI的权限部分([URI第3.2节])。权限必须不能(MUST NOT)在”http”或”https”方案中包含已废弃的”userinfo”子模块。 +":authority”:包含了目标URI的权限部分([URI第3.2节])。权限不得(MUST NOT)在”http”或”https”模式中包含已废弃的”userinfo”子模块。 为了保证准确赋值HTTP/1.1请求行,对 - 具有原始形式或者星号形式请求目标的 - HTTP/1.1请求进行转换时,必须(MUST)删除这个伪头部字段。(参考[SEMANTICS]第7.1节)。 直接生成HTTP/3请求的客户端应当(SHOULD)使用":authority”伪头部字段来替代Host字段。 如果HTTP/3请求中没有Host字段,一个将HTTP/3请求转换成HTTP/1.1请求的中间媒介必须(MUST)构建一个Host字段,并将”:authority”伪头部字段的值拷贝到这个字段中。 ":path”:包含了目标URI的路径和查询部分(“path-absolute”,一个可选的 ’?’ 字符且跟着一个”query”),参考[URI]的3.3节和3.4节。星号格式的请求中,":path”伪头部字段包含了’*’。 -“http”或 “https” URI中,这个伪头部字段必须不能(MUST NOT)为空; +“http”或 “https” URI中,这个伪头部字段不得(MUST NOT)为空; 不包含路径模块的“http” 或 “https” URI必须包含一个 ‘/’ 。但是有一个例外:”http”或”https”URI的OPTIONS请求不包含路径模块。 这些必须(MUST)包含带 ’*‘ 符号的”:path”伪头部字段(参考[SEMANTICS]第7.1节)。 所有HTTP/3请求的”:method”,”:scheme", 以及”:path”伪头部字段必须(MUST)只包含一个值,除非是CONNECT请求;(参考4.2节) -如果":scheme”伪头部字段标识了一个有强制权限模块的方案(包括”http”和”https”), -请求必须(MUST)包含":authority”伪头部字段或”Host”头部字段,且必须不能为空。 +如果":scheme”伪头部字段标识了一个有强制权限模块的模式(包括”http”和”https”), +请求必须(MUST)包含":authority”伪头部字段或”Host”头部字段,且不得为空。 如果同时出现了这两个字段,它们必须(MUST)包含相同的值。 -如果方案没有强制权限模块,并且请求目标中也没有,那么请求必须不能(MUST NOT)包含”.authority”伪头部字段或“Host”头部字段。 +如果模式没有强制权限模块,并且请求目标中也没有,那么请求不得(MUST NOT)包含”.authority”伪头部字段或“Host”头部字段。 删除了强制的伪头部字段,或者包含无效伪头部字段的HTTP请求是非法的。(4.1.3小节) HTTP/3没有定义像HTTP/1.1请求行一样携带版本标识的方式。 @@ -285,7 +280,7 @@ HTTP/3没有定义像HTTP/1.1请求行一样携带版本或者原因的方式。 [QPACK]描述了HPACK的一种变体,它使编码器能够对压缩可能造成的队头阻塞进行某种程度的控制, 这使得编码器能够平衡压缩效率和延迟。 HTTP/3使用QPACK来压缩头部和尾部字段,包括头部字段中的伪头部字段。 为了更高的压缩效率,可以(MAY)在压缩前,将“Cookie”字段分割到多个单独的字段行中,每个带一个或多个cookie对。 -如果解压缩字段区包含了多个cookie字段行,在将它们传到到上下文(比如HTTP/1.1连接,或者一个通用的HTTP服务应用)而非HTTP/2或者HTTP/3之前,必须(MUST)使用两个8位字节定位符0x3B和0x20(ASCII字符串“; ”,一个分号和一个空格)来将它们整合成单个8位字节字符串。 +如果解压缩段包含了多个cookie字段行,在将它们传到到上下文(比如HTTP/1.1连接,或者一个通用的HTTP服务应用)而非HTTP/2或者HTTP/3之前,必须(MUST)使用两个8位字节定位符0x3B和0x20(ASCII字符串“; ”,一个分号和一个空格)来将它们整合成单个8位字节字符串。 #### 4.1.1.3. Header Size Constraints @@ -306,17 +301,17 @@ HTTP/3没有定义像HTTP/1.1请求行一样携带版本或者原因的方式。 实现应该(SHOULD)通过 突然终止仍然打开的流的任何方向 来取消请求,这意味着重置流的发送部分和中止流的接收部分的读取(参见第2.4节[QUIC-TRANSPORT])。 -当服务端在没有执行任何应用处理的情况下取消请求时,该请求被视为 "已拒绝 "。 服务端应该以错误码H3_REQUEST_REJECTED中止其响应流。 +当服务端在没有执行任何应用处理的情况下取消请求时,该请求被视为 "已拒绝"。 服务端应该以错误码H3_REQUEST_REJECTED中止其响应流。 在这种情况下,"已处理 "意味着流中的一些数据被传递给了一些更高一层的软件,而这些软件可能因此采取了一些行动。 客户端可以把被服务端拒绝的请求当作根本没有发送过,从而可以在以后重试。 -对于服务端已经部分或全部处理的request,必须不能(MUST NOT)用H3_REQUEST_REJECTED错误码。 -当服务端在部分处理之后放弃响应,它应当(SHOULD)以H3_REQUEST_CANCELLED错误码退出响应流。 -when a server has requested closure of the request stream with this error code. +对于服务端已经部分或全部处理的request,不得(MUST NOT)以H3_REQUEST_REJECTED错误码响应。 +当服务端在部分处理之后放弃响应,其应当(SHOULD)以H3_REQUEST_CANCELLED错误码退出响应流。 + 客户端应该使用错误码H3_REQUEST_CANCELLED来取消请求。 收到这个错误码后,如果没有进行任何处理,服务端可以使用错误代码H3_REQUEST_REJECTED突然终止响应。 客户端不能使用H3_REQUEST_REJECTED错误码,除非服务端用这个错误码要求关闭请求流。 -如果一个stream在接收到完整的response之后被取消,客户端可以忽略取消,使用这个response。然而,如果一个stream在接收到部分response之后被取消,这个response不应当被使用。 +如果一个stream在接收到完整的response之后被取消,客户端可以忽略此取消消息而使用这个response。但是,如果一个stream在接收到部分response之后被取消,这个response不应当被使用。 只有像GET、PUT或DELETE这样的幂等操作才可以安全地重试;客户端不应该自动重试一个非幂等方法的请求,除非它有办法知道请求语义是独立于方法的幂等操作,或者有办法检测到原始请求从未被应用(详见[SEMANTICS]第9.2.2节)。 @@ -335,11 +330,11 @@ when a server has requested closure of the request stream with this error code. 包含载荷体的请求或响应可以包含Content-Length头部字段。如果请求的Content-length头部字段的值和组成实体的DATA帧的载荷长度之和不相等,那么这个请求也是非法的。 如果响应没有载荷(详见[SEMANTICS]第6.4.1小节),即使DATA帧中没有内容,它的content-length字段也可以为非0值. -处理HTTP请求或响应的中间媒介(例如任何不是通道的中间媒介)必须不能(MUST NOT)转发非法的请求或响应, +处理HTTP请求或响应的中间媒介(例如任何不是通道的中间媒介)不得(MUST NOT)转发非法的请求或响应, 一旦检测到非法请求或响应,必须认为这是一种 H3_MESSAGE_ERROR 类型的流错误。 服务端在收到非法请求之后,可以(MAY)在关闭或重置流之前,发送一个HTTP响应,来指示错误。 -客户端必须不能(MUST NOT)接受非法响应。 +客户端不得(MUST NOT)接受非法响应。 这些要求是为了抵御几种针对HTTP的常见攻击;必须极其严格遵守这些要求,否则可能导致实现收到这些攻击。 ## 4.2. The CONNECT Method @@ -356,7 +351,7 @@ CONNECT请求必须(MUST)按如下规则进行构建: 请求流在请求的结尾保持打开来传输数据。一个不遵守这些约束的CONNECT请求是非法的。 -支持CONNECT的代理建立的到服务端TCP连接([RFC0793]) 以":authority"伪头部字段区分。一旦成功建连,代理就会给客户端发送包含2xx系列状态码的HEADERS帧,如[SEMANTICS]第15.3节定义。 +支持CONNECT的代理建立的到服务端TCP连接([RFC0793]) 以":authority"伪头部段分。一旦成功建连,代理就会给客户端发送包含2xx系列状态码的HEADERS帧,如[SEMANTICS]第15.3节定义。 所有流上的DATA帧和TCP连接上收发的数据相对应。客户端发送的任何DATA帧的有效载荷被代理传送给TCP服务端;代理从TCP服务端收到的数据会被打包成DATA帧。注意不能保证尺寸和TPC分段数能够和HTTP DATA或QUIC STREAM帧的尺寸和数量匹配。 @@ -418,7 +413,7 @@ Push ID在一个或多个PUSH_PROMISE帧(第7.2.5节)中使用,这些帧 如果客户端实现了HTTP缓存,那么可以缓存那些可缓存的(参考[CACHING]第3章)推送的响应。在服务端收到推送响应的时候,认为它在原始服务器(例如,如果出现了”no-cache”缓存响应指令([CACHING]第5.2.2.3小节))上就被成功校验了。 -不能缓存的推送响应必须不能(MUST NOT)被任何HTTP缓存存储。它们可以(MAY)被单独提供给应用。 +不能缓存的推送响应不得(MUST NOT)被任何HTTP缓存存储。它们可以(MAY)被单独提供给应用。 # 5. Connection Closure @@ -442,7 +437,7 @@ GOAWAY帧包含了一个标识,向接收者表明了这个连接中已经或 GOAWAY帧中的信息可以让客户端和服务端在连接关闭前,在接受哪个请求或推送的问题上达成一致。 在发送GOAWAY帧的时候,终端应当(SHOULD)显式地取消所有标识大于等于指示值的请求或推送,从而来清除受影响流的传输状态。如果后续还有请求或者推送达到,终端应当(SHOULD)继续如此处理。 -从对端收到GOAWAY帧之后,终端必须不能(MUST NOT)在这个连接上发起新的请求、承诺新的推送。 +从对端收到GOAWAY帧之后,终端不得(MUST NOT)在这个连接上发起新的请求、承诺新的推送。 客户端可以(MAY)建立新的连接来发送额外的请求。 一些请求或推送可能已经在传输中: @@ -475,7 +470,6 @@ HTTP/3的实现可以在任何时候关闭QUIC connection。这会向对端发 在关闭connection之前,发送GOAWAY允许客户端重试一些requests。将GOAWAY帧和QUIC CONNECTION_CLOSE帧合并到同一个包能够增加帧被客户端接收的几率。 -If there are open streams that have not been explicitly closed, they are implicitly closed when the connection is closed; see Section 10.2 of [QUIC-TRANSPORT]. 如果有打开的流没有被显式关闭,那么当连接关闭时,它们会被隐式关闭;参见 [QUIC-TRANSPORT] 的 10.2 节。 ## 5.4 Transport Closure @@ -520,12 +514,12 @@ HTTP/3不使用服务器初始化的双向流,尽管扩展可以定义这些 过度限制这些流的数量和流控窗口的终端,会增加远端很早就达到限制并阻塞的风险。 实际上,实现应该考虑远端可能希望在它们允许使用的单向流上进行预留的流操作(6.2.3小节)。 -为了避免阻塞,客户端和服务端发送的传输参数都必须(MUST)允许对端创建至少一个用于HTTP控制流的单向流,以及强制扩展要求的单向流(基础HTTP/3协议和QPACK要求最少3个),同时应当(SHOULD)在每个流上提供最少1024个字节的流控信用。 +为了避免阻塞,客户端和服务端发送的传输参数都必须(MUST)允许对端创建至少一个用于HTTP控制流的单向流,以及强制扩展要求的单向流(基础HTTP/3协议和QPACK要求最少3个),同时应当(SHOULD)在每个流上提供最少1024个字节的流控credit。 -值得注意的是,如果对端在创建关键的单向流之前耗尽了初始的流控信用,不要求终端生成额外的流控窗口来创建更多的单向流。终端应当(SHOULD)和创建强制扩展要求的单向流(比如QPACK编、解码流)一样,先创建HTTP控制流,再在它们对端允许的前提下创建额外的流。 +值得注意的是,如果对端在创建关键的单向流之前耗尽了初始的流控credit,不要求终端生成额外的流控窗口来创建更多的单向流。终端应当(SHOULD)和创建强制扩展要求的单向流(比如QPACK编、解码流)一样,先创建HTTP控制流,再在它们对端允许的前提下创建额外的流。 如果接收者不支持流头部指示的流类型,流中剩余的数据会因为语义未知而不能被消费。 -收到未知流类型,可以(MAY)以一个H3_STREAM_CREATION_ERROR错误码终止流的读取。但是必须不能(MUST NOT)认为这个流是哪种类型的连接错误。 +收到未知流类型,可以(MAY)以一个H3_STREAM_CREATION_ERROR错误码终止流的读取。但是不得(MUST NOT)认为这个流是哪种类型的连接错误。 实现可以(MAY)在知道对端是否支持该type前发送stream types。但是能修改已存在协议组件(QPACK或其他扩展)状态和语意的type,不能(MUST NOT)在知道对端支持前发送。 @@ -535,10 +529,9 @@ HTTP/3不使用服务器初始化的双向流,尽管扩展可以定义这些 控制流的流类型是0x00。这条流上的数据只发送HTTP/3帧。(7.2节) -两端必须在连接开始时发起一个crontrol stream,并且发送SETTINGS frame作为这个stream的第一帧。如果这个control stream的第一帧不是SETTINGS frame,必须被视作连接错误H3_MISSING_SETTINGS。两端各自只允许发送一个crontrol stream;接收到第二个control stream必须被视作连接错误H3_STREAM_CREATE_ERROR。发送者必须不能(MUST NOT)关闭控制流,接收者必须不能(MUST NOT)请求发送者关闭控制流。如果crontrol stream被关闭,必须被视为连接错误H3_CLOSED_CRITICAL_STREAM。连接错误在第8章中介绍。 +两端必须在连接开始时发起一个crontrol stream,并且发送SETTINGS frame作为这个stream的第一帧。如果这个control stream的第一帧不是SETTINGS frame,必须被视作连接错误H3_MISSING_SETTINGS。两端各自只允许发送一个crontrol stream;接收到第二个control stream必须被视作连接错误H3_STREAM_CREATE_ERROR。发送者不得(MUST NOT)关闭控制流,接收者不得(MUST NOT)请求发送者关闭控制流。如果crontrol stream被关闭,必须被视为连接错误H3_CLOSED_CRITICAL_STREAM。连接错误在第8章中介绍。 -Because the contents of the control stream are used to manage the behavior of other streams, endpoints SHOULD provide enough flow control credit to keep the peer's control stream from becoming blocked. -因为控制流的内容被用来管理其他流的行为,所以终端应该提供足够的流控信用,以防止对端的控制流被阻塞。 +因为控制流的内容被用来管理其他流的行为,所以终端应该提供足够的流控credit,以防止对端的控制流被阻塞。 使用一对单向流而不是一个双向流,是为了两端都能尽快发送自己的数据。根据QUIC连接上0-RTT的使能情况,客户端或服务端可以先发送流数据。 @@ -546,7 +539,7 @@ Because the contents of the control stream are used to manage the behavior of ot 服务端推送是一种在HTTP/2引入的可选的特性,允许服务端在尚未请求之前先发起响应,详情参考4.4节。 -推送流的流类型为0x01,紧接着为Push ID,编码为可变长数字。剩下的数据由HTTP/3帧组成(见7.2),通过0个或多个临时HTTP响应,后跟一个最终的HTTP响应,从而兑现了承诺。Server Push和Push ID详见4.1节。 +推送流的流类型为0x01,紧接着为Push ID,编码为可变长数字。剩下的数据由HTTP/3帧组成(见7.2),通过0个或多个中间HTTP响应,后跟一个最终HTTP响应,从而兑现了承诺。Server Push和Push ID详见4.1节。 只有服务端才能push,收到客户端发起的推送流,服务端应当视为H3_STREAM_CREATION_ERROR类型的连接错误。 @@ -632,7 +625,6 @@ Length:表示帧载荷长度的变长整数, Frame Payload:载荷,语意由Type字段决定 每个帧必须恰好包含以上定义的字段, payload实际长度和length不符合的情况,必须被当成H3_FRAME_ERROR类型的连接错误(见第8章)。 -In particular, redundant length encodings MUST be verified to be self-consistent; see Section 10.8. 特别是,必须验证冗余长度编码是自洽的,参见10.8节。 文明终止一个流,如果流的最后一个帧被截断,必须(MUST)把这种情况当成是H3_FRAME_ERROR类型的连接错误(第8章)。可以在粗暴终止的流 @@ -705,13 +697,13 @@ CANCEL_PUSH frame携带变长的整数Push ID,表示将被取消的server push SETTINGS frame (type=0x4) 用来传输配置参数,从而来影响终端通信的方式,比如终端行为的偏好与约束。一个SETTINGS参数也可以被称为“setting”;每个配置参数的标识和值可以被称为“setting identifier”和“setting value”。 -SETTINGS帧往往用于连接,而不是单个流。SETTINGS帧必须(MUST)以每个对等端的每个控制流的第一个帧发送,且必须不能(MUST NOT)后续再发。如果终端在控制流上收到第二个SETTINGS帧,必须(MUST)以 H3_FRAME_UNEXPECTED类型的连接错误进行响应。 +SETTINGS帧往往用于连接,而不是单个流。SETTINGS帧必须(MUST)以每个对等端的每个控制流的第一个帧发送,且不得(MUST NOT)后续再发。如果终端在控制流上收到第二个SETTINGS帧,必须(MUST)以 H3_FRAME_UNEXPECTED类型的连接错误进行响应。 -除了控制流,其他类型的流必须不能(MUST NOT)发送SETTINGS帧。如果终端在其他流类型上收到SETTINGS帧,必须(MUST)以H3_FRAME_UNEXPECTED类型的连接错误进行响应。 +除了控制流,其他类型的流不得(MUST NOT)发送SETTINGS帧。如果终端在其他流类型上收到SETTINGS帧,必须(MUST)以H3_FRAME_UNEXPECTED类型的连接错误进行响应。 SETTINGS参数不是协商生成的;参数表明发送方的特性,被接收方使用。但是SETTINGS可以实现隐式协商,每个对端使用SETTINGS宣告自己支持的集合,双方根据两个集合选择哪些被使用。SETTINGS不提供机制表明选择何时生效。 -同一个参数两端可能会宣告不同的值。比如,客户端可能希望回包字段区足够大,而服务端对请求大小更谨慎。 +同一个参数两端可能会宣告不同的值。比如,客户端可能希望回包段足够大,而服务端对请求大小更谨慎。 setting identifier不能出现超过一次。否则被视作H3_SETTINGS_ERROR类型的连接错误。 @@ -743,16 +735,16 @@ SETTINGS_MAX_FIELD_SECTION_SIZE (0x6): 默认值为无穷大,见4.1.1.3节。 "0x1f * N + 0x21"格式的Setting identifiers被预留用在测试忽略不认识的identifiers。这种setting没有定义的意义,终端应当(SHOULD)至少包含一个这种setting在SETTINGS frame中,但是接收方必须忽略这种setting。 因为该setting没有任何定义的含义,所以它的值可以为任何值。 -HTTP/2中同样预留了(参考11.2.2小节)设置标识,但和HTTP/3的不冲突。必须不能(MUST NOT)发送这些设置,一旦收到,则必须(MUST)当成H3_SETTINGS_ERROR类型的连接错误。 +HTTP/2中同样预留了(参考11.2.2小节)设置标识,但和HTTP/3的不冲突。不得(MUST NOT)发送这些设置,一旦收到,则必须(MUST)当成H3_SETTINGS_ERROR类型的连接错误。 可以通过对HTTP 3的扩展来定义其他设置项。更多详细信息请参见第9章。 #### 7.2.4.2 Initialization -HTTP实现必须不能(MUST NOT)发送对端不能理解的帧或者请求。 +HTTP实现不得(MUST NOT)发送对端不能理解的帧或者请求。 一开始所有配置都是初始值。由于携带配置的包有可能丢失或延迟,每个终端应当(SHOULD)在收到对端SETTINGS帧使用初始值。当SETTINGS帧到达,需要将配置的值改为SETTINGS中的新值。 -这避免了在发送消息前等待SETTINGS帧。终端必须不能(MUST NOT)在发送SETTINGS帧之前要求从对端收到数据;一旦传输层就绪,必须(MUST)尽快发送配置。 +这避免了在发送消息前等待SETTINGS帧。终端不得(MUST NOT)在发送SETTINGS帧之前要求从对端收到数据;一旦传输层就绪,必须(MUST)尽快发送配置。 而服务端上,每个客户端的配置的初始值都是默认值。 @@ -764,15 +756,15 @@ HTTP实现必须不能(MUST NOT)发送对端不能理解的帧或者请求 当使用了0-RTT QUIC连接,每个服务端配置的初始值是之前会话中使用的值。 客户端应当(SHOULD)将服务端提供的配置和连接的恢复信息存在一起, 但是可以(MAY)选择某些情况下不存储配置(比如,在SETTINGS帧之前收到会话票证)。 -当使用0-RTT方案时,客户端必须(MUST)使用存储的配置,如果没有存储,就使用默认值。 +当使用0-RTT模式时,客户端必须(MUST)使用存储的配置,如果没有存储,就使用默认值。 一旦服务端提供了新的配置,客户端必须(MUST)使用新的值。 服务端可以记住它通告的配置,或者将其加上完整性保护,存在票证中,并在收到0-RTT数据的时候恢复这个信息。服务端将HTTP/3配置值用于决定是否接受0-RTT数据。 -如果服务端不能决定客户端记住的配置是否跟它当前配置兼容,它必须不能(MUST NOT)接受0-RTT数据。 +如果服务端不能决定客户端记住的配置是否跟它当前配置兼容,它不得(MUST NOT)接受0-RTT数据。 如果客户端遵循的配置不违反服务端当前配置,记住的配置就是兼容的。 服务端可以(MAY)接受0-RTT,后续在它的SETTINGS帧中提供不同配置。 -如果服务端接受了0-RTT数据,它的SETTINGS帧必须不能(MUST NOT)减少任何限制或者改变任何值,否则客户端的0-RTT可能违反这个配置。 +如果服务端接受了0-RTT数据,它的SETTINGS帧不得(MUST NOT)减少任何限制或者改变任何值,否则客户端的0-RTT可能违反这个配置。 服务端必须(MUST)包括所有与默认值不同的配置。 如果服务端接受0-RTT但接着又发了和之前阐述的配置不兼容的配置,这就是一种H3_SETTINGS_ERROR类型的错误。 如果服务端接受了0-RTT,但是接着发了一个SETTINGS帧,并且这个帧删除了一个之前非默认值、且客户端理解的配置值, @@ -780,7 +772,7 @@ HTTP实现必须不能(MUST NOT)发送对端不能理解的帧或者请求 ### 7.2.5 PUSH_PROMISE -PUSH_PROMISE frame (type=0x05)用于服务端向客户端发送请求头部字段区,如HTTP/2中一样。 +PUSH_PROMISE frame (type=0x05)用于服务端向客户端发送请求头部段,如HTTP/2中一样。 ``` PUSH_PROMISE Frame { @@ -799,13 +791,13 @@ Encoded Field Section: QPACK编码的request header,详见[QPACK] 服务端不能使用超过客户端在MAX_PUSH_ID frame声明的Push ID, 客户端接收到大于自己声明的Push ID时必须视作H3_ID_ERROR类型的连接错误。 -服务端可以(MAY)在多个PUSH_PROMISE帧中使用同一个Push ID。如果这么做的话解压缩后的请求头部集合中的字段必须(MUST)相同且顺序也一致,同时每个字段中的名称和值也必须严格匹配。客户端应当(SHOULD)对比请求头部区中承诺的资源进行多次对比。如果客户端收到了一个已经承诺过的但不匹配的Push ID,必须(MUST)响应H3_GENERAL_PROTOCOL_ERROR类型的连接错误。如果解压后的字段区严格匹配,客户端应当(SHOULD)推送的内容与每个流关联起来。 +服务端可以(MAY)在多个PUSH_PROMISE帧中使用同一个Push ID。如果这么做的话解压缩后的请求头部集合中的字段必须(MUST)相同且顺序也一致,同时每个字段中的名称和值也必须严格匹配。客户端应当(SHOULD)对比请求头部区中承诺的资源进行多次对比。如果客户端收到了一个已经承诺过的但不匹配的Push ID,必须(MUST)响应H3_GENERAL_PROTOCOL_ERROR类型的连接错误。如果解压后的段严格匹配,客户端应当(SHOULD)推送的内容与每个流关联起来。 允许对同一个Push ID重复索引主要是为了减少并发请求引起的冗余。服务端应当(SHOULD)避免长时间复用一个Push ID。客户端可能会消费服务端推送响应,但不会保存以作后用。客户端如果发现一个PUSH_PROMISE帧中的Push ID是它们已经消费过的,则需要强制忽略这个PUSH_PROMISE。 如果在控制流上收到PUSH_PROMISE,客户端必须(MUST)以 H3_FRAME_UNEXPECTED类型的连接错误进行响应。 -客户端必须不能(MUST NOT)发送PUSH_PROMISE帧。如果服务端收到PUSH_PROMISE帧,必须(MUST)将这种情况当成H3_FRAME_UNEXPECTED类型的连接错误。 +客户端不得(MUST NOT)发送PUSH_PROMISE帧。如果服务端收到PUSH_PROMISE帧,必须(MUST)将这种情况当成H3_FRAME_UNEXPECTED类型的连接错误。 服务端推送机制详见4.4章。 @@ -856,11 +848,11 @@ MAX_PUSH_ID 帧携带一个可变长度的整数,该整数标识了服务器 ### 7.2.8 Reserved Frame Types -保留了"0x1f * N + 0x21"类型的帧来满足忽略未知类型的需求。这些类型没有语意,可以在允许发送帧的任何流上发送, 这使它们可以用于应用程序层填充。对端收到这些帧时必须不能(MUST NOT)认为有任何意义。 +保留了"0x1f * N + 0x21"类型的帧来满足忽略未知类型的需求。这些类型没有语意,可以在允许发送帧的任何流上发送, 这使它们可以用于应用程序层填充。对端收到这些帧时不得(MUST NOT)认为有任何意义。 载荷和长度由实现方式选择。 -有一些帧类型,在HTTP/2中使用,但HTTP/3中没有,也被预留了(11.2.1小节)。必须不能(MUST NOT)发送这些帧类型,一旦收到必须(MUST)当成是H3_FRAME_UNEXPECTED类型的连接错误。 +有一些帧类型,在HTTP/2中使用,但HTTP/3中没有,也被预留了(11.2.1小节)。不得(MUST NOT)发送这些帧类型,一旦收到必须(MUST)当成是H3_FRAME_UNEXPECTED类型的连接错误。 # 8. Error Handling @@ -985,7 +977,7 @@ PUSH_PROMISE帧的数量也受到了类似方法的限制。接收服务端推 设置多个未定义的SETTINGS参数、未知帧类型、未知流类型实现,可以导致这个问题。 而且有些使用方法还是完全合法的,比如可选扩展、抵御流量分析的填充。 -字段区的压缩同样会浪费处理资源;(潜在的滥用情况详见[QPACK]第7章) +段的压缩同样会浪费处理资源;(潜在的滥用情况详见[QPACK]第7章) 所有这些特性,比如服务端推送、未知协议元素、字段压缩,都有合法的使用场景。 只有在不必要或者超量使用的时候,这些特征才会额外带来负担。 @@ -995,16 +987,16 @@ PUSH_PROMISE帧的数量也受到了类似方法的限制。接收服务端推 ### 10.5.1. Limits on Field Section Size -一个大字段区(第4.1节)会导致实现消耗大量状态。对路由敏感的头部字段可以出现在头部字段区域的尾部,这阻止了头部字段区流向它最终的目标。 -这种排序以及其他原因,比如保证缓存的正确性,意味着终端可能需要缓存整个头部字段区。 -因为没有针对字段区域的硬性限制,一些终端可能被迫为头部字段消耗大量内存。 +一个大段(第4.1节)会导致实现消耗大量状态。对路由敏感的头部字段可以出现在头部段域的尾部,这阻止了头部段流向它最终的目标。 +这种排序以及其他原因,比如保证缓存的正确性,意味着终端可能需要缓存整个头部段。 +因为没有针对段域的硬性限制,一些终端可能被迫为头部字段消耗大量内存。 -终端可以使用SETTINGS_MAX_FIELD_SECTION_SIZE(第4.1.1.3小节)配置来建议对端限制字段区域的大小。 -这个配置只是建议性的,因此终端可以(MAY)发送超过这个限制的字段区,但是面临着请求或响应被当成异常的风险。 +终端可以使用SETTINGS_MAX_FIELD_SECTION_SIZE(第4.1.1.3小节)配置来建议对端限制段域的大小。 +这个配置只是建议性的,因此终端可以(MAY)发送超过这个限制的段,但是面临着请求或响应被当成异常的风险。 这个配置适用于一个连接,因此任何请求或响应都可能碰到一个具有更低未知限制的路由节点。 一个中间媒介可以通过传递不同对端展示的值来避免这个问题,但是他们没有这么做的义务。 -如果一个服务端收到的字段区比它愿意处理的尺寸更大,可以发送一个HTTP 431(请求头部字段过大)状态码[RFC6585]。 +如果一个服务端收到的段比它愿意处理的尺寸更大,可以发送一个HTTP 431(请求头部字段过大)状态码[RFC6585]。 客户端可以丢弃它无法处理的响应。 ### 10.5.2. CONNECT Issues @@ -1022,11 +1014,11 @@ HTTP/3可以进行字段的压缩(4.1.1小节); 有一些攻击明确针对了暴露web特性的压缩(比如[BREACH]攻击)。 攻击者诱发了多个包含不同文本的请求,再观察每个请求的秘文长度,如果猜对了密钥,那么秘文长度就更短。 -在一个安全通道上进行通信的实现必须不能(MUST NOT)压缩那些既包含了机密数据又包含了受攻击者控制数据的内容, +在一个安全通道上进行通信的实现不得(MUST NOT)压缩那些既包含了机密数据又包含了受攻击者控制数据的内容, 除非为每种数据源提供了隔离的压缩上下文(compression contexts)。 -如果不能决定数据源的可靠性,必须不能(MUST NOT)对数据进行压缩。 +如果不能决定数据源的可靠性,不得(MUST NOT)对数据进行压缩。 -[QPACK]描述了更多关于字段区压缩的注意事项。 +[QPACK]描述了更多关于段压缩的注意事项。 ## 10.7. Padding and Traffic Analysis @@ -1045,7 +1037,7 @@ HTTP/2在另外帧中采用了PADDING帧和Padding字段来抵御流量分析, 使用填充的保护效果可能没有想象中那样立竿见影。多余的填充甚至可能适得其反。 填充最多只能增大攻击者观察的数据量,从而让攻击者更加难以推断出长度信息。 -错误的填充方案很容易就会被破解。实际上,使用可预测分布的随机填充的保护强度很低; +错误的填充模式很容易就会被破解。实际上,使用可预测分布的随机填充的保护强度很低; 如果将载荷填充为固定大小,载荷大小跨越固定尺寸边界的时候,同样泄露了信息。 如果攻击者可以控制明文的话,它就可能做到这个。 diff --git a/docs/translation/rfc9000-transport-zh.md b/docs/translation/rfc9000-transport-zh.md index 125659f1e..46b86217c 100644 --- a/docs/translation/rfc9000-transport-zh.md +++ b/docs/translation/rfc9000-transport-zh.md @@ -1,3540 +1,3540 @@ -# 摘要 -本文定义了QUIC传输协议的核心特性。QUIC为应用提供经过流控的流(Stream),可用于结构化通信、低延迟连接建立和网络路径迁移。QUIC还包含在多种部署情况下确保机密性、完整性和可用性的安全措施。随附文件描述了用于密钥协商的TLS集成、丢包检测和一个拥塞控制算法示例。 - -# 1. 概述(Overview) -QUIC是一种安全的通用传输层协议。本文定义了QUIC的版本1,它符合在[QUIC-INVARIANTS]中定义的QUIC的版本无关特性。 - -QUIC是一种面向连接的协议,可在客户端和服务端之间建立有状态的交互。 - -QUIC握手结合了加密和传输参数的协商。QUIC集成了TLS握手[TLS13],使用定制的框架来保护数据包。TLS和QUIC的集成在[QUIC-TLS]中有更详细的说明。握手的设计使得可以尽快交换应用数据:如果之前有过握手过程和保存配置,那么客户端可以启用立即发送数据(0-RTT)的选项。 - -在QUIC协议中,终端(Endpoint)通过交换QUIC包(Packet)通信。大多数数据包中包含帧(Frame),帧携带控制信息和应用数据。QUIC验证每个数据包的完整性,并尽可能对所有数据包进行加密。QUIC协议承载在[UDP]协议之上,以更方便其在现有系统和网络中部署。 - -应用层协议建立QUIC连接,在其上通过流(Stream)来交换信息,流是有序的字节序列。QUIC可以创建两种类型的流:双向流和单向流,双向流允许两端互相收发数据,单向流只允许单个终端发送数据。基于credit的方案用于限制流的创建数并限制可以发送的数据量。 - -QUIC提供必要的反馈,以实现可靠传输和拥塞控制。在[QUIC-RECOVERY]第6章中描述了一种用于检测丢包和恢复数据的算法。QUIC依靠拥塞控制来避免网络拥塞。在[QUIC-RECOVERY]第7章有一个示例性的拥塞控制算法。 - -QUIC连接不严格绑定到某条网络路径。连接标识符(CID)的引入允许连接迁移到新的网络路径。不过在当前的QUIC版本中只有客户端才能迁移。这种设计还允许在网络拓扑或地址映射发生变更后重新建链,例如可NAT重新绑定导致的接续。 - -QUIC为终止连接提供多个选项,使得应用可以优雅关闭,终端可以协商超时时间,出现错误会立即导致连接关闭,并且无状态重置(Stateless Reset)机制可以在一个终端失去状态后终止连接。 - -## 1.1. 文档结构(Document Structure) - -本文描述了QUIC协议的核心,文档结构如下: - -* 流是QUIC提供的基本服务抽象: - - 第2章 流相关的核心概念 - - 第3章 流状态参考模型 - - 第4章 流控操作 - -* 连接是QUIC终端通信的上下文 - - 第5章 连接相关的核心概念 - - 第6章 版本协商 - - 第7章 建链流程 - - 第8章 地址校验和拒绝服务攻击的规避措施 - - 第9章 连接迁移 - - 第10章 连接关闭的选项及流程 - - 第11章 流和连接的错误处理指引 - -* 包和帧是QUIC通信的基本单位 - - 第12章 包和帧相关的概念 - - 第13章 数据传输、重传和确认模型 - - 第14章 承载QUIC包的UDP报文大小规则 - -* 最后是QUIC协议元素的编码细节 - - 第15章 版本号约定 - - 第16章 变长整数编码 - - 第17章 包格式详解 - - 第18章 传输参数 - - 第19章 帧格式详解 - - 第20章 错误码 - -随附文档描述了QUIC的丢包检测和拥塞控制(参见[QUIC-RECOVERY])、TLS的使用和其他加密机制(参见[QUIC-TLS])。 - -本文描述了QUIC版本1,其满足[QUIC-INVARIANTS]描述的与版本无关的QUIC协议特性。 - -需要引用QUIC版本1,参考这篇文档,需要引用QUIC版本无关特性的受限集合,请参考[QUIC-INVARIANTS]。 - -## 1.2. 术语与定义(Terms and Definitions) -本文中的关键词"MUST","MUST NOT","REQUIRED","SHALL","SHALL NOT","SHOULD","SHOULD NOT","RECOMMENDED","NOT RECOMMENDED","MAY",以及"OPTIONAL",当且仅当他们全部以大写字母出现的时候,需要按BCP 14[RFC2119][RFC8174]所述的方式理解。 - -本文中常用术语如下所示: - -QUIC(QUIC):本文描述的传输协议,QUIC是一个名字,不是缩略语。 - -终端(Endpoint)参与QUIC连接的实体,可以生成、接收和处理QUIC包。QUIC只有两种类型的终端:客户端和服务端。 - -客户端(Client):发起连接的QUIC终端。 - -服务端(Server):接收连接的QUIC终端。 - -QUIC包(QUIC packet):可被一个UDP报文封装的完整的QUIC处理单元,一个UDP报文可以包含一个或多个QUIC包。 - -ACK触发包(Ack-eliciting packet):包含除ACK、PADDING和CONNECTION_CLOSE以外的帧的QUIC包,触发接收端回应一个ACK确认,参见第13.2.1小节。 - -帧(Frame):QUIC定义的结构化的协议信息单元。QUIC有多种帧类型,不同类型的帧携带不同的信息。一个QUIC包中可包含一个或多个帧。 - -地址(Address):在不加限制的情况下,是由IP版本、IP地址和UDP端口号组成的元组,表示网络路径的一端。 - -连接ID(Connection ID,CID):用于标识QUIC连接终端的标识符。每个终端为其对端选择一个或多个CID,并包含在发送给对端的数据包中。该值在对端是可见的。 - -流(Stream):在QUIC连接中传输有序字节的单向或双向通道。一个QUIC连接可以同时承载多个流。 - -应用(Application):使用QUIC收发数据的实体。 - -本文使用术语"QUIC包", "UDP报文"和"IP包"表示对应的协议单元,也就是,一个或多个QUIC包可以被封装在一个UDP报文内,一个UDP报文被封装在一个IP数据包中。 - -## 1.3. 符号约定(Notational Conventions) -本文中的数据包和数据帧使用自定义的格式说明,引入这种格式是为了简要描述协议元素,而不是正式定义。 - -复合字段:首先给出命名,后接由一对大括号括起的字段列表,列表中的每个字段都用逗号分隔。 - -单个字段:包括长度信息,固定、可选和重复指示,遵循如下符号约定,其中长度单位为位(bit): - -x (A):表示x的长度为A位 - -x (i):表示x是变长整数值,其长度编码参见第16章。 - -x (A..B):表示x是一个长度介于A到B位之间的值,A省略的话,表示从0位开始,B省略的话表示无上限,一般按字节取整 - -x (L) = C:表示x是一个固定值C,x的长度为L,L可以使用上述3种长度形式之一。 - -x (L) = C..D:表示x的取值范围为从C到D闭区间的值,长度为L如上所示。 - -[x (L)]:表示x为可选,且长度为L。 - -x (L) ...:表示x以长度L重复0到多次 - -本文使用网络字节序,即大端字节序。字段从每个字节最高有效位开始,按照惯例,单个字段通过名字来引用复合字段。 - -``` - Example Structure { - One-bit Field (1), - 7-bit Field with Fixed Value (7) = 61, - Field with Variable-Length Integer (i), - Arbitrary-Length Field (..), - Variable-Length Field (8..24), - Field With Minimum Length (16..), - Field With Maximum Length (..128), - [Optional Field (64)], - Repeated Field (8) ..., - } -``` -图1: Example Format - -当在上下文中提到某个1比特长度的字段,可以通过在其所在字节中将该字段所在位设为1,其余置0,来指示该字段的位置,例如,0x80可以表示该字段在该字节的最高有效位上,如图1的One-bit字段。 - -# 2. 流(Streams) -QUIC中的流为应用提供了一个轻量级的、有序的字节流抽象。流可以是单向或双向的。 - -可以通过直接发送数据来创建流。与流管理相关的其他流程,如关闭、取消和流控管理,都旨在带来最小的开销。例如,单个STREAM帧(第19.8节)可以打开流、携带数据并关闭流。流也可以是持久的,可以持续整个连接的生命周期。 - -流可以由任一终端创建,可以与其他流并行交错发送数据,并且可以取消。QUIC不保证不同流上的字节是保序的。 - -QUIC允许任意数量的流并行发送数据,并允许在一个流上发送任意数量的数据,但需要接受流控约束和流限制,参见第4章。 - -## 2.1. 流类型和标识(Stream Types and Identifiers) -流可以是单向和双向的,单向流只从流的发起方发送数据到对端。双向流可以同时收发数据。 - -一个连接中的流由一个62位的整数(0到2^62^-1)标识,称为流ID,由第16章中定义的变长整数编码。在一个连接中,任意流的ID都是不同的,QUIC端禁止(**MUST NOT**)重用流ID。 - -流ID的最低有效位(0x01)标识流的发起者。客户端启动的流的流ID为偶数(位设置为0),服务端启动的流的流ID为奇数(位设置为1)。 - -流ID的第二个最低有效位(0x02)区分双向流(位设置为0)和单向流(位设置为1),因此,来自流ID的两个最低有效位将流识别为四种类型之一,如表1中总结的: - -|Bits| Stream Type | -|:---|:---| -|0x0 | Client-Initiated, Bidirectional | -|0x1 | Server-Initiated, Bidirectional | -|0x2 | Client-Initiated, Unidirectional | -|0x3 | Server-Initiated, Unidirectional | - -表1: Stream ID Types - -每个类型流的ID取值空间从最小值开始(0x00到0x03,相应的),后续的流ID在此基础上线性递增。乱序使用流ID会导致该类型下所有较低编号的流都被打开。 - -## 2.2. 收发数据(Sending and Receiving Data) -STREAM帧(参见第19.8节)封装应用发送的数据,QUIC端在STREAM帧中使用流ID和Offset字段按序放置数据。 - -QUIC端必须(**MUST**)将流数据按序投递给应用,投递有序字节流要求QUIC端缓冲任何无序接收的数据,直至达到通告的流控限制。 - -QUIC协议中没有特别指出允许乱序投递流数据。然而,在实现中可以(**MAY**)选择提供向应用投递无序数据的能力。 - -QUIC端可从某个流中多次接收相同Offset的数据,重复的数据可以(**MAY**)被丢弃,但这些相同Offset的数据不得(**MUST NOT**)改变,如果有变更则视为PROTOCOL_VIOLATION类型的连接错误。 - -流是有序的字节流抽象,QUIC看不到除流外的其他结构。在数据传输、丢包重传或投递给接收端的应用时,QUIC不会保存每个STREAM帧的边界。 - -终端不得(**MUST NOT**)在任何流上发送数据,除非确保数据在其对端设置的流控窗口内,流量控制在第4章中有详细描述。 - -## 2.3. 流的优先级(Stream Prioritization) -如果分配给流的资源的优先级正确,则流复用会对应用性能产生重大改进。 - -QUIC不提供彼此交换优先级信息的机制,而是依赖于从应用接收优先级信息。 - -QUIC的实现应该(**SHOULD**)提供给应用可以指示流的相对优先级的方法,QUIC依据应用提供的信息来确定如何将资源分配给活动流。 - -## 2.4. 基于流的操作(Operations on Streams) -本文没有定义QUIC的API,但定义了一组应用层协议可以依赖的流上的函数。应用层协议可以假设QUIC实现提供了一个接口,其中包括本节中描述的操作。设计用于特定应用层协议的实现可能仅提供该协议需要的那些操作。 - -在流的发送侧,应用层协议可以: - -* 写数据,确保流上有为待写数据保留基于流控的发送许可(第4.1节); -* 结束流(优雅关闭),触发设置了FIN位的STREAM帧(第19.8节); -* 重置流(突然终止),如果流不在终止态,则触发一个RESET_STREAM帧(第19.4节)。 - -在流的接收侧,应用层协议可以: - -* 读数据; -* 终止读取流,请求关闭,可能触发STOP_SENDING帧(第19.5节)。 - -应用层协议还可以请求获知流上的状态变化,包括对端何时打开或重置流、对端何时中止读取流、新数据何时可用以及基于流控,何时可以或不可以写入数据。 - -# 3. 流状态(Stream States) -本节以描述流的接收侧与发送侧来说明流的特性,并介绍了两种状态机:一种是流的发送侧对应的状态机(第3.1节),另一种是流的接收侧的状态机(第3.2节)。 - -单向流依据流类型和端角色,决定用发送状态机还是接收状态机,双向流的两侧,两种状态机都采用。在大多数情况下,无论是单向流还是双向流,这些状态机的使用方式都是相同的。对双向流来说,打开流的条件有一点复杂,因为无论是由发送侧还是接收侧的打开,都会使流双向开启。 - -本节中显示的状态机能提供大量信息。本文使用流状态机来描述这些规则,即在什么场景下,发送什么类型的帧,期待怎样的回应,在什么场景能接收什么类型的帧。尽管状态机旨在方便实现QUIC,但并不限制实现。一个实现可以定义不同的状态机,只要它的行为与其他实现这些状态的实现一致即可。 - -> 注意:在某些场景下,一个事件或动作可能引起状态迁移多次,例如,发送设置FIN位的STREAM帧的动作可引起流的发送侧的两次状态迁移:从Ready态跃迁到Send态,再从Send态迁移到Data Send态。 - -## 3.1. 发送方流状态(Sending Stream States) -下图是流的发送侧的状态机 - -``` - o - | Create Stream (Sending) - | Peer Creates Bidirectional Stream - v - +-------+ - | Ready | Send RESET_STREAM - | |-----------------------. - +-------+ | - | | - | Send STREAM / | - | STREAM_DATA_BLOCKED | - v | - +-------+ | - | Send | Send RESET_STREAM | - | |---------------------->| - +-------+ | - | | - | Send STREAM + FIN | - v v - +-------+ +-------+ - | Data | Send RESET_STREAM | Reset | - | Sent |------------------>| Sent | - +-------+ +-------+ - | | - | Recv All ACKs | Recv ACK - v v - +-------+ +-------+ - | Data | | Reset | - | Recvd | | Recvd | - +-------+ +-------+ - -``` -图2: States for Sending Parts of Streams - -由终端(类型0、2是客户端,1、3是服务端)启动的流的发送侧是由应用创建。Ready态表示新创建的流可以从应用接收数据,流数据在这种状态下可以被缓存以备发送。 - -第一个STREAM或STREAM_DATA_BLOCKED帧的发送使得流的发送侧进入Send态,一个实现可以选择延迟为流分配流ID,直到第一个STREAM发送并进入Send态,这样可以采取更好的优先级策略。 - -由对端(类型0是服务端,1是客户端)启动的双向流,在本端接收侧创建的同时,同步创建发送侧,进入Ready态。 - -在Send态,本端使用STREAM帧传输(必要时重传)流数据,并遵守由对端设置的流控限制,接收和处理MAX_STREAM_DATA帧。如果因为流控限制(第4.1节)暂时不能发送,则会生成并发送STREAM_DATA_BLOCKED帧。 - -在应用指示已发送完所有流数据,并已发送设置了FIN位的STREAM帧后,流的发送侧进入Data Sent态。在此状态下,本端仅在必要时重传流数据(对端ACK显示有丢包),不需要继续检查流控限制或发送STREAM_DATA_BLOCKED帧,此时可能会收到对端的MAX_STREAM_DATA帧,本端可以安全地忽略它们,对端收齐数据之后将不再重复发送MAX_STREAM_DATA。 - -一旦所有流数据都被成功确认,流的发送侧进入Data Recvd状态,这是一个终止状态。 - -在Ready、Send或Data Sent任一状态,应用可以发信号表示它希望放弃流数据传输,或者,本端也可能会收到对端的STOP_SENDING帧。在上述情况下,本端都要发送RESET_STREAM帧,之后进入Reset Sent状态。 - -终端可能(**MAY**)在流上发送的第一帧就是RESET_STREAM,这使得流的发送侧打开后立即切换到Reset Sent状态。 - -一旦包含RESET_STREAM的数据包被确认,流的发送侧立即进入Reset Recvd状态,这也是一个终止状态。 - -## 3.2. 接收方流状态(Receiving Stream States) -图3是从对端接收数据的流的接收侧的状态机,流的接收侧的状态仅反映流的发送侧的一些状态,不会跟踪发送侧上无法观察到的状态,例如Ready状态。相应的,流的接收侧会跟踪向应用投递数据的情况,其中一些状态发送方不可见。 - -``` - o - | Recv STREAM / STREAM_DATA_BLOCKED / RESET_STREAM - | Create Bidirectional Stream (Sending) - | Recv MAX_STREAM_DATA / STOP_SENDING (Bidirectional) - | Create Higher-Numbered Stream - v - +-------+ - | Recv | Recv RESET_STREAM - | |-----------------------. - +-------+ | - | | - | Recv STREAM + FIN | - v | - +-------+ | - | Size | Recv RESET_STREAM | - | Known |---------------------->| - +-------+ | - | | - | Recv All Data | - v v - +-------+ Recv RESET_STREAM +-------+ - | Data |--- (optional) --->| Reset | - | Recvd | Recv All Data | Recvd | - +-------+<-- (optional) ----+-------+ - | | - | App Read All Data | App Read Reset - v v - +-------+ +-------+ - | Data | | Reset | - | Read | | Read | - +-------+ +-------+ - -``` -图3: States for Receiving Parts of Streams - -当端接收到某流的第一个STREAM、STREAM_DATA_BLOCKED或RESET_STREAM帧时,会创建该流的接收侧(客户端的类型1和3,或服务端的类型0和2)。对于由对端发起的双向流,端接收到某流的MAX_STREAM_DATA或STOP_SENDING帧也会创建该流的接收侧,流的接收侧的初始状态是Recv。 - -对于双向流,当流的发送侧(类型0为客户端,类型1为服务端)进入Ready状态时,接收侧进入Recv状态。 - -当接收到MAX_STREAM_DATA或STOP_SENDING帧时,端会打开双向流,因为收到未打开的流的MAX_STREAM_DATA帧表示对端已打开流并提供流控管理,接收到未打开的流的STOP_SENDING帧表示对端不再希望接收此流上的数据。如果数据包丢失或乱序,任一帧都可能在STREAM或STREAM_DATA_BLOCKED帧之前到达。 - -在创建某流ID对应的流之前,所有同类型的流ID比之要小的流都必须(**MUST**)已创建完成,这能确保流的创建顺序在两端都是一致的。 - -在Recv状态下,端接收STREAM和STREAM_DATA_BLOCKED帧,缓存收到的数据,按序重组之后投递给应用,应用消费完数据,缓冲区可用之后可发送MAX_STREAM_DATA帧以允许对端发送更多数据。 - -当接收到带有FIN位的STREAM帧时,流的最终大小确定(参见第4.5节),流的接收侧因此进入Size Known状态。在此状态下,端不再需要发送MAX_STREAM_DATA帧,只需接收某些流数据的重传即可。 - -一旦接收完所有数据,流的接收侧就进入Data Recvd状态,接收到与导致流切换为Size Known态相同的STREAM帧后也可能会切入此状态。接收齐所有数据后,可以丢弃该流上收到的任何STREAM或STREAM_DATA_BLOCKED帧。 - -Data Recvd状态持续到数据被投递给应用,一旦数据投递完,流即进入Data Read状态,这是一个终止状态。 - -在Recv或Size Known状态下收到RESET_STREAM帧会使得流进入Reset Recvd状态。这可能会导致向应用投递流数据的过程中断。 - -在Data Recvd状态,收到RESET_STREAM时,可能已经接收完所有流数据;类似地,在Reset Recvd态,即接收到RESET_STREAM帧后,剩余的流数据也有可能到达。一个实现可以自由选择如何处置这种状况。 - -发送RESET_STREAM意味着端不能保证流数据的完整投递,但收到RESET_STREAM,并不意味着不再投递流数据。一个实现可以中断流数据的投递,丢弃任何未消费的数据,并发出RESET_STREAM的接收信号,但如果此时流数据被完全接收并被缓冲以供应用读取,则实现可以选择扣压RESET_STREAM信号或保留。如果RESET_STREAM被扣压,则流的接收侧仍然处于Data Recvd态。 - -一旦应用接收到指示流被重置的信号,流的接收侧即切换到Reset Read状态,这也是一个终止状态。 - -## 3.3. 允许的帧类型(Permitted Frame Types) - -流的发送方仅发送三种影响发送方或接收方的流状态的帧类型:STREAM(第19.8节),STREAM_DATA_BLOCKED(第19.13节),和RESET_STREAM(第19.4节)。 - -发送方在终止状态(Data Recvd或Reset Recvd)不得(**MUST NOT**)发送任何这些帧。发送方在处于Reset Sent态(即在发送RESET_STREAM帧之后)或终止态时不得(**MUST NOT**)发送STREAM或STREAM_DATA_BLOCKED帧。接收方在任何状态下都可接收这三种帧,因为承载它们的数据包可能存在延迟或乱序。 - -流的接收方可发送MAX_STREAM_DATA帧(第19.10节)和STOP_SENDING帧(第19.5节)。 - -接收方在Recv态时仅可发送MAX_STREAM_DATA帧,在除了Reset Recvd和Reset Read态(即尚未收到RESET_STREAM)的其他状态,都可以发送STOP_SENDING帧。不过,在Data Recvd状态下发送STOP_SENDING帧几乎没有价值,因为所有流数据都已接收。由于数据包的延迟或乱序,发送方可以在任何状态下收到上述两种类型的帧。 - -## 3.4. 双向流状态(Bidirectional Stream States) - -双向流由发送侧和接收侧组成。实现可以将双向流的状态表示为发送侧和接收侧流状态的组合。最简单的模型是在发送侧或接收侧处于非终止状态时将流状态表示为“打开”,当发送侧和接收侧都处于终止状态时将流表示为“关闭”。 - -表2呈现了一个更复杂的双向流状态映射模型,其可以松散地对应到HTTP/2[HTTP2]中定义的流状态。此时,发送侧或接收侧的多个状态被映射到同一个复合状态。请注意,这只是这种映射的一种可能表达,其要求在切换到“关闭”或“半关闭”状态之前确认数据。 - -|Sending Part |Receiving Part |Composite State | -|:---|:---|:---| -|No Stream / Ready |No Stream / Recv (*1) |idle| -|Ready / Send / Data Sent |Recv / Size Known |open| -|Ready / Send / Data Sent |Data Recvd / Data Read |half-closed (remote)| -|Ready / Send / Data Sent |Reset Recvd / Reset Read |half-closed (remote)| -|Data Recvd |Recv / Size Known |half-closed (local)| -|Reset Sent / Reset Recvd |Recv / Size Known |half-closed (local)| -|Reset Sent / Reset Recvd |Data Recvd / Data Read |closed| -|Reset Sent / Reset Recvd |Reset Recvd / Reset Read |closed| -|Data Recvd |Data Recvd / Data Read |closed| -|Data Recvd |Reset Recvd / Reset Read |closed| - -表2: Possible Mapping of Stream States to HTTP/2 - ->Note (*1):如果流尚未被创建或者其接收侧处于Recv态但尚未接收到任何数据,则将流置为 idle态。 - -## 3.5. 请求状态转换(Solicited State Transitions) - -如果应用不再对它在流上接收的数据感兴趣,它可以中止读取流并指定应用级错误码。 - -如果流处于Recv或Size Known状态,传输应该(**SHOULD**)通过发送一个STOP_SENDING帧来提示相反方向的流关闭。这通常表示应用不再读取它从流中接收的数据,但这不是说传入的数据将被忽略。 - -发送STOP_SENDING帧后接收到的STREAM帧仍计入连接和流控,即使这些帧在接收方可能被丢弃。 - -STOP_SENDING帧请求接收到该帧的终端发送RESET_STREAM帧。如果流处于Ready或Send状态,则接收到STOP_SENDING帧的终端必须(**MUST**)发送RESET_STREAM帧。如果流处于Data Sent态,此端可以(**MAY**)延迟发送RESET_STREAM帧,直到已发送的数据都被确认或声明丢失。如果有数据被声明丢失,终端应该(**SHOULD**)发送一个RESET_STREAM帧而不是重传数据。 - -终端应该(**SHOULD**)将错误码从STOP_SENDING帧复制到它发送的RESET_STREAM帧,但也可以使用任何应用级错误码。发送了STOP_SENDING帧的终端可以(**MAY**)忽略随后接收到的RESET_STREAM帧中的错误码。 - -STOP_SENDING应该(**SHOULD**)只由尚未被对端重置的流这一侧发送。STOP_SENDING对于处于Recv或Size Known状态的流最有用。 - -如果先前包含STOP_SENDING的数据包丢失,则本端应再次发送STOP_SENDING帧。但是,一旦所有流数据都收齐或接收到RESET_STREAM帧——也就是说,流处于除Recv或Size Known之外的其他状态——发送STOP_SENDING帧就不必要了。 - -希望将双向流的两个方向都关闭的终端可以通过发送RESET_STREAM帧来终止一个方向,并且可以通过发送STOP_SENDING帧触发相反方向的关闭。 - -# 4. 流量控制(Flow Control) -接收方需要限制他们需要缓冲数据的数据量,以防止被较快的发送方的大量数据淹没或被恶意发送方消耗大量内存。为了使接收方能够限制一个连接的内存消耗,可以将各个流或一个多流连接视为一个整体进行流控。QUIC接收方可以控制发送方在某一个流或所有流上可发送的最大数据量,如第4.1节和第4.2节所述。 - -类似地,为了限制一个连接内的并发流数,QUIC一端可限制对端能发起的最大累加流个数,如第4.6节所述。 - -对CRYPTO帧数据的流控方式与普通数据不同。QUIC依靠加密协议实现来避免数据的过度缓冲,参见[QUIC-TLS]。为了避免在多个层进行过多的缓冲,QUIC实现应该(**SHOULD**)为加密协议实现提供一个接口来告知其缓冲限制。 - -## 4.1. 数据流控(Data Flow Control) - -QUIC采用基于限额的流量控制方案,其中接收方通告它在给定流上或整个连接上可以接收的总字节数的限额。这导致QUIC中有两个级别的数据流控制: -* 基于流的流控,它通过限制可以在单个流上可发送的数据量来防止单个流消耗整个连接的接收缓冲区。 - -* 基于连接的流控,它通过限制所有流上通过STREAM帧发送的流数据的总字节数来防止发送方超出接收方的连接缓冲区容量。 - -发送方不得(**MUST NOT**)发送超过任一限制的数据。 - -接收方在握手期间通过传输参数为所有流设置初始限制(第7.4节)。随后,接收方向发送方发送MAX_STREAM_DATA帧(第19.10节)或MAX_DATA帧(第19.9节)以通告更大的限制。 - -接收方可以通过发送带有相应流ID的MAX_STREAM_DATA帧来通告流的更大限额。MAX_STREAM_DATA帧指定流的绝对字节偏移量上限。接收方可以基于流上当前消费的数据偏移量来确定要通告的流控偏移量。 - -接收方可以通过发送MAX_DATA帧来为一个连接通告更大的限额,该帧指定所有流的绝对字节偏移总和的上限。接收方维护在所有流上接收到的字节的累积总和,用于判定是否违反了通告的连接或流上数据限额。接收方可以根据所有流上消耗的字节总和来确定要通告的最大数据限额。 - -一旦接收方通告了连接或流上的限额,再通告一个更小的限额不会导致错误,但会被发送方忽略。 - -如果发送方违反了通告的连接或流上的数据限额,则接收方必须(**MUST**)以FLOW_CONTROL_ERROR类型的错误关闭连接,有关错误处理的详细信息,请参阅第11章。 - -发送方必须(**MUST**)忽略不增加流控限额的任何MAX_STREAM_DATA或MAX_DATA帧。 - -如果发送方已发送数据达到限额,则将无法发送新数据并被视为阻塞,发送方应该(**SHOULD**)发送一个STREAM_DATA_BLOCKED或DATA_BLOCKED帧来向接收方表明它有数据要写入但被流控限额阻塞。如果发送方被阻塞的时间长于空闲超时定时器(第10.1节),即使发送方有可用于传输的数据,接收方也可能关闭连接。为了防止连接关闭,受流控阻塞的发送方应该(**SHOULD**)在没有ACK触发包数据包传输时定期发送STREAM_DATA_BLOCKED或DATA_BLOCKED帧。 - -## 4.2. 增加流控上限(Increasing Flow Control Limits) - -在MAX_STREAM_DATA和MAX_DATA帧中通告多少限额以及何时发送这两种帧,是由实现决定的,但本节将提供了一些注意事项。 - -为避免阻塞发送方,接收方可以(**MAY**)在一个RTT中多次发送MAX_STREAM_DATA或MAX_DATA帧,或者提前足够时间量发送这两种帧,以便为丢包和后续恢复留出时间。 - -控制帧会增加连接开销,因此,频繁发送变化很小的MAX_STREAM_DATA和MAX_DATA帧是不可取的。另一方面,如果更新不那么频繁,则需要更大的限额增量以避免阻塞发送方,从而要求接收方承担更大的资源消耗。在确定通告多大的限额时,需要在资源消耗和连接开销之间进行权衡。 - -接收方可以使用自动调整机制,根据RTT估计或应用消费接收到的数据的速率来调整通告限额的频率和数值,类似于常见的TCP实现。作为一种优化,只有当有其他帧要发送时,本端才可以发送与流控相关的帧,以确保流控不会导致发送额外的数据包。 - -被阻塞的发送方可以不发送STREAM_DATA_BLOCKED或DATA_BLOCKED帧。因此,接收方不得(**MUST NOT**)等待STREAM_DATA_BLOCKED或DATA_BLOCKED帧到达之后再发送MAX_STREAM_DATA或MAX_DATA帧,因为这样做可能会导致发送方在连接的其余时间被阻塞。即使发送方发送了这些帧,接收方等待它们到达再回应也会导致发送方被阻塞至少整个RTT。 - -当发送方在被阻塞后收到新的限额时,它可能会发送大量数据作为响应,导致短期拥塞。有关发送方如何避免这种拥塞的讨论,请参见[QUIC-RECOVERY]第7.7节。 - -## 4.3. 流控性能(Flow Control Performance) - -如果本端无法确保其对端始终具有大于此连接上的带宽延迟乘积的可用流控限额,则其接收吞吐量将受到流控的限制。 - -数据包丢失会导致接收缓冲区出现间隙,从而阻止应用消费数据并释放接收缓冲区空间。 - -及时发送流控限额的更新可以提高性能。仅发送携带流控帧的数据包会增加网络负载并对性能产生不利影响。将流控帧与其他帧(例如ACK帧)一起发送可降低这些更新的成本。 - -## 4.4. 处理流关闭(Handling Stream Cancellation) - -终端间需要最终就每个流消耗的流控限额达成一致,以便能够满足连接级流量控制。 - -收到RESET_STREAM帧后,终端将设置匹配的流的状态为终止态,并忽略到达该流的其他数据。 - -RESET_STREAM可以立即终止流的一个方向。对于双向流,RESET_STREAM对相反方向的数据流没有影响。两端都必须(**MUST**)在未终止方向上保持流的流控状态,直到该方向进入终止状态。 - -## 4.5. 流最终大小(Stream Final Size) - -最终大小是流消耗的流控限额。假设流上的每个连续字节都发送一次,最终大小就是发送的字节数,更一般地说,是比这些字节中的最大偏移字节量高1,如果没有发送字节,则为零。 - -无论流如何终止,发送方始终将流的最终大小可靠地传递给接收方。最终大小是带有FIN标志的STREAM帧的Offset字段与Length字段的总和,注意这些字段可能是隐式的。或者,RESET_STREAM帧的Final Size字段会携带此值,这保证了两端就发送方在该流上消耗了多少流控限额达成一致。 - -当流的接收侧进入Size Known或Reset Recvd态(参见第3章)时,终端将知道流的最终大小。接收方必须(**MUST**)使用流的最终大小作为流上发送的所有字节数来参与连接级流控的计算。 - -终端不得(**MUST NOT**)在流上发送等于或超过最终大小的数据。 - -一旦知道流的最终大小,它就不能变化。如果接收到的RESET_STREAM或STREAM帧指示流的最终大小发生变化,终端应该(**SHOULD**)响应类型为FINAL_SIZE_ERROR的错误。有关错误处理的详细信息,请参阅第11章。接收方应该(**SHOULD**)将接收到大于等于最终大小的数据视为FINAL_SIZE_ERROR类型的错误,即使在流关闭之后也是如此。不过生成这些错误不是强制性的,因为要求端生成这些错误也意味着端需要维护流关闭后的最终大小,这可能意味着重要的状态承诺。 - -## 4.6. 控制并发(Controlling Concurrency) - -本端可以限制对端可以打开的传入流的累积数量,只能打开流ID小于 (max_streams * 4 + first_stream_id_of_type)的流,见表1。初始限制在传输参数中设置,参见第18.2节,随后可以使用MAX_STREAMS帧通告后续限额,参见第19.11节。单向和双向流分别有各自的限额。 - -如果接收到的max_streams传输参数或MAX_STREAMS帧的值大于2^60^,这将导致最大的流ID不能被变长整数进行表示,参见第16章。上述两种情况,连接都必须(**MUST**)关闭。如果有问题的值是在传输参数中收到的,连接错误类型为TRANSPORT_PARAMETER_ERROR;如果是在帧中收到,则连接错误类型为FRAME_ENCODING_ERROR。详情参见第16章。 - -终端不得(**MUST NOT**)超过其对端设置的限制。接收到流ID超过其发送限制的帧的终端必须(**MUST**)将此视为STREAM_LIMIT_ERROR类型的连接错误,有关错误处理的详细信息,请参阅第11章。 - -一旦接收方使用MAX_STREAMS帧通告流限额,再通告较小的限额就无效。必须(**MUST**)忽略不增加流限额的MAX_STREAMS帧。 - -与流和连接上的流控制一样,本文让实现来决定何时以及什么场景应该通过MAX_STREAMS向对端通告允许多少个流。当流关闭时,实现可能会选择增加限制,以保持对端可用的流数量大致一致。 - -由于对端的限制而无法打开新流的终端应该(**SHOULD**)发送一个STREAMS_BLOCKED帧 (参见第19.14节)。该信号对调试很有用。终端不得(**MUST NOT**)等待此信号到达后再通告额外的限额,否则将意味着对端将被阻塞至少整个RTT,如果对端选择不发送STREAMS_BLOCKED帧,则可能无限期地阻塞。 - -# 5. 连接(Connections) - -QUIC连接在客户端和服务端之间共享状态。 - -每个连接都从握手阶段开始,在此期间,两端使用加密握手协议[QUIC-TLS]协商共享秘钥及应用层协议。两端通过握手(第7章)确认通讯意愿(第8.1节)并为连接交换参数(第7.4节)。 - -应用层协议可以在握手阶段使用连接,但有一些限制。0-RTT允许客户端在收到服务端响应之前发送应用数据。然而,0-RTT没有提供针对重放攻击的保护,参见[QUIC-TLS]第9.2节。服务端也可以在收到最终加密握手消息(确认客户端的身份和活性之用)之前发送应用数据到客户端。这些功能为应用层协议提供牺牲安全性以换取较低时延的选项。 - -连接ID的使用(第5.1节)允许连接迁移到新的网络路径,终端可以直接发起迁移,也可以在中间设备变更时强制迁移。第9章描述了与迁移相关的安全和隐私问题的治理措施。 - -对于不再需要的连接,客户端和服务端可以通过多种方式终止连接,如第10章所述。 - -## 5.1. 连接ID(Connection ID,CID) - -每个连接都拥有一组连接标识符或称连接ID(后续统一简称“CID”),每个连接标识符都可以标识该连接。每个终端都可以独立选择自己的CID供对端使用。 - -CID的主要功能是确保较低协议层(UDP、IP)的寻址更改不会导致QUIC连接的数据包传输到错误的终端。任一终端都使用特定于实现(也可能是特定于部署)的方法选择CID,使得对端发送的携带该CID的数据包能路由过来,并在接收到时识别出来。 - -终端可以使用多个CID,以便外部观察者在没有终端协作上下文时无法识别来自同一连接的不同CID的数据包,参见第9.5节。 - -CID不得(**MUST NOT**)包含任何可被外部观察者(即不与发送端合作的观察者)用于将它们与同一连接的其他CID相关联的信息。作为一个简单的例子,这意味着同一连接上不得(**MUST NOT**)多次给出相同CID。 - -具有长报文头的数据包包含源连接ID(Source Connection ID,后续统一简称“SCID”)和目的连接ID(Destination Connection ID,后续统一简称“DCID”)字段。这些字段可用于填写新的CID,详细信息请参阅第7.2节。 - -具有短报文头的数据包(第17.3节)仅包含DCID,并显式省略长度。对端应明确知道DCID字段的长度。使用基于CID进行路由的负载均衡器的终端,可以与负载均衡器就CID的固定长度达成一致,或者就编码方式达成一致,约定在某固定部分显式编码长度信息,这样即使CID的长度不同,仍然能为负载均衡器使用。 - -Version Negotiation包(第17.2.1小节)回显客户端填写的CID,以便正确路由到客户端并证明该数据包是对客户端发送的数据包的响应。 - -当不需要用CID路由到正确的终端时,可以使用零长度CID。但是,使用零长度CID时,如果在同一本地IP地址和UDP端口上复用连接,可能导致对端在连接迁移、NAT重新绑定和客户端端口重用的时候失败。使用零长度CID时,终端不得(**MUST NOT**)为多个并发连接使用相同的IP地址和端口,除非确定不需要使用这些协议功能。 - -当终端使用非零长度的DCID发送数据包时,它需要在对端用NEW_CONNECTION_ID帧(参见第19.15节)提供的CID列表中选择一个。 - -### 5.1.1. 发布连接ID(Issuing Connection IDs) - -每个CID都有一个对应的序号,以便在NEW_CONNECTION_ID或RETIRE_CONNECTION_ID帧时引用它。在握手期间,终端发出的长包头(第17.2节)的SCID字段中会携带初始CID,其序号为0,如果在传输参数中也携带了preferred_address CID,则该CID的序号为1。 - -可以使用NEW_CONNECTION_ID帧(第19.15节)将其他CID发送给对端,但每个新发布的CID的序号必须(**MUST**)加1。客户端发送的第一个DCID字段指定的CID和Retry包中的CID的都不需要指定序号。 - -当终端发布CID后,它在连接期间或者说直到对端通过RETIRE_CONNECTION_ID帧(第19.16节)停用该CID之前,都必须(**MUST**)接收携带此CID的数据包。已发布但未停用的CID被视为活动ID,任何活动CID都可以在任何时间在当前连接的任意类型数据包中使用。这也包括服务端通过preferred_address传输参数发布的CID。 - -终端应该(**SHOULD**)确保它的对端有足够数量的可用和未使用的CID。终端使用active_connection_id_limit传输参数通告他们愿意维护的活动CID的数量。终端不得(**MUST NOT**)提供超过对端限制的CID数。如果在NEW_CONNECTION_ID帧中同时指定足够大的Retire Prior To字段让对端停用该序号之前的所有CID,可以(**MAY**)临时超出限制。 - -NEW_CONNECTION_ID帧可能会导致终端增加一些活动CID并根据Retire Prior To字段停用某些CID。在处理NEW_CONNECTION_ID帧增加或停用活动CID后,如果活动CID的数量仍然超过其active_connection_id_limit传输参数中通告的值,则终端必须(**MUST**)关闭连接并显示CONNECTION_ID_LIMIT_ERROR类型的错误。 - -当对端停用某个CID后,终端应该(**SHOULD**)提供一个新的CID。如果该终端提供的CID数量未达到对端的active_connection_id_limit限制,则其在收到具有以前未使用(但发布过)的CID的数据包时可以(**MAY**)提供新的CID。终端可以(**MAY**)自行限制连接发布的CID总数,以避免CID耗尽的风险(参见第10.3.2小节),这样做也可以(**MAY**)减少它需要维护的每条路径的状态数量,就如路径验证(活跃)状态,每个发布的CID,都有可能对应一条交互路径(维护起来很耗资源)。 - -发起连接迁移并需要非零长度CID的终端应该(**SHOULD**)确保其对端可用的CID池还有余量,以允许对端在迁移时使用新的CID,如果该池耗尽,对端将无法响应。 - -在握手期间选择零长度CID的终端不能发布新的CID,通过任何网络路径向此终端发送的任何数据包的DCID字段的长度都必须为0。 - -### 5.1.2. 消费和停用连接ID(Consuming and Retiring Connection IDs) - -在连接期间,终端可以随时将其填写的DCID变更为另一个可用的CID。终端在迁移时使用CID以响应对端,有关更多详细信息,请参阅第9.5节。 - -终端维护一组从其对端接收到的CID,在发送数据包时可以使用其中的任何一个。当终端希望在使用中删除某个CID时,它会向其对端发送RETIRE_CONNECTION_ID帧,表示不会再次使用该CID,并请求对端发送NEW_CONNECTION_ID帧将其替换为新的CID。 - -如第9.5节所述,终端将CID限制为关联单个本端地址或单个目的地址。当终端不再需要使用该CID关联的本地地址或目的地址时,则应该(**SHOULD**)停用该CID。 - -在某些情况下,终端可能需要停止接受某些其先前发布的CID,则可以发送NEW_CONNECTION_ID,内含Retire Prior To字段,表示先于此序号的CID全部停用。终端应该(**SHOULD**)继续允许接收先前发布的CID,直到它们被对端停用。如果终端不能再处理指定的CID,它可以(**MAY**)关闭连接。 - -在接收到内含增长的Retire Prior To字段的NEW_CONNECTION_ID帧后,在将新提供的CID添加到活动CID集合之前,对端必须(**MUST**)停用相应的CID并通过发送RETIRE_CONNECTION_ID帧通知对方。这种顺序使得终端可以替换所有活动CID,而不会出现对端没有可用CID的可能性,并且不会超出对端在active_connection_id_limit传输参数中设置的限制,参见第18.2节。在发送RETIRE_CONNECTION_ID请求时未能及时停用该CID可能导致连接失败,因为对端可能已经无法使用该CID。 - -在尚未收到对应的RETIRE_CONNECTION_ID帧的确认之前,终端应该(**SHOULD**)限制本地停用的CID的数量。终端应该(**SHOULD**)允许发送和跟踪至少两倍于active_connection_id_limit传输参数值的RETIRE_CONNECTION_ID帧的数量。就算可以(**MAY**)将超过active_connection_id_limit限制的需要停用的CID视为CONNECTION_ID_LIMIT_ERROR类型的连接错误,终端也不得(**MUST NOT**)在未停用CID的情况下遗忘该CID。 - -在发送NEW_CONNECTION_ID帧,携带的Retire Prior To值停用所有CID之后,收到对端响应的RETIRE_CONNECTION_ID帧之前,终端不应(**SHOULD NOT**)发送新的NEW_CONNECTION_ID帧更新Retire Prior To字段。 - -## 5.2. 匹配连接与数据包(Matching Packets to Connections) - -传入的数据包在接收时会被分类。一类数据包可以关联到现有连接,另一类数据包——对服务端——可能创建一个新连接。 - -如果数据包具有与现有连接对应的非零长度的DCID,QUIC会相应地处理该数据包,将之与该CID关联。请注意,一个连接可以关联多个CID,参见第5.1节。 - -如果DCID的长度为零,并且数据包中的寻址信息,与终端用来标识具有零长度CID的连接的寻址信息匹配,则QUIC会将数据包作为该连接的一部分进行处理。终端可以仅使用目的IP和端口或同时使用源地址和目的地址进行标识,尽管这会使连接变得脆弱,如第5.1节所述。 - -终端可以为任何不能归属于现有连接的数据包发送Stateless Reset包(参见第10.3节)。无状态重置机制允许对端更快地识别连接何时变得不可用。 - -如果数据包与该连接的状态不一致,则即使与现有连接匹配的数据包也将被丢弃。例如,如果数据包指示的协议版本与连接的协议版本不同,或者预期的密钥变得可用,但数据包保护策略去除失败,则数据包将会被丢弃。 - -缺乏强大完整性保护的无效数据包,例如Initial包、Retry包或Version Negotiation包之类的数据包也可以(**MAY**)被丢弃。如果在发现错误之前处理了这些数据包的内容,则终端必须(**MUST**)生成连接错误,或者完全回滚在该处理期间所做的任何更改。 - -### 5.2.1. 客户端数据包处理(Client Packet Handling) - -发往客户端的有效数据包总是包含与客户端选择的值匹配的DCID。选择接收零长度CID数据包的客户端可以使用本地地址和端口来标识连接。与现有连接不匹配的数据包——基于DCID,或者,如果此值为零长度,基于本地IP地址和端口——将被丢弃。 - -由于数据包乱序或丢失,客户端可能会收到使用尚未启用的密钥进行加密的数据包。客户端可以(**MAY**)丢弃这些数据包,或者也可以(**MAY**)缓存它们以待后续秘钥解密。 - -如果客户端接收到的数据包使用的版本与它最初选择的版本不同,都必须(**MUST**)丢弃该数据包。 - -### 5.2.2. 服务端数据包处理(Server Packet Handling) -如果服务端收到一个其不支持的版本的数据包,但是该数据包的长度满足它支持的某个版本的数据包大小限制,则服务端应该(**SHOULD**)发送一个Version Negotiation包以开启新连接,如第6.1节所述。同时,服务端可以(**MAY**)限制Version Negotiation包的回应频率,服务端必须(**MUST**)丢弃比它支持的版本长度要小的数据包。 - -对服务端收到的第一个其不支持版本的数据包,某些特定于版本的字段可能有不同的语义和编码。特别的是,不同的版本可能有不同的数据包保护秘钥,服务端不太可能解码其不支持的版本的数据包。但如上所述,如果其长度合适,服务端应该(**SHOULD**)回以其支持版本的协商包。 - -具有受支持版本或无版本字段的数据包使用CID或本地地址和端口(对于具有零长度CID的数据包)匹配连接,之后由匹配的连接进行后续处理。如果匹配失败,服务端的处理如下所述: - -如果数据包是完全符合规范的Initial包,则服务端继续握手(参见第7章),这会使得服务端切换到客户端选择的版本。 - -如果服务端拒绝接受新连接,它应该(**SHOULD**)发送一个Initial包,其中包含一个错误码为CONNECTION_REFUSED的CONNECTION_CLOSE帧。 - -如果数据包是0-RTT包,服务端可以(**MAY**)缓存有限数量的这类数据包,以等待随后发送的Initial包。客户端在收到服务端响应之前无法发送Handshake包,因此服务端在这种场景下应该(**SHOULD**)忽略任何此类Handshake包。 - -除此以外的其他任何场景下,服务端都必须(**MUST**)丢弃传入的数据包。 - -### 5.2.3. LB注意事项( Considerations for Simple Load Balancers) - -服务端部署的时候可以仅使用源和目的IP地址和端口在服务端之间进行负载均衡。对客户端IP地址或端口的更改可能会导致数据包被转发到错误的服务端。当客户端的地址更改时,可以使用以下方法之一部署服务端来保证连接的连续性: - -服务端可以使用带外机制根据CID将数据包转发到正确的服务端。 - -如果服务端可以使用专用的服务端IP地址或端口,而不是客户端最初连接到的IP地址或端口,则服务端可以使用preferred_address传输参数来请求客户端将连接移动到该专用地址。请注意,客户端可以选择不使用preferred_address。 - -如果部署中的服务端没有具体的实施方案以在客户端地址更改时保持连接的连续性,应该(**SHOULD**)使用disable_active_migration传输参数指示其不支持迁移。disable_active_migration传输参数不会在客户端对preferred_address传输参数进行操作后禁止连接迁移。 - -使用这种简单形式的负载均衡的服务端部署必须(**MUST**)避免创建无状态重置预言机制;参见第21.11节。 - -## 5.3. 连接操作(Operations on Connections) - -本文没有定义QUIC的API,不过,它为应用层协议可以依赖的QUIC连接定义了一组函数。应用层协议可以假设QUIC实现提供了一个接口,其中包括本节中描述的操作。设计用于特定应用层协议的QUIC实现可能仅提供该协议要用到的那些操作。 - -作为客户端角色,应用层协议可以: - -* 打开一个连接,与服务端开始第7章中描述的交互; -* 在可用时启用Early Data(0-RTT数据); - -当Early Data被服务端接受或拒绝时被通知。 - -作为服务端角色,应用层协议可以: - -* 侦听传入连接,为第7章中描述的交互做准备; -* 如果支持Early Data,则在发送给客户端的TLS会话恢复消息中嵌入应用控制数据; -* 如果支持Early Data,则从客户端的TLS会话恢复消息中提取应用控制数据,并根据该信息选择接受或拒绝Early Data。 - -作为任一角色,应用层协议都可以: - -* 为每种类型允许的流个数初始值设置最小值,如传输参数中所述(第7.4节); -* 通过为流和连接设置流控限额来控制接收缓冲区的资源分配; -* 确认握手是成功完成还是仍在进行中; -* 通过生成PING帧(第19.2节)或在空闲超时到期之前请求传输层发送其他帧(第10.1节),保持连接不静默关闭; -* 立即关闭(第10.2节)连接。 - -# 6. 版本协商(Version Negotiation) - -版本协商机制使得服务端可以表明它不支持客户端正在使用的版本。服务端发送Version Negotiation包以响应可能启动新连接的每个数据包,详细信息请参阅第5.2节。 - -客户端发送的第一个数据包的大小将决定服务端是否发送Version Negotiation包。支持多个QUIC版本的客户端应该(**SHOULD**)确保他们发送的第一个UDP报文的大小,是其支持的所有版本的最小容许数据报文长度中的最大值,必要时使用PADDING帧(第19.1节)。这可确保服务端在存在彼此都支持的版本时做出响应。如果服务端收到的UDP报文长度小于其支持的所有版本的最小容许报文长度的最小值,则它可能不会发送Version Negotiation包,见第14.1节。 - -## 6.1. 发送版本协商包(Sending Version Negotiation Packets) - -如果客户端选择的版本不为服务端所接受,则服务端以Version Negotiation包回应(参见第17.2.1小节),包里面包括服务端可接受的版本列表。客户端不得(**MUST NOT**)发送Version Negotiation包来响应接收到的Version Negotiation包。 - -本系统允许服务端在丢失状态的情况下处理其不支持版本的数据包。因为其作为响应发送的Initial包或Version Negotiation包可能会丢失,但客户端会发送新数据包,直到它成功收到响应或放弃连接尝试。 - -服务端可以(**MAY**)控制它发送的Version Negotiation包的数量。例如,能够将数据包识别为0-RTT包的服务端可能会选择不发送Version Negotiation包响应,期望客户端最终会发送Initial包。 - -## 6.2. 处理版本协商包(Handling Version Negotiation Packets) - -Version Negotiation包的设计是为了让QUIC协商用于连接的版本以支持将来定义的功能。未来的标准跟踪规范可能会改变使用当前版本建立连接,但支持多QUIC版本的客户端在收到Version Negotiation包时的实现方式。 - -仅支持当前QUIC版本的客户端如果收到一个Version Negotiation包,则必须(**MUST**)放弃当前的连接尝试,以下场景除外: - -如果客户端收到并成功处理了其他数据包(包括早前的Version Negotiation包),则客户端必须(**MUST**)忽略此Version Negotiation包。 - -如果此Version Negotiation包内支持的版本列表包含了当前客户端使用的版本,则客户端必须(**MUST**)忽略此Version Negotiation包。 - -如何实现版本协商是留给未来的标准跟踪规范定义的工作。特别是,未来的工作需要确保对版本降级攻击的鲁棒性,见第21.12节。 - -## 6.3. 使用保留版本(Using Reserved Versions) - -为了支持将来服务端使用的新版本,客户端需要正确处理不受支持的版本。一些版本号(0x?a?a?a?a,如第15章所定义)被保留用于包含版本号的字段中。 - -终端可以(**MAY**)在任何会被未知或不支持版本忽略的字段中添加保留版本号,以测试对端是否正确忽略了该值。例如,终端可以在Version Negotiation包中包含保留版本号,参见第17.2.1小节。终端可以(**MAY**)发送带有保留版本号的数据包,以测试对端是否正确丢弃了该数据包。 - -# 7. 加密和传输握手(Cryptographic and Transport Handshake) - -QUIC依靠结合加密和传输握手来尽可能降低连接建立时延。QUIC使用CRYPTO帧(第19.6节)来传输加密握手信息。本文中定义的QUIC版本标识为0x00000001,并使用[QUIC-TLS]中描述的TLS加密协议,不同的QUIC版本可能使用不同的加密握手协议。 - -QUIC提供可靠、保序的加密握手数据传输。QUIC的数据包保护机制会加密尽可能多的握手报文。加密握手必须(**MUST**)提供以下属性: - -* 经过认证的秘钥交换,其中 - - 服务端需要进行身份认证; - - 客户端可选地进行身份认证; - - 每次连接都需要产生不同且不相关的密钥,并且 - - 密钥材料可用于0-RTT和1-RTT包保护。 -* 经过认证的两个终端进行传输参数交换,以及服务端传输参数的机密性保护(参见第7.4节)。 -* 经过认证的应用层协议协商(TLS为此使用应用层协议 (ALPN)[ALPN]协商)。 - -CRYPTO帧可以在不同的数据包编号(Packet Number,后续统一简称“包号”)空间中发送(第12.3节)。CRYPTO帧使用的偏移量用于确保加密握手数据的有序传输,每个包号空间中的编号从0开始。 - -图4显示了一个简化的握手以及握手期间数据包和帧的交换过程。以星号(“*”)提示的是在握手期间可以进行应用数据交换的步骤。一旦握手完成,终端就可以自由地交换应用数据了。 - -``` - Client Server - - Initial (CRYPTO) - 0-RTT (*) ----------> - Initial (CRYPTO) - Handshake (CRYPTO) - <---------- 1-RTT (*) - Handshake (CRYPTO) - 1-RTT (*) ----------> - <---------- 1-RTT (HANDSHAKE_DONE) - - 1-RTT <=========> 1-RTT - -``` -图4: Simplified QUIC Handshake - -终端可以在握手期间发送数据包来测试显式拥塞通知(ECN)支持,详情参见第13.4节。如果支持ECN,终端会在回应对端的第一个数据包的ACK帧内携带ECN计数,如第13.4.2小节所述。 - -终端必须(**MUST**)明确协商应用层协议,这可以避免对正在使用的协议存在分歧的情况。 - -## 7.1. 握手流程示例(Example Handshake Flows) - -[QUIC-TLS]中提供了有关TLS如何与QUIC集成的详细信息,此处仅展示一些示例。第8.1.2小节的示例有支持客户端地址验证交换的扩展版本。 - -一旦任何地址验证交换完成,就可以启动加密握手协商加密密钥。加密握手在Initial(第17.2.2小节)和Handshake(第17.2.4小节)数据包中携带。 - -图5是1-RTT握手的概要图,其每行显示一个QUIC包,行首显示数据包类型和包号,冒号“:”后是通常包含在这些数据包中的帧。例如,第一个数据包的类型为Initial,包号为0,并包含一个携带ClientHello的CRYPTO帧。 - -多个QUIC包——即使是不同的数据包类型——都可以合并成一个单一的UDP报文,参见第12.2节节。因此,这种握手可以包含少则四个UDP报文或更多(受协议固有的限制,例如拥塞控制和反放大机制)。例如,服务端的第一个UDP报文就包含Initial包、Handshake包和1-RTT包中的“0.5-RTT数据”。 - -``` - Client Server - - Initial[0]: CRYPTO[CH] -> - - Initial[0]: CRYPTO[SH] ACK[0] - Handshake[0]: CRYPTO[EE, CERT, CV, FIN] - <- 1-RTT[0]: STREAM[1, "..."] - - Initial[1]: ACK[0] - Handshake[0]: CRYPTO[FIN], ACK[0] - 1-RTT[0]: STREAM[0, "..."], ACK[0] -> - - Handshake[1]: ACK[0] - <- 1-RTT[1]: HANDSHAKE_DONE, STREAM[3, "..."], ACK[0] - -``` -图5: Example 1-RTT Handshake - -图6是0-RTT握手,发送单个0-RTT包的连接示例。注意到,如第12.3节所述,服务端在1-RTT包中确认客户端0-RTT数据,客户端在相同的包号空间中发送1-RTT包。 - -``` - Client Server - - Initial[0]: CRYPTO[CH] - 0-RTT[0]: STREAM[0, "..."] -> - - Initial[0]: CRYPTO[SH] ACK[0] - Handshake[0] CRYPTO[EE, FIN] - <- 1-RTT[0]: STREAM[1, "..."] ACK[0] - - Initial[1]: ACK[0] - Handshake[0]: CRYPTO[FIN], ACK[0] - 1-RTT[1]: STREAM[0, "..."] ACK[0] -> - - Handshake[1]: ACK[0] - <- 1-RTT[1]: HANDSHAKE_DONE, STREAM[3, "..."], ACK[1] - -``` -图6: Example 0-RTT Handshake - -## 7.2. 协商连接ID(Negotiating Connection IDs) - -CID用于确保数据包的路由一致,如第5.1节所述。长包头含两个CID:DCID由数据包的接收方选择,用于提供一致的路由,SCID用于让对端设置DCID。 - -在握手期间,带有长包头(第17.2节)的数据包用于确立两端使用的CID。每个终端使用SCID字段来告知对端,让对端在发送给他们的数据包中填入DCID字段。在处理第一个Initial包后,每个终端将其发送的后续数据包中的DCID字段设置为其接收到的SCID字段的值。 - -如果之前未从服务端收到Initial包或Retry包,客户端会使用不可预测的值填充DCID字段。该DCID的长度必须(**MUST**)至少为8个字节。在收到来自服务端的数据包之前,客户端在此连接上发送的所有数据包都必须(**MUST**)使用相同的DCID值。 - -客户端发送的第一个Initial包中的DCID字段用于确定Initial包的数据包保护密钥,这些密钥在收到Retry包后会发生变化;参见[QUIC-TLS]第5.2节。 - -客户端使用其选择的值填充SCID字段,并设置SCID长度字段以指示长度。 - -客户端第一次发送的0-RTT包与第一个Initial包需要填写相同的DCID和SCID值。 - -在第一次从服务端收到Initial或Retry包后,客户端使用服务端提供的SCID作为后续发送的数据包的DCID,包括任何0-RTT包。这意味着客户端可能需要在连接建立期间两次更改它在DCID字段中填入的CID:一次响应来自服务端的Initial包,一次响应Retry包。一旦客户端从服务端收到一个有效的Initial包,在该连接上接收到的具有不同SCID的任何后续数据包都必须(**MUST**)丢弃。 - -在收到的第一个Initial或Retry包后,客户端必须(**MUST**)将发送数据包的DCID改为新的CID。服务端必须(**MUST**)根据第一个收到的Initial包设置它用于发送数据包的DCID。仅当值取自NEW_CONNECTION_ID帧时,才允许变更DCID;如果后续的Initial包包含不同的SCID,它们必须(**MUST**)被丢弃。这避免了对具有不同SCID的多个Initial包进行可能的无状态处理而导致的不可预测的结果。 - -终端发送的DCID可以在连接的生命周期内改变,特别是在响应连接迁移时(第9章),详细信息请参阅第5.1.1小节。 - -## 7.3. 认证连接ID(Authenticating Connection IDs) - -在握手期间对CID进行操作,另一个选择是对所有相关传输参数进行认证,参见第7.4节。这确保用于握手的所有CID也通过加密握手进行身份认证。 - -每个终端在发送Initial包时,需将其在数据包头中填写的SCID字段,也填入initial_source_connection_id传输参数中,参见第18.2节。服务端收到Initial包后,也需要将收到的数据包头中的DCID值,填入其响应的数据包的original_destination_connection_id传输参数中。如果因为某些原因,服务端需要回应Retry包,则需要在retry_source_connection_id传输参数中填入Retry包头的SCID字段。 - -上述握手期间填写的传输参数的值必须(**MUST**)与本端发送(或接收,用于服务端)的实际值相匹配,接收端也必须(**MUST**)验证这一点。这样可以确保攻击者无法在握手过程中,在其篡改的数据包中注入自己的CID,影响后续正确CID的选择。 - -若来自任一终端的initial_source_connection_id传输参数缺失,或来自服务端的original_destination_connection_id传输参数缺失,终端必须(**MUST**)视为TRANSPORT_PARAMETER_ERROR类型的连接错误。 - -终端必须(**MUST**)将以下内容视为TRANSPORT_PARAMETER_ERROR或PROTOCOL_VIOLATION类型的连接错误: -* 收到服务端Retry包,但未填写retry_source_connection_id传输参数; -* 未收到服务端的Retry包,但却填写了retry_source_connection_id传输参数; -* 对端填写的相应传输参数与Initial包中的DCID或SCID字段不匹配; - -如果选择了零长度CID,则相应的传输参数将包含一个零长度值。 - -图7展示了在完整握手中如何使用连接ID(DCID=目的连接ID,SCID=源连接ID),包括Initial包交换时的CID填写,以及后续1-RTT包交换时DCID的填写方式: - -``` - Client Server - - Initial: DCID=S1, SCID=C1 -> - <- Initial: DCID=C1, SCID=S3 - ... - 1-RTT: DCID=S3 -> - <- 1-RTT: DCID=C1 - -``` -图7: Use of Connection IDs in a Handshake - -图8展示了增加了Retry包交换的握手过程: - -``` - Client Server - - Initial: DCID=S1, SCID=C1 -> - <- Retry: DCID=C1, SCID=S2 - Initial: DCID=S2, SCID=C1 -> - <- Initial: DCID=C1, SCID=S3 - ... - 1-RTT: DCID=S3 -> - <- 1-RTT: DCID=C1 - -``` -图8: Use of Connection IDs in a Handshake with Retry - -在这两种情况下(图7和图8),客户端都将initial_source_connection_id传输参数的值设置为C1。 - -当握手不包括Retry时(图7),服务端将original_destination_connection_id设置为S1(注意该值由客户端选择),并将initial_source_connection_id设置为S3。在这种情况下,服务端不填写retry_source_connection_id传输参数。 - -当握手包括Retry时(图8),服务端将original_destination_connection_id设置为S1,retry_source_connection_id设置为S2,并将initial_source_connection_id设置为S3。 - -## 7.4. 传输参数(Transport Parameters) - -在连接建立期间,两端都会对其传输参数进行了经过身份认证的声明。终端需要遵循每个参数定义的限制,也需要遵循其参数描述中的处理原则。 - -传输参数由两端独立声明。每个终端都可以无视对端参数,自行选择传输参数值。 - -传输参数的编码在第18章中有详细说明。 - -QUIC在加密Handshake包中携带编码的传输参数。握手完成后,对端声明的传输参数即可用。每个终端都需验证对端提供的传输参数的值。 - -第18.2节中有每个传输参数的定义。 - -终端必须(**MUST**)将收到的具有无效值的传输参数视为TRANSPORT_PARAMETER_ERROR类型的连接错误。 - -在给定的传输参数扩展中,终端不得(**MUST NOT**)包含一个传输参数的多个副本,终端应该(**SHOULD**)将此种情况视为TRANSPORT_PARAMETER_ERROR类型的连接错误。 - -终端在握手期间使用传输参数来认证CID的协商过程,参见第7.3节。 - -ALPN(参见[ALPN])允许客户端在连接建立期间通告其支持的多种应用层协议。在握手期间通告的传输参数适用于所有这些应用层协议。应用层协议也可以推荐传输参数的值,例如初始流控限额。客户端可以为传输参数设置约束,但如果这些约束有冲突,则可能使得客户端无法支持多个应用层协议。 - -### 7.4.1. 0-RTT的传输参数(Values of Transport Parameters for 0-RTT) - -是否使用0-RTT取决于客户端和服务端是否可以使用先前连接协商的协议参数。为了启用0-RTT,终端需要将服务端传输参数的值与它在连接上收到的其他会话凭证一起存储。终端还要存储应用层协议或加密握手所需的所有信息;参见[QUIC-TLS]第4.6节。在使用会话凭证尝试0-RTT连接时将会用到先前存储的传输参数的值。 - -握手完成后,客户端使用握手中商定的传输参数。记住直到握手完成并且客户端开始发送1-RTT包,传输参数才可适用于新连接。并非所有传输参数都要被保存,因为有些参数不适用于未来的连接,或者它们对0-RTT的使用没有影响。 - -定义新的传输参数(第7.4.2小节),必须(**MUST**)指定:对0-RTT来说,其存储方式是强制的、可选的还是禁止的。客户端不需要保存它无法处理的传输参数。 - -客户端不得(**MUST NOT**)保存如下传输参数: -* ack_delay_exponent -* max_ack_delay -* initial_source_connection_id -* original_destination_connection_id -* preferred_address -* retry_source_connection_id -* stateless_reset_token - -客户端必须(**MUST**)使用在握手中拿到的服务端的最新值,如果服务端未提供,则使用默认值。 - -尝试发送0-RTT数据的客户端必须(**MUST**)保存服务端处理需要用到的所有其他传输参数。服务端可以保存这些传输参数,或者可以在会话凭证中存储受完整性保护的副本,并在收到0-RTT数据时从中恢复信息。服务端使用这些传输参数来确定是否可以接受0-RTT数据。 - -如果服务端接受0-RTT数据,则服务端不得(**MUST NOT**)自行降低任何限制或更改可能违反客户端0-RTT约束的任何值。特别是,接受0-RTT数据的服务端,如下参数不得(**MUST NOT**)小于其存储过的传输参数的值: -* active_connection_id_limit -* initial_max_data -* initial_max_stream_data_bidi_local -* initial_max_stream_data_bidi_remote -* initial_max_stream_data_uni -* initial_max_streams_bidi -* initial_max_streams_uni - -为某些传输参数省略或设置零值可能会导致0-RTT数据能启用但不可用。对于0-RTT,允许发送应用数据的传输参数的适用子集应该(**SHOULD**)设置为非零值。这些参数集包括initial_max_data和(1)initial_max_streams_bidi或initial_max_stream_data_bidi_remote或(2)initial_max_streams_uni或initial_max_stream_data_uni。 - -服务端可能在流上采用更大的初始流流控限额值,比客户端发送0-RTT时采用的值要大。握手完成后,客户端使用initial_max_stream_data_bidi_remote或initial_max_stream_data_uni的新值来更新所有发送流上的流控限额。 - -服务端可以(**MAY**)存储和恢复先前通告的max_idle_timeout、max_udp_payload_size和disable_active_migration参数的值,如果它选择较小的值,则表示其拒绝0-RTT。因为在接受0-RTT数据的同时降低这些参数的值可能会降低连接的性能。具体来说,降低max_udp_payload_size可能会导致丢包,与直接拒绝0-RTT数据相比,性能会更差。 - -如果不能恢复传输参数,服务端必须(**MUST**)拒绝0-RTT数据。 - -当以0-RTT包发送帧时,客户端必须(**MUST NOT**)只使用保存的传输参数,重要的是,它不得(**MUST NOT**)使用从服务端更新的传输参数或从1-RTT包中接收的帧中学习到的新值。来自握手的传输参数更新值仅适用于1-RTT包。例如,所有0-RTT包都需要采用来自先前保存的流控限额参数,即使这些值因握手或1-RTT包中的帧加大也不考虑。在0-RTT中使用更新的传输参数,服务端可以(**MAY**)将其视为PROTOCOL_VIOLATION类型的连接错误。 - -### 7.4.2. 新传输参数(New Transport Parameters) - -新的传输参数可用于协商新的协议行为。终端必须(**MUST**)忽略它不支持的传输参数。缺少某个传输参数会因此禁用使用该参数协商的任何可选协议功能。如第18.1节所述,保留了一些标识符以满足此要求。 - -不理解某个传输参数的客户端可以丢弃它并在后续连接上尝试0-RTT。但是,如果客户端之后添加对该传输参数的支持,则它可能会在尝试0-RTT时违反此传输参数建立的约束。新的传输参数可以通过设置最保守的默认值来规避此问题。客户端可以通过存储所有参数来规避这个问题,包括当前不支持的参数。 - -可以根据第22.3节中的规则注册新的传输参数。 - -## 7.5. 加密消息缓存考虑(Cryptographic Message Buffering) - -实现需要为乱序接收到的CRYPTO帧维护一个数据缓冲区。由于CRYPTO帧没有流量控制,因此终端可能会潜在要求其对端缓冲无限量的数据。 - -在握手期间,终端实现必须(**MUST**)支持至少缓存4096字节的乱序CRYPTO帧数据,当然其也可以(**MAY**)缓冲更多数据,因为更大的缓冲区允许握手期间交换更大的密钥或凭证。在整个连接的生命周期内,终端的缓冲区大小不需要保持不变。 - -在握手期间无法缓存CRYPTO帧可能会导致连接失败。如果在握手期间超出终端的缓冲区大小,它可以临时扩展其缓冲区以完成握手。如果终端不扩展其缓冲区,则必须(**MUST**)使用CRYPTO_BUFFER_EXCEEDED错误码关闭连接。 - -握手完成后,如果终端无法缓冲CRYPTO帧中的数据,它可以(**MAY**)丢弃该CRYPTO帧和后续的CRYPTO帧,或者它也可用CRYPTO_BUFFER_EXCEEDED错误码关闭连接。包含被丢弃的CRYPTO帧的数据包必须(**MUST**)被确认,因为即使CRYPTO帧被丢弃,其他数据包也可以(**MAY**)被接收和处理。 - -# 8. 地址验证(Address Validation) - -地址验证功能确保终端不能用于流量放大攻击。在这种攻击中,攻击者发送数据包到服务端,其中包含指向受害者的欺骗性源地址信息。如果服务端生成更多或更大的数据包来响应,则会放出比攻击者本身更大的流量来攻击受害者。 - -针对放大攻击的主要防御措施,是验证对端是否可在其声明的传输地址上接收数据包。因此,在从尚未验证的地址收到数据包后,终端必须(**MUST**)将发往此地址的流量限制为不超过其接收的三倍。这种对响应大小的限制称为反放大限制。 - -地址验证在连接建立期间(参见第8.1节)和连接迁移期间(参见第8.2节)执行。 - -## 8.1. 连接建立期间的地址验证( Address Validation during Connection Establishment) - -连接建立隐式地为两端提供地址验证。特别的,收到用Initial包中通告的握手密钥保护的数据包,可以确认对端成功处理了该Initial包。一旦终端成功处理了来自对端的Handshake包,它就可以认为对端地址已经过验证。 - -此外,如果对端使用本端指定的CID并且CID包含至少64位,则本端可以(**MAY**)考虑对端地址已经验证。 - -对于客户端,其第一个Initial包中的DCID字段的值,使得它可以将验证服务端地址,作为成功处理任何数据包的一部分。因为来自服务端的Initial包使用从该值派生的密钥进行保护(参见[QUIC-TLS]第5.2节)。 - -initial_secret = HKDF-Extract(initial_salt, client_dst_connection_id) -或者,该值可由服务端在Version Negotiation包中回显(第6章),或包含在Retry包中的完整性标签中(参见[QUIC-TLS]第5.8节)。 - -在验证客户端地址之前,服务端发送的字节数不得(**MUST NOT**)超过它们接收到的字节数的三倍。这限制了可以使用欺骗源地址进行的任何放大攻击的规模。为了在地址验证之前避免放大,服务端必须(**MUST**)计算数据报中收到的所有负载字节,这些字节是唯一归属于单个连接的。其总量包含成功处理的数据包的数据报文和全部丢弃的数据包的数据报文。 - -客户端必须(**MUST**)确保包含Initial包的UDP报文具有至少1200字节的UDP负载,并根据需要添加PADDING帧。这样做使得服务端在完成地址验证之前可发送更多数据。 - -如果服务端回应的Initial或Handshake包丢失,但客户端已收到其发送的所有数据的确认,如果此时服务端达到其反放大限制时,则可能会发生死锁。在这种情况下,客户端没有理由发送额外的数据包,服务端无法发送更多数据,因为它没有验证客户端的地址。为了防止这种死锁,客户端必须(**MUST**)在探测超时(PTO)时发送一个数据包,参见[QUIC-RECOVERY]第6.2节。具体来说,如果客户端没有握手密钥,则客户端必须(**MUST**)在包含至少1200字节的UDP报文中发送Initial包,或者发送Handshake包。 - -服务端可能希望在开始加密握手之前验证客户端地址,客户端会在Initial包中携带令牌来提供地址验证。此令牌是由服务端在连接建立期间发送的Retry包(参见第8.1.2小节)携带,或者客户端可以使用之前连接中服务端用NEW_TOKEN(参见第8.1.3小节)帧通告的令牌。 - -服务端除了在地址验证之前的发送限制之外,还有拥塞控制器限制它可以发送的流量。客户端仅受拥塞控制器的约束。 - -### 8.1.1. 令牌生成(Token Construction) - -服务端在NEW_TOKEN帧或Retry包中发送的令牌是在相同的字段中填写的,但需要进行不同的处理,令牌生成时必须(**MUST**)考虑能区分是以那种方式提供给客户端的。 - -### 8.1.2. 使用Retry包进行地址验证(Address Validation Using Retry Packets) - -收到客户端的Initial包后,服务端可以通过发送包含令牌的Retry包(第17.2.5小节)来请求地址验证。客户端收到Retry包后,在该连接上随后发送的所有Initial包中都必须(**MUST**)携带此令牌。 - -收到客户端的Initial包后,如果其中携带了包含在Retry包中提供的令牌,服务端不能发送另一个Retry包,它只能拒绝连接或允许连接继续。 - -攻击者不可能为自己的地址生成有效的令牌(参见第8.1.4小节)而客户端能够返回该令牌,就可向服务端证明其已经收到该令牌。 - -服务端也可以使用Retry包来推迟连接建立的状态维护和处理成本。QUIC要求服务端提供不同的CID,以及第18.2节中定义的original_destination_connection_id传输参数,可以强制服务端证明它或它合作的实体已经从客户端接收到初始Initial包。提供不同的CID还使得服务端可以对后续数据包的路由方式进行一些控制,例如可用于将连接路由到不同的服务端实例。 - -如果服务端收到携带无效Retry令牌,但其他字段都有效的Initial包,因为知道客户端不会再接受另一个Retry令牌,服务端可以丢弃这样的数据包并让客户端超时以检测到握手失败,但这可能会对客户端造成明显的延迟损失。因此,服务端应该(**SHOULD**)立即以错误码INVALID_TOKEN关闭连接(参见第10.2节)。请注意,此时服务端尚未为连接建立任何状态,因此不会进入关闭周期。 - -图9展示了使用Retry包的流程。 - -``` - Client Server - - Initial[0]: CRYPTO[CH] -> - - <- Retry+Token - - Initial+Token[1]: CRYPTO[CH] -> - - Initial[0]: CRYPTO[SH] ACK[1] - Handshake[0]: CRYPTO[EE, CERT, CV, FIN] - <- 1-RTT[0]: STREAM[1, "..."] - -``` -图9: Example Handshake with Retry - -### 8.1.3. 后续连接的地址验证(Address Validation for Future Connections) - -服务端可以(**MAY**)在连接期间为客户端提供地址验证令牌,方便其将此令牌用于后续连接。地址验证对于0-RTT尤为重要,因为服务端可能会向客户端发送大量数据以响应0-RTT数据。 - -服务端使用NEW_TOKEN帧(第19.7节)为客户端提供可用于验证后续连接的地址验证令牌。在后续的连接中,客户端在Initial包中携带此令牌以提供地址验证。客户端必须(**MUST**)在它发送的所有Initial包中携带令牌,除非服务端通过Retry包用更新的令牌替换旧令牌。客户端不得(**MUST NOT**)将在Retry包中提供的令牌用于未来的连接。服务端可以(**MAY**)丢弃任何不携带预期令牌的Initial包。 - -Retry包提供的令牌可立即使用,NEW_TOKEN帧中携带的令牌要在一段时间之后才使用。因此,令牌应该(**SHOULD**)有一个截止时间,它可以是一个明确的截止时间,也可以是一个时间戳但可用于动态计算截止时间。服务端可以将截止时间直接或以加密形式保存在令牌中。 - -使用NEW_TOKEN发布的令牌不得(**MUST NOT**)包含能够让观察者将值关联到发出它的连接上的信息。例如,它不能包含先前的CID或寻址信息,除非这些值已加密。服务端必须(**MUST**)确保它发送的每个NEW_TOKEN帧在所有客户端中都是唯一的,不过因为丢包而重传的NEW_TOKEN帧除外。使得服务端能够区分令牌是来自Retry还是NEW_TOKEN的信息可以(**MAY**)被服务端以外的实体访问。 - -两个不同连接上的客户端端口号不太可能相同,因此验证端口不大可行。 - -在NEW_TOKEN帧中发布的令牌,可以让服务端判定该连接是否可信(例如,证书中包含服务端名称)。当客户端连接到一个其保存了可用而未用的令牌的服务端时,它应该(**SHOULD**)在其Initial包的Token字段中填入该令牌。携带令牌使得服务端可以及时验证客户端地址,而无需额外的环回时间。客户端不得(**MUST NOT**)携带不适用于它所连接的服务端的令牌,除非客户端明确知道发出令牌的服务端和客户端正连接的服务端正在共同管理此令牌。客户端可以(**MAY**)使用以前连接到该服务端的任何令牌。 - -令牌使得服务端可以将发出令牌的连接与任何使用它的连接之间的活动关联起来。客户端想要打破与服务端的连接一致性的话,可以丢弃NEW_TOKEN帧提供的令牌。相比之下,在Retry包中获得的令牌必须(**MUST**)在连接尝试期间立即使用,不能在后续连接尝试中使用。 - -客户端不应该(**SHOULD NOT**)在不同的连接尝试中重用来自NEW_TOKEN帧的令牌。在不同连接中重用令牌的话,可能被网络路径上的其他实体关联,参见第9.5节。 - -客户端可能会在单个连接上收到多个令牌。除了避免被关联之外,这些令牌都可以用于连接尝试。服务端可以发送额外的令牌以在多次连接尝试中启动地址验证,或者替换可能变得无效的旧令牌。对于客户端,这种模糊性意味着发送最新未使用的令牌是最有可能是有效的,虽然保存和使用旧令牌没有负面影响,但客户端可以认为旧令牌对服务端进行地址验证的用处不大。 - -当服务端收到带有地址验证令牌的Initial包时,它必须(**MUST**)尝试验证令牌,除非它已经完成了地址验证。如果令牌无效,那么服务端应该(**SHOULD**)像地址未经过验证一样处理,比如可能发送一个Retry包。NEW_TOKEN帧和Retry包提供的令牌可以被服务端区分(参见第8.1.1小节),后者需要严格地验证。如果验证成功,服务端应该(**SHOULD**)允许握手继续进行。 - -> 注意:将客户端视为未验证而不是丢弃数据包的基本原理是:客户端可能已经在使用先前连接中NEW_TOKEN帧收到的令牌,并且如果服务端丢失状态,它可能无法验证令牌,如果丢弃该数据包可能会导致连接失败。 - -在无状态设计中,服务端可以使用加密和经过身份验证的令牌将信息传递给客户端,服务端在未来连接中可以从中恢复有效信息并用于验证客户端地址。令牌未集成到加密握手中,就不会对其进行身份验证。客户端可能重用令牌,为了避免针对此属性的攻击,服务端可以将令牌限制为仅用于客户端地址验证。 - -客户端可以(**MAY**)使用在连接上获得的令牌进行后续任何使用相同版本的连接尝试。在选择要使用的令牌时,客户端不需要考虑正在尝试的连接的其他属性,包括可能的应用层协议、会话凭证或其他连接属性的选择。 - -### 8.1.4. 地址验证的令牌完整性考虑(Address Validation Token Integrity) -地址验证令牌必须(**MUST**)难以猜测。在令牌中包含一个至少128位的随机值就足够了,但这取决于服务端是否记住它发送给客户端的值。 - -基于令牌的方案允许服务端将与验证相关的任何状态维护转嫁给客户端。为了使此设计起作用,令牌必须(**MUST**)受到完整性保护以防止客户端修改或伪造。如果没有完整性保护,恶意客户端可能会生成或猜测服务端可接受的令牌值。只有服务端需要访问令牌的完整性保护密钥。 - -令牌不需要单一的明确定义的格式,因为生成和使用令牌的都是服务端。在Retry包中发送的令牌应该(**SHOULD**)包含允许服务端验证客户端数据包中的源IP地址和端口是否保持不变的信息。 - -在NEW_TOKEN帧携带的令牌必须(**MUST**)包含让服务端验证客户端IP地址从令牌发出时起是否更改的信息。服务端也可以通过NEW_TOKEN帧中的令牌来决定是否发送Retry包,即使客户端地址已更改。如果客户端IP地址发生变化,服务端必须(**MUST**)遵守反放大限制,详情参阅第8章。请注意,在存在NAT的情况下,此要求可能不足以保护共享NAT的其他主机免受放大攻击。 - -攻击者可以重放令牌以将服务端用作DDoS攻击中的放大器。为了防止此类攻击,服务端必须(**MUST**)确保阻止或限制令牌的重放。服务端应该(**SHOULD**)确保在Retry包中发送的令牌仅在短时间内被接受,因为它们会被客户端立即返回。NEW_TOKEN帧(第19.7节)中提供的令牌需要更长的生效时间,但不应该(**SHOULD NOT**)重复接受。如果可能,鼓励服务端只认可一次令牌。令牌也可以(**MAY**)包含有关客户端的附加信息,以进一步缩小适用性或重用性。 - -## 8.2. 路径验证(Path Validation) - -在连接迁移期间,两端都使用路径验证(参见第9章)来验证地址变更后的可达性。在路径验证中,终端测试特定本地地址与特定对端地址之间的可达性,其中地址是IP地址和端口的二元组。 - -路径验证测试在路径上发往对端的数据包是否可被对端接收。路径验证用于确保从迁移中的对端收到的数据包不携带欺骗性源地址。 - -路径验证不验证对端是否可以在返回方向发包。不能使用确认包用于返回路径验证,因为它们包含的信息不足并且可能被欺骗。终端独立确定路径每个方向的可达性,因此返回方向可达性只能由对端确认。 - -任一终端均可随时启用路径验证。例如,一个终端可能会检查对端在静默一段时间后是否仍然保有其源地址。 - -路径验证并非设计为NAT穿越机制。尽管此处描述的机制对于创建支持NAT穿越的NAT绑定可能有效,但我们期待的是一个终端能够接收数据包,而无需先在该路径上发送数据包。有效的NAT穿越需要额外的同步机制,这里没有涉及。 - -在进行路径验证时,终端可以(**MAY**)在发送PATH_CHALLENGE或PATH_RESPONSE帧时捎带其他类型的帧。例如终端可以在发送PATH_CHALLENGE时捎带PADDING帧用于路径最大传输单元发现 (PMTUD),参见见第14.2.1小节,终端在发送PATH_RESPONSE响应时也可以捎带它自己的PATH_CHALLENGE帧。 - -终端在从新的本地地址发送探测包时需要使用新的CID,参见第9.5节。在探测新路径时,终端需要确保其对端具有可用于响应的未使用的CID。如果active_connection_id_limit允许,在同一个数据包中发送NEW_CONNECTION_ID和PATH_CHALLENGE帧,可确保对端在发送响应时有未使用的CID。 - -终端可以选择同时探测多条路径。用于同时探测的路径的数量受其对端先前提供的额外CID数量的限制,因为用于探测的每个新本地地址都需要一个以前未使用的CID。 - -### 8.2.1. 启动路径验证(Initiating Path Validation) - -为了启动路径验证,终端需发送一个PATH_CHALLENGE帧,其中包含不可预测的要在路径上验证的负载。 - -终端可以(**MAY**)发送多个PATH_CHALLENGE帧以防止数据包丢失。不过,终端不应该(**SHOULD NOT**)在单个数据包中携带多个PATH_CHALLENGE帧。 - -终端不应该(**SHOULD NOT**)以超过Initial包的频率发送包含PATH_CHALLENGE帧的数据包来探测新路径。这确保了在新路径上的连接迁移不会比建立新连接更多。 - -终端必须(**MUST**)在每个PATH_CHALLENGE帧中使用不同的不可预测的数据,以便它可以将对端的PATH_RESPONSE响应与相应的PATH_CHALLENGE帧相关联。 - -终端必须(**MUST**)将包含PATH_CHALLENGE帧的UDP报文扩展到至少1200字节大小,除非路径的反放大限制不允许发送此大小的数据报。发送这种大小的UDP报文可以确保从本端到对端的网络路径可以用于QUIC连接,参见第14章。 - -当终端由于反放大限制而无法将数据报大小扩展到1200字节时,将不能验证路径MTU。为确保路径MTU足够大,在成功接收到PATH_RESPONSE之后,或在路径上接收到足够多的字节以发送更大的数据报而不会导致超出反放大限制后,终端必须(**MUST**)通过发送包含PATH_CHALLENGE帧的至少1200字节的UDP报文来执行第二次路径验证。 - -与扩展数据报文的其他情况不同,终端不得(**MUST NOT**)丢弃包含PATH_CHALLENGE或PATH_RESPONSE帧的较小的UDP报文。 - -### 8.2.2. 路径验证响应(Path Validation Responses) - -在接收到PATH_CHALLENGE帧时,终端必须(**MUST**)通过在PATH_RESPONSE帧中回显包含在PATH_CHALLENGE帧中的数据来响应。除非受到拥塞控制的约束,否则终端不得延迟包含PATH_RESPONSE帧的数据包的发送。 - -PATH_RESPONSE帧必须(**MUST**)在收到PATH_CHALLENGE帧的网络路径上发送。这确保只有当路径在两个方向都有效时,对端的路径验证才会成功。发起路径验证的终端不得(**MUST NOT**)强制执行此要求,因为这会导致对迁移的攻击,参见第9.3.3小节。 - -终端必须(**MUST**)将包含PATH_RESPONSE帧的数据报文扩展到至少1200字节的最小允许最大数据报大小。这可验证该路径是否能够在两个方向上都携带这种大小的数据报文。但是,如果最终待发数据报文大小超过反放大限制,终端不得(**MUST NOT**)扩展包含PATH_RESPONSE的数据报文。不过这种情况只有在其收到的PATH_CHALLENGE帧也未扩展时才有可能。(反放大机制,如果包含PATH_CHALLENGE的数据报文有1200,那么响应报文可以到1200,不超过3600即可) - -终端不得(**MUST NOT**)发送多个PATH_RESPONSE帧以响应一个PATH_CHALLENGE帧,参见第13.3节。终端应根据需要发送更多PATH_CHALLENGE帧以唤起更多的PATH_RESPONSE帧。 - -### 8.2.3. 路径验证成功(Successful Path Validation) - -当收到与先前PATH_CHALLENGE帧中携带的数据相同的PATH_RESPONSE帧时,路径验证成功。在任何网络路径上收到的PATH_RESPONSE帧都能验证发送PATH_CHALLENGE的路径可达性。 - -如果终端在未扩展到至少1200字节的数据报文中发送PATH_CHALLENGE帧,并且PATH_RESPONSE响应可以验证其地址,则表示验证路径通过,但不表示验证了路径MTU。因此,终端现在可以发送已接收数据量三倍的数据。然后,终端必须(**MUST**)用扩展的数据报发起另一次路径验证,以验证该路径是否支持所需的MTU。 - -收到对包含PATH_CHALLENGE帧的数据包的ACK确认不是充分的验证,因为该确认可能被恶意第三方欺骗。 - -### 8.2.4. 路径验证失败(Failed Path Validation) - -仅当尝试路径验证的终端放弃其路径验证的尝试时,路径验证才算失败。 - -终端应该(**SHOULD**)基于定时器来决定是否放弃路径验证。设置此计时器时,实现要注意新路径的往返时间可能比初始路径更长。建议(**RECOMMENDED**)使用当前PTO或新路径的PTO(使用kInitialRtt,如[QUIC-RECOVERY]中定义)中较大值的三倍。 - -在路径验证失败之前,会等待多个PTO,因此单个PATH_CHALLENGE或PATH_RESPONSE帧的丢失才不会导致路径验证失败。 - -注意到终端可能会在新路径上接收包含其他帧的数据包,但路径验证需要收到携带满足条件的数据的PATH_RESPONSE帧才算成功。 - -当终端放弃路径验证时,它已经判定该路径不可用。这并不一定意味着连接失败——终端可以根据需要继续通过其他路径发送数据包。如果没有可用路径,终端可以等待新路径可用或关闭连接。没有到达对端的有效网络路径的终端可以(**MAY**)发出NO_VIABLE_PATH连接错误信号,注意这只有在网络路径存在但不支持所需的MTU时才有可能发生(第14章)。 - -也有可能因为失败之外的其他原因而放弃路径验证。一般在旧路径上的路径验证正在进行的同时启动了到新路径的连接迁移,就会发生这种情况。 - -# 9. 连接迁移(Connection Migration) - -CID的使用使得连接在终端地址(IP地址和端口)的变更中存续下来,如由终端迁移到新网络引起的变更。本节介绍终端迁移到新地址的过程。 - -QUIC的设计依赖于终端在握手期间保持地址稳定。在握手完成确认之前,终端不得(**MUST NOT**)发起连接迁移,如[QUIC-TLS]第4.1.2小节所定义。 - -如果终端设置了disable_active_migration传输参数,在握手期间,终端也不得(**MUST NOT**)使用不同的本地地址向对端发送数据包(包括探测数据包,参见第9.1节),除非终端是按preferred_address传输参数进行回应的。如果对端违反此要求,本端必须(**MUST**)要么丢弃该路径上的传入数据包,不生成Stateless Reset包,要么继续进行路径验证并允许对端迁移。生成Stateless Reset包或启动连接关闭都将使得网络中的第三方可以通过欺骗的方式关闭连接或以其他方式操纵观察到的流量。 - -并非所有对端地址的变更都是有意或主动的迁移。对端可能会遇到NAT重绑定:由于中间节点(通常是NAT)为连接分配新的传出端口或是新的传出IP地址而导致的地址变更。如果终端检测到对端地址的任何变更,终端必须(**MUST**)执行路径验证(第8.2节),除非它先前已验证该地址。 - -当终端从没有经过验证的路径发送数据包时,它可以(**MAY**)丢失连接状态。需要进行连接迁移的终端可以(**MAY**)在丢失连接状态之前等待新路径变得可用。 - -本文限制服务端将连接迁移到新的客户端地址,除非是如第9.6节中所述的场景。客户端负责启动所有迁移。在接收到来自客户端地址的非探测数据包之前,服务端不会主动向该地址发送非探测数据包(参见第9.1节)。如果客户端收到来自未知服务端地址的数据包,其必须(**MUST**)丢弃这些数据包。 - -## 9.1. 探测新路径(Probing a New Path) - -在将连接迁移到新的本地地址之前,终端可以(**MAY**)使用路径验证(第8.2节)从新的本地地址探测对端可达性。路径验证失败仅意味着新路径不可用于此连接。除非没有可用的有效替代路径,否则路径验证失败不会导致连接结束。 - -PATH_CHALLENGE、PATH_RESPONSE、NEW_CONNECTION_ID和PADDING帧是“探测帧”,所有其他帧都是“非探测帧”。仅包含探测帧的数据包是“探测数据包”,包含任何其他帧的数据包是“非探测数据包”。 - -## 9.2. 启动连接迁移(Initiating Connection Migration) - -终端可以通过从新地址发送包含非探测帧的数据包,将连接迁移到这个新的本地地址。 - -每个终端在连接建立期间会验证其对端的地址。因此,待迁移的终端是知道对端愿意在其当前地址上接收报文的,因此,终端可以迁移到新的本地地址,而无需先验证对端的地址。 - -为了在新路径上确认可达性,终端需要在新路径上启动路径验证(第8.2节)。终端可以(**MAY**)推迟路径验证,直到对端发送下一个非探测帧到其新地址。 - -迁移时,新路径可能不支持终端的当前发送速率。因此,终端需要重置其拥塞控制器和RTT估计,如第9.4节所述。 - -新路径可能不具有相同的ECN功能。因此,终端需要验证ECN功能,如第13.4节所述。 - -## 9.3. 响应连接迁移(Responding to Connection Migration) - -接收到对端从新地址发来的包含非探测帧的数据包表明对端已迁移到该地址。 - -如果接收方允许迁移,它必须(**MUST**)将后续数据包发往新的对端地址,并且必须(**MUST**)启动路径验证(第8.2节)以验证对端对该地址的所有权(如果验证尚未进行)。如果接收方没有来自对端的未使用CID,则在对端提供之前,它将无法在新路径上发送任何数据,参见第9.5节。 - -终端仅在回应最高包号的非探测数据包时变更其目的地址,这可确保终端在收到重新排序的数据包时不会将数据包投递到旧的对端地址。 - -终端可以(**MAY**)将数据发往未经验证的对端地址,但它必须(**MUST**)防止潜在的攻击,如第9.3.1和9.3.2小节所述。如果该地址最近可见,则终端可以(**MAY**)跳过对端地址的验证。特别是,如果终端在检测到某种形式的虚假迁移后迁回到先前经过验证的路径,则跳过地址验证并恢复丢包检测和拥塞状态可以降低攻击对性能的影响。 - -在变更其发送非探测数据包的地址后,终端可以放弃对其他地址的路径验证。 - -从新的对端地址接收数据包可能是对端NAT重绑定的结果。 - -在验证新的客户端地址后,服务端应该(**SHOULD**)向客户端发送新的地址验证令牌(第8章)。 - -### 9.3.1. 对端地址欺骗(Peer Address Spoofing) - -第三方可能会填写假的源地址指向受害方,从而导致终端向受害方发送过多的数据。如果终端发送的数据明显多于第三方,则可能导致连接迁移会放大攻击者可以向受害者生成的数据量。 - -如第9.3节所述,终端需要验证对端的新地址以确认对端对新地址的所有权。在对端的地址被认为有效之前,终端会限制它发往该地址的数据量,参见第8章。如果没有此限制,终端有可能被用于对毫无戒心的受害者进行拒绝服务攻击。 - -如果终端如上所述跳过对端地址的验证,则不需要限制其发送速率。 - -### 9.3.2. On-Path地址欺骗(On-Path Address Spoofing) - -on-path攻击者可以通过复制和转发具有虚假源地址的数据包使其在原数据包之前到达,带有虚假源地址的数据包将被视为来自迁移连接,从而导致受害端向虚假地址的连接迁移,而原数据包将被视为重复数据包并被丢弃。在连接迁移之后,地址验证将失败,因为虚假源地址的实体没有必要的加密密钥来读取或响应发送给它的PATH_CHALLENGE帧,即使它想发也不能。 - -为防止连接因此类虚假迁移而失败,当新对端地址的验证失败时,终端必须(**MUST**)迁回最后验证过的对端地址。此外,从合法对端地址接收到具有更大数据包序号的数据包将触发另一次连接迁移,这将导致对虚假迁移地址的验证被放弃,这使得攻击者在迁移中仅注入了一个数据包。 - -如果终端没有保存最后验证的对端地址的状态,它必须(**MUST**)放弃所有连接状态并以静默方式关闭连接。这导致连接上的新数据包按照一般方式处理,例如,终端可以(**MAY**)发送Stateless Reset包以响应任何后续传入的数据包。 - -### 9.3.3. Off-Path包转发(Off-Path Packet Forwarding) - -能够观察到数据包的非路径(off-path)攻击者可能会将真实数据包的副本转发到终端。如果复制的数据包在真正的数据包之前到达,这将被识别为NAT重绑定。后到的真实的数据包都将被作为副本丢弃。如果攻击者能够继续转发数据包,则可能会导致终端迁移到经过攻击者的某条路径。这将攻击者置于路径上,使其能够观察或丢弃所有后续数据包。 - -这种类型的攻击依赖于攻击者使用与两终端之间的直接路径具有大致相同特征的路径。如果发送的数据包相对较少,或者数据包丢失与攻击尝试同时发生,则更可能被攻击。 - -在初始路径上接收到具有更大的数据包号的非探测包(non-probing packets)将导致终端移回合法的路径。在此路径上发送触发包会增加攻击失败的可能性。因此,减轻这种攻击依赖于触发包的交换。 - -为了回迁到原来的路径,终端必须(**MUST**)使用PATH_CHALLENGE帧验证之前的活动路径。这会导致在该路径上发送新的探测包。如果路径不再可行(viable),验证尝试将超时并失败;如果路径可行(viable)但不再需要,则验证将成功,但只会在路径上发送探测包(probing packets)。 - -在活动路径上接收到PATH_CHALLENGE的终端应该(**SHOULD**)发送非探测数据包作为响应。如果非探测数据包(non-probing packet)在攻击者生成的任何副本之前到达,则会导致连接迁移回初始路径。任何后续迁移到其他路径都会重新启动整个过程。 - -这种防御是不完善的,但这并不是一个严重的问题。尽管多次尝试使用初始路径,但通过攻击者的路径确实比初始路径快,则无法区分攻击和路由改进。 - -终端还可以使用启发式探索法(heuristics)来提高对这种类型攻击的检测。例如,如果最近在旧路径上接收到数据包,则不大可能是NAT重绑定,同样,在IPv6路径上很少进行重绑定。终端也可以查找重复的数据包。相反,CID的更改更可能表示有意的(intentional)迁移,而不是攻击。 - -## 9.4. 丢包检测和拥塞控制(Loss Detection and Congestion Control) - -新路径上的可用容量可能与旧路径不同。在旧路径上发送的数据包不得(**MUST NOT**)参与新路径的拥塞控制或RTT估计。 - -在确认对端对其新地址的所有权后,终端必须(**MUST**)立即将新路径的拥塞控制器和往返时间估计器重置为初始值(参见[QUIC-RECOVERY]的附录A.3或B.3),除非对端地址唯一变化的是其端口号。由于仅端口变更通常是NAT重绑定或其他中间设备活动的结果,因此在这些情况下,终端可以(**MAY**)保留其拥塞控制状态和RTT估计,而不是恢复到初始值。如果将旧路径保留的拥塞控制状态用于具有显著不同特性的新路径,发送方可能会过于激进地传输,直到拥塞控制器和RTT估计器适应为止。通常,建议实现在新路径上使用历史数据时要谨慎。 - -当终端在迁移期间从/向多个地址发送数据和探测包时,接收端处可能会出现明显的乱序,因为不同路径可能具有不同的RTT。接收方仍将发送覆盖所有接收到的数据包的ACK帧。 - -尽管在连接迁移期间可能使用多条路径,但只需要维护一个单独的拥塞控制上下文和丢包恢复上下文(如[QUIC-RECOVERY]中所述)可能就足够了。例如,终端可能会延迟切换到新的拥塞控制上下文,直到确认不再需要旧路径(例如第9.3.3小节中描述的情况)。 - -发送方可以对探测数据包进行单独处理,以保证它们的丢包检测是独立的,不会导致拥塞控制器过度降低其发送速率。当发送PATH_CHALLENGE时,终端可能会设置一个单独的定时器,如果收到相应的PATH_RESPONSE,则停止该定时器,如果定时器在收到PATH_RESPONSE之前到期,终端可能会发送一个新的PATH_CHALLENGE帧并启动一个更长的定时器。这个定时器应该(**SHOULD**)按照[QUIC-RECOVERY]第6.2.1小节的描述设置,并且不得(**MUST NOT**)更激进。 - -## 9.5. 连接迁移对隐私的影响(Privacy Implications of Connection Migration) - -在多条网络路径上使用稳定的CID将使得被动观察者可以关联这些路径之间的活动。需要迁移网络的终端可能不希望它们的活动被除对端以外的任何实体关联,因此从不同的本地地址发送时会使用不同的CID,如第5.1节所述。为了有效实现这一点,终端需要确保它们提供的CID不能被任何其他实体关联起来。 - -在任何时候,终端都可以(**MAY**)将它们填写的DCID变更为尚未在另一条路径上使用的值。 - -当从多个本地地址发送数据时,终端不得(**MUST NOT**)重用SCID——例如在第9.2节中描述的启动连接迁移或在第9.1节中描述的探测新的网络路径时。 - -类似地,当发往多个目的地址时,终端也不得(**MUST NOT**)重用DCID。由于网络变更不受其对端控制,终端可能会收到具有新的源地址但与旧地址有相同DCID字段的数据包,在这种情况下,终端如果从同一个本地地址发往新地址的话,可以(**MAY**)继续使用当前CID。 - -这些关于CID重用的要求仅适用于数据包的发送,因为在不改变CID的情况下无意识变更路径是可能的,例如,经过一段时间的网络静默之后,NAT重绑定可能会导致在客户端恢复发包时在新路径上发送数据包。终端如何响应此类事件参见第9.3节。 - -在每条新网络路径上的双向数据包中启用不同的CID,可以消除将同一连接的不同路径关联起来的可能性。包头保护确保不能用数据包序号关联活动,但不能阻止使用数据包的其他属性(例如时间和大小)来关联活动。 - -终端不应该(**SHOULD NOT**)向要求零长度CID的对端发起迁移,因为新路径上的流量可能很容易关联到旧路径上的流量。如果服务端能够将具有零长度CID的数据包关联到正确的连接,则意味着服务端正在使用其他信息来解复用并关联数据包。例如,服务端可能会为每个客户端提供一个唯一的地址——例如,使用HTTP替代服务[ALTSVC]。使得跨多个网络路径也能正确路由数据包的信息,但同时也可能使得这些路径上的活动被除对端以外的其他实体关联。 - -在一段时间静默之后再发送流量时,客户端可能希望通过切换到新的CID、源UDP端口或IP地址(参见[RFC8981])来降低可关联性。变更数据包的源地址也可能会导致服务端检测到连接迁移。这确保即使对于没有经历NAT重绑定或真正迁移的客户端,也可以使用支持迁移的机制。变更地址会导致对端重置其拥塞控制状态(请参阅第9.4节),因此地址应该(**SHOULD**)不要频繁变更。 - -耗尽可用CID的终端无法再继续探测新路径或启动迁移,也无法响应其对端的探测或迁移尝试。为了确保迁移可行,并且在不同路径上发送的数据包不能被相互关联,终端应该(**SHOULD**)在对端体迁移之前提供新的CID,参见第5.1.1小节。如果判断对端可能已经用尽了可用的CID,则待迁移终端可能需要在新网络路径上发送的所有数据包中都包含NEW_CONNECTION_ID帧。 - -## 9.6. 服务端的首选地址(Server's Preferred Address) - -QUIC允许多个服务端接受同一个IP地址上的入向连接,可以在握手后不久尝试将连接转到某个服务端提供的其他首选地址。这对客户端接入时连接到多个服务端共享的地址,之后再使用单播地址以确保连接稳定性时特别有用。本节介绍将连接迁移到首选服务端地址的协议内容。 - -本文中指定的QUIC版本不支持在连接中将连接迁移到新的服务端地址。如果客户端在未启动迁移到该地址时收到来自新服务端地址的数据包,则客户端应该(**SHOULD**)丢弃这些数据包。 - -### 9.6.1. 传输首选地址(Communicating a Preferred Address) - -服务端通过在TLS握手中包含preferred_address传输参数来告知首选地址。 - -服务端可以(**MAY**)告知两个地址族(IPv4和IPv6)的首选地址,以允许客户端选择最适合其网络连接的地址。 - -一旦握手确认完成,客户端应该(**SHOULD**)选择服务端提供的两个地址之一并启动路径验证(见第8.2节)。客户端使用任何以前未使用的活动CID构造数据包,这些ID取自preferred_address传输参数或NEW_CONNECTION_ID帧。 - -一旦路径验证成功,客户端应该(**SHOULD**)开始使用新的CID发送后续数据包到新的服务端地址,并停止使用旧的服务端地址。如果路径验证失败,客户端必须(**MUST**)将后续数据包发往服务端的初始IP地址。 - -### 9.6.2. 迁移到首选地址(Migration to a Preferred Address) - -迁移到首选地址的客户端必须(**MUST**)在迁移前验证它选择的地址,参见第21.5.3小节。 - -服务端在接受连接后的任何时候都可能收到寻址到其首选IP地址的数据包。如果此数据包包含PATH_CHALLENGE帧,则服务端会如第8.2节所述回复包含PATH_RESPONSE帧的数据包。服务端必须(**MUST**)从其初始地址发送非探测数据包,直到它在其首选地址从客户端接收到非探测数据包,直到服务端验证了该新路径。 - -服务端必须(**MUST**)探测从其首选地址到客户端的路径。这有助于防止攻击者发起的虚假迁移。 - -一旦服务端完成其路径验证并在其首选地址上接收到具有新的最大数据包序号的非探测数据包,服务端就开始仅从其首选IP地址向客户端发送非探测数据包。服务端应该(**SHOULD**)丢弃在旧IP地址上接收到的此连接的新数据包,但可以(**MAY**)继续处理在旧IP地址上接收到的延迟数据包。 - -服务端在preferred_address传输参数中提供的地址仅对提供它们的连接有效。客户端不得(**MUST NOT**)将这些用于其他连接,包括从当前连接恢复的连接。 - -### 9.6.3. 客户端迁移与首选地址的交互(Interaction of Client Migration and Preferred Address) - -客户端可能需要在迁移到服务端的首选地址之前执行连接迁移。在这种情况下,客户端应该(**SHOULD**)同时执行从客户端新地址到初始和首选服务端地址的路径验证。 - -如果服务端首选地址的路径验证成功,则客户端必须(**MUST**)放弃对服务端初始地址的验证并迁移到服务端的首选地址。如果服务端首选地址的路径验证失败但服务端初始地址的验证成功,则客户端可以(**MAY**)切换到客户端新地址并继续发包到服务端的初始地址。 - -如果在服务端首选地址收到的数据包与握手期间从客户端观察到的源地址不同,则服务端必须(**MUST**)防止潜在的攻击,如第9.3.1和第9.3.2小节所述。除了有意的同时迁移之外,这也可能是因为客户端的访问网络对服务端的首选地址使用了不同的NAT绑定。 - -服务端应该(**SHOULD**)在收到来自不同地址的探测包时启动到客户端新地址的路径验证,参见第8章。 - -迁移到新地址的客户端应该(**SHOULD**)为服务端使用来自相同地址族的首选地址。 - -preferred_address传输参数中提供的CID不特定于所提供的地址。提供此CID是为了确保客户端具有可用于迁移的CID,但客户端可以(**MAY**)在任何路径上使用此CID。 - -## 9.7. IPv6流标签的使用和迁移(Use of IPv6 Flow Label and Migration) - -使用IPv6发送数据的终端应该(**SHOULD**)应用符合[RFC6437]的IPv6流标签,除非本地API不允许设置IPv6流标签。 - -流标签生成方式,必须(**MUST**)尽量降低与先前使用的流标签关联的可能,因为稳定的流标签将能够关联多个路径上的活动,参见第9.5节。 - -[RFC6437] 建议使用伪随机数函数来生成流标签。一种可能的实现是用加密散列函数生成流标签,散列函数的参数有本地秘钥、源地址和目的地址、DCID字段,这可确保变更与其他可观察到的标识符的变更同步。 - -# 10. 连接关闭(Connection Termination) - -可以通过以下三种方式之一终止已建立的QUIC连接: - -* 空闲超时(第10.1节) -* 立即关闭(第10.2节) -* 无状态重置(第10.3节) - -如果终端没有可以(**MAY**)发送数据包的经过验证的路径,则它可以丢弃连接状态,参见第8.2节。 - -## 10.1. 空闲超时(Idle Timeout) -如果任一终端在其传输参数(第18.2节)中指定max_idle_timeout,则当连接保持空闲的时间超过两端通告的max_idle_timeout值的最小值时,连接将被静默关闭并丢弃其状态。 - -虽然每个终端都通告一个max_idle_timeout,但终端中真正生效的有效值是两个通告值(或唯一通告值,如果只有一个终端通告非零值)中的最小值。如果终端在有效值超时之前放弃连接,则它应启动立即关闭(第10.2节)。 - -当一个终端收到并成功处理来自其对端的数据包时,终端会重置其空闲定时器。如果自上次接收和处理数据包后没有发送其他确认数据包,终端也会在发送确认数据包时重置其空闲定时器。在发送数据包时重置定时器可确保在有新活动后不会关闭连接。 - -为避免空闲超时时间过短,终端必须(**MUST**)将空闲超时时间增加到至少是当前探测超时(PTO for Probe Timeout)的三倍。这在空闲超时之前可以允许多次探测或丢包。 - -### 10.1.1. 活性测试(Liveness Testing) -临近有效空闲超时时间发送的数据包有可能被对端丢弃,因为在这些数据包到达之前,对端的空闲超时期限可能已经到期。 - -如果对端可能很快超时,例如在PTO内,终端可以发送PING包或另一个ACK触发帧来测试连接的活跃性,参见[QUIC-RECOVERY]第6.2节。这在无法安全地重传可用的应用数据时尤其有用。请注意,哪些数据可以安全重传由应用程序确定。) - -### 10.1.2. 延迟空闲超时(Deferring Idle Timeout) -如果终端正在等待响应数据但没有或无法发送应用数据,则它可能需要发送ACK触发包以避免空闲超时。 - -QUIC的实现可为应用提供延迟空闲超时的选项。当应用希望避免丢失与打开的连接相关联的状态,但不希望在一段时间内交换应用数据时,可以使用此功能。使用此选项,终端可以周期性发送PING帧(第19.2节),这将导致对端重置空闲超时定时器。如果这是本端自收到数据包后发送的第一个ACK触发包,则也会重置此终端的空闲超时定时器。发送PING帧会导致对端回以确认ACK响应,这也会重置对端的空闲超时定时器。 - -使用QUIC的应用层协议应该(**SHOULD**)提供合适的有关何时推迟空闲超时的指导。不必要地发送PING帧可能会对性能产生不利影响。 - -如果超过max_idle_timeout传输参数协商的时间还没有发送或接收数据包,则连接将超时(请参阅第10章)。另外,中间设备的状态也可能会提前超时。尽管[RFC4787]中的REQ-5建议2分钟的超时间隔,但经验表明,每30秒发送一次数据包是必要的,可以防止大多数中间设备丢失UDP流[GATEWAY]的状态。 - -## 10.2. 立即关闭(Immediate Close) -终端发送CONNECTION_CLOSE帧(第19.19节)以立即终止连接。CONNECTION_CLOSE帧会导致所有流立即关闭。可以假定打开的流被隐式重置。 - -发送CONNECTION_CLOSE帧后,终端立即进入“关闭中”(closing) 状态,参见第10.2.1小节。终端收到CONNECTION_CLOSE帧后,进入draining 状态,见第10.2.2小节。 - -违反协议会导致立即关闭。 - -在应用层协议指示关闭连接后,可以使用立即关闭。这可能发生在应用层协议之间协商优雅关闭之后。两个应用终端先交换同意关闭连接所需的消息,然后应用请求QUIC关闭连接。当QUIC因此启动关闭流程时,会向对端发出关闭信号,携带附有应用提供的错误码的CONNECTION_CLOSE帧。 - -closing和draining连接状态的存在是为了确保连接干净利落地关闭,并正确丢弃延迟或乱序的数据包。这些状态应该(**SHOULD**)至少持续三倍于[QUIC-RECOVERY]中定义的当前PTO间隔。 - -在退出closing或draining状态之前清除连接状态,可能会导致终端在接收到迟到的数据包时不必要地生成Stateless Reset包。有一些替代方法来确保迟到的数据包不会触发终端响应措施,例如那些能够关闭UDP套接字的终端,可以(**MAY**)提前结束这些状态以允许更快的资源恢复。保留打开套接字以接受新连接的服务端不应该(**SHOULD NOT**)提前结束closing或draining状态。 - -一旦其closing或draining状态结束,终端应该(**SHOULD**)丢弃所有连接状态。终端可以(**MAY**)发送一个Stateless Reset包来响应这个连接的后续传入数据包。 - -### 10.2.1. 关闭中连接状态(Closing Connection State) -终端在发起立即关闭后进入closing状态。 - -在closing状态下,终端仅保留足够的信息来生成包含CONNECTION_CLOSE帧的数据包并将数据包标识为属于该连接。处于closing状态的终端发送一个包含CONNECTION_CLOSE帧的数据包,以响应该连接上的传入数据包。 - -终端应该(**SHOULD**)限制它在关闭状态下生成数据包的速率。例如,终端可以等待足够数量的数据包或足够的时间之后再响应接收到的数据包。 - -终端选择的CID和QUIC版本足以识别closing态连接的数据包,终端可以(**MAY**)丢弃所有其他连接状态。closing态的终端不需要处理任何接收到的帧。终端可以(**MAY**)为传入的数据包保留数据包保护密钥,在读取时可以用于解密和处理CONNECTION_CLOSE帧。 - -终端可以(**MAY**)在进入closing状态时丢弃数据包保护密钥,并发送包含CONNECTION_CLOSE帧的数据包以响应收到的任何UDP报文。但是,丢弃数据包保护密钥的终端无法识别和丢弃无效数据包。为避免被用于放大攻击,此类终端必须(**MUST**)限制其发送的数据包的总字节数为接收的数据包的三倍。为了最小化终端为关闭连接保持的状态,终端可以发送完全相同的数据包以响应任何接收到的数据包。 - -> 注意:允许重传关闭数据包是对每个数据包均需使用新包号的要求的一个例外,参见第12.3节。发送新的包号主要有利于丢包恢复和拥塞控制,这与关闭连接无关。重传最终数据包需要较少的状态。 - -当处于closing状态时,终端可以从新的源地址接收数据包,可能表示连接迁移,参见第9章。处于closing状态的终端必须(**MUST**)要么丢弃从未验证地址接收的数据包,要么将其发往未验证地址的数据包的总字节数限制为从该地址接收的数据包的三倍。 - -终端在关闭时不应处理密钥更新([QUIC-TLS]第6章)。密钥更新可能会阻止终端从closing状态迁移到draining状态,因为终端将无法处理随后收到的数据包,但也不会产生任何影响。 - -### 10.2.2. 耗尽连接状态(Draining Connection State) -一旦终端接收到CONNECTION_CLOSE帧,即表示其对端正在关闭或正在耗尽,就进入耗尽状态。虽然其他方面与closing状态相同,但处于draining状态的终端不得(**MUST NOT**)发送任何数据包。一旦连接处于draining状态,就不需要保留数据包保护密钥。 - -接收CONNECTION_CLOSE帧的终端可以(**MAY**)在进入draining状态之前发送包含CONNECTION_CLOSE帧的单个数据包,如果合适,使用NO_ERROR代码。终端不得(**MUST NOT**)发送更多数据包。这样做可能会导致CONNECTION_CLOSE帧的不断交换,直到终端之一退出closing状态。 - -如果终端收到CONNECTION_CLOSE帧,则它可以(**MAY**)从closing状态进入draining状态,这表明对端也在closing或draining。在这种情况下,当closing状态将结束时,draining状态结束。换句话说,终端使用相同的结束时间,但停止在此连接上传输任何数据包。 - -### 10.2.3. 握手期间立即关闭(Immediate Close during the Handshake) -发送CONNECTION_CLOSE帧时需要确保对端能够处理该帧。通常,这意味着需要在具有最高数据包保护级别的数据包中发送该帧以避免数据包被丢弃。在握手确认后(参见[QUIC-TLS]第4.1.2小节),终端必须(**MUST**)在1-RTT包中发送CONNECTION_CLOSE帧。但是,在确认握手之前,对端可能无法使用更高级的数据包保护密钥,因此可以(**MAY**)在使用较低数据包保护级别的数据包中发送另一个CONNECTION_CLOSE帧。特别地: - -* 客户端知道服务端是否有握手密钥(见第17.2.2.1小节),但服务端可能不知道客户端是否有握手密钥。在这些情况下,服务端应该(**SHOULD**)在Handshake和Initial包中发送一个CONNECTION_CLOSE帧,以确保其中至少一个可以被客户端处理; - -* 在0-RTT包中发送CONNECTION_CLOSE帧的客户端不能保证服务端可接受0-RTT。在Initial包中发送CONNECTION_CLOSE帧使服务端更有可能收到关闭信号,即使可能不理解应用层错误码。 - -* 在确认握手之前,对端可能无法处理1-RTT包,因此终端应该(**SHOULD**)在握手和1-RTT包中发送CONNECTION_CLOSE帧。服务端还应该(**SHOULD**)在Initial包中发送一个CONNECTION_CLOSE帧。 - -在Initial或Handshake包中发送0x1d类型的CONNECTION_CLOSE可能会暴露应用状态或用于更改应用状态。当在Initial或Handshake包中发送帧时,类型0x1d的CONNECTION_CLOSE必须(**MUST**)替换为类型为0x1c的CONNECTION_CLOSE帧。否则可能会泄露有关应用状态的信息。终端必须(**MUST**)清除Reason Phrase字段的值,并且在转换为0x1c类型的CONNECTION_CLOSE时应该(**SHOULD**)使用APPLICATION_ERROR错误码。 - -以多种数据包类型发送的CONNECTION_CLOSE帧可以合并为单个UDP报文,参见第12.2节。 - -终端可以在Initial包中发送CONNECTION_CLOSE帧,这可能是为了响应在Initial或Handshake包中收到的未经认证的信息。这种立即关闭可能会将合法连接暴露给拒绝服务攻击。QUIC没有在握手期间对路径攻击的防御措施,参见第21.2节。然而,以减少对合法对端的错误反馈为代价,如果终端丢弃非法数据包而不是使用CONNECTION_CLOSE终止连接,则某些形式的拒绝服务可能会使攻击变得更加困难。因此,如果在缺少认证的数据包中检测到错误,终端(**MAY**)丢弃数据包而不是立即关闭。 - -尚未建立状态的终端,例如在Initial包中检测到错误的服务端,不会进入关闭状态。没有连接状态的终端在发送CONNECTION_CLOSE帧时也不会进入closing或draining。 - -## 10.3. 无状态重置(Stateless Reset) -无状态重置是作为无法访问连接状态的终端的最后手段提供的。因为崩溃或中断,对端可能继续向无法正常处理连接的终端发送数据,此时终端可以(**MAY**)发送Stateless Reset包,以响应接收到它无法与活动连接关联的数据包。 - -Stateless Reset包不适用于指示活动连接中的错误。如果可以的话,希望传达致命连接错误的终端必须(**MUST**)使用CONNECTION_CLOSE帧。 - -为了支持此过程,终端需要发出无状态重置令牌,这是一个很难猜测的16字节值。如果对端随后收到Stateless Reset包,即以该无状态重置令牌结尾的UDP报文,则对端将立即终止连接。 - -无状态重置令牌特定于CID。终端通过在NEW_CONNECTION_ID帧的Stateless Reset Token字段来发布无状态重置令牌。服务端还可以在握手期间发出stateless_reset_token传输参数,该参数适用于它在握手期间选择的CID。这些交换受加密保护,因此只有客户端和服务端知道它们的真实值。请注意,客户端不能使用stateless_reset_token传输参数,因为它们的传输参数没有机密性保护。 - -当通过RETIRE_CONNECTION_ID帧(第19.16节)退出关联的CID时,令牌将失效。 - -接收到它无法处理的数据包的终端发送如下结构的数据包(参见第1.3节): - -``` - Stateless Reset { - Fixed Bits(2)= 1, - Unpredictable Bits (38..), - Stateless Reset Token (128), - } -``` -图10: Stateless Reset Packet - -这种设计使得Stateless Reset包——在可能的范围内——与具有短包头的常规数据包无法区分。 - -Stateless Reset包对应一整个UDP报文,从数据包头的前两位开始。第一个字节的其余部分和它后面的任意数量的字节被设置为应该(**SHOULD**)与随机值无法区分的值。数据报的最后16个字节包含一个无状态重置令牌。 - -对于预期接收方以外的实体,Stateless Reset包将被识别为具有短标头的数据包。为了使Stateless Reset包识别为有效的QUIC包,Unpredictable Bits字段需要包含至少38位数据(或5个字节,减去两个固定位)。 - -如果接收方需要使用CID,则生成的21字节的最小大小并不能保证Stateless Reset包和其他数据包无法区分。为了达到这个目的,终端应该(**SHOULD**)确保它发送的所有数据包,至少要比携带最小CID长度的数据包长22个字节,并根据需要添加PADDING帧。这确保了对端发送的任何Stateless Reset包与发送给终端的有效数据包是不可区分的。需要发送Stateless Reset包以响应43个字节或更短的数据包的话,终端应该(**SHOULD**)发送一个比该数据包短一个字节的Stateless Reset包。 - -这些值假设无状态重置令牌的长度与数据包保护协议AEAD约定的最小扩展长度相同。如果终端可以协商具有更大最小扩展的数据包保护方案,则需要额外的Unpredictable Bits。 - -终端不得(**MUST NOT**)发送比它接收的数据包大三倍或以上的Stateless Reset包,以避免被用于放大攻击。第10.3.3小节描述了对Stateless Reset包大小的附加限制。 - -终端必须(**MUST**)丢弃因太短不合规的QUIC包。举个例子,用[QUIC-TLS]中定义的一组AEAD函数加密后,小于21字节的短包头数据包永远是无效的。 - -终端必须(**MUST**)发送形式类似短包头的数据包的Stateless Reset包。另外,终端必须(**MUST**)将任何以有效无状态重置令牌结尾的数据包视为Stateless Reset包,其他QUIC版本可能允许使用长包头。 - -终端可以(**MAY**)发送Stateless Reset包以响应具有长包头的数据包。在无状态重置令牌可供对端使用之前,发送Stateless Reset包无效。在当前QUIC版本中,具有长包头的数据包仅在连接建立期间使用。由于无状态重置令牌在连接建立完成或接近完成之前不可用,因此忽略具有长包头的未知数据包可能与发送Stateless Reset包有一样的效果。 - -终端无法从具有短包头的数据包中确定SCID,它不也能在Stateless Reset包中设置DCID。因此,DCID将与先前数据包中使用的值不同。随机DCID使CID看起来像连接迁移的NEW_CONNECTION_ID帧提供的新CID,参见第19.15节。 - -使用随机CID会导致两个问题: - -* 数据包可能无法到达对端,如果DCID对于路由到对端至关重要的话,则此数据包可能会被错误地路由。这也可能会触发另一个Stateless Reset包作为响应,参见第10.3.3小节。发送不能正确路由的Stateless Reset包是一种无效的错误检测和恢复机制。在这种情况下,终端将需要依赖其他方法(例如定时器)来检测连接是否失败。 - -* 随机生成的CID可由对端实体以外的实体使用,以将其识别为潜在的Stateless Reset包。偶尔使用不同CID的终端可能会对此引入一些不确定性。 - -这种无状态重置设计特定于QUIC版本1。支持多个QUIC版本的终端需要生成一个Stateless Reset包,该Stateless Reset包应该可被对端接收,对端应该支持本端可能支持(或在丢失状态之前可能已支持)的任何版本,新版本QUIC的设计人员需要意识到这一点,并且要么(1)重用此设计,要么(2)使用除最后16个字节以外的数据包的一部分来承载数据。 - -### 10.3.1. 检测无状态重置包(Detecting a Stateless Reset) -终端使用UDP报文的最后的16字节检测潜在的Stateless Reset包。终端应记住所有与它最近发送的数据报文的CID和远端地址相关联的无状态重置令牌。这包括来自NEW_CONNECTION_ID帧的Stateless Reset Token字段值和服务端的传输参数,但不包括与未使用或已停用的CID关联的无状态重置令牌。终端通过将数据报文的最后16个字节与所有关联活动连接的无状态重置令牌进行比较,将接收到的数据报识别为Stateless Reset包。 - -可以对每个入向数据报文执行此比较。如果来自数据报文的任何数据包被成功处理,终端可以(**MAY**)跳过此检查。但是,当入向数据报文中的第一个数据包无法与连接关联或无法解密时,必须(**MUST**)执行此比较。 - -终端不得(**MUST NOT**)检查与未使用或停用的CID相关联的无状态重置令牌。 - -当将数据报文与无状态重置令牌值进行比较时,终端必须(**MUST**)不泄漏有关令牌值的信息。例如,在特定时间周期内执行此比较可以保护各个无状态重置令牌的值免于通过timing side channels泄漏信息。另一种方法是存储和比较无状态重置令牌的转换值而不是初始令牌值,例如可使用以密钥(例如,分块加密算法Hashed Message Authentication Code(HMAC)[RFC2104])为入参的加密安全伪随机数函数做转换。终端不应保护诸如数据包是否成功解密或有效的无状态重置令牌的数量之类的信息。 - -如果数据报文的最后16个字节与无状态重置令牌的值相同,则终端必须(**MUST**)进入draining期并且不再在此连接上发送任何其他数据包。 - -### 10.3.2. 计算无状态重置令牌(Calculating a Stateless Reset Token) -无状态重置令牌必须(**MUST**)难以猜测。为了创建无状态重置令牌,终端可以为其创建的每个连接随机生成[RANDOM]一个秘钥。但是,当在一个集群或存储池中有多个实例终端可能会丢失状态时,这会带来协调问题。无状态重置机制是专门用于处理状态丢失的情况,因此这种方法不是最理想的。 - -可以通过使用伪随机函数生成证明(proof),该函数采用静态密钥和终端选择的CID(参见第5.1节)作为输入,可以在到同一终端的所有连接中使用相同的静态密钥。这类函数可选HMAC[RFC2104](HMAC(static_key, connection_id))或基于HMAC的密钥派生函数 (HKDF)[RFC5869](例如使用静态密钥作为输入密钥材料,CID作为salt)。函数的输出被截断为16个字节,以作为该连接的无状态重置令牌。 - -失去状态的终端可以使用相同的方法生成有效的无状态重置令牌。CID来自终端接收的数据包。 - -这种设计依赖于对端始终在其数据包中发送CID,以便终端可以使用数据包中的CID来重置连接。使用这种设计的终端必须(**MUST**)要么对所有连接使用相同的CID长度,要么对CID的长度进行编码,以便它可以在没有状态的情况下恢复。此外,它不能提供零长度的CID。 - -注意无状态重置令牌允许任何实体终止连接,因此一个值只能使用一次。这种选择无状态重置令牌的方法意味着CID和静态密钥的组合不得(**MUST NOT**)用于另一个连接。如果共享静态密钥的实例使用相同的CID,或者如果攻击者可以将数据包路由到没有状态但具有相同静态密钥的实例,则可能发生拒绝服务攻击,参见第21.11节。如果CID曾被无状态重置令牌重置的连接使用过,则其不得(**MUST NOT**)重用于与此连接共享静态密钥的节点上。 - -相同的无状态重置令牌不得(**MUST NOT**)用于多个CID。终端不需要将新值与所有以前的值进行比较,但重复值可以(**MAY**)被视为PROTOCOL_VIOLATION类型的连接错误。 - -请注意,Stateless Reset包没有任何加密保护。 - -### 10.3.3. 循环(Looping) -无状态重置的设计使得在不知道无状态重置令牌的情况下,它与有效数据包无法区分。例如,如果服务端向另一台服务端发送Stateless Reset包,它可能会收到另一个Stateless Reset包作为响应,这可能导致循环交换。 - -终端必须(**MUST**)确保它发送的每个Stateless Reset包都小于触发它的数据包,除非它保持足够的状态以防止循环。在出现环路的情况下,这会导致数据包最终太小而无法触发响应。 - -终端也可以记住它已发送的Stateless Reset包的数量,一旦达到限制就停止生成新的Stateless Reset包。对不同的远端地址使用单独的限制将确保当其他对端或连接已用完限制时,可以使用Stateless Reset包来关闭连接。 - -小于41字节的Stateless Reset包可能会被第三方观察者识别为Stateless Reset包,具体取决于对端CID的长度。另外,不发送Stateless Reset包来响应小数据包,可能会导致仅存在小数据包交互的连接断链场景下,无状态重置机制失效,此类故障可能只能通过其他方式(例如定时器)检测到。 - -# 11. 错误处理(Error Handling) -检测到错误的终端应该(**SHOULD**)向它的对端发出指示错误的信号。传输级和应用级错误都会影响整个连接,参见第11.1节。只有应用级别的错误才能隔离到单个流中,见第11.2节。 - -发出错误信号的帧中应该(**SHOULD**)包含最符合错误原因的代码(参见第20章)。此规约有约定错误条件以及其对应的错误码,尽管在规约条文中这些是必备的条件,但不同的实现策略可能会导致报告不同的错误。特别是,终端可以(**MAY**)在检测到错误情况时使用任何适用的错误码,不过通用错误码(例如PROTOCOL_VIOLATION或INTERNAL_ERROR)总是可以用来取代特定的错误码。 - -无状态复位(见第10.3节)不适用于可以用CONNECTION_CLOSE或RESET_STREAM帧指示的任何错误。具有可在连接上发送帧所需状态的终端不得(**MUST NOT**)使用无状态复位。 - -## 11.1. 连接错误(Connection Errors) - -导致连接不可用的错误,例如明显违反协议语义或影响整个连接的状态崩溃,必须(**MUST**)使用CONNECTION_CLOSE帧(第19.19节)发出信号。 - -特定于应用的协议错误,需要使用帧类型为0x1d的CONNECTION_CLOSE帧发出信号。特定于传输的错误,包括本文中描述的所有错误,都需要包含在帧类型为0x1c的CONNECTION_CLOSE帧中。 - -CONNECTION_CLOSE帧可能丢失,如果终端在终止的连接上接收到更多数据包,则它应该(**SHOULD**)准备重传包含CONNECTION_CLOSE帧的数据包。限制重传次数和重传持续时间可以限制在终止连接上花费更多精力。 - -如果选择不重传包含CONNECTION_CLOSE帧的数据包,其对端有可能收不到第一包CONNECTION_CLOSE帧。对继续在已终止的连接上收到数据包的终端,其唯一可行的机制是尝试无状态重置过程(第10.3节)。 - -由于Initial包的AEAD不提供强身份验证,终端可以(**MAY**)丢弃无效的Initial包。不过本规约要求上报连接错误。如果终端不处理数据包中的帧或需要回退某些处理流程,则它只能丢弃数据包。丢弃无效的Initial包可用于减少拒绝服务的风险,参见第21.2节。 - -## 11.2. 流错误(Stream Errors) - -如果应用级错误影响单个流,但因其他因素,连接仍处于可恢复状态,则终端可以发送带有适当错误码的RESET_STREAM帧(第19.4节)以仅终止受影响的流。 - -在不通知应用层协议的情况下重置流可能导致应用层协议进入不可恢复状态,因此RESET_STREAM必须(**MUST**)由调用QUIC的应用层协议发起。 - -RESET_STREAM中携带的应用层错误码的语义由应用层协议定义。只有应用层协议能够终止流。应用层协议的本地实例可直接通过API调用,远端实例使用STOP_SENDING帧,以触发本端启动RESET_STREAM流程。 - -应用层协议应该(**SHOULD**)定义规则,处理被任一终端提前取消的流。 - -# 12. 数据包和帧( Packets and Frames) - -QUIC终端通过交换数据包进行通信。数据包具有机密性和完整性保护,参见第12.1节。数据包在UDP报文中携带,参见第12.2节。 - -此QUIC版本在连接建立时使用长包头,参见第17.2节。带有长包头的数据包是Initial(第17.2.2小节),0-RTT(第17.2.3小节),Handshake(第17.2.4小节),和Retry(第17.2.5小节)。版本协商使用与版本无关的带有长包头的数据包,参见第17.2.1小节。 - -带有短包头的数据包为最小开销设计,并在建立连接且1-RTT密钥可用后使用,参见第17.3节。 - -## 12.1. 受保护的数据包(Protected Packets) - -QUIC包根据类型具有不同级别的加密保护。数据包保护的详细信息可在[QUIC-TLS]中找到,本节概述其所能提供的保护措施。 - -Version Negotiation包没有加密保护,参见[QUIC-INVARIANTS]。 - -Retry包使用AEAD功能[AEAD]来防止意外修改。 - -Initial包使用AEAD保护,其密钥是使用在线上可见的值派生的。因此Initial包没有有效的机密性保护。Initial保护的引入是确保数据包的发送方位于网络路径上。任何从客户端接收到Initial包的实体都可以恢复密钥,这将允许它们既能正确读取数据包的内容,也能生成对端可以成功验证的Initial包。AEAD还保护Initial包免受意外修改。 - -所有其他数据包都使用来自加密Handshake的密钥进行保护。加密Handshake确保只有通信终端才能收到Handshake、0-RTT和1-RTT包的相应密钥。使用0-RTT和1-RTT密钥保护的数据包具有很强的机密性和完整性保护。 - -出现在某些数据包类型中的Packet Number字段作为包头保护的一部分可以替代机密性保护,有关详细信息,请参阅[QUIC-TLS]第5.4节。在特定包号空间中发送的每个包的底层包号都会递增,详细信息参见第12.3节。 - -## 12.2. 合并数据包(Coalescing Packets) - -Initial(第17.2.2小节)、0-RTT(第17.2.3小节)和Handshake(第17.2.4小节)数据包包含指示数据包尾部的Length字段。包长包括Packet Number和Packet Payload字段的长度,这两个字段都是机密性保护的并且最初长度未知。一旦包头保护被移除,Payload字段的长度就会暴露。 - -使用Length字段,发送方可以将多个QUIC包合并为一个UDP报文。这可以减少完成加密握手所需的UDP报文的数量,之后开始发送数据。这也可用于构建路径最大传输单元(PMTU)探针,参见第14.4.1小节。接收方必须(**MUST**)能够处理合并的数据包。 - -按加密级别递增的顺序(Initial、0-RTT、Handshake、1-RTT,参见[QUIC-TLS]第4.1.4小节)合并数据包使得接收方更有可能一次处理所有数据包。具有短包头的数据包不包含长度,因此它只能是UDP报文中包含的最后一个数据包。如果多个帧有相同的加密级别,终端应该(**SHOULD**)尽量将它们组帧在单个数据包中,而不是分开在多个数据包中发送。 - -接收方可以(**MAY**)根据UDP报文中包含的第一个数据包中的信息进行路由。发送方不得(**MUST NOT**)将具有不同CID的QUIC包合并为单个UDP报文。接收方应该(**SHOULD**)忽略与数据报文中的第一个数据包具有不同DCID的任何后续数据包。 - -合并成单个UDP报文的每个QUIC包都是独立且完整的。接收到多QUIC包合并的UDP报文,接收方必须(**MUST**)单独处理每个QUIC包并分别确认它们,就好像它们是作为不同UDP报文的负载被接收的一样。例如,如果某包解密失败(因为密钥不可用或出于任何其他原因),接收方可以(**MAY**)丢弃或缓冲这个数据包以待后续处理,之后必须(**MUST**)尝试处理UDP报文中的剩余数据包。 - -Retry包(第17.2.5小节)、Version Negotiation包(第17.2.1小节)和具有短包头的数据包(第17.3节)不包含Length字段,因此不能做UDP报文的中间数据包,其实也不会出现Retry或Version Negotiation包与另一个数据包合并的情况。 - -## 12.3. 包号(Packet Numbers) - -包号是0到2^62^-1范围内的整数。该编号用于确定数据包保护的加密随机数。每个终端对发送和接收部分分别维护一个单独的包号。 - -包号限制在此范围内,因为它们需要在ACK帧的最大确认字段中完整表示(第19.3节)。然而,当出现在长或短包头中时,包号长度会压缩并编码为1到4个字节,参见第17.1节。 - -Version Negotiation包(第17.2.1小节)和Retry包(第17.2.5小节)不包含包号。 - -QUIC中包号分为三个空间: -* Initial空间:所有Initial包(第17.2.2小节)都在此空间中。 -* Handshake空间:所有Handshake包(第17.2.4小节)都在这个空间中。 -* 应用数据空间:所有0-RTT(第17.2.3小节)和1-RTT(第17.3.1小节)数据包都在这个空间中。 - -如[QUIC-TLS]中所述,每种数据包类型使用不同的保护密钥。 - -从概念上讲,包号空间是可以处理和确认数据包的上下文。Initial包只能使用Initial包保护密钥发送,并只能在Initial包中进行确认。同样,Handshake包使用Handshake级别加密发送,只能在Handshake包中确认。 - -这强制在不同包号空间中发送的数据之间进行加密分离。每个空间中的包号从0开始,在同一包号空间中发送的后续包号至少增加1。 - -0-RTT和1-RTT数据存在于同一个包号空间中,使两种数据包类型之间的丢包恢复算法更容易实现。 - -QUIC终端不得(**MUST NOT**)在一个连接中的相同包号空间内重用包号。如果要发送的数据包数量达到2^62^-1,则发送方必须(**MUST**)关闭连接,并且不发送CONNECTION_CLOSE帧或任何其他数据包,在接收到对端后续数据包时,可以(**MAY**)发送Stateless Reset包(第10.3节)。 - -接收方必须(**MUST**)丢弃一个新的未受保护的数据包,除非在相同的包号空间,它处理过相同包号的另一个数据包。因为由于[QUIC-TLS]第9.5节中描述的原因,删除数据包保护后必须(**MUST**)发生重复抑制(Duplicate Suppression)。 - -为检测重复数据而跟踪所有单个数据包,终端存在累积过多状态的风险。可以通过维护一个最小处理包号来限制检测重复所需的数据量,所有小于该编号的数据包可被立即丢弃。任何最小值的设定都需要考虑RTT的巨大变化,其中需要包括对端可能在更大的RTT下探测网络路径的可能性,参见第9章。 - -第17.1节描述了发送方的包号编码和接收方的解码。 - -## 12.4. 帧和帧类型(Frames and Frame Types) - -去除数据包保护后,QUIC包的负载由一系列完整的帧组成,如图11所示。Version Negotiation包、Stateless Reset包和Retry包不包含帧。 - -``` - Packet Payload { - Frame (8..) ..., - } -``` -图11: QUIC Payload - -包含帧的数据包的负载必须(**MUST**)至少包含一个帧,并且可以(**MAY**)包含多个帧和多种帧类型。终端必须(**MUST**)将收到不包含帧的数据包视为PROTOCOL_VIOLATION类型的连接错误。一个帧只能包含在单个QUIC包中,不能跨越多个数据包。 - -每个帧都以一个帧类型开始,表明它的类型,然后是附加的类型相关字段: - -``` - Frame { - Frame Type (i), - Type-Dependent Fields (..), - } -``` -图12: Generic Frame Layout - -表3列出并总结了有关本规约中定义的每种帧类型的信息。表格之后有具体说明。 - - | Type Value | Frame Type Name | Definition | Pkts | Spec | - |:---|:---|:---|:---|:---| - |0x00 | PADDING | 第19.1节 | IH01 | NP | - |0x01 | PING | 第19.2节 | IH01 | | - |0x02-0x03| ACK | 第19.3节 | IH_1 | NC | - |0x04 | RESET_STREAM | 第19.4节 | __01 | | - |0x05 | STOP_SENDING | 第19.5节 | __01 | | - |0x06 | CRYPTO | 第19.6节 | IH_1 | | - |0x07 | NEW_TOKEN | 第19.7节 | ___1 | | - |0x08-0x0f| STREAM | 第19.8节 | __01 | F | - |0x10 | MAX_DATA | 第19.9节 | __01 | | - |0x11 | MAX_STREAM_DATA | 第19.10节| __01 | | - |0x12-0x13| MAX_STREAMS | 第19.11节| __01 | | - |0x14 | DATA_BLOCKED | 第19.12节| __01 | | - |0x15 | STREAM_DATA_BLOCKED | 第19.13节| __01 | | - |0x16-0x17| STREAMS_BLOCKED | 第19.14节| __01 | | - |0x18 | NEW_CONNECTION_ID | 第19.15节| __01 | P | - |0x19 | RETIRE_CONNECTION_ID | 第19.16节| __01 | | - |0x1a | PATH_CHALLENGE | 第19.17节| __01 | P | - |0x1b | PATH_RESPONSE | 第19.18节| ___1 | P | - |0x1c-0x1d| CONNECTION_CLOSE | 第19.19节| ih01 | N | - |0x1e | HANDSHAKE_DONE | 第19.20节| ___1 | | - -表3: Frame Types - -第19章.更详细地解释了每种帧类型的格式和语义。本节的其余部分提供了重要和一般信息的摘要。 - -ACK、STREAM、MAX_STREAMS、STREAMS_BLOCKED和CONNECTION_CLOSE帧中的Frame Type字段用于携带其他特定于帧的标志。对于所有其他帧,Frame Type字段仅标识帧。 - -表3中的Pkts列列出了每种帧类型可能出现的数据包类型,由以下字符表示: -* I: Initial(第17.2.2小节) -* H: Handshake(第17.2.4小节) -* 0: 0-RTT(第17.2.3小节) -* 1: 1-RTT(第17.3.1小节) -* ih: 只有0x1c类型的CONNECTION_CLOSE帧可以出现在Initial或Handshake包中。 - -有关这些限制的更多详细信息,请参阅第12.5节。请注意,所有帧都可以出现在1-RTT包中。终端必须(**MUST**)将接收到的数据包类型中不允许出现的帧视为PROTOCOL_VIOLATION类型的连接错误。 - -表3中的Spec列总结了控制帧类型处理或生成的任何特殊规则,如以下字符所示: -* N:仅包含带有此标记的帧的数据包不是ACK触发包,参见第13.2节。 -* C:出于拥塞控制目的,仅包含带有此标记的帧的数据包,不计入传输中的字节数计算,见[QUIC-RECOVERY]。 -* P:仅包含带有此标记的帧的数据包,可用于在连接迁移期间探测新的网络路径,参见第9.1节。 -* F:带有此标记的是流控帧,参见第4章。 - -表3中的Pkts和Spec列不构成IANA注册管理机制的一部分,参见第22.4节。 - -终端必须(**MUST**)将收到未知类型的帧视为FRAME_ENCODING_ERROR类型的连接错误。 - -在此QUIC版本中,所有的帧都是幂等的。也就是说,多次接收相同的有效帧不应触发不良效果或报错。 - -Frame Type字段使用变长整数编码(第16章),但有一个前提。为确保帧解析实现的简单有效,帧类型必须(**MUST**)使用尽可能短的编码。对于本文中定义的帧类型,这意味着使用单字节编码,即使可以将这些值编码为两字节、四字节或八字节的变长整数。例如,尽管0x4001也是值为1的变长整数的合法两字节编码,但PING帧只能编码为值为0x01的单个字节。此规则适用于所有当前和未来的QUIC帧类型。终端接收到将帧类型编码为比实际所需更长字节的帧,可以(**MAY**)视为PROTOCOL_VIOLATION类型的连接错误。 - -## 12.5. 帧和包号空间(Frames and Number Spaces) - -某些帧在其他的数据包空间中是被禁止的。这里的规则概括了TLS的规则,与连接建立相关的帧,通常可以出现在任何包号空间的数据包中,而与传输数据相关的那些只能出现在应用包号空间中: - -* PADDING、PING和CRYPTO帧可以(**MAY**)出现在任何包号空间中。 -* CONNECTION_CLOSE指示QUIC层错误(类型0x1c)可以(**MAY**)出现在任何包号空间中。CONNECTION_CLOSE指示应用错误(类型0x1d)必须(**MUST**)只出现在应用包号空间中。 -* ACK帧可以(**MAY**)出现在任何包号空间中,但只能确认出现在该包号空间中的数据包。但是,如下所述的0-RTT包不能包含ACK帧。 -* 所有其他帧类型必须(**MUST**)仅在应用包号空间中发送。 - -请注意,由于各种原因,无法在0-RTT包中发送以下帧:ACK、CRYPTO、HANDSHAKE_DONE、NEW_TOKEN、PATH_RESPONSE或RETIRE_CONNECTION_ID。服务端可以(**MAY**)将在0-RTT包中收到这些帧视为PROTOCOL_VIOLATION类型的连接错误。 - -# 13. 组包与可靠性(Packetization and Reliability) - -发送方可在一个QUIC包中发送一个或多个帧,参见第12.4节。 - -发送方可以通过在每个QUIC包中,包含尽可能多的帧来最小化每个数据包的带宽和计算成本。发送方可以(**MAY**)等待一小段时间来收集多个帧,然后再打包发送一个大的但未超限的数据包,以避免发送大量小数据包。实现可以(**MAY**)收集应用发送行为规律或先验知识来确定是否等待以及等待多长时间。这个等待时间由实现决定,实现应谨慎地考虑这个时间,因为任何迟滞都可能增加应用可见的时延。 - -流复用是通过将来自多个流的STREAM帧交织成一个或多个QUIC包来实现的。单个QUIC包可以包含来自一个或多个流的多个STREAM帧。 - -QUIC的好处之一是避免跨多个流的队头阻塞。当发生数据包丢失时,只有在该数据包中有数据的流才会被阻塞,等待接收重传,而其他流可以继续前进。请注意,当来自多个流的数据包含在单个QUIC包中时,该数据包的丢失会阻止所有这些流的前进。建议实现在出向数据包中包含尽可能少的流,但也尽量不要因为大量padding而降低传输效率。 - -## 13.1. 数据包处理(Packet Processing) - -在成功解密并处理完数据包内所有帧之前,不得(**MUST NOT**)回数据包确认ACK。对于STREAM帧,"处理完"意味着数据已经入队以备应用层协议接收,但不意味着已经投递给应用或应用已经消费数据。 - -一旦数据包被完全处理,接收端通过发送一个或多个包含已接收包号的ACK帧来确认接收。 - -如果终端收到对其未发送数据包的确认ACK,应该(**SHOULD**)将其视为PROTOCOL_VIOLATION类型的连接错误。有关如何实现这一点的进一步讨论,请参见第21.4节。 - -## 13.2. 生成确认ACK(Generating Acknowledgments) - -终端需要确认它们接收和处理的所有数据包。然而,只有ACK触发包(ACK触发)会导致在ACK发送窗口期内发送ACK帧。非ACK触发包仅在因其他原因发送ACK帧时才被确认。 - -在无论因为什么原因需要发送数据包时,如果最近没有发过包,终端应该(**SHOULD**)尝试包含一个ACK帧。这样做有助于对端及时检测是否丢包。 - -通常,来自接收端的频繁确认会改善丢包和拥塞情况,但如果接收端对每个ACK触发包都回以ACK的话就太过了。必须考虑平衡这种过度负载,下面提供的指引旨在实现这种平衡。 - -### 13.2.1. 发送ACK帧(Sending ACK Frames) - -每个数据包应该(**SHOULD**)至少被确认一次,并且必须(**MUST**)在终端用max_ack_delay传输参数通告的窗口期内至少确认一次ACK触发包,参见第18.2节。max_ack_delay声明了一个明确的约定:终端承诺会在max_ack_delay超时之前发送ACK帧确认ACK触发包。否则,RTT的估值会无谓放大,并可能导致对端错误的超时重传。发送方使用接收方通告的max_ack_delay值来确定基于定时器的超时重传,如[QUIC-RECOVERY]第6.2节所述。 - -终端必须(**MUST**)立即确认所有Initial和Handshake触发包,以及在通告的max_ack_delay内确认所有0-RTT和1-RTT触发包,以下情况除外:在握手确认之前,终端可能没有可用的秘钥在收到Handshake、0-RTT或1-RTT包时对其解密。因此,它可能会先缓存它们并在密钥可用时再确认它们。 - -由于仅包含ACK帧的数据包不受拥塞控制,因此终端在收到一个ACK触发包时不得(**MUST NOT**)发送多个这种包。 - -终端不得(**MUST NOT**)发送非ACK触发包来响应非ACK触发包,即使收到的包号不连续。这可以避免确认形成反馈死循环,也可以避免因为连接空闲导致的断链。只有当终端发送ACK帧以响应其他事件时,才可以确认非ACK触发包。 - -仅发送ACK帧的终端将不会收到来自其对端的确认ACK,除非这些确认包含在ACK触发包内。当有新的ACK触发包要确认时,ACK帧可以与其他帧一起发送。当只需要确认非ACK触发包时,终端可以(**MAY**)选择不发送ACK帧,直到收到ACK触发包需要发送ACK帧为止。 - -仅发送非ACK触发包的终端可能会选择偶尔向这些数据包内添加ACK触发帧,以保证能收到ACK。但在第13.2.4小节这种场景下,终端不得(**MUST NOT**)在非ACK触发包内插入ACK触发帧,否则会导致确认陷入死循环。 - -为了帮助发送方进行丢包检测,终端应该(**SHOULD**)在接收到ACK触发包时立即生成并发送一个ACK帧: -* 当收到的数据包的编号小于另一个已收到的ACK触发包时; -* 当数据包的编号大于已接收到的最高编号的ACK触发包,并且编号不连续时; - -类似地,在收到IP报头中标有ECN Congestion Experienced(CE)码点的数据包时应该(**SHOULD**)立即确认,以减少对端对拥塞事件的响应时间。 - -[QUIC-RECOVERY]中的算法需要对不遵循上述指引的接收端具有弹性。然而,只有在仔细考虑变更对终端和网络其他用户的连接的性能影响后,实现才可以不考虑这些要求。 - -### 13.2.2. 确认包频率(Acknowledgment Frequency) -接收端决定响应ACK触发包的ACK的发送频率。这个频率需要权衡考量。 - -终端依靠及时的ACK来检测丢包,参见[QUIC-RECOVERY]第6章。基于窗口的拥塞控制器,参见[QUIC-RECOVERY]第7章中描述的控制器,依靠ACK来管理它们的拥塞窗口。在这两种情况下,延迟确认都会对性能产生不利影响。 - -另一方面,减少仅携带ACK的数据包的频率会降低两端数据包传输和处理成本。它可以改善严重不对称链路上的连接吞吐量,并使用返回路径容量以减少确认流量,参见[RFC3449]第3章。 - -接收方应该(**SHOULD**)在收到至少两个ACK触发包后才发送一个ACK帧。该建议本质上是通用的,与TCP的ACK建议策略也是一致的,参见[RFC5681]。对网络条件、对端的拥塞控制器等的先验知识和进一步的研究和实验,可能会有更好的确认机制选择方案以提升性能。 - -接收方可以(**MAY**)考虑处理多少个可用数据包之后再发送ACK响应帧。 - -### 13.2.3. 管理ACKRanges(Managing ACK Ranges) - -ACK帧内包括一个或多个ACK Ranges确认接收数据包。包含对旧数据包的确认可以减少由于前序ACK帧丢失导致的不当重传的可能,但代价是ACK帧会更大。 - -ACK帧应该(**SHOULD**)总是确认最近收到的数据包,并且数据包越是乱序,更快发送更新的ACK帧就越重要,以防止对端认为数据包丢失并不当重传它包含的帧。一个ACK帧应完整放入一个QUIC包,放不进去的话,则忽略较老的Ranges(具有更小包号的Ranges )。 - -接收方需要限制其在ACK帧中发送的ACK Ranges的数量(第19.3.1小节),以降低ACK帧的大小并避免资源耗尽。在收到对ACK帧的确认后,接收方应该(**SHOULD**)停止跟踪那些确认的ACK Ranges。发送方可以预期能收到大多数数据包的确认,但QUIC不保证能收到接收方对其处理的每个数据包的确认。 - -携带多个ACK Ranges可能会导致ACK帧变得太大而无法装入数据包,接收方可以丢弃部分未确认的ACK Ranges以限制ACK帧的大小,其代价是发送方的重传次数增加,但这是必要的。接收方还可以(**MAY**)进一步限制ACK帧的大小或限制ACK帧的容量占比以腾出其他帧的空间。 - -接收方必须(**MUST**)保留某个ACK Range,除非其可以确保随后不会接受具有该Range范围内编号的数据包。维护一个随着Ranges丢弃而增加的最小包号,可以最小状态实现这一目标。 - -接收方可以丢弃所有的ACK Ranges,但必须(**MUST**)维护已成功处理的最大包号,因为它用于从后续数据包中恢复包号,参见第17.1节。 - -接收方在每个ACK帧中都应该(**SHOULD**)包含一个ACK Range,该Range包含最大接收包号。Largest Acknowledged字段用于发送方ECN验证,如果该值比前序ACK帧中的值要小的话可能导致ECN被不必要地禁用,参见第13.4.2小节。 - -第13.4.2小节给出了决定需要在ACK帧中确认哪些数据包的示例性方法。尽管此算法的目标是为每个处理的数据包生成确认,但也适用于确认丢失的情况。 - -### 13.2.4. 通过跟踪ACK帧来限制Ranges(Limiting Ranges by Tracking ACK Frames) - -当发送包含ACK帧的数据包时,可以保存该帧中的Largest Acknowledged字段。当包含此ACK帧的数据包被确认时,接收方可以停止确认小于或等于此帧中的Largest Acknowledged字段的数据包。 - -仅发送非ACK触发数据包(例如ACK帧)的接收方可能在很长一段时间内都不会收到确认。这可能会导致接收方长时间维护大量ACK帧的状态,且在组帧时ACK帧可能会过大。在这种情况下,接收方可以偶尔发送PING帧或其他小的ACK触发帧,例如每个RTT一次,以触发对端回ACK。 - -在没有ACK帧丢失的情况下,该算法容忍至少1个RTT的乱序。在ACK帧丢失和乱序的情况下,这种方法不能保证在某些ACK Ranges移出ACK帧之前,发送方能看到它。数据包可能会被乱序接收,并且对他们进行确认的所有ACK帧都可能丢失。在这种情况下,丢包恢复算法可能会导致不当重传,但发送方的处理会继续。 - -### 13.2.5. 测量和报告主机延迟(Measuring and Reporting Host Delay) - -接收方可以测量从接收到最大包号的数据包到发送ACK之间主动引入的延迟,并在ACK帧的ACK Delay字段中对此延迟时间进行编码,参见第19.3节。这使得此ACK帧的接收方可以依据此延迟,对路径RTT估计进行更精确的调整。 - -数据包在被处理之前可能会保存在操作系统内核或主机上的其他地方,当在ACK帧中填充ACK Delay字段时,终端不得(**MUST NOT**)包括它无法控制的延迟。然而,终端应该(**SHOULD**)将解密密钥不可用引起的缓冲延迟计算在内,因为这些延迟可能很大并且很可能是不可复现的。 - -当测量的确认延迟大于其max_ack_delay时,终端应该(**SHOULD**)上报测量延迟。在延迟可能很大时,此信息在握手期间特别有用,参见第13.2.1小节。 - -### 13.2.6. ACK帧和数据包保护(ACK Frames and Packet Protection) - -ACK帧必须(**MUST**)只在与被确认的数据包具有相同包号空间的数据包中携带,参见第12.1节。例如,使用1-RTT密钥保护的数据包必须(**MUST**)在同样使用1-RTT密钥保护的数据包中确认。 - -客户端使用0-RTT包保护发送的数据包必须(**MUST**)由服务端在由1-RTT密钥保护的数据包中确认。这可能意味着如果服务端加密握手消息延迟或丢失,客户端将无法使用这些确认。请注意,同样的限制适用于受1-RTT密钥保护的服务端发送的其他数据。 - -### 13.2.7. PADDING帧消耗拥塞窗口(PADDING Frames Consume Congestion Window) - -在数据包中包含PADDING帧是出于拥塞控制的目的,参见[QUIC-RECOVERY]。但仅包含PADDING帧的数据包只会消耗拥塞窗口,却不会生成ACK移动或扩大窗口。为避免死锁,发送方应该(**SHOULD**)确保定期发送PADDING帧之外的其他帧以触发接收方的确认。 - -## 13.3. 信息重传(Retransmission of Information) - -确定丢失的QUIC包不需要全部重传。这同样适用于包含在丢失数据包中的帧,在新的帧中可根据需要再次发送在丢失帧中携带的信息。 - -新的帧和数据包用于重传确定已丢失的信息。通常,当确定包含该信息的数据包丢失时将再次发送信息,并在包含该信息的数据包被确认时停止发送。 - -* CRYPTO帧:在CRYPTO帧中发送的数据根据[QUIC-RECOVERY]中的规则进行重传,直到所有数据都得到确认。当相应包号空间的密钥被丢弃时,Initial和Handshake包的CRYPTO帧中的数据也将被丢弃。 - -* STREAM帧:除非终端在该流上发送了RESET_STREAM,否则在STREAM帧中发送的应用数据将在新的STREAM帧中进行重传。一旦终端发送RESET_STREAM帧,就不再发送其他STREAM帧。 - -* ACK帧:ACK帧携带最近的一组确认和Largest Acknowledged包的确认延迟,如第13.2.1小节所述。包含ACK帧的数据包的传输延迟或旧的ACK帧重传可能会导致对端计算出较大的RTT或不必要地禁用ECN。 - -* RESET_STREAM帧:携带流传输取消信息的RESET_STREAM帧,会一直发送直到被确认或所有流数据都被对端确认(即流的发送侧达到Reset Recvd或Data Recvd状态)。RESET_STREAM帧的内容在重传时不得(**MUST NOT**)更改。 - -* STOP_SENDING帧:类似地,携带取消流传输STOP_SENDING帧,也会一直发送,直到流的接收侧进入Data Recvd或Reset Recvd状态,参见第3.5节。 - -* CONNECTION_CLOSE帧:携带连接关闭信号的CONNECTION_CLOSE帧,被检测到丢包时,不需要重传,如何重发这些信号参见第10章。 - -* MAX_DATA帧:MAX_DATA帧携带连接最大可发数据量信息。如果包含最近发送的MAX_DATA帧的数据包被声明丢失或当终端决定更新其值时,则需要在MAX_DATA帧中发送更新后的值。需要小心避免过于频繁地发送此帧,因为这会导致该值的频繁更新,并发送大量不必要的MAX_DATA帧,参见第4.2节。 - -* MAX_STREAM_DATA帧:MAX_STREAM_DATA帧中携带当前最大流数据偏移量。与MAX_DATA一样,当包含流的最新MAX_STREAM_DATA帧的数据包丢失或需要更新该值时,将发送更新后的值,注意防止帧发送过于频繁。当流的接收部分进入Size Known 或Reset Recvd状态时,终端应该(**SHOULD**)停止发送MAX_STREAM_DATA帧。 - -* MAX_STREAMS帧:MAX_STREAMS帧中携带给定类型的流个数的限制。与MAX_DATA一样,当最新包含MAX_STREAMS的数据包被声明丢失或值更新时,将发送更新的值,注意防止帧发送过于频繁。 - -* DATA_BLOCKED、STREAM_DATA_BLOCKED和STREAMS_BLOCKED帧:这几类帧承载阻塞信号。DATA_BLOCKED帧具有连接范围,STREAM_DATA_BLOCKED帧具有流范围,而STREAMS_BLOCKED帧的范围限定为特定的流类型。如果上述某个范围中包含最近帧的数据包丢失,则发送新帧。BLOCKED帧仅当终端因为相应限制被阻塞时才发送,这些帧总是包含导致阻塞的限制原因。 - -* PATH_CHALLENGE帧:定期发送PATH_CHALLENGE帧以执行活性或路径验证检查,直到接收到匹配的PATH_RESPONSE帧或不再需要活性或路径验证检查。PATH_CHALLENGE帧每次发送时都需包含不同的负载。 - -* PATH_RESPONSE帧:PATH_RESPONSE帧对路径验证的响应只发送一次。对端应根据需要发送更多PATH_CHALLENGE帧以唤起相应的PATH_RESPONSE帧。 - -* NEW_CONNECTION_ID帧:新的CID在NEW_CONNECTION_ID帧中发送,如果包含它们的数据包丢失,则需要重传,重传帧携带相同的序列号值。同样,停用的CID在RETIRE_CONNECTION_ID帧中发送,如果包含它们的数据包丢失,则进行重传。 - -* NEW_TOKEN帧:如果包含NEW_TOKEN帧的数据包丢失,则重传它们。除了直接比较帧内容之外,不需要别的手段处理乱序或重复的NEW_TOKEN帧。 - -* PING和PADDING帧:PING和PADDING帧不包含任何信息,因此丢失的PING或PADDING帧不需要恢复。 - -* HANDSHAKE_DONE帧:HANDSHAKE_DONE帧丢包必须(**MUST**)重传,直到它被确认为止。 - -终端应该(**SHOULD**)优先重传数据而不是发送新数据,除非应用指定的优先级另有说明,参见第2.3节。 - -尽管鼓励发送方在每次发送数据包时组合包含最新信息的帧,但不禁止重传丢失包中包含的帧副本。重传丢失帧的发送方需要考虑因为包号长度、CID长度和路径MTU的变化而导致的可用负载大小的变化。接收方必须(**MUST**)接受包含过时帧的数据包,例如MAX_DATA帧携带的最大数据值小于旧数据包中的最大数据值。 - -一旦数据包被确认,发送方应该(**SHOULD**)避免重传来自该数据包的信息,包括在在网络乱序的情况下声明丢失后又被确认的数据包。这样做要求发送方在声明丢失后仍然保留有关数据包的信息。发送方可以在其容忍乱序的时限到期后丢弃此信息,例如PTO([QUIC-RECOVERY]第6.2节),或基于其他事件,例如达到内存限制。 - -在检测到丢包时,发送方必须(**MUST**)采取适当的拥塞控制措施。[QUIC-RECOVERY]中描述了丢包检测和拥塞控制的细节。 - -## 13.4. 显式拥塞通知(Explicit Congestion Notification) - -QUIC终端可以使用ECN[RFC3168] 来检测和响应网络拥塞。ECN允许终端在IP数据包的ECN字段中设置支持ECN的传输(ECT)码点。然后,网络节点可以通过在ECN字段中设置ECN-CE码点而不是丢弃数据包来指示拥塞[RFC8087]。如[QUIC-RECOVERY]中所述,终端通过降低其发送速率来响应报告的拥塞。 - -要启用ECN,QUIC发送端首先确定路径是否支持ECN标记以及对端是否报告接收到的IP标头中的ECN值,参见第13.4.2小节。 - -### 13.4.1. 报告ECN计数(Reporting ECN Counts) - -使用ECN需要接收端从IP数据包中读取ECN字段,这在所有平台上都是不可能的。如果终端不支持ECN或无权访问接收到的ECN字段,它不会报告它接收到的数据包的ECN计数。 - -即使终端没有在它发送的数据包中设置ECT字段,如果可行的话,终端也必须(**MUST**)提供有关它收到的ECN标记的反馈。未能报告ECN计数将导致发送方在此连接禁用ECN。 - -在接收到带有ECT(0)、ECT(1)或ECN-CE码点的IP数据包时,启用ECN的终端访问ECN字段并增加相应的ECT(0)、ECT(1)或ECN-CE记数。后续的ACK帧中可携带这些ECN计数,参见第13.2节和第19.3节。 - -每个包号空间维护单独的确认状态和单独的ECN计数。合并的QUIC包(参见第12.2节)共享相同的IP报头,因此对于其中的每个QUIC包,ECN计数都需要增加一次。 - -例如,如果Initial、Handshake和1-RTT QUIC包合并为单个UDP报文,则所有三个包号空间的ECN计数都需要加1。 - -ECN计数仅在处理承载在IP报文中的QUIC包时增加。重复的QUIC包不会被处理,也就不会增加ECN计数。有关安全问题的考虑,请参阅第21.10节。 - -### 13.4.2. ECN验证(ECN Validation) - -有故障的网络设备可能会损坏或错误地丢弃携带非零ECN码点的数据包。因为此类设备的存在,终端为了确保连接性,将验证每条网络路径的ECN计数,并在检测到错误时禁用该路径上的ECN。 - -要对新路径执行ECN验证: - -终端在通过新路径发往对端的早期出向数据包的IP标头中设置ECT(0) 码点[RFC8311]。 - -终端监控所有带有ECT码点的数据包是否最终都被视为丢失(参见[QUIC-RECOVERY]第6章),此现象表明ECN验证失败。 - -如果终端有理由预期带有ECT码点的IP数据包可能会被故障网元丢弃,则终端可以仅为路径上前十个出向数据包设置ECT码点,或仅在三个PTO的时间段设置ECT码点(参见[QUIC-RECOVERY]第6.2节。如果所有标有非零ECN码点的数据包随后丢失,则可以假设是因为标记导致的丢失,需要禁用标记。 - -因此,在切换到服务端的首选地址或将活动连接迁移到新路径时,终端尝试为每个新连接使用ECN验证。Appendix A.4描述了一种可能的算法。 - -其他探测路径是否支持ECN的方法也是可行的,可以采取不同的标记策略。实现可以(**MAY**)使用RFC中定义的其他方法,参见[RFC8311]。使用ECT(1) 码点的实现需要使用报告的ECT(1)计数执行ECN验证。 - -#### 13.4.2.1. 接收带有ECN计数的ACK帧(Receiving ACK Frames with ECN Counts) -网络错误应用ECN-CE标记会导致连接性能的下降。因此,接收带有ECN计数的ACK帧的终端,会在使用计数之前验证这些计数。它通过将新接收的计数与上次成功处理的ACK帧的计数进行比较,来执行此验证。对ECN计数的任何增加是否正常的验证,都是基于ACK帧中新确认的数据包的ECN标记。 - -如果ACK帧新确认了终端发送的带有ECT(0)或ECT(1)码点集的数据包,如果相应的ECN计数不存在于ACK帧中,则ECN验证失败。这种检查将探测把ECN字段置零的网元或不报告ECN标记的对端。 - -如果ECT(0)或ECN-CE计数增加的总和小于最初使用ECT(0)标记发送的新确认数据包的数量,则ECN验证也会失败。类似地,如果ECT(1)或ECN-CE计数增加的总和小于使用ECT(1)标记发送的新确认数据包的数量,则ECN也验证失败。这些检查可以检测网络对ECN-CE标记的重新标记。 - -当ACK帧丢失时,终端可能会错过对数据包的确认。因此,ECT(0)、ECT(1)和ECN-CE计数的总增加可能大于ACK帧新确认的数据包数量。这就是为什么允许ECN计数大于已确认的数据包总数的原因。 - -从乱序的ACK帧验证ECN计数可能会导致失败。终端不得(**MUST NOT**)因处理到未增加Largest Acknowledged包数的ACK帧而判定ECN验证失败。 - -如果ECT(0)或ECT(1)的接收总数超过每个相应ECT码点发送的数据包总数,则ECN验证可能会失败。特别是,当终端收到来自从未应用的ECT码点对应的非零ECN计数时,验证将失败。此检查检测数据包何时在网络中被标记为ECT(0)或ECT(1)。 - -#### 13.4.2.2. ECN验证结果(ECN Validation Outcomes) - -如果验证失败,则终端必须(**MUST**)禁用ECN。终端将停止在其发送的IP数据包中设置ECT码点,并假设网络路径或对端不支持ECN。 - -即使验证失败,终端也可以(**MAY**)在连接中的任何稍后时间重新验证相同路径的ECN。终端可以继续定期尝试验证ECN。 - -成功验证后,终端可以(**MAY**)继续在它发送的后续数据包中设置ECT码点,并期望该路径具有ECN能力。网络路由和路径元素可以改变中间连接,如果稍后验证失败,终端必须(**MUST**)禁用ECN。 - -# 14. 报文大小(Datagram Size) - -一个UDP报文可以包含一个或多个QUIC包。报文大小是指携带QUIC包的单个UDP报文的负载大小。报文大小包括一个或多个QUIC包头和受保护的负载长度,但不包括UDP或IP头。 - -最大报文大小定义为可以使用单个UDP报文跨网络路径传输的最大UDP负载大小。如果网络路径不支持至少1200字节的最大报文大小,则不得(**MUST NOT**)使用QUIC。 - -QUIC假设最小IP报文大小至少为1280字节。这是IPv6最小大小[IPv6],大多数现代IPv4网络也支持。假设IPv6的最小IP报头大小为40字节,IPv4为20字节,UDP报头大小为8字节,这将导致IPv6的最大报文大小为1232字节,IPv4为1252字节。因此,现代IPv4和所有IPv6网络路径都有望支持QUIC。 - -> 注意:如果路径仅支持1280字节的IPv6最小MTU,则此支持1200字节UDP负载的要求会将IPv6扩展标头的可用空间限制为32字节或IPv4选项的可用空间为52字节。这会影响Initial包和路径验证。 - -任何大于1200字节的最大报文大小都可以使用路径最大传输单元发现 (PMTUD)(参见第14.2.1小节))或报文分组层PMTU发现(DPLPMTUD)(参见第14.3节)来发现。 - -使用max_udp_payload_size传输参数(第18.2节)可以对最大报文大小附加强制限制。一旦知道该值,发送方就可以避免超过此限制。但在此之前,如果终端发送的报文大于1200字节的最小允许最大报文大小,则它们可能会出现丢包。 - -UDP报文绝不得(**MUST NOT**)在IP层分片。在IPv4[IPv4]中,如果可能,必须(**MUST**)设置Don't Fragment(DF)位,以防止在路径上分片。 - -QUIC有时要求报文不小于一定大小,参见第8.1节的示例。但是,报文的大小没有经过验证。也就是说,如果一个终端接收到一个特定大小的报文,它无法知道其是否与发送方发送的报文大小相同。因此,当终端收到不满足大小限制的报文时,它不得(**MUST NOT**)关闭连接,终端可以(**MAY**)丢弃这样的报文。 - -## 14.1. 初始报文大小(Initial Datagram Size) - -客户端必须(**MUST**)将所有承载Initial包的UDP报文负载扩展到至少1200字节的最小允许最大报文大小,这可以通过将PADDING帧添加到Initial包或合并Initial包实现,参见第12.2节。Initial包甚至可以与无效数据包合并,接收端可以丢弃无效数据包。类似地,服务端必须(**MUST**)将所有携带ACK触发Initial包的UDP报文负载扩展到至少1200字节的最小允许最大报文大小。 - -发送这种大小的UDP报文可确保网络路径在两个方向上都支持合理的路径最大传输单元 (PMTU)。此外,扩展Initial包的客户端有助于减少由服务端响应未经验证的客户端地址引起的放大攻击的幅度,参见第8章。 - -如果发送方认为网络路径和对端都支持它选择的大小,则包含Initial包的报文可以(**MAY**)超过1200字节。 - -如果UDP报文负载小于1200字节的最小允许最大报文大小,服务端必须(**MUST**)丢弃在UDP报文中携带的Initial包。服务端也可以(**MAY**)通过发送一个错误码为PROTOCOL_VIOLATION的CONNECTION_CLOSE帧来立即关闭连接,参见第10.2.3小节。 - -服务端还必须(**MUST**)在验证客户端地址之前限制它发送的字节数,参见第8章。 - -## 14.2. 路径最大传输单元(Path Maximum Transmission Unit) - -PMTU是整个IP数据包的最大大小,包括IP报头、UDP报头和UDP负载。UDP负载包括一个或多个QUIC包头和受保护的负载。PMTU可以取决于路径特性,因此可以随时间变化。终端在任何给定时间发送的最大UDP负载称为终端的最大报文大小。 - -终端应该(**SHOULD**)使用DPLPMTUD(第14.3节)或PMTUD(第14.2.1小节)来确定到目的地的路径是否支持所需的最大报文大小而不会分片。在没有这些机制的情况下,QUIC终端不应该(**SHOULD NOT**)发送大于最小允许最大报文大小的报文。 - -DPLPMTUD和PMTUD都发送大于当前最大报文大小的报文,称为PMTU探测。未在PMTU探测中发送的所有QUIC包应该(**SHOULD**)调整大小以适应最大报文大小,以避免报文被分段或丢弃[RFC8085]。 - -如果QUIC终端确定任何一对本地和远程IP地址之间的PMTU不支持1200字节的最小允许最大报文大小,则它必须(**MUST**)在受影响的路径上立即停止发送QUIC包,除了那些在PMTU探测中的数据包或包含CONNECTION_CLOSE帧的数据包。如果找不到替代路径,终端可以(**MAY**)终止连接。 - -每对本地和远程地址对可以有不同的PMTU。因此,实现任何类型PMTU发现的QUIC实现应该(**SHOULD**)为本地和远程IP地址的每个组合维护最大报文大小。 - -QUIC实现在计算最大报文大小时可以(**MAY**)更加保守,以允许未知的隧道开销或IP报头选项/扩展。 - -### 14.2.1. PMTUD处理ICMP消息(Handling of ICMP Messages by PMTUD) - -PMTUD[RFC1191][RFC8201] 的计算依赖于ICMP消息(也就是IPv6 Packet Too Big(PTB)消息)的接收,该消息指示IP数据包何时因为其大于本地路由器MTU被丢弃。DPLPMTUD也可以选择使用这类消息。ICMP消息的这种用法可能容易受到某些实体的攻击,这类实体可能无法观察到数据包但能成功猜测路径上使用的地址。这些攻击可能会将PMTU降低到使带宽低效的值。 - -终端必须(**MUST**)忽略声称PMTU已减小到低于QUIC允许的最小报文大小的ICMP消息。 - -[RFC1812][RFC4443]协议对ICMP的生成有要求,其引用的数据包应包含尽可能多的原数据包,且不会超过IP版本的最小MTU。引用的数据包实际上可能小一些,或者信息难以理解,如[DPLPMTUD]第1.1节。 - -使用PMTUD的QUIC终端应该(**SHOULD**)验证ICMP消息,以防止在[RFC8201]和[RFC8085]第5.2节指出的数据包注入。此验证应该(**SHOULD**)将ICMP消息负载中的引用数据包或相应的传输层连接的消息相关联(参见[DPLPMTUD]第4.6.1小节)。ICMP消息验证必须(**MUST**)包括匹配的IP地址和UDP端口[RFC8085],并在可能的情况下,包括QUIC活动会话的CID。终端应该(**SHOULD**)忽略所有验证失败的ICMP消息。 - -终端不得(**MUST NOT**)根据ICMP消息增加PMTU,参见[DPLPMTUD]第3章的第6项。在QUIC的丢包检测算法判定引用的数据包确实丢失之前,任何响应ICMP消息减少QUIC最大报文大小的措施都可以(**MAY**)是暂时性的。 - -## 14.3. 报文分组层PMTU发现(Datagram Packetization Layer PMTU Discovery) - -DPLPMTUD[DPLPMTUD]实现依赖于跟踪PMTU探测中携带的QUIC包的丢失或确认。使用PADDING帧的DPLPMTUD的PMTU探测实现“使用填充数据进行探测”,如[DPLPMTUD]第4.1节中所定义。 - -终端应该(**SHOULD**)将BASE_PLPMTU的初始值([DPLPMTUD]第5.1节)设置为与QUIC最小允许的最大报文大小一致。MIN_PLPMTU与BASE_PLPMTU相同。 - -实现DPLPMTUD的QUIC终端为本地和远程IP地址的每种组合维护一个DPLPMTUD最大数据包大小 (MPS)([DPLPMTUD]第4.4节)。这与UDP最大报文大小对应。 - -### 14.3.1. DPLPMTUD和Initial连接(DPLPMTUD and Initial Connectivity) - -从DPLPMTUD的角度来看,QUIC是公认的分组层 (Packetization Layer,PL)。因此,当QUIC连接握手完成时,QUIC发送方可以进入DPLPMTUDBASE状态([DPLPMTUD]第5.2节)。 - -### 14.3.2. 使用DPLPMTUD验证网络路径(Validating the Network Path with DPLPMTUD) - -QUIC是公认的PL,因此,QUIC发送方在SEARCH_COMPLETE状态下不会实现DPLPMTUD的CONFIRMATION_TIMER,参见[DPLPMTUD]第5.2节。 - -### 14.3.3. DPLPMTUD处理ICMP消息(Handling of ICMP Messages by DPLPMTUD) - -使用DPLPMTUD的终端需要在使用PTB信息之前验证任何接收到的ICMP PTB消息,如[DPLPMTUD]第4.6节中所定义。除了UDP端口验证之外,QUIC还可以通过其他PL信息来验证ICMP消息(例如,验证任何收到的ICMP消息的引用数据包中的CID)。 - -如果DPLPMTUD用到这些消息,则第14.2.1小节中描述的处理ICMP消息的注意事项也适用。 - -## 14.4. 发送QUIC(PMTU探测 Sending QUIC PMTU Probes) - -PMTU探测是发送ACK触发包。 - -终端可以将PMTU探测的内容限制为PING和PADDING帧,因为大于当前最大报文大小的数据包更有可能被网络丢弃。因此,PMTU探测中携带的QUIC包丢失不是拥塞的可靠指示,不应该(**SHOULD**)触发拥塞控制反应,参见[DPLPMTUD]第3章的第7项。但是,PMTU探测会消耗拥塞窗口,这可能会延迟应用的后续传输。 - -### 14.4.1. 包含SCID的PMTU探测(PMTU Probes Containing Source Connection ID) - -依赖DCID字段来路由入向QUIC包的终端可能需要在PMTU探测中包含CID,以将任何产生的ICMP消息(第14.2.1小节)路由回正确的终端。然而,只有长包头数据包(第17.2节)包含SCID字段,并且一旦握手完成,长包头数据包不会被对端解密或确认。 - -构建PMTU探测的一种方法是在单个UDP报文中合并(参见第12.2节)具有长包头的数据包(例如握手或0-RTT包(第17.2节))和短包头数据包。如果PMTU探测到达终端,带有长包头的数据包被忽略,但短包头数据包将被确认。如果PMTU探测触发ICMP消息,则ICMP消息将引用该探测的前面部分,如果SCID字段在其中,则可用于路由或验证ICMP消息。 - -> 注意:使用长报文头的目的只是为了保证ICMP报文中引用的报文包含一个SCID字段。这个数据包不需要是一个有效的数据包,即使当前没有使用该类型的数据包,它也可以被发送。 - -# 15. 版本号(Versions) -QUIC版本使用32位无符号整数标识。 - -版本0x00000000保留用于表示版本协商。此版本的规约标识为编号0x00000001。 - -其他版本的QUIC可能与此版本具有不同的属性。在[QUIC-INVARIANTS]中描述了保证在所有版本的协议中保持一致的QUIC属性。 - -QUIC的0x00000001版本使用TLS作为加密握手协议,如[QUIC-TLS]中所述。 - -版本号最高16位置零的版本保留用于未来的IETF协议文件。 - -遵循模式0x?a?a?a?a的版本是保留标识(即所有字节的低四位为1010(二进制)的任何版本号),用于强制执行版本协商。客户端或服务端可以(**MAY**)宣称支持这些保留版本中的任意一个。 - -保留的版本号永远不会表示真正的协议。客户端可以(**MAY**)使用这些版本号之一,寄望服务端启动版本协商;服务端也可以(**MAY**)宣称对这些版本之一的支持,寄望于客户端忽略该值。 - -# 16. 变长整数编码(Variable-Length Integer Encoding) - -QUIC包和帧通常对非负整数值使用变长编码。这种编码确保较小的整数可以使用更少的字节来编码。¶ - -QUIC变长整数编码保留第一个字节的最高两位,其值为n表示编码的整数长度为2的n次方。整数值按网络字节序在剩余位上编码。 - -这意味着整数可以编码为1,2,4,8字节,其值分别不超过6-,14-,30-,62位大小。表4列举了这几种编码属性: - -| 2MSB | Length | Usable Bits | Range | -|:---|:---|:---|:---| -| 00 | 1 | 6 | 0-63 | -| 01 | 2 | 14 | 0-16383 | -| 10 | 4 | 30 | 0-1073741823 | -| 11 | 8 | 62 | 0-4611686018427387903 | - -表4: Summary of Integer Encodings - -在附录Appendix A.1有编码算法和应用示例。 - -除了Frame Type字段外,其余值不需要以所需的最小字节数进行编码,参见第12.4节。 - -版本号(第15章),报头中的包号(第17.1节)和长包头中的CID长度(第17.2节)是整数但不需要使用此编码。 - -# 17. 数据包格式(Packet Formats) - -以下约定所有数值都以网络字节序(即大端)编码,所有字段大小均以位为单位。十六进制表示法用于表示字段的值。 - -## 17.1. 包号编解码(Packet Number Encoding and Decoding) - -包号(以下简称“包号”)是从0到2^62^-1(第12.3节)范围内的整数。当出现在长或短包头中时,其被编码为1到4个字节。通过仅编码包号的最少有效位,可以减少其占用的空间位数。 - -编码后的包号受到加密保护,如[QUIC-TLS]第5.4节。 - -在收到对包号空间的确认之前,数据包内必须(**MUST**)包括完整包号,它不能被截断,如下所述。 - -在收到某个包号空间的确认ACK后,发送方必须(**MUST**)确定一个包号长度,该包号长度需要能够表示最大已确认包号与当前包号两者差值的两倍以上范围内的数值。对端收到以后应能正确解码该包号,除非其在传输过程中有延迟,导致它在很多更大编号的数据包之后到达。终端应该(**SHOULD**)使用足够大的包号进行编码,使得即使数据包在后发数据包之后到达,也可以恢复包号。 - -因此,包号编码的大小应至少比包括新包在内的连续未确认包的数量的以2为基的对数多一位。Appendix A.2中有伪代码和包号编码示例。 - -在接收方处需要先移除数据包保护,再依据其有效位的数量、这些位的值以及在成功验证的数据包中接收到的最大包号来重建完整包号。恢复完整的包号才算是成功完成数据包保护移除。 - -一旦移除了包头保护,解码之后的包号为最接近预期包号的值。预期包号是接收到的最高包号加一。Appendix A.3中有伪代码和包号解码的示例。 - -## 17.2. 长包头数据包(Long Header Packets) - -``` - Long Header Packet { - Header Form (1) = 1, - Fixed Bit (1) = 1, - Long Packet Type (2), - Type-Specific Bits (4), - Version (32), - Destination Connection ID Length (8), - Destination Connection ID (0..160), - Source Connection ID Length (8), - Source Connection ID (0..160), - Type-Specific Payload (..), - } -``` -图13: Long Header Packet Format - -长包头数据包是在确立1-RTT密钥之前收发的数据包。一旦1-RTT密钥可用,发送方就会切换到使用短包头发送数据包(第17.3节)。长包头形式可使得特殊数据包——例如Version Negotiation包——以这种统一的固定长度数据包的格式来表示。使用长包头的数据包包含以下字段: - -* Header Form:头类型,对于长包头,字节0(第一个字节)的最高有效位(0x80)设置为1。 -* Fixed Bit:固定位,字节0的下一位(0x40)一般设置为1,设为0的话,除了Version Negotiation包之外,其他情况都不是有效数据包,必须(**MUST**)丢弃。该位的值为1允许QUIC与其他协议复用,参见[RFC7983]。 -* Long Packet Type:长报文类型,字节0的下两位(掩码为0x30)表示数据包类型。数据包类型在表5中列出。 -* Type-Specific Bits:类型相关位,字节0的低四位(掩码为0x0f)的语义由数据包类型决定。 -* Version :版本号,QUIC版本是字节0之后的32位字段。此字段指示正在使用的QUIC版本并确定如何解释其余协议字段。 -* Destination Connection ID Length:DCID长度,Version之后的一个字节表示随后的DCID字段的字节长度。此长度编码为8位无符号整数。在QUIC版本1中,该值不得(**MUST NOT**)超过20个字节。收到值大于20的版本1长包头的终端必须(**MUST**)丢弃该数据包,但为了正确构造Version Negotiation包,服务端也应该(**SHOULD**)支持其他版本的QUIC协议有更长的CID。 -* Destination Connection ID:目的连接ID(DCID),Destination Connection ID紧跟Destination Connection ID Length字段。第7.2节更详细地描述了该字段的使用方式。 -* Source Connection ID Length:SCID长度,DCID之后的一个字节表示其后的SCID字段的字节长度。此长度编码为8位无符号整数。在QUIC版本1中,该值不得(**MUST NOT**)超过20个字节。收到值大于20的版本1长包头的终端必须(**MUST**)丢弃该数据包,但为了正确构造Version Negotiation包,服务端也应该(**SHOULD**)支持其他版本的QUIC协议有更长的CID。 -* Source Connection ID:源连接ID(SCID),SCID字段紧跟Source Connection ID Length字段。第7.2节更详细地描述了该字段的使用方式。 -* Type-Specific Payload:类型相关负载,数据包的其余部分(如果有)与Long Packet Type指定的类型有关。 - -在此QUIC版本中,定义了以下带有长包头的数据包类型: - -| Type | Name | Section | -| :--- | :--- | :--- | -|0x0| Initial | Section 17.2.2 | -|0x1| 0-RTT | Section 17.2.3 | -|0x2| Handshake | Section 17.2.4 | -|0x3| Retry | Section 17.2.5 | - -表5: Long Header Packet Types - -长包头数据包的长报文类型、DCID和SCID长度、DCID和SCID字段以及版本字段与版本无关。第一个字节中的其他字段是特定于版本的。关于来自不同QUIC版本的数据包如何解释,请参阅[QUIC-INVARIANTS]。 - -部分字段和负载的解释特定于版本和数据包类型。关于此版本的特定于类型的语义在以下其他章节中会有描述,下面介绍此QUIC版本中的几个长包头数据包中都包含的附加字段: - -* Reserved Bits:保留位,多个类型的长包头数据包中字节0有两个保留位(掩码为0x0c),这些位都使用包头保护策略来保护,参见[QUIC-TLS]第5.4节。保护之前其值必须(**MUST**)设置为0。终端在移除数据包保护和包头保护后,接收到的这些位具有非零值的数据包必须(**MUST**)视为PROTOCOL_VIOLATION类型的连接错误。仅在去除包头保护后丢弃此类数据包会使终端暴露于攻击,参见[QUIC-TLS]第9.5节。 - -* Packet Number Length:包号长度,在包含Packet Number字段的数据包类型中,字节0的最低两个有效位(掩码为0x03)表示Packet Number字段的长度,编码为无符号的两位整数,比Packet Number字段的长度(以字节为单位)小1,即Packet Number字段的长度是该字段的值加一。这些位使用包头保护来保护,参见[QUIC-TLS]第5.4节。 - -* Length:长度,这是数据包剩余部分的长度(即Packet Number和Packet Payload字段),以字节为单位,编码为变长整数(第16章)。 - -* Packet Number:包号,该字段的长度为1到4个字节。包号采用包头保护,参见[QUIC-TLS]第5.4节。 Packet Number字段的长度编码在字节0的Packet Number Length位中,如上所述。 - -* Packet Payload:包负载,这是数据包的有效载荷——包含一系列帧——使用数据包保护策略进行保护。 - -### 17.2.1. 版本协商包(Version Negotiation Packet) - -Version Negotiation包本质上不是特定于版本的。客户端收到后,将Version字段值为0的数据包识别为Version Negotiation包。 - -Version Negotiation包仅由服务端发送,是在收到不支持版本的客户端数据包时回的响应。 - -Version Negotiation包的格式是: - -``` - Version Negotiation Packet { - Header Form (1) = 1, - Unused (7), - Version (32) = 0, - Destination Connection ID Length (8), - Destination Connection ID (0..2040), - Source Connection ID Length (8), - Source Connection ID (0..2040), - Supported Version (32) ..., - } -``` -图14: Version Negotiation Packet - -服务端可设置Unused字段为任意值,客户端必须(**MUST**)忽略该字段的值。在QUIC可能与其他协议复用的情况下(参见[RFC7983]),服务端应该(**SHOULD**)将此字段的最高有效位(0x40)设置为1,以便Version Negotiation包看起来具有如上所述的固定位(Fixed Bit)字段。请注意,其他版本的QUIC可能不会遵循类似的建议。 - -Version Negotiation包的版本字段必须(**MUST**)设置为0x00000000。 - -服务端必须(**MUST**)将DCID字段设置为其接收到的数据包的SCID字段的值。SCID的值必须(**MUST**)从接收到的数据包的DCID的值复制而来,它最初是由客户端随机选择的。回显两个CID为客户端提供了一些证明,即服务端收到了数据包,并且Version Negotiation包不是由没有收到Initial包的实体生成的。 - -未来版本的QUIC可能对CID的长度有不同的要求。特别是,CID可能具有较小的最小长度或较大的最大长度。因此,CID的版本相关规则不得(**MUST NOT**)影响关于是否发送Version Negotiation包的决定。 - -Version Negotiation包的其余部分是服务端支持的版本列表,每个版本号都是32位的。 - -Version Negotiation包不需要确认。它仅在响应指示不支持版本的数据包时发送,参见第5.2.2小节。 - -Version Negotiation包不包含包号和包长字段,不过在长包头格式的其他数据包中是存在的。Version Negotiation包会消耗整个UDP报文。 - -服务端不得(**MUST NOT**)发送多个Version Negotiation包以响应单个UDP报文。 - -有关版本协商过程的描述,请参见第6章。 - -### 17.2.2. Initial包(Initial Packet) - -Initial包使用类型值为0x00的长包头。它携带客户端和服务端发送的第一个CRYPTO帧以执行密钥交换,双向都可携带ACK帧。 -``` -Initial Packet { - Header Form (1) = 1, - Fixed Bit (1) = 1, - Long Packet Type (2) = 0, - Reserved Bits (2), - Packet Number Length (2), - Version (32), - Destination Connection ID Length (8), - Destination Connection ID (0..160), - Source Connection ID Length (8), - Source Connection ID (0..160), - Token Length (i), - Token (..), - Length (i), - Packet Number (8..32), - Packet Payload (8..), -} -``` -图15: Initial Packet - -Initial包包含一个长包头,内含长度或包号等字段,参见第17.2节。第一个字节包含保留位和包号长度位,也参见第17.2节。在SCID和Length字段之间,有两个额外的字段是Initial包特有的: - -* Token Length:令牌长度,变长整数,指定Token字段的长度,以字节为单位。如果不存在Token,则此值为0。服务端发送的Initial包必须(**MUST**)将Token Length字段置为0。客户端收到具有非零Token Length字段的Initial包,必须(**MUST**)要么丢弃该数据包,要么回以类型为PROTOCOL_VIOLATION的连接错误。 - -* Token :令牌,先前在Retry包或NEW_TOKEN帧中提供的令牌值,参见第8.1节。 - -为了防止被版本未知的中间设备篡改,Initial包使用与连接和版本相关的密钥(Initial密钥)保护,如[QUIC-TLS]中所述。这种方式对可以观察数据包的攻击者来说,不能提供机密性或完整性保护,但可以防止无法观察到数据包的攻击者伪装Initial包。 - -客户端和服务端在发送包含初始加密握手消息的任何数据包时,都采用Initial包类型。这规则适用于需要创建包含初始加密消息的新数据包的所有场景,例如在收到Retry包后须发送数据包时,参见第17.2.5小节。 - -服务端发送它的第一个Initial包以响应客户端Initial包。服务端可以(**MAY**)发送多个Initial包。加密密钥交换可能需要多次往返或重传。 - -Initial包的负载包括一个或多个包含了加密握手消息的CRYPTO帧或ACK帧,或者两者都有。0x1c类型的PING、PADDING和CONNECTION_CLOSE帧也是允许的。接收到包含其他帧的Initial包的终端可以将其视为虚假数据包或连接错误。 - -客户端发送的第一个数据包总是包含一个CRYPTO帧,该帧包含第一个加密握手消息的起始部分或全部。第一个CRYPTO帧总是从偏移量0开始,参见第7章。 - -请注意,如果服务端发送TLS HelloRetryRequest(参见[QUIC-TLS]第4.7节),则客户端将发送另一系列Initial包。这些Initial包将继续加密握手,并将包含CRYPTO帧,其起始偏移量与Initial包的第一次发送的CRYPTO帧的大小相匹配。 - -#### 17.2.2.1. 丢弃Initial包(Abandoning Initial Packets) - -客户端在发送第一个Handshake包后停止发送和处理Initial包。服务端在收到第一个Handshake包后停止发送和处理Initial包。尽管数据包可能仍在传输中或等待确认,但在此之后无需再交换Initial包。Initial包保护密钥与任何丢包恢复和拥塞控制状态(参见[QUIC-RECOVERY]第6.4节)一起被丢弃(参见[QUIC-TLS]第4.9.1小节)。 - -当Initial密钥被丢弃时,CRYPTO帧中的任何数据都会被丢弃,并且不再重传。 - -### 17.2.3. 0-RTT包(0-RTT) - -0-RTT包使用类型值为0x01的长包头,后跟Length或Packet Number字段,参见第17.2节。第一个字节包含保留位和包号长度位,参见第17.2节。作为首次传输的一部分,0-RTT包用于将early数据从客户端发往服务端,在Handshake完成之前。作为TLS握手的一部分,服务端可以接受或拒绝此数据。 - -有关0-RTT数据及其局限性的讨论,请参见[TLS13]第2.3节。 - -``` -0-RTT Packet { - Header Form (1) = 1, - Fixed Bit (1) = 1, - Long Packet Type (2) = 1, - Reserved Bits (2), - Packet Number Length (2), - Version (32), - Destination Connection ID Length (8), - Destination Connection ID (0..160), - Source Connection ID Length (8), - Source Connection ID (0..160), - Length (i), - Packet Number (8..32), - Packet Payload (8..), -} -``` -图16: 0-RTT Packet - -0-RTT保护数据包与1-RTT保护数据包使用相同的包号空间。 - -客户端收到Retry报文,很可能是0-RTT报文丢失或被服务端丢弃。客户端应该(**SHOULD**)在发送新的Initial包后尝试重新发送0-RTT包中的数据。新数据包必须(**MUST**)使用新的包号,如第17.2.5.3小节所述,重用包号可能会损害数据包保护。 - -如[QUIC-TLS]第4.1.1小节中所定义,客户端仅在Handshake完成后才接收其0-RTT包的确认。 - -一旦开始处理来自服务端的1-RTT包,客户端不得(**MUST NOT**)发送0-RTT包。这意味着0-RTT包不能包含对来自1-RTT包的帧的任何响应。例如,客户端不能在0-RTT包中发送ACK帧,因为ACK帧只能确认1-RTT包。1-RTT包的确认必须(**MUST**)在1-RTT包中携带。 - -服务端应该(**SHOULD**)将违反之前保存的(传输参数)限制的情况(参见第7.4.1小节)视为适当类型的连接错误(例如,超出流控限制的FLOW_CONTROL_ERROR)。 - -### 17.2.4. Handshake握手包(Handshake Packet) - -Handshake包使用类型值为0x02的长包头,后跟Length或Packet Number字段,参见第17.2节。第一个字节包含保留位和包号长度位,参见第17.2节。它用于携带来自服务端和客户端的加密握手消息和确认。 -``` -Handshake Packet { - Header Form (1) = 1, - Fixed Bit (1) = 1, - Long Packet Type (2) = 2, - Reserved Bits (2), - Packet Number Length (2), - Version (32), - Destination Connection ID Length (8), - Destination Connection ID (0..160), - Source Connection ID Length (8), - Source Connection ID (0..160), - Length (i), - Packet Number (8..32), - Packet Payload (8..), -} -``` -图17: Handshake Protected Packet - -一旦客户端收到来自服务端的Handshake包,它就会使用Handshake包向服务端发送后续的加密握手消息和确认。 - -Handshake包中的DCID字段包含一个由数据包接收方选择的CID,SCID是数据包的发送方希望对端使用的CID,参见第17.2节。 - -Handshake包有自己的包号空间,因此服务端发送的第一个Handshake包的包号为0。 - -此数据包的负载包含CRYPTO帧,也可能包含PING、PADDING或ACK帧。Handshake包可能(**MAY**)包含0x1c类型的CONNECTION_CLOSE帧。终端必须(**MUST**)将接收到其他帧的Handshake包视为PROTOCOL_VIOLATION类型的连接错误。 - -与Initial包(参见第17.2.2.1小节)一样,当Handshake保护密钥被丢弃时,其中的CRYPTO帧中的数据将被丢弃,并且不再重传。 - -### 17.2.5. Retry包(Retry Packet) - -如图18所示,Retry包使用类型值为0x03的长包头。它携带由服务端生成的地址验证令牌。仅由希望进行重试的服务端使用,参见第8.1节。 -``` -Retry Packet { - Header Form (1) = 1, - Fixed Bit (1) = 1, - Long Packet Type (2) = 3, - Unused (4), - Version (32), - Destination Connection ID Length (8), - Destination Connection ID (0..160), - Source Connection ID Length (8), - Source Connection ID (0..160), - Retry Token (..), - Retry Integrity Tag (128), -} -``` -图18: Retry Packet - -Retry包不包含任何受保护的字段。 Unused字段中的值由服务端设置为任意值,客户端必须(**MUST**)忽略它。除了来自长包头的字段外,它还包含以下附加字段: - -* Retry Token:重试令牌,服务端可用于验证客户端地址的不透明令牌。 - -* Retry Integrity Tag:重试完整性标签,在[QUIC-TLS]第5.8节("Retry Packet Integrity") 中定义。 - -#### 17.2.5.1. 发送Retry包(Sending a Retry Packet) -服务端将客户端Initial包的SCID填入Retry包的DCID字段。 - -服务端在SCID字段中包含其选择的CID。该值不得(**MUST NOT**)等于客户端发送的数据包的DCID字段。客户端必须(**MUST**)丢弃包含与Initial包的DCID字段相同的SCID字段的Retry包。客户端必须(**MUST**)在它发送的后续数据包的DCID字段中使用Retry包的SCID字段中的值。 - -服务端可以(**MAY**)发送Retry包以响应Initial和0-RTT包。服务端可以丢弃或缓冲它收到的0-RTT包。服务端可以在接收Initial或0-RTT包时发送多个Retry包。服务端不得(**MUST NOT**)发送多个Retry包以响应单个UDP报文。 - -#### 17.2.5.2. 处理Retry包(Handling a Retry Packet) -对于每次连接尝试,客户端必须(**MUST**)最多接受和处理一个Retry包。在客户端接收并处理来自服务端的Initial或Retry包后,它必须(**MUST**)丢弃它接收到的任何后续Retry包。 - -客户端必须(**MUST**)丢弃具有无法验证的重试完整性标签的Retry包,参见[QUIC-TLS]第5.8节。这会削弱攻击者注入Retry包的能力并防止重Retry包意外损坏。客户端必须(**MUST**)丢弃带有零长度Retry Token字段的Retry包。 - -客户端使用包含提供的重试令牌的Initial包响应Retry包以继续建立连接。 - -客户端将此Initial包的DCID字段设置为Retry包中SCID字段的值。更改DCID字段还会导致更改用于保护Initial包的密钥。还需要将Token字段设置为Retry包中提供的令牌。客户端不得(**MUST NOT**)更改SCID,因为服务端会将CID作为其令牌验证逻辑的一部分,参见第8.1.4小节。 - -Retry包不包含包号,客户端无法明确确认。 - -#### 17.2.5.3. 重试后继续握手(Continuing a Handshake after Retry) -来自客户端的后续Initial包包含来自Retry包的CID和令牌值。客户端将SCID字段从Retry包复制到DCID字段并使用该值,直到收到具有更新值的Initial包,参见第7.2节。 Token字段的值被复制到所有后续的Initial包中,参见第8.1.2小节。 - -除了更新DCID和Token字段外,客户端发送的Initial包与第一个Initial包受到相同的限制。客户端必须(**MUST**)使用它包含在此数据包中的相同加密握手消息。服务端可以(**MAY**)将包含不同加密握手消息的数据包视为连接错误或丢弃它。请注意,包含Token字段会减少加密握手消息的可用空间,这可能导致客户端需要发送多个Initial包。 - -客户端可以(**MAY**)通过向服务端提供的CID发送0-RTT包,在收到Retry包后尝试0-RTT。 - -在处理Retry包后,客户端不得(**MUST NOT**)在任何包号空间重置包号。特别是,0-RTT包包含加密信息,这些信息很可能会在收到Retry包时重新传输。用于保护这些新0-RTT包的密钥不会因响应Retry包而改变。但是,这些数据包中发送的数据可能与之前发送的数据不同。使用相同的包号发送这些新数据包可能会损害这些数据包的数据包保护,因为相同的密钥和随机数可用于保护不同的内容。如果服务端检测到客户端重置了包号,则它可以(**MAY**)中止连接。 - -在客户端和服务端之间交换的Initial和Retry包中使用的CID需要复制到传输参数中,并按照第7.3节的描述进行验证。 - -## 17.3. 短包头数据包(Short Header Packets) - -此QUIC版本定义了使用短数据包头的单个数据包类型。 - -### 17.3.1. 1-RTT包(1-RTT Packet) - -1-RTT包使用短包头。它在版本协商和1-RTT秘钥协商后使用。 - -``` -1-RTT Packet { - Header Form (1) = 0, - Fixed Bit (1) = 1, - Spin Bit (1), - Reserved Bits (2), - Key Phase (1), - Packet Number Length (2), - Destination Connection ID (0..160), - Packet Number (8..32), - Packet Payload (8..), -} -``` -图19: 1-RTT Packet - -1-RTT包包含以下字段: - -* Header Form:报头格式,对于短包头,字节0的最高有效位 (掩码为0x80) 设置为0。 - -* Fixed Bit:固定位,字节0的下一位 (掩码为0x40) 设置为1。该位为0的数据包在此版本中不是有效数据包,必须(**MUST**)丢弃。该位的值为1允许QUIC与其他协议复用,请参见[RFC7983]。 - -* Spin Bit:自旋位,字节0的第三个最高有效位 (掩码为0x20) 是延迟自旋位,按第17.4节所述设置。 - -* Reserved Bits:保留位,字节0的下两位(掩码为0x18)是保留位。这些位使用包头保护策略来保护,参见[QUIC-TLS]第5.4节。在保护之前其值必须(**MUST**)设置为0。收到数据包且在移除包和包头保护后,这些位为非零值的话,终端必须(**MUST**)将之视为PROTOCOL_VIOLATION类型的连接错误。仅在去除包头保护后丢弃此类数据包会使终端暴露于攻击,参见[QUIC-TLS].的第9.5节。 - -* Key Phase:秘钥阶段,字节0的下一位 (掩码为0x04) 表示密钥时段,它允许数据包的接收方识别用于保护数据包的密钥。详情参阅[QUIC-TLS]。该位采用包头保护,参见[QUIC-TLS]第5.4节。 - -* Packet Number Length:包号长度,字节0的最低有效两位(掩码为0x03)表示Packet Number字段的长度,编码为无符号的两位整数,比Packet Number字段的长度(以字节为单位)小1,即Packet Number字段的长度是该字段的值加一。这些位采用包头保护,参见[QUIC-TLS]第5.4节。 - -* Destination Connection ID:目的连接ID(DCID),DCID是由数据包的预期接收方选择的CID。更多详细信息,请参阅第5.1节。 - -* Packet Number:包号,长度为1到4个字节。Packet Number字段采用包头保护,参见[QUIC-TLS]第5.4节。Packet Number字段的长度在Packet Number Length字段中编码。有关详细信息,请参阅第17.1节。 - -* Packet Payload:数据包负载,1-RTT包中始终包含受1-RTT保护的有效载荷。 - -短包头包的报头格式位和DCID字段与版本无关。其余字段与所选的QUIC版本有关。来自不同QUIC版本的数据包如何解释详细信息,请参阅[QUIC-INVARIANTS]。 - -## 17.4. 延迟自旋位(Latency Spin Bit) - -为1-RTT包定义的延迟自旋位(第17.3.1小节),可以启动整个连接期间从网络路径上的观察点的被动延迟监控。服务端反射收到的旋转值,而客户端在一个RTT后“翻转”它。on-path观察者可以测量两个自旋位翻转事件之间的时间,以估计连接的终端到端RTT。 - -自旋位仅出现在1-RTT包中,因为可以通过观察握手来测量连接的Initial RTT。因此,在版本协商和连接建立完成后,自旋位即可用。[QUIC-MANAGEABILITY]中进一步讨论了在路径上进行测量和延迟自旋位的使用方式。 - -自旋位是此版本QUIC的可选(**OPTIONAL**)功能。不支持该特性的终端必须(**MUST**)禁用它,如下所述。 - -每个终端单方面决定是否启用或禁用连接的自旋位。实现必须(**MUST**)允许客户端和服务端的上层应用全局禁用自旋位或只在每个连接的基础上禁用。即使上层应用没有禁用自旋位,终端也必须(**MUST**)随机选择每16个网络路径中的至少一个,或每16个CID中的一个,禁用自旋位的使用,以确保QUIC连接不启用自旋位在网络上能经常观察到。由于每个终端单方面地禁用自旋位,这可以保证有大约八分之一的网络路径上禁用自旋位信号。 - -当自旋位被禁用时,终端可以(**MAY**)将自旋位设置为任意值并且必须(**MUST**)忽略任何入向值。建议(**RECOMMENDED**)终端将自旋位设置为随机值,可为每个数据包独立选择或为每个CID独立选择。 - -如果在连接上启用了自旋位,终端会为每条网络路径维护一个自旋值,并在该路径上发送1-RTT包时将包头中的自旋位设置为当前存储的值。每条网络路径的终端中的自旋值初始化为0。每个终端还记住从其对端看到的每条路径上的最高包号。 - -当服务端收到一个1-RTT包时,如果服务端从给定网络路径上的看到的客户端的最高包号递增,它将该路径的自旋值设置为收到的数据包中的自旋位。 - -当客户端收到一个1-RTT包时,如果客户端从给定网络路径上的看到的服务端的最高包号递增,它将该路径的自旋值设置为接收到的数据包中自旋位的翻转值。 - -当变更该网络路径上使用的CID时,终端会将该网络路径的自旋值重置为0。 - -# 18. 传输参数编码(Transport Parameter Encoding) - -[QUIC-TLS]中定义的quic_transport_parameters扩展的extension_data字段包含QUIC传输参数。它们被编码为一系列传输参数,如图20所示: -``` -Transport Parameters { - Transport Parameter (..) ..., -} -``` -图20: Sequence of Transport Parameters - -每个传输参数都被编码为一个(标识符、长度、值)三元组,如图21所示: - -``` -Transport Parameter { - Transport Parameter ID (i), - Transport Parameter Length (i), - Transport Parameter Value (..), -} -``` -图21: Transport Parameter Encoding - -Transport Parameter Length字段是以字节为单位的Transport Parameter Value字段的长度。 - -QUIC将传输参数编码为字节流,然后在加密握手时交互。 - -## 18.1. 保留传输参数(Reserved Transport Parameters) -保留传输参数的ID是具有 31 * N + 27 形式的标识符,其中N为整数,其引入的目的是为了执行忽略未知传输参数的要求。这些传输参数没有语义,可以携带任意值。 - -## 18.2. 传输参数定义(Transport Parameter Definitions) - -本节详细介绍本文中定义的传输参数。 - -此处列出的许多传输参数都具有整数值。标识为整数的传输参数使用变长整数编码,请参阅第16章。除非另有说明,否则如果传输参数不存在,则其默认值为0。 - -传输参数定义如下: -* original_destination_connection_id (0x00):该参数是客户端发送的第一个Initial包中的DCID字段的值,见第7.3节。此传输参数仅由服务端发送。 - -* max_idle_timeout (0x01):最大空闲超时时间是一个以毫秒为单位的值,它被编码为一个整数,见(第10.1节)。当两端都忽略此传输参数或指定值为0时,空闲超时被禁用。 - -* stateless_reset_token (0x02):无状态重置令牌(Stateless Reset Token)用于验证无状态重置,参见第10.3节。该参数是一个16字节的字符串。该传输参数不得(**MUST NOT**)由客户端发送,只可以(**MAY**)由服务端发送。不发送此传输参数的服务端不能对握手期间协商的CID使用无状态重置(第10.3节)。 - -* max_udp_payload_size (0x03):最大UDP负载大小,该参数是一个整数值,用于限制终端愿意接收的UDP负载大小。接收方不太可能处理负载大于此限额的UDP报文。 -此参数的默认值是允许的最大UDP负载65527,低于1200的值无效。 -此限额确实以与路径MTU相同的方式作为对数据报文大小的附加约束,但它是终端的属性而不是路径,参见第14章。预期这是终端专用于保存入向数据包的空间大小。 - -* initial_max_data (0x04):初始最大数据大小,该参数是一个整数值,指示可以在连接上发送的最大数据量(字节数)的初始值。这相当于在完成握手后立即为连接发送一个MAX_DATA(参见第19.9节)。 - -* initial_max_stream_data_bidi_local (0x05):此参数是一个整数值,指定本地发起的双向流的初始流控限额。此限额适用于由发送传输参数的终端打开的新创建的双向流。在客户端传输参数中,这适用于标识符的最低有效位设置为0x00的流,在服务端传输参数中,这适用于最低有效位设置为0x01的流。 - -* initial_max_stream_data_bidi_remote (0x06):此参数是一个整数值,指定对端发起的双向流的初始流控限额。此限额适用于由接收传输参数的终端打开的新创建的双向流。在客户端传输参数中,这适用于标识符的最低有效位设置为0x01的流,在服务端传输参数中,这适用于最低有效两位设置为0x00的流。 - -* initial_max_stream_data_uni (0x07):此参数是一个整数值,指定单向流的初始流控限额。此限额适用于由接收传输参数的终端打开的新创建的单向流。在客户端传输参数中,这适用于标识符的最低有效位设置为0x03的流,在服务端传输参数中,这适用于最低有效两位设置为0x02的流。 - -* initial_max_streams_bidi (0x08):初始最大双向流个数,此参数是一个整数值,指示接收此传输参数的终端允许发起的初始最大双向流个数。如果此参数不存在或为零,则在发送MAX_STREAMS帧之前,对端无法打开双向流。设置这个参数相当于发送一个具有相同值的对应类型的MAX_STREAMS(第19.11节)。 - -* initial_max_streams_uni (0x09):初始最大单向流个数,此参数是一个整数值,指示接收此传输参数的终端允许发起的初始最大单向流个数。如果此参数不存在或为零,则在发送MAX_STREAMS帧之前,对端无法打开单向流。设置这个参数相当于发送一个具有相同值的对应类型的MAX_STREAMS(第19.11节)。 - -* ack_delay_exponent (0x0a):确认延迟指数,此参数是一个整数值,指示用于解码ACK帧中的ACK延迟字段的幂(第19.3节)。如果此值不存在,则假定默认值为3(表示乘数为8),超过20的值无效。 - -* max_ack_delay (0x0b):最大确认延迟,此参数是一个整数值,表示终端将延迟发送确认的最长时间(以毫秒为单位)。这个值应该(**SHOULD**)包括接收端告警触发的预期延迟。例如,如果接收端将定时器设置为5毫秒,而告警通常最多延迟1毫秒,则它应该(**SHOULD**)发送6毫秒的max_ack_delay。如果此值不存在,则假定默认值为25毫秒,2^14^或更大的值无效。 - -* disable_active_migration (0x0c):如果终端不支持在握手期间正在使用的地址上的主动连接迁移(第9章),则需要设置此参数,禁用主动迁移。接收此传输参数的终端在握手期间在发包给对端时不得(**MUST NOT**)使用新的本地地址。在客户端对preferred_address传输参数进行操作后,可以解禁连接迁移。此参数是零长度值。 - -* preferred_address(0x0d):服务端的首选地址,用于在握手结束时影响服务端地址的更改,如第9.6节所述。此传输参数仅由服务端发送。服务端可以(**MAY**)选择只发送一个地址族的首选地址,方法是为另一个地址族发送全零地址和端口(0.0.0.0:0或[::]:0)。IP地址以网络字节序编码。 -preferred_address传输参数包含IPv4和IPv6的地址和端口。四字节IPv4 Address字段后跟相关的两字节IPv4 Port字段。后面跟着一个16字节的IPv6 Address字段和两字节的IPv6 Port字段。在地址和端口对之后,Connection ID Length字段指示了随后的CID字段的长度。最后,一个16字节的Stateless Reset Token字段包括与CID关联的无状态重置令牌。此传输参数的格式如下图22所示。 -CID字段和Stateless Reset Token字段包含一个序列号为1的替代CID,参见第5.1.1小节。将这些值与preferred_address一起发送可确保在客户端启动到preferred_address的迁移时至少有一个未使用的活动CID。 -preferred_address的CID和Stateless Reset Token字段在语法和语义上与NEW_CONNECTION_ID帧(第19.15节)的相应字段相同。选择零长度CID的服务端不得(**MUST NOT**)提供首选地址。类似地,服务端不得(**MUST NOT**)在此传输参数中包含零长度的CID。客户端必须(**MUST**)将违反这些规定的行为视为TRANSPORT_PARAMETER_ERROR类型的连接错误。 -``` -Preferred Address { - IPv4 Address (32), - IPv4 Port (16), - IPv6 Address (128), - IPv6 Port (16), - Connection ID Length (8), - Connection ID (..), - Stateless Reset Token (128), -} -``` -图22: Preferred Address Format - -* active_connection_id_limit (0x0e):该参数是一个整数值,指示终端愿意存储的来自对端的最大CID个数。该值包括在握手期间收到的CID、在preferred_address传输参数中收到的CID以及在NEW_CONNECTION_ID帧中收到的CID。 active_connection_id_limit参数的值必须(**MUST**)至少为2。收到小于2的值的终端必须(**MUST**)关闭连接,错误类型为TRANSPORT_PARAMETER_ERROR。如果此传输参数不存在,则假定默认值为2。如果终端发出零长度CID,其后永远不会发送NEW_CONNECTION_ID帧,因此会忽略从其对端收到的active_connection_id_limit值。 - -* initial_source_connection_id (0x0f):这是终端在连接上发送的第一个Initial包的SCID字段中填写的值,参见第7.3节。 - -* retry_source_connection_id (0x10):这是服务端在Retry包的SCID字段填写的值,参见第7.3节。此传输参数仅由服务端发送。 - -如果初始流控限额(initial_max_stream_data_bidi_local、initial_max_stream_data_bidi_remote或initial_max_stream_data_uni)传输参数存在,则其等效于在打开后立即在相应类型的每个流上发送MAX_STREAM_DATA帧(第19.10节)。如果传输参数不存在,则该类型的流以0的流控限额开始。 - -客户端不得(**MUST NOT**)包含任何仅服务端适用的传输参数:original_destination_connection_id、preferred_address、retry_source_connection_id或stateless_reset_token。服务端收到上述传输参数,必须(**MUST**)将其视为TRANSPORT_PARAMETER_ERROR类型的连接错误。 - -# 19. 帧类型和格式(Frame Types and Formats) -如第12.4节所述,数据包包含一个或多个帧。本节描述核心QUIC帧类型的格式和语义。 - -## 19.1. PADDING帧(PADDING Frames) -PADDING帧(Type=0x00)没有其他语义值。PADDING帧可用于增加数据包的大小。PADDING帧可用于将Initial包填充到所需的最小大小或为受保护数据包提供针对流量分析的保护。 - -PADDING帧的格式如图23所示,这表明PADDING帧没有内容。即PADDING帧只由将帧标识为PADDING帧的单个字节组成。 - -``` -PADDING Frame { - Type (i) = 0x00, -} -``` -图23: PADDING Frame Format - -## 19.2. PING帧(PING Frames) -终端可以使用PING帧(Type=0x01)来验证其对端是否仍然存在或检查对端的可达性。 - -PING帧的格式如图24所示,这表明PING帧没有内容。 - -``` -PING Frame { - Type (i) = 0x01, -} -``` -图24: PING Frame Format - -PING帧的接收方只需要确认包含该帧的数据包。 - -当应用或应用层协议希望防止连接超时时,PING帧可用于保持连接处于活动状态,参见第10.1.2小节。 - -## 19.3. ACK帧(ACK Frames) -接收方通过发送ACK帧(Type为0x02和0x03)告知发送方他们已接收和处理数的据包。ACK帧包含一个或多个ACK Range。ACK Range标识已确认的数据包。如果帧类型为0x03,则ACK帧还包含直到此时为止,在连接上接收到的有ECN标记的QUIC包的累积计数。QUIC实现必须(**MUST**)正确处理这两种类型,并且,如果启用了ECN,终端应该(**SHOULD**)使用ECN部分中的信息来管理他们的拥塞状态。 - -QUIC确认是不可撤销的。一旦确认,数据包将保持确认状态,甚至不会出现在未来的ACK帧中。这与TCP选择性确认 (SACK)[RFC2018]的违例不同。 - -来自不同包号空间的数据包可以使用相同的包号。对数据包的确认需要指明包号和包号空间。这是通过约定承载ACK帧的数据包,只能与被确认的数据包具有相同包号空间来实现的。 - -Version Negotiation包和Retry包不需要确认,因为它们不含包号。这些数据包不依赖于ACK帧,而是由客户端发送的下一个Initial包隐式确认。 - -ACK帧的格式如图25所示。 - -``` -ACK Frame { - Type (i) = 0x02..0x03, - Largest Acknowledged (i), - ACK Delay (i), - ACK Range Count (i), - First ACK Range (i), - ACK Range (..) ..., - [ECN Counts (..)], -} -``` -图25: ACK Frame Format - -ACK帧包含以下字段: -* Largest Acknowledged:最大确认包号,变长整数,表示对端确认的最大数据包号,这通常是对端在生成ACK帧之前收到的最大包号。与QUIC长或短包头中的包号不同,ACK帧中的值不会被截断。 - -* ACK Delay:确认延迟,变长整数,确认延迟以微秒为单位,参见第13.2.5小节。通过将字段中的值乘以2^ack_delay_exponent^来解码,其中ack_delay_exponent是对端约定的传输参数,参见第18.2节。与简单地将延迟表示为整数相比,这种编码允许在相同字节数内使用更大范围的值,但代价是分辨率降低。 - -* ACK Range Count:ACK Range个数,变长整数,指定帧中ACK Range字段的个数。 -* First ACK Range:第一个ACK Range,变长整数,指示在最大确认包号之前收到的连续数据包的个数。也就是说,该范围确认的最小数据包号等于Largest Acknowledged - First ACK Range。 - -* ACK Range:确认范围,包含额外的未确认(Gap) 数据包个数和确认 (ACK Range)数据包个数,两者交替出现,参见第19.3.1小节。 - -* ECN Counts:ECN计数,三项ECN计数,参见第19.3.2小节。 - -### 19.3.1. 确认范围(ACK Range) -每个ACK Range由交替的Gap和ACK Range Length值组成,按包号降序排列。ACK Range可以重复。Gap和ACK Range Length值的总个数由ACK Range Count字段决定,两者分开累加计入ACK Range Count。 - -ACK Range的结构如图26所示。 -``` -ACK Range { - Gap (i), - ACK Range Length (i), -} -``` -图26: ACK Ranges - -形成每个ACK Range的字段是: - -* Gap:间隔,变长整数,表示在前面Range中最小确认包之前连续未确认的数据包的数量,编码值比实际个数小1。 - -* ACK Range Length:ACK范围长度,变长整数,表示在前面Gap中最小未确认包之前连续确认的数据包的数量。 - -Gap和ACK Range Length值使用相对整数编码以提高效率。虽然每个编码值都是正数,但这些值是相减的,因此每个ACK Range表示的包号逐渐降低。 - -每个ACK Range通过给出该范围中最大包号之前已确认的包数来确认一段连续范围的数据包。值为0表示仅确认最大的包号。ACK Range值越大表示范围越大,相应的,ACK Range值越小表示范围越小。因此,给定范围的最大包号,最小值由以下公式确定: - smallest = largest - ack_range -ACK Range确认最小包号和最大包号之间的所有数据包。 - -每个ACK Range的最大值是通过计算减去所有前面的ACK Range Length或Gap的大小来确定的。 - -每个Gap表示未被确认的数据包范围。Gap中的数据包个数比Gap字段的编码值大1。 - -Gap字段的值使用以下公式为后续ACK Range确定最大包号值: - largest = previous_smallest - gap - 2 -如果出现计算的包号为负数,终端必须(**MUST**)生成FRAME_ENCODING_ERROR类型的连接错误。 - -### 19.3.2. ECN计数(ECN Counts) -ACK帧使用类型值(即类型0x03)的最低有效位来表示ECN反馈,并报告QUIC包的IP报头中接收到具有相关ECN码点ECT(0)、ECT(1)或ECN-CE。ECN计数仅在ACK帧类型为0x03时出现。 - -当ACK帧携带ECN时,共有三个ECN计数,如图27所示: - -``` -ECN Counts { - ECT0 Count (i), - ECT1 Count (i), - ECN-CE Count (i), -} -``` -图27: ECN Count Format - -ECN Counts字段有: - -* ECT0 Count:ECT0计数,变长整数,表示与ACK帧相同的包号空间中接收到的携带ECT(0) 码点的数据包总数。 - -* ECT1 Count:ECT1计数,变长整数,表示与ACK帧相同的包号空间中接收到的携带ECT(1) 码点的数据包总数。 - -* ECT-CE Count:ECN-CE计数,变长整数,表示与ACK帧相同的包号空间中接收到的携带ECN-CE码点的数据包总数。 - -ECN计数为每个包号空间单独维护。 - -## 19.4. RESET_STREAM帧(RESET_STREAM Frames) -终端使用RESET_STREAM帧(Type=0x04)立即终止流的发送部分。 - -发送RESET_STREAM后,终端停止传输和重传已识别流上的STREAM帧。RESET_STREAM的接收方可以丢弃在该流上已接收到的任何数据。 - -在单向发送流上收到RESET_STREAM帧,终端必须(**MUST**)以STREAM_STATE_ERROR错误码终止连接。 - -RESET_STREAM帧的格式如图28所示。 - -``` -RESET_STREAM Frame { - Type (i) = 0x04, - Stream ID (i), - Application Protocol Error Code (i), - Final Size (i), -} -``` -图28: RESET_STREAM Frame Format - -RESET_STREAM帧包含以下字段: - -* Stream ID:流标识,变长整数,表示正在终止的流的流ID。 - -* Application Protocol Error Code:应用层协议错误码,变长整数,包含应用层协议错误码(参见第20.2节),指示流关闭的原因。 - -* Final Size:最终大小,变长整数,指示RESET_STREAM发送方的流的最终大小,以字节为单位,参见第4.5节。 - -## 19.5. STOP_SENDING帧(STOP_SENDING Frames) -终端使用STOP_SENDING帧(Type=0x05)来向发送方传达入向数据被每个应用请求接收时丢弃的信息。STOP_SENDING请求对端停止在该流上发送数据。 - -可以为处于Recv或Size Known 状态的流发送STOP_SENDING帧,参见第3.2节。在本地发起但尚未完成创建的流上收到STOP_SENDING帧,必须(**MUST**)视为STREAM_STATE_ERROR类型的连接错误。在receive-only流上接收到STOP_SENDING帧的终端必须(**MUST**)以STREAM_STATE_ERROR错误码终止连接。 - -STOP_SENDING帧的格式如图29所示。 - -``` -STOP_SENDING Frame { - Type (i) = 0x05, - Stream ID (i), - Application Protocol Error Code (i), -} -``` -图29: STOP_SENDING Frame Format - -STOP_SENDING帧包含以下字段: - -* Stream ID:流ID,变长整数,携带被忽略流的流ID的变长整数。 -* Application Protocol Error Code:应用层协议错误码,变长整数,包含应用指定的发送方忽略流的原因,参见第20.2节。 - -## 19.6. CRYPTO帧(CRYPTO Frame) -CRYPTO帧(Type=0x06)用于传输加密握手消息。它可以在除0-RTT之外的所有数据包类型中发送。CRYPTO帧为加密协议提供了一个有序的字节流。CRYPTO帧在功能上与STREAM帧相同,只是其不带流ID,不受流控,不携带可选偏移量、可选长度和流结束标记。 - -CRYPTO帧的格式如图30所示。 - -``` -CRYPTO Frame { - Type (i) = 0x06, - Offset (i), - Length (i), - Crypto Data (..), -} -``` -图30: CRYPTO Frame Format - -CRYPTO帧包含以下字段: - -* Offset :偏移,变长整数,指定此CRYPTO帧中数据在流中的字节偏移量。 - -* Length:长度,变长整数,指定此CRYPTO帧中加密数据字段的长度。 - -* Crypto Data:加密数据,加密消息数据负载。 - -每个加密级别都对应一个单独的加密握手数据流,每个数据流都从偏移量0开始。这意味着每个加密级别都被视为单独的CRYPTO数据流。 - -流上传输的最大偏移量——偏移量和数据长度的总和——不能超过2^62^-1。收到超过此限制的帧必须(**MUST**)被视为FRAME_ENCODING_ERROR或CRYPTO_BUFFER_EXCEEDED类型的连接错误。 - -与包含指示数据属于哪个流的流ID的STREAM帧不同,CRYPTO帧携带每个加密级别的单个流的数据。流没有明确的结束,因此CRYPTO帧没有FIN位。 - -## 19.7. NEW_TOKEN帧(NEW_TOKEN Frames) -服务端发送一个NEW_TOKEN帧(Type=0x07),给客户端提供一个令牌,以便在未来连接发送Initial包时在报文中携带。 - -NEW_TOKEN帧的格式如图31所示。 - -``` -NEW_TOKEN Frame { - Type (i) = 0x07, - Token Length (i), - Token (..), -} -``` -图31: NEW_TOKEN Frame Format - -NEW_TOKEN帧包含以下字段: - -* Token Length:令牌长度,变长整数,指定令牌的长度,以字节为单位。 - -* Token :令牌,客户端在未来发送Initial包携带的未明字符块。令牌不得(**MUST NOT**)为空。客户端在收到带有空Token字段的NEW_TOKEN帧时必须(**MUST**)视为FRAME_ENCODING_ERROR类型的连接错误。 - -如果包含NEW_TOKEN帧的数据包被错误地认定为丢失,客户端可能会收到多个包含相同令牌值的NEW_TOKEN帧,客户端需要丢弃重复值。NEW_TOKEN帧可用于关联连接尝试,参见第8.1.3小节。 - -客户端不得(**MUST NOT**)发送NEW_TOKEN帧。服务端收到NEW_TOKEN帧必须(**MUST**)将其视为PROTOCOL_VIOLATION类型的连接错误。 - -## 19.8. STREAM帧(STREAM Frames) -STREAM帧隐式地创建一个流并携带流数据。STREAM帧中的Type字段采用0b00001XXX形式(或从0x08到0x0f的一组值)。帧类型的三个低位决定了帧中存在的字段: - -* 帧类型中的OFF位(0x04)指示是否存在Offset字段。设置为1时,存在Offset字段;设置为0时,Offset字段不存在并且Stream Data从偏移量0开始(即,帧包含流的第一个字节,或数据长度为0的流的末尾)。 - -* 帧类型中的LEN位(0x02)指示是否存在Length字段。如果该位设置为0,则Length字段不存在并且Stream Data字段扩展到数据包的末尾。如果该位设置为1,则存在Length字段。 - -* 帧类型中的FIN位(0x01)指示该帧是否标志着流的结束。流的最终大小是偏移量和该帧的长度之和。 - -如果终端在一个本地发起的尚未创建成功的流或send-only类型的流上收到STREAM帧,它必须(**MUST**)以错误码STREAM_STATE_ERROR终止连接。 - -STREAM帧的格式如图32所示。 - -``` -STREAM Frame { - Type (i) = 0x08..0x0f, - Stream ID (i), - [Offset (i)], - [Length (i)], - Stream Data (..), -} -``` -图32: STREAM Frame Format - -STREAM帧包含以下字段: - -* Stream ID:流标识,变长整数,表示流的流ID,参见第2.1节。 - -* Offset:偏移,变长整数,指定此STREAM帧中数据在流中的字节偏移量。当OFF位设置为1时,此字段存在。当Offset字段不存在时,偏移量为0。 - -* Length:长度,变长整数,指定此STREAM帧中Stream Data字段的长度。该字段在LEN位设置为1时出现。当LEN位设置为0时,数据包中的所有剩余字节都是流数据。 - -* Stream Data:流数据,该流中要传输的字节。 - -当Stream Data字段的长度为0时,STREAM帧中的Offset是将要发送的下一个字节的偏移量。 - -流中的第一个字节的偏移量为0。流上可传输的最大偏移量 ——偏移量和数据长度的总和——不能超过2^62^-1,因为无法为这么大量的数据提供流控限额,收到超过此限制的帧必须(**MUST**)被视为FRAME_ENCODING_ERROR或FLOW_CONTROL_ERROR类型的连接错误。 - -## 19.9. MAX_DATA帧(MAX_DATA Frames) -MAX_DATA帧(Type=0x10)用于流控,以通知对端其可以在整个连接上发送的最大数据量。 - -MAX_DATA帧的格式如图33所示。 -``` -MAX_DATA Frame { - Type (i) = 0x10, - Maximum Data (i), -} -``` -图33: MAX_DATA Frame Format - -MAX_DATA帧包含以下字段: - -* Maximum Data:最大数据量,变长整数,表示整个连接上可以发送的最大数据量,以字节为单位。 - -在STREAM帧中发送的所有数据都计入此限制。所有流的最终大小的总和——包括处于终结状态的流 ——不得(**MUST NOT**)超过接收端通告的Maximum Data值。如果终端接收到的数据大于它发送的Maximum Data,则终端必须(**MUST**)以FLOW_CONTROL_ERROR类型的错误终止连接。这也包括违反早期建链过程中保存的传输参数限制,参见第7.4.1小节。 - -## 19.10. MAX_STREAM_DATA帧(MAX_STREAM_DATA Frames) -MAX_STREAM_DATA帧(Type=0x11)用于流控,以通知对端其可以在该流上发送的最大数据量。 - -可以为处于Recv状态的流发送MAX_STREAM_DATA帧,参见第3.2节。本地发起但尚未创建成功的流收到MAX_STREAM_DATA帧必须(**MUST**)将其视为STREAM_STATE_ERROR类型的连接错误。接收到receive-only流的MAX_STREAM_DATA帧的终端必须(**MUST**)以错误STREAM_STATE_ERROR终止连接。 - -MAX_STREAM_DATA帧的格式如图34所示。 - -``` -MAX_STREAM_DATA Frame { - Type (i) = 0x11, - Stream ID (i), - Maximum Stream Data (i), -} -``` -图34: MAX_STREAM_DATA Frame Format - -MAX_STREAM_DATA帧包含以下字段: - -* Stream ID:流标识,变长整数,关联的流的流ID。 - -* Maximum Stream Data:最大流数据量,变长整数,指示可以在该流上可发送的最大数据量,以字节为单位。 - -当针对此限制计算数据时,终端应该计算在流上发送或接收的数据的最大接收偏移量。丢包和乱序可能意味着该流上接收到的最大偏移量可能大于在该流上接收到的数据的总大小。收到STREAM帧可能不会增加最大接收偏移。 - -在流上发送的数据量不得(**MUST NOT**)超过接收端通告的最大流数据量值。如果终端接收到的数据多于它为该流设置的最大流数据量,则终端必须(**MUST**)以FLOW_CONTROL_ERROR类型错误终止连接。这包括违反建链时保存的传输参数,参见第7.4.1小节。 - -## 19.11. MAX_STREAMS帧(MAX_STREAMS Frames) -MAX_STREAMS帧(Type=0x12或0x13)通知对端其允许打开的给定类型的流的累积个数。类型为0x12的MAX_STREAMS帧适用于双向流,类型为0x13的MAX_STREAMS帧适用于单向流。 - -MAX_STREAMS帧的格式如图35所示。 - -``` -MAX_STREAMS Frame { - Type (i) = 0x12..0x13, - Maximum Streams (i), -} -``` -图35: MAX_STREAMS Frame Format - -MAX_STREAMS帧包含以下字段: - -* Maximum Streams:最大流个数,在连接的生命周期内可以打开的相应类型的流的累积总数。此值不能超过2^60^,因为无法对大于2^62^-1的流ID进行编码。接收到大于此限制的流的帧,必须(**MUST**)将其视为FRAME_ENCODING_ERROR类型的连接错误。 - -丢包和乱序可能会导致终端接收到一个MAX_STREAMS帧,其流个数限制比之前接收到的要低。必须(**MUST**)忽略这种不增加流个数限制的MAX_STREAMS帧。 - -终端不得(**MUST NOT**)打开超过其对端设置的当前流个数限制所允许的流。例如,接收单向流个数限制为3的服务端可以打开流3、7或11,但不能打开流15。如果对端打开的流比原来多,则终端必须(**MUST**)以STREAM_LIMIT_ERROR类型的错误终止连接。这也包括违反建链阶段保存的传输参数,参见第7.4.1小节。 - -请注意,这些帧(以及相应的传输参数)并未指定可以同时打开的流的数量。MAX_STREAMS帧只限制已关闭的流以及打开的流的总数。 - -## 19.12. DATA_BLOCKED帧(DATA_BLOCKED Frames) -当发送方希望发送数据但由于连接级流控而无法发送时,发送方应该(**SHOULD**)发送DATA_BLOCKED帧(Type=0x14),参见第4章。DATA_BLOCKED帧可用作流控算法调整的输入,参见第4.2节。 - -DATA_BLOCKED帧的格式如图36所示。 - -``` -DATA_BLOCKED Frame { - Type (i) = 0x14, - Maximum Data (i), -} -``` -图36: DATA_BLOCKED Frame Format - -DATA_BLOCKED帧包含以下字段: - -* Maximum Data:最大数据量,变长整数,指示阻塞发生时的连接级别的流量限制值。 - -## 19.13. STREAM_DATA_BLOCKED帧(STREAM_DATA_BLOCKED Frames) -当发送方希望发送数据但由于流级流控而无法发送时,发送方应该(**SHOULD**)发送STREAM_DATA_BLOCKED帧(Type=0x15)。该帧类似于DATA_BLOCKED(第19.12节)。 - -在send-only流上接收到STREAM_DATA_BLOCKED帧的终端必须(**MUST**)以错误STREAM_STATE_ERROR终止连接。 - -STREAM_DATA_BLOCKED帧的格式如图37所示。 - -``` -STREAM_DATA_BLOCKED Frame { - Type (i) = 0x15, - Stream ID (i), - Maximum Stream Data (i), -} -``` -图37: STREAM_DATA_BLOCKED Frame Format - -STREAM_DATA_BLOCKED帧包含以下字段: - -* Stream ID:流标识,变长整数,指示由于流控而阻塞的流的ID。 - -* Maximum Stream Data:最大流数据量,变长整数,指示发生阻塞时流的偏移量。 - -## 19.14. STREAMS_BLOCKED帧(STREAMS_BLOCKED Frames) -当发送方希望打开流但由于其对端设置的最大流个数限制而无法打开时,发送方应该(**SHOULD**)发送STREAMS_BLOCKED帧(Type=0x16或0x17),参见第19.11节。类型为0x16的STREAMS_BLOCKED帧用于指示达到双向流限制,类型为0x17的STREAMS_BLOCKED帧用于指示达到单向流限制。 - -STREAMS_BLOCKED帧不会打开流,但会通知对端需要新的流并且因为流个数限制阻止了流的创建。 - -STREAMS_BLOCKED帧的格式如图38所示。 - -``` -STREAMS_BLOCKED Frame { - Type (i) = 0x16..0x17, - Maximum Streams (i), -} -``` -图38: STREAMS_BLOCKED Frame Format - -STREAMS_BLOCKED帧包含以下字段: - -* Maximum Streams:最大流个数,变长整数,指示发送此帧时允许的最大流个数。此值不能超过2^60^,因为无法对大于2^62^-1的流ID进行编码。接收到大于此限制的流的帧,必须(**MUST**)被其视为STREAM_LIMIT_ERROR或FRAME_ENCODING_ERROR类型的连接错误。 - -## 19.15. NEW_CONNECTION_ID帧(NEW_CONNECTION_ID Frames) -终端发送一个NEW_CONNECTION_ID帧(Type=0x18)来为其对端提供替代CID,这些CID可用于在迁移连接时打破可关联性,参见第9.5节。 - -NEW_CONNECTION_ID帧的格式如图39所示。 - -``` -NEW_CONNECTION_ID Frame { - Type (i) = 0x18, - Sequence Number (i), - Retire Prior To (i), - Length (8), - Connection ID (8..160), - Stateless Reset Token (128), -} -``` -图39: NEW_CONNECTION_ID Frame Format - -NEW_CONNECTION_ID帧包含以下字段: - -* Sequence Number:序号,发送方分配给此CID的序号,编码为变长整数,参见第5.1.1小节。 - -* Retire Prior To:停用此序号之前ID,变长整数,指示应该停用哪些CID,参见第5.1.2小节。 - -* Length:长度,一个8位无符号整数,表示CID的长度。小于1和大于20的值无效,必须(**MUST**)视为FRAME_ENCODING_ERROR类型的连接错误。 - -* Connection ID:连接ID(CID),由Length指定长度的CID。 - -* Stateless Reset Token:无状态重置令牌,一个128位值,当上述关联的CID在使用时,此令牌将用于无状态重置,参见第10.3节。 - -当前如果终端要求其对端发送具有零长度DCID的数据包,则该终端不得(**MUST NOT**)发送此帧。将CID的长度更改为零长度或变更零长度会导致难以识别到CID的值何时被改变。发送具有零长度DCID的数据包的终端,必须(**MUST**)将收到NEW_CONNECTION_ID帧视为PROTOCOL_VIOLATION类型的连接错误。 - -传输错误、超时和重传可能会导致多次收到相同的NEW_CONNECTION_ID帧。多次收到同一帧不得(**MUST NOT**)视为连接错误。接收方可以使用NEW_CONNECTION_ID帧中提供的序号来判决多次接收相同的NEW_CONNECTION_ID帧的情况。 - -如果终端接收到一个NEW_CONNECTION_ID帧,该帧与之前收到的NEW_CONNECTION_ID具有相同的CID,但是具有不同的无状态重置令牌或序号,或者序号相同而CID不同,则终端可以(**MAY**)将该情况视为PROTOCOL_VIOLATION类型的连接错误。 - -Retire Prior To字段适用于连接建立期间确定的CID和preferred_address传输参数,参见第5.1.2小节。 Retire Prior To字段中的值必须(**MUST**)小于或等于Sequence Number字段的值。接收到Retire Prior To大于Sequence Number的NEW_CONNECTION_ID帧,必须(**MUST**)将其视为FRAME_ENCODING_ERROR类型的连接错误。 - -一旦发送方表示Retire Prior To值之前的序号停用,在后续NEW_CONNECTION_ID帧中发送的较小的序号值将无效。接收方必须(**MUST**)忽略任何不增加Retire Before To值的NEW_CONNECTION_ID帧。 - -接收到序号小于先前接收的NEW_CONNECTION_ID帧中Retire Prior To字段的NEW_CONNECTION_ID帧的终端,必须(**MUST**)发送相应的RETIRE_CONNECTION_ID帧,该帧停用新接收的CID,除非它已经停用了该CID对应的序号。 - -## 19.16. RETIRE_CONNECTION_ID帧(RETIRE_CONNECTION_ID Frames) -终端发送RETIRE_CONNECTION_ID帧(Type=0x19)以指示它将不再使用由其对端发布的指定CID。这包括握手期间提供的CID。发送RETIRE_CONNECTION_ID帧也可以作为一个请求,请求对端发送额外CID以备将来使用,参见第5.1节。可以使用NEW_CONNECTION_ID帧(第19.15节)将新的CID传递给对端。 - -停用CID会使与此CID关联的无状态重置令牌失效。 - -RETIRE_CONNECTION_ID帧的格式如图40所示。 - -``` -RETIRE_CONNECTION_ID Frame { - Type (i) = 0x19, - Sequence Number (i), -} -``` -图40: RETIRE_CONNECTION_ID Frame Format - -RETIRE_CONNECTION_ID帧包含以下字段: - -* Sequence Number:序号,被停用的CID的序号,参见第5.1.2小节。 - -收到包含序号大于之前发送的所有CID的序号的RETIRE_CONNECTION_ID帧,必须(**MUST**)将其视为PROTOCOL_VIOLATION类型的连接错误。 - -在RETIRE_CONNECTION_ID帧中指定的序号不得(**MUST**)引用包含该帧的数据包的DCID。对端可以(**MAY**)将此视为PROTOCOL_VIOLATION类型的连接错误。 - -如果终端为它的对端提供了一个零长度的CID,它就不能发送这个帧。提供零长度CID的终端必须(**MUST**)将收到RETIRE_CONNECTION_ID帧视为PROTOCOL_VIOLATION类型的连接错误。 - -## 19.17. PATH_CHALLENGE帧(PATH_CHALLENGE Frames) -终端可以使用PATH_CHALLENGE帧(Type=0x1a)来检查对端的可达性以及用于连接迁移期间的路径验证。 - -PATH_CHALLENGE帧的格式如图41所示。 - -``` -PATH_CHALLENGE Frame { - Type (i) = 0x1a, - Data (64), -} -``` -图41: PATH_CHALLENGE Frame Format - -PATH_CHALLENGE帧包含以下字段: - -* Data :数据,这个8字节字段可以包含任意数据。 - -在PATH_CHALLENGE帧中包含64位熵可确保接收到此数据的第三方猜测不出正确值。 - -此帧的接收方必须(**MUST**)生成包含相同Data值的PATH_RESPONSE帧(第19.18节)。 - -## 19.18. PATH_RESPONSE帧( PATH_RESPONSE Frames) -发送PATH_RESPONSE帧(Type=0x1b)以响应PATH_CHALLENGE帧。 - -PATH_RESPONSE帧的格式如图42所示。PATH_RESPONSE帧的格式与PATH_CHALLENGE帧的格式相同,参见第19.17节。 - -``` -PATH_RESPONSE Frame { - Type (i) = 0x1b, - Data (64), -} -``` -图42: PATH_RESPONSE Frame Format - -如果PATH_RESPONSE帧的Data与终端先前发送的PATH_CHALLENGE帧的Data不匹配,终端可以(**MAY**)生成PROTOCOL_VIOLATION类型的连接错误。 - -## 19.19. CONNECTION_CLOSE帧(CONNECTION_CLOSE Frames) -终端发送CONNECTION_CLOSE帧(Type=0x1c或0x1d)以通知其对端连接正在关闭。类型为0x1c的CONNECTION_CLOSE帧仅用于在QUIC层发出错误信号,或者无错误关闭(使用NO_ERROR码)。类型为0x1d的CONNECTION_CLOSE帧用于向使用QUIC的应用发出错误信号。 - -如果存在尚未显式关闭的打开流,则在连接关闭时它们会被隐式关闭。 - -CONNECTION_CLOSE帧的格式如图43所示。 - -``` -CONNECTION_CLOSE Frame { - Type (i) = 0x1c..0x1d, - Error Code (i), - [Frame Type (i)], - Reason Phrase Length (i), - Reason Phrase (..), -} -``` -图43: CONNECTION_CLOSE Frame Format - -CONNECTION_CLOSE帧包含以下字段: - -* Error Code:错误码,变长整数,指示关闭此连接的原因。0x1c类型的CONNECTION_CLOSE帧使用来自第20.1节空间中定义的代码。0x1d类型的CONNECTION_CLOSE帧使用应用层协议定义的代码,参见第20.2节。 - -* Frame Type:帧类型,变长整数,编码触发此错误的帧类型。当帧类型未知时,使用值0(相当于引用PADDING帧)。与应用有关的CONNECTION_CLOSE(类型0x1d)不包括此字段。 - -* Reason Phrase Length:原因短语长度,变长整数,以字节为单位,指定原因短语的长度。由于CONNECTION_CLOSE帧不能拆分到不同数据包,因此对数据包大小的任何限制也会限制原因短语的可用空间。 - -* Reason Phrase:原因短语,连接关闭的附加诊断信息。如果发送方选择不提供除了错误码之外的详细信息,则长度可以为零。这应该(**SHOULD**)是一个UTF-8编码的字符串[RFC3629],就算此帧不携带信息,例如语言标签之类,这也有助于创建文本的实体之外的其他实体的理解。 - -应用有关的CONNECTION_CLOSE(类型0x1d)帧只能使用0-RTT或1-RTT包发送,参见第12.5节。当应用希望在握手期间放弃连接时,终端可以在Initial或Handshake包中发送带有APPLICATION_ERROR错误码的CONNECTION_CLOSE帧(类型0x1c)。 - -## 19.20. HANDSHAKE_DONE帧(HANDSHAKE_DONE Frames) -服务端使用HANDSHAKE_DONE帧(Type=0x1e)向客户端发出握手确认信号。 - -HANDSHAKE_DONE帧的格式如图44所示,这表明HANDSHAKE_DONE帧没有内容。 - -``` -HANDSHAKE_DONE Frame { - Type (i) = 0x1e, -} -``` -图44: HANDSHAKE_DONE Frame Format - -HANDSHAKE_DONE帧只能由服务端发送。服务端在完成握手之前不得(**MUST NOT**)发送HANDSHAKE_DONE帧。服务端必须(**MUST**)将收到HANDSHAKE_DONE帧视为PROTOCOL_VIOLATION类型的连接错误。 - -## 19.21. 扩展帧(Extension Frames) -QUIC帧不使用自解释编码。因此,终端在成功处理数据包之前需要了解所有帧的语法。这允许对帧进行有效编码,但这意味着终端无法发送其对端未知类型的帧。 - -希望使用自定义帧的QUIC扩展必须(**MUST**)首先确保对端能够理解该帧。终端可以使用传输参数来表示它愿意接收的扩展帧的类型。一个传输参数可以指示对一种或多种扩展帧类型的支持。 - -除非明确定义了组合的行为,否则修改或替换核心协议功能(包括帧类型)的扩展将难以与其他修改或替换相同功能的扩展相结合。这样的扩展应该(**SHOULD**)定义它们与先前定义的扩展的之间的交互,修改相同的协议组件。 - -扩展帧必须(**MUST**)是受拥塞控制的,并且必须(**MUST**)触发一个ACK帧响应。替代或补充ACK帧的扩展帧除外。除非在扩展中指定,否则扩展帧不受流控限制。 - -IANA注册中心用于管理帧类型的分配,参见第22.4节。 - -# 20. 错误码(Error Codes) - -QUIC的传输层错误码和应用层错误码是62位无符号整数。 - -## 20.1. 传输层错误码(Transport Error Codes) - -本节列出了定义的QUIC传输层错误码,这些错误码可以在CONNECTION_CLOSE帧中使用,类型为0x1c。这些错误适用于整个连接。 - -* NO_ERROR (0x00):终端将此与CONNECTION_CLOSE一起使用,以表示在没有任何错误的情况下即时关闭连接。 - -* INTERNAL_ERROR(0x01):终端遇到内部错误,无法继续连接。 - -* CONNECTION_REFUSED (0x02):服务端拒绝接受新连接。 - -* FLOW_CONTROL_ERROR (0x03):终端接收到的数据多于其公布的流控限额中允许的数据,参见第4章。 - -* STREAM_LIMIT_ERROR (0x04):终端接收到携带某个流标识的帧,这个流标识超过了其通告的对应的流类型的流数限制。 - -* STREAM_STATE_ERROR (0x05):终端收到了某个流的帧,但该流所处状态不支持发送该帧,参见第3章。 - -* FINAL_SIZE_ERROR (0x06):(1) 终端收到一个包含超过先前确定的final size的数据的STREAM帧,(2)终端收到一个包含final size小于已接收的流数据大小的STREAM帧或RESET_STREAM帧,(3) 终端收到一个STREAM帧或一个RESET_STREAM帧,其中包含与已确定的final size不同的final size。 - -* FRAME_ENCODING_ERROR (0x07):终端接收到格式错误的帧——例如,未知类型的帧或确认范围大于数据包其余部分所能承载大小的ACK帧。 - -* TRANSPORT_PARAMETER_ERROR (0x08):终端接收到格式错误、包含无效值、遗漏强制传输参数、包含禁止传输参数或其他错误的传输参数。 - -* CONNECTION_ID_LIMIT_ERROR (0x09):对端提供的CID数量超过了通告的active_connection_id_limit。 - -* PROTOCOL_VIOLATION (0x0a):终端检测到一个协议合规性错误,该错误类型不能用更具体的错误码表示。 - -* INVALID_TOKEN(0x0b):服务端收到包含INVALID_TOKEN字段的客户端Initial包。 - -* APPLICATION_ERROR (0x0c):应用或应用层协议错误导致连接关闭。 - -* CRYPTO_BUFFER_EXCEEDED (0x0d):终端在CRYPTO帧中接收到的数据多于它可以缓冲的数据。 - -* KEY_UPDATE_ERROR (0x0e):终端在执行密钥更新时检测到错误,参见[QUIC-TLS]第6章。 - -* AEAD_LIMIT_REACHED (0x0f):终端已达到给定连接使用的AEAD算法的机密性或完整性限制。 - -* NO_VIABLE_PATH (0x10):终端已确定网络路径无法支持QUIC。除非路径不支持足够大的MTU,否则终端不太可能收到携带此错误码的CONNECTION_CLOSE帧。 - -* CRYPTO_ERROR (0x0100-0x01ff):加密握手失败。QUIC保留256个值用于携带特定于所使用的加密握手的错误码。[QUIC-TLS]第4.8节列举了使用TLS进行加密握手时发生的错误码。 - -有关注册新错误码的详细信息,请参阅第22.5节。 - -在定义这些错误码时,应用了几个原则: - -* 可能需要接收方执行特定操作的错误条件被赋予单独的错误码; -* 表示常见情况的错误被赋予特定错误码; -* 错误码还将用于表示通用功能,如流量控制或传输参数处理时的错误。 -* 最后提供通用错误码供实现在无法或不愿意使用更具体错误码的情况下使用。 - -## 20.2. 应用层协议错误码(Application Protocol Error Codes) - -应用层错误码的管理留给应用层协议。RESET_STREAM帧(第19.4节)、STOP_SENDING帧(第19.5节)和类型为0x1d的CONNECTION_CLOSE帧(第19.19节)携带应用层协议错误码。 - -# 21. 安全考虑(Security Considerations) -QUIC的目标是提供安全的传输层连接。第21.1节概述了这些属性,随后的部分讨论了有关这些属性的限制和注意事项,包括对已知攻击和应对策略的描述。 - -## 21.1. 安全性概述(Overview of Security Properties) -完整的QUIC安全性分析超出了本文的范围。本节是对所需安全性的非正式描述,以指导实现者进行协议分析。 - -QUIC采用[SEC-CONS]中描述的威胁模型,并针对该模型的多种攻击提供保护机制。 - -为此,我们将攻击分为被动攻击和主动攻击。被动攻击者能够从网络读取数据包,而主动攻击者也能够将数据包写入网络。然而,被动攻击可能会让攻击者能够在QUIC连接所处路径中引起路由更改或其他变更。 - -攻击者还可以分为on-path攻击者或off-path的攻击者。on-path攻击者可以读取、修改或删除它观察到的任何数据包,从而使数据包到达不了其目的地,而off-path的攻击者可以观察到数据包但无法阻止原数据包到达其预定目的地。这两种类型的攻击者也可以发送任意数据包。与[SEC-CONS]第3.5节的定义不同的是,(注:在[SEC-CONS]的定义中,off-path的攻击者只能发包不能收包),off-path的攻击者能够观察数据包。 - -握手、数据包保护和连接迁移相关安全性是分别考虑的。 - -### 21.1.1. 握手(Handshake) -QUIC握手结合了TLS1.3握手并继承了[TLS13]的Appendix E.1中描述的加密属性。QUIC的许多安全性取决于提供这些属性的TLS握手。对TLS握手的任何攻击都可能影响QUIC。 - -任何对TLS握手的攻击,或危及会话密钥的保密性或唯一性,或伤及对端的身份认证,都会影响QUIC提供的依赖于这些密钥的其他安全保证。例如,连接迁移(第9章)取决于机密性保护的有效性,无论是TLS握手的密钥协商还是QUIC包保护,都是为了避免跨网络路径的可链接性。 - -对TLS握手完整性的攻击可能使得攻击者能够影响应用层协议或QUIC版本的选择。 - -除了TLS提供的特性外,QUIC握手还提供了一些针对握手的DoS攻击的防御措施。 - -#### 21.1.1.1. 防放大攻击(Anti-Amplification) -地址验证(第8章)用于验证声明一个给定地址的实体能否在该地址收到数据包。地址验证将Amplification攻击目标限制在攻击者可以观察到的数据包地址。 - -在地址验证之前,终端能够发送的数据量受到限制。终端向未验证地址发送不能超过接收的三倍。 - -> 注意:Anti-Amplification限制仅限于终端响应从未经验证的地址收到数据包时。在建立新连接或启动连接迁移时,Anti-Amplification限制不适用于客户端。 - -#### 21.1.1.2. 服务端DoS攻击(Server-Side DoS) -服务端对一次完整握手的往返数据计算比较费资源,因为需要进行数字签名和密钥交换。为了防止针对服务端计算资源的DoS攻击,Retry包提供了一种低耗的令牌交换机制,使得服务端可以在执行此计算之前验证客户端的IP地址,代价只是一个RTT时间。握手成功后,服务端可以向客户端发出新令牌,使得后续新连接建立的时候可以采用0-RTT。 - -#### 21.1.1.3. On-Path握手终止攻击(On-Path Handshake Termination) -on-path或off-path的攻击者可以通过替换或加速伪Initial包致使握手失败。因为一旦交换了有效的Initial包,后续的Handshake包就会受到握手密钥的保护,on-path攻击者除了通过丢弃数据包使得终端放弃尝试之外,无法以其他方式强制握手失败。 - -on-path攻击者还可以替换任一端的数据包地址,从而使其弄错对端地址。这种攻击与NAT导致的结果没有区别。 - -#### 21.1.1.4. 参数协商攻击(Parameter Negotiation) -整个握手过程都受到加密保护,Initial包使用版本特定的密钥进行加密,Handshake和后续数据包使用从TLS密钥交换派生的密钥进行加密。此外,传输参数的协商被打包到TLS中,提供与普通TLS协商相同的完整性保证。攻击者可以观察到客户端的传输参数(只要它知道版本对应的salt),但无法观察服务端的传输参数,也无法影响传输参数协商。 - -CID未加密,但在所有数据包中都受到完整性保护。 - -此QUIC版本没有包含版本协商机制,不兼容版本的实现将无法建立连接。 - -### 21.1.2. 数据包保护(Protected Packets) -数据包保护(第12.1节)对除Version Negotiation包之外的所有数据包,都进行加密认证,但由于使用特定于版本的密钥材料,对Initial和Retry包的保护受限,更多详细信息请参阅[QUIC-TLS]。本节仅考虑对受保护数据包的被动和主动攻击。 - -on-path和off-path的攻击者都可以发起被动攻击,在这种攻击中,他们保存观察到的数据包,以便将来针对数据包保护进行离线攻击,这对于任意网络上任意数据包的任意观察者来说都可以做到。 - -在无法观察到连接的有效数据包的情况下,注入数据包攻击不太可能成功,因为数据包保护确保有效数据包仅由拥有在握手期间商定密钥材料的终端生成,见第7章和第21.1.1小节。类似地,除了Initial包外,任何可观察到数据包,并尝试在这些数据包中插入新数据或修改现有数据的主动攻击者,都不能够生成接收端认为有效的数据包。 - -在欺骗攻击中,主动攻击者可能篡改其转发或注入的数据包中未受保护的部分,例如源地址或目的地址,这只有当攻击者可以将数据包转发到初始终端时才有效。数据包保护可以确保数据包负载只能由完成握手的终端处理,而无效数据包将被忽略。 - -攻击者还可以改变QUIC包和UDP报文之间的边界,致使多个QUIC包合并为一个UDP报文或将合并后的QUIC包拆分为多个UDP报文。除了包含Initial包(需要填充)的UDP报文外,修改UDP报文中数据包的排列方式对连接没有功能性影响,但可能会改变一些性能特性。 - -### 21.1.3. 连接迁移(Connection Migration) -连接迁移(第9章)使得终端能够在不同IP地址和Port端口的多条路径上进行传输切换,每次使用一条路径发送和接收非探测帧。路径验证(第8.2节)确定对端愿意并且能够接收在特定路径上发送的数据包。这有助于通过限制发往欺骗地址的数据包数量来减少地址欺骗的影响。 - -本节介绍在各种类型的DoS攻击下连接迁移的安全特性。 - -#### 21.1.3.1. On-Path主动攻击(On-Path Active Attacks) -可以使其观察到的数据包不再到达其预期目的地的攻击者被视为“on-path攻击者”。当客户端和服务端之间存在攻击者时,终端发送的数据包需要通过攻击者,以在给定路径上建立连接。 - -on-path攻击者可以: - -* 观察到数据包 -* 修改IP和UDP报文头 -* 注入新数据包 -* 使数据包延迟 -* 使数据包乱序 -* 丢弃数据包 -* 沿数据包边界拆分和合并报文 - -on-path攻击者不能: - -* 修改数据包的经过认证的部分并使接收方接受该数据包 - -on-path攻击者有机会修改它观察到的数据包,但是,对数据包经过认证的部分的任何修改,都将导致它被接收端视为无效包丢弃,因为数据包负载需要经过认证和加密。 - -QUIC旨在限制on-path攻击者的能力,如下所述: - -1. on-path攻击者可以阻止两端在其所在路径上建立连接,如果两端不能选择不经过攻击者的其他路径,则可能连接失败。攻击者可以通过丢弃所有数据包、修改使其无法解密或其他方法来实现这个目的。 -2. on-path攻击者如果也在新路径上,则可以通过使得新路径验证失败来阻止两端往新路径迁移。 -3. on-path攻击者无法阻止客户端迁移到不经过攻击者的其他路径。 -4. on-path攻击者可以通过延迟或丢弃数据包来降低连接的吞吐量。 -5. on-path攻击者不能迫使终端接受它篡改了认证部分的数据包。 - -#### 21.1.3.2. Off-Path主动攻击(Off-Path Active Attacks) -off-path攻击者并不直接位于客户端和服务端之间的路径上,但可以获得客户端和服务端之间发送的部分或全部数据包的副本。它还能够将这些数据包的副本发往任一终端。 - -off-path攻击者可以: - -* 观察到数据包 -* 注入新数据包 -* 乱序注入数据包 - -off-path攻击者不能: -* 修改终端发送的数据包 -* 延迟数据包 -* 丢弃数据包 -* 使原数据包乱序 - -off-path攻击者可以修改它观察到的数据包的副本,并将这些副本注入网络,可能具有欺骗性的源地址和目的地址。 - -出于本次讨论的目的,假设off-path攻击者能够将修改后的数据包副本注入网络,而且该网络可以使副本比原数据包更早到达目的终端。换句话说,攻击者有能力持续“赢得”与终端之间的合法数据包的竞争,从而可能使得接收方忽略原数据包。 - -此处还假设攻击者拥有影响NAT状态所需的资源。特别是,攻击者可以致使终端丢失其NAT绑定,然后获得相同的终端口以用于其自己的流量。 - -QUIC旨在限制off-path攻击者的能力,如下所述: - -1. off-path攻击者可以竞争数据包并试图成为“受限”的on-path攻击者。 -2. off-path攻击者只要能够改善客户端和服务端之间的连接性,就可以使将源地址列为off-path攻击者的转发数据包的路径验证成功。 -3. 一旦握手完成,off-path攻击者就不能致使连接关闭。 -4. 如果off-path攻击者无法观察到新路径,它就不能致使新路径迁移失败。 -5. 在迁移到新路径的过程中,off-path攻击者可能会成为“受限”的on-path攻击者,如果其在新路径也是off-path攻击者。 -6. off-path攻击者可以通过影响共享的NAT状态,使其可以用客户端最初使用的同一IP地址和端口向服务端发送数据包,从而成为“受限”的on-path攻击者。 - -#### 21.1.3.3. “受限”的On-Path主动攻击 (Limited On-Path Active Attacks) -“受限”的on-path攻击者是一种off-path攻击者,它通过在服务端和客户端之间复制和转发原数据包来提供改进的数据包路由,使得这些数据包在原数据包副本之前到达,从而使其被目的端丢弃。 - -“受限”的on-path攻击者与普通on-path攻击者的区别在于它不在两端之间的初始路径上,因此终端发送的原数据包仍然能到达其目的地。这意味着将来如果无法以比初始路径更快的速度将复制的数据包路由到目的地,将不能阻止原数据包到达目的地。 - -“受限”的on-path攻击者可以: - -* 观察到数据包 -* 注入新数据包 -* 修改未加密的包头 -* 使数据包乱序 - -“受限”的on-path攻击者不能: - -* 延迟数据包,使它们晚于初始路径上发送的数据包到达 -* 丢弃数据包 -* 修改数据包的认证加密的部分,并使接收方接受该数据包 - -“受限”的on-path攻击者只能将数据包延迟到原数据包到达之前的时间点,这意味着它无法提供比初始路径更糟糕的路由。如果受限on-path攻击者丢弃数据包,则原数据包副本仍将到达目的终端。 - -QUIC旨在限制有限的off-path攻击者的能力如下: - -1. 一旦握手完成,“受限”的on-path攻击者就不能致使连接关闭。 -2. 空闲连接如果客户端首先恢复活动,则“受限”的on-path攻击者无法致使该连接关闭。 -3. 空闲连接如果服务端首先恢复活动,则“受限”的on-path攻击者可能会致使该连接被视为丢失状态。 - -请注意,因为同样的原因,这些保证与为任何NAT提供的保证相同。 - -## 21.2. 握手拒绝服务(Handshake Denial of Service) -作为提供加密认证的传输层协议,QUIC提供了一系列针对拒绝服务的保护机制。加密握手完成后,QUIC终端会丢弃大多数未经认证的数据包,从而极大地限制了攻击者干扰现有连接的能力。 - -一旦连接建立,QUIC终端可能会接受一些未经认证的ICMP数据包(参见第14.2.1小节),但这些数据包的使用非常有限。终端唯一能接受的其他类型的数据包是Stateless Reset包(第10.3节),要求其包含的令牌在使用之前一直保密。 - -在创建连接的过程中,QUIC仅提供针对off-path攻击的保护。所有的QUIC包都包含了证据,证明其收到了来自对端的上一包。 - -地址在握手期间不能更改,因此终端可以丢弃在不同网络路径上收到的数据包。 - -SCID和DCID字段是在握手期间防止off-path攻击的主要手段,见第8.1节。这些需要与对端通告的CID相匹配。除了Initial包和Stateless Reset包,终端只接受包含与终端先前选择的值对应的DCID字段的数据包。这是为Version Negotiation包提供的唯一保护。 - -客户端选择的Initial包中的DCID字段是不可预测的,这是有原因的。携带加密握手的数据包受从该CID派生的密钥和特定于QUIC版本的salt保护。这使得终端在加密握手完成后可用相同的过程来验证它们接收到的数据包。无法认证的数据包将被丢弃。以这种方式为数据包保护提供了强有力的保证,即数据包的发送方收到了Initial包并能理解它。 - -对于能够在连接建立之前接收QUIC包的攻击者,这些保护措施并不是有效的。这些攻击者可能发送能被QUIC终端接受的数据包。此QUIC版本尝试检测此类攻击,但可预期的是终端将无法建立连接而不是恢复。在大多数情况下,加密握手协议[QUIC-TLS]负责检测握手期间是否被篡改。 - -允许终端使用其他方法来检测并尝试从握手干扰中恢复。终端可以使用其他方法来识别和丢弃无效的数据包,但本文中不做强制约定。 - -## 21.3. 放大攻击(Amplification Attack) -攻击者可能能够从服务端接收到地址验证令牌(第8章),然后释放它用于获取该令牌的IP地址。稍后,攻击者可以通过伪装成相同地址来启动与服务端的0-RTT连接,该地址现在可能指向一个不同的(受害者)终端。因此,攻击者可能会导致服务端向受害者发送初始拥塞窗口允许的数据。 - -服务端应该(**SHOULD**)通过限制地址验证令牌的使用和生命周期来缓解这种攻击,见第8.1.3小节。 - -## 21.4. Optimistic ACK攻击(Optimistic ACK Attack) -终端确认它没有接收到的数据包可能会导致拥塞控制器允许以超出网络支持的速率发包。终端可以(**MAY**)在发送数据包时略过包号以检测此行为,终端可以将之视为PROTOCOL_VIOLATION的连接错误,立即关闭连接,见第10.2节。 - -## 21.5. 请求伪造攻击(Request Forgery Attacks) -请求伪造攻击指的是某攻击者终端可控制其对端,使对端向第三方受害者发出指定的攻击请求。请求伪造攻击旨在让攻击者能够获得其对端的能力,这些能力是它不具备的。对于网络协议,请求伪造攻击通常用于利用由于对端在网络中的位置,而获得的由受害者授予对端的任何隐式权限。 - -为了使伪造的请求有效,攻击者需要能够影响对端发送的数据包的内容和发送位置。如果攻击者可以用受控的负载来攻击某个脆弱的服务,该服务可能会执行由攻击者的对端发起但由攻击者决定的操作。 - -例如,Web上的跨域请求伪造[CSRF]漏洞会致使客户端发出包含授权cookie[COOKIE]的请求,允许一个站点访问本应被授权给另一个站点的信息和操作。 - -由于QUIC承载在UDP协议上,所以主要的攻击方式是攻击者选择对端UDP报文的回应地址,并可以控制这些数据包中的一些不受保护的内容。(QUIC终端发送的大部分数据都受到保护,这也包括对密文的控制。)如果攻击者可以使对端向受害者主机发送特定UDP报文,该主机将根据报文中的内容执行某些操作,那么攻击就成功了。 - -本节讨论QUIC可被用于发起请求伪造攻击的方式。 - -本节还描述了QUIC终端可以实施的有限对策。这些对策可以由QUIC实现或部署单方面采用,而不需要请求伪造攻击的潜在目标采取行动。但是,如果基于UDP的服务没有正确地对请求授权,这些对策可能是不够的。 - -因为第21.5.4小节中描述的迁移攻击非常强大并且没有足够的对策,QUIC服务端实现应该假设攻击者可以使他们生成任意目的地的任意UDP负载。QUIC服务端不应该(**SHOULD NOT**)部署在没有部署入口过滤[BCP38]并且也没有足够安全的UDP终端的网络中。 - -尽管通常无法保证客户端不与易受攻击的服务端位于同一网络,但此QUIC版本不允许服务端迁移,从而防止了对客户端的欺骗迁移攻击。未来任何允许服务端迁移的扩展都必须(**MUST**)为伪造攻击定义对策。 - - -### 21.5.1. 终端的控制选项(Control Options for Endpoints) -QUIC为攻击者提供了一些机会,以影响或控制其对端发送UDP报文的目的地址: - -* 初始连接建立(第7章),服务端可以在其中指定客户端发送报文的目的地址——例如,通过填充DNS记录; - -* 首选地址(第9.6节),服务端可以在其中指定客户端发送报文的目的地址; - -* 欺骗连接迁移(第9.3.1小节),客户端能够使用源地址欺骗使得服务端将后续报文发往目的地址; - -* 欺骗数据包,使得服务端发送Version Negotiation包(第21.5.5小节)。 - -在所有情况下,攻击者都可以使其对端向可能不理解QUIC协议的受害者发送UDP报文。也就是说,这些数据包是在地址验证之前由对端发出,见第8章。 - -在数据包的加密部分之外,QUIC为终端提供了几个选项来控制其对端发送的UDP报文内容。DCID字段提供了对其对端发送的早期报文中某些字节的直接控制,参见第5.1节。客户端Initial包中的Token字段可以控制服务端Initial包某些字节,见第17.2.2小节。 - -此QUIC版本中没有任何措施来防止对数据包的加密部分进行间接控制。有必要假设终端能够控制对端发送的帧的内容,尤其是那些携带应用数据的帧,例如STREAM帧。尽管这在某种程度上取决于应用层协议的细节,但在许多协议中使用的上下文中可以进行一些控制。由于攻击者可以访问包保护密钥,他们很可能能够预测对端将如何加密未来的数据包。只需要能够以一定程度的成功概率预测到数据包数量和帧在数据包中的位置,攻击者就可以成功控制报文内容。 - -本节假设限制对报文内容的控制是不可行的。在后面的章节中所列对策的重点,是限制在地址验证之前发送的报文可用于请求伪造的方式。 - -### 21.5.2. 客户端Initial包请求伪造(Request Forgery with Client Initial Packets) -作为服务端的攻击者可以选择用于发布其可用性的IP地址和端口,因此假设来自客户端的Initial包可用来进行此类攻击。握手中隐含的地址验证确保对于新连接,客户端不会将其他类型的数据包发往不理解QUIC或不愿意接受QUIC连接的目的地。 - -Initial包保护([QUIC-TLS]第5.2节)使服务端难以控制客户端发送的Initial包的内容。选择不可预测的DCID的客户端使得服务端无法控制来自客户端的Initial包的任何加密部分。 - -但是,Token字段对服务端控制开放,并允许服务端使用客户端进行请求伪造攻击。使用NEW_TOKEN帧(第8.1.3小节)提供的令牌为连接建立期间的请求伪造提供了唯一选择。 - -但是,客户端没有义务使用NEW_TOKEN帧。如果客户端在接收到NEW_TOKEN帧后,服务端地址发生变化时发送空Token字段,则可以避免依赖Token字段的请求伪造攻击。 - -如果服务端地址发生变化,客户端可以避免使用NEW_TOKEN帧。但是,不包含Token字段可能会对性能产生不利影响。服务端可以依赖NEW_TOKEN来允许发送超过3倍限制的数据,见第8.1节。特别是,这会影响客户端使用0-RTT从服务端请求数据的情况。 - -发送Retry包(第17.2.5小节)为服务端提供了更改Token字段的选项。发送Retry包后,服务端还可以控制来自客户端的后续Initial包的DCID字段。这也可能允许对Initial包的加密内容进行间接控制。然而,Retry包的交换验证了服务端的地址,从而防止使用后续Initial包进行请求伪造。 - -### 21.5.3. 首选地址请求伪造(Request Forgery with Preferred Addresses) -服务端可以指定一个首选地址,客户端在握手确认后迁移到该地址,见第9.6节。客户端发往首选地址的数据包的DCID字段可用于请求伪造。 - -在验证该地址之前,客户端不得(**MUST NOT**)向首选地址发送非探测帧,见第8章。这大大减少了服务端需控制的数据包加密部分的选项。 - -本文不提供任何特定于首选地址的使用并且可由终端实施的额外对策。第21.5.6小节中描述的通用措施可用作进一步对策。 - -### 21.5.4. 欺骗迁移请求伪造(Request Forgery with Spoofed Migration) -客户端能够将欺骗源地址作为显式连接迁移的一部分,从而使服务端向该地址发送报文。 - -服务端随后发往此欺骗地址的任何数据包中的DCID字段可用于请求伪造。客户端也可能能够影响密文。 - -如果服务器在地址验证之前只发送探测包(第9.1节)到某个地址,则攻击者只能对数据包的加密部分进行有限的控制。然而,特别是对于NAT重新绑定,这会对性能产生不利影响。如果服务器发送携带应用数据的帧,则攻击者可能能够控制数据报文的大部分内容。 - -除了第21.5.6小节中描述的通用措施外,本文不提供可由终端实施的具体对策。然而,在网络级别针对地址欺骗的对策——特别是入向过滤[BCP38]——对于使用欺骗和源自外部网络的攻击特别有效。 - -### 21.5.5. 通过版本协商请求伪造(Request Forgery with Version Negotiation) -如果客户端能够在包上携带一个欺骗的源地址,那么服务端可以向该地址发送Version Negotiation包(第17.2.1小节)。 - -对于未知版本的包,CID字段没有大小限制,这增加了客户端从结果数据报控制的数据量。该数据包的第一个字节不受客户端控制,接下来的四个字节是零,但客户端可以控制从第5个字节开始的最多512个字节。 - -本文没有为此攻击提供具体的对策,但可以应用通用保护(第21.5.6小节)。在这种情况下,入向过滤[BCP38]也是有效的。 - -### 21.5.6. 通用请求伪造对策(Generic Request Forgery Countermeasures) -防御请求伪造攻击的最有效方法是修改易受攻击的服务以使用强认证。然而,这并不总是在QUIC部署的控制范围内。本节概述了QUIC终端可以单方面采取的其他一些步骤。这些额外的步骤可自行决定,因为根据情况,它们可能会干扰或阻止QUIC的合法用法。 - -通过环回接口提供的服务通常缺乏适当的认证。终端可以(**MAY**)阻止连接尝试或迁移到环回地址。如果同一服务以前在不同的接口可用,或者地址是由非环回地址的服务提供的,则终端不应该(**SHOULD NOT**)允许连接或迁移到环回地址。依赖于这些功能的终端可以提供禁用这些保护的选项。 - -类似地,终端可以视将地址从来自global、unique-local[RFC4193]或non-private范围,更改为link-local地址[RFC4291]或private-use[RFC1918]范围中的地址,作为潜在的请求伪造攻击尝试。终端可以完全拒绝使用这些地址,但这会带来干扰合法地址的显著风险。终端不应该(**SHOULD NOT**)拒绝使用地址,除非它们对网络有特定的了解,表明将报文发往给定范围内的未验证地址是不安全的。 - -终端可以(**MAY**)选择通过在Initial包中不包含来自NEW_TOKEN帧的值,或在完成地址验证之前仅在数据包中发送探测帧来降低请求伪造的风险。请注意,这并不能阻止攻击者使用DCID字段进行攻击。 - -终端不应具有关于某些服务端位置的特定信息,这些服务端可能成为请求伪造攻击的脆弱目标。但是,随着时间的推移,可能会识别出特定的UDP端口或特定的报文模式作为攻击的常见目标。在验证目的地址之前,终端可以(**MAY**)避免向这些端口发送报文或不发送与这些模式匹配的报文。终端可以(**MAY**)不使用包含已知有问题的模式的CID。 - -> 注意:修改终端以应用这些保护措施比部署基于网络的保护更有效,因为终端在发包到已验证的地址时不需要执行任何额外的处理。 - -## 21.6. Slowloris攻击(Slowloris Attacks) -这种攻击通常被称为Slowloris [Slowloris],它试图保持多个与目的终端的连接,并尽可能长时间地保持打开状态。针对QUIC终端,这些攻击可以通过生成避免因不活动而关闭连接所需的最小活动流量来实现。这可能包括发送少量数据,逐渐打开流控窗口以控制发送方速率,或模拟高丢包率生成ACK帧。 - -QUIC部署应该(**SHOULD**)为Slowloris攻击提供对策,例如增加服务端允许的最大客户端数量、限制单个IP地址允许建立的连接数量、限制连接允许的最低传输速度,并限制终端允许保持连接的时间长度。 - -## 21.7. 流分片和重组攻击(Stream Fragmentation and Reassembly Attacks) -恶意发送方可能故意不发送部分流数据,从而致使接收方为这些未发数据请求资源。这可能会致使不成比例的接收缓冲区内存分配和/或在接收方处创建大型且低效的数据结构。 - -恶意接收方可能故意不确认某些包含流数据的数据包,试图强制发送方存储大量未确认的流数据以进行重传。 - -如果流控窗口对应于可用内存,则可以减轻对接收方的攻击。但是,某些接收方会过量申请内存并通告超出实际可用内存的流控偏移。当终端表现良好时,超量策略可以带来更好的性能,但会使终端容易受到流分片攻击。 - -QUIC部署应该(**SHOULD**)为流分片攻击提供对策。这些措施可能包括避免过度申请内存、限制跟踪数据结构的大小、延迟重组STREAM帧、基于重组间隙的时间和持续时间采用启发式方法,或这些方法的某些组合。 - -## 21.8. 流提交攻击(Stream Commitment Attack) -恶意终端可以打开大量的流,耗尽对端状态。恶意终端可以在大量连接上重复该过程,其方式类似于TCP中的SYN洪水攻击。 - -通常,客户端将按顺序打开流,如第2.1节所述。然而,当几个流以较短间隔开启时,丢包或乱序可能致使打开流的STREAM帧被乱序接收。在接收到更高编号的流ID时,接收方需要打开所有相同类型的中间流,见第3.2节。因此,在新连接上,打开流ID为4000000的流将会打开1000001个客户端启动的双向流。 - -活动流的数量受initial_max_streams_bidi或initial_max_streams_uni传输参数的限制,并由MAX_STREAMS帧更新,如第4.6节所述。如果合理设定,这些限制可以减轻流提交攻击的影响。但是,当应用希望打开大量流时,将限制设置得太低可能会影响性能。 - -## 21.9. 对端拒绝服务攻击(Peer Denial of Service) -QUIC和TLS都包含在某些上下文中具有合法用途的帧或消息,但这些帧或消息可能会被滥用,致使对端消耗处理资源,而不会对连接状态产生任何可观察到的影响。 - -终端可以发送较小或无关紧要的消息来更改和恢复状态,例如通过向流控限制发送小增量。 - -如果与带宽消耗或对状态的影响相比,处理成本不成比例地大,那么这可能使得恶意对端耗尽处理能力。 - -虽然所有消息都有合法用途,但实现应该(**SHOULD**)跟踪与进度相关的处理成本,并将过量的任何非生产性数据包视为攻击的指示。终端可以(**MAY**)以连接错误关闭连接或丢弃数据包来响应这种情况。 - -## 21.10. 显式拥塞通知攻击(Explicit Congestion Notification Attacks) -on-path攻击者可以操纵IP报头中ECN字段的值来影响发送方的速率。[RFC3168]更详细地讨论了这种操作及其影响。 - -“受限”的on-path攻击者可以复制和发送带有被修改ECN字段的数据包,以影响发送方的速率。如果接收方会丢弃重复的数据包,攻击者需要将重复数据包与原数据包竞速才能在这次攻击中取得成功。因此,QUIC终端可忽略IP数据包中的ECN字段,除非该IP数据包中至少有一个QUIC包被成功处理,见第13.4节。 - -## 21.11. 无状态重置攻击( Stateless Reset Oracle) -无状态重置可能会产生类似于TCP重置注入的拒绝服务攻击。如果攻击者能够为具有特定CID的连接生成无状态重置令牌,则此攻击是可行的。导致此令牌生成的攻击者可重置具有相同CID的活动连接。 - -如果数据包可以路由到共享静态密钥的不同实例(例如,通过更改IP地址或端口),则攻击者可以使服务端发送Stateless Reset包。为了防止这种拒绝服务的类型,共享一个静态密钥用于无状态重置的终端必须(**MUST**)合理考虑(见第10.3.2小节),以便具有给定CID的数据包始终能到达具有同一连接状态的实例,除非该连接不再活跃。 - -更一般地,如果具有相应CID的连接可以在使用相同静态密钥的任何终端上处于活动状态,则服务端不得(**MUST NOT**)生成Stateless Reset包。 - -对于使用动态负载均衡的集群,当活动实例保持连接状态时,负载均衡器配置可能会发生变化。即使实例保持连接状态,路由的改变和由此产生的无状态重置也会致使连接被终止。如果无法将数据包路由到正确的实例,最好发送Stateless Reset包而不是等待连接超时。不过,这只有在路由不受攻击者影响时才可以接受。 - -## 21.12. 版本降级(Version Downgrade) -本文定义了QUIC的Version Negotiation包(第6章),可用于协商两端之间使用的QUIC版本。但是,本文没有具体说明如何在此版本和后续版本之间进行协商。特别是,Version Negotiation包中没有任何防止版本降级攻击的机制。未来使用版本协商的QUIC版本必须(**MUST**)定义一种对版本降级攻击具有鲁棒性的机制。 - -## 21.13. 通过路由进行针对性攻击(Targeted Attacks by Routing) -部署应该限制攻击者针对特定服务器实例的新连接进行攻击的能力。理想情况下,路由决策独立于客户端选择的值,包括地址。一旦选择了一个实例,就可以选择一个CID,以便后续数据包能路由到同一个实例。 - -## 21.14. 流量分析(Traffic Analysis) -QUIC包的长度可以揭示有关这些数据包内容长度的信息。PADDING帧的引入是为了使终端有模糊数据包内容长度的能力,见第19.1节。 - -对抗流量分析具有挑战性,也是一个主动研究的课题。长度不是信息泄漏的唯一途径。终端也可能从其他侧信道泄露敏感信息,例如数据包的耗时。 - -# 22. IANA考虑(IANA Considerations) -本文为管理QUIC码点新设了几个注册表,这些注册表遵循第22.1节中定义的一组通用策略。 - -## 22.1. QUIC注册表的注册策略(Registration Policies forQUICRegistries) -所有QUIC注册表都容许注册临时或永久性质的码点。本节描述通用的注册策略。 - -### 22.1.1. 临时注册(Provisional Registrations) -临时码点注册的引入旨在允许私有用途和实验性质的QUIC的扩展。临时注册申请只需要包含码点的值和联系人信息,同时,这类码点也可以被回收并重新分配用于其他目的。 - -根据[RFC8126]第4.5节的规定,临时注册需要专家审查。建议专家仅拒绝申请码点范围占用剩余码点空间比例过大,或申请第一个未分配值(参见第22.1.2小节)的注册。 - -临时注册包括一个Date字段,指示注册上次创建或更新的时间。可在不经指定专家审查的情况下提出更新任何临时注册日期的申请。 - -所有QUIC注册表都包含如下字段以支持临时注册: - -Value: 码点值。 - -Status:"永久"或"临时"。 - -Specification:引用该值的公开可用的规约。 - -Date:创建或更新日期。 - -Change Controller:负责该注册表项的实体。 - -Contact:注册人的联系方式。 - -Notes:关于该注册项的补充信息。 - -临时注册可以(**MAY**)省略Specification和Notes字段,也可以包含永久注册可能需要的其他字段。申请注册时不需要携带Date字段,因为其可被设置为正式创建或更新注册项的日期。 - -### 22.1.2. 码点选择(Selecting Codepoints) -向QUIC注册表申请新码点,应该(**SHOULD**)使用随机选择的值,该值不能是已经分配的值和所选空间第一个未分配的码点。一次申请多个码点的话,可以(**MAY**)选择一个连续的范围。这将不同的实现对同一码点赋予不同语义的风险降到最低。 - -第一个未分配的码点保留,需用Standards Action策略进行分配,参见[RFC8126]第4.9节。早期码点分配过程[EARLY-ASSIGN]可用于指导这些值的分配。 - -对于以变长整数(参见第16章)编码的码点,例如帧类型,应该(**SHOULD**)使用编码为4或8个字节(即2^14^及以上的值)的码点,除非其对较长编码特别敏感。 - -需要在QUIC注册表中注册码点的应用,可以(**MAY**)将申请码点作为注册过程的一部分。如果该码点尚未分配并且满足注册策略的要求,IANA必须(**MUST**)分配所选的码点。 - -### 22.1.3. 回收临时码点(Reclaiming Provisional Codepoints) -为了从注册表中回收一个或部分注册项(例如使用变长编码的64-16383范围的码点)释放注册表空间,可以申请删除未使用的临时注册项。应该(**SHOULD**)仅从具有最早记录日期的码点开始执行,并且不应该(**SHOULD NOT**)回收在不到一年之前更新的注册项。 - -删除码点的申请必须(**MUST**)由指定的专家审核。专家必须(**MUST**)先尝试确定码点是否仍在使用中。建议专家联系注册项中列出的联系人,以及尽可能广泛联系到协议的实现者,以确定其是否知道这些码点的使用情况。另外还建议专家至少留出四个星期的时间做出回应。 - -如果上述流程识别出码点正在使用中或有新提出更新注册项的申请,则不得(**MUST NOT**)回收码点,而应更新注册日期。修改时可以添加备注,记录了解到的相关情况。 - -如果识别出码点未在使用并且没有申请注册更新,则可以(**MAY**)从注册表中删除码点。 - -此审查和咨询过程也适用于将临时注册变更为永久注册的申请,但其目的不是确定是否有没有使用的码点,而是确定注册项是否准备表达了任何已部署的用途。 - -### 22.1.4. 永久注册(Permanent Registrations) -除非另有说明,QUIC注册表中的永久注册项遵循([RFC8126]第4.6节)指定的规约策略。指定的一个或多个专家验证规约是否存在且易于访问。鼓励专家偏向于批准注册,除非它们是滥用的、轻率的或有害的(不仅仅是看起来不漂亮或架构上可疑)。新建永久注册项时可以(**MAY**)指定附加限制。 - -新建注册表可能(**MAY**)需要指定一系列码点,这些码点分别由不同的注册策略管理。例如,QUIC Frame Types注册表(第22.4节)对0到63范围内的码点有更严格的策略。 - -对永久注册项的严格限制不会影响临时码点的注册。例如,可以申请临时注册帧类型为61的码点。 - -进入标准化阶段公布的注册表项都必须(**MUST**)是永久性的。 - -本文中的所有注册项都是永久状态,且随附有IETF的变更控制人和QUIC工作组的联系方式(quic@ietf.org)。 - -## 22.2. QUIC版本注册表(QUIC Versions Registry) -IANA在QUIC标题下为QUIC Versions添加了一个注册表。 - -QUIC Versions注册表管理32位空间,参见第15章。此注册表遵循第22.1节的注册策略。注册表中的永久注册项遵循[RFC8126]第4.6节的策略。 - -本文中定义的协议被分配了0x00000001码点。0x00000000码点是永久保留的,这个码点的注释有说明0x00000000是为版本协商保留的。 - -遵循模式0x?a?a?a?a的所有码点都是保留的,IANA不得(**MUST NOT**)分配,并且不得(**MUST NOT**)出现在分配值列表中。 - -## 22.3. QUIC传输参数注册表(QUIC Transport Parameters Registry) -IANA在QUIC标题下为QUIC Transport Parameters添加了一个注册表。 - -QUIC Transport Parameters注册表管理62位空间。此注册表遵循第22.1节中的注册策略。此注册表中的永久注册项遵循([RFC8126]第4.6节)规约策略进行分配,但0x00和0x3f(十六进制)之间的值除外,这些值使用[RFC8126]第4.9和4.10节定义的Standards Action或IESG许可进行分配。 - -除了第22.1.1小节中列出的字段外,此注册表中的永久注册项必须(**MUST**)包括以下字段: - -Parameter Name:参数名称,简短的参数助记符。 - -该注册表的初始内容如表6所示: - -| Value | Parameter Name | Specification | -|:----|:-----|:-----| -|0x00| original_destination_connection_id |第18.2节| -|0x01| max_idle_timeout |第18.2节| -|0x02| stateless_reset_token |第18.2节| -|0x03| max_udp_payload_size |第18.2节| -|0x04| initial_max_data |第18.2节| -|0x05| initial_max_stream_data_bidi_local |第18.2节| -|0x06| initial_max_stream_data_bidi_remote |第18.2节| -|0x07| initial_max_stream_data_uni |第18.2节| -|0x08| initial_max_streams_bidi |第18.2节| -|0x09| initial_max_streams_uni |第18.2节| -|0x0a| ack_delay_exponent |第18.2节| -|0x0b| max_ack_delay |第18.2节| -|0x0c| disable_active_migration |第18.2节| -|0x0d| preferred_address |第18.2节| -|0x0e| active_connection_id_limit |第18.2节| -|0x0f| initial_source_connection_id |第18.2节| -|0x10| retry_source_connection_id |第18.2节| - -表6: Initial QUIC Transport Parameters Registry - -对于形如 31 * N + 27(N为整数)值(即27, 58, 89, ...)都是保留的,这些值不得(**MUST NOT**)由IANA分配,也不得(**MUST NOT**)出现在分配值列表中。 - -## 22.4. QUIC帧类型注册表(QUIC Frame Types Registry) -IANA在QUIC标题下添加了一个QUIC Frame Types注册表。 - -QUIC Frame Types注册表管理62位空间。此注册表遵循第22.1节中的注册策略。此注册表中的永久注册项遵循([RFC8126]第4.6节)规约策略进行分配,但0x00和0x3f(十六进制)之间的值除外,这些值使用[RFC8126]第4.9和4.10节定义的Standards Action或IESG许可进行分配。 - -除了第22.1.1小节中列出的字段外,此注册表中的永久注册项必须(**MUST**)包括以下字段: - -Frame Type Name:帧类型名称,简短的帧类型助记符。 - -除了第22.1节的建议外,规约中增加永久注册项,都应该(**SHOULD**)描述终端是否可以发送该种类型的帧。大多数永久注册项都需要在传输参数中注册,参见第22.3节。另外,还需要描述帧格式及帧中所有字段的赋值语义。 - -初始注册表参见表3。请注意,注册表不包括表3中的Pkts和Spec列。 - -## 22.5. QUIC传输层错误码注册表(QUIC Transport Error Codes Registry) -IANA在QUIC标题下添加了QUIC Transport Error Codes的注册表。 - -QUIC Transport Error Codes注册表管理62位空间。该空间分为三个范围,由不同的策略管理。此注册表中的永久注册项分配要求遵循([RFC8126]第4.6节)规约策略,但0x00和0x3f(十六进制)之间的值除外,这些值使用[RFC8126]第4.9和4.10节定义的Standards Action或IESG许可进行分配。 - -除了第22.1.1小节中列出的字段外,此注册表中的永久注册项必须(**MUST**)包括以下字段: - -Code:错误码,简短参数助记符。 - -Description:描述,错误码语义的简要描述,如果被规约引用,则可能(**MAY**)是摘要。 - -初始注册表如表7所示。 - -|Value |Code |Description |Specification| -|:---|:---|:---|:---| -|0x00 |NO_ERROR |没有错误 |第20章| -|0x01 |INTERNAL_ERROR |实现错误 |第20章| -|0x02 |CONNECTION_REFUSED |服务端拒绝连接 |第20章| -|0x03 |FLOW_CONTROL_ERROR |流控错误 |第20章| -|0x04 |STREAM_LIMIT_ERROR |打开流数量超限制 |第20章| -|0x05 |STREAM_STATE_ERROR |在当前流状态下收到无效帧 |第20章| -|0x06 |FINAL_SIZE_ERROR |流上的FINAL SIZE出错 |第20章| -|0x07 |FRAME_ENCODING_ERROR |帧解码错 |第20章| -|0x08 |TRANSPORT_PARAMETER_ERROR |传输参数有误 |第20章| -|0x09 |CONNECTION_ID_LIMIT_ERROR |CID超限制 |第20章| -|0x0a |PROTOCOL_VIOLATION |通用协议错误 |第20章| -|0x0b |INVALID_TOKEN |收到无效令牌 |第20章| -|0x0c |APPLICATION_ERROR |应用程序错误 |第20章| -|0x0d |CRYPTO_BUFFER_EXCEEDED |CRYPTO缓存溢出 |第20章| -|0x0e |KEY_UPDATE_ERROR |收到无效的TLS KEY UPDATE请求 |第20章| -|0x0f |AEAD_LIMIT_REACHED |AEAD当前秘钥加密数据长度超限制(需要更换秘钥) |第20章| -|0x10 |NO_VIABLE_PATH |没有可用的网路路径 |第20章| -|0x0100-0x01ff |CRYPTO_ERROR |TLS告警码 |第20章| - -表7: Initial QUIC Transport Error Codes Registry Entries - -# 23. 参考资料(References) -## 23.1. 规范引用(Normative References) -[BCP38] -Ferguson, P. and D. Senie, "Network Ingress Filtering: Defeating Denial of Service Attacks which employ IP Source Address Spoofing", BCP 38, RFC 2827, May 2000, . - -[DPLPMTUD] -Fairhurst, G., Jones, T., Tüxen, M., Rüngeler, I., and T. Völker, "Packetization Layer Path MTU Discovery for Datagram Transports", RFC 8899, DOI 10.17487/RFC8899, September 2020, . - -[EARLY-ASSIGN] -Cotton, M., "Early IANA Allocation of Standards Track Code Points", BCP 100, RFC 7120, DOI 10.17487/RFC7120, January 2014, . - -[IPv4] -Postel, J., "Internet Protocol", STD 5, RFC 791, DOI 10.17487/RFC0791, September 1981, . - -[QUIC-INVARIANTS] -Thomson, M., "Version-Independent Properties of QUIC", RFC 8999, DOI 10.17487/RFC8999, May 2021, . - -[QUIC-RECOVERY] -Iyengar, J., Ed. and I. Swett, Ed., "QUIC Loss Detection and Congestion Control", RFC 9002, DOI 10.17487/RFC9002, May 2021, . - -[QUIC-TLS] -Thomson, M., Ed. and S. Turner, Ed., "Using TLS to Secure QUIC", RFC 9001, DOI 10.17487/RFC9001, May 2021, . - -[RFC1191] -Mogul, J. and S. Deering, "Path MTU discovery", RFC 1191, DOI 10.17487/RFC1191, November 1990, . - -[RFC2119] -Bradner, S., "Key words for use in RFCs to Indicate Requirement Levels", BCP 14, RFC 2119, DOI 10.17487/RFC2119, March 1997, . - -[RFC3168] -Ramakrishnan, K., Floyd, S., and D. Black, "The Addition of Explicit Congestion Notification (ECN) to IP", RFC 3168, DOI 10.17487/RFC3168, September 2001, . - -[RFC3629] -Yergeau, F., "UTF-8, a transformation format of ISO 10646", STD 63, RFC 3629, DOI 10.17487/RFC3629, November 2003, . - -[RFC6437] -Amante, S., Carpenter, B., Jiang, S., and J. Rajahalme, "IPv6 Flow Label Specification", RFC 6437, DOI 10.17487/RFC6437, November 2011, . - -[RFC8085] -Eggert, L., Fairhurst, G., and G. Shepherd, "UDP Usage Guidelines", BCP 145, RFC 8085, DOI 10.17487/RFC8085, March 2017, . - -[RFC8126] -Cotton, M., Leiba, B., and T. Narten, "Guidelines for Writing an IANA Considerations Section in RFCs", BCP 26, RFC 8126, DOI 10.17487/RFC8126, June 2017, . - -[RFC8174] -Leiba, B., "Ambiguity of Uppercase vs Lowercase in RFC 2119 Key Words", BCP 14, RFC 8174, DOI 10.17487/RFC8174, May 2017, . - -[RFC8201] -McCann, J., Deering, S., Mogul, J., and R. Hinden, Ed., "Path MTU Discovery for IP version 6", STD 87, RFC 8201, DOI 10.17487/RFC8201, July 2017, . - -[RFC8311] -Black, D., "Relaxing Restrictions on Explicit Congestion Notification (ECN) Experimentation", RFC 8311, DOI 10.17487/RFC8311, January 2018, . - -[TLS13] -Rescorla, E., "The Transport Layer Security (TLS) Protocol Version 1.3", RFC 8446, DOI 10.17487/RFC8446, August 2018, . - -[UDP] -Postel, J., "User Datagram Protocol", STD 6, RFC 768, DOI 10.17487/RFC0768, August 1980, . - -## 23.2. 资料引用(Informative References) -[AEAD] -McGrew, D., "An Interface and Algorithms for Authenticated Encryption", RFC 5116, DOI 10.17487/RFC5116, January 2008, . - -[ALPN] -Friedl, S., Popov, A., Langley, A., and E. Stephan, "Transport Layer Security (TLS) Application-Layer Protocol Negotiation Extension", RFC 7301, DOI 10.17487/RFC7301, July 2014, . - -[ALTSVC] -Nottingham, M., McManus, P., and J. Reschke, "HTTP Alternative Services", RFC 7838, DOI 10.17487/RFC7838, April 2016, . - -[COOKIE] -Barth, A., "HTTP State Management Mechanism", RFC 6265, DOI 10.17487/RFC6265, April 2011, . - -[CSRF] -Barth, A., Jackson, C., and J. Mitchell, "Robust defenses for cross-site request forgery", Proceedings of the 15th ACM conference on Computer and communications security - CCS '08, DOI 10.1145/1455770.1455782, 2008, . - -[EARLY-DESIGN] -Roskind, J., "QUIC: Multiplexed Stream Transport Over UDP", 2 December 2013, . - -[GATEWAY] -Hätönen, S., Nyrhinen, A., Eggert, L., Strowes, S., Sarolahti, P., and M. Kojo, "An experimental study of home gateway characteristics", Proceedings of the 10th ACM SIGCOMM conference on Internet measurement - IMC '10, DOI 10.1145/1879141.1879174, November 2010, . - -[HTTP2] -Belshe, M., Peon, R., and M. Thomson, Ed., "Hypertext Transfer Protocol Version 2 (HTTP/2)", RFC 7540, DOI 10.17487/RFC7540, May 2015, . - -[IPv6] -Deering, S. and R. Hinden, "Internet Protocol, Version 6 (IPv6) Specification", STD 86, RFC 8200, DOI 10.17487/RFC8200, July 2017, . - -[QUIC-MANAGEABILITY] -Kuehlewind, M. and B. Trammell, "Manageability of the QUIC Transport Protocol", Work in Progress, Internet-Draft, draft-ietf-quic-manageability-11, 21 April 2021, . - -[RANDOM] -Eastlake 3rd, D., Schiller, J., and S. Crocker, "Randomness Requirements for Security", BCP 106, RFC 4086, DOI 10.17487/RFC4086, June 2005, . - -[RFC1812] -Baker, F., Ed., "Requirements for IP Version 4 Routers", RFC 1812, DOI 10.17487/RFC1812, June 1995, . - -[RFC1918] -Rekhter, Y., Moskowitz, B., Karrenberg, D., de Groot, G. J., and E. Lear, "Address Allocation for Private Internets", BCP 5, RFC 1918, DOI 10.17487/RFC1918, February 1996, . - -[RFC2018] -Mathis, M., Mahdavi, J., Floyd, S., and A. Romanow, "TCP Selective Acknowledgment Options", RFC 2018, DOI 10.17487/RFC2018, October 1996, . - -[RFC2104] -Krawczyk, H., Bellare, M., and R. Canetti, "HMAC: Keyed-Hashing for Message Authentication", RFC 2104, DOI 10.17487/RFC2104, February 1997, . - -[RFC3449] -Balakrishnan, H., Padmanabhan, V., Fairhurst, G., and M. Sooriyabandara, "TCP Performance Implications of Network Path Asymmetry", BCP 69, RFC 3449, DOI 10.17487/RFC3449, December 2002, . - -[RFC4193] -Hinden, R. and B. Haberman, "Unique Local IPv6 Unicast Addresses", RFC 4193, DOI 10.17487/RFC4193, October 2005, . - -[RFC4291] -Hinden, R. and S. Deering, "IP Version 6 Addressing Architecture", RFC 4291, DOI 10.17487/RFC4291, February 2006, . - -[RFC4443] -Conta, A., Deering, S., and M. Gupta, Ed., "Internet Control Message Protocol (ICMPv6) for the Internet Protocol Version 6 (IPv6) Specification", STD 89, RFC 4443, DOI 10.17487/RFC4443, March 2006, . - -[RFC4787] -Audet, F., Ed. and C. Jennings, "Network Address Translation (NAT) Behavioral Requirements for Unicast UDP", BCP 127, RFC 4787, DOI 10.17487/RFC4787, January 2007, . - -[RFC5681] -Allman, M., Paxson, V., and E. Blanton, "TCP Congestion Control", RFC 5681, DOI 10.17487/RFC5681, September 2009, . - -[RFC5869] -Krawczyk, H. and P. Eronen, "HMAC-based Extract-and-Expand Key Derivation Function (HKDF)", RFC 5869, DOI 10.17487/RFC5869, May 2010, . - -[RFC7983] -Petit-Huguenin, M. and G. Salgueiro, "Multiplexing Scheme Updates for Secure Real-time Transport Protocol (SRTP) Extension for Datagram Transport Layer Security (DTLS)", RFC 7983, DOI 10.17487/RFC7983, September 2016, . - -[RFC8087] -Fairhurst, G. and M. Welzl, "The Benefits of Using Explicit Congestion Notification (ECN)", RFC 8087, DOI 10.17487/RFC8087, March 2017, . - -[RFC8981] -Gont, F., Krishnan, S., Narten, T., and R. Draves, "Temporary Address Extensions for Stateless Address Autoconfiguration in IPv6", RFC 8981, DOI 10.17487/RFC8981, February 2021, . - -[SEC-CONS] -Rescorla, E. and B. Korver, "Guidelines for Writing RFC Text on Security Considerations", BCP 72, RFC 3552, DOI 10.17487/RFC3552, July 2003, . - -[SLOWLORIS] -"RSnake" Hansen, R., "Welcome to Slowloris - the low bandwidth, yet greedy and poisonous HTTP client!", June 2009, . +# 摘要 +本文定义了QUIC传输协议的核心特性。QUIC为应用提供经过流控的流(Stream),可用于结构化通信、低延迟连接建立和网络路径迁移。QUIC还包含在多种部署情况下确保机密性、完整性和可用性的安全措施。随附文件描述了用于密钥协商的TLS集成、丢包检测和一个拥塞控制算法示例。 + +# 1. 概述(Overview) +QUIC是一种安全的通用传输层协议。本文定义了QUIC的版本1,它符合在[QUIC-INVARIANTS]中定义的QUIC的版本无关特性。 + +QUIC是一种面向连接的协议,可在客户端和服务端之间建立有状态的交互。 + +QUIC握手结合了加密和传输参数的协商。QUIC集成了TLS握手[TLS13],使用定制的框架来保护数据包。TLS和QUIC的集成在[QUIC-TLS]中有更详细的说明。握手的设计使得可以尽快交换应用数据:如果之前有过握手过程和保存配置,那么客户端可以启用立即发送数据(0-RTT)的选项。 + +在QUIC协议中,终端(Endpoint)通过交换QUIC包(Packet)通信。大多数数据包中包含帧(Frame),帧携带控制信息和应用数据。QUIC验证每个数据包的完整性,并尽可能对所有数据包进行加密。QUIC协议承载在[UDP]协议之上,以更方便其在现有系统和网络中部署。 + +应用层协议建立QUIC连接,在其上通过流(Stream)来交换信息,流是有序的字节序列。QUIC可以创建两种类型的流:双向流和单向流,双向流允许两端互相收发数据,单向流只允许单个终端发送数据。基于credit的方案用于限制流的创建数并限制可以发送的数据量。 + +QUIC提供必要的反馈,以实现可靠传输和拥塞控制。在[QUIC-RECOVERY]第6章中描述了一种用于检测丢包和恢复数据的算法。QUIC依靠拥塞控制来避免网络拥塞。在[QUIC-RECOVERY]第7章有一个示例性的拥塞控制算法。 + +QUIC连接不严格绑定到某条网络路径。连接标识符(CID)的引入允许连接迁移到新的网络路径。不过在当前的QUIC版本中只有客户端才能迁移。这种设计还允许在网络拓扑或地址映射发生变更后重新建链,例如可NAT重新绑定导致的接续。 + +QUIC为终止连接提供多个选项,使得应用可以优雅关闭,终端可以协商超时时间,出现错误会立即导致连接关闭,并且无状态重置(Stateless Reset)机制可以在一个终端失去状态后终止连接。 + +## 1.1. 文档结构(Document Structure) + +本文描述了QUIC协议的核心,文档结构如下: + +* 流是QUIC提供的基本服务抽象: + - 第2章 流相关的核心概念 + - 第3章 流状态参考模型 + - 第4章 流控操作 + +* 连接是QUIC终端通信的上下文 + - 第5章 连接相关的核心概念 + - 第6章 版本协商 + - 第7章 建链流程 + - 第8章 地址校验和拒绝服务攻击的规避措施 + - 第9章 连接迁移 + - 第10章 连接关闭的选项及流程 + - 第11章 流和连接的错误处理指引 + +* 包和帧是QUIC通信的基本单位 + - 第12章 包和帧相关的概念 + - 第13章 数据传输、重传和确认模型 + - 第14章 承载QUIC包的UDP报文大小规则 + +* 最后是QUIC协议元素的编码细节 + - 第15章 版本号约定 + - 第16章 变长整数编码 + - 第17章 包格式详解 + - 第18章 传输参数 + - 第19章 帧格式详解 + - 第20章 错误码 + +随附文档描述了QUIC的丢包检测和拥塞控制(参见[QUIC-RECOVERY])、TLS的使用和其他加密机制(参见[QUIC-TLS])。 + +本文描述了QUIC版本1,其满足[QUIC-INVARIANTS]描述的与版本无关的QUIC协议特性。 + +需要引用QUIC版本1,参考这篇文档,需要引用QUIC版本无关特性的受限集合,请参考[QUIC-INVARIANTS]。 + +## 1.2. 术语与定义(Terms and Definitions) +本文中的关键词"MUST","MUST NOT","REQUIRED","SHALL","SHALL NOT","SHOULD","SHOULD NOT","RECOMMENDED","NOT RECOMMENDED","MAY",以及"OPTIONAL",当且仅当他们全部以大写字母出现的时候,需要按BCP 14[RFC2119][RFC8174]所述的方式理解。 + +本文中常用术语如下所示: + +QUIC(QUIC):本文描述的传输协议,QUIC是一个名字,不是缩略语。 + +终端(Endpoint)参与QUIC连接的实体,可以生成、接收和处理QUIC包。QUIC只有两种类型的终端:客户端和服务端。 + +客户端(Client):发起连接的QUIC终端。 + +服务端(Server):接收连接的QUIC终端。 + +QUIC包(QUIC packet):可被一个UDP报文封装的完整的QUIC处理单元,一个UDP报文可以包含一个或多个QUIC包。 + +ACK触发包(Ack-eliciting packet):包含除ACK、PADDING和CONNECTION_CLOSE以外的帧的QUIC包,触发接收端回应一个ACK确认,参见第13.2.1小节。 + +帧(Frame):QUIC定义的结构化的协议信息单元。QUIC有多种帧类型,不同类型的帧携带不同的信息。一个QUIC包中可包含一个或多个帧。 + +地址(Address):在不加限制的情况下,是由IP版本、IP地址和UDP端口号组成的元组,表示网络路径的一端。 + +连接ID(Connection ID,CID):用于标识QUIC连接终端的标识符。每个终端为其对端选择一个或多个CID,并包含在发送给对端的数据包中。该值在对端是可见的。 + +流(Stream):在QUIC连接中传输有序字节的单向或双向通道。一个QUIC连接可以同时承载多个流。 + +应用(Application):使用QUIC收发数据的实体。 + +本文使用术语"QUIC包", "UDP报文"和"IP包"表示对应的协议单元,也就是,一个或多个QUIC包可以被封装在一个UDP报文内,一个UDP报文被封装在一个IP数据包中。 + +## 1.3. 符号约定(Notational Conventions) +本文中的数据包和数据帧使用自定义的格式说明,引入这种格式是为了简要描述协议元素,而不是正式定义。 + +复合字段:首先给出命名,后接由一对大括号括起的字段列表,列表中的每个字段都用逗号分隔。 + +单个字段:包括长度信息,固定、可选和重复指示,遵循如下符号约定,其中长度单位为位(bit): + +x (A):表示x的长度为A位 + +x (i):表示x是变长整数值,其长度编码参见第16章。 + +x (A..B):表示x是一个长度介于A到B位之间的值,A省略的话,表示从0位开始,B省略的话表示无上限,一般按字节取整 + +x (L) = C:表示x是一个固定值C,x的长度为L,L可以使用上述3种长度形式之一。 + +x (L) = C..D:表示x的取值范围为从C到D闭区间的值,长度为L如上所示。 + +[x (L)]:表示x为可选,且长度为L。 + +x (L) ...:表示x以长度L重复0到多次 + +本文使用网络字节序,即大端字节序。字段从每个字节最高有效位开始,按照惯例,单个字段通过名字来引用复合字段。 + +``` + Example Structure { + One-bit Field (1), + 7-bit Field with Fixed Value (7) = 61, + Field with Variable-Length Integer (i), + Arbitrary-Length Field (..), + Variable-Length Field (8..24), + Field With Minimum Length (16..), + Field With Maximum Length (..128), + [Optional Field (64)], + Repeated Field (8) ..., + } +``` +图1: Example Format + +当在上下文中提到某个1比特长度的字段,可以通过在其所在字节中将该字段所在位设为1,其余置0,来指示该字段的位置,例如,0x80可以表示该字段在该字节的最高有效位上,如图1的One-bit字段。 + +# 2. 流(Streams) +QUIC中的流为应用提供了一个轻量级的、有序的字节流抽象。流可以是单向或双向的。 + +可以通过直接发送数据来创建流。与流管理相关的其他流程,如关闭、取消和流控管理,都旨在带来最小的开销。例如,单个STREAM帧(第19.8节)可以打开流、携带数据并关闭流。流也可以是持久的,可以持续整个连接的生命周期。 + +流可以由任一终端创建,可以与其他流并行交错发送数据,并且可以取消。QUIC不保证不同流上的字节是保序的。 + +QUIC允许任意数量的流并行发送数据,并允许在一个流上发送任意数量的数据,但需要接受流控约束和流限制,参见第4章。 + +## 2.1. 流类型和标识(Stream Types and Identifiers) +流可以是单向和双向的,单向流只从流的发起方发送数据到对端。双向流可以同时收发数据。 + +一个连接中的流由一个62位的整数(0到2^62^-1)标识,称为流ID,由第16章中定义的变长整数编码。在一个连接中,任意流的ID都是不同的,QUIC端禁止(**MUST NOT**)重用流ID。 + +流ID的最低有效位(0x01)标识流的发起者。客户端启动的流的流ID为偶数(位设置为0),服务端启动的流的流ID为奇数(位设置为1)。 + +流ID的第二个最低有效位(0x02)区分双向流(位设置为0)和单向流(位设置为1),因此,来自流ID的两个最低有效位将流识别为四种类型之一,如表1中总结的: + +|Bits| Stream Type | +|:---|:---| +|0x0 | Client-Initiated, Bidirectional | +|0x1 | Server-Initiated, Bidirectional | +|0x2 | Client-Initiated, Unidirectional | +|0x3 | Server-Initiated, Unidirectional | + +表1: Stream ID Types + +每个类型流的ID取值空间从最小值开始(0x00到0x03,相应的),后续的流ID在此基础上线性递增。乱序使用流ID会导致该类型下所有较低编号的流都被打开。 + +## 2.2. 收发数据(Sending and Receiving Data) +STREAM帧(参见第19.8节)封装应用发送的数据,QUIC端在STREAM帧中使用流ID和Offset字段按序放置数据。 + +QUIC端必须(**MUST**)将流数据按序投递给应用,投递有序字节流要求QUIC端缓冲任何无序接收的数据,直至达到通告的流控限制。 + +QUIC协议中没有特别指出允许乱序投递流数据。然而,在实现中可以(**MAY**)选择提供向应用投递无序数据的能力。 + +QUIC端可从某个流中多次接收相同Offset的数据,重复的数据可以(**MAY**)被丢弃,但这些相同Offset的数据不得(**MUST NOT**)改变,如果有变更则视为PROTOCOL_VIOLATION类型的连接错误。 + +流是有序的字节流抽象,QUIC看不到除流外的其他结构。在数据传输、丢包重传或投递给接收端的应用时,QUIC不会保存每个STREAM帧的边界。 + +终端不得(**MUST NOT**)在任何流上发送数据,除非确保数据在其对端设置的流控窗口内,流量控制在第4章中有详细描述。 + +## 2.3. 流的优先级(Stream Prioritization) +如果分配给流的资源的优先级正确,则流复用会对应用性能产生重大改进。 + +QUIC不提供彼此交换优先级信息的机制,而是依赖于从应用接收优先级信息。 + +QUIC的实现应该(**SHOULD**)提供给应用可以指示流的相对优先级的方法,QUIC依据应用提供的信息来确定如何将资源分配给活动流。 + +## 2.4. 基于流的操作(Operations on Streams) +本文没有定义QUIC的API,但定义了一组应用层协议可以依赖的流上的函数。应用层协议可以假设QUIC实现提供了一个接口,其中包括本节中描述的操作。设计用于特定应用层协议的实现可能仅提供该协议需要的那些操作。 + +在流的发送侧,应用层协议可以: + +* 写数据,确保流上有为待写数据保留基于流控的发送许可(第4.1节); +* 结束流(优雅关闭),触发设置了FIN位的STREAM帧(第19.8节); +* 重置流(突然终止),如果流不在终止态,则触发一个RESET_STREAM帧(第19.4节)。 + +在流的接收侧,应用层协议可以: + +* 读数据; +* 终止读取流,请求关闭,可能触发STOP_SENDING帧(第19.5节)。 + +应用层协议还可以请求获知流上的状态变化,包括对端何时打开或重置流、对端何时中止读取流、新数据何时可用以及基于流控,何时可以或不可以写入数据。 + +# 3. 流状态(Stream States) +本节以描述流的接收侧与发送侧来说明流的特性,并介绍了两种状态机:一种是流的发送侧对应的状态机(第3.1节),另一种是流的接收侧的状态机(第3.2节)。 + +单向流依据流类型和端角色,决定用发送状态机还是接收状态机,双向流的两侧,两种状态机都采用。在大多数情况下,无论是单向流还是双向流,这些状态机的使用方式都是相同的。对双向流来说,打开流的条件有一点复杂,因为无论是由发送侧还是接收侧的打开,都会使流双向开启。 + +本节中显示的状态机能提供大量信息。本文使用流状态机来描述这些规则,即在什么场景下,发送什么类型的帧,期待怎样的回应,在什么场景能接收什么类型的帧。尽管状态机旨在方便实现QUIC,但并不限制实现。一个实现可以定义不同的状态机,只要它的行为与其他实现这些状态的实现一致即可。 + +> 注意:在某些场景下,一个事件或动作可能引起状态迁移多次,例如,发送设置FIN位的STREAM帧的动作可引起流的发送侧的两次状态迁移:从Ready态跃迁到Send态,再从Send态迁移到Data Send态。 + +## 3.1. 发送方流状态(Sending Stream States) +下图是流的发送侧的状态机 + +``` + o + | Create Stream (Sending) + | Peer Creates Bidirectional Stream + v + +-------+ + | Ready | Send RESET_STREAM + | |-----------------------. + +-------+ | + | | + | Send STREAM / | + | STREAM_DATA_BLOCKED | + v | + +-------+ | + | Send | Send RESET_STREAM | + | |---------------------->| + +-------+ | + | | + | Send STREAM + FIN | + v v + +-------+ +-------+ + | Data | Send RESET_STREAM | Reset | + | Sent |------------------>| Sent | + +-------+ +-------+ + | | + | Recv All ACKs | Recv ACK + v v + +-------+ +-------+ + | Data | | Reset | + | Recvd | | Recvd | + +-------+ +-------+ + +``` +图2: States for Sending Parts of Streams + +由终端(类型0、2是客户端,1、3是服务端)启动的流的发送侧是由应用创建。Ready态表示新创建的流可以从应用接收数据,流数据在这种状态下可以被缓存以备发送。 + +第一个STREAM或STREAM_DATA_BLOCKED帧的发送使得流的发送侧进入Send态,一个实现可以选择延迟为流分配流ID,直到第一个STREAM发送并进入Send态,这样可以采取更好的优先级策略。 + +由对端(类型0是服务端,1是客户端)启动的双向流,在本端接收侧创建的同时,同步创建发送侧,进入Ready态。 + +在Send态,本端使用STREAM帧传输(必要时重传)流数据,并遵守由对端设置的流控限制,接收和处理MAX_STREAM_DATA帧。如果因为流控限制(第4.1节)暂时不能发送,则会生成并发送STREAM_DATA_BLOCKED帧。 + +在应用指示已发送完所有流数据,并已发送设置了FIN位的STREAM帧后,流的发送侧进入Data Sent态。在此状态下,本端仅在必要时重传流数据(对端ACK显示有丢包),不需要继续检查流控限制或发送STREAM_DATA_BLOCKED帧,此时可能会收到对端的MAX_STREAM_DATA帧,本端可以安全地忽略它们,对端收齐数据之后将不再重复发送MAX_STREAM_DATA。 + +一旦所有流数据都被成功确认,流的发送侧进入Data Recvd状态,这是一个终止状态。 + +在Ready、Send或Data Sent任一状态,应用可以发信号表示它希望放弃流数据传输,或者,本端也可能会收到对端的STOP_SENDING帧。在上述情况下,本端都要发送RESET_STREAM帧,之后进入Reset Sent状态。 + +终端可能(**MAY**)在流上发送的第一帧就是RESET_STREAM,这使得流的发送侧打开后立即切换到Reset Sent状态。 + +一旦包含RESET_STREAM的数据包被确认,流的发送侧立即进入Reset Recvd状态,这也是一个终止状态。 + +## 3.2. 接收方流状态(Receiving Stream States) +图3是从对端接收数据的流的接收侧的状态机,流的接收侧的状态仅反映流的发送侧的一些状态,不会跟踪发送侧上无法观察到的状态,例如Ready状态。相应的,流的接收侧会跟踪向应用投递数据的情况,其中一些状态发送方不可见。 + +``` + o + | Recv STREAM / STREAM_DATA_BLOCKED / RESET_STREAM + | Create Bidirectional Stream (Sending) + | Recv MAX_STREAM_DATA / STOP_SENDING (Bidirectional) + | Create Higher-Numbered Stream + v + +-------+ + | Recv | Recv RESET_STREAM + | |-----------------------. + +-------+ | + | | + | Recv STREAM + FIN | + v | + +-------+ | + | Size | Recv RESET_STREAM | + | Known |---------------------->| + +-------+ | + | | + | Recv All Data | + v v + +-------+ Recv RESET_STREAM +-------+ + | Data |--- (optional) --->| Reset | + | Recvd | Recv All Data | Recvd | + +-------+<-- (optional) ----+-------+ + | | + | App Read All Data | App Read Reset + v v + +-------+ +-------+ + | Data | | Reset | + | Read | | Read | + +-------+ +-------+ + +``` +图3: States for Receiving Parts of Streams + +当端接收到某流的第一个STREAM、STREAM_DATA_BLOCKED或RESET_STREAM帧时,会创建该流的接收侧(客户端的类型1和3,或服务端的类型0和2)。对于由对端发起的双向流,端接收到某流的MAX_STREAM_DATA或STOP_SENDING帧也会创建该流的接收侧,流的接收侧的初始状态是Recv。 + +对于双向流,当流的发送侧(类型0为客户端,类型1为服务端)进入Ready状态时,接收侧进入Recv状态。 + +当接收到MAX_STREAM_DATA或STOP_SENDING帧时,端会打开双向流,因为收到未打开的流的MAX_STREAM_DATA帧表示对端已打开流并提供流控管理,接收到未打开的流的STOP_SENDING帧表示对端不再希望接收此流上的数据。如果数据包丢失或乱序,任一帧都可能在STREAM或STREAM_DATA_BLOCKED帧之前到达。 + +在创建某流ID对应的流之前,所有同类型的流ID比之要小的流都必须(**MUST**)已创建完成,这能确保流的创建顺序在两端都是一致的。 + +在Recv状态下,端接收STREAM和STREAM_DATA_BLOCKED帧,缓存收到的数据,按序重组之后投递给应用,应用消费完数据,缓冲区可用之后可发送MAX_STREAM_DATA帧以允许对端发送更多数据。 + +当接收到带有FIN位的STREAM帧时,流的最终大小确定(参见第4.5节),流的接收侧因此进入Size Known状态。在此状态下,端不再需要发送MAX_STREAM_DATA帧,只需接收某些流数据的重传即可。 + +一旦接收完所有数据,流的接收侧就进入Data Recvd状态,接收到与导致流切换为Size Known态相同的STREAM帧后也可能会切入此状态。接收齐所有数据后,可以丢弃该流上收到的任何STREAM或STREAM_DATA_BLOCKED帧。 + +Data Recvd状态持续到数据被投递给应用,一旦数据投递完,流即进入Data Read状态,这是一个终止状态。 + +在Recv或Size Known状态下收到RESET_STREAM帧会使得流进入Reset Recvd状态。这可能会导致向应用投递流数据的过程中断。 + +在Data Recvd状态,收到RESET_STREAM时,可能已经接收完所有流数据;类似地,在Reset Recvd态,即接收到RESET_STREAM帧后,剩余的流数据也有可能到达。一个实现可以自由选择如何处置这种状况。 + +发送RESET_STREAM意味着端不能保证流数据的完整投递,但收到RESET_STREAM,并不意味着不再投递流数据。一个实现可以中断流数据的投递,丢弃任何未消费的数据,并发出RESET_STREAM的接收信号,但如果此时流数据被完全接收并被缓冲以供应用读取,则实现可以选择扣压RESET_STREAM信号或保留。如果RESET_STREAM被扣压,则流的接收侧仍然处于Data Recvd态。 + +一旦应用接收到指示流被重置的信号,流的接收侧即切换到Reset Read状态,这也是一个终止状态。 + +## 3.3. 允许的帧类型(Permitted Frame Types) + +流的发送方仅发送三种影响发送方或接收方的流状态的帧类型:STREAM(第19.8节),STREAM_DATA_BLOCKED(第19.13节),和RESET_STREAM(第19.4节)。 + +发送方在终止状态(Data Recvd或Reset Recvd)不得(**MUST NOT**)发送任何这些帧。发送方在处于Reset Sent态(即在发送RESET_STREAM帧之后)或终止态时不得(**MUST NOT**)发送STREAM或STREAM_DATA_BLOCKED帧。接收方在任何状态下都可接收这三种帧,因为承载它们的数据包可能存在延迟或乱序。 + +流的接收方可发送MAX_STREAM_DATA帧(第19.10节)和STOP_SENDING帧(第19.5节)。 + +接收方在Recv态时仅可发送MAX_STREAM_DATA帧,在除了Reset Recvd和Reset Read态(即尚未收到RESET_STREAM)的其他状态,都可以发送STOP_SENDING帧。不过,在Data Recvd状态下发送STOP_SENDING帧几乎没有价值,因为所有流数据都已接收。由于数据包的延迟或乱序,发送方可以在任何状态下收到上述两种类型的帧。 + +## 3.4. 双向流状态(Bidirectional Stream States) + +双向流由发送侧和接收侧组成。实现可以将双向流的状态表示为发送侧和接收侧流状态的组合。最简单的模型是在发送侧或接收侧处于非终止状态时将流状态表示为“打开”,当发送侧和接收侧都处于终止状态时将流表示为“关闭”。 + +表2呈现了一个更复杂的双向流状态映射模型,其可以松散地对应到HTTP/2[HTTP2]中定义的流状态。此时,发送侧或接收侧的多个状态被映射到同一个复合状态。请注意,这只是这种映射的一种可能表达,其要求在切换到“关闭”或“半关闭”状态之前确认数据。 + +|Sending Part |Receiving Part |Composite State | +|:---|:---|:---| +|No Stream / Ready |No Stream / Recv (*1) |idle| +|Ready / Send / Data Sent |Recv / Size Known |open| +|Ready / Send / Data Sent |Data Recvd / Data Read |half-closed (remote)| +|Ready / Send / Data Sent |Reset Recvd / Reset Read |half-closed (remote)| +|Data Recvd |Recv / Size Known |half-closed (local)| +|Reset Sent / Reset Recvd |Recv / Size Known |half-closed (local)| +|Reset Sent / Reset Recvd |Data Recvd / Data Read |closed| +|Reset Sent / Reset Recvd |Reset Recvd / Reset Read |closed| +|Data Recvd |Data Recvd / Data Read |closed| +|Data Recvd |Reset Recvd / Reset Read |closed| + +表2: Possible Mapping of Stream States to HTTP/2 + +>Note (*1):如果流尚未被创建或者其接收侧处于Recv态但尚未接收到任何数据,则将流置为 idle态。 + +## 3.5. 请求状态转换(Solicited State Transitions) + +如果应用不再对它在流上接收的数据感兴趣,它可以中止读取流并指定应用级错误码。 + +如果流处于Recv或Size Known状态,传输应该(**SHOULD**)通过发送一个STOP_SENDING帧来提示相反方向的流关闭。这通常表示应用不再读取它从流中接收的数据,但这不是说传入的数据将被忽略。 + +发送STOP_SENDING帧后接收到的STREAM帧仍计入连接和流控,即使这些帧在接收方可能被丢弃。 + +STOP_SENDING帧请求接收到该帧的终端发送RESET_STREAM帧。如果流处于Ready或Send状态,则接收到STOP_SENDING帧的终端必须(**MUST**)发送RESET_STREAM帧。如果流处于Data Sent态,此端可以(**MAY**)延迟发送RESET_STREAM帧,直到已发送的数据都被确认或声明丢失。如果有数据被声明丢失,终端应该(**SHOULD**)发送一个RESET_STREAM帧而不是重传数据。 + +终端应该(**SHOULD**)将错误码从STOP_SENDING帧复制到它发送的RESET_STREAM帧,但也可以使用任何应用级错误码。发送了STOP_SENDING帧的终端可以(**MAY**)忽略随后接收到的RESET_STREAM帧中的错误码。 + +STOP_SENDING应该(**SHOULD**)只由尚未被对端重置的流这一侧发送。STOP_SENDING对于处于Recv或Size Known状态的流最有用。 + +如果先前包含STOP_SENDING的数据包丢失,则本端应再次发送STOP_SENDING帧。但是,一旦所有流数据都收齐或接收到RESET_STREAM帧——也就是说,流处于除Recv或Size Known之外的其他状态——发送STOP_SENDING帧就不必要了。 + +希望将双向流的两个方向都关闭的终端可以通过发送RESET_STREAM帧来终止一个方向,并且可以通过发送STOP_SENDING帧触发相反方向的关闭。 + +# 4. 流量控制(Flow Control) +接收方需要限制他们需要缓冲数据的数据量,以防止被较快的发送方的大量数据淹没或被恶意发送方消耗大量内存。为了使接收方能够限制一个连接的内存消耗,可以将各个流或一个多流连接视为一个整体进行流控。QUIC接收方可以控制发送方在某一个流或所有流上可发送的最大数据量,如第4.1节和第4.2节所述。 + +类似地,为了限制一个连接内的并发流数,QUIC一端可限制对端能发起的最大累加流个数,如第4.6节所述。 + +对CRYPTO帧数据的流控方式与普通数据不同。QUIC依靠加密协议实现来避免数据的过度缓冲,参见[QUIC-TLS]。为了避免在多个层进行过多的缓冲,QUIC实现应该(**SHOULD**)为加密协议实现提供一个接口来告知其缓冲限制。 + +## 4.1. 数据流控(Data Flow Control) + +QUIC采用基于限额的流量控制方案,其中接收方通告它在给定流上或整个连接上可以接收的总字节数的限额。这导致QUIC中有两个级别的数据流控制: +* 基于流的流控,它通过限制可以在单个流上可发送的数据量来防止单个流消耗整个连接的接收缓冲区。 + +* 基于连接的流控,它通过限制所有流上通过STREAM帧发送的流数据的总字节数来防止发送方超出接收方的连接缓冲区容量。 + +发送方不得(**MUST NOT**)发送超过任一限制的数据。 + +接收方在握手期间通过传输参数为所有流设置初始限制(第7.4节)。随后,接收方向发送方发送MAX_STREAM_DATA帧(第19.10节)或MAX_DATA帧(第19.9节)以通告更大的限制。 + +接收方可以通过发送带有相应流ID的MAX_STREAM_DATA帧来通告流的更大限额。MAX_STREAM_DATA帧指定流的绝对字节偏移量上限。接收方可以基于流上当前消费的数据偏移量来确定要通告的流控偏移量。 + +接收方可以通过发送MAX_DATA帧来为一个连接通告更大的限额,该帧指定所有流的绝对字节偏移总和的上限。接收方维护在所有流上接收到的字节的累积总和,用于判定是否违反了通告的连接或流上数据限额。接收方可以根据所有流上消耗的字节总和来确定要通告的最大数据限额。 + +一旦接收方通告了连接或流上的限额,再通告一个更小的限额不会导致错误,但会被发送方忽略。 + +如果发送方违反了通告的连接或流上的数据限额,则接收方必须(**MUST**)以FLOW_CONTROL_ERROR类型的错误关闭连接,有关错误处理的详细信息,请参阅第11章。 + +发送方必须(**MUST**)忽略不增加流控限额的任何MAX_STREAM_DATA或MAX_DATA帧。 + +如果发送方已发送数据达到限额,则将无法发送新数据并被视为阻塞,发送方应该(**SHOULD**)发送一个STREAM_DATA_BLOCKED或DATA_BLOCKED帧来向接收方表明它有数据要写入但被流控限额阻塞。如果发送方被阻塞的时间长于空闲超时定时器(第10.1节),即使发送方有可用于传输的数据,接收方也可能关闭连接。为了防止连接关闭,受流控阻塞的发送方应该(**SHOULD**)在没有ACK触发包数据包传输时定期发送STREAM_DATA_BLOCKED或DATA_BLOCKED帧。 + +## 4.2. 增加流控上限(Increasing Flow Control Limits) + +在MAX_STREAM_DATA和MAX_DATA帧中通告多少限额以及何时发送这两种帧,是由实现决定的,但本节将提供了一些注意事项。 + +为避免阻塞发送方,接收方可以(**MAY**)在一个RTT中多次发送MAX_STREAM_DATA或MAX_DATA帧,或者提前足够时间量发送这两种帧,以便为丢包和后续恢复留出时间。 + +控制帧会增加连接开销,因此,频繁发送变化很小的MAX_STREAM_DATA和MAX_DATA帧是不可取的。另一方面,如果更新不那么频繁,则需要更大的限额增量以避免阻塞发送方,从而要求接收方承担更大的资源消耗。在确定通告多大的限额时,需要在资源消耗和连接开销之间进行权衡。 + +接收方可以使用自动调整机制,根据RTT估计或应用消费接收到的数据的速率来调整通告限额的频率和数值,类似于常见的TCP实现。作为一种优化,只有当有其他帧要发送时,本端才可以发送与流控相关的帧,以确保流控不会导致发送额外的数据包。 + +被阻塞的发送方可以不发送STREAM_DATA_BLOCKED或DATA_BLOCKED帧。因此,接收方不得(**MUST NOT**)等待STREAM_DATA_BLOCKED或DATA_BLOCKED帧到达之后再发送MAX_STREAM_DATA或MAX_DATA帧,因为这样做可能会导致发送方在连接的其余时间被阻塞。即使发送方发送了这些帧,接收方等待它们到达再回应也会导致发送方被阻塞至少整个RTT。 + +当发送方在被阻塞后收到新的限额时,它可能会发送大量数据作为响应,导致短期拥塞。有关发送方如何避免这种拥塞的讨论,请参见[QUIC-RECOVERY]第7.7节。 + +## 4.3. 流控性能(Flow Control Performance) + +如果本端无法确保其对端始终具有大于此连接上的带宽延迟乘积的可用流控限额,则其接收吞吐量将受到流控的限制。 + +数据包丢失会导致接收缓冲区出现间隙,从而阻止应用消费数据并释放接收缓冲区空间。 + +及时发送流控限额的更新可以提高性能。仅发送携带流控帧的数据包会增加网络负载并对性能产生不利影响。将流控帧与其他帧(例如ACK帧)一起发送可降低这些更新的成本。 + +## 4.4. 处理流关闭(Handling Stream Cancellation) + +终端间需要最终就每个流消耗的流控限额达成一致,以便能够满足连接级流量控制。 + +收到RESET_STREAM帧后,终端将设置匹配的流的状态为终止态,并忽略到达该流的其他数据。 + +RESET_STREAM可以立即终止流的一个方向。对于双向流,RESET_STREAM对相反方向的数据流没有影响。两端都必须(**MUST**)在未终止方向上保持流的流控状态,直到该方向进入终止状态。 + +## 4.5. 流最终大小(Stream Final Size) + +最终大小是流消耗的流控限额。假设流上的每个连续字节都发送一次,最终大小就是发送的字节数,更一般地说,是比这些字节中的最大偏移字节量高1,如果没有发送字节,则为零。 + +无论流如何终止,发送方始终将流的最终大小可靠地传递给接收方。最终大小是带有FIN标志的STREAM帧的Offset字段与Length字段的总和,注意这些字段可能是隐式的。或者,RESET_STREAM帧的Final Size字段会携带此值,这保证了两端就发送方在该流上消耗了多少流控限额达成一致。 + +当流的接收侧进入Size Known或Reset Recvd态(参见第3章)时,终端将知道流的最终大小。接收方必须(**MUST**)使用流的最终大小作为流上发送的所有字节数来参与连接级流控的计算。 + +终端不得(**MUST NOT**)在流上发送等于或超过最终大小的数据。 + +一旦知道流的最终大小,它就不能变化。如果接收到的RESET_STREAM或STREAM帧指示流的最终大小发生变化,终端应该(**SHOULD**)响应类型为FINAL_SIZE_ERROR的错误。有关错误处理的详细信息,请参阅第11章。接收方应该(**SHOULD**)将接收到大于等于最终大小的数据视为FINAL_SIZE_ERROR类型的错误,即使在流关闭之后也是如此。不过生成这些错误不是强制性的,因为要求端生成这些错误也意味着端需要维护流关闭后的最终大小,这可能意味着重要的状态承诺。 + +## 4.6. 控制并发(Controlling Concurrency) + +本端可以限制对端可以打开的传入流的累积数量,只能打开流ID小于 (max_streams * 4 + first_stream_id_of_type)的流,见表1。初始限制在传输参数中设置,参见第18.2节,随后可以使用MAX_STREAMS帧通告后续限额,参见第19.11节。单向和双向流分别有各自的限额。 + +如果接收到的max_streams传输参数或MAX_STREAMS帧的值大于2^60^,这将导致最大的流ID不能被变长整数进行表示,参见第16章。上述两种情况,连接都必须(**MUST**)关闭。如果有问题的值是在传输参数中收到的,连接错误类型为TRANSPORT_PARAMETER_ERROR;如果是在帧中收到,则连接错误类型为FRAME_ENCODING_ERROR。详情参见第16章。 + +终端不得(**MUST NOT**)超过其对端设置的限制。接收到流ID超过其发送限制的帧的终端必须(**MUST**)将此视为STREAM_LIMIT_ERROR类型的连接错误,有关错误处理的详细信息,请参阅第11章。 + +一旦接收方使用MAX_STREAMS帧通告流限额,再通告较小的限额就无效。必须(**MUST**)忽略不增加流限额的MAX_STREAMS帧。 + +与流和连接上的流控制一样,本文让实现来决定何时以及什么场景应该通过MAX_STREAMS向对端通告允许多少个流。当流关闭时,实现可能会选择增加限制,以保持对端可用的流数量大致一致。 + +由于对端的限制而无法打开新流的终端应该(**SHOULD**)发送一个STREAMS_BLOCKED帧 (参见第19.14节)。该信号对调试很有用。终端不得(**MUST NOT**)等待此信号到达后再通告额外的限额,否则将意味着对端将被阻塞至少整个RTT,如果对端选择不发送STREAMS_BLOCKED帧,则可能无限期地阻塞。 + +# 5. 连接(Connections) + +QUIC连接在客户端和服务端之间共享状态。 + +每个连接都从握手阶段开始,在此期间,两端使用加密握手协议[QUIC-TLS]协商共享秘钥及应用层协议。两端通过握手(第7章)确认通讯意愿(第8.1节)并为连接交换参数(第7.4节)。 + +应用层协议可以在握手阶段使用连接,但有一些限制。0-RTT允许客户端在收到服务端响应之前发送应用数据。然而,0-RTT没有提供针对重放攻击的保护,参见[QUIC-TLS]第9.2节。服务端也可以在收到最终加密握手消息(确认客户端的身份和活性之用)之前发送应用数据到客户端。这些功能为应用层协议提供牺牲安全性以换取较低时延的选项。 + +连接ID的使用(第5.1节)允许连接迁移到新的网络路径,终端可以直接发起迁移,也可以在中间设备变更时强制迁移。第9章描述了与迁移相关的安全和隐私问题的治理措施。 + +对于不再需要的连接,客户端和服务端可以通过多种方式终止连接,如第10章所述。 + +## 5.1. 连接ID(Connection ID,CID) + +每个连接都拥有一组连接标识符或称连接ID(后续统一简称“CID”),每个连接标识符都可以标识该连接。每个终端都可以独立选择自己的CID供对端使用。 + +CID的主要功能是确保较低协议层(UDP、IP)的寻址更改不会导致QUIC连接的数据包传输到错误的终端。任一终端都使用特定于实现(也可能是特定于部署)的方法选择CID,使得对端发送的携带该CID的数据包能路由过来,并在接收到时识别出来。 + +终端可以使用多个CID,以便外部观察者在没有终端协作上下文时无法识别来自同一连接的不同CID的数据包,参见第9.5节。 + +CID不得(**MUST NOT**)包含任何可被外部观察者(即不与发送端合作的观察者)用于将它们与同一连接的其他CID相关联的信息。作为一个简单的例子,这意味着同一连接上不得(**MUST NOT**)多次给出相同CID。 + +具有长报文头的数据包包含源连接ID(Source Connection ID,后续统一简称“SCID”)和目的连接ID(Destination Connection ID,后续统一简称“DCID”)字段。这些字段可用于填写新的CID,详细信息请参阅第7.2节。 + +具有短报文头的数据包(第17.3节)仅包含DCID,并显式省略长度。对端应明确知道DCID字段的长度。使用基于CID进行路由的负载均衡器的终端,可以与负载均衡器就CID的固定长度达成一致,或者就编码方式达成一致,约定在某固定部分显式编码长度信息,这样即使CID的长度不同,仍然能为负载均衡器使用。 + +Version Negotiation包(第17.2.1小节)回显客户端填写的CID,以便正确路由到客户端并证明该数据包是对客户端发送的数据包的响应。 + +当不需要用CID路由到正确的终端时,可以使用零长度CID。但是,使用零长度CID时,如果在同一本地IP地址和UDP端口上复用连接,可能导致对端在连接迁移、NAT重新绑定和客户端端口重用的时候失败。使用零长度CID时,终端不得(**MUST NOT**)为多个并发连接使用相同的IP地址和端口,除非确定不需要使用这些协议功能。 + +当终端使用非零长度的DCID发送数据包时,它需要在对端用NEW_CONNECTION_ID帧(参见第19.15节)提供的CID列表中选择一个。 + +### 5.1.1. 发布连接ID(Issuing Connection IDs) + +每个CID都有一个对应的序号,以便在NEW_CONNECTION_ID或RETIRE_CONNECTION_ID帧时引用它。在握手期间,终端发出的长包头(第17.2节)的SCID字段中会携带初始CID,其序号为0,如果在传输参数中也携带了preferred_address CID,则该CID的序号为1。 + +可以使用NEW_CONNECTION_ID帧(第19.15节)将其他CID发送给对端,但每个新发布的CID的序号必须(**MUST**)加1。客户端发送的第一个DCID字段指定的CID和Retry包中的CID的都不需要指定序号。 + +当终端发布CID后,它在连接期间或者说直到对端通过RETIRE_CONNECTION_ID帧(第19.16节)停用该CID之前,都必须(**MUST**)接收携带此CID的数据包。已发布但未停用的CID被视为活动ID,任何活动CID都可以在任何时间在当前连接的任意类型数据包中使用。这也包括服务端通过preferred_address传输参数发布的CID。 + +终端应该(**SHOULD**)确保它的对端有足够数量的可用和未使用的CID。终端使用active_connection_id_limit传输参数通告他们愿意维护的活动CID的数量。终端不得(**MUST NOT**)提供超过对端限制的CID数。如果在NEW_CONNECTION_ID帧中同时指定足够大的Retire Prior To字段让对端停用该序号之前的所有CID,可以(**MAY**)临时超出限制。 + +NEW_CONNECTION_ID帧可能会导致终端增加一些活动CID并根据Retire Prior To字段停用某些CID。在处理NEW_CONNECTION_ID帧增加或停用活动CID后,如果活动CID的数量仍然超过其active_connection_id_limit传输参数中通告的值,则终端必须(**MUST**)关闭连接并显示CONNECTION_ID_LIMIT_ERROR类型的错误。 + +当对端停用某个CID后,终端应该(**SHOULD**)提供一个新的CID。如果该终端提供的CID数量未达到对端的active_connection_id_limit限制,则其在收到具有以前未使用(但发布过)的CID的数据包时可以(**MAY**)提供新的CID。终端可以(**MAY**)自行限制连接发布的CID总数,以避免CID耗尽的风险(参见第10.3.2小节),这样做也可以(**MAY**)减少它需要维护的每条路径的状态数量,就如路径验证(活跃)状态,每个发布的CID,都有可能对应一条交互路径(维护起来很耗资源)。 + +发起连接迁移并需要非零长度CID的终端应该(**SHOULD**)确保其对端可用的CID池还有余量,以允许对端在迁移时使用新的CID,如果该池耗尽,对端将无法响应。 + +在握手期间选择零长度CID的终端不能发布新的CID,通过任何网络路径向此终端发送的任何数据包的DCID字段的长度都必须为0。 + +### 5.1.2. 消费和停用连接ID(Consuming and Retiring Connection IDs) + +在连接期间,终端可以随时将其填写的DCID变更为另一个可用的CID。终端在迁移时使用CID以响应对端,有关更多详细信息,请参阅第9.5节。 + +终端维护一组从其对端接收到的CID,在发送数据包时可以使用其中的任何一个。当终端希望在使用中删除某个CID时,它会向其对端发送RETIRE_CONNECTION_ID帧,表示不会再次使用该CID,并请求对端发送NEW_CONNECTION_ID帧将其替换为新的CID。 + +如第9.5节所述,终端将CID限制为关联单个本端地址或单个目的地址。当终端不再需要使用该CID关联的本地地址或目的地址时,则应该(**SHOULD**)停用该CID。 + +在某些情况下,终端可能需要停止接受某些其先前发布的CID,则可以发送NEW_CONNECTION_ID,内含Retire Prior To字段,表示先于此序号的CID全部停用。终端应该(**SHOULD**)继续允许接收先前发布的CID,直到它们被对端停用。如果终端不能再处理指定的CID,它可以(**MAY**)关闭连接。 + +在接收到内含增长的Retire Prior To字段的NEW_CONNECTION_ID帧后,在将新提供的CID添加到活动CID集合之前,对端必须(**MUST**)停用相应的CID并通过发送RETIRE_CONNECTION_ID帧通知对方。这种顺序使得终端可以替换所有活动CID,而不会出现对端没有可用CID的可能性,并且不会超出对端在active_connection_id_limit传输参数中设置的限制,参见第18.2节。在发送RETIRE_CONNECTION_ID请求时未能及时停用该CID可能导致连接失败,因为对端可能已经无法使用该CID。 + +在尚未收到对应的RETIRE_CONNECTION_ID帧的确认之前,终端应该(**SHOULD**)限制本地停用的CID的数量。终端应该(**SHOULD**)允许发送和跟踪至少两倍于active_connection_id_limit传输参数值的RETIRE_CONNECTION_ID帧的数量。就算可以(**MAY**)将超过active_connection_id_limit限制的需要停用的CID视为CONNECTION_ID_LIMIT_ERROR类型的连接错误,终端也不得(**MUST NOT**)在未停用CID的情况下遗忘该CID。 + +在发送NEW_CONNECTION_ID帧,携带的Retire Prior To值停用所有CID之后,收到对端响应的RETIRE_CONNECTION_ID帧之前,终端不应(**SHOULD NOT**)发送新的NEW_CONNECTION_ID帧更新Retire Prior To字段。 + +## 5.2. 匹配连接与数据包(Matching Packets to Connections) + +传入的数据包在接收时会被分类。一类数据包可以关联到现有连接,另一类数据包——对服务端——可能创建一个新连接。 + +如果数据包具有与现有连接对应的非零长度的DCID,QUIC会相应地处理该数据包,将之与该CID关联。请注意,一个连接可以关联多个CID,参见第5.1节。 + +如果DCID的长度为零,并且数据包中的寻址信息,与终端用来标识具有零长度CID的连接的寻址信息匹配,则QUIC会将数据包作为该连接的一部分进行处理。终端可以仅使用目的IP和端口或同时使用源地址和目的地址进行标识,尽管这会使连接变得脆弱,如第5.1节所述。 + +终端可以为任何不能归属于现有连接的数据包发送Stateless Reset包(参见第10.3节)。无状态重置机制允许对端更快地识别连接何时变得不可用。 + +如果数据包与该连接的状态不一致,则即使与现有连接匹配的数据包也将被丢弃。例如,如果数据包指示的协议版本与连接的协议版本不同,或者预期的密钥变得可用,但数据包保护策略去除失败,则数据包将会被丢弃。 + +缺乏强大完整性保护的无效数据包,例如Initial包、Retry包或Version Negotiation包之类的数据包也可以(**MAY**)被丢弃。如果在发现错误之前处理了这些数据包的内容,则终端必须(**MUST**)生成连接错误,或者完全回滚在该处理期间所做的任何更改。 + +### 5.2.1. 客户端数据包处理(Client Packet Handling) + +发往客户端的有效数据包总是包含与客户端选择的值匹配的DCID。选择接收零长度CID数据包的客户端可以使用本地地址和端口来标识连接。与现有连接不匹配的数据包——基于DCID,或者,如果此值为零长度,基于本地IP地址和端口——将被丢弃。 + +由于数据包乱序或丢失,客户端可能会收到使用尚未启用的密钥进行加密的数据包。客户端可以(**MAY**)丢弃这些数据包,或者也可以(**MAY**)缓存它们以待后续秘钥解密。 + +如果客户端接收到的数据包使用的版本与它最初选择的版本不同,都必须(**MUST**)丢弃该数据包。 + +### 5.2.2. 服务端数据包处理(Server Packet Handling) +如果服务端收到一个其不支持的版本的数据包,但是该数据包的长度满足它支持的某个版本的数据包大小限制,则服务端应该(**SHOULD**)发送一个Version Negotiation包以开启新连接,如第6.1节所述。同时,服务端可以(**MAY**)限制Version Negotiation包的回应频率,服务端必须(**MUST**)丢弃比它支持的版本长度要小的数据包。 + +对服务端收到的第一个其不支持版本的数据包,某些特定于版本的字段可能有不同的语义和编码。特别的是,不同的版本可能有不同的数据包保护秘钥,服务端不太可能解码其不支持的版本的数据包。但如上所述,如果其长度合适,服务端应该(**SHOULD**)回以其支持版本的协商包。 + +具有受支持版本或无版本字段的数据包使用CID或本地地址和端口(对于具有零长度CID的数据包)匹配连接,之后由匹配的连接进行后续处理。如果匹配失败,服务端的处理如下所述: + +如果数据包是完全符合规范的Initial包,则服务端继续握手(参见第7章),这会使得服务端切换到客户端选择的版本。 + +如果服务端拒绝接受新连接,它应该(**SHOULD**)发送一个Initial包,其中包含一个错误码为CONNECTION_REFUSED的CONNECTION_CLOSE帧。 + +如果数据包是0-RTT包,服务端可以(**MAY**)缓存有限数量的这类数据包,以等待随后发送的Initial包。客户端在收到服务端响应之前无法发送Handshake包,因此服务端在这种场景下应该(**SHOULD**)忽略任何此类Handshake包。 + +除此以外的其他任何场景下,服务端都必须(**MUST**)丢弃传入的数据包。 + +### 5.2.3. LB注意事项( Considerations for Simple Load Balancers) + +服务端部署的时候可以仅使用源和目的IP地址和端口在服务端之间进行负载均衡。对客户端IP地址或端口的更改可能会导致数据包被转发到错误的服务端。当客户端的地址更改时,可以使用以下方法之一部署服务端来保证连接的连续性: + +服务端可以使用带外机制根据CID将数据包转发到正确的服务端。 + +如果服务端可以使用专用的服务端IP地址或端口,而不是客户端最初连接到的IP地址或端口,则服务端可以使用preferred_address传输参数来请求客户端将连接移动到该专用地址。请注意,客户端可以选择不使用preferred_address。 + +如果部署中的服务端没有具体的实施方案以在客户端地址更改时保持连接的连续性,应该(**SHOULD**)使用disable_active_migration传输参数指示其不支持迁移。disable_active_migration传输参数不会在客户端对preferred_address传输参数进行操作后禁止连接迁移。 + +使用这种简单形式的负载均衡的服务端部署必须(**MUST**)避免创建无状态重置预言机制;参见第21.11节。 + +## 5.3. 连接操作(Operations on Connections) + +本文没有定义QUIC的API,不过,它为应用层协议可以依赖的QUIC连接定义了一组函数。应用层协议可以假设QUIC实现提供了一个接口,其中包括本节中描述的操作。设计用于特定应用层协议的QUIC实现可能仅提供该协议要用到的那些操作。 + +作为客户端角色,应用层协议可以: + +* 打开一个连接,与服务端开始第7章中描述的交互; +* 在可用时启用Early Data(0-RTT数据); + +当Early Data被服务端接受或拒绝时被通知。 + +作为服务端角色,应用层协议可以: + +* 侦听传入连接,为第7章中描述的交互做准备; +* 如果支持Early Data,则在发送给客户端的TLS会话恢复消息中嵌入应用控制数据; +* 如果支持Early Data,则从客户端的TLS会话恢复消息中提取应用控制数据,并根据该信息选择接受或拒绝Early Data。 + +作为任一角色,应用层协议都可以: + +* 为每种类型允许的流个数初始值设置最小值,如传输参数中所述(第7.4节); +* 通过为流和连接设置流控限额来控制接收缓冲区的资源分配; +* 确认握手是成功完成还是仍在进行中; +* 通过生成PING帧(第19.2节)或在空闲超时到期之前请求传输层发送其他帧(第10.1节),保持连接不静默关闭; +* 立即关闭(第10.2节)连接。 + +# 6. 版本协商(Version Negotiation) + +版本协商机制使得服务端可以表明它不支持客户端正在使用的版本。服务端发送Version Negotiation包以响应可能启动新连接的每个数据包,详细信息请参阅第5.2节。 + +客户端发送的第一个数据包的大小将决定服务端是否发送Version Negotiation包。支持多个QUIC版本的客户端应该(**SHOULD**)确保他们发送的第一个UDP报文的大小,是其支持的所有版本的最小容许数据报文长度中的最大值,必要时使用PADDING帧(第19.1节)。这可确保服务端在存在彼此都支持的版本时做出响应。如果服务端收到的UDP报文长度小于其支持的所有版本的最小容许报文长度的最小值,则它可能不会发送Version Negotiation包,见第14.1节。 + +## 6.1. 发送版本协商包(Sending Version Negotiation Packets) + +如果客户端选择的版本不为服务端所接受,则服务端以Version Negotiation包回应(参见第17.2.1小节),包里面包括服务端可接受的版本列表。客户端不得(**MUST NOT**)发送Version Negotiation包来响应接收到的Version Negotiation包。 + +本系统允许服务端在丢失状态的情况下处理其不支持版本的数据包。因为其作为响应发送的Initial包或Version Negotiation包可能会丢失,但客户端会发送新数据包,直到它成功收到响应或放弃连接尝试。 + +服务端可以(**MAY**)控制它发送的Version Negotiation包的数量。例如,能够将数据包识别为0-RTT包的服务端可能会选择不发送Version Negotiation包响应,期望客户端最终会发送Initial包。 + +## 6.2. 处理版本协商包(Handling Version Negotiation Packets) + +Version Negotiation包的设计是为了让QUIC协商用于连接的版本以支持将来定义的功能。未来的标准跟踪规范可能会改变使用当前版本建立连接,但支持多QUIC版本的客户端在收到Version Negotiation包时的实现方式。 + +仅支持当前QUIC版本的客户端如果收到一个Version Negotiation包,则必须(**MUST**)放弃当前的连接尝试,以下场景除外: + +如果客户端收到并成功处理了其他数据包(包括早前的Version Negotiation包),则客户端必须(**MUST**)忽略此Version Negotiation包。 + +如果此Version Negotiation包内支持的版本列表包含了当前客户端使用的版本,则客户端必须(**MUST**)忽略此Version Negotiation包。 + +如何实现版本协商是留给未来的标准跟踪规范定义的工作。特别是,未来的工作需要确保对版本降级攻击的鲁棒性,见第21.12节。 + +## 6.3. 使用保留版本(Using Reserved Versions) + +为了支持将来服务端使用的新版本,客户端需要正确处理不受支持的版本。一些版本号(0x?a?a?a?a,如第15章所定义)被保留用于包含版本号的字段中。 + +终端可以(**MAY**)在任何会被未知或不支持版本忽略的字段中添加保留版本号,以测试对端是否正确忽略了该值。例如,终端可以在Version Negotiation包中包含保留版本号,参见第17.2.1小节。终端可以(**MAY**)发送带有保留版本号的数据包,以测试对端是否正确丢弃了该数据包。 + +# 7. 加密和传输握手(Cryptographic and Transport Handshake) + +QUIC依靠结合加密和传输握手来尽可能降低连接建立时延。QUIC使用CRYPTO帧(第19.6节)来传输加密握手信息。本文中定义的QUIC版本标识为0x00000001,并使用[QUIC-TLS]中描述的TLS加密协议,不同的QUIC版本可能使用不同的加密握手协议。 + +QUIC提供可靠、保序的加密握手数据传输。QUIC的数据包保护机制会加密尽可能多的握手报文。加密握手必须(**MUST**)提供以下属性: + +* 经过认证的秘钥交换,其中 + - 服务端需要进行身份认证; + - 客户端可选地进行身份认证; + - 每次连接都需要产生不同且不相关的密钥,并且 + - 密钥材料可用于0-RTT和1-RTT包保护。 +* 经过认证的两个终端进行传输参数交换,以及服务端传输参数的机密性保护(参见第7.4节)。 +* 经过认证的应用层协议协商(TLS为此使用应用层协议 (ALPN)[ALPN]协商)。 + +CRYPTO帧可以在不同的数据包编号(Packet Number,后续统一简称“包号”)空间中发送(第12.3节)。CRYPTO帧使用的偏移量用于确保加密握手数据的有序传输,每个包号空间中的编号从0开始。 + +图4显示了一个简化的握手以及握手期间数据包和帧的交换过程。以星号(“*”)提示的是在握手期间可以进行应用数据交换的步骤。一旦握手完成,终端就可以自由地交换应用数据了。 + +``` + Client Server + + Initial (CRYPTO) + 0-RTT (*) ----------> + Initial (CRYPTO) + Handshake (CRYPTO) + <---------- 1-RTT (*) + Handshake (CRYPTO) + 1-RTT (*) ----------> + <---------- 1-RTT (HANDSHAKE_DONE) + + 1-RTT <=========> 1-RTT + +``` +图4: Simplified QUIC Handshake + +终端可以在握手期间发送数据包来测试显式拥塞通知(ECN)支持,详情参见第13.4节。如果支持ECN,终端会在回应对端的第一个数据包的ACK帧内携带ECN计数,如第13.4.2小节所述。 + +终端必须(**MUST**)明确协商应用层协议,这可以避免对正在使用的协议存在分歧的情况。 + +## 7.1. 握手流程示例(Example Handshake Flows) + +[QUIC-TLS]中提供了有关TLS如何与QUIC集成的详细信息,此处仅展示一些示例。第8.1.2小节的示例有支持客户端地址验证交换的扩展版本。 + +一旦任何地址验证交换完成,就可以启动加密握手协商加密密钥。加密握手在Initial(第17.2.2小节)和Handshake(第17.2.4小节)数据包中携带。 + +图5是1-RTT握手的概要图,其每行显示一个QUIC包,行首显示数据包类型和包号,冒号“:”后是通常包含在这些数据包中的帧。例如,第一个数据包的类型为Initial,包号为0,并包含一个携带ClientHello的CRYPTO帧。 + +多个QUIC包——即使是不同的数据包类型——都可以合并成一个单一的UDP报文,参见第12.2节节。因此,这种握手可以包含少则四个UDP报文或更多(受协议固有的限制,例如拥塞控制和反放大机制)。例如,服务端的第一个UDP报文就包含Initial包、Handshake包和1-RTT包中的“0.5-RTT数据”。 + +``` + Client Server + + Initial[0]: CRYPTO[CH] -> + + Initial[0]: CRYPTO[SH] ACK[0] + Handshake[0]: CRYPTO[EE, CERT, CV, FIN] + <- 1-RTT[0]: STREAM[1, "..."] + + Initial[1]: ACK[0] + Handshake[0]: CRYPTO[FIN], ACK[0] + 1-RTT[0]: STREAM[0, "..."], ACK[0] -> + + Handshake[1]: ACK[0] + <- 1-RTT[1]: HANDSHAKE_DONE, STREAM[3, "..."], ACK[0] + +``` +图5: Example 1-RTT Handshake + +图6是0-RTT握手,发送单个0-RTT包的连接示例。注意到,如第12.3节所述,服务端在1-RTT包中确认客户端0-RTT数据,客户端在相同的包号空间中发送1-RTT包。 + +``` + Client Server + + Initial[0]: CRYPTO[CH] + 0-RTT[0]: STREAM[0, "..."] -> + + Initial[0]: CRYPTO[SH] ACK[0] + Handshake[0] CRYPTO[EE, FIN] + <- 1-RTT[0]: STREAM[1, "..."] ACK[0] + + Initial[1]: ACK[0] + Handshake[0]: CRYPTO[FIN], ACK[0] + 1-RTT[1]: STREAM[0, "..."] ACK[0] -> + + Handshake[1]: ACK[0] + <- 1-RTT[1]: HANDSHAKE_DONE, STREAM[3, "..."], ACK[1] + +``` +图6: Example 0-RTT Handshake + +## 7.2. 协商连接ID(Negotiating Connection IDs) + +CID用于确保数据包的路由一致,如第5.1节所述。长包头含两个CID:DCID由数据包的接收方选择,用于提供一致的路由,SCID用于让对端设置DCID。 + +在握手期间,带有长包头(第17.2节)的数据包用于确立两端使用的CID。每个终端使用SCID字段来告知对端,让对端在发送给他们的数据包中填入DCID字段。在处理第一个Initial包后,每个终端将其发送的后续数据包中的DCID字段设置为其接收到的SCID字段的值。 + +如果之前未从服务端收到Initial包或Retry包,客户端会使用不可预测的值填充DCID字段。该DCID的长度必须(**MUST**)至少为8个字节。在收到来自服务端的数据包之前,客户端在此连接上发送的所有数据包都必须(**MUST**)使用相同的DCID值。 + +客户端发送的第一个Initial包中的DCID字段用于确定Initial包的数据包保护密钥,这些密钥在收到Retry包后会发生变化;参见[QUIC-TLS]第5.2节。 + +客户端使用其选择的值填充SCID字段,并设置SCID长度字段以指示长度。 + +客户端第一次发送的0-RTT包与第一个Initial包需要填写相同的DCID和SCID值。 + +在第一次从服务端收到Initial或Retry包后,客户端使用服务端提供的SCID作为后续发送的数据包的DCID,包括任何0-RTT包。这意味着客户端可能需要在连接建立期间两次更改它在DCID字段中填入的CID:一次响应来自服务端的Initial包,一次响应Retry包。一旦客户端从服务端收到一个有效的Initial包,在该连接上接收到的具有不同SCID的任何后续数据包都必须(**MUST**)丢弃。 + +在收到的第一个Initial或Retry包后,客户端必须(**MUST**)将发送数据包的DCID改为新的CID。服务端必须(**MUST**)根据第一个收到的Initial包设置它用于发送数据包的DCID。仅当值取自NEW_CONNECTION_ID帧时,才允许变更DCID;如果后续的Initial包包含不同的SCID,它们必须(**MUST**)被丢弃。这避免了对具有不同SCID的多个Initial包进行可能的无状态处理而导致的不可预测的结果。 + +终端发送的DCID可以在连接的生命周期内改变,特别是在响应连接迁移时(第9章),详细信息请参阅第5.1.1小节。 + +## 7.3. 认证连接ID(Authenticating Connection IDs) + +在握手期间对CID进行操作,另一个选择是对所有相关传输参数进行认证,参见第7.4节。这确保用于握手的所有CID也通过加密握手进行身份认证。 + +每个终端在发送Initial包时,需将其在数据包头中填写的SCID字段,也填入initial_source_connection_id传输参数中,参见第18.2节。服务端收到Initial包后,也需要将收到的数据包头中的DCID值,填入其响应的数据包的original_destination_connection_id传输参数中。如果因为某些原因,服务端需要回应Retry包,则需要在retry_source_connection_id传输参数中填入Retry包头的SCID字段。 + +上述握手期间填写的传输参数的值必须(**MUST**)与本端发送(或接收,用于服务端)的实际值相匹配,接收端也必须(**MUST**)验证这一点。这样可以确保攻击者无法在握手过程中,在其篡改的数据包中注入自己的CID,影响后续正确CID的选择。 + +若来自任一终端的initial_source_connection_id传输参数缺失,或来自服务端的original_destination_connection_id传输参数缺失,终端必须(**MUST**)视为TRANSPORT_PARAMETER_ERROR类型的连接错误。 + +终端必须(**MUST**)将以下内容视为TRANSPORT_PARAMETER_ERROR或PROTOCOL_VIOLATION类型的连接错误: +* 收到服务端Retry包,但未填写retry_source_connection_id传输参数; +* 未收到服务端的Retry包,但却填写了retry_source_connection_id传输参数; +* 对端填写的相应传输参数与Initial包中的DCID或SCID字段不匹配; + +如果选择了零长度CID,则相应的传输参数将包含一个零长度值。 + +图7展示了在完整握手中如何使用连接ID(DCID=目的连接ID,SCID=源连接ID),包括Initial包交换时的CID填写,以及后续1-RTT包交换时DCID的填写方式: + +``` + Client Server + + Initial: DCID=S1, SCID=C1 -> + <- Initial: DCID=C1, SCID=S3 + ... + 1-RTT: DCID=S3 -> + <- 1-RTT: DCID=C1 + +``` +图7: Use of Connection IDs in a Handshake + +图8展示了增加了Retry包交换的握手过程: + +``` + Client Server + + Initial: DCID=S1, SCID=C1 -> + <- Retry: DCID=C1, SCID=S2 + Initial: DCID=S2, SCID=C1 -> + <- Initial: DCID=C1, SCID=S3 + ... + 1-RTT: DCID=S3 -> + <- 1-RTT: DCID=C1 + +``` +图8: Use of Connection IDs in a Handshake with Retry + +在这两种情况下(图7和图8),客户端都将initial_source_connection_id传输参数的值设置为C1。 + +当握手不包括Retry时(图7),服务端将original_destination_connection_id设置为S1(注意该值由客户端选择),并将initial_source_connection_id设置为S3。在这种情况下,服务端不填写retry_source_connection_id传输参数。 + +当握手包括Retry时(图8),服务端将original_destination_connection_id设置为S1,retry_source_connection_id设置为S2,并将initial_source_connection_id设置为S3。 + +## 7.4. 传输参数(Transport Parameters) + +在连接建立期间,两端都会对其传输参数进行了经过身份认证的声明。终端需要遵循每个参数定义的限制,也需要遵循其参数描述中的处理原则。 + +传输参数由两端独立声明。每个终端都可以无视对端参数,自行选择传输参数值。 + +传输参数的编码在第18章中有详细说明。 + +QUIC在加密Handshake包中携带编码的传输参数。握手完成后,对端声明的传输参数即可用。每个终端都需验证对端提供的传输参数的值。 + +第18.2节中有每个传输参数的定义。 + +终端必须(**MUST**)将收到的具有无效值的传输参数视为TRANSPORT_PARAMETER_ERROR类型的连接错误。 + +在给定的传输参数扩展中,终端不得(**MUST NOT**)包含一个传输参数的多个副本,终端应该(**SHOULD**)将此种情况视为TRANSPORT_PARAMETER_ERROR类型的连接错误。 + +终端在握手期间使用传输参数来认证CID的协商过程,参见第7.3节。 + +ALPN(参见[ALPN])允许客户端在连接建立期间通告其支持的多种应用层协议。在握手期间通告的传输参数适用于所有这些应用层协议。应用层协议也可以推荐传输参数的值,例如初始流控限额。客户端可以为传输参数设置约束,但如果这些约束有冲突,则可能使得客户端无法支持多个应用层协议。 + +### 7.4.1. 0-RTT的传输参数(Values of Transport Parameters for 0-RTT) + +是否使用0-RTT取决于客户端和服务端是否可以使用先前连接协商的协议参数。为了启用0-RTT,终端需要将服务端传输参数的值与它在连接上收到的其他会话凭证一起存储。终端还要存储应用层协议或加密握手所需的所有信息;参见[QUIC-TLS]第4.6节。在使用会话凭证尝试0-RTT连接时将会用到先前存储的传输参数的值。 + +握手完成后,客户端使用握手中商定的传输参数。记住直到握手完成并且客户端开始发送1-RTT包,传输参数才可适用于新连接。并非所有传输参数都要被保存,因为有些参数不适用于未来的连接,或者它们对0-RTT的使用没有影响。 + +定义新的传输参数(第7.4.2小节),必须(**MUST**)指定:对0-RTT来说,其存储方式是强制的、可选的还是禁止的。客户端不需要保存它无法处理的传输参数。 + +客户端不得(**MUST NOT**)保存如下传输参数: +* ack_delay_exponent +* max_ack_delay +* initial_source_connection_id +* original_destination_connection_id +* preferred_address +* retry_source_connection_id +* stateless_reset_token + +客户端必须(**MUST**)使用在握手中拿到的服务端的最新值,如果服务端未提供,则使用默认值。 + +尝试发送0-RTT数据的客户端必须(**MUST**)保存服务端处理需要用到的所有其他传输参数。服务端可以保存这些传输参数,或者可以在会话凭证中存储受完整性保护的副本,并在收到0-RTT数据时从中恢复信息。服务端使用这些传输参数来确定是否可以接受0-RTT数据。 + +如果服务端接受0-RTT数据,则服务端不得(**MUST NOT**)自行降低任何限制或更改可能违反客户端0-RTT约束的任何值。特别是,接受0-RTT数据的服务端,如下参数不得(**MUST NOT**)小于其存储过的传输参数的值: +* active_connection_id_limit +* initial_max_data +* initial_max_stream_data_bidi_local +* initial_max_stream_data_bidi_remote +* initial_max_stream_data_uni +* initial_max_streams_bidi +* initial_max_streams_uni + +为某些传输参数省略或设置零值可能会导致0-RTT数据能启用但不可用。对于0-RTT,允许发送应用数据的传输参数的适用子集应该(**SHOULD**)设置为非零值。这些参数集包括initial_max_data和(1)initial_max_streams_bidi或initial_max_stream_data_bidi_remote或(2)initial_max_streams_uni或initial_max_stream_data_uni。 + +服务端可能在流上采用更大的初始流流控限额值,比客户端发送0-RTT时采用的值要大。握手完成后,客户端使用initial_max_stream_data_bidi_remote或initial_max_stream_data_uni的新值来更新所有发送流上的流控限额。 + +服务端可以(**MAY**)存储和恢复先前通告的max_idle_timeout、max_udp_payload_size和disable_active_migration参数的值,如果它选择较小的值,则表示其拒绝0-RTT。因为在接受0-RTT数据的同时降低这些参数的值可能会降低连接的性能。具体来说,降低max_udp_payload_size可能会导致丢包,与直接拒绝0-RTT数据相比,性能会更差。 + +如果不能恢复传输参数,服务端必须(**MUST**)拒绝0-RTT数据。 + +当以0-RTT包发送帧时,客户端必须(**MUST NOT**)只使用保存的传输参数,重要的是,它不得(**MUST NOT**)使用从服务端更新的传输参数或从1-RTT包中接收的帧中学习到的新值。来自握手的传输参数更新值仅适用于1-RTT包。例如,所有0-RTT包都需要采用来自先前保存的流控限额参数,即使这些值因握手或1-RTT包中的帧加大也不考虑。在0-RTT中使用更新的传输参数,服务端可以(**MAY**)将其视为PROTOCOL_VIOLATION类型的连接错误。 + +### 7.4.2. 新传输参数(New Transport Parameters) + +新的传输参数可用于协商新的协议行为。终端必须(**MUST**)忽略它不支持的传输参数。缺少某个传输参数会因此禁用使用该参数协商的任何可选协议功能。如第18.1节所述,保留了一些标识符以满足此要求。 + +不理解某个传输参数的客户端可以丢弃它并在后续连接上尝试0-RTT。但是,如果客户端之后添加对该传输参数的支持,则它可能会在尝试0-RTT时违反此传输参数建立的约束。新的传输参数可以通过设置最保守的默认值来规避此问题。客户端可以通过存储所有参数来规避这个问题,包括当前不支持的参数。 + +可以根据第22.3节中的规则注册新的传输参数。 + +## 7.5. 加密消息缓存考虑(Cryptographic Message Buffering) + +实现需要为乱序接收到的CRYPTO帧维护一个数据缓冲区。由于CRYPTO帧没有流量控制,因此终端可能会潜在要求其对端缓冲无限量的数据。 + +在握手期间,终端实现必须(**MUST**)支持至少缓存4096字节的乱序CRYPTO帧数据,当然其也可以(**MAY**)缓冲更多数据,因为更大的缓冲区允许握手期间交换更大的密钥或凭证。在整个连接的生命周期内,终端的缓冲区大小不需要保持不变。 + +在握手期间无法缓存CRYPTO帧可能会导致连接失败。如果在握手期间超出终端的缓冲区大小,它可以临时扩展其缓冲区以完成握手。如果终端不扩展其缓冲区,则必须(**MUST**)使用CRYPTO_BUFFER_EXCEEDED错误码关闭连接。 + +握手完成后,如果终端无法缓冲CRYPTO帧中的数据,它可以(**MAY**)丢弃该CRYPTO帧和后续的CRYPTO帧,或者它也可用CRYPTO_BUFFER_EXCEEDED错误码关闭连接。包含被丢弃的CRYPTO帧的数据包必须(**MUST**)被确认,因为即使CRYPTO帧被丢弃,其他数据包也可以(**MAY**)被接收和处理。 + +# 8. 地址验证(Address Validation) + +地址验证功能确保终端不能用于流量放大攻击。在这种攻击中,攻击者发送数据包到服务端,其中包含指向受害者的欺骗性源地址信息。如果服务端生成更多或更大的数据包来响应,则会放出比攻击者本身更大的流量来攻击受害者。 + +针对放大攻击的主要防御措施,是验证对端是否可在其声明的传输地址上接收数据包。因此,在从尚未验证的地址收到数据包后,终端必须(**MUST**)将发往此地址的流量限制为不超过其接收的三倍。这种对响应大小的限制称为反放大限制。 + +地址验证在连接建立期间(参见第8.1节)和连接迁移期间(参见第8.2节)执行。 + +## 8.1. 连接建立期间的地址验证( Address Validation during Connection Establishment) + +连接建立隐式地为两端提供地址验证。特别的,收到用Initial包中通告的握手密钥保护的数据包,可以确认对端成功处理了该Initial包。一旦终端成功处理了来自对端的Handshake包,它就可以认为对端地址已经过验证。 + +此外,如果对端使用本端指定的CID并且CID包含至少64位,则本端可以(**MAY**)考虑对端地址已经验证。 + +对于客户端,其第一个Initial包中的DCID字段的值,使得它可以将验证服务端地址,作为成功处理任何数据包的一部分。因为来自服务端的Initial包使用从该值派生的密钥进行保护(参见[QUIC-TLS]第5.2节)。 + +initial_secret = HKDF-Extract(initial_salt, client_dst_connection_id) +或者,该值可由服务端在Version Negotiation包中回显(第6章),或包含在Retry包中的完整性标签中(参见[QUIC-TLS]第5.8节)。 + +在验证客户端地址之前,服务端发送的字节数不得(**MUST NOT**)超过它们接收到的字节数的三倍。这限制了可以使用欺骗源地址进行的任何放大攻击的规模。为了在地址验证之前避免放大,服务端必须(**MUST**)计算数据报中收到的所有负载字节,这些字节是唯一归属于单个连接的。其总量包含成功处理的数据包的数据报文和全部丢弃的数据包的数据报文。 + +客户端必须(**MUST**)确保包含Initial包的UDP报文具有至少1200字节的UDP负载,并根据需要添加PADDING帧。这样做使得服务端在完成地址验证之前可发送更多数据。 + +如果服务端回应的Initial或Handshake包丢失,但客户端已收到其发送的所有数据的确认,如果此时服务端达到其反放大限制时,则可能会发生死锁。在这种情况下,客户端没有理由发送额外的数据包,服务端无法发送更多数据,因为它没有验证客户端的地址。为了防止这种死锁,客户端必须(**MUST**)在探测超时(PTO)时发送一个数据包,参见[QUIC-RECOVERY]第6.2节。具体来说,如果客户端没有握手密钥,则客户端必须(**MUST**)在包含至少1200字节的UDP报文中发送Initial包,或者发送Handshake包。 + +服务端可能希望在开始加密握手之前验证客户端地址,客户端会在Initial包中携带令牌来提供地址验证。此令牌是由服务端在连接建立期间发送的Retry包(参见第8.1.2小节)携带,或者客户端可以使用之前连接中服务端用NEW_TOKEN(参见第8.1.3小节)帧通告的令牌。 + +服务端除了在地址验证之前的发送限制之外,还有拥塞控制器限制它可以发送的流量。客户端仅受拥塞控制器的约束。 + +### 8.1.1. 令牌生成(Token Construction) + +服务端在NEW_TOKEN帧或Retry包中发送的令牌是在相同的字段中填写的,但需要进行不同的处理,令牌生成时必须(**MUST**)考虑能区分是以那种方式提供给客户端的。 + +### 8.1.2. 使用Retry包进行地址验证(Address Validation Using Retry Packets) + +收到客户端的Initial包后,服务端可以通过发送包含令牌的Retry包(第17.2.5小节)来请求地址验证。客户端收到Retry包后,在该连接上随后发送的所有Initial包中都必须(**MUST**)携带此令牌。 + +收到客户端的Initial包后,如果其中携带了包含在Retry包中提供的令牌,服务端不能发送另一个Retry包,它只能拒绝连接或允许连接继续。 + +攻击者不可能为自己的地址生成有效的令牌(参见第8.1.4小节)而客户端能够返回该令牌,就可向服务端证明其已经收到该令牌。 + +服务端也可以使用Retry包来推迟连接建立的状态维护和处理成本。QUIC要求服务端提供不同的CID,以及第18.2节中定义的original_destination_connection_id传输参数,可以强制服务端证明它或它合作的实体已经从客户端接收到初始Initial包。提供不同的CID还使得服务端可以对后续数据包的路由方式进行一些控制,例如可用于将连接路由到不同的服务端实例。 + +如果服务端收到携带无效Retry令牌,但其他字段都有效的Initial包,因为知道客户端不会再接受另一个Retry令牌,服务端可以丢弃这样的数据包并让客户端超时以检测到握手失败,但这可能会对客户端造成明显的延迟损失。因此,服务端应该(**SHOULD**)立即以错误码INVALID_TOKEN关闭连接(参见第10.2节)。请注意,此时服务端尚未为连接建立任何状态,因此不会进入关闭周期。 + +图9展示了使用Retry包的流程。 + +``` + Client Server + + Initial[0]: CRYPTO[CH] -> + + <- Retry+Token + + Initial+Token[1]: CRYPTO[CH] -> + + Initial[0]: CRYPTO[SH] ACK[1] + Handshake[0]: CRYPTO[EE, CERT, CV, FIN] + <- 1-RTT[0]: STREAM[1, "..."] + +``` +图9: Example Handshake with Retry + +### 8.1.3. 后续连接的地址验证(Address Validation for Future Connections) + +服务端可以(**MAY**)在连接期间为客户端提供地址验证令牌,方便其将此令牌用于后续连接。地址验证对于0-RTT尤为重要,因为服务端可能会向客户端发送大量数据以响应0-RTT数据。 + +服务端使用NEW_TOKEN帧(第19.7节)为客户端提供可用于验证后续连接的地址验证令牌。在后续的连接中,客户端在Initial包中携带此令牌以提供地址验证。客户端必须(**MUST**)在它发送的所有Initial包中携带令牌,除非服务端通过Retry包用更新的令牌替换旧令牌。客户端不得(**MUST NOT**)将在Retry包中提供的令牌用于未来的连接。服务端可以(**MAY**)丢弃任何不携带预期令牌的Initial包。 + +Retry包提供的令牌可立即使用,NEW_TOKEN帧中携带的令牌要在一段时间之后才使用。因此,令牌应该(**SHOULD**)有一个截止时间,它可以是一个明确的截止时间,也可以是一个时间戳但可用于动态计算截止时间。服务端可以将截止时间直接或以加密形式保存在令牌中。 + +使用NEW_TOKEN发布的令牌不得(**MUST NOT**)包含能够让观察者将值关联到发出它的连接上的信息。例如,它不能包含先前的CID或寻址信息,除非这些值已加密。服务端必须(**MUST**)确保它发送的每个NEW_TOKEN帧在所有客户端中都是唯一的,不过因为丢包而重传的NEW_TOKEN帧除外。使得服务端能够区分令牌是来自Retry还是NEW_TOKEN的信息可以(**MAY**)被服务端以外的实体访问。 + +两个不同连接上的客户端端口号不太可能相同,因此验证端口不大可行。 + +在NEW_TOKEN帧中发布的令牌,可以让服务端判定该连接是否可信(例如,证书中包含服务端名称)。当客户端连接到一个其保存了可用而未用的令牌的服务端时,它应该(**SHOULD**)在其Initial包的Token字段中填入该令牌。携带令牌使得服务端可以及时验证客户端地址,而无需额外的环回时间。客户端不得(**MUST NOT**)携带不适用于它所连接的服务端的令牌,除非客户端明确知道发出令牌的服务端和客户端正连接的服务端正在共同管理此令牌。客户端可以(**MAY**)使用以前连接到该服务端的任何令牌。 + +令牌使得服务端可以将发出令牌的连接与任何使用它的连接之间的活动关联起来。客户端想要打破与服务端的连接一致性的话,可以丢弃NEW_TOKEN帧提供的令牌。相比之下,在Retry包中获得的令牌必须(**MUST**)在连接尝试期间立即使用,不能在后续连接尝试中使用。 + +客户端不应该(**SHOULD NOT**)在不同的连接尝试中重用来自NEW_TOKEN帧的令牌。在不同连接中重用令牌的话,可能被网络路径上的其他实体关联,参见第9.5节。 + +客户端可能会在单个连接上收到多个令牌。除了避免被关联之外,这些令牌都可以用于连接尝试。服务端可以发送额外的令牌以在多次连接尝试中启动地址验证,或者替换可能变得无效的旧令牌。对于客户端,这种模糊性意味着发送最新未使用的令牌是最有可能是有效的,虽然保存和使用旧令牌没有负面影响,但客户端可以认为旧令牌对服务端进行地址验证的用处不大。 + +当服务端收到带有地址验证令牌的Initial包时,它必须(**MUST**)尝试验证令牌,除非它已经完成了地址验证。如果令牌无效,那么服务端应该(**SHOULD**)像地址未经过验证一样处理,比如可能发送一个Retry包。NEW_TOKEN帧和Retry包提供的令牌可以被服务端区分(参见第8.1.1小节),后者需要严格地验证。如果验证成功,服务端应该(**SHOULD**)允许握手继续进行。 + +> 注意:将客户端视为未验证而不是丢弃数据包的基本原理是:客户端可能已经在使用先前连接中NEW_TOKEN帧收到的令牌,并且如果服务端丢失状态,它可能无法验证令牌,如果丢弃该数据包可能会导致连接失败。 + +在无状态设计中,服务端可以使用加密和经过身份验证的令牌将信息传递给客户端,服务端在未来连接中可以从中恢复有效信息并用于验证客户端地址。令牌未集成到加密握手中,就不会对其进行身份验证。客户端可能重用令牌,为了避免针对此属性的攻击,服务端可以将令牌限制为仅用于客户端地址验证。 + +客户端可以(**MAY**)使用在连接上获得的令牌进行后续任何使用相同版本的连接尝试。在选择要使用的令牌时,客户端不需要考虑正在尝试的连接的其他属性,包括可能的应用层协议、会话凭证或其他连接属性的选择。 + +### 8.1.4. 地址验证的令牌完整性考虑(Address Validation Token Integrity) +地址验证令牌必须(**MUST**)难以猜测。在令牌中包含一个至少128位的随机值就足够了,但这取决于服务端是否记住它发送给客户端的值。 + +基于令牌的方案允许服务端将与验证相关的任何状态维护转嫁给客户端。为了使此设计起作用,令牌必须(**MUST**)受到完整性保护以防止客户端修改或伪造。如果没有完整性保护,恶意客户端可能会生成或猜测服务端可接受的令牌值。只有服务端需要访问令牌的完整性保护密钥。 + +令牌不需要单一的明确定义的格式,因为生成和使用令牌的都是服务端。在Retry包中发送的令牌应该(**SHOULD**)包含允许服务端验证客户端数据包中的源IP地址和端口是否保持不变的信息。 + +在NEW_TOKEN帧携带的令牌必须(**MUST**)包含让服务端验证客户端IP地址从令牌发出时起是否更改的信息。服务端也可以通过NEW_TOKEN帧中的令牌来决定是否发送Retry包,即使客户端地址已更改。如果客户端IP地址发生变化,服务端必须(**MUST**)遵守反放大限制,详情参阅第8章。请注意,在存在NAT的情况下,此要求可能不足以保护共享NAT的其他主机免受放大攻击。 + +攻击者可以重放令牌以将服务端用作DDoS攻击中的放大器。为了防止此类攻击,服务端必须(**MUST**)确保阻止或限制令牌的重放。服务端应该(**SHOULD**)确保在Retry包中发送的令牌仅在短时间内被接受,因为它们会被客户端立即返回。NEW_TOKEN帧(第19.7节)中提供的令牌需要更长的生效时间,但不应该(**SHOULD NOT**)重复接受。如果可能,鼓励服务端只认可一次令牌。令牌也可以(**MAY**)包含有关客户端的附加信息,以进一步缩小适用性或重用性。 + +## 8.2. 路径验证(Path Validation) + +在连接迁移期间,两端都使用路径验证(参见第9章)来验证地址变更后的可达性。在路径验证中,终端测试特定本地地址与特定对端地址之间的可达性,其中地址是IP地址和端口的二元组。 + +路径验证测试在路径上发往对端的数据包是否可被对端接收。路径验证用于确保从迁移中的对端收到的数据包不携带欺骗性源地址。 + +路径验证不验证对端是否可以在返回方向发包。不能使用确认包用于返回路径验证,因为它们包含的信息不足并且可能被欺骗。终端独立确定路径每个方向的可达性,因此返回方向可达性只能由对端确认。 + +任一终端均可随时启用路径验证。例如,一个终端可能会检查对端在静默一段时间后是否仍然保有其源地址。 + +路径验证并非设计为NAT穿越机制。尽管此处描述的机制对于创建支持NAT穿越的NAT绑定可能有效,但我们期待的是一个终端能够接收数据包,而无需先在该路径上发送数据包。有效的NAT穿越需要额外的同步机制,这里没有涉及。 + +在进行路径验证时,终端可以(**MAY**)在发送PATH_CHALLENGE或PATH_RESPONSE帧时捎带其他类型的帧。例如终端可以在发送PATH_CHALLENGE时捎带PADDING帧用于路径最大传输单元发现 (PMTUD),参见见第14.2.1小节,终端在发送PATH_RESPONSE响应时也可以捎带它自己的PATH_CHALLENGE帧。 + +终端在从新的本地地址发送探测包时需要使用新的CID,参见第9.5节。在探测新路径时,终端需要确保其对端具有可用于响应的未使用的CID。如果active_connection_id_limit允许,在同一个数据包中发送NEW_CONNECTION_ID和PATH_CHALLENGE帧,可确保对端在发送响应时有未使用的CID。 + +终端可以选择同时探测多条路径。用于同时探测的路径的数量受其对端先前提供的额外CID数量的限制,因为用于探测的每个新本地地址都需要一个以前未使用的CID。 + +### 8.2.1. 启动路径验证(Initiating Path Validation) + +为了启动路径验证,终端需发送一个PATH_CHALLENGE帧,其中包含不可预测的要在路径上验证的负载。 + +终端可以(**MAY**)发送多个PATH_CHALLENGE帧以防止数据包丢失。不过,终端不应该(**SHOULD NOT**)在单个数据包中携带多个PATH_CHALLENGE帧。 + +终端不应该(**SHOULD NOT**)以超过Initial包的频率发送包含PATH_CHALLENGE帧的数据包来探测新路径。这确保了在新路径上的连接迁移不会比建立新连接更多。 + +终端必须(**MUST**)在每个PATH_CHALLENGE帧中使用不同的不可预测的数据,以便它可以将对端的PATH_RESPONSE响应与相应的PATH_CHALLENGE帧相关联。 + +终端必须(**MUST**)将包含PATH_CHALLENGE帧的UDP报文扩展到至少1200字节大小,除非路径的反放大限制不允许发送此大小的数据报。发送这种大小的UDP报文可以确保从本端到对端的网络路径可以用于QUIC连接,参见第14章。 + +当终端由于反放大限制而无法将数据报大小扩展到1200字节时,将不能验证路径MTU。为确保路径MTU足够大,在成功接收到PATH_RESPONSE之后,或在路径上接收到足够多的字节以发送更大的数据报而不会导致超出反放大限制后,终端必须(**MUST**)通过发送包含PATH_CHALLENGE帧的至少1200字节的UDP报文来执行第二次路径验证。 + +与扩展数据报文的其他情况不同,终端不得(**MUST NOT**)丢弃包含PATH_CHALLENGE或PATH_RESPONSE帧的较小的UDP报文。 + +### 8.2.2. 路径验证响应(Path Validation Responses) + +在接收到PATH_CHALLENGE帧时,终端必须(**MUST**)通过在PATH_RESPONSE帧中回显包含在PATH_CHALLENGE帧中的数据来响应。除非受到拥塞控制的约束,否则终端不得延迟包含PATH_RESPONSE帧的数据包的发送。 + +PATH_RESPONSE帧必须(**MUST**)在收到PATH_CHALLENGE帧的网络路径上发送。这确保只有当路径在两个方向都有效时,对端的路径验证才会成功。发起路径验证的终端不得(**MUST NOT**)强制执行此要求,因为这会导致对迁移的攻击,参见第9.3.3小节。 + +终端必须(**MUST**)将包含PATH_RESPONSE帧的数据报文扩展到至少1200字节的最小允许最大数据报大小。这可验证该路径是否能够在两个方向上都携带这种大小的数据报文。但是,如果最终待发数据报文大小超过反放大限制,终端不得(**MUST NOT**)扩展包含PATH_RESPONSE的数据报文。不过这种情况只有在其收到的PATH_CHALLENGE帧也未扩展时才有可能。(反放大机制,如果包含PATH_CHALLENGE的数据报文有1200,那么响应报文可以到1200,不超过3600即可) + +终端不得(**MUST NOT**)发送多个PATH_RESPONSE帧以响应一个PATH_CHALLENGE帧,参见第13.3节。终端应根据需要发送更多PATH_CHALLENGE帧以唤起更多的PATH_RESPONSE帧。 + +### 8.2.3. 路径验证成功(Successful Path Validation) + +当收到与先前PATH_CHALLENGE帧中携带的数据相同的PATH_RESPONSE帧时,路径验证成功。在任何网络路径上收到的PATH_RESPONSE帧都能验证发送PATH_CHALLENGE的路径可达性。 + +如果终端在未扩展到至少1200字节的数据报文中发送PATH_CHALLENGE帧,并且PATH_RESPONSE响应可以验证其地址,则表示验证路径通过,但不表示验证了路径MTU。因此,终端现在可以发送已接收数据量三倍的数据。然后,终端必须(**MUST**)用扩展的数据报发起另一次路径验证,以验证该路径是否支持所需的MTU。 + +收到对包含PATH_CHALLENGE帧的数据包的ACK确认不是充分的验证,因为该确认可能被恶意第三方欺骗。 + +### 8.2.4. 路径验证失败(Failed Path Validation) + +仅当尝试路径验证的终端放弃其路径验证的尝试时,路径验证才算失败。 + +终端应该(**SHOULD**)基于定时器来决定是否放弃路径验证。设置此计时器时,实现要注意新路径的往返时间可能比初始路径更长。建议(**RECOMMENDED**)使用当前PTO或新路径的PTO(使用kInitialRtt,如[QUIC-RECOVERY]中定义)中较大值的三倍。 + +在路径验证失败之前,会等待多个PTO,因此单个PATH_CHALLENGE或PATH_RESPONSE帧的丢失才不会导致路径验证失败。 + +注意到终端可能会在新路径上接收包含其他帧的数据包,但路径验证需要收到携带满足条件的数据的PATH_RESPONSE帧才算成功。 + +当终端放弃路径验证时,它已经判定该路径不可用。这并不一定意味着连接失败——终端可以根据需要继续通过其他路径发送数据包。如果没有可用路径,终端可以等待新路径可用或关闭连接。没有到达对端的有效网络路径的终端可以(**MAY**)发出NO_VIABLE_PATH连接错误信号,注意这只有在网络路径存在但不支持所需的MTU时才有可能发生(第14章)。 + +也有可能因为失败之外的其他原因而放弃路径验证。一般在旧路径上的路径验证正在进行的同时启动了到新路径的连接迁移,就会发生这种情况。 + +# 9. 连接迁移(Connection Migration) + +CID的使用使得连接在终端地址(IP地址和端口)的变更中存续下来,如由终端迁移到新网络引起的变更。本节介绍终端迁移到新地址的过程。 + +QUIC的设计依赖于终端在握手期间保持地址稳定。在握手完成确认之前,终端不得(**MUST NOT**)发起连接迁移,如[QUIC-TLS]第4.1.2小节所定义。 + +如果终端设置了disable_active_migration传输参数,在握手期间,终端也不得(**MUST NOT**)使用不同的本地地址向对端发送数据包(包括探测数据包,参见第9.1节),除非终端是按preferred_address传输参数进行回应的。如果对端违反此要求,本端必须(**MUST**)要么丢弃该路径上的传入数据包,不生成Stateless Reset包,要么继续进行路径验证并允许对端迁移。生成Stateless Reset包或启动连接关闭都将使得网络中的第三方可以通过欺骗的方式关闭连接或以其他方式操纵观察到的流量。 + +并非所有对端地址的变更都是有意或主动的迁移。对端可能会遇到NAT重绑定:由于中间节点(通常是NAT)为连接分配新的传出端口或是新的传出IP地址而导致的地址变更。如果终端检测到对端地址的任何变更,终端必须(**MUST**)执行路径验证(第8.2节),除非它先前已验证该地址。 + +当终端从没有经过验证的路径发送数据包时,它可以(**MAY**)丢失连接状态。需要进行连接迁移的终端可以(**MAY**)在丢失连接状态之前等待新路径变得可用。 + +本文限制服务端将连接迁移到新的客户端地址,除非是如第9.6节中所述的场景。客户端负责启动所有迁移。在接收到来自客户端地址的非探测数据包之前,服务端不会主动向该地址发送非探测数据包(参见第9.1节)。如果客户端收到来自未知服务端地址的数据包,其必须(**MUST**)丢弃这些数据包。 + +## 9.1. 探测新路径(Probing a New Path) + +在将连接迁移到新的本地地址之前,终端可以(**MAY**)使用路径验证(第8.2节)从新的本地地址探测对端可达性。路径验证失败仅意味着新路径不可用于此连接。除非没有可用的有效替代路径,否则路径验证失败不会导致连接结束。 + +PATH_CHALLENGE、PATH_RESPONSE、NEW_CONNECTION_ID和PADDING帧是“探测帧”,所有其他帧都是“非探测帧”。仅包含探测帧的数据包是“探测数据包”,包含任何其他帧的数据包是“非探测数据包”。 + +## 9.2. 启动连接迁移(Initiating Connection Migration) + +终端可以通过从新地址发送包含非探测帧的数据包,将连接迁移到这个新的本地地址。 + +每个终端在连接建立期间会验证其对端的地址。因此,待迁移的终端是知道对端愿意在其当前地址上接收报文的,因此,终端可以迁移到新的本地地址,而无需先验证对端的地址。 + +为了在新路径上确认可达性,终端需要在新路径上启动路径验证(第8.2节)。终端可以(**MAY**)推迟路径验证,直到对端发送下一个非探测帧到其新地址。 + +迁移时,新路径可能不支持终端的当前发送速率。因此,终端需要重置其拥塞控制器和RTT估计,如第9.4节所述。 + +新路径可能不具有相同的ECN功能。因此,终端需要验证ECN功能,如第13.4节所述。 + +## 9.3. 响应连接迁移(Responding to Connection Migration) + +接收到对端从新地址发来的包含非探测帧的数据包表明对端已迁移到该地址。 + +如果接收方允许迁移,它必须(**MUST**)将后续数据包发往新的对端地址,并且必须(**MUST**)启动路径验证(第8.2节)以验证对端对该地址的所有权(如果验证尚未进行)。如果接收方没有来自对端的未使用CID,则在对端提供之前,它将无法在新路径上发送任何数据,参见第9.5节。 + +终端仅在回应最高包号的非探测数据包时变更其目的地址,这可确保终端在收到重新排序的数据包时不会将数据包投递到旧的对端地址。 + +终端可以(**MAY**)将数据发往未经验证的对端地址,但它必须(**MUST**)防止潜在的攻击,如第9.3.1和9.3.2小节所述。如果该地址最近可见,则终端可以(**MAY**)跳过对端地址的验证。特别是,如果终端在检测到某种形式的虚假迁移后迁回到先前经过验证的路径,则跳过地址验证并恢复丢包检测和拥塞状态可以降低攻击对性能的影响。 + +在变更其发送非探测数据包的地址后,终端可以放弃对其他地址的路径验证。 + +从新的对端地址接收数据包可能是对端NAT重绑定的结果。 + +在验证新的客户端地址后,服务端应该(**SHOULD**)向客户端发送新的地址验证令牌(第8章)。 + +### 9.3.1. 对端地址欺骗(Peer Address Spoofing) + +第三方可能会填写假的源地址指向受害方,从而导致终端向受害方发送过多的数据。如果终端发送的数据明显多于第三方,则可能导致连接迁移会放大攻击者可以向受害者生成的数据量。 + +如第9.3节所述,终端需要验证对端的新地址以确认对端对新地址的所有权。在对端的地址被认为有效之前,终端会限制它发往该地址的数据量,参见第8章。如果没有此限制,终端有可能被用于对毫无戒心的受害者进行拒绝服务攻击。 + +如果终端如上所述跳过对端地址的验证,则不需要限制其发送速率。 + +### 9.3.2. On-Path地址欺骗(On-Path Address Spoofing) + +on-path攻击者可以通过复制和转发具有虚假源地址的数据包使其在原数据包之前到达,带有虚假源地址的数据包将被视为来自迁移连接,从而导致受害端向虚假地址的连接迁移,而原数据包将被视为重复数据包并被丢弃。在连接迁移之后,地址验证将失败,因为虚假源地址的实体没有必要的加密密钥来读取或响应发送给它的PATH_CHALLENGE帧,即使它想发也不能。 + +为防止连接因此类虚假迁移而失败,当新对端地址的验证失败时,终端必须(**MUST**)迁回最后验证过的对端地址。此外,从合法对端地址接收到具有更大数据包序号的数据包将触发另一次连接迁移,这将导致对虚假迁移地址的验证被放弃,这使得攻击者在迁移中仅注入了一个数据包。 + +如果终端没有保存最后验证的对端地址的状态,它必须(**MUST**)放弃所有连接状态并以静默方式关闭连接。这导致连接上的新数据包按照一般方式处理,例如,终端可以(**MAY**)发送Stateless Reset包以响应任何后续传入的数据包。 + +### 9.3.3. Off-Path包转发(Off-Path Packet Forwarding) + +能够观察到数据包的非路径(off-path)攻击者可能会将真实数据包的副本转发到终端。如果复制的数据包在真正的数据包之前到达,这将被识别为NAT重绑定。后到的真实的数据包都将被作为副本丢弃。如果攻击者能够继续转发数据包,则可能会导致终端迁移到经过攻击者的某条路径。这将攻击者置于路径上,使其能够观察或丢弃所有后续数据包。 + +这种类型的攻击依赖于攻击者使用与两终端之间的直接路径具有大致相同特征的路径。如果发送的数据包相对较少,或者数据包丢失与攻击尝试同时发生,则更可能被攻击。 + +在初始路径上接收到具有更大的数据包号的非探测包(non-probing packets)将导致终端移回合法的路径。在此路径上发送触发包会增加攻击失败的可能性。因此,减轻这种攻击依赖于触发包的交换。 + +为了回迁到原来的路径,终端必须(**MUST**)使用PATH_CHALLENGE帧验证之前的活动路径。这会导致在该路径上发送新的探测包。如果路径不再可行(viable),验证尝试将超时并失败;如果路径可行(viable)但不再需要,则验证将成功,但只会在路径上发送探测包(probing packets)。 + +在活动路径上接收到PATH_CHALLENGE的终端应该(**SHOULD**)发送非探测数据包作为响应。如果非探测数据包(non-probing packet)在攻击者生成的任何副本之前到达,则会导致连接迁移回初始路径。任何后续迁移到其他路径都会重新启动整个过程。 + +这种防御是不完善的,但这并不是一个严重的问题。尽管多次尝试使用初始路径,但通过攻击者的路径确实比初始路径快,则无法区分攻击和路由改进。 + +终端还可以使用启发式探索法(heuristics)来提高对这种类型攻击的检测。例如,如果最近在旧路径上接收到数据包,则不大可能是NAT重绑定,同样,在IPv6路径上很少进行重绑定。终端也可以查找重复的数据包。相反,CID的更改更可能表示有意的(intentional)迁移,而不是攻击。 + +## 9.4. 丢包检测和拥塞控制(Loss Detection and Congestion Control) + +新路径上的可用容量可能与旧路径不同。在旧路径上发送的数据包不得(**MUST NOT**)参与新路径的拥塞控制或RTT估计。 + +在确认对端对其新地址的所有权后,终端必须(**MUST**)立即将新路径的拥塞控制器和往返时间估计器重置为初始值(参见[QUIC-RECOVERY]的附录A.3或B.3),除非对端地址唯一变化的是其端口号。由于仅端口变更通常是NAT重绑定或其他中间设备活动的结果,因此在这些情况下,终端可以(**MAY**)保留其拥塞控制状态和RTT估计,而不是恢复到初始值。如果将旧路径保留的拥塞控制状态用于具有显著不同特性的新路径,发送方可能会过于激进地传输,直到拥塞控制器和RTT估计器适应为止。通常,建议实现在新路径上使用历史数据时要谨慎。 + +当终端在迁移期间从/向多个地址发送数据和探测包时,接收端处可能会出现明显的乱序,因为不同路径可能具有不同的RTT。接收方仍将发送覆盖所有接收到的数据包的ACK帧。 + +尽管在连接迁移期间可能使用多条路径,但只需要维护一个单独的拥塞控制上下文和丢包恢复上下文(如[QUIC-RECOVERY]中所述)可能就足够了。例如,终端可能会延迟切换到新的拥塞控制上下文,直到确认不再需要旧路径(例如第9.3.3小节中描述的情况)。 + +发送方可以对探测数据包进行单独处理,以保证它们的丢包检测是独立的,不会导致拥塞控制器过度降低其发送速率。当发送PATH_CHALLENGE时,终端可能会设置一个单独的定时器,如果收到相应的PATH_RESPONSE,则停止该定时器,如果定时器在收到PATH_RESPONSE之前到期,终端可能会发送一个新的PATH_CHALLENGE帧并启动一个更长的定时器。这个定时器应该(**SHOULD**)按照[QUIC-RECOVERY]第6.2.1小节的描述设置,并且不得(**MUST NOT**)更激进。 + +## 9.5. 连接迁移对隐私的影响(Privacy Implications of Connection Migration) + +在多条网络路径上使用稳定的CID将使得被动观察者可以关联这些路径之间的活动。需要迁移网络的终端可能不希望它们的活动被除对端以外的任何实体关联,因此从不同的本地地址发送时会使用不同的CID,如第5.1节所述。为了有效实现这一点,终端需要确保它们提供的CID不能被任何其他实体关联起来。 + +在任何时候,终端都可以(**MAY**)将它们填写的DCID变更为尚未在另一条路径上使用的值。 + +当从多个本地地址发送数据时,终端不得(**MUST NOT**)重用SCID——例如在第9.2节中描述的启动连接迁移或在第9.1节中描述的探测新的网络路径时。 + +类似地,当发往多个目的地址时,终端也不得(**MUST NOT**)重用DCID。由于网络变更不受其对端控制,终端可能会收到具有新的源地址但与旧地址有相同DCID字段的数据包,在这种情况下,终端如果从同一个本地地址发往新地址的话,可以(**MAY**)继续使用当前CID。 + +这些关于CID重用的要求仅适用于数据包的发送,因为在不改变CID的情况下无意识变更路径是可能的,例如,经过一段时间的网络静默之后,NAT重绑定可能会导致在客户端恢复发包时在新路径上发送数据包。终端如何响应此类事件参见第9.3节。 + +在每条新网络路径上的双向数据包中启用不同的CID,可以消除将同一连接的不同路径关联起来的可能性。包头保护确保不能用数据包序号关联活动,但不能阻止使用数据包的其他属性(例如时间和大小)来关联活动。 + +终端不应该(**SHOULD NOT**)向要求零长度CID的对端发起迁移,因为新路径上的流量可能很容易关联到旧路径上的流量。如果服务端能够将具有零长度CID的数据包关联到正确的连接,则意味着服务端正在使用其他信息来解复用并关联数据包。例如,服务端可能会为每个客户端提供一个唯一的地址——例如,使用HTTP替代服务[ALTSVC]。使得跨多个网络路径也能正确路由数据包的信息,但同时也可能使得这些路径上的活动被除对端以外的其他实体关联。 + +在一段时间静默之后再发送流量时,客户端可能希望通过切换到新的CID、源UDP端口或IP地址(参见[RFC8981])来降低可关联性。变更数据包的源地址也可能会导致服务端检测到连接迁移。这确保即使对于没有经历NAT重绑定或真正迁移的客户端,也可以使用支持迁移的机制。变更地址会导致对端重置其拥塞控制状态(请参阅第9.4节),因此地址应该(**SHOULD**)不要频繁变更。 + +耗尽可用CID的终端无法再继续探测新路径或启动迁移,也无法响应其对端的探测或迁移尝试。为了确保迁移可行,并且在不同路径上发送的数据包不能被相互关联,终端应该(**SHOULD**)在对端体迁移之前提供新的CID,参见第5.1.1小节。如果判断对端可能已经用尽了可用的CID,则待迁移终端可能需要在新网络路径上发送的所有数据包中都包含NEW_CONNECTION_ID帧。 + +## 9.6. 服务端的首选地址(Server's Preferred Address) + +QUIC允许多个服务端接受同一个IP地址上的入向连接,可以在握手后不久尝试将连接转到某个服务端提供的其他首选地址。这对客户端接入时连接到多个服务端共享的地址,之后再使用单播地址以确保连接稳定性时特别有用。本节介绍将连接迁移到首选服务端地址的协议内容。 + +本文中指定的QUIC版本不支持在连接中将连接迁移到新的服务端地址。如果客户端在未启动迁移到该地址时收到来自新服务端地址的数据包,则客户端应该(**SHOULD**)丢弃这些数据包。 + +### 9.6.1. 传输首选地址(Communicating a Preferred Address) + +服务端通过在TLS握手中包含preferred_address传输参数来告知首选地址。 + +服务端可以(**MAY**)告知两个地址族(IPv4和IPv6)的首选地址,以允许客户端选择最适合其网络连接的地址。 + +一旦握手确认完成,客户端应该(**SHOULD**)选择服务端提供的两个地址之一并启动路径验证(见第8.2节)。客户端使用任何以前未使用的活动CID构造数据包,这些ID取自preferred_address传输参数或NEW_CONNECTION_ID帧。 + +一旦路径验证成功,客户端应该(**SHOULD**)开始使用新的CID发送后续数据包到新的服务端地址,并停止使用旧的服务端地址。如果路径验证失败,客户端必须(**MUST**)将后续数据包发往服务端的初始IP地址。 + +### 9.6.2. 迁移到首选地址(Migration to a Preferred Address) + +迁移到首选地址的客户端必须(**MUST**)在迁移前验证它选择的地址,参见第21.5.3小节。 + +服务端在接受连接后的任何时候都可能收到寻址到其首选IP地址的数据包。如果此数据包包含PATH_CHALLENGE帧,则服务端会如第8.2节所述回复包含PATH_RESPONSE帧的数据包。服务端必须(**MUST**)从其初始地址发送非探测数据包,直到它在其首选地址从客户端接收到非探测数据包,直到服务端验证了该新路径。 + +服务端必须(**MUST**)探测从其首选地址到客户端的路径。这有助于防止攻击者发起的虚假迁移。 + +一旦服务端完成其路径验证并在其首选地址上接收到具有新的最大数据包序号的非探测数据包,服务端就开始仅从其首选IP地址向客户端发送非探测数据包。服务端应该(**SHOULD**)丢弃在旧IP地址上接收到的此连接的新数据包,但可以(**MAY**)继续处理在旧IP地址上接收到的延迟数据包。 + +服务端在preferred_address传输参数中提供的地址仅对提供它们的连接有效。客户端不得(**MUST NOT**)将这些用于其他连接,包括从当前连接恢复的连接。 + +### 9.6.3. 客户端迁移与首选地址的交互(Interaction of Client Migration and Preferred Address) + +客户端可能需要在迁移到服务端的首选地址之前执行连接迁移。在这种情况下,客户端应该(**SHOULD**)同时执行从客户端新地址到初始和首选服务端地址的路径验证。 + +如果服务端首选地址的路径验证成功,则客户端必须(**MUST**)放弃对服务端初始地址的验证并迁移到服务端的首选地址。如果服务端首选地址的路径验证失败但服务端初始地址的验证成功,则客户端可以(**MAY**)切换到客户端新地址并继续发包到服务端的初始地址。 + +如果在服务端首选地址收到的数据包与握手期间从客户端观察到的源地址不同,则服务端必须(**MUST**)防止潜在的攻击,如第9.3.1和第9.3.2小节所述。除了有意的同时迁移之外,这也可能是因为客户端的访问网络对服务端的首选地址使用了不同的NAT绑定。 + +服务端应该(**SHOULD**)在收到来自不同地址的探测包时启动到客户端新地址的路径验证,参见第8章。 + +迁移到新地址的客户端应该(**SHOULD**)为服务端使用来自相同地址族的首选地址。 + +preferred_address传输参数中提供的CID不特定于所提供的地址。提供此CID是为了确保客户端具有可用于迁移的CID,但客户端可以(**MAY**)在任何路径上使用此CID。 + +## 9.7. IPv6流标签的使用和迁移(Use of IPv6 Flow Label and Migration) + +使用IPv6发送数据的终端应该(**SHOULD**)应用符合[RFC6437]的IPv6流标签,除非本地API不允许设置IPv6流标签。 + +流标签生成方式,必须(**MUST**)尽量降低与先前使用的流标签关联的可能,因为稳定的流标签将能够关联多个路径上的活动,参见第9.5节。 + +[RFC6437] 建议使用伪随机数函数来生成流标签。一种可能的实现是用加密散列函数生成流标签,散列函数的参数有本地秘钥、源地址和目的地址、DCID字段,这可确保变更与其他可观察到的标识符的变更同步。 + +# 10. 连接关闭(Connection Termination) + +可以通过以下三种方式之一终止已建立的QUIC连接: + +* 空闲超时(第10.1节) +* 立即关闭(第10.2节) +* 无状态重置(第10.3节) + +如果终端没有可以(**MAY**)发送数据包的经过验证的路径,则它可以丢弃连接状态,参见第8.2节。 + +## 10.1. 空闲超时(Idle Timeout) +如果任一终端在其传输参数(第18.2节)中指定max_idle_timeout,则当连接保持空闲的时间超过两端通告的max_idle_timeout值的最小值时,连接将被静默关闭并丢弃其状态。 + +虽然每个终端都通告一个max_idle_timeout,但终端中真正生效的有效值是两个通告值(或唯一通告值,如果只有一个终端通告非零值)中的最小值。如果终端在有效值超时之前放弃连接,则它应启动立即关闭(第10.2节)。 + +当一个终端收到并成功处理来自其对端的数据包时,终端会重置其空闲定时器。如果自上次接收和处理数据包后没有发送其他确认数据包,终端也会在发送确认数据包时重置其空闲定时器。在发送数据包时重置定时器可确保在有新活动后不会关闭连接。 + +为避免空闲超时时间过短,终端必须(**MUST**)将空闲超时时间增加到至少是当前探测超时(PTO for Probe Timeout)的三倍。这在空闲超时之前可以允许多次探测或丢包。 + +### 10.1.1. 活性测试(Liveness Testing) +临近有效空闲超时时间发送的数据包有可能被对端丢弃,因为在这些数据包到达之前,对端的空闲超时期限可能已经到期。 + +如果对端可能很快超时,例如在PTO内,终端可以发送PING包或另一个ACK触发帧来测试连接的活跃性,参见[QUIC-RECOVERY]第6.2节。这在无法安全地重传可用的应用数据时尤其有用。请注意,哪些数据可以安全重传由应用程序确定。) + +### 10.1.2. 延迟空闲超时(Deferring Idle Timeout) +如果终端正在等待响应数据但没有或无法发送应用数据,则它可能需要发送ACK触发包以避免空闲超时。 + +QUIC的实现可为应用提供延迟空闲超时的选项。当应用希望避免丢失与打开的连接相关联的状态,但不希望在一段时间内交换应用数据时,可以使用此功能。使用此选项,终端可以周期性发送PING帧(第19.2节),这将导致对端重置空闲超时定时器。如果这是本端自收到数据包后发送的第一个ACK触发包,则也会重置此终端的空闲超时定时器。发送PING帧会导致对端回以确认ACK响应,这也会重置对端的空闲超时定时器。 + +使用QUIC的应用层协议应该(**SHOULD**)提供合适的有关何时推迟空闲超时的指导。不必要地发送PING帧可能会对性能产生不利影响。 + +如果超过max_idle_timeout传输参数协商的时间还没有发送或接收数据包,则连接将超时(请参阅第10章)。另外,中间设备的状态也可能会提前超时。尽管[RFC4787]中的REQ-5建议2分钟的超时间隔,但经验表明,每30秒发送一次数据包是必要的,可以防止大多数中间设备丢失UDP流[GATEWAY]的状态。 + +## 10.2. 立即关闭(Immediate Close) +终端发送CONNECTION_CLOSE帧(第19.19节)以立即终止连接。CONNECTION_CLOSE帧会导致所有流立即关闭。可以假定打开的流被隐式重置。 + +发送CONNECTION_CLOSE帧后,终端立即进入“关闭中”(closing) 状态,参见第10.2.1小节。终端收到CONNECTION_CLOSE帧后,进入draining 状态,见第10.2.2小节。 + +违反协议会导致立即关闭。 + +在应用层协议指示关闭连接后,可以使用立即关闭。这可能发生在应用层协议之间协商优雅关闭之后。两个应用终端先交换同意关闭连接所需的消息,然后应用请求QUIC关闭连接。当QUIC因此启动关闭流程时,会向对端发出关闭信号,携带附有应用提供的错误码的CONNECTION_CLOSE帧。 + +closing和draining连接状态的存在是为了确保连接干净利落地关闭,并正确丢弃延迟或乱序的数据包。这些状态应该(**SHOULD**)至少持续三倍于[QUIC-RECOVERY]中定义的当前PTO间隔。 + +在退出closing或draining状态之前清除连接状态,可能会导致终端在接收到迟到的数据包时不必要地生成Stateless Reset包。有一些替代方法来确保迟到的数据包不会触发终端响应措施,例如那些能够关闭UDP套接字的终端,可以(**MAY**)提前结束这些状态以允许更快的资源恢复。保留打开套接字以接受新连接的服务端不应该(**SHOULD NOT**)提前结束closing或draining状态。 + +一旦其closing或draining状态结束,终端应该(**SHOULD**)丢弃所有连接状态。终端可以(**MAY**)发送一个Stateless Reset包来响应这个连接的后续传入数据包。 + +### 10.2.1. 关闭中连接状态(Closing Connection State) +终端在发起立即关闭后进入closing状态。 + +在closing状态下,终端仅保留足够的信息来生成包含CONNECTION_CLOSE帧的数据包并将数据包标识为属于该连接。处于closing状态的终端发送一个包含CONNECTION_CLOSE帧的数据包,以响应该连接上的传入数据包。 + +终端应该(**SHOULD**)限制它在关闭状态下生成数据包的速率。例如,终端可以等待足够数量的数据包或足够的时间之后再响应接收到的数据包。 + +终端选择的CID和QUIC版本足以识别closing态连接的数据包,终端可以(**MAY**)丢弃所有其他连接状态。closing态的终端不需要处理任何接收到的帧。终端可以(**MAY**)为传入的数据包保留数据包保护密钥,在读取时可以用于解密和处理CONNECTION_CLOSE帧。 + +终端可以(**MAY**)在进入closing状态时丢弃数据包保护密钥,并发送包含CONNECTION_CLOSE帧的数据包以响应收到的任何UDP报文。但是,丢弃数据包保护密钥的终端无法识别和丢弃无效数据包。为避免被用于放大攻击,此类终端必须(**MUST**)限制其发送的数据包的总字节数为接收的数据包的三倍。为了最小化终端为关闭连接保持的状态,终端可以发送完全相同的数据包以响应任何接收到的数据包。 + +> 注意:允许重传关闭数据包是对每个数据包均需使用新包号的要求的一个例外,参见第12.3节。发送新的包号主要有利于丢包恢复和拥塞控制,这与关闭连接无关。重传最终数据包需要较少的状态。 + +当处于closing状态时,终端可以从新的源地址接收数据包,可能表示连接迁移,参见第9章。处于closing状态的终端必须(**MUST**)要么丢弃从未验证地址接收的数据包,要么将其发往未验证地址的数据包的总字节数限制为从该地址接收的数据包的三倍。 + +终端在关闭时不应处理密钥更新([QUIC-TLS]第6章)。密钥更新可能会阻止终端从closing状态迁移到draining状态,因为终端将无法处理随后收到的数据包,但也不会产生任何影响。 + +### 10.2.2. 耗尽连接状态(Draining Connection State) +一旦终端接收到CONNECTION_CLOSE帧,即表示其对端正在关闭或正在耗尽,就进入耗尽状态。虽然其他方面与closing状态相同,但处于draining状态的终端不得(**MUST NOT**)发送任何数据包。一旦连接处于draining状态,就不需要保留数据包保护密钥。 + +接收CONNECTION_CLOSE帧的终端可以(**MAY**)在进入draining状态之前发送包含CONNECTION_CLOSE帧的单个数据包,如果合适,使用NO_ERROR代码。终端不得(**MUST NOT**)发送更多数据包。这样做可能会导致CONNECTION_CLOSE帧的不断交换,直到终端之一退出closing状态。 + +如果终端收到CONNECTION_CLOSE帧,则它可以(**MAY**)从closing状态进入draining状态,这表明对端也在closing或draining。在这种情况下,当closing状态将结束时,draining状态结束。换句话说,终端使用相同的结束时间,但停止在此连接上传输任何数据包。 + +### 10.2.3. 握手期间立即关闭(Immediate Close during the Handshake) +发送CONNECTION_CLOSE帧时需要确保对端能够处理该帧。通常,这意味着需要在具有最高数据包保护级别的数据包中发送该帧以避免数据包被丢弃。在握手确认后(参见[QUIC-TLS]第4.1.2小节),终端必须(**MUST**)在1-RTT包中发送CONNECTION_CLOSE帧。但是,在确认握手之前,对端可能无法使用更高级的数据包保护密钥,因此可以(**MAY**)在使用较低数据包保护级别的数据包中发送另一个CONNECTION_CLOSE帧。特别地: + +* 客户端知道服务端是否有握手密钥(见第17.2.2.1小节),但服务端可能不知道客户端是否有握手密钥。在这些情况下,服务端应该(**SHOULD**)在Handshake和Initial包中发送一个CONNECTION_CLOSE帧,以确保其中至少一个可以被客户端处理; + +* 在0-RTT包中发送CONNECTION_CLOSE帧的客户端不能保证服务端可接受0-RTT。在Initial包中发送CONNECTION_CLOSE帧使服务端更有可能收到关闭信号,即使可能不理解应用层错误码。 + +* 在确认握手之前,对端可能无法处理1-RTT包,因此终端应该(**SHOULD**)在握手和1-RTT包中发送CONNECTION_CLOSE帧。服务端还应该(**SHOULD**)在Initial包中发送一个CONNECTION_CLOSE帧。 + +在Initial或Handshake包中发送0x1d类型的CONNECTION_CLOSE可能会暴露应用状态或用于更改应用状态。当在Initial或Handshake包中发送帧时,类型0x1d的CONNECTION_CLOSE必须(**MUST**)替换为类型为0x1c的CONNECTION_CLOSE帧。否则可能会泄露有关应用状态的信息。终端必须(**MUST**)清除Reason Phrase字段的值,并且在转换为0x1c类型的CONNECTION_CLOSE时应该(**SHOULD**)使用APPLICATION_ERROR错误码。 + +以多种数据包类型发送的CONNECTION_CLOSE帧可以合并为单个UDP报文,参见第12.2节。 + +终端可以在Initial包中发送CONNECTION_CLOSE帧,这可能是为了响应在Initial或Handshake包中收到的未经认证的信息。这种立即关闭可能会将合法连接暴露给拒绝服务攻击。QUIC没有在握手期间对路径攻击的防御措施,参见第21.2节。然而,以减少对合法对端的错误反馈为代价,如果终端丢弃非法数据包而不是使用CONNECTION_CLOSE终止连接,则某些形式的拒绝服务可能会使攻击变得更加困难。因此,如果在缺少认证的数据包中检测到错误,终端(**MAY**)丢弃数据包而不是立即关闭。 + +尚未建立状态的终端,例如在Initial包中检测到错误的服务端,不会进入关闭状态。没有连接状态的终端在发送CONNECTION_CLOSE帧时也不会进入closing或draining。 + +## 10.3. 无状态重置(Stateless Reset) +无状态重置是作为无法访问连接状态的终端的最后手段提供的。因为崩溃或中断,对端可能继续向无法正常处理连接的终端发送数据,此时终端可以(**MAY**)发送Stateless Reset包,以响应接收到它无法与活动连接关联的数据包。 + +Stateless Reset包不适用于指示活动连接中的错误。如果可以的话,希望传达致命连接错误的终端必须(**MUST**)使用CONNECTION_CLOSE帧。 + +为了支持此过程,终端需要发出无状态重置令牌,这是一个很难猜测的16字节值。如果对端随后收到Stateless Reset包,即以该无状态重置令牌结尾的UDP报文,则对端将立即终止连接。 + +无状态重置令牌特定于CID。终端通过在NEW_CONNECTION_ID帧的Stateless Reset Token字段来发布无状态重置令牌。服务端还可以在握手期间发出stateless_reset_token传输参数,该参数适用于它在握手期间选择的CID。这些交换受加密保护,因此只有客户端和服务端知道它们的真实值。请注意,客户端不能使用stateless_reset_token传输参数,因为它们的传输参数没有机密性保护。 + +当通过RETIRE_CONNECTION_ID帧(第19.16节)退出关联的CID时,令牌将失效。 + +接收到它无法处理的数据包的终端发送如下结构的数据包(参见第1.3节): + +``` + Stateless Reset { + Fixed Bits(2)= 1, + Unpredictable Bits (38..), + Stateless Reset Token (128), + } +``` +图10: Stateless Reset Packet + +这种设计使得Stateless Reset包——在可能的范围内——与具有短包头的常规数据包无法区分。 + +Stateless Reset包对应一整个UDP报文,从数据包头的前两位开始。第一个字节的其余部分和它后面的任意数量的字节被设置为应该(**SHOULD**)与随机值无法区分的值。数据报的最后16个字节包含一个无状态重置令牌。 + +对于预期接收方以外的实体,Stateless Reset包将被识别为具有短标头的数据包。为了使Stateless Reset包识别为有效的QUIC包,Unpredictable Bits字段需要包含至少38位数据(或5个字节,减去两个固定位)。 + +如果接收方需要使用CID,则生成的21字节的最小大小并不能保证Stateless Reset包和其他数据包无法区分。为了达到这个目的,终端应该(**SHOULD**)确保它发送的所有数据包,至少要比携带最小CID长度的数据包长22个字节,并根据需要添加PADDING帧。这确保了对端发送的任何Stateless Reset包与发送给终端的有效数据包是不可区分的。需要发送Stateless Reset包以响应43个字节或更短的数据包的话,终端应该(**SHOULD**)发送一个比该数据包短一个字节的Stateless Reset包。 + +这些值假设无状态重置令牌的长度与数据包保护协议AEAD约定的最小扩展长度相同。如果终端可以协商具有更大最小扩展的数据包保护方案,则需要额外的Unpredictable Bits。 + +终端不得(**MUST NOT**)发送比它接收的数据包大三倍或以上的Stateless Reset包,以避免被用于放大攻击。第10.3.3小节描述了对Stateless Reset包大小的附加限制。 + +终端必须(**MUST**)丢弃因太短不合规的QUIC包。举个例子,用[QUIC-TLS]中定义的一组AEAD函数加密后,小于21字节的短包头数据包永远是无效的。 + +终端必须(**MUST**)发送形式类似短包头的数据包的Stateless Reset包。另外,终端必须(**MUST**)将任何以有效无状态重置令牌结尾的数据包视为Stateless Reset包,其他QUIC版本可能允许使用长包头。 + +终端可以(**MAY**)发送Stateless Reset包以响应具有长包头的数据包。在无状态重置令牌可供对端使用之前,发送Stateless Reset包无效。在当前QUIC版本中,具有长包头的数据包仅在连接建立期间使用。由于无状态重置令牌在连接建立完成或接近完成之前不可用,因此忽略具有长包头的未知数据包可能与发送Stateless Reset包有一样的效果。 + +终端无法从具有短包头的数据包中确定SCID,它不也能在Stateless Reset包中设置DCID。因此,DCID将与先前数据包中使用的值不同。随机DCID使CID看起来像连接迁移的NEW_CONNECTION_ID帧提供的新CID,参见第19.15节。 + +使用随机CID会导致两个问题: + +* 数据包可能无法到达对端,如果DCID对于路由到对端至关重要的话,则此数据包可能会被错误地路由。这也可能会触发另一个Stateless Reset包作为响应,参见第10.3.3小节。发送不能正确路由的Stateless Reset包是一种无效的错误检测和恢复机制。在这种情况下,终端将需要依赖其他方法(例如定时器)来检测连接是否失败。 + +* 随机生成的CID可由对端实体以外的实体使用,以将其识别为潜在的Stateless Reset包。偶尔使用不同CID的终端可能会对此引入一些不确定性。 + +这种无状态重置设计特定于QUIC版本1。支持多个QUIC版本的终端需要生成一个Stateless Reset包,该Stateless Reset包应该可被对端接收,对端应该支持本端可能支持(或在丢失状态之前可能已支持)的任何版本,新版本QUIC的设计人员需要意识到这一点,并且要么(1)重用此设计,要么(2)使用除最后16个字节以外的数据包的一部分来承载数据。 + +### 10.3.1. 检测无状态重置包(Detecting a Stateless Reset) +终端使用UDP报文的最后的16字节检测潜在的Stateless Reset包。终端应记住所有与它最近发送的数据报文的CID和远端地址相关联的无状态重置令牌。这包括来自NEW_CONNECTION_ID帧的Stateless Reset Token字段值和服务端的传输参数,但不包括与未使用或已停用的CID关联的无状态重置令牌。终端通过将数据报文的最后16个字节与所有关联活动连接的无状态重置令牌进行比较,将接收到的数据报识别为Stateless Reset包。 + +可以对每个入向数据报文执行此比较。如果来自数据报文的任何数据包被成功处理,终端可以(**MAY**)跳过此检查。但是,当入向数据报文中的第一个数据包无法与连接关联或无法解密时,必须(**MUST**)执行此比较。 + +终端不得(**MUST NOT**)检查与未使用或停用的CID相关联的无状态重置令牌。 + +当将数据报文与无状态重置令牌值进行比较时,终端必须(**MUST**)不泄漏有关令牌值的信息。例如,在特定时间周期内执行此比较可以保护各个无状态重置令牌的值免于通过timing side channels泄漏信息。另一种方法是存储和比较无状态重置令牌的转换值而不是初始令牌值,例如可使用以密钥(例如,分块加密算法Hashed Message Authentication Code(HMAC)[RFC2104])为入参的加密安全伪随机数函数做转换。终端不应保护诸如数据包是否成功解密或有效的无状态重置令牌的数量之类的信息。 + +如果数据报文的最后16个字节与无状态重置令牌的值相同,则终端必须(**MUST**)进入draining期并且不再在此连接上发送任何其他数据包。 + +### 10.3.2. 计算无状态重置令牌(Calculating a Stateless Reset Token) +无状态重置令牌必须(**MUST**)难以猜测。为了创建无状态重置令牌,终端可以为其创建的每个连接随机生成[RANDOM]一个秘钥。但是,当在一个集群或存储池中有多个实例终端可能会丢失状态时,这会带来协调问题。无状态重置机制是专门用于处理状态丢失的情况,因此这种方法不是最理想的。 + +可以通过使用伪随机函数生成证明(proof),该函数采用静态密钥和终端选择的CID(参见第5.1节)作为输入,可以在到同一终端的所有连接中使用相同的静态密钥。这类函数可选HMAC[RFC2104](HMAC(static_key, connection_id))或基于HMAC的密钥派生函数 (HKDF)[RFC5869](例如使用静态密钥作为输入密钥材料,CID作为salt)。函数的输出被截断为16个字节,以作为该连接的无状态重置令牌。 + +失去状态的终端可以使用相同的方法生成有效的无状态重置令牌。CID来自终端接收的数据包。 + +这种设计依赖于对端始终在其数据包中发送CID,以便终端可以使用数据包中的CID来重置连接。使用这种设计的终端必须(**MUST**)要么对所有连接使用相同的CID长度,要么对CID的长度进行编码,以便它可以在没有状态的情况下恢复。此外,它不能提供零长度的CID。 + +注意无状态重置令牌允许任何实体终止连接,因此一个值只能使用一次。这种选择无状态重置令牌的方法意味着CID和静态密钥的组合不得(**MUST NOT**)用于另一个连接。如果共享静态密钥的实例使用相同的CID,或者如果攻击者可以将数据包路由到没有状态但具有相同静态密钥的实例,则可能发生拒绝服务攻击,参见第21.11节。如果CID曾被无状态重置令牌重置的连接使用过,则其不得(**MUST NOT**)重用于与此连接共享静态密钥的节点上。 + +相同的无状态重置令牌不得(**MUST NOT**)用于多个CID。终端不需要将新值与所有以前的值进行比较,但重复值可以(**MAY**)被视为PROTOCOL_VIOLATION类型的连接错误。 + +请注意,Stateless Reset包没有任何加密保护。 + +### 10.3.3. 循环(Looping) +无状态重置的设计使得在不知道无状态重置令牌的情况下,它与有效数据包无法区分。例如,如果服务端向另一台服务端发送Stateless Reset包,它可能会收到另一个Stateless Reset包作为响应,这可能导致循环交换。 + +终端必须(**MUST**)确保它发送的每个Stateless Reset包都小于触发它的数据包,除非它保持足够的状态以防止循环。在出现环路的情况下,这会导致数据包最终太小而无法触发响应。 + +终端也可以记住它已发送的Stateless Reset包的数量,一旦达到限制就停止生成新的Stateless Reset包。对不同的远端地址使用单独的限制将确保当其他对端或连接已用完限制时,可以使用Stateless Reset包来关闭连接。 + +小于41字节的Stateless Reset包可能会被第三方观察者识别为Stateless Reset包,具体取决于对端CID的长度。另外,不发送Stateless Reset包来响应小数据包,可能会导致仅存在小数据包交互的连接断链场景下,无状态重置机制失效,此类故障可能只能通过其他方式(例如定时器)检测到。 + +# 11. 错误处理(Error Handling) +检测到错误的终端应该(**SHOULD**)向它的对端发出指示错误的信号。传输级和应用级错误都会影响整个连接,参见第11.1节。只有应用级别的错误才能隔离到单个流中,见第11.2节。 + +发出错误信号的帧中应该(**SHOULD**)包含最符合错误原因的代码(参见第20章)。此规约有约定错误条件以及其对应的错误码,尽管在规约条文中这些是必备的条件,但不同的实现策略可能会导致报告不同的错误。特别是,终端可以(**MAY**)在检测到错误情况时使用任何适用的错误码,不过通用错误码(例如PROTOCOL_VIOLATION或INTERNAL_ERROR)总是可以用来取代特定的错误码。 + +无状态复位(见第10.3节)不适用于可以用CONNECTION_CLOSE或RESET_STREAM帧指示的任何错误。具有可在连接上发送帧所需状态的终端不得(**MUST NOT**)使用无状态复位。 + +## 11.1. 连接错误(Connection Errors) + +导致连接不可用的错误,例如明显违反协议语义或影响整个连接的状态崩溃,必须(**MUST**)使用CONNECTION_CLOSE帧(第19.19节)发出信号。 + +特定于应用的协议错误,需要使用帧类型为0x1d的CONNECTION_CLOSE帧发出信号。特定于传输的错误,包括本文中描述的所有错误,都需要包含在帧类型为0x1c的CONNECTION_CLOSE帧中。 + +CONNECTION_CLOSE帧可能丢失,如果终端在终止的连接上接收到更多数据包,则它应该(**SHOULD**)准备重传包含CONNECTION_CLOSE帧的数据包。限制重传次数和重传持续时间可以限制在终止连接上花费更多精力。 + +如果选择不重传包含CONNECTION_CLOSE帧的数据包,其对端有可能收不到第一包CONNECTION_CLOSE帧。对继续在已终止的连接上收到数据包的终端,其唯一可行的机制是尝试无状态重置过程(第10.3节)。 + +由于Initial包的AEAD不提供强身份验证,终端可以(**MAY**)丢弃无效的Initial包。不过本规约要求上报连接错误。如果终端不处理数据包中的帧或需要回退某些处理流程,则它只能丢弃数据包。丢弃无效的Initial包可用于减少拒绝服务的风险,参见第21.2节。 + +## 11.2. 流错误(Stream Errors) + +如果应用级错误影响单个流,但因其他因素,连接仍处于可恢复状态,则终端可以发送带有适当错误码的RESET_STREAM帧(第19.4节)以仅终止受影响的流。 + +在不通知应用层协议的情况下重置流可能导致应用层协议进入不可恢复状态,因此RESET_STREAM必须(**MUST**)由调用QUIC的应用层协议发起。 + +RESET_STREAM中携带的应用层错误码的语义由应用层协议定义。只有应用层协议能够终止流。应用层协议的本地实例可直接通过API调用,远端实例使用STOP_SENDING帧,以触发本端启动RESET_STREAM流程。 + +应用层协议应该(**SHOULD**)定义规则,处理被任一终端提前取消的流。 + +# 12. 数据包和帧( Packets and Frames) + +QUIC终端通过交换数据包进行通信。数据包具有机密性和完整性保护,参见第12.1节。数据包在UDP报文中携带,参见第12.2节。 + +此QUIC版本在连接建立时使用长包头,参见第17.2节。带有长包头的数据包是Initial(第17.2.2小节),0-RTT(第17.2.3小节),Handshake(第17.2.4小节),和Retry(第17.2.5小节)。版本协商使用与版本无关的带有长包头的数据包,参见第17.2.1小节。 + +带有短包头的数据包为最小开销设计,并在建立连接且1-RTT密钥可用后使用,参见第17.3节。 + +## 12.1. 受保护的数据包(Protected Packets) + +QUIC包根据类型具有不同级别的加密保护。数据包保护的详细信息可在[QUIC-TLS]中找到,本节概述其所能提供的保护措施。 + +Version Negotiation包没有加密保护,参见[QUIC-INVARIANTS]。 + +Retry包使用AEAD功能[AEAD]来防止意外修改。 + +Initial包使用AEAD保护,其密钥是使用在线上可见的值派生的。因此Initial包没有有效的机密性保护。Initial保护的引入是确保数据包的发送方位于网络路径上。任何从客户端接收到Initial包的实体都可以恢复密钥,这将允许它们既能正确读取数据包的内容,也能生成对端可以成功验证的Initial包。AEAD还保护Initial包免受意外修改。 + +所有其他数据包都使用来自加密Handshake的密钥进行保护。加密Handshake确保只有通信终端才能收到Handshake、0-RTT和1-RTT包的相应密钥。使用0-RTT和1-RTT密钥保护的数据包具有很强的机密性和完整性保护。 + +出现在某些数据包类型中的Packet Number字段作为包头保护的一部分可以替代机密性保护,有关详细信息,请参阅[QUIC-TLS]第5.4节。在特定包号空间中发送的每个包的底层包号都会递增,详细信息参见第12.3节。 + +## 12.2. 合并数据包(Coalescing Packets) + +Initial(第17.2.2小节)、0-RTT(第17.2.3小节)和Handshake(第17.2.4小节)数据包包含指示数据包尾部的Length字段。包长包括Packet Number和Packet Payload字段的长度,这两个字段都是机密性保护的并且最初长度未知。一旦包头保护被移除,Payload字段的长度就会暴露。 + +使用Length字段,发送方可以将多个QUIC包合并为一个UDP报文。这可以减少完成加密握手所需的UDP报文的数量,之后开始发送数据。这也可用于构建路径最大传输单元(PMTU)探针,参见第14.4.1小节。接收方必须(**MUST**)能够处理合并的数据包。 + +按加密级别递增的顺序(Initial、0-RTT、Handshake、1-RTT,参见[QUIC-TLS]第4.1.4小节)合并数据包使得接收方更有可能一次处理所有数据包。具有短包头的数据包不包含长度,因此它只能是UDP报文中包含的最后一个数据包。如果多个帧有相同的加密级别,终端应该(**SHOULD**)尽量将它们组帧在单个数据包中,而不是分开在多个数据包中发送。 + +接收方可以(**MAY**)根据UDP报文中包含的第一个数据包中的信息进行路由。发送方不得(**MUST NOT**)将具有不同CID的QUIC包合并为单个UDP报文。接收方应该(**SHOULD**)忽略与数据报文中的第一个数据包具有不同DCID的任何后续数据包。 + +合并成单个UDP报文的每个QUIC包都是独立且完整的。接收到多QUIC包合并的UDP报文,接收方必须(**MUST**)单独处理每个QUIC包并分别确认它们,就好像它们是作为不同UDP报文的负载被接收的一样。例如,如果某包解密失败(因为密钥不可用或出于任何其他原因),接收方可以(**MAY**)丢弃或缓冲这个数据包以待后续处理,之后必须(**MUST**)尝试处理UDP报文中的剩余数据包。 + +Retry包(第17.2.5小节)、Version Negotiation包(第17.2.1小节)和具有短包头的数据包(第17.3节)不包含Length字段,因此不能做UDP报文的中间数据包,其实也不会出现Retry或Version Negotiation包与另一个数据包合并的情况。 + +## 12.3. 包号(Packet Numbers) + +包号是0到2^62^-1范围内的整数。该编号用于确定数据包保护的加密随机数。每个终端对发送和接收部分分别维护一个单独的包号。 + +包号限制在此范围内,因为它们需要在ACK帧的最大确认字段中完整表示(第19.3节)。然而,当出现在长或短包头中时,包号长度会压缩并编码为1到4个字节,参见第17.1节。 + +Version Negotiation包(第17.2.1小节)和Retry包(第17.2.5小节)不包含包号。 + +QUIC中包号分为三个空间: +* Initial空间:所有Initial包(第17.2.2小节)都在此空间中。 +* Handshake空间:所有Handshake包(第17.2.4小节)都在这个空间中。 +* 应用数据空间:所有0-RTT(第17.2.3小节)和1-RTT(第17.3.1小节)数据包都在这个空间中。 + +如[QUIC-TLS]中所述,每种数据包类型使用不同的保护密钥。 + +从概念上讲,包号空间是可以处理和确认数据包的上下文。Initial包只能使用Initial包保护密钥发送,并只能在Initial包中进行确认。同样,Handshake包使用Handshake级别加密发送,只能在Handshake包中确认。 + +这强制在不同包号空间中发送的数据之间进行加密分离。每个空间中的包号从0开始,在同一包号空间中发送的后续包号至少增加1。 + +0-RTT和1-RTT数据存在于同一个包号空间中,使两种数据包类型之间的丢包恢复算法更容易实现。 + +QUIC终端不得(**MUST NOT**)在一个连接中的相同包号空间内重用包号。如果要发送的数据包数量达到2^62^-1,则发送方必须(**MUST**)关闭连接,并且不发送CONNECTION_CLOSE帧或任何其他数据包,在接收到对端后续数据包时,可以(**MAY**)发送Stateless Reset包(第10.3节)。 + +接收方必须(**MUST**)丢弃一个新的未受保护的数据包,除非在相同的包号空间,它处理过相同包号的另一个数据包。因为由于[QUIC-TLS]第9.5节中描述的原因,删除数据包保护后必须(**MUST**)发生重复抑制(Duplicate Suppression)。 + +为检测重复数据而跟踪所有单个数据包,终端存在累积过多状态的风险。可以通过维护一个最小处理包号来限制检测重复所需的数据量,所有小于该编号的数据包可被立即丢弃。任何最小值的设定都需要考虑RTT的巨大变化,其中需要包括对端可能在更大的RTT下探测网络路径的可能性,参见第9章。 + +第17.1节描述了发送方的包号编码和接收方的解码。 + +## 12.4. 帧和帧类型(Frames and Frame Types) + +去除数据包保护后,QUIC包的负载由一系列完整的帧组成,如图11所示。Version Negotiation包、Stateless Reset包和Retry包不包含帧。 + +``` + Packet Payload { + Frame (8..) ..., + } +``` +图11: QUIC Payload + +包含帧的数据包的负载必须(**MUST**)至少包含一个帧,并且可以(**MAY**)包含多个帧和多种帧类型。终端必须(**MUST**)将收到不包含帧的数据包视为PROTOCOL_VIOLATION类型的连接错误。一个帧只能包含在单个QUIC包中,不能跨越多个数据包。 + +每个帧都以一个帧类型开始,表明它的类型,然后是附加的类型相关字段: + +``` + Frame { + Frame Type (i), + Type-Dependent Fields (..), + } +``` +图12: Generic Frame Layout + +表3列出并总结了有关本规约中定义的每种帧类型的信息。表格之后有具体说明。 + + | Type Value | Frame Type Name | Definition | Pkts | Spec | + |:---|:---|:---|:---|:---| + |0x00 | PADDING | 第19.1节 | IH01 | NP | + |0x01 | PING | 第19.2节 | IH01 | | + |0x02-0x03| ACK | 第19.3节 | IH_1 | NC | + |0x04 | RESET_STREAM | 第19.4节 | __01 | | + |0x05 | STOP_SENDING | 第19.5节 | __01 | | + |0x06 | CRYPTO | 第19.6节 | IH_1 | | + |0x07 | NEW_TOKEN | 第19.7节 | ___1 | | + |0x08-0x0f| STREAM | 第19.8节 | __01 | F | + |0x10 | MAX_DATA | 第19.9节 | __01 | | + |0x11 | MAX_STREAM_DATA | 第19.10节| __01 | | + |0x12-0x13| MAX_STREAMS | 第19.11节| __01 | | + |0x14 | DATA_BLOCKED | 第19.12节| __01 | | + |0x15 | STREAM_DATA_BLOCKED | 第19.13节| __01 | | + |0x16-0x17| STREAMS_BLOCKED | 第19.14节| __01 | | + |0x18 | NEW_CONNECTION_ID | 第19.15节| __01 | P | + |0x19 | RETIRE_CONNECTION_ID | 第19.16节| __01 | | + |0x1a | PATH_CHALLENGE | 第19.17节| __01 | P | + |0x1b | PATH_RESPONSE | 第19.18节| ___1 | P | + |0x1c-0x1d| CONNECTION_CLOSE | 第19.19节| ih01 | N | + |0x1e | HANDSHAKE_DONE | 第19.20节| ___1 | | + +表3: Frame Types + +第19章.更详细地解释了每种帧类型的格式和语义。本节的其余部分提供了重要和一般信息的摘要。 + +ACK、STREAM、MAX_STREAMS、STREAMS_BLOCKED和CONNECTION_CLOSE帧中的Frame Type字段用于携带其他特定于帧的标志。对于所有其他帧,Frame Type字段仅标识帧。 + +表3中的Pkts列列出了每种帧类型可能出现的数据包类型,由以下字符表示: +* I: Initial(第17.2.2小节) +* H: Handshake(第17.2.4小节) +* 0: 0-RTT(第17.2.3小节) +* 1: 1-RTT(第17.3.1小节) +* ih: 只有0x1c类型的CONNECTION_CLOSE帧可以出现在Initial或Handshake包中。 + +有关这些限制的更多详细信息,请参阅第12.5节。请注意,所有帧都可以出现在1-RTT包中。终端必须(**MUST**)将接收到的数据包类型中不允许出现的帧视为PROTOCOL_VIOLATION类型的连接错误。 + +表3中的Spec列总结了控制帧类型处理或生成的任何特殊规则,如以下字符所示: +* N:仅包含带有此标记的帧的数据包不是ACK触发包,参见第13.2节。 +* C:出于拥塞控制目的,仅包含带有此标记的帧的数据包,不计入传输中的字节数计算,见[QUIC-RECOVERY]。 +* P:仅包含带有此标记的帧的数据包,可用于在连接迁移期间探测新的网络路径,参见第9.1节。 +* F:带有此标记的是流控帧,参见第4章。 + +表3中的Pkts和Spec列不构成IANA注册管理机制的一部分,参见第22.4节。 + +终端必须(**MUST**)将收到未知类型的帧视为FRAME_ENCODING_ERROR类型的连接错误。 + +在此QUIC版本中,所有的帧都是幂等的。也就是说,多次接收相同的有效帧不应触发不良效果或报错。 + +Frame Type字段使用变长整数编码(第16章),但有一个前提。为确保帧解析实现的简单有效,帧类型必须(**MUST**)使用尽可能短的编码。对于本文中定义的帧类型,这意味着使用单字节编码,即使可以将这些值编码为两字节、四字节或八字节的变长整数。例如,尽管0x4001也是值为1的变长整数的合法两字节编码,但PING帧只能编码为值为0x01的单个字节。此规则适用于所有当前和未来的QUIC帧类型。终端接收到将帧类型编码为比实际所需更长字节的帧,可以(**MAY**)视为PROTOCOL_VIOLATION类型的连接错误。 + +## 12.5. 帧和包号空间(Frames and Number Spaces) + +某些帧在其他的数据包空间中是被禁止的。这里的规则概括了TLS的规则,与连接建立相关的帧,通常可以出现在任何包号空间的数据包中,而与传输数据相关的那些只能出现在应用包号空间中: + +* PADDING、PING和CRYPTO帧可以(**MAY**)出现在任何包号空间中。 +* CONNECTION_CLOSE指示QUIC层错误(类型0x1c)可以(**MAY**)出现在任何包号空间中。CONNECTION_CLOSE指示应用错误(类型0x1d)必须(**MUST**)只出现在应用包号空间中。 +* ACK帧可以(**MAY**)出现在任何包号空间中,但只能确认出现在该包号空间中的数据包。但是,如下所述的0-RTT包不能包含ACK帧。 +* 所有其他帧类型必须(**MUST**)仅在应用包号空间中发送。 + +请注意,由于各种原因,无法在0-RTT包中发送以下帧:ACK、CRYPTO、HANDSHAKE_DONE、NEW_TOKEN、PATH_RESPONSE或RETIRE_CONNECTION_ID。服务端可以(**MAY**)将在0-RTT包中收到这些帧视为PROTOCOL_VIOLATION类型的连接错误。 + +# 13. 组包与可靠性(Packetization and Reliability) + +发送方可在一个QUIC包中发送一个或多个帧,参见第12.4节。 + +发送方可以通过在每个QUIC包中,包含尽可能多的帧来最小化每个数据包的带宽和计算成本。发送方可以(**MAY**)等待一小段时间来收集多个帧,然后再打包发送一个大的但未超限的数据包,以避免发送大量小数据包。实现可以(**MAY**)收集应用发送行为规律或先验知识来确定是否等待以及等待多长时间。这个等待时间由实现决定,实现应谨慎地考虑这个时间,因为任何迟滞都可能增加应用可见的时延。 + +流复用是通过将来自多个流的STREAM帧交织成一个或多个QUIC包来实现的。单个QUIC包可以包含来自一个或多个流的多个STREAM帧。 + +QUIC的好处之一是避免跨多个流的队头阻塞。当发生数据包丢失时,只有在该数据包中有数据的流才会被阻塞,等待接收重传,而其他流可以继续前进。请注意,当来自多个流的数据包含在单个QUIC包中时,该数据包的丢失会阻止所有这些流的前进。建议实现在出向数据包中包含尽可能少的流,但也尽量不要因为大量padding而降低传输效率。 + +## 13.1. 数据包处理(Packet Processing) + +在成功解密并处理完数据包内所有帧之前,不得(**MUST NOT**)回数据包确认ACK。对于STREAM帧,"处理完"意味着数据已经入队以备应用层协议接收,但不意味着已经投递给应用或应用已经消费数据。 + +一旦数据包被完全处理,接收端通过发送一个或多个包含已接收包号的ACK帧来确认接收。 + +如果终端收到对其未发送数据包的确认ACK,应该(**SHOULD**)将其视为PROTOCOL_VIOLATION类型的连接错误。有关如何实现这一点的进一步讨论,请参见第21.4节。 + +## 13.2. 生成确认ACK(Generating Acknowledgments) + +终端需要确认它们接收和处理的所有数据包。然而,只有ACK触发包(ACK触发)会导致在ACK发送窗口期内发送ACK帧。非ACK触发包仅在因其他原因发送ACK帧时才被确认。 + +在无论因为什么原因需要发送数据包时,如果最近没有发过包,终端应该(**SHOULD**)尝试包含一个ACK帧。这样做有助于对端及时检测是否丢包。 + +通常,来自接收端的频繁确认会改善丢包和拥塞情况,但如果接收端对每个ACK触发包都回以ACK的话就太过了。必须考虑平衡这种过度负载,下面提供的指引旨在实现这种平衡。 + +### 13.2.1. 发送ACK帧(Sending ACK Frames) + +每个数据包应该(**SHOULD**)至少被确认一次,并且必须(**MUST**)在终端用max_ack_delay传输参数通告的窗口期内至少确认一次ACK触发包,参见第18.2节。max_ack_delay声明了一个明确的约定:终端承诺会在max_ack_delay超时之前发送ACK帧确认ACK触发包。否则,RTT的估值会无谓放大,并可能导致对端错误的超时重传。发送方使用接收方通告的max_ack_delay值来确定基于定时器的超时重传,如[QUIC-RECOVERY]第6.2节所述。 + +终端必须(**MUST**)立即确认所有Initial和Handshake触发包,以及在通告的max_ack_delay内确认所有0-RTT和1-RTT触发包,以下情况除外:在握手确认之前,终端可能没有可用的秘钥在收到Handshake、0-RTT或1-RTT包时对其解密。因此,它可能会先缓存它们并在密钥可用时再确认它们。 + +由于仅包含ACK帧的数据包不受拥塞控制,因此终端在收到一个ACK触发包时不得(**MUST NOT**)发送多个这种包。 + +终端不得(**MUST NOT**)发送非ACK触发包来响应非ACK触发包,即使收到的包号不连续。这可以避免确认形成反馈死循环,也可以避免因为连接空闲导致的断链。只有当终端发送ACK帧以响应其他事件时,才可以确认非ACK触发包。 + +仅发送ACK帧的终端将不会收到来自其对端的确认ACK,除非这些确认包含在ACK触发包内。当有新的ACK触发包要确认时,ACK帧可以与其他帧一起发送。当只需要确认非ACK触发包时,终端可以(**MAY**)选择不发送ACK帧,直到收到ACK触发包需要发送ACK帧为止。 + +仅发送非ACK触发包的终端可能会选择偶尔向这些数据包内添加ACK触发帧,以保证能收到ACK。但在第13.2.4小节这种场景下,终端不得(**MUST NOT**)在非ACK触发包内插入ACK触发帧,否则会导致确认陷入死循环。 + +为了帮助发送方进行丢包检测,终端应该(**SHOULD**)在接收到ACK触发包时立即生成并发送一个ACK帧: +* 当收到的数据包的编号小于另一个已收到的ACK触发包时; +* 当数据包的编号大于已接收到的最高编号的ACK触发包,并且编号不连续时; + +类似地,在收到IP报头中标有ECN Congestion Experienced(CE)码点的数据包时应该(**SHOULD**)立即确认,以减少对端对拥塞事件的响应时间。 + +[QUIC-RECOVERY]中的算法需要对不遵循上述指引的接收端具有弹性。然而,只有在仔细考虑变更对终端和网络其他用户的连接的性能影响后,实现才可以不考虑这些要求。 + +### 13.2.2. 确认包频率(Acknowledgment Frequency) +接收端决定响应ACK触发包的ACK的发送频率。这个频率需要权衡考量。 + +终端依靠及时的ACK来检测丢包,参见[QUIC-RECOVERY]第6章。基于窗口的拥塞控制器,参见[QUIC-RECOVERY]第7章中描述的控制器,依靠ACK来管理它们的拥塞窗口。在这两种情况下,延迟确认都会对性能产生不利影响。 + +另一方面,减少仅携带ACK的数据包的频率会降低两端数据包传输和处理成本。它可以改善严重不对称链路上的连接吞吐量,并使用返回路径容量以减少确认流量,参见[RFC3449]第3章。 + +接收方应该(**SHOULD**)在收到至少两个ACK触发包后才发送一个ACK帧。该建议本质上是通用的,与TCP的ACK建议策略也是一致的,参见[RFC5681]。对网络条件、对端的拥塞控制器等的先验知识和进一步的研究和实验,可能会有更好的确认机制选择方案以提升性能。 + +接收方可以(**MAY**)考虑处理多少个可用数据包之后再发送ACK响应帧。 + +### 13.2.3. 管理ACKRanges(Managing ACK Ranges) + +ACK帧内包括一个或多个ACK Ranges确认接收数据包。包含对旧数据包的确认可以减少由于前序ACK帧丢失导致的不当重传的可能,但代价是ACK帧会更大。 + +ACK帧应该(**SHOULD**)总是确认最近收到的数据包,并且数据包越是乱序,更快发送更新的ACK帧就越重要,以防止对端认为数据包丢失并不当重传它包含的帧。一个ACK帧应完整放入一个QUIC包,放不进去的话,则忽略较老的Ranges(具有更小包号的Ranges )。 + +接收方需要限制其在ACK帧中发送的ACK Ranges的数量(第19.3.1小节),以降低ACK帧的大小并避免资源耗尽。在收到对ACK帧的确认后,接收方应该(**SHOULD**)停止跟踪那些确认的ACK Ranges。发送方可以预期能收到大多数数据包的确认,但QUIC不保证能收到接收方对其处理的每个数据包的确认。 + +携带多个ACK Ranges可能会导致ACK帧变得太大而无法装入数据包,接收方可以丢弃部分未确认的ACK Ranges以限制ACK帧的大小,其代价是发送方的重传次数增加,但这是必要的。接收方还可以(**MAY**)进一步限制ACK帧的大小或限制ACK帧的容量占比以腾出其他帧的空间。 + +接收方必须(**MUST**)保留某个ACK Range,除非其可以确保随后不会接受具有该Range范围内编号的数据包。维护一个随着Ranges丢弃而增加的最小包号,可以最小状态实现这一目标。 + +接收方可以丢弃所有的ACK Ranges,但必须(**MUST**)维护已成功处理的最大包号,因为它用于从后续数据包中恢复包号,参见第17.1节。 + +接收方在每个ACK帧中都应该(**SHOULD**)包含一个ACK Range,该Range包含最大接收包号。Largest Acknowledged字段用于发送方ECN验证,如果该值比前序ACK帧中的值要小的话可能导致ECN被不必要地禁用,参见第13.4.2小节。 + +第13.4.2小节给出了决定需要在ACK帧中确认哪些数据包的示例性方法。尽管此算法的目标是为每个处理的数据包生成确认,但也适用于确认丢失的情况。 + +### 13.2.4. 通过跟踪ACK帧来限制Ranges(Limiting Ranges by Tracking ACK Frames) + +当发送包含ACK帧的数据包时,可以保存该帧中的Largest Acknowledged字段。当包含此ACK帧的数据包被确认时,接收方可以停止确认小于或等于此帧中的Largest Acknowledged字段的数据包。 + +仅发送非ACK触发数据包(例如ACK帧)的接收方可能在很长一段时间内都不会收到确认。这可能会导致接收方长时间维护大量ACK帧的状态,且在组帧时ACK帧可能会过大。在这种情况下,接收方可以偶尔发送PING帧或其他小的ACK触发帧,例如每个RTT一次,以触发对端回ACK。 + +在没有ACK帧丢失的情况下,该算法容忍至少1个RTT的乱序。在ACK帧丢失和乱序的情况下,这种方法不能保证在某些ACK Ranges移出ACK帧之前,发送方能看到它。数据包可能会被乱序接收,并且对他们进行确认的所有ACK帧都可能丢失。在这种情况下,丢包恢复算法可能会导致不当重传,但发送方的处理会继续。 + +### 13.2.5. 测量和报告主机延迟(Measuring and Reporting Host Delay) + +接收方可以测量从接收到最大包号的数据包到发送ACK之间主动引入的延迟,并在ACK帧的ACK Delay字段中对此延迟时间进行编码,参见第19.3节。这使得此ACK帧的接收方可以依据此延迟,对路径RTT估计进行更精确的调整。 + +数据包在被处理之前可能会保存在操作系统内核或主机上的其他地方,当在ACK帧中填充ACK Delay字段时,终端不得(**MUST NOT**)包括它无法控制的延迟。然而,终端应该(**SHOULD**)将解密密钥不可用引起的缓冲延迟计算在内,因为这些延迟可能很大并且很可能是不可复现的。 + +当测量的确认延迟大于其max_ack_delay时,终端应该(**SHOULD**)上报测量延迟。在延迟可能很大时,此信息在握手期间特别有用,参见第13.2.1小节。 + +### 13.2.6. ACK帧和数据包保护(ACK Frames and Packet Protection) + +ACK帧必须(**MUST**)只在与被确认的数据包具有相同包号空间的数据包中携带,参见第12.1节。例如,使用1-RTT密钥保护的数据包必须(**MUST**)在同样使用1-RTT密钥保护的数据包中确认。 + +客户端使用0-RTT包保护发送的数据包必须(**MUST**)由服务端在由1-RTT密钥保护的数据包中确认。这可能意味着如果服务端加密握手消息延迟或丢失,客户端将无法使用这些确认。请注意,同样的限制适用于受1-RTT密钥保护的服务端发送的其他数据。 + +### 13.2.7. PADDING帧消耗拥塞窗口(PADDING Frames Consume Congestion Window) + +在数据包中包含PADDING帧是出于拥塞控制的目的,参见[QUIC-RECOVERY]。但仅包含PADDING帧的数据包只会消耗拥塞窗口,却不会生成ACK移动或扩大窗口。为避免死锁,发送方应该(**SHOULD**)确保定期发送PADDING帧之外的其他帧以触发接收方的确认。 + +## 13.3. 信息重传(Retransmission of Information) + +确定丢失的QUIC包不需要全部重传。这同样适用于包含在丢失数据包中的帧,在新的帧中可根据需要再次发送在丢失帧中携带的信息。 + +新的帧和数据包用于重传确定已丢失的信息。通常,当确定包含该信息的数据包丢失时将再次发送信息,并在包含该信息的数据包被确认时停止发送。 + +* CRYPTO帧:在CRYPTO帧中发送的数据根据[QUIC-RECOVERY]中的规则进行重传,直到所有数据都得到确认。当相应包号空间的密钥被丢弃时,Initial和Handshake包的CRYPTO帧中的数据也将被丢弃。 + +* STREAM帧:除非终端在该流上发送了RESET_STREAM,否则在STREAM帧中发送的应用数据将在新的STREAM帧中进行重传。一旦终端发送RESET_STREAM帧,就不再发送其他STREAM帧。 + +* ACK帧:ACK帧携带最近的一组确认和Largest Acknowledged包的确认延迟,如第13.2.1小节所述。包含ACK帧的数据包的传输延迟或旧的ACK帧重传可能会导致对端计算出较大的RTT或不必要地禁用ECN。 + +* RESET_STREAM帧:携带流传输取消信息的RESET_STREAM帧,会一直发送直到被确认或所有流数据都被对端确认(即流的发送侧达到Reset Recvd或Data Recvd状态)。RESET_STREAM帧的内容在重传时不得(**MUST NOT**)更改。 + +* STOP_SENDING帧:类似地,携带取消流传输STOP_SENDING帧,也会一直发送,直到流的接收侧进入Data Recvd或Reset Recvd状态,参见第3.5节。 + +* CONNECTION_CLOSE帧:携带连接关闭信号的CONNECTION_CLOSE帧,被检测到丢包时,不需要重传,如何重发这些信号参见第10章。 + +* MAX_DATA帧:MAX_DATA帧携带连接最大可发数据量信息。如果包含最近发送的MAX_DATA帧的数据包被声明丢失或当终端决定更新其值时,则需要在MAX_DATA帧中发送更新后的值。需要小心避免过于频繁地发送此帧,因为这会导致该值的频繁更新,并发送大量不必要的MAX_DATA帧,参见第4.2节。 + +* MAX_STREAM_DATA帧:MAX_STREAM_DATA帧中携带当前最大流数据偏移量。与MAX_DATA一样,当包含流的最新MAX_STREAM_DATA帧的数据包丢失或需要更新该值时,将发送更新后的值,注意防止帧发送过于频繁。当流的接收部分进入Size Known 或Reset Recvd状态时,终端应该(**SHOULD**)停止发送MAX_STREAM_DATA帧。 + +* MAX_STREAMS帧:MAX_STREAMS帧中携带给定类型的流个数的限制。与MAX_DATA一样,当最新包含MAX_STREAMS的数据包被声明丢失或值更新时,将发送更新的值,注意防止帧发送过于频繁。 + +* DATA_BLOCKED、STREAM_DATA_BLOCKED和STREAMS_BLOCKED帧:这几类帧承载阻塞信号。DATA_BLOCKED帧具有连接范围,STREAM_DATA_BLOCKED帧具有流范围,而STREAMS_BLOCKED帧的范围限定为特定的流类型。如果上述某个范围中包含最近帧的数据包丢失,则发送新帧。BLOCKED帧仅当终端因为相应限制被阻塞时才发送,这些帧总是包含导致阻塞的限制原因。 + +* PATH_CHALLENGE帧:定期发送PATH_CHALLENGE帧以执行活性或路径验证检查,直到接收到匹配的PATH_RESPONSE帧或不再需要活性或路径验证检查。PATH_CHALLENGE帧每次发送时都需包含不同的负载。 + +* PATH_RESPONSE帧:PATH_RESPONSE帧对路径验证的响应只发送一次。对端应根据需要发送更多PATH_CHALLENGE帧以唤起相应的PATH_RESPONSE帧。 + +* NEW_CONNECTION_ID帧:新的CID在NEW_CONNECTION_ID帧中发送,如果包含它们的数据包丢失,则需要重传,重传帧携带相同的序列号值。同样,停用的CID在RETIRE_CONNECTION_ID帧中发送,如果包含它们的数据包丢失,则进行重传。 + +* NEW_TOKEN帧:如果包含NEW_TOKEN帧的数据包丢失,则重传它们。除了直接比较帧内容之外,不需要别的手段处理乱序或重复的NEW_TOKEN帧。 + +* PING和PADDING帧:PING和PADDING帧不包含任何信息,因此丢失的PING或PADDING帧不需要恢复。 + +* HANDSHAKE_DONE帧:HANDSHAKE_DONE帧丢包必须(**MUST**)重传,直到它被确认为止。 + +终端应该(**SHOULD**)优先重传数据而不是发送新数据,除非应用指定的优先级另有说明,参见第2.3节。 + +尽管鼓励发送方在每次发送数据包时组合包含最新信息的帧,但不禁止重传丢失包中包含的帧副本。重传丢失帧的发送方需要考虑因为包号长度、CID长度和路径MTU的变化而导致的可用负载大小的变化。接收方必须(**MUST**)接受包含过时帧的数据包,例如MAX_DATA帧携带的最大数据值小于旧数据包中的最大数据值。 + +一旦数据包被确认,发送方应该(**SHOULD**)避免重传来自该数据包的信息,包括在在网络乱序的情况下声明丢失后又被确认的数据包。这样做要求发送方在声明丢失后仍然保留有关数据包的信息。发送方可以在其容忍乱序的时限到期后丢弃此信息,例如PTO([QUIC-RECOVERY]第6.2节),或基于其他事件,例如达到内存限制。 + +在检测到丢包时,发送方必须(**MUST**)采取适当的拥塞控制措施。[QUIC-RECOVERY]中描述了丢包检测和拥塞控制的细节。 + +## 13.4. 显式拥塞通知(Explicit Congestion Notification) + +QUIC终端可以使用ECN[RFC3168] 来检测和响应网络拥塞。ECN允许终端在IP数据包的ECN字段中设置支持ECN的传输(ECT)码点。然后,网络节点可以通过在ECN字段中设置ECN-CE码点而不是丢弃数据包来指示拥塞[RFC8087]。如[QUIC-RECOVERY]中所述,终端通过降低其发送速率来响应报告的拥塞。 + +要启用ECN,QUIC发送端首先确定路径是否支持ECN标记以及对端是否报告接收到的IP标头中的ECN值,参见第13.4.2小节。 + +### 13.4.1. 报告ECN计数(Reporting ECN Counts) + +使用ECN需要接收端从IP数据包中读取ECN字段,这在所有平台上都是不可能的。如果终端不支持ECN或无权访问接收到的ECN字段,它不会报告它接收到的数据包的ECN计数。 + +即使终端没有在它发送的数据包中设置ECT字段,如果可行的话,终端也必须(**MUST**)提供有关它收到的ECN标记的反馈。未能报告ECN计数将导致发送方在此连接禁用ECN。 + +在接收到带有ECT(0)、ECT(1)或ECN-CE码点的IP数据包时,启用ECN的终端访问ECN字段并增加相应的ECT(0)、ECT(1)或ECN-CE记数。后续的ACK帧中可携带这些ECN计数,参见第13.2节和第19.3节。 + +每个包号空间维护单独的确认状态和单独的ECN计数。合并的QUIC包(参见第12.2节)共享相同的IP报头,因此对于其中的每个QUIC包,ECN计数都需要增加一次。 + +例如,如果Initial、Handshake和1-RTT QUIC包合并为单个UDP报文,则所有三个包号空间的ECN计数都需要加1。 + +ECN计数仅在处理承载在IP报文中的QUIC包时增加。重复的QUIC包不会被处理,也就不会增加ECN计数。有关安全问题的考虑,请参阅第21.10节。 + +### 13.4.2. ECN验证(ECN Validation) + +有故障的网络设备可能会损坏或错误地丢弃携带非零ECN码点的数据包。因为此类设备的存在,终端为了确保连接性,将验证每条网络路径的ECN计数,并在检测到错误时禁用该路径上的ECN。 + +要对新路径执行ECN验证: + +终端在通过新路径发往对端的早期出向数据包的IP标头中设置ECT(0) 码点[RFC8311]。 + +终端监控所有带有ECT码点的数据包是否最终都被视为丢失(参见[QUIC-RECOVERY]第6章),此现象表明ECN验证失败。 + +如果终端有理由预期带有ECT码点的IP数据包可能会被故障网元丢弃,则终端可以仅为路径上前十个出向数据包设置ECT码点,或仅在三个PTO的时间段设置ECT码点(参见[QUIC-RECOVERY]第6.2节。如果所有标有非零ECN码点的数据包随后丢失,则可以假设是因为标记导致的丢失,需要禁用标记。 + +因此,在切换到服务端的首选地址或将活动连接迁移到新路径时,终端尝试为每个新连接使用ECN验证。Appendix A.4描述了一种可能的算法。 + +其他探测路径是否支持ECN的方法也是可行的,可以采取不同的标记策略。实现可以(**MAY**)使用RFC中定义的其他方法,参见[RFC8311]。使用ECT(1) 码点的实现需要使用报告的ECT(1)计数执行ECN验证。 + +#### 13.4.2.1. 接收带有ECN计数的ACK帧(Receiving ACK Frames with ECN Counts) +网络错误应用ECN-CE标记会导致连接性能的下降。因此,接收带有ECN计数的ACK帧的终端,会在使用计数之前验证这些计数。它通过将新接收的计数与上次成功处理的ACK帧的计数进行比较,来执行此验证。对ECN计数的任何增加是否正常的验证,都是基于ACK帧中新确认的数据包的ECN标记。 + +如果ACK帧新确认了终端发送的带有ECT(0)或ECT(1)码点集的数据包,如果相应的ECN计数不存在于ACK帧中,则ECN验证失败。这种检查将探测把ECN字段置零的网元或不报告ECN标记的对端。 + +如果ECT(0)或ECN-CE计数增加的总和小于最初使用ECT(0)标记发送的新确认数据包的数量,则ECN验证也会失败。类似地,如果ECT(1)或ECN-CE计数增加的总和小于使用ECT(1)标记发送的新确认数据包的数量,则ECN也验证失败。这些检查可以检测网络对ECN-CE标记的重新标记。 + +当ACK帧丢失时,终端可能会错过对数据包的确认。因此,ECT(0)、ECT(1)和ECN-CE计数的总增加可能大于ACK帧新确认的数据包数量。这就是为什么允许ECN计数大于已确认的数据包总数的原因。 + +从乱序的ACK帧验证ECN计数可能会导致失败。终端不得(**MUST NOT**)因处理到未增加Largest Acknowledged包数的ACK帧而判定ECN验证失败。 + +如果ECT(0)或ECT(1)的接收总数超过每个相应ECT码点发送的数据包总数,则ECN验证可能会失败。特别是,当终端收到来自从未应用的ECT码点对应的非零ECN计数时,验证将失败。此检查检测数据包何时在网络中被标记为ECT(0)或ECT(1)。 + +#### 13.4.2.2. ECN验证结果(ECN Validation Outcomes) + +如果验证失败,则终端必须(**MUST**)禁用ECN。终端将停止在其发送的IP数据包中设置ECT码点,并假设网络路径或对端不支持ECN。 + +即使验证失败,终端也可以(**MAY**)在连接中的任何稍后时间重新验证相同路径的ECN。终端可以继续定期尝试验证ECN。 + +成功验证后,终端可以(**MAY**)继续在它发送的后续数据包中设置ECT码点,并期望该路径具有ECN能力。网络路由和路径元素可以改变中间连接,如果稍后验证失败,终端必须(**MUST**)禁用ECN。 + +# 14. 报文大小(Datagram Size) + +一个UDP报文可以包含一个或多个QUIC包。报文大小是指携带QUIC包的单个UDP报文的负载大小。报文大小包括一个或多个QUIC包头和受保护的负载长度,但不包括UDP或IP头。 + +最大报文大小定义为可以使用单个UDP报文跨网络路径传输的最大UDP负载大小。如果网络路径不支持至少1200字节的最大报文大小,则不得(**MUST NOT**)使用QUIC。 + +QUIC假设最小IP报文大小至少为1280字节。这是IPv6最小大小[IPv6],大多数现代IPv4网络也支持。假设IPv6的最小IP报头大小为40字节,IPv4为20字节,UDP报头大小为8字节,这将导致IPv6的最大报文大小为1232字节,IPv4为1252字节。因此,现代IPv4和所有IPv6网络路径都有望支持QUIC。 + +> 注意:如果路径仅支持1280字节的IPv6最小MTU,则此支持1200字节UDP负载的要求会将IPv6扩展标头的可用空间限制为32字节或IPv4选项的可用空间为52字节。这会影响Initial包和路径验证。 + +任何大于1200字节的最大报文大小都可以使用路径最大传输单元发现 (PMTUD)(参见第14.2.1小节))或报文分组层PMTU发现(DPLPMTUD)(参见第14.3节)来发现。 + +使用max_udp_payload_size传输参数(第18.2节)可以对最大报文大小附加强制限制。一旦知道该值,发送方就可以避免超过此限制。但在此之前,如果终端发送的报文大于1200字节的最小允许最大报文大小,则它们可能会出现丢包。 + +UDP报文绝不得(**MUST NOT**)在IP层分片。在IPv4[IPv4]中,如果可能,必须(**MUST**)设置Don't Fragment(DF)位,以防止在路径上分片。 + +QUIC有时要求报文不小于一定大小,参见第8.1节的示例。但是,报文的大小没有经过验证。也就是说,如果一个终端接收到一个特定大小的报文,它无法知道其是否与发送方发送的报文大小相同。因此,当终端收到不满足大小限制的报文时,它不得(**MUST NOT**)关闭连接,终端可以(**MAY**)丢弃这样的报文。 + +## 14.1. 初始报文大小(Initial Datagram Size) + +客户端必须(**MUST**)将所有承载Initial包的UDP报文负载扩展到至少1200字节的最小允许最大报文大小,这可以通过将PADDING帧添加到Initial包或合并Initial包实现,参见第12.2节。Initial包甚至可以与无效数据包合并,接收端可以丢弃无效数据包。类似地,服务端必须(**MUST**)将所有携带ACK触发Initial包的UDP报文负载扩展到至少1200字节的最小允许最大报文大小。 + +发送这种大小的UDP报文可确保网络路径在两个方向上都支持合理的路径最大传输单元 (PMTU)。此外,扩展Initial包的客户端有助于减少由服务端响应未经验证的客户端地址引起的放大攻击的幅度,参见第8章。 + +如果发送方认为网络路径和对端都支持它选择的大小,则包含Initial包的报文可以(**MAY**)超过1200字节。 + +如果UDP报文负载小于1200字节的最小允许最大报文大小,服务端必须(**MUST**)丢弃在UDP报文中携带的Initial包。服务端也可以(**MAY**)通过发送一个错误码为PROTOCOL_VIOLATION的CONNECTION_CLOSE帧来立即关闭连接,参见第10.2.3小节。 + +服务端还必须(**MUST**)在验证客户端地址之前限制它发送的字节数,参见第8章。 + +## 14.2. 路径最大传输单元(Path Maximum Transmission Unit) + +PMTU是整个IP数据包的最大大小,包括IP报头、UDP报头和UDP负载。UDP负载包括一个或多个QUIC包头和受保护的负载。PMTU可以取决于路径特性,因此可以随时间变化。终端在任何给定时间发送的最大UDP负载称为终端的最大报文大小。 + +终端应该(**SHOULD**)使用DPLPMTUD(第14.3节)或PMTUD(第14.2.1小节)来确定到目的地的路径是否支持所需的最大报文大小而不会分片。在没有这些机制的情况下,QUIC终端不应该(**SHOULD NOT**)发送大于最小允许最大报文大小的报文。 + +DPLPMTUD和PMTUD都发送大于当前最大报文大小的报文,称为PMTU探测。未在PMTU探测中发送的所有QUIC包应该(**SHOULD**)调整大小以适应最大报文大小,以避免报文被分段或丢弃[RFC8085]。 + +如果QUIC终端确定任何一对本地和远程IP地址之间的PMTU不支持1200字节的最小允许最大报文大小,则它必须(**MUST**)在受影响的路径上立即停止发送QUIC包,除了那些在PMTU探测中的数据包或包含CONNECTION_CLOSE帧的数据包。如果找不到替代路径,终端可以(**MAY**)终止连接。 + +每对本地和远程地址对可以有不同的PMTU。因此,实现任何类型PMTU发现的QUIC实现应该(**SHOULD**)为本地和远程IP地址的每个组合维护最大报文大小。 + +QUIC实现在计算最大报文大小时可以(**MAY**)更加保守,以允许未知的隧道开销或IP报头选项/扩展。 + +### 14.2.1. PMTUD处理ICMP消息(Handling of ICMP Messages by PMTUD) + +PMTUD[RFC1191][RFC8201] 的计算依赖于ICMP消息(也就是IPv6 Packet Too Big(PTB)消息)的接收,该消息指示IP数据包何时因为其大于本地路由器MTU被丢弃。DPLPMTUD也可以选择使用这类消息。ICMP消息的这种用法可能容易受到某些实体的攻击,这类实体可能无法观察到数据包但能成功猜测路径上使用的地址。这些攻击可能会将PMTU降低到使带宽低效的值。 + +终端必须(**MUST**)忽略声称PMTU已减小到低于QUIC允许的最小报文大小的ICMP消息。 + +[RFC1812][RFC4443]协议对ICMP的生成有要求,其引用的数据包应包含尽可能多的原数据包,且不会超过IP版本的最小MTU。引用的数据包实际上可能小一些,或者信息难以理解,如[DPLPMTUD]第1.1节。 + +使用PMTUD的QUIC终端应该(**SHOULD**)验证ICMP消息,以防止在[RFC8201]和[RFC8085]第5.2节指出的数据包注入。此验证应该(**SHOULD**)将ICMP消息负载中的引用数据包或相应的传输层连接的消息相关联(参见[DPLPMTUD]第4.6.1小节)。ICMP消息验证必须(**MUST**)包括匹配的IP地址和UDP端口[RFC8085],并在可能的情况下,包括QUIC活动会话的CID。终端应该(**SHOULD**)忽略所有验证失败的ICMP消息。 + +终端不得(**MUST NOT**)根据ICMP消息增加PMTU,参见[DPLPMTUD]第3章的第6项。在QUIC的丢包检测算法判定引用的数据包确实丢失之前,任何响应ICMP消息减少QUIC最大报文大小的措施都可以(**MAY**)是暂时性的。 + +## 14.3. 报文分组层PMTU发现(Datagram Packetization Layer PMTU Discovery) + +DPLPMTUD[DPLPMTUD]实现依赖于跟踪PMTU探测中携带的QUIC包的丢失或确认。使用PADDING帧的DPLPMTUD的PMTU探测实现“使用填充数据进行探测”,如[DPLPMTUD]第4.1节中所定义。 + +终端应该(**SHOULD**)将BASE_PLPMTU的初始值([DPLPMTUD]第5.1节)设置为与QUIC最小允许的最大报文大小一致。MIN_PLPMTU与BASE_PLPMTU相同。 + +实现DPLPMTUD的QUIC终端为本地和远程IP地址的每种组合维护一个DPLPMTUD最大数据包大小 (MPS)([DPLPMTUD]第4.4节)。这与UDP最大报文大小对应。 + +### 14.3.1. DPLPMTUD和Initial连接(DPLPMTUD and Initial Connectivity) + +从DPLPMTUD的角度来看,QUIC是公认的分组层 (Packetization Layer,PL)。因此,当QUIC连接握手完成时,QUIC发送方可以进入DPLPMTUDBASE状态([DPLPMTUD]第5.2节)。 + +### 14.3.2. 使用DPLPMTUD验证网络路径(Validating the Network Path with DPLPMTUD) + +QUIC是公认的PL,因此,QUIC发送方在SEARCH_COMPLETE状态下不会实现DPLPMTUD的CONFIRMATION_TIMER,参见[DPLPMTUD]第5.2节。 + +### 14.3.3. DPLPMTUD处理ICMP消息(Handling of ICMP Messages by DPLPMTUD) + +使用DPLPMTUD的终端需要在使用PTB信息之前验证任何接收到的ICMP PTB消息,如[DPLPMTUD]第4.6节中所定义。除了UDP端口验证之外,QUIC还可以通过其他PL信息来验证ICMP消息(例如,验证任何收到的ICMP消息的引用数据包中的CID)。 + +如果DPLPMTUD用到这些消息,则第14.2.1小节中描述的处理ICMP消息的注意事项也适用。 + +## 14.4. 发送QUIC(PMTU探测 Sending QUIC PMTU Probes) + +PMTU探测是发送ACK触发包。 + +终端可以将PMTU探测的内容限制为PING和PADDING帧,因为大于当前最大报文大小的数据包更有可能被网络丢弃。因此,PMTU探测中携带的QUIC包丢失不是拥塞的可靠指示,不应该(**SHOULD**)触发拥塞控制反应,参见[DPLPMTUD]第3章的第7项。但是,PMTU探测会消耗拥塞窗口,这可能会延迟应用的后续传输。 + +### 14.4.1. 包含SCID的PMTU探测(PMTU Probes Containing Source Connection ID) + +依赖DCID字段来路由入向QUIC包的终端可能需要在PMTU探测中包含CID,以将任何产生的ICMP消息(第14.2.1小节)路由回正确的终端。然而,只有长包头数据包(第17.2节)包含SCID字段,并且一旦握手完成,长包头数据包不会被对端解密或确认。 + +构建PMTU探测的一种方法是在单个UDP报文中合并(参见第12.2节)具有长包头的数据包(例如握手或0-RTT包(第17.2节))和短包头数据包。如果PMTU探测到达终端,带有长包头的数据包被忽略,但短包头数据包将被确认。如果PMTU探测触发ICMP消息,则ICMP消息将引用该探测的前面部分,如果SCID字段在其中,则可用于路由或验证ICMP消息。 + +> 注意:使用长报文头的目的只是为了保证ICMP报文中引用的报文包含一个SCID字段。这个数据包不需要是一个有效的数据包,即使当前没有使用该类型的数据包,它也可以被发送。 + +# 15. 版本号(Versions) +QUIC版本使用32位无符号整数标识。 + +版本0x00000000保留用于表示版本协商。此版本的规约标识为编号0x00000001。 + +其他版本的QUIC可能与此版本具有不同的属性。在[QUIC-INVARIANTS]中描述了保证在所有版本的协议中保持一致的QUIC属性。 + +QUIC的0x00000001版本使用TLS作为加密握手协议,如[QUIC-TLS]中所述。 + +版本号最高16位置零的版本保留用于未来的IETF协议文件。 + +遵循模式0x?a?a?a?a的版本是保留标识(即所有字节的低四位为1010(二进制)的任何版本号),用于强制执行版本协商。客户端或服务端可以(**MAY**)宣称支持这些保留版本中的任意一个。 + +保留的版本号永远不会表示真正的协议。客户端可以(**MAY**)使用这些版本号之一,寄望服务端启动版本协商;服务端也可以(**MAY**)宣称对这些版本之一的支持,寄望于客户端忽略该值。 + +# 16. 变长整数编码(Variable-Length Integer Encoding) + +QUIC包和帧通常对非负整数值使用变长编码。这种编码确保较小的整数可以使用更少的字节来编码。¶ + +QUIC变长整数编码保留第一个字节的最高两位,其值为n表示编码的整数长度为2的n次方。整数值按网络字节序在剩余位上编码。 + +这意味着整数可以编码为1,2,4,8字节,其值分别不超过6-,14-,30-,62位大小。表4列举了这几种编码属性: + +| 2MSB | Length | Usable Bits | Range | +|:---|:---|:---|:---| +| 00 | 1 | 6 | 0-63 | +| 01 | 2 | 14 | 0-16383 | +| 10 | 4 | 30 | 0-1073741823 | +| 11 | 8 | 62 | 0-4611686018427387903 | + +表4: Summary of Integer Encodings + +在附录Appendix A.1有编码算法和应用示例。 + +除了Frame Type字段外,其余值不需要以所需的最小字节数进行编码,参见第12.4节。 + +版本号(第15章),报头中的包号(第17.1节)和长包头中的CID长度(第17.2节)是整数但不需要使用此编码。 + +# 17. 数据包格式(Packet Formats) + +以下约定所有数值都以网络字节序(即大端)编码,所有字段大小均以位为单位。十六进制表示法用于表示字段的值。 + +## 17.1. 包号编解码(Packet Number Encoding and Decoding) + +包号(以下简称“包号”)是从0到2^62^-1(第12.3节)范围内的整数。当出现在长或短包头中时,其被编码为1到4个字节。通过仅编码包号的最少有效位,可以减少其占用的空间位数。 + +编码后的包号受到加密保护,如[QUIC-TLS]第5.4节。 + +在收到对包号空间的确认之前,数据包内必须(**MUST**)包括完整包号,它不能被截断,如下所述。 + +在收到某个包号空间的确认ACK后,发送方必须(**MUST**)确定一个包号长度,该包号长度需要能够表示最大已确认包号与当前包号两者差值的两倍以上范围内的数值。对端收到以后应能正确解码该包号,除非其在传输过程中有延迟,导致它在很多更大编号的数据包之后到达。终端应该(**SHOULD**)使用足够大的包号进行编码,使得即使数据包在后发数据包之后到达,也可以恢复包号。 + +因此,包号编码的大小应至少比包括新包在内的连续未确认包的数量的以2为基的对数多一位。Appendix A.2中有伪代码和包号编码示例。 + +在接收方处需要先移除数据包保护,再依据其有效位的数量、这些位的值以及在成功验证的数据包中接收到的最大包号来重建完整包号。恢复完整的包号才算是成功完成数据包保护移除。 + +一旦移除了包头保护,解码之后的包号为最接近预期包号的值。预期包号是接收到的最高包号加一。Appendix A.3中有伪代码和包号解码的示例。 + +## 17.2. 长包头数据包(Long Header Packets) + +``` + Long Header Packet { + Header Form (1) = 1, + Fixed Bit (1) = 1, + Long Packet Type (2), + Type-Specific Bits (4), + Version (32), + Destination Connection ID Length (8), + Destination Connection ID (0..160), + Source Connection ID Length (8), + Source Connection ID (0..160), + Type-Specific Payload (..), + } +``` +图13: Long Header Packet Format + +长包头数据包是在确立1-RTT密钥之前收发的数据包。一旦1-RTT密钥可用,发送方就会切换到使用短包头发送数据包(第17.3节)。长包头形式可使得特殊数据包——例如Version Negotiation包——以这种统一的固定长度数据包的格式来表示。使用长包头的数据包包含以下字段: + +* Header Form:头类型,对于长包头,字节0(第一个字节)的最高有效位(0x80)设置为1。 +* Fixed Bit:固定位,字节0的下一位(0x40)一般设置为1,设为0的话,除了Version Negotiation包之外,其他情况都不是有效数据包,必须(**MUST**)丢弃。该位的值为1允许QUIC与其他协议复用,参见[RFC7983]。 +* Long Packet Type:长报文类型,字节0的下两位(掩码为0x30)表示数据包类型。数据包类型在表5中列出。 +* Type-Specific Bits:类型相关位,字节0的低四位(掩码为0x0f)的语义由数据包类型决定。 +* Version :版本号,QUIC版本是字节0之后的32位字段。此字段指示正在使用的QUIC版本并确定如何解释其余协议字段。 +* Destination Connection ID Length:DCID长度,Version之后的一个字节表示随后的DCID字段的字节长度。此长度编码为8位无符号整数。在QUIC版本1中,该值不得(**MUST NOT**)超过20个字节。收到值大于20的版本1长包头的终端必须(**MUST**)丢弃该数据包,但为了正确构造Version Negotiation包,服务端也应该(**SHOULD**)支持其他版本的QUIC协议有更长的CID。 +* Destination Connection ID:目的连接ID(DCID),Destination Connection ID紧跟Destination Connection ID Length字段。第7.2节更详细地描述了该字段的使用方式。 +* Source Connection ID Length:SCID长度,DCID之后的一个字节表示其后的SCID字段的字节长度。此长度编码为8位无符号整数。在QUIC版本1中,该值不得(**MUST NOT**)超过20个字节。收到值大于20的版本1长包头的终端必须(**MUST**)丢弃该数据包,但为了正确构造Version Negotiation包,服务端也应该(**SHOULD**)支持其他版本的QUIC协议有更长的CID。 +* Source Connection ID:源连接ID(SCID),SCID字段紧跟Source Connection ID Length字段。第7.2节更详细地描述了该字段的使用方式。 +* Type-Specific Payload:类型相关负载,数据包的其余部分(如果有)与Long Packet Type指定的类型有关。 + +在此QUIC版本中,定义了以下带有长包头的数据包类型: + +| Type | Name | Section | +| :--- | :--- | :--- | +|0x0| Initial | Section 17.2.2 | +|0x1| 0-RTT | Section 17.2.3 | +|0x2| Handshake | Section 17.2.4 | +|0x3| Retry | Section 17.2.5 | + +表5: Long Header Packet Types + +长包头数据包的长报文类型、DCID和SCID长度、DCID和SCID字段以及版本字段与版本无关。第一个字节中的其他字段是特定于版本的。关于来自不同QUIC版本的数据包如何解释,请参阅[QUIC-INVARIANTS]。 + +部分字段和负载的解释特定于版本和数据包类型。关于此版本的特定于类型的语义在以下其他章节中会有描述,下面介绍此QUIC版本中的几个长包头数据包中都包含的附加字段: + +* Reserved Bits:保留位,多个类型的长包头数据包中字节0有两个保留位(掩码为0x0c),这些位都使用包头保护策略来保护,参见[QUIC-TLS]第5.4节。保护之前其值必须(**MUST**)设置为0。终端在移除数据包保护和包头保护后,接收到的这些位具有非零值的数据包必须(**MUST**)视为PROTOCOL_VIOLATION类型的连接错误。仅在去除包头保护后丢弃此类数据包会使终端暴露于攻击,参见[QUIC-TLS]第9.5节。 + +* Packet Number Length:包号长度,在包含Packet Number字段的数据包类型中,字节0的最低两个有效位(掩码为0x03)表示Packet Number字段的长度,编码为无符号的两位整数,比Packet Number字段的长度(以字节为单位)小1,即Packet Number字段的长度是该字段的值加一。这些位使用包头保护来保护,参见[QUIC-TLS]第5.4节。 + +* Length:长度,这是数据包剩余部分的长度(即Packet Number和Packet Payload字段),以字节为单位,编码为变长整数(第16章)。 + +* Packet Number:包号,该字段的长度为1到4个字节。包号采用包头保护,参见[QUIC-TLS]第5.4节。 Packet Number字段的长度编码在字节0的Packet Number Length位中,如上所述。 + +* Packet Payload:包负载,这是数据包的有效载荷——包含一系列帧——使用数据包保护策略进行保护。 + +### 17.2.1. 版本协商包(Version Negotiation Packet) + +Version Negotiation包本质上不是特定于版本的。客户端收到后,将Version字段值为0的数据包识别为Version Negotiation包。 + +Version Negotiation包仅由服务端发送,是在收到不支持版本的客户端数据包时回的响应。 + +Version Negotiation包的格式是: + +``` + Version Negotiation Packet { + Header Form (1) = 1, + Unused (7), + Version (32) = 0, + Destination Connection ID Length (8), + Destination Connection ID (0..2040), + Source Connection ID Length (8), + Source Connection ID (0..2040), + Supported Version (32) ..., + } +``` +图14: Version Negotiation Packet + +服务端可设置Unused字段为任意值,客户端必须(**MUST**)忽略该字段的值。在QUIC可能与其他协议复用的情况下(参见[RFC7983]),服务端应该(**SHOULD**)将此字段的最高有效位(0x40)设置为1,以便Version Negotiation包看起来具有如上所述的固定位(Fixed Bit)字段。请注意,其他版本的QUIC可能不会遵循类似的建议。 + +Version Negotiation包的版本字段必须(**MUST**)设置为0x00000000。 + +服务端必须(**MUST**)将DCID字段设置为其接收到的数据包的SCID字段的值。SCID的值必须(**MUST**)从接收到的数据包的DCID的值复制而来,它最初是由客户端随机选择的。回显两个CID为客户端提供了一些证明,即服务端收到了数据包,并且Version Negotiation包不是由没有收到Initial包的实体生成的。 + +未来版本的QUIC可能对CID的长度有不同的要求。特别是,CID可能具有较小的最小长度或较大的最大长度。因此,CID的版本相关规则不得(**MUST NOT**)影响关于是否发送Version Negotiation包的决定。 + +Version Negotiation包的其余部分是服务端支持的版本列表,每个版本号都是32位的。 + +Version Negotiation包不需要确认。它仅在响应指示不支持版本的数据包时发送,参见第5.2.2小节。 + +Version Negotiation包不包含包号和包长字段,不过在长包头格式的其他数据包中是存在的。Version Negotiation包会消耗整个UDP报文。 + +服务端不得(**MUST NOT**)发送多个Version Negotiation包以响应单个UDP报文。 + +有关版本协商过程的描述,请参见第6章。 + +### 17.2.2. Initial包(Initial Packet) + +Initial包使用类型值为0x00的长包头。它携带客户端和服务端发送的第一个CRYPTO帧以执行密钥交换,双向都可携带ACK帧。 +``` +Initial Packet { + Header Form (1) = 1, + Fixed Bit (1) = 1, + Long Packet Type (2) = 0, + Reserved Bits (2), + Packet Number Length (2), + Version (32), + Destination Connection ID Length (8), + Destination Connection ID (0..160), + Source Connection ID Length (8), + Source Connection ID (0..160), + Token Length (i), + Token (..), + Length (i), + Packet Number (8..32), + Packet Payload (8..), +} +``` +图15: Initial Packet + +Initial包包含一个长包头,内含长度或包号等字段,参见第17.2节。第一个字节包含保留位和包号长度位,也参见第17.2节。在SCID和Length字段之间,有两个额外的字段是Initial包特有的: + +* Token Length:令牌长度,变长整数,指定Token字段的长度,以字节为单位。如果不存在Token,则此值为0。服务端发送的Initial包必须(**MUST**)将Token Length字段置为0。客户端收到具有非零Token Length字段的Initial包,必须(**MUST**)要么丢弃该数据包,要么回以类型为PROTOCOL_VIOLATION的连接错误。 + +* Token :令牌,先前在Retry包或NEW_TOKEN帧中提供的令牌值,参见第8.1节。 + +为了防止被版本未知的中间设备篡改,Initial包使用与连接和版本相关的密钥(Initial密钥)保护,如[QUIC-TLS]中所述。这种方式对可以观察数据包的攻击者来说,不能提供机密性或完整性保护,但可以防止无法观察到数据包的攻击者伪装Initial包。 + +客户端和服务端在发送包含初始加密握手消息的任何数据包时,都采用Initial包类型。这规则适用于需要创建包含初始加密消息的新数据包的所有场景,例如在收到Retry包后须发送数据包时,参见第17.2.5小节。 + +服务端发送它的第一个Initial包以响应客户端Initial包。服务端可以(**MAY**)发送多个Initial包。加密密钥交换可能需要多次往返或重传。 + +Initial包的负载包括一个或多个包含了加密握手消息的CRYPTO帧或ACK帧,或者两者都有。0x1c类型的PING、PADDING和CONNECTION_CLOSE帧也是允许的。接收到包含其他帧的Initial包的终端可以将其视为虚假数据包或连接错误。 + +客户端发送的第一个数据包总是包含一个CRYPTO帧,该帧包含第一个加密握手消息的起始部分或全部。第一个CRYPTO帧总是从偏移量0开始,参见第7章。 + +请注意,如果服务端发送TLS HelloRetryRequest(参见[QUIC-TLS]第4.7节),则客户端将发送另一系列Initial包。这些Initial包将继续加密握手,并将包含CRYPTO帧,其起始偏移量与Initial包的第一次发送的CRYPTO帧的大小相匹配。 + +#### 17.2.2.1. 丢弃Initial包(Abandoning Initial Packets) + +客户端在发送第一个Handshake包后停止发送和处理Initial包。服务端在收到第一个Handshake包后停止发送和处理Initial包。尽管数据包可能仍在传输中或等待确认,但在此之后无需再交换Initial包。Initial包保护密钥与任何丢包恢复和拥塞控制状态(参见[QUIC-RECOVERY]第6.4节)一起被丢弃(参见[QUIC-TLS]第4.9.1小节)。 + +当Initial密钥被丢弃时,CRYPTO帧中的任何数据都会被丢弃,并且不再重传。 + +### 17.2.3. 0-RTT包(0-RTT) + +0-RTT包使用类型值为0x01的长包头,后跟Length或Packet Number字段,参见第17.2节。第一个字节包含保留位和包号长度位,参见第17.2节。作为首次传输的一部分,0-RTT包用于将early数据从客户端发往服务端,在Handshake完成之前。作为TLS握手的一部分,服务端可以接受或拒绝此数据。 + +有关0-RTT数据及其局限性的讨论,请参见[TLS13]第2.3节。 + +``` +0-RTT Packet { + Header Form (1) = 1, + Fixed Bit (1) = 1, + Long Packet Type (2) = 1, + Reserved Bits (2), + Packet Number Length (2), + Version (32), + Destination Connection ID Length (8), + Destination Connection ID (0..160), + Source Connection ID Length (8), + Source Connection ID (0..160), + Length (i), + Packet Number (8..32), + Packet Payload (8..), +} +``` +图16: 0-RTT Packet + +0-RTT保护数据包与1-RTT保护数据包使用相同的包号空间。 + +客户端收到Retry报文,很可能是0-RTT报文丢失或被服务端丢弃。客户端应该(**SHOULD**)在发送新的Initial包后尝试重新发送0-RTT包中的数据。新数据包必须(**MUST**)使用新的包号,如第17.2.5.3小节所述,重用包号可能会损害数据包保护。 + +如[QUIC-TLS]第4.1.1小节中所定义,客户端仅在Handshake完成后才接收其0-RTT包的确认。 + +一旦开始处理来自服务端的1-RTT包,客户端不得(**MUST NOT**)发送0-RTT包。这意味着0-RTT包不能包含对来自1-RTT包的帧的任何响应。例如,客户端不能在0-RTT包中发送ACK帧,因为ACK帧只能确认1-RTT包。1-RTT包的确认必须(**MUST**)在1-RTT包中携带。 + +服务端应该(**SHOULD**)将违反之前保存的(传输参数)限制的情况(参见第7.4.1小节)视为适当类型的连接错误(例如,超出流控限制的FLOW_CONTROL_ERROR)。 + +### 17.2.4. Handshake握手包(Handshake Packet) + +Handshake包使用类型值为0x02的长包头,后跟Length或Packet Number字段,参见第17.2节。第一个字节包含保留位和包号长度位,参见第17.2节。它用于携带来自服务端和客户端的加密握手消息和确认。 +``` +Handshake Packet { + Header Form (1) = 1, + Fixed Bit (1) = 1, + Long Packet Type (2) = 2, + Reserved Bits (2), + Packet Number Length (2), + Version (32), + Destination Connection ID Length (8), + Destination Connection ID (0..160), + Source Connection ID Length (8), + Source Connection ID (0..160), + Length (i), + Packet Number (8..32), + Packet Payload (8..), +} +``` +图17: Handshake Protected Packet + +一旦客户端收到来自服务端的Handshake包,它就会使用Handshake包向服务端发送后续的加密握手消息和确认。 + +Handshake包中的DCID字段包含一个由数据包接收方选择的CID,SCID是数据包的发送方希望对端使用的CID,参见第17.2节。 + +Handshake包有自己的包号空间,因此服务端发送的第一个Handshake包的包号为0。 + +此数据包的负载包含CRYPTO帧,也可能包含PING、PADDING或ACK帧。Handshake包可能(**MAY**)包含0x1c类型的CONNECTION_CLOSE帧。终端必须(**MUST**)将接收到其他帧的Handshake包视为PROTOCOL_VIOLATION类型的连接错误。 + +与Initial包(参见第17.2.2.1小节)一样,当Handshake保护密钥被丢弃时,其中的CRYPTO帧中的数据将被丢弃,并且不再重传。 + +### 17.2.5. Retry包(Retry Packet) + +如图18所示,Retry包使用类型值为0x03的长包头。它携带由服务端生成的地址验证令牌。仅由希望进行重试的服务端使用,参见第8.1节。 +``` +Retry Packet { + Header Form (1) = 1, + Fixed Bit (1) = 1, + Long Packet Type (2) = 3, + Unused (4), + Version (32), + Destination Connection ID Length (8), + Destination Connection ID (0..160), + Source Connection ID Length (8), + Source Connection ID (0..160), + Retry Token (..), + Retry Integrity Tag (128), +} +``` +图18: Retry Packet + +Retry包不包含任何受保护的字段。 Unused字段中的值由服务端设置为任意值,客户端必须(**MUST**)忽略它。除了来自长包头的字段外,它还包含以下附加字段: + +* Retry Token:重试令牌,服务端可用于验证客户端地址的不透明令牌。 + +* Retry Integrity Tag:重试完整性标签,在[QUIC-TLS]第5.8节("Retry Packet Integrity") 中定义。 + +#### 17.2.5.1. 发送Retry包(Sending a Retry Packet) +服务端将客户端Initial包的SCID填入Retry包的DCID字段。 + +服务端在SCID字段中包含其选择的CID。该值不得(**MUST NOT**)等于客户端发送的数据包的DCID字段。客户端必须(**MUST**)丢弃包含与Initial包的DCID字段相同的SCID字段的Retry包。客户端必须(**MUST**)在它发送的后续数据包的DCID字段中使用Retry包的SCID字段中的值。 + +服务端可以(**MAY**)发送Retry包以响应Initial和0-RTT包。服务端可以丢弃或缓冲它收到的0-RTT包。服务端可以在接收Initial或0-RTT包时发送多个Retry包。服务端不得(**MUST NOT**)发送多个Retry包以响应单个UDP报文。 + +#### 17.2.5.2. 处理Retry包(Handling a Retry Packet) +对于每次连接尝试,客户端必须(**MUST**)最多接受和处理一个Retry包。在客户端接收并处理来自服务端的Initial或Retry包后,它必须(**MUST**)丢弃它接收到的任何后续Retry包。 + +客户端必须(**MUST**)丢弃具有无法验证的重试完整性标签的Retry包,参见[QUIC-TLS]第5.8节。这会削弱攻击者注入Retry包的能力并防止重Retry包意外损坏。客户端必须(**MUST**)丢弃带有零长度Retry Token字段的Retry包。 + +客户端使用包含提供的重试令牌的Initial包响应Retry包以继续建立连接。 + +客户端将此Initial包的DCID字段设置为Retry包中SCID字段的值。更改DCID字段还会导致更改用于保护Initial包的密钥。还需要将Token字段设置为Retry包中提供的令牌。客户端不得(**MUST NOT**)更改SCID,因为服务端会将CID作为其令牌验证逻辑的一部分,参见第8.1.4小节。 + +Retry包不包含包号,客户端无法明确确认。 + +#### 17.2.5.3. 重试后继续握手(Continuing a Handshake after Retry) +来自客户端的后续Initial包包含来自Retry包的CID和令牌值。客户端将SCID字段从Retry包复制到DCID字段并使用该值,直到收到具有更新值的Initial包,参见第7.2节。 Token字段的值被复制到所有后续的Initial包中,参见第8.1.2小节。 + +除了更新DCID和Token字段外,客户端发送的Initial包与第一个Initial包受到相同的限制。客户端必须(**MUST**)使用它包含在此数据包中的相同加密握手消息。服务端可以(**MAY**)将包含不同加密握手消息的数据包视为连接错误或丢弃它。请注意,包含Token字段会减少加密握手消息的可用空间,这可能导致客户端需要发送多个Initial包。 + +客户端可以(**MAY**)通过向服务端提供的CID发送0-RTT包,在收到Retry包后尝试0-RTT。 + +在处理Retry包后,客户端不得(**MUST NOT**)在任何包号空间重置包号。特别是,0-RTT包包含加密信息,这些信息很可能会在收到Retry包时重新传输。用于保护这些新0-RTT包的密钥不会因响应Retry包而改变。但是,这些数据包中发送的数据可能与之前发送的数据不同。使用相同的包号发送这些新数据包可能会损害这些数据包的数据包保护,因为相同的密钥和随机数可用于保护不同的内容。如果服务端检测到客户端重置了包号,则它可以(**MAY**)中止连接。 + +在客户端和服务端之间交换的Initial和Retry包中使用的CID需要复制到传输参数中,并按照第7.3节的描述进行验证。 + +## 17.3. 短包头数据包(Short Header Packets) + +此QUIC版本定义了使用短数据包头的单个数据包类型。 + +### 17.3.1. 1-RTT包(1-RTT Packet) + +1-RTT包使用短包头。它在版本协商和1-RTT秘钥协商后使用。 + +``` +1-RTT Packet { + Header Form (1) = 0, + Fixed Bit (1) = 1, + Spin Bit (1), + Reserved Bits (2), + Key Phase (1), + Packet Number Length (2), + Destination Connection ID (0..160), + Packet Number (8..32), + Packet Payload (8..), +} +``` +图19: 1-RTT Packet + +1-RTT包包含以下字段: + +* Header Form:报头格式,对于短包头,字节0的最高有效位 (掩码为0x80) 设置为0。 + +* Fixed Bit:固定位,字节0的下一位 (掩码为0x40) 设置为1。该位为0的数据包在此版本中不是有效数据包,必须(**MUST**)丢弃。该位的值为1允许QUIC与其他协议复用,请参见[RFC7983]。 + +* Spin Bit:自旋位,字节0的第三个最高有效位 (掩码为0x20) 是延迟自旋位,按第17.4节所述设置。 + +* Reserved Bits:保留位,字节0的下两位(掩码为0x18)是保留位。这些位使用包头保护策略来保护,参见[QUIC-TLS]第5.4节。在保护之前其值必须(**MUST**)设置为0。收到数据包且在移除包和包头保护后,这些位为非零值的话,终端必须(**MUST**)将之视为PROTOCOL_VIOLATION类型的连接错误。仅在去除包头保护后丢弃此类数据包会使终端暴露于攻击,参见[QUIC-TLS].的第9.5节。 + +* Key Phase:秘钥阶段,字节0的下一位 (掩码为0x04) 表示密钥时段,它允许数据包的接收方识别用于保护数据包的密钥。详情参阅[QUIC-TLS]。该位采用包头保护,参见[QUIC-TLS]第5.4节。 + +* Packet Number Length:包号长度,字节0的最低有效两位(掩码为0x03)表示Packet Number字段的长度,编码为无符号的两位整数,比Packet Number字段的长度(以字节为单位)小1,即Packet Number字段的长度是该字段的值加一。这些位采用包头保护,参见[QUIC-TLS]第5.4节。 + +* Destination Connection ID:目的连接ID(DCID),DCID是由数据包的预期接收方选择的CID。更多详细信息,请参阅第5.1节。 + +* Packet Number:包号,长度为1到4个字节。Packet Number字段采用包头保护,参见[QUIC-TLS]第5.4节。Packet Number字段的长度在Packet Number Length字段中编码。有关详细信息,请参阅第17.1节。 + +* Packet Payload:数据包负载,1-RTT包中始终包含受1-RTT保护的有效载荷。 + +短包头包的报头格式位和DCID字段与版本无关。其余字段与所选的QUIC版本有关。来自不同QUIC版本的数据包如何解释详细信息,请参阅[QUIC-INVARIANTS]。 + +## 17.4. 延迟自旋位(Latency Spin Bit) + +为1-RTT包定义的延迟自旋位(第17.3.1小节),可以启动整个连接期间从网络路径上的观察点的被动延迟监控。服务端反射收到的旋转值,而客户端在一个RTT后“翻转”它。on-path观察者可以测量两个自旋位翻转事件之间的时间,以估计连接的终端到端RTT。 + +自旋位仅出现在1-RTT包中,因为可以通过观察握手来测量连接的Initial RTT。因此,在版本协商和连接建立完成后,自旋位即可用。[QUIC-MANAGEABILITY]中进一步讨论了在路径上进行测量和延迟自旋位的使用方式。 + +自旋位是此版本QUIC的可选(**OPTIONAL**)功能。不支持该特性的终端必须(**MUST**)禁用它,如下所述。 + +每个终端单方面决定是否启用或禁用连接的自旋位。实现必须(**MUST**)允许客户端和服务端的上层应用全局禁用自旋位或只在每个连接的基础上禁用。即使上层应用没有禁用自旋位,终端也必须(**MUST**)随机选择每16个网络路径中的至少一个,或每16个CID中的一个,禁用自旋位的使用,以确保QUIC连接不启用自旋位在网络上能经常观察到。由于每个终端单方面地禁用自旋位,这可以保证有大约八分之一的网络路径上禁用自旋位信号。 + +当自旋位被禁用时,终端可以(**MAY**)将自旋位设置为任意值并且必须(**MUST**)忽略任何入向值。建议(**RECOMMENDED**)终端将自旋位设置为随机值,可为每个数据包独立选择或为每个CID独立选择。 + +如果在连接上启用了自旋位,终端会为每条网络路径维护一个自旋值,并在该路径上发送1-RTT包时将包头中的自旋位设置为当前存储的值。每条网络路径的终端中的自旋值初始化为0。每个终端还记住从其对端看到的每条路径上的最高包号。 + +当服务端收到一个1-RTT包时,如果服务端从给定网络路径上的看到的客户端的最高包号递增,它将该路径的自旋值设置为收到的数据包中的自旋位。 + +当客户端收到一个1-RTT包时,如果客户端从给定网络路径上的看到的服务端的最高包号递增,它将该路径的自旋值设置为接收到的数据包中自旋位的翻转值。 + +当变更该网络路径上使用的CID时,终端会将该网络路径的自旋值重置为0。 + +# 18. 传输参数编码(Transport Parameter Encoding) + +[QUIC-TLS]中定义的quic_transport_parameters扩展的extension_data字段包含QUIC传输参数。它们被编码为一系列传输参数,如图20所示: +``` +Transport Parameters { + Transport Parameter (..) ..., +} +``` +图20: Sequence of Transport Parameters + +每个传输参数都被编码为一个(标识符、长度、值)三元组,如图21所示: + +``` +Transport Parameter { + Transport Parameter ID (i), + Transport Parameter Length (i), + Transport Parameter Value (..), +} +``` +图21: Transport Parameter Encoding + +Transport Parameter Length字段是以字节为单位的Transport Parameter Value字段的长度。 + +QUIC将传输参数编码为字节流,然后在加密握手时交互。 + +## 18.1. 保留传输参数(Reserved Transport Parameters) +保留传输参数的ID是具有 31 * N + 27 形式的标识符,其中N为整数,其引入的目的是为了执行忽略未知传输参数的要求。这些传输参数没有语义,可以携带任意值。 + +## 18.2. 传输参数定义(Transport Parameter Definitions) + +本节详细介绍本文中定义的传输参数。 + +此处列出的许多传输参数都具有整数值。标识为整数的传输参数使用变长整数编码,请参阅第16章。除非另有说明,否则如果传输参数不存在,则其默认值为0。 + +传输参数定义如下: +* original_destination_connection_id (0x00):该参数是客户端发送的第一个Initial包中的DCID字段的值,见第7.3节。此传输参数仅由服务端发送。 + +* max_idle_timeout (0x01):最大空闲超时时间是一个以毫秒为单位的值,它被编码为一个整数,见(第10.1节)。当两端都忽略此传输参数或指定值为0时,空闲超时被禁用。 + +* stateless_reset_token (0x02):无状态重置令牌(Stateless Reset Token)用于验证无状态重置,参见第10.3节。该参数是一个16字节的字符串。该传输参数不得(**MUST NOT**)由客户端发送,只可以(**MAY**)由服务端发送。不发送此传输参数的服务端不能对握手期间协商的CID使用无状态重置(第10.3节)。 + +* max_udp_payload_size (0x03):最大UDP负载大小,该参数是一个整数值,用于限制终端愿意接收的UDP负载大小。接收方不太可能处理负载大于此限额的UDP报文。 +此参数的默认值是允许的最大UDP负载65527,低于1200的值无效。 +此限额确实以与路径MTU相同的方式作为对数据报文大小的附加约束,但它是终端的属性而不是路径,参见第14章。预期这是终端专用于保存入向数据包的空间大小。 + +* initial_max_data (0x04):初始最大数据大小,该参数是一个整数值,指示可以在连接上发送的最大数据量(字节数)的初始值。这相当于在完成握手后立即为连接发送一个MAX_DATA(参见第19.9节)。 + +* initial_max_stream_data_bidi_local (0x05):此参数是一个整数值,指定本地发起的双向流的初始流控限额。此限额适用于由发送传输参数的终端打开的新创建的双向流。在客户端传输参数中,这适用于标识符的最低有效位设置为0x00的流,在服务端传输参数中,这适用于最低有效位设置为0x01的流。 + +* initial_max_stream_data_bidi_remote (0x06):此参数是一个整数值,指定对端发起的双向流的初始流控限额。此限额适用于由接收传输参数的终端打开的新创建的双向流。在客户端传输参数中,这适用于标识符的最低有效位设置为0x01的流,在服务端传输参数中,这适用于最低有效两位设置为0x00的流。 + +* initial_max_stream_data_uni (0x07):此参数是一个整数值,指定单向流的初始流控限额。此限额适用于由接收传输参数的终端打开的新创建的单向流。在客户端传输参数中,这适用于标识符的最低有效位设置为0x03的流,在服务端传输参数中,这适用于最低有效两位设置为0x02的流。 + +* initial_max_streams_bidi (0x08):初始最大双向流个数,此参数是一个整数值,指示接收此传输参数的终端允许发起的初始最大双向流个数。如果此参数不存在或为零,则在发送MAX_STREAMS帧之前,对端无法打开双向流。设置这个参数相当于发送一个具有相同值的对应类型的MAX_STREAMS(第19.11节)。 + +* initial_max_streams_uni (0x09):初始最大单向流个数,此参数是一个整数值,指示接收此传输参数的终端允许发起的初始最大单向流个数。如果此参数不存在或为零,则在发送MAX_STREAMS帧之前,对端无法打开单向流。设置这个参数相当于发送一个具有相同值的对应类型的MAX_STREAMS(第19.11节)。 + +* ack_delay_exponent (0x0a):确认延迟指数,此参数是一个整数值,指示用于解码ACK帧中的ACK延迟字段的幂(第19.3节)。如果此值不存在,则假定默认值为3(表示乘数为8),超过20的值无效。 + +* max_ack_delay (0x0b):最大确认延迟,此参数是一个整数值,表示终端将延迟发送确认的最长时间(以毫秒为单位)。这个值应该(**SHOULD**)包括接收端告警触发的预期延迟。例如,如果接收端将定时器设置为5毫秒,而告警通常最多延迟1毫秒,则它应该(**SHOULD**)发送6毫秒的max_ack_delay。如果此值不存在,则假定默认值为25毫秒,2^14^或更大的值无效。 + +* disable_active_migration (0x0c):如果终端不支持在握手期间正在使用的地址上的主动连接迁移(第9章),则需要设置此参数,禁用主动迁移。接收此传输参数的终端在握手期间在发包给对端时不得(**MUST NOT**)使用新的本地地址。在客户端对preferred_address传输参数进行操作后,可以解禁连接迁移。此参数是零长度值。 + +* preferred_address(0x0d):服务端的首选地址,用于在握手结束时影响服务端地址的更改,如第9.6节所述。此传输参数仅由服务端发送。服务端可以(**MAY**)选择只发送一个地址族的首选地址,方法是为另一个地址族发送全零地址和端口(0.0.0.0:0或[::]:0)。IP地址以网络字节序编码。 +preferred_address传输参数包含IPv4和IPv6的地址和端口。四字节IPv4 Address字段后跟相关的两字节IPv4 Port字段。后面跟着一个16字节的IPv6 Address字段和两字节的IPv6 Port字段。在地址和端口对之后,Connection ID Length字段指示了随后的CID字段的长度。最后,一个16字节的Stateless Reset Token字段包括与CID关联的无状态重置令牌。此传输参数的格式如下图22所示。 +CID字段和Stateless Reset Token字段包含一个序列号为1的替代CID,参见第5.1.1小节。将这些值与preferred_address一起发送可确保在客户端启动到preferred_address的迁移时至少有一个未使用的活动CID。 +preferred_address的CID和Stateless Reset Token字段在语法和语义上与NEW_CONNECTION_ID帧(第19.15节)的相应字段相同。选择零长度CID的服务端不得(**MUST NOT**)提供首选地址。类似地,服务端不得(**MUST NOT**)在此传输参数中包含零长度的CID。客户端必须(**MUST**)将违反这些规定的行为视为TRANSPORT_PARAMETER_ERROR类型的连接错误。 +``` +Preferred Address { + IPv4 Address (32), + IPv4 Port (16), + IPv6 Address (128), + IPv6 Port (16), + Connection ID Length (8), + Connection ID (..), + Stateless Reset Token (128), +} +``` +图22: Preferred Address Format + +* active_connection_id_limit (0x0e):该参数是一个整数值,指示终端愿意存储的来自对端的最大CID个数。该值包括在握手期间收到的CID、在preferred_address传输参数中收到的CID以及在NEW_CONNECTION_ID帧中收到的CID。 active_connection_id_limit参数的值必须(**MUST**)至少为2。收到小于2的值的终端必须(**MUST**)关闭连接,错误类型为TRANSPORT_PARAMETER_ERROR。如果此传输参数不存在,则假定默认值为2。如果终端发出零长度CID,其后永远不会发送NEW_CONNECTION_ID帧,因此会忽略从其对端收到的active_connection_id_limit值。 + +* initial_source_connection_id (0x0f):这是终端在连接上发送的第一个Initial包的SCID字段中填写的值,参见第7.3节。 + +* retry_source_connection_id (0x10):这是服务端在Retry包的SCID字段填写的值,参见第7.3节。此传输参数仅由服务端发送。 + +如果初始流控限额(initial_max_stream_data_bidi_local、initial_max_stream_data_bidi_remote或initial_max_stream_data_uni)传输参数存在,则其等效于在打开后立即在相应类型的每个流上发送MAX_STREAM_DATA帧(第19.10节)。如果传输参数不存在,则该类型的流以0的流控限额开始。 + +客户端不得(**MUST NOT**)包含任何仅服务端适用的传输参数:original_destination_connection_id、preferred_address、retry_source_connection_id或stateless_reset_token。服务端收到上述传输参数,必须(**MUST**)将其视为TRANSPORT_PARAMETER_ERROR类型的连接错误。 + +# 19. 帧类型和格式(Frame Types and Formats) +如第12.4节所述,数据包包含一个或多个帧。本节描述核心QUIC帧类型的格式和语义。 + +## 19.1. PADDING帧(PADDING Frames) +PADDING帧(Type=0x00)没有其他语义值。PADDING帧可用于增加数据包的大小。PADDING帧可用于将Initial包填充到所需的最小大小或为受保护数据包提供针对流量分析的保护。 + +PADDING帧的格式如图23所示,这表明PADDING帧没有内容。即PADDING帧只由将帧标识为PADDING帧的单个字节组成。 + +``` +PADDING Frame { + Type (i) = 0x00, +} +``` +图23: PADDING Frame Format + +## 19.2. PING帧(PING Frames) +终端可以使用PING帧(Type=0x01)来验证其对端是否仍然存在或检查对端的可达性。 + +PING帧的格式如图24所示,这表明PING帧没有内容。 + +``` +PING Frame { + Type (i) = 0x01, +} +``` +图24: PING Frame Format + +PING帧的接收方只需要确认包含该帧的数据包。 + +当应用或应用层协议希望防止连接超时时,PING帧可用于保持连接处于活动状态,参见第10.1.2小节。 + +## 19.3. ACK帧(ACK Frames) +接收方通过发送ACK帧(Type为0x02和0x03)告知发送方他们已接收和处理数的据包。ACK帧包含一个或多个ACK Range。ACK Range标识已确认的数据包。如果帧类型为0x03,则ACK帧还包含直到此时为止,在连接上接收到的有ECN标记的QUIC包的累积计数。QUIC实现必须(**MUST**)正确处理这两种类型,并且,如果启用了ECN,终端应该(**SHOULD**)使用ECN部分中的信息来管理他们的拥塞状态。 + +QUIC确认是不可撤销的。一旦确认,数据包将保持确认状态,甚至不会出现在未来的ACK帧中。这与TCP选择性确认 (SACK)[RFC2018]的违例不同。 + +来自不同包号空间的数据包可以使用相同的包号。对数据包的确认需要指明包号和包号空间。这是通过约定承载ACK帧的数据包,只能与被确认的数据包具有相同包号空间来实现的。 + +Version Negotiation包和Retry包不需要确认,因为它们不含包号。这些数据包不依赖于ACK帧,而是由客户端发送的下一个Initial包隐式确认。 + +ACK帧的格式如图25所示。 + +``` +ACK Frame { + Type (i) = 0x02..0x03, + Largest Acknowledged (i), + ACK Delay (i), + ACK Range Count (i), + First ACK Range (i), + ACK Range (..) ..., + [ECN Counts (..)], +} +``` +图25: ACK Frame Format + +ACK帧包含以下字段: +* Largest Acknowledged:最大确认包号,变长整数,表示对端确认的最大数据包号,这通常是对端在生成ACK帧之前收到的最大包号。与QUIC长或短包头中的包号不同,ACK帧中的值不会被截断。 + +* ACK Delay:确认延迟,变长整数,确认延迟以微秒为单位,参见第13.2.5小节。通过将字段中的值乘以2^ack_delay_exponent^来解码,其中ack_delay_exponent是对端约定的传输参数,参见第18.2节。与简单地将延迟表示为整数相比,这种编码允许在相同字节数内使用更大范围的值,但代价是分辨率降低。 + +* ACK Range Count:ACK Range个数,变长整数,指定帧中ACK Range字段的个数。 +* First ACK Range:第一个ACK Range,变长整数,指示在最大确认包号之前收到的连续数据包的个数。也就是说,该范围确认的最小数据包号等于Largest Acknowledged - First ACK Range。 + +* ACK Range:确认范围,包含额外的未确认(Gap) 数据包个数和确认 (ACK Range)数据包个数,两者交替出现,参见第19.3.1小节。 + +* ECN Counts:ECN计数,三项ECN计数,参见第19.3.2小节。 + +### 19.3.1. 确认范围(ACK Range) +每个ACK Range由交替的Gap和ACK Range Length值组成,按包号降序排列。ACK Range可以重复。Gap和ACK Range Length值的总个数由ACK Range Count字段决定,两者分开累加计入ACK Range Count。 + +ACK Range的结构如图26所示。 +``` +ACK Range { + Gap (i), + ACK Range Length (i), +} +``` +图26: ACK Ranges + +形成每个ACK Range的字段是: + +* Gap:间隔,变长整数,表示在前面Range中最小确认包之前连续未确认的数据包的数量,编码值比实际个数小1。 + +* ACK Range Length:ACK范围长度,变长整数,表示在前面Gap中最小未确认包之前连续确认的数据包的数量。 + +Gap和ACK Range Length值使用相对整数编码以提高效率。虽然每个编码值都是正数,但这些值是相减的,因此每个ACK Range表示的包号逐渐降低。 + +每个ACK Range通过给出该范围中最大包号之前已确认的包数来确认一段连续范围的数据包。值为0表示仅确认最大的包号。ACK Range值越大表示范围越大,相应的,ACK Range值越小表示范围越小。因此,给定范围的最大包号,最小值由以下公式确定: + smallest = largest - ack_range +ACK Range确认最小包号和最大包号之间的所有数据包。 + +每个ACK Range的最大值是通过计算减去所有前面的ACK Range Length或Gap的大小来确定的。 + +每个Gap表示未被确认的数据包范围。Gap中的数据包个数比Gap字段的编码值大1。 + +Gap字段的值使用以下公式为后续ACK Range确定最大包号值: + largest = previous_smallest - gap - 2 +如果出现计算的包号为负数,终端必须(**MUST**)生成FRAME_ENCODING_ERROR类型的连接错误。 + +### 19.3.2. ECN计数(ECN Counts) +ACK帧使用类型值(即类型0x03)的最低有效位来表示ECN反馈,并报告QUIC包的IP报头中接收到具有相关ECN码点ECT(0)、ECT(1)或ECN-CE。ECN计数仅在ACK帧类型为0x03时出现。 + +当ACK帧携带ECN时,共有三个ECN计数,如图27所示: + +``` +ECN Counts { + ECT0 Count (i), + ECT1 Count (i), + ECN-CE Count (i), +} +``` +图27: ECN Count Format + +ECN Counts字段有: + +* ECT0 Count:ECT0计数,变长整数,表示与ACK帧相同的包号空间中接收到的携带ECT(0) 码点的数据包总数。 + +* ECT1 Count:ECT1计数,变长整数,表示与ACK帧相同的包号空间中接收到的携带ECT(1) 码点的数据包总数。 + +* ECT-CE Count:ECN-CE计数,变长整数,表示与ACK帧相同的包号空间中接收到的携带ECN-CE码点的数据包总数。 + +ECN计数为每个包号空间单独维护。 + +## 19.4. RESET_STREAM帧(RESET_STREAM Frames) +终端使用RESET_STREAM帧(Type=0x04)立即终止流的发送部分。 + +发送RESET_STREAM后,终端停止传输和重传已识别流上的STREAM帧。RESET_STREAM的接收方可以丢弃在该流上已接收到的任何数据。 + +在单向发送流上收到RESET_STREAM帧,终端必须(**MUST**)以STREAM_STATE_ERROR错误码终止连接。 + +RESET_STREAM帧的格式如图28所示。 + +``` +RESET_STREAM Frame { + Type (i) = 0x04, + Stream ID (i), + Application Protocol Error Code (i), + Final Size (i), +} +``` +图28: RESET_STREAM Frame Format + +RESET_STREAM帧包含以下字段: + +* Stream ID:流标识,变长整数,表示正在终止的流的流ID。 + +* Application Protocol Error Code:应用层协议错误码,变长整数,包含应用层协议错误码(参见第20.2节),指示流关闭的原因。 + +* Final Size:最终大小,变长整数,指示RESET_STREAM发送方的流的最终大小,以字节为单位,参见第4.5节。 + +## 19.5. STOP_SENDING帧(STOP_SENDING Frames) +终端使用STOP_SENDING帧(Type=0x05)来向发送方传达入向数据被每个应用请求接收时丢弃的信息。STOP_SENDING请求对端停止在该流上发送数据。 + +可以为处于Recv或Size Known 状态的流发送STOP_SENDING帧,参见第3.2节。在本地发起但尚未完成创建的流上收到STOP_SENDING帧,必须(**MUST**)视为STREAM_STATE_ERROR类型的连接错误。在receive-only流上接收到STOP_SENDING帧的终端必须(**MUST**)以STREAM_STATE_ERROR错误码终止连接。 + +STOP_SENDING帧的格式如图29所示。 + +``` +STOP_SENDING Frame { + Type (i) = 0x05, + Stream ID (i), + Application Protocol Error Code (i), +} +``` +图29: STOP_SENDING Frame Format + +STOP_SENDING帧包含以下字段: + +* Stream ID:流ID,变长整数,携带被忽略流的流ID的变长整数。 +* Application Protocol Error Code:应用层协议错误码,变长整数,包含应用指定的发送方忽略流的原因,参见第20.2节。 + +## 19.6. CRYPTO帧(CRYPTO Frame) +CRYPTO帧(Type=0x06)用于传输加密握手消息。它可以在除0-RTT之外的所有数据包类型中发送。CRYPTO帧为加密协议提供了一个有序的字节流。CRYPTO帧在功能上与STREAM帧相同,只是其不带流ID,不受流控,不携带可选偏移量、可选长度和流结束标记。 + +CRYPTO帧的格式如图30所示。 + +``` +CRYPTO Frame { + Type (i) = 0x06, + Offset (i), + Length (i), + Crypto Data (..), +} +``` +图30: CRYPTO Frame Format + +CRYPTO帧包含以下字段: + +* Offset :偏移,变长整数,指定此CRYPTO帧中数据在流中的字节偏移量。 + +* Length:长度,变长整数,指定此CRYPTO帧中加密数据字段的长度。 + +* Crypto Data:加密数据,加密消息数据负载。 + +每个加密级别都对应一个单独的加密握手数据流,每个数据流都从偏移量0开始。这意味着每个加密级别都被视为单独的CRYPTO数据流。 + +流上传输的最大偏移量——偏移量和数据长度的总和——不能超过2^62^-1。收到超过此限制的帧必须(**MUST**)被视为FRAME_ENCODING_ERROR或CRYPTO_BUFFER_EXCEEDED类型的连接错误。 + +与包含指示数据属于哪个流的流ID的STREAM帧不同,CRYPTO帧携带每个加密级别的单个流的数据。流没有明确的结束,因此CRYPTO帧没有FIN位。 + +## 19.7. NEW_TOKEN帧(NEW_TOKEN Frames) +服务端发送一个NEW_TOKEN帧(Type=0x07),给客户端提供一个令牌,以便在未来连接发送Initial包时在报文中携带。 + +NEW_TOKEN帧的格式如图31所示。 + +``` +NEW_TOKEN Frame { + Type (i) = 0x07, + Token Length (i), + Token (..), +} +``` +图31: NEW_TOKEN Frame Format + +NEW_TOKEN帧包含以下字段: + +* Token Length:令牌长度,变长整数,指定令牌的长度,以字节为单位。 + +* Token :令牌,客户端在未来发送Initial包携带的未明字符块。令牌不得(**MUST NOT**)为空。客户端在收到带有空Token字段的NEW_TOKEN帧时必须(**MUST**)视为FRAME_ENCODING_ERROR类型的连接错误。 + +如果包含NEW_TOKEN帧的数据包被错误地认定为丢失,客户端可能会收到多个包含相同令牌值的NEW_TOKEN帧,客户端需要丢弃重复值。NEW_TOKEN帧可用于关联连接尝试,参见第8.1.3小节。 + +客户端不得(**MUST NOT**)发送NEW_TOKEN帧。服务端收到NEW_TOKEN帧必须(**MUST**)将其视为PROTOCOL_VIOLATION类型的连接错误。 + +## 19.8. STREAM帧(STREAM Frames) +STREAM帧隐式地创建一个流并携带流数据。STREAM帧中的Type字段采用0b00001XXX形式(或从0x08到0x0f的一组值)。帧类型的三个低位决定了帧中存在的字段: + +* 帧类型中的OFF位(0x04)指示是否存在Offset字段。设置为1时,存在Offset字段;设置为0时,Offset字段不存在并且Stream Data从偏移量0开始(即,帧包含流的第一个字节,或数据长度为0的流的末尾)。 + +* 帧类型中的LEN位(0x02)指示是否存在Length字段。如果该位设置为0,则Length字段不存在并且Stream Data字段扩展到数据包的末尾。如果该位设置为1,则存在Length字段。 + +* 帧类型中的FIN位(0x01)指示该帧是否标志着流的结束。流的最终大小是偏移量和该帧的长度之和。 + +如果终端在一个本地发起的尚未创建成功的流或send-only类型的流上收到STREAM帧,它必须(**MUST**)以错误码STREAM_STATE_ERROR终止连接。 + +STREAM帧的格式如图32所示。 + +``` +STREAM Frame { + Type (i) = 0x08..0x0f, + Stream ID (i), + [Offset (i)], + [Length (i)], + Stream Data (..), +} +``` +图32: STREAM Frame Format + +STREAM帧包含以下字段: + +* Stream ID:流标识,变长整数,表示流的流ID,参见第2.1节。 + +* Offset:偏移,变长整数,指定此STREAM帧中数据在流中的字节偏移量。当OFF位设置为1时,此字段存在。当Offset字段不存在时,偏移量为0。 + +* Length:长度,变长整数,指定此STREAM帧中Stream Data字段的长度。该字段在LEN位设置为1时出现。当LEN位设置为0时,数据包中的所有剩余字节都是流数据。 + +* Stream Data:流数据,该流中要传输的字节。 + +当Stream Data字段的长度为0时,STREAM帧中的Offset是将要发送的下一个字节的偏移量。 + +流中的第一个字节的偏移量为0。流上可传输的最大偏移量 ——偏移量和数据长度的总和——不能超过2^62^-1,因为无法为这么大量的数据提供流控限额,收到超过此限制的帧必须(**MUST**)被视为FRAME_ENCODING_ERROR或FLOW_CONTROL_ERROR类型的连接错误。 + +## 19.9. MAX_DATA帧(MAX_DATA Frames) +MAX_DATA帧(Type=0x10)用于流控,以通知对端其可以在整个连接上发送的最大数据量。 + +MAX_DATA帧的格式如图33所示。 +``` +MAX_DATA Frame { + Type (i) = 0x10, + Maximum Data (i), +} +``` +图33: MAX_DATA Frame Format + +MAX_DATA帧包含以下字段: + +* Maximum Data:最大数据量,变长整数,表示整个连接上可以发送的最大数据量,以字节为单位。 + +在STREAM帧中发送的所有数据都计入此限制。所有流的最终大小的总和——包括处于终结状态的流 ——不得(**MUST NOT**)超过接收端通告的Maximum Data值。如果终端接收到的数据大于它发送的Maximum Data,则终端必须(**MUST**)以FLOW_CONTROL_ERROR类型的错误终止连接。这也包括违反早期建链过程中保存的传输参数限制,参见第7.4.1小节。 + +## 19.10. MAX_STREAM_DATA帧(MAX_STREAM_DATA Frames) +MAX_STREAM_DATA帧(Type=0x11)用于流控,以通知对端其可以在该流上发送的最大数据量。 + +可以为处于Recv状态的流发送MAX_STREAM_DATA帧,参见第3.2节。本地发起但尚未创建成功的流收到MAX_STREAM_DATA帧必须(**MUST**)将其视为STREAM_STATE_ERROR类型的连接错误。接收到receive-only流的MAX_STREAM_DATA帧的终端必须(**MUST**)以错误STREAM_STATE_ERROR终止连接。 + +MAX_STREAM_DATA帧的格式如图34所示。 + +``` +MAX_STREAM_DATA Frame { + Type (i) = 0x11, + Stream ID (i), + Maximum Stream Data (i), +} +``` +图34: MAX_STREAM_DATA Frame Format + +MAX_STREAM_DATA帧包含以下字段: + +* Stream ID:流标识,变长整数,关联的流的流ID。 + +* Maximum Stream Data:最大流数据量,变长整数,指示可以在该流上可发送的最大数据量,以字节为单位。 + +当针对此限制计算数据时,终端应该计算在流上发送或接收的数据的最大接收偏移量。丢包和乱序可能意味着该流上接收到的最大偏移量可能大于在该流上接收到的数据的总大小。收到STREAM帧可能不会增加最大接收偏移。 + +在流上发送的数据量不得(**MUST NOT**)超过接收端通告的最大流数据量值。如果终端接收到的数据多于它为该流设置的最大流数据量,则终端必须(**MUST**)以FLOW_CONTROL_ERROR类型错误终止连接。这包括违反建链时保存的传输参数,参见第7.4.1小节。 + +## 19.11. MAX_STREAMS帧(MAX_STREAMS Frames) +MAX_STREAMS帧(Type=0x12或0x13)通知对端其允许打开的给定类型的流的累积个数。类型为0x12的MAX_STREAMS帧适用于双向流,类型为0x13的MAX_STREAMS帧适用于单向流。 + +MAX_STREAMS帧的格式如图35所示。 + +``` +MAX_STREAMS Frame { + Type (i) = 0x12..0x13, + Maximum Streams (i), +} +``` +图35: MAX_STREAMS Frame Format + +MAX_STREAMS帧包含以下字段: + +* Maximum Streams:最大流个数,在连接的生命周期内可以打开的相应类型的流的累积总数。此值不能超过2^60^,因为无法对大于2^62^-1的流ID进行编码。接收到大于此限制的流的帧,必须(**MUST**)将其视为FRAME_ENCODING_ERROR类型的连接错误。 + +丢包和乱序可能会导致终端接收到一个MAX_STREAMS帧,其流个数限制比之前接收到的要低。必须(**MUST**)忽略这种不增加流个数限制的MAX_STREAMS帧。 + +终端不得(**MUST NOT**)打开超过其对端设置的当前流个数限制所允许的流。例如,接收单向流个数限制为3的服务端可以打开流3、7或11,但不能打开流15。如果对端打开的流比原来多,则终端必须(**MUST**)以STREAM_LIMIT_ERROR类型的错误终止连接。这也包括违反建链阶段保存的传输参数,参见第7.4.1小节。 + +请注意,这些帧(以及相应的传输参数)并未指定可以同时打开的流的数量。MAX_STREAMS帧只限制已关闭的流以及打开的流的总数。 + +## 19.12. DATA_BLOCKED帧(DATA_BLOCKED Frames) +当发送方希望发送数据但由于连接级流控而无法发送时,发送方应该(**SHOULD**)发送DATA_BLOCKED帧(Type=0x14),参见第4章。DATA_BLOCKED帧可用作流控算法调整的输入,参见第4.2节。 + +DATA_BLOCKED帧的格式如图36所示。 + +``` +DATA_BLOCKED Frame { + Type (i) = 0x14, + Maximum Data (i), +} +``` +图36: DATA_BLOCKED Frame Format + +DATA_BLOCKED帧包含以下字段: + +* Maximum Data:最大数据量,变长整数,指示阻塞发生时的连接级别的流量限制值。 + +## 19.13. STREAM_DATA_BLOCKED帧(STREAM_DATA_BLOCKED Frames) +当发送方希望发送数据但由于流级流控而无法发送时,发送方应该(**SHOULD**)发送STREAM_DATA_BLOCKED帧(Type=0x15)。该帧类似于DATA_BLOCKED(第19.12节)。 + +在send-only流上接收到STREAM_DATA_BLOCKED帧的终端必须(**MUST**)以错误STREAM_STATE_ERROR终止连接。 + +STREAM_DATA_BLOCKED帧的格式如图37所示。 + +``` +STREAM_DATA_BLOCKED Frame { + Type (i) = 0x15, + Stream ID (i), + Maximum Stream Data (i), +} +``` +图37: STREAM_DATA_BLOCKED Frame Format + +STREAM_DATA_BLOCKED帧包含以下字段: + +* Stream ID:流标识,变长整数,指示由于流控而阻塞的流的ID。 + +* Maximum Stream Data:最大流数据量,变长整数,指示发生阻塞时流的偏移量。 + +## 19.14. STREAMS_BLOCKED帧(STREAMS_BLOCKED Frames) +当发送方希望打开流但由于其对端设置的最大流个数限制而无法打开时,发送方应该(**SHOULD**)发送STREAMS_BLOCKED帧(Type=0x16或0x17),参见第19.11节。类型为0x16的STREAMS_BLOCKED帧用于指示达到双向流限制,类型为0x17的STREAMS_BLOCKED帧用于指示达到单向流限制。 + +STREAMS_BLOCKED帧不会打开流,但会通知对端需要新的流并且因为流个数限制阻止了流的创建。 + +STREAMS_BLOCKED帧的格式如图38所示。 + +``` +STREAMS_BLOCKED Frame { + Type (i) = 0x16..0x17, + Maximum Streams (i), +} +``` +图38: STREAMS_BLOCKED Frame Format + +STREAMS_BLOCKED帧包含以下字段: + +* Maximum Streams:最大流个数,变长整数,指示发送此帧时允许的最大流个数。此值不能超过2^60^,因为无法对大于2^62^-1的流ID进行编码。接收到大于此限制的流的帧,必须(**MUST**)被其视为STREAM_LIMIT_ERROR或FRAME_ENCODING_ERROR类型的连接错误。 + +## 19.15. NEW_CONNECTION_ID帧(NEW_CONNECTION_ID Frames) +终端发送一个NEW_CONNECTION_ID帧(Type=0x18)来为其对端提供替代CID,这些CID可用于在迁移连接时打破可关联性,参见第9.5节。 + +NEW_CONNECTION_ID帧的格式如图39所示。 + +``` +NEW_CONNECTION_ID Frame { + Type (i) = 0x18, + Sequence Number (i), + Retire Prior To (i), + Length (8), + Connection ID (8..160), + Stateless Reset Token (128), +} +``` +图39: NEW_CONNECTION_ID Frame Format + +NEW_CONNECTION_ID帧包含以下字段: + +* Sequence Number:序号,发送方分配给此CID的序号,编码为变长整数,参见第5.1.1小节。 + +* Retire Prior To:停用此序号之前ID,变长整数,指示应该停用哪些CID,参见第5.1.2小节。 + +* Length:长度,一个8位无符号整数,表示CID的长度。小于1和大于20的值无效,必须(**MUST**)视为FRAME_ENCODING_ERROR类型的连接错误。 + +* Connection ID:连接ID(CID),由Length指定长度的CID。 + +* Stateless Reset Token:无状态重置令牌,一个128位值,当上述关联的CID在使用时,此令牌将用于无状态重置,参见第10.3节。 + +当前如果终端要求其对端发送具有零长度DCID的数据包,则该终端不得(**MUST NOT**)发送此帧。将CID的长度更改为零长度或变更零长度会导致难以识别到CID的值何时被改变。发送具有零长度DCID的数据包的终端,必须(**MUST**)将收到NEW_CONNECTION_ID帧视为PROTOCOL_VIOLATION类型的连接错误。 + +传输错误、超时和重传可能会导致多次收到相同的NEW_CONNECTION_ID帧。多次收到同一帧不得(**MUST NOT**)视为连接错误。接收方可以使用NEW_CONNECTION_ID帧中提供的序号来判决多次接收相同的NEW_CONNECTION_ID帧的情况。 + +如果终端接收到一个NEW_CONNECTION_ID帧,该帧与之前收到的NEW_CONNECTION_ID具有相同的CID,但是具有不同的无状态重置令牌或序号,或者序号相同而CID不同,则终端可以(**MAY**)将该情况视为PROTOCOL_VIOLATION类型的连接错误。 + +Retire Prior To字段适用于连接建立期间确定的CID和preferred_address传输参数,参见第5.1.2小节。 Retire Prior To字段中的值必须(**MUST**)小于或等于Sequence Number字段的值。接收到Retire Prior To大于Sequence Number的NEW_CONNECTION_ID帧,必须(**MUST**)将其视为FRAME_ENCODING_ERROR类型的连接错误。 + +一旦发送方表示Retire Prior To值之前的序号停用,在后续NEW_CONNECTION_ID帧中发送的较小的序号值将无效。接收方必须(**MUST**)忽略任何不增加Retire Before To值的NEW_CONNECTION_ID帧。 + +接收到序号小于先前接收的NEW_CONNECTION_ID帧中Retire Prior To字段的NEW_CONNECTION_ID帧的终端,必须(**MUST**)发送相应的RETIRE_CONNECTION_ID帧,该帧停用新接收的CID,除非它已经停用了该CID对应的序号。 + +## 19.16. RETIRE_CONNECTION_ID帧(RETIRE_CONNECTION_ID Frames) +终端发送RETIRE_CONNECTION_ID帧(Type=0x19)以指示它将不再使用由其对端发布的指定CID。这包括握手期间提供的CID。发送RETIRE_CONNECTION_ID帧也可以作为一个请求,请求对端发送额外CID以备将来使用,参见第5.1节。可以使用NEW_CONNECTION_ID帧(第19.15节)将新的CID传递给对端。 + +停用CID会使与此CID关联的无状态重置令牌失效。 + +RETIRE_CONNECTION_ID帧的格式如图40所示。 + +``` +RETIRE_CONNECTION_ID Frame { + Type (i) = 0x19, + Sequence Number (i), +} +``` +图40: RETIRE_CONNECTION_ID Frame Format + +RETIRE_CONNECTION_ID帧包含以下字段: + +* Sequence Number:序号,被停用的CID的序号,参见第5.1.2小节。 + +收到包含序号大于之前发送的所有CID的序号的RETIRE_CONNECTION_ID帧,必须(**MUST**)将其视为PROTOCOL_VIOLATION类型的连接错误。 + +在RETIRE_CONNECTION_ID帧中指定的序号不得(**MUST**)引用包含该帧的数据包的DCID。对端可以(**MAY**)将此视为PROTOCOL_VIOLATION类型的连接错误。 + +如果终端为它的对端提供了一个零长度的CID,它就不能发送这个帧。提供零长度CID的终端必须(**MUST**)将收到RETIRE_CONNECTION_ID帧视为PROTOCOL_VIOLATION类型的连接错误。 + +## 19.17. PATH_CHALLENGE帧(PATH_CHALLENGE Frames) +终端可以使用PATH_CHALLENGE帧(Type=0x1a)来检查对端的可达性以及用于连接迁移期间的路径验证。 + +PATH_CHALLENGE帧的格式如图41所示。 + +``` +PATH_CHALLENGE Frame { + Type (i) = 0x1a, + Data (64), +} +``` +图41: PATH_CHALLENGE Frame Format + +PATH_CHALLENGE帧包含以下字段: + +* Data :数据,这个8字节字段可以包含任意数据。 + +在PATH_CHALLENGE帧中包含64位熵可确保接收到此数据的第三方猜测不出正确值。 + +此帧的接收方必须(**MUST**)生成包含相同Data值的PATH_RESPONSE帧(第19.18节)。 + +## 19.18. PATH_RESPONSE帧( PATH_RESPONSE Frames) +发送PATH_RESPONSE帧(Type=0x1b)以响应PATH_CHALLENGE帧。 + +PATH_RESPONSE帧的格式如图42所示。PATH_RESPONSE帧的格式与PATH_CHALLENGE帧的格式相同,参见第19.17节。 + +``` +PATH_RESPONSE Frame { + Type (i) = 0x1b, + Data (64), +} +``` +图42: PATH_RESPONSE Frame Format + +如果PATH_RESPONSE帧的Data与终端先前发送的PATH_CHALLENGE帧的Data不匹配,终端可以(**MAY**)生成PROTOCOL_VIOLATION类型的连接错误。 + +## 19.19. CONNECTION_CLOSE帧(CONNECTION_CLOSE Frames) +终端发送CONNECTION_CLOSE帧(Type=0x1c或0x1d)以通知其对端连接正在关闭。类型为0x1c的CONNECTION_CLOSE帧仅用于在QUIC层发出错误信号,或者无错误关闭(使用NO_ERROR码)。类型为0x1d的CONNECTION_CLOSE帧用于向使用QUIC的应用发出错误信号。 + +如果存在尚未显式关闭的打开流,则在连接关闭时它们会被隐式关闭。 + +CONNECTION_CLOSE帧的格式如图43所示。 + +``` +CONNECTION_CLOSE Frame { + Type (i) = 0x1c..0x1d, + Error Code (i), + [Frame Type (i)], + Reason Phrase Length (i), + Reason Phrase (..), +} +``` +图43: CONNECTION_CLOSE Frame Format + +CONNECTION_CLOSE帧包含以下字段: + +* Error Code:错误码,变长整数,指示关闭此连接的原因。0x1c类型的CONNECTION_CLOSE帧使用来自第20.1节空间中定义的代码。0x1d类型的CONNECTION_CLOSE帧使用应用层协议定义的代码,参见第20.2节。 + +* Frame Type:帧类型,变长整数,编码触发此错误的帧类型。当帧类型未知时,使用值0(相当于引用PADDING帧)。与应用有关的CONNECTION_CLOSE(类型0x1d)不包括此字段。 + +* Reason Phrase Length:原因短语长度,变长整数,以字节为单位,指定原因短语的长度。由于CONNECTION_CLOSE帧不能拆分到不同数据包,因此对数据包大小的任何限制也会限制原因短语的可用空间。 + +* Reason Phrase:原因短语,连接关闭的附加诊断信息。如果发送方选择不提供除了错误码之外的详细信息,则长度可以为零。这应该(**SHOULD**)是一个UTF-8编码的字符串[RFC3629],就算此帧不携带信息,例如语言标签之类,这也有助于创建文本的实体之外的其他实体的理解。 + +应用有关的CONNECTION_CLOSE(类型0x1d)帧只能使用0-RTT或1-RTT包发送,参见第12.5节。当应用希望在握手期间放弃连接时,终端可以在Initial或Handshake包中发送带有APPLICATION_ERROR错误码的CONNECTION_CLOSE帧(类型0x1c)。 + +## 19.20. HANDSHAKE_DONE帧(HANDSHAKE_DONE Frames) +服务端使用HANDSHAKE_DONE帧(Type=0x1e)向客户端发出握手确认信号。 + +HANDSHAKE_DONE帧的格式如图44所示,这表明HANDSHAKE_DONE帧没有内容。 + +``` +HANDSHAKE_DONE Frame { + Type (i) = 0x1e, +} +``` +图44: HANDSHAKE_DONE Frame Format + +HANDSHAKE_DONE帧只能由服务端发送。服务端在完成握手之前不得(**MUST NOT**)发送HANDSHAKE_DONE帧。服务端必须(**MUST**)将收到HANDSHAKE_DONE帧视为PROTOCOL_VIOLATION类型的连接错误。 + +## 19.21. 扩展帧(Extension Frames) +QUIC帧不使用自解释编码。因此,终端在成功处理数据包之前需要了解所有帧的语法。这允许对帧进行有效编码,但这意味着终端无法发送其对端未知类型的帧。 + +希望使用自定义帧的QUIC扩展必须(**MUST**)首先确保对端能够理解该帧。终端可以使用传输参数来表示它愿意接收的扩展帧的类型。一个传输参数可以指示对一种或多种扩展帧类型的支持。 + +除非明确定义了组合的行为,否则修改或替换核心协议功能(包括帧类型)的扩展将难以与其他修改或替换相同功能的扩展相结合。这样的扩展应该(**SHOULD**)定义它们与先前定义的扩展的之间的交互,修改相同的协议组件。 + +扩展帧必须(**MUST**)是受拥塞控制的,并且必须(**MUST**)触发一个ACK帧响应。替代或补充ACK帧的扩展帧除外。除非在扩展中指定,否则扩展帧不受流控限制。 + +IANA注册中心用于管理帧类型的分配,参见第22.4节。 + +# 20. 错误码(Error Codes) + +QUIC的传输层错误码和应用层错误码是62位无符号整数。 + +## 20.1. 传输层错误码(Transport Error Codes) + +本节列出了定义的QUIC传输层错误码,这些错误码可以在CONNECTION_CLOSE帧中使用,类型为0x1c。这些错误适用于整个连接。 + +* NO_ERROR (0x00):终端将此与CONNECTION_CLOSE一起使用,以表示在没有任何错误的情况下即时关闭连接。 + +* INTERNAL_ERROR(0x01):终端遇到内部错误,无法继续连接。 + +* CONNECTION_REFUSED (0x02):服务端拒绝接受新连接。 + +* FLOW_CONTROL_ERROR (0x03):终端接收到的数据多于其公布的流控限额中允许的数据,参见第4章。 + +* STREAM_LIMIT_ERROR (0x04):终端接收到携带某个流标识的帧,这个流标识超过了其通告的对应的流类型的流数限制。 + +* STREAM_STATE_ERROR (0x05):终端收到了某个流的帧,但该流所处状态不支持发送该帧,参见第3章。 + +* FINAL_SIZE_ERROR (0x06):(1) 终端收到一个包含超过先前确定的final size的数据的STREAM帧,(2)终端收到一个包含final size小于已接收的流数据大小的STREAM帧或RESET_STREAM帧,(3) 终端收到一个STREAM帧或一个RESET_STREAM帧,其中包含与已确定的final size不同的final size。 + +* FRAME_ENCODING_ERROR (0x07):终端接收到格式错误的帧——例如,未知类型的帧或确认范围大于数据包其余部分所能承载大小的ACK帧。 + +* TRANSPORT_PARAMETER_ERROR (0x08):终端接收到格式错误、包含无效值、遗漏强制传输参数、包含禁止传输参数或其他错误的传输参数。 + +* CONNECTION_ID_LIMIT_ERROR (0x09):对端提供的CID数量超过了通告的active_connection_id_limit。 + +* PROTOCOL_VIOLATION (0x0a):终端检测到一个协议合规性错误,该错误类型不能用更具体的错误码表示。 + +* INVALID_TOKEN(0x0b):服务端收到包含INVALID_TOKEN字段的客户端Initial包。 + +* APPLICATION_ERROR (0x0c):应用或应用层协议错误导致连接关闭。 + +* CRYPTO_BUFFER_EXCEEDED (0x0d):终端在CRYPTO帧中接收到的数据多于它可以缓冲的数据。 + +* KEY_UPDATE_ERROR (0x0e):终端在执行密钥更新时检测到错误,参见[QUIC-TLS]第6章。 + +* AEAD_LIMIT_REACHED (0x0f):终端已达到给定连接使用的AEAD算法的机密性或完整性限制。 + +* NO_VIABLE_PATH (0x10):终端已确定网络路径无法支持QUIC。除非路径不支持足够大的MTU,否则终端不太可能收到携带此错误码的CONNECTION_CLOSE帧。 + +* CRYPTO_ERROR (0x0100-0x01ff):加密握手失败。QUIC保留256个值用于携带特定于所使用的加密握手的错误码。[QUIC-TLS]第4.8节列举了使用TLS进行加密握手时发生的错误码。 + +有关注册新错误码的详细信息,请参阅第22.5节。 + +在定义这些错误码时,应用了几个原则: + +* 可能需要接收方执行特定操作的错误条件被赋予单独的错误码; +* 表示常见情况的错误被赋予特定错误码; +* 错误码还将用于表示通用功能,如流量控制或传输参数处理时的错误。 +* 最后提供通用错误码供实现在无法或不愿意使用更具体错误码的情况下使用。 + +## 20.2. 应用层协议错误码(Application Protocol Error Codes) + +应用层错误码的管理留给应用层协议。RESET_STREAM帧(第19.4节)、STOP_SENDING帧(第19.5节)和类型为0x1d的CONNECTION_CLOSE帧(第19.19节)携带应用层协议错误码。 + +# 21. 安全考虑(Security Considerations) +QUIC的目标是提供安全的传输层连接。第21.1节概述了这些属性,随后的部分讨论了有关这些属性的限制和注意事项,包括对已知攻击和应对策略的描述。 + +## 21.1. 安全性概述(Overview of Security Properties) +完整的QUIC安全性分析超出了本文的范围。本节是对所需安全性的非正式描述,以指导实现者进行协议分析。 + +QUIC采用[SEC-CONS]中描述的威胁模型,并针对该模型的多种攻击提供保护机制。 + +为此,我们将攻击分为被动攻击和主动攻击。被动攻击者能够从网络读取数据包,而主动攻击者也能够将数据包写入网络。然而,被动攻击可能会让攻击者能够在QUIC连接所处路径中引起路由更改或其他变更。 + +攻击者还可以分为on-path攻击者或off-path的攻击者。on-path攻击者可以读取、修改或删除它观察到的任何数据包,从而使数据包到达不了其目的地,而off-path的攻击者可以观察到数据包但无法阻止原数据包到达其预定目的地。这两种类型的攻击者也可以发送任意数据包。与[SEC-CONS]第3.5节的定义不同的是,(注:在[SEC-CONS]的定义中,off-path的攻击者只能发包不能收包),off-path的攻击者能够观察数据包。 + +握手、数据包保护和连接迁移相关安全性是分别考虑的。 + +### 21.1.1. 握手(Handshake) +QUIC握手结合了TLS1.3握手并继承了[TLS13]的Appendix E.1中描述的加密属性。QUIC的许多安全性取决于提供这些属性的TLS握手。对TLS握手的任何攻击都可能影响QUIC。 + +任何对TLS握手的攻击,或危及会话密钥的保密性或唯一性,或伤及对端的身份认证,都会影响QUIC提供的依赖于这些密钥的其他安全保证。例如,连接迁移(第9章)取决于机密性保护的有效性,无论是TLS握手的密钥协商还是QUIC包保护,都是为了避免跨网络路径的可链接性。 + +对TLS握手完整性的攻击可能使得攻击者能够影响应用层协议或QUIC版本的选择。 + +除了TLS提供的特性外,QUIC握手还提供了一些针对握手的DoS攻击的防御措施。 + +#### 21.1.1.1. 防放大攻击(Anti-Amplification) +地址验证(第8章)用于验证声明一个给定地址的实体能否在该地址收到数据包。地址验证将Amplification攻击目标限制在攻击者可以观察到的数据包地址。 + +在地址验证之前,终端能够发送的数据量受到限制。终端向未验证地址发送不能超过接收的三倍。 + +> 注意:Anti-Amplification限制仅限于终端响应从未经验证的地址收到数据包时。在建立新连接或启动连接迁移时,Anti-Amplification限制不适用于客户端。 + +#### 21.1.1.2. 服务端DoS攻击(Server-Side DoS) +服务端对一次完整握手的往返数据计算比较费资源,因为需要进行数字签名和密钥交换。为了防止针对服务端计算资源的DoS攻击,Retry包提供了一种低耗的令牌交换机制,使得服务端可以在执行此计算之前验证客户端的IP地址,代价只是一个RTT时间。握手成功后,服务端可以向客户端发出新令牌,使得后续新连接建立的时候可以采用0-RTT。 + +#### 21.1.1.3. On-Path握手终止攻击(On-Path Handshake Termination) +on-path或off-path的攻击者可以通过替换或加速伪Initial包致使握手失败。因为一旦交换了有效的Initial包,后续的Handshake包就会受到握手密钥的保护,on-path攻击者除了通过丢弃数据包使得终端放弃尝试之外,无法以其他方式强制握手失败。 + +on-path攻击者还可以替换任一端的数据包地址,从而使其弄错对端地址。这种攻击与NAT导致的结果没有区别。 + +#### 21.1.1.4. 参数协商攻击(Parameter Negotiation) +整个握手过程都受到加密保护,Initial包使用版本特定的密钥进行加密,Handshake和后续数据包使用从TLS密钥交换派生的密钥进行加密。此外,传输参数的协商被打包到TLS中,提供与普通TLS协商相同的完整性保证。攻击者可以观察到客户端的传输参数(只要它知道版本对应的salt),但无法观察服务端的传输参数,也无法影响传输参数协商。 + +CID未加密,但在所有数据包中都受到完整性保护。 + +此QUIC版本没有包含版本协商机制,不兼容版本的实现将无法建立连接。 + +### 21.1.2. 数据包保护(Protected Packets) +数据包保护(第12.1节)对除Version Negotiation包之外的所有数据包,都进行加密认证,但由于使用特定于版本的密钥材料,对Initial和Retry包的保护受限,更多详细信息请参阅[QUIC-TLS]。本节仅考虑对受保护数据包的被动和主动攻击。 + +on-path和off-path的攻击者都可以发起被动攻击,在这种攻击中,他们保存观察到的数据包,以便将来针对数据包保护进行离线攻击,这对于任意网络上任意数据包的任意观察者来说都可以做到。 + +在无法观察到连接的有效数据包的情况下,注入数据包攻击不太可能成功,因为数据包保护确保有效数据包仅由拥有在握手期间商定密钥材料的终端生成,见第7章和第21.1.1小节。类似地,除了Initial包外,任何可观察到数据包,并尝试在这些数据包中插入新数据或修改现有数据的主动攻击者,都不能够生成接收端认为有效的数据包。 + +在欺骗攻击中,主动攻击者可能篡改其转发或注入的数据包中未受保护的部分,例如源地址或目的地址,这只有当攻击者可以将数据包转发到初始终端时才有效。数据包保护可以确保数据包负载只能由完成握手的终端处理,而无效数据包将被忽略。 + +攻击者还可以改变QUIC包和UDP报文之间的边界,致使多个QUIC包合并为一个UDP报文或将合并后的QUIC包拆分为多个UDP报文。除了包含Initial包(需要填充)的UDP报文外,修改UDP报文中数据包的排列方式对连接没有功能性影响,但可能会改变一些性能特性。 + +### 21.1.3. 连接迁移(Connection Migration) +连接迁移(第9章)使得终端能够在不同IP地址和Port端口的多条路径上进行传输切换,每次使用一条路径发送和接收非探测帧。路径验证(第8.2节)确定对端愿意并且能够接收在特定路径上发送的数据包。这有助于通过限制发往欺骗地址的数据包数量来减少地址欺骗的影响。 + +本节介绍在各种类型的DoS攻击下连接迁移的安全特性。 + +#### 21.1.3.1. On-Path主动攻击(On-Path Active Attacks) +可以使其观察到的数据包不再到达其预期目的地的攻击者被视为“on-path攻击者”。当客户端和服务端之间存在攻击者时,终端发送的数据包需要通过攻击者,以在给定路径上建立连接。 + +on-path攻击者可以: + +* 观察到数据包 +* 修改IP和UDP报文头 +* 注入新数据包 +* 使数据包延迟 +* 使数据包乱序 +* 丢弃数据包 +* 沿数据包边界拆分和合并报文 + +on-path攻击者不能: + +* 修改数据包的经过认证的部分并使接收方接受该数据包 + +on-path攻击者有机会修改它观察到的数据包,但是,对数据包经过认证的部分的任何修改,都将导致它被接收端视为无效包丢弃,因为数据包负载需要经过认证和加密。 + +QUIC旨在限制on-path攻击者的能力,如下所述: + +1. on-path攻击者可以阻止两端在其所在路径上建立连接,如果两端不能选择不经过攻击者的其他路径,则可能连接失败。攻击者可以通过丢弃所有数据包、修改使其无法解密或其他方法来实现这个目的。 +2. on-path攻击者如果也在新路径上,则可以通过使得新路径验证失败来阻止两端往新路径迁移。 +3. on-path攻击者无法阻止客户端迁移到不经过攻击者的其他路径。 +4. on-path攻击者可以通过延迟或丢弃数据包来降低连接的吞吐量。 +5. on-path攻击者不能迫使终端接受它篡改了认证部分的数据包。 + +#### 21.1.3.2. Off-Path主动攻击(Off-Path Active Attacks) +off-path攻击者并不直接位于客户端和服务端之间的路径上,但可以获得客户端和服务端之间发送的部分或全部数据包的副本。它还能够将这些数据包的副本发往任一终端。 + +off-path攻击者可以: + +* 观察到数据包 +* 注入新数据包 +* 乱序注入数据包 + +off-path攻击者不能: +* 修改终端发送的数据包 +* 延迟数据包 +* 丢弃数据包 +* 使原数据包乱序 + +off-path攻击者可以修改它观察到的数据包的副本,并将这些副本注入网络,可能具有欺骗性的源地址和目的地址。 + +出于本次讨论的目的,假设off-path攻击者能够将修改后的数据包副本注入网络,而且该网络可以使副本比原数据包更早到达目的终端。换句话说,攻击者有能力持续“赢得”与终端之间的合法数据包的竞争,从而可能使得接收方忽略原数据包。 + +此处还假设攻击者拥有影响NAT状态所需的资源。特别是,攻击者可以致使终端丢失其NAT绑定,然后获得相同的终端口以用于其自己的流量。 + +QUIC旨在限制off-path攻击者的能力,如下所述: + +1. off-path攻击者可以竞争数据包并试图成为“受限”的on-path攻击者。 +2. off-path攻击者只要能够改善客户端和服务端之间的连接性,就可以使将源地址列为off-path攻击者的转发数据包的路径验证成功。 +3. 一旦握手完成,off-path攻击者就不能致使连接关闭。 +4. 如果off-path攻击者无法观察到新路径,它就不能致使新路径迁移失败。 +5. 在迁移到新路径的过程中,off-path攻击者可能会成为“受限”的on-path攻击者,如果其在新路径也是off-path攻击者。 +6. off-path攻击者可以通过影响共享的NAT状态,使其可以用客户端最初使用的同一IP地址和端口向服务端发送数据包,从而成为“受限”的on-path攻击者。 + +#### 21.1.3.3. “受限”的On-Path主动攻击 (Limited On-Path Active Attacks) +“受限”的on-path攻击者是一种off-path攻击者,它通过在服务端和客户端之间复制和转发原数据包来提供改进的数据包路由,使得这些数据包在原数据包副本之前到达,从而使其被目的端丢弃。 + +“受限”的on-path攻击者与普通on-path攻击者的区别在于它不在两端之间的初始路径上,因此终端发送的原数据包仍然能到达其目的地。这意味着将来如果无法以比初始路径更快的速度将复制的数据包路由到目的地,将不能阻止原数据包到达目的地。 + +“受限”的on-path攻击者可以: + +* 观察到数据包 +* 注入新数据包 +* 修改未加密的包头 +* 使数据包乱序 + +“受限”的on-path攻击者不能: + +* 延迟数据包,使它们晚于初始路径上发送的数据包到达 +* 丢弃数据包 +* 修改数据包的认证加密的部分,并使接收方接受该数据包 + +“受限”的on-path攻击者只能将数据包延迟到原数据包到达之前的时间点,这意味着它无法提供比初始路径更糟糕的路由。如果受限on-path攻击者丢弃数据包,则原数据包副本仍将到达目的终端。 + +QUIC旨在限制有限的off-path攻击者的能力如下: + +1. 一旦握手完成,“受限”的on-path攻击者就不能致使连接关闭。 +2. 空闲连接如果客户端首先恢复活动,则“受限”的on-path攻击者无法致使该连接关闭。 +3. 空闲连接如果服务端首先恢复活动,则“受限”的on-path攻击者可能会致使该连接被视为丢失状态。 + +请注意,因为同样的原因,这些保证与为任何NAT提供的保证相同。 + +## 21.2. 握手拒绝服务(Handshake Denial of Service) +作为提供加密认证的传输层协议,QUIC提供了一系列针对拒绝服务的保护机制。加密握手完成后,QUIC终端会丢弃大多数未经认证的数据包,从而极大地限制了攻击者干扰现有连接的能力。 + +一旦连接建立,QUIC终端可能会接受一些未经认证的ICMP数据包(参见第14.2.1小节),但这些数据包的使用非常有限。终端唯一能接受的其他类型的数据包是Stateless Reset包(第10.3节),要求其包含的令牌在使用之前一直保密。 + +在创建连接的过程中,QUIC仅提供针对off-path攻击的保护。所有的QUIC包都包含了证据,证明其收到了来自对端的上一包。 + +地址在握手期间不能更改,因此终端可以丢弃在不同网络路径上收到的数据包。 + +SCID和DCID字段是在握手期间防止off-path攻击的主要手段,见第8.1节。这些需要与对端通告的CID相匹配。除了Initial包和Stateless Reset包,终端只接受包含与终端先前选择的值对应的DCID字段的数据包。这是为Version Negotiation包提供的唯一保护。 + +客户端选择的Initial包中的DCID字段是不可预测的,这是有原因的。携带加密握手的数据包受从该CID派生的密钥和特定于QUIC版本的salt保护。这使得终端在加密握手完成后可用相同的过程来验证它们接收到的数据包。无法认证的数据包将被丢弃。以这种方式为数据包保护提供了强有力的保证,即数据包的发送方收到了Initial包并能理解它。 + +对于能够在连接建立之前接收QUIC包的攻击者,这些保护措施并不是有效的。这些攻击者可能发送能被QUIC终端接受的数据包。此QUIC版本尝试检测此类攻击,但可预期的是终端将无法建立连接而不是恢复。在大多数情况下,加密握手协议[QUIC-TLS]负责检测握手期间是否被篡改。 + +允许终端使用其他方法来检测并尝试从握手干扰中恢复。终端可以使用其他方法来识别和丢弃无效的数据包,但本文中不做强制约定。 + +## 21.3. 放大攻击(Amplification Attack) +攻击者可能能够从服务端接收到地址验证令牌(第8章),然后释放它用于获取该令牌的IP地址。稍后,攻击者可以通过伪装成相同地址来启动与服务端的0-RTT连接,该地址现在可能指向一个不同的(受害者)终端。因此,攻击者可能会导致服务端向受害者发送初始拥塞窗口允许的数据。 + +服务端应该(**SHOULD**)通过限制地址验证令牌的使用和生命周期来缓解这种攻击,见第8.1.3小节。 + +## 21.4. Optimistic ACK攻击(Optimistic ACK Attack) +终端确认它没有接收到的数据包可能会导致拥塞控制器允许以超出网络支持的速率发包。终端可以(**MAY**)在发送数据包时略过包号以检测此行为,终端可以将之视为PROTOCOL_VIOLATION的连接错误,立即关闭连接,见第10.2节。 + +## 21.5. 请求伪造攻击(Request Forgery Attacks) +请求伪造攻击指的是某攻击者终端可控制其对端,使对端向第三方受害者发出指定的攻击请求。请求伪造攻击旨在让攻击者能够获得其对端的能力,这些能力是它不具备的。对于网络协议,请求伪造攻击通常用于利用由于对端在网络中的位置,而获得的由受害者授予对端的任何隐式权限。 + +为了使伪造的请求有效,攻击者需要能够影响对端发送的数据包的内容和发送位置。如果攻击者可以用受控的负载来攻击某个脆弱的服务,该服务可能会执行由攻击者的对端发起但由攻击者决定的操作。 + +例如,Web上的跨域请求伪造[CSRF]漏洞会致使客户端发出包含授权cookie[COOKIE]的请求,允许一个站点访问本应被授权给另一个站点的信息和操作。 + +由于QUIC承载在UDP协议上,所以主要的攻击方式是攻击者选择对端UDP报文的回应地址,并可以控制这些数据包中的一些不受保护的内容。(QUIC终端发送的大部分数据都受到保护,这也包括对密文的控制。)如果攻击者可以使对端向受害者主机发送特定UDP报文,该主机将根据报文中的内容执行某些操作,那么攻击就成功了。 + +本节讨论QUIC可被用于发起请求伪造攻击的方式。 + +本节还描述了QUIC终端可以实施的有限对策。这些对策可以由QUIC实现或部署单方面采用,而不需要请求伪造攻击的潜在目标采取行动。但是,如果基于UDP的服务没有正确地对请求授权,这些对策可能是不够的。 + +因为第21.5.4小节中描述的迁移攻击非常强大并且没有足够的对策,QUIC服务端实现应该假设攻击者可以使他们生成任意目的地的任意UDP负载。QUIC服务端不应该(**SHOULD NOT**)部署在没有部署入口过滤[BCP38]并且也没有足够安全的UDP终端的网络中。 + +尽管通常无法保证客户端不与易受攻击的服务端位于同一网络,但此QUIC版本不允许服务端迁移,从而防止了对客户端的欺骗迁移攻击。未来任何允许服务端迁移的扩展都必须(**MUST**)为伪造攻击定义对策。 + + +### 21.5.1. 终端的控制选项(Control Options for Endpoints) +QUIC为攻击者提供了一些机会,以影响或控制其对端发送UDP报文的目的地址: + +* 初始连接建立(第7章),服务端可以在其中指定客户端发送报文的目的地址——例如,通过填充DNS记录; + +* 首选地址(第9.6节),服务端可以在其中指定客户端发送报文的目的地址; + +* 欺骗连接迁移(第9.3.1小节),客户端能够使用源地址欺骗使得服务端将后续报文发往目的地址; + +* 欺骗数据包,使得服务端发送Version Negotiation包(第21.5.5小节)。 + +在所有情况下,攻击者都可以使其对端向可能不理解QUIC协议的受害者发送UDP报文。也就是说,这些数据包是在地址验证之前由对端发出,见第8章。 + +在数据包的加密部分之外,QUIC为终端提供了几个选项来控制其对端发送的UDP报文内容。DCID字段提供了对其对端发送的早期报文中某些字节的直接控制,参见第5.1节。客户端Initial包中的Token字段可以控制服务端Initial包某些字节,见第17.2.2小节。 + +此QUIC版本中没有任何措施来防止对数据包的加密部分进行间接控制。有必要假设终端能够控制对端发送的帧的内容,尤其是那些携带应用数据的帧,例如STREAM帧。尽管这在某种程度上取决于应用层协议的细节,但在许多协议中使用的上下文中可以进行一些控制。由于攻击者可以访问包保护密钥,他们很可能能够预测对端将如何加密未来的数据包。只需要能够以一定程度的成功概率预测到数据包数量和帧在数据包中的位置,攻击者就可以成功控制报文内容。 + +本节假设限制对报文内容的控制是不可行的。在后面的章节中所列对策的重点,是限制在地址验证之前发送的报文可用于请求伪造的方式。 + +### 21.5.2. 客户端Initial包请求伪造(Request Forgery with Client Initial Packets) +作为服务端的攻击者可以选择用于发布其可用性的IP地址和端口,因此假设来自客户端的Initial包可用来进行此类攻击。握手中隐含的地址验证确保对于新连接,客户端不会将其他类型的数据包发往不理解QUIC或不愿意接受QUIC连接的目的地。 + +Initial包保护([QUIC-TLS]第5.2节)使服务端难以控制客户端发送的Initial包的内容。选择不可预测的DCID的客户端使得服务端无法控制来自客户端的Initial包的任何加密部分。 + +但是,Token字段对服务端控制开放,并允许服务端使用客户端进行请求伪造攻击。使用NEW_TOKEN帧(第8.1.3小节)提供的令牌为连接建立期间的请求伪造提供了唯一选择。 + +但是,客户端没有义务使用NEW_TOKEN帧。如果客户端在接收到NEW_TOKEN帧后,服务端地址发生变化时发送空Token字段,则可以避免依赖Token字段的请求伪造攻击。 + +如果服务端地址发生变化,客户端可以避免使用NEW_TOKEN帧。但是,不包含Token字段可能会对性能产生不利影响。服务端可以依赖NEW_TOKEN来允许发送超过3倍限制的数据,见第8.1节。特别是,这会影响客户端使用0-RTT从服务端请求数据的情况。 + +发送Retry包(第17.2.5小节)为服务端提供了更改Token字段的选项。发送Retry包后,服务端还可以控制来自客户端的后续Initial包的DCID字段。这也可能允许对Initial包的加密内容进行间接控制。然而,Retry包的交换验证了服务端的地址,从而防止使用后续Initial包进行请求伪造。 + +### 21.5.3. 首选地址请求伪造(Request Forgery with Preferred Addresses) +服务端可以指定一个首选地址,客户端在握手确认后迁移到该地址,见第9.6节。客户端发往首选地址的数据包的DCID字段可用于请求伪造。 + +在验证该地址之前,客户端不得(**MUST NOT**)向首选地址发送非探测帧,见第8章。这大大减少了服务端需控制的数据包加密部分的选项。 + +本文不提供任何特定于首选地址的使用并且可由终端实施的额外对策。第21.5.6小节中描述的通用措施可用作进一步对策。 + +### 21.5.4. 欺骗迁移请求伪造(Request Forgery with Spoofed Migration) +客户端能够将欺骗源地址作为显式连接迁移的一部分,从而使服务端向该地址发送报文。 + +服务端随后发往此欺骗地址的任何数据包中的DCID字段可用于请求伪造。客户端也可能能够影响密文。 + +如果服务器在地址验证之前只发送探测包(第9.1节)到某个地址,则攻击者只能对数据包的加密部分进行有限的控制。然而,特别是对于NAT重新绑定,这会对性能产生不利影响。如果服务器发送携带应用数据的帧,则攻击者可能能够控制数据报文的大部分内容。 + +除了第21.5.6小节中描述的通用措施外,本文不提供可由终端实施的具体对策。然而,在网络级别针对地址欺骗的对策——特别是入向过滤[BCP38]——对于使用欺骗和源自外部网络的攻击特别有效。 + +### 21.5.5. 通过版本协商请求伪造(Request Forgery with Version Negotiation) +如果客户端能够在包上携带一个欺骗的源地址,那么服务端可以向该地址发送Version Negotiation包(第17.2.1小节)。 + +对于未知版本的包,CID字段没有大小限制,这增加了客户端从结果数据报控制的数据量。该数据包的第一个字节不受客户端控制,接下来的四个字节是零,但客户端可以控制从第5个字节开始的最多512个字节。 + +本文没有为此攻击提供具体的对策,但可以应用通用保护(第21.5.6小节)。在这种情况下,入向过滤[BCP38]也是有效的。 + +### 21.5.6. 通用请求伪造对策(Generic Request Forgery Countermeasures) +防御请求伪造攻击的最有效方法是修改易受攻击的服务以使用强认证。然而,这并不总是在QUIC部署的控制范围内。本节概述了QUIC终端可以单方面采取的其他一些步骤。这些额外的步骤可自行决定,因为根据情况,它们可能会干扰或阻止QUIC的合法用法。 + +通过环回接口提供的服务通常缺乏适当的认证。终端可以(**MAY**)阻止连接尝试或迁移到环回地址。如果同一服务以前在不同的接口可用,或者地址是由非环回地址的服务提供的,则终端不应该(**SHOULD NOT**)允许连接或迁移到环回地址。依赖于这些功能的终端可以提供禁用这些保护的选项。 + +类似地,终端可以视将地址从来自global、unique-local[RFC4193]或non-private范围,更改为link-local地址[RFC4291]或private-use[RFC1918]范围中的地址,作为潜在的请求伪造攻击尝试。终端可以完全拒绝使用这些地址,但这会带来干扰合法地址的显著风险。终端不应该(**SHOULD NOT**)拒绝使用地址,除非它们对网络有特定的了解,表明将报文发往给定范围内的未验证地址是不安全的。 + +终端可以(**MAY**)选择通过在Initial包中不包含来自NEW_TOKEN帧的值,或在完成地址验证之前仅在数据包中发送探测帧来降低请求伪造的风险。请注意,这并不能阻止攻击者使用DCID字段进行攻击。 + +终端不应具有关于某些服务端位置的特定信息,这些服务端可能成为请求伪造攻击的脆弱目标。但是,随着时间的推移,可能会识别出特定的UDP端口或特定的报文模式作为攻击的常见目标。在验证目的地址之前,终端可以(**MAY**)避免向这些端口发送报文或不发送与这些模式匹配的报文。终端可以(**MAY**)不使用包含已知有问题的模式的CID。 + +> 注意:修改终端以应用这些保护措施比部署基于网络的保护更有效,因为终端在发包到已验证的地址时不需要执行任何额外的处理。 + +## 21.6. Slowloris攻击(Slowloris Attacks) +这种攻击通常被称为Slowloris [Slowloris],它试图保持多个与目的终端的连接,并尽可能长时间地保持打开状态。针对QUIC终端,这些攻击可以通过生成避免因不活动而关闭连接所需的最小活动流量来实现。这可能包括发送少量数据,逐渐打开流控窗口以控制发送方速率,或模拟高丢包率生成ACK帧。 + +QUIC部署应该(**SHOULD**)为Slowloris攻击提供对策,例如增加服务端允许的最大客户端数量、限制单个IP地址允许建立的连接数量、限制连接允许的最低传输速度,并限制终端允许保持连接的时间长度。 + +## 21.7. 流分片和重组攻击(Stream Fragmentation and Reassembly Attacks) +恶意发送方可能故意不发送部分流数据,从而致使接收方为这些未发数据请求资源。这可能会致使不成比例的接收缓冲区内存分配和/或在接收方处创建大型且低效的数据结构。 + +恶意接收方可能故意不确认某些包含流数据的数据包,试图强制发送方存储大量未确认的流数据以进行重传。 + +如果流控窗口对应于可用内存,则可以减轻对接收方的攻击。但是,某些接收方会过量申请内存并通告超出实际可用内存的流控偏移。当终端表现良好时,超量策略可以带来更好的性能,但会使终端容易受到流分片攻击。 + +QUIC部署应该(**SHOULD**)为流分片攻击提供对策。这些措施可能包括避免过度申请内存、限制跟踪数据结构的大小、延迟重组STREAM帧、基于重组间隙的时间和持续时间采用启发式方法,或这些方法的某些组合。 + +## 21.8. 流提交攻击(Stream Commitment Attack) +恶意终端可以打开大量的流,耗尽对端状态。恶意终端可以在大量连接上重复该过程,其方式类似于TCP中的SYN洪水攻击。 + +通常,客户端将按顺序打开流,如第2.1节所述。然而,当几个流以较短间隔开启时,丢包或乱序可能致使打开流的STREAM帧被乱序接收。在接收到更高编号的流ID时,接收方需要打开所有相同类型的中间流,见第3.2节。因此,在新连接上,打开流ID为4000000的流将会打开1000001个客户端启动的双向流。 + +活动流的数量受initial_max_streams_bidi或initial_max_streams_uni传输参数的限制,并由MAX_STREAMS帧更新,如第4.6节所述。如果合理设定,这些限制可以减轻流提交攻击的影响。但是,当应用希望打开大量流时,将限制设置得太低可能会影响性能。 + +## 21.9. 对端拒绝服务攻击(Peer Denial of Service) +QUIC和TLS都包含在某些上下文中具有合法用途的帧或消息,但这些帧或消息可能会被滥用,致使对端消耗处理资源,而不会对连接状态产生任何可观察到的影响。 + +终端可以发送较小或无关紧要的消息来更改和恢复状态,例如通过向流控限制发送小增量。 + +如果与带宽消耗或对状态的影响相比,处理成本不成比例地大,那么这可能使得恶意对端耗尽处理能力。 + +虽然所有消息都有合法用途,但实现应该(**SHOULD**)跟踪与进度相关的处理成本,并将过量的任何非生产性数据包视为攻击的指示。终端可以(**MAY**)以连接错误关闭连接或丢弃数据包来响应这种情况。 + +## 21.10. 显式拥塞通知攻击(Explicit Congestion Notification Attacks) +on-path攻击者可以操纵IP报头中ECN字段的值来影响发送方的速率。[RFC3168]更详细地讨论了这种操作及其影响。 + +“受限”的on-path攻击者可以复制和发送带有被修改ECN字段的数据包,以影响发送方的速率。如果接收方会丢弃重复的数据包,攻击者需要将重复数据包与原数据包竞速才能在这次攻击中取得成功。因此,QUIC终端可忽略IP数据包中的ECN字段,除非该IP数据包中至少有一个QUIC包被成功处理,见第13.4节。 + +## 21.11. 无状态重置攻击( Stateless Reset Oracle) +无状态重置可能会产生类似于TCP重置注入的拒绝服务攻击。如果攻击者能够为具有特定CID的连接生成无状态重置令牌,则此攻击是可行的。导致此令牌生成的攻击者可重置具有相同CID的活动连接。 + +如果数据包可以路由到共享静态密钥的不同实例(例如,通过更改IP地址或端口),则攻击者可以使服务端发送Stateless Reset包。为了防止这种拒绝服务的类型,共享一个静态密钥用于无状态重置的终端必须(**MUST**)合理考虑(见第10.3.2小节),以便具有给定CID的数据包始终能到达具有同一连接状态的实例,除非该连接不再活跃。 + +更一般地,如果具有相应CID的连接可以在使用相同静态密钥的任何终端上处于活动状态,则服务端不得(**MUST NOT**)生成Stateless Reset包。 + +对于使用动态负载均衡的集群,当活动实例保持连接状态时,负载均衡器配置可能会发生变化。即使实例保持连接状态,路由的改变和由此产生的无状态重置也会致使连接被终止。如果无法将数据包路由到正确的实例,最好发送Stateless Reset包而不是等待连接超时。不过,这只有在路由不受攻击者影响时才可以接受。 + +## 21.12. 版本降级(Version Downgrade) +本文定义了QUIC的Version Negotiation包(第6章),可用于协商两端之间使用的QUIC版本。但是,本文没有具体说明如何在此版本和后续版本之间进行协商。特别是,Version Negotiation包中没有任何防止版本降级攻击的机制。未来使用版本协商的QUIC版本必须(**MUST**)定义一种对版本降级攻击具有鲁棒性的机制。 + +## 21.13. 通过路由进行针对性攻击(Targeted Attacks by Routing) +部署应该限制攻击者针对特定服务器实例的新连接进行攻击的能力。理想情况下,路由决策独立于客户端选择的值,包括地址。一旦选择了一个实例,就可以选择一个CID,以便后续数据包能路由到同一个实例。 + +## 21.14. 流量分析(Traffic Analysis) +QUIC包的长度可以揭示有关这些数据包内容长度的信息。PADDING帧的引入是为了使终端有模糊数据包内容长度的能力,见第19.1节。 + +对抗流量分析具有挑战性,也是一个主动研究的课题。长度不是信息泄漏的唯一途径。终端也可能从其他侧信道泄露敏感信息,例如数据包的耗时。 + +# 22. IANA考虑(IANA Considerations) +本文为管理QUIC码点新设了几个注册表,这些注册表遵循第22.1节中定义的一组通用策略。 + +## 22.1. QUIC注册表的注册策略(Registration Policies forQUICRegistries) +所有QUIC注册表都容许注册临时或永久性质的码点。本节描述通用的注册策略。 + +### 22.1.1. 临时注册(Provisional Registrations) +临时码点注册的引入旨在允许私有用途和实验性质的QUIC的扩展。临时注册申请只需要包含码点的值和联系人信息,同时,这类码点也可以被回收并重新分配用于其他目的。 + +根据[RFC8126]第4.5节的规定,临时注册需要专家审查。建议专家仅拒绝申请码点范围占用剩余码点空间比例过大,或申请第一个未分配值(参见第22.1.2小节)的注册。 + +临时注册包括一个Date字段,指示注册上次创建或更新的时间。可在不经指定专家审查的情况下提出更新任何临时注册日期的申请。 + +所有QUIC注册表都包含如下字段以支持临时注册: + +Value: 码点值。 + +Status:"永久"或"临时"。 + +Specification:引用该值的公开可用的规约。 + +Date:创建或更新日期。 + +Change Controller:负责该注册表项的实体。 + +Contact:注册人的联系方式。 + +Notes:关于该注册项的补充信息。 + +临时注册可以(**MAY**)省略Specification和Notes字段,也可以包含永久注册可能需要的其他字段。申请注册时不需要携带Date字段,因为其可被设置为正式创建或更新注册项的日期。 + +### 22.1.2. 码点选择(Selecting Codepoints) +向QUIC注册表申请新码点,应该(**SHOULD**)使用随机选择的值,该值不能是已经分配的值和所选空间第一个未分配的码点。一次申请多个码点的话,可以(**MAY**)选择一个连续的范围。这将不同的实现对同一码点赋予不同语义的风险降到最低。 + +第一个未分配的码点保留,需用Standards Action策略进行分配,参见[RFC8126]第4.9节。早期码点分配过程[EARLY-ASSIGN]可用于指导这些值的分配。 + +对于以变长整数(参见第16章)编码的码点,例如帧类型,应该(**SHOULD**)使用编码为4或8个字节(即2^14^及以上的值)的码点,除非其对较长编码特别敏感。 + +需要在QUIC注册表中注册码点的应用,可以(**MAY**)将申请码点作为注册过程的一部分。如果该码点尚未分配并且满足注册策略的要求,IANA必须(**MUST**)分配所选的码点。 + +### 22.1.3. 回收临时码点(Reclaiming Provisional Codepoints) +为了从注册表中回收一个或部分注册项(例如使用变长编码的64-16383范围的码点)释放注册表空间,可以申请删除未使用的临时注册项。应该(**SHOULD**)仅从具有最早记录日期的码点开始执行,并且不应该(**SHOULD NOT**)回收在不到一年之前更新的注册项。 + +删除码点的申请必须(**MUST**)由指定的专家审核。专家必须(**MUST**)先尝试确定码点是否仍在使用中。建议专家联系注册项中列出的联系人,以及尽可能广泛联系到协议的实现者,以确定其是否知道这些码点的使用情况。另外还建议专家至少留出四个星期的时间做出回应。 + +如果上述流程识别出码点正在使用中或有新提出更新注册项的申请,则不得(**MUST NOT**)回收码点,而应更新注册日期。修改时可以添加备注,记录了解到的相关情况。 + +如果识别出码点未在使用并且没有申请注册更新,则可以(**MAY**)从注册表中删除码点。 + +此审查和咨询过程也适用于将临时注册变更为永久注册的申请,但其目的不是确定是否有没有使用的码点,而是确定注册项是否准备表达了任何已部署的用途。 + +### 22.1.4. 永久注册(Permanent Registrations) +除非另有说明,QUIC注册表中的永久注册项遵循([RFC8126]第4.6节)指定的规约策略。指定的一个或多个专家验证规约是否存在且易于访问。鼓励专家偏向于批准注册,除非它们是滥用的、轻率的或有害的(不仅仅是看起来不漂亮或架构上可疑)。新建永久注册项时可以(**MAY**)指定附加限制。 + +新建注册表可能(**MAY**)需要指定一系列码点,这些码点分别由不同的注册策略管理。例如,QUIC Frame Types注册表(第22.4节)对0到63范围内的码点有更严格的策略。 + +对永久注册项的严格限制不会影响临时码点的注册。例如,可以申请临时注册帧类型为61的码点。 + +进入标准化阶段公布的注册表项都必须(**MUST**)是永久性的。 + +本文中的所有注册项都是永久状态,且随附有IETF的变更控制人和QUIC工作组的联系方式(quic@ietf.org)。 + +## 22.2. QUIC版本注册表(QUIC Versions Registry) +IANA在QUIC标题下为QUIC Versions添加了一个注册表。 + +QUIC Versions注册表管理32位空间,参见第15章。此注册表遵循第22.1节的注册策略。注册表中的永久注册项遵循[RFC8126]第4.6节的策略。 + +本文中定义的协议被分配了0x00000001码点。0x00000000码点是永久保留的,这个码点的注释有说明0x00000000是为版本协商保留的。 + +遵循模式0x?a?a?a?a的所有码点都是保留的,IANA不得(**MUST NOT**)分配,并且不得(**MUST NOT**)出现在分配值列表中。 + +## 22.3. QUIC传输参数注册表(QUIC Transport Parameters Registry) +IANA在QUIC标题下为QUIC Transport Parameters添加了一个注册表。 + +QUIC Transport Parameters注册表管理62位空间。此注册表遵循第22.1节中的注册策略。此注册表中的永久注册项遵循([RFC8126]第4.6节)规约策略进行分配,但0x00和0x3f(十六进制)之间的值除外,这些值使用[RFC8126]第4.9和4.10节定义的Standards Action或IESG许可进行分配。 + +除了第22.1.1小节中列出的字段外,此注册表中的永久注册项必须(**MUST**)包括以下字段: + +Parameter Name:参数名称,简短的参数助记符。 + +该注册表的初始内容如表6所示: + +| Value | Parameter Name | Specification | +|:----|:-----|:-----| +|0x00| original_destination_connection_id |第18.2节| +|0x01| max_idle_timeout |第18.2节| +|0x02| stateless_reset_token |第18.2节| +|0x03| max_udp_payload_size |第18.2节| +|0x04| initial_max_data |第18.2节| +|0x05| initial_max_stream_data_bidi_local |第18.2节| +|0x06| initial_max_stream_data_bidi_remote |第18.2节| +|0x07| initial_max_stream_data_uni |第18.2节| +|0x08| initial_max_streams_bidi |第18.2节| +|0x09| initial_max_streams_uni |第18.2节| +|0x0a| ack_delay_exponent |第18.2节| +|0x0b| max_ack_delay |第18.2节| +|0x0c| disable_active_migration |第18.2节| +|0x0d| preferred_address |第18.2节| +|0x0e| active_connection_id_limit |第18.2节| +|0x0f| initial_source_connection_id |第18.2节| +|0x10| retry_source_connection_id |第18.2节| + +表6: Initial QUIC Transport Parameters Registry + +对于形如 31 * N + 27(N为整数)值(即27, 58, 89, ...)都是保留的,这些值不得(**MUST NOT**)由IANA分配,也不得(**MUST NOT**)出现在分配值列表中。 + +## 22.4. QUIC帧类型注册表(QUIC Frame Types Registry) +IANA在QUIC标题下添加了一个QUIC Frame Types注册表。 + +QUIC Frame Types注册表管理62位空间。此注册表遵循第22.1节中的注册策略。此注册表中的永久注册项遵循([RFC8126]第4.6节)规约策略进行分配,但0x00和0x3f(十六进制)之间的值除外,这些值使用[RFC8126]第4.9和4.10节定义的Standards Action或IESG许可进行分配。 + +除了第22.1.1小节中列出的字段外,此注册表中的永久注册项必须(**MUST**)包括以下字段: + +Frame Type Name:帧类型名称,简短的帧类型助记符。 + +除了第22.1节的建议外,规约中增加永久注册项,都应该(**SHOULD**)描述终端是否可以发送该种类型的帧。大多数永久注册项都需要在传输参数中注册,参见第22.3节。另外,还需要描述帧格式及帧中所有字段的赋值语义。 + +初始注册表参见表3。请注意,注册表不包括表3中的Pkts和Spec列。 + +## 22.5. QUIC传输层错误码注册表(QUIC Transport Error Codes Registry) +IANA在QUIC标题下添加了QUIC Transport Error Codes的注册表。 + +QUIC Transport Error Codes注册表管理62位空间。该空间分为三个范围,由不同的策略管理。此注册表中的永久注册项分配要求遵循([RFC8126]第4.6节)规约策略,但0x00和0x3f(十六进制)之间的值除外,这些值使用[RFC8126]第4.9和4.10节定义的Standards Action或IESG许可进行分配。 + +除了第22.1.1小节中列出的字段外,此注册表中的永久注册项必须(**MUST**)包括以下字段: + +Code:错误码,简短参数助记符。 + +Description:描述,错误码语义的简要描述,如果被规约引用,则可能(**MAY**)是摘要。 + +初始注册表如表7所示。 + +|Value |Code |Description |Specification| +|:---|:---|:---|:---| +|0x00 |NO_ERROR |没有错误 |第20章| +|0x01 |INTERNAL_ERROR |实现错误 |第20章| +|0x02 |CONNECTION_REFUSED |服务端拒绝连接 |第20章| +|0x03 |FLOW_CONTROL_ERROR |流控错误 |第20章| +|0x04 |STREAM_LIMIT_ERROR |打开流数量超限制 |第20章| +|0x05 |STREAM_STATE_ERROR |在当前流状态下收到无效帧 |第20章| +|0x06 |FINAL_SIZE_ERROR |流上的FINAL SIZE出错 |第20章| +|0x07 |FRAME_ENCODING_ERROR |帧解码错 |第20章| +|0x08 |TRANSPORT_PARAMETER_ERROR |传输参数有误 |第20章| +|0x09 |CONNECTION_ID_LIMIT_ERROR |CID超限制 |第20章| +|0x0a |PROTOCOL_VIOLATION |通用协议错误 |第20章| +|0x0b |INVALID_TOKEN |收到无效令牌 |第20章| +|0x0c |APPLICATION_ERROR |应用程序错误 |第20章| +|0x0d |CRYPTO_BUFFER_EXCEEDED |CRYPTO缓存溢出 |第20章| +|0x0e |KEY_UPDATE_ERROR |收到无效的TLS KEY UPDATE请求 |第20章| +|0x0f |AEAD_LIMIT_REACHED |AEAD当前秘钥加密数据长度超限制(需要更换秘钥) |第20章| +|0x10 |NO_VIABLE_PATH |没有可用的网路路径 |第20章| +|0x0100-0x01ff |CRYPTO_ERROR |TLS告警码 |第20章| + +表7: Initial QUIC Transport Error Codes Registry Entries + +# 23. 参考资料(References) +## 23.1. 规范引用(Normative References) +[BCP38] +Ferguson, P. and D. Senie, "Network Ingress Filtering: Defeating Denial of Service Attacks which employ IP Source Address Spoofing", BCP 38, RFC 2827, May 2000, . + +[DPLPMTUD] +Fairhurst, G., Jones, T., Tüxen, M., Rüngeler, I., and T. Völker, "Packetization Layer Path MTU Discovery for Datagram Transports", RFC 8899, DOI 10.17487/RFC8899, September 2020, . + +[EARLY-ASSIGN] +Cotton, M., "Early IANA Allocation of Standards Track Code Points", BCP 100, RFC 7120, DOI 10.17487/RFC7120, January 2014, . + +[IPv4] +Postel, J., "Internet Protocol", STD 5, RFC 791, DOI 10.17487/RFC0791, September 1981, . + +[QUIC-INVARIANTS] +Thomson, M., "Version-Independent Properties of QUIC", RFC 8999, DOI 10.17487/RFC8999, May 2021, . + +[QUIC-RECOVERY] +Iyengar, J., Ed. and I. Swett, Ed., "QUIC Loss Detection and Congestion Control", RFC 9002, DOI 10.17487/RFC9002, May 2021, . + +[QUIC-TLS] +Thomson, M., Ed. and S. Turner, Ed., "Using TLS to Secure QUIC", RFC 9001, DOI 10.17487/RFC9001, May 2021, . + +[RFC1191] +Mogul, J. and S. Deering, "Path MTU discovery", RFC 1191, DOI 10.17487/RFC1191, November 1990, . + +[RFC2119] +Bradner, S., "Key words for use in RFCs to Indicate Requirement Levels", BCP 14, RFC 2119, DOI 10.17487/RFC2119, March 1997, . + +[RFC3168] +Ramakrishnan, K., Floyd, S., and D. Black, "The Addition of Explicit Congestion Notification (ECN) to IP", RFC 3168, DOI 10.17487/RFC3168, September 2001, . + +[RFC3629] +Yergeau, F., "UTF-8, a transformation format of ISO 10646", STD 63, RFC 3629, DOI 10.17487/RFC3629, November 2003, . + +[RFC6437] +Amante, S., Carpenter, B., Jiang, S., and J. Rajahalme, "IPv6 Flow Label Specification", RFC 6437, DOI 10.17487/RFC6437, November 2011, . + +[RFC8085] +Eggert, L., Fairhurst, G., and G. Shepherd, "UDP Usage Guidelines", BCP 145, RFC 8085, DOI 10.17487/RFC8085, March 2017, . + +[RFC8126] +Cotton, M., Leiba, B., and T. Narten, "Guidelines for Writing an IANA Considerations Section in RFCs", BCP 26, RFC 8126, DOI 10.17487/RFC8126, June 2017, . + +[RFC8174] +Leiba, B., "Ambiguity of Uppercase vs Lowercase in RFC 2119 Key Words", BCP 14, RFC 8174, DOI 10.17487/RFC8174, May 2017, . + +[RFC8201] +McCann, J., Deering, S., Mogul, J., and R. Hinden, Ed., "Path MTU Discovery for IP version 6", STD 87, RFC 8201, DOI 10.17487/RFC8201, July 2017, . + +[RFC8311] +Black, D., "Relaxing Restrictions on Explicit Congestion Notification (ECN) Experimentation", RFC 8311, DOI 10.17487/RFC8311, January 2018, . + +[TLS13] +Rescorla, E., "The Transport Layer Security (TLS) Protocol Version 1.3", RFC 8446, DOI 10.17487/RFC8446, August 2018, . + +[UDP] +Postel, J., "User Datagram Protocol", STD 6, RFC 768, DOI 10.17487/RFC0768, August 1980, . + +## 23.2. 资料引用(Informative References) +[AEAD] +McGrew, D., "An Interface and Algorithms for Authenticated Encryption", RFC 5116, DOI 10.17487/RFC5116, January 2008, . + +[ALPN] +Friedl, S., Popov, A., Langley, A., and E. Stephan, "Transport Layer Security (TLS) Application-Layer Protocol Negotiation Extension", RFC 7301, DOI 10.17487/RFC7301, July 2014, . + +[ALTSVC] +Nottingham, M., McManus, P., and J. Reschke, "HTTP Alternative Services", RFC 7838, DOI 10.17487/RFC7838, April 2016, . + +[COOKIE] +Barth, A., "HTTP State Management Mechanism", RFC 6265, DOI 10.17487/RFC6265, April 2011, . + +[CSRF] +Barth, A., Jackson, C., and J. Mitchell, "Robust defenses for cross-site request forgery", Proceedings of the 15th ACM conference on Computer and communications security - CCS '08, DOI 10.1145/1455770.1455782, 2008, . + +[EARLY-DESIGN] +Roskind, J., "QUIC: Multiplexed Stream Transport Over UDP", 2 December 2013, . + +[GATEWAY] +Hätönen, S., Nyrhinen, A., Eggert, L., Strowes, S., Sarolahti, P., and M. Kojo, "An experimental study of home gateway characteristics", Proceedings of the 10th ACM SIGCOMM conference on Internet measurement - IMC '10, DOI 10.1145/1879141.1879174, November 2010, . + +[HTTP2] +Belshe, M., Peon, R., and M. Thomson, Ed., "Hypertext Transfer Protocol Version 2 (HTTP/2)", RFC 7540, DOI 10.17487/RFC7540, May 2015, . + +[IPv6] +Deering, S. and R. Hinden, "Internet Protocol, Version 6 (IPv6) Specification", STD 86, RFC 8200, DOI 10.17487/RFC8200, July 2017, . + +[QUIC-MANAGEABILITY] +Kuehlewind, M. and B. Trammell, "Manageability of the QUIC Transport Protocol", Work in Progress, Internet-Draft, draft-ietf-quic-manageability-11, 21 April 2021, . + +[RANDOM] +Eastlake 3rd, D., Schiller, J., and S. Crocker, "Randomness Requirements for Security", BCP 106, RFC 4086, DOI 10.17487/RFC4086, June 2005, . + +[RFC1812] +Baker, F., Ed., "Requirements for IP Version 4 Routers", RFC 1812, DOI 10.17487/RFC1812, June 1995, . + +[RFC1918] +Rekhter, Y., Moskowitz, B., Karrenberg, D., de Groot, G. J., and E. Lear, "Address Allocation for Private Internets", BCP 5, RFC 1918, DOI 10.17487/RFC1918, February 1996, . + +[RFC2018] +Mathis, M., Mahdavi, J., Floyd, S., and A. Romanow, "TCP Selective Acknowledgment Options", RFC 2018, DOI 10.17487/RFC2018, October 1996, . + +[RFC2104] +Krawczyk, H., Bellare, M., and R. Canetti, "HMAC: Keyed-Hashing for Message Authentication", RFC 2104, DOI 10.17487/RFC2104, February 1997, . + +[RFC3449] +Balakrishnan, H., Padmanabhan, V., Fairhurst, G., and M. Sooriyabandara, "TCP Performance Implications of Network Path Asymmetry", BCP 69, RFC 3449, DOI 10.17487/RFC3449, December 2002, . + +[RFC4193] +Hinden, R. and B. Haberman, "Unique Local IPv6 Unicast Addresses", RFC 4193, DOI 10.17487/RFC4193, October 2005, . + +[RFC4291] +Hinden, R. and S. Deering, "IP Version 6 Addressing Architecture", RFC 4291, DOI 10.17487/RFC4291, February 2006, . + +[RFC4443] +Conta, A., Deering, S., and M. Gupta, Ed., "Internet Control Message Protocol (ICMPv6) for the Internet Protocol Version 6 (IPv6) Specification", STD 89, RFC 4443, DOI 10.17487/RFC4443, March 2006, . + +[RFC4787] +Audet, F., Ed. and C. Jennings, "Network Address Translation (NAT) Behavioral Requirements for Unicast UDP", BCP 127, RFC 4787, DOI 10.17487/RFC4787, January 2007, . + +[RFC5681] +Allman, M., Paxson, V., and E. Blanton, "TCP Congestion Control", RFC 5681, DOI 10.17487/RFC5681, September 2009, . + +[RFC5869] +Krawczyk, H. and P. Eronen, "HMAC-based Extract-and-Expand Key Derivation Function (HKDF)", RFC 5869, DOI 10.17487/RFC5869, May 2010, . + +[RFC7983] +Petit-Huguenin, M. and G. Salgueiro, "Multiplexing Scheme Updates for Secure Real-time Transport Protocol (SRTP) Extension for Datagram Transport Layer Security (DTLS)", RFC 7983, DOI 10.17487/RFC7983, September 2016, . + +[RFC8087] +Fairhurst, G. and M. Welzl, "The Benefits of Using Explicit Congestion Notification (ECN)", RFC 8087, DOI 10.17487/RFC8087, March 2017, . + +[RFC8981] +Gont, F., Krishnan, S., Narten, T., and R. Draves, "Temporary Address Extensions for Stateless Address Autoconfiguration in IPv6", RFC 8981, DOI 10.17487/RFC8981, February 2021, . + +[SEC-CONS] +Rescorla, E. and B. Korver, "Guidelines for Writing RFC Text on Security Considerations", BCP 72, RFC 3552, DOI 10.17487/RFC3552, July 2003, . + +[SLOWLORIS] +"RSnake" Hansen, R., "Welcome to Slowloris - the low bandwidth, yet greedy and poisonous HTTP client!", June 2009, . diff --git a/docs/translation/rfc9114-http3-zh.md b/docs/translation/rfc9114-http3-zh.md new file mode 100644 index 000000000..405b91260 --- /dev/null +++ b/docs/translation/rfc9114-http3-zh.md @@ -0,0 +1,1298 @@ +# 0. Abstract +QUIC具有HTTP传输层协议所需的几个特性:流多路复用、逐流流控及低时延连接建立。本文描述了HTTP语义在QUIC上的映射,同时也描述了QUIC协议中来自HTTP/2的特性,并说明了HTTP/2扩展如何移植到HTTP/3。 + +# 1. Introduction + +互联网上大量服务使用了HTTP语义,这些语义通常在HTTP/1.1和HTTP/2协议层上表示。HTTP/1.1基于各种传输层和会话层协议,而HTTP/2主要由TCP之上的TLS层承载。HTTP/3在一个新的传输层协议QUIC上支持了相同的语义。 + +## 1.1. Prior versions of HTTP + +HTTP/1.1([[HTTP/1.1]](#http1))使用以空格分隔的文本字段来表示HTTP消息。虽然这种方式的可读性较好,但是使用空白来格式化消息,会增加解析的复杂度和面对更多变数。 + +由于HTTP/1.1不包括多路复用层,所以经常使用多个并发的TCP连接发送请求。这对拥塞控制和网络效率有负面影响,因为TCP不会跨连接共享拥塞控制。 + +HTTP/2([[HTTP/2]](#http2))引入了二进制组帧和多路复用层,从而可以在不修改传输层的前提下改善延迟。然而,因为HTTP/2多路复用的并发原理对于TCP的丢包恢复机制是不可见的,一个数据包的丢失或乱序就会导致所有活动的事务暂停,而不管事务是否受到这个数据包的影响。 + +## 1.2. Delegation to QUIC + +QUIC传输层协议纳入了流多路复用和逐流流控,与HTTP/2组帧层提供的特性类似。通过在stream层提供可靠性,在整个连接上提供拥塞控制,相对于TCP,QUIC有能力改善HTTP的性能。 +QUIC同样在传输层引入了TLS 1.3([[TLS]](#tls)),提供了与TLS over TCP同等的机密性和完整性保护,改善了TCP快速打开[[TFO]](#tfo)带来的建连延迟。 + +本文定义了一种基于QUIC传输协议的HTTP语义映射方式,主要借鉴了HTTP/2的设计。HTTP/3依靠QUIC提供数据的机密性和完整性保护、身份验证,以及流上的可靠有序传输。HTTP/3将流的生命周期和流控管理委托给了QUIC,每条流上也采用了与HTTP/2类似的二进制组帧方式。部分HTTP/2特性归入到QUIC协议中,其余的特性仍由QUIC的上层(HTTP/3协议)实现。 + +QUIC核心协议参见[[QUIC-TRANSPORT]](#quic),HTTP/2完整说明参见[[HTTP/2]](#http2)。 + +# 2. HTTP/3 Protocol Overview + +HTTP/3通过QUIC协议传输HTTP语义,并提供了类似HTTP/2的内部组帧层。 + +一旦客户端得知某个指定终端上支持HTTP/3服务,它就可以打开一个QUIC连接。QUIC提供了协议协商,基于流的多路复用以及流控。3.1节中描述了如何发现HTTP/3终端。 + +在每条流中,HTTP/3通信的基本单元是帧(frame,参见7.2节)。不同帧类型对应不同的用途。比如,HTTP请求和响应由HEADERS和DATA帧承载(参考4.1节),适用于整个连接的帧在专用的控制流上传输。 + +在[[QUIC-TRANSPORT]](#quic)第2章中,描述了如何使用QUIC流来实现请求的多路复用。每个request-response对由一条QUIC流承载。流之间是独立的,一条流上出现丢包或阻塞不会影响其他流的前进。 + +服务端推送(Server push)是由HTTP/2([[HTTP/2]](#http2))引入的交互模式,这种模式允许服务端在客户端发送request消息之前提前推送预测的response。这是在牺牲网络利用率但获得潜在延迟收益之间做的取舍。HTTP/3使用了一些帧来实施服务端推送功能,比如PUSH_PROMISE,MAX_PUSH_ID以及CANCEL_PUSH。 + +在HTTP/2中,request与response字段遵循HPACK协议进行头压缩后再传输。因为HPACK [[HPACK]](#hpack)要求连接上所有流数据都保序投递(QUIC不提供这样的保证),所以HTTP/3用QPACK [[QPACK]](#qpack) 协议取代了HPACK。QPACK使用独立的单向流来修改和跟踪field表状态,而(数据流上的)encoded field段只引用表状态而不做修改。 + +## 2.1. Document Organization + +以下章节提供了HTTP/3连接的生命周期详细概述: + +* “连接建立和管理”(第3章)涵盖了如何发现HTTP/3终端,以及如何建立HTTP/3连接。 +* “用HTTP/3表示HTTP语义”(第4章)描述了如何使用帧来表示HTTP语义。 +* “连接关闭”(第5章)描述了如何关闭HTTP/3连接,包括优雅关闭和立即关闭。 + +以下章节描述了上层协议与传输的交互细节: + +* “stream映射方式与用法”(第6章)描述了QUIC流的使用方式。 +* “HTTP组帧层”(第7章)描述了在大多数流上使用的帧类型。 +* “错误处理”(第8章)描述了如何处理和表示错误条件,无论是在特定流上还是整个连接上。 + +最后章节提供了如下资源: + +* “HTTP/3扩展”(第9章)描述了如何在未来的文档中添加新功能。 +* 附录A提供了HTTP/2 与 HTTP/3之间更具体的对比。 + +## 2.2. Conventions and Terminology + +终止(abort):连接或流的立即终止,可能由错误条件引起。 + +客户端(client):发起HTTP/3连接的终端。客户端发送HTTP请求,并接受HTTP响应。 + +连接(connection):两个终端之间的传输层连接,使用QUIC作为传输层协议。 + +连接错误(connection error):影响整个HTTP/3连接的错误。 + +终端(endpoint):连接的客户端或者服务端。 + +帧(frame):HTTP/3在流上通信时的最小单元,包含一个帧头和一个根据帧类型组织的变长字节序列。 +本文和[[QUIC-TRANSPORT]](#quic)中都存在称为“帧”的协议元素。当提及[[QUIC-TRANSPORT]](#quic)中的帧时,帧的名字之前会添加前缀“QUIC”。比如,“QUIC CONNECTION_CLOSE 帧”。没有这个前缀的帧均指7.2节中定义的帧。 + +HTTP/3连接(HTTP/3 connection):一个QUIC连接,其上协商的应用层协议是HTTP/3。 + +对端(peer):一个终端。当提及某个终端时,"对端"指的是与当前讨论对象遥相对应的终端。 + +接收端(receiver):正在接收帧的终端。 + +发送端(sender):正在发送帧的终端。 + +服务端:接受HTTP/3连接的终端。 服务端接收HTTP请求,并发送HTTP响应。 + +流(stream):QUIC传输提供的一个双向或者单向的字节流。一个HTTP/3连接内的所有流都可以被认为是"HTTP/3流",但HTTP/3内定义了多种流类型。 + +流错误(stream error):个别HTTP/3流上的应用级(application-level)错误。 + +术语 "content" 在 [[HTTP]](#http) 第6.4节中定义。 + +最后,术语 "resource"、"message"、"user agent"、"origin server"、"gateway"、"intermediary"、"proxy" 和 "tunnel"在 [[HTTP]](#http) 第3章中定义。 + +本文中的数据包图表使用 [[QUIC-TRANSPORT]](#quic) 第1.3节中定义的格式来说明字段的顺序和大小。 + +# 3. Connection Setup and Management + +## 3.1. Discovering an HTTP/3 Endpoint + +HTTP依赖于权威响应的概念:一个由目标URI标识所确定的服务端的响应,是在给定响应消息生成时的目标资源状态下,该请求的最合适的响应。在 [[HTTP]](#http) 的4.3节中讨论了如何给一个HTTP URI确定权威服务端。 + +“Https”模式将权限和证书结合起来,其中客户端认为(通过URI的权威模块识别的)主机的证书是可信的。在收到TLS握手的服务端证书后,客户端必须(**MUST**)使用[[HTTP]](#http)第4.3.4节中描述的过程来验证该证书是否与URI的源服务端相匹配。如果该证书不能对URI的源服务端进行验证,客户端不得(**MUST NOT**)考虑该服务端对该源的权威性。 + +客户端可以(**MAY**)使用带“https”的URI来访问资源:首先将主机标识解析为IP地址,接着建立一个到这个地址和指定端口的QUIC连接(包括上述服务端证书的验证),然后在这个安全的连接上发送一个将URI指向该服务端的HTTP/3请求消息。除非有其他机制选择HTTP/3,我们在TLS握手过程中,在应用层协议协商(ALPN;见[[RFC7301]](#rfc7301))扩展中使用"h3"标识来选择HTTP/3协议。 + +连通性问题(例如,UDP阻塞)可能导致QUIC建连失败;这种情况下客户端应当(**SHOULD**)尝试使用基于TCP的HTTP版本。 + +服务端可以(**MAY**)在任意UDP端口上开启HTTP/3服务;一个可选的服务通告通常包含一个显式指定的端口,其URI包含一个显式端口或者使用基于此模式的默认端口(译者注:如http默认80端口,https默认433端口)。 + +### 3.1.1. HTTP Alternative Services + +终端可以通过HTTP/1 Alt-Svc响应头或者HTTP/2 ALTSVC帧宣告自己支持HTTP/3。 + +例如如下格式的响应头表示相同主机下的50781端口支持HTTP/3: + +Alt-Svc: h3=":50781" + +当接收到表明支持HTTP/3的响应头时,客户端可以(**MAY**)尝试在给定的主机IP和端口上建立QUIC连接,如果连接建立成功,客户端可以使用本文中描述的对应方式发送HTTP请求。 + +### 3.1.2. Other Schemes + +尽管HTTP独立于传输协议,但“http”模式将权限和在权限模块认证的,任意主机的指定端口上,接收TCP连接的能力结合起来。由于HTTP/3不使用TCP,因此它无法直接访问权威服务端上由“http”URI标识的资源。尽管如此,诸如[[ALTSVR]](#altsvr)的协议扩展允许权威服务端识别其他同样具有权威性且可通过HTTP/3访问的服务。 + +在对不是“https”模式的源发送请求之前,客户端必须(**MUST**)确认服务端愿意为该模式提供服务。 +对于“http”模式的源,[[RFC8164]](#rfc8164)中描述了一种实验性的方法来实现这一目标。将来也可能会为其他模式定义对应机制。 + +## 3.2 Connection Establishment + +HTTP/3目前依赖QUIC version 1作为底层传输,将来的规范可能(**MAY**)会定义HTTP/3使用的其他QUIC版本。 + +QUIC握手协议版本大于等于TLS 1.3。HTTP/3客户端必须(**MUST**)支持在TLS握手期间向服务端指示目的主机的机制。如果服务端由域名([[DNS-TERMS]](#dns-terms))标识,则客户端必须(**MUST**)发送服务端名称指示(SNI; [[RFC6066]](#rfc6066))TLS扩展,除非使用了其他指示目的主机的替代机制。 + +QUIC连接的建立如 [[QUIC-TRANSPORT]](#quic) 所述。在建立连接的过程中,TLS 握手中 ALPN 标识 "h3" 用来表明是否支持HTTP/3。对其他应用层协议的支持也可以(**MAY**)在这次握手中提供。 + +虽然与核心QUIC协议有关的连接级选项是在初始加密握手中设置的,但HTTP/3特定的设置项是在SETTINGS帧中表示的。在建立QUIC连接后,每个终端必须(**MUST**)在相应的HTTP控制流上发送一个SETTINGS帧作为初始帧。 + +## 3.3. Connection Reuse + +HTTP/3连接可用于多个请求。为了性能考虑,客户端一般不会主动关闭连接,除非客户端认为不再需要继续和服务端进行通信(比如,用户离开相应web页),或者直到服务端关闭连接。 + +一旦建立上与服务端的连接,该连接可以(**MAY**)用于请求其他URI,只要服务端是经过认证的。要将现有连接用于新的源,客户端必须(**MUST**)通过 [[HTTP]](#http) 第4.3.4节中所述的过程,来验证服务端为新的源服务端提供的证书。这意味着客户端将需要保留服务端证书以及验证该证书所需的任何其他信息,不这样做的客户端将无法为其他的源重用该连接。 + +如果证书因一些原因不能被新源接受,则该连接不得(**MUST NOT**)被重用,应该(**SHOULD**)为新源建立新连接。如果证书验证不通过的原因也可能适用于其他与此连接相关联的源,则客户端应该(**SHOULD**)为这些源重新验证服务端证书。例如,如果因为过期或被撤销的原因导致证书验证失败,则可能导致使用该证书进行授权的所有其他源失效。 + +客户端不应(**SHOULD NOT**)打开多个到给定IP地址和UDP端口的HTTP/3连接,其中IP地址和端口可能来自URI、选定的替代服务([[ALTSVC]](#altsvc))、配置的代理或名称解析中的任何一个。客户端可以(**MAY**)使用不同的传输或TLS配置打开到相同IP地址和UDP端口的多个HTTP/3连接,但应避免使用相同配置创建多个连接。 + +建议服务端尽可能长地维护HTTP/3连接,但在必要时允许终止空闲连接。如果任一终端选择关闭HTTP/3连接,它应该(**SHOULD**)先发送一个GOAWAY帧(5.2节),这样两端可以明确知道之前发送的帧是否已被处理,且可以优雅完成或终止剩下的所有任务。 + +如果服务端不希望客户端针对特定来源复用HTTP/3连接,可以通过在请求响应中发送421(错误定向请求)状态码来表明该请求对服务端不具有权威性(见 [[HTTP]](#http) 的7.4节) + +# 4. Expressing HTTP Semantics in HTTP/3 + +## 4.1. HTTP Message Framing + +客户端在其发起的双向QUIC流上发送HTTP请求,见第 6.1 节。客户端必须(**MUST**)在给定的请求流上只发送一个请求,服务端在此流上发送零到多个中间HTTP响应,最后接一个最终HTTP响应,如下所述。关于中间和最终HTTP响应的描述,请参见[[HTTP]](#http)的第15章。 + +推送的响应在由服务端发起的单向QUIC流上进行发送,参见6.2.2小节。与标准的响应方式一样,服务端先发送零到多个中间HTTP响应,最后接一个最终HTTP响应。在4.6节中有对推送做更详尽的描述。 + +如果在给定的流上收到多个请求,或者在最终HTTP响应之后再收到一个HTTP响应,则必须(**MUST**)将这种情况视为异常。 + +一个HTTP消息(请求或响应)由以下内容组成: + +1. 头部段:包含控制消息,由单个HEADERS帧发送; +2. (可选项)载荷:如果存在,由一系列DATA帧发送; +3. (可选项)尾部段:如果存在,由单个HEADERS帧发送; + +头部段和尾部段分别在 [[HTTP]](#http) 的6.3和6.5节中描述,载荷在 [[HTTP]](#http) 的6.4节中有说明。 + +如果收到一系列无效帧,必须(**MUST**)视为H3_FRAME_UNEXPECTED类型的连接错误(第8章)。特别是,在HEADERS帧之前收到DATA帧,在尾部HEADERS帧之后收到HEADERS或DATA帧,都是无效的。其他帧类型(尤其是未知帧类型)可能由其自身规则是容许的,参见第9章。 + +服务端可以(**MAY**)在响应消息帧之前、之后、中间发送一个或多个PUSH_PROMISE帧。这些PUSH_PROMISE帧不是响应的一部分(参考4.6节)。PUSH_PROMISE帧不允许出现在推送流中;必须(**MUST**)把包含PUSH_PROMISE的推送响应视为H3_FRAME_UNEXPECTED类型的连接错误。 + +在一个请求或者推送流上,未知类型的帧(第9章),包括保留帧(第7.2.8小节),可以(**MAY**)在本章描述的其他帧之前、之后、中间交替发送。 + +HEADERS和PUSH_PROMISE帧可能引用QPACK动态表的更新。虽然这些更新不是消息交换的直接部分,但是他们必须在消息消费前被接收并处理,详情参考 4.2节。 + +HTTP/3未定义传输编码(请参见[[HTTP/1.1]](#http1)第7章),因此不得(**MUST NOT**)使用Transfer-Encoding标头字段。 + +响应可以(**MAY**)包含多个消息 当且仅当 在同一个请求中,最终响应消息之前有一个或多个中间响应消息(1xx,参考 [[HTTP]](#http) 第15.2节)。中间响应消息不包含载荷或尾部段。 + +一个HTTP请求/响应交换完全占用客户端发起的双向QUIC流。在发送请求之后,客户端必须(**MUST**)关闭流的发送。除非使用CONNECT方法(参考4.4节),否则客户端不得(**MUST NOT**)由接收到对应响应来决定关闭流。在发送最终响应之后,服务端必须(**MUST**)关闭流的发送。此刻,QUIC流就完全关闭了。 + +当一个流关闭时,这表示最终HTTP消息的结束。由于某些消息很大或无上限,终端应该(**SHOULD**)在收到足够长度的消息可以进行后续动作时开始处理这部分HTTP消息。如果客户端发起的流,在没有发送足够长度的HTTP请求消息,服务端不能给出完整响应的情况下终止,则服务端应该(**SHOULD**)中止其响应流并返回错误码H3_REQUEST_INCOMPLETE。 + +如果响应不取决于尚未发送和接收的请求的任何部分,则服务端可以在客户端发送完整个请求之前发送完整的响应。当服务端不需要接收请求的剩余部分时,它可能(**MAY**)会中止读取请求流,发送完整的响应,并干脆地关闭流的发送部分。请求客户端停止在请求流上发送时应该(**SHOULD**)使用错误码H3_NO_ERROR。客户端不得(**MUST NOT**)因为他们的请求被立即终止而丢弃完整的响应,不过出于其他原因,其自行决定丢弃响应。如果服务端发送了部分或完整的响应但没有中止读取请求,客户端应该(**SHOULD**)继续发送请求的载荷并正常关闭流。 + +### 4.1.1. Request Cancellation and Rejection + +一旦请求流已经打开,任一终端都可以(**MAY**)关闭它。如果对响应不再感兴趣,客户端可以关闭流,如果服务端无法响应或选择不响应,则服务端也可以取消请求。如果可能,建议(**RECOMMENDED**)服务端发送带有适当状态码的HTTP响应,而不是取消它已经开始处理的请求。 + +实现应该(**SHOULD**)(SHOULD)通过立即终止仍然打开的流的任何方向来取消请求。为此,实现会重置流的发送部分并中止对流的接收部分的读取;请参阅[[QUIC-TRANSPORT]](#quic)的第2.4节。 + +当服务端取消请求而不执行任何应用层处理时,该请求被视为“拒绝”。服务端应该(**SHOULD**)以错误码H3_REQUEST_REJECTED 中止其响应流. 在这种情况下,“已处理”意味着来自流的一些数据已经被投递到某些更上层的软件,这些软件可能因此执行了一些动作。客户端可以将被服务端拒绝的请求视为根本没有发送过,因此稍后允许进行重试。 + +对已经部分或全部处理的请求,服务端不得(**MUST NOT**)返回H3_REQUEST_REJECTED错误码。在服务端在部分处理后放弃响应时,它 应该(**SHOULD**)以错误码H3_REQUEST_CANCELLED中止其响应流。 + +客户端应该(**SHOULD**)以错误码H3_REQUEST_CANCELLED取消请求。收到此错误码后,如果没有进行任何处理,服务端可以(**MAY**)回以错误码H3_REQUEST_REJECTED,并立即终止响应。客户端不得(**MUST NOT**)使用H3_REQUEST_REJECTED错误码,除非服务端以此错误码请求关闭请求流。 + +如果在收齐完整响应后再取消流,客户端可以(**MAY**)忽略此取消并使用此响应消息。但是,如果在收到部分响应后取消流,则不应使用该响应消息。只有GET、PUT或DELETE等幂等操作可以安全地重试;客户端不应(**SHOULD NOT**)使用非幂等方法自动重试请求,除非它有某种方法可以知道请求语义是独立于方法的幂等操作,或者有某种方法可以检测到原始请求从未被应用。有关详细信息,请参阅[[HTTP]](#http)的第 9.2.2 节。 + +### 4.1.2. Malformed Requests and Responses + +格式错误的请求或响应是某些方面有效的帧,但由于以下原因而无效: + +* 存在禁用的字段或伪标头字段; +* 缺失必选伪标头字段; +* 伪标头字段为无效值; +* 伪标头字段位于其他字段之后; +* HTTP消息错误; +* 字段名包含大写字母; +* 字段名或值中包含无效字符; + +定义为包含内容的响应类型,如果Content-Length标头字段([[HTTP]](#http)的第8.6节)的值不等于接收到的DATA帧长度的总和,则认为是格式错误的。定义为从不包含内容的响应类型(即使存在Content-Length)可以具有非零的Content-Length标头字段,即使DATA帧中不包含任何内容也没关系。 + +处理HTTP请求或响应的中间设备(即任何不充当隧道的中间设备)不得(**MUST NOT**)转发格式错误的请求或响应。检测到格式错误的请求或响应必须(**MUST**)视为H3_MESSAGE_ERROR流错误。 + +对于格式错误的请求,服务端可以(**MAY**)在关闭或重置流之前发送一个指示错误的HTTP响应。客户端不得(**MUST NOT**)接受格式错误的响应。请注意,这些要求旨在防止针对HTTP的几种常见攻击,这种严格约束是故意为之的,可以防止将实现暴露给这些漏洞。 + +## 4.2. HTTP Fields + +HTTP消息以一系列称为“HTTP字段”的键值对的方式携带元数据;请参阅[[HTTP]](#http)的第6.3和6.5节。有关已注册HTTP字段列表,请参阅维护于 的“Hypertext Transfer Protocol (HTTP) Field Name Registry”一节。HTTP/3在字段名、连接头字段和伪标头字段中,具有与HTTP/2一样的与字符相关的其他注意事项。 + +字段名是包含ASCII字符子集的字符串。[[HTTP]](#http)的5.1节更详细地讨论了HTTP字段名和值的属性。字段名中的字符必须(**MUST**)在编码之前转换为小写。字段名中包含大写字符的请求或响应必须(**MUST**)被视为格式错误。 + +HTTP/3不使用连接头字段来指示连接相关的字段;在此协议中,连接相关的元数据通过其他方式传送。终端不得(**MUST NOT**)生成包含连接相关字段的HTTP/3段;任何包含连接相关字段的消息必须(**MUST**)被视为格式错误。 + +唯一的例外是TE标头字段,它可以(**MAY**)出现在HTTP/3请求标头中;如果有,它不得(**MUST NOT**)包含除“trailers”之外的其他值。 + +将HTTP/1.x消息转换为HTTP/3的中间节点必须(**MUST**)删除如[[HTTP]](#http)的第7.6.1节所述的连接相关的头字段,否则它们的消息将被其他HTTP/3终端视为格式错误。 + +### 4.2.1. Field Compression +[[QPACK]](#qpack)描述了HPACK协议的一种变更,它使编码器可以(**MAY**)控制压缩导致的队头阻塞量。这使得编码器可以平衡压缩效率和网络延迟。HTTP/3使用QPACK来压缩头部和尾部部分,包括头部部分中的控制数据。 + +为了提高压缩效率,Cookie标头字段([[COOKIES]](#cookies))可以在压缩之前拆分为单独的字段行,每行包含一个或多个cookie对。如果解压缩的字段部分包含多个cookie字段行,则在投递到HTTP/2或HTTP/3以外,如HTTP/1.1连接或通用HTTP服务端应用的上下文之前,必须(**MUST**)使用"; "(ASCII 0x3b,0x20)的两字节分隔符将它们串接成字符串。 + +### 4.2.2. Header Size Constraints + +HTTP/3实现可以(**MAY**)限制它在单个HTTP消息上可接受的标头的最大长度。收到标头超过此长度的服务端可以发送HTTP431(请求头字段超长 Request Header Fields Too Large)状态码([[RFC6585]](#rfc6585))。客户端可以丢弃它无法处理的响应。字段列表的长度是根据未压缩的字段长度计算的,包括名称和值的长度(以字节为单位)加上每个字段32字节的开销。 + +如果一个实现希望向对端通告此限制,它可以字节为单位设置 SETTINGS_MAX_FIELD_SECTION_SIZE的值。收到此参数的实现不应(**SHOULD NOT**)发送超过指示大小的HTTP消息头,因为对端可能会拒绝处理它。但是,HTTP消息在到达源服务端之前可能经过一个或多个中间设备;请参阅[[HTTP]](#http)的第3.7节。因为这个限制是由处理消息的每个实现单独处理的,所以低于此限制的消息能否被接受也不能保证。 + +## 4.3. HTTP Control Data + +与HTTP/2一样,HTTP/3 使用一系列伪标头字段,其中字段名以“:”字符(ASCII 0x3a)开头。这些伪标头字段表示消息控制数据;请参阅[[HTTP]](#http)的第6.2节。 + +伪标头字段不是HTTP字段。终端不得(**MUST NOT**)生成除本文定义以外的伪标头字段。但是,有个扩展可以协商修改此限制;见第9章。 + +伪标头字段仅在定义它们的上下文中有效。为请求定义的伪标头字段不得(**MUST NOT**)出现在响应中;为响应定义的伪标头字段不得(**MUST NOT**)出现在请求中。伪标头字段不得(**MUST NOT**)出现在尾部段中。终端必须(**MUST**)将包含未定义或无效伪标头字段的请求或响应视为格式错误。 + +在头部段中,所有伪标头字段必须(**MUST**)出现在常规标头字段之前。任何违反此约定的请求或响应必须(**MUST**)被视为格式错误。 + +### 4.3.1 Pseudo-Header Fields + +HTTP/3 为请求消息定义了如下伪标头字段: + +* ":method":包含HTTP方法([[HTTP]](#http)的第9章)。 +* ":scheme":包含目的URI的模式部分([[URI]](#uri)的第3.1节)。 + + :scheme伪标头不局限于“http”和“https”模式的URI。代理或网关可以转换非HTTP模式的请求,使HTTP能够与非HTTP服务进行交互。 + + 有关使用“https”以外的模式的指导,请参见第3.1.2节。 + +* ":authority":包含目的URI的授权部分([[URI]](#uri)的第3.2节)。权限不得(**MUST NOT**)包含“http”或“https”模式的URI中已弃用的userinfo子组件。 + + 为确保如HTTP/1.1请求行可以精确重构出来,当从具有method相关形式HTTP/1.1请求转换过来时,必须(**MUST**)忽略此伪标头字段;请参阅[[HTTP]](#http)的第7.1节。直接生成HTTP/3请求的客户端应该(**SHOULD**)使用:authority伪标头字段而不是Host头字段。如果请求中不存在主机字段,则将HTTP/3请求转换为HTTP/1.1的中间设备必须(**MUST**)通过复制:authority伪标头字段的值来创建主机字段。 + +* ":path":包含目的URI的路径和查询部分(“绝对路径”表达式和可选的?字符(ASCII 0x3f),后跟“查询”表达式;参见[[URI]](#uri)的第3.3和3.4节。 + + 对于“http”或“https”模式的URI,此伪标头字段不得(**MUST NOT**)为空;不包含路径组件的“http”或“https”模式URI必须(**MUST**)包含“/”(ASCII 0x2f)的值。不包含路径组件的OPTIONS请求的:path 伪标头字段包含“*”(ASCII 0x2a) ;请参阅[[HTTP]](#http)的第7.1节。 + +所有HTTP/3请求必须(**MUST**)只包含一个:method、:scheme 和 :path伪标头字段的值,除非请求是CONNECT请求;见第 4.4节。 + +如果:scheme伪标头字段指定的是具有必选授权组件(包括“http”和“https”)的模式,则请求必须(**MUST**)包含:authority伪标头字段或Host标头字段。如果字段存在,则不得(**MUST NOT**)为空。如果两个字段都存在,它们必须(**MUST**)包含相同的值。如果该模式没有必选授权组件并且在请求目标中没有提供任何内容,则请求不得(**MUST NOT**)包含:authority伪标头或Host标头字段。 + +缺失必选伪标头字段或伪标头字段包含无效值的HTTP请求为格式错误。 + +HTTP/3 没有定义如在HTTP/1.1请求行中携带的版本标识符的方法。HTTP/3请求隐含的协议版本为“3.0”。 + +### 4.3.2. Response Pseudo-Header Fields +HTTP/3对于响应,定义了一个带有HTTP状态码的“:status”伪标头字段;请参阅[[HTTP]](#http)的第15章。这个伪标头字段必须(**MUST**)包含在所有响应中;否则为响应格式错误(见第4.1.2 节)。 + +HTTP/3没有定义如在HTTP/1.1状态行中携带的版本或原因短语的方法。HTTP/3响应隐含的协议版本为“3.0”。 + +## 4.4. The CONNECT Method +CONNECT方法请求接收方建立一条到由request-target标识的目的源服务端的隧道;请参阅[[HTTP]](#http)的第9.3.6节。它主要与HTTP代理一起使用,以与源服务端建立TLS会话,以便与“https”资源进行交互。 + +在HTTP/1.x中,CONNECT用于将整个HTTP连接转换为到远程主机的隧道。在HTTP/2和HTTP/3中,CONNECT 方法用于在单个流上建立隧道。 + +一个CONNECT请求必须(**MUST**)有如下格式: + +* :method 伪标头字段设置为 "CONNECT" +* :scheme 和 :path 伪标头字段被省略 +* :authority 伪标头字段包含要连接的主机和端口(相当于CONNECT请求的request-target的授权形式;参见[[HTTP]](#http)的第7.1节)。 + +请求流在携带要传输的数据的请求结束前保持打开状态。不遵循这些限制的CONNECT请求格式不正确。 + +支持CONNECT的代理与:authority伪标头字段标识的服务端建立TCP连接([[RFC0793]](#rfc0793))。成功建立此连接后,代理发送给客户端一个包含2xx系列状态码的HEADERS帧,如[[HTTP]](#http)的第15.3节中所定义。 + +所有数据流上的DATA帧对应于TCP连接上发送或接收的数据。客户端发出的任何DATA帧的有效载荷由代理投递到TCP服务端;从TCP服务端接收到的数据被代理打包成DATA帧。请注意,不能保证TCP段的长度和数量映射到HTTP DATA帧或QUIC STREAM帧时的长度或数量。 + +一旦CONNECT方法完成,只有DATA帧允许在该流上发送。如果扩展定义特别允许,可以(**MAY**)使用扩展帧。收到任何其他已知帧类型必须(**MUST**)被视为H3_FRAME_UNEXPECTED类型的连接错误。 + +TCP连接可以由任一终端关闭。当客户端结束请求流时(即代理处的接收流进入“Data Recvd”状态),代理将在其与TCP服务端的连接上设置FIN位。当代理接收到一个设置了FIN位的数据包时,它将关闭它到客户端的发送流。在单个方向上保持半关闭状态的TCP连接并非无效,但服务端通常处理不当,因此客户端在仍期望从CONNECT对端接收数据时,不应(**SHOULD NOT**)关闭用于发送的流。 + +TCP通过突然终止流来发出连接错误信号。代理将TCP连接中的任何错误(包括接收设置了RST位的TCP段)视为H3_CONNECT_ERROR类型流错误。 + +相应地,如果代理检测到流或QUIC连接错误,它必须(**MUST**)关闭TCP连接。如果代理检测到客户端已重置流或中止从流中读取,它必须(**MUST**)关闭TCP连接。如果流被重置或读取被客户端中止,代理应该(**SHOULD**)在另一个方向上执行相同的操作,以确保流的两个方向都被取消。在所有这些情况下,如果底层TCP实现允许,代理应该(**SHOULD**)发送一个设置了RST位的TCP段。 + +由于CONNECT创建了一个到任意服务端的隧道,支持CONNECT的代理应该(**SHOULD**)将其使用限制在一组已知端口或安全请求目标列表;有关详细信息,请参阅[[HTTP]](#http)的第9.3.6节。 + +## 4.5. HTTP Upgrade +HTTP/3不支持HTTP升级机制([[HTTP]](#http)的第7.8节)或 101(交换协议)信息状代码(参见[[HTTP]](#http)第15.2.2节)。 + +## 4.6. Server Push +服务端推送是一种交互模式,它允许服务端在客户端发出请求之前,将请求-响应交换报文推送到客户端。这是在牺牲网络利用率但获得潜在延迟收益之间做的取舍。HTTP/3服务端推送类似于[[HTTP/2]](#http2)8.2节中描述的内容,但它使用不同的机制。 + +每个服务端推送都由服务端分配一个唯一的push ID。push ID用于在HTTP/3连接的整个生命周期中在各种上下文中的标识某个推送。 + +push ID空间从零开始,上限是MAX_PUSH_ID帧通告的值。特别是,在客户端发送MAX_PUSH_ID帧之前,服务端无法进行。客户端使用MAX_PUSH_ID帧来控制服务端允许的推送次数。服务端应该(**SHOULD**)从零开始按序使用push ID。客户端在没有发送MAX_PUSH_ID帧而收到推送或流引用超过上限的push ID时,必须(**MUST**)视为H3_ID_ERROR类型的连接错误。 + +push ID在携带了包含控制数据和头字段的一个或多个PUSH_PROMISE帧的请求消息中使用。这些帧在生成推送的请求流上发送。这使得服务端推送与客户端请求相关联。在多个请求流上承诺相同的push ID时,解压后的请求字段部分必须(**MUST**)以相同的顺序包含相同的字段,并且每个字段中的名称和值都必须(**MUST**)相同。 + +包含在推送流中的push ID最终实现了这些承诺。推送流标识其实现承诺的push ID,然后包含对承诺请求的响应,如第4.1节所述。 + +最后,可以在CANCEL_PUSH中包含push ID帧,如第7.2.3节所述。客户端发送此帧表示他们不希望收到承诺的资源。服务端使用这个帧来表明他们不会履行之前的承诺。 + +并非所有请求都可以推送。服务端可以(**MAY**)推送具有如下属性的请求: + +* 可缓存的;请参阅[[HTTP]](#http)的第9.2.3节 +* 安全的;请参阅[[HTTP]](#http)的第9.2.1节 +* 不包含请求内容或尾部段 + +服务端必须(**MUST**)在:authority伪标头字段中包含一个服务端对其具有权威的值。如果客户端尚未验证推送请求所指示的源的连接,它必须(**MUST**)执行与此连接上,往该源发送请求之前相同的验证过程;见第3.3节。如果此验证失败,则客户端不得(**MUST NOT**)认为服务端对该源具有权威性。 + +客户端应在收到包含不可缓存、不安全、存在请求内容或认为服务端不具有权威性的请求的 PUSH_PROMISE帧后发送CANCEL_PUSH帧。不得(**MUST NOT**)使用或缓存任何相应的响应。 + +每个推送响应都与一个或多个客户端请求相关联。此推送与收到PUSH_PROMISE帧的请求流相关联。在多个请求流上发送具有相同push ID的PUSH_PROMISE帧,可以将此服务端推送与其他客户端请求相关联。这些关联不会影响协议的操作,但用户代理在决定如何使用推送资源时可以(**MAY**)参考这些关联。 + +PUSH_PROMISE帧的顺序相对于某些部分的响应很重要。服务端应该(**SHOULD**)在HEADERS和DATA帧之前发送PUSH_PROMISE帧。这减少了客户端请求即将由服务端推送的资源的机会。 + +因为乱序,推送的数据可能在相应的PUSH_PROMISE帧之前到达。当客户端收到新的推送流时如果push ID未知,则关联的客户端请求和推送的请求标头字段都是未知的。客户端可以缓存流数据以期待匹配的PUSH_PROMISE帧. 客户端可以使用流控([[QUIC-TRANSPORT]](#quic)第4.1节)来限制服务端可能提交给推送流的数据量。如果在合理的时间内,没有收到对应的PUSH_PROMISE帧,客户端应该(**SHOULD**)中止读取并丢弃已经从推送流中读取的数据。 + +推送的流数据也可以在客户端取消推送后到达。在这种情况下,客户端可用H3_REQUEST_CANCELLED错误码中止读取流。这要求服务端不要传输额外的数据,并表明它将在收到时丢弃。 + +如果客户端实现了HTTP缓存,则可缓存的推送响应(参见[[HTTP-CACHING]](#http-caching)的第3章)可以被客户端存储。在接收到推送的响应时,应该(**SHOULD**)被认为已在源服务端上成功验证(例如,存在“no-cache”缓存响应指令时;参见[[HTTP-CACHING]](#http-caching)第5.2.2.4节)。 + +不可缓存的推送响应不得(**MUST NOT**)由任何HTTP缓存存储。它们可以(**MAY**)单独提供给上层应用。 + + +# 5. Connection Closure + +一旦建立,HTTP/3 连接就可以持续用于收发请求和响应,直到连接关闭。连接关闭可以有多种方式。 + +## 5.1 Idle Connections + +每个QUIC终端在握手期间声明一个空闲超时时间。如果QUIC连接持续空闲(没有收到数据包)超过此时间,则对端将假定连接已关闭。如果现有连接的空闲时间超过了QUIC握手期间协商的空闲超时时间,HTTP/3实现将需要为新请求打开一个新的HTTP/3连接,如果接近空闲超时时间,他们也应该(**SHOULD**)这样做;请参阅[[QUIC-TRANSPORT]](#quic)的第10.1节。 + +如[[QUIC-TRANSPORT]](#quic)的第10.1.2节所述,当请求或服务端推送有未完成的响应时,HTTP客户端应要求传输保持连接打开状态。如果客户端不期待来自服务端的响应了,则允许空闲连接自然超时比花费精力维护可能不需要的连接更可取。网关可以(**MAY**)根据需要保持连接,权衡考虑与服务端建立连接的时延成本。服务端不应(**SHOULD NOT**)主动保持连接打开状态。 + +## 5.2. Connection Shutdown + +即使连接非空闲,任一终端都可以决定停止使用该连接,并启动一个正常的连接关闭。终端通过发送GOAWAY帧启动HTTP/3连接的正常关闭。GOAWAY帧包含一个ID,该ID向接收方指示在此连接中,已处理或可能处理的请求或推送的范围。服务端在此帧中包含客户端发起的双向流的流ID,客户端在此帧中包含push ID。GOAWAY帧的发送方会拒绝比此帧中ID更大的ID的请求或推送(第4.1.1节)。如果没有处理请求或推送,则此ID可能(**MAY**)为零。 + +GOAWAY帧携带的信息使得客户端和服务端能够在关闭HTTP/3连接之前,就接受哪些请求或推送达成一致。在发送GOAWAY帧时,终端应该(**SHOULD**)显式取消(参见第4.1.1和7.2.3节)任何ID大于或等于所指示的ID的请求或推送,以便清除受影响流的传输状态。后续更多请求或推送到达,终端也应该(**SHOULD**)继续照此处理。 + +终端在收到对端GOAWAY帧后,不得(**MUST NOT**)在此连接上发起新请求或推送新Promise。客户端可以(**MAY**)发起一个新连接来发送其他请求。 + +一些请求或推送可能已经在传输中: + +* 收到GOAWAY帧时,如果客户端已经发送了流ID大于或等于GOAWAY帧中ID的请求,这些请求将不会被处理。客户端可以在不同的HTTP连接上安全地重试未被处理的请求。当服务端关闭连接时,无法重试请求的客户端会丢失所有在途的请求。 + + 来自服务端的,流ID小于GOAWAY帧中流ID的请求可能已被处理,在如下情况下才能确定这些请求的状态:收到响应;流被单独重置;收到另一个GOAWAY帧,其流ID小于当前请求的流ID;连接终止。 + + 如果未处理这些请求,服务端可以(**MAY**)会拒绝小于指定ID的流上的单独请求。 + +* 如果服务端收到GOAWAY帧,那么此前发出的push ID大于或等于此帧中ID的推送,可能都不会被客户端接受。 + +在知道连接即将关闭时,服务端应该(**SHOULD**)提前发送一个GOAWAY帧通知对端,即使提前量很小,以便对端可以知道请求是否已被部分处理。例如,如果HTTP客户端在服务端关闭QUIC连接的同时,发送了POST请求,如果服务端不发送GOAWAY帧来指示它可能已经处理的流,客户端将无法知道服务端是否已经开始处理该POST请求。 + +一个终端可以(**MAY**)发送多个带有不同ID的GOAWAY帧,但每个帧中的ID不得(**MUST NOT**)大于任何先前GOAWAY帧中的ID,因为客户端可能已经在另一个HTTP连接上重试了未处理的请求。收到比之前GOAWAY帧中的更大的ID必须(**MUST**)视为H3_ID_ERROR类型的连接错误。 + +尝试正常关闭连接的终端可以发送GOAWAY帧,将ID值设置为最大可选值(服务端为262-4,客户端为262-1)。对端收到此帧后会停止创建新请求或推送。在途的请求或推送到达之后,终端可以发出另一个GOAWAY帧,指示在连接结束之前它可以接受的请求或推送。这保证可以优雅关闭连接而不丢失请求。 + +客户端对它发送的GOAWAY帧中的Push ID字段的取值方面具有更大的灵活性。值262-1表示服务端可以继续完成已经承诺的推送。较小的值表示客户端将拒绝push ID大于或等于该值的推送。和服务端一样,客户端可以(**MAY**)继续发送GOAWAY帧,只要其携带的push ID不大于任何先前发送的值。 + +即使已经用GOAWAY帧表示给定的请求或推送在收到时也不会被处理或接受,底层传输资源仍然存在。发起这些请求的终端可以关闭它们以清除传输状态。 + +一旦处理完所有接收到的请求和推送,终端就可以将连接变为空闲状态,或者可以(**MAY**)启动连接的立即关闭。完成优雅关闭的终端应该(**SHOULD**)使用H3_NO_ERROR码关闭连接。 + +如果客户端的请求耗尽了所有可用的双向流ID,则服务端无需发送GOAWAY帧,因为客户端无法发出更多请求了。 + +## 5.3 Immediate Application Closure + +HTTP/3实现可以随时以这种方式立即关闭QUIC连接:向对端发送QUIC CONNECTION_CLOSE帧,指示应用层已终止连接。此帧中的应用层错误码可以向对端指示关闭连接的原因。有关在HTTP/3中关闭连接时可以使用的错误码,请参阅第8章。 + +在关闭连接之前发送GOAWAY帧可以(**MAY**)允许客户端重试某些请求。将GOAWAY帧与QUIC CONNECTION_CLOSE帧放在同一数据包中发送可以提高客户端接收该帧的机会。 + +如果存在未显式关闭的流,则在关闭连接时会将其隐式关闭;请参阅[[QUIC-TRANSPORT]](#quic)的第10.2节。 + +## 5.4 Transport Closure + +由于各种原因,QUIC传输可能会向应用层指示连接已终止。这可能是由于对端的显式关闭、传输级错误或中断,连接的网络拓扑更改导致的。 + +如果连接在没有GOAWAY帧的情况下终止,客户端必须(**MUST**)假设任何发送的请求,无论是全部还是部分,都可能已被处理。 + +# 6. Stream Mapping and Usage + +QUIC在每个流上都提供可靠的、按序的字节传输,但不保证与其他流上的字节按序投递。在QUIC版本1中,包含HTTP帧的流数据由QUIC STREAM帧承载,但这种帧在HTTP层是不可见的。传输层对接收到的流数据进行缓冲和排序,向应用层投递可靠的字节流。尽管QUIC允许在流上进行不保序投递,但HTTP/3并未使用此功能。 + +QUIC流可以是单向的,仅从发送方到接收方单向传输数据,也可以是双向的,两个方向都可传输数据。流可以由客户端或服务端发起。有关QUIC流的更多详细信息,请参阅[[QUIC-TRANSPORT]](#quic)的第2章。 + +当通过QUIC发送HTTP字段和数据时,QUIC层处理大部分流管理功能。使用QUIC时,HTTP不需要进行任何单独的多路复用:通过QUIC流发送的数据总是映射到特定的HTTP事务或整个HTTP/3连接上下文。 + +## 6.1 Bidirectional Streams + +所有客户端发起的双向流都用于HTTP请求和响应。双向流的设计确保响应可以很容易地与请求相关联。这些流称为请求流。 + +这意味着客户端的第一个请求在流ID为0的流上发送,随后的请求在流ID 4、8上发送,依此类推。为了这些流可以正常打开,HTTP/3服务端应该(**SHOULD**)为其允许接入的流的个数和初始流控窗口配置非零的最小值。为了避免不必要地限制并发数,一次至少应该(**SHOULD**)允许100个请求流。 + +HTTP/3不使用由服务端启动的双向流,不过允许扩展定义这类流的用途。客户端必须(**MUST**)将收到服务端发起的双向流视为H3_STREAM_CREATION_ERROR类型的连接错误,除非已协商此类扩展用途。 + +## 6.2 Unidirectional Streams + +在任一方向上的单向流用于多种目的。其目的由流类型指示,在流的开头作为可变长度整数发送。此整数之后的数据格式和结构由流类型决定: + +``` + Unidirectional Stream Header { + Stream Type (i), + } + + Figure 1: Unidirectional Stream Header +``` + +本文中定义了两种流类型:控制流(第6.2.1节)和推送流(第6.2.2节)。[[QPACK]](#qpack)定义了两种额外的流类型。其他流类型可以通过对HTTP/3的扩展来定义;有关详细信息,请参阅第9章。某些流类型保留以备后用(第6.2.3节)。 + +HTTP/3连接在其生命周期的早期阶段的性能,对单向流上数据的创建和交换很敏感。终端过度限制流的个数或这些流的流控窗口,将增加对端提前达到限制并被阻塞的机会。特别是,实现应该考虑对端可能希望使用一些合法的单向流,执行保留的流行为(第6.2.3节)。 + +每个终端需要为HTTP控制流创建至少一个单向流。QPACK需要两个额外的单向流,而其他扩展可能需要更多的流。因此,客户端和服务端协商的传输参数必须(**MUST**)允许对端创建至少三个单向流。这些传输参数还应该(**SHOULD**)为每个单向流提供至少1,024字节的流控credit。 + +请注意,如果终端在创建关键单向流之前耗尽了所有初始credit,则不需要授予额外credit来创建更多单向流。终端应该(**SHOULD**)创建HTTP控制流以及必选扩展所需的单向流(例如QPACK编码器和解码器流),然后在其对端允许的情况下创建其他流。 + +如果Stream Type指示的是接收方不支持的流类型,则流的其余部分也无法处理,因为语义未知。收到未知流类型的终端必须(**MUST**)中止读取流或丢弃传入数据,而不执行进一步处理。如果读取被中止,接收方应该(**SHOULD**)使用H3_STREAM_CREATION_ERROR类型的错误码或保留的错误码(第8.1节)。接收方不得(**MUST NOT**)将未知流类型视为连接错误。 + +由于某些流类型会影响连接状态,因此接收方不应(**SHOULD NOT**)在未读取流类型之前丢弃来自传入单向流的数据。 + +实现可以(**MAY**)在不知对端是否支持某种流类型的情况下发送此类型数据。但是,针对某种流类型,可以修改现有协议组件(包括QPACK或其他扩展)的状态或语义,要求在未知对端是否支持的情况下,不得(**MUST NOT**)发送此类型数据。 + +除非另有说明,否则发送方可以关闭或重置单向流。接收方必须(**MUST**)容忍在收到单向流标头之前此流就已经被关闭或重置了。 + +### 6.2.1 Control Streams + +流类型0x00表示控制流。此流上的数据由第7.2节中定义的HTTP/3帧构成。 + +每一终端都必须(**MUST**)在连接开始时,启动一个控制流,并发送SETTINGS帧作为此流上的第一帧。如果控制流的第一帧是其他帧类型,则必须(**MUST**)将其视为H3_MISSING_SETTINGS类型的连接错误。每个终端只允许发起一个控制流,收到声明是控制流的第二个流必须(**MUST**)被视为H3_STREAM_CREATION_ERROR类型的连接错误。发送方不得(**MUST NOT**)关闭控制流,接收方不得(**MUST NOT**)要求发送方关闭控制流。如果任一控制流在任意时间关闭,则必须(**MUST**)都将其视为H3_CLOSED_CRITICAL_STREAM类型的连接错误。连接错误在第8章中描述。 + +因为控制流的内容是用于管理其他流的行为,终端应该(**SHOULD**)提供足够的流控credit来防止终端的控制流被阻塞。 + +使用一对单向流而不是单个双向流的设计,允许任一对端尽快发送数据。如果QUIC连接上的0-RTT可用,客户端或服务端可以提前发送流数据。 + +### 6.2.2 Push Streams + +服务端推送是HTTP/2中引入的一个可选功能,它允许服务端在客户端发出请求之前发送响应。有关详细信息,请参阅第4.6节。 + +流类型0x01表示推送流,后跟它承诺的推送的ID,编码为可变长度整数。此流上的其余数据由第7.2节中定义的HTTP/3帧组成,并通过零个或多个中间HTTP响应、后跟一个最终HTTP响应来满足承诺的服务端推送,如第4.1节中定义。服务端推送和push ID在4.6节中描述。 + +只有服务端可以推送;如果服务端收到客户端发起的推送流,则必须(**MUST**)将其视为H3_STREAM_CREATION_ERROR类型的连接错误。 + +``` + Push Stream Header { + Stream Type (i) = 0x01, + Push ID (i), + } + + Figure 2: Push Stream Header +``` + +客户端应该(**SHOULD**)在读取推送流标头之后再确定是否终止读取后续数据,不然可能导致客户端和服务端对已使用的push ID出现分歧。 + +每个push ID必须(**MUST**)只能在推送流标头中使用一次。如果客户端检测到推送流标头包含的push ID在另一个推送流标头中使用过,客户端必须(**MUST**)将此视为H3_ID_ERROR类型的连接错误。 + +### 6.2.3 Reserved Stream Types + +形如 0x1f*N+0x21(N为非负整数)的保留流类型用来测试是否未知流类型会被终端忽略。这些流没有语义,可以在需要应用层填充时发送。它们也可以(**MAY**)在当前没有数据传输的连接上发送。终端在收到时不得(**MUST NOT**)认为这些流具有任何意义。 + +未知类型流的有效负载和长度随便发送方怎么填。当收到保留的流类型时,实现可以(**MAY**)干脆地终止流或重置它。重置流时,应该(**SHOULD**)使用H3_NO_ERROR类型的错误码或其他保留的错误码(第8.1节)。 + +# 7. HTTP Framing Layer + +HTTP帧在QUIC流上承载,如第6章所述。HTTP/3定义了三种流类型:控制流、请求流、推送流。本节介绍HTTP/3帧格式及其允许的流类型;概览见表1。附录A.2中给出了HTTP/2和HTTP/3帧之间的比较。 + + +Table 1: HTTP/3 Frames and Stream Type Overview +| Frame | Control Stream | Request Stream | Push Stream | Section | +|:---|:---|:---|:---|:---| +| DATA | No | Yes | Yes | Section 7.2.1 | +| HEADERS | No | Yes | Yes | Section 7.2.2 | +| CANCEL_PUSH | Yes | No | No | Section 7.2.3 | +| SETTINGS | Yes (1) | No | No | Section 7.2.4 | +| PUSH_PROMISE | No | Yes | No | Section 7.2.5 | +| GOAWAY | Yes | No | No | Section 7.2.6 | +| MAX_PUSH_ID | Yes | No | No | Section 7.2.7 | +| Reserved | Yes | Yes | Yes | Section 7.2.8 | + + +SETTINGS 帧只能作为控制流的第一帧出现;这在表1中用(1)表示。具体指导见相关章节。 + +请注意,与QUIC帧不同,HTTP/3帧可以跨越多个QUIC数据包。 + +## 7.1. Frame Layout + +HTTP/3帧格式如下: + +``` + HTTP/3 Frame Format { + Type (i), + Length (i), + Frame Payload (..), + } + + Figure 3: HTTP/3 Frame Format +``` + +每个帧都包括以下字段: + +Type 类型: +标识帧类型的可变长度整数。 + +Length 长度: +可变长度整数,用于描述帧有效负载的长度(以字节为单位)。 + +Frame Payload 帧载荷: +有效负载,其语义由Type字段确定。 + +每个帧的有效负载必须(**MUST**)准确包含在其帧结构中标识的字段。在已标识字段之后还包含额外字节或中途被截断的帧都必须(**MUST**)被视为H3_FRAME_ERROR类型的连接错误。特别是,帧中有多重长度编码的话必须(**MUST**)是自洽的;见第10.8节。 + +当流被突然终止时,如果流上的最后一帧被截断,则必须(**MUST**)将其视为H3_FRAME_ERROR类型的连接错误。突然终止的流可能会在帧中的任意位置重置。 + +## 7.2. Frame Definitions + +### 7.2.1. DATA + +DATA帧(type=0x00)承载与HTTP请求或响应内容相关的任意可变长度字节序列。 + +DATA帧必须(**MUST**)与HTTP请求或响应相关联。如果在控制流上接收到DATA帧,接收方必须(**MUST**)视为H3_FRAME_UNEXPECTED类型的连接错误。 + +``` + DATA Frame { + Type (i) = 0x0, + Length (i), + Data (..), + } + + Figure 4: DATA Frame +``` + +### 7.2.2 HEADERS + +HEADERS帧(type=0x01)用于携带使用QPACK编码的HTTP字段部分。有关详细信息,请参阅[[QPACK]](#qpack)。 + +``` + HEADERS Frame { + Type (i) = 0x1, + Length (i), + Encoded Field Section (..), + } + + Figure 5: HEADERS Frame +``` + +HEADERS帧只能在请求流上或推送流发送。如果在控制流上接收到HEADERS帧,接收方必须(**MUST**)视为H3_FRAME_UNEXPECTED类型的连接错误。 + +### 7.2.3. CANCEL_PUSH + +CANCEL_PUSH帧(type=0x03)用于在推送流被接收之前,要求取消服务端推送。CANCEL_PUSH帧通过push ID标识服务端推送(参见第4.6节),push ID编码为可变长度整数。 + +客户端发送CANCEL_PUSH帧即表明它不希望接收承诺的资源。服务端应该(**SHOULD**)中止发送资源,但其处理机制取决于相应的推送流的状态。如果此时还没有创建该推送流,则不继续创建;如果推送流已经打开,则应该(**SHOULD**)立即终止该流;如果推送流已经结束,服务端可以(**MAY**)立即终止流或不采取任何动作。 + +服务端发送CANCEL_PUSH帧即表明它不会履行之前发送的承诺。客户端不能期望服务端继续履行承诺,除非它已经收齐并处理了承诺的响应。不管推送流是否已被打开,服务端应该(**SHOULD**)在它确定不能继续时发送CANCEL_PUSH帧。如果流已被打开,服务端可以中止在流上发送,错误码为H3_REQUEST_CANCELLED。 + +发送CANCEL_PUSH帧对现有推送流的状态没有直接影响。当客户端已经接收到相应的推送流时,它不应(**SHOULD NOT**)发送CANCEL_PUSH帧。推送流可能在客户端发送CANCEL_PUSH帧之后到达,因为服务端可能尚未处理CANCEL_PUSH帧。客户端应该(**SHOULD**)中止读取流,错误码为H3_REQUEST_CANCELLED。 + +在控制流上发送CANCEL_PUSH帧、在控制流以外的流上接收到CANCEL_PUSH帧必须(**MUST**)被视为H3_FRAME_UNEXPECTED类型的连接错误。 + +``` + CANCEL_PUSH Frame { + Type (i) = 0x3, + Length (i), + Push ID (i), + } + + Figure 6: CANCEL_PUSH Frame +``` + +CANCEL_PUSH帧带有编码为可变长度整数的push ID。Push ID字段标识正在被取消的服务端推送流;见第4.6节。如果收到CANCEL_PUSH帧中的push ID大于当前连接容许的值,则必须(**MUST**)被视为H3_ID_ERROR类型的连接错误。 + +如果客户端收到一个CANCEL_PUSH帧,由于乱序的关系,该帧中的push ID可能尚未被其他PUSH_PROMISE帧提及。如果服务端收到的CANCEL_PUSH帧中的push ID未被任意PUSH_PROMISE帧提到,这必须(**MUST**)视为H3_ID_ERROR类型的连接错误。 + +### 7.2.4. SETTINGS + +SETTINGS帧(type=0x04)承载影响终端通信方式的配置参数,例如终端行为的首选项和限制。单独地,一个SETTINGS参数也可以称为“setting”;每个设置参数的标识和值可以称为“setting标识”和“setting值”。 + +SETTINGS帧适用于整个HTTP/3连接,而不是单个流。SETTINGS帧必须(**MUST**)作为每个终端控制流的第一帧发送(见第6.2.1节),并且随后不得(**MUST NOT**)再发。如果终端在控制流上接收到第二个SETTINGS帧,必须(**MUST**)视为H3_FRAME_UNEXPECTED类型的连接错误。 + +SETTINGS帧不得(**MUST NOT**)在控制流以外的任何流上发送.如果终端在不同的流上接收到SETTINGS帧,终端必须(**MUST**)视为H3_FRAME_UNEXPECTED类型的连接错误。 + +SETTINGS参数不经过协商,它们定义了接收端可以使用的,发送端的特性。不过,SETTINGS的使用暗涵协商:每个终端都使用SETTINGS帧来通告一组其支持的值。setting的定义可以得出使用这两个集合中的哪个选项的结论。SETTINGS不提供明确选项何时生效的机制。 + +每个终端可以通告相同参数的不同值。例如,客户端可能愿意使用非常大的响应字段部分,而服务端对请求长度更加谨慎。 + +相同的setting标识不得(**MUST NOT**)在SETTINGS帧中出现多次。接收方可以(**MAY**)将setting标识重复视为H3_SETTINGS_ERROR类型的连接错误。 + +SETTINGS帧的有效载荷由零个或多个参数组成。每个参数由一个setting标识和一个setting值组成,两者都编码为QUIC可变长度整数。 + +``` + Setting { + Identifier (i), + Value (i), + } + + SETTINGS Frame { + Type (i) = 0x4, + Length (i), + Setting (..) ..., + } + + Figure 7: SETTINGS Frame +``` + +实现必须(**MUST**)忽略任何带有它不理解的setting标识的参数。 + +#### 7.2.4.1. Defined SETTINGS Parameters + +HTTP/3中定义了以下setting: + +SETTINGS_MAX_FIELD_SECTION_SIZE(0x06):默认值为无限制。用法见第4.2.2节。 + +形如 0x1f*N+0x21(N为非负整数)格式的setting标识,是为了满足忽略未知标识符的要求而保留的。此类setting没有明确的含义。终端应该(**SHOULD**)在它们的SETTINGS帧中至少包含一个这样的setting。终端在收到时不得(**MUST NOT**)认为此类setting具有任何意义。 + +因为此类setting没有确定含义,所以其值可由实现任意选择。 + +在[[HTTP/2]](#http2)中定义的,HTTP/3中没有对应setting的标识也被保留(第11.2.2节)。不得(**MUST NOT**)发送这些保留的setting,收到的话必须(**MUST**)视为H3_SETTINGS_ERROR类型的连接错误。 + +其他setting可以通过HTTP/3扩展来定义;有关详细信息,请参阅第9章。 + +#### 7.2.4.2 Initialization + +HTTP实现不得(**MUST NOT**)发送基于其当前终端setting,理解为无效的帧或请求。 + +所有setting都有初始值。因为携带setting的数据包可能会丢失或延迟,每个终端在对端的SETTINGS帧到达之前发送消息时都应该(**SHOULD**)使用初始值。当SETTINGS帧到达时,所有setting都更新为新值。 + +这消除了在发送消息之前等待SETTINGS帧的需要。终端在发送SETTINGS帧之前不得(**MUST NOT**)要求从对端收到任何数据;必须(**MUST**)在传输准备好发送数据后立即发送setting。 + +对于服务端,每个客户端setting的初始值都是默认值。 + +对于使用1-RTT QUIC连接的客户端,每个服务端setting的初始值都是默认值。在QUIC处理包含SETTINGS的数据包之前,1-RTT密钥就是可用状态了,即使服务端立即发送SETTINGS也是如此。客户端不应(**SHOULD NOT**)在发送请求之前无限期地等待SETTINGS帧到达,但他们应该(**SHOULD**)处理接收到的数据报文,以增加在发送第一个请求之前处理SETTINGS帧的可能性。 + +当使用0-RTT QUIC连接时,每个服务端setting的初始值是前一次会话中使用的值。客户端应该(**SHOULD**)保存服务端在HTTP/3连接中提供的与会话恢复有关的setting,但他们可以(**MAY**)选择在某些情况下不存储setting(例如,如果在SETTINGS帧之前收到会话凭证)。客户端在尝试0-RTT时必须(**MUST**)使用存储的setting——如果没有存储值,则为默认值。一旦服务端给出了新setting,客户端必须(**MUST**)使用这些值。 + +服务端可以(**MAY**)记住它公布的setting或存储的会话凭证的完整性保护副本,并在收到0-RTT数据时恢复信息。服务端使用HTTP/3 setting值来确定是否接受0-RTT数据。如果服务端无法确定客户端保存的setting与其当前setting是否兼容,则它不得(**MUST NOT**)接受0-RTT数据。客户端保存的setting是兼容的,如果这些setting不会违反服务端的当前setting。 + +服务端可以接受0-RTT,并随后在其SETTINGS帧中提供不同的setting。如果服务端接受0-RTT数据,则服务端不得(**MUST NOT**)在其SETTINGS帧中减少任何限制,或更改为与客户端使用0-RTT数据不兼容的任何值。服务端必须(**MUST**)包含与其默认值不同的所有setting。如果服务端接受0-RTT但随后发送的setting与先前指定的setting不兼容,则必须(**MUST**)将其视为H3_SETTINGS_ERROR类型的连接错误。如果服务端接受0-RTT但随后发送的SETTINGS帧,未携带被客户端认为是具有非默认值的setting(不同于保留的setting标识),则必须(**MUST**)将其视为H3_SETTINGS_ERROR类型的连接错误。 + +### 7.2.5 PUSH_PROMISE + +PUSH_PROMISE帧(type=0x05)用于在请求流上承载服务端承诺的,发往客户端的请求头部段。 + +``` + PUSH_PROMISE Frame { + Type (i) = 0x5, + Length (i), + Push ID (i), + Encoded Field Section (..), + } + + Figure 8: PUSH_PROMISE Frame +``` + +有效载荷包括: + +Push ID 推送ID: +标识服务端推送操作的可变长度整数。push ID用于推送流标头(第4.6节)和CANCEL_PUSH帧。 + +Encoded Field Section 编码字段部分: +承诺响应的,QPACK编码的请求头字段。有关详细信息,请参阅[[QPACK]](#qpack)。 + +服务端不得(**MUST NOT**)使用大于客户端在MAX_PUSH_ID帧中给出的push ID(第7.2.7节)。客户端收到违反此约定的PUSH_PROMISE帧必须(**MUST**)视为H3_ID_ERROR类型的连接错误。 + +服务端在多个PUSH_PROMISE帧中可以(**MAY**)使用相同的push ID。这种情况下,解压缩后的请求,其头部集合必须(**MUST**)以相同顺序排列相同字段,并且每个字段中的名称和值都必须(**MUST**)完全匹配。客户端应该(**SHOULD**)多次比较承诺资源的请求头部段。如果客户端收到的push ID已经承诺过,但检测到头部段不匹配,则必须(**MUST**)视为H3_GENERAL_PROTOCOL_ERROR类型的连接错误。如果解压缩的字段部分完全匹配,客户端应该(**SHOULD**)将推送的内容与接收到PUSH_PROMISE帧的每个流相关联。 + +允许对同一个push ID的重复引用主要是为了减少并发请求造成的重复。服务端在很长一段时间内应该(**SHOULD**)避免重用push ID。客户端可能会使用服务端推送响应,而不会保留这些响应以供长期重用。客户端收到PUSH_PROMISE帧,包含之前已经使用和忽略的push ID的话,将被迫忽略此承诺。 + +如果在控制流上收到PUSH_PROMISE帧,客户端必须(**MUST**)视为H3_FRAME_UNEXPECTED类型的连接错误。 + +客户端不得(**MUST NOT**)发送PUSH_PROMISE帧。服务端必须(**MUST**)将收到PUSH_PROMISE帧视为H3_FRAME_UNEXPECTED类型的连接错误。 + +有关服务端推送机制的完整描述,请参见第4.6节。 + +### 7.2.6. GOAWAY + +GOAWAY帧(type=0x07)用于启动任一终端的HTTP/3连接的正常关闭。GOAWAY允许终端停止接受新请求或推送,同时仍完成对先前接收到的请求和推送的处理。这将启动管理操作,例如服务端维护。GOAWAY本身不会关闭连接。 + +``` + GOAWAY Frame { + Type (i) = 0x7, + Length (i), + Stream ID/Push ID (..), + } + + Figure 9: GOAWAY Frame +``` + +GOAWAY帧总是在控制流上发送。在服务端到客户端方向上,它携带客户端发起的QUIC双向流的流ID,编码为可变长度整数。客户端收到GOAWAY帧的流ID为其他类型的话,必须(**MUST**)视为H3_ID_ERROR类型的连接错误。 + +在客户端到服务端的方向上,GOAWAY帧携带一个push ID,编码为可变长度整数。 + +GOAWAY帧适用于整个连接,而不是特定的流。客户端在控制流以外的流上收到GOAWAY帧必须(**MUST**)视为H3_FRAME_UNEXPECTED类型的连接错误。 + +有关GOAWAY帧用法的更多信息,请参见第5.2节。 + +### 7.2.7 MAX_PUSH_ID + +客户端使用MAX_PUSH_ID帧(type=0x0d)来控制服务端可以发起的服务端推送次数。这将设置服务端可以在PUSH_PROMISE帧和CANCEL_PUSH帧中使用的push ID的最大值。因此,除了QUIC传输维护的限制之外,这也限制了服务端可以发起的推送流的数量。 + +MAX_PUSH_ID帧总是在控制流上发送。在任何其他流上收到MAX_PUSH_ID帧必须(**MUST**)被视为H3_FRAME_UNEXPECTED类型的连接错误。 + +服务端不得(**MUST NOT**)发送MAX_PUSH_ID帧。客户端必须(**MUST**)将收到MAX_PUSH_ID帧视为H3_FRAME_UNEXPECTED类型的连接错误。 + +最大push ID在创建HTTP/3连接时是未设置的,这意味着服务端在收到MAX_PUSH_ID帧之前无法进行推送。客户端希望管理服务端推送承诺次数的话,可以发送MAX_PUSH_ID帧增大最大push ID值,使得服务端履行或取消推送。 + +``` + MAX_PUSH_ID Frame { + Type (i) = 0xd, + Length (i), + Push ID (i), + } + + Figure 10: MAX_PUSH_ID Frame +``` + + +MAX_PUSH_ID帧携带一个可变长度整数,用于标识服务端可以使用的push ID的最大值;见第4.6节。MAX_PUSH_ID帧不能减少最大push ID的值。收到该值小于之前MAX_PUSH_ID帧通告的最大push ID值的帧,必须(**MUST**)视为H3_ID_ERROR类型的连接错误。 + +### 7.2.8 Reserved Frame Types + +形如 0x1f*N+0x21(N为非负整数)格式的帧类型,以测试是否忽略未知类型的要求(第9章)。这些帧没有语义,它们可以(**MAY**)在允许发送帧的任何流上发送。这使得它们能够用于应用层填充。终端在收到这些帧时不得(**MUST NOT**)认为这些帧具有任何意义。 + +帧的有效负载和长度由实现自行指定。 + +在HTTP/2中使用,但在HTTP/3没有对应的帧类型也被保留(第11.2.1节)。但不得(**MUST NOT**)发送这些帧类型,收到的话必须(**MUST**)视为H3_FRAME_UNEXPECTED类型的连接错误。 + +# 8. Error Handling + +当流无法成功结束时,QUIC允许应用层突然终止(重置)该流并给出原因;请参阅[[QUIC-TRANSPORT]](#quic)的第2.4节。这被称为“流错误”。HTTP/3实现可以决定是否关闭QUIC流并给出错误类型。错误码表在第8.1节中定义。流错误码不同于指示错误情况的HTTP状态码。流错误码表示发送方没有传输或消费完整的请求或响应,而HTTP状态码表明成功接收到的请求的处理结果。 + +如果需要终止整个连接,QUIC类似地提供了给出错误原因的机制;请参阅[[QUIC-TRANSPORT]](#quic)的第5.3节。这被称为“连接错误”。与流错误类似,HTTP/3实现可以终止QUIC连接并使用第8.1节中的错误码给出原因。 + +尽管关闭流和连接的原因称为“错误码”,但这些操作并不一定表明连接或任一实现存在问题。例如,如果不再需要请求的资源,则可以重置流。 + +在某些情况下,终端可以(**MAY**)选择将流错误视为连接错误,关闭整个连接以响应单个流上的问题。在做出此选择之前,实现需要考虑对未完成请求的影响。 + +因为可以在没有协商的情况下定义新的错误码(参见第9章),所以在非预期的上下文中收到错误码或收到未知错误码,必须(**MUST**)被视为等同于H3_NO_ERROR。但是,无论错误码是什么,关闭流都会产生其他影响;参见第4.1节的例子。 + +## 8.1. HTTP/3 Error Codes + +以下错误码定义为在突然终止流、中止读取流或立即关闭HTTP/3连接时使用: + +H3_NO_ERROR(0x0100): 没有错误。当需要关闭连接或流,但发出信号时没有错误时,使用此选项。 + +H3_GENERAL_PROTOCOL_ERROR(0x0101): 某种形式上,对端违反了协议,但又没有对应更具体的错误码,或者终端不愿意使用更具体的错误码。 + +H3_INTERNAL_ERROR(0x0102): HTTP协议栈发生了一个内部错误。 + +H3_STREAM_CREATION_ERROR(0x0103): 终端发现其对端创建了一个它不能接受的流。 + +H3_CLOSED_CRITICAL_STREAM(0x0104): HTTP/3连接要求的流被关闭或者重置了。 + +H3_FRAME_UNEXPECTED(0x0105): 在当前状态或者当前流上,收到了一个不允许的帧。 + +H3_FRAME_ERROR(0x0106): 收到的帧格式不对,或者大小不对。 + +H3_EXCESSIVE_LOAD(0x0107): 终端检测到其对端显示出可能产生过量负载的行为。 + +H3_ID_ERROR(0x0108): 错误使用流ID或Push ID,比如超过了限制,减小了限制,或者重用。 + +H3_SETTINGS_ERROR(0x0109): 终端在SETTNGS帧的载荷中检测到了错误。 + +H3_MISSING_SETTINGS(0x010a): 控制流的第一帧不是SETTING帧。 + +H3_REQUEST_REJECTED(0x010b): 服务端拒绝了请求,并不会交由应用层处理。 + +H3_REQUEST_CANCELLED(0x010c): 请求或它的响应(包括推送的响应)被取消。 + +H3_REQUEST_INCOMPLETE(0x010d): 客户端请求不完整,流被终止。 + +H3_MESSAGE_ERROR(0x010e): HTTP消息格式错误,无法处理。 + +H3_CONNECT_ERROR(0x010f): 响应CONNECT请求而建立的TCP连接被重置或突然地关闭了。 + +H3_VERSION_FALLBACK(0x0110): 无法在HTTP/3上为请求提供服务,对端应当在HTTP/1.1上重试。 + +形如 0x1f*N+0x21(N为非负整数)格式的错误码,测试对端是否履行将未知错误码视为等同于H3_NO_ERROR的要求(第9章)。实现应该(**SHOULD**)在发送H3_NO_ERROR时,以一定概率从此格式集中选择一个错误码发送。 + +# 9. Extensions to HTTP/3 + +HTTP/3允许扩展协议。在本节描述的限制范围内,协议扩展可用于提供附加服务或更改协议的任何方面。扩展仅在单个HTTP/3连接范围内有效。 + +这适用于本文中定义的协议元素,不会影响扩展HTTP的现有选项,例如定义新方法、状态码或字段。 + +允许扩展使用新的帧类型(第7.2节)、新的settings(第7.2.4.1节)、新的错误码(第8章)或新的单向流类型(第6.2节)。为管理这些扩展点在IANA建立了注册表:帧类型(第11.2.1节)、settings(第11.2.2节)、错误码(第11.2.3节)和流类型(第11.2.4节)。 + +实现必须(**MUST**)忽略所有可扩展协议元素中的未知或不受支持的值。实现必须(**MUST**)丢弃数据或中止对具有未知或不受支持类型的单向流的读取。这意味着这些扩展点中的任何一个都可以被扩展安全地使用,而无需事先安排或协商。但是,如果已知帧类型需要位于特定位置,例如SETTINGS帧作为控制流的第一帧(见第6.2.1节),未知的帧类型不满足该要求,应该(**SHOULD**)被视为错误。 + +可能会改变现有协议组件语义的扩展必须(**MUST**)在使用之前进行协商。例如,更改HEADERS格式的扩展在对端给出可以接受的肯定信号之前,不能使用该HEADERS格式。修改现有格式以待生效的方式实现比较复杂,因此,为现有协议元素新定义、分配新标识符可能更有效率。 + +本文没有规定协商使用扩展的具体方法,不过有指出可以使用setting(第7.2.4.1节)达成此目的。如果两端都设置了表示其愿意使用的扩展的值,则可以使用扩展。如果setting用于扩展协商,则必须(**MUST**)以这样一种方式定义默认值,即如果省略该setting,则禁用扩展。 + + +# 10. Security Considerations + +HTTP/3的安全注意事项考量应当和HTTP/2 over TLS类似。不过,[[HTTP/2]](#http2)第10章中的许多考虑因素适用于[[QUIC-TRANSPORT]](#quic) ,参见[[QUIC-TRANSPORT]](#quic) 的讨论。 + +## 10.1. Server Authority + +HTTP/3依赖于HTTP的权限定义。[[HTTP]](#http)的第17.1节讨论了建立授权的安全注意事项。 + +## 10.2. Cross-Protocol Attacks + +在TLS和QUIC握手时使用ALPN可以在应用层数据传输之前协商好应用层协议。这种机制可以保证两端使用相同的协议。 + +这并不能保证免受所有跨协议攻击。[[QUIC-TRANSPORT]](#quic)的第21.5节描述了QUIC数据包的明文可用于对不使用认证传输的终端进行请求伪造一些方法。 + +## 10.3. Intermediary Encapsulation Attacks + +HTTP/3字段编码允许在HTTP使用的语法中表达不是有效字段名的名称([[HTTP]](#http)的第5.1节)。包含无效字段名的请求或响应必须(**MUST**)被视为格式错误。因此,中间设备无法将包含无效字段名的HTTP/3请求或响应转换为HTTP/1.1消息。 + +同样,HTTP/3可以传输无效的字段值。虽然大多数可以编码的值不会影响字段解析,但如果逐字翻译,攻击者可能会利用回车符(ASCII 0x0d)、换行符(ASCII 0x0a)和空字符(ASCII 0x00)。任何包含字段值中不允许的字符的请求或响应必须(**MUST**)被视为格式错误。有效字符在[[HTTP]](#http)的第5.5节中的“字段内容”ABNF规则中定义。 + +## 10.4. Cacheability of Pushed Responses + +推送的响应没有来自客户端的明确请求,该请求由服务端在PUSH_PROMISE帧中提供。 + +根据源服务端在Cache-Control标头字段中的指引,客户端可以缓存推送的响应。但是,如果一台服务器托管多个租户,这可能会出现问题。例如,服务器可能会将其URI空间分散提供给多个租户。 + +当多个租户在同一台​​服务端上共享空间时,该服务端必须(**MUST**)确保租户无法推送他们无权访问的资源表示。如果不执行此操作,租户将可以从缓存中提取URI表示,从而覆盖已授权租户提供的实际资源。 + +客户端被要求拒绝非授权源服务端的推送响应;见第4.6节。 + +## 10.5. Denial-of-Service Considerations + +与HTTP/1.1或HTTP/2连接相比,HTTP/3连接可能需要更大的资源投入才能运行。字段压缩和流控的使用需要存储大量状态,因此需要更多资源。这些特性的设置项可确保其内存要求受到严格限制。 + +PUSH_PROMISE帧的数量以类似的方式受到约束。接受服务端推送的客户端应该(**SHOULD**)限制它一次发布的push ID的数量。 + +处理能力不能像状态能力那样得到有效保护。 + +发送要求对端忽略的,未定义的协议元素的能力,可能会被滥用,以导致对端花费额外的处理时间。这可以通过设置多个未定义的SETTINGS参数、未知帧类型或未知流类型来达成目的。但是请注意,某些用途是完全合法的,例如用可选可理解的扩展和填充以增加对流量分析的对抗能力。 + +压缩字段部分也提供了一些浪费处理资源的机会;有关潜在滥用的更多详细信息,请参阅[[QPACK]](#qpack)的第7章。 + +所有这些特性——即服务端推送、未知协议元素、字段压缩——都有合法用途。这些功能只有在不必要或过度使用时才会成为负担。 + +不监视此类行为的终端会将自己暴露在拒绝服务攻击的风险中。实现应该(**SHOULD**)跟踪这些特性的使用并设置它们的使用限制。终端可以(**MAY**)将可疑的活动视为H3_EXCESSIVE_LOAD类型的连接错误,但误报会导致中断有效的连接和请求。 + +### 10.5.1. Limits on Field Section Size + +大的字段部分(第4.1节)可能导致实现提交大量状态。对路由至关重要的标头字段可能出现在标头部分的末尾,这可能阻止标头部段流到达其最终目的地。这种乱序和其他原因(例如缓存校对)意味着终端可能需要缓冲整个标头段。由于字段部分的大小没有硬性限制,一些终端可能被迫为标头字段提交大量可用内存。 + +终端可以使用SETTINGS_MAX_FIELD_SECTION_SIZE(第4.2.2节)setting,以告知对端可能适用于字段部分大小的限制。此setting只是建议性的,因此终端可以(**MAY**)选择发送超出此限制的字段部分,并有可能被接收端将请求或响应视为格式错误。此setting特定于HTTP/3连接,因此任何请求或响应都可能遇到具有较低或未知限制的下一跳。中间设备可以尝试不同终端提供的值来避免这个问题,但他们不是强制的。 + +接收到比它愿意处理的更大的字段部分的服务端可以发送HTTP 431(请求标头字段太大)状态码([[RFC6585]](#rfc6585))。客户端可以丢弃它无法处理的响应。 + +### 10.5.2. CONNECT Issues + +CONNECT方法可用于在代理上创建不对称的负载,因为与创建和维护TCP连接相比,流创建性价比相对较高。因此,支持CONNECT的代理可能在它接受的并发数量上更加保守。 + +代理还可能会在关闭携带CONNECT请求的流之后,为TCP连接维护一些资源,因为传出TCP连接仍处于TIME_WAIT状态。考虑到这一点,代理可能会在TCP连接终止后,稍后增大QUIC流限制一段时间。 + +## 10.6. Use of Compression + +当加密数据在与攻击者掌握的数据相同的上下文中被压缩时,攻击者可能可以恢复加密数据。HTTP/3启用字段压缩(第4.2节);以下担心也适用于HTTP压缩内容编码;请参阅[[HTTP]](#http)的第8.4.1节。 + +有一些比较常见的利用网络特性的压缩攻击(例如,[[BREACH]](#breach))。攻击者诱发出多个包含不同明文的请求,观察每个请求中生成的密文的长度,当对秘钥的猜测正确时,会显示较短的长度。 + +在安全通道上通信的实现,不得(**MUST NOT**)同时压缩包含加密和攻击者掌握的数据的内容,除非每个数据源使用单独的压缩上下文。如果不能可靠地确定数据源,则不得(**MUST NOT**)使用压缩。 + +[[QPACK]](#qpack)中描述了有关字段部分压缩的进一步考虑。 + + +## 10.7. Padding and Traffic Analysis + +填充可用于模糊帧内容的确切长度,减轻HTTP中的特定攻击,例如如上节所述的对压缩内容包括密文数据和攻击者掌握的明文的攻击(例如,[[BREACH]](#breach))。 + +HTTP/2使用PADDING帧,和其他帧中的填充字段,以使连接更能抵抗流量分析,而HTTP/3依赖传输层PADDING或使用第7.2.8节和第6.2节中讨论的保留帧和流类型。这些填充方法产生的什么样的结果与如下事项相关:填充的粒度、如何根据受保护的信息进行填充、是否在数据包丢失的情况下应用填充以及实现如何控制填充等方面。 + +即使连接空闲,保留流类型也可用于流量整形。由于HTTP流量经常以突发的形式出现,因此可以使用明显的流量来掩盖这种突发的时间点或持续时间,甚至使之看起来像是在发送恒定的数据流。但是,由于此类流量仍由接收方进行流控,因此流量耗尽却没有获得足够的流控credit的话可能会限制发送方发送真实流量的能力。 + +为了减轻基于压缩的攻击,禁用或限制压缩可能比使用填充更可取。 + +使用填充可能会导致保护效果比看上去明显的要差。冗余填充甚至可能适得其反。充其量,填充只会增加攻击者通过增加观察帧数来推断长度信息的难度。错误实现的填充方案更容易被破解。特别的,具有可预测分布的随机填充提供的保护也非常少。同样,将有效负载填充到固定大小会在其长度跨越固定大小边界时暴露信息,如果攻击者可以控制明文,这种情况是可能出现的。 + +## 10.8. Frame Parsing + +少数协议元素包含嵌套的长度元素,通常是在包含以可变长度整数编码的显式长度的帧中。这可能会给粗心的实现带来安全风险。实现必须(**MUST**)确保帧的长度与其包含的长度字段完全匹配。 + +## 10.9. Early Data + +HTTP/3使用0-RTT会导致重放攻击。当以0-RTT机制发送HTTP/3数据时,必须(**MUST**)应用[HTTP-REPLAY]中的反重放规避措施。在HTTP/3应用[[HTTP-REPLAY]](#http-replay)时,提及的TLS是指在QUIC中的TLS握手,而提及的所有应用数据都是指流的内容。 + +## 10.10. Migration + +某些HTTP实现使用客户端地址进行日志记录或访问控制。由于QUIC客户端的地址可能会在连接期间发生变化(并且未来版本可能支持同时使用多个地址),因此此类实现将需要主动检索客户端的当前地址或相关地址,或者明确接受初始地址可能会更改的事实。 + +## 10.11. Privacy Considerations + +HTTP/3的几个特性为观察者提供了将单个客户端或服务端的操作随时间关联的机会。这些包括setting的值、对激励的反应时间以及对settings控制的任何功能的处理。 + +只要这些行为产生了可观察到的差异,它们就可以作为对特定客户进行指纹识别的基础。 + +HTTP/3对使用单个QUIC连接的偏好可以关联用户在站点上的活动。不同源重用连接可以关联跨这些源的活动。 + +有几个QUIC特性会要求立即响应,终端可以使用它来测量对端的延迟;在某些情况下,这可能对隐私有些影响。 + +# 11. IANA Considerations +本文注册了一个新的ALPN协议ID(第11.1节)并创建了管理HTTP/3中码点分配的新注册表。 + +## 11.1. Registration of HTTP/3 Identification String +本文为在[[RFC7301]](#rfc7301)中创建的“TLS Application-Layer Protocol Negotiation (ALPN) Protocol IDs”注册表中的HTTP/3标识创建了一个新的注册项。 + +"h3" 标识 HTTP/3: + +Protocol: +HTTP/3 + +Identification Sequence: +0x68 0x33 ("h3") + +Specification: +This document + +## 11.2. New Registries + +本文中创建的新注册项遵循QUIC注册策略[[QUIC-TRANSPORT]](#quic)第22.1节中的约定,都包含列在[[QUIC-TRANSPORT]](#quic)第22.1.1节中的通用字段集。这些注册项都归类在"Hypertext Transfer Protocol version 3 (HTTP/3)"标题下。 + +这些注册表项的初始值都为永久状态,都列出了IETF变更控制者和HTTP工作组的联系人 (ietf-http-wg@w3.org)。 + +## 11.2.1. Frame Types + +本文为HTTP/3帧类型编码创建了一个注册表,取值为62位空间。此注册表遵循第22.1节中的注册策略。此注册表中的永久注册项遵循([[RFC8126]](#rfc8126)第4.6节)规约策略进行分配,但0x00和0x3f(十六进制)之间的值除外,这些值使用[[RFC8126]](#rfc8126)第4.9和4.10节定义的Standards Action或IESG许可进行分配。 + +尽管此注册表与[[HTTP/2]](#http2)中定义的"HTTP/2 Frame Type"注册表是分开的,但在码点空间重叠的情况下,分配值最好彼此平行。如果一个条目仅存在于一个注册表中,则应该(**SHOULD**)努力避免将相应的值分配给不相关的操作。评审专家可以(**MAY**)拒绝与相应注册表中的相同值冲突的无关注册。 + +除了第11.2节中列出的字段外,此注册表中的永久注册项必须(**MUST**)包括以下字段: + +Frame Type: 帧类型名称或标签 + +帧类型的定义必须包含对帧格式及其语义的描述,也必须(**MUST**)包含任何可选条件项。 +表2中的条目由本文注册: + +Table 2: Initial HTTP/3 Frame Types +| Frame Type | Value | Specification | +|:---|:---|:---| +| DATA | 0x00 | Section 7.2.1 | +| HEADERS | 0x01 | Section 7.2.2 | +| Reserved | 0x02 | This document | +| CANCEL_PUSH | 0x03 | Section 7.2.3 | +| SETTINGS | 0x04 | Section 7.2.4 | +| PUSH_PROMISE | 0x05 | Section 7.2.5 | +| Reserved | 0x06 | This document | +| GOAWAY | 0x07 | Section 7.2.6 | +| Reserved | 0x08 | This document | +| Reserved | 0x09 | This document | +| MAX_PUSH_ID | 0x0d | Section 7.2.7 | + +形如 0x1f * N + 0x21(N为非负整数)值(即0x21, 0x40, ..., 直至0x3ffffffffffffffe)不得(**MUST NOT**)由IANA分配,也不得(**MUST NOT**)出现在分配值列表中。 + +## 11.2.2. Settings Parameters + +本文为HTTP/3帧类型编码创建了一个注册表,取值为62位空间。此注册表遵循第22.1节中的注册策略。此注册表中的永久注册项遵循([[RFC8126]](#rfc8126)第4.6节)规约策略进行分配,但0x00和0x3f(十六进制)之间的值除外,这些值使用[[RFC8126]](#rfc8126)第4.9和4.10节定义的Standards Action或IESG许可进行分配。 + +尽管此注册表与[[HTTP/2]](#http2)中定义的"HTTP/2 Frame Type"注册表是分开的,但在码点空间重叠的情况下,分配值最好彼此平行。如果一个条目仅存在于一个注册表中,则应该(**SHOULD**)努力避免将相应的值分配给不相关的操作。评审专家可以(**MAY**)拒绝与相应注册表中的相同值冲突的无关注册。 + +除了第11.2节中列出的字段外,此注册表中的永久注册项必须(**MUST**)包括以下字段: + +Setting Name: setting的符号名称,指定setting名称是可选的。 + +Default: 默认值。默认值应该(**SHOULD**)严格指定。 + +表3中的条目由本文注册: + +Table 3: Initial HTTP/3 Settings +| Setting Name | Value | Specification | Default | +|:---|:---|:---|:---| +| Reserved | 0x00 | This document | N/A | +| Reserved | 0x02 | This document | N/A | +| Reserved | 0x03 | This document | N/A | +| Reserved | 0x04 | This document | N/A | +| Reserved | 0x05 | This document | N/A | +| MAX_FIELD_SECTION_SIZE | 0x06 | Section 7.2.4.1 | Unlimited | + +出于格式原因,可以删除“SETTINGS_”前缀来缩写Setting名称。 + +形如 0x1f * N + 0x21(N为非负整数)值(即0x21, 0x40, ..., 直至0x3ffffffffffffffe)不得(**MUST NOT**)由IANA分配,也不得(**MUST NOT**)出现在分配值列表中。 + +## 11.2.3. Error Codes +本文为HTTP/3帧类型编码创建了一个注册表,取值为62位空间。此注册表遵循第22.1节中的注册策略。此注册表中的永久注册项遵循([[RFC8126]](#rfc8126)第4.6节)规约策略进行分配,但0x00和0x3f(十六进制)之间的值除外,这些值使用[[RFC8126]](#rfc8126)第4.9和4.10节定义的Standards Action或IESG许可进行分配。 + +尽管此注册表与[[HTTP/2]](#http2)中定义的"HTTP/2 Frame Type"注册表是分开的,但在码点空间重叠的情况下,分配值最好彼此平行。如果一个条目仅存在于一个注册表中,则应努力避免将相应的值分配给不相关的操作。评审专家可以(**MAY**)拒绝与相应注册表中的相同值冲突的无关注册。 + +除了第11.2节中列出的字段外,此注册表中的永久注册项必须(**MUST**)包括以下字段: + +Name: 错误码名称。 + +Description: 错误码语义的简要描述。 + +表4中的条目由本文注册,本文定义的错误码遵循Specification Required策略约定的范围,尽量避免与HTTP/2的错误码项冲突: + +Table 4: Initial HTTP/3 Error Codes +| Name | Value | Description | Specification | +|:---|:---|:---|:---| +| H3_NO_ERROR | 0x0100 | No error | Section 8.1 | +| H3_GENERAL_PROTOCOL_ERROR | 0x0101 | General protocol error | Section 8.1 | +| H3_INTERNAL_ERROR | 0x0102 | Internal error | Section 8.1 | +| H3_STREAM_CREATION_ERROR | 0x0103 | Stream creation error | Section 8.1 | +| H3_CLOSED_CRITICAL_STREAM | 0x0104 | Critical stream was closed | Section 8.1 | +| H3_FRAME_UNEXPECTED | 0x0105 | Frame not permitted in the current state | Section 8.1 | +| H3_FRAME_ERROR | 0x0106 | Frame violated layout or size rules | Section 8.1 | +| H3_EXCESSIVE_LOAD | 0x0107 | Peer generating excessive load | Section 8.1 | +| H3_ID_ERROR | 0x0108 | An identifier was used incorrectly | Section 8.1 | +| H3_SETTINGS_ERROR | 0x0109 | SETTINGS frame contained invalid values | Section 8.1 | +| H3_MISSING_SETTINGS | 0x010a | No SETTINGS frame received | Section 8.1 | +| H3_REQUEST_REJECTED | 0x010b | Request not processed | Section 8.1 | +| H3_REQUEST_CANCELLED | 0x010c | Data no longer needed | Section 8.1 | +| H3_REQUEST_INCOMPLETE | 0x010d | Stream terminated early | Section 8.1 | +| H3_MESSAGE_ERROR | 0x010e | Malformed message | Section 8.1 | +| H3_CONNECT_ERROR | 0x010f | TCP reset or error on CONNECT request | Section 8.1 | +| H3_VERSION_FALLBACK | 0x0110 | Retry over HTTP/1.1 | Section 8.1 | + +形如 0x1f * N + 0x21(N为非负整数)值(即0x21, 0x40, ..., 直至0x3ffffffffffffffe)不得(**MUST NOT**)由IANA分配,也不得(**MUST NOT**)出现在分配值列表中。 + +## 11.2.4. Stream Types +本文为HTTP/3帧类型编码创建了一个注册表,取值为62位空间。此注册表遵循第22.1节中的注册策略。此注册表中的永久注册项遵循([[RFC8126]](#rfc8126)第4.6节)规约策略进行分配,但0x00和0x3f(十六进制)之间的值除外,这些值使用[[RFC8126]](#rfc8126)第4.9和4.10节定义的Standards Action或IESG许可进行分配。 + +除了第11.2节中列出的字段外,此注册表中的永久注册项必须(**MUST**)包括以下字段: + +Stream Type: 流类型名称或标签。 + +Sender: 发送方,在HTTP/3连接上发起此类型流的终端,取值可为:客户端、服务端、两端 + +永久注册项的定义必须(**MUST**)包含流类型、流格式及流内容的语义。 + +表5中的条目由本文注册, + +Table 5: Initial Stream Types +| Stream Type | Value | Specification | Sender | +| :------| :----- | :----- |:----- | +| Control Stream | 0x00 | Section 6.2.1 | Both | +| Push Stream | 0x01 | Section 4.6 | Server | + +形如 0x1f * N + 0x21(N为非负整数)值(即0x21, 0x40, ..., 直至0x3ffffffffffffffe)不得(**MUST NOT**)由IANA分配,也不得(**MUST NOT**)出现在分配值列表中。 + +# 12. References +## 12.1. Normative References +[ALTSVC] +Nottingham, M., McManus, P., and J. Reschke, "HTTP Alternative Services", RFC 7838, DOI 10.17487/RFC7838, April 2016, . + +[COOKIES] +Barth, A., "HTTP State Management Mechanism", RFC 6265, DOI 10.17487/RFC6265, April 2011, . + +[HTTP] +Fielding, R., Ed., Nottingham, M., Ed., and J. Reschke, Ed., "HTTP Semantics", STD 97, RFC 9110, DOI 10.17487/RFC9110, June 2022, . + +[HTTP-CACHING] +Fielding, R., Ed., Nottingham, M., Ed., and J. Reschke, Ed., "HTTP Caching", STD 98, RFC 9111, DOI 10.17487/RFC9111, June 2022, . + +[HTTP-REPLAY] +Thomson, M., Nottingham, M., and W. Tarreau, "Using Early Data in HTTP", RFC 8470, DOI 10.17487/RFC8470, September 2018, . + +[QPACK] +Krasic, C., Bishop, M., and A. Frindell, Ed., "QPACK: Field Compression for HTTP/3", RFC 9204, DOI 10.17487/RFC9204, June 2022, . + +[QUIC-TRANSPORT] +Iyengar, J., Ed. and M. Thomson, Ed., "QUIC: A UDP-Based Multiplexed and Secure Transport", RFC 9000, DOI 10.17487/RFC9000, May 2021, . + +[RFC0793] +Postel, J., "Transmission Control Protocol", STD 7, RFC 793, DOI 10.17487/RFC0793, September 1981, . + +[RFC2119] +Bradner, S., "Key words for use in RFCs to Indicate Requirement Levels", BCP 14, RFC 2119, DOI 10.17487/RFC2119, March 1997, . + +[RFC6066] +Eastlake 3rd, D., "Transport Layer Security (TLS) Extensions: Extension Definitions", RFC 6066, DOI 10.17487/RFC6066, January 2011, . + +[RFC7301] +Friedl, S., Popov, A., Langley, A., and E. Stephan, "Transport Layer Security (TLS) Application-Layer Protocol Negotiation Extension", RFC 7301, DOI 10.17487/RFC7301, July 2014, . + +[RFC8126] +Cotton, M., Leiba, B., and T. Narten, "Guidelines for Writing an IANA Considerations Section in RFCs", BCP 26, RFC 8126, DOI 10.17487/RFC8126, June 2017, . + +[RFC8174] +Leiba, B., "Ambiguity of Uppercase vs Lowercase in RFC 2119 Key Words", BCP 14, RFC 8174, DOI 10.17487/RFC8174, May 2017, . + +[URI] +Berners-Lee, T., Fielding, R., and L. Masinter, "Uniform Resource Identifier (URI): Generic Syntax", STD 66, RFC 3986, DOI 10.17487/RFC3986, January 2005, . + + +## 12.2. Informative References +[BREACH] +Gluck, Y., Harris, N., and A. Prado, "BREACH: Reviving the CRIME Attack", July 2013, . + +[DNS-TERMS] +Hoffman, P., Sullivan, A., and K. Fujiwara, "DNS Terminology", BCP 219, RFC 8499, DOI 10.17487/RFC8499, January 2019, . + +[HPACK] +Peon, R. and H. Ruellan, "HPACK: Header Compression for HTTP/2", RFC 7541, DOI 10.17487/RFC7541, May 2015, . + +[HTTP/1.1] +Fielding, R., Ed., Nottingham, M., Ed., and J. Reschke, Ed., "HTTP/1.1", STD 99, RFC 9112, DOI 10.17487/RFC9112, June 2022, . + +[HTTP/2] +Thomson, M., Ed. and C. Benfield, Ed., "HTTP/2", RFC 9113, DOI 10.17487/RFC9113, June 2022, . + +[RFC6585] +Nottingham, M. and R. Fielding, "Additional HTTP Status Codes", RFC 6585, DOI 10.17487/RFC6585, April 2012, . + +[RFC8164] +Nottingham, M. and M. Thomson, "Opportunistic Security for HTTP/2", RFC 8164, DOI 10.17487/RFC8164, May 2017, . + +[TFO] +Cheng, Y., Chu, J., Radhakrishnan, S., and A. Jain, "TCP Fast Open", RFC 7413, DOI 10.17487/RFC7413, December 2014, . + +[TLS] +Rescorla, E., "The Transport Layer Security (TLS) Protocol Version 1.3", RFC 8446, DOI 10.17487/RFC8446, August 2018, . + +# Appendix A. Considerations for Transitioning from HTTP/2 +HTTP/3受到HTTP/2的深刻影响,两者有许多相似之处。本部分介绍HTTP/3的设计方法,指出与HTTP/2的重要区别,并说明如何将HTTP/2扩展映射到HTTP/3。 + +HTTP/3设计尽量向HTTP/2靠拢,但不是硬性要求。HTTP/3与HTTP/2不同的,正如QUIC不同于TCP,既可充分利用QUIC的特性(如流),也可适应其大的不足(如缺失连接上的保序)。HTTP/3在流上请求和响应的关系等关键方面与HTTP/2类似,但HTTP/3设计的细节与HTTP/2有很大差异。 + +本部分指出了一些重要的差异。 + +## A.1. Streams +HTTP/3允许使用比HTTP/2更多的流(262-1)。但关于耗尽流ID空间的考虑也是相同的,尽管HTTP/3空间大得多,使得可能首先达到QUIC中的其他限制,例如连接流控窗口的限制。 + +与HTTP/2不同,HTTP/3中的流并发由QUIC管理。当所有数据都已收齐,并且其发送的数据都已被对端确认时,QUIC认为流关闭。HTTP/2是在包含END_STREAM位的帧已提交给传输层时,即认为流关闭。因此,交换同等内容的话,HTTP/3的流可能在更长的时间段内保持“活动”状态。HTTP/3服务端可以根据预期的使用模式,选择允许客户端发起更多的并发双向流,以实现与HTTP/2等效。 + +在HTTP/2中,只有请求和响应的body内容(DATA帧的有效载荷)受流控处理。HTTP/3所有帧都在QUIC流上发送,因此所有流上承载的帧都在HTTP/3中进行流控。 + +由于存在其他单向流类型,HTTP/3并不完全由并发单向流的数量,来决定在途的服务端并发推送的数量,而是由客户端发送MAX_PUSH_ID帧进行控制。 + +## A.2. HTTP Frame Types +HTTP/2与传输有关的许多帧概念可以下沉至QUIC。由于帧已经在各自的流上发送,因此可以省略流编号。由于帧不会阻塞多路复用(QUIC的多路复用在此层之下),因此对variable-maximum-length数据包的支持可以移除。由于流关闭由QUIC处理,不再需要END_STREAM标记,因此可从通用帧格式中删除Flags字段。 + +帧有效载荷主要来自[[HTTP/2]](#http2)。不过因为QUIC包括HTTP/2中也存在的许多特性(如流控),HTTP/3映射不会再重复实现一次。因此,HTTP/2帧类型中有一些在HTTP/3中不再需要。这些帧ID会被保留,以便在HTTP/2和HTTP/3实现之间实现最大程度的可移植性。不过,两种映射中都出现的帧类型,其语义也不尽相同。 + +许多差异源于HTTP/2在整个连接的所有流上提供帧与帧之间的保序,而QUIC仅在每个流上提供这种保证。因此,如果帧类型假定需要HTTP/2这种保序传输,则HTTP/3将重新设计适配机制。 + +下面描述一些特性适配实例,以及将HTTP/2帧扩展转换为HTTP/3的通用指导。 + +### A.2.1. Prioritization Differences +HTTP/2在PRIORITY帧和HEADERS帧(可选)中指定优先级,HTTP/3不提供指示信令优先级的手段。 + +请注意,虽然没有明确的信令优先级,但这并不意味着优先级对于改善性能不重要。 + +### A.2.2. Field Compression Differences +HPACK设计的前提是按保序传输。编码后的字段区间序列必须按其编码顺序到达对端(并被解码)。这可确保两个终端的动态状态保持同步。 + +由于QUIC不提供此保序承诺,HTTP/3使用改版后的HPACK,称为QPACK。QPACK使用单向流发送对动态表的所有修改,从而使更新保序。包含编码字段的帧仅在给定时间引用表状态,而不能修改它。 + +详情参见[[QPACK]](#qpack)。 + +### A.2.3. Flow-Control Differences +HTTP/2定义了一套流控机制。即使所有HTTP/2帧都在流上传送,但只有DATA帧负载才受流控管理。QUIC为流数据提供流控,本文定义的所有HTTP/3帧类型都在流上发送。因此,所有帧报头和有效载荷都受流控制。 + +### A.2.4. Guidance for New Frame Type Definitions +HTTP/3中的帧类型定义通常使用QUIC可变长度整数编码。特别是,流ID也使用此编码,比HTTP/2中的取值范围更大。HTTP/3中的某些帧使用非流ID(例如,push ID)的标识符。如果编码中包含流ID,则可能需要重新定义扩展帧类型编码格式。 + +由于通用HTTP/3帧中不存在Flags字段,因此依赖Flags存在的那些帧需要为Flag分配空间,作为其帧负载的一部分。 + +除了上述问题之外,HTTP/2帧类型扩展通常只需将HTTP/2中的流0替换为HTTP/3中的控制流,即可移植到QUIC。HTTP/3扩展不假定连接级保序,也不受乱序影响,预期可移植到HTTP/2。 + +### A.2.5. Comparison of HTTP/2 and HTTP/3 Frame Types +DATA(0x00): +HTTP/3 DATA帧中没有定义填充。请参阅7.2.1节。 + +HEADERS(0x01): +HTTP/3 HEADERS帧中没有定义PRIORITY字段、没有定义填充。请参阅7.2.2节。 + +PRIORITY(0x02): +如附录A.2.1所述,HTTP/3不提供信令优先级的手段。 + +RST_STREAM(0x03): +HTTP/3中不存在RST_STREAM帧,因为QUIC提供流生命周期管理。CANCEL_PUSH帧使用相同的码点(7.2.3节)。 + +SETTINGS(0x04): +SETTINGS帧仅在连接开始时发送。请参阅7.2.4和附录A.3。 + +PUSH_PROMISE(0x05): +PUSH_PROMISE帧不引用一个流;相反,推送流中通过push ID引用PUSH_PROMISE帧。请参阅7.2.5节。 + +PING(0x06): +HTTP/3中不存在PING帧,因为QUIC提供等价功能。 + +GOAWAY(0x07): +GOAWAY不包含错代码。在客户端到服务端方向,它携带push ID,而不是服务端发起的流ID。请参阅7.2.6节。 + +WINDOW_UPDATE(0x08): +HTTP/3中不存在WINDOW_UPDATE帧,因为QUIC提供流控。 + +CONTINUATION(0x09):无效 +HTTP/3中不存在连续帧;相反,允许比HTTP/2更大的HEADERS/PUSH_PROMISE帧。 + +如果仍然适用,则需要为HTTP/3单独注册由HTTP/2扩展定义的帧类型。为了简单起见,保留了在[[HTTP/2]](#http2)中定义的帧的ID。请注意,HTTP/3中的帧类型取值空间大得多(62位比8位),因此许多HTTP/3帧类型没有等价的HTTP/2码点。请参阅第11.2.1节。 + +## A.3. HTTP/2 SETTINGS Parameters +与HTTP/2的一个重要区别是,HTTP/3的SETTINGS帧作为控制流的第一个帧,只发送一次,之后不能更改。这样就避免了许多围绕同步更改的边角场景。 + +HTTP/2通过SETTINGS帧指定的,某些传输级别的setting,已被被HTTP/3中的QUIC传输参数取代。HTTP/3中保留的HTTP级别setting与HTTP/2中值相同。已被取代的setting是保留的,接收到此类setting要报错。有关保留值和保留值的讨论,请参阅7.2.4.1节。 + +以下是HTTP/2 SETTINGS参数的映射列表: + +SETTINGS_HEADER_TABLE_SIZE(0x01): +请参阅[[QPACK]](#qpack) + +SETTINGS_ENABLE_PUSH(0x02): +删除此选项,代以MAX_PUSH_ID帧,该帧可对服务端推送进行更精细的控制。在 HTTP/3 SETTINGS帧中设置标识符为0x02(对应于SETTINGS_ENABLE_PUSH参数)的setting是错误的。 + +SETTINGS_MAX_CONCURRENT_STREAMS(0x03): +QUIC管理最大打开流ID,作为其流控逻辑的一部分。在 HTTP/3 SETTINGS帧中设置标识符为0x03的setting(对应于SETTINGS_MAX_CONCURRENT_STREAMS参数)是错误的。 + +SETTINGS_INITIAL_WINDOW_SIZE(0x04): +QUIC要求在初始传输握手中指定流和连接的流控窗口大小。在 HTTP/3 SETTINGS帧中设置标识符为0x04的setting(对应于SETTINGS_INITIAL_WINDOW_SIZE参数)是错误的。 + +SETTINGS_MAX_FRAME_SIZE(0x05): +此setting在HTTP/3中没有等价项。在 HTTP/3 SETTINGS帧中设置标识符为为0x05的setting(对应参数SETTINGS_MAX_FRAME_SIZE)是错误的。 + +SETTINGS_MAX_HEADER_LIST_SIZE(0x06): +此setting标识符已重命名SETTINGS_MAX_FIELD_SECTION_SIZE。 + +在HTTP/3中,setting值是可变长度整数(6、14、30或62位),而不是HTTP/2中的固定长度32位。这通常产生较短的编码,但使用32位取值空间的setting会产生更长的编码。从HTTP/2移植过来的setting可以选择重新定义其值,将其限制为30位进行更高效的编码,或者在需要超过30位的情况下使用62位空间。 + +需要为HTTP/2和HTTP/3分别定义setting。为简化起见,保留了[[HTTP/2]](#http2)中定义的setting ID。请注意,HTTP/3中的setting标识符空间大得多(62位比16位),因此许多HTTP/3 setting没有等价的HTTP/2码点。请参阅第11.2.2节。 + +由于QUIC流可能乱序,建议终端不要等待对端的setting到达之后再响应其他流。请参阅7.2.4.2节。 + +## A.4. HTTP/2 Error Codes +QUIC对“流”和“连接”级错误的定义与HTTP/2一样。不过,HTTP/2和HTTP/3之间的差异,意味着两个版本间的错误码不会直接迁移。 + +[[HTTP/2]](#http2)第7章中定义的HTTP/2错误码与HTTP/3错误码的逻辑映射如下所示: + +NO_ERROR(0x00): +H3_NO_ERROR 参见第8.1节。 + +PROTOCOL_ERROR(0x01): +映射到H3_GENERAL_PROTOCOL_ERROR,除非有更具体的错误码。如第8.1节中定义的H3_FRAME_UNEXPECTED、H3_MESSAGE_ERROR和H3_CLOSED_CRITAL_STREAM。 + +INTERNAL_ERROR(0x02): +参见第8.1节的H3_INTERNAL_ERROR。 + +FLOW_CONTROL_ERROR(0x03): +不适用,因为QUIC处理流控。 + +SETTINGS_TIMEOUT(0x04): +不适用,因为未定义SETTINGS的确认。 + +STREAM_CLOSED(0x05): +不适用,因为QUIC处理流管理。 + +FRAME_SIZE_ERROR(0x06): +FRAME_SIZE_ERROR,在第8.1节定义。 + +REFUSED_STREAM(0x07): +部分映射,H3_REQUEST_REJECTED(第8.1节)用于指示某个请求未被处理。其他情况不适用,因为QUIC进行流管理。 + +CANCEL(0x08): +参见H3_REQUEST_CANCELLED。 + +COMPRESSION_ERROR(0x09): +[[QPACK]](#qpack)中定义了多个相关错误码。 + +CONNECT_ERROR(0x0a): +参见第8.1节的H3_CONNECT_ERROR。 + +ENHANCE_YOUR_CALM(0x0b): +参见第8.1节的H3_EXCESSIVE_LOAD。 + +INADEQUATE_SECURITY(0x0c): +不适用,因为假定QUIC已为所有连接提供了足够的安全性。 + +HTTP_1_1_REQUIRED(0x0d): +参见第8.1节的H3_VERSION_FALLBACK。 + +需要分别定义的HTTP/2和HTTP/3错误码请参阅第11.2.3节。 + +## A.4.1. Mapping between HTTP/2 and HTTP/3 Errors +在HTTP/2和HTTP/3之间转换的中间设备可能需要传递上层错误条件,将错误的发生告知下层是有必要的,但错误码在很大程度上反映的是连接局部问题,这些问题通常没有传播的意义。 + +从上游来源收到错误码的中间设备,可以通过发送HTTP状态码(例如502(BadGateway))来指示这一点,该状态码适合于广泛的一类错误。 + +在少数情况下,对接收方来说,通过将错误映射到最接近的错误类型是有帮助的。例如,中间设备从HTTP/2源接收到流REFUSED_STREAM错误,表明请求未被处理、可以安全重试的信号,将此作为H3_REQUEST_REJECTED类型的HTTP/3流错误,发送到客户端,使得客户端可以执行其认为最适当的操作。在相反的方向上,中间设备可以用H3_REQUEST_CANCELLED错误关闭数据流,来传递客户请求取消的信号,见4.1.1节。 + +上节逻辑映射中描述了错误之间的转换关系。错误码定义空间不重叠,能防止意外转换导致对目标版本使用错误或未知的错误码。允许中间设备将流错误提升为连接错误,但其应知道对暂时或间歇性错误的话,这种处理对HTTP/3连接的成本有点大(可能导致连接关闭)。 diff --git a/include/xquic/xqc_configure.h b/include/xquic/xqc_configure.h new file mode 100644 index 000000000..1139f9153 --- /dev/null +++ b/include/xquic/xqc_configure.h @@ -0,0 +1,9 @@ +/* the configured options and settings for xquic */ +#define xquic_VERSION_MAJOR 0 +#define xquic_VERSION_MINOR 1 + +/* #undef XQC_ENABLE_BBR2 */ +/* #undef XQC_ENABLE_RENO */ +/* #undef XQC_ENABLE_COPA */ +/* #undef XQC_ENABLE_UNLIMITED */ +/* #undef XQC_ENABLE_MP_INTEROP */ diff --git a/include/xquic/xqc_errno.h b/include/xquic/xqc_errno.h index c8beba4f0..ea20d7051 100644 --- a/include/xquic/xqc_errno.h +++ b/include/xquic/xqc_errno.h @@ -23,8 +23,11 @@ typedef enum { TRA_INVALID_TOKEN = 0xB, TRA_APPLICATION_ERROR = 0xC, TRA_CRYPTO_BUFFER_EXCEEDED = 0xD, + TRA_0RTT_TRANS_PARAMS_ERROR = 0xE, /* MUST delete the current saved 0RTT transport parameters */ TRA_HS_CERTIFICATE_VERIFY_FAIL = 0x1FE, /* for handshake certificate verify error */ TRA_CRYPTO_ERROR = 0x1FF, /* 0x1XX */ + TRA_MP_PROTOCOL_VIOLATION_04 = 0xba01, + TRA_MP_PROTOCOL_VIOLATION_05 = 0x1001d76d3ded42f3, } xqc_trans_err_code_t; #define TRA_CRYPTO_ERROR_BASE 0x100 @@ -118,10 +121,16 @@ typedef enum { XQC_EMP_PATH_STATE_ERROR = 654, /* Multipath - abnormal path status */ XQC_EMP_SCHEDULE_PATH = 655, /* Multipath - fail to schedule path for sending */ XQC_EMP_NO_ACTIVE_PATH = 656, /* Multipath - no another active path */ + XQC_EMP_INVALID_MP_VERTION = 657, /* Multipath - the multipath version value is invalid */ XQC_EENCRYPT_LB_CID = 670, /* load balance connection ID encryption error */ XQC_EENCRYPT_AES_128_ECB = 671, /* aes_128_ecb algorithm error */ - + + XQC_EDGRAM_NOT_SUPPORTED = 680, /* Datagram - not supported */ + XQC_EDGRAM_TOO_LARGE = 681, /* Datagram - payload size too large */ + + XQC_EPMTUD_PROBING_SIZE = 682, /* PMTUD - probing size error */ + XQC_E_MAX, } xqc_transport_error_t; @@ -216,6 +225,11 @@ typedef enum { XQC_H3_BLOCKED_STREAM_EXCEED = 825, /* blocked_stream exceed limit */ XQC_H3_STREAM_RECV_ERROR = 826, /* call xqc_stream_recv error */ XQC_H3_INVALID_PRIORITY = 827, /* invalid http priority params or values */ + XQC_H3_INVALID_BIDI_STREAM_TYPE = 828, /* invalid bidi stream type */ + XQC_H3_ECREATE_BYTESTREAM = 829, /* fail to create a bytestream */ + XQC_H3_EPROC_BYTESTREAM = 830, /* fail to process bytestream */ + XQC_H3_BYTESTREAM_FIN_SENT = 831, /* try to send data on a bytestream that already sent FIN */ + XQC_H3_BYTESTREAM_MSG_BUF_EXIST = 832, /* try to create a msg buf while it already exists */ XQC_H3_ERR_MAX, } xqc_h3_error_t; diff --git a/include/xquic/xqc_http3.h b/include/xquic/xqc_http3.h index 5f4eabb3a..f14cae4e1 100644 --- a/include/xquic/xqc_http3.h +++ b/include/xquic/xqc_http3.h @@ -57,6 +57,8 @@ typedef int (*xqc_h3_request_notify_pt)(xqc_h3_request_t *h3_request, void *h3s_ typedef int (*xqc_h3_request_read_notify_pt)(xqc_h3_request_t *h3_request, xqc_request_notify_flag_t flag, void *h3s_user_data); +typedef void (*xqc_h3_request_closing_notify_pt)(xqc_h3_request_t *h3_request, + xqc_int_t err, void *h3s_user_data); /** * @brief encode flags of http headers @@ -112,7 +114,7 @@ typedef struct xqc_http_headers_s { } xqc_http_headers_t; -#define XQC_STREAM_INFO_LEN 400 +#define XQC_STREAM_INFO_LEN 100 /** * @brief request statistics structure @@ -120,8 +122,10 @@ typedef struct xqc_http_headers_s { typedef struct xqc_request_stats_s { size_t send_body_size; size_t recv_body_size; - size_t send_header_size; /* compressed header size */ - size_t recv_header_size; /* compressed header size */ + size_t send_header_size; /* plaintext header size */ + size_t recv_header_size; /* plaintext header size */ + size_t send_hdr_compressed; /* compressed header size */ + size_t recv_hdr_compressed; /* compressed header size */ int stream_err; /* QUIC layer error code, 0 for no error */ xqc_usec_t blocked_time; /* time of h3 stream being blocked */ xqc_usec_t unblocked_time; /* time of h3 stream being unblocked */ @@ -139,17 +143,39 @@ typedef struct xqc_request_stats_s { /** * @brief 请求级别MP状态 - * 0: 该请求所在连接未成功建立起双路 (validated_path_count <= 1) - * 1: 该请求是通过2条路径传输(发包or收包 任意方向,包括重注入数据) (validated_path_count > 1 && aggregate_cnt > 1) - * 2: 该请求所在连接有成功建立起双路,但请求在单条路径上传输 (validated_path_count > 1 && aggregate_cnt <= 1) + * 0: 该请求所在连接当前仅有一条可用路径 + * 1: 该请求所在连接当前有多条可用路径,该请求同时在 Available 和 Standby 路径传输 + * 2: 该请求所在连接当前有多条可用路径,但该请求仅在 Standby 路径传输 + * 3: 该请求所在连接当前有多条可用路径,但该请求仅在 Available 路径传输 */ int mp_state; float mp_default_path_send_weight; float mp_default_path_recv_weight; + float mp_standby_path_send_weight; + float mp_standby_path_recv_weight; + + uint64_t rate_limit; char stream_info[XQC_STREAM_INFO_LEN]; } xqc_request_stats_t; +/** + * @brief bytestream statistics + * + */ +typedef struct xqc_h3_ext_bytestream_stats_s { + size_t bytes_sent; + size_t bytes_rcvd; + int stream_err; + const char *stream_close_msg; + xqc_usec_t create_time; + xqc_usec_t fin_rcvd_time; + xqc_usec_t fin_read_time; + xqc_usec_t fin_sent_time; + xqc_usec_t fin_acked_time; + xqc_usec_t first_byte_sent_time; + xqc_usec_t first_byte_rcvd_time; +} xqc_h3_ext_bytestream_stats_t; /* connection settings for http3 */ typedef struct xqc_h3_conn_settings_s { @@ -175,8 +201,104 @@ typedef struct xqc_h3_conn_settings_s { } xqc_h3_conn_settings_t; +/** + * @brief callback for h3 bytestream read + * @param h3_ext_bs bytestream + * @param data data to be read. NOTE, this could be a NULL pointer, please ONLY read it if data_len > 0. + * @param data_len length of data to be read + * @param fin the bytestream is finished + * @param bs_user_data bytestream user data + * @param data_recv_time time spent for receiving data + */ +typedef int (*xqc_h3_ext_bytestream_read_notify_pt)(xqc_h3_ext_bytestream_t *h3_ext_bs, + const void *data, size_t data_len, uint8_t fin, void *bs_user_data, uint64_t data_recv_time); + +/** + * @brief callbacks for extended h3 bytestream + */ +typedef int (*xqc_h3_ext_bytestream_notify_pt)(xqc_h3_ext_bytestream_t *h3_ext_bs, + void *bs_user_data); + + +/** + * @brief the callback API to notify the application that there is a datagram to be read + * + * @param conn the connection handle + * @param user_data the user_data set by xqc_h3_ext_datagram_set_user_data + * @param data the data delivered by this callback + * @param data_len the length of the delivered data + * @param data_recv_time time spent for receiving data + */ +typedef void (*xqc_h3_ext_datagram_read_notify_pt)(xqc_h3_conn_t *conn, + const void *data, size_t data_len, void *user_data, uint64_t data_recv_time); + +/** + * @brief the callback API to notify the application that datagrams can be sent + * + * @param conn the connection handle + * @param user_data the user_data set by xqc_h3_ext_datagram_set_user_data + */ +typedef void (*xqc_h3_ext_datagram_write_notify_pt)(xqc_h3_conn_t *conn, + void *user_data); + +/** + * @brief the callback API to notify the application that a datagram is declared lost. + * However, the datagram could also be acknowledged later, as the underlying + * loss detection is not fully accurate. Applications should handle this type of + * spurious loss. The return value is used to ask the QUIC stack to retransmit the lost + * datagram packet. + * + * @param conn the connection handle + * @param user_data the user_data set by xqc_h3_ext_datagram_set_user_data + * @param dgram_id the id of the lost datagram + * @return 0, do not retransmit; + * XQC_DGRAM_RETX_ASKED_BY_APP, retransmit; + * others, ignored by the QUIC stack. + */ +typedef int (*xqc_h3_ext_datagram_lost_notify_pt)(xqc_h3_conn_t *conn, + uint64_t dgram_id, void *user_data); + +/** + * @brief the callback API to notify the application that a datagram is acked + * + * @param conn the connection handle + * @param user_data the user_data set by xqc_h3_ext_datagram_set_user_data + * @param dgram_id the id of the acked datagram + */ +typedef void (*xqc_h3_ext_datagram_acked_notify_pt)(xqc_h3_conn_t *conn, + uint64_t dgram_id, void *user_data); +/** + * @brief the callback to notify application the MSS of QUIC datagrams. Note, + * the MSS of QUIC datagrams will never shrink. If the MSS is zero, it + * means this connection does not support sending QUIC datagrams. + * + * @param conn the connection handle + * @param user_data the dgram_data set by xqc_h3_ext_datagram_set_user_data + * @param mss the MSS of QUIC datagrams + */ +typedef void (*xqc_h3_ext_datagram_mss_updated_notify_pt)(xqc_h3_conn_t *conn, + size_t mss, void *user_data); + + +typedef struct xqc_h3_ext_dgram_callbacks_s { + + /* the return value is ignored by XQUIC stack */ + xqc_h3_ext_datagram_read_notify_pt dgram_read_notify; + + /* the return value is ignored by XQUIC stack */ + xqc_h3_ext_datagram_write_notify_pt dgram_write_notify; + + /* the return value is ignored by XQUIC stack */ + xqc_h3_ext_datagram_acked_notify_pt dgram_acked_notify; + + /* the return value is ignored by XQUIC stack */ + xqc_h3_ext_datagram_lost_notify_pt dgram_lost_notify; + xqc_h3_ext_datagram_mss_updated_notify_pt dgram_mss_updated_notify; + +} xqc_h3_ext_dgram_callbacks_t; + /** * @brief http3 connection callbacks for application layer */ @@ -202,27 +324,52 @@ typedef struct xqc_h3_conn_callbacks_s { typedef struct xqc_h3_request_callbacks_s { /* request creation notify. it will be triggered after a request was created, and is required for server, optional for client */ - xqc_h3_request_notify_pt h3_request_create_notify; + xqc_h3_request_notify_pt h3_request_create_notify; /* request close notify. which will be triggered after a request was closed */ - xqc_h3_request_notify_pt h3_request_close_notify; + xqc_h3_request_notify_pt h3_request_close_notify; /* request read notify callback. which will be triggered after received http headers or body */ - xqc_h3_request_read_notify_pt h3_request_read_notify; + xqc_h3_request_read_notify_pt h3_request_read_notify; /* request write notify callback. when triggered, users can continue to send headers or body */ - xqc_h3_request_notify_pt h3_request_write_notify; + xqc_h3_request_notify_pt h3_request_write_notify; + + /* request closing notify callback, will be triggered when request is closing */ + xqc_h3_request_closing_notify_pt h3_request_closing_notify; } xqc_h3_request_callbacks_t; +typedef struct xqc_h3_ext_bytestream_callbacks_s { + + /* the return value is ignored by XQUIC stack */ + xqc_h3_ext_bytestream_notify_pt bs_create_notify; + + /* the return value is ignored by XQUIC stack */ + xqc_h3_ext_bytestream_notify_pt bs_close_notify; + + /* negative return values will cause the connection to be closed */ + xqc_h3_ext_bytestream_read_notify_pt bs_read_notify; + + /* negative return values will cause the connection to be closed */ + xqc_h3_ext_bytestream_notify_pt bs_write_notify; + +} xqc_h3_ext_bytestream_callbacks_t; + typedef struct xqc_h3_callbacks_s { /* http3 connection callbacks */ - xqc_h3_conn_callbacks_t h3c_cbs; + xqc_h3_conn_callbacks_t h3c_cbs; /* http3 request callbacks */ - xqc_h3_request_callbacks_t h3r_cbs; + xqc_h3_request_callbacks_t h3r_cbs; + + /* datagram callbacks */ + xqc_h3_ext_dgram_callbacks_t h3_ext_dgram_cbs; + + /* bytestream callbacks */ + xqc_h3_ext_bytestream_callbacks_t h3_ext_bs_cbs; } xqc_h3_callbacks_t; @@ -337,6 +484,16 @@ XQC_EXPORT_PUBLIC_API xqc_int_t xqc_h3_conn_get_errno(xqc_h3_conn_t *h3c); +/** + * @brief get ssl handler of http3 connection + * + * @param h3c handler of http3 connection + * @return ssl handler of http3 connection + */ +XQC_EXPORT_PUBLIC_API +void *xqc_h3_conn_get_ssl(xqc_h3_conn_t *h3c); + + /** * @brief set user_data for http3 connection, user_data could be the application layer context of * http3 connection @@ -348,6 +505,16 @@ xqc_int_t xqc_h3_conn_get_errno(xqc_h3_conn_t *h3c); XQC_EXPORT_PUBLIC_API void xqc_h3_conn_set_user_data(xqc_h3_conn_t *h3c, void *user_data); +/** + * @brief get user_data for http3 connection, user_data could be the application layer context of + * http3 connection + * + * @param h3c handler of http3 connection + * @return user_data + */ +XQC_EXPORT_PUBLIC_API +void *xqc_h3_conn_get_user_data(xqc_h3_conn_t *h3_conn); + /** * User can set h3 settings when h3_conn_create_notify callbacks @@ -422,11 +589,12 @@ xqc_int_t xqc_h3_conn_set_qpack_dtable_cap(xqc_h3_conn_t *h3c, size_t capacity); * @param engine handler created by xqc_engine_create * @param cid connection id of http3 connection * @param user_data For request + * @param settings stream settings * @return handler of http3 request */ XQC_EXPORT_PUBLIC_API xqc_h3_request_t *xqc_h3_request_create(xqc_engine_t *engine, const xqc_cid_t *cid, - void *user_data); + xqc_stream_settings_t *settings, void *user_data); /** * @brief get statistics of a http3 request user can get it before request destroyed @@ -464,6 +632,10 @@ void xqc_h3_request_set_user_data(xqc_h3_request_t *h3_request, void *user_data) XQC_EXPORT_PUBLIC_API xqc_int_t xqc_h3_request_close(xqc_h3_request_t *h3_request); +XQC_EXPORT_PUBLIC_API +xqc_int_t xqc_h3_request_update_settings(xqc_h3_request_t *h3_request, + xqc_stream_settings_t *settings); + /** * @brief send http headers to peer * @@ -554,6 +726,166 @@ XQC_EXPORT_PUBLIC_API xqc_int_t xqc_h3_request_set_priority(xqc_h3_request_t *h3r, xqc_h3_priority_t *prio); +/****************************/ +/* New APIs for extended H3 */ +/****************************/ + +/** + * @brief create a bytestream based on extended H3 + * @param engine handler created by xqc_engine_create + * @param cid connection id of http3 connection + * @param user_data For bytestream + * @return handler of bytestream + */ +XQC_EXPORT_PUBLIC_API +xqc_h3_ext_bytestream_t *xqc_h3_ext_bytestream_create(xqc_engine_t *engine, + const xqc_cid_t *cid, void *user_data); + +/** + * @brief close bytestream, send QUIC RESET_STREAM frame to peer. h3_ext_bytestream_close_notify will + * triggered when bytestream is finally destroyed + * + * @param xqc_h3_ext_bytestream_t handler of bytestream + * @return XQC_OK for success, others for error + */ +XQC_EXPORT_PUBLIC_API +xqc_int_t xqc_h3_ext_bytestream_close(xqc_h3_ext_bytestream_t *h3_ext_bs); + +/** + * @brief finish bytestream. if fin is not sent yet, and application has nothing to send anymore, call + * this function to send a QUIC STREAM frame with only fin + * + * @return > 0 for Bytes sent,-XQC_EAGAIN try next time, < 0 for error, 0 for bytestream finished + */ +XQC_EXPORT_PUBLIC_API +ssize_t xqc_h3_ext_bytestream_finish(xqc_h3_ext_bytestream_t *h3_ext_bs); + +/** + * @brief set user_data of a bytestream, which will be used as the parameter of the bytestream + * callback functions. server should set user_data when h3_ext_bytestream_create_notify triggers + * + * @param xqc_h3_ext_bytestream_t handler of the bytestream + * @param user_data user data of the bytestream callback functions + */ +XQC_EXPORT_PUBLIC_API +void xqc_h3_ext_bytestream_set_user_data(xqc_h3_ext_bytestream_t *h3_ext_bs, + void *user_data); + + +/** + * @brief get the user data associcated with the bytestream object + * + * @param xqc_h3_ext_bytestream_t handler of the bytestream + * @param user_data user data of the bytestream callback functions + * @return the pointer of user data + */ +XQC_EXPORT_PUBLIC_API +void *xqc_h3_ext_bytestream_get_user_data(xqc_h3_ext_bytestream_t *h3_ext_bs); + +/** + * @brief get statistics of a bytestream + * + * @param xqc_h3_ext_bytestream_t handler of the bytestream + * @return statistics information of the bytestream + */ +XQC_EXPORT_PUBLIC_API +xqc_h3_ext_bytestream_stats_t xqc_h3_ext_bytestream_get_stats( + xqc_h3_ext_bytestream_t *h3_ext_bs); + +/** + * @brief send data + * + * @param xqc_h3_ext_bytestream_t handler of the bytestream + * @param data content + * @param data_size data length + * @param fin request finish flag, 1 for finish. + * @param qos level (must be the values defined in xqc_data_qos_level_t) + * @return > 0 for bytes sent,-XQC_EAGAIN try next time, < 0 for error, 0 for bytestream finished + */ +XQC_EXPORT_PUBLIC_API +ssize_t xqc_h3_ext_bytestream_send(xqc_h3_ext_bytestream_t *h3_ext_bs, + unsigned char *data, size_t data_size, uint8_t fin, + xqc_data_qos_level_t qos_level); + +/** + * @brief Get QUIC stream ID by a bytestream + * + * @param xqc_h3_ext_bytestream_t handler of a bytestream + * @return QUIC stream id + */ +XQC_EXPORT_PUBLIC_API +xqc_stream_id_t xqc_h3_ext_bytestream_id(xqc_h3_ext_bytestream_t *h3_ext_bs); + +/** + * @brief get the h3 connection associated with a bytestream + * + * @param xqc_h3_ext_bytestream_t handler of a bytestream + * @return an h3 connection + */ +XQC_EXPORT_PUBLIC_API +xqc_h3_conn_t *xqc_h3_ext_bytestream_get_h3_conn( + xqc_h3_ext_bytestream_t *h3_ext_bs); + +/** + * @brief the API to get the max length of the data that can be sent + * via a single call of xqc_datagram_send + * + * @param conn the connection handle + * @return 0 = the peer does not support datagram, >0 = the max length + */ +XQC_EXPORT_PUBLIC_API +size_t xqc_h3_ext_datagram_get_mss(xqc_h3_conn_t *conn); + +/** + * Server should set datagram user_data when datagram callbacks + * @dgram_data: the user_data of all datagram callbacks + */ +XQC_EXPORT_PUBLIC_API +void xqc_h3_ext_datagram_set_user_data(xqc_h3_conn_t *conn, void *user_data); + +/** + * @return the user_data of all datagram callbacks + */ +XQC_EXPORT_PUBLIC_API +void *xqc_h3_ext_datagram_get_user_data(xqc_h3_conn_t *conn); + + +/** + * @brief the API to send a datagram over the h3 connection + * + * @param conn the connection handle + * @param data the data to be sent + * @param data_len the length of the data + * @param *dgram_id the pointer to return the id the datagram + * @param qos level (must be the values defined in xqc_data_qos_level_t) + * @return <0 = error (-XQC_EAGAIN, -XQC_CLOSING, -XQC_DGRAM_NOT_SUPPORTED, -XQC_DGRAM_TOO_LARGE, ...), + * 0 success + */ +XQC_EXPORT_PUBLIC_API +xqc_int_t xqc_h3_ext_datagram_send(xqc_h3_conn_t *conn, void *data, + size_t data_len, uint64_t *dgram_id, + xqc_data_qos_level_t qos_level); + +/** + * @brief the API to send a datagram over the h3 connection + * + * @param conn the connection handle + * @param iov multiple data buffers need to be sent + * @param *dgram_id the pointer to return the list of dgram_id + * @param iov_size the size of iov list + * @param *sent_cnt the number of successfully sent datagrams + * @param *sent_bytes the total bytes of successfully sent datagrams + * @param qos level (must be the values defined in xqc_data_qos_level_t) + * @return <0 = error (-XQC_EAGAIN, -XQC_CLOSING, -XQC_DGRAM_NOT_SUPPORTED, -XQC_DGRAM_TOO_LARGE, ...), + * 0 success + */ +XQC_EXPORT_PUBLIC_API +xqc_int_t xqc_h3_ext_datagram_send_multiple(xqc_h3_conn_t *conn, + struct iovec *iov, uint64_t *dgram_id_list, size_t iov_size, + size_t *sent_cnt, size_t *sent_bytes, + xqc_data_qos_level_t qos_level); + + #ifdef __cplusplus } #endif diff --git a/include/xquic/xquic.h b/include/xquic/xquic.h index b2d8f4e88..77c9a21b2 100644 --- a/include/xquic/xquic.h +++ b/include/xquic/xquic.h @@ -15,6 +15,8 @@ #include #include #endif + +#include "xqc_configure.h" #include "xquic_typedef.h" #ifdef __cplusplus @@ -68,6 +70,8 @@ typedef enum xqc_proto_version_s { #define XQC_INITIAL_PATH_ID 0 +#define XQC_DGRAM_RETX_ASKED_BY_APP 1 + /** * @brief get timestamp callback function. this might be useful on different platforms @@ -98,6 +102,14 @@ typedef void (*xqc_set_event_timer_pt)(xqc_usec_t wake_after, void *engine_user_ typedef ssize_t (*xqc_cid_generate_pt)(const xqc_cid_t *ori_cid, uint8_t *cid_buf, size_t cid_buflen, void *engine_user_data); +/** + * @brief engine secret log callback. will only be effective when build with XQC_PRINT_SECRET + * + * this callback will be invoked everytime when TLS layer generates a secret, and will be triggered + * multiple times during handshake. keylog could be used in wireshark to parse QUIC packets + */ +typedef void (*xqc_eng_keylog_pt)(const xqc_cid_t *scid, const char *line, void *engine_user_data); + /** * @brief tls secret log callback. will only be effective when build with XQC_PRINT_SECRET * @@ -231,6 +243,7 @@ typedef xqc_save_string_pt xqc_save_session_pt; */ typedef xqc_save_string_pt xqc_save_trans_param_pt; + /** * @brief handshake finished callback function * @@ -404,17 +417,6 @@ typedef enum { XQC_PATH_RECOVERY, } xqc_path_status_change_type_t; -/** - * @brief multi-path quality callback function - * - * @param conn connection handler - * @param scid source connection id of endpoint - * @param path_id id of path - * @param conn_user_data user_data of connection - */ -typedef xqc_bool_t (*xqc_path_status_controller_pt)(xqc_connection_t *conn, - const xqc_cid_t *scid, xqc_path_status_change_type_t type, uint64_t path_id, - void *conn_user_data); /** * @brief multi-path write socket callback function @@ -465,7 +467,85 @@ typedef ssize_t (*xqc_send_mmsg_ex_pt)(uint64_t path_id, * client, or the parameter of xqc_stream_set_user_data set by server * @return 0 for success, -1 for failure */ -typedef int (*xqc_stream_notify_pt)(xqc_stream_t *stream, void *strm_user_data); +typedef xqc_int_t (*xqc_stream_notify_pt)(xqc_stream_t *stream, + void *strm_user_data); + +/** + * @brief stream closing callback function, this will be triggered when some + * error on a stream happens. + * + * @param stream QUIC stream handler + * @param err_code error code + * @param strm_user_data stream level user_data, which was the parameter of xqc_stream_create set by + * client, or the parameter of xqc_stream_set_user_data set by server + * @return 0 for success, -1 for failure + */ +typedef void (*xqc_stream_closing_notify_pt)(xqc_stream_t *stream, + xqc_int_t err_code, void *strm_user_data); + +/** + * @brief the callback API to notify application that there is a datagram to be read + * + * @param conn the connection handle + * @param user_data the dgram_data set by xqc_datagram_set_user_data + * @param data the data delivered by this callback + * @param data_len the length of the delivered data + * @param dgram_recv_ts the unix timestamp when the datagram is received from socket + */ +typedef void (*xqc_datagram_read_notify_pt)(xqc_connection_t *conn, + void *user_data, const void *data, size_t data_len, uint64_t unix_ts); + +/** + * @brief the callback API to notify application that datagrams can be sent + * + * @param conn the connection handle + * @param user_data the dgram_data set by xqc_datagram_set_user_data + */ +typedef void (*xqc_datagram_write_notify_pt)(xqc_connection_t *conn, + void *user_data); + +/** + * @brief the callback API to notify application that a datagram is declared lost. + * However, the datagram could also be acknowledged later, as the underlying + * loss detection is not fully accurate. Applications should handle this type of + * spurious loss. The return value indicates how this lost datagram is + * handled by the QUIC stack. NOTE, if the QUIC stack replicates the datagram + * (e.g. reinjection or retransmission), this callback can be triggered + * multiple times for a dgram_id. + * + * @param conn the connection handle + * @param user_data the dgram_data set by xqc_datagram_set_user_data + * @param dgram_id the id of the lost datagram + * @return 0: the stack will not retransmit the packet; + * XQC_DGRAM_RETX_ASKED_BY_APP (1): the stack will retransmit the packet; + * others are ignored by the QUIC stack. + */ +typedef xqc_int_t (*xqc_datagram_lost_notify_pt)(xqc_connection_t *conn, + uint64_t dgram_id, void *user_data); + +/** + * @brief the callback API to notify application that a datagram is acked. Note, + * for every unique dgram_id, this callback will be only called once. + * + * @param conn the connection handle + * @param user_data the dgram_data set by xqc_datagram_set_user_data + * @param dgram_id the id of the acked datagram + */ +typedef void (*xqc_datagram_acked_notify_pt)(xqc_connection_t *conn, + uint64_t dgram_id, void *user_data); + + +/** + * @brief the callback to notify application the MSS of QUIC datagrams. Note, + * the MSS of QUIC datagrams will never shrink. If the MSS is zero, it + * means this connection does not support sending QUIC datagrams. + * + * @param conn the connection handle + * @param user_data the dgram_data set by xqc_datagram_set_user_data + * @param mss the MSS of QUIC datagrams + */ +typedef void (*xqc_datagram_mss_updated_notify_pt)(xqc_connection_t *conn, + size_t mss, void *user_data); /** @@ -572,11 +652,6 @@ typedef struct xqc_transport_callbacks_s { */ xqc_path_removed_notify_pt path_removed_notify; - /* - * Decision function for path status change. OPTIONAL for both client and server - */ - xqc_path_status_controller_pt path_status_controller; - /** * connection closing callback function. OPTIONAL for both client and server */ @@ -613,7 +688,7 @@ typedef struct xqc_conn_callbacks_s { * * return 0 for success, -1 for failure, e.g. malloc error, on which xquic will close connection */ - xqc_conn_notify_pt conn_create_notify; + xqc_conn_notify_pt conn_create_notify; /** * connection close notify. REQUIRED for both client and server @@ -621,17 +696,17 @@ typedef struct xqc_conn_callbacks_s { * this function will be invoked after QUIC connection is closed. user can free application * level context created in conn_create_notify callback function */ - xqc_conn_notify_pt conn_close_notify; + xqc_conn_notify_pt conn_close_notify; /** * handshake complete callback. OPTIONAL for client and server */ - xqc_handshake_finished_pt conn_handshake_finished; + xqc_handshake_finished_pt conn_handshake_finished; /** * active PING acked callback. OPTIONAL for both client and server */ - xqc_conn_ping_ack_notify_pt conn_ping_acked; + xqc_conn_ping_ack_notify_pt conn_ping_acked; } xqc_conn_callbacks_t; @@ -644,7 +719,7 @@ typedef struct xqc_stream_callbacks_s { * this will be triggered when QUIC stream data is ready for read. application layer could read * data when xqc_stream_recv interface. */ - xqc_stream_notify_pt stream_read_notify; + xqc_stream_notify_pt stream_read_notify; /** * stream write callback function. REQUIRED for both client and server @@ -652,7 +727,7 @@ typedef struct xqc_stream_callbacks_s { * when sending data with xqc_stream_send, xquic might be blocked or send part of the data. if * this callback function is triggered, applications can continue to send the rest data. */ - xqc_stream_notify_pt stream_write_notify; + xqc_stream_notify_pt stream_write_notify; /** * stream create callback function. REQUIRED for server, OPTIONAL for client. @@ -660,7 +735,7 @@ typedef struct xqc_stream_callbacks_s { * this will be triggered when QUIC stream is created. applications can create its own stream * context in this callback function. */ - xqc_stream_notify_pt stream_create_notify; + xqc_stream_notify_pt stream_create_notify; /** * stream close callback function. REQUIRED for both server and client. @@ -669,10 +744,53 @@ typedef struct xqc_stream_callbacks_s { * sending or receiving RESET_STREAM frame after 3 times of PTO, or when connection is closed. * Applications can free the context which was created in stream_create_notify here. */ - xqc_stream_notify_pt stream_close_notify; + xqc_stream_notify_pt stream_close_notify; + + /** + * @brief stream reset callback function. OPTIONAL for both server and client + * + * this function will be triggered when a RESET_STREAM frame is received. + */ + xqc_stream_closing_notify_pt stream_closing_notify; } xqc_stream_callbacks_t; +/* QUIC layer datagram callback functions */ +typedef struct xqc_datagram_callbacks_s { + /** + * datagram read callback function. REQUIRED for both client and server if they want to use datagram + * + * this will be triggered when a QUIC datagram is received. application layer could read + * data from the arguments of this callback. + */ + xqc_datagram_read_notify_pt datagram_read_notify; + + /** + * datagram write callback function. REQUIRED for both client and server if they want to use datagram + * + * when sending data with xqc_datagram_send or xqc_datagram_send_multiple, xquic might be blocked or send part of the data. if + * this callback function is triggered, applications can continue to send the rest data. + */ + xqc_datagram_write_notify_pt datagram_write_notify; + + /** + * datagram acked callback function. OPTIONAL for server and client. + * + * this will be triggered when a QUIC packet containing a DATAGRAM frame is acked. + */ + xqc_datagram_acked_notify_pt datagram_acked_notify; + + /** + * datagram lost callback function. OPTIONAL for server and client. + * + * this will be triggered when a QUIC packet containing a DATAGRAM frame is lost. + */ + xqc_datagram_lost_notify_pt datagram_lost_notify; + + xqc_datagram_mss_updated_notify_pt datagram_mss_updated_notify; + +} xqc_datagram_callbacks_t; + /** * @brief connection and stream callbacks for QUIC level, Application-Layer-Protocol shall implement @@ -686,15 +804,37 @@ typedef struct xqc_app_proto_callbacks_s { /* QUIC stream callback functions */ xqc_stream_callbacks_t stream_cbs; + /* QUIC datagram callback functions */ + xqc_datagram_callbacks_t dgram_cbs; + } xqc_app_proto_callbacks_t; +typedef enum { + XQC_DATA_QOS_HIGHEST = 1, + XQC_DATA_QOS_HIGH = 2, + XQC_DATA_QOS_MEDIUM = 3, + XQC_DATA_QOS_NORMAL = 4, + XQC_DATA_QOS_LOW = 5, + XQC_DATA_QOS_LOWEST = 6, + XQC_DATA_QOS_PROBING = 7, +} xqc_data_qos_level_t; typedef struct xqc_cc_params_s { uint32_t customize_on; uint32_t init_cwnd; + uint32_t min_cwnd; uint32_t expect_bw; uint32_t max_expect_bw; uint32_t cc_optimization_flags; + /* 0 < delta <= delta_max, default 0.05, ->0 = more throughput-oriented */ + double copa_delta_base; + /* 0 < delta_max <= 1.0, default 0.5 */ + double copa_delta_max; + /* + * 1.0 <= delta_ai_unit, default 1.0, greater values mean more aggressive + * when Copa competes with loss-based CCAs. + */ + double copa_delta_ai_unit; } xqc_cc_params_t; typedef struct xqc_scheduler_params_u { @@ -757,7 +897,7 @@ typedef struct xqc_congestion_control_callback_s { xqc_bbr_info_interface_t *xqc_cong_ctl_info_cb; } xqc_cong_ctrl_callback_t; -#ifndef XQC_DISABLE_RENO +#ifdef XQC_ENABLE_RENO XQC_EXPORT_PUBLIC_API extern const xqc_cong_ctrl_callback_t xqc_reno_cb; #endif #ifdef XQC_ENABLE_BBR2 @@ -765,6 +905,12 @@ XQC_EXPORT_PUBLIC_API extern const xqc_cong_ctrl_callback_t xqc_bbr2_cb; #endif XQC_EXPORT_PUBLIC_API extern const xqc_cong_ctrl_callback_t xqc_bbr_cb; XQC_EXPORT_PUBLIC_API extern const xqc_cong_ctrl_callback_t xqc_cubic_cb; +#ifdef XQC_ENABLE_UNLIMITED +XQC_EXPORT_PUBLIC_API extern const xqc_cong_ctrl_callback_t xqc_unlimited_cc_cb; +#endif +#ifdef XQC_ENABLE_COPA +XQC_EXPORT_PUBLIC_API extern const xqc_cong_ctrl_callback_t xqc_copa_cb; +#endif typedef enum xqc_scheduler_path_event_e { XQC_SCHED_EVENT_PATH_NOT_FULL = 0, @@ -783,7 +929,7 @@ typedef struct xqc_scheduler_callback_s { xqc_path_ctx_t * (*xqc_scheduler_get_path)(void *scheduler, xqc_connection_t *conn, xqc_packet_out_t *packet_out, - int check_cwnd, int reinject); + int check_cwnd, int reinject, xqc_bool_t *cc_blocked); void (*xqc_scheduler_handle_path_event)(void *scheduler, xqc_path_ctx_t *path, xqc_scheduler_path_event_t event, void *event_arg); @@ -795,25 +941,34 @@ typedef struct xqc_scheduler_callback_s { XQC_EXPORT_PUBLIC_API extern const xqc_scheduler_callback_t xqc_minrtt_scheduler_cb; XQC_EXPORT_PUBLIC_API extern const xqc_scheduler_callback_t xqc_backup_scheduler_cb; +XQC_EXPORT_PUBLIC_API extern const xqc_scheduler_callback_t xqc_rap_scheduler_cb; +#ifdef XQC_ENABLE_MP_INTEROP +XQC_EXPORT_PUBLIC_API extern const xqc_scheduler_callback_t xqc_interop_scheduler_cb; +#endif +typedef enum { + XQC_REINJ_UNACK_AFTER_SCHED = 1 << 0, + XQC_REINJ_UNACK_BEFORE_SCHED = 1 << 1, + XQC_REINJ_UNACK_AFTER_SEND = 1 << 2, +} xqc_reinjection_mode_t; typedef struct xqc_reinj_ctl_callback_s { size_t (*xqc_reinj_ctl_size)(void); - void (*xqc_reinj_ctl_init)(void *reinj_ctl, const xqc_conn_settings_t *settings, xqc_log_t *log); - - xqc_bool_t (*xqc_reinj_ctl_lost_queue)(void *reinj_ctl, void *qoe_ctx, xqc_connection_t *conn); + void (*xqc_reinj_ctl_init)(void *reinj_ctl, xqc_connection_t *conn); - xqc_bool_t (*xqc_reinj_ctl_unack_queue)(void *reinj_ctl, void *qoe_ctx, xqc_connection_t *conn); + void (*xqc_reinj_ctl_update)(void *reinj_ctl, void *qoe_info); - xqc_bool_t (*xqc_reinj_ctl_send_queue)(void *reinj_ctl, void *qoe_ctx, xqc_connection_t *conn); + void (*xqc_reinj_ctl_reset)(void *reinj_ctl, void *qoe_info); - xqc_bool_t (*xqc_reinj_ctl_single_packet)(void *reinj_ctl, void *qoe_ctx, xqc_connection_t *conn, xqc_packet_out_t *po); + xqc_bool_t (*xqc_reinj_ctl_can_reinject)(void *reinj_ctl, xqc_packet_out_t *po, xqc_reinjection_mode_t mode); } xqc_reinj_ctl_callback_t; -XQC_EXPORT_PUBLIC_API extern const xqc_reinj_ctl_callback_t xqc_xlink_reinj_ctl_cb; +XQC_EXPORT_PUBLIC_API extern const xqc_reinj_ctl_callback_t xqc_default_reinj_ctl_cb; +XQC_EXPORT_PUBLIC_API extern const xqc_reinj_ctl_callback_t xqc_deadline_reinj_ctl_cb; +XQC_EXPORT_PUBLIC_API extern const xqc_reinj_ctl_callback_t xqc_dgram_reinj_ctl_cb; /** @@ -878,6 +1033,12 @@ typedef struct xqc_config_s { * engine. if write_mmsg is NULL and sendmmsg_on is non-zero, xqc_engine_create will fail */ int sendmmsg_on; + + /** + * @brief enable h3 ext (default: 0) + * + */ + uint8_t enable_h3_ext; } xqc_config_t; @@ -895,7 +1056,7 @@ typedef struct xqc_engine_callback_s { xqc_cid_generate_pt cid_generate_cb; /* tls secret callback, OPTIONAL */ - xqc_keylog_pt keylog_cb; + xqc_eng_keylog_pt keylog_cb; /* get realtime timestamp callback function. if not set, xquic will get timestamp with inner function xqc_now, which relies on gettimeofday */ @@ -965,13 +1126,24 @@ typedef struct xqc_linger_s { xqc_usec_t linger_timeout; /* 3*PTO if linger_timeout is 0 */ } xqc_linger_t; +typedef enum { + XQC_ERR_MULTIPATH_VERSION = 0x00, + XQC_MULTIPATH_04 = 0x04, + XQC_MULTIPATH_05 = 0x05, +} xqc_multipath_version_t; + + typedef struct xqc_conn_settings_s { int pacing_on; /* default: 0 */ int ping_on; /* client sends PING to keepalive, default:0 */ xqc_cong_ctrl_callback_t cong_ctrl_callback; /* default: xqc_cubic_cb */ xqc_cc_params_t cc_params; uint32_t so_sndbuf; /* socket option SO_SNDBUF, 0 for unlimited */ - uint64_t sndq_packets_used_max; /* default: XQC_SNDQ_PACKETS_USED_MAX */ + uint64_t sndq_packets_used_max; /* + * default: XQC_SNDQ_PACKETS_USED_MAX. + * It should be set to buffer 2xBDP packets at least for performance consideration. + * The default value is 16000 pkts. + */ xqc_linger_t linger; xqc_proto_version_t proto_version; /* QUIC protocol version */ xqc_msec_t init_idle_time_out; /* initial idle timeout interval, effective before handshake completion */ @@ -981,22 +1153,34 @@ typedef struct xqc_conn_settings_s { uint64_t keyupdate_pkt_threshold; /* packet limit of a single 1-rtt key, 0 for unlimited */ size_t max_pkt_out_size; + /* + * datgram option + * 0: no support for datagram mode (default) + * >0: the max size of datagrams that the local end is willing to receive + * 65535: the local end is willing to receive a datagram with any length as + * long as it fits in a QUIC packet + */ + uint16_t max_datagram_frame_size; + /* * multipath option: - * https://datatracker.ietf.org/doc/html/draft-ietf-quic-multipath-02#section-3 + * https://datatracker.ietf.org/doc/html/draft-ietf-quic-multipath-05#section-3 * 0: don't support multipath * 1: supports multipath (unified solution) - multiple PN spaces */ uint64_t enable_multipath; + xqc_multipath_version_t multipath_version; + + /* * reinjection option: * 0: default, no reinjection * bit0 = 1: - * try to reinject unacked packets if paths still have cwnd. - * the current reinjection mode is very aggressive, please use with caution. + * reinject unacked packets after scheduling packets to paths. * bit1 = 1: - * try to reinject unacked packets if they have missed the deadline. - * The deadline is controlled by the following three parameters. + * reinject unacked packets before scheduling packets to paths. + * bit2 = 1 + * reinject unacked packets after sending packets. */ int mp_enable_reinjection; /* @@ -1009,18 +1193,73 @@ typedef struct xqc_conn_settings_s { double reinj_flexible_deadline_srtt_factor; uint64_t reinj_hard_deadline; uint64_t reinj_deadline_lower_bound; + + /* + * By default, XQUIC returns ACK_MPs on the path where the data + * is received unless the path is not avaliable anymore. + * + * Setting mp_ack_on_any_path to 1 can enable XQUIC to return ACK_MPs on any + * paths according to the scheduler. + */ + uint8_t mp_ack_on_any_path; + + /* + * When sending a ping packet for connection keep-alive, we replicate the + * the packet on all acitve paths to keep all paths alive (disable:0, enable:1). + * The default value is 0. + */ + uint8_t mp_ping_on; /* scheduler callback, default: xqc_minrtt_scheduler_cb */ xqc_scheduler_callback_t scheduler_callback; xqc_scheduler_params_t scheduler_params; - /* reinj_ctl callback, default: xqc_xlink_reinj_ctl_cb */ + /* reinj_ctl callback, default: xqc_default_reinj_ctl_cb */ xqc_reinj_ctl_callback_t reinj_ctl_callback; - /* mark path unreachable if ctl_pto_count > path_unreachable_pto_count */ - uint64_t path_unreachable_pto_count; - + /* ms */ xqc_msec_t standby_path_probe_timeout; + + /* params for performance tuning */ + /* max ack delay: ms */ + uint32_t max_ack_delay; + /* generate an ACK if received ack-eliciting pkts >= ack_frequency */ + uint32_t ack_frequency; + uint64_t loss_detection_pkt_thresh; + double pto_backoff_factor; + + /* datagram redundancy: 0 disable, 1 enable, 2 only enable multipath redundancy */ + uint8_t datagram_redundancy; + uint8_t datagram_force_retrans_on; + uint64_t datagram_redundant_probe; + + /* enable PMTUD */ + uint8_t enable_pmtud; + /* probing interval (us), default: 500000 */ + uint64_t pmtud_probing_interval; + + /* enable marking reinjected packets with reserved bits */ + uint8_t marking_reinjection; + + /* + * The limitation on conn recv rate (only applied to stream data) in bytes per second. + * NOTE: the minimal rate limitation is (63000/RTT) Bps. For instance, if RTT is 60ms, + * the minimal valid rate limitation is about 1MBps. Any recv_rate_bytes_per_sec less + * than the minimal valid rate limitation will not be guaranteed. + * default: 0 (no limitation). + */ + uint64_t recv_rate_bytes_per_sec; + + /* + * The switch to enable stream-level recv rate throttling. Default: off (0) + */ + uint8_t enable_stream_rate_limit; + + /* + * initial recv window. Default: 0 (use the internal default value) + */ + uint32_t init_recv_window; + } xqc_conn_settings_t; @@ -1047,6 +1286,9 @@ typedef struct xqc_path_metrics_s { uint64_t path_recv_reinject_bytes; uint64_t path_recv_effective_bytes; uint64_t path_recv_effective_reinject_bytes; + + uint64_t path_srtt; + uint8_t path_app_status; } xqc_path_metrics_t; @@ -1055,7 +1297,10 @@ typedef struct xqc_conn_stats_s { uint32_t lost_count; uint32_t tlp_count; uint32_t spurious_loss_count; - xqc_usec_t srtt; + uint32_t lost_dgram_count; /*how many datagram frames (pkts) are lost*/ + xqc_usec_t srtt; /* smoothed SRTT at present: initial value = 250000 */ + xqc_usec_t min_rtt; /* minimum RTT until now: initial value = 0xFFFFFFFF */ + uint64_t inflight_bytes; /* initial value = 0 */ xqc_0rtt_flag_t early_data_flag; uint32_t recv_count; int spurious_loss_detect_on; @@ -1080,15 +1325,9 @@ typedef struct xqc_conn_stats_s { xqc_path_metrics_t paths_info[XQC_MAX_PATHS_COUNT]; char conn_info[XQC_CONN_INFO_LEN]; -} xqc_conn_stats_t; -typedef struct xqc_path_stats_s { - uint8_t get_stats_success; - - xqc_usec_t last_tra_path_status_changed_time; - uint32_t send_count_since_last_tra_path_status_changed; - uint32_t pto_count_since_last_tra_path_status_changed; -} xqc_path_stats_t; + char alpn[XQC_MAX_ALPN_BUF_LEN]; +} xqc_conn_stats_t; /************************************************************* @@ -1155,7 +1394,11 @@ xqc_int_t xqc_engine_unregister_alpn(xqc_engine_t *engine, const char *alpn, siz * Pass received UDP packet payload into xquic engine. * @param recv_time UDP packet received time in microsecond * @param user_data connection user_data, server is NULL + * @param path_id XQC_UNKNOWN_PATH_ID = unknown path (only use cid to identify the path) */ + +#ifdef XQC_NO_PID_PACKET_PROCESS + XQC_EXPORT_PUBLIC_API xqc_int_t xqc_engine_packet_process(xqc_engine_t *engine, const unsigned char *packet_in_buf, size_t packet_in_size, @@ -1163,6 +1406,17 @@ xqc_int_t xqc_engine_packet_process(xqc_engine_t *engine, const struct sockaddr *peer_addr, socklen_t peer_addrlen, xqc_usec_t recv_time, void *user_data); +#else + +XQC_EXPORT_PUBLIC_API +xqc_int_t xqc_engine_packet_process(xqc_engine_t *engine, + const unsigned char *packet_in_buf, size_t packet_in_size, + const struct sockaddr *local_addr, socklen_t local_addrlen, + const struct sockaddr *peer_addr, socklen_t peer_addrlen, + uint64_t path_id, xqc_usec_t recv_time, void *user_data); + +#endif + /** * @brief Process all connections, application implements MUST call this function in timer callback @@ -1204,6 +1458,15 @@ XQC_EXPORT_PUBLIC_API void xqc_engine_set_log_level(xqc_engine_t *engine, xqc_log_level_t log_level); +/** + * @brief enable/disable the log module of xquic + * + * @param enable XQC_TRUE for enable, XQC_FALSE for disable + */ +XQC_EXPORT_PUBLIC_API +void xqc_log_enable(xqc_bool_t enable); + + /** * user should call after a number of packet processed in xqc_engine_packet_process * call after recv a batch packets, may destroy connection when error @@ -1264,6 +1527,21 @@ XQC_EXPORT_PUBLIC_API xqc_int_t xqc_conn_get_errno(xqc_connection_t *conn); +/** + * Get ssl handler of specified connection + */ +XQC_EXPORT_PUBLIC_API +void *xqc_conn_get_ssl(xqc_connection_t *conn); + + +/** + * @brief get latest rtt sample of the initial path + * + */ +XQC_EXPORT_PUBLIC_API +xqc_usec_t xqc_conn_get_lastest_rtt(xqc_engine_t *engine, const xqc_cid_t *cid); + + /** * Server should set user_data when conn_create_notify callbacks */ @@ -1324,12 +1602,42 @@ XQC_EXPORT_PUBLIC_API void xqc_conn_unset_pkt_filter_callback(xqc_connection_t *conn); +/** + * @brief get public local transport settings. + */ +XQC_EXPORT_PUBLIC_API +xqc_conn_public_local_trans_settings_t +xqc_conn_get_public_local_trans_settings(xqc_connection_t *conn); + +/** + * @brief set public local transport settings + */ +XQC_EXPORT_PUBLIC_API +void xqc_conn_set_public_local_trans_settings(xqc_connection_t *conn, + xqc_conn_public_local_trans_settings_t *settings); + +/** + * @brief get public remote transport settings. + */ +XQC_EXPORT_PUBLIC_API +xqc_conn_public_remote_trans_settings_t +xqc_conn_get_public_remote_trans_settings(xqc_connection_t *conn); + +/** + * @brief set public remote transport settings + */ +XQC_EXPORT_PUBLIC_API +void xqc_conn_set_public_remote_trans_settings(xqc_connection_t *conn, + xqc_conn_public_remote_trans_settings_t *settings); + + /** * Create new stream in quic connection. * @param user_data user_data for this stream */ XQC_EXPORT_PUBLIC_API -xqc_stream_t *xqc_stream_create(xqc_engine_t *engine, const xqc_cid_t *cid, void *user_data); +xqc_stream_t *xqc_stream_create(xqc_engine_t *engine, + const xqc_cid_t *cid, xqc_stream_settings_t *settings, void *user_data); XQC_EXPORT_PUBLIC_API xqc_stream_t *xqc_stream_create_with_direction(xqc_connection_t *conn, @@ -1344,6 +1652,11 @@ xqc_stream_direction_t xqc_stream_get_direction(xqc_stream_t *strm); XQC_EXPORT_PUBLIC_API void xqc_stream_set_user_data(xqc_stream_t *stream, void *user_data); + +XQC_EXPORT_PUBLIC_API +xqc_int_t xqc_stream_update_settings(xqc_stream_t *stream, + xqc_stream_settings_t *settings); + /** * Get connection's user_data by stream */ @@ -1387,6 +1700,68 @@ XQC_EXPORT_PUBLIC_API ssize_t xqc_stream_send(xqc_stream_t *stream, unsigned char *send_data, size_t send_data_size, uint8_t fin); +/** + * @brief the API to get the max length of the data that can be sent + * via a single call of xqc_datagram_send; NOTE, if the DCID length could + * be changed during the lifetime of the connection, applications is + * suggested to call xqc_datagram_get_mss every time before + * send datagram data or when getting -XQC_EDGRAM_TOO_LARGE error + * from sending datagram data. In MPQUIC cases, the DCID of all paths + * MUST be the same. Otherwise, there might be unexpected errors. + * + * @param conn the connection handle + * @return 0 = the peer does not support datagram, >0 = the max length + */ +XQC_EXPORT_PUBLIC_API +size_t xqc_datagram_get_mss(xqc_connection_t *conn); + +/** + * Server should set datagram user_data when datagram callbacks + * @dgram_data: the user_data of all datagram callbacks + */ +XQC_EXPORT_PUBLIC_API +void xqc_datagram_set_user_data(xqc_connection_t *conn, void *dgram_data); + +/** + * @dgram_data: the user_data of all datagram callbacks + */ +XQC_EXPORT_PUBLIC_API +void *xqc_datagram_get_user_data(xqc_connection_t *conn); + +/** + * @brief the API to send a datagram over the QUIC connection + * + * @param conn the connection handle + * @param data the data to be sent + * @param data_len the length of the data + * @param *dgram_id the pointer to return the id the datagram + * @param qos level (must be the values defined in xqc_data_qos_level_t) + * @return <0 = error (-XQC_EAGAIN, -XQC_CLOSING, -XQC_EDGRAM_NOT_SUPPORTED, -XQC_EDGRAM_TOO_LARGE, ...), + * 0 success + */ +XQC_EXPORT_PUBLIC_API +xqc_int_t xqc_datagram_send(xqc_connection_t *conn, void *data, + size_t data_len, uint64_t *dgram_id, xqc_data_qos_level_t qos_level); + +/** + * @brief the API to send a datagram over the QUIC connection + * + * @param conn the connection handle + * @param iov multiple data buffers need to be sent + * @param *dgram_id the pointer to return the list of dgram_id + * @param iov_size the size of iov list + * @param *sent_cnt the number of successfully sent datagrams + * @param *sent_bytes the total bytes of successfully sent datagrams + * @param qos level (must be the values defined in xqc_data_qos_level_t) + * @return <0 = error (-XQC_EAGAIN, -XQC_CLOSING, -XQC_EDGRAM_NOT_SUPPORTED, -XQC_EDGRAM_TOO_LARGE, ...), + * 0 success + */ +XQC_EXPORT_PUBLIC_API +xqc_int_t xqc_datagram_send_multiple(xqc_connection_t *conn, + struct iovec *iov, uint64_t *dgram_id_list, size_t iov_size, + size_t *sent_cnt, size_t *sent_bytes, xqc_data_qos_level_t qos_level); + + /** * Get dcid and scid before process packet */ @@ -1425,6 +1800,12 @@ uint8_t xqc_engine_config_get_cid_len(xqc_engine_t *engine); XQC_EXPORT_PUBLIC_API xqc_int_t xqc_conn_continue_send(xqc_engine_t *engine, const xqc_cid_t *cid); +/** + * User should call xqc_conn_continue_send when write event ready + */ +XQC_EXPORT_PUBLIC_API +void xqc_conn_continue_send_by_conn(xqc_connection_t *conn); + /** * User can get xqc_conn_stats_t by cid */ @@ -1435,11 +1816,13 @@ xqc_conn_stats_t xqc_conn_get_stats(xqc_engine_t *engine, const xqc_cid_t *cid); * create new path for client * @param cid scid for connection * @param new_path_id if new path is created successfully, return new_path_id in this param + * @param path_status the initial status of the new path (1 = STANDBY, other values = AVAILABLE) * @return XQC_OK (0) when success, <0 for error */ XQC_EXPORT_PUBLIC_API xqc_int_t xqc_conn_create_path(xqc_engine_t *engine, - const xqc_cid_t *cid, uint64_t *new_path_id); + const xqc_cid_t *cid, uint64_t *new_path_id, + int path_status); /** @@ -1473,6 +1856,15 @@ xqc_int_t xqc_conn_mark_path_standby(xqc_engine_t *engine, const xqc_cid_t *cid, XQC_EXPORT_PUBLIC_API xqc_int_t xqc_conn_mark_path_available(xqc_engine_t *engine, const xqc_cid_t *cid, uint64_t path_id); +/** + * Mark a path as "frozen", i.e., both peers should not send any traffic on this path. + * @param cid scid for connection + * @param path_id path identifier for the path + * @return XQC_OK (0) when success, <0 for error + */ +XQC_EXPORT_PUBLIC_API +xqc_int_t xqc_conn_mark_path_frozen(xqc_engine_t *engine, const xqc_cid_t *cid, uint64_t path_id); + XQC_EXPORT_PUBLIC_API xqc_conn_type_t xqc_conn_get_type(xqc_connection_t *conn); @@ -1495,12 +1887,7 @@ XQC_EXPORT_PUBLIC_API xqc_int_t xqc_path_get_local_addr(xqc_connection_t *conn, uint64_t path_id, struct sockaddr *addr, socklen_t addr_cap, socklen_t *local_addr_len); -/** - * User can get xqc_path_stats_t by path_id - */ -XQC_EXPORT_PUBLIC_API -xqc_path_stats_t xqc_path_get_stats(xqc_engine_t *engine, const xqc_cid_t *cid, - uint64_t path_id); + /** * @brief load balance cid encryption. @@ -1520,6 +1907,24 @@ xqc_path_stats_t xqc_path_get_stats(xqc_engine_t *engine, const xqc_cid_t *cid, XQC_EXPORT_PUBLIC_API xqc_int_t xqc_lb_cid_encryption(uint8_t *cid_buf, size_t enc_len, uint8_t *out_buf, size_t out_buf_len, uint8_t *lb_cid_key, size_t lb_cid_key_len, xqc_engine_t *engine); +/** + * @brief client calls this API to check if it should delete 0rtt ticket according to + * the errorcode of xqc_conn in conn_close_notify + * @return XQC_TRUE = yes; + */ +XQC_EXPORT_PUBLIC_API +xqc_bool_t xqc_conn_should_clear_0rtt_ticket(xqc_int_t conn_err); + +/** + * @brief Users call this function to get a template of conn settings, which serves + * as the starting point for users who want to refine conn settings according + * to their needs + * @param settings_type there are different types of templates in XQUIC + * @return conn settings + */ +XQC_EXPORT_PUBLIC_API +xqc_conn_settings_t xqc_conn_get_conn_settings_template(xqc_conn_settings_type_t settings_type); + #ifdef __cplusplus } #endif diff --git a/include/xquic/xquic_typedef.h b/include/xquic/xquic_typedef.h index be5e3ec39..d429fb1fa 100644 --- a/include/xquic/xquic_typedef.h +++ b/include/xquic/xquic_typedef.h @@ -50,6 +50,8 @@ typedef struct xqc_memory_pool_s xqc_memory_pool_t; typedef struct xqc_bbr_info_interface_s xqc_bbr_info_interface_t; typedef struct xqc_path_ctx_s xqc_path_ctx_t; typedef struct xqc_timer_manager_s xqc_timer_manager_t; +typedef struct xqc_h3_ext_bytestream_s xqc_h3_ext_bytestream_t; +typedef struct xqc_ping_record_s xqc_ping_record_t; typedef uint64_t xqc_msec_t; /* store millisecond values */ typedef uint64_t xqc_usec_t; /* store microsecond values */ @@ -73,10 +75,14 @@ typedef uint8_t xqc_bool_t; /* restrictions of key length in lb cid encryption */ #define XQC_LB_CID_KEY_LEN 16 +/* length of stateless reset token */ +#define XQC_STATELESS_RESET_TOKENLEN 16 + typedef struct xqc_cid_s { uint8_t cid_len; uint8_t cid_buf[XQC_MAX_CID_LEN]; uint64_t cid_seq_num; + uint8_t sr_token[XQC_STATELESS_RESET_TOKENLEN]; } xqc_cid_t; typedef enum xqc_log_level_s { @@ -138,5 +144,32 @@ typedef struct xqc_http_priority_s { uint8_t reinject; } xqc_h3_priority_t; +/* ALPN definition */ +#define XQC_DEFINED_ALPN_H3 "h3" +#define XQC_DEFINED_ALPN_H3_29 "h3-29" +#define XQC_DEFINED_ALPN_H3_EXT "h3-ext" + +/* max alpn buffer length */ +#define XQC_MAX_ALPN_BUF_LEN 256 + +#define XQC_UNKNOWN_PATH_ID ((uint64_t)-1) + +typedef enum xqc_conn_settings_type_e { + XQC_CONN_SETTINGS_DEFAULT, + XQC_CONN_SETTINGS_LOW_DELAY, +} xqc_conn_settings_type_t; + +typedef struct xqc_conn_public_local_trans_settings_s { + uint16_t max_datagram_frame_size; + uint8_t datagram_redundancy; +} xqc_conn_public_local_trans_settings_t; + +typedef struct xqc_conn_public_remote_trans_settings_s { + uint16_t max_datagram_frame_size; +} xqc_conn_public_remote_trans_settings_t; + +typedef struct xqc_stream_settings_s { + uint64_t recv_rate_bytes_per_sec; +} xqc_stream_settings_t; #endif /*_XQUIC_TYPEDEF_H_INCLUDED_*/ diff --git a/interop/run_endpoint.sh b/interop/run_endpoint.sh index 88d6f2b4f..190a1218f 100644 --- a/interop/run_endpoint.sh +++ b/interop/run_endpoint.sh @@ -4,7 +4,7 @@ /setup.sh case $TESTCASE in - versionnegotiation|handshake|longrtt|transfer|zerortt|multiconnect|chacha20|resumption|http3|retry|keyupdate) + versionnegotiation|handshake|longrtt|transfer|zerortt|multiconnect|chacha20|resumption|http3|retry|keyupdate|mphandshake|mptransfer|mppathabandon|mppathstatus) : ;; *) @@ -62,7 +62,19 @@ if [ "$ROLE" == "client" ]; then elif [ "$TESTCASE" == "keyupdate" ]; then echo "./demo_client $ARGS -U $REQUESTS -u 30" ./demo_client $ARGS -U "$REQUESTS" -u 30 + + elif [ "$TESTCASE" == "mphandshake" ] || [ "$TESTCASE" == "mptransfer" ]; then + echo "./demo_client $ARGS -U $REQUESTS -M -i eth0 -i eth0 -w 3000" + ./demo_client $ARGS -U "$REQUESTS" -M -i eth0 -i eth0 -w 3000 + elif [ "$TESTCASE" == "mppathabandon" ]; then + echo "./demo_client $ARGS -U $REQUESTS -M -i eth0 -i eth0 -w 3000 -Z 1000" + ./demo_client $ARGS -U "$REQUESTS" -M -i eth0 -i eth0 -w 3000 -Z 1000 + + elif [ "$TESTCASE" == "mppathstatus" ]; then + echo "./demo_client $ARGS -U $REQUESTS -M -i eth0 -i eth0 -w 3000 -b" + ./demo_client $ARGS -U "$REQUESTS" -M -i eth0 -i eth0 -w 3000 -b + # common testcase else echo -e "./demo_client $ARGS -U \"$REQUESTS\"\n" diff --git a/scripts/case_test.sh b/scripts/case_test.sh index 6e356819f..083e83219 100755 --- a/scripts/case_test.sh +++ b/scripts/case_test.sh @@ -5,6 +5,9 @@ #macOS #export EVENT_NOKQUEUE=1 +LOCAL_TEST=0 +#LOCAL_TEST=1 + cd ../build # start test_server @@ -35,6 +38,99 @@ function case_print_result() { } +clear_log +echo -e "log switch off ...\c" +./test_client -s 1024000 -l d -t 1 -E -x 44 >> stdlog +log_size=`wc -c clog | awk -F ' ' '{print $1}'` +if [ $log_size -eq 0 ]; then + echo ">>>>>>>> pass:1" + case_print_result "log_switch_off" "pass" +else + echo ">>>>>>>> pass:0" + case_print_result "log_switch_off" "fail" +fi + +rm -f test_session tp_localhost xqc_token + +killall test_server 2> /dev/null +./test_server -l d -e -x 17 > /dev/null & + +clear_log +echo -e "server-inited stream ...\c" +./test_client -l d -E -t 3 >> stdlog +client_refuse=`grep "ignore server initiated bidi-streams at client" clog` +client_discard=`grep "data discarded" clog` +client_check=`grep "xqc_h3_stream_close_notify" clog | grep "|stream_id:1|"` +client_std_res=`grep ">>>>>>>> pass" stdlog` +clog_res=`grep "xqc_destroy_stream" clog | grep "close_msg:finished" | grep "stream_id:1"` +if [ -n "$client_refuse" ] && [ -n "$client_discard" ] && [ -n "$client_std_res" ] && [ -n "$clog_res" ] && [ -z "$client_check" ]; then + echo ">>>>>>>> pass:1" + case_print_result "server_inited_stream" "pass" +else + echo ">>>>>>>> pass:0" + case_print_result "server_inited_stream" "fail" +fi + + + +killall test_server 2> /dev/null +./test_server -l d -e -x 99 > /dev/null & +sleep 1 + +rm -f test_session tp_localhost xqc_token + +clear_log +echo -e "stream send pure fin ...\c" +./test_client -s 1024 -l d -t 1 -E -x 99 -T 1 >> clog +errlog=`grep_err_log` +clog_res=`cat clog | grep "|send_state:3|recv_state:3|stream_id:0|stream_type:0|send_bytes:0|read_bytes:0|recv_bytes:0|stream_len:0|"` +slog_res=`cat slog | grep "|send_state:3|recv_state:3|stream_id:0|stream_type:0|send_bytes:0|read_bytes:0|recv_bytes:0|stream_len:0|"` +if [ -z "$errlog" ] && [ -n "$clog_res" ] && [ -n "$slog_res" ] ; then + echo ">>>>>>>> pass:1" + case_print_result "stream_send_pure_fin" "pass" +else + echo ">>>>>>>> pass:0" + case_print_result "stream_send_pure_fin" "fail" +fi + +rm -f test_session + +clear_log +echo -e "h3 stream send pure fin ...\c" +./test_client -s 1024 -l d -t 1 -E -x 99 >> clog +errlog=`grep_err_log | grep -v "send data after fin sent"` +clog_res=`cat clog | grep "|send_state:3|recv_state:3|stream_id:0|stream_type:0|send_bytes:0|read_bytes:0|recv_bytes:0|stream_len:0|"` +slog_res=`cat slog | grep "|send_state:3|recv_state:3|stream_id:0|stream_type:0|send_bytes:0|read_bytes:0|recv_bytes:0|stream_len:0|"` +if [ -z "$errlog" ] && [ -n "$clog_res" ] && [ -n "$slog_res" ] ; then + echo ">>>>>>>> pass:1" + case_print_result "h3_stream_send_pure_fin" "pass" +else + echo ">>>>>>>> pass:0" + case_print_result "h3_stream_send_pure_fin" "fail" +fi + +rm -f test_session + +clear_log +echo -e "h3_ext_bytestream send pure fin ...\c" +./test_client -s 1024 -l d -t 1 -E -x 310 -T 2 >> clog +errlog=`grep_err_log` +clog_res=`cat clog | grep "|send_state:3|recv_state:3|stream_id:0|stream_type:0|send_bytes:5|read_bytes:2|recv_bytes:2|stream_len:2|"` +slog_res=`cat slog | grep "|send_state:3|recv_state:3|stream_id:0|stream_type:0|send_bytes:2|read_bytes:5|recv_bytes:5|stream_len:5|"` +if [ -z "$errlog" ] && [ -n "$clog_res" ] && [ -n "$slog_res" ] ; then + echo ">>>>>>>> pass:1" + case_print_result "h3_ext_bytestream_send_pure_fin" "pass" +else + echo ">>>>>>>> pass:0" + case_print_result "h3_ext_bytestream_send_pure_fin" "fail" +fi + +rm -rf test_session + +killall test_server 2> /dev/null +./test_server -l d -e > /dev/null & +sleep 1 + clear_log echo -e "stream read notify fail ...\c" ./test_client -s 1024000 -l d -t 1 -E -x 12 >> clog @@ -182,6 +278,43 @@ else echo "$errlog" fi +clear_log +echo -e "send data after fin ...\c" +result=`./test_client -s 5120 -l d -t 1 -E -x 50 |grep ">>>>>>>> pass"` +errlog=`grep_err_log | grep -v "send data after fin sent"` +echo "$result" +if [ -z "$errlog" ] && [ "$result" == ">>>>>>>> pass:1" ]; then + case_print_result "send_data_after_fin" "pass" +else + case_print_result "send_data_after_fin" "fail" + echo "$errlog" +fi + +clear_log +echo -e "send header after fin ...\c" +result=`./test_client -s 5120 -l d -t 1 -E -x 51 |grep ">>>>>>>> pass"` +errlog=`grep_err_log | grep -v "send data after fin sent"` +echo "$result" +if [ -z "$errlog" ] && [ "$result" == ">>>>>>>> pass:1" ]; then + case_print_result "send_header_after_fin" "pass" +else + case_print_result "send_header_after_fin" "fail" + echo "$errlog" +fi + +clear_log +echo -e "send fin after fin ...\c" +result=`./test_client -s 5120 -l d -t 1 -E -x 52 |grep ">>>>>>>> pass"` +errlog=`grep_err_log | grep -v "send data after fin sent"` +echo "$result" +if [ -z "$errlog" ] && [ "$result" == ">>>>>>>> pass:1" ]; then + case_print_result "send_fin_after_fin" "pass" +else + case_print_result "send_fin_after_fin" "fail" + echo "$errlog" +fi + + clear_log echo -e "header header data ...\c" ./test_client -s 5120 -l d -t 1 -E -x 30 >> clog @@ -395,7 +528,7 @@ fi clear_log echo -e "alp negotiation failure ...\c" rm -f test_session -./test_client -l e -t 1 -T -x 43 > stdlog +./test_client -l e -t 1 -T 1 -x 43 > stdlog alpn_res=`grep "xqc_ssl_alpn_select_cb|select proto error" slog` if [ -n "$alpn_res" ]; then echo ">>>>>>>> pass:1" @@ -426,7 +559,7 @@ fi clear_log rm -f test_session xqc_token tp_localhost echo -e "transport ping ...\c" -./test_client -s 1024 -l d -E -x 28 -T >> clog +./test_client -s 1024 -l d -E -x 28 -T 1 >> clog ret_ping_id=`grep "====>ping_id:" clog` ret_no_ping_id=`grep "====>no ping_id" clog` if [ -n "$ret_ping_id" ] && [ -n "$ret_no_ping_id" ]; then @@ -490,7 +623,7 @@ fi clear_log echo -e "transport only ...\c" rm -f test_session -result=`./test_client -s 1024000 -l d -T -t 1 -E|grep ">>>>>>>> pass"` +result=`./test_client -s 1024000 -l d -T 1 -t 1 -E|grep ">>>>>>>> pass"` errlog=`grep_err_log` echo "$result" if [ -z "$errlog" ] && [ "$result" == ">>>>>>>> pass:1" ]; then @@ -502,7 +635,7 @@ fi clear_log echo -e "transport 0RTT ...\c" -./test_client -s 1024000 -l e -T -t 1 -E > stdlog +./test_client -s 1024000 -l e -T 1 -t 1 -E > stdlog result=`grep ">>>>>>>> pass:" stdlog` echo "$result" flag=`grep "early_data_flag:1" stdlog` @@ -579,7 +712,7 @@ echo -e "NULL stream callback ...\c" killall test_server ./test_server -l i -e -x 2 > /dev/null & sleep 1 -./test_client -l d -T -E >> clog +./test_client -l d -T 1 -E >> clog if grep "stream_read_notify is NULL" slog >/dev/null; then echo ">>>>>>>> pass:1" case_print_result "NULL_stream_callback" "pass" @@ -812,6 +945,56 @@ else echo "$errlog" fi +clear_log +echo -e "unlimited_cc...\c" +result=`./test_client -s 102400 -l e -t 1 -E -c u|grep ">>>>>>>> pass"` +errlog=`grep_err_log` +echo "$result" +if [ -z "$errlog" ] && [ "$result" == ">>>>>>>> pass:1" ]; then + case_print_result "unlimited_cc" "pass" +else + case_print_result "unlimited_cc" "fail" + echo "$errlog" +fi + +clear_log +echo -e "Copa with default parameters (delta=0.05, ai_unit=1.0) ...\c" +result=`./test_client -s 10240000 -l e -t 1 -E -c P|grep ">>>>>>>> pass"` +errlog=`grep_err_log` +echo "$result" +if [ -z "$errlog" ] && [ "$result" == ">>>>>>>> pass:1" ]; then + case_print_result "copa_with_default_parameters" "pass" +else + case_print_result "copa_with_default_parameters" "fail" + echo "$errlog" +fi + +clear_log +echo -e "Copa with customized parameters (delta=0.5, ai_unit=5.0) ...\c" +result=`./test_client -s 10240000 -l e -t 1 -E -c P --copa_delta 0.5 --copa_ai_unit 5.0 |grep ">>>>>>>> pass"` +errlog=`grep_err_log` +echo "$result" +if [ -z "$errlog" ] && [ "$result" == ">>>>>>>> pass:1" ]; then + case_print_result "copa_with_customized_parameters" "pass" +else + case_print_result "copa_with_customized_parameters" "fail" + echo "$errlog" +fi + + +clear_log +echo -e "low_delay_settings...\c" +result=`./test_client -s 102400 -l e -t 1 -E -x 400|grep ">>>>>>>> pass"` +errlog=`grep_err_log` +echo "$result" +if [ -z "$errlog" ] && [ "$result" == ">>>>>>>> pass:1" ]; then + case_print_result "low_delay_settings" "pass" +else + case_print_result "low_delay_settings" "fail" + echo "$errlog" +fi + + clear_log result=`./test_client -s 10240000 -l e -t 1 -E -x 26|grep ">>>>>>>> pass"` errlog=`grep_err_log` @@ -839,8 +1022,9 @@ fi clear_log echo -e "connection level flow control ...\c" -./test_client -s 512000 -l e -E -n 10 >> clog -if [[ `grep ">>>>>>>> pass:1" clog|wc -l` -eq 10 ]]; then +./test_client -s 512000 -l e -E -n 10 > stdlog +sleep 1 +if [[ `grep ">>>>>>>> pass:1" stdlog|wc -l` -eq 10 ]]; then echo ">>>>>>>> pass:1" case_print_result "connection_level_flow_control" "pass" else @@ -934,8 +1118,8 @@ echo -e "test client long header ...\c" ./test_client -l d -x 29 >> clog #clog_res=`grep "xqc_process_conn_close_frame|with err:" clog` #slog_res=`grep "READ_VALUE error" slog` -slog_res=`grep "large nv|conn" slog` -clog_res=`grep "xqc_process_conn_close_frame|with err:" clog` +slog_res=`grep -a "large nv|conn" slog` +clog_res=`grep -a "xqc_process_conn_close_frame|with err:" clog` if [ -n "$clog_res" ] && [ -n "$slog_res" ]; then case_print_result "test_client_long_header" "pass" else @@ -1167,7 +1351,7 @@ sleep 1 clear_log echo -e "linger close transport ...\c" rm -f test_session xqc_token tp_localhost -result=`./test_client -l e -T -t 1 -E|grep ">>>>>>>> pass"` +result=`./test_client -l e -T 1 -t 1 -E|grep ">>>>>>>> pass"` errlog=`grep_err_log` echo "$result" if [ -z "$errlog" ] && [ "$result" == ">>>>>>>> pass:1" ]; then @@ -1228,7 +1412,8 @@ grep_err_log echo -e "max pkt out size...\c" -result=`./test_client -l d -x 42 -1 -E | grep ">>>>>>>> pass"` +./test_client -l d -x 42 -1 -E > stdlog +result=`grep ">>>>>>>> pass" stdlog` if [ -n "$result" ]; then echo ">>>>>>>> pass:1" case_print_result "max_pkt_out_size" "pass" @@ -1243,8 +1428,8 @@ killall test_server sleep 1 clear_log echo -e "stateless reset...\c" -./test_client -l d -x 41 -1 >> stdlog -result=`grep "receive reset, enter draining" clog` +./test_client -l d -x 41 -1 -t 5 > stdlog +result=`grep "|====>|receive stateless reset" clog` cloing_notify=`grep "conn closing: 641" stdlog` if [ -n "$result" ] && [ -n "$cloing_notify" ]; then echo ">>>>>>>> pass:1" @@ -1255,6 +1440,21 @@ else fi +clear_log +echo -e "stateless reset during hsk...\c" +./test_client -l d -t 5 -x 45 -1 -s 100 -G > stdlog +result=`grep "|====>|receive stateless reset" clog` +cloing_notify=`grep "conn closing: 641" stdlog` +svr_hsk=`grep "handshake_time:0" slog` +if [ -n "$result" ] && [ -n "$cloing_notify" ] && [ -n "$svr_hsk" ]; then + echo ">>>>>>>> pass:1" + case_print_result "stateless_reset_during_hsk" "pass" +else + echo ">>>>>>>> pass:0" + case_print_result "stateless_reset_during_hsk" "fail" + exit +fi + killall test_server ./test_server -l d -e -M > /dev/null & sleep 1 @@ -1262,7 +1462,8 @@ sleep 1 clear_log echo -e "MPNS enable multipath negotiate ...\c" -result=`sudo ./test_client -s 1024000 -l d -t 1 -M -A -i lo | grep "enable_multipath=2"` +sudo ./test_client -s 1024000 -l d -t 1 -M -i lo > stdlog +result=` grep "enable_multipath=1" stdlog` errlog=`grep_err_log` if [ -z "$errlog" ] && [ "$result" != "" ]; then echo ">>>>>>>> pass:1" @@ -1275,7 +1476,8 @@ grep_err_log clear_log echo -e "MPNS send 1M data on multiple paths ...\c" -result=`sudo ./test_client -s 1024000 -l d -t 1 -M -A -i lo -i lo -E|grep ">>>>>>>> pass"` +sudo ./test_client -s 1024000 -l d -t 1 -M -i lo -i lo -E > stdlog +result=`grep ">>>>>>>> pass" stdlog` errlog=`grep_err_log` if [ -z "$errlog" ] && [ "$result" == ">>>>>>>> pass:1" ]; then echo ">>>>>>>> pass:1" @@ -1288,7 +1490,8 @@ grep_err_log clear_log echo -e "MPNS multipath 30 percent loss ...\c" -result=`sudo ./test_client -s 10240000 -t 5 -l e -E -d 300 -M -A -i lo -i lo|grep ">>>>>>>> pass"` +sudo ./test_client -s 10240000 -t 5 -l e -E -d 300 -M -i lo -i lo > stdlog +result=`grep ">>>>>>>> pass" stdlog` errlog=`grep_err_log` if [ -z "$errlog" ] && [ "$result" == ">>>>>>>> pass:1" ]; then echo ">>>>>>>> pass:1" @@ -1301,8 +1504,8 @@ grep_err_log clear_log echo -e "MPNS multipath close initial path ...\c" -sudo ./test_client -s 1024000 -l d -t 3 -M -A -i lo -i lo -E -x 100 >> clog -result=`grep ">>>>>>>> pass" clog` +sudo ./test_client -s 1024000 -l d -t 3 -M -i lo -i lo -E -x 100 > stdlog +result=`grep ">>>>>>>> pass" stdlog` svr_res=`grep "|path closed|path:0|" slog` cli_res=`grep "|path closed|path:0|" clog` errlog=`grep_err_log` @@ -1317,16 +1520,17 @@ grep_err_log clear_log echo -e "MPNS multipath 30 percent loss close initial path ...\c" -result=`sudo ./test_client -s 10240000 -t 5 -l d -E -d 300 -M -A -i lo -i lo -x 100|grep ">>>>>>>> pass"` +sudo ./test_client -s 10240000 -t 5 -l d -E -d 300 -M -i lo -i lo -x 100 > stdlog +result=`grep ">>>>>>>> pass" stdlog` svr_res=`grep "|path closed|path:0|" slog` cli_res=`grep "|path closed|path:0|" clog` errlog=`grep_err_log` -if [ -z "$errlog" ] && [ "$result" == ">>>>>>>> pass:1" ] && [ "$svr_res" != "" ] && [ "$result" == ">>>>>>>> pass:1" ]; then +if [ -z "$errlog" ] && [ "$result" == ">>>>>>>> pass:1" ] && [ "$svr_res" != "" ] && [ "$cli_res" != "" ]; then echo ">>>>>>>> pass:1" - case_print_result "MPNS_multipath_10_percent_loss_close_initial_path" "pass" + case_print_result "MPNS_multipath_30_percent_loss_close_initial_path" "pass" else echo ">>>>>>>> pass:0" - case_print_result "MPNS_multipath_10_percent_loss_close_initial_path" "fail" + case_print_result "MPNS_multipath_30_percent_loss_close_initial_path" "fail" fi grep_err_log @@ -1348,11 +1552,12 @@ grep_err_log clear_log echo -e "MPNS multipath 30 percent loss close new path ...\c" -result=`sudo ./test_client -s 10240000 -t 5 -l d -E -d 300 -M -A -i lo -i lo -x 101|grep ">>>>>>>> pass"` +sudo ./test_client -s 10240000 -t 5 -l d -E -d 300 -M -i lo -i lo -x 101 > stdlog +result=`grep ">>>>>>>> pass" stdlog` svr_res=`grep "|path closed|path:1|" slog` cli_res=`grep "|path closed|path:1|" clog` errlog=`grep_err_log` -if [ -z "$errlog" ] && [ "$result" == ">>>>>>>> pass:1" ] && [ "$svr_res" != "" ] && [ "$result" == ">>>>>>>> pass:1" ]; then +if [ -z "$errlog" ] && [ "$result" == ">>>>>>>> pass:1" ] && [ "$svr_res" != "" ] && [ "$cli_res" != "" ]; then echo ">>>>>>>> pass:1" case_print_result "MPNS_multipath_30_percent_loss_close_new_path" "pass" else @@ -1361,13 +1566,45 @@ else fi grep_err_log +killall test_server +./test_server -l d -e -M > /dev/null & +sleep 1 + + +clear_log +echo -e "send 1M data on multiple paths with multipath vertion 04" +sudo ./test_client -s 1024000 -l d -t 1 -M -i lo -i lo -E -v 4 > stdlog +cli_result=`grep "multipath version negotiation succeed on multipath 04" clog` +if [ -n "$cli_result" ]; then + echo ">>>>>>>> pass:1" + case_print_result "MPNS_send_data_with_multipath_04" "pass" +else + echo ">>>>>>>> pass:0" + case_print_result "MPNS_send_data_with_multipath_04" "fail" +fi +rm -f test_session tp_localhost xqc_token + +clear_log +echo -e "send 1M data on multiple paths with multipath vertion 05" +sudo ./test_client -s 1024000 -l d -t 1 -M -i lo -i lo -E -v 5 > stdlog +cli_result=`grep "multipath version negotiation succeed on multipath 05" clog` +if [ -n "$cli_result" ]; then + echo ">>>>>>>> pass:1" + case_print_result "MPNS_send_data_with_multipath_05" "pass" +else + echo ">>>>>>>> pass:0" + case_print_result "MPNS_send_data_with_multipath_05" "fail" +fi +rm -f test_session tp_localhost xqc_token + killall test_server ./test_server -l d -e -M -R 1 > /dev/null & sleep 1 clear_log echo -e "MPNS reinject unack packets by capacity ...\c" -result=`sudo ./test_client -s 1024000 -l d -t 1 -M -A -i lo -i lo -E -R 1 |grep ">>>>>>>> pass"` +sudo ./test_client -s 1024000 -l d -t 1 -M -i lo -i lo -E -R 1 > stdlog +result=`grep ">>>>>>>> pass" stdlog` errlog=`grep_err_log` if [ -z "$errlog" ] && [ "$result" == ">>>>>>>> pass:1" ]; then echo ">>>>>>>> pass:1" @@ -1385,7 +1622,8 @@ sleep 1 clear_log echo -e "MPNS reinject unack packets by deadline ...\c" -result=`sudo ./test_client -s 1024000 -l d -t 1 -M -A -i lo -i lo -E -R 2 |grep ">>>>>>>> pass"` +sudo ./test_client -s 1024000 -l d -t 1 -M -i lo -i lo -E -R 2 > stdlog +result=`grep ">>>>>>>> pass" stdlog` errlog=`grep_err_log` if [ -z "$errlog" ] && [ "$result" == ">>>>>>>> pass:1" ]; then echo ">>>>>>>> pass:1" @@ -1403,7 +1641,8 @@ sleep 1 clear_log echo -e "NAT rebinding path 0 ...\c" -result=`sudo ./test_client -s 102400 -l d -t 3 -M -A -i lo -i lo -E -n 2 -x 103 |grep ">>>>>>>> pass:0"` +sudo ./test_client -s 102400 -l d -t 3 -M -i lo -i lo -E -n 2 -x 103 > stdlog +result=`grep ">>>>>>>> pass:0" stdlog` errlog=`grep_err_log` rebind=`grep "|path:0|REBINDING|validate NAT rebinding addr|" slog` if [ -z "$errlog" ] && [ -z "$result" ] && [ "$rebind" != "" ]; then @@ -1420,7 +1659,8 @@ grep_err_log clear_log echo -e "NAT rebinding path 1 ...\c" -result=`sudo ./test_client -s 102400 -l d -t 3 -M -A -i lo -i lo -E -n 2 -x 104 |grep ">>>>>>>> pass:0"` +sudo ./test_client -s 1024000 -l d -t 3 -M -i lo -i lo -E -n 2 -x 104 > stdlog +result=`grep ">>>>>>>> pass:0" stdlog` errlog=`grep_err_log` rebind=`grep "|path:1|REBINDING|validate NAT rebinding addr|" slog` if [ -z "$errlog" ] && [ -z "$result" ] && [ "$rebind" != "" ]; then @@ -1433,12 +1673,12 @@ fi grep_err_log killall test_server -./test_server -l d -e -M -Q > /dev/null & +./test_server -l d -e -M -y > /dev/null & sleep 1 clear_log echo -e "Multipath Compensate and Accelerate ...\c" -sudo ./test_client -s 102400 -l d -t 3 -M -A -i lo -i lo -E -P 2 -Q > ccfc.log +sudo ./test_client -s 102400 -l d -t 3 -M -A -i lo -i lo -E -P 2 -y > ccfc.log errlog=`grep_err_log` svr_res=`grep "path_status:2->1" slog` cli_res=`grep "path_status:2->1" clog` @@ -1453,7 +1693,7 @@ grep_err_log clear_log echo -e "Multipath Compensate but not Accelerate ...\c" -sudo ./test_client -s 102400 -l d -t 3 -M -i lo -i lo -E -P 2 -Q > ccfc.log +sudo ./test_client -s 102400 -l d -t 3 -M -i lo -i lo -E -P 2 -y > ccfc.log errlog=`grep_err_log` svr_res=`grep "path_status:2->1" slog` cli_res=`grep "path_status:2->1" clog` @@ -1467,9 +1707,2471 @@ fi grep_err_log +killall test_server +if [ -f test_session ]; then + rm -f test_session +fi +if [ -f tp_localhost ]; then + rm -f tp_localhost +fi +if [ -f xqc_token ]; then + rm -f xqc_token +fi +if [ -f stdlog ]; then + rm -f stdlog +fi +./test_server -l d -Q 9000 > /dev/null & +sleep 1 +clear_log +echo -e "datagram frame size negotiation...\c" +./test_client -l d -Q 9000 >> stdlog +cli_result=`grep "|1RTT_transport_params|max_datagram_frame_size:9000|" clog` +svr_result=`grep "|1RTT_transport_params|max_datagram_frame_size:9000|" slog` +errlog=`grep_err_log` +if [ -n "$cli_result" ] && [ -n "$svr_result" ] && [ -z "$errlog" ]; then + echo ">>>>>>>> pass:1" + case_print_result "datagram_frame_size_negotiation" "pass" +else + echo ">>>>>>>> pass:0" + case_print_result "datagram_frame_size_negotiation" "fail" +fi + +clear_log +echo -e "0RTT max_datagram_frame_size is valid...\c" +./test_client -l d >> stdlog +cli_result=`grep "|0RTT_transport_params|max_datagram_frame_size:9000|" clog` +cli_result2=`grep "|1RTT_transport_params|max_datagram_frame_size:9000|" clog` +errlog=`grep_err_log` +if [ -n "$cli_result" ] && [ -n "$cli_result2" ] && [ -z "$errlog" ]; then + echo ">>>>>>>> pass:1" + case_print_result "0rtt_max_datagram_frame_size_is_valid" "pass" +else + echo ">>>>>>>> pass:0" + case_print_result "0rtt_max_datagram_frame_size_is_valid" "fail" +fi + +killall test_server +./test_server -l d -Q 8000 > /dev/null & +sleep 1 +clear_log +echo -e "0RTT max_datagram_frame_size is invalid...\c" +./test_client -l d >> stdlog +cli_result=`grep "|0RTT_transport_params|max_datagram_frame_size:9000|" clog` +cli_err=`grep "[error].*err:0xe" clog` +svr_err=`grep "[error].*err:0xe" slog` +if [ -n "$cli_result" ] && [ -n "$cli_err" ] && [ -n "$svr_err" ]; then + echo ">>>>>>>> pass:1" + case_print_result "0rtt_max_datagram_frame_size_is_invalid" "pass" +else + echo ">>>>>>>> pass:0" + case_print_result "0rtt_max_datagram_frame_size_is_invalid" "fail" +fi +rm -f test_session tp_localhost xqc_token + +killall test_server +if [ -f test_session ]; then + rm -f test_session +fi +if [ -f tp_localhost ]; then + rm -f tp_localhost +fi +if [ -f xqc_token ]; then + rm -f xqc_token +fi +stdbuf -oL ./test_server -l d -Q 1000 -x 200 > svr_stdlog & +sleep 1 +clear_log +echo -e "datagram_get_mss(no_saved_transport_params)...\c" +./test_client -l d -T 1 -x 200 -Q 1000 -s 1 -U 1 > stdlog +cli_res1=`grep "\[dgram-200\]|.*|initial_mss:0|" stdlog` +cli_res2=`grep "\[dgram-200\]|.*|updated_mss:997|" stdlog` +svr_res=`grep -a "\[dgram-200\]|.*|initial_mss:997|" svr_stdlog` +errlog=`grep_err_log` +if [ -n "$cli_res1" ] && [ -n "$cli_res2" ] && [ -n "$svr_res" ] && [ -z "$errlog" ]; then + echo ">>>>>>>> pass:1" + case_print_result "datagram_get_mss_no_saved_transport_params" "pass" +else + echo ">>>>>>>> pass:0" + case_print_result "datagram_get_mss_no_saved_transport_params" "fail" +fi + +> svr_stdlog +clear_log +echo -e "datagram_get_mss(saved_transport_params)...\c" +./test_client -l d -T 1 -x 200 -Q 1000 -s 1 -U 1 > stdlog +cli_res1=`grep "\[dgram-200\]|.*|initial_mss:997|" stdlog` +cli_res2=`grep "\[dgram-200\]|.*|updated_mss:997|" stdlog` +svr_res=`grep -a "\[dgram-200\]|.*|initial_mss:997|" svr_stdlog` +errlog=`grep_err_log` +if [ -n "$cli_res1" ] && [ -n "$cli_res2" ] && [ -n "$svr_res" ] && [ -z "$errlog" ]; then + echo ">>>>>>>> pass:1" + case_print_result "datagram_get_mss_saved_transport_params" "pass" +else + echo ">>>>>>>> pass:0" + case_print_result "datagram_get_mss_saved_transport_params" "fail" +fi + +killall test_server +if [ -f test_session ]; then + rm -f test_session +fi +if [ -f tp_localhost ]; then + rm -f tp_localhost +fi +if [ -f xqc_token ]; then + rm -f xqc_token +fi +stdbuf -oL ./test_server -l d -Q 65535 -x 201 > svr_stdlog & +sleep 1 +clear_log +echo -e "datagram_mss_limited_by_MTU...\c" +./test_client -l d -T 1 -x 201 -Q 65535 -s 1 -U 1 > stdlog +cli_res1=`grep "\[dgram-200\]|.*|initial_mss:0|" stdlog` +cli_res2=`grep "\[dgram-200\]|.*|updated_mss:1200|" stdlog` +svr_res=`grep -a "\[dgram-200\]|.*|initial_mss:1200|" svr_stdlog` +errlog=`grep_err_log` +if [ -n "$cli_res1" ] && [ -n "$cli_res2" ] && [ -n "$svr_res" ] && [ -z "$errlog" ]; then + echo ">>>>>>>> pass:1" + case_print_result "datagram_mss_limited_by_MTU" "pass" +else + echo ">>>>>>>> pass:0" + case_print_result "datagram_mss_limited_by_MTU" "fail" +fi + +killall test_server +if [ -f test_session ]; then + rm -f test_session +fi +if [ -f tp_localhost ]; then + rm -f tp_localhost +fi +if [ -f xqc_token ]; then + rm -f xqc_token +fi + +# timer-based dgram probe +stdbuf -oL ./test_server -l d -Q 65535 -x 209 -e -U 2 > svr_stdlog & +sleep 1 +clear_log +echo -e "timer_based_dgram_probe...\c" +./test_client -l d -T 1 -x 209 -s 1000 -U 1 -Q 65535 -x 209 > stdlog +killall test_server +cli_res1=`grep "|recv_dgram_bytes:3000|sent_dgram_bytes:1000|" stdlog` +svr_res=`grep -a "|recv_dgram_bytes:2000|sent_dgram_bytes:2000|" svr_stdlog` +errlog=`grep_err_log` +if [ -n "$cli_res1" ] && [ -n "$svr_res" ] && [ -z "$errlog" ]; then + echo ">>>>>>>> pass:1" + case_print_result "timer_based_dgram_probe" "pass" +else + echo ">>>>>>>> pass:0" + case_print_result "timer_based_dgram_probe" "fail" +fi + +killall test_server &> /dev/null +if [ -f test_session ]; then + rm -f test_session +fi +if [ -f tp_localhost ]; then + rm -f tp_localhost +fi +if [ -f xqc_token ]; then + rm -f xqc_token +fi + +stdbuf -oL ./test_server -l d -Q 1000 -x 200 > svr_stdlog & +sleep 1 +clear_log +echo -e "datagram_mss_limited_by_max_datagram_frame_size...\c" +./test_client -l d -T 1 -x 200 -s 1 -U 1 -Q 1000 > stdlog +cli_res1=`grep "\[dgram-200\]|.*|initial_mss:0|" stdlog` +cli_res2=`grep "\[dgram-200\]|.*|updated_mss:997|" stdlog` +svr_res=`grep -a "\[dgram-200\]|.*|initial_mss:997|" svr_stdlog` +errlog=`grep_err_log` +if [ -n "$cli_res1" ] && [ -n "$cli_res2" ] && [ -n "$svr_res" ] && [ -z "$errlog" ]; then + echo ">>>>>>>> pass:1" + case_print_result "datagram_mss_limited_by_max_datagram_frame_size" "pass" +else + echo ">>>>>>>> pass:0" + case_print_result "datagram_mss_limited_by_max_datagram_frame_size" "fail" +fi +rm -f test_session tp_localhost xqc_token + +killall test_server + +./test_server -l e -Q 65535 -e -U 1 > /dev/null & +sleep 1 +clear_log +#generate 0rtt data +./test_client -l e -T 1 -s 1 -U 1 -Q 65535 > stdlog +clear_log +echo -e "send_0RTT_datagram_100KB...\c" +./test_client -l e -T 1 -s 102400 -U 1 -Q 65535 -E > stdlog +cli_res1=`grep "\[dgram\]|echo_check|same_content:yes|" stdlog` +errlog=`grep_err_log` +if [ -n "$cli_res1" ] && [ -z "$errlog" ]; then + echo ">>>>>>>> pass:1" + case_print_result "send_0RTT_datagram_100KB" "pass" +else + echo ">>>>>>>> pass:0" + case_print_result "send_0RTT_datagram_100KB" "fail" +fi + +if [ $LOCAL_TEST -ne 0 ]; then + clear_log + echo -e "send_0RTT_datagram_1MB...\c" + ./test_client -l e -T 1 -s 1048576 -U 1 -Q 65535 -E > stdlog + cli_res1=`grep "\[dgram\]|echo_check|same_content:yes|" stdlog` + errlog=`grep_err_log` + if [ -n "$cli_res1" ] && [ -z "$errlog" ]; then + echo ">>>>>>>> pass:1" + case_print_result "send_0RTT_datagram_1MB" "pass" + else + echo ">>>>>>>> pass:0" + case_print_result "send_0RTT_datagram_1MB" "fail" + fi + + clear_log + echo -e "send_0RTT_datagram_10MB...\c" + ./test_client -l e -T 1 -s 10485760 -U 1 -Q 65535 -E > stdlog + cli_res1=`grep "\[dgram\]|echo_check|same_content:yes|" stdlog` + errlog=`grep_err_log` + if [ -n "$cli_res1" ] && [ -z "$errlog" ]; then + echo ">>>>>>>> pass:1" + case_print_result "send_0RTT_datagram_10MB" "pass" + else + echo ">>>>>>>> pass:0" + case_print_result "send_0RTT_datagram_10MB" "fail" + fi + + clear_log + echo -e "send_0RTT_datagram_100MB...\c" + ./test_client -l e -T 1 -s 104857600 -U 1 -Q 65535 -E > stdlog + cli_res1=`grep "\[dgram\]|echo_check|same_content:yes|" stdlog` + errlog=`grep_err_log` + if [ -n "$cli_res1" ] && [ -z "$errlog" ]; then + echo ">>>>>>>> pass:1" + case_print_result "send_0RTT_datagram_100MB" "pass" + else + echo ">>>>>>>> pass:0" + case_print_result "send_0RTT_datagram_100MB" "fail" + fi + +fi +rm -f test_session tp_localhost xqc_token + killall test_server +./test_server -l e -Q 65535 -e -U 2 > /dev/null & +sleep 1 +clear_log +#generate 0rtt data +./test_client -l e -T 1 -s 1 -U 2 -Q 65535 > stdlog +clear_log +echo -e "send_0RTT_datagram_100KB_batch...\c" +./test_client -l e -T 1 -s 102400 -U 2 -Q 65535 -E > stdlog +cli_res1=`grep "\[dgram\]|echo_check|same_content:yes|" stdlog` +errlog=`grep_err_log` +if [ -n "$cli_res1" ] && [ -z "$errlog" ]; then + echo ">>>>>>>> pass:1" + case_print_result "send_0RTT_datagram_100KB_batch" "pass" +else + echo ">>>>>>>> pass:0" + case_print_result "send_0RTT_datagram_100KB_batch" "fail" +fi + +if [ $LOCAL_TEST -ne 0 ]; then + clear_log + echo -e "send_0RTT_datagram_1MB_batch...\c" + ./test_client -l e -T 1 -s 1048576 -U 2 -Q 65535 -E > stdlog + cli_res1=`grep "\[dgram\]|echo_check|same_content:yes|" stdlog` + errlog=`grep_err_log` + if [ -n "$cli_res1" ] && [ -z "$errlog" ]; then + echo ">>>>>>>> pass:1" + case_print_result "send_0RTT_datagram_1MB_batch" "pass" + else + echo ">>>>>>>> pass:0" + case_print_result "send_0RTT_datagram_1MB_batch" "fail" + fi + clear_log + echo -e "send_0RTT_datagram_10MB_batch...\c" + ./test_client -l e -T 1 -s 10485760 -U 2 -Q 65535 -E > stdlog + cli_res1=`grep "\[dgram\]|echo_check|same_content:yes|" stdlog` + errlog=`grep_err_log` + if [ -n "$cli_res1" ] && [ -z "$errlog" ]; then + echo ">>>>>>>> pass:1" + case_print_result "send_0RTT_datagram_10MB_batch" "pass" + else + echo ">>>>>>>> pass:0" + case_print_result "send_0RTT_datagram_10MB_batch" "fail" + fi + + clear_log + echo -e "send_0RTT_datagram_100MB_batch...\c" + ./test_client -l e -T 1 -s 104857600 -U 2 -Q 65535 -E > stdlog + cli_res1=`grep "\[dgram\]|echo_check|same_content:yes|" stdlog` + errlog=`grep_err_log` + if [ -n "$cli_res1" ] && [ -z "$errlog" ]; then + echo ">>>>>>>> pass:1" + case_print_result "send_0RTT_datagram_100MB_batch" "pass" + else + echo ">>>>>>>> pass:0" + case_print_result "send_0RTT_datagram_100MB_batch" "fail" + fi + +fi +rm -f test_session tp_localhost xqc_token + + +killall test_server +./test_server -l e -Q 65535 -e -U 1 > /dev/null & +sleep 1 +clear_log +echo -e "send_1RTT_datagram_100KB...\c" +./test_client -l e -T 1 -s 102400 -U 1 -Q 65535 -E -1 > stdlog +cli_res1=`grep "\[dgram\]|echo_check|same_content:yes|" stdlog` +errlog=`grep_err_log` +if [ -n "$cli_res1" ] && [ -z "$errlog" ]; then + echo ">>>>>>>> pass:1" + case_print_result "send_1RTT_datagram_100KB" "pass" +else + echo ">>>>>>>> pass:0" + case_print_result "send_1RTT_datagram_100KB" "fail" +fi + +if [ $LOCAL_TEST -ne 0 ]; then + clear_log + echo -e "send_1RTT_datagram_1MB...\c" + ./test_client -l e -T 1 -s 1048576 -U 1 -Q 65535 -E -1 > stdlog + cli_res1=`grep "\[dgram\]|echo_check|same_content:yes|" stdlog` + errlog=`grep_err_log` + if [ -n "$cli_res1" ] && [ -z "$errlog" ]; then + echo ">>>>>>>> pass:1" + case_print_result "send_1RTT_datagram_1MB" "pass" + else + echo ">>>>>>>> pass:0" + case_print_result "send_1RTT_datagram_1MB" "fail" + fi + + clear_log + echo -e "send_1RTT_datagram_10MB...\c" + ./test_client -l e -T 1 -s 10485760 -U 1 -Q 65535 -E -1 > stdlog + cli_res1=`grep "\[dgram\]|echo_check|same_content:yes|" stdlog` + errlog=`grep_err_log` + if [ -n "$cli_res1" ] && [ -z "$errlog" ]; then + echo ">>>>>>>> pass:1" + case_print_result "send_1RTT_datagram_10MB" "pass" + else + echo ">>>>>>>> pass:0" + case_print_result "send_1RTT_datagram_10MB" "fail" + fi + + clear_log + echo -e "send_1RTT_datagram_100MB...\c" + ./test_client -l e -T 1 -s 104857600 -U 1 -Q 65535 -E -1 > stdlog + cli_res1=`grep "\[dgram\]|echo_check|same_content:yes|" stdlog` + errlog=`grep_err_log` + if [ -n "$cli_res1" ] && [ -z "$errlog" ]; then + echo ">>>>>>>> pass:1" + case_print_result "send_1RTT_datagram_100MB" "pass" + else + echo ">>>>>>>> pass:0" + case_print_result "send_1RTT_datagram_100MB" "fail" + fi +fi +rm -f test_session tp_localhost xqc_token + + +killall test_server +./test_server -l e -Q 65535 -e -U 2 > /dev/null & +sleep 1 +clear_log +echo -e "send_1RTT_datagram_100KB_batch...\c" +./test_client -l e -T 1 -s 102400 -U 2 -Q 65535 -E -1 > stdlog +cli_res1=`grep "\[dgram\]|echo_check|same_content:yes|" stdlog` +errlog=`grep_err_log` +if [ -n "$cli_res1" ] && [ -z "$errlog" ]; then + echo ">>>>>>>> pass:1" + case_print_result "send_1RTT_datagram_100KB_batch" "pass" +else + echo ">>>>>>>> pass:0" + case_print_result "send_1RTT_datagram_100KB_batch" "fail" +fi + +if [ $LOCAL_TEST -ne 0 ]; then + clear_log + echo -e "send_1RTT_datagram_1MB_batch...\c" + ./test_client -l e -T 1 -s 1048576 -U 2 -Q 65535 -E -1 > stdlog + cli_res1=`grep "\[dgram\]|echo_check|same_content:yes|" stdlog` + errlog=`grep_err_log` + if [ -n "$cli_res1" ] && [ -z "$errlog" ]; then + echo ">>>>>>>> pass:1" + case_print_result "send_1RTT_datagram_1MB_batch" "pass" + else + echo ">>>>>>>> pass:0" + case_print_result "send_1RTT_datagram_1MB_batch" "fail" + fi + + clear_log + echo -e "send_1RTT_datagram_10MB_batch...\c" + ./test_client -l e -T 1 -s 10485760 -U 2 -Q 65535 -E -1 > stdlog + cli_res1=`grep "\[dgram\]|echo_check|same_content:yes|" stdlog` + errlog=`grep_err_log` + if [ -n "$cli_res1" ] && [ -z "$errlog" ]; then + echo ">>>>>>>> pass:1" + case_print_result "send_1RTT_datagram_10MB_batch" "pass" + else + echo ">>>>>>>> pass:0" + case_print_result "send_1RTT_datagram_10MB_batch" "fail" + fi + + clear_log + echo -e "send_1RTT_datagram_100MB_batch...\c" + ./test_client -l e -T 1 -s 104857600 -U 2 -Q 65535 -E -1 > stdlog + cli_res1=`grep "\[dgram\]|echo_check|same_content:yes|" stdlog` + errlog=`grep_err_log` + if [ -n "$cli_res1" ] && [ -z "$errlog" ]; then + echo ">>>>>>>> pass:1" + case_print_result "send_1RTT_datagram_100MB_batch" "pass" + else + echo ">>>>>>>> pass:0" + case_print_result "send_1RTT_datagram_100MB_batch" "fail" + fi +fi +rm -f test_session tp_localhost xqc_token + +killall test_server +./test_server -l d -Q 65535 -e -U 1 -s 1 > /dev/null & +sleep 1 +clear_log +echo -e "send_queue_full...\c" +./test_client -l d -T 1 -s 40000000 -U 1 -Q 65535 -1 > stdlog +cli_res1=`grep "\[dgram\]|retry_datagram_send_later|" stdlog` +cli_res2=`grep "|too many packets used|ctl_packets_used:" clog` +cli_res3=`grep "\[dgram\]|dgram_write|" stdlog` +errlog=`grep_err_log` +if [ -n "$cli_res1" ] && [ -n "$cli_res2" ] && [ -n "$cli_res3" ] && [ -z "$errlog" ]; then + echo ">>>>>>>> pass:1" + case_print_result "send_queue_full" "pass" +else + echo ">>>>>>>> pass:0" + case_print_result "send_queue_full" "fail" +fi + +clear_log +echo -e "send_queue_full_batch...\c" +./test_client -l d -T 1 -s 40000000 -U 2 -Q 65535 -1 > stdlog +cli_res1=`grep "\[dgram\]|retry_datagram_send_multiple_later|" stdlog` +cli_res2=`grep "|too many packets used|ctl_packets_used:" clog` +cli_res3=`grep "\[dgram\]|dgram_write|" stdlog` +errlog=`grep_err_log` +if [ -n "$cli_res1" ] && [ -n "$cli_res2" ] && [ -n "$cli_res3" ] && [ -z "$errlog" ]; then + echo ">>>>>>>> pass:1" + case_print_result "send_queue_full_batch" "pass" +else + echo ">>>>>>>> pass:0" + case_print_result "send_queue_full_batch" "fail" +fi + +clear_log +echo -e "send_0rtt_datagram_without_saved_datagram_tp...\c" +./test_client -l d -T 1 -s 999 -U 1 -Q 65535 -1 -E -x 202 > stdlog +cli_res1=`grep "\[dgram\]|retry_datagram_send_later|" stdlog` +cli_res2=`grep "|waiting_for_max_datagram_frame_size_from_peer|" clog` +cli_res3=`grep "\[dgram\]|echo_check|same_content:yes|" stdlog` +cli_res4=`grep "\[dgram\]|dgram_write|" stdlog` +errlog=`grep_err_log` +if [ -n "$cli_res1" ] && [ -n "$cli_res2" ] && [ -n "$cli_res3" ] && [ -n "$cli_res4" ] && [ -z "$errlog" ]; then + echo ">>>>>>>> pass:1" + case_print_result "send_0rtt_datagram_without_saved_datagram_tp" "pass" +else + echo ">>>>>>>> pass:0" + case_print_result "send_0rtt_datagram_without_saved_datagram_tp" "fail" +fi + +clear_log +echo -e "send_0rtt_datagram_without_saved_datagram_tp_batch...\c" +./test_client -l d -T 1 -s 999 -U 2 -Q 65535 -1 -E -x 202 > stdlog +cli_res1=`grep "\[dgram\]|retry_datagram_send_multiple_later|" stdlog` +cli_res2=`grep "|waiting_for_max_datagram_frame_size_from_peer|" clog` +cli_res3=`grep "\[dgram\]|echo_check|same_content:yes|" stdlog` +cli_res4=`grep "\[dgram\]|dgram_write|" stdlog` +errlog=`grep_err_log` +if [ -n "$cli_res1" ] && [ -n "$cli_res2" ] && [ -n "$cli_res3" ] && [ -n "$cli_res4" ] && [ -z "$errlog" ]; then + echo ">>>>>>>> pass:1" + case_print_result "send_0rtt_datagram_without_saved_datagram_tp_batch" "pass" +else + echo ">>>>>>>> pass:0" + case_print_result "send_0rtt_datagram_without_saved_datagram_tp_batch" "fail" +fi + + +clear_log +echo -e "send_too_many_0rtt_datagrams...\c" +./test_client -l d -T 1 -s 40000 -U 1 -Q 65535 -E > stdlog +cli_res1=`grep "\[dgram\]|retry_datagram_send_later|" stdlog` +cli_res2=`grep "|too many 0rtt packets|" clog` +cli_res3=`grep "\[dgram\]|echo_check|same_content:yes|" stdlog` +cli_res4=`grep "\[dgram\]|dgram_write|" stdlog` +errlog=`grep_err_log` +if [ -n "$cli_res1" ] && [ -n "$cli_res2" ] && [ -n "$cli_res3" ] && [ -n "$cli_res4" ] && [ -z "$errlog" ]; then + echo ">>>>>>>> pass:1" + case_print_result "send_too_many_0rtt_datagrams" "pass" +else + echo ">>>>>>>> pass:0" + case_print_result "send_too_many_0rtt_datagrams" "fail" +fi + +clear_log +echo -e "send_too_many_0rtt_datagrams_batch...\c" +./test_client -l d -T 1 -s 40000 -U 2 -Q 65535 -E > stdlog +cli_res1=`grep "\[dgram\]|retry_datagram_send_multiple_later|" stdlog` +cli_res2=`grep "|too many 0rtt packets|" clog` +cli_res3=`grep "\[dgram\]|echo_check|same_content:yes|" stdlog` +cli_res4=`grep "\[dgram\]|dgram_write|" stdlog` +errlog=`grep_err_log` +if [ -n "$cli_res1" ] && [ -n "$cli_res2" ] && [ -n "$cli_res3" ] && [ -n "$cli_res4" ] && [ -z "$errlog" ]; then + echo ">>>>>>>> pass:1" + case_print_result "send_too_many_0rtt_datagrams_batch" "pass" +else + echo ">>>>>>>> pass:0" + case_print_result "send_too_many_0rtt_datagrams_batch" "fail" +fi + +killall test_server +./test_server -l d -Q 65535 -e -U 1 -s 1 > /dev/null & +sleep 1 +clear_log +echo -e "send_0rtt_datagram_reject...\c" +./test_client -l d -T 1 -s 4800 -U 1 -Q 65535 -E > stdlog +cli_res1=`grep "xqc_conn_early_data_reject" clog` +cli_res2=`grep "\[dgram\]|echo_check|same_content:yes|" stdlog` +errlog=`grep_err_log` +if [ -n "$cli_res1" ] && [ -n "$cli_res2" ] && [ -z "$errlog" ]; then + echo ">>>>>>>> pass:1" + case_print_result "send_0rtt_datagram_reject" "pass" +else + echo ">>>>>>>> pass:0" + case_print_result "send_0rtt_datagram_reject" "fail" +fi + + +killall test_server +./test_server -l d -Q 1000 -e -U 1 -s 1 > /dev/null & +sleep 1 +clear_log +echo -e "send_oversized_datagram...\c" +./test_client -l d -T 1 -s 4800 -U 1 -Q 65535 -E -1 -x 203 > stdlog +cli_res1=`grep "datagram_is_too_large" clog` +cli_res2=`grep "trying_to_send_an_oversized_datagram" stdlog` +errlog=`grep_err_log` +if [ -n "$cli_res1" ] && [ -n "$cli_res2" ] && [ -z "$errlog" ]; then + echo ">>>>>>>> pass:1" + case_print_result "send_oversized_datagram" "pass" +else + echo ">>>>>>>> pass:0" + case_print_result "send_oversized_datagram" "fail" +fi + +clear_log +echo -e "send_oversized_datagram_batch...\c" +./test_client -l d -T 1 -s 4800 -U 2 -Q 65535 -E -1 -x 203 > stdlog +cli_res1=`grep "datagram_is_too_large" clog` +cli_res2=`grep "trying_to_send_an_oversized_datagram" stdlog` +cli_res3=`grep "|partially_sent_pkts_in_a_batch|cnt:1|" stdlog` +errlog=`grep_err_log` +if [ -n "$cli_res1" ] && [ -n "$cli_res2" ] && [ -z "$errlog" ]; then + echo ">>>>>>>> pass:1" + case_print_result "send_oversized_datagram_batch" "pass" +else + echo ">>>>>>>> pass:0" + case_print_result "send_oversized_datagram_batch" "fail" +fi +rm -rf tp_localhost test_session xqc_token + +killall test_server +./test_server -l d -Q 0 -e -U 1 -s 1 > /dev/null & +sleep 1 +clear_log +echo -e "send_datagram_while_peer_does_not_support...\c" +./test_client -l d -T 1 -s 4800 -U 1 -Q 65535 -E -1 -x 204 > stdlog +cli_res1=`grep "|does not support datagram|" clog` +cli_res2=`grep "\[dgram\]|send_datagram_error|" stdlog` +errlog=`grep_err_log` +if [ -n "$cli_res1" ] && [ -n "$cli_res2" ] && [ -z "$errlog" ]; then + echo ">>>>>>>> pass:1" + case_print_result "send_datagram_while_peer_does_not_support" "pass" +else + echo ">>>>>>>> pass:0" + case_print_result "send_datagram_while_peer_does_not_support" "fail" +fi + +clear_log +echo -e "send_datagram_batch_while_peer_does_not_support...\c" +./test_client -l d -T 1 -s 4800 -U 2 -Q 65535 -E -1 -x 204 > stdlog +cli_res1=`grep "|does not support datagram|" clog` +cli_res2=`grep "\[dgram\]|send_datagram_multiple_error|" stdlog` +errlog=`grep_err_log` +if [ -n "$cli_res1" ] && [ -n "$cli_res2" ] && [ -z "$errlog" ]; then + echo ">>>>>>>> pass:1" + case_print_result "send_datagram_batch_while_peer_does_not_support" "pass" +else + echo ">>>>>>>> pass:0" + case_print_result "send_datagram_batch_while_peer_does_not_support" "fail" +fi + +killall test_server +./test_server -l d -Q 65535 -e -U 1 -s 1 > /dev/null & +sleep 1 +./test_client -l d -T 1 -s 1 -U 1 -Q 65535 -E -N > stdlog +clear_log +echo -e "send_0rtt_datagram_dgram1_lost...\c" +./test_client -l d -T 1 -s 4800 -U 1 -Q 65535 -E -x 205 -N > stdlog +cli_res1=`grep "\[dgram\]|dgram_lost|dgram_id:0|" stdlog` +errlog=`grep_err_log` +if [ -n "$cli_res1" ] && [ -z "$errlog" ]; then + echo ">>>>>>>> pass:1" + case_print_result "send_0rtt_datagram_dgram1_lost" "pass" +else + echo ">>>>>>>> pass:0" + case_print_result "send_0rtt_datagram_dgram1_lost" "fail" +fi + +clear_log +echo -e "send_1rtt_datagram_dgram1_lost...\c" +./test_client -l d -T 1 -s 4800 -U 1 -Q 65535 -E -x 205 -N -1 > stdlog +cli_res1=`grep "\[dgram\]|dgram_lost|dgram_id:0|" stdlog` +errlog=`grep_err_log` +if [ -n "$cli_res1" ] && [ -z "$errlog" ]; then + echo ">>>>>>>> pass:1" + case_print_result "send_1rtt_datagram_dgram1_lost" "pass" +else + echo ">>>>>>>> pass:0" + case_print_result "send_1rtt_datagram_dgram1_lost" "fail" +fi + +clear_log +echo -e "send_0rtt_datagram_reorder...\c" +./test_client -l d -T 1 -s 1800 -U 1 -Q 65535 -E -x 206 -N > stdlog +cli_res1=`grep "\[dgram\]|echo_check|same_content:yes|" stdlog` +errlog=`grep_err_log` +if [ -n "$cli_res1" ] && [ -z "$errlog" ]; then + echo ">>>>>>>> pass:1" + case_print_result "send_0rtt_datagram_reorder" "pass" +else + echo ">>>>>>>> pass:0" + case_print_result "send_0rtt_datagram_reorder" "fail" +fi + +clear_log +echo -e "send_1rtt_datagram_reorder...\c" +./test_client -l d -T 1 -s 1800 -U 1 -Q 65535 -E -x 206 -N -1 > stdlog +cli_res1=`grep "\[dgram\]|echo_check|same_content:yes|" stdlog` +errlog=`grep_err_log` +if [ -n "$cli_res1" ] && [ -z "$errlog" ]; then + echo ">>>>>>>> pass:1" + case_print_result "send_1rtt_datagram_reorder" "pass" +else + echo ">>>>>>>> pass:0" + case_print_result "send_1rtt_datagram_reorder" "fail" +fi + +clear_log +echo -e "datagram_lost_callback...\c" +./test_client -l d -T 1 -s 1000 -U 1 -Q 65535 -E -x 205 -N -1 > stdlog +cli_res1=`grep "\[dgram\]|dgram_lost|dgram_id:0|" stdlog` +errlog=`grep_err_log` +if [ -n "$cli_res1" ] && [ -z "$errlog" ]; then + echo ">>>>>>>> pass:1" + case_print_result "datagram_lost_callback" "pass" +else + echo ">>>>>>>> pass:0" + case_print_result "datagram_lost_callback" "fail" +fi + +clear_log +echo -e "datagram_acked_callback...\c" +./test_client -l d -T 1 -s 1000 -U 1 -Q 65535 -E -x 207 > stdlog +cli_res1=`grep "\[dgram\]|dgram_acked|dgram_id:0|" stdlog` +errlog=`grep_err_log` +if [ -n "$cli_res1" ] && [ -z "$errlog" ]; then + echo ">>>>>>>> pass:1" + case_print_result "datagram_acked_callback" "pass" +else + echo ">>>>>>>> pass:0" + case_print_result "datagram_acked_callback" "fail" +fi + +killall test_server +if [ -f test_session ]; then + rm -f test_session +fi +if [ -f tp_localhost ]; then + rm -f tp_localhost +fi +if [ -f xqc_token ]; then + rm -f xqc_token +fi +stdbuf -oL ./test_server -l d -Q 65535 -x 208 -e -U 1 > svr_stdlog & +sleep 1 + +clear_log +echo -e "1RTT_datagram_send_redundancy...\c" +./test_client -l d -T 1 -s 2000 -U 1 -Q 65535 -x 208 > stdlog +cli_res1=`grep "\[dgram\]|recv_dgram_bytes:8000" stdlog` +errlog=`grep_err_log` +if [ -n "$cli_res1" ] && [ -z "$errlog" ]; then + echo ">>>>>>>> pass:1" + case_print_result "1RTT_datagram_send_redundancy" "pass" +else + echo ">>>>>>>> pass:0" + case_print_result "1RTT_datagram_send_redundancy" "fail" +fi + +if [ -f test_session ]; then + rm -f test_session +fi + +clear_log +echo -e "1RTT_datagram_send_multiple_redundancy...\c" +./test_client -l d -T 1 -s 2000 -U 2 -Q 65535 -x 208 > stdlog +cli_res1=`grep "\[dgram\]|recv_dgram_bytes:8000" stdlog` +errlog=`grep_err_log` +if [ -n "$cli_res1" ] && [ -z "$errlog" ]; then + echo ">>>>>>>> pass:1" + case_print_result "1RTT_datagram_send_multiple_redundancy" "pass" +else + echo ">>>>>>>> pass:0" + case_print_result "1RTT_datagram_send_multiple_redundancy" "fail" +fi + + +clear_log +echo -e "0RTT_datagram_send_redundancy...\c" +./test_client -l d -T 1 -s 2000 -U 1 -Q 65535 -x 208 > stdlog +cli_res1=`grep "\[dgram\]|recv_dgram_bytes:8000" stdlog` +errlog=`grep_err_log` +if [ -n "$cli_res1" ] && [ -z "$errlog" ]; then + echo ">>>>>>>> pass:1" + case_print_result "0RTT_datagram_send_redundancy" "pass" +else + echo ">>>>>>>> pass:0" + case_print_result "0RTT_datagram_send_redundancy" "fail" +fi + +clear_log +echo -e "0RTT_datagram_send_multiple_redundancy...\c" +./test_client -l d -T 1 -s 2000 -U 2 -Q 65535 -x 208 > stdlog +cli_res1=`grep "\[dgram\]|recv_dgram_bytes:8000" stdlog` +errlog=`grep_err_log` +if [ -n "$cli_res1" ] && [ -z "$errlog" ]; then + echo ">>>>>>>> pass:1" + case_print_result "0RTT_datagram_send_multiple_redundancy" "pass" +else + echo ">>>>>>>> pass:0" + case_print_result "0RTT_datagram_send_multiple_redundancy" "fail" +fi + + +killall test_server +./test_server -l d -e -x 208 -Q 65535 -U 1 --dgram_qos 3 > /dev/null & +sleep 1 + +rm -rf tp_localhost test_session xqc_token +clear_log +echo -e "No reinjection for normal datagrams...\c" +sudo ./test_client -s 1024 -l d -t 1 -E -x 208 -Q 65535 -U 1 -T 1 --dgram_qos 3 > stdlog +result=`grep "\[dgram\]|recv_dgram_bytes:1024|sent_dgram_bytes:1024|" stdlog` +cli_res=`grep -E "xqc_conn_destroy.*mp_enable:0" clog` +errlog=`grep_err_log` +if [ -z "$errlog" ] && [ -n "$result" ] && [ -n "$cli_res" ] ; then + echo ">>>>>>>> pass:1" + case_print_result "No_reinjection_for_normal_datagrams" "pass" +else + echo ">>>>>>>> pass:0" + case_print_result "No_reinjection_for_normal_datagrams" "fail" +fi +grep_err_log + +rm -rf tp_localhost test_session xqc_token +clear_log +echo -e "No reinjection for normal h3-ext datagrams...\c" +sudo ./test_client -s 1024 -l d -t 1 -E -x 208 -Q 65535 -U 1 -T 2 --dgram_qos 3 > stdlog +result=`grep "\[h3-dgram\]|recv_dgram_bytes:1024|sent_dgram_bytes:1024|" stdlog` +cli_res=`grep -E "xqc_conn_destroy.*mp_enable:0" clog` +errlog=`grep_err_log` +if [ -z "$errlog" ] && [ -n "$result" ] && [ -n "$cli_res" ] ; then + echo ">>>>>>>> pass:1" + case_print_result "No_reinjection_for_normal_h3_ext_datagrams" "pass" +else + echo ">>>>>>>> pass:0" + case_print_result "No_reinjection_for_normal_h3_ext_datagrams" "fail" +fi +grep_err_log + + +# h3 ext datagram + +killall test_server +if [ -f test_session ]; then + rm -f test_session +fi +if [ -f tp_localhost ]; then + rm -f tp_localhost +fi +if [ -f xqc_token ]; then + rm -f xqc_token +fi +stdbuf -oL ./test_server -l d -Q 1000 -x 200 > svr_stdlog & +sleep 1 +clear_log +echo -e "h3_ext_datagram_get_mss(no_saved_transport_params)...\c" +./test_client -l d -T 2 -x 200 -Q 1000 -s 1 -U 1 > stdlog +cli_res1=`grep "\[h3-dgram-200\]|.*|initial_mss:0|" stdlog` +cli_res2=`grep "\[h3-dgram-200\]|.*|updated_mss:997|" stdlog` +svr_res=`grep -a "\[h3-dgram-200\]|.*|initial_mss:997|" svr_stdlog` +errlog=`grep_err_log` +if [ -n "$cli_res1" ] && [ -n "$cli_res2" ] && [ -n "$svr_res" ] && [ -z "$errlog" ]; then + echo ">>>>>>>> pass:1" + case_print_result "h3_ext_datagram_get_mss_no_saved_transport_params" "pass" +else + echo ">>>>>>>> pass:0" + case_print_result "h3_ext_datagram_get_mss_no_saved_transport_params" "fail" +fi + +> svr_stdlog +clear_log +echo -e "h3_ext_datagram_get_mss(saved_transport_params)...\c" +./test_client -l d -T 2 -x 200 -Q 1000 -s 1 -U 1 > stdlog +cli_res1=`grep "\[h3-dgram-200\]|.*|initial_mss:997|" stdlog` +cli_res2=`grep "\[h3-dgram-200\]|.*|updated_mss:997|" stdlog` +svr_res=`grep -a "\[h3-dgram-200\]|.*|initial_mss:997|" svr_stdlog` +errlog=`grep_err_log` +if [ -n "$cli_res1" ] && [ -n "$cli_res2" ] && [ -n "$svr_res" ] && [ -z "$errlog" ]; then + echo ">>>>>>>> pass:1" + case_print_result "h3_ext_datagram_get_mss_saved_transport_params" "pass" +else + echo ">>>>>>>> pass:0" + case_print_result "h3_ext_datagram_get_mss_saved_transport_params" "fail" +fi + +killall test_server +if [ -f test_session ]; then + rm -f test_session +fi +if [ -f tp_localhost ]; then + rm -f tp_localhost +fi +if [ -f xqc_token ]; then + rm -f xqc_token +fi +stdbuf -oL ./test_server -l d -Q 65535 -x 201 > svr_stdlog & +sleep 1 +clear_log +echo -e "h3_ext_datagram_mss_limited_by_MTU...\c" +./test_client -l d -T 2 -x 201 -Q 65535 -s 1 -U 1 > stdlog +cli_res1=`grep "\[h3-dgram-200\]|.*|initial_mss:0|" stdlog` +cli_res2=`grep "\[h3-dgram-200\]|.*|updated_mss:1200|" stdlog` +svr_res=`grep -a "\[h3-dgram-200\]|.*|initial_mss:1200|" svr_stdlog` +errlog=`grep_err_log` +if [ -n "$cli_res1" ] && [ -n "$cli_res2" ] && [ -n "$svr_res" ] && [ -z "$errlog" ]; then + echo ">>>>>>>> pass:1" + case_print_result "h3_ext_datagram_mss_limited_by_MTU" "pass" +else + echo ">>>>>>>> pass:0" + case_print_result "h3_ext_datagram_mss_limited_by_MTU" "fail" +fi + +killall test_server +if [ -f test_session ]; then + rm -f test_session +fi +if [ -f tp_localhost ]; then + rm -f tp_localhost +fi +if [ -f xqc_token ]; then + rm -f xqc_token +fi +stdbuf -oL ./test_server -l d -Q 1000 -x 200 > svr_stdlog & +sleep 1 +clear_log +echo -e "h3_ext_datagram_mss_limited_by_max_datagram_frame_size...\c" +./test_client -l d -T 2 -x 200 -s 1 -U 1 -Q 1000 > stdlog +cli_res1=`grep "\[h3-dgram-200\]|.*|initial_mss:0|" stdlog` +cli_res2=`grep "\[h3-dgram-200\]|.*|updated_mss:997|" stdlog` +svr_res=`grep -a "\[h3-dgram-200\]|.*|initial_mss:997|" svr_stdlog` +errlog=`grep_err_log` +if [ -n "$cli_res1" ] && [ -n "$cli_res2" ] && [ -n "$svr_res" ] && [ -z "$errlog" ]; then + echo ">>>>>>>> pass:1" + case_print_result "h3_ext_datagram_mss_limited_by_max_datagram_frame_size" "pass" +else + echo ">>>>>>>> pass:0" + case_print_result "h3_ext_datagram_mss_limited_by_max_datagram_frame_size" "fail" +fi +rm -f test_session tp_localhost xqc_token + +killall test_server + +./test_server -l e -Q 65535 -e -U 1 > /dev/null & +sleep 1 +clear_log +#generate 0rtt data +./test_client -l e -T 2 -s 1 -U 1 -Q 65535 > stdlog +clear_log +echo -e "send_0RTT_h3_ext_datagram_100KB...\c" +./test_client -l e -T 2 -s 102400 -U 1 -Q 65535 -E > stdlog +cli_res1=`grep "\[dgram\]|echo_check|same_content:yes|" stdlog` +errlog=`grep_err_log` +if [ -n "$cli_res1" ] && [ -z "$errlog" ]; then + echo ">>>>>>>> pass:1" + case_print_result "send_0RTT_h3_ext_datagram_100KB" "pass" +else + echo ">>>>>>>> pass:0" + case_print_result "send_0RTT_h3_ext_datagram_100KB" "fail" +fi + +if [ $LOCAL_TEST -ne 0 ]; then + clear_log + echo -e "send_0RTT_h3_ext_datagram_1MB...\c" + ./test_client -l e -T 2 -s 1048576 -U 1 -Q 65535 -E > stdlog + cli_res1=`grep "\[dgram\]|echo_check|same_content:yes|" stdlog` + errlog=`grep_err_log` + if [ -n "$cli_res1" ] && [ -z "$errlog" ]; then + echo ">>>>>>>> pass:1" + case_print_result "send_0RTT_h3_ext_datagram_1MB" "pass" + else + echo ">>>>>>>> pass:0" + case_print_result "send_0RTT_h3_ext_datagram_1MB" "fail" + fi + + clear_log + echo -e "send_0RTT_h3_ext_datagram_10MB...\c" + ./test_client -l e -T 2 -s 10485760 -U 1 -Q 65535 -E > stdlog + cli_res1=`grep "\[dgram\]|echo_check|same_content:yes|" stdlog` + errlog=`grep_err_log` + if [ -n "$cli_res1" ] && [ -z "$errlog" ]; then + echo ">>>>>>>> pass:1" + case_print_result "send_0RTT_h3_ext_datagram_10MB" "pass" + else + echo ">>>>>>>> pass:0" + case_print_result "send_0RTT_h3_ext_datagram_10MB" "fail" + fi + + clear_log + echo -e "send_0RTT_h3_ext_datagram_100MB...\c" + ./test_client -l e -T 2 -s 104857600 -U 1 -Q 65535 -E > stdlog + cli_res1=`grep "\[dgram\]|echo_check|same_content:yes|" stdlog` + errlog=`grep_err_log` + if [ -n "$cli_res1" ] && [ -z "$errlog" ]; then + echo ">>>>>>>> pass:1" + case_print_result "send_0RTT_h3_ext_datagram_100MB" "pass" + else + echo ">>>>>>>> pass:0" + case_print_result "send_0RTT_h3_ext_datagram_100MB" "fail" + fi + +fi +rm -f test_session tp_localhost xqc_token + + +killall test_server + +./test_server -l e -Q 65535 -e -U 2 > /dev/null & +sleep 1 +clear_log +#generate 0rtt data +./test_client -l e -T 2 -s 1 -U 2 -Q 65535 > stdlog +clear_log +echo -e "send_0RTT_h3_ext_datagram_100KB_batch...\c" +./test_client -l e -T 2 -s 102400 -U 2 -Q 65535 -E > stdlog +cli_res1=`grep "\[dgram\]|echo_check|same_content:yes|" stdlog` +errlog=`grep_err_log` +if [ -n "$cli_res1" ] && [ -z "$errlog" ]; then + echo ">>>>>>>> pass:1" + case_print_result "send_0RTT_h3_ext_datagram_100KB_batch" "pass" +else + echo ">>>>>>>> pass:0" + case_print_result "send_0RTT_h3_ext_datagram_100KB_batch" "fail" +fi + +if [ $LOCAL_TEST -ne 0 ]; then + clear_log + echo -e "send_0RTT_h3_ext_datagram_1MB_batch...\c" + ./test_client -l e -T 2 -s 1048576 -U 2 -Q 65535 -E > stdlog + cli_res1=`grep "\[dgram\]|echo_check|same_content:yes|" stdlog` + errlog=`grep_err_log` + if [ -n "$cli_res1" ] && [ -z "$errlog" ]; then + echo ">>>>>>>> pass:1" + case_print_result "send_0RTT_h3_ext_datagram_1MB_batch" "pass" + else + echo ">>>>>>>> pass:0" + case_print_result "send_0RTT_h3_ext_datagram_1MB_batch" "fail" + fi + + clear_log + echo -e "send_0RTT_h3_ext_datagram_10MB_batch...\c" + ./test_client -l e -T 2 -s 10485760 -U 2 -Q 65535 -E > stdlog + cli_res1=`grep "\[dgram\]|echo_check|same_content:yes|" stdlog` + errlog=`grep_err_log` + if [ -n "$cli_res1" ] && [ -z "$errlog" ]; then + echo ">>>>>>>> pass:1" + case_print_result "send_0RTT_h3_ext_datagram_10MB_batch" "pass" + else + echo ">>>>>>>> pass:0" + case_print_result "send_0RTT_h3_ext_datagram_10MB_batch" "fail" + fi + + clear_log + echo -e "send_0RTT_h3_ext_datagram_100MB_batch...\c" + ./test_client -l e -T 2 -s 104857600 -U 2 -Q 65535 -E > stdlog + cli_res1=`grep "\[dgram\]|echo_check|same_content:yes|" stdlog` + errlog=`grep_err_log` + if [ -n "$cli_res1" ] && [ -z "$errlog" ]; then + echo ">>>>>>>> pass:1" + case_print_result "send_0RTT_h3_ext_datagram_100MB_batch" "pass" + else + echo ">>>>>>>> pass:0" + case_print_result "send_0RTT_h3_ext_datagram_100MB_batch" "fail" + fi + +fi +rm -f test_session tp_localhost xqc_token + + +killall test_server +./test_server -l e -Q 65535 -e -U 1 > /dev/null & +sleep 1 +clear_log +echo -e "send_1RTT_h3_ext_datagram_100KB...\c" +./test_client -l e -T 2 -s 102400 -U 1 -Q 65535 -E -1 > stdlog +cli_res1=`grep "\[dgram\]|echo_check|same_content:yes|" stdlog` +errlog=`grep_err_log` +if [ -n "$cli_res1" ] && [ -z "$errlog" ]; then + echo ">>>>>>>> pass:1" + case_print_result "send_1RTT_h3_ext_datagram_100KB" "pass" +else + echo ">>>>>>>> pass:0" + case_print_result "send_1RTT_h3_ext_datagram_100KB" "fail" +fi + +if [ $LOCAL_TEST -ne 0 ]; then + clear_log + echo -e "send_1RTT_h3_ext_datagram_1MB...\c" + ./test_client -l e -T 2 -s 1048576 -U 1 -Q 65535 -E -1 > stdlog + cli_res1=`grep "\[dgram\]|echo_check|same_content:yes|" stdlog` + errlog=`grep_err_log` + if [ -n "$cli_res1" ] && [ -z "$errlog" ]; then + echo ">>>>>>>> pass:1" + case_print_result "send_1RTT_h3_ext_datagram_1MB" "pass" + else + echo ">>>>>>>> pass:0" + case_print_result "send_1RTT_h3_ext_datagram_1MB" "fail" + fi + + clear_log + echo -e "send_1RTT_h3_ext_datagram_10MB...\c" + ./test_client -l e -T 2 -s 10485760 -U 1 -Q 65535 -E -1 > stdlog + cli_res1=`grep "\[dgram\]|echo_check|same_content:yes|" stdlog` + errlog=`grep_err_log` + if [ -n "$cli_res1" ] && [ -z "$errlog" ]; then + echo ">>>>>>>> pass:1" + case_print_result "send_1RTT_h3_ext_datagram_10MB" "pass" + else + echo ">>>>>>>> pass:0" + case_print_result "send_1RTT_h3_ext_datagram_10MB" "fail" + fi + + clear_log + echo -e "send_1RTT_h3_ext_datagram_100MB...\c" + ./test_client -l e -T 2 -s 104857600 -U 1 -Q 65535 -E -1 > stdlog + cli_res1=`grep "\[dgram\]|echo_check|same_content:yes|" stdlog` + errlog=`grep_err_log` + if [ -n "$cli_res1" ] && [ -z "$errlog" ]; then + echo ">>>>>>>> pass:1" + case_print_result "send_1RTT_h3_ext_datagram_100MB" "pass" + else + echo ">>>>>>>> pass:0" + case_print_result "send_1RTT_h3_ext_datagram_100MB" "fail" + fi +fi +rm -f test_session tp_localhost xqc_token + + +killall test_server +./test_server -l e -Q 65535 -e -U 2 > /dev/null & +sleep 1 +clear_log +echo -e "send_1RTT_h3_ext_datagram_100KB_batch...\c" +./test_client -l e -T 2 -s 102400 -U 2 -Q 65535 -E -1 > stdlog +cli_res1=`grep "\[dgram\]|echo_check|same_content:yes|" stdlog` +errlog=`grep_err_log` +if [ -n "$cli_res1" ] && [ -z "$errlog" ]; then + echo ">>>>>>>> pass:1" + case_print_result "send_1RTT_h3_ext_datagram_100KB_batch" "pass" +else + echo ">>>>>>>> pass:0" + case_print_result "send_1RTT_h3_ext_datagram_100KB_batch" "fail" +fi + +if [ $LOCAL_TEST -ne 0 ]; then + clear_log + echo -e "send_1RTT_h3_ext_datagram_1MB_batch...\c" + ./test_client -l e -T 2 -s 1048576 -U 2 -Q 65535 -E -1 > stdlog + cli_res1=`grep "\[dgram\]|echo_check|same_content:yes|" stdlog` + errlog=`grep_err_log` + if [ -n "$cli_res1" ] && [ -z "$errlog" ]; then + echo ">>>>>>>> pass:1" + case_print_result "send_1RTT_h3_ext_datagram_1MB_batch" "pass" + else + echo ">>>>>>>> pass:0" + case_print_result "send_1RTT_h3_ext_datagram_1MB_batch" "fail" + fi + + clear_log + echo -e "send_1RTT_h3_ext_datagram_10MB_batch...\c" + ./test_client -l e -T 2 -s 10485760 -U 2 -Q 65535 -E -1 > stdlog + cli_res1=`grep "\[dgram\]|echo_check|same_content:yes|" stdlog` + errlog=`grep_err_log` + if [ -n "$cli_res1" ] && [ -z "$errlog" ]; then + echo ">>>>>>>> pass:1" + case_print_result "send_1RTT_h3_ext_datagram_10MB_batch" "pass" + else + echo ">>>>>>>> pass:0" + case_print_result "send_1RTT_h3_ext_datagram_10MB_batch" "fail" + fi + + clear_log + echo -e "send_1RTT_h3_ext_datagram_100MB_batch...\c" + ./test_client -l e -T 2 -s 104857600 -U 2 -Q 65535 -E -1 > stdlog + cli_res1=`grep "\[dgram\]|echo_check|same_content:yes|" stdlog` + errlog=`grep_err_log` + if [ -n "$cli_res1" ] && [ -z "$errlog" ]; then + echo ">>>>>>>> pass:1" + case_print_result "send_1RTT_h3_ext_datagram_100MB_batch" "pass" + else + echo ">>>>>>>> pass:0" + case_print_result "send_1RTT_h3_ext_datagram_100MB_batch" "fail" + fi +fi +rm -f test_session tp_localhost xqc_token + +killall test_server +./test_server -l d -Q 65535 -e -U 1 -s 1 > /dev/null & +sleep 1 +clear_log +echo -e "h3_ext_dgram_send_queue_full...\c" +./test_client -l d -T 2 -s 40000000 -U 1 -Q 65535 -1 > stdlog +cli_res1=`grep "\[h3-dgram\]|retry_datagram_send_later|" stdlog` +cli_res2=`grep "|too many packets used|ctl_packets_used:" clog` +cli_res3=`grep "\[h3-dgram\]|dgram_write|" stdlog` +errlog=`grep_err_log` +if [ -n "$cli_res1" ] && [ -n "$cli_res2" ] && [ -n "$cli_res3" ] && [ -z "$errlog" ]; then + echo ">>>>>>>> pass:1" + case_print_result "h3_ext_dgram_send_queue_full" "pass" +else + echo ">>>>>>>> pass:0" + case_print_result "h3_ext_dgram_send_queue_full" "fail" +fi + +clear_log +echo -e "h3_ext_dgram_send_queue_full_batch...\c" +./test_client -l d -T 2 -s 40000000 -U 2 -Q 65535 -1 > stdlog +cli_res1=`grep "\[h3-dgram\]|retry_datagram_send_multiple_later|" stdlog` +cli_res2=`grep "|too many packets used|ctl_packets_used:" clog` +cli_res3=`grep "\[h3-dgram\]|dgram_write|" stdlog` +errlog=`grep_err_log` +if [ -n "$cli_res1" ] && [ -n "$cli_res2" ] && [ -n "$cli_res3" ] && [ -z "$errlog" ]; then + echo ">>>>>>>> pass:1" + case_print_result "h3_ext_dgram_send_queue_full_batch" "pass" +else + echo ">>>>>>>> pass:0" + case_print_result "h3_ext_dgram_send_queue_full_batch" "fail" +fi + +clear_log +echo -e "send_0rtt_h3_ext_datagram_without_saved_datagram_tp...\c" +./test_client -l d -T 2 -s 999 -U 1 -Q 65535 -1 -E -x 202 > stdlog +cli_res1=`grep "\[h3-dgram\]|retry_datagram_send_later|" stdlog` +cli_res2=`grep "|waiting_for_max_datagram_frame_size_from_peer|" clog` +cli_res3=`grep "\[dgram\]|echo_check|same_content:yes|" stdlog` +cli_res4=`grep "\[h3-dgram\]|dgram_write|" stdlog` +errlog=`grep_err_log` +if [ -n "$cli_res1" ] && [ -n "$cli_res2" ] && [ -n "$cli_res3" ] && [ -n "$cli_res4" ] && [ -z "$errlog" ]; then + echo ">>>>>>>> pass:1" + case_print_result "send_0rtt_h3_ext_datagram_without_saved_datagram_tp" "pass" +else + echo ">>>>>>>> pass:0" + case_print_result "send_0rtt_h3_ext_datagram_without_saved_datagram_tp" "fail" +fi + +clear_log +echo -e "send_0rtt_h3_ext_datagram_without_saved_datagram_tp_batch...\c" +./test_client -l d -T 2 -s 999 -U 2 -Q 65535 -1 -E -x 202 > stdlog +cli_res1=`grep "\[h3-dgram\]|retry_datagram_send_multiple_later|" stdlog` +cli_res2=`grep "|waiting_for_max_datagram_frame_size_from_peer|" clog` +cli_res3=`grep "\[dgram\]|echo_check|same_content:yes|" stdlog` +cli_res4=`grep "\[h3-dgram\]|dgram_write|" stdlog` +errlog=`grep_err_log` +if [ -n "$cli_res1" ] && [ -n "$cli_res2" ] && [ -n "$cli_res3" ] && [ -n "$cli_res4" ] && [ -z "$errlog" ]; then + echo ">>>>>>>> pass:1" + case_print_result "send_0rtt_h3_ext_datagram_without_saved_datagram_tp_batch" "pass" +else + echo ">>>>>>>> pass:0" + case_print_result "send_0rtt_h3_ext_datagram_without_saved_datagram_tp_batch" "fail" +fi + + +clear_log +echo -e "send_too_many_0rtt_h3_ext_datagrams...\c" +./test_client -l d -T 2 -s 40000 -U 1 -Q 65535 -E > stdlog +cli_res1=`grep "\[h3-dgram\]|retry_datagram_send_later|" stdlog` +cli_res2=`grep "|too many 0rtt packets|" clog` +cli_res3=`grep "\[dgram\]|echo_check|same_content:yes|" stdlog` +cli_res4=`grep "\[h3-dgram\]|dgram_write|" stdlog` +errlog=`grep_err_log` +if [ -n "$cli_res1" ] && [ -n "$cli_res2" ] && [ -n "$cli_res3" ] && [ -n "$cli_res4" ] && [ -z "$errlog" ]; then + echo ">>>>>>>> pass:1" + case_print_result "send_too_many_0rtt_h3_ext_datagrams" "pass" +else + echo ">>>>>>>> pass:0" + case_print_result "send_too_many_0rtt_h3_ext_datagrams" "fail" +fi + +clear_log +echo -e "send_too_many_0rtt_h3_ext_datagrams_batch...\c" +./test_client -l d -T 2 -s 40000 -U 2 -Q 65535 -E > stdlog +cli_res1=`grep "\[h3-dgram\]|retry_datagram_send_multiple_later|" stdlog` +cli_res2=`grep "|too many 0rtt packets|" clog` +cli_res3=`grep "\[dgram\]|echo_check|same_content:yes|" stdlog` +cli_res4=`grep "\[h3-dgram\]|dgram_write|" stdlog` +errlog=`grep_err_log` +if [ -n "$cli_res1" ] && [ -n "$cli_res2" ] && [ -n "$cli_res3" ] && [ -n "$cli_res4" ] && [ -z "$errlog" ]; then + echo ">>>>>>>> pass:1" + case_print_result "send_too_many_0rtt_h3_ext_datagrams_batch" "pass" +else + echo ">>>>>>>> pass:0" + case_print_result "send_too_many_0rtt_h3_ext_datagrams_batch" "fail" +fi + +killall test_server +./test_server -l d -Q 65535 -e -U 1 -s 1 > /dev/null & +sleep 1 +clear_log +echo -e "send_0rtt_h3_ext_datagram_reject...\c" +./test_client -l d -T 2 -s 4800 -U 1 -Q 65535 -E > stdlog +cli_res1=`grep "xqc_conn_early_data_reject" clog` +cli_res2=`grep "\[dgram\]|echo_check|same_content:yes|" stdlog` +errlog=`grep_err_log` +if [ -n "$cli_res1" ] && [ -n "$cli_res2" ] && [ -z "$errlog" ]; then + echo ">>>>>>>> pass:1" + case_print_result "send_0rtt_h3_ext_datagram_reject" "pass" +else + echo ">>>>>>>> pass:0" + case_print_result "send_0rtt_h3_ext_datagram_reject" "fail" +fi + + +killall test_server +./test_server -l d -Q 1000 -e -U 1 -s 1 > /dev/null & +sleep 1 +clear_log +echo -e "send_oversized_h3_ext_datagram...\c" +./test_client -l d -T 2 -s 4800 -U 1 -Q 65535 -E -1 -x 203 > stdlog +cli_res1=`grep "datagram_is_too_large" clog` +cli_res2=`grep "trying_to_send_an_oversized_datagram" stdlog` +#errlog=`grep_err_log` +if [ -n "$cli_res1" ] && [ -n "$cli_res2" ]; then + echo ">>>>>>>> pass:1" + case_print_result "send_oversized_h3_ext_datagram" "pass" +else + echo ">>>>>>>> pass:0" + case_print_result "send_oversized_h3_ext_datagram" "fail" +fi + +clear_log +echo -e "send_oversized_h3_ext_datagram_batch...\c" +./test_client -l d -T 2 -s 4800 -U 2 -Q 65535 -E -1 -x 203 > stdlog +cli_res1=`grep "datagram_is_too_large" clog` +cli_res2=`grep "trying_to_send_an_oversized_datagram" stdlog` +cli_res3=`grep "|partially_sent_pkts_in_a_batch|cnt:1|" stdlog` +#errlog=`grep_err_log` +if [ -n "$cli_res1" ] && [ -n "$cli_res2" ]; then + echo ">>>>>>>> pass:1" + case_print_result "send_oversized_h3_ext_datagram_batch" "pass" +else + echo ">>>>>>>> pass:0" + case_print_result "send_oversized_h3_ext_datagram_batch" "fail" +fi +rm -rf tp_localhost test_session xqc_token + +killall test_server +./test_server -l d -Q 0 -e -U 1 -s 1 > /dev/null & +sleep 1 +clear_log +echo -e "send_h3_ext_datagram_while_peer_does_not_support...\c" +./test_client -l d -T 2 -s 4800 -U 1 -Q 65535 -E -1 -x 204 > stdlog +cli_res1=`grep "|does not support datagram|" clog` +cli_res2=`grep "\[h3-dgram\]|send_datagram_error|" stdlog` +#errlog=`grep_err_log` +if [ -n "$cli_res1" ] && [ -n "$cli_res2" ]; then + echo ">>>>>>>> pass:1" + case_print_result "send_h3_ext_datagram_while_peer_does_not_support" "pass" +else + echo ">>>>>>>> pass:0" + case_print_result "send_h3_ext_datagram_while_peer_does_not_support" "fail" +fi + +clear_log +echo -e "send_h3_ext_datagram_batch_while_peer_does_not_support...\c" +./test_client -l d -T 2 -s 4800 -U 2 -Q 65535 -E -1 -x 204 > stdlog +cli_res1=`grep "|does not support datagram|" clog` +cli_res2=`grep "\[h3-dgram\]|send_datagram_multiple_error|" stdlog` +#errlog=`grep_err_log` +if [ -n "$cli_res1" ] && [ -n "$cli_res2" ]; then + echo ">>>>>>>> pass:1" + case_print_result "send_h3_ext_datagram_batch_while_peer_does_not_support" "pass" +else + echo ">>>>>>>> pass:0" + case_print_result "send_h3_ext_datagram_batch_while_peer_does_not_support" "fail" +fi + +killall test_server +./test_server -l d -Q 65535 -e -U 1 -s 1 > /dev/null & +sleep 1 +./test_client -l d -T 2 -s 1 -U 1 -Q 65535 -E -N > stdlog +clear_log +echo -e "send_0rtt_h3_ext_datagram_dgram1_lost...\c" +./test_client -l d -T 2 -s 4800 -U 1 -Q 65535 -E -x 205 -N > stdlog +cli_res1=`grep "\[h3-dgram\]|dgram_lost|dgram_id:0|" stdlog` +errlog=`grep_err_log` +if [ -n "$cli_res1" ] && [ -z "$errlog" ]; then + echo ">>>>>>>> pass:1" + case_print_result "send_0rtt_h3_ext_datagram_dgram1_lost" "pass" +else + echo ">>>>>>>> pass:0" + case_print_result "send_0rtt_h3_ext_datagram_dgram1_lost" "fail" +fi + +clear_log +echo -e "send_1rtt_h3_ext_datagram_dgram1_lost...\c" +./test_client -l d -T 2 -s 4800 -U 1 -Q 65535 -E -x 205 -N -1 > stdlog +cli_res1=`grep "\[h3-dgram\]|dgram_lost|dgram_id:0|" stdlog` +errlog=`grep_err_log` +if [ -n "$cli_res1" ] && [ -z "$errlog" ]; then + echo ">>>>>>>> pass:1" + case_print_result "send_1rtt_h3_ext_datagram_dgram1_lost" "pass" +else + echo ">>>>>>>> pass:0" + case_print_result "send_1rtt_h3_ext_datagram_dgram1_lost" "fail" +fi + +clear_log +echo -e "send_0rtt_h3_ext_datagram_reorder...\c" +./test_client -l d -T 2 -s 1800 -U 1 -Q 65535 -E -x 206 -N > stdlog +cli_res1=`grep "\[dgram\]|echo_check|same_content:yes|" stdlog` +errlog=`grep_err_log` +if [ -n "$cli_res1" ] && [ -z "$errlog" ]; then + echo ">>>>>>>> pass:1" + case_print_result "send_0rtt_h3_ext_datagram_reorder" "pass" +else + echo ">>>>>>>> pass:0" + case_print_result "send_0rtt_h3_ext_datagram_reorder" "fail" +fi + +clear_log +echo -e "send_1rtt_h3_ext_datagram_reorder...\c" +./test_client -l d -T 2 -s 1800 -U 1 -Q 65535 -E -x 206 -N -1 > stdlog +cli_res1=`grep "\[dgram\]|echo_check|same_content:yes|" stdlog` +errlog=`grep_err_log` +if [ -n "$cli_res1" ] && [ -z "$errlog" ]; then + echo ">>>>>>>> pass:1" + case_print_result "send_1rtt_h3_ext_datagram_reorder" "pass" +else + echo ">>>>>>>> pass:0" + case_print_result "send_1rtt_h3_ext_datagram_reorder" "fail" +fi + +clear_log +echo -e "h3_ext_datagram_lost_callback...\c" +./test_client -l d -T 2 -s 1000 -U 1 -Q 65535 -E -x 205 -N -1 > stdlog +cli_res1=`grep "\[h3-dgram\]|dgram_lost|dgram_id:0|" stdlog` +errlog=`grep_err_log` +if [ -n "$cli_res1" ] && [ -z "$errlog" ]; then + echo ">>>>>>>> pass:1" + case_print_result "h3_ext_datagram_lost_callback" "pass" +else + echo ">>>>>>>> pass:0" + case_print_result "h3_ext_datagram_lost_callback" "fail" +fi + +clear_log +echo -e "h3_ext_datagram_acked_callback...\c" +./test_client -l d -T 2 -s 1000 -U 1 -Q 65535 -E -x 207 > stdlog +cli_res1=`grep "\[h3-dgram\]|dgram_acked|dgram_id:0|" stdlog` +errlog=`grep_err_log` +if [ -n "$cli_res1" ] && [ -z "$errlog" ]; then + echo ">>>>>>>> pass:1" + case_print_result "h3_ext_datagram_acked_callback" "pass" +else + echo ">>>>>>>> pass:0" + case_print_result "h3_ext_datagram_acked_callback" "fail" +fi + +rm -f test_session tp_localhost xqc_token + +killall test_server + +./test_server -l e -Q 65535 -e -U 1 -x 208 > /dev/null & +sleep 1 + +clear_log +echo -e "1RTT_h3_ext_datagram_send_redundancy...\c" +./test_client -l d -T 2 -s 2000 -U 1 -Q 65535 -x 208 > stdlog +cli_res1=`grep "\[h3-dgram\]|recv_dgram_bytes:8000" stdlog` +errlog=`grep_err_log` +if [ -n "$cli_res1" ] && [ -z "$errlog" ]; then + echo ">>>>>>>> pass:1" + case_print_result "1RTT_h3_ext_datagram_send_redundancy" "pass" +else + echo ">>>>>>>> pass:0" + case_print_result "1RTT_h3_ext_datagram_send_redundancy" "fail" +fi + +rm -f test_session tp_localhost xqc_token + +clear_log +echo -e "1RTT_h3_ext_datagram_send_multiple_redundancy...\c" +./test_client -l d -T 2 -s 2000 -U 2 -Q 65535 -x 208 > stdlog +cli_res1=`grep "\[h3-dgram\]|recv_dgram_bytes:8000" stdlog` +errlog=`grep_err_log` +if [ -n "$cli_res1" ] && [ -z "$errlog" ]; then + echo ">>>>>>>> pass:1" + case_print_result "1RTT_h3_ext_datagram_send_multiple_redundancy" "pass" +else + echo ">>>>>>>> pass:0" + case_print_result "1RTT_h3_ext_datagram_send_multiple_redundancy" "fail" +fi + +clear_log +echo -e "0RTT_h3_ext_datagram_send_redundancy...\c" +./test_client -l d -T 2 -s 2000 -U 1 -Q 65535 -x 208 > stdlog +cli_res1=`grep "\[h3-dgram\]|recv_dgram_bytes:8000" stdlog` +errlog=`grep_err_log` +if [ -n "$cli_res1" ] && [ -z "$errlog" ]; then + echo ">>>>>>>> pass:1" + case_print_result "0RTT_h3_ext_datagram_send_redundancy" "pass" +else + echo ">>>>>>>> pass:0" + case_print_result "0RTT_h3_ext_datagram_send_redundancy" "fail" +fi + +clear_log +echo -e "0RTT_h3_ext_datagram_send_multiple_redundancy...\c" +./test_client -l d -T 2 -s 2000 -U 2 -Q 65535 -x 208 > stdlog +cli_res1=`grep "\[h3-dgram\]|recv_dgram_bytes:8000" stdlog` +errlog=`grep_err_log` +if [ -n "$cli_res1" ] && [ -z "$errlog" ]; then + echo ">>>>>>>> pass:1" + case_print_result "0RTT_h3_ext_datagram_send_multiple_redundancy" "pass" +else + echo ">>>>>>>> pass:0" + case_print_result "0RTT_h3_ext_datagram_send_multiple_redundancy" "fail" +fi + + +# send h3 request / bytestream / datagram in one h3_conn (-x 300) + +rm -f test_session tp_localhost xqc_token + +killall test_server + +./test_server -l e -Q 65535 -e -U 1 > /dev/null & +sleep 1 + + +## 1RTT +clear_log +echo -e "h3_ext_1RTT_send_test...\c" +./test_client -l e -T 2 -s 102400 -U 1 -Q 65535 -E -x 300 -1 > stdlog +cli_res1=`grep ">>>>>>>> pass:1" stdlog` +cli_res2=`grep "\[dgram\]|echo_check|same_content:yes|" stdlog` +cli_res3=`grep "\[h3-dgram\]|recv_dgram_bytes:102400|sent_dgram_bytes:102400|lost_dgram_bytes:0|lost_cnt:0|" stdlog` +cli_res4=`grep "\[bytestream\]|bytes_sent:102400|bytes_rcvd:102400|recv_fin:1|" stdlog` +cli_res5=`grep "\[bytestream\]|same_content:yes|" stdlog` +cli_res6=`grep "early_data_flag:0" stdlog` +errlog=`grep_err_log` +if [ -n "$cli_res1" ] && [ -n "$cli_res2" ] && [ -n "$cli_res3" ] && [ -n "$cli_res4" ] && [ -n "$cli_res5" ] && [ -n "$cli_res6" ] && [ -z "$errlog" ]; then + echo ">>>>>>>> pass:1" + case_print_result "h3_ext_1RTT_send_test" "pass" +else + echo ">>>>>>>> pass:0" + case_print_result "h3_ext_1RTT_send_test" "fail" +fi + +## 0RTT +clear_log +echo -e "h3_ext_0RTT_accept_send_test...\c" +./test_client -l e -T 2 -s 102400 -U 1 -Q 65535 -E -x 300 > stdlog +cli_res1=`grep ">>>>>>>> pass:1" stdlog` +cli_res2=`grep "\[dgram\]|echo_check|same_content:yes|" stdlog` +cli_res3=`grep "\[h3-dgram\]|recv_dgram_bytes:102400|sent_dgram_bytes:102400|lost_dgram_bytes:0|lost_cnt:0|" stdlog` +cli_res4=`grep "\[bytestream\]|bytes_sent:102400|bytes_rcvd:102400|recv_fin:1|" stdlog` +cli_res5=`grep "\[bytestream\]|same_content:yes|" stdlog` +cli_res6=`grep "early_data_flag:1" stdlog` +errlog=`grep_err_log` +if [ -n "$cli_res1" ] && [ -n "$cli_res2" ] && [ -n "$cli_res3" ] && [ -n "$cli_res4" ] && [ -n "$cli_res5" ] && [ -n "$cli_res6" ] && [ -z "$errlog" ]; then + echo ">>>>>>>> pass:1" + case_print_result "h3_ext_0RTT_accept_send_test" "pass" +else + echo ">>>>>>>> pass:0" + case_print_result "h3_ext_0RTT_accept_send_test" "fail" +fi + +## 0RTT reject + +killall test_server + +./test_server -l e -Q 65535 -e -U 1 > /dev/null & +sleep 1 + +clear_log +echo -e "h3_ext_0RTT_reject_send_test...\c" +./test_client -l e -T 2 -s 102400 -U 1 -Q 65535 -E -x 300 > stdlog +cli_res1=`grep ">>>>>>>> pass:1" stdlog` +cli_res2=`grep "\[dgram\]|echo_check|same_content:yes|" stdlog` +cli_res3=`grep "\[h3-dgram\]|recv_dgram_bytes:102400|sent_dgram_bytes:102400|lost_dgram_bytes:0|lost_cnt:0|" stdlog` +cli_res4=`grep "\[bytestream\]|bytes_sent:102400|bytes_rcvd:102400|recv_fin:1|" stdlog` +cli_res5=`grep "\[bytestream\]|same_content:yes|" stdlog` +cli_res6=`grep "early_data_flag:2" stdlog` +errlog=`grep_err_log` +if [ -n "$cli_res1" ] && [ -n "$cli_res2" ] && [ -n "$cli_res3" ] && [ -n "$cli_res4" ] && [ -n "$cli_res5" ] && [ -n "$cli_res6" ] && [ -z "$errlog" ]; then + echo ">>>>>>>> pass:1" + case_print_result "h3_ext_0RTT_reject_send_test" "pass" +else + echo ">>>>>>>> pass:0" + case_print_result "h3_ext_0RTT_reject_send_test" "fail" +fi + +# send concurrent h3 req / open concurrent bytestreams / send datagrams in one h3_conn (-x 301) + +## 1RTT +clear_log +echo -e "h3_ext_1RTT_concurrent_send_test...\c" +./test_client -l e -T 2 -s 102400 -U 1 -Q 65535 -E -x 301 -P 2 -1 > stdlog +cli_res1=`grep ">>>>>>>> pass:1" stdlog | wc -l` +cli_res2=`grep "\[dgram\]|echo_check|same_content:yes|" stdlog` +cli_res3=`grep "\[h3-dgram\]|recv_dgram_bytes:102400|sent_dgram_bytes:102400|lost_dgram_bytes:0|lost_cnt:0|" stdlog` +cli_res4=`grep "\[bytestream\]|bytes_sent:102400|bytes_rcvd:102400|recv_fin:1|" stdlog` +cli_res5=`grep "\[bytestream\]|same_content:yes|" stdlog | wc -l` +cli_res6=`grep "early_data_flag:0" stdlog` + +errlog=`grep_err_log` +if [ "$cli_res1" == "2" ] && [ -n "$cli_res2" ] && [ -n "$cli_res3" ] && [ -n "$cli_res4" ] && [ "$cli_res5" == "2" ] && [ -n "$cli_res6" ] && [ -z "$errlog" ]; then + echo ">>>>>>>> pass:1" + case_print_result "h3_ext_1RTT_concurrent_send_test" "pass" +else + echo ">>>>>>>> pass:0" + case_print_result "h3_ext_1RTT_concurrent_send_test" "fail" +fi + +## 0RTT +clear_log +echo -e "h3_ext_0RTT_accept_concurrent_send_test...\c" +./test_client -l e -T 2 -s 102400 -U 1 -Q 65535 -E -x 301 -P 2 > stdlog +cli_res1=`grep ">>>>>>>> pass:1" stdlog | wc -l` +cli_res2=`grep "\[dgram\]|echo_check|same_content:yes|" stdlog` +cli_res3=`grep "\[h3-dgram\]|recv_dgram_bytes:102400|sent_dgram_bytes:102400|lost_dgram_bytes:0|lost_cnt:0|" stdlog` +cli_res4=`grep "\[bytestream\]|bytes_sent:102400|bytes_rcvd:102400|recv_fin:1|" stdlog` +cli_res5=`grep "\[bytestream\]|same_content:yes|" stdlog | wc -l` +cli_res6=`grep "early_data_flag:1" stdlog` + +errlog=`grep_err_log` +if [ "$cli_res1" == "2" ] && [ -n "$cli_res2" ] && [ -n "$cli_res3" ] && [ -n "$cli_res4" ] && [ "$cli_res5" == "2" ] && [ -n "$cli_res6" ] && [ -z "$errlog" ]; then + echo ">>>>>>>> pass:1" + case_print_result "h3_ext_0RTT_accept_concurrent_send_test" "pass" +else + echo ">>>>>>>> pass:0" + case_print_result "h3_ext_0RTT_accept_concurrent_send_test" "fail" +fi + +## 0RTT reject + +killall test_server + +./test_server -l e -Q 65535 -e -U 1 > /dev/null & +sleep 1 + +clear_log +echo -e "h3_ext_0RTT_reject_concurrent_send_test...\c" +./test_client -l e -T 2 -s 102400 -U 1 -Q 65535 -E -x 301 -P 2 > stdlog +cli_res1=`grep ">>>>>>>> pass:1" stdlog | wc -l` +cli_res2=`grep "\[dgram\]|echo_check|same_content:yes|" stdlog` +cli_res3=`grep "\[h3-dgram\]|recv_dgram_bytes:102400|sent_dgram_bytes:102400|lost_dgram_bytes:0|lost_cnt:0|" stdlog` +cli_res4=`grep "\[bytestream\]|bytes_sent:102400|bytes_rcvd:102400|recv_fin:1|" stdlog` +cli_res5=`grep "\[bytestream\]|same_content:yes|" stdlog | wc -l` +cli_res6=`grep "early_data_flag:2" stdlog` + +errlog=`grep_err_log` +if [ "$cli_res1" == "2" ] && [ -n "$cli_res2" ] && [ -n "$cli_res3" ] && [ -n "$cli_res4" ] && [ "$cli_res5" == "2" ] && [ -n "$cli_res6" ] && [ -z "$errlog" ]; then + echo ">>>>>>>> pass:1" + case_print_result "h3_ext_0RTT_reject_concurrent_send_test" "pass" +else + echo ">>>>>>>> pass:0" + case_print_result "h3_ext_0RTT_reject_concurrent_send_test" "fail" +fi + + +# send bytestream with pure fin (-x 302 -x 303) + +## 1RTT + +clear_log +echo -e "h3_ext_1RTT_send_pure_fin1...\c" +./test_client -l e -T 2 -s 102400 -U 1 -Q 65535 -E -x 302 -1 > stdlog +cli_res1=`grep ">>>>>>>> pass:1" stdlog | wc -l` +cli_res2=`grep "\[dgram\]|echo_check|same_content:yes|" stdlog` +cli_res3=`grep "\[h3-dgram\]|recv_dgram_bytes:102400|sent_dgram_bytes:102400|lost_dgram_bytes:0|lost_cnt:0|" stdlog` +cli_res4=`grep "\[bytestream\]|bytes_sent:0|bytes_rcvd:0|recv_fin:1|" stdlog` +cli_res5=`grep "\[bytestream\]|same_content:no|" stdlog | wc -l` +cli_res6=`grep "early_data_flag:0" stdlog` + +errlog=`grep_err_log` +if [ "$cli_res1" == "1" ] && [ -n "$cli_res2" ] && [ -n "$cli_res3" ] && [ -n "$cli_res4" ] && [ "$cli_res5" == "1" ] && [ -n "$cli_res6" ] && [ -z "$errlog" ]; then + echo ">>>>>>>> pass:1" + case_print_result "h3_ext_1RTT_send_pure_fin1" "pass" +else + echo ">>>>>>>> pass:0" + case_print_result "h3_ext_1RTT_send_pure_fin1" "fail" +fi + +clear_log +echo -e "h3_ext_1RTT_send_pure_fin2...\c" +./test_client -l e -T 2 -s 102400 -U 1 -Q 65535 -E -x 303 -1 > stdlog +cli_res1=`grep ">>>>>>>> pass:1" stdlog | wc -l` +cli_res2=`grep "\[dgram\]|echo_check|same_content:yes|" stdlog` +cli_res3=`grep "\[h3-dgram\]|recv_dgram_bytes:102400|sent_dgram_bytes:102400|lost_dgram_bytes:0|lost_cnt:0|" stdlog` +cli_res4=`grep "\[bytestream\]|bytes_sent:0|bytes_rcvd:0|recv_fin:1|" stdlog` +cli_res5=`grep "\[bytestream\]|same_content:no|" stdlog | wc -l` +cli_res6=`grep "early_data_flag:0" stdlog` + +errlog=`grep_err_log` +if [ "$cli_res1" == "1" ] && [ -n "$cli_res2" ] && [ -n "$cli_res3" ] && [ -n "$cli_res4" ] && [ "$cli_res5" == "1" ] && [ -n "$cli_res6" ] && [ -z "$errlog" ]; then + echo ">>>>>>>> pass:1" + case_print_result "h3_ext_1RTT_send_pure_fin2" "pass" +else + echo ">>>>>>>> pass:0" + case_print_result "h3_ext_1RTT_send_pure_fin2" "fail" +fi + +## 0RTT + +clear_log +echo -e "h3_ext_0RTT_accept_send_pure_fin1...\c" +./test_client -l e -T 2 -s 102400 -U 1 -Q 65535 -E -x 302 > stdlog +cli_res1=`grep ">>>>>>>> pass:1" stdlog | wc -l` +cli_res2=`grep "\[dgram\]|echo_check|same_content:yes|" stdlog` +cli_res3=`grep "\[h3-dgram\]|recv_dgram_bytes:102400|sent_dgram_bytes:102400|lost_dgram_bytes:0|lost_cnt:0|" stdlog` +cli_res4=`grep "\[bytestream\]|bytes_sent:0|bytes_rcvd:0|recv_fin:1|" stdlog` +cli_res5=`grep "\[bytestream\]|same_content:no|" stdlog | wc -l` +cli_res6=`grep "early_data_flag:1" stdlog` + +errlog=`grep_err_log` +if [ "$cli_res1" == "1" ] && [ -n "$cli_res2" ] && [ -n "$cli_res3" ] && [ -n "$cli_res4" ] && [ "$cli_res5" == "1" ] && [ -n "$cli_res6" ] && [ -z "$errlog" ]; then + echo ">>>>>>>> pass:1" + case_print_result "h3_ext_0RTT_accept_send_pure_fin1" "pass" +else + echo ">>>>>>>> pass:0" + case_print_result "h3_ext_0RTT_accept_send_pure_fin1" "fail" +fi + +clear_log +echo -e "h3_ext_0RTT_accept_send_pure_fin2...\c" +./test_client -l e -T 2 -s 102400 -U 1 -Q 65535 -E -x 303 > stdlog +cli_res1=`grep ">>>>>>>> pass:1" stdlog | wc -l` +cli_res2=`grep "\[dgram\]|echo_check|same_content:yes|" stdlog` +cli_res3=`grep "\[h3-dgram\]|recv_dgram_bytes:102400|sent_dgram_bytes:102400|lost_dgram_bytes:0|lost_cnt:0|" stdlog` +cli_res4=`grep "\[bytestream\]|bytes_sent:0|bytes_rcvd:0|recv_fin:1|" stdlog` +cli_res5=`grep "\[bytestream\]|same_content:no|" stdlog | wc -l` +cli_res6=`grep "early_data_flag:1" stdlog` + +errlog=`grep_err_log` +if [ "$cli_res1" == "1" ] && [ -n "$cli_res2" ] && [ -n "$cli_res3" ] && [ -n "$cli_res4" ] && [ "$cli_res5" == "1" ] && [ -n "$cli_res6" ] && [ -z "$errlog" ]; then + echo ">>>>>>>> pass:1" + case_print_result "h3_ext_0RTT_accept_send_pure_fin2" "pass" +else + echo ">>>>>>>> pass:0" + case_print_result "h3_ext_0RTT_accept_send_pure_fin2" "fail" +fi + +## 0RTT reject + +killall test_server + +./test_server -l e -Q 65535 -e -U 1 > /dev/null & +sleep 1 + +clear_log +echo -e "h3_ext_0RTT_reject_send_pure_fin1...\c" +./test_client -l e -T 2 -s 102400 -U 1 -Q 65535 -E -x 302 > stdlog +cli_res1=`grep ">>>>>>>> pass:1" stdlog | wc -l` +cli_res2=`grep "\[dgram\]|echo_check|same_content:yes|" stdlog` +cli_res3=`grep "\[h3-dgram\]|recv_dgram_bytes:102400|sent_dgram_bytes:102400|lost_dgram_bytes:0|lost_cnt:0|" stdlog` +cli_res4=`grep "\[bytestream\]|bytes_sent:0|bytes_rcvd:0|recv_fin:1|" stdlog` +cli_res5=`grep "\[bytestream\]|same_content:no|" stdlog | wc -l` +cli_res6=`grep "early_data_flag:2" stdlog` + +errlog=`grep_err_log` +if [ "$cli_res1" == "1" ] && [ -n "$cli_res2" ] && [ -n "$cli_res3" ] && [ -n "$cli_res4" ] && [ "$cli_res5" == "1" ] && [ -n "$cli_res6" ] && [ -z "$errlog" ]; then + echo ">>>>>>>> pass:1" + case_print_result "h3_ext_0RTT_reject_send_pure_fin1" "pass" +else + echo ">>>>>>>> pass:0" + case_print_result "h3_ext_0RTT_reject_send_pure_fin1" "fail" +fi + +killall test_server + +./test_server -l e -Q 65535 -e -U 1 > /dev/null & +sleep 1 + +clear_log +echo -e "h3_ext_0RTT_reject_send_pure_fin2...\c" +./test_client -l e -T 2 -s 102400 -U 1 -Q 65535 -E -x 303 > stdlog +cli_res1=`grep ">>>>>>>> pass:1" stdlog | wc -l` +cli_res2=`grep "\[dgram\]|echo_check|same_content:yes|" stdlog` +cli_res3=`grep "\[h3-dgram\]|recv_dgram_bytes:102400|sent_dgram_bytes:102400|lost_dgram_bytes:0|lost_cnt:0|" stdlog` +cli_res4=`grep "\[bytestream\]|bytes_sent:0|bytes_rcvd:0|recv_fin:1|" stdlog` +cli_res5=`grep "\[bytestream\]|same_content:no|" stdlog | wc -l` +cli_res6=`grep "early_data_flag:2" stdlog` + +errlog=`grep_err_log` +if [ "$cli_res1" == "1" ] && [ -n "$cli_res2" ] && [ -n "$cli_res3" ] && [ -n "$cli_res4" ] && [ "$cli_res5" == "1" ] && [ -n "$cli_res6" ] && [ -z "$errlog" ]; then + echo ">>>>>>>> pass:1" + case_print_result "h3_ext_0RTT_reject_send_pure_fin2" "pass" +else + echo ">>>>>>>> pass:0" + case_print_result "h3_ext_0RTT_reject_send_pure_fin2" "fail" +fi + +# finish bytestream during transmission (-x 304) + +clear_log +echo -e "h3_ext_finish_bytestream_during_transmission...\c" +./test_client -l d -T 2 -s 102400 -U 1 -Q 65535 -E -x 304 > stdlog +cli_res1=`grep ">>>>>>>> pass:1" stdlog | wc -l` +cli_res2=`grep "\[dgram\]|echo_check|same_content:yes|" stdlog` +cli_res3=`grep "\[h3-dgram\]|recv_dgram_bytes:102400|sent_dgram_bytes:102400|lost_dgram_bytes:0|lost_cnt:0|" stdlog` +cli_res4=`grep "\[bytestream\]|bytes_sent:102400|bytes_rcvd:102400|recv_fin:1|" stdlog` +cli_res5=`grep "\[bytestream\]|same_content:yes|" stdlog | wc -l` +cli_res6=`grep "send pure fin" clog` +errlog=`grep_err_log | grep -v "send data after fin sent"` +if [ "$cli_res1" == "1" ] && [ -n "$cli_res2" ] && [ -n "$cli_res3" ] && [ -n "$cli_res4" ] && [ "$cli_res5" == "1" ] && [ -n "$cli_res6" ] && [ -z "$errlog" ]; then + echo ">>>>>>>> pass:1" + case_print_result "h3_ext_finish_bytestream_during_transmission" "pass" +else + echo ">>>>>>>> pass:0" + case_print_result "h3_ext_finish_bytestream_during_transmission" "fail" +fi + +# close bytestream during transmission (-x 305) + +clear_log +echo -e "h3_ext_close_bytestream_during_transmission...\c" +./test_client -l d -T 2 -s 102400 -U 1 -Q 65535 -E -x 305 > stdlog +cli_res1=`grep ">>>>>>>> pass:1" stdlog | wc -l` +cli_res2=`grep "\[dgram\]|echo_check|same_content:yes|" stdlog` +cli_res3=`grep "\[h3-dgram\]|recv_dgram_bytes:102400|sent_dgram_bytes:102400|lost_dgram_bytes:0|lost_cnt:0|" stdlog` +cli_res4=`grep "\[bytestream\]|bytes_sent:102400|.*|recv_fin:0|" stdlog` +cli_res5=`grep "\[bytestream\]|same_content:.*|" stdlog | wc -l` +cli_res6=`grep "xqc_h3_ext_bytestream_close|success" clog` +errlog=`grep_err_log | grep -v "xqc_h3_stream_process_data|xqc_stream_recv"` +if [ "$cli_res1" == "1" ] && [ -n "$cli_res2" ] && [ -n "$cli_res3" ] && [ -n "$cli_res4" ] && [ "$cli_res5" == "1" ] && [ -n "$cli_res6" ] && [ -z "$errlog" ]; then + echo ">>>>>>>> pass:1" + case_print_result "h3_ext_close_bytestream_during_transmission" "pass" +else + echo ">>>>>>>> pass:0" + case_print_result "h3_ext_close_bytestream_during_transmission" "fail" +fi + +# bytestream write blocked by streamlevel flowctl (-x 306) + +clear_log +echo -e "h3_ext_bytestream_blocked_by_stream_flowctl...\c" +./test_client -l d -T 2 -s 32000000 -U 1 -Q 65535 -E -x 306 > stdlog +cli_res2=`grep "|xqc_stream_send|exceed max_stream_data" clog` +cli_res3=`grep "|h3_ext_bytestream_write_notify|success|" clog` +cli_res4=`grep "\[bytestream\]|bytes_sent:32000000|bytes_rcvd:32000000|recv_fin:1|" stdlog` +cli_res5=`grep "\[bytestream\]|same_content:yes|" stdlog | wc -l` +errlog=`grep_err_log` +if [ -n "$cli_res2" ] && [ -n "$cli_res3" ] && [ -n "$cli_res4" ] && [ "$cli_res5" == "1" ] && [ -z "$errlog" ]; then + echo ">>>>>>>> pass:1" + case_print_result "h3_ext_bytestream_blocked_by_stream_flowctl" "pass" +else + echo ">>>>>>>> pass:0" + case_print_result "h3_ext_bytestream_blocked_by_stream_flowctl" "fail" +fi + +# bytestream write blocked by 0RTT limit (-x 307) + +clear_log +echo -e "h3_ext_bytestream_blocked_by_0RTT_limit...\c" +./test_client -l d -T 2 -s 10000000 -U 1 -Q 65535 -E -x 307 > stdlog +cli_res2=`grep "|too many 0rtt packets|" clog` +cli_res3=`grep "|h3_ext_bytestream_write_notify|success|" clog` +cli_res4=`grep "\[bytestream\]|bytes_sent:10000000|bytes_rcvd:10000000|recv_fin:1|" stdlog` +cli_res5=`grep "\[bytestream\]|same_content:yes|" stdlog | wc -l` +errlog=`grep_err_log` +if [ -n "$cli_res2" ] && [ -n "$cli_res3" ] && [ -n "$cli_res4" ] && [ "$cli_res5" == "1" ] && [ -z "$errlog" ]; then + echo ">>>>>>>> pass:1" + case_print_result "h3_ext_bytestream_blocked_by_0RTT_limit" "pass" +else + echo ">>>>>>>> pass:0" + case_print_result "h3_ext_bytestream_blocked_by_0RTT_limit" "fail" +fi + +# bytestream 0RTT write blocked by no 0RTT support (-x 308) + +clear_log +echo -e "h3_ext_bytestream_blocked_by_no_0RTT_support...\c" +./test_client -l d -T 2 -s 1024 -U 1 -Q 65535 -E -x 308 -1 > stdlog +cli_res2=`grep "|blocked by no 0RTT support|" clog` +cli_res3=`grep "|h3_ext_bytestream_write_notify|success|" clog` +cli_res4=`grep "\[bytestream\]|bytes_sent:1024|bytes_rcvd:1024|recv_fin:1|" stdlog` +cli_res5=`grep "\[bytestream\]|same_content:yes|" stdlog | wc -l` +errlog=`grep_err_log` +if [ -n "$cli_res2" ] && [ -n "$cli_res3" ] && [ -n "$cli_res4" ] && [ "$cli_res5" == "1" ] && [ -z "$errlog" ]; then + echo ">>>>>>>> pass:1" + case_print_result "h3_ext_bytestream_blocked_by_no_0RTT_support" "pass" +else + echo ">>>>>>>> pass:0" + case_print_result "h3_ext_bytestream_blocked_by_no_0RTT_support" "fail" +fi + +# bytestream/h3_request/datagram all blocked by sndq size (-x 309) + +clear_log +echo -e "h3_ext_bytestream_blocked_by_sndq_full...\c" +./test_client -l e -T 2 -s 16000000 -U 1 -Q 65535 -E -x 309 > stdlog +cli_res1=`grep ">>>>>>>> pass:1" stdlog | wc -l` +cli_res2=`grep "\[dgram\]|echo_check|same_content:.*|" stdlog` +cli_res4=`grep "\[bytestream\]|bytes_sent:16000000|bytes_rcvd:16000000|recv_fin:1|" stdlog` +cli_res5=`grep "\[bytestream\]|same_content:yes|" stdlog | wc -l` +errlog=`grep_err_log` + +if [ "$cli_res1" == "1" ] && [ -n "$cli_res2" ] && [ -n "$cli_res4" ] && [ "$cli_res5" == "1" ] && [ -z "$errlog" ]; then + echo ">>>>>>>> pass:1" + case_print_result "h3_ext_bytestream_blocked_by_sndq_full" "pass" +else + echo ">>>>>>>> pass:0" + case_print_result "h3_ext_bytestream_blocked_by_sndq_full" "fail" +fi + +# read / write full messages even if blocking happens + +clear_log +echo -e "h3_ext_bytestream_full_message_flow_ctrl...\c" +./test_client -l d -T 2 -s 32000000 -U 1 -Q 65535 -E -x 311 > stdlog +cli_res2=`grep "|xqc_stream_send|exceed max_stream_data" clog` +cli_res3=`grep "|h3_ext_bytestream_write_notify|success|" clog` +cli_res4=`grep "\[bytestream\]|bytes_sent:32001000|bytes_rcvd:32001000|recv_fin:1|snd_times:2|rcv_times:2|" stdlog` +cli_res5=`grep "\[bytestream\]|same_content:yes|" stdlog | wc -l` +errlog=`grep_err_log` +if [ -n "$cli_res2" ] && [ -n "$cli_res3" ] && [ -n "$cli_res4" ] && [ "$cli_res5" == "1" ] && [ -z "$errlog" ]; then + echo ">>>>>>>> pass:1" + case_print_result "h3_ext_bytestream_full_message_under_flow_ctrl" "pass" +else + echo ">>>>>>>> pass:0" + case_print_result "h3_ext_bytestream_full_message_under_flow_ctrl" "fail" +fi + +clear_log +echo -e "h3_ext_bytestream_full_message_0RTT_blocking...\c" +./test_client -l d -T 2 -s 10000000 -U 1 -Q 65535 -E -x 312 > stdlog +cli_res2=`grep "|too many 0rtt packets|" clog` +cli_res3=`grep "|h3_ext_bytestream_write_notify|success|" clog` +cli_res4=`grep "\[bytestream\]|bytes_sent:10001000|bytes_rcvd:10001000|recv_fin:1|snd_times:2|rcv_times:2|" stdlog` +cli_res5=`grep "\[bytestream\]|same_content:yes|" stdlog | wc -l` +errlog=`grep_err_log` +if [ -n "$cli_res2" ] && [ -n "$cli_res3" ] && [ -n "$cli_res4" ] && [ "$cli_res5" == "1" ] && [ -z "$errlog" ]; then + echo ">>>>>>>> pass:1" + case_print_result "h3_ext_bytestream_full_message_0RTT_blocking" "pass" +else + echo ">>>>>>>> pass:0" + case_print_result "h3_ext_bytestream_full_message_0RTT_blocking" "fail" +fi + +clear_log +echo -e "h3_ext_bytestream_full_message_no_0RTT_suppport...\c" +./test_client -l d -T 2 -s 1024 -U 1 -Q 65535 -E -x 313 -1 > stdlog +cli_res2=`grep "|blocked by no 0RTT support|" clog` +cli_res3=`grep "|h3_ext_bytestream_write_notify|success|" clog` +cli_res4=`grep "\[bytestream\]|bytes_sent:2024|bytes_rcvd:2024|recv_fin:1|snd_times:2|rcv_times:2|" stdlog` +cli_res5=`grep "\[bytestream\]|same_content:yes|" stdlog | wc -l` +errlog=`grep_err_log` +if [ -n "$cli_res2" ] && [ -n "$cli_res3" ] && [ -n "$cli_res4" ] && [ "$cli_res5" == "1" ] && [ -z "$errlog" ]; then + echo ">>>>>>>> pass:1" + case_print_result "h3_ext_bytestream_full_message_no_0RTT_suppport" "pass" +else + echo ">>>>>>>> pass:0" + case_print_result "h3_ext_bytestream_full_message_no_0RTT_suppport" "fail" +fi + +clear_log +echo -e "h3_ext_bytestream_full_message_sndq_full...\c" +./test_client -l e -T 2 -s 16000000 -U 1 -Q 65535 -E -x 314 > stdlog +cli_res1=`grep ">>>>>>>> pass:1" stdlog | wc -l` +cli_res2=`grep "\[dgram\]|echo_check|same_content:.*|" stdlog` +cli_res4=`grep "\[bytestream\]|bytes_sent:16001000|bytes_rcvd:16001000|recv_fin:1|snd_times:2|rcv_times:2|" stdlog` +cli_res5=`grep "\[bytestream\]|same_content:yes|" stdlog | wc -l` +errlog=`grep_err_log` + +if [ "$cli_res1" == "1" ] && [ -n "$cli_res2" ] && [ -n "$cli_res4" ] && [ "$cli_res5" == "1" ] && [ -z "$errlog" ]; then + echo ">>>>>>>> pass:1" + case_print_result "h3_ext_bytestream_full_message_sndq_full" "pass" +else + echo ">>>>>>>> pass:0" + case_print_result "h3_ext_bytestream_full_message_sndq_full" "fail" +fi + +killall test_server + +./test_server -l e -Q 65535 -e -U 1 -H > /dev/null & +sleep 1 + +clear_log +echo -e "connect to an h3_ext disabled server...\c" +./test_client -l e -T 2 -s 1024 -U 1 -Q 65535 -E > stdlog +svr_log=`grep "select proto error" slog` + +if [ -n "$svr_log" ]; then + echo ">>>>>>>> pass:1" + case_print_result "connect_to_an_h3_ext_disabled_server" "pass" +else + echo ">>>>>>>> pass:0" + case_print_result "connect_to_an_h3_ext_disabled_server" "fail" +fi + +killall test_server + +./test_server -l e -Q 65535 -e -U 1 > /dev/null & +sleep 1 + +clear_log +echo -e "h3_ext is disabled on the client...\c" +./test_client -l e -T 2 -s 1024 -U 1 -Q 65535 -E -x 315 > stdlog +cli_res1=`grep "can't get application layer callback" clog` + +if [ -n "$cli_res1" ]; then + echo ">>>>>>>> pass:1" + case_print_result "h3_ext_is_disabled_on_the_client" "pass" +else + echo ">>>>>>>> pass:0" + case_print_result "h3_ext_is_disabled_on_the_client" "fail" +fi + +rm -rf tp_localhost test_session xqc_token +killall test_server + + +./test_server -l d -Q 65535 -e -U 1 -s 1 > /dev/null & +sleep 1 +./test_client -l d -T 1 -s 4800 -U 1 -Q 65535 -E &> /dev/null #generate 0rtt ticket +killall test_server +./test_server -l d -e -s 1 > /dev/null & #disable datagram +sleep 1 +clear_log +echo -e "check_clear_0rtt_ticket_flag_in_close_notify...\c" +./test_client -l d -T 1 -s 4800 -U 1 -Q 65535 -E > stdlog +cli_res2=`grep "should_clear_0rtt_ticket, conn_err:14, clear_0rtt_ticket:1" stdlog` +errlog=`grep_err_log` +if [ -n "$cli_res2" ] && [ -n "$errlog" ]; then + echo ">>>>>>>> pass:1" + case_print_result "check_clear_0rtt_ticket_flag_in_close_notify" "pass" +else + echo ">>>>>>>> pass:0" + case_print_result "check_clear_0rtt_ticket_flag_in_close_notify" "fail" +fi + +rm -rf tp_localhost test_session xqc_token +killall test_server + +./test_server -l d -Q 65535 -e -s 1 > /dev/null & +sleep 1 +./test_client -l d -s 4800 -Q 65535 -E &> /dev/null #generate 0rtt ticket +killall test_server +./test_server -l d -e -s 1 > /dev/null & #disable datagram +sleep 1 +clear_log +echo -e "check_clear_0rtt_ticket_flag_in_h3_close_notify...\c" +./test_client -l d -s 4800 -Q 65535 -E > stdlog +cli_res2=`grep "should_clear_0rtt_ticket, conn_err:14, clear_0rtt_ticket:1" stdlog` +errlog=`grep_err_log` +if [ -n "$cli_res2" ] && [ -n "$errlog" ]; then + echo ">>>>>>>> pass:1" + case_print_result "check_clear_0rtt_ticket_flag_in_h3_close_notify" "pass" +else + echo ">>>>>>>> pass:0" + case_print_result "check_clear_0rtt_ticket_flag_in_h3_close_notify" "fail" +fi + +rm -rf tp_localhost test_session xqc_token +killall test_server + +./test_server -l d -Q 65535 -e -s 1 > /dev/null & +sleep 1 +./test_client -l d -s 4800 -Q 65535 -E &> /dev/null #generate 0rtt ticket +killall test_server +./test_server -l d -e -s 1 > /dev/null & #disable datagram +sleep 1 +clear_log +echo -e "check_clear_0rtt_ticket_flag_in_h3_close_notify...\c" +./test_client -l d -s 4800 -Q 65535 -E > stdlog +cli_res2=`grep "should_clear_0rtt_ticket, conn_err:14, clear_0rtt_ticket:1" stdlog` +errlog=`grep_err_log` +if [ -n "$cli_res2" ] && [ -n "$errlog" ]; then + echo ">>>>>>>> pass:1" + case_print_result "check_clear_0rtt_ticket_flag_in_h3_close_notify" "pass" +else + echo ">>>>>>>> pass:0" + case_print_result "check_clear_0rtt_ticket_flag_in_h3_close_notify" "fail" +fi + +rm -rf tp_localhost test_session xqc_token +killall test_server + +clear_log +echo -e "request_closing_notify...\c" +./test_server -l d -x 14 > /dev/null & +sleep 1 +./test_client -l d >> stdlog +res=`grep "request closing notify triggered" stdlog` +if [ -n "$res" ]; then + echo ">>>>>>>> pass:1" + case_print_result "request_closing_notify" "pass" +else + echo ">>>>>>>> pass:0" + case_print_result "request_closing_notify" "fail" +fi + + +killall test_server +./test_server -l d -e -M -R 3 -Q 65535 -U 1 > /dev/null & +sleep 1 + +rm -rf tp_localhost test_session xqc_token +clear_log +echo -e "SP reinject datagrams ...\c" +sudo ./test_client -s 1024 -l d -t 1 -E -R 3 -Q 65535 -U 1 -T 1 > stdlog +result=`grep "\[dgram\]|recv_dgram_bytes:4096|sent_dgram_bytes:1024|" stdlog` +cli_res=`grep -E "xqc_conn_destroy.*mp_enable:0" clog` +errlog=`grep_err_log` +if [ -z "$errlog" ] && [ -n "$result" ] && [ -n "$cli_res" ] ; then + echo ">>>>>>>> pass:1" + case_print_result "SP_reinject_datagrams" "pass" +else + echo ">>>>>>>> pass:0" + case_print_result "SP_reinject_datagrams" "fail" +fi +grep_err_log + +rm -rf tp_localhost test_session xqc_token +clear_log +echo -e "SP reinject h3-ext datagrams ...\c" +sudo ./test_client -s 1024 -l d -t 1 -E -R 3 -Q 65535 -U 1 -T 2 > stdlog +result=`grep "\[h3-dgram\]|recv_dgram_bytes:4096|sent_dgram_bytes:1024|" stdlog` +cli_res=`grep -E "xqc_conn_destroy.*mp_enable:0" clog` +errlog=`grep_err_log` +if [ -z "$errlog" ] && [ -n "$result" ] && [ -n "$cli_res" ] ; then + echo ">>>>>>>> pass:1" + case_print_result "SP_reinject_h3_ext_datagrams" "pass" +else + echo ">>>>>>>> pass:0" + case_print_result "SP_reinject_h3_ext_datagrams" "fail" +fi +grep_err_log + + +rm -rf tp_localhost test_session xqc_token +clear_log +echo -e "MP reinject datagrams ...\c" +sudo ./test_client -s 1024 -l d -t 1 -E -R 3 -Q 65535 -U 1 -T 1 -M -i lo -i lo > stdlog +result=`grep "\[dgram\]|recv_dgram_bytes:4096|sent_dgram_bytes:1024|" stdlog` +cli_res=`grep -E "xqc_conn_destroy.*mp_enable:1" clog` +errlog=`grep_err_log` +if [ -z "$errlog" ] && [ -n "$result" ] && [ -n "$cli_res" ] ; then + echo ">>>>>>>> pass:1" + case_print_result "MP_reinject_datagrams" "pass" +else + echo ">>>>>>>> pass:0" + case_print_result "MP_reinject_datagrams" "fail" +fi +grep_err_log + +rm -rf tp_localhost test_session xqc_token +clear_log +echo -e "MP reinject h3-ext datagrams ...\c" +sudo ./test_client -s 1024 -l d -t 1 -E -R 3 -Q 65535 -U 1 -T 2 -M -i lo -i lo > stdlog +result=`grep "\[h3-dgram\]|recv_dgram_bytes:4096|sent_dgram_bytes:1024|" stdlog` +cli_res=`grep -E "xqc_conn_destroy.*mp_enable:1" clog` +errlog=`grep_err_log` +if [ -z "$errlog" ] && [ -n "$result" ] && [ -n "$cli_res" ] ; then + echo ">>>>>>>> pass:1" + case_print_result "MP_reinject_h3_ext_datagrams" "pass" +else + echo ">>>>>>>> pass:0" + case_print_result "MP_reinject_h3_ext_datagrams" "fail" +fi +grep_err_log + + +killall test_server +./test_server -l d -e -M -x 208 -Q 65535 -U 1 > /dev/null & +sleep 1 + + +rm -rf tp_localhost test_session xqc_token +clear_log +echo -e "MP datagrams redundancy...\c" +sudo ./test_client -s 1024 -l d -t 1 -E -x 208 -Q 65535 -U 1 -T 1 -M -i lo -i lo > stdlog +result=`grep "\[dgram\]|recv_dgram_bytes:4096|sent_dgram_bytes:1024|" stdlog` +cli_res=`grep -E "xqc_conn_destroy.*mp_enable:1" clog` +errlog=`grep_err_log` +if [ -z "$errlog" ] && [ -n "$result" ] && [ -n "$cli_res" ] ; then + echo ">>>>>>>> pass:1" + case_print_result "MP_datagrams_redundancy" "pass" +else + echo ">>>>>>>> pass:0" + case_print_result "MP_datagrams_redundancy" "fail" +fi +grep_err_log + +rm -rf tp_localhost test_session xqc_token +clear_log +echo -e "MP h3-ext datagrams redundancy...\c" +sudo ./test_client -s 1024 -l d -t 1 -E -x 208 -Q 65535 -U 1 -T 2 -M -i lo -i lo > stdlog +result=`grep "\[h3-dgram\]|recv_dgram_bytes:4096|sent_dgram_bytes:1024|" stdlog` +cli_res=`grep -E "xqc_conn_destroy.*mp_enable:1" clog` +errlog=`grep_err_log` +if [ -z "$errlog" ] && [ -n "$result" ] && [ -n "$cli_res" ] ; then + echo ">>>>>>>> pass:1" + case_print_result "MP_h3_ext_datagrams_redundancy" "pass" +else + echo ">>>>>>>> pass:0" + case_print_result "MP_h3_ext_datagrams_redundancy" "fail" +fi +grep_err_log + + +killall test_server +./test_server -l d -e -M -x 208 -Q 65535 -U 1 --dgram_qos 3 > /dev/null & +sleep 1 + + +rm -rf tp_localhost test_session xqc_token +clear_log +echo -e "MP no reinjection for normal datagrams...\c" +sudo ./test_client -s 1024 -l d -t 1 -E -x 208 -Q 65535 -U 1 -T 1 -M -i lo -i lo --dgram_qos 3 > stdlog +result=`grep "\[dgram\]|recv_dgram_bytes:1024|sent_dgram_bytes:1024|" stdlog` +cli_res=`grep -E "xqc_conn_destroy.*mp_enable:1" clog` +errlog=`grep_err_log` +if [ -z "$errlog" ] && [ -n "$result" ] && [ -n "$cli_res" ] ; then + echo ">>>>>>>> pass:1" + case_print_result "MP_no_reinjection_for_normal_datagrams" "pass" +else + echo ">>>>>>>> pass:0" + case_print_result "MP_no_reinjection_for_normal_datagrams" "fail" +fi +grep_err_log + +rm -rf tp_localhost test_session xqc_token +clear_log +echo -e "MP no reinjection for normal h3-ext datagrams...\c" +sudo ./test_client -s 1024 -l d -t 1 -E -x 208 -Q 65535 -U 1 -T 2 -M -i lo -i lo --dgram_qos 3 > stdlog +result=`grep "\[h3-dgram\]|recv_dgram_bytes:1024|sent_dgram_bytes:1024|" stdlog` +cli_res=`grep -E "xqc_conn_destroy.*mp_enable:1" clog` +errlog=`grep_err_log` +if [ -z "$errlog" ] && [ -n "$result" ] && [ -n "$cli_res" ] ; then + echo ">>>>>>>> pass:1" + case_print_result "MP_no_reinjection_for_normal_h3_ext_datagrams" "pass" +else + echo ">>>>>>>> pass:0" + case_print_result "MP_no_reinjection_for_normal_h3_ext_datagrams" "fail" +fi +grep_err_log + + +killall test_server +stdbuf -oL ./test_server -l d -e -M -Q 65535 -U 1 --pmtud 1 -x 200 > svr_stdlog & +sleep 1 + +rm -rf tp_localhost test_session xqc_token +> svr_stdlog +clear_log +echo -e "SP datagram PMTUD 1RTT...\c" +sudo ./test_client -s 1024 -l d -t 1 -E -Q 65535 -U 1 -T 1 --pmtud 1 > stdlog +result=`grep "\[dgram\]|recv_dgram_bytes:1024|sent_dgram_bytes:1024|" stdlog` +mtu_res1=`grep "\[dgram\]|mss_callback|updated_mss:1404|" stdlog` +mtu_res2=`grep -a "\[dgram\]|1RTT|updated_mss:1404|" svr_stdlog` +cli_res=`grep -E "xqc_conn_destroy.*mp_enable:0" clog` +errlog=`grep_err_log` +if [ -z "$errlog" ] && [ -n "$result" ] && [ -n "$cli_res" ] && [ -n "$mtu_res1" ] && [ -n "$mtu_res2" ] ; then + echo ">>>>>>>> pass:1" + case_print_result "SP_datagram_PMTUD_1RTT" "pass" +else + echo ">>>>>>>> pass:0" + case_print_result "SP_datagram_PMTUD_1RTT" "fail" +fi +grep_err_log + +> svr_stdlog +clear_log +echo -e "SP datagram PMTUD 0RTT...\c" +sudo ./test_client -s 1024 -l d -t 1 -E -Q 65535 -U 1 -T 1 --pmtud 1 > stdlog +result=`grep "\[dgram\]|recv_dgram_bytes:1024|sent_dgram_bytes:1024|" stdlog` +mtu_res1=`grep "\[dgram\]|mss_callback|updated_mss:1404|" stdlog` +mtu_res2=`grep -a "\[dgram\]|1RTT|updated_mss:1404|" svr_stdlog` +cli_res=`grep -E "xqc_conn_destroy.*mp_enable:0" clog` +errlog=`grep_err_log` +if [ -z "$errlog" ] && [ -n "$result" ] && [ -n "$cli_res" ] && [ -n "$mtu_res1" ] && [ -n "$mtu_res2" ] ; then + echo ">>>>>>>> pass:1" + case_print_result "SP_datagram_PMTUD_0RTT" "pass" +else + echo ">>>>>>>> pass:0" + case_print_result "SP_datagram_PMTUD_0RTT" "fail" +fi +grep_err_log + +rm -rf tp_localhost test_session xqc_token +> svr_stdlog +clear_log +echo -e "SP h3-ext datagram PMTUD 1RTT...\c" +sudo ./test_client -s 1024 -l d -t 1 -E -Q 65535 -U 1 -T 2 --pmtud 1 > stdlog +result=`grep "\[h3-dgram\]|recv_dgram_bytes:1024|sent_dgram_bytes:1024|" stdlog` +mtu_res1=`grep "\[h3-dgram\]|callback|updated_mss:1404|" stdlog` +mtu_res2=`grep -a "\[h3-dgram\]|1RTT|updated_mss:1404|" svr_stdlog` +cli_res=`grep -E "xqc_conn_destroy.*mp_enable:0" clog` +errlog=`grep_err_log` +if [ -z "$errlog" ] && [ -n "$result" ] && [ -n "$cli_res" ] && [ -n "$mtu_res1" ] && [ -n "$mtu_res2" ] ; then + echo ">>>>>>>> pass:1" + case_print_result "SP_h3_ext_datagram_PMTUD_1RTT" "pass" +else + echo ">>>>>>>> pass:0" + case_print_result "SP_h3_ext_datagram_PMTUD_1RTT" "fail" +fi +grep_err_log + +> svr_stdlog +clear_log +echo -e "SP h3-ext datagram PMTUD 0RTT...\c" +sudo ./test_client -s 1024 -l d -t 1 -E -Q 65535 -U 1 -T 2 --pmtud 1 > stdlog +result=`grep "\[h3-dgram\]|recv_dgram_bytes:1024|sent_dgram_bytes:1024|" stdlog` +mtu_res1=`grep "\[h3-dgram\]|callback|updated_mss:1404|" stdlog` +mtu_res2=`grep -a "\[h3-dgram\]|1RTT|updated_mss:1404|" svr_stdlog` +cli_res=`grep -E "xqc_conn_destroy.*mp_enable:0" clog` +errlog=`grep_err_log` +if [ -z "$errlog" ] && [ -n "$result" ] && [ -n "$cli_res" ] && [ -n "$mtu_res1" ] && [ -n "$mtu_res2" ] ; then + echo ">>>>>>>> pass:1" + case_print_result "SP_h3_ext_datagram_PMTUD_0RTT" "pass" +else + echo ">>>>>>>> pass:0" + case_print_result "SP_h3_ext_datagram_PMTUD_0RTT" "fail" +fi +grep_err_log + + +rm -rf tp_localhost test_session xqc_token +> svr_stdlog +clear_log +echo -e "MP datagram PMTUD 1RTT...\c" +sudo ./test_client -s 1024 -l d -t 1 -E -Q 65535 -U 1 -T 1 --pmtud 1 -M -i lo -i lo > stdlog +result=`grep "\[dgram\]|recv_dgram_bytes:1024|sent_dgram_bytes:1024|" stdlog` +mtu_res1=`grep "\[dgram\]|mss_callback|updated_mss:1404|" stdlog` +mtu_res2=`grep -a "\[dgram\]|1RTT|updated_mss:1404|" svr_stdlog` +cli_res=`grep -E "xqc_conn_destroy.*mp_enable:1" clog` +errlog=`grep_err_log` +if [ -z "$errlog" ] && [ -n "$result" ] && [ -n "$cli_res" ] && [ -n "$mtu_res1" ] && [ -n "$mtu_res2" ] ; then + echo ">>>>>>>> pass:1" + case_print_result "MP_datagram_PMTUD_1RTT" "pass" +else + echo ">>>>>>>> pass:0" + case_print_result "MP_datagram_PMTUD_1RTT" "fail" +fi +grep_err_log + +> svr_stdlog +clear_log +echo -e "MP datagram PMTUD 0RTT...\c" +sudo ./test_client -s 1024 -l d -t 1 -E -Q 65535 -U 1 -T 1 --pmtud 1 -M -i lo -i lo > stdlog +result=`grep "\[dgram\]|recv_dgram_bytes:1024|sent_dgram_bytes:1024|" stdlog` +mtu_res1=`grep "\[dgram\]|mss_callback|updated_mss:1404|" stdlog` +mtu_res2=`grep -a "\[dgram\]|1RTT|updated_mss:1404|" svr_stdlog` +cli_res=`grep -E "xqc_conn_destroy.*mp_enable:1" clog` +errlog=`grep_err_log` +if [ -z "$errlog" ] && [ -n "$result" ] && [ -n "$cli_res" ] && [ -n "$mtu_res1" ] && [ -n "$mtu_res2" ] ; then + echo ">>>>>>>> pass:1" + case_print_result "MP_datagram_PMTUD_0RTT" "pass" +else + echo ">>>>>>>> pass:0" + case_print_result "MP_datagram_PMTUD_0RTT" "fail" +fi +grep_err_log + +rm -rf tp_localhost test_session xqc_token +> svr_stdlog +clear_log +echo -e "MP h3-ext datagram PMTUD 1RTT...\c" +sudo ./test_client -s 1024 -l d -t 1 -E -Q 65535 -U 1 -T 2 --pmtud 1 -M -i lo -i lo > stdlog +result=`grep "\[h3-dgram\]|recv_dgram_bytes:1024|sent_dgram_bytes:1024|" stdlog` +mtu_res1=`grep "\[h3-dgram\]|callback|updated_mss:1404|" stdlog` +mtu_res2=`grep -a "\[h3-dgram\]|1RTT|updated_mss:1404|" svr_stdlog` +cli_res=`grep -E "xqc_conn_destroy.*mp_enable:1" clog` +errlog=`grep_err_log` +if [ -z "$errlog" ] && [ -n "$result" ] && [ -n "$cli_res" ] && [ -n "$mtu_res1" ] && [ -n "$mtu_res2" ] ; then + echo ">>>>>>>> pass:1" + case_print_result "MP_h3_ext_datagram_PMTUD_1RTT" "pass" +else + echo ">>>>>>>> pass:0" + case_print_result "MP_h3_ext_datagram_PMTUD_1RTT" "fail" +fi +grep_err_log + +> svr_stdlog +clear_log +echo -e "MP h3-ext datagram PMTUD 0RTT...\c" +sudo ./test_client -s 1024 -l d -t 1 -E -Q 65535 -U 1 -T 2 --pmtud 1 -M -i lo -i lo > stdlog +result=`grep "\[h3-dgram\]|recv_dgram_bytes:1024|sent_dgram_bytes:1024|" stdlog` +mtu_res1=`grep "\[h3-dgram\]|callback|updated_mss:1404|" stdlog` +mtu_res2=`grep -a "\[h3-dgram\]|1RTT|updated_mss:1404|" svr_stdlog` +cli_res=`grep -E "xqc_conn_destroy.*mp_enable:1" clog` +errlog=`grep_err_log` +if [ -z "$errlog" ] && [ -n "$result" ] && [ -n "$cli_res" ] && [ -n "$mtu_res1" ] && [ -n "$mtu_res2" ] ; then + echo ">>>>>>>> pass:1" + case_print_result "MP_h3_ext_datagram_PMTUD_0RTT" "pass" +else + echo ">>>>>>>> pass:0" + case_print_result "MP_h3_ext_datagram_PMTUD_0RTT" "fail" +fi +grep_err_log + + +killall test_server +stdbuf -oL ./test_server -l d -e -M > /dev/null & +sleep 1 + +rm -rf tp_localhost test_session xqc_token +clear_log +echo -e "transport MP ping ...\c" +sudo ./test_client -s 1024 -l d -E -T 1 -e 1 --epoch_timeout 2000000 -t 3 --mp_ping 1 -M -i lo -i lo >> clog +ret_ping_id=`grep "====>ping_id:" clog` +ret_no_ping_id=`grep "====>no ping_id" clog` +path0_ping=`grep -E "xqc_send_packet_with_pn.*path:0.*PING" clog` +path1_ping=`grep -E "xqc_send_packet_with_pn.*path:1.*PING" clog` +if [ -n "$ret_ping_id" ] && [ -n "$ret_no_ping_id" ] && [ -n "$path0_ping" ] && [ -n "$path1_ping" ] ; then + echo ">>>>>>>> pass:1" + case_print_result "transport_MP_ping" "pass" +else + echo ">>>>>>>> pass:0" + case_print_result "transport_MP_ping" "fail" +fi + + +rm -rf tp_localhost test_session xqc_token +clear_log +echo -e "h3 MP ping ...\c" +sudo ./test_client -s 1024 -l d -E -e 1 --epoch_timeout 2000000 -t 3 --mp_ping 1 -M -i lo -i lo >> clog +ret_ping_id=`grep "====>ping_id:" clog` +ret_no_ping_id=`grep "====>no ping_id" clog` +path0_ping=`grep -E "xqc_send_packet_with_pn.*path:0.*PING" clog` +path1_ping=`grep -E "xqc_send_packet_with_pn.*path:1.*PING" clog` +if [ -n "$ret_ping_id" ] && [ -n "$ret_no_ping_id" ] && [ -n "$path0_ping" ] && [ -n "$path1_ping" ] ; then + echo ">>>>>>>> pass:1" + case_print_result "h3_MP_ping" "pass" +else + echo ">>>>>>>> pass:0" + case_print_result "h3_MP_ping" "fail" +fi + + +rm -rf tp_localhost test_session xqc_token +clear_log +echo -e "freeze path0 ...\c" +sudo ./test_client -s 1024000 -l d -E -e 4 -T 2 --epoch_timeout 2000000 -t 4 -M -i lo -i lo -x 107 > stdlog +stream_info3=`grep "stream_info:" stdlog | head -n 3 | tail -n 1 | grep -v "#0" | grep "#1"` +stream_info5=`grep "stream_info:" stdlog | tail -n 1 | grep -E "#0.*#1"` +clog_res1=`grep -E "path:0.*app_path_status:2->3" clog` +clog_res2=`grep -E "path:0.*app_path_status:3->1" clog` +slog_res1=`grep -E "path:0.*app_path_status:2->3" slog` +slog_res2=`grep -E "path:0.*app_path_status:3->1" slog` +if [ -n "$stream_info3" ] && [ -n "$stream_info5" ] && [ -n "$clog_res1" ] && [ -n "$clog_res2" ] && [ -n "$slog_res1" ] && [ -n "$slog_res2" ] ; then + echo ">>>>>>>> pass:1" + case_print_result "freeze_path0" "pass" +else + echo ">>>>>>>> pass:0" + case_print_result "freeze_path0" "fail" +fi + +rm -rf tp_localhost test_session xqc_token +clear_log +echo -e "freeze path1 ...\c" +sudo ./test_client -s 1024000 -l d -E -e 4 -T 2 --epoch_timeout 2000000 -t 4 -M -i lo -i lo -x 108 > stdlog +stream_info3=`grep "stream_info:" stdlog | head -n 3 | tail -n 1 | grep -v "#1" | grep "#0"` +stream_info5=`grep "stream_info:" stdlog | tail -n 1 | grep -E "#0.*#1"` +clog_res1=`grep -E "path:1.*app_path_status:2->3" clog` +clog_res2=`grep -E "path:1.*app_path_status:3->1" clog` +slog_res1=`grep -E "path:1.*app_path_status:2->3" slog` +slog_res2=`grep -E "path:1.*app_path_status:3->1" slog` +if [ -n "$stream_info3" ] && [ -n "$stream_info5" ] && [ -n "$clog_res1" ] && [ -n "$clog_res2" ] && [ -n "$slog_res1" ] && [ -n "$slog_res2" ] ; then + echo ">>>>>>>> pass:1" + case_print_result "freeze_path1" "pass" +else + echo ">>>>>>>> pass:0" + case_print_result "freeze_path1" "fail" +fi + +killall test_server +stdbuf -oL ./test_server -l d -e -M > /dev/null & +sleep 1 + +rm -rf tp_localhost test_session xqc_token +clear_log +echo -e "probing standby paths ...\c" +sudo ./test_client -s 1024000 -l d -E -e 1 --epoch_timeout 2000000 -t 4 -M -i lo -i lo -x 501 -y > stdlog +clog_res1=`grep -E "|xqc_path_standby_probe|PING|path:1|" clog` +if [ -n "$clog_res1" ] ; then + echo ">>>>>>>> pass:1" + case_print_result "probing_standby_path" "pass" +else + echo ">>>>>>>> pass:0" + case_print_result "probing_standby_path" "fail" +fi + + +sudo rm -rf tp_localhost test_session xqc_token clog stdlog ckeys.log +clear_log +echo -e "conn_rate_throttling ...\c" +result=`./test_client -s 1024000 -l d -E --rate_limit 1000000 |grep ">>>>>>>> pass"` +errlog=`grep_err_log` +echo "$result" +if [ -z "$errlog" ] && [ "$result" == ">>>>>>>> pass:1" ]; then + case_print_result "conn_rate_throttling" "pass" +else + case_print_result "conn_rate_throttling" "fail" + echo "$errlog" +fi + +clear_log +echo -e "stream_rate_throttling ...\c" +result=`./test_client -s 1024000 -l d -E -x 109 |grep ">>>>>>>> pass"` +errlog=`grep_err_log` +echo "$result" +if [ -z "$errlog" ] && [ "$result" == ">>>>>>>> pass:1" ]; then + case_print_result "stream_rate_throttling" "pass" +else + case_print_result "stream_rate_throttling" "fail" + echo "$errlog" +fi + +killall test_server cd - diff --git a/scripts/xquic.lds b/scripts/xquic.lds index f2a324108..17cdd7b22 100644 --- a/scripts/xquic.lds +++ b/scripts/xquic.lds @@ -8,7 +8,9 @@ XQUIC_VERS_1.0 { xqc_h3_conn_close; xqc_scid_str; xqc_h3_conn_get_errno; + xqc_h3_conn_get_ssl; xqc_h3_conn_set_user_data; + xqc_h3_conn_get_user_data; xqc_h3_conn_get_peer_addr; xqc_h3_conn_get_local_addr; xqc_h3_conn_is_ready_to_send_early_data; @@ -34,6 +36,7 @@ XQUIC_VERS_1.0 { xqc_connect; xqc_conn_close; xqc_conn_get_errno; + xqc_conn_get_ssl; xqc_conn_set_transport_user_data; xqc_conn_get_peer_addr; xqc_conn_get_local_addr; @@ -60,7 +63,9 @@ XQUIC_VERS_1.0 { xqc_reno_cb; xqc_cubic_cb; xqc_minrtt_scheduler_cb; + xqc_interop_scheduler_cb; xqc_backup_scheduler_cb; + xqc_rap_scheduler_cb; xqc_conn_is_ready_to_send_early_data; xqc_h3_conn_send_ping; xqc_conn_send_ping; @@ -69,6 +74,7 @@ XQUIC_VERS_1.0 { xqc_conn_close_path; xqc_conn_mark_path_standby; xqc_conn_mark_path_available; + xqc_conn_mark_path_frozen; xqc_dcid_str_by_scid; xqc_h3_ctx_init; xqc_h3_ctx_destroy; @@ -81,8 +87,36 @@ XQUIC_VERS_1.0 { xqc_lb_cid_encryption; xqc_path_get_peer_addr; xqc_path_get_local_addr; - xqc_xlink_reinj_ctl_cb; - xqc_path_get_stats; + xqc_default_reinj_ctl_cb; + xqc_deadline_reinj_ctl_cb; + xqc_dgram_reinj_ctl_cb; + xqc_datagram_get_mss; + xqc_datagram_send; + xqc_datagram_send_multiple; + xqc_datagram_set_user_data; + xqc_datagram_get_user_data; + xqc_h3_ext_datagram_get_mss; + xqc_h3_ext_datagram_set_user_data; + xqc_h3_ext_datagram_get_user_data; + xqc_h3_ext_datagram_send; + xqc_h3_ext_datagram_send_multiple; + xqc_h3_ext_bytestream_create; + xqc_h3_ext_bytestream_close; + xqc_h3_ext_bytestream_finish; + xqc_h3_ext_bytestream_set_user_data; + xqc_h3_ext_bytestream_get_user_data; + xqc_h3_ext_bytestream_get_stats; + xqc_h3_ext_bytestream_send; + xqc_h3_ext_bytestream_id; + xqc_h3_ext_bytestream_get_h3_conn; + xqc_conn_should_clear_0rtt_ticket; + xqc_unlimited_cc_cb; + xqc_copa_cb; + xqc_conn_get_conn_settings_template; + xqc_conn_get_lastest_rtt; + xqc_log_enable; + xqc_h3_request_update_settings; + xqc_stream_update_settings; local: *; }; diff --git a/scripts/xquic_test.sh b/scripts/xquic_test.sh index c4274ba75..31df14314 100644 --- a/scripts/xquic_test.sh +++ b/scripts/xquic_test.sh @@ -61,7 +61,7 @@ function do_compile() { fi #turn on Code Coverage - cmake -DGCOV=on -DCMAKE_BUILD_TYPE=Debug -DXQC_ENABLE_TESTING=1 -DXQC_PRINT_SECRET=1 -DXQC_SUPPORT_SENDMMSG_BUILD=1 -DXQC_ENABLE_EVENT_LOG=1 -DXQC_ENABLE_BBR2=1 -DXQC_DISABLE_RENO=0 -DSSL_TYPE=${SSL_TYPE_STR} -DSSL_PATH=${SSL_PATH_STR} -DSSL_INC_PATH=${SSL_INC_PATH_STR} -DSSL_LIB_PATH=${SSL_LIB_PATH_STR} .. + cmake -DGCOV=on -DCMAKE_BUILD_TYPE=Debug -DXQC_ENABLE_TESTING=1 -DXQC_PRINT_SECRET=1 -DXQC_SUPPORT_SENDMMSG_BUILD=1 -DXQC_ENABLE_EVENT_LOG=1 -DXQC_ENABLE_BBR2=1 -DXQC_ENABLE_RENO=1 -DSSL_TYPE=${SSL_TYPE_STR} -DSSL_PATH=${SSL_PATH_STR} -DSSL_INC_PATH=${SSL_INC_PATH_STR} -DSSL_LIB_PATH=${SSL_LIB_PATH_STR} .. make -j rm -f CMakeCache.txt diff --git a/src/common/xqc_log.c b/src/common/xqc_log.c index 06c185bab..a69210864 100644 --- a/src/common/xqc_log.c +++ b/src/common/xqc_log.c @@ -9,6 +9,14 @@ FILE *g_malloc_info_fp; #endif +static xqc_bool_t xqc_log_switch = XQC_TRUE; + +void +xqc_log_enable(xqc_bool_t enable) +{ + xqc_log_switch = enable; +} + void xqc_log_level_set(xqc_log_t *log, xqc_log_level_t level) { @@ -66,6 +74,7 @@ xqc_log_type_2_level(xqc_log_type_t type) case TRA_DATAGRAMS_SENT: case TRA_DATAGRAMS_RECEIVED: case TRA_STREAM_STATE_UPDATED: + case TRA_STATELESS_RESET: case REC_METRICS_UPDATED: case REC_PARAMETERS_SET: case REC_CONGESTION_STATE_UPDATED: @@ -131,6 +140,7 @@ xqc_log_type_str(xqc_log_type_t type) [TRA_STREAM_STATE_UPDATED] = "stream_state_updated", [TRA_FRAMES_PROCESSED] = "frames_processed", [TRA_DATA_MOVED] = "data_moved", + [TRA_STATELESS_RESET] = "stateless_reset", [REC_PARAMETERS_SET] = "rec_parameters_set", [REC_METRICS_UPDATED] = "rec_metrics_updated", [REC_CONGESTION_STATE_UPDATED] = "congestion_state_updated", @@ -166,12 +176,17 @@ xqc_log_type_str(xqc_log_type_t type) void xqc_log_implement(xqc_log_t *log, xqc_log_type_t type, const char *func, const char *fmt, ...) { + /* do nothing if switch is off */ + if (!xqc_log_switch) { + return; + } + xqc_log_level_t level = xqc_log_type_2_level(type); if (level > log->log_level) { return; } - unsigned char buf[XQC_MAX_LOG_LEN]; + unsigned char buf[XQC_MAX_LOG_LEN] = {0}; unsigned char *p = buf; unsigned char *last = buf + sizeof(buf); diff --git a/src/common/xqc_log.h b/src/common/xqc_log.h index dda0ba2cb..465d17bd5 100644 --- a/src/common/xqc_log.h +++ b/src/common/xqc_log.h @@ -74,6 +74,7 @@ typedef enum { TRA_STREAM_STATE_UPDATED, TRA_FRAMES_PROCESSED, TRA_DATA_MOVED, + TRA_STATELESS_RESET, /* recovery event */ REC_PARAMETERS_SET, diff --git a/src/common/xqc_log_event_callback.c b/src/common/xqc_log_event_callback.c index bb4a0d1c3..66af99906 100644 --- a/src/common/xqc_log_event_callback.c +++ b/src/common/xqc_log_event_callback.c @@ -287,6 +287,13 @@ xqc_log_TRA_FRAMES_PROCESSED_callback(xqc_log_t *log, const char *func, ...) break; } + case XQC_FRAME_DATAGRAM: { + uint64_t length = va_arg(args, uint64_t); + xqc_log_implement(log, TRA_FRAMES_PROCESSED, func, + "|type:%d|data_length:%ui|", frame_type, length); + break; + } + case XQC_FRAME_MAX_DATA: { uint64_t max_data = va_arg(args, uint64_t); xqc_log_implement(log, TRA_FRAMES_PROCESSED, func, @@ -391,6 +398,14 @@ xqc_log_TRA_FRAMES_PROCESSED_callback(xqc_log_t *log, const char *func, ...) va_end(args); } + +void +xqc_log_TRA_STATELESS_RESET_callback(xqc_log_t *log, const char *func, xqc_connection_t *conn) +{ + xqc_log_implement(log, TRA_DATAGRAMS_SENT, func, "|stateless reset|cid:%s", + log->scid); +} + void xqc_log_REC_PARAMETERS_SET_callback(xqc_log_t *log, const char *func, xqc_send_ctl_t *send_ctl) { @@ -497,7 +512,7 @@ xqc_log_HTTP_STREAM_TYPE_SET_callback(xqc_log_t *log, const char *func, xqc_h3_s { xqc_log_implement(log, HTTP_STREAM_TYPE_SET, func, "|%s|stream_id:%ui|stream_type:%d|", - local == XQC_LOG_LOCAL_EVENT ? "local" : "remote", h3_stream->stream->stream_id, h3_stream->type); + local == XQC_LOG_LOCAL_EVENT ? "local" : "remote", h3_stream->stream_id, h3_stream->type); } void @@ -507,7 +522,7 @@ xqc_log_HTTP_FRAME_CREATED_callback(xqc_log_t *log, const char *func, ...) va_list args; va_start(args, func); xqc_h3_stream_t *h3_stream = va_arg(args, xqc_h3_stream_t*); - xqc_stream_id_t stream_id = h3_stream->stream->stream_id; + xqc_stream_id_t stream_id = h3_stream->stream_id; xqc_h3_frm_type_t type = va_arg(args, xqc_h3_frm_type_t); switch (type) { case XQC_H3_FRM_DATA: { @@ -567,7 +582,7 @@ void xqc_log_HTTP_FRAME_PARSED_callback(xqc_log_t *log, const char *func, xqc_h3_stream_t *h3_stream) { xqc_h3_frame_t *frame = &h3_stream->pctx.frame_pctx.frame; - xqc_stream_id_t stream_id = h3_stream->stream->stream_id; + xqc_stream_id_t stream_id = h3_stream->stream_id; switch (frame->type) { case XQC_H3_FRM_DATA: case XQC_H3_FRM_HEADERS: @@ -614,7 +629,7 @@ void xqc_log_QPACK_STREAM_STATE_UPDATED_callback(xqc_log_t *log, const char *func, xqc_h3_stream_t *h3_stream) { xqc_log_implement(log, QPACK_STREAM_STATE_UPDATED, func, - "|stream_id:%ui|%s|", h3_stream->stream->stream_id, + "|stream_id:%ui|%s|", h3_stream->stream_id, h3_stream->flags & XQC_HTTP3_STREAM_FLAG_QPACK_DECODE_BLOCKED ? "blocked" : "unblocked"); } diff --git a/src/common/xqc_log_event_callback.h b/src/common/xqc_log_event_callback.h index 8aa37ca9a..ff0cf9d0e 100644 --- a/src/common/xqc_log_event_callback.h +++ b/src/common/xqc_log_event_callback.h @@ -55,6 +55,8 @@ void xqc_log_TRA_STREAM_STATE_UPDATED_callback(xqc_log_t *log, const char *func, void xqc_log_TRA_FRAMES_PROCESSED_callback(xqc_log_t *log, const char *func, ...); +void xqc_log_TRA_STATELESS_RESET_callback(xqc_log_t *log, const char *func, xqc_connection_t *c); + void xqc_log_REC_PARAMETERS_SET_callback(xqc_log_t *log, const char *func, xqc_send_ctl_t *send_ctl); void xqc_log_REC_METRICS_UPDATED_callback(xqc_log_t *log, const char *func, xqc_send_ctl_t *send_ctl); diff --git a/src/common/xqc_str.h b/src/common/xqc_str.h index 9852c194c..6c4b6f036 100644 --- a/src/common/xqc_str.h +++ b/src/common/xqc_str.h @@ -51,6 +51,21 @@ unsigned char *xqc_vsprintf(unsigned char *buf, unsigned char *last, const char unsigned char *xqc_sprintf_num(unsigned char *buf, unsigned char *last, uint64_t ui64, unsigned char zero, uintptr_t hexadecimal, uintptr_t width); +static inline int +xqc_memcpy_with_cap(void *dst, size_t cap, const void *src, size_t n) +{ + if (n == 0) { + return XQC_OK; + } + + if (n <= cap) { + xqc_memcpy(dst, src, n); + return XQC_OK; + } + + return -XQC_ENOBUF; +} + static inline void xqc_str_tolower(unsigned char *dst, unsigned char *src, size_t n) { diff --git a/src/congestion_control/xqc_cubic.c b/src/congestion_control/xqc_cubic.c index 6bce7a340..d9307d2a5 100644 --- a/src/congestion_control/xqc_cubic.c +++ b/src/congestion_control/xqc_cubic.c @@ -18,6 +18,7 @@ #define XQC_CUBIC_MAX_SSTHRESH 0xFFFFFFFF #define XQC_CUBIC_MIN_WIN (4 * XQC_CUBIC_MSS) +#define XQC_CUBIC_MAX_MIN_WIN (16 * XQC_CUBIC_MSS) #define XQC_CUBIC_MAX_INIT_WIN (100 * XQC_CUBIC_MSS) #define XQC_CUBIC_INIT_WIN (32 * XQC_CUBIC_MSS) @@ -127,12 +128,18 @@ xqc_cubic_init(void *cong_ctl, xqc_send_ctl_t *ctl_ctx, xqc_cc_params_t cc_param cubic->last_max_cwnd = XQC_CUBIC_INIT_WIN; cubic->ssthresh = XQC_CUBIC_MAX_SSTHRESH; cubic->congestion_recovery_start_time = 0; + cubic->init_cwnd = XQC_CUBIC_INIT_WIN; + cubic->min_cwnd = XQC_CUBIC_MIN_WIN; if (cc_params.customize_on) { + cc_params.min_cwnd *= XQC_CUBIC_MSS; cc_params.init_cwnd *= XQC_CUBIC_MSS; cubic->init_cwnd = cc_params.init_cwnd >= XQC_CUBIC_MIN_WIN && cc_params.init_cwnd <= XQC_CUBIC_MAX_INIT_WIN ? cc_params.init_cwnd : XQC_CUBIC_INIT_WIN; + cubic->min_cwnd = + cc_params.min_cwnd >= XQC_CUBIC_MIN_WIN && cc_params.min_cwnd <= XQC_CUBIC_MAX_MIN_WIN ? + cc_params.min_cwnd : XQC_CUBIC_MIN_WIN; } } @@ -163,7 +170,7 @@ xqc_cubic_on_lost(void *cong_ctl, xqc_usec_t lost_sent_time) /* Multiplicative Decrease */ cubic->cwnd = cubic->cwnd * XQC_CUBIC_BETA / XQC_CUBIC_BETA_SCALE; - cubic->cwnd = xqc_max(cubic->cwnd, XQC_CUBIC_MIN_WIN); + cubic->cwnd = xqc_max(cubic->cwnd, cubic->min_cwnd); cubic->tcp_cwnd = cubic->cwnd; cubic->ssthresh = cubic->cwnd; } @@ -215,9 +222,9 @@ xqc_cubic_reset_cwnd(void *cong_ctl) { xqc_cubic_t *cubic = (xqc_cubic_t *)(cong_ctl); cubic->epoch_start = 0; - cubic->cwnd = XQC_CUBIC_MIN_WIN; - cubic->tcp_cwnd = XQC_CUBIC_MIN_WIN; - cubic->last_max_cwnd = XQC_CUBIC_MIN_WIN; + cubic->cwnd = cubic->min_cwnd; + cubic->tcp_cwnd = cubic->min_cwnd; + cubic->last_max_cwnd = cubic->min_cwnd; } int32_t diff --git a/src/congestion_control/xqc_cubic.h b/src/congestion_control/xqc_cubic.h index 48b44997c..8d70fa6b9 100644 --- a/src/congestion_control/xqc_cubic.h +++ b/src/congestion_control/xqc_cubic.h @@ -11,6 +11,7 @@ #include "src/transport/xqc_packet_out.h" typedef struct { + uint32_t min_cwnd; uint64_t init_cwnd; /* initial window size in MSS */ uint64_t cwnd; /* current window size in bytes */ uint64_t tcp_cwnd; /* cwnd calculated according to Reno's algorithm */ diff --git a/src/congestion_control/xqc_unlimited_cc.c b/src/congestion_control/xqc_unlimited_cc.c new file mode 100644 index 000000000..258998482 --- /dev/null +++ b/src/congestion_control/xqc_unlimited_cc.c @@ -0,0 +1,74 @@ +/** + * @copyright Copyright (c) 2022, Alibaba Group Holding Limited + */ + +#include "src/congestion_control/xqc_unlimited_cc.h" +#include "src/common/xqc_config.h" + +size_t +xqc_unlimited_cc_size() +{ + return 0; +} + +static void +xqc_unlimited_cc_init(void *cong_ctl, xqc_send_ctl_t *ctl_ctx, xqc_cc_params_t cc_params) +{ + return; +} + + +static void +xqc_unlimited_cc_on_lost(void *cong_ctl, xqc_usec_t lost_sent_time) +{ + return; +} + + +static void +xqc_unlimited_cc_on_ack(void *cong_ctl, xqc_packet_out_t *po, xqc_usec_t now) +{ + return; +} + +uint64_t +xqc_unlimited_cc_get_cwnd(void *cong_ctl) +{ + return XQC_MAX_UINT64_VALUE; +} + +void +xqc_unlimited_cc_reset_cwnd(void *cong_ctl) +{ + return; +} + +int32_t +xqc_unlimited_cc_in_slow_start(void *cong_ctl) +{ + return 0; +} + +void +xqc_unlimited_cc_restart_from_idle(void *cong_ctl, uint64_t arg) +{ + return; +} + +static int +xqc_unlimited_cc_in_recovery(void *cong_ctl) +{ + return 0; +} + +const xqc_cong_ctrl_callback_t xqc_unlimited_cc_cb = { + .xqc_cong_ctl_size = xqc_unlimited_cc_size, + .xqc_cong_ctl_init = xqc_unlimited_cc_init, + .xqc_cong_ctl_on_lost = xqc_unlimited_cc_on_lost, + .xqc_cong_ctl_on_ack = xqc_unlimited_cc_on_ack, + .xqc_cong_ctl_get_cwnd = xqc_unlimited_cc_get_cwnd, + .xqc_cong_ctl_reset_cwnd = xqc_unlimited_cc_reset_cwnd, + .xqc_cong_ctl_in_slow_start = xqc_unlimited_cc_in_slow_start, + .xqc_cong_ctl_restart_from_idle = xqc_unlimited_cc_restart_from_idle, + .xqc_cong_ctl_in_recovery = xqc_unlimited_cc_in_recovery, +}; diff --git a/src/congestion_control/xqc_unlimited_cc.h b/src/congestion_control/xqc_unlimited_cc.h new file mode 100644 index 000000000..6ba04022a --- /dev/null +++ b/src/congestion_control/xqc_unlimited_cc.h @@ -0,0 +1,13 @@ +/** + * @copyright Copyright (c) 2022, Alibaba Group Holding Limited + */ + +#ifndef _XQC_UNLIMITED_CC_H_INCLUDED_ +#define _XQC_UNLIMITED_CC_H_INCLUDED_ + +#include +#include + +extern const xqc_cong_ctrl_callback_t xqc_unlimited_cc_cb; + +#endif diff --git a/src/http3/frame/xqc_h3_frame.c b/src/http3/frame/xqc_h3_frame.c index cb56f1d14..162c7b51a 100644 --- a/src/http3/frame/xqc_h3_frame.c +++ b/src/http3/frame/xqc_h3_frame.c @@ -45,6 +45,8 @@ xqc_h3_frm_reset_pctx(xqc_h3_frame_pctx_t *pctx) break; case XQC_H3_FRM_MAX_PUSH_ID: break; + case XQC_H3_EXT_FRM_BIDI_STREAM_TYPE: + break; case XQC_H3_FRM_UNKNOWN: break; } @@ -151,6 +153,16 @@ xqc_h3_frm_parse_max_push_id(const unsigned char *p, size_t sz, xqc_h3_frame_t * return pos - p; } +ssize_t +xqc_h3_ext_frm_parse_bidi_stream_type(const unsigned char *p, size_t sz, xqc_h3_frame_t *frame, xqc_bool_t *fin) +{ + const unsigned char *pos = p; + *fin = XQC_FALSE; + xqc_h3_ext_frame_bidi_stream_type_t *stream_type = &frame->frame_payload.stream_type; + XQC_H3_DECODE_DISCRETE_VINT_VALUE(pos, sz, stream_type->stream_type, fin); + return pos - p; +} + ssize_t xqc_h3_frm_parse_reserved(const unsigned char *p, size_t sz, xqc_h3_frame_t *frame, xqc_bool_t *fin) @@ -241,6 +253,10 @@ xqc_h3_frm_parse(const unsigned char *p, size_t sz, xqc_h3_frame_pctx_t *pctx) XQC_H3_DECODE_FRM(xqc_h3_frm_parse_max_push_id, pos, sz, pctx->frame, &fin); break; } + case XQC_H3_EXT_FRM_BIDI_STREAM_TYPE: { + XQC_H3_DECODE_FRM(xqc_h3_ext_frm_parse_bidi_stream_type, pos, sz, pctx->frame, &fin); + break; + } default: { XQC_H3_DECODE_FRM(xqc_h3_frm_parse_reserved, pos, sz, pctx->frame, &fin); break; @@ -512,3 +528,31 @@ xqc_h3_frm_write_max_push_id(xqc_list_head_t *send_buf, uint64_t push_id, uint8_ return XQC_OK; } + +xqc_int_t +xqc_h3_ext_frm_write_bidi_stream_type(xqc_list_head_t *send_buf, + uint64_t stream_type, uint8_t fin) +{ + size_t len = xqc_put_varint_len(stream_type); + xqc_var_buf_t *buf = xqc_var_buf_create(xqc_put_varint_len(XQC_H3_EXT_FRM_BIDI_STREAM_TYPE) + + xqc_put_varint_len(len) + + len); + if (buf == NULL) { + return -XQC_EMALLOC; + } + + unsigned char *pos = buf->data; + pos = xqc_put_varint(pos, XQC_H3_EXT_FRM_BIDI_STREAM_TYPE); + pos = xqc_put_varint(pos, len); + pos = xqc_put_varint(pos, stream_type); + buf->data_len = pos - buf->data; + buf->fin_flag = fin; + + xqc_int_t ret = xqc_list_buf_to_tail(send_buf, buf); + if (ret != XQC_OK) { + xqc_var_buf_free(buf); + return ret; + } + + return XQC_OK; +} diff --git a/src/http3/frame/xqc_h3_frame.h b/src/http3/frame/xqc_h3_frame.h index d85634f01..0e8811938 100644 --- a/src/http3/frame/xqc_h3_frame.h +++ b/src/http3/frame/xqc_h3_frame.h @@ -9,13 +9,14 @@ #include "src/http3/frame/xqc_h3_frame_defs.h" typedef union xqc_h3_frame_payload_s { - xqc_h3_frame_headers_t headers; - xqc_h3_frame_data_t data; - xqc_h3_frame_cancel_push_t cancel_push; - xqc_h3_frame_settings_t settings; - xqc_h3_frame_push_promise_t push_promise; - xqc_h3_frame_goaway_t goaway; - xqc_h3_frame_max_push_id_t max_push_id; + xqc_h3_frame_headers_t headers; + xqc_h3_frame_data_t data; + xqc_h3_frame_cancel_push_t cancel_push; + xqc_h3_frame_settings_t settings; + xqc_h3_frame_push_promise_t push_promise; + xqc_h3_frame_goaway_t goaway; + xqc_h3_frame_max_push_id_t max_push_id; + xqc_h3_ext_frame_bidi_stream_type_t stream_type; } xqc_h3_frame_pl_t; @@ -86,6 +87,8 @@ xqc_int_t xqc_h3_frm_write_goaway(xqc_list_head_t *send_buf, uint64_t push_id, u xqc_int_t xqc_h3_frm_write_max_push_id(xqc_list_head_t *send_buf, uint64_t push_id, uint8_t fin); +xqc_int_t xqc_h3_ext_frm_write_bidi_stream_type(xqc_list_head_t *send_buf, uint64_t stream_type, uint8_t fin); + void xqc_h3_frm_reset_pctx(xqc_h3_frame_pctx_t *pctx); #endif diff --git a/src/http3/frame/xqc_h3_frame_defs.h b/src/http3/frame/xqc_h3_frame_defs.h index 883125c9b..74f9f367d 100644 --- a/src/http3/frame/xqc_h3_frame_defs.h +++ b/src/http3/frame/xqc_h3_frame_defs.h @@ -9,14 +9,18 @@ typedef enum xqc_h3_frm_type_s { - XQC_H3_FRM_DATA = 0x00, - XQC_H3_FRM_HEADERS = 0x01, - XQC_H3_FRM_CANCEL_PUSH = 0x03, - XQC_H3_FRM_SETTINGS = 0x04, - XQC_H3_FRM_PUSH_PROMISE = 0x05, - XQC_H3_FRM_GOAWAY = 0x07, - XQC_H3_FRM_MAX_PUSH_ID = 0x0d, - XQC_H3_FRM_UNKNOWN = UINT64_MAX, + XQC_H3_FRM_DATA = 0x00, + XQC_H3_FRM_HEADERS = 0x01, + XQC_H3_FRM_CANCEL_PUSH = 0x03, + XQC_H3_FRM_SETTINGS = 0x04, + XQC_H3_FRM_PUSH_PROMISE = 0x05, + XQC_H3_FRM_GOAWAY = 0x07, + XQC_H3_FRM_MAX_PUSH_ID = 0x0d, + + /* extension */ + XQC_H3_EXT_FRM_BIDI_STREAM_TYPE = 0x20, + + XQC_H3_FRM_UNKNOWN = UINT64_MAX, } xqc_h3_frm_type_t; @@ -55,4 +59,8 @@ typedef struct xqc_h3_frm_max_push_id_t { xqc_discrete_int_pctx_t push_id; } xqc_h3_frame_max_push_id_t; +typedef struct xqc_h3_ext_frm_bidi_stream_type_s { + xqc_discrete_int_pctx_t stream_type; +} xqc_h3_ext_frame_bidi_stream_type_t; + #endif \ No newline at end of file diff --git a/src/http3/xqc_h3_conn.c b/src/http3/xqc_h3_conn.c index c9e838c09..70400c382 100644 --- a/src/http3/xqc_h3_conn.c +++ b/src/http3/xqc_h3_conn.c @@ -146,6 +146,17 @@ xqc_h3_conn_get_errno(xqc_h3_conn_t *h3_conn) } +void * +xqc_h3_conn_get_ssl(xqc_h3_conn_t *h3_conn) +{ + if (h3_conn->conn) { + return xqc_conn_get_ssl(h3_conn->conn); + } + + return NULL; +} + + void xqc_h3_conn_set_user_data(xqc_h3_conn_t *h3_conn, void *user_data) @@ -296,10 +307,13 @@ xqc_h3_conn_init_callbacks(xqc_h3_conn_t *h3c) xqc_int_t ret = xqc_h3_ctx_get_app_callbacks(&h3_cbs); if (XQC_OK != ret || h3_cbs == NULL) { xqc_log(h3c->log, XQC_LOG_ERROR, "|can't get app callbacks, not initialized?"); - return ret; + return -XQC_EFATAL; } h3c->h3_conn_callbacks = h3_cbs->h3c_cbs; + if (h3c->flags & XQC_H3_CONN_FLAG_EXT_ENABLED) { + h3c->h3_ext_dgram_callbacks = h3_cbs->h3_ext_dgram_cbs; + } return XQC_OK; } @@ -320,8 +334,14 @@ xqc_h3_conn_create(xqc_connection_t *conn, void *user_data) h3c->control_stream_out = NULL; + if (conn->engine->config->enable_h3_ext) { + h3c->flags |= XQC_H3_CONN_FLAG_EXT_ENABLED; + } + /* set callback functions from application layer to http3 layer */ - xqc_h3_conn_init_callbacks(h3c); + if (xqc_h3_conn_init_callbacks(h3c) != XQC_OK) { + h3c->flags &= ~XQC_H3_CONN_FLAG_EXT_ENABLED; + } h3c->local_h3_conn_settings = default_local_h3_conn_settings; h3c->peer_h3_conn_settings = default_peer_h3_conn_settings; @@ -463,7 +483,7 @@ xqc_h3_conn_create_uni_stream(xqc_h3_conn_t *h3c, xqc_h3_stream_type_t h3s_type) /* create transport stream */ xqc_stream_t *stream = xqc_create_stream_with_conn( - h3c->conn, XQC_UNDEFINE_STREAM_ID, stream_type, NULL); + h3c->conn, XQC_UNDEFINE_STREAM_ID, stream_type, NULL, NULL); if (!stream) { xqc_log(h3c->log, XQC_LOG_ERROR, "|xqc_create_stream_with_conn error|type:%d|", h3s_type); goto error; @@ -744,3 +764,4 @@ const xqc_conn_callbacks_t h3_conn_callbacks = { .conn_handshake_finished = xqc_h3_conn_handshake_finished, .conn_ping_acked = xqc_h3_conn_ping_acked_notify, }; + diff --git a/src/http3/xqc_h3_conn.h b/src/http3/xqc_h3_conn.h index 566068f19..30f63fe6e 100644 --- a/src/http3/xqc_h3_conn.h +++ b/src/http3/xqc_h3_conn.h @@ -57,38 +57,44 @@ typedef enum { the upper layer of h3 connection creation */ XQC_H3_CONN_FLAG_UPPER_CONN_EXIST = 1 << 7, + /* is h3 ext enabled ? */ + XQC_H3_CONN_FLAG_EXT_ENABLED = 1 << 8, + } xqc_http3_conn_flag; typedef struct xqc_h3_conn_s { /* transport contexts */ - xqc_connection_t *conn; - xqc_log_t *log; - void *user_data; + xqc_connection_t *conn; + xqc_log_t *log; + void *user_data; /* h3 connection state flags */ - uint64_t flags; + uint64_t flags; /* h3 connection callback functions for user */ - xqc_h3_conn_callbacks_t h3_conn_callbacks; + xqc_h3_conn_callbacks_t h3_conn_callbacks; + + /* h3 datagram callback functions for user */ + xqc_h3_ext_dgram_callbacks_t h3_ext_dgram_callbacks; - uint64_t max_stream_id_recvd; - uint64_t goaway_stream_id; + uint64_t max_stream_id_recvd; + uint64_t goaway_stream_id; /* qpack context */ - xqc_qpack_t *qpack; + xqc_qpack_t *qpack; /* uni-streams */ - xqc_h3_stream_t *qdec_stream; - xqc_h3_stream_t *qenc_stream; - xqc_h3_stream_t *control_stream_out; + xqc_h3_stream_t *qdec_stream; + xqc_h3_stream_t *qenc_stream; + xqc_h3_stream_t *control_stream_out; /* blocked streams */ - xqc_list_head_t block_stream_head; - uint64_t block_stream_count; + xqc_list_head_t block_stream_head; + uint64_t block_stream_count; /* h3 settings */ - xqc_h3_conn_settings_t local_h3_conn_settings; /* set by user for sending to the peer */ - xqc_h3_conn_settings_t peer_h3_conn_settings; /* receive from peer */ + xqc_h3_conn_settings_t local_h3_conn_settings; /* set by user for sending to the peer */ + xqc_h3_conn_settings_t peer_h3_conn_settings; /* receive from peer */ } xqc_h3_conn_t; diff --git a/src/http3/xqc_h3_ctx.c b/src/http3/xqc_h3_ctx.c index 26e96071b..59a72c392 100644 --- a/src/http3/xqc_h3_ctx.c +++ b/src/http3/xqc_h3_ctx.c @@ -2,9 +2,12 @@ * @copyright Copyright (c) 2022, Alibaba Group Holding Limited */ +#include #include "xqc_h3_ctx.h" #include "xqc_h3_conn.h" #include "xqc_h3_stream.h" +#include "xqc_h3_ext_dgram.h" +#include "src/transport/xqc_engine.h" /* 应用层注册回调,放到engine */ typedef struct xqc_h3_ctx_s { @@ -34,17 +37,29 @@ xqc_h3_ctx_init(xqc_engine_t *engine, xqc_h3_callbacks_t *h3_cbs) /* init http3 layer callbacks */ xqc_app_proto_callbacks_t ap_cbs = { .conn_cbs = h3_conn_callbacks, - .stream_cbs = h3_stream_callbacks + .stream_cbs = h3_stream_callbacks, }; /* register ALPN and transport layer callbacks */ - if (xqc_engine_register_alpn(engine, XQC_ALPN_H3, XQC_ALPN_H3_LEN, &ap_cbs) != XQC_OK - || xqc_engine_register_alpn(engine, XQC_ALPN_H3_29, XQC_ALPN_H3_29_LEN, &ap_cbs) != XQC_OK) + if (xqc_engine_register_alpn(engine, XQC_ALPN_H3, strlen(XQC_ALPN_H3), &ap_cbs) != XQC_OK + || xqc_engine_register_alpn(engine, XQC_ALPN_H3_29, strlen(XQC_ALPN_H3_29), &ap_cbs) != XQC_OK) { xqc_h3_ctx_destroy(engine); return -XQC_EFATAL; } + if (engine->config->enable_h3_ext) { + + ap_cbs.dgram_cbs = h3_ext_datagram_callbacks; + + /* register h3-ext ALPN */ + if (xqc_engine_register_alpn(engine, XQC_ALPN_H3_EXT, strlen(XQC_ALPN_H3_EXT), &ap_cbs) != XQC_OK) + { + xqc_h3_ctx_destroy(engine); + return -XQC_EFATAL; + } + } + return XQC_OK; } @@ -52,8 +67,9 @@ xqc_h3_ctx_init(xqc_engine_t *engine, xqc_h3_callbacks_t *h3_cbs) xqc_int_t xqc_h3_ctx_destroy(xqc_engine_t *engine) { - xqc_engine_unregister_alpn(engine, XQC_ALPN_H3_29, XQC_ALPN_H3_29_LEN); - xqc_engine_unregister_alpn(engine, XQC_ALPN_H3, XQC_ALPN_H3_LEN); + xqc_engine_unregister_alpn(engine, XQC_ALPN_H3_29, strlen(XQC_ALPN_H3_29)); + xqc_engine_unregister_alpn(engine, XQC_ALPN_H3, strlen(XQC_ALPN_H3)); + xqc_engine_unregister_alpn(engine, XQC_ALPN_H3_EXT, strlen(XQC_ALPN_H3_EXT)); if (h3_ctx) { xqc_free(h3_ctx); diff --git a/src/http3/xqc_h3_defs.c b/src/http3/xqc_h3_defs.c index 32f842e4b..b2d724568 100644 --- a/src/http3/xqc_h3_defs.c +++ b/src/http3/xqc_h3_defs.c @@ -12,3 +12,10 @@ const char *const xqc_h3_alpn[] = { [XQC_IDRAFT_VER_29] = XQC_ALPN_H3_29, /* draft-29 */ [XQC_IDRAFT_VER_NEGOTIATION] = "", }; + +const char *const xqc_h3_ext_alpn[] = { + [XQC_IDRAFT_INIT_VER] = "", /* placeholder */ + [XQC_VERSION_V1] = XQC_ALPN_H3_EXT, /* QUIC v1 */ + [XQC_IDRAFT_VER_29] = "", /* draft-29 */ + [XQC_IDRAFT_VER_NEGOTIATION] = "", +}; \ No newline at end of file diff --git a/src/http3/xqc_h3_defs.h b/src/http3/xqc_h3_defs.h index 818d461c5..c0fb93ba2 100644 --- a/src/http3/xqc_h3_defs.h +++ b/src/http3/xqc_h3_defs.h @@ -5,13 +5,14 @@ #ifndef _XQC_H3_DEFS_H_ #define _XQC_H3_DEFS_H_ +#include + /** * ALPN definitions */ -#define XQC_ALPN_H3 "h3" -#define XQC_ALPN_H3_LEN 2 -#define XQC_ALPN_H3_29 "h3-29" -#define XQC_ALPN_H3_29_LEN 5 +#define XQC_ALPN_H3 XQC_DEFINED_ALPN_H3 +#define XQC_ALPN_H3_29 XQC_DEFINED_ALPN_H3_29 +#define XQC_ALPN_H3_EXT XQC_DEFINED_ALPN_H3_EXT extern const char *const xqc_h3_alpn[]; diff --git a/src/http3/xqc_h3_ext_bytestream.c b/src/http3/xqc_h3_ext_bytestream.c new file mode 100644 index 000000000..070d46acf --- /dev/null +++ b/src/http3/xqc_h3_ext_bytestream.c @@ -0,0 +1,725 @@ +#include +#include +#include "src/transport/xqc_engine.h" +#include "src/transport/xqc_stream.h" +#include "src/common/xqc_log.h" +#include "src/http3/xqc_h3_ext_bytestream.h" +#include "src/http3/xqc_h3_stream.h" +#include "src/common/xqc_common.h" +#include "src/http3/xqc_h3_ctx.h" + +typedef enum { + XQC_H3_EXT_BYTESTREAM_FLAG_FIN_RCVD = 1, + XQC_H3_EXT_BYTESTREAM_FLAG_FIN_SENT = 1 << 1, + XQC_H3_EXT_BYTESTREAM_FLAG_FIN_READ = 1 << 2, + XQC_H3_EXT_BYTESTREAM_FLAG_UPPER_LAYER_EXIST = 1 << 3, +} xqc_h3_ext_bytestream_flag_t; + +typedef struct xqc_h3_ext_bytestream_s { + + /* h3 stream handler */ + xqc_h3_stream_t *h3_stream; + + /* user data for request callback */ + void *user_data; + + /* bytestream callback */ + xqc_h3_ext_bytestream_callbacks_t *bs_callbacks; + + /* flag */ + xqc_h3_ext_bytestream_flag_t flag; + + /* received body buf list and statistic information */ + xqc_list_head_t data_buf_list; + uint64_t data_buf_cnt; + + /* var_buf */ + xqc_var_buf_t *msg_buf; + + /* statistic */ + size_t bytes_rcvd; + size_t bytes_sent; + const char *stream_close_msg; + xqc_usec_t create_time; + xqc_usec_t fin_rcvd_time; + xqc_usec_t fin_read_time; + xqc_usec_t fin_sent_time; + xqc_usec_t fin_acked_time; + xqc_usec_t first_byte_sent_time; + xqc_usec_t first_byte_rcvd_time; + +} xqc_h3_ext_bytestream_t; + +xqc_h3_ext_bytestream_data_buf_t* +xqc_h3_ext_bytestream_create_data_buf(xqc_h3_frame_pctx_t *pctx) +{ + xqc_h3_ext_bytestream_data_buf_t *new_buf = xqc_calloc(1, sizeof(xqc_h3_ext_bytestream_data_buf_t)); + if (new_buf) { + xqc_init_list_head(&new_buf->list); + xqc_init_list_head(&new_buf->buf_list); + new_buf->total_len = pctx->frame.len; + new_buf->curr_len = 0; + } + return new_buf; +} + +void +xqc_h3_ext_bytestream_free_data_buf(xqc_h3_ext_bytestream_data_buf_t *buf) +{ + if (buf) { + xqc_list_buf_list_free(&buf->buf_list); + xqc_list_del_init(&buf->list); + xqc_free(buf); + } +} + +xqc_h3_ext_bytestream_data_buf_t* +xqc_h3_ext_bytestream_get_last_data_buf(xqc_h3_ext_bytestream_t *bs, xqc_h3_frame_pctx_t *pctx) +{ + if (bs->data_buf_cnt > 0) { + xqc_list_head_t *tail_node; + xqc_h3_ext_bytestream_data_buf_t *tail_buf; + tail_node = bs->data_buf_list.prev; + tail_buf = xqc_list_entry(tail_node, xqc_h3_ext_bytestream_data_buf_t, list); + if (tail_buf->curr_len < tail_buf->total_len) { + return tail_buf; + } + } + + /* create a new buf */ + xqc_h3_ext_bytestream_data_buf_t *new_buf = xqc_h3_ext_bytestream_create_data_buf(pctx); + if (new_buf) { + bs->data_buf_cnt++; + xqc_list_add_tail(&new_buf->list, &bs->data_buf_list); + new_buf->start_time = xqc_monotonic_timestamp(); + } + return new_buf; +} + +xqc_int_t +xqc_h3_ext_bytestream_save_data_to_buf(xqc_h3_ext_bytestream_data_buf_t *buf, + const uint8_t *data, size_t data_len) +{ + /* sanity check */ + if (buf->curr_len + data_len > buf->total_len) { + return -XQC_H3_DECODE_ERROR; + } + + xqc_int_t ret = XQC_OK; + xqc_var_buf_t *v_buf = xqc_var_buf_create_with_limit(data_len, data_len); + if (!v_buf) { + return -XQC_EMALLOC; + } + + ret = xqc_list_buf_to_tail(&buf->buf_list, v_buf); + if (ret != XQC_OK) { + xqc_var_buf_free(v_buf); + return -XQC_EMALLOC; + } + + xqc_var_buf_save_data(v_buf, data, data_len); + buf->curr_len += data_len; + buf->buf_cnt++; + + return XQC_OK; +} + +xqc_var_buf_t* +xqc_h3_ext_bytestream_data_buf_merge(xqc_h3_ext_bytestream_data_buf_t *buf) +{ + xqc_var_buf_t *v_buf = NULL; + xqc_list_buf_t *segment; + + /* optimization: reduce memcpy for short messages */ + if (buf->buf_cnt == 1) { + segment = xqc_list_entry(buf->buf_list.next, xqc_list_buf_t, list_head); + v_buf = segment->buf; + segment->buf = NULL; + xqc_list_buf_free(segment); + buf->buf_cnt--; + return v_buf; + } + + v_buf = xqc_var_buf_create_with_limit(buf->total_len, buf->total_len); + if (!v_buf) { + return NULL; + } + + xqc_list_head_t *pos, *next; + xqc_list_for_each_safe(pos, next, &buf->buf_list) { + segment = xqc_list_entry(pos, xqc_list_buf_t, list_head); + xqc_var_buf_save_data(v_buf, segment->buf->data, segment->buf->data_len); + xqc_list_buf_free(segment); + buf->buf_cnt--; + } + return v_buf; +} + +xqc_int_t +xqc_h3_ext_bytestream_init_callbacks(xqc_h3_ext_bytestream_t *bs) +{ + xqc_h3_callbacks_t *h3_cbs = NULL; + xqc_int_t ret = xqc_h3_ctx_get_app_callbacks(&h3_cbs); + if (XQC_OK != ret || h3_cbs == NULL) { + xqc_log(bs->h3_stream->log, XQC_LOG_ERROR, "|can't get app callbacks, not registered ?|"); + return ret; + } + + bs->bs_callbacks = &h3_cbs->h3_ext_bs_cbs; + return XQC_OK; +} + +xqc_h3_ext_bytestream_t* +xqc_h3_ext_bytestream_create_inner(xqc_h3_conn_t *h3_conn, + xqc_h3_stream_t *h3_stream, void *user_data) +{ + xqc_h3_ext_bytestream_t *bs; + bs = xqc_calloc(1, sizeof(xqc_h3_ext_bytestream_t)); + if (!bs) { + xqc_log(h3_conn->log, XQC_LOG_ERROR, "|xqc_calloc error|"); + return NULL; + } + + if (xqc_h3_ext_bytestream_init_callbacks(bs) != XQC_OK) { + xqc_free(bs); + return NULL; + } + + bs->h3_stream = h3_stream; + bs->user_data = user_data; + bs->flag = 0; + bs->create_time = xqc_monotonic_timestamp(); + bs->msg_buf = NULL; + + xqc_init_list_head(&bs->data_buf_list); + bs->data_buf_cnt = 0; + + return bs; +} + + +xqc_h3_ext_bytestream_t* +xqc_h3_ext_bytestream_create(xqc_engine_t *engine, + const xqc_cid_t *cid, void *user_data) +{ + xqc_stream_t *stream; + xqc_h3_stream_t *h3_stream; + xqc_h3_ext_bytestream_t *h3_ext_bs; + xqc_h3_conn_t *h3_conn; + int ret; + + stream = xqc_stream_create(engine, cid, NULL, NULL); + if (!stream) { + xqc_log(engine->log, XQC_LOG_ERROR, "|xqc_stream_create error|"); + return NULL; + } + + h3_conn = (xqc_h3_conn_t*)stream->stream_conn->proto_data; + + if (!(h3_conn->flags & XQC_H3_CONN_FLAG_EXT_ENABLED)) { + // it is safe to destroy the stream here, as it is not notified to upper layer. + xqc_destroy_stream(stream); + xqc_log(engine->log, XQC_LOG_ERROR, "|try to create bytestream while it is disabled on the connection|"); + return NULL; + } + + h3_stream = xqc_h3_stream_create(h3_conn, stream, XQC_H3_STREAM_TYPE_BYTESTEAM, user_data); + if (!h3_stream) { + // it is safe to destroy the stream here, as it is not notified to upper layer. + xqc_destroy_stream(stream); + xqc_log(engine->log, XQC_LOG_ERROR, "|xqc_h3_stream_create error|"); + return NULL; + } + + h3_ext_bs = xqc_h3_ext_bytestream_create_inner(h3_conn, h3_stream, user_data); + if (!h3_ext_bs) { + // it is safe to destroy the stream here, as it is not notified to upper layer. + xqc_destroy_stream(stream); + // the h3_stream will be destroyed in the close_notify triggered by xqc_destroy_stream + xqc_log(engine->log, XQC_LOG_ERROR, "|xqc_h3_ext_bytestream_create_inner error|"); + return NULL; + } + + h3_stream->h3_ext_bs = h3_ext_bs; + + // the initiator of the bytestream should send bidi_stream_type frame + if (xqc_h3_stream_send_bidi_stream_type(h3_stream, XQC_H3_BIDI_STREAM_TYPE_BYTESTREAM, 0) != XQC_OK) { + // it is safe to destroy the stream here, as it is not notified to upper layer. + xqc_destroy_stream(stream); + // the h3_stream will be destroyed in the close_notify triggered by xqc_destroy_stream + // the h3_ext_bytestream will be destroyed when h3_stream is destroyed + xqc_log(engine->log, XQC_LOG_ERROR, "|send bidi_stream_type frame error|"); + return NULL; + } + + if (h3_ext_bs->bs_callbacks->bs_create_notify + && !(h3_ext_bs->flag & XQC_H3_EXT_BYTESTREAM_FLAG_UPPER_LAYER_EXIST)) + { + ret = h3_ext_bs->bs_callbacks->bs_create_notify(h3_ext_bs, h3_ext_bs->user_data); + if (ret < 0) { + xqc_log(engine->log, XQC_LOG_INFO, "|app create callback error|"); + } + + } + + h3_ext_bs->flag |= XQC_H3_EXT_BYTESTREAM_FLAG_UPPER_LAYER_EXIST; + + xqc_log(engine->log, XQC_LOG_DEBUG, "|success|stream_id:%ui|conn:%p|conn_state:%s|flag:%s|", + h3_stream->stream_id, h3_conn->conn, xqc_conn_state_2_str(h3_conn->conn->conn_state), + xqc_conn_flag_2_str(h3_conn->conn->conn_flag)); + + return h3_ext_bs; +} + +xqc_h3_ext_bytestream_t* +xqc_h3_ext_bytestream_create_passive(xqc_h3_conn_t *h3_conn, + xqc_h3_stream_t *h3_stream, void *user_data) +{ + xqc_h3_ext_bytestream_t *h3_ext_bs = xqc_h3_ext_bytestream_create_inner(h3_conn, h3_stream, user_data); + int ret; + if (!h3_ext_bs) { + xqc_log(h3_conn->log, XQC_LOG_ERROR, "|xqc_h3_ext_bytestream_create_inner error|"); + return NULL; + } + + h3_stream->h3_ext_bs = h3_ext_bs; + + if (h3_ext_bs->bs_callbacks->bs_create_notify + && !(h3_ext_bs->flag & XQC_H3_EXT_BYTESTREAM_FLAG_UPPER_LAYER_EXIST)) + { + + ret = h3_ext_bs->bs_callbacks->bs_create_notify(h3_ext_bs, h3_ext_bs->user_data); + if (ret < 0) { + xqc_log(h3_conn->log, XQC_LOG_INFO, "|app create notify error|%d|", ret); + } + } + + h3_ext_bs->flag |= XQC_H3_EXT_BYTESTREAM_FLAG_UPPER_LAYER_EXIST; + + return h3_ext_bs; +} + +void +xqc_h3_ext_bytestream_destroy(xqc_h3_ext_bytestream_t *bs) +{ + xqc_h3_stream_t *h3s = bs->h3_stream; + + /* print request statistic log */ + xqc_h3_ext_bytestream_stats_t stats = xqc_h3_ext_bytestream_get_stats(bs); + + xqc_log(h3s->log, XQC_LOG_REPORT, "|stream_id:%ui|close_msg:%s|err:%d" + "|bytes_sent:%uz|bytes_rcvd:%uz|create_time:%ui|fb_sent_delay:%ui|fb_rcvd_delay:%ui" + "|fin_sent_delay:%ui|fin_acked_delay:%ui|fin_rcvd_delay:%ui|", + xqc_h3_ext_bytestream_id(bs), stats.stream_close_msg ? : "", + stats.stream_err, stats.bytes_sent, stats.bytes_rcvd, + stats.create_time, + xqc_calc_delay(stats.first_byte_sent_time, stats.create_time), + xqc_calc_delay(stats.first_byte_rcvd_time, stats.create_time), + xqc_calc_delay(stats.fin_sent_time, stats.create_time), + xqc_calc_delay(stats.fin_acked_time, stats.create_time), + xqc_calc_delay(stats.fin_rcvd_time, stats.create_time)); + + if (bs->bs_callbacks->bs_close_notify + && (bs->flag & XQC_H3_EXT_BYTESTREAM_FLAG_UPPER_LAYER_EXIST)) + { + bs->bs_callbacks->bs_close_notify(bs, bs->user_data); + bs->flag &= ~XQC_H3_EXT_BYTESTREAM_FLAG_UPPER_LAYER_EXIST; + } + + xqc_list_head_t *pos, *next; + xqc_h3_ext_bytestream_data_buf_t *buf; + xqc_list_for_each_safe(pos, next, &bs->data_buf_list) { + buf = xqc_list_entry(pos, xqc_h3_ext_bytestream_data_buf_t, list); + xqc_h3_ext_bytestream_free_data_buf(buf); + } + + if (bs->msg_buf) { + xqc_var_buf_free(bs->msg_buf); + } + + xqc_free(bs); +} + +xqc_int_t +xqc_h3_ext_bytestream_close(xqc_h3_ext_bytestream_t *h3_ext_bs) +{ + xqc_connection_t *conn = h3_ext_bs->h3_stream->h3c->conn; + xqc_h3_stream_t *h3s = h3_ext_bs->h3_stream; + + xqc_int_t ret = xqc_h3_stream_close(h3s); + if (ret) { + xqc_log(conn->log, XQC_LOG_ERROR, "|fail|ret:%d|stream_id:%ui|conn:%p|conn_state:%s|" + "flag:%s|", ret, h3s->stream_id, conn, xqc_conn_state_2_str(conn->conn_state), + xqc_conn_flag_2_str(conn->conn_flag)); + return ret; + } + + xqc_log(conn->log, XQC_LOG_DEBUG, "|success|stream_id:%ui|conn:%p|conn_state:%s|flag:%s|", + h3s->stream_id, conn, xqc_conn_state_2_str(conn->conn_state), + xqc_conn_flag_2_str(conn->conn_flag)); + + return XQC_OK; +} + + +ssize_t +xqc_h3_ext_bytestream_finish(xqc_h3_ext_bytestream_t *h3_ext_bs) +{ + xqc_int_t ret = 0; + if (h3_ext_bs->flag & XQC_H3_EXT_BYTESTREAM_FLAG_FIN_SENT) { + xqc_log(h3_ext_bs->h3_stream->log, XQC_LOG_DEBUG, "|already sent fin|"); + return XQC_OK; + } + ret = xqc_h3_ext_bytestream_send(h3_ext_bs, NULL, 0, 1, XQC_DATA_QOS_HIGHEST); + if (ret == 0) { + xqc_log(h3_ext_bs->h3_stream->log, XQC_LOG_DEBUG, "|send pure fin|"); + } + + return ret; +} + + +void +xqc_h3_ext_bytestream_set_user_data(xqc_h3_ext_bytestream_t *h3_ext_bs, + void *user_data) +{ + h3_ext_bs->user_data = user_data; +} + + + +void * +xqc_h3_ext_bytestream_get_user_data(xqc_h3_ext_bytestream_t *h3_ext_bs) +{ + return h3_ext_bs->user_data; +} + + +void +xqc_h3_ext_bytestream_save_stats_from_stream(xqc_h3_ext_bytestream_t *bs, + xqc_stream_t *stream) +{ + bs->fin_acked_time = stream->stream_stats.first_fin_ack_time; + bs->stream_close_msg = stream->stream_close_msg; +} + + +xqc_h3_ext_bytestream_stats_t +xqc_h3_ext_bytestream_get_stats(xqc_h3_ext_bytestream_t *h3_ext_bs) +{ + xqc_h3_ext_bytestream_stats_t stats; + stats.create_time = h3_ext_bs->create_time; + stats.bytes_rcvd = h3_ext_bs->bytes_rcvd; + stats.bytes_sent = h3_ext_bs->bytes_sent; + stats.fin_sent_time = h3_ext_bs->fin_sent_time; + stats.fin_acked_time = h3_ext_bs->fin_acked_time; + stats.fin_rcvd_time = h3_ext_bs->fin_rcvd_time; + stats.fin_read_time = h3_ext_bs->fin_read_time; + stats.first_byte_rcvd_time = h3_ext_bs->first_byte_rcvd_time; + stats.first_byte_sent_time = h3_ext_bs->first_byte_sent_time; + stats.stream_close_msg = h3_ext_bs->stream_close_msg; + stats.stream_err = xqc_h3_stream_get_err(h3_ext_bs->h3_stream); + return stats; +} + + +ssize_t +xqc_h3_ext_bytestream_send(xqc_h3_ext_bytestream_t *h3_ext_bs, + unsigned char *data, size_t data_size, uint8_t fin, + xqc_data_qos_level_t qos_level) +{ + /* data_size is allowed if it's fin only */ + if (data_size > 0 && data == NULL) { + return -XQC_H3_EPARAM; + } + + if (h3_ext_bs->flag & XQC_H3_EXT_BYTESTREAM_FLAG_FIN_SENT) { + xqc_log(h3_ext_bs->h3_stream->log, XQC_LOG_WARN, + "|send data after FIN sent|stream_id:%ui|", + xqc_h3_ext_bytestream_id(h3_ext_bs)); + return -XQC_H3_BYTESTREAM_FIN_SENT; + } + + if (h3_ext_bs->msg_buf) { + xqc_log(h3_ext_bs->h3_stream->log, XQC_LOG_DEBUG, + "|msg_buf_has_blocked_data|stream_id:%ui|data_size:%uz|fin:%ud|", + h3_ext_bs->h3_stream->stream_id, data_size, (unsigned int)fin); + return -XQC_EAGAIN; + } + + ssize_t sent = xqc_h3_stream_send_data(h3_ext_bs->h3_stream, data, data_size, fin); + + if (sent < 0 && sent != -XQC_EAGAIN) { + xqc_log(h3_ext_bs->h3_stream->log, XQC_LOG_ERROR, + "|xqc_h3_stream_send_data error|stream_id:%ui|ret:%z|data_size:%z|fin:%d|", + h3_ext_bs->h3_stream->stream_id, sent, data_size, fin); + return sent; + } + + if (sent >= 0) { + h3_ext_bs->bytes_sent += sent; + if (fin && sent == data_size) { + xqc_h3_ext_bytestream_fin_sent(h3_ext_bs); + xqc_h3_ext_bytestream_set_fin_sent_flag(h3_ext_bs); + } + + xqc_h3_ext_bytestream_send_begin(h3_ext_bs); + } + + if ((sent == -XQC_EAGAIN) || (sent >= 0 && sent != data_size)) { + xqc_log(h3_ext_bs->h3_stream->log, XQC_LOG_DEBUG, + "|xqc_h3_stream_send_data blocked|stream_id:%ui|data_size:%uz|fin:%ud|", + h3_ext_bs->h3_stream->stream_id, data_size, (unsigned int)fin); + /* create a msg buffer for the current msg */ + if (h3_ext_bs->msg_buf) { + xqc_log(h3_ext_bs->h3_stream->log, XQC_LOG_ERROR, + "|msg_buf_already_exist|stream_id:%ui|data_size:%uz|fin:%ud|sent:%z|", + h3_ext_bs->h3_stream->stream_id, data_size, (unsigned int)fin, sent); + XQC_H3_CONN_ERR(h3_ext_bs->h3_stream->h3c, H3_INTERNAL_ERROR, -XQC_H3_BYTESTREAM_MSG_BUF_EXIST); + return -XQC_H3_BYTESTREAM_MSG_BUF_EXIST; + } + + sent = sent == -XQC_EAGAIN ? 0 : sent; + h3_ext_bs->msg_buf = xqc_var_buf_create(data_size - sent); + + if (!h3_ext_bs->msg_buf) { + xqc_log(h3_ext_bs->h3_stream->log, XQC_LOG_ERROR, + "|malloc_msg_buffer_failed|stream_id:%ui|data_size:%uz|fin:%ud|buf_sz:%z|", + h3_ext_bs->h3_stream->stream_id, data_size, (unsigned int)fin, data_size - sent); + XQC_H3_CONN_ERR(h3_ext_bs->h3_stream->h3c, H3_INTERNAL_ERROR, -XQC_H3_EMALLOC); + return -XQC_H3_EMALLOC; + } + + xqc_var_buf_save_data(h3_ext_bs->msg_buf, data + sent, data_size - sent); + h3_ext_bs->msg_buf->fin_flag = fin; + + return data_size; + } + + xqc_log(h3_ext_bs->h3_stream->log, XQC_LOG_DEBUG, "|stream_id:%ui|data_size:%uz|sent:%z|" + "total_bytes_sent:%uz|fin:%ud|conn:%p|", + xqc_h3_ext_bytestream_id(h3_ext_bs), data_size, sent, h3_ext_bs->bytes_sent, + (unsigned int)fin, h3_ext_bs->h3_stream->h3c->conn); + + return sent; +} + +xqc_stream_id_t +xqc_h3_ext_bytestream_id(xqc_h3_ext_bytestream_t *h3_ext_bs) +{ + return h3_ext_bs->h3_stream->stream_id; +} + + +xqc_h3_conn_t* +xqc_h3_ext_bytestream_get_h3_conn(xqc_h3_ext_bytestream_t *h3_ext_bs) +{ + return h3_ext_bs->h3_stream->h3c; +} + +xqc_int_t +xqc_h3_ext_bytestream_append_data_buf(xqc_h3_ext_bytestream_t *bs, + xqc_var_buf_t *buf) +{ + xqc_int_t ret = XQC_OK; + ret = xqc_list_buf_to_tail(&bs->data_buf_list, buf); + if (ret == XQC_OK) { + bs->data_buf_cnt++; + } + return ret; +} + +#define XQC_H3_EXT_BYTESTREAM_RECORD_TIME(a) \ + if ((a) == 0) { \ + (a) = xqc_monotonic_timestamp(); \ + } \ + +void +xqc_h3_ext_bytestream_recv_begin(xqc_h3_ext_bytestream_t *bs) +{ + XQC_H3_EXT_BYTESTREAM_RECORD_TIME(bs->first_byte_rcvd_time); +} + +void +xqc_h3_ext_bytestream_send_begin(xqc_h3_ext_bytestream_t *bs) +{ + XQC_H3_EXT_BYTESTREAM_RECORD_TIME(bs->first_byte_sent_time); +} + +void +xqc_h3_ext_bytestream_fin_rcvd(xqc_h3_ext_bytestream_t *bs) +{ + XQC_H3_EXT_BYTESTREAM_RECORD_TIME(bs->fin_rcvd_time); +} + +void +xqc_h3_ext_bytestream_fin_read(xqc_h3_ext_bytestream_t *bs) +{ + XQC_H3_EXT_BYTESTREAM_RECORD_TIME(bs->fin_read_time); +} + +void +xqc_h3_ext_bytestream_fin_sent(xqc_h3_ext_bytestream_t *bs) +{ + XQC_H3_EXT_BYTESTREAM_RECORD_TIME(bs->fin_sent_time); +} + + +xqc_int_t +xqc_h3_ext_bytestream_notify_write(xqc_h3_ext_bytestream_t *bs) +{ + xqc_int_t ret = XQC_OK; + xqc_bool_t msg_buf_done = XQC_FALSE; + + /* send msg buf first */ + if (bs->msg_buf) { + ssize_t sent = xqc_h3_stream_send_data(bs->h3_stream, + bs->msg_buf->data + bs->msg_buf->consumed_len, + bs->msg_buf->data_len - bs->msg_buf->consumed_len, + bs->msg_buf->fin_flag); + if (XQC_UNLIKELY(sent == -XQC_EAGAIN)) { + return XQC_OK; + + } else if (XQC_UNLIKELY(sent < 0)) { + xqc_log(bs->h3_stream->h3c->log, XQC_LOG_ERROR, + "|send_msg_buf_err|stream_id:%ui|msg_sz:%z|fin_flag:%d|ret:%z|", + xqc_h3_ext_bytestream_id(bs), + bs->msg_buf->data_len - bs->msg_buf->consumed_len, + bs->msg_buf->fin_flag, + sent); + return sent; + } + + bs->msg_buf->consumed_len += sent; + bs->bytes_sent += sent; + + if (bs->msg_buf->data_len == bs->msg_buf->consumed_len) { + if (bs->msg_buf->fin_flag) { + xqc_h3_ext_bytestream_fin_sent(bs); + xqc_h3_ext_bytestream_set_fin_sent_flag(bs); + } + xqc_var_buf_free(bs->msg_buf); + bs->msg_buf = NULL; + } + + xqc_h3_ext_bytestream_send_begin(bs); + } + + if (bs->bs_callbacks->bs_write_notify + && (bs->flag & XQC_H3_EXT_BYTESTREAM_FLAG_UPPER_LAYER_EXIST)) + { + ret = bs->bs_callbacks->bs_write_notify(bs, bs->user_data); + } + return ret; +} + +void +xqc_h3_ext_bytestream_set_fin_sent_flag(xqc_h3_ext_bytestream_t *bs) +{ + bs->flag |= XQC_H3_EXT_BYTESTREAM_FLAG_FIN_SENT; +} + +void +xqc_h3_ext_bytestream_set_fin_rcvd_flag(xqc_h3_ext_bytestream_t *bs) +{ + bs->flag |= XQC_H3_EXT_BYTESTREAM_FLAG_FIN_RCVD; +} + +xqc_bool_t +xqc_h3_ext_bytestream_should_notify_read(xqc_h3_ext_bytestream_t *bs) +{ + if(!xqc_list_empty(&bs->data_buf_list) + || ((bs->flag & XQC_H3_EXT_BYTESTREAM_FLAG_FIN_RCVD) + && !(bs->flag & XQC_H3_EXT_BYTESTREAM_FLAG_FIN_READ))) + { + return XQC_TRUE; + } + return XQC_FALSE; +} + +xqc_int_t +xqc_h3_ext_bytestream_notify_read(xqc_h3_ext_bytestream_t *bs) +{ + xqc_int_t ret = XQC_OK; + xqc_list_head_t *node, *next; + xqc_h3_ext_bytestream_data_buf_t *buf; + xqc_var_buf_t *merged_buf; + size_t data_sz; + uint8_t fin = 0; + + xqc_list_for_each_safe(node, next, &bs->data_buf_list) { + buf = xqc_list_entry(node, xqc_h3_ext_bytestream_data_buf_t, list); + + if (buf->end_time) { + if (bs->data_buf_cnt == 1 + && (bs->flag & XQC_H3_EXT_BYTESTREAM_FLAG_FIN_RCVD)) + { + fin = 1; + bs->flag |= XQC_H3_EXT_BYTESTREAM_FLAG_FIN_READ; + xqc_h3_ext_bytestream_fin_read(bs); + } + ret = XQC_OK; + data_sz = buf->total_len; + + merged_buf = xqc_h3_ext_bytestream_data_buf_merge(buf); + if (merged_buf) { + if (bs->bs_callbacks->bs_read_notify + && (bs->flag & XQC_H3_EXT_BYTESTREAM_FLAG_UPPER_LAYER_EXIST)) + { + ret = bs->bs_callbacks->bs_read_notify(bs, merged_buf->data, + merged_buf->data_len, + fin, + bs->user_data, + buf->end_time - buf->start_time); + } + + + bs->bytes_rcvd += data_sz; + xqc_log(bs->h3_stream->h3c->log, XQC_LOG_DEBUG, + "|msg_read_notify|stream_id:%ui|msg_sz:%z|fin:%d|rcv_start:%ui|rcv_end:%ui|", + xqc_h3_ext_bytestream_id(bs), data_sz, fin, buf->start_time, buf->end_time); + + } else { + ret = -XQC_EMALLOC; + xqc_log(bs->h3_stream->h3c->log, XQC_LOG_ERROR, "|merge_data_buf_error|"); + } + + xqc_h3_ext_bytestream_free_data_buf(buf); + bs->data_buf_cnt--; + if (ret < 0) { + xqc_log(bs->h3_stream->h3c->log, XQC_LOG_ERROR, "|bs_read_notify_err|stream_id:%ui|ret:%d|data_sz:%z|fin:%d|", + xqc_h3_ext_bytestream_id(bs), ret, data_sz, fin); + return ret; + } + } + } + + /* fin only */ + if (bs->data_buf_cnt == 0 + && ((bs->flag & XQC_H3_EXT_BYTESTREAM_FLAG_FIN_RCVD) + && !(bs->flag & XQC_H3_EXT_BYTESTREAM_FLAG_FIN_READ))) + { + bs->flag |= XQC_H3_EXT_BYTESTREAM_FLAG_FIN_READ; + xqc_h3_ext_bytestream_fin_read(bs); + ret = XQC_OK; + + if (bs->bs_callbacks->bs_read_notify + && (bs->flag & XQC_H3_EXT_BYTESTREAM_FLAG_UPPER_LAYER_EXIST)) + { + ret = bs->bs_callbacks->bs_read_notify(bs, NULL, + 0, + 1, + bs->user_data, + bs->fin_read_time - bs->fin_rcvd_time); + } + xqc_log(bs->h3_stream->h3c->log, XQC_LOG_DEBUG, + "|pure_fin_read_notify|stream_id:%ui|fin_rcv:%ui|fin_read:%ui|", + xqc_h3_ext_bytestream_id(bs), bs->fin_rcvd_time, bs->fin_read_time); + if (ret < 0) { + xqc_log(bs->h3_stream->h3c->log, XQC_LOG_ERROR, "|bs_read_notify_app_err|stream_id:%ui|ret:%d|data_sz:0|fin:1|", + xqc_h3_ext_bytestream_id(bs), ret); + } + } + + return ret; +} \ No newline at end of file diff --git a/src/http3/xqc_h3_ext_bytestream.h b/src/http3/xqc_h3_ext_bytestream.h new file mode 100644 index 000000000..b0b76b624 --- /dev/null +++ b/src/http3/xqc_h3_ext_bytestream.h @@ -0,0 +1,58 @@ +/** + * @copyright Copyright (c) 2022, Alibaba Group Holding Limited + */ + +#ifndef _XQC_H3_EXT_BYTESTREAM_H_INCLUDED_ +#define _XQC_H3_EXT_BYTESTREAM_H_INCLUDED_ + +#include +#include +#include "src/http3/xqc_h3_conn.h" +#include "src/http3/xqc_h3_stream.h" + +typedef struct xqc_h3_ext_bytestream_data_buf_s { + xqc_list_head_t list; + xqc_list_head_t buf_list; + uint32_t buf_cnt; + uint64_t total_len; + uint64_t curr_len; + xqc_usec_t start_time; + xqc_usec_t end_time; +} xqc_h3_ext_bytestream_data_buf_t; + +typedef struct xqc_h3_ext_bytestream_s xqc_h3_ext_bytestream_t; + +void xqc_h3_ext_bytestream_destroy(xqc_h3_ext_bytestream_t *bs); + +xqc_int_t xqc_h3_ext_bytestream_notify_write(xqc_h3_ext_bytestream_t *bs); + +xqc_h3_ext_bytestream_t *xqc_h3_ext_bytestream_create_passive( + xqc_h3_conn_t *h3_conn, xqc_h3_stream_t *h3_stream, void *user_data); + +xqc_int_t xqc_h3_ext_bytestream_append_data_buf(xqc_h3_ext_bytestream_t *bs, + xqc_var_buf_t *buf); + +xqc_h3_ext_bytestream_data_buf_t* xqc_h3_ext_bytestream_get_last_data_buf(xqc_h3_ext_bytestream_t *bs, xqc_h3_frame_pctx_t *pctx); +xqc_int_t xqc_h3_ext_bytestream_save_data_to_buf(xqc_h3_ext_bytestream_data_buf_t *buf, + const uint8_t *data, size_t data_len); + +void xqc_h3_ext_bytestream_set_fin_sent_flag(xqc_h3_ext_bytestream_t *bs); +void xqc_h3_ext_bytestream_set_fin_rcvd_flag(xqc_h3_ext_bytestream_t *bs); + +xqc_bool_t xqc_h3_ext_bytestream_should_notify_read(xqc_h3_ext_bytestream_t *bs); + +xqc_int_t xqc_h3_ext_bytestream_notify_read(xqc_h3_ext_bytestream_t *bs); + +void xqc_h3_ext_bytestream_save_stats_from_stream(xqc_h3_ext_bytestream_t *bs, + xqc_stream_t *stream); + +/* to record performance statistics */ +void xqc_h3_ext_bytestream_recv_begin(xqc_h3_ext_bytestream_t *bs); +void xqc_h3_ext_bytestream_send_begin(xqc_h3_ext_bytestream_t *bs); +void xqc_h3_ext_bytestream_fin_rcvd(xqc_h3_ext_bytestream_t *bs); +void xqc_h3_ext_bytestream_fin_read(xqc_h3_ext_bytestream_t *bs); +void xqc_h3_ext_bytestream_fin_sent(xqc_h3_ext_bytestream_t *bs); + + + +#endif \ No newline at end of file diff --git a/src/http3/xqc_h3_ext_dgram.c b/src/http3/xqc_h3_ext_dgram.c new file mode 100644 index 000000000..bd816c4b8 --- /dev/null +++ b/src/http3/xqc_h3_ext_dgram.c @@ -0,0 +1,120 @@ +#include +#include +#include "src/http3/xqc_h3_ext_dgram.h" +#include "src/http3/xqc_h3_conn.h" + +void +xqc_h3_ext_datagram_read_notify(xqc_connection_t *conn, + void *user_data, const void *data, size_t data_len, uint64_t recv_time) +{ + xqc_h3_conn_t *h3c = (xqc_h3_conn_t*)conn->proto_data; + if (h3c->h3_ext_dgram_callbacks.dgram_read_notify + && (h3c->flags & XQC_H3_CONN_FLAG_UPPER_CONN_EXIST)) + { + h3c->h3_ext_dgram_callbacks.dgram_read_notify(h3c, + data, data_len, user_data, recv_time); + xqc_log(h3c->log, XQC_LOG_DEBUG, "|notify datagram read event to app|"); + } +} + +void +xqc_h3_ext_datagram_write_notify(xqc_connection_t *conn, + void *user_data) +{ + xqc_h3_conn_t *h3c = (xqc_h3_conn_t*)conn->proto_data; + if (h3c->h3_ext_dgram_callbacks.dgram_write_notify + && (h3c->flags & XQC_H3_CONN_FLAG_UPPER_CONN_EXIST)) + { + h3c->h3_ext_dgram_callbacks.dgram_write_notify(h3c, user_data); + xqc_log(h3c->log, XQC_LOG_DEBUG, + "|notify datagram write event to app|"); + } +} + +xqc_int_t +xqc_h3_ext_datagram_lost_notify(xqc_connection_t *conn, + uint64_t dgram_id, void *user_data) +{ + xqc_int_t ret = 0; + xqc_h3_conn_t *h3c = (xqc_h3_conn_t*)conn->proto_data; + if (h3c->h3_ext_dgram_callbacks.dgram_lost_notify + && (h3c->flags & XQC_H3_CONN_FLAG_UPPER_CONN_EXIST)) + { + ret = h3c->h3_ext_dgram_callbacks.dgram_lost_notify(h3c, dgram_id, user_data); + xqc_log(h3c->log, XQC_LOG_DEBUG, + "|notify lost datagram to app|dgram_id:%ui|", + dgram_id); + } + return ret; +} + +void +xqc_h3_ext_datagram_acked_notify(xqc_connection_t *conn, + uint64_t dgram_id, void *user_data) +{ + xqc_h3_conn_t *h3c = (xqc_h3_conn_t*)conn->proto_data; + if (h3c->h3_ext_dgram_callbacks.dgram_acked_notify + && (h3c->flags & XQC_H3_CONN_FLAG_UPPER_CONN_EXIST)) + { + h3c->h3_ext_dgram_callbacks.dgram_acked_notify(h3c, dgram_id, user_data); + xqc_log(h3c->log, XQC_LOG_DEBUG, + "|notify acked datagram to app|dgram_id:%ui|", + dgram_id); + } +} + +void +xqc_h3_ext_datagram_mss_updated_notify(xqc_connection_t *conn, + size_t mss, void *user_data) +{ + xqc_h3_conn_t *h3c = (xqc_h3_conn_t*)conn->proto_data; + if (h3c->h3_ext_dgram_callbacks.dgram_mss_updated_notify + && (h3c->flags & XQC_H3_CONN_FLAG_UPPER_CONN_EXIST)) + { + h3c->h3_ext_dgram_callbacks.dgram_mss_updated_notify(h3c, mss, user_data); + xqc_log(h3c->log, XQC_LOG_DEBUG, + "|notify datagram mss to app|mss:%z|", mss); + } +} + +const xqc_datagram_callbacks_t h3_ext_datagram_callbacks = { + .datagram_read_notify = xqc_h3_ext_datagram_read_notify, + .datagram_write_notify = xqc_h3_ext_datagram_write_notify, + .datagram_lost_notify = xqc_h3_ext_datagram_lost_notify, + .datagram_acked_notify = xqc_h3_ext_datagram_acked_notify, + .datagram_mss_updated_notify = xqc_h3_ext_datagram_mss_updated_notify, +}; + +size_t +xqc_h3_ext_datagram_get_mss(xqc_h3_conn_t *conn) +{ + return xqc_datagram_get_mss(conn->conn); +} + +void +xqc_h3_ext_datagram_set_user_data(xqc_h3_conn_t *conn, void *user_data) +{ + xqc_datagram_set_user_data(conn->conn, user_data); +} + +void * +xqc_h3_ext_datagram_get_user_data(xqc_h3_conn_t *conn) +{ + return xqc_datagram_get_user_data(conn->conn); +} + +xqc_int_t +xqc_h3_ext_datagram_send(xqc_h3_conn_t *conn, void *data, + size_t data_len, uint64_t *dgram_id, xqc_data_qos_level_t qos_level) +{ + return xqc_datagram_send(conn->conn, data, data_len, dgram_id, qos_level); +} + +xqc_int_t +xqc_h3_ext_datagram_send_multiple(xqc_h3_conn_t *conn, + struct iovec *iov, uint64_t *dgram_id_list, size_t iov_size, + size_t *sent_cnt, size_t *sent_bytes, xqc_data_qos_level_t qos_level) +{ + return xqc_datagram_send_multiple(conn->conn, iov, dgram_id_list, iov_size, + sent_cnt, sent_bytes, qos_level); +} \ No newline at end of file diff --git a/src/http3/xqc_h3_ext_dgram.h b/src/http3/xqc_h3_ext_dgram.h new file mode 100644 index 000000000..3e48b4b41 --- /dev/null +++ b/src/http3/xqc_h3_ext_dgram.h @@ -0,0 +1,12 @@ +/** + * @copyright Copyright (c) 2022, Alibaba Group Holding Limited + */ + +#ifndef _XQC_H3_EXT_DGRAM_H_INCLUDED_ +#define _XQC_H3_EXT_DGRAM_H_INCLUDED_ + +#include + +extern const xqc_datagram_callbacks_t h3_ext_datagram_callbacks; + +#endif \ No newline at end of file diff --git a/src/http3/xqc_h3_request.c b/src/http3/xqc_h3_request.c index b8efc9c98..7c6e74e4f 100644 --- a/src/http3/xqc_h3_request.c +++ b/src/http3/xqc_h3_request.c @@ -11,14 +11,15 @@ xqc_h3_request_t * -xqc_h3_request_create(xqc_engine_t *engine, const xqc_cid_t *cid, void *user_data) +xqc_h3_request_create(xqc_engine_t *engine, + const xqc_cid_t *cid, xqc_stream_settings_t *settings, void *user_data) { xqc_stream_t *stream; xqc_h3_stream_t *h3_stream; xqc_h3_request_t *h3_request; xqc_h3_conn_t *h3_conn; - stream = xqc_stream_create(engine, cid, NULL); + stream = xqc_stream_create(engine, cid, settings, NULL); if (!stream) { xqc_log(engine->log, XQC_LOG_ERROR, "|xqc_stream_create error|"); return NULL; @@ -51,8 +52,6 @@ xqc_h3_request_destroy(xqc_h3_request_t *h3_request) /* print request statistic log */ xqc_request_stats_t stats = xqc_h3_request_get_stats(h3_request); - char path_info_buff[100 * XQC_MAX_PATHS_COUNT] = {'\0'}; - xqc_h3s_path_metrics_print(h3s, path_info_buff, 50 * XQC_MAX_PATHS_COUNT); xqc_usec_t create_time = h3_request->h3r_begin_time; @@ -60,7 +59,7 @@ xqc_h3_request_destroy(xqc_h3_request_t *h3_request) "|rcvd_bdy_sz:%uz|snd_bdy_sz:%uz|rcvd_hdr_sz:%uz|snd_hdr_sz:%uz" "|create:%ui|blkd:%ui|nblkd:%ui|hdr_b:%ui|hdr_e:%ui|bdy_b:%ui|fin:%ui|recv_end:%ui" "|hrd_send:%ui|bdy_send:%ui|fin_send:%ui|fin_ack:%ui|last_send:%ui|last_recv:%ui" - "|mp_state:%d|path_info:%s|", + "|mp_state:%d|path_info:%s|comp_hdr_s:%uz|comp_hdr_r:%uz|", h3s->stream_id, stats.stream_close_msg ? : "", stats.stream_err, stats.recv_body_size, stats.send_body_size, stats.recv_header_size, stats.send_header_size, @@ -78,7 +77,8 @@ xqc_h3_request_destroy(xqc_h3_request_t *h3_request) xqc_calc_delay(stats.stream_fin_ack_time, create_time), xqc_calc_delay(h3_request->h3_stream->h3c->conn->conn_last_send_time, create_time), xqc_calc_delay(h3_request->h3_stream->h3c->conn->conn_last_recv_time, create_time), - stats.mp_state, path_info_buff); + stats.mp_state, stats.stream_info, stats.send_hdr_compressed, + stats.recv_hdr_compressed); if (h3_request->request_if->h3_request_close_notify) { h3_request->request_if->h3_request_close_notify(h3_request, h3_request->user_data); @@ -171,34 +171,104 @@ xqc_h3_request_create_inner(xqc_h3_conn_t *h3_conn, xqc_h3_stream_t *h3_stream, void xqc_stream_info_print(xqc_h3_stream_t *h3_stream, xqc_request_stats_t *stats) { + xqc_h3_conn_t *h3c = h3_stream->h3c; char *buff = stats->stream_info; size_t buff_size = XQC_STREAM_INFO_LEN; - size_t cursor = 0, ret = 0; + int i; + int flag = 0; + uint32_t mp_settings = 0; + + if (h3c->conn->handshake_complete_time > 0) { + flag = 1; + } + + if (h3c->conn->conn_settings.enable_stream_rate_limit) { + flag |= 1 << 1; + } + + if (h3c->conn->enable_multipath) { + mp_settings |= 1; + } + + if (h3c->conn->local_settings.enable_multipath) { + mp_settings |= (1 << 1); + } + + if (h3c->conn->remote_settings.enable_multipath) { + mp_settings |= (1 << 2); + } + + if (h3c->conn->conn_settings.multipath_version == XQC_MULTIPATH_05) { + mp_settings |= (1 << 3); + } + + if (h3c->conn->local_settings.multipath_version == XQC_MULTIPATH_05) { + mp_settings |= (1 << 4); + } + + if (h3c->conn->remote_settings.multipath_version == XQC_MULTIPATH_05) { + mp_settings |= (1 << 5); + } + + ret = snprintf(buff, buff_size, "(%d,%"PRIu64",%d)#", + flag, h3_stream->recv_rate_limit, mp_settings); + + cursor += ret; + + if (cursor >= buff_size) { + goto full; + } for (int i = 0; i < XQC_MAX_PATHS_COUNT; ++i) { if ((h3_stream->paths_info[i].path_send_bytes > 0) || (h3_stream->paths_info[i].path_recv_bytes > 0)) { - /* check buffer size */ - if (cursor + 100 >= buff_size) { - break; - } - ret = snprintf(buff + cursor, buff_size - cursor, - "#%"PRIu64"-%"PRIu64"-%"PRIu64"-%"PRIu64"-%"PRIu64"-%"PRIu64"-%"PRIu64"-%"PRIu64"-%"PRIu64, + "%"PRIu64"-%"PRIu64"-%"PRIu64"-%"PRIu64"-%"PRIu64"-%"PRIu64"-%d#", h3_stream->paths_info[i].path_id, h3_stream->paths_info[i].path_pkt_send_count, h3_stream->paths_info[i].path_pkt_recv_count, h3_stream->paths_info[i].path_send_bytes, - h3_stream->paths_info[i].path_send_reinject_bytes, h3_stream->paths_info[i].path_recv_bytes, - h3_stream->paths_info[i].path_recv_reinject_bytes, - h3_stream->paths_info[i].path_recv_effective_bytes, - h3_stream->paths_info[i].path_recv_effective_reinject_bytes); + h3_stream->paths_info[i].path_srtt, + h3_stream->paths_info[i].path_app_status); cursor += ret; + + if (cursor >= buff_size) { + goto full; + } + } + } + +full: + cursor = xqc_min(cursor, buff_size); + for (i = cursor - 1; i >= 0; i--) { + if (buff[i] == '-' || buff[i] == '#') { + buff[i] = '\0'; + break; + } + } + buff[buff_size - 1] = '\0'; +} + + +xqc_int_t +xqc_h3_request_update_settings(xqc_h3_request_t *h3_request, + xqc_stream_settings_t *settings) +{ + if (h3_request && settings + && h3_request->h3_stream && h3_request->h3_stream->stream) + { + if (xqc_stream_update_settings(h3_request->h3_stream->stream, + settings) == XQC_OK) + { + h3_request->h3_stream->recv_rate_limit = settings->recv_rate_bytes_per_sec; + return XQC_OK; } } + + return -XQC_EPARAM; } xqc_request_stats_t @@ -226,6 +296,9 @@ xqc_h3_request_get_stats(xqc_h3_request_t *h3_request) stats.stream_fin_send_time = h3_request->stream_fin_send_time; stats.stream_fin_ack_time = h3_request->stream_fin_ack_time; stats.stream_close_msg = h3_request->stream_close_msg; + stats.send_hdr_compressed = h3_request->compressed_header_sent; + stats.recv_hdr_compressed = h3_request->compressed_header_recvd; + stats.rate_limit = h3_request->h3_stream->recv_rate_limit; xqc_h3_stream_get_path_info(h3_request->h3_stream); xqc_request_path_metrics_print(h3_request->h3_stream->h3c->conn, @@ -762,6 +835,15 @@ xqc_h3_request_end(xqc_h3_request_t *h3r) XQC_H3_REQUEST_RECORD_TIME(h3r->h3r_end_time); } +void +xqc_h3_request_closing(xqc_h3_request_t *h3r, xqc_int_t err) +{ + if (h3r->request_if->h3_request_closing_notify) { + h3r->request_if->h3_request_closing_notify(h3r, err, h3r->user_data); + } +} + + #define XQC_PRIORITY_URGENCY "u=" #define XQC_PRIORITY_URGENCY_LEN 2 diff --git a/src/http3/xqc_h3_request.h b/src/http3/xqc_h3_request.h index 8f735e9bb..c76c18ca1 100644 --- a/src/http3/xqc_h3_request.h +++ b/src/http3/xqc_h3_request.h @@ -27,6 +27,7 @@ typedef struct xqc_h3_request_s { xqc_request_notify_flag_t read_flag; /* compressed header size recved */ + size_t compressed_header_recvd; size_t header_recvd; /* received header buf */ xqc_http_headers_t h3_header[XQC_H3_REQUEST_MAX_HEADERS_CNT]; @@ -40,6 +41,7 @@ typedef struct xqc_h3_request_s { size_t body_recvd_final_size; /* compressed header size sent */ + size_t compressed_header_sent; size_t header_sent; /* send body statistic information */ @@ -63,10 +65,6 @@ typedef struct xqc_h3_request_s { } xqc_h3_request_t; - -xqc_h3_request_t *xqc_h3_request_create(xqc_engine_t *engine, const xqc_cid_t *cid, - void *user_data); - xqc_h3_request_t *xqc_h3_request_create_inner(xqc_h3_conn_t *h3_conn, xqc_h3_stream_t *h3_stream, void *user_data); @@ -92,5 +90,6 @@ void xqc_h3_request_on_body_send(xqc_h3_request_t *h3r); void xqc_h3_request_stream_fin(xqc_h3_request_t *h3r); void xqc_h3_request_begin(xqc_h3_request_t *h3r); void xqc_h3_request_end(xqc_h3_request_t *h3r); +void xqc_h3_request_closing(xqc_h3_request_t *h3r, xqc_int_t err); #endif /* _XQC_H3_REQUEST_H_INCLUDED_ */ diff --git a/src/http3/xqc_h3_stream.c b/src/http3/xqc_h3_stream.c index 5eb7f0629..9c960b062 100644 --- a/src/http3/xqc_h3_stream.c +++ b/src/http3/xqc_h3_stream.c @@ -8,8 +8,7 @@ #include "src/transport/xqc_stream.h" #include "src/transport/xqc_engine.h" #include "src/http3/xqc_h3_conn.h" - - +#include "src/http3/xqc_h3_ext_bytestream.h" xqc_h3_stream_t * xqc_h3_stream_create(xqc_h3_conn_t *h3c, xqc_stream_t *stream, xqc_h3_stream_type_t type, @@ -37,6 +36,7 @@ xqc_h3_stream_create(xqc_h3_conn_t *h3c, xqc_stream_t *stream, xqc_h3_stream_typ xqc_init_list_head(&h3s->blocked_buf); h3s->ctx = xqc_qpack_create_req_ctx(stream->stream_id); h3s->log = h3c->log; + h3s->recv_rate_limit = stream->recv_rate_bytes_per_sec; stream->user_data = h3s; stream->stream_flag |= XQC_STREAM_FLAG_HAS_H3; @@ -85,10 +85,15 @@ xqc_h3_stream_destroy(xqc_h3_stream_t *h3s) h3s->blocked_stream = NULL; } - if (h3s->h3r) { + if (h3s->h3r && h3s->type == XQC_H3_STREAM_TYPE_REQUEST) { xqc_h3_request_destroy(h3s->h3r); } + //TODO: destroy bytestream + if (h3s->h3_ext_bs && h3s->type == XQC_H3_STREAM_TYPE_BYTESTEAM) { + xqc_h3_ext_bytestream_destroy(h3s->h3_ext_bs); + } + xqc_h3_frm_reset_pctx(&h3s->pctx.frame_pctx); xqc_qpack_destroy_req_ctx(h3s->ctx); xqc_list_buf_list_free(&h3s->send_buf); @@ -115,35 +120,47 @@ xqc_h3_stream_send_buffer(xqc_h3_stream_t *h3s) xqc_list_buf_t *list_buf = xqc_list_entry(pos, xqc_list_buf_t, list_head); xqc_var_buf_t *buf = list_buf->buf; - if (buf->data != NULL) { - /* buf with bytes remain and buf with fin only */ - if (buf->consumed_len < buf->data_len - || (buf->data_len == 0 && buf->fin_flag)) - { - /* send buffer with transport stream */ - ssize_t sent = xqc_stream_send(h3s->stream, buf->data + buf->consumed_len, - buf->data_len - buf->consumed_len, buf->fin_flag); - if (sent < 0) { - /* don't print XQC_EAGAIN and XQC_ESTREAM_RESET */ - if (sent != -XQC_EAGAIN && sent != -XQC_ESTREAM_RESET) { - xqc_log(h3s->log, XQC_LOG_ERROR, "|xqc_stream_send error|ret:%z|", sent); + if (!(h3s->flags & XQC_HTTP3_STREAM_FLAG_FIN_SENT)) { + + if (buf->data != NULL) { + /* buf with bytes remain and buf with fin only */ + if (buf->consumed_len < buf->data_len + || (buf->data_len == 0 && buf->fin_flag)) + { + /* send buffer with transport stream */ + ssize_t sent = xqc_stream_send(h3s->stream, buf->data + buf->consumed_len, + buf->data_len - buf->consumed_len, buf->fin_flag); + if (sent < 0) { + /* don't print XQC_EAGAIN and XQC_ESTREAM_RESET */ + if (sent != -XQC_EAGAIN && sent != -XQC_ESTREAM_RESET) { + xqc_log(h3s->log, XQC_LOG_ERROR, "|xqc_stream_send error|ret:%z|", sent); + } + + return sent; + } + + buf->consumed_len += sent; + if (buf->consumed_len != buf->data_len) { + return -XQC_EAGAIN; } - return sent; + } else if (buf->data_len > 0) { + xqc_log(h3s->log, XQC_LOG_ERROR, "|send_buf is empty|buf->consumed_len:%uz" + "|buf->data_len:%uz", buf->consumed_len, buf->data_len); } - buf->consumed_len += sent; - if (buf->consumed_len != buf->data_len) { - return -XQC_EAGAIN; + /* a buffer with fin flag is sent, mark the FIN_SENT flag */ + if (buf->data_len == buf->consumed_len && buf->fin_flag) { + h3s->flags |= XQC_HTTP3_STREAM_FLAG_FIN_SENT; } - } else if (buf->data_len > 0) { - xqc_log(h3s->log, XQC_LOG_ERROR, "|send_buf is empty|buf->consumed_len:%uz" - "|buf->data_len:%uz", buf->consumed_len, buf->data_len); + } else { + xqc_log(h3s->log, XQC_LOG_ERROR, "|send_buf is NULL|"); } } else { - xqc_log(h3s->log, XQC_LOG_ERROR, "|send_buf is NULL|"); + xqc_log(h3s->log, XQC_LOG_ERROR, + "|send data after fin sent|stream_id:%ui", h3s->stream_id); } xqc_list_buf_free(list_buf); @@ -214,6 +231,7 @@ xqc_h3_stream_write_headers(xqc_h3_stream_t *h3s, xqc_http_headers_t *headers, u } +//TODO: remove obsoloted internal functions ssize_t xqc_h3_stream_write_data_to_buffer(xqc_h3_stream_t *h3s, unsigned char *data, uint64_t data_size, uint8_t fin) @@ -305,20 +323,48 @@ xqc_h3_stream_write_goaway_to_buffer(xqc_h3_stream_t *h3s, uint64_t push_id, uin return XQC_OK; } +xqc_int_t +xqc_h3_stream_write_bidi_stream_type_to_buffer(xqc_h3_stream_t *h3s, uint64_t stype, uint8_t fin) +{ + xqc_int_t ret = xqc_h3_ext_frm_write_bidi_stream_type(&h3s->send_buf, stype, fin); + if (ret != XQC_OK) { + xqc_log(h3s->log, XQC_LOG_ERROR, "|write BIDI_STREAM_TYPE frame error|%d|stream_id:%ui|fin:%d|", + ret, h3s->stream_id, (unsigned int)fin); + return ret; + } + xqc_log_event(h3s->log, HTTP_FRAME_CREATED, h3s, XQC_H3_EXT_FRM_BIDI_STREAM_TYPE, stype); + + ret = xqc_h3_stream_send_buffer(h3s); + if (ret < 0 && ret != -XQC_EAGAIN) { + xqc_log(h3s->log, XQC_LOG_ERROR, "|send BIDI_STREAM_TYPE frame error|%d|stream_id:%ui|fin:%d|", + ret, h3s->stream_id, (unsigned int)fin); + return ret; + } + + return XQC_OK; +} + + + ssize_t xqc_h3_stream_send_headers(xqc_h3_stream_t *h3s, xqc_http_headers_t *headers, uint8_t fin) { + xqc_h3_conn_t *h3c; + uint64_t fields_size; + uint64_t max_field_section_size; + ssize_t write; + /* nothing to send */ if (headers->count == 0) { return 0; } - xqc_h3_conn_t *h3c = h3s->h3c; + h3c = h3s->h3c; /* header size constrains */ - uint64_t fields_size = xqc_h3_uncompressed_fields_size(headers); - uint64_t max_field_section_size = h3c->peer_h3_conn_settings.max_field_section_size; + fields_size = xqc_h3_uncompressed_fields_size(headers); + max_field_section_size = h3c->peer_h3_conn_settings.max_field_section_size; if (fields_size > max_field_section_size) { xqc_log(h3c->log, XQC_LOG_ERROR, "|large nv|conn:%p|fields_size:%ui|exceed|" "SETTINGS_MAX_FIELD_SECTION_SIZE:%ui|", h3c->conn, fields_size, @@ -329,13 +375,15 @@ xqc_h3_stream_send_headers(xqc_h3_stream_t *h3s, xqc_http_headers_t *headers, ui h3s->flags |= XQC_HTTP3_STREAM_NEED_WRITE_NOTIFY; /* QPACK & gen HEADERS frame */ - ssize_t write = xqc_h3_stream_write_headers(h3s, headers, fin); + write = xqc_h3_stream_write_headers(h3s, headers, fin); if (write < 0) { xqc_log(h3c->log, XQC_LOG_ERROR, "|xqc_h3_stream_write_headers error|ret:%z||stream_id:%ui", write, h3s->stream_id); XQC_H3_CONN_ERR(h3c, H3_INTERNAL_ERROR, write); - } + } else { + h3s->h3r->compressed_header_sent += write; + } /* header_sent is the sum of plaintext header name value length */ h3s->h3r->header_sent += headers->total_len; @@ -357,6 +405,7 @@ xqc_h3_stream_send_data_frame(xqc_h3_stream_t *h3s, unsigned char *data, size_t xqc_int_t ret; uint8_t fin_only = fin && !data_size; uint8_t fin_only_sent = 0; + uint8_t fin_flag_for_quic_stream = 0; unsigned char *pos; /* Send buffered HEADERS frame if any */ @@ -364,8 +413,16 @@ xqc_h3_stream_send_data_frame(xqc_h3_stream_t *h3s, unsigned char *data, size_t if (ret < 0) { return ret; } + + xqc_log(h3s->log, XQC_LOG_DEBUG, "|xqc_h3_stream_send_buffer|success|"); do { + if (h3s->flags & XQC_HTTP3_STREAM_FLAG_FIN_SENT) { + xqc_log(h3s->log, XQC_LOG_ERROR, + "|send data after fin sent|stream_id:%ui", h3s->stream_id); + break; + } + if (h3s->data_frame.data_sent > h3s->data_frame.data_len) { xqc_log(h3s->log, XQC_LOG_ERROR, "|data_sent exceed data_len|"); XQC_H3_CONN_ERR(h3s->h3c, H3_INTERNAL_ERROR, -XQC_H3_STATE_ERROR); @@ -383,6 +440,17 @@ xqc_h3_stream_send_data_frame(xqc_h3_stream_t *h3s, unsigned char *data, size_t h3s->data_frame.header_len = pos - h3s->data_frame.header_buf; h3s->data_frame.header_sent = 0; + fin_flag_for_quic_stream = fin; + + } else { + /* there is already a old data_frame */ + if ((data_size - data_sent) <= (h3s->data_frame.data_len - h3s->data_frame.data_sent)) { + fin_flag_for_quic_stream = fin; + + } else { + /* !!! this is important, otherwise an infinite-loop will happen. */ + fin_flag_for_quic_stream = 0; + } } /* Send frame header */ @@ -391,14 +459,19 @@ xqc_h3_stream_send_data_frame(xqc_h3_stream_t *h3s, unsigned char *data, size_t h3s->data_frame.header_len - h3s->data_frame.header_sent, fin_only); if (sent == -XQC_EAGAIN) { break; + } else if (sent < 0) { return sent; } + h3s->data_frame.header_sent += sent; + xqc_log(h3s->log, XQC_LOG_DEBUG, "|send_header|success|fin:%d|fin_only:%d|\n", fin, fin_only); + if (fin_only) { if (h3s->data_frame.header_sent == h3s->data_frame.header_len) { fin_only_sent = 1; + h3s->flags |= XQC_HTTP3_STREAM_FLAG_FIN_SENT; } break; } @@ -406,14 +479,24 @@ xqc_h3_stream_send_data_frame(xqc_h3_stream_t *h3s, unsigned char *data, size_t /* Send frame data */ sent = xqc_stream_send(h3s->stream, data + data_sent, - xqc_min(data_size - data_sent, h3s->data_frame.data_len - h3s->data_frame.data_sent), fin); + xqc_min(data_size - data_sent, h3s->data_frame.data_len - h3s->data_frame.data_sent), fin_flag_for_quic_stream); if (sent == -XQC_EAGAIN) { break; + } else if (sent < 0) { return sent; } + data_sent += sent; h3s->data_frame.data_sent += sent; + + /* if all data and fin is sent, mark the FIN_SENT flag */ + if (fin_flag_for_quic_stream + && h3s->data_frame.data_sent == h3s->data_frame.data_len) + { + h3s->flags |= XQC_HTTP3_STREAM_FLAG_FIN_SENT; + } + } while (data_sent < data_size); /* Nothing sent */ @@ -552,6 +635,19 @@ xqc_h3_stream_send_goaway(xqc_h3_stream_t *h3s, uint64_t push_id, uint8_t fin) return XQC_OK; } +xqc_int_t +xqc_h3_stream_send_bidi_stream_type(xqc_h3_stream_t *h3s, + xqc_h3_bidi_stream_type_t stype, uint8_t fin) +{ + xqc_int_t ret = xqc_h3_stream_write_bidi_stream_type_to_buffer(h3s, stype, fin); + if (ret < 0) { + return ret; + } + + xqc_engine_main_logic_internal(h3s->h3c->conn->engine); + return XQC_OK; +} + int xqc_h3_stream_write_notify(xqc_stream_t *stream, void *user_data) @@ -580,6 +676,8 @@ xqc_h3_stream_write_notify(xqc_stream_t *stream, void *user_data) return ret; } + xqc_log(h3s->log, XQC_LOG_DEBUG, "|xqc_h3_stream_send_buffer|success|"); + /* request write */ if (h3s->type == XQC_H3_STREAM_TYPE_REQUEST && (h3s->flags & XQC_HTTP3_STREAM_NEED_WRITE_NOTIFY)) @@ -593,6 +691,20 @@ xqc_h3_stream_write_notify(xqc_stream_t *stream, void *user_data) xqc_log(h3s->log, XQC_LOG_DEBUG, "|h3_request_write_notify|success|"); } + //TODO: implement the notification of bytestream writable event + if (h3s->type == XQC_H3_STREAM_TYPE_BYTESTEAM + && (h3s->flags & XQC_HTTP3_STREAM_NEED_WRITE_NOTIFY)) + { + xqc_log(h3s->log, XQC_LOG_DEBUG, "|h3_ext_bytestream_write_notify|start|"); + ret = xqc_h3_ext_bytestream_notify_write(h3s->h3_ext_bs); + if (ret < 0) { + xqc_log(stream->stream_conn->log, XQC_LOG_ERROR, + "|h3_ext_bytestream_write_notify error|%d|", ret); + return ret; + } + xqc_log(h3s->log, XQC_LOG_DEBUG, "|h3_ext_bytestream_write_notify|success|"); + } + return XQC_OK; } @@ -855,6 +967,7 @@ xqc_h3_stream_process_request(xqc_h3_stream_t *h3s, unsigned char *data, size_t if (pctx->frame.len == pctx->frame.consumed_len) { xqc_log_event(h3s->log, HTTP_FRAME_PARSED, h3s); fin = 1; + h3s->h3r->compressed_header_recvd += pctx->frame.len; xqc_h3_frm_reset_pctx(pctx); xqc_qpack_clear_req_ctx(h3s->ctx); if (fin_flag && processed == data_len) { @@ -935,6 +1048,97 @@ xqc_h3_stream_process_request(xqc_h3_stream_t *h3s, unsigned char *data, size_t return processed; } +ssize_t +xqc_h3_stream_process_bytestream(xqc_h3_stream_t *h3s, + unsigned char *data, size_t data_len, + xqc_bool_t fin_flag) +{ + + if (data == NULL) { + return -XQC_H3_EPARAM; + } + + xqc_h3_frame_pctx_t *pctx = &h3s->pctx.frame_pctx; + ssize_t processed = 0; + ssize_t len = 0; + xqc_int_t ret; + xqc_h3_ext_bytestream_data_buf_t *buf; + + /* process bytestream bytes */ + while (processed < data_len) { + xqc_log(h3s->log, XQC_LOG_DEBUG, "|parse frame|state:%d|data_len:%uz|process:%z|", + pctx->state, data_len, processed); + + /* parse frame, mainly the type, length field */ + ssize_t read = xqc_h3_frm_parse(data + processed, data_len - processed, pctx); + if (read < 0) { + xqc_log(h3s->log, XQC_LOG_ERROR, "|parse frame error|ret:%z|state:%d|frame_type:%xL|", + read, pctx->state, pctx->frame.type); + xqc_h3_frm_reset_pctx(pctx); + return read; + } + + xqc_log(h3s->log, XQC_LOG_DEBUG, "|parse frame success|frame_type:%xL|len:%ui|read:%z|", + pctx->frame.type, pctx->frame.len, read); + processed += read; + + xqc_bool_t fin = pctx->state == XQC_H3_FRM_STATE_END ? XQC_TRUE : XQC_FALSE; + + /* begin to parse the payload of a frame */ + if (pctx->state >= XQC_H3_FRM_STATE_PAYLOAD) { + switch (pctx->frame.type) { + case XQC_H3_FRM_HEADERS: + /* ignore header frames */ + len = xqc_min(pctx->frame.len - pctx->frame.consumed_len, data_len - processed); + processed += len; + pctx->frame.consumed_len += len; + if (pctx->frame.len == pctx->frame.consumed_len) { + fin = 1; + } + break; + + case XQC_H3_FRM_DATA: + buf = xqc_h3_ext_bytestream_get_last_data_buf(h3s->h3_ext_bs, pctx); + if (buf == NULL) { + xqc_h3_frm_reset_pctx(pctx); + return -XQC_EMALLOC; + } + + len = xqc_min(pctx->frame.len - pctx->frame.consumed_len, data_len - processed); + ret = xqc_h3_ext_bytestream_save_data_to_buf(buf, data + processed, len); + if (ret != XQC_OK) { + xqc_h3_frm_reset_pctx(pctx); + return ret; + } + + + processed += len; + pctx->frame.consumed_len += len; + + xqc_h3_ext_bytestream_recv_begin(h3s->h3_ext_bs); + + if (pctx->frame.len == pctx->frame.consumed_len) { + fin = 1; + buf->end_time = xqc_monotonic_timestamp(); + } + break; + + default: + xqc_log(h3s->log, XQC_LOG_INFO, "|ignore unexpected frame|" + "frame type:%xL|", pctx->frame.type); + break; + } + + if (fin) { + xqc_log_event(h3s->log, HTTP_FRAME_PARSED, h3s); + xqc_h3_frm_reset_pctx(pctx); + } + } + } + + return processed; +} + ssize_t xqc_h3_stream_process_uni_stream_type(const uint8_t *data, size_t data_len, @@ -954,7 +1158,7 @@ xqc_h3_stream_process_uni_stream_type(const uint8_t *data, size_t data_len, ssize_t xqc_h3_stream_process_uni_payload(xqc_h3_stream_t *h3s, unsigned char *data, size_t data_len) { - ssize_t processed; + ssize_t processed = 0; xqc_log(h3s->log, XQC_LOG_DEBUG, "|xqc_h3_stream_process_uni_payload|type:%d|sz:%uz|", h3s->type, data_len); @@ -1043,12 +1247,17 @@ ssize_t xqc_h3_stream_process_bidi_payload(xqc_h3_stream_t *h3s, unsigned char *data, size_t data_len, xqc_bool_t fin_flag) { - ssize_t processed; + ssize_t processed = 0; switch (h3s->type) { case XQC_H3_STREAM_TYPE_REQUEST: processed = xqc_h3_stream_process_request(h3s, data, data_len, fin_flag); break; + case XQC_H3_STREAM_TYPE_BYTESTEAM: + //TODO: process bytestream + processed = xqc_h3_stream_process_bytestream(h3s, data, data_len, fin_flag); + break; + /* bytes from reserved stream type will be ignored */ default: processed = data_len; @@ -1058,27 +1267,196 @@ xqc_h3_stream_process_bidi_payload(xqc_h3_stream_t *h3s, unsigned char *data, si return processed; } +xqc_int_t +xqc_h3_stream_create_inner_request_stream(xqc_h3_stream_t *h3s) +{ + if (h3s->h3r && h3s->type == XQC_H3_STREAM_TYPE_UNKNOWN) { + // if the request is already created for a stream with unknown type, + // we should return an error. + xqc_log(h3s->log, XQC_LOG_ERROR, + "|xqc_h3_request already exist in an h3 stream with unknown type|" + "stream_id:%ui|", h3s->stream_id); + return -XQC_H3_ECREATE_REQUEST; + } + + if (h3s->h3r == NULL) { + h3s->h3r = xqc_h3_request_create_inner(h3s->h3c, h3s, NULL); + if (!h3s->h3r) { + xqc_log(h3s->log, XQC_LOG_ERROR, "|xqc_h3_request_create_inner error|"); + return -XQC_H3_ECREATE_REQUEST; + } + + h3s->type = XQC_H3_STREAM_TYPE_REQUEST; + } + + return XQC_OK; +} + +xqc_int_t +xqc_h3_stream_create_inner_bytestream(xqc_h3_stream_t *h3s) +{ + if (h3s->h3_ext_bs && h3s->type == XQC_H3_STREAM_TYPE_UNKNOWN) { + // if the bytestream is already created for a stream with unknown type, + // we should return an error. + xqc_log(h3s->log, XQC_LOG_ERROR, + "|xqc_h3_ext_bytestream already exist in an h3 stream with unknown type|" + "stream_id:%ui|", h3s->stream_id); + return -XQC_H3_ECREATE_BYTESTREAM; + } + + if (h3s->h3_ext_bs == NULL) { + h3s->h3_ext_bs = xqc_h3_ext_bytestream_create_passive(h3s->h3c, h3s, NULL); + if (!h3s->h3_ext_bs) { + xqc_log(h3s->log, XQC_LOG_ERROR, "|xqc_h3_ext_bytestream_create_passive error|"); + return -XQC_H3_ECREATE_BYTESTREAM; + } + + h3s->type = XQC_H3_STREAM_TYPE_BYTESTEAM; + } + + return XQC_OK; +} + + +ssize_t +xqc_h3_stream_process_bidi_type_unknown(xqc_h3_stream_t *h3s, + unsigned char *data, size_t data_len, + xqc_bool_t fin_flag) +{ + if (data == NULL) { + return -XQC_H3_EPARAM; + } + + xqc_h3_frame_pctx_t *pctx = &h3s->pctx.frame_pctx; + ssize_t processed = 0; + xqc_int_t ret = 0; + xqc_h3_bidi_stream_type_t bidi_stream_type = 0; + + /* trying to parse the first frame */ + xqc_log(h3s->log, XQC_LOG_DEBUG, "|parse frame|state:%d|data_len:%uz|process:%z|", + pctx->state, data_len, processed); + + /* parse frame, mainly the type, length field */ + ssize_t read = xqc_h3_frm_parse(data + processed, data_len - processed, pctx); + if (read < 0) { + xqc_log(h3s->log, XQC_LOG_ERROR, "|parse frame error|ret:%z|state:%d|frame_type:%xL|", + read, pctx->state, pctx->frame.type); + xqc_h3_frm_reset_pctx(pctx); + return read; + } + + processed += read; + + xqc_bool_t fin = pctx->state == XQC_H3_FRM_STATE_END ? XQC_TRUE : XQC_FALSE; + + if (pctx->state >= XQC_H3_FRM_STATE_LEN) { + /* the type of the 1st frame is determined */ + xqc_log(h3s->log, XQC_LOG_DEBUG, "|parse frame type success|frame_type:%xL|read:%z|", + pctx->frame.type, read); + + if (pctx->frame.type != XQC_H3_EXT_FRM_BIDI_STREAM_TYPE) { + /* the first frame is not BIDI_STREAM_TYPE */ + ret = xqc_h3_stream_create_inner_request_stream(h3s); + + } else { + /* the first frame is BIDI_STREAM_TYPE, and it is parsed */ + if (fin) { + bidi_stream_type = pctx->frame.frame_payload.stream_type.stream_type.vi; + xqc_log(h3s->log, XQC_LOG_DEBUG, + "|read bidi_stream_type from BIDI_STREAM_TYPE frame|" + "bidi_stream_type:%d|", + bidi_stream_type); + switch (bidi_stream_type) { + case XQC_H3_BIDI_STREAM_TYPE_REQUEST: + ret = xqc_h3_stream_create_inner_request_stream(h3s); + break; + case XQC_H3_BIDI_STREAM_TYPE_BYTESTREAM: + //TODO: create byte stream + ret = xqc_h3_stream_create_inner_bytestream(h3s); + break; + default: + xqc_log(h3s->log, XQC_LOG_ERROR, + "|unexpected bidi stream type|" + "bidi_stream_type:%ui|", + bidi_stream_type); + ret = -XQC_H3_INVALID_BIDI_STREAM_TYPE; + break; + } + } + } + + if (ret != XQC_OK) { + xqc_h3_frm_reset_pctx(pctx); + return ret; + } + } + + if (processed == data_len && fin_flag + && h3s->type == XQC_H3_STREAM_TYPE_UNKNOWN) + { + // if we have received a fin_flag while the type of stream is still undetermined + // we treat the stream as a request stream + ret = xqc_h3_stream_create_inner_request_stream(h3s); + if (ret != XQC_OK) { + xqc_h3_frm_reset_pctx(pctx); + return ret; + } + } + + if (fin) { + xqc_log_event(h3s->log, HTTP_FRAME_PARSED, h3s); + xqc_h3_frm_reset_pctx(pctx); + } + + return processed; +} ssize_t xqc_h3_stream_process_bidi(xqc_h3_stream_t *h3s, unsigned char *data, size_t data_len, xqc_bool_t fin_flag) { + ssize_t processed = 0, total_processed = 0; + if (h3s->flags & XQC_HTTP3_STREAM_FLAG_QPACK_DECODE_BLOCKED) { return 0; } + xqc_log(h3s->log, XQC_LOG_DEBUG, + "|xqc_h3_stream_process_bidi|%uz|%d|", + data_len, fin_flag); + + /* + * TODO: if the stream type is unknown, + * we parse the first H3 frame to decide what type the stream is + */ + if (XQC_H3_STREAM_TYPE_UNKNOWN == h3s->type) { - h3s->type = XQC_H3_STREAM_TYPE_REQUEST; - if (!h3s->h3r) { - h3s->h3r = xqc_h3_request_create_inner(h3s->h3c, h3s, NULL); - if (!h3s->h3r) { - xqc_log(h3s->log, XQC_LOG_ERROR, "|xqc_h3_request_create_inner error|"); - return -XQC_H3_ECREATE_REQUEST; - } + if (h3s->h3c->flags & XQC_H3_CONN_FLAG_EXT_ENABLED) { + processed = xqc_h3_stream_process_bidi_type_unknown(h3s, data, data_len, fin_flag); + + } else { + /* MUST set the stream type to h3 request */ + processed = xqc_h3_stream_create_inner_request_stream(h3s); } } - return xqc_h3_stream_process_bidi_payload(h3s, data, data_len, fin_flag); + if (processed < 0) { + xqc_log(h3s->log, XQC_LOG_ERROR, + "|xqc_h3_stream_process_bidi_type_unknown error|%z|", + processed); + return processed; + } + + total_processed += processed; + + processed = xqc_h3_stream_process_bidi_payload(h3s, data + processed, data_len - processed, fin_flag); + if (processed < 0) { + return processed; + } + + total_processed += processed; + + return total_processed; } @@ -1086,8 +1464,9 @@ xqc_int_t xqc_h3_stream_process_in(xqc_h3_stream_t *h3s, unsigned char *data, size_t data_len, xqc_bool_t fin_flag) { - ssize_t processed; + ssize_t processed = 0; xqc_h3_conn_t *h3c = h3s->h3c; + xqc_int_t errcode = XQC_OK; /* nothing to process */ if (data_len == 0 && !fin_flag) { @@ -1116,14 +1495,22 @@ xqc_h3_stream_process_in(xqc_h3_stream_t *h3s, unsigned char *data, size_t data_ if (processed < 0) { /* error occurred */ xqc_log(h3c->log, XQC_LOG_ERROR, "|xqc_h3_stream_process_bidi|%z|", processed); + + errcode = -XQC_H3_EPROC_REQUEST; + + if (h3s->type == XQC_H3_STREAM_TYPE_BYTESTEAM) { + errcode = -XQC_H3_EPROC_BYTESTREAM; + } + if (processed == -XQC_H3_INVALID_HEADER) { - XQC_H3_CONN_ERR(h3c, H3_GENERAL_PROTOCOL_ERROR, -XQC_H3_EPROC_REQUEST); + XQC_H3_CONN_ERR(h3c, H3_GENERAL_PROTOCOL_ERROR, errcode); } else { - XQC_H3_CONN_ERR(h3c, H3_FRAME_ERROR, -XQC_H3_EPROC_REQUEST); + XQC_H3_CONN_ERR(h3c, H3_FRAME_ERROR, errcode); } - return -XQC_H3_EPROC_REQUEST; + // TODO: define an errorcode for bytstream? + return errcode; } else if (processed != data_len) { /* if not all bytes are processed, the decoder shall be blocked */ @@ -1285,8 +1672,15 @@ xqc_h3_stream_process_data(xqc_stream_t *stream, xqc_h3_stream_t *h3s, xqc_bool_ } while (read == buff_size && !*fin); - if (*fin && h3s->type == XQC_H3_STREAM_TYPE_REQUEST) { - h3s->h3r->fin_flag = *fin; + if (*fin) { + if (h3s->type == XQC_H3_STREAM_TYPE_REQUEST) { + h3s->h3r->fin_flag = *fin; + + } else if (h3s->type == XQC_H3_STREAM_TYPE_BYTESTEAM) { + //TODO: mark the fin flag of the bytestream and record time + xqc_h3_ext_bytestream_fin_rcvd(h3s->h3_ext_bs); + xqc_h3_ext_bytestream_set_fin_rcvd_flag(h3s->h3_ext_bs); + } } if (xqc_qpack_get_dec_insert_count(h3s->qpack) > insert_cnt) { @@ -1372,6 +1766,28 @@ xqc_h3_stream_process_blocked_stream(xqc_h3_stream_t *h3s) } +xqc_int_t +xqc_h3_stream_create_notify(xqc_stream_t *stream, void *user_data) +{ + xqc_connection_t *conn = stream->stream_conn; + xqc_h3_conn_t *h3c = conn->proto_data; + + /* do not accept server initiated bidirectional streams at client */ + if (conn->conn_type == XQC_CONN_TYPE_CLIENT + && stream->stream_type == XQC_SVR_BID) + { + /* xquic do not support server-inited bidi stream, return error and + discard all subsequent stream data */ + xqc_log(h3c->log, XQC_LOG_ERROR, + "|ignore server initiated bidi-streams at client|"); + return -XQC_ECREATE_STREAM; + } + + return XQC_OK; +} + + + int xqc_h3_stream_read_notify(xqc_stream_t *stream, void *user_data) { @@ -1443,7 +1859,7 @@ xqc_h3_stream_read_notify(xqc_stream_t *stream, void *user_data) return ret; } - /* notify DATA to application ASAP */ + /* REQUEST: notify DATA to application ASAP */ if (h3s->type == XQC_H3_STREAM_TYPE_REQUEST && !xqc_list_empty(&h3s->h3r->body_buf)) { @@ -1454,6 +1870,17 @@ xqc_h3_stream_read_notify(xqc_stream_t *stream, void *user_data) return ret; } } + + /* TODO: BYTESTRAM: notify DATA to application ASAP */ + if (h3s->type == XQC_H3_STREAM_TYPE_BYTESTEAM + && xqc_h3_ext_bytestream_should_notify_read(h3s->h3_ext_bs)) + { + ret = xqc_h3_ext_bytestream_notify_read(h3s->h3_ext_bs); + if (ret < 0) { + xqc_log(h3s->log, XQC_LOG_ERROR, "|recv bytestream error|%d|", ret); + return ret; + } + } } xqc_log(h3c->log, XQC_LOG_DEBUG, "|success|stream_id:%ui|conn:%p|", @@ -1467,7 +1894,8 @@ int xqc_h3_stream_close_notify(xqc_stream_t *stream, void *user_data) { if (!(stream->stream_flag & XQC_STREAM_FLAG_HAS_H3)) { - xqc_log(stream->stream_conn->log, XQC_LOG_DEBUG, "|has no h3 stream|"); + xqc_log(stream->stream_conn->log, XQC_LOG_DEBUG, + "|has no h3 stream|stream_id:%ui", xqc_stream_id(stream)); return XQC_OK; } @@ -1475,11 +1903,18 @@ xqc_h3_stream_close_notify(xqc_stream_t *stream, void *user_data) h3s->flags |= XQC_HTTP3_STREAM_FLAG_CLOSED; xqc_h3_stream_get_err(h3s); xqc_h3_stream_get_path_info(h3s); - if (h3s->h3r) { + + if (h3s->h3r && h3s->type == XQC_H3_STREAM_TYPE_REQUEST) { h3s->h3r->stream_fin_send_time = h3s->stream->stream_stats.local_fin_snd_time; h3s->h3r->stream_fin_ack_time = h3s->stream->stream_stats.first_fin_ack_time; h3s->h3r->stream_close_msg = h3s->stream->stream_close_msg; } + + if (h3s->h3_ext_bs && h3s->type == XQC_H3_STREAM_TYPE_BYTESTEAM) { + //TODO: record stats info + xqc_h3_ext_bytestream_save_stats_from_stream(h3s->h3_ext_bs, h3s->stream); + } + h3s->stream = NULL; /* stream closed, MUST NOT use it any more */ /* @@ -1511,13 +1946,34 @@ xqc_h3_stream_close_notify(xqc_stream_t *stream, void *user_data) } +void +xqc_h3_stream_closing_notify(xqc_stream_t *stream, + xqc_int_t err_code, void *strm_user_data) +{ + xqc_h3_stream_t *h3s; + + h3s = (xqc_h3_stream_t *)strm_user_data; + if (NULL == h3s) { + return; + } + + /* only http3 request shall be notified */ + if (h3s->type == XQC_H3_STREAM_TYPE_REQUEST + && h3s->h3r) + { + xqc_h3_request_closing(h3s->h3r, err_code); + } +} + /** * transport callback */ const xqc_stream_callbacks_t h3_stream_callbacks = { - .stream_write_notify = xqc_h3_stream_write_notify, - .stream_read_notify = xqc_h3_stream_read_notify, - .stream_close_notify = xqc_h3_stream_close_notify, + .stream_create_notify = xqc_h3_stream_create_notify, + .stream_write_notify = xqc_h3_stream_write_notify, + .stream_read_notify = xqc_h3_stream_read_notify, + .stream_close_notify = xqc_h3_stream_close_notify, + .stream_closing_notify = xqc_h3_stream_closing_notify, }; diff --git a/src/http3/xqc_h3_stream.h b/src/http3/xqc_h3_stream.h index 3589750b3..b1a151149 100644 --- a/src/http3/xqc_h3_stream.h +++ b/src/http3/xqc_h3_stream.h @@ -22,11 +22,17 @@ typedef enum { /* bidi stream type */ XQC_H3_STREAM_TYPE_REQUEST = 0x10, + XQC_H3_STREAM_TYPE_BYTESTEAM = 0x20, /* reserved stream type or others */ XQC_H3_STREAM_TYPE_UNKNOWN = 0xFFFFFFFFFFFFFFFFull, } xqc_h3_stream_type_t; +typedef enum { + XQC_H3_BIDI_STREAM_TYPE_REQUEST = 0, + XQC_H3_BIDI_STREAM_TYPE_BYTESTREAM = 1, +} xqc_h3_bidi_stream_type_t; + typedef enum { XQC_HTTP3_STREAM_FLAG_NONE = 0x0000, XQC_HTTP3_STREAM_FLAG_TYPE_IDENTIFIED = 0x0001, @@ -65,6 +71,8 @@ typedef enum { blocked and waiting for encoder stream insertions while Transport stream notify its close */ XQC_HTTP3_STREAM_FLAG_ACTIVELY_CLOSED = 0x1000, + /* FIN was sent and no data will be sent any more */ + XQC_HTTP3_STREAM_FLAG_FIN_SENT = 0x2000, } xqc_h3_stream_flag; typedef struct xqc_h3_stream_pctx_s { @@ -92,7 +100,11 @@ typedef struct xqc_h3_stream_s { * available only in request streams, create or dereference in control or * reserved streams is forbidden. */ - xqc_h3_request_t *h3r; + union { + xqc_h3_request_t *h3r; + xqc_h3_ext_bytestream_t *h3_ext_bs; + }; + /* stream type */ xqc_h3_stream_type_t type; @@ -135,6 +147,8 @@ typedef struct xqc_h3_stream_s { /* referred count of h3 stream */ uint32_t ref_cnt; + uint64_t recv_rate_limit; + } xqc_h3_stream_t; @@ -174,4 +188,7 @@ void xqc_h3_stream_get_path_info(xqc_h3_stream_t *h3s); void xqc_h3_stream_set_priority(xqc_h3_stream_t *h3s, xqc_h3_priority_t *prio); +xqc_int_t xqc_h3_stream_send_bidi_stream_type(xqc_h3_stream_t *h3s, + xqc_h3_bidi_stream_type_t stype, uint8_t fin); + #endif diff --git a/src/tls/babassl/xqc_crypto.c b/src/tls/babassl/xqc_crypto.c index 99c51ee1a..7147d196d 100644 --- a/src/tls/babassl/xqc_crypto.c +++ b/src/tls/babassl/xqc_crypto.c @@ -7,7 +7,7 @@ #include #include -void* +void * xqc_aead_ctx_new(const xqc_pkt_protect_aead_t *pp_aead, xqc_key_type_t type, const uint8_t *key, size_t noncelen) { @@ -176,7 +176,7 @@ xqc_ossl_aead_decrypt(const xqc_pkt_protect_aead_t *pp_aead, void *aead_ctx, return -XQC_TLS_DECRYPT_DATA_ERROR; } -void* +void * xqc_hp_ctx_new(const xqc_hdr_protect_cipher_t *hp_cipher, const uint8_t *key) { EVP_CIPHER_CTX *ctx = EVP_CIPHER_CTX_new(); diff --git a/src/tls/boringssl/xqc_crypto.c b/src/tls/boringssl/xqc_crypto.c index f0c4c3545..5cf382043 100644 --- a/src/tls/boringssl/xqc_crypto.c +++ b/src/tls/boringssl/xqc_crypto.c @@ -6,7 +6,7 @@ #include -void* +void * xqc_aead_ctx_new(const xqc_pkt_protect_aead_t *pp_aead, xqc_key_type_t type, const uint8_t *key, size_t noncelen) { @@ -86,7 +86,7 @@ xqc_bssl_aead_decrypt(const xqc_pkt_protect_aead_t *pp_aead, void *aead_ctx, } -void* +void * xqc_hp_ctx_new(const xqc_hdr_protect_cipher_t *hp_cipher, const uint8_t *key) { EVP_CIPHER_CTX *ctx = EVP_CIPHER_CTX_new(); diff --git a/src/tls/xqc_tls.c b/src/tls/xqc_tls.c index 6d332ece3..5f1f20078 100644 --- a/src/tls/xqc_tls.c +++ b/src/tls/xqc_tls.c @@ -169,7 +169,7 @@ xqc_tls_set_alpn(SSL *ssl, const char *alpn) * the rest bytes, set the last byte with '\0' */ p_alpn[0] = alpn_len; - strncpy(&p_alpn[1], alpn, protos_len); + memcpy(&p_alpn[1], alpn, alpn_len); p_alpn[protos_len] = '\0'; SSL_set_alpn_protos(ssl, p_alpn, protos_len); @@ -311,6 +311,21 @@ xqc_tls_create_ssl(xqc_tls_t *tls, xqc_tls_config_t *cfg) return ret; } +xqc_int_t +xqc_tls_update_tp(xqc_tls_t *tls, uint8_t *tp_buf, size_t tp_len) +{ + xqc_int_t ret = XQC_OK; + int ssl_ret; + /* set local transport parameter */ + ssl_ret = SSL_set_quic_transport_params(tls->ssl, tp_buf, tp_len); + if (ssl_ret != XQC_SSL_SUCCESS) { + xqc_log(tls->log, XQC_LOG_ERROR, "|set transport params error|%s|", + ERR_error_string(ERR_get_error(), NULL)); + ret = -XQC_TLS_INTERNAL; + } + return ret; +} + xqc_tls_t * xqc_tls_create(xqc_tls_ctx_t *ctx, xqc_tls_config_t *cfg, xqc_log_t *log, void *user_data) @@ -1109,6 +1124,16 @@ xqc_tls_get_selected_alpn(xqc_tls_t *tls, const char **out_alpn, (unsigned *)out_len); } +void * +xqc_tls_get_ssl(xqc_tls_t *tls) +{ + if (!tls) { + return NULL; + } + + return tls->ssl; +} + /** * ============================================================================ diff --git a/src/tls/xqc_tls.h b/src/tls/xqc_tls.h index 4c70c859e..b3f7a4dc0 100644 --- a/src/tls/xqc_tls.h +++ b/src/tls/xqc_tls.h @@ -198,4 +198,12 @@ xqc_int_t xqc_tls_cal_retry_integrity_tag(xqc_tls_t *tls, void xqc_tls_get_selected_alpn(xqc_tls_t *tls, const char **out_alpn, size_t *out_len); +xqc_int_t xqc_tls_update_tp(xqc_tls_t *tls, uint8_t *tp_buf, size_t tp_len); + +/** + * @brief get SSL handler + */ +void *xqc_tls_get_ssl(xqc_tls_t *tls); + + #endif diff --git a/src/transport/reinjection_control/xqc_reinj_deadline.c b/src/transport/reinjection_control/xqc_reinj_deadline.c new file mode 100644 index 000000000..60d29a716 --- /dev/null +++ b/src/transport/reinjection_control/xqc_reinj_deadline.c @@ -0,0 +1,99 @@ +/** + * @copyright Copyright (c) 2022, Alibaba Group Holding Limited + */ + + +#include "src/transport/reinjection_control/xqc_reinj_deadline.h" + +#include "src/transport/xqc_reinjection.h" +#include "src/transport/xqc_multipath.h" +#include "src/transport/xqc_conn.h" +#include "src/transport/xqc_send_ctl.h" +#include "src/transport/xqc_engine.h" +#include "src/transport/xqc_cid.h" +#include "src/transport/xqc_stream.h" +#include "src/transport/xqc_utils.h" +#include "src/transport/xqc_wakeup_pq.h" +#include "src/transport/xqc_packet_out.h" + +#include "src/common/xqc_common.h" +#include "src/common/xqc_malloc.h" +#include "src/common/xqc_str_hash.h" +#include "src/common/xqc_hash.h" +#include "src/common/xqc_priority_q.h" +#include "src/common/xqc_memory_pool.h" +#include "src/common/xqc_random.h" + +#include "xquic/xqc_errno.h" + + +static size_t +xqc_deadline_reinj_ctl_size() +{ + return sizeof(xqc_deadline_reinj_ctl_t); +} + +static void +xqc_deadline_reinj_ctl_init(void *reinj_ctl, xqc_connection_t *conn) +{ + xqc_deadline_reinj_ctl_t *rctl = (xqc_deadline_reinj_ctl_t *)reinj_ctl; + + rctl->log = conn->log; + rctl->conn = conn; +} + +static xqc_bool_t +xqc_deadline_reinj_can_reinject_before_sched(xqc_deadline_reinj_ctl_t *rctl, + xqc_packet_out_t *po) +{ + xqc_connection_t *conn = rctl->conn; + xqc_usec_t now = xqc_monotonic_timestamp(); + xqc_usec_t min_srtt = xqc_conn_get_min_srtt(conn, 0); + + double factor = conn->conn_settings.reinj_flexible_deadline_srtt_factor; + double flexible = factor * min_srtt; + uint64_t hard = conn->conn_settings.reinj_hard_deadline; + uint64_t lower_bound = conn->conn_settings.reinj_deadline_lower_bound; + double deadline = xqc_max(xqc_min(flexible, (double)hard), (double)lower_bound); + + xqc_log(conn->log, XQC_LOG_DEBUG, "|deadline:%f|factor:%.4f|min_srtt:%ui|flexible:%f|hard:%ui|lower_bound:%ui|now:%ui|sent_time:%ui|frame:%s|", + deadline, factor, min_srtt, flexible, hard, lower_bound, now, po->po_sent_time, xqc_frame_type_2_str(po->po_frame_types)); + + if (((po->po_frame_types & XQC_FRAME_BIT_STREAM) + || (po->po_frame_types & XQC_FRAME_BIT_MAX_STREAM_DATA)) + && !(po->po_flag & XQC_POF_NOT_REINJECT) + && !(XQC_MP_PKT_REINJECTED(po)) + && (po->po_flag & XQC_POF_IN_FLIGHT) + && ((double)(now - po->po_sent_time) >= deadline)) + { + return XQC_TRUE; + } + + return XQC_FALSE; +} + +static xqc_bool_t +xqc_deadline_reinj_can_reinject(void *ctl, + xqc_packet_out_t *po, xqc_reinjection_mode_t mode) +{ + xqc_bool_t can_reinject = XQC_FALSE; + xqc_deadline_reinj_ctl_t *rctl = (xqc_deadline_reinj_ctl_t*)ctl; + + switch (mode) { + case XQC_REINJ_UNACK_BEFORE_SCHED: + can_reinject = xqc_deadline_reinj_can_reinject_before_sched(rctl, po); + break; + default: + can_reinject = XQC_FALSE; + break; + } + + return can_reinject; +} + + +const xqc_reinj_ctl_callback_t xqc_deadline_reinj_ctl_cb = { + .xqc_reinj_ctl_size = xqc_deadline_reinj_ctl_size, + .xqc_reinj_ctl_init = xqc_deadline_reinj_ctl_init, + .xqc_reinj_ctl_can_reinject = xqc_deadline_reinj_can_reinject, +}; \ No newline at end of file diff --git a/src/transport/reinjection_control/xqc_reinj_deadline.h b/src/transport/reinjection_control/xqc_reinj_deadline.h new file mode 100644 index 000000000..3b1a8e9ce --- /dev/null +++ b/src/transport/reinjection_control/xqc_reinj_deadline.h @@ -0,0 +1,18 @@ +/** + * @copyright Copyright (c) 2022, Alibaba Group Holding Limited + */ + +#ifndef _XQC_REINJ_DEADLINE_H_INCLUDED_ +#define _XQC_REINJ_DEADLINE_H_INCLUDED_ + +#include +#include + +typedef struct { + xqc_log_t *log; + xqc_connection_t *conn; +} xqc_deadline_reinj_ctl_t; + +extern const xqc_reinj_ctl_callback_t xqc_deadline_reinj_ctl_cb; + +#endif diff --git a/src/transport/reinjection_control/xqc_reinj_default.c b/src/transport/reinjection_control/xqc_reinj_default.c new file mode 100644 index 000000000..265ada8b1 --- /dev/null +++ b/src/transport/reinjection_control/xqc_reinj_default.c @@ -0,0 +1,89 @@ +/** + * @copyright Copyright (c) 2022, Alibaba Group Holding Limited + */ + + +#include "src/transport/reinjection_control/xqc_reinj_default.h" + +#include "src/transport/xqc_reinjection.h" +#include "src/transport/xqc_multipath.h" +#include "src/transport/xqc_conn.h" +#include "src/transport/xqc_send_ctl.h" +#include "src/transport/xqc_engine.h" +#include "src/transport/xqc_cid.h" +#include "src/transport/xqc_stream.h" +#include "src/transport/xqc_utils.h" +#include "src/transport/xqc_wakeup_pq.h" +#include "src/transport/xqc_packet_out.h" + +#include "src/common/xqc_common.h" +#include "src/common/xqc_malloc.h" +#include "src/common/xqc_str_hash.h" +#include "src/common/xqc_hash.h" +#include "src/common/xqc_priority_q.h" +#include "src/common/xqc_memory_pool.h" +#include "src/common/xqc_random.h" + +#include "xquic/xqc_errno.h" + + +static size_t +xqc_default_reinj_ctl_size() +{ + return sizeof(xqc_default_reinj_ctl_t); +} + +static void +xqc_default_reinj_ctl_init(void *reinj_ctl, xqc_connection_t *conn) +{ + xqc_default_reinj_ctl_t *rctl = (xqc_default_reinj_ctl_t *)reinj_ctl; + + rctl->log = conn->log; + rctl->conn = conn; +} + +static xqc_bool_t +xqc_default_reinj_can_reinject_after_sched(xqc_default_reinj_ctl_t *rctl, + xqc_packet_out_t *po) +{ + xqc_connection_t *conn = rctl->conn; + + if (xqc_list_empty(&conn->conn_send_queue->sndq_send_packets) + && ((po->po_frame_types & XQC_FRAME_BIT_STREAM) + || (po->po_frame_types & XQC_FRAME_BIT_MAX_STREAM_DATA)) + && !(po->po_flag & XQC_POF_NOT_REINJECT) + && !(XQC_MP_PKT_REINJECTED(po)) + && (po->po_flag & XQC_POF_IN_FLIGHT)) + { + return XQC_TRUE; + } + + return XQC_FALSE; +} + +static xqc_bool_t +xqc_default_reinj_can_reinject(void *ctl, + xqc_packet_out_t *po, xqc_reinjection_mode_t mode) +{ + xqc_bool_t can_reinject = XQC_FALSE; + xqc_default_reinj_ctl_t *rctl = (xqc_default_reinj_ctl_t*)ctl; + + switch (mode) { + case XQC_REINJ_UNACK_AFTER_SCHED: + can_reinject = xqc_default_reinj_can_reinject_after_sched(rctl, po); + break; + default: + can_reinject = XQC_FALSE; + break; + } + + return can_reinject; +} + + + +const xqc_reinj_ctl_callback_t xqc_default_reinj_ctl_cb = { + .xqc_reinj_ctl_size = xqc_default_reinj_ctl_size, + .xqc_reinj_ctl_init = xqc_default_reinj_ctl_init, + .xqc_reinj_ctl_can_reinject = xqc_default_reinj_can_reinject, +}; \ No newline at end of file diff --git a/src/transport/reinjection_control/xqc_reinj_default.h b/src/transport/reinjection_control/xqc_reinj_default.h new file mode 100644 index 000000000..52aa43825 --- /dev/null +++ b/src/transport/reinjection_control/xqc_reinj_default.h @@ -0,0 +1,18 @@ +/** + * @copyright Copyright (c) 2022, Alibaba Group Holding Limited + */ + +#ifndef _XQC_REINJ_DEFAULT_H_INCLUDED_ +#define _XQC_REINJ_DEFAULT_H_INCLUDED_ + +#include +#include + +typedef struct { + xqc_log_t *log; + xqc_connection_t *conn; +} xqc_default_reinj_ctl_t; + +extern const xqc_reinj_ctl_callback_t xqc_default_reinj_ctl_cb; + +#endif diff --git a/src/transport/reinjection_control/xqc_reinj_dgram.c b/src/transport/reinjection_control/xqc_reinj_dgram.c new file mode 100644 index 000000000..dba910e9b --- /dev/null +++ b/src/transport/reinjection_control/xqc_reinj_dgram.c @@ -0,0 +1,86 @@ +/** + * @copyright Copyright (c) 2022, Alibaba Group Holding Limited + */ + + +#include "src/transport/reinjection_control/xqc_reinj_dgram.h" + +#include "src/transport/xqc_reinjection.h" +#include "src/transport/xqc_multipath.h" +#include "src/transport/xqc_conn.h" +#include "src/transport/xqc_send_ctl.h" +#include "src/transport/xqc_engine.h" +#include "src/transport/xqc_cid.h" +#include "src/transport/xqc_stream.h" +#include "src/transport/xqc_utils.h" +#include "src/transport/xqc_wakeup_pq.h" +#include "src/transport/xqc_packet_out.h" + +#include "src/common/xqc_common.h" +#include "src/common/xqc_malloc.h" +#include "src/common/xqc_str_hash.h" +#include "src/common/xqc_hash.h" +#include "src/common/xqc_priority_q.h" +#include "src/common/xqc_memory_pool.h" +#include "src/common/xqc_random.h" + +#include "xquic/xqc_errno.h" + + +static size_t +xqc_dgram_reinj_ctl_size() +{ + return sizeof(xqc_dgram_reinj_ctl_t); +} + +static void +xqc_dgram_reinj_ctl_init(void *reinj_ctl, xqc_connection_t *conn) +{ + xqc_dgram_reinj_ctl_t *rctl = (xqc_dgram_reinj_ctl_t *)reinj_ctl; + + rctl->log = conn->log; + rctl->conn = conn; +} + +static xqc_bool_t +xqc_dgram_reinj_can_reinject_after_send(xqc_dgram_reinj_ctl_t *rctl, + xqc_packet_out_t *po) +{ + xqc_connection_t *conn = rctl->conn; + + if ((po->po_frame_types & XQC_FRAME_BIT_DATAGRAM) + && !(po->po_flag & XQC_POF_NOT_REINJECT) + && !(XQC_MP_PKT_REINJECTED(po)) + && (po->po_flag & XQC_POF_IN_FLIGHT)) + { + return XQC_TRUE; + } + + return XQC_FALSE; +} + +static xqc_bool_t +xqc_dgram_reinj_can_reinject(void *ctl, + xqc_packet_out_t *po, xqc_reinjection_mode_t mode) +{ + xqc_bool_t can_reinject = XQC_FALSE; + xqc_dgram_reinj_ctl_t *rctl = (xqc_dgram_reinj_ctl_t*)ctl; + + switch (mode) { + case XQC_REINJ_UNACK_AFTER_SEND: + can_reinject = xqc_dgram_reinj_can_reinject_after_send(rctl, po); + break; + default: + can_reinject = XQC_FALSE; + break; + } + + return can_reinject; +} + + +const xqc_reinj_ctl_callback_t xqc_dgram_reinj_ctl_cb = { + .xqc_reinj_ctl_size = xqc_dgram_reinj_ctl_size, + .xqc_reinj_ctl_init = xqc_dgram_reinj_ctl_init, + .xqc_reinj_ctl_can_reinject = xqc_dgram_reinj_can_reinject, +}; \ No newline at end of file diff --git a/src/transport/reinjection_control/xqc_reinj_dgram.h b/src/transport/reinjection_control/xqc_reinj_dgram.h new file mode 100644 index 000000000..de7e73d37 --- /dev/null +++ b/src/transport/reinjection_control/xqc_reinj_dgram.h @@ -0,0 +1,18 @@ +/** + * @copyright Copyright (c) 2022, Alibaba Group Holding Limited + */ + +#ifndef _XQC_REINJ_DGRAM_H_INCLUDED_ +#define _XQC_REINJ_DGRAM_H_INCLUDED_ + +#include +#include + +typedef struct { + xqc_log_t *log; + xqc_connection_t *conn; +} xqc_dgram_reinj_ctl_t; + +extern const xqc_reinj_ctl_callback_t xqc_dgram_reinj_ctl_cb; + +#endif diff --git a/src/transport/reinjection_control/xqc_reinj_xlink.c b/src/transport/reinjection_control/xqc_reinj_xlink.c deleted file mode 100644 index 83f09f7d4..000000000 --- a/src/transport/reinjection_control/xqc_reinj_xlink.c +++ /dev/null @@ -1,80 +0,0 @@ -/** - * @copyright Copyright (c) 2022, Alibaba Group Holding Limited - */ - - -#include "src/transport/reinjection_control/xqc_reinj_xlink.h" - -#include "src/transport/xqc_reinjection.h" -#include "src/transport/xqc_multipath.h" -#include "src/transport/xqc_conn.h" -#include "src/transport/xqc_send_ctl.h" -#include "src/transport/xqc_engine.h" -#include "src/transport/xqc_cid.h" -#include "src/transport/xqc_stream.h" -#include "src/transport/xqc_utils.h" -#include "src/transport/xqc_wakeup_pq.h" -#include "src/transport/xqc_packet_out.h" - -#include "src/common/xqc_common.h" -#include "src/common/xqc_malloc.h" -#include "src/common/xqc_str_hash.h" -#include "src/common/xqc_hash.h" -#include "src/common/xqc_priority_q.h" -#include "src/common/xqc_memory_pool.h" -#include "src/common/xqc_random.h" - -#include "xquic/xqc_errno.h" - - -static size_t -xqc_xlink_reinj_ctl_size() -{ - return sizeof(xqc_xlink_reinj_ctl_t); -} - -static void -xqc_xlink_reinj_ctl_init(void *reinj_ctl, const xqc_conn_settings_t *settings, xqc_log_t *log) -{ - xqc_xlink_reinj_ctl_t *xlink = (xqc_xlink_reinj_ctl_t *)reinj_ctl; - - xlink->log = log; -} - - -static xqc_bool_t -xqc_xlink_reinj_ctl_lost_queue(void *reinj_ctl, void *qoe_ctx, xqc_connection_t *conn) -{ - if(!conn->conn_settings.mp_enable_reinjection) { - return XQC_FALSE; - } - - if (conn->active_path_count < 2) { - return XQC_FALSE; - } - - return XQC_TRUE; -} - - -static xqc_bool_t -xqc_xlink_reinj_ctl_unack_queue(void *reinj_ctl, void *qoe_ctx, xqc_connection_t *conn) -{ - if(!conn->conn_settings.mp_enable_reinjection) { - return XQC_FALSE; - } - - if (conn->active_path_count < 2) { - return XQC_FALSE; - } - - return XQC_TRUE; -} - - -const xqc_reinj_ctl_callback_t xqc_xlink_reinj_ctl_cb = { - .xqc_reinj_ctl_size = xqc_xlink_reinj_ctl_size, - .xqc_reinj_ctl_init = xqc_xlink_reinj_ctl_init, - .xqc_reinj_ctl_lost_queue = xqc_xlink_reinj_ctl_lost_queue, - .xqc_reinj_ctl_unack_queue = xqc_xlink_reinj_ctl_unack_queue, -}; \ No newline at end of file diff --git a/src/transport/reinjection_control/xqc_reinj_xlink.h b/src/transport/reinjection_control/xqc_reinj_xlink.h deleted file mode 100644 index a33bd46b7..000000000 --- a/src/transport/reinjection_control/xqc_reinj_xlink.h +++ /dev/null @@ -1,17 +0,0 @@ -/** - * @copyright Copyright (c) 2022, Alibaba Group Holding Limited - */ - -#ifndef _XQC_REINJ_XLINK_H_INCLUDED_ -#define _XQC_REINJ_XLINK_H_INCLUDED_ - -#include -#include - -typedef struct { - xqc_log_t *log; -} xqc_xlink_reinj_ctl_t; - -extern const xqc_reinj_ctl_callback_t xqc_xlink_reinj_ctl_cb; - -#endif /* _XQC_REINJ_XLINK_H_INCLUDED_ */ diff --git a/src/transport/scheduler/xqc_scheduler_backup.c b/src/transport/scheduler/xqc_scheduler_backup.c index 38904ec45..a6950779e 100644 --- a/src/transport/scheduler/xqc_scheduler_backup.c +++ b/src/transport/scheduler/xqc_scheduler_backup.c @@ -7,9 +7,6 @@ #include "src/transport/scheduler/xqc_scheduler_common.h" #include "src/transport/xqc_send_ctl.h" -#define BACKUP_PATH_PROBE_SUCCESS_TIMEOUT 3000000 -#define BACKUP_PATH_PROBE_TIMEOUT 3000000 - static size_t xqc_backup_scheduler_size() @@ -23,54 +20,14 @@ xqc_backup_scheduler_init(void *scheduler, xqc_log_t *log, xqc_scheduler_params_ return; } -static inline xqc_bool_t -xqc_path_can_schedule(xqc_path_ctx_t *path, xqc_packet_out_t *packet_out) -{ - if (path->path_state != XQC_PATH_STATE_ACTIVE) { - return XQC_FALSE; - } - - if (packet_out->po_flag & XQC_POF_NOT_SCHEDULE) { - if ((path->tra_path_status != XQC_TRA_PATH_STATUS_IN_USE) && (path->parent_conn->in_use_active_path_count > 0)) { - return XQC_FALSE; - } - } - - return XQC_TRUE; -} - -static inline xqc_bool_t -xqc_path_can_reinject(xqc_path_ctx_t *path, xqc_packet_out_t *packet_out) -{ - if (path->path_state != XQC_PATH_STATE_ACTIVE) { - return XQC_FALSE; - } - - if (path->path_id == packet_out->po_path_id) { - return XQC_FALSE; - } - - if (packet_out->po_flag & XQC_POF_NOT_REINJECT) { - if ((path->tra_path_status != XQC_TRA_PATH_STATUS_IN_USE) && (path->app_path_status != XQC_APP_PATH_STATUS_AVAILABLE)) { - return XQC_FALSE; - } - } - - return XQC_TRUE; -} - -static inline xqc_bool_t -xqc_path_can_probe(xqc_path_ctx_t *path) -{ - return (path->path_state == XQC_PATH_STATE_ACTIVE) - && (path->tra_path_status == XQC_TRA_PATH_STATUS_BACKUP); -} xqc_path_ctx_t * xqc_backup_scheduler_get_path(void *scheduler, - xqc_connection_t *conn, xqc_packet_out_t *packet_out, int check_cwnd, int reinject) + xqc_connection_t *conn, xqc_packet_out_t *packet_out, int check_cwnd, int reinject, + xqc_bool_t *cc_blocked) { xqc_path_ctx_t *best_path = NULL; + xqc_path_ctx_t *best_standby_path = NULL; xqc_list_head_t *pos, *next; xqc_path_ctx_t *path; @@ -78,232 +35,131 @@ xqc_backup_scheduler_get_path(void *scheduler, /* min RTT */ uint64_t min_rtt = XQC_MAX_UINT64_VALUE; + uint64_t min_rtt_standby = XQC_MAX_UINT64_VALUE; uint64_t path_srtt; + uint32_t avail_path_cnt = 0; + xqc_bool_t reached_cwnd_check = XQC_FALSE; + + if (cc_blocked) { + *cc_blocked = XQC_FALSE; + } xqc_list_for_each_safe(pos, next, &conn->conn_paths_list) { path = xqc_list_entry(pos, xqc_path_ctx_t, path_list); - if (reinject) { - if (!xqc_path_can_reinject(path, packet_out)) { - continue; - } - - } else { - if (!xqc_path_can_schedule(path, packet_out)) { - continue; - } + /* skip inactive paths */ + if (path->path_state != XQC_PATH_STATE_ACTIVE) { + continue; } - if (!xqc_scheduler_check_path_can_send(path, packet_out, check_cwnd)) { + /* skip frozen paths */ + if (path->app_path_status == XQC_APP_PATH_STATUS_FROZEN) { continue; } - path_srtt = xqc_send_ctl_get_srtt(path->path_send_ctl); - xqc_log(conn->log, XQC_LOG_DEBUG, "|path srtt|conn:%p|path_id:%ui|path_srtt:%ui|", - conn, path->path_id, path_srtt); - - if (path_srtt < min_rtt) { - best_path = path; - min_rtt = path_srtt; + if (path->app_path_status == XQC_APP_PATH_STATUS_AVAILABLE) { + avail_path_cnt++; } - } - - if (best_path == NULL) { - xqc_log(conn->log, XQC_LOG_DEBUG, "|No available paths to schedule|conn:%p|", conn); - } else { - xqc_log(conn->log, XQC_LOG_DEBUG, "|best path:%ui|frame_type:%s|", - best_path->path_id, xqc_frame_type_2_str(packet_out->po_frame_types)); - } - - return best_path; - -} - -static inline xqc_int_t -xqc_backup_probe_standby_path(xqc_connection_t *conn, - xqc_path_ctx_t **default_path, xqc_path_ctx_t **standby_path) -{ - xqc_int_t ret; - xqc_list_head_t *pos, *next; - xqc_path_ctx_t *path; - xqc_send_ctl_t *send_ctl; - - xqc_usec_t now = xqc_monotonic_timestamp(); - xqc_usec_t last_time; + if (reinject && (packet_out->po_path_id == path->path_id)) { + continue; + } - xqc_list_for_each_safe(pos, next, &conn->conn_paths_list) { - path = xqc_list_entry(pos, xqc_path_ctx_t, path_list); + if (!reached_cwnd_check) { + reached_cwnd_check = XQC_TRUE; + if (cc_blocked) { + *cc_blocked = XQC_TRUE; + } + } - if (path->path_state != XQC_PATH_STATE_ACTIVE) { + if (!xqc_scheduler_check_path_can_send(path, packet_out, check_cwnd)) { continue; } - if (path->app_path_status == XQC_APP_PATH_STATUS_AVAILABLE) { - *default_path = path; - - } else if (path->app_path_status == XQC_APP_PATH_STATUS_STANDBY) { - *standby_path = path; + if (cc_blocked) { + *cc_blocked = XQC_FALSE; } - if (conn->conn_settings.standby_path_probe_timeout > 0 - && xqc_path_can_probe(path)) - { - send_ctl = path->path_send_ctl; - last_time = send_ctl->ctl_time_of_last_sent_ack_eliciting_packet[XQC_PNS_APP_DATA]; - - if ((now - last_time) >= conn->conn_settings.standby_path_probe_timeout * 1000) { - ret = xqc_path_standby_probe(path); - if (ret != XQC_OK) { - xqc_log(conn->log, XQC_LOG_ERROR, "|xqc_path_standby_probe error|ret:%d|path%ui|", ret, path->path_id); - return ret; - } + path_srtt = xqc_send_ctl_get_srtt(path->path_send_ctl); + xqc_log(conn->log, XQC_LOG_DEBUG, "|path srtt|conn:%p|path_id:%ui|path_srtt:%ui|", + conn, path->path_id, path_srtt); - xqc_log(conn->log, XQC_LOG_DEBUG, "|xqc_path_standby_probe ret:%d|" - "path:%ui|state:%d|app_path_status:%d|tra_path_status:%d|last_time:%ui|now:%ui|now-last_time:%ui|cwnd:%ui|srtt:%ui|retrans:%.4f|spurious:%.4f|\n", - ret, path->path_id, path->path_state, path->app_path_status, path->tra_path_status, - last_time, now, (now - last_time), - send_ctl->ctl_cong_callback->xqc_cong_ctl_get_cwnd(send_ctl->ctl_cong), - xqc_send_ctl_get_srtt(send_ctl), - xqc_send_ctl_get_retrans_rate(send_ctl), - xqc_send_ctl_get_spurious_loss_rate(send_ctl)); + if (path_srtt < min_rtt) { + if (path->app_path_status == XQC_APP_PATH_STATUS_AVAILABLE) { + best_path = path; + min_rtt = path_srtt; + + } else { + best_standby_path = path; + min_rtt_standby = path_srtt; } - } } - return XQC_OK; -} - -static inline xqc_bool_t -xqc_default_path_is_in_use(xqc_path_ctx_t *default_path, xqc_path_ctx_t *standby_path) -{ - return (default_path->tra_path_status == XQC_TRA_PATH_STATUS_IN_USE) - && (standby_path->tra_path_status == XQC_TRA_PATH_STATUS_BACKUP); -} - -static inline xqc_bool_t -xqc_standby_path_is_in_use(xqc_path_ctx_t *default_path, xqc_path_ctx_t *standby_path) -{ - return (default_path->tra_path_status == XQC_TRA_PATH_STATUS_BACKUP) - && (standby_path->tra_path_status == XQC_TRA_PATH_STATUS_IN_USE); -} - -static inline xqc_bool_t -xqc_default_path_need_degrade(xqc_connection_t *conn, - xqc_path_ctx_t *default_path, xqc_path_ctx_t *standby_path) -{ - if (conn->transport_cbs.path_status_controller) { - return conn->transport_cbs.path_status_controller(conn, &conn->scid_set.user_scid, - default_path->path_id, XQC_PATH_DEGRADE, - xqc_conn_get_user_data(conn)); - } + if (best_path == NULL) { + if (best_standby_path != NULL + && (avail_path_cnt == 0 || reinject)) + { + best_path = best_standby_path; - if (default_path->path_send_ctl->ctl_pto_count_since_last_tra_path_status_changed > conn->conn_settings.path_unreachable_pto_count) { - return XQC_TRUE; + } else { + xqc_log(conn->log, XQC_LOG_DEBUG, "|No available paths to schedule|conn:%p|", conn); + } } - return XQC_FALSE; -} -static inline xqc_bool_t -xqc_default_path_need_recovery(xqc_connection_t *conn, - xqc_path_ctx_t *default_path, xqc_path_ctx_t *standby_path) -{ - if (conn->transport_cbs.path_status_controller) { - return conn->transport_cbs.path_status_controller(conn, &conn->scid_set.user_scid, - default_path->path_id, XQC_PATH_RECOVERY, - xqc_conn_get_user_data(conn)); + if (best_path) { + xqc_log(conn->log, XQC_LOG_DEBUG, "|best path:%ui|frame_type:%s|app_status:%d|", + best_path->path_id, xqc_frame_type_2_str(packet_out->po_frame_types), + best_path->app_path_status); } - xqc_send_ctl_t *send_ctl = default_path->path_send_ctl; - if (send_ctl->ctl_send_count - send_ctl->ctl_send_count_at_last_tra_path_status_changed_time > 3 - && xqc_monotonic_timestamp() - default_path->last_tra_path_status_changed_time > BACKUP_PATH_PROBE_TIMEOUT - && send_ctl->ctl_pto_count_since_last_tra_path_status_changed < 1) - { - return XQC_TRUE; - } - - return XQC_FALSE; -} - -void -xqc_probe_before_use(xqc_connection_t *conn, - xqc_path_ctx_t *next_in_use_path, xqc_path_ctx_t *next_backup_path) -{ - xqc_usec_t now = xqc_monotonic_timestamp(); - xqc_usec_t last_recv_time = next_in_use_path->path_send_ctl->ctl_largest_recv_time[XQC_PNS_APP_DATA]; - xqc_usec_t last_send_time = next_in_use_path->path_send_ctl->ctl_time_of_last_sent_ack_eliciting_packet[XQC_PNS_APP_DATA]; - - if ((now - last_recv_time) <= BACKUP_PATH_PROBE_SUCCESS_TIMEOUT) { - xqc_set_transport_path_status(next_in_use_path, XQC_TRA_PATH_STATUS_IN_USE, now); - xqc_set_transport_path_status(next_backup_path, XQC_TRA_PATH_STATUS_BACKUP, now); - - } else if ((now - last_send_time) >= BACKUP_PATH_PROBE_TIMEOUT) { - xqc_int_t ret = xqc_path_standby_probe(next_in_use_path); - if (ret != XQC_OK) { - xqc_log(conn->log, XQC_LOG_ERROR, "|xqc_path_standby_probe error|ret:%d|path%ui|", ret, next_in_use_path->path_id); - return; - } - } + return best_path; } - -void +void xqc_backup_scheduler_handle_conn_event(void *scheduler, xqc_connection_t *conn, xqc_scheduler_conn_event_t event, void *event_arg) { - if (event == XQC_SCHED_EVENT_CONN_ROUND_START) { - - if (conn->active_path_count < 2) { - return; - } - - if (XQC_UNLIKELY(conn->conn_state >= XQC_CONN_STATE_CLOSING)) { - return; - } + xqc_list_head_t *pos, *next; + xqc_path_ctx_t *path; + xqc_send_ctl_t *send_ctl; + xqc_usec_t now = 0, deadline; - xqc_int_t ret = XQC_ERROR; + if (event == XQC_SCHED_EVENT_CONN_ROUND_START + && conn->conn_settings.standby_path_probe_timeout + && conn->enable_multipath + && (conn->conn_state == XQC_CONN_STATE_ESTABED)) + { + /* check if we need to probe standby paths */ + xqc_list_for_each_safe(pos, next, &conn->conn_paths_list) { + path = xqc_list_entry(pos, xqc_path_ctx_t, path_list); + if (path->path_state == XQC_PATH_STATE_ACTIVE + && path->app_path_status == XQC_APP_PATH_STATUS_STANDBY) + { + if (!now) { + now = xqc_monotonic_timestamp(); + } - xqc_path_ctx_t *default_path = NULL; - xqc_path_ctx_t *standby_path = NULL; + send_ctl = path->path_send_ctl; - ret = xqc_backup_probe_standby_path(conn, &default_path, &standby_path); - if (ret != XQC_OK) { - xqc_log(conn->log, XQC_LOG_ERROR, "|xqc_backup_probe_standby_path error|ret:%d|", ret); - return; - } + deadline = send_ctl->ctl_time_of_last_sent_ack_eliciting_packet[XQC_PNS_APP_DATA] + conn->conn_settings.standby_path_probe_timeout * 1000; - if (default_path == NULL || standby_path == NULL) { - xqc_log(conn->log, XQC_LOG_DEBUG, "|identify default/standby path error|"); - return; - } + xqc_log(conn->log, XQC_LOG_DEBUG, "|standby_probe|path:%ui|deadline:%ui|now:%ui|now>=deadline:%d|last_send:%ui|", + path->path_id, deadline, now, now >= deadline, send_ctl->ctl_time_of_last_sent_ack_eliciting_packet[XQC_PNS_APP_DATA]); - if (xqc_default_path_is_in_use(default_path, standby_path)) { - if (xqc_default_path_need_degrade(conn, default_path, standby_path)) { - xqc_probe_before_use(conn, standby_path, default_path); - } - - } else if (xqc_standby_path_is_in_use(default_path, standby_path)) { - if (xqc_default_path_need_recovery(conn, default_path, standby_path)) { - xqc_probe_before_use(conn,default_path, standby_path); + if (now >= deadline) { + xqc_path_standby_probe(path); + } } - - } else { - xqc_log(conn->log, XQC_LOG_ERROR, - "|path status error|default_path:%ui|app_path_status:%d|tra_path_status:%d|standby_path:%ui|app_path_status:%d|tra_path_status:%d|", - default_path->path_id, default_path->app_path_status, default_path->tra_path_status, - standby_path->path_id, standby_path->app_path_status, standby_path->tra_path_status); - return; } } - } const xqc_scheduler_callback_t xqc_backup_scheduler_cb = { - .xqc_scheduler_size = xqc_backup_scheduler_size, - .xqc_scheduler_init = xqc_backup_scheduler_init, - .xqc_scheduler_get_path = xqc_backup_scheduler_get_path, + .xqc_scheduler_size = xqc_backup_scheduler_size, + .xqc_scheduler_init = xqc_backup_scheduler_init, + .xqc_scheduler_get_path = xqc_backup_scheduler_get_path, .xqc_scheduler_handle_conn_event = xqc_backup_scheduler_handle_conn_event, }; \ No newline at end of file diff --git a/src/transport/scheduler/xqc_scheduler_interop.c b/src/transport/scheduler/xqc_scheduler_interop.c new file mode 100644 index 000000000..1d207a79b --- /dev/null +++ b/src/transport/scheduler/xqc_scheduler_interop.c @@ -0,0 +1,115 @@ +/** + * @copyright Copyright (c) 2022, Alibaba Group Holding Limited + */ + +#include "src/transport/scheduler/xqc_scheduler_interop.h" +#include "src/transport/scheduler/xqc_scheduler_common.h" +#include "src/transport/xqc_send_ctl.h" + + +static size_t +xqc_interop_scheduler_size() +{ + return 0; +} + +static void +xqc_interop_scheduler_init(void *scheduler, xqc_log_t *log, xqc_scheduler_params_t *param) +{ + return; +} + +xqc_path_ctx_t * +xqc_interop_scheduler_get_path(void *scheduler, + xqc_connection_t *conn, xqc_packet_out_t *packet_out, int check_cwnd, int reinject, + xqc_bool_t *cc_blocked) +{ + xqc_path_ctx_t *best_path = NULL; + xqc_path_ctx_t *best_standby_path = NULL; + + xqc_list_head_t *pos, *next; + xqc_path_ctx_t *path; + + xqc_send_ctl_t *send_ctl; + + /* min RTT */ + uint64_t min_rtt = XQC_MAX_UINT64_VALUE; + uint64_t min_rtt_standby = XQC_MAX_UINT64_VALUE; + uint64_t path_srtt; + uint32_t avail_path_cnt = 0; + xqc_bool_t reached_cwnd_check = XQC_FALSE; + + if (cc_blocked) { + *cc_blocked = XQC_FALSE; + } + + xqc_list_for_each_safe(pos, next, &conn->conn_paths_list) { + path = xqc_list_entry(pos, xqc_path_ctx_t, path_list); + + if (path->path_state != XQC_PATH_STATE_ACTIVE) { + continue; + } + + if (path->app_path_status == XQC_APP_PATH_STATUS_AVAILABLE) { + avail_path_cnt++; + } + + if (reinject && (packet_out->po_path_id == path->path_id)) { + continue; + } + + if (!reached_cwnd_check) { + reached_cwnd_check = XQC_TRUE; + if (cc_blocked) { + *cc_blocked = XQC_TRUE; + } + } + + if (!xqc_scheduler_check_path_can_send(path, packet_out, check_cwnd)) { + continue; + } + + if (cc_blocked) { + *cc_blocked = XQC_FALSE; + } + + path_srtt = xqc_send_ctl_get_srtt(path->path_send_ctl); + xqc_log(conn->log, XQC_LOG_DEBUG, "|path srtt|conn:%p|path_id:%ui|path_srtt:%ui|", + conn, path->path_id, path_srtt); + + if (path_srtt < min_rtt) { + if (path->app_path_status == XQC_APP_PATH_STATUS_AVAILABLE) { + best_path = path; + min_rtt = path_srtt; + + } else { + best_standby_path = path; + min_rtt_standby = path_srtt; + } + } + } + + if (best_path == NULL) { + if (avail_path_cnt > 0 || best_standby_path == NULL) { + xqc_log(conn->log, XQC_LOG_DEBUG, "|No available paths to schedule|conn:%p|", conn); + + } else { + best_path = best_standby_path; + } + + } + + if (best_path) { + xqc_log(conn->log, XQC_LOG_DEBUG, "|best path:%ui|frame_type:%s|app_status:%d|", + best_path->path_id, xqc_frame_type_2_str(packet_out->po_frame_types), + best_path->app_path_status); + } + + return best_path; +} + +const xqc_scheduler_callback_t xqc_interop_scheduler_cb = { + .xqc_scheduler_size = xqc_interop_scheduler_size, + .xqc_scheduler_init = xqc_interop_scheduler_init, + .xqc_scheduler_get_path = xqc_interop_scheduler_get_path, +}; diff --git a/src/transport/scheduler/xqc_scheduler_interop.h b/src/transport/scheduler/xqc_scheduler_interop.h new file mode 100644 index 000000000..3d9cd5b8b --- /dev/null +++ b/src/transport/scheduler/xqc_scheduler_interop.h @@ -0,0 +1,13 @@ +/** + * @copyright Copyright (c) 2022, Alibaba Group Holding Limited + */ + +#ifndef _XQC_SCHEDULER_INTEROP_H_INCLUDED_ +#define _XQC_SCHEDULER_INTEROP_H_INCLUDED_ + +#include +#include + +extern const xqc_scheduler_callback_t xqc_interop_scheduler_cb; + +#endif diff --git a/src/transport/scheduler/xqc_scheduler_minrtt.c b/src/transport/scheduler/xqc_scheduler_minrtt.c index e35f0eb32..ea2783734 100644 --- a/src/transport/scheduler/xqc_scheduler_minrtt.c +++ b/src/transport/scheduler/xqc_scheduler_minrtt.c @@ -22,7 +22,8 @@ xqc_minrtt_scheduler_init(void *scheduler, xqc_log_t *log, xqc_scheduler_params_ xqc_path_ctx_t * xqc_minrtt_scheduler_get_path(void *scheduler, - xqc_connection_t *conn, xqc_packet_out_t *packet_out, int check_cwnd, int reinject) + xqc_connection_t *conn, xqc_packet_out_t *packet_out, int check_cwnd, int reinject, + xqc_bool_t *cc_blocked) { xqc_path_ctx_t *best_path = NULL; @@ -33,6 +34,11 @@ xqc_minrtt_scheduler_get_path(void *scheduler, /* min RTT */ uint64_t min_rtt = XQC_MAX_UINT64_VALUE; uint64_t path_srtt; + xqc_bool_t reached_cwnd_check = XQC_FALSE; + + if (cc_blocked) { + *cc_blocked = XQC_FALSE; + } xqc_list_for_each_safe(pos, next, &conn->conn_paths_list) { path = xqc_list_entry(pos, xqc_path_ctx_t, path_list); @@ -41,7 +47,8 @@ xqc_minrtt_scheduler_get_path(void *scheduler, continue; } - if ((path->tra_path_status != XQC_TRA_PATH_STATUS_IN_USE) && (conn->in_use_active_path_count > 0)) { + /* skip the frozen path */ + if (path->app_path_status == XQC_APP_PATH_STATUS_FROZEN) { continue; } @@ -49,6 +56,13 @@ xqc_minrtt_scheduler_get_path(void *scheduler, continue; } + if (!reached_cwnd_check) { + reached_cwnd_check = XQC_TRUE; + if (cc_blocked) { + *cc_blocked = XQC_TRUE; + } + } + /* @TODO: It is not correct for BBR/BBRv2, as they do not used cwnd to decide * how much data can be sent in one RTT. But, currently, BBR does not * work well for MPQUIC due to the problem of applimit. We may adapt this @@ -58,6 +72,10 @@ xqc_minrtt_scheduler_get_path(void *scheduler, continue; } + if (cc_blocked) { + *cc_blocked = XQC_FALSE; + } + path_srtt = xqc_send_ctl_get_srtt(path->path_send_ctl); xqc_log(conn->log, XQC_LOG_DEBUG, "|path srtt|conn:%p|path_id:%ui|path_srtt:%ui|", conn, path->path_id, path_srtt); diff --git a/src/transport/scheduler/xqc_scheduler_minrtt.h b/src/transport/scheduler/xqc_scheduler_minrtt.h index 8e3a1b69f..29afef566 100644 --- a/src/transport/scheduler/xqc_scheduler_minrtt.h +++ b/src/transport/scheduler/xqc_scheduler_minrtt.h @@ -10,8 +10,4 @@ extern const xqc_scheduler_callback_t xqc_minrtt_scheduler_cb; -xqc_path_ctx_t* -xqc_minrtt_scheduler_get_path(void *scheduler, xqc_connection_t *conn, - xqc_packet_out_t *packet_out, int check_cwnd, int reinject); - #endif /* _XQC_SCHEDULER_MINRTT_H_INCLUDED_ */ diff --git a/src/transport/scheduler/xqc_scheduler_rap.c b/src/transport/scheduler/xqc_scheduler_rap.c new file mode 100644 index 000000000..96a13373d --- /dev/null +++ b/src/transport/scheduler/xqc_scheduler_rap.c @@ -0,0 +1,112 @@ +/** + * @copyright Copyright (c) 2022, Alibaba Group Holding Limited + */ + + +#include "src/transport/scheduler/xqc_scheduler_rap.h" +#include "src/transport/scheduler/xqc_scheduler_common.h" +#include "src/transport/xqc_send_ctl.h" + + +static size_t +xqc_rap_scheduler_size() +{ + return 0; +} + +static void +xqc_rap_scheduler_init(void *scheduler, xqc_log_t *log, xqc_scheduler_params_t *param) +{ + return; +} + +xqc_path_ctx_t * +xqc_rap_scheduler_get_path(void *scheduler, + xqc_connection_t *conn, xqc_packet_out_t *packet_out, int check_cwnd, int reinject, + xqc_bool_t *cc_blocked) +{ + xqc_path_ctx_t *best_path = NULL; + xqc_path_ctx_t *original_path = NULL; + + xqc_list_head_t *pos, *next; + xqc_path_ctx_t *path; + xqc_send_ctl_t *send_ctl; + + /* min RTT */ + uint64_t min_rtt = XQC_MAX_UINT64_VALUE; + uint64_t path_srtt; + xqc_bool_t reached_cwnd_check = XQC_FALSE; + + if (cc_blocked) { + *cc_blocked = XQC_FALSE; + } + + xqc_list_for_each_safe(pos, next, &conn->conn_paths_list) { + path = xqc_list_entry(pos, xqc_path_ctx_t, path_list); + + if (path->path_state != XQC_PATH_STATE_ACTIVE) { + continue; + } + + /* skip the frozen path */ + if (path->app_path_status == XQC_APP_PATH_STATUS_FROZEN) { + continue; + } + + if (!reached_cwnd_check) { + reached_cwnd_check = XQC_TRUE; + if (cc_blocked) { + *cc_blocked = XQC_TRUE; + } + } + + if (!xqc_scheduler_check_path_can_send(path, packet_out, check_cwnd)) { + continue; + } + + if (cc_blocked) { + *cc_blocked = XQC_FALSE; + } + + if (reinject && (packet_out->po_path_id == path->path_id)) { + original_path = path; + continue; + } + + path_srtt = xqc_send_ctl_get_srtt(path->path_send_ctl); + xqc_log(conn->log, XQC_LOG_DEBUG, "|path srtt|conn:%p|path_id:%ui|path_srtt:%ui|", + conn, path->path_id, path_srtt); + + if (path_srtt < min_rtt) { + best_path = path; + min_rtt = path_srtt; + } + } + + if (best_path == NULL) { + if (original_path == NULL) { + xqc_log(conn->log, XQC_LOG_DEBUG, "|No available paths to schedule|conn:%p|", conn); + + } else { + if (!(packet_out->po_flag & XQC_POF_REINJECT_DIFF_PATH)) { + best_path = original_path; + xqc_log(conn->log, XQC_LOG_DEBUG, "|the original path is selected|conn:%p|", conn); + + } else { + xqc_log(conn->log, XQC_LOG_DEBUG, "|the packet must be reinjected on a different path|conn:%p|", conn); + } + } + + } else { + xqc_log(conn->log, XQC_LOG_DEBUG, "|best path:%ui|frame_type:%s|", + best_path->path_id, xqc_frame_type_2_str(packet_out->po_frame_types)); + } + + return best_path; +} + +const xqc_scheduler_callback_t xqc_rap_scheduler_cb = { + .xqc_scheduler_size = xqc_rap_scheduler_size, + .xqc_scheduler_init = xqc_rap_scheduler_init, + .xqc_scheduler_get_path = xqc_rap_scheduler_get_path, +}; \ No newline at end of file diff --git a/src/transport/scheduler/xqc_scheduler_rap.h b/src/transport/scheduler/xqc_scheduler_rap.h new file mode 100644 index 000000000..4f457338a --- /dev/null +++ b/src/transport/scheduler/xqc_scheduler_rap.h @@ -0,0 +1,19 @@ +/** + * @copyright Copyright (c) 2022, Alibaba Group Holding Limited + */ + +#ifndef _XQC_SCHEDULER_MINRTT_H_INCLUDED_ +#define _XQC_SCHEDULER_MINRTT_H_INCLUDED_ + +#include +#include + +/* + * RAP: Reinjection on Any Path. Reinjected packets can be scheduled onto + * the same path where the original packets were sent if no other paths + * are available. + */ + +extern const xqc_scheduler_callback_t xqc_rap_scheduler_cb; + +#endif diff --git a/src/transport/xqc_cid.c b/src/transport/xqc_cid.c index 12b9a3cf4..68a9d3ff0 100644 --- a/src/transport/xqc_cid.c +++ b/src/transport/xqc_cid.c @@ -63,6 +63,7 @@ xqc_cid_copy(xqc_cid_t *dst, xqc_cid_t *src) dst->cid_len = src->cid_len; xqc_memcpy(dst->cid_buf, src->cid_buf, dst->cid_len); dst->cid_seq_num = src->cid_seq_num; + xqc_memcpy(dst->sr_token, src->sr_token, XQC_STATELESS_RESET_TOKENLEN); } void @@ -83,6 +84,7 @@ xqc_cid_set(xqc_cid_t *cid, const unsigned char *data, uint8_t len) static unsigned char g_scid_buf[XQC_MAX_CID_LEN * 2 + 1]; static unsigned char g_dcid_buf[XQC_MAX_CID_LEN * 2 + 1]; +static unsigned char g_sr_token_buf[XQC_STATELESS_RESET_TOKENLEN * 2 + 1]; unsigned char * xqc_dcid_str(const xqc_cid_t *dcid) @@ -100,6 +102,14 @@ xqc_scid_str(const xqc_cid_t *scid) return g_scid_buf; } +unsigned char * +xqc_sr_token_str(const char *sr_token) +{ + xqc_hex_dump(g_sr_token_buf, sr_token, XQC_STATELESS_RESET_TOKENLEN); + g_sr_token_buf[XQC_STATELESS_RESET_TOKENLEN * 2] = '\0'; + return g_sr_token_buf; +} + unsigned char * xqc_dcid_str_by_scid(xqc_engine_t *engine, const xqc_cid_t *scid) { diff --git a/src/transport/xqc_cid.h b/src/transport/xqc_cid.h index 4758ab343..9acfe4a76 100644 --- a/src/transport/xqc_cid.h +++ b/src/transport/xqc_cid.h @@ -71,5 +71,8 @@ xqc_cid_inner_t *xqc_cid_in_cid_set(const xqc_cid_set_t *cid_set, xqc_cid_t *cid xqc_int_t xqc_cid_switch_to_next_state(xqc_cid_set_t *cid_set, xqc_cid_inner_t *cid, xqc_cid_state_t state); xqc_int_t xqc_get_unused_cid(xqc_cid_set_t *cid_set, xqc_cid_t *cid); + +unsigned char *xqc_sr_token_str(const char *sr_token); + #endif /* _XQC_CID_H_INCLUDED_ */ diff --git a/src/transport/xqc_client.c b/src/transport/xqc_client.c index 729867924..48c2609a8 100644 --- a/src/transport/xqc_client.c +++ b/src/transport/xqc_client.c @@ -14,6 +14,7 @@ #include "src/transport/xqc_utils.h" #include "src/transport/xqc_defs.h" #include "src/tls/xqc_tls.h" +#include "src/transport/xqc_datagram.h" xqc_connection_t * xqc_client_connect(xqc_engine_t *engine, const xqc_conn_settings_t *conn_settings, @@ -118,6 +119,7 @@ xqc_connect(xqc_engine_t *engine, const xqc_conn_settings_t *conn_settings, return &conn->scid_set.user_scid; } + xqc_log(engine->log, XQC_LOG_ERROR, "|xqc_client_connect error|"); return NULL; } @@ -158,7 +160,7 @@ xqc_client_create_tls(xqc_connection_t *conn, const xqc_conn_ssl_config_t *conn_ ret = -XQC_EMALLOC; goto end; } - strncpy(cfg.alpn, alpn, alpn_cap); + memcpy(cfg.alpn, alpn, alpn_cap); /* copy hostname */ host_cap = strlen(hostname) + 1; @@ -168,7 +170,7 @@ xqc_client_create_tls(xqc_connection_t *conn, const xqc_conn_ssl_config_t *conn_ ret = -XQC_EMALLOC; goto end; } - strncpy(cfg.hostname, hostname, host_cap); + memcpy(cfg.hostname, hostname, host_cap); /* encode local transport parameters, and set to tls config */ cfg.trans_params = tp_buf; @@ -253,11 +255,13 @@ xqc_client_create_connection(xqc_engine_t *engine, xqc_cid_t dcid, xqc_cid_t sci if (conn_ssl_config->transport_parameter_data && conn_ssl_config->transport_parameter_data_len > 0) { - xqc_memzero(&tp, sizeof(xqc_transport_params_t)); + xqc_init_transport_params(&tp); ret = xqc_read_transport_params(conn_ssl_config->transport_parameter_data, conn_ssl_config->transport_parameter_data_len, &tp); if (ret == XQC_OK) { xqc_conn_set_early_remote_transport_params(xc, &tp); + xqc_log(xc->log, XQC_LOG_DEBUG, "|0RTT_transport_params|max_datagram_frame_size:%ud|", + xc->remote_settings.max_datagram_frame_size); } } @@ -265,6 +269,8 @@ xqc_client_create_connection(xqc_engine_t *engine, xqc_cid_t dcid, xqc_cid_t sci goto fail; } + xqc_datagram_record_mss(xc); + return xc; fail: diff --git a/src/transport/xqc_conn.c b/src/transport/xqc_conn.c index ffbc2a328..3cf8efef7 100644 --- a/src/transport/xqc_conn.c +++ b/src/transport/xqc_conn.c @@ -28,6 +28,8 @@ #include "src/transport/xqc_multipath.h" #include "src/transport/xqc_reinjection.h" #include "src/tls/xqc_tls.h" +#include "src/transport/xqc_datagram.h" +#include xqc_conn_settings_t default_conn_settings = { @@ -40,20 +42,52 @@ xqc_conn_settings_t default_conn_settings = { .init_idle_time_out = XQC_CONN_INITIAL_IDLE_TIMEOUT, .idle_time_out = XQC_CONN_DEFAULT_IDLE_TIMEOUT, .enable_multipath = 0, + .multipath_version = XQC_MULTIPATH_04, .spurious_loss_detect_on = 0, .anti_amplification_limit = XQC_DEFAULT_ANTI_AMPLIFICATION_LIMIT, .keyupdate_pkt_threshold = 0, .max_pkt_out_size = XQC_PACKET_OUT_SIZE, + .max_datagram_frame_size = 0, .mp_enable_reinjection = 0, + .mp_ack_on_any_path = 0, + .mp_ping_on = 0, + .max_ack_delay = XQC_DEFAULT_MAX_ACK_DELAY, + .ack_frequency = 2, + .loss_detection_pkt_thresh = XQC_kPacketThreshold, + .pto_backoff_factor = 2.0, + .datagram_redundancy = 0, + .datagram_force_retrans_on = 0, + .datagram_redundant_probe = 0, + + .reinj_flexible_deadline_srtt_factor = 1.1, + .reinj_hard_deadline = 500000, /* 500ms */ + .reinj_deadline_lower_bound = 20000, /* 20ms */ - .reinj_flexible_deadline_srtt_factor = 2.0, - .reinj_hard_deadline = XQC_MAX_UINT64_VALUE, - .reinj_deadline_lower_bound = 0, - - .path_unreachable_pto_count = 6, .standby_path_probe_timeout = 0, + .enable_pmtud = 0, + .pmtud_probing_interval = 500000, + .marking_reinjection = 0, + + .recv_rate_bytes_per_sec = 0, + .enable_stream_rate_limit = 0, }; + +static void +xqc_conn_dgram_probe_timeout(xqc_gp_timer_id_t gp_timer_id, + xqc_usec_t now, void *user_data) +{ + xqc_connection_t *conn = user_data; + xqc_int_t ret = XQC_OK; + size_t probe_size; + if (conn->last_dgram && conn->last_dgram->data_len != 0) { + probe_size = conn->last_dgram->data_len; + ret = xqc_datagram_send(conn, conn->last_dgram->data, probe_size, NULL, XQC_DATA_QOS_PROBING); + xqc_log(conn->log, XQC_LOG_DEBUG, "|timer_based_dgram_probe|ret:%d|dgram_sz:%z|", ret, probe_size); + } +} + + void xqc_server_set_conn_settings(const xqc_conn_settings_t *settings) { @@ -66,6 +100,35 @@ xqc_server_set_conn_settings(const xqc_conn_settings_t *settings) default_conn_settings.sndq_packets_used_max = settings->sndq_packets_used_max; default_conn_settings.linger = settings->linger; default_conn_settings.spurious_loss_detect_on = settings->spurious_loss_detect_on; + default_conn_settings.datagram_force_retrans_on = settings->datagram_force_retrans_on; + default_conn_settings.enable_pmtud = settings->enable_pmtud; + default_conn_settings.marking_reinjection = settings->marking_reinjection; + default_conn_settings.mp_ack_on_any_path = settings->mp_ack_on_any_path; + default_conn_settings.mp_ping_on = settings->mp_ping_on; + default_conn_settings.recv_rate_bytes_per_sec = settings->recv_rate_bytes_per_sec; + default_conn_settings.enable_stream_rate_limit = settings->enable_stream_rate_limit; + default_conn_settings.init_recv_window = settings->init_recv_window; + + if (default_conn_settings.init_recv_window) { + default_conn_settings.init_recv_window = xqc_max(default_conn_settings.init_recv_window, XQC_QUIC_MAX_MSS); + } + + if (settings->pmtud_probing_interval) { + default_conn_settings.pmtud_probing_interval = settings->pmtud_probing_interval; + } + + if (settings->max_ack_delay) { + default_conn_settings.max_ack_delay = xqc_min(settings->max_ack_delay, XQC_DEFAULT_MAX_ACK_DELAY); + } + + if (settings->datagram_redundant_probe) { + default_conn_settings.datagram_redundant_probe = xqc_max(settings->datagram_redundant_probe, + XQC_MIN_DATAGRAM_REDUNDANT_PROBE_INTERVAL); + } + + if (settings->datagram_redundancy <= XQC_MAX_DATAGRAM_REDUNDANCY) { + default_conn_settings.datagram_redundancy = settings->datagram_redundancy; + } if (settings->init_idle_time_out > 0) { default_conn_settings.init_idle_time_out = settings->init_idle_time_out; @@ -84,17 +147,41 @@ xqc_server_set_conn_settings(const xqc_conn_settings_t *settings) } default_conn_settings.keyupdate_pkt_threshold = settings->keyupdate_pkt_threshold; + default_conn_settings.max_datagram_frame_size = settings->max_datagram_frame_size; if (settings->max_pkt_out_size > default_conn_settings.max_pkt_out_size) { default_conn_settings.max_pkt_out_size = settings->max_pkt_out_size; } + if (default_conn_settings.max_pkt_out_size > XQC_MAX_PACKET_OUT_SIZE) { + default_conn_settings.max_pkt_out_size = XQC_MAX_PACKET_OUT_SIZE; + } + default_conn_settings.enable_multipath = settings->enable_multipath; + if (xqc_conn_is_current_mp_version_supported(settings->multipath_version) == XQC_OK) { + default_conn_settings.multipath_version = settings->multipath_version; + + } else { + default_conn_settings.multipath_version = XQC_MULTIPATH_04; + } + default_conn_settings.scheduler_callback = settings->scheduler_callback; default_conn_settings.reinj_ctl_callback = settings->reinj_ctl_callback; default_conn_settings.mp_enable_reinjection = settings->mp_enable_reinjection; + if (settings->ack_frequency > 0) { + default_conn_settings.ack_frequency = settings->ack_frequency; + } + + if (settings->pto_backoff_factor > 0) { + default_conn_settings.pto_backoff_factor = settings->pto_backoff_factor; + } + + if (settings->loss_detection_pkt_thresh > 0) { + default_conn_settings.loss_detection_pkt_thresh = settings->loss_detection_pkt_thresh; + } + if (settings->reinj_flexible_deadline_srtt_factor > 0) { default_conn_settings.reinj_flexible_deadline_srtt_factor = settings->reinj_flexible_deadline_srtt_factor; } @@ -107,13 +194,11 @@ xqc_server_set_conn_settings(const xqc_conn_settings_t *settings) default_conn_settings.reinj_deadline_lower_bound = settings->reinj_deadline_lower_bound; } - if (settings->path_unreachable_pto_count > 0) { - default_conn_settings.path_unreachable_pto_count = settings->path_unreachable_pto_count; - } - if (settings->standby_path_probe_timeout > 0) { - default_conn_settings.standby_path_probe_timeout = settings->standby_path_probe_timeout; + /* no less than 500ms */ + default_conn_settings.standby_path_probe_timeout = xqc_max(settings->standby_path_probe_timeout, XQC_MIN_STANDBY_RPOBE_TIMEOUT); } + } static const char * const xqc_conn_flag_to_str[XQC_CONN_FLAG_SHIFT_NUM] = { @@ -145,14 +230,22 @@ static const char * const xqc_conn_flag_to_str[XQC_CONN_FLAG_SHIFT_NUM] = { [XQC_CONN_FLAG_HANDSHAKE_CONFIRMED_SHIFT] = "HSK_CONFIRMED", [XQC_CONN_FLAG_HANDSHAKE_DONE_ACKED_SHIFT] = "HSK_DONE_ACKED", [XQC_CONN_FLAG_ADDR_VALIDATED_SHIFT] = "ADDR_VALIDATED", - [XQC_CONN_FLAG_NEW_CID_RECEIVED_SHIFT] = "NEW_CID_RECEIVED", + [XQC_CONN_FLAG_NEW_CID_ACKED_SHIFT] = "NEW_CID_ACKED", [XQC_CONN_FLAG_LINGER_CLOSING_SHIFT] = "LINGER_CLOSING", [XQC_CONN_FLAG_RETRY_RECVD_SHIFT] = "RETRY_RECVD", [XQC_CONN_FLAG_TLS_HSK_COMPLETED_SHIFT] = "TLS_HSK_CMPTD", [XQC_CONN_FLAG_RECV_NEW_PATH_SHIFT] = "RECV_NEW_PATH", [XQC_CONN_FLAG_VALIDATE_REBINDING_SHIFT] = "VALIDATE_REBINDING", [XQC_CONN_FLAG_CONN_CLOSING_NOTIFY_SHIFT] = "CLOSING_NOTIFY", - [XQC_CONN_FLAG_CONN_CLOSING_NOTIFIED_SHIFT] = "CLOSING_NOTIFIED" + [XQC_CONN_FLAG_CONN_CLOSING_NOTIFIED_SHIFT] = "CLOSING_NOTIFIED", + [XQC_CONN_FLAG_DGRAM_WAIT_FOR_1RTT_SHIFT] = "DGRAM_WAIT_FOR_1RTT", + [XQC_CONN_FLAG_LOCAL_TP_UPDATED_SHIFT] = "LOCAL_TP_UPDATED", + [XQC_CONN_FLAG_PMTUD_PROBING_SHIFT] = "PMTUD_PROBING", + [XQC_CONN_FLAG_NO_DGRAM_NOTIFIED_SHIFT] = "NO_DGRAM_NOTIFIED", + [XQC_CONN_FLAG_DGRAM_MSS_NOTIFY_SHIFT] = "DGRAM_MSS_NOTIFY", + [XQC_CONN_FLAG_MP_WAIT_SCID_SHIFT] = "MP_WAIT_SCID", + [XQC_CONN_FLAG_MP_WAIT_DCID_SHIFT] = "MP_WAIT_DCID", + [XQC_CONN_FLAG_MP_READY_NOTIFY_SHIFT] = "MP_READY", }; unsigned char g_conn_flag_buf[1024]; @@ -231,24 +324,42 @@ xqc_conn_init_trans_settings(xqc_connection_t *conn) /* set local default setting values */ ls->max_streams_bidi = 128; ls->max_stream_data_bidi_remote = 16 * 1024 * 1024; - ls->max_stream_data_bidi_local = 16 * 1024 * 1024; + + if (conn->conn_settings.enable_stream_rate_limit) { + ls->max_stream_data_bidi_local = conn->conn_settings.init_recv_window; + + } else { + ls->max_stream_data_bidi_local = 16 * 1024 * 1024; + } ls->max_streams_uni = 128; ls->max_stream_data_uni = 16 * 1024 * 1024; - /* max_data is the sum of stream_data on all uni and bidi streams */ - ls->max_data = ls->max_streams_bidi * ls->max_stream_data_bidi_local - + ls->max_streams_uni * ls->max_stream_data_uni; + if (conn->conn_settings.recv_rate_bytes_per_sec) { + ls->max_data = conn->conn_settings.recv_rate_bytes_per_sec * XQC_FC_INIT_RTT / 1000000; + ls->max_data = xqc_max(XQC_MIN_RECV_WINDOW, ls->max_data); + ls->max_data = xqc_min(XQC_MAX_RECV_WINDOW, ls->max_data); + + } else { + /* max_data is the sum of stream_data on all uni and bidi streams */ + ls->max_data = ls->max_streams_bidi * ls->max_stream_data_bidi_local + + ls->max_streams_uni * ls->max_stream_data_uni; + } - ls->max_idle_timeout = default_conn_settings.idle_time_out; + ls->max_idle_timeout = conn->conn_settings.idle_time_out; ls->max_udp_payload_size = XQC_CONN_MAX_UDP_PAYLOAD_SIZE; ls->active_connection_id_limit = XQC_CONN_ACTIVE_CID_LIMIT; ls->enable_multipath = conn->conn_settings.enable_multipath; + + ls->multipath_version = conn->conn_settings.multipath_version; + ls->max_datagram_frame_size = conn->conn_settings.max_datagram_frame_size; ls->disable_active_migration = ls->enable_multipath ? 0 : 1; + + ls->max_ack_delay = conn->conn_settings.max_ack_delay; } @@ -302,6 +413,15 @@ xqc_conn_init_timer_manager(xqc_connection_t *conn) { xqc_timer_set(timer_manager, XQC_TIMER_PING, now, XQC_PING_TIMEOUT * 1000); } + + if (conn->conn_settings.enable_pmtud) { + if (conn->conn_settings.enable_multipath) { + xqc_timer_set(timer_manager, XQC_TIMER_PMTUD_PROBING, now, XQC_PMTUD_START_DELAY * 1000); + + } else { + xqc_timer_set(timer_manager, XQC_TIMER_PMTUD_PROBING, now, 1); + } + } } xqc_connection_t * @@ -321,10 +441,69 @@ xqc_conn_create(xqc_engine_t *engine, xqc_cid_t *dcid, xqc_cid_t *scid, xc->conn_settings = *settings; + if (xc->conn_settings.max_ack_delay == 0) { + xc->conn_settings.max_ack_delay = XQC_DEFAULT_MAX_ACK_DELAY; + } + xc->conn_settings.max_ack_delay = xqc_min(xc->conn_settings.max_ack_delay, XQC_DEFAULT_MAX_ACK_DELAY); + + if (xc->conn_settings.datagram_redundant_probe) { + xc->conn_settings.datagram_redundant_probe = xqc_max(xc->conn_settings.datagram_redundant_probe, + XQC_MIN_DATAGRAM_REDUNDANT_PROBE_INTERVAL); + } + + if (xc->conn_settings.datagram_redundancy > XQC_MAX_DATAGRAM_REDUNDANCY) { + xc->conn_settings.datagram_redundancy = XQC_MAX_DATAGRAM_REDUNDANCY; + } + + if (xc->conn_settings.datagram_redundancy) { + if (xc->conn_settings.datagram_redundancy == 1) { + /* reinject packets on any path */ + xc->conn_settings.scheduler_callback = xqc_rap_scheduler_cb; + + } else { + /* do not reinject packets on the same path */ + xc->conn_settings.scheduler_callback = xqc_minrtt_scheduler_cb; + } + + xc->conn_settings.reinj_ctl_callback = xqc_dgram_reinj_ctl_cb; + xc->conn_settings.mp_enable_reinjection |= XQC_REINJ_UNACK_AFTER_SEND; + } + + if (xc->conn_settings.init_recv_window) { + xc->conn_settings.init_recv_window = xqc_max(xc->conn_settings.init_recv_window, XQC_QUIC_MAX_MSS); + + } else { + xc->conn_settings.init_recv_window = XQC_MIN_RECV_WINDOW; + } + + if (xc->conn_settings.standby_path_probe_timeout) { + xc->conn_settings.standby_path_probe_timeout = xqc_max(xc->conn_settings.standby_path_probe_timeout, XQC_MIN_STANDBY_RPOBE_TIMEOUT); + } + if (xc->conn_settings.max_pkt_out_size < default_conn_settings.max_pkt_out_size) { xc->conn_settings.max_pkt_out_size = default_conn_settings.max_pkt_out_size; } + if (xc->conn_settings.max_pkt_out_size > XQC_MAX_PACKET_OUT_SIZE) { + xc->conn_settings.max_pkt_out_size = XQC_MAX_PACKET_OUT_SIZE; + } + + if (xc->conn_settings.pmtud_probing_interval == 0) { + xc->conn_settings.pmtud_probing_interval = default_conn_settings.pmtud_probing_interval; + } + + if (xc->conn_settings.ack_frequency == 0) { + xc->conn_settings.ack_frequency = default_conn_settings.ack_frequency; + } + + if (xc->conn_settings.pto_backoff_factor == 0) { + xc->conn_settings.pto_backoff_factor = default_conn_settings.pto_backoff_factor; + } + + if (xc->conn_settings.loss_detection_pkt_thresh == 0) { + xc->conn_settings.loss_detection_pkt_thresh = default_conn_settings.loss_detection_pkt_thresh; + } + xc->version = (type == XQC_CONN_TYPE_CLIENT) ? settings->proto_version : XQC_IDRAFT_INIT_VER; if (type == XQC_CONN_TYPE_CLIENT @@ -339,12 +518,12 @@ xqc_conn_create(xqc_engine_t *engine, xqc_cid_t *dcid, xqc_cid_t *scid, xc->conn_settings.init_idle_time_out = XQC_CONN_INITIAL_IDLE_TIMEOUT; } - if (xc->conn_settings.anti_amplification_limit < XQC_DEFAULT_ANTI_AMPLIFICATION_LIMIT) { - xc->conn_settings.anti_amplification_limit = XQC_DEFAULT_ANTI_AMPLIFICATION_LIMIT; + if (xc->conn_settings.idle_time_out == 0) { + xc->conn_settings.idle_time_out = XQC_CONN_DEFAULT_IDLE_TIMEOUT; } - if (xc->conn_settings.path_unreachable_pto_count == 0) { - xc->conn_settings.path_unreachable_pto_count = default_conn_settings.path_unreachable_pto_count; + if (xc->conn_settings.anti_amplification_limit < XQC_DEFAULT_ANTI_AMPLIFICATION_LIMIT) { + xc->conn_settings.anti_amplification_limit = XQC_DEFAULT_ANTI_AMPLIFICATION_LIMIT; } if (xc->conn_settings.reinj_flexible_deadline_srtt_factor == 0) { @@ -355,6 +534,10 @@ xqc_conn_create(xqc_engine_t *engine, xqc_cid_t *dcid, xqc_cid_t *scid, xc->conn_settings.reinj_hard_deadline = default_conn_settings.reinj_hard_deadline; } + if (xqc_conn_is_current_mp_version_supported(xc->conn_settings.multipath_version) != XQC_OK) { + xc->conn_settings.multipath_version = XQC_MULTIPATH_04; + } + xqc_conn_init_trans_settings(xc); xqc_conn_init_flow_ctl(xc); xqc_conn_init_key_update_ctx(xc); @@ -401,18 +584,22 @@ xqc_conn_create(xqc_engine_t *engine, xqc_cid_t *dcid, xqc_cid_t *scid, xc->first_data_send_time = 0; xc->max_stream_id_bidi_remote = -1; xc->max_stream_id_uni_remote = -1; - xc->history_path = xqc_calloc(1, sizeof(xqc_conn_path_history_t)); + xc->last_dgram = NULL; + xc->pkt_out_size = xc->conn_settings.max_pkt_out_size; + xc->max_pkt_out_size = XQC_MAX_PACKET_OUT_SIZE; + xc->probing_pkt_out_size = XQC_MAX_PACKET_OUT_SIZE; + xc->probing_cnt = 0; for (xqc_encrypt_level_t encrypt_level = XQC_ENC_LEV_INIT; encrypt_level < XQC_ENC_LEV_MAX; encrypt_level++) { xc->undecrypt_count[encrypt_level] = 0; } - xc->enc_pkt = xqc_malloc(xc->conn_settings.max_pkt_out_size + XQC_PACKET_OUT_EXT_SPACE); + xc->enc_pkt = xqc_malloc(XQC_PACKET_OUT_BUF_CAP); if (NULL == xc->enc_pkt) { xqc_log(xc->log, XQC_LOG_ERROR, "|malloc enc pkt buf fail"); goto fail; } - xc->enc_pkt_cap = xc->conn_settings.max_pkt_out_size + XQC_PACKET_OUT_EXT_SPACE; + xc->enc_pkt_cap = XQC_PACKET_OUT_BUF_CAP; xc->conn_send_queue = xqc_send_queue_create(xc); if (xc->conn_send_queue == NULL) { @@ -421,6 +608,19 @@ xqc_conn_create(xqc_engine_t *engine, xqc_cid_t *dcid, xqc_cid_t *scid, xqc_conn_init_timer_manager(xc); + if (xc->conn_settings.datagram_redundant_probe) { + xc->last_dgram = xqc_var_buf_create_with_limit(XQC_MAX_PACKET_OUT_SIZE, XQC_MAX_PACKET_OUT_SIZE); + if (xc->last_dgram == NULL) { + goto fail; + } + + xc->dgram_probe_timer = xqc_conn_register_gp_timer(xc, "dgram_probe", xqc_conn_dgram_probe_timeout, xc); + if (xc->dgram_probe_timer < 0) { + xqc_log(xc->log, XQC_LOG_ERROR, "|register dgram probe timer error|"); + goto fail; + } + } + xqc_init_list_head(&xc->conn_write_streams); xqc_init_list_head(&xc->conn_read_streams); xqc_init_list_head(&xc->conn_closing_streams); @@ -457,7 +657,10 @@ xqc_conn_create(xqc_engine_t *engine, xqc_cid_t *dcid, xqc_cid_t *scid, } /* insert into engine's conns_hash */ - if (xqc_insert_conns_hash(engine->conns_hash, xc, &xc->scid_set.user_scid)) { + if (xqc_insert_conns_hash(engine->conns_hash, xc, + xc->scid_set.user_scid.cid_buf, + xc->scid_set.user_scid.cid_len)) + { goto fail; } @@ -465,28 +668,34 @@ xqc_conn_create(xqc_engine_t *engine, xqc_cid_t *dcid, xqc_cid_t *scid, goto fail; } - if (xc->conn_settings.enable_multipath != 0) { - /* set scheduler callback (default: minRTT) */ - if (xc->conn_settings.scheduler_callback.xqc_scheduler_init) { - xc->scheduler_callback = &xc->conn_settings.scheduler_callback; - - } else { - xc->scheduler_callback = &xqc_minrtt_scheduler_cb; - } + /* set scheduler callback (default: minRTT) */ + if (xc->conn_settings.scheduler_callback.xqc_scheduler_init) { + xc->scheduler_callback = &xc->conn_settings.scheduler_callback; - xc->scheduler = xqc_pcalloc(xc->conn_pool, xc->scheduler_callback->xqc_scheduler_size()); - xc->scheduler_callback->xqc_scheduler_init(xc->scheduler, xc->log, &xc->conn_settings.scheduler_params); + } else { + xc->scheduler_callback = &xqc_minrtt_scheduler_cb; } + xc->scheduler = xqc_pcalloc(xc->conn_pool, xc->scheduler_callback->xqc_scheduler_size()); + xc->scheduler_callback->xqc_scheduler_init(xc->scheduler, xc->log, &xc->conn_settings.scheduler_params); + /* set reinject control callback if reinjection enabled */ if (xc->conn_settings.reinj_ctl_callback.xqc_reinj_ctl_init) { xc->reinj_callback = &xc->conn_settings.reinj_ctl_callback; xc->reinj_ctl = xqc_pcalloc(xc->conn_pool, xc->reinj_callback->xqc_reinj_ctl_size()); - xc->reinj_callback->xqc_reinj_ctl_init(xc->reinj_ctl, &xc->conn_settings, xc->log); + if (xc->reinj_ctl == NULL) { + goto fail; + } + xc->reinj_callback->xqc_reinj_ctl_init(xc->reinj_ctl, xc); } xc->pkt_filter_cb = NULL; + /* for datagram */ + xc->next_dgram_id = 0; + xqc_init_list_head(&xc->dgram_0rtt_buffer_list); + xqc_init_list_head(&xc->ping_notification_list); + xqc_log(xc->log, XQC_LOG_DEBUG, "|success|scid:%s|dcid:%s|conn:%p|", xqc_scid_str(&xc->scid_set.user_scid), xqc_dcid_str(&xc->dcid_set.current_dcid), xc); xqc_log_event(xc->log, TRA_PARAMETERS_SET, xc, XQC_LOG_LOCAL_EVENT); @@ -582,6 +791,12 @@ xqc_conn_server_create(xqc_engine_t *engine, const struct sockaddr *local_addr, } } + /* generate sr token for server's initial cid */ + xqc_gen_reset_token(&new_scid, new_scid.sr_token, + XQC_STATELESS_RESET_TOKENLEN, + engine->config->reset_token_key, + engine->config->reset_token_keylen); + conn = xqc_conn_create(engine, dcid, &new_scid, settings, user_data, XQC_CONN_TYPE_SERVER); if (conn == NULL) { xqc_log(engine->log, XQC_LOG_ERROR, "|fail to create connection|"); @@ -596,7 +811,10 @@ xqc_conn_server_create(xqc_engine_t *engine, const struct sockaddr *local_addr, * and if client Initial retransmit, server might use odcid to * find the created conn */ - if (xqc_insert_conns_hash(engine->conns_hash, conn, &conn->original_dcid)) { + if (xqc_insert_conns_hash(engine->conns_hash, conn, + conn->original_dcid.cid_buf, + conn->original_dcid.cid_len)) + { goto fail; } @@ -604,10 +822,29 @@ xqc_conn_server_create(xqc_engine_t *engine, const struct sockaddr *local_addr, xqc_dcid_str(&conn->original_dcid), conn); } - xqc_memcpy(conn->local_addr, local_addr, local_addrlen); - xqc_memcpy(conn->peer_addr, peer_addr, peer_addrlen); - conn->local_addrlen = local_addrlen; - conn->peer_addrlen = peer_addrlen; + ret = xqc_memcpy_with_cap(conn->local_addr, sizeof(conn->local_addr), + local_addr, local_addrlen); + if (ret == XQC_OK) { + conn->local_addrlen = local_addrlen; + + } else { + xqc_log(conn->log, XQC_LOG_ERROR, + "|local addr too large|addr_len:%d|", + (int)local_addrlen); + goto fail; + } + + ret = xqc_memcpy_with_cap(conn->peer_addr, sizeof(conn->peer_addr), + peer_addr, peer_addrlen); + if (ret == XQC_OK) { + conn->peer_addrlen = peer_addrlen; + + } else { + xqc_log(conn->log, XQC_LOG_ERROR, + "|peer addr too large|addr_len:%d|", + (int)peer_addrlen); + goto fail; + } ret = xqc_conn_create_server_tls(conn); if (ret != XQC_OK) { @@ -669,18 +906,47 @@ xqc_conn_server_on_alpn(xqc_connection_t *conn, const unsigned char *alpn, size_ return ret; } + uint8_t tp_buf[XQC_MAX_TRANSPORT_PARAM_BUF_LEN] = {0}; + size_t tp_len = 0; + /* do callback */ if (conn->app_proto_cbs.conn_cbs.conn_create_notify) { if (conn->app_proto_cbs.conn_cbs.conn_create_notify(conn, &conn->scid_set.user_scid, conn->user_data, conn->proto_data)) { - XQC_CONN_ERR(conn, TRA_INTERNAL_ERROR); - return -TRA_INTERNAL_ERROR; + goto err; } conn->conn_flag |= XQC_CONN_FLAG_UPPER_CONN_EXIST; } + xqc_datagram_record_mss(conn); + + if (conn->conn_flag & XQC_CONN_FLAG_LOCAL_TP_UPDATED) { + ret = xqc_conn_encode_local_tp(conn, tp_buf, + XQC_MAX_TRANSPORT_PARAM_BUF_LEN, &tp_len); + if (ret != XQC_OK) { + xqc_log(conn->log, XQC_LOG_ERROR, "|server encode tp error|ret:%d|", ret); + goto err; + } + + ret = xqc_tls_update_tp(conn->tls, tp_buf, tp_len); + + if (ret != XQC_OK) { + xqc_log(conn->log, XQC_LOG_ERROR, "|server tls update tp error|ret:%d|", ret); + goto err; + } + + conn->conn_flag &= ~XQC_CONN_FLAG_LOCAL_TP_UPDATED; + xqc_log(conn->log, XQC_LOG_INFO, + "|update tp|max_datagram_frame_size:%ud|", + conn->local_settings.max_datagram_frame_size); + } + return XQC_OK; + +err: + XQC_CONN_ERR(conn, TRA_INTERNAL_ERROR); + return -TRA_INTERNAL_ERROR; } @@ -701,8 +967,8 @@ xqc_conn_destroy(xqc_connection_t *xc) return; } - char path_info_buff[100 * XQC_MAX_PATHS_COUNT] = {'\0'}; - xqc_path_metrics_print(xc, path_info_buff, 50 * XQC_MAX_PATHS_COUNT); + xqc_conn_stats_t conn_stats; + xqc_conn_get_stats_internal(xc, &conn_stats); if (xc->tls) { xqc_tls_get_selected_alpn(xc->tls, &out_alpn, &out_alpn_len); @@ -732,7 +998,7 @@ xqc_conn_destroy(xqc_connection_t *xc) xqc_calc_delay(xc->conn_last_recv_time, xc->conn_create_time), xqc_calc_delay(xc->conn_last_send_time, xc->conn_create_time), xc->enable_multipath, xc->create_path_count, xc->validated_path_count, xc->active_path_count, - path_info_buff, out_alpn_len, out_alpn); + conn_stats.conn_info, out_alpn_len, out_alpn); xqc_log_event(xc->log, CON_CONNECTION_CLOSED, xc); if (xc->conn_flag & XQC_CONN_FLAG_WAIT_WAKEUP) { @@ -751,6 +1017,18 @@ xqc_conn_destroy(xqc_connection_t *xc) xqc_destroy_stream(stream); } + xqc_conn_destroy_0rtt_datagram_buffer_list(xc); + + if (xc->conn_settings.datagram_redundant_probe + && xc->dgram_probe_timer >= 0) { + xqc_conn_unregister_gp_timer(xc, xc->dgram_probe_timer); + } + + if (xc->last_dgram) { + xqc_var_buf_free(xc->last_dgram); + xc->last_dgram = NULL; + } + /* notify destruction */ if (xc->conn_flag & XQC_CONN_FLAG_UPPER_CONN_EXIST) { /* ALPN negotiated, notify close through application layer protocol callback function */ @@ -773,6 +1051,9 @@ xqc_conn_destroy(xqc_connection_t *xc) xc->conn_flag &= ~XQC_CONN_FLAG_UPPER_CONN_EXIST; } + /* destroy gp_timer list */ + xqc_timer_destroy_gp_timer_list(&xc->conn_timer_manager); + xqc_send_queue_destroy(xc->conn_send_queue); /* free streams hash */ @@ -788,9 +1069,6 @@ xqc_conn_destroy(xqc_connection_t *xc) xqc_conn_destroy_paths_list(xc); - /* remove path history */ - xqc_free((void *)xc->history_path); - for (xqc_encrypt_level_t encrypt_level = XQC_ENC_LEV_INIT; encrypt_level < XQC_ENC_LEV_MAX; encrypt_level++) { xqc_list_for_each_safe(pos, next, &xc->undecrypt_packet_in[encrypt_level]) { packet_in = xqc_list_entry(pos, xqc_packet_in_t, pi_list); @@ -807,6 +1085,8 @@ xqc_conn_destroy(xqc_connection_t *xc) } } + xqc_conn_destroy_ping_notification_list(xc); + /* remove from engine's conns_hash and destroy cid_set*/ xqc_conn_destroy_cids(xc); @@ -864,6 +1144,51 @@ xqc_conn_get_local_addr(xqc_connection_t *conn, struct sockaddr *addr, socklen_t return XQC_OK; } +xqc_int_t +xqc_conn_send_ping_internal(xqc_connection_t *conn, void *ping_user_data, xqc_bool_t notify) +{ + xqc_int_t ret = XQC_OK; + + if (conn->conn_state >= XQC_CONN_STATE_CLOSING) { + return ret; + } + + if (conn->enable_multipath && conn->conn_settings.mp_ping_on) { + + xqc_path_ctx_t *path; + xqc_list_head_t *pos, *next; + xqc_bool_t has_ping = XQC_FALSE; + xqc_ping_record_t *pr = xqc_conn_create_ping_record(conn); + + xqc_list_for_each_safe(pos, next, &conn->conn_paths_list) { + path = xqc_list_entry(pos, xqc_path_ctx_t, path_list); + if (path->path_state == XQC_PATH_STATE_ACTIVE) { + ret = xqc_write_ping_to_packet(conn, path, ping_user_data, notify, pr); + if (ret < 0) { + xqc_log(conn->log, XQC_LOG_ERROR, "|write ping error|path:%ui|", path->path_id); + + } else { + has_ping = XQC_TRUE; + } + } + } + + if (!has_ping) { + xqc_conn_destroy_ping_record(pr); + return ret; + } + + } else { + ret = xqc_write_ping_to_packet(conn, NULL, ping_user_data, notify, NULL); + if (ret < 0) { + xqc_log(conn->log, XQC_LOG_ERROR, "|write ping error|"); + return ret; + } + } + + return XQC_OK; +} + /* used by upper level, shall never be invoked in xquic */ xqc_int_t xqc_conn_send_ping(xqc_engine_t *engine, const xqc_cid_t *cid, void *ping_user_data) @@ -877,13 +1202,8 @@ xqc_conn_send_ping(xqc_engine_t *engine, const xqc_cid_t *cid, void *ping_user_d return -XQC_ECONN_NFOUND; } - if (conn->conn_state >= XQC_CONN_STATE_CLOSING) { - return XQC_OK; - } - - ret = xqc_write_ping_to_packet(conn, ping_user_data, XQC_TRUE); - if (ret < 0) { - xqc_log(engine->log, XQC_LOG_ERROR, "|write ping error|"); + ret = xqc_conn_send_ping_internal(conn, ping_user_data, XQC_TRUE); + if (ret) { return ret; } @@ -920,6 +1240,43 @@ xqc_conn_should_close(xqc_connection_t *conn, xqc_path_ctx_t *path) return XQC_FALSE; } +void +xqc_conn_try_to_update_mss(xqc_connection_t *conn) +{ + xqc_path_ctx_t *path; + xqc_list_head_t *pos, *next; + size_t min_pkt_out_size = 0; + size_t max_pkt_out_size = 0; + xqc_usec_t probing_interval = conn->conn_settings.pmtud_probing_interval; + + /* try to update conn MTU */ + xqc_list_for_each_safe(pos, next, &conn->conn_paths_list) { + path = xqc_list_entry(pos, xqc_path_ctx_t, path_list); + + if (path->path_state >= XQC_PATH_STATE_CLOSING) { + continue; + } + + if (min_pkt_out_size == 0 || path->curr_pkt_out_size < min_pkt_out_size) { + min_pkt_out_size = path->curr_pkt_out_size; + max_pkt_out_size = path->path_max_pkt_out_size; + } + } + + if (min_pkt_out_size > conn->pkt_out_size) { + conn->pkt_out_size = min_pkt_out_size; + /* try to update PMTUD probing info */ + conn->max_pkt_out_size = max_pkt_out_size; + conn->probing_pkt_out_size = max_pkt_out_size; + conn->probing_cnt = 0; + /* launch new probing immediately */ + conn->conn_flag |= XQC_CONN_FLAG_PMTUD_PROBING; + xqc_timer_unset(&conn->conn_timer_manager, XQC_TIMER_PMTUD_PROBING); + /* update datagram mss */ + xqc_datagram_record_mss(conn); + } +} + ssize_t xqc_send_burst(xqc_connection_t *conn, xqc_path_ctx_t *path, struct iovec *iov, int cnt) @@ -935,6 +1292,7 @@ xqc_send_burst(xqc_connection_t *conn, xqc_path_ctx_t *path, struct iovec *iov, if (sent < 0) { xqc_log(conn->log, XQC_LOG_ERROR, "|error send mmsg|"); if (sent == XQC_SOCKET_ERROR) { + path->path_flag |= XQC_PATH_FLAG_SOCKET_ERROR; if (xqc_conn_should_close(conn, path)) { xqc_log(conn->log, XQC_LOG_ERROR, "|socket exception, close connection|"); conn->conn_state = XQC_CONN_STATE_CLOSED; @@ -960,42 +1318,24 @@ xqc_send_burst(xqc_connection_t *conn, xqc_path_ctx_t *path, struct iovec *iov, } xqc_int_t -xqc_check_duplicate_acked_pkt(xqc_connection_t *conn, +xqc_check_acked_or_dropped_pkt(xqc_connection_t *conn, xqc_packet_out_t *packet_out, xqc_send_type_t send_type) { + if (xqc_send_ctl_indirectly_ack_or_drop_po(conn, packet_out)) { + return XQC_TRUE; + } + if (send_type == XQC_SEND_TYPE_RETRANS) { - if (xqc_send_ctl_indirectly_ack_po(conn, packet_out)) { - return XQC_TRUE; - } /* If not a TLP packet, mark it LOST */ packet_out->po_flag |= XQC_POF_LOST; - xqc_log(conn->log, XQC_LOG_DEBUG, - "|retransmit_lost_packets|conn:%p|pkt_num:%ui|size:%ud|pkt_type:%s|frame:%s|", - conn, packet_out->po_pkt.pkt_num, packet_out->po_used_size, - xqc_pkt_type_2_str(packet_out->po_pkt.pkt_type), - xqc_frame_type_2_str(packet_out->po_frame_types)); - - } else if (send_type == XQC_SEND_TYPE_PTO_PROBE) { - if (xqc_send_ctl_indirectly_ack_po(conn, packet_out)) { - return XQC_TRUE; - } - xqc_log(conn->log, XQC_LOG_DEBUG, - "|transmit_pto_probe_packets|conn:%p|pkt_num:%ui|size:%ud|pkt_type:%s|frame:%s|", - conn, packet_out->po_pkt.pkt_num, packet_out->po_used_size, - xqc_pkt_type_2_str(packet_out->po_pkt.pkt_type), - xqc_frame_type_2_str(packet_out->po_frame_types)); - - } else if (packet_out->po_flag & XQC_POF_REINJECTED_REPLICA) { - if (xqc_send_ctl_indirectly_ack_po(conn, packet_out)) { - return XQC_TRUE; - } - xqc_log(conn->log, XQC_LOG_DEBUG, - "|transmit_reinject_replica_packets|conn:%p|pkt_num:%ui|size:%ud|pkt_type:%s|frame:%s|", - conn, packet_out->po_pkt.pkt_num, packet_out->po_used_size, - xqc_pkt_type_2_str(packet_out->po_pkt.pkt_type), - xqc_frame_type_2_str(packet_out->po_frame_types)); } + xqc_log(conn->log, XQC_LOG_DEBUG, + "|conn:%p|pkt_num:%ui|size:%ud|pkt_type:%s|frame:%s|", + conn, packet_out->po_pkt.pkt_num, packet_out->po_used_size, + xqc_pkt_type_2_str(packet_out->po_pkt.pkt_type), + xqc_frame_type_2_str(packet_out->po_frame_types)); + return XQC_FALSE; } @@ -1009,6 +1349,7 @@ xqc_conn_schedule_packets(xqc_connection_t *conn, xqc_list_head_t *head, xqc_list_head_t *pos, *next; xqc_path_ctx_t *path; xqc_packet_out_t *packet_out; + xqc_bool_t cc_blocked; xqc_list_for_each_safe(pos, next, head) { packet_out = xqc_list_entry(pos, xqc_packet_out_t, po_list); @@ -1018,14 +1359,11 @@ xqc_conn_schedule_packets(xqc_connection_t *conn, xqc_list_head_t *head, path = conn->conn_initial_path; /* 2. 已设置特定路径发送的包,例如:PATH_CHALLENGE PATH_RESPONSE MP_ACK(原路径ACK) */ - } else if (packet_out->po_is_path_specified) { - path = xqc_conn_find_path_by_path_id(conn, packet_out->po_path_id); - if ((path == NULL) || (path->path_state == XQC_PATH_STATE_CLOSED)) { - xqc_send_queue_remove_send(&packet_out->po_list); - xqc_send_queue_insert_free(packet_out, &conn->conn_send_queue->sndq_free_packets, conn->conn_send_queue); + } else if (xqc_packet_out_on_specific_path(conn, packet_out, &path)) { + + if (path == NULL) { continue; } - xqc_log(conn->log, XQC_LOG_DEBUG, "|specify|path:%ui|state:%d|frame_type:%s|stream_id:%ui|stream_offset:%ui|", path->path_id, path->path_state, xqc_frame_type_2_str(packet_out->po_frame_types), packet_out->po_stream_id, packet_out->po_stream_offset); @@ -1035,8 +1373,12 @@ xqc_conn_schedule_packets(xqc_connection_t *conn, xqc_list_head_t *head, path = conn->scheduler_callback-> xqc_scheduler_get_path(conn->scheduler, conn, packet_out, - packets_are_limited_by_cc, 0); + packets_are_limited_by_cc, + 0, &cc_blocked); if (path == NULL) { + if (cc_blocked) { + conn->sched_cc_blocked++; + } break; } } @@ -1045,6 +1387,22 @@ xqc_conn_schedule_packets(xqc_connection_t *conn, xqc_list_head_t *head, } } +static inline void +xqc_conn_log_sent_packet(xqc_connection_t *c, xqc_packet_out_t *po, + xqc_usec_t timestamp) +{ + int index = c->snd_pkt_stats.curr_index; + c->snd_pkt_stats.pkt_frames[index] = po->po_frame_types; + c->snd_pkt_stats.pkt_size[index] = po->po_used_size; + c->snd_pkt_stats.pkt_timestamp[index] = xqc_calc_delay(timestamp, + c->conn_create_time); + c->snd_pkt_stats.pkt_timestamp[index] /= 1000; + c->snd_pkt_stats.pkt_types[index] = po->po_pkt.pkt_type; + c->snd_pkt_stats.pkt_pn[index] = po->po_pkt.pkt_num; + c->snd_pkt_stats.conn_sent_pkts++; + c->snd_pkt_stats.curr_index = (index + 1) % 3; +} + void xqc_on_packets_send_burst(xqc_connection_t *conn, xqc_path_ctx_t *path, ssize_t sent, xqc_usec_t now, xqc_send_type_t send_type) { @@ -1062,6 +1420,9 @@ xqc_on_packets_send_burst(xqc_connection_t *conn, xqc_path_ctx_t *path, ssize_t } packet_out = xqc_list_entry(pos, xqc_packet_out_t, po_list); + + xqc_conn_log_sent_packet(conn, packet_out, now); + if (xqc_has_packet_number(&packet_out->po_pkt)) { /* count packets with pkt_num in the send control */ if (XQC_CAN_IN_FLIGHT(packet_out->po_frame_types @@ -1106,7 +1467,8 @@ xqc_convert_pkt_0rtt_2_1rtt(xqc_connection_t *conn, xqc_packet_out_t *packet_out /* long header to short header, directly write old buffer */ unsigned int ori_po_used_size = packet_out->po_used_size; unsigned char *ori_payload = packet_out->po_payload; - unsigned int ori_payload_len = ori_po_used_size - (packet_out->po_payload - packet_out->po_buf); + unsigned int ori_hdr_len = packet_out->po_payload - packet_out->po_buf; + unsigned int ori_payload_len = ori_po_used_size - ori_hdr_len; /* convert pkt info */ packet_out->po_pkt.pkt_pns = XQC_PNS_APP_DATA; @@ -1119,11 +1481,28 @@ xqc_convert_pkt_0rtt_2_1rtt(xqc_connection_t *conn, xqc_packet_out_t *packet_out conn->key_update_ctx.cur_out_key_phase); packet_out->po_used_size = ret; + if (ori_hdr_len < ret) { + xqc_log(conn->log, XQC_LOG_ERROR, "|fatal|long_header_is_shorter_than_short_header|"); + XQC_CONN_ERR(conn, TRA_INTERNAL_ERROR); + return; + } + + unsigned int hdr_offset_diff = (ori_hdr_len - ret); + /* copy frame directly */ memmove(packet_out->po_buf + ret, ori_payload, ori_payload_len); packet_out->po_payload = packet_out->po_buf + ret; packet_out->po_used_size += ori_payload_len; + if (packet_out->po_ack_offset > 0) { + if (packet_out->po_ack_offset < hdr_offset_diff) { + xqc_log(conn->log, XQC_LOG_ERROR, "|fatal|wrong_ack_frame_offset|"); + XQC_CONN_ERR(conn, TRA_INTERNAL_ERROR); + return; + } + packet_out->po_ack_offset -= hdr_offset_diff; + } + xqc_log(conn->log, XQC_LOG_DEBUG, "|0RTT to 1RTT|conn:%p|type:%d|pkt_num:%ui|pns:%d|frame:%s|", conn, packet_out->po_pkt.pkt_type, packet_out->po_pkt.pkt_num, packet_out->po_pkt.pkt_pns, xqc_frame_type_2_str(packet_out->po_frame_types)); @@ -1152,7 +1531,7 @@ xqc_path_send_burst_packets(xqc_connection_t *conn, xqc_path_ctx_t *path, iov_array[burst_cnt].iov_len = XQC_CONN_MAX_UDP_PAYLOAD_SIZE; if (xqc_has_packet_number(&packet_out->po_pkt)) { - if (xqc_check_duplicate_acked_pkt(conn, packet_out, send_type)) { + if (xqc_check_acked_or_dropped_pkt(conn, packet_out, send_type)) { continue; } @@ -1168,6 +1547,7 @@ xqc_path_send_burst_packets(xqc_connection_t *conn, xqc_path_ctx_t *path, if (congest && !xqc_send_packet_check_cc(send_ctl, packet_out, total_bytes_to_send)) { + send_ctl->ctl_conn->send_cc_blocked++; break; } @@ -1278,7 +1658,7 @@ xqc_path_send_packets(xqc_connection_t *conn, xqc_path_ctx_t *path, xqc_list_for_each_safe(pos, next, &path->path_schedule_buf[send_type]) { packet_out = xqc_list_entry(pos, xqc_packet_out_t, po_list); - if (xqc_check_duplicate_acked_pkt(conn, packet_out, send_type)) { + if (xqc_check_acked_or_dropped_pkt(conn, packet_out, send_type)) { xqc_log(conn->log, XQC_LOG_DEBUG, "|path:%ui|canceled_bytes:%ud|reinj:%d|", path->path_id, packet_out->po_used_size, XQC_MP_PKT_REINJECTED(packet_out)); continue; } @@ -1295,6 +1675,7 @@ xqc_path_send_packets(xqc_connection_t *conn, xqc_path_ctx_t *path, if (congest && !xqc_send_packet_check_cc(send_ctl, packet_out, 0)) { + send_ctl->ctl_conn->send_cc_blocked++; break; } @@ -1309,6 +1690,11 @@ xqc_path_send_packets(xqc_connection_t *conn, xqc_path_ctx_t *path, xqc_pacing_on_packet_sent(&send_ctl->ctl_pacing, packet_out->po_used_size); } + if (packet_out->po_frame_types & XQC_FRAME_BIT_DATAGRAM) { + xqc_log(conn->log, XQC_LOG_DEBUG, "|dgram_id:%ui|", packet_out->po_dgram_id); + } + + /* move send list to unacked list */ xqc_path_send_buffer_remove(path, packet_out); if (XQC_IS_ACK_ELICITING(packet_out->po_frame_types)) { @@ -1379,7 +1765,8 @@ xqc_need_padding(xqc_connection_t *conn, xqc_packet_out_t *packet_out) } } else if ((packet_out->po_frame_types & XQC_FRAME_BIT_PATH_CHALLENGE) - || (packet_out->po_frame_types & XQC_FRAME_BIT_PATH_RESPONSE)) + || (packet_out->po_frame_types & XQC_FRAME_BIT_PATH_RESPONSE) + || (packet_out->po_flag & XQC_POF_PMTUD_PROBING)) { return XQC_TRUE; } @@ -1398,7 +1785,7 @@ xqc_conn_enc_packet(xqc_connection_t *conn, /* pad packet if needed */ if (xqc_need_padding(conn, packet_out)) { - xqc_gen_padding_frame(packet_out); + xqc_gen_padding_frame(conn, packet_out); } /* generate packet number and update packet length, might do packet number encoding here */ @@ -1407,7 +1794,9 @@ xqc_conn_enc_packet(xqc_connection_t *conn, xqc_write_packet_number(packet_out->po_ppktno, packet_out->po_pkt.pkt_num, XQC_PKTNO_BITS); xqc_long_packet_update_length(packet_out); xqc_short_packet_update_key_phase(packet_out, conn->key_update_ctx.cur_out_key_phase); - xqc_short_packet_update_custom_spin_bit(packet_out); + if (conn->conn_settings.marking_reinjection) { + xqc_packet_update_reserved_bits(packet_out); + } /* encrypt */ xqc_int_t ret = xqc_packet_encrypt_buf(conn, packet_out, enc_pkt, enc_pkt_cap, enc_pkt_len); @@ -1430,7 +1819,7 @@ xqc_send(xqc_connection_t *conn, xqc_path_ctx_t *path, unsigned char *data, unsi if (conn->pkt_filter_cb) { sent = conn->pkt_filter_cb(data, len, (struct sockaddr *)conn->peer_addr, - conn->peer_addrlen, conn->pkt_filter_cb_user_data); + conn->peer_addrlen, conn->pkt_filter_cb_user_data); if (sent < 0) { xqc_log(conn->log, XQC_LOG_ERROR, "|pkt_filter_cb error|conn:%p|" "size:%ud|sent:%z|", conn, len, sent); @@ -1446,9 +1835,13 @@ xqc_send(xqc_connection_t *conn, xqc_path_ctx_t *path, unsigned char *data, unsi if (sent != len) { xqc_log(conn->log, XQC_LOG_ERROR, "|write_socket error|conn:%p|size:%ud|sent:%z|", conn, len, sent); + xqc_log(conn->log, XQC_LOG_ERROR, + "|write_socket error|path:%ui|path_addr:%s|peer_addrlen:%d|", + path->path_id, xqc_path_addr_str(path), (int)path->peer_addrlen); /* if callback return XQC_SOCKET_ERROR, close the connection */ if (sent == XQC_SOCKET_ERROR) { + path->path_flag |= XQC_PATH_FLAG_SOCKET_ERROR; if (xqc_conn_should_close(conn, path)) { xqc_log(conn->log, XQC_LOG_ERROR, "|conn:%p|socket exception, close connection|", conn); conn->conn_state = XQC_CONN_STATE_CLOSED; @@ -1490,6 +1883,7 @@ xqc_process_packet_without_pn(xqc_connection_t *conn, xqc_path_ctx_t *path, xqc_ conn, packet_out->po_used_size, sent, xqc_pkt_type_2_str(packet_out->po_pkt.pkt_type)); if (sent > 0) { + xqc_conn_log_sent_packet(conn, packet_out, xqc_monotonic_timestamp()); xqc_log_event(conn->log, TRA_PACKET_SENT, packet_out); } return sent; @@ -1526,6 +1920,8 @@ xqc_send_packet_with_pn(xqc_connection_t *conn, xqc_path_ctx_t *path, xqc_packet /* deliver packet to send control */ xqc_pn_ctl_t *pn_ctl = xqc_get_pn_ctl(conn, path); pn_ctl->ctl_packet_number[packet_out->po_pkt.pkt_pns]++; + + xqc_conn_log_sent_packet(conn, packet_out, now); xqc_send_ctl_on_packet_sent(path->path_send_ctl, pn_ctl, packet_out, now, sent); return sent; } @@ -1538,7 +1934,7 @@ xqc_enc_packet_with_pn(xqc_connection_t *conn, xqc_path_ctx_t *path, xqc_packet_ /* pad packet if needed */ if (xqc_need_padding(conn, packet_out)) { - xqc_gen_padding_frame(packet_out); + xqc_gen_padding_frame(conn, packet_out); } /* @@ -1557,7 +1953,9 @@ xqc_enc_packet_with_pn(xqc_connection_t *conn, xqc_path_ctx_t *path, xqc_packet_ xqc_write_packet_number(packet_out->po_ppktno, packet_out->po_pkt.pkt_num, XQC_PKTNO_BITS); xqc_long_packet_update_length(packet_out); xqc_short_packet_update_key_phase(packet_out, conn->key_update_ctx.cur_out_key_phase); - xqc_short_packet_update_custom_spin_bit(packet_out); + if (conn->conn_settings.marking_reinjection) { + xqc_packet_update_reserved_bits(packet_out); + } /* encrypt packet body */ if (xqc_packet_encrypt(conn, packet_out) < 0) { @@ -1575,7 +1973,11 @@ xqc_process_packet_with_pn(xqc_connection_t *conn, xqc_path_ctx_t *path, xqc_pac { ssize_t ret = xqc_enc_packet_with_pn(conn, path, packet_out); if (ret != XQC_OK) { - xqc_log(conn->log, XQC_LOG_ERROR, "|xqc_enc_packet_with_pn error|"); + xqc_log(conn->log, XQC_LOG_ERROR, + "|path:%ui|pkt_type:%d|pn:%ui|frames:%ui|size:%ud|", + path->path_id, packet_out->po_pkt.pkt_type, + packet_out->po_pkt.pkt_num, (uint64_t)packet_out->po_frame_types, + packet_out->po_used_size); return ret; } @@ -1611,10 +2013,6 @@ xqc_conn_check_path_utilization(xqc_connection_t *conn) continue; } - if ((path->tra_path_status != XQC_TRA_PATH_STATUS_IN_USE) && (conn->in_use_active_path_count > 0)) { - continue; - } - if (!xqc_path_is_full(path) && conn->scheduler_callback->xqc_scheduler_handle_path_event) { @@ -1667,13 +2065,8 @@ xqc_conn_schedule_packets_to_paths(xqc_connection_t *conn) XQC_SEND_TYPE_NORMAL_HIGH_PRI); /* try to reinject unacked packets if paths still have cwnd */ - if ((conn->conn_settings.mp_enable_reinjection & XQC_REINJ_UNACK_BY_DEADLINE) - && conn->reinj_callback - && conn->reinj_callback->xqc_reinj_ctl_unack_queue - && conn->reinj_callback->xqc_reinj_ctl_unack_queue(conn->reinj_ctl, - NULL, conn)) - { - xqc_conn_reinject_unack_packets_by_deadline(conn); + if (conn->conn_settings.mp_enable_reinjection & XQC_REINJ_UNACK_BEFORE_SCHED) { + xqc_conn_reinject_unack_packets(conn, XQC_REINJ_UNACK_BEFORE_SCHED); } head = &conn->conn_send_queue->sndq_send_packets; @@ -1683,13 +2076,8 @@ xqc_conn_schedule_packets_to_paths(xqc_connection_t *conn) xqc_conn_check_path_utilization(conn); xqc_conn_schedule_end(conn); - if ((conn->conn_settings.mp_enable_reinjection & XQC_REINJ_UNACK_BY_CAPACITY) - && xqc_list_empty(&conn->conn_send_queue->sndq_send_packets) - && conn->reinj_callback - && conn->reinj_callback->xqc_reinj_ctl_unack_queue - && conn->reinj_callback->xqc_reinj_ctl_unack_queue(conn->reinj_ctl, NULL, conn)) - { - xqc_conn_reinject_unack_packets_by_capacity(conn); + if (conn->conn_settings.mp_enable_reinjection & XQC_REINJ_UNACK_AFTER_SCHED) { + xqc_conn_reinject_unack_packets(conn, XQC_REINJ_UNACK_AFTER_SCHED); } } @@ -1802,7 +2190,8 @@ xqc_conn_gen_ping(xqc_connection_t *conn, xqc_pkt_num_space_t pns) } xqc_int_t -xqc_path_send_ping_to_probe(xqc_path_ctx_t *path, xqc_pkt_num_space_t pns) +xqc_path_send_ping_to_probe(xqc_path_ctx_t *path, xqc_pkt_num_space_t pns, + xqc_path_specified_flag_t flag) { xqc_connection_t *conn = path->parent_conn; @@ -1812,7 +2201,7 @@ xqc_path_send_ping_to_probe(xqc_path_ctx_t *path, xqc_pkt_num_space_t pns) return -XQC_EWRITE_PKT; } - packet_out->po_is_path_specified = XQC_TRUE; + packet_out->po_path_flag |= flag; packet_out->po_path_id = path->path_id; /* put PING into probe list, which is not limited by amplification or congestion-control */ @@ -1822,79 +2211,164 @@ xqc_path_send_ping_to_probe(xqc_path_ctx_t *path, xqc_pkt_num_space_t pns) return XQC_OK; } - void -xqc_path_send_one_or_two_ack_elicit_pkts(xqc_path_ctx_t *path, xqc_pkt_num_space_t pns) -{ - xqc_connection_t *c = path->parent_conn; - xqc_log(c->log, XQC_LOG_DEBUG, "|send two ack-eliciting pkts|path:%ui|pns:%d|", path->path_id, pns); +xqc_conn_send_probe_pkt(xqc_connection_t *c, xqc_path_ctx_t *path, + xqc_packet_out_t *packet_out) +{ + xqc_reinjection_mode_t mode; + + mode = c->conn_settings.mp_enable_reinjection & XQC_REINJ_UNACK_BEFORE_SCHED; + + xqc_log(c->log, XQC_LOG_DEBUG, "|conn:%p|path:%ui|pkt_num:%ui" + "|size:%ud|pkt_type:%s|frame:%s|conn_state:%s|", c, + packet_out->po_path_id, packet_out->po_pkt.pkt_num, + packet_out->po_used_size, + xqc_pkt_type_2_str(packet_out->po_pkt.pkt_type), + xqc_frame_type_2_str(packet_out->po_frame_types), + xqc_conn_state_2_str(c->conn_state)); + + /* reinjection */ + if (c->enable_multipath + && c->reinj_callback + && c->reinj_callback->xqc_reinj_ctl_can_reinject + && c->reinj_callback->xqc_reinj_ctl_can_reinject( + c->reinj_ctl, packet_out, mode)) + { + if (xqc_conn_try_reinject_packet(c, packet_out) == XQC_OK) { + xqc_log(c->log, XQC_LOG_DEBUG, "|MP|REINJ|reinject pto packets|" + "pkt_num:%ui|size:%ud|pkt_type:%s|frame:%s|", + packet_out->po_pkt.pkt_num, packet_out->po_used_size, + xqc_pkt_type_2_str(packet_out->po_pkt.pkt_type), + xqc_frame_type_2_str(packet_out->po_frame_types)); + } + } - xqc_packet_out_t *packet_out; - xqc_list_head_t *pos, *next; - xqc_int_t ret; - xqc_int_t probe_num = XQC_CONN_PTO_PKT_CNT_MAX; - xqc_bool_t find_hsd = XQC_FALSE; + xqc_send_ctl_decrease_inflight(c, packet_out); + xqc_send_queue_copy_to_probe(packet_out, c->conn_send_queue, path); + + packet_out->po_flag |= XQC_POF_TLP; - if ((c->conn_type == XQC_CONN_TYPE_SERVER) && !(c->conn_flag & XQC_CONN_FLAG_HANDSHAKE_DONE_ACKED)) { - find_hsd = XQC_TRUE; + if (packet_out->po_frame_types & XQC_FRAME_BIT_DATAGRAM) { + path->path_send_ctl->ctl_lost_dgram_cnt++; } +} - /* if only one packet is in pns unacked list, this loop will try to send this packet again */ - while (probe_num > 0) { - xqc_list_for_each_safe(pos, next, &c->conn_send_queue->sndq_unacked_packets[pns]) { - packet_out = xqc_list_entry(pos, xqc_packet_out_t, po_list); +void +xqc_path_send_one_or_two_ack_elicit_pkts(xqc_path_ctx_t *path, + xqc_pkt_num_space_t pns) +{ + xqc_int_t ret; + xqc_connection_t *c; + xqc_packet_out_t *packet_out; + xqc_packet_out_t *packet_out_last_sent; /* for dup pto pkt */ + xqc_packet_out_t *packet_out_later_send; /* for sending HSK_DONE first */ + xqc_list_head_t *pos, *next; + xqc_list_head_t *sndq; + xqc_int_t probe_num; + xqc_bool_t send_hsd; + xqc_bool_t send_hsd_next; + + c = path->parent_conn; + sndq = &c->conn_send_queue->sndq_unacked_packets[pns]; + + /* on PTO xquic will try to send 2 ack-eliciting pkts at most. and server + shall send HANDSHAKE_DONE on PTO as it has not been acknowledged. */ + probe_num = XQC_CONN_PTO_PKT_CNT_MAX; + send_hsd = XQC_FALSE; + send_hsd_next = XQC_FALSE; + + packet_out_last_sent = NULL; + packet_out_later_send = NULL; + + xqc_log(c->log, XQC_LOG_DEBUG, "|send two ack-eliciting pkts" + "|path:%ui|pns:%d|", path->path_id, pns); + + /* if server's HANDSHAKE_DONE frame has not been acked, try to send it */ + if ((c->conn_type == XQC_CONN_TYPE_SERVER) + && !(c->conn_flag & XQC_CONN_FLAG_HANDSHAKE_DONE_ACKED)) + { + send_hsd = XQC_TRUE; + } - if (xqc_send_ctl_indirectly_ack_po(c, packet_out)) { - continue; - } + xqc_list_for_each_safe(pos, next, sndq) { + packet_out = xqc_list_entry(pos, xqc_packet_out_t, po_list); - if (packet_out->po_is_path_specified && packet_out->po_path_id != path->path_id) { - continue; - } + if (xqc_send_ctl_indirectly_ack_or_drop_po(c, packet_out)) { + continue; + } + + if (!xqc_packet_out_can_pto_probe(packet_out, path->path_id)) { + continue; + } - if (XQC_IS_ACK_ELICITING(packet_out->po_frame_types) - && XQC_NEED_REPAIR(packet_out->po_frame_types)) + if (XQC_IS_ACK_ELICITING(packet_out->po_frame_types) + && (XQC_NEED_REPAIR(packet_out->po_frame_types) + || (packet_out->po_frame_types & XQC_FRAME_BIT_DATAGRAM + && c->conn_settings.datagram_force_retrans_on))) + { + /* if HSK_DONE is not confirmed, will skip all the pkts do not + contain HSK_DONE frame, until a pkt with HSK_DONE is found, make + HSK_DONE is always with the highest priority */ + if (send_hsd + && !(packet_out->po_frame_types & XQC_FRAME_BIT_HANDSHAKE_DONE)) { - if (find_hsd && !(packet_out->po_frame_types & XQC_FRAME_BIT_HANDSHAKE_DONE)) { - continue; + if (packet_out_later_send == NULL) { + /* remember the first ack-eliciting pkt if pkt with HSK_DONE + frame is not the first one */ + packet_out_later_send = packet_out; } - packet_out->po_flag |= XQC_POF_TLP; + continue; + } - xqc_log(c->log, XQC_LOG_DEBUG, "|conn:%p|path:%ui|pkt_num:%ui|size:%ud|pkt_type:%s|frame:%s|conn_state:%s|", - c, packet_out->po_path_id, packet_out->po_pkt.pkt_num, packet_out->po_used_size, - xqc_pkt_type_2_str(packet_out->po_pkt.pkt_type), - xqc_frame_type_2_str(packet_out->po_frame_types), - xqc_conn_state_2_str(c->conn_state)); + xqc_conn_send_probe_pkt(c, path, packet_out); + packet_out_last_sent = packet_out; - xqc_send_ctl_decrease_inflight(c, packet_out); - xqc_send_queue_copy_to_probe(packet_out, c->conn_send_queue, path); + if (--probe_num == 0) { + break; + } - if (--probe_num == 0) { - break; - } + /* if a pkt with HSK_DONE is after any other ack-eliciting pkts is + sent, try to send the first ack-eliciting pkt */ + if (send_hsd && + (packet_out->po_frame_types & XQC_FRAME_BIT_HANDSHAKE_DONE)) + { + send_hsd = XQC_FALSE; - if (find_hsd) { - find_hsd = XQC_FALSE; - break; + /* try to send the first ack-eliciting pkt do not contain + HSK_DONE frame */ + if (packet_out_later_send) { + xqc_conn_send_probe_pkt(c, path, packet_out_later_send); + packet_out_last_sent = packet_out_later_send; + packet_out_later_send = NULL; + + if (--probe_num == 0) { + break; + } } } } + } - /* no data found in PTO pns, break and send PING */ - if (XQC_CONN_PTO_PKT_CNT_MAX == probe_num) { - if (find_hsd) { - find_hsd = XQC_FALSE; - } else { - break; + if (probe_num > 0) { + if (packet_out_last_sent) { + /* at least one pkt was sent, and there is still budget for send + more ack-eliciting pkts, try to send the pkt again */ + while (probe_num > 0) { + xqc_log(c->log, XQC_LOG_DEBUG, "|dup pkt on PTO, pkt_num:%ui|", + packet_out_last_sent->po_pkt.pkt_num); + xqc_conn_send_probe_pkt(c, path, packet_out_last_sent); + probe_num--; } - } - } - while (probe_num > 0) { - xqc_log(c->log, XQC_LOG_DEBUG, "|PING on PTO, cnt: %d|", probe_num); - xqc_path_send_ping_to_probe(path, pns); - probe_num--; + } else { + /* if no packet was sent, try to send PING frame */ + while (probe_num > 0) { + xqc_log(c->log, XQC_LOG_DEBUG, "|PING on PTO, cnt: %d|", probe_num); + xqc_path_send_ping_to_probe(path, pns, XQC_PATH_SPECIFIED_BY_PTO); + probe_num--; + } + } } } @@ -1983,9 +2457,47 @@ xqc_conn_get_errno(xqc_connection_t *conn) return conn->conn_err; } +void * +xqc_conn_get_ssl(xqc_connection_t *conn) +{ + if (conn->tls) { + return xqc_tls_get_ssl(conn->tls); + } + + return NULL; +} + +/* cleanup connection and wait for draining */ +void +xqc_conn_shutdown(xqc_connection_t *conn) +{ + xqc_path_ctx_t *path; + xqc_list_head_t *pos, *next; + xqc_send_ctl_t *send_ctl; + xqc_usec_t now; + + now = xqc_monotonic_timestamp(); + xqc_usec_t pto = xqc_conn_get_max_pto(conn); + if (!xqc_timer_is_set(&conn->conn_timer_manager, XQC_TIMER_CONN_DRAINING)) { + xqc_timer_set(&conn->conn_timer_manager, XQC_TIMER_CONN_DRAINING, now, 3 * pto); + } + + xqc_send_queue_drop_packets(conn); + + xqc_list_for_each_safe(pos, next, &conn->conn_paths_list) { + path = xqc_list_entry(pos, xqc_path_ctx_t, path_list); + for (int i = 0; i <= XQC_TIMER_LOSS_DETECTION; i++) { + xqc_timer_unset(&path->path_send_ctl->path_timer_manager, i); + } + } +} + + xqc_int_t xqc_conn_immediate_close(xqc_connection_t *conn) { + int ret; + if (conn->conn_state >= XQC_CONN_STATE_DRAINING) { return XQC_OK; } @@ -1998,34 +2510,14 @@ xqc_conn_immediate_close(xqc_connection_t *conn) return XQC_OK; } - int ret; - xqc_send_ctl_t *send_ctl; - xqc_usec_t now; - if (conn->conn_state < XQC_CONN_STATE_CLOSING) { + xqc_conn_shutdown(conn); + /* convert state to CLOSING */ xqc_log(conn->log, XQC_LOG_INFO, "|state to closing|state:%s|flags:%s", - xqc_conn_state_2_str(conn->conn_state), xqc_conn_flag_2_str(conn->conn_flag)); - + xqc_conn_state_2_str(conn->conn_state), + xqc_conn_flag_2_str(conn->conn_flag)); conn->conn_state = XQC_CONN_STATE_CLOSING; - - xqc_send_queue_drop_packets(conn); - - now = xqc_monotonic_timestamp(); - xqc_usec_t pto = xqc_conn_get_max_pto(conn); - if (!xqc_timer_is_set(&conn->conn_timer_manager, XQC_TIMER_CONN_DRAINING)) { - xqc_timer_set(&conn->conn_timer_manager, XQC_TIMER_CONN_DRAINING, now, 3 * pto); - } - - - xqc_path_ctx_t *path = NULL; - xqc_list_head_t *pos, *next; - xqc_list_for_each_safe(pos, next, &conn->conn_paths_list) { - path = xqc_list_entry(pos, xqc_path_ctx_t, path_list); - for (int i = 0; i <= XQC_TIMER_LOSS_DETECTION; i++) { - xqc_timer_unset(&path->path_send_ctl->path_timer_manager, i); - } - } } /* @@ -2050,7 +2542,7 @@ xqc_int_t xqc_conn_send_retry(xqc_connection_t *conn, unsigned char *token, unsigned token_len) { xqc_engine_t *engine = conn->engine; - unsigned char buf[XQC_PACKET_OUT_SIZE]; + unsigned char buf[XQC_PACKET_OUT_BUF_CAP]; xqc_int_t size = (xqc_int_t)xqc_gen_retry_packet(buf, conn->dcid_set.current_dcid.cid_buf, conn->dcid_set.current_dcid.cid_len, @@ -2173,6 +2665,36 @@ xqc_conn_send_version_negotiation(xqc_connection_t *c) return XQC_OK; } +void +xqc_conn_continue_send_by_conn(xqc_connection_t *conn) +{ + xqc_log(conn->log, XQC_LOG_DEBUG, "|conn:%p|", conn); + if (!conn) { + xqc_log(conn->engine->log, XQC_LOG_ERROR, "|can not find connection|conn:%p|", conn); + return ; + } + xqc_log(conn->log, XQC_LOG_DEBUG, "|conn:%p|", conn); + + xqc_conn_schedule_packets_to_paths(conn); + + if (xqc_engine_is_sendmmsg_on(conn->engine)) { + xqc_conn_transmit_pto_probe_packets_batch(conn); + xqc_conn_retransmit_lost_packets_batch(conn); + xqc_conn_send_packets_batch(conn); + + } else { + xqc_conn_transmit_pto_probe_packets(conn); + xqc_conn_retransmit_lost_packets(conn); + xqc_conn_send_packets(conn); + } + + if (conn->conn_settings.mp_enable_reinjection & XQC_REINJ_UNACK_AFTER_SEND) { + xqc_conn_reinject_unack_packets(conn, XQC_REINJ_UNACK_AFTER_SEND); + xqc_conn_send_packets(conn); + } + + xqc_engine_main_logic_internal(conn->engine); +} int xqc_conn_continue_send(xqc_engine_t *engine, const xqc_cid_t *cid) @@ -2194,73 +2716,187 @@ xqc_conn_info_print(xqc_connection_t *conn, xqc_conn_stats_t *conn_stats) { char *buff = conn_stats->conn_info; size_t buff_size = XQC_CONN_INFO_LEN; + size_t curr_size = 0; int ret = 0; int record_cnt = 0; + int i = 0; xqc_list_head_t *pos, *next; xqc_path_ctx_t *path = NULL; xqc_path_info_t path_info; + uint32_t mp_settings = 0; + uint32_t sock_err_flag = 0; + + if (conn->enable_multipath) { + mp_settings |= 1; + } + + if (conn->local_settings.enable_multipath) { + mp_settings |= (1 << 1); + } + + if (conn->remote_settings.enable_multipath) { + mp_settings |= (1 << 2); + } + + if (conn->conn_settings.multipath_version == XQC_MULTIPATH_05) { + mp_settings |= (1 << 3); + } + + if (conn->local_settings.multipath_version == XQC_MULTIPATH_05) { + mp_settings |= (1 << 4); + } + + if (conn->remote_settings.multipath_version == XQC_MULTIPATH_05) { + mp_settings |= (1 << 5); + } + + /* conn info */ + ret = snprintf(buff, buff_size, "%u,%u,%u,%u,%u,%u,%u," + "%u,%u,%u,%u,%u,%u,%u,%"PRIu64",%"PRIu64",%"PRIu64",", + mp_settings, + conn->create_path_count, + conn->validated_path_count, + conn->active_path_count, + conn->dgram_stats.total_dgram, + conn->dgram_stats.hp_dgram, + conn->dgram_stats.hp_red_dgram, + conn->dgram_stats.hp_red_dgram_mp, + conn->dgram_stats.timer_red_dgram, + conn->sched_cc_blocked, + conn->send_cc_blocked, + conn->snd_pkt_stats.conn_sent_pkts, + conn->rcv_pkt_stats.conn_rcvd_pkts, + conn->rcv_pkt_stats.conn_udp_pkts, + conn->stream_stats.send_bytes, + conn->stream_stats.reinjected_bytes, + conn->stream_stats.recv_bytes); + + curr_size += ret; + + if (curr_size >= buff_size) { + goto full; + } + + /* recv_stats */ + for (i = 0; i < 3; i++) { + + ret = snprintf(buff + curr_size, buff_size - curr_size, + "%u,%u,%u,%"PRIu64",%"PRIu64"," + "%"PRIu64",%"PRIu64",", + (uint32_t)conn->rcv_pkt_stats.pkt_types[i], + conn->rcv_pkt_stats.pkt_size[i], + conn->rcv_pkt_stats.pkt_udp_size[i], + (uint64_t)conn->rcv_pkt_stats.pkt_frames[i], + (uint64_t)conn->rcv_pkt_stats.pkt_pn[i], + (uint64_t)conn->rcv_pkt_stats.pkt_timestamp[i], + (uint64_t)conn->rcv_pkt_stats.pkt_err[i]); + + curr_size += ret; + + if (curr_size >= buff_size) { + goto full; + } + } + + /* send_stats */ + for (i = 0; i < 3; i++) { + ret = snprintf(buff + curr_size, buff_size - curr_size, + "%u,%u,%"PRIu64",%"PRIu64"," + "%"PRIu64",", + (uint32_t)conn->snd_pkt_stats.pkt_types[i], + conn->snd_pkt_stats.pkt_size[i], + (uint64_t)conn->snd_pkt_stats.pkt_frames[i], + (uint64_t)conn->snd_pkt_stats.pkt_pn[i], + (uint64_t)conn->snd_pkt_stats.pkt_timestamp[i]); + + curr_size += ret; + + if (curr_size >= buff_size) { + goto full; + } + } /* path layer 自定义 */ xqc_list_for_each_safe(pos, next, &conn->conn_paths_list) { path = xqc_list_entry(pos, xqc_path_ctx_t, path_list); xqc_path_record_info(path, &path_info); - ret = xqc_path_info_print(&path_info, &buff, &buff_size); - if (ret <= 0) { - xqc_log(conn->log, XQC_LOG_WARN, "|write cur_path_info error|"); - return; - } - record_cnt++; - if (record_cnt >= XQC_MAX_PATHS_COUNT) { - return; + sock_err_flag = (path->path_flag & XQC_PATH_FLAG_SOCKET_ERROR) != 0; + + ret = snprintf(buff + curr_size, buff_size - curr_size, + "%d-%d,%d-%d,%d-%d,%d-%"PRIu64"," + "%d-%"PRIu64",%d-%"PRIu64"," + "%d-%"PRIu64",%d-%"PRIu64"," + "%d-%u,%d-%u," + "%d-%u,%d-%u," + "%d-%u,%d-%u," + "%d-%u,%d-%u,", + (int)path_info.path_id, path_info.path_state, + (int)path_info.path_id, path_info.app_path_status, + (int)path_info.path_id, sock_err_flag, + (int)path_info.path_id, path_info.path_create_time, + (int)path_info.path_id, path_info.path_destroy_time, + (int)path_info.path_id, path_info.srtt, + (int)path_info.path_id, path_info.path_bytes_send, + (int)path_info.path_id, path_info.path_bytes_recv, + (int)path_info.path_id, path_info.pkt_send_cnt, + (int)path_info.path_id, path_info.pkt_recv_cnt, + (int)path_info.path_id, path_info.loss_cnt, + (int)path_info.path_id, path_info.tlp_cnt, + (int)path_info.path_id, path_info.dgram_send_cnt, + (int)path_info.path_id, path_info.dgram_recv_cnt, + (int)path_info.path_id, path_info.red_dgram_send_cnt, + (int)path_info.path_id, path_info.red_dgram_recv_cnt); + + curr_size += ret; + + if (curr_size >= buff_size) { + goto full; } - } - /* history */ - int his_cnt = conn->history_path->path_cnt - 1; - int index = 0; - while (his_cnt >= 0 && record_cnt < XQC_MAX_PATHS_COUNT) { - index = his_cnt % XQC_MAX_PATHS_COUNT; - ret = xqc_path_info_print(&conn->history_path->path_info[index], &buff, &buff_size); - if (ret <= 0) { - xqc_log(conn->log, XQC_LOG_WARN, "|write his_path_info error|"); - return; + } +full: + curr_size = xqc_min(curr_size, buff_size); + for (i = curr_size - 1; i >= 0; i--) { + if (buff[i] == ',') { + buff[i] = '\0'; + break; } - - his_cnt--; - record_cnt++; } + buff[buff_size - 1] = '\0'; } -xqc_conn_stats_t -xqc_conn_get_stats(xqc_engine_t *engine, const xqc_cid_t *cid) +void +xqc_conn_get_stats_internal(xqc_connection_t *conn, xqc_conn_stats_t *conn_stats) { - xqc_connection_t *conn; - xqc_conn_stats_t conn_stats; - xqc_memzero(&conn_stats, sizeof(conn_stats)); - for (int i = 0; i < XQC_MAX_PATHS_COUNT; ++i) { - conn_stats.paths_info[i].path_id = XQC_MAX_UINT64_VALUE; + /* 1. 与路径无关的连接级别埋点 */ + const char *out_alpn = NULL; + size_t out_alpn_len = 0; + + if (conn->tls) { + xqc_tls_get_selected_alpn(conn->tls, &out_alpn, &out_alpn_len); } - conn = xqc_engine_conns_hash_find(engine, cid, 's'); - if (!conn) { - xqc_log(engine->log, XQC_LOG_ERROR, "|can not find connection|cid:%s", - xqc_scid_str(cid)); - return conn_stats; + xqc_memset(conn_stats->alpn, 0, XQC_MAX_ALPN_BUF_LEN); + if (out_alpn) { + strncpy(conn_stats->alpn, out_alpn, xqc_min(out_alpn_len, XQC_MAX_ALPN_BUF_LEN)); + + } else { + conn_stats->alpn[0] = '-'; + conn_stats->alpn[1] = '1'; } - /* 1. 与路径无关的连接级别埋点 */ - conn_stats.conn_err = (int)conn->conn_err; - conn_stats.early_data_flag = XQC_0RTT_NONE; - conn_stats.enable_multipath = conn->enable_multipath; - conn_stats.spurious_loss_detect_on = conn->conn_settings.spurious_loss_detect_on; + conn_stats->conn_err = (int)conn->conn_err; + conn_stats->early_data_flag = XQC_0RTT_NONE; + conn_stats->enable_multipath = conn->enable_multipath; + conn_stats->spurious_loss_detect_on = conn->conn_settings.spurious_loss_detect_on; if (conn->conn_flag & XQC_CONN_FLAG_HAS_0RTT) { if (conn->conn_flag & XQC_CONN_FLAG_0RTT_OK) { - conn_stats.early_data_flag = XQC_0RTT_ACCEPT; + conn_stats->early_data_flag = XQC_0RTT_ACCEPT; } else if (conn->conn_flag & XQC_CONN_FLAG_0RTT_REJ) { - conn_stats.early_data_flag = XQC_0RTT_REJECT; + conn_stats->early_data_flag = XQC_0RTT_REJECT; } } @@ -2269,9 +2905,17 @@ xqc_conn_get_stats(xqc_engine_t *engine, const xqc_cid_t *cid) && conn->conn_initial_path->path_send_ctl && conn->conn_initial_path->path_pn_ctl) { - conn_stats.srtt = conn->conn_initial_path->path_send_ctl->ctl_srtt; + if (conn_stats->min_rtt == XQC_MAX_UINT32_VALUE) { + conn_stats->srtt = 0; + conn_stats->min_rtt = 0; + + } else { + conn_stats->srtt = conn->conn_initial_path->path_send_ctl->ctl_srtt; + conn_stats->min_rtt = conn->conn_initial_path->path_send_ctl->ctl_minrtt; + } + xqc_recv_record_print(conn, &conn->conn_initial_path->path_pn_ctl->ctl_recv_record[XQC_PNS_APP_DATA], - conn_stats.ack_info, sizeof(conn_stats.ack_info)); + conn_stats->ack_info, sizeof(conn_stats->ack_info)); } @@ -2292,22 +2936,69 @@ xqc_conn_get_stats(xqc_engine_t *engine, const xqc_cid_t *cid) continue; } - conn_stats.lost_count += send_ctl->ctl_lost_count; - conn_stats.send_count += send_ctl->ctl_send_count; - conn_stats.tlp_count += send_ctl->ctl_tlp_count; - conn_stats.spurious_loss_count += send_ctl->ctl_spurious_loss_count; - conn_stats.recv_count += send_ctl->ctl_recv_count; + conn_stats->lost_count += send_ctl->ctl_lost_count; + conn_stats->send_count += send_ctl->ctl_send_count; + conn_stats->tlp_count += send_ctl->ctl_tlp_count; + conn_stats->spurious_loss_count += send_ctl->ctl_spurious_loss_count; + conn_stats->recv_count += send_ctl->ctl_recv_count; + conn_stats->lost_dgram_count += send_ctl->ctl_lost_dgram_cnt; + conn_stats->inflight_bytes += send_ctl->ctl_bytes_in_flight; } /* 路径信息 */ - xqc_conn_path_metrics_print(conn, &conn_stats); + xqc_conn_path_metrics_print(conn, conn_stats); /* 自定义信息 */ - xqc_conn_info_print(conn, &conn_stats); + xqc_conn_info_print(conn, conn_stats); +} +xqc_conn_stats_t +xqc_conn_get_stats(xqc_engine_t *engine, const xqc_cid_t *cid) +{ + xqc_connection_t *conn; + xqc_conn_stats_t conn_stats; + xqc_memzero(&conn_stats, sizeof(conn_stats)); + for (int i = 0; i < XQC_MAX_PATHS_COUNT; ++i) { + conn_stats.paths_info[i].path_id = XQC_MAX_UINT64_VALUE; + } + + conn = xqc_engine_conns_hash_find(engine, cid, 's'); + if (!conn) { + xqc_log(engine->log, XQC_LOG_ERROR, "|can not find connection|cid:%s", + xqc_scid_str(cid)); + return conn_stats; + } + + xqc_conn_get_stats_internal(conn, &conn_stats); + return conn_stats; } + +xqc_usec_t +xqc_conn_get_lastest_rtt(xqc_engine_t *engine, const xqc_cid_t *cid) +{ + xqc_connection_t *conn; + xqc_path_ctx_t *path; + + conn = xqc_engine_conns_hash_find(engine, cid, 's'); + if (!conn) { + xqc_log(engine->log, XQC_LOG_ERROR, "|can not find connection|cid:%s", + xqc_scid_str(cid)); + return 0; + } + + path = conn->conn_initial_path; + if (!path) { + xqc_log(engine->log, XQC_LOG_ERROR, "|can not find initial path|cid:%s", + xqc_scid_str(cid)); + return 0; + } + + return path->path_send_ctl->ctl_latest_rtt; +} + + xqc_int_t xqc_conn_check_token(xqc_connection_t *conn, const unsigned char *token, unsigned token_len) { @@ -2406,6 +3097,47 @@ xqc_conn_gen_token(xqc_connection_t *conn, unsigned char *token, unsigned *token memcpy(token, &expire, sizeof(expire)); } +void +xqc_conn_resend_0rtt_datagram(xqc_connection_t *conn) +{ + xqc_list_head_t *pos, *next; + xqc_datagram_0rtt_buffer_t *dgram_buffer; + struct iovec iov[XQC_MAX_SEND_MSG_ONCE]; + uint64_t dgram_id_list[XQC_MAX_SEND_MSG_ONCE]; + size_t iov_size, sent, sent_bytes; + int ret; + + iov_size = 0; + + xqc_list_for_each_safe(pos, next, &conn->dgram_0rtt_buffer_list) { + dgram_buffer = xqc_list_entry(pos, xqc_datagram_0rtt_buffer_t, list); + iov[iov_size].iov_base = dgram_buffer->iov.iov_base; + iov[iov_size].iov_len = dgram_buffer->iov.iov_len; + dgram_id_list[iov_size] = dgram_buffer->dgram_id; + iov_size++; + if (iov_size >= XQC_MAX_SEND_MSG_ONCE) { + ret = xqc_datagram_send_multiple_internal(conn, iov, dgram_id_list, XQC_MAX_SEND_MSG_ONCE, &sent, &sent_bytes, dgram_buffer->qos_level, XQC_TRUE); + if (ret < 0) { + xqc_log(conn->log, XQC_LOG_ERROR, "|unable_to_resend_0rtt_pkts_in_1rtt_way|"); + XQC_CONN_ERR(conn, TRA_INTERNAL_ERROR); + iov_size = 0; + break; + } + iov_size -= XQC_MAX_SEND_MSG_ONCE; + } + } + + if (iov_size > 0) { + ret = xqc_datagram_send_multiple_internal(conn, iov, dgram_id_list, iov_size, &sent, &sent_bytes, dgram_buffer->qos_level, XQC_TRUE); + if (ret < 0) { + xqc_log(conn->log, XQC_LOG_ERROR, "|unbale_to_resend_0rtt_pkts_in_1rtt_way|"); + XQC_CONN_ERR(conn, TRA_INTERNAL_ERROR); + } + } + + xqc_conn_destroy_0rtt_datagram_buffer_list(conn); +} + xqc_int_t xqc_conn_early_data_reject(xqc_connection_t *conn) { @@ -2427,6 +3159,8 @@ xqc_conn_early_data_reject(xqc_connection_t *conn) xqc_send_queue_drop_0rtt_packets(conn); + xqc_conn_resend_0rtt_datagram(conn); + xqc_list_for_each_safe(pos, next, &conn->conn_all_streams) { stream = xqc_list_entry(pos, xqc_stream_t, all_stream_list); if (stream->stream_flag & XQC_STREAM_FLAG_HAS_0RTT) { @@ -2443,6 +3177,7 @@ xqc_conn_early_data_reject(xqc_connection_t *conn) xqc_stream_write_buffed_data_to_packets(stream); } } + return XQC_OK; } @@ -2463,6 +3198,9 @@ xqc_conn_early_data_accept(xqc_connection_t *conn) stream = xqc_list_entry(pos, xqc_stream_t, all_stream_list); xqc_destroy_write_buff_list(&stream->stream_write_buff_list.write_buff_list); } + + xqc_conn_destroy_0rtt_datagram_buffer_list(conn); + return XQC_OK; } @@ -2484,6 +3222,18 @@ xqc_conn_handshake_confirmed(xqc_connection_t *conn) return XQC_OK; } + +void +xqc_conn_update_flow_ctl_settings(xqc_connection_t *conn) +{ + xqc_conn_flow_ctl_t *flow_ctl = &conn->conn_flow_ctl; + xqc_trans_settings_t *remote_settings = &conn->remote_settings; + + flow_ctl->fc_max_data_can_send = remote_settings->max_data; + flow_ctl->fc_max_streams_bidi_can_send = remote_settings->max_streams_bidi; + flow_ctl->fc_max_streams_uni_can_send = remote_settings->max_streams_uni; +} + xqc_int_t xqc_conn_handshake_complete(xqc_connection_t *conn) { @@ -2491,19 +3241,37 @@ xqc_conn_handshake_complete(xqc_connection_t *conn) xqc_list_head_t *pos, *next; xqc_stream_t *stream; + xqc_multipath_version_t mp_version_ret; /* update flow control */ - conn->conn_flow_ctl.fc_max_data_can_send = conn->remote_settings.max_data; - conn->conn_flow_ctl.fc_max_streams_bidi_can_send = conn->remote_settings.max_streams_bidi; - conn->conn_flow_ctl.fc_max_streams_uni_can_send = conn->remote_settings.max_streams_uni; + xqc_conn_update_flow_ctl_settings(conn); xqc_list_for_each_safe(pos, next, &conn->conn_all_streams) { stream = xqc_list_entry(pos, xqc_stream_t, all_stream_list); - xqc_stream_set_flow_ctl(stream); + xqc_stream_update_flow_ctl(stream); } /* determine multipath mode */ conn->enable_multipath = xqc_conn_enable_multipath(conn); + if (!conn->enable_multipath + && xqc_timer_is_set(&conn->conn_timer_manager, XQC_TIMER_PMTUD_PROBING)) + { + conn->probing_cnt = 0; + conn->conn_flag |= XQC_CONN_FLAG_PMTUD_PROBING; + xqc_timer_unset(&conn->conn_timer_manager, XQC_TIMER_PMTUD_PROBING); + } + + mp_version_ret = xqc_conn_multipath_version_negotiation(conn); + if (mp_version_ret == XQC_ERR_MULTIPATH_VERSION) { + xqc_log(conn->log, XQC_LOG_WARN, "|multipath_version_negotiation err|"); + conn->enable_multipath = 0; + } + conn->conn_settings.multipath_version = mp_version_ret; + + if (conn->enable_multipath) { + conn->conn_flag |= XQC_CONN_FLAG_MP_WAIT_SCID; + } + /* conn's handshake is complete when TLS stack has reported handshake complete */ conn->conn_flag |= XQC_CONN_FLAG_HANDSHAKE_COMPLETED; @@ -2618,8 +3386,11 @@ xqc_conn_process_undecrypt_packet_in(xqc_connection_t *conn, xqc_encrypt_level_t packet_in = xqc_list_entry(pos, xqc_packet_in_t, pi_list); xqc_log(conn->log, XQC_LOG_DEBUG, "|delay|undecrypt_count:%ud|encrypt_level:%d|", conn->undecrypt_count[encrypt_level], encrypt_level); - +#ifdef XQC_NO_PID_PACKET_PROCESS ret = xqc_conn_process_packet(conn, packet_in->buf, packet_in->buf_size, packet_in->pkt_recv_time); +#else + ret = xqc_conn_process_packet(conn, packet_in->buf, packet_in->buf_size, XQC_UNKNOWN_PATH_ID, packet_in->pkt_recv_time); +#endif if (ret) { xqc_log(conn->log, XQC_LOG_ERROR, "|xqc_packet_process error|ret:%d|", ret); return ret; @@ -2688,8 +3459,18 @@ xqc_conn_next_wakeup_time(xqc_connection_t *conn) } } - xqc_path_ctx_t *path; xqc_list_head_t *pos, *next; + xqc_gp_timer_t *gp_timer; + /* gp timer */ + xqc_list_for_each_safe(pos, next, &conn->conn_timer_manager.gp_timer_list) { + gp_timer = xqc_list_entry(pos, xqc_gp_timer_t, list); + if (gp_timer->timer_is_set) { + min_time = xqc_min(min_time, gp_timer->expire_time); + } + } + + xqc_path_ctx_t *path; + xqc_list_for_each_safe(pos, next, &conn->conn_paths_list) { path = xqc_list_entry(pos, xqc_path_ctx_t, path_list); if (path->path_state != XQC_PATH_STATE_ACTIVE) { @@ -2818,11 +3599,21 @@ xqc_conn_record_single(xqc_connection_t *c, xqc_packet_in_t *packet_in) if (!xqc_has_packet_number(&packet_in->pi_pkt)) { return; } + xqc_path_ctx_t *path; + + if (c->enable_multipath) { + //TODO: MPQUIC fix migration + path = xqc_conn_find_path_by_path_id(c, packet_in->pi_path_id); + + } else { + path = c->conn_initial_path; + } + - xqc_path_ctx_t *path = xqc_conn_find_path_by_path_id(c, packet_in->pi_path_id); if (path == NULL) { return; } + xqc_pn_ctl_t *pn_ctl = xqc_get_pn_ctl(c, path); xqc_send_ctl_t *send_ctl = path->path_send_ctl; @@ -2840,11 +3631,6 @@ xqc_conn_record_single(xqc_connection_t *c, xqc_packet_in_t *packet_in) send_ctl->ctl_largest_received[pns] = pkt_num; send_ctl->ctl_largest_recv_time[pns] = packet_in->pkt_recv_time; } - - if (pkt_num < send_ctl->ctl_unack_received[pns]) { - send_ctl->ctl_unack_received[pns] = pkt_num; - xqc_log(c->log, XQC_LOG_DEBUG, "|path:%ui|ctl_unack_received:%ui|", path->path_id, send_ctl->ctl_unack_received[pns]); - } } if (pkt_num != xqc_recv_record_largest(&pn_ctl->ctl_recv_record[pns])) { @@ -2888,11 +3674,16 @@ xqc_conn_confirm_cid(xqc_connection_t *c, xqc_packet_t *pkt) if (XQC_OK != xqc_cid_is_equal(&c->dcid_set.current_dcid, &pkt->pkt_scid)) { xqc_log(c->log, XQC_LOG_INFO, "|dcid change|ori:%s|new:%s|", xqc_dcid_str(&c->dcid_set.current_dcid), xqc_scid_str(&pkt->pkt_scid)); + // TODO: DCID changes xqc_cid_copy(&c->dcid_set.current_dcid, &pkt->pkt_scid); xqc_cid_copy(&c->conn_initial_path->path_dcid, &pkt->pkt_scid); + xqc_datagram_record_mss(c); } - if (xqc_insert_conns_hash(c->engine->conns_hash_dcid, c, &c->dcid_set.current_dcid)) { + if (xqc_insert_conns_hash(c->engine->conns_hash_dcid, c, + c->dcid_set.current_dcid.cid_buf, + c->dcid_set.current_dcid.cid_len)) + { xqc_log(c->log, XQC_LOG_ERROR, "|insert conn hash error"); return -XQC_EMALLOC; } @@ -3062,7 +3853,7 @@ xqc_conn_on_pkt_processed(xqc_connection_t *c, xqc_packet_in_t *pi, xqc_usec_t n /* record packet */ xqc_conn_record_single(c, pi); - if (pi->pi_frame_types & (~(XQC_FRAME_BIT_STREAM|XQC_FRAME_BIT_PADDING))) { + if (pi->pi_frame_types & (~(XQC_FRAME_BIT_STREAM|XQC_FRAME_BIT_DATAGRAM|XQC_FRAME_BIT_PADDING))) { c->conn_flag |= XQC_CONN_FLAG_NEED_RUN; } @@ -3078,18 +3869,46 @@ xqc_conn_on_pkt_processed(xqc_connection_t *c, xqc_packet_in_t *pi, xqc_usec_t n uint8_t xqc_conn_tolerant_error(xqc_int_t ret) { - if (-XQC_EVERSION == ret || -XQC_EILLPKT == ret || -XQC_EWAITING == ret || -XQC_EIGNORE_PKT == ret) { + if (-XQC_EVERSION == ret || -XQC_EILLPKT == ret || -XQC_EWAITING == ret + || -XQC_EIGNORE_PKT == ret || -XQC_EDECRYPT == ret) + { return XQC_TRUE; } return XQC_FALSE; } +static inline void +xqc_conn_log_recvd_packet(xqc_connection_t *c, xqc_packet_in_t *pi, + size_t udp_size, xqc_int_t err, xqc_usec_t timestamp) +{ + int index = c->rcv_pkt_stats.curr_index; + c->rcv_pkt_stats.pkt_frames[index] = pi->pi_frame_types; + c->rcv_pkt_stats.pkt_err[index] = err; + c->rcv_pkt_stats.pkt_size[index] = pi->pi_pkt.length; + c->rcv_pkt_stats.pkt_timestamp[index] = xqc_calc_delay(timestamp, + c->conn_create_time); + c->rcv_pkt_stats.pkt_timestamp[index] /= 1000; // ms + c->rcv_pkt_stats.pkt_udp_size[index] = udp_size; + c->rcv_pkt_stats.pkt_types[index] = pi->pi_pkt.pkt_type; + c->rcv_pkt_stats.pkt_pn[index] = pi->pi_pkt.pkt_num; + c->rcv_pkt_stats.conn_rcvd_pkts++; + c->rcv_pkt_stats.curr_index = (index + 1) % 3; +} + +#ifdef XQC_NO_PID_PACKET_PROCESS +xqc_int_t +xqc_conn_process_packet(xqc_connection_t *c, + const unsigned char *packet_in_buf, size_t packet_in_size, + xqc_usec_t recv_time) +#else xqc_int_t xqc_conn_process_packet(xqc_connection_t *c, - const unsigned char *packet_in_buf, size_t packet_in_size, xqc_usec_t recv_time) + const unsigned char *packet_in_buf, size_t packet_in_size, + uint64_t path_id, xqc_usec_t recv_time) +#endif { - xqc_int_t ret = XQC_ERROR; + xqc_int_t ret = XQC_OK; const unsigned char *last_pos = NULL; const unsigned char *pos = packet_in_buf; /* start of QUIC pkt */ const unsigned char *end = packet_in_buf + packet_in_size; /* end of udp datagram */ @@ -3104,18 +3923,26 @@ xqc_conn_process_packet(xqc_connection_t *c, xqc_packet_in_t *packet_in = &packet; memset(packet_in, 0, sizeof(*packet_in)); xqc_packet_in_init(packet_in, pos, end - pos, decrypt_payload, XQC_MAX_PACKET_IN_LEN, recv_time); +#ifndef XQC_NO_PID_PACKET_PROCESS + packet_in->pi_path_id = path_id; +#else + packet_in->pi_path_id = XQC_UNKNOWN_PATH_ID; +#endif /* packet_in->pos will update inside */ ret = xqc_packet_process_single(c, packet_in); + + xqc_conn_log_recvd_packet(c, packet_in, packet_in_size, ret, recv_time); + if (ret == XQC_OK) { - if (XQC_OK != (ret = xqc_conn_on_pkt_processed(c, packet_in, recv_time))) { - xqc_log(c->log, XQC_LOG_ERROR, "|on_pkt_process error|ret:%d|", ret); - } + ret = xqc_conn_on_pkt_processed(c, packet_in, recv_time); } else if (xqc_conn_tolerant_error(ret)) { + /* ignore the remain bytes */ xqc_log(c->log, XQC_LOG_INFO, "|ignore err|%d|", ret); packet_in->pos = packet_in->last; - return XQC_OK; + ret = XQC_OK; + goto end; } /* error occurred or read state is error */ @@ -3130,16 +3957,32 @@ xqc_conn_process_packet(xqc_connection_t *c, pos = packet_in->last; xqc_log_event(c->log, TRA_PACKET_RECEIVED, packet_in); } - - return XQC_OK; +end: + return ret; } - +#ifdef XQC_NO_PID_PACKET_PROCESS void -xqc_conn_process_packet_recved_path(xqc_connection_t *conn, xqc_cid_t *scid, +xqc_conn_process_packet_recved_path(xqc_connection_t *conn, xqc_cid_t *scid, size_t packet_in_size, xqc_usec_t recv_time) +#else +void +xqc_conn_process_packet_recved_path(xqc_connection_t *conn, xqc_cid_t *scid, + uint64_t path_id, size_t packet_in_size, xqc_usec_t recv_time) +#endif { - xqc_path_ctx_t *path = xqc_conn_find_path_by_scid(conn, scid); + xqc_path_ctx_t *path = NULL; + if (conn->enable_multipath) { + path = xqc_conn_find_path_by_scid(conn, scid); +#ifndef XQC_NO_PID_PACKET_PROCESS + if (path == NULL && path_id != XQC_UNKNOWN_PATH_ID) { + path = xqc_conn_find_path_by_path_id(conn, path_id); + } +#endif + } else { + path = conn->conn_initial_path; + } + if (path == NULL) { xqc_log(conn->log, XQC_LOG_INFO, "|ignore unknown path|scid:%s|", xqc_scid_str(scid)); return; @@ -3158,9 +4001,11 @@ xqc_int_t xqc_conn_check_tx_key(xqc_connection_t *conn) { /* if tx key is ready, conn can send 1RTT packets */ - if (xqc_tls_is_key_ready(conn->tls, XQC_ENC_LEV_1RTT, XQC_KEY_TYPE_TX_WRITE)) { + if (!(conn->conn_flag & XQC_CONN_FLAG_CAN_SEND_1RTT) + && xqc_tls_is_key_ready(conn->tls, XQC_ENC_LEV_1RTT, XQC_KEY_TYPE_TX_WRITE)) { xqc_log(conn->log, XQC_LOG_INFO, "|keys are ready, can send 1rtt now|"); conn->conn_flag |= XQC_CONN_FLAG_CAN_SEND_1RTT; + xqc_datagram_record_mss(conn); } return XQC_OK; @@ -3169,19 +4014,19 @@ xqc_conn_check_tx_key(xqc_connection_t *conn) xqc_int_t xqc_conn_check_handshake_complete(xqc_connection_t *conn) { + /* check tx keys after handshake complete */ + xqc_conn_check_tx_key(conn); + if (!(conn->conn_flag & XQC_CONN_FLAG_HANDSHAKE_COMPLETED) && conn->conn_state == XQC_CONN_STATE_ESTABED) { xqc_log(conn->log, XQC_LOG_DEBUG, "|HANDSHAKE_COMPLETED|conn:%p|", conn); xqc_conn_handshake_complete(conn); - if (conn->app_proto_cbs.conn_cbs.conn_handshake_finished) { conn->app_proto_cbs.conn_cbs.conn_handshake_finished(conn, conn->user_data, conn->proto_data); } } - /* check tx keys after handshake complete */ - xqc_conn_check_tx_key(conn); return XQC_OK; } @@ -3206,14 +4051,22 @@ xqc_conn_destroy_cids(xqc_connection_t *conn) xqc_list_head_t *pos, *next; if (conn->engine->conns_hash) { - if (xqc_find_conns_hash(conn->engine->conns_hash, conn, &conn->original_dcid)) { - xqc_remove_conns_hash(conn->engine->conns_hash, conn, &conn->original_dcid); + if (xqc_find_conns_hash(conn->engine->conns_hash, conn, + conn->original_dcid.cid_buf, + conn->original_dcid.cid_len)) + { + xqc_remove_conns_hash(conn->engine->conns_hash, conn, + conn->original_dcid.cid_buf, + conn->original_dcid.cid_len); } xqc_list_for_each_safe(pos, next, &conn->scid_set.cid_set.list_head) { cid = xqc_list_entry(pos, xqc_cid_inner_t, list); - if (xqc_find_conns_hash(conn->engine->conns_hash, conn, &cid->cid)) { - xqc_remove_conns_hash(conn->engine->conns_hash, conn, &cid->cid); + if (xqc_find_conns_hash(conn->engine->conns_hash, conn, + cid->cid.cid_buf, cid->cid.cid_len)) + { + xqc_remove_conns_hash(conn->engine->conns_hash, conn, + cid->cid.cid_buf, cid->cid.cid_len); } } } @@ -3221,8 +4074,23 @@ xqc_conn_destroy_cids(xqc_connection_t *conn) if (conn->engine->conns_hash_dcid && (conn->conn_flag & XQC_CONN_FLAG_DCID_OK)) { xqc_list_for_each_safe(pos, next, &conn->dcid_set.cid_set.list_head) { cid = xqc_list_entry(pos, xqc_cid_inner_t, list); - if (xqc_find_conns_hash(conn->engine->conns_hash_dcid, conn, &cid->cid)) { - xqc_remove_conns_hash(conn->engine->conns_hash_dcid, conn, &cid->cid); + + /* delete relationship from conns_hash_dcid */ + if (xqc_find_conns_hash(conn->engine->conns_hash_dcid, conn, + cid->cid.cid_buf, cid->cid.cid_len)) + { + xqc_remove_conns_hash(conn->engine->conns_hash_dcid, conn, + cid->cid.cid_buf, cid->cid.cid_len); + } + + /* delete relationship from conns_hash_sr_token */ + if (xqc_find_conns_hash(conn->engine->conns_hash_sr_token, conn, + cid->cid.sr_token, + XQC_STATELESS_RESET_TOKENLEN)) + { + xqc_remove_conns_hash(conn->engine->conns_hash_sr_token, conn, + cid->cid.sr_token, + XQC_STATELESS_RESET_TOKENLEN); } } } @@ -3235,18 +4103,102 @@ xqc_conn_destroy_cids(xqc_connection_t *conn) xqc_int_t xqc_conn_try_add_new_conn_id(xqc_connection_t *conn, uint64_t retire_prior_to) { - if ((xqc_conn_is_handshake_confirmed(conn)) - && (conn->scid_set.cid_set.unused_cnt == 0) - && (conn->scid_set.cid_set.used_cnt < conn->remote_settings.active_connection_id_limit)) + uint64_t active_cid_cnt = conn->scid_set.cid_set.unused_cnt + conn->scid_set.cid_set.used_cnt; +#ifdef XQC_NO_PID_PACKET_PROCESS + uint64_t unused_limit = 1; +#else + uint64_t unused_limit = conn->enable_multipath ? 2 : 1; +#endif + if (xqc_conn_is_handshake_confirmed(conn)) { + while (active_cid_cnt < conn->remote_settings.active_connection_id_limit + && conn->scid_set.cid_set.unused_cnt < unused_limit) + { + xqc_int_t ret = xqc_write_new_conn_id_frame_to_packet(conn, retire_prior_to); + if (ret != XQC_OK) { + xqc_log(conn->log, XQC_LOG_ERROR, "|xqc_write_new_conn_id_frame_to_packet error|"); + return ret; + } + active_cid_cnt++; + } + } + + return XQC_OK; +} + +void +xqc_conn_ptmud_probing(xqc_connection_t *conn) +{ + if (conn->conn_state >= XQC_CONN_STATE_CLOSING) { + xqc_log(conn->log, XQC_LOG_INFO, "|conn closing, cannot send PMTUD probing|"); + } + /* probing can only be sent in 0RTT/1RTT packets */ + + xqc_pkt_type_t pkt_type = XQC_PTYPE_SHORT_HEADER; + int support_0rtt = xqc_conn_is_ready_to_send_early_data(conn); + + if (!(conn->conn_flag & XQC_CONN_FLAG_CAN_SEND_1RTT)) { + if ((conn->conn_type == XQC_CONN_TYPE_CLIENT) + && (conn->conn_state == XQC_CONN_STATE_CLIENT_INITIAL_SENT) + && support_0rtt) + { + pkt_type = XQC_PTYPE_0RTT; + conn->conn_flag |= XQC_CONN_FLAG_HAS_0RTT; + + } else { + return; + } + } + + if (pkt_type == XQC_PTYPE_0RTT + && conn->zero_rtt_count >= XQC_PACKET_0RTT_MAX_COUNT) { - xqc_int_t ret = xqc_write_new_conn_id_frame_to_packet(conn, retire_prior_to); - if (ret != XQC_OK) { - xqc_log(conn->log, XQC_LOG_ERROR, "|xqc_write_new_conn_id_frame_to_packet error|"); - return ret; + return; + } + + /* generate PING packets */ + if (conn->probing_cnt >= 3) { + /* if the current MSS has been already probed for 3 times + * while the MSS is not updated, we need to shrink the probing size + */ + conn->max_pkt_out_size = xqc_max(conn->probing_pkt_out_size - 1, conn->pkt_out_size); + conn->probing_pkt_out_size = xqc_max(conn->pkt_out_size, (conn->max_pkt_out_size + conn->pkt_out_size) >> 1); + conn->probing_cnt = 0; + } + + /* stop probing if the range is less than 10B */ + if ((conn->max_pkt_out_size - conn->pkt_out_size) < 10) { + xqc_log(conn->log, XQC_LOG_DEBUG, "|stop pmtud probing|max:%z|curr:%z|", + conn->max_pkt_out_size, conn->pkt_out_size); + conn->conn_flag &= ~XQC_CONN_FLAG_PMTUD_PROBING; + return; + } + + size_t probing_size = conn->probing_pkt_out_size; + xqc_list_head_t *pos, *next; + xqc_path_ctx_t *path; + xqc_int_t ret = XQC_OK; + xqc_usec_t probing_interval = conn->conn_settings.pmtud_probing_interval; + + /* only probing on active paths */ + xqc_list_for_each_safe(pos, next, &conn->conn_paths_list) { + path = xqc_list_entry(pos, xqc_path_ctx_t, path_list); + if (path->path_state != XQC_PATH_STATE_ACTIVE) { + continue; + } + ret = xqc_write_pmtud_ping_to_packet(path, probing_size, pkt_type); + if (ret < 0) { + xqc_log(conn->log, XQC_LOG_ERROR, "|genrate PMTUD ping packet error|ret:%d|", ret); } } - return XQC_OK; + /* set timer: default 500ms, 1000ms, or 2000ms according to probing_cnt */ + xqc_timer_set(&conn->conn_timer_manager, + XQC_TIMER_PMTUD_PROBING, + xqc_monotonic_timestamp(), + probing_interval * (1 << conn->probing_cnt)); + + conn->probing_cnt++; + conn->conn_flag &= ~XQC_CONN_FLAG_PMTUD_PROBING; } @@ -3324,6 +4276,12 @@ xqc_conn_set_cid_retired_ts(xqc_connection_t *conn, xqc_cid_inner_t *inner_cid) return ret; } + xqc_log(conn->log, XQC_LOG_DEBUG, + "|retired|cid:%s|seq:%ui|len:%d|", + xqc_scid_str(&inner_cid->cid), + inner_cid->cid.cid_seq_num, + inner_cid->cid.cid_len); + xqc_usec_t now = xqc_monotonic_timestamp(); xqc_usec_t pto = xqc_conn_get_max_pto(conn); @@ -3356,7 +4314,9 @@ xqc_conn_update_user_scid(xqc_connection_t *conn, xqc_scid_set_t *scid_set) xqc_conn_get_user_data(conn)); } + // TODO: SCID changes xqc_cid_copy(&scid_set->user_scid, &scid->cid); + xqc_datagram_record_mss(conn); return XQC_OK; } } @@ -3505,7 +4465,9 @@ xqc_conn_on_recv_retry(xqc_connection_t *conn, xqc_cid_t *retry_scid) conn->conn_flag |= XQC_CONN_FLAG_RETRY_RECVD; /* change the DCID it uses for sending packets in response to Retry packet. */ + // TODO: DCID changes xqc_cid_copy(&conn->dcid_set.current_dcid, retry_scid); + xqc_datagram_record_mss(conn); /* reset initial keys */ ret = xqc_tls_reset_initial(conn->tls, conn->version, retry_scid); @@ -3593,6 +4555,9 @@ xqc_conn_set_remote_transport_params(xqc_connection_t *conn, settings->active_connection_id_limit = params->active_connection_id_limit; settings->enable_multipath = params->enable_multipath; + settings->multipath_version = params->multipath_version; + settings->max_datagram_frame_size = params->max_datagram_frame_size; + return XQC_OK; } @@ -3627,6 +4592,8 @@ xqc_conn_get_local_transport_params(xqc_connection_t *conn, xqc_transport_params params->active_connection_id_limit = settings->active_connection_id_limit; params->no_crypto = settings->no_crypto; params->enable_multipath = settings->enable_multipath; + params->multipath_version = settings->multipath_version; + params->max_datagram_frame_size = settings->max_datagram_frame_size; /* set other transport parameters */ if (conn->conn_type == XQC_CONN_TYPE_SERVER @@ -3636,6 +4603,17 @@ xqc_conn_get_local_transport_params(xqc_connection_t *conn, xqc_transport_params conn->original_dcid.cid_buf, conn->original_dcid.cid_len); params->original_dest_connection_id_present = 1; + xqc_gen_reset_token(&conn->original_dcid, + params->stateless_reset_token, + XQC_STATELESS_RESET_TOKENLEN, + conn->engine->config->reset_token_key, + conn->engine->config->reset_token_keylen); + params->stateless_reset_token_present = 1; + + xqc_log(conn->log, XQC_LOG_INFO, "|generate sr_token[%s] for cid[%s]", + xqc_sr_token_str(params->stateless_reset_token), + xqc_scid_str(&conn->original_dcid)); + } else { params->original_dest_connection_id_present = 0; } @@ -3692,8 +4670,11 @@ xqc_conn_check_transport_params(xqc_connection_t *conn, const xqc_transport_para void xqc_conn_tls_transport_params_cb(const uint8_t *tp, size_t len, void *user_data) { - xqc_int_t ret; - xqc_transport_params_t params; + xqc_int_t ret; + xqc_transport_params_t params; + xqc_list_head_t *node; + xqc_cid_inner_t *cid_node; + xqc_cid_t *cid; xqc_connection_t *conn = (xqc_connection_t *)user_data; xqc_transport_params_type_t tp_type = (conn->conn_type == XQC_CONN_TYPE_CLIENT @@ -3715,6 +4696,14 @@ xqc_conn_tls_transport_params_cb(const uint8_t *tp, size_t len, void *user_data) return; } + /* check datagram parameter */ + if (params.max_datagram_frame_size < conn->remote_settings.max_datagram_frame_size) { + /* 0RTT: remote_settings.max_datagram_frame_size = X */ + /* 1RTT: remote_settings.max_datagram_frame_size = 0 */ + XQC_CONN_ERR(conn, TRA_0RTT_TRANS_PARAMS_ERROR); + return; + } + /* set remote transport param */ ret = xqc_conn_set_remote_transport_params(conn, ¶ms, tp_type); if (ret != XQC_OK) { @@ -3724,6 +4713,10 @@ xqc_conn_tls_transport_params_cb(const uint8_t *tp, size_t len, void *user_data) return; } + xqc_log(conn->log, XQC_LOG_DEBUG, "|1RTT_transport_params|max_datagram_frame_size:%ud|", + conn->remote_settings.max_datagram_frame_size); + + /* save no crypto flag */ if (params.no_crypto == 1) { conn->remote_settings.no_crypto = 1; @@ -3731,6 +4724,61 @@ xqc_conn_tls_transport_params_cb(const uint8_t *tp, size_t len, void *user_data) xqc_tls_set_no_crypto(conn->tls); } + /* sr token will only present in server's transport parameter, it means + client have already confirmed server's cid, associate the sr token with + server's cid */ + if (params.stateless_reset_token_present) { + /* it is supposed to be only one existing cid in the dcid set, find the + first node and copy the sr token to that cid */ + node = conn->dcid_set.cid_set.list_head.next; + if (NULL != node) { + cid_node = xqc_list_entry(node, xqc_cid_inner_t, list); + xqc_memcpy(cid_node->cid.sr_token, params.stateless_reset_token, + XQC_STATELESS_RESET_TOKENLEN); + + xqc_log(conn->log, XQC_LOG_INFO, "|store sr_token with cid: %s" + "|token:%s", xqc_dcid_str(&cid_node->cid), + xqc_sr_token_str(params.stateless_reset_token)); + + + if (xqc_insert_conns_hash(conn->engine->conns_hash_sr_token, conn, + cid_node->cid.sr_token, + XQC_STATELESS_RESET_TOKENLEN)) + { + xqc_log(conn->log, XQC_LOG_ERROR, "|insert sr conn hash error"); + } + + } else { + /* it's weired if sr token present while cid not confirmed */ + xqc_log(conn->log, XQC_LOG_ERROR, + "|cid not confirmed while sr token present"); + } + } + + if (conn->conn_type == XQC_CONN_TYPE_SERVER + && params.multipath_version != conn->local_settings.multipath_version + && xqc_conn_is_current_mp_version_supported(params.multipath_version) == XQC_OK) + { + uint8_t tp_buf[XQC_MAX_TRANSPORT_PARAM_BUF_LEN] = {0}; + size_t tp_len = 0; + conn->local_settings.multipath_version = params.multipath_version; + ret = xqc_conn_encode_local_tp(conn, tp_buf, + XQC_MAX_TRANSPORT_PARAM_BUF_LEN, &tp_len); + if (ret != XQC_OK) { + xqc_log(conn->log, XQC_LOG_ERROR, + "|cannot reset local transport parameters while multipath version is different"); + XQC_CONN_ERR(conn, TRA_INTERNAL_ERROR); + return; + } + + ret = xqc_tls_update_tp(conn->tls, tp_buf, tp_len); + if (ret != XQC_OK) { + xqc_log(conn->log, XQC_LOG_ERROR, "|server tls update transport param error|ret:%d|", ret); + XQC_CONN_ERR(conn, TRA_INTERNAL_ERROR); + return; + } + } + /* notify application layer to save transport parameter */ if (conn->transport_cbs.save_tp_cb) { char tp_buf[8192] = {0}; @@ -3845,20 +4893,9 @@ xqc_settings_copy_from_transport_params(xqc_trans_settings_t *dest, dest->active_connection_id_limit = src->active_connection_id_limit; dest->enable_multipath = src->enable_multipath; + dest->max_datagram_frame_size = src->max_datagram_frame_size; } -void -xqc_conn_update_flow_ctl_settings(xqc_connection_t *conn) -{ - xqc_conn_flow_ctl_t *flow_ctl = &conn->conn_flow_ctl; - xqc_trans_settings_t *remote_settings = &conn->remote_settings; - - flow_ctl->fc_max_data_can_send = remote_settings->max_data; - flow_ctl->fc_max_streams_bidi_can_send = remote_settings->max_streams_bidi; - flow_ctl->fc_max_streams_uni_can_send = remote_settings->max_streams_uni; -} - - xqc_int_t xqc_conn_set_early_remote_transport_params(xqc_connection_t *conn, const xqc_transport_params_t *params) @@ -3905,7 +4942,7 @@ xqc_conn_tls_keylog_cb(const char *line, void *user_data) /* invoke engine's callback */ if (eng->eng_callback.keylog_cb) { - eng->eng_callback.keylog_cb(line, eng->user_data); + eng->eng_callback.keylog_cb(&(conn->scid_set.user_scid), line, eng->user_data); } #endif } @@ -4108,7 +5145,7 @@ xqc_conn_get_max_pto(xqc_connection_t *conn) } xqc_usec_t -xqc_conn_get_min_srtt(xqc_connection_t *conn) +xqc_conn_get_min_srtt(xqc_connection_t *conn, xqc_bool_t available_only) { xqc_path_ctx_t *path = NULL; xqc_usec_t min_srtt = XQC_MAX_UINT64_VALUE; @@ -4120,12 +5157,35 @@ xqc_conn_get_min_srtt(xqc_connection_t *conn) continue; } + if (available_only && path->app_path_status != XQC_APP_PATH_STATUS_AVAILABLE) { + continue; + } + min_srtt = xqc_min(path->path_send_ctl->ctl_srtt, min_srtt); } return min_srtt; } +xqc_usec_t +xqc_conn_get_max_srtt(xqc_connection_t *conn) +{ + xqc_path_ctx_t *path = NULL; + xqc_usec_t max_rtt = 0; + + xqc_list_head_t *pos, *next; + xqc_list_for_each_safe(pos, next, &conn->conn_paths_list) { + path = xqc_list_entry(pos, xqc_path_ctx_t, path_list); + if (path->path_state != XQC_PATH_STATE_ACTIVE) { + continue; + } + + max_rtt = xqc_max(path->path_send_ctl->ctl_srtt, max_rtt); + } + + return max_rtt; +} + void xqc_conn_timer_expire(xqc_connection_t *conn, xqc_usec_t now) { @@ -4182,20 +5242,21 @@ xqc_conn_closing_notify(xqc_connection_t *conn) } } -void -xqc_conn_record_histroy_path(xqc_connection_t *conn, xqc_path_ctx_t *path) -{ - xqc_conn_path_history_t *history = conn->history_path; - - size_t index = history->path_cnt % XQC_MAX_PATHS_COUNT; - xqc_path_record_info(path, &history->path_info[index]); - history->path_cnt++; -} - xqc_int_t xqc_conn_send_path_challenge(xqc_connection_t *conn, xqc_path_ctx_t *path) { - xqc_int_t ret = XQC_OK; + xqc_int_t ret = XQC_OK; + xqc_packet_out_t *packet_out; + xqc_usec_t now; + ssize_t sent; + xqc_pn_ctl_t *pn_ctl; + + + /* send data */ + if (NULL == conn->transport_cbs.write_socket_ex) { + xqc_log(conn->log, XQC_LOG_WARN, "|write_socket_ex not registered while sending PATH_CHALLENGE"); + return XQC_ERROR; + } /* generate random data for path challenge, store it to validate path_response */ ret = xqc_generate_path_challenge_data(conn, path); @@ -4206,7 +5267,7 @@ xqc_conn_send_path_challenge(xqc_connection_t *conn, xqc_path_ctx_t *path) /* write path challenge frame & send immediately */ - xqc_packet_out_t *packet_out = xqc_write_new_packet(conn, XQC_PTYPE_SHORT_HEADER); + packet_out = xqc_write_new_packet(conn, XQC_PTYPE_SHORT_HEADER); if (packet_out == NULL) { xqc_log(conn->log, XQC_LOG_ERROR, "|xqc_write_new_packet error|"); return -XQC_EWRITE_PKT; @@ -4219,7 +5280,7 @@ xqc_conn_send_path_challenge(xqc_connection_t *conn, xqc_path_ctx_t *path) } packet_out->po_used_size += ret; - packet_out->po_is_path_specified = XQC_TRUE; + packet_out->po_path_flag |= XQC_PATH_SPECIFIED_BY_PCPR; packet_out->po_path_id = path->path_id; ret = xqc_enc_packet_with_pn(conn, path, packet_out); @@ -4229,11 +5290,10 @@ xqc_conn_send_path_challenge(xqc_connection_t *conn, xqc_path_ctx_t *path) } /* record the send time of packet */ - xqc_usec_t now = xqc_monotonic_timestamp(); + now = xqc_monotonic_timestamp(); packet_out->po_sent_time = now; - /* send data */ - ssize_t sent = conn->transport_cbs.write_socket_ex(path->path_id, conn->enc_pkt, conn->enc_pkt_len, + sent = conn->transport_cbs.write_socket_ex(path->path_id, conn->enc_pkt, conn->enc_pkt_len, (struct sockaddr *)path->rebinding_addr, path->rebinding_addrlen, xqc_conn_get_user_data(conn)); @@ -4257,7 +5317,7 @@ xqc_conn_send_path_challenge(xqc_connection_t *conn, xqc_path_ctx_t *path) xqc_log_event(conn->log, TRA_PACKET_SENT, packet_out); } - xqc_pn_ctl_t *pn_ctl = xqc_get_pn_ctl(conn, path); + pn_ctl = xqc_get_pn_ctl(conn, path); pn_ctl->ctl_packet_number[packet_out->po_pkt.pkt_pns]++; end: @@ -4321,3 +5381,242 @@ xqc_conn_unset_pkt_filter_callback(xqc_connection_t *conn) "use write_socket again"); } } + +int +xqc_conn_buff_0rtt_datagram(xqc_connection_t *conn, void *data, + size_t data_len, uint64_t dgram_id, xqc_data_qos_level_t qos_level) +{ + xqc_datagram_0rtt_buffer_t *buffer = xqc_datagram_create_0rtt_buffer(data, data_len, dgram_id, qos_level); + if (buffer == NULL) { + return -XQC_EMALLOC; + } + xqc_list_add_tail(&buffer->list, &conn->dgram_0rtt_buffer_list); + return XQC_OK; +} + +void +xqc_conn_destroy_0rtt_datagram_buffer_list(xqc_connection_t *conn) +{ + xqc_list_head_t *pos, *next; + xqc_datagram_0rtt_buffer_t *buffer; + xqc_list_for_each_safe(pos, next, &conn->dgram_0rtt_buffer_list) { + buffer = xqc_list_entry(pos, xqc_datagram_0rtt_buffer_t, list); + xqc_list_del_init(pos); + xqc_datagram_destroy_0rtt_buffer(buffer); + } +} + +xqc_ping_record_t* +xqc_conn_create_ping_record(xqc_connection_t *conn) +{ + xqc_ping_record_t *pr = xqc_calloc(1, sizeof(xqc_ping_record_t)); + xqc_init_list_head(&pr->list); + xqc_list_add_tail(&pr->list, &conn->ping_notification_list); + return pr; +} + +void +xqc_conn_destroy_ping_record(xqc_ping_record_t *pr) +{ + xqc_list_del_init(&pr->list); + xqc_free(pr); +} + +void +xqc_conn_destroy_ping_notification_list(xqc_connection_t *conn) +{ + xqc_list_head_t *pos, *next; + xqc_ping_record_t *pr; + xqc_list_for_each_safe(pos, next, &conn->ping_notification_list) { + pr = xqc_list_entry(pos, xqc_ping_record_t, list); + xqc_conn_destroy_ping_record(pr); + } +} + +xqc_bool_t +xqc_conn_should_clear_0rtt_ticket(xqc_int_t conn_err) +{ + if (conn_err == TRA_0RTT_TRANS_PARAMS_ERROR) { + return XQC_TRUE; + } + return XQC_FALSE; +} + +xqc_conn_settings_t +xqc_conn_get_conn_settings_template(xqc_conn_settings_type_t settings_type) +{ + xqc_conn_settings_t conn_settings = default_conn_settings; + + if (settings_type == XQC_CONN_SETTINGS_LOW_DELAY) { + conn_settings.ack_frequency = 1; + conn_settings.loss_detection_pkt_thresh = 2; + conn_settings.pto_backoff_factor = 1.5; + } + + return conn_settings; +} + +xqc_gp_timer_id_t +xqc_conn_register_gp_timer(xqc_connection_t *conn, char *timer_name, xqc_gp_timer_timeout_pt cb, void *user_data) +{ + return xqc_timer_register_gp_timer(&conn->conn_timer_manager, timer_name, cb, user_data); +} + +void +xqc_conn_unregister_gp_timer(xqc_connection_t *conn, xqc_gp_timer_id_t gp_timer_id) +{ + xqc_timer_unregister_gp_timer(&conn->conn_timer_manager, gp_timer_id); +} + +xqc_int_t +xqc_conn_gp_timer_set(xqc_connection_t *conn, xqc_gp_timer_id_t gp_timer_id, + xqc_usec_t expire_time) +{ + return xqc_timer_gp_timer_set(&conn->conn_timer_manager, gp_timer_id, expire_time); +} + +xqc_int_t +xqc_conn_gp_timer_unset(xqc_connection_t *conn, xqc_gp_timer_id_t gp_timer_id) +{ + return xqc_timer_gp_timer_unset(&conn->conn_timer_manager, gp_timer_id); +} + +xqc_int_t +xqc_conn_gp_timer_get_info(xqc_connection_t *conn, xqc_gp_timer_id_t gp_timer_id, + xqc_bool_t *is_set, xqc_usec_t *expire_time) +{ + return xqc_timer_gp_timer_get_info(&conn->conn_timer_manager, gp_timer_id, is_set, expire_time); +} + + +/** + * @brief get public local transport settings. + */ +xqc_conn_public_local_trans_settings_t +xqc_conn_get_public_local_trans_settings(xqc_connection_t *conn) +{ + xqc_conn_public_local_trans_settings_t settings; + settings.max_datagram_frame_size = conn->local_settings.max_datagram_frame_size; + return settings; +} + +/** + * @brief set public local transport settings + */ +void +xqc_conn_set_public_local_trans_settings(xqc_connection_t *conn, + xqc_conn_public_local_trans_settings_t *settings) +{ + if (conn == NULL || settings == NULL) { + return; + } + + if (settings->max_datagram_frame_size != conn->local_settings.max_datagram_frame_size) { + conn->local_settings.max_datagram_frame_size = settings->max_datagram_frame_size; + conn->conn_settings.max_datagram_frame_size = settings->max_datagram_frame_size; + conn->conn_flag |= XQC_CONN_FLAG_LOCAL_TP_UPDATED; + } +} + +/** + * @brief get public remote transport settings. + */ +xqc_conn_public_remote_trans_settings_t +xqc_conn_get_public_remote_trans_settings(xqc_connection_t *conn) +{ + xqc_conn_public_remote_trans_settings_t settings; + settings.max_datagram_frame_size = conn->remote_settings.max_datagram_frame_size; + return settings; +} + +/** + * @brief set public remote transport settings + */ +void +xqc_conn_set_public_remote_trans_settings(xqc_connection_t *conn, + xqc_conn_public_remote_trans_settings_t *settings) +{ + conn->remote_settings.max_datagram_frame_size = settings->max_datagram_frame_size; +} + +void +xqc_conn_reset(xqc_connection_t *conn) +{ + xqc_conn_shutdown(conn); + + /* set error code and close message, notify to application */ + conn->conn_state = XQC_CONN_STATE_DRAINING; + conn->conn_err = XQC_ESTATELESS_RESET; + XQC_CONN_CLOSE_MSG(conn, "stateless reset"); + xqc_conn_closing(conn); +} + +xqc_int_t +xqc_conn_handle_stateless_reset(xqc_connection_t *conn, + const uint8_t *sr_token) +{ + xqc_int_t ret; + int res; + xqc_list_head_t *pos, *next; + xqc_cid_inner_t *cid; + + if (NULL == conn || NULL == sr_token) { + return -XQC_EPARAM; + } + + if (conn->conn_state >= XQC_CONN_STATE_DRAINING) { + xqc_log(conn->log, XQC_LOG_INFO, "|conn closing, ignore pkt"); + return XQC_OK; + } + + /* compare received stateless reset token with the ones peer sent */ + xqc_list_for_each_safe(pos, next, &conn->dcid_set.cid_set.list_head) { + cid = xqc_list_entry(pos, xqc_cid_inner_t, list); + + res = xqc_memcmp(sr_token, cid->cid.sr_token, + XQC_STATELESS_RESET_TOKENLEN); + if (0 == res) { + xqc_log(conn->log, XQC_LOG_INFO, "|====>|receive stateless reset" + "|cid:%s", xqc_dcid_str(&cid->cid)); + xqc_log_event(conn->log, TRA_STATELESS_RESET, conn); + + /* stateless reset received, close connection */ + xqc_conn_reset(conn); + + goto end; + } + } + + /* sr_token not matched */ + return -XQC_ERROR; + +end: + return XQC_OK; +} + + +#ifdef XQC_COMPAT_GENERATE_SR_PKT + +xqc_int_t +xqc_conn_handle_deprecated_stateless_reset(xqc_connection_t *conn, + const xqc_cid_t *scid) +{ + if (NULL == conn) { + return -XQC_EPARAM; + } + + if (conn->conn_state >= XQC_CONN_STATE_DRAINING) { + xqc_log(conn->log, XQC_LOG_INFO, "|conn closing, ignore pkt"); + return XQC_OK; + } + + xqc_log(conn->log, XQC_LOG_INFO, "|====>|receive stateless reset" + "|deprecated|cid:%s", xqc_dcid_str(scid)); + + /* reset state of connection */ + xqc_conn_reset(conn); + + return XQC_OK; +} + +#endif diff --git a/src/transport/xqc_conn.h b/src/transport/xqc_conn.h index 5a53132cd..2700a8d6d 100644 --- a/src/transport/xqc_conn.h +++ b/src/transport/xqc_conn.h @@ -20,7 +20,13 @@ #include "src/transport/xqc_timer.h" #include "src/transport/xqc_multipath.h" #include "src/tls/xqc_tls.h" +#include "src/common/xqc_list.h" +#define XQC_MAX_DATAGRAM_REDUNDANCY 2 +#define XQC_MIN_DATAGRAM_REDUNDANT_PROBE_INTERVAL 30000 /* 30ms min probing interval */ +#define XQC_FC_INIT_RTT 60000 +#define XQC_MIN_RECV_WINDOW (63000) /* ~ 1MBps when RTT = 60ms */ +#define XQC_MIN_STANDBY_RPOBE_TIMEOUT 500 /* 500ms */ #define XQC_TOKEN_EXPIRE_DELTA (7 * 24 * 60 * 60) /* expire in N seconds */ #define XQC_TOKEN_UPDATE_DELTA (XQC_TOKEN_EXPIRE_DELTA / 2) /* early update */ @@ -117,7 +123,7 @@ typedef enum { XQC_CONN_FLAG_HANDSHAKE_CONFIRMED_SHIFT, XQC_CONN_FLAG_HANDSHAKE_DONE_ACKED_SHIFT, XQC_CONN_FLAG_ADDR_VALIDATED_SHIFT, - XQC_CONN_FLAG_NEW_CID_RECEIVED_SHIFT, + XQC_CONN_FLAG_NEW_CID_ACKED_SHIFT, XQC_CONN_FLAG_LINGER_CLOSING_SHIFT, XQC_CONN_FLAG_RETRY_RECVD_SHIFT, XQC_CONN_FLAG_TLS_HSK_COMPLETED_SHIFT, @@ -125,6 +131,14 @@ typedef enum { XQC_CONN_FLAG_VALIDATE_REBINDING_SHIFT, XQC_CONN_FLAG_CONN_CLOSING_NOTIFY_SHIFT, XQC_CONN_FLAG_CONN_CLOSING_NOTIFIED_SHIFT, + XQC_CONN_FLAG_DGRAM_WAIT_FOR_1RTT_SHIFT, + XQC_CONN_FLAG_LOCAL_TP_UPDATED_SHIFT, + XQC_CONN_FLAG_PMTUD_PROBING_SHIFT, + XQC_CONN_FLAG_NO_DGRAM_NOTIFIED_SHIFT, + XQC_CONN_FLAG_DGRAM_MSS_NOTIFY_SHIFT, + XQC_CONN_FLAG_MP_WAIT_SCID_SHIFT, + XQC_CONN_FLAG_MP_WAIT_DCID_SHIFT, + XQC_CONN_FLAG_MP_READY_NOTIFY_SHIFT, XQC_CONN_FLAG_SHIFT_NUM, } xqc_conn_flag_shift_t; @@ -157,7 +171,7 @@ typedef enum { XQC_CONN_FLAG_HANDSHAKE_CONFIRMED = 1ULL << XQC_CONN_FLAG_HANDSHAKE_CONFIRMED_SHIFT, XQC_CONN_FLAG_HANDSHAKE_DONE_ACKED = 1ULL << XQC_CONN_FLAG_HANDSHAKE_DONE_ACKED_SHIFT, XQC_CONN_FLAG_ADDR_VALIDATED = 1ULL << XQC_CONN_FLAG_ADDR_VALIDATED_SHIFT, - XQC_CONN_FLAG_NEW_CID_RECEIVED = 1ULL << XQC_CONN_FLAG_NEW_CID_RECEIVED_SHIFT, + XQC_CONN_FLAG_NEW_CID_ACKED = 1ULL << XQC_CONN_FLAG_NEW_CID_ACKED_SHIFT, XQC_CONN_FLAG_LINGER_CLOSING = 1ULL << XQC_CONN_FLAG_LINGER_CLOSING_SHIFT, XQC_CONN_FLAG_RETRY_RECVD = 1ULL << XQC_CONN_FLAG_RETRY_RECVD_SHIFT, XQC_CONN_FLAG_TLS_HSK_COMPLETED = 1ULL << XQC_CONN_FLAG_TLS_HSK_COMPLETED_SHIFT, @@ -165,6 +179,14 @@ typedef enum { XQC_CONN_FLAG_VALIDATE_REBINDING = 1ULL << XQC_CONN_FLAG_VALIDATE_REBINDING_SHIFT, XQC_CONN_FLAG_CLOSING_NOTIFY = 1ULL << XQC_CONN_FLAG_CONN_CLOSING_NOTIFY_SHIFT, XQC_CONN_FLAG_CLOSING_NOTIFIED = 1ULL << XQC_CONN_FLAG_CONN_CLOSING_NOTIFIED_SHIFT, + XQC_CONN_FLAG_DGRAM_WAIT_FOR_1RTT = 1ULL << XQC_CONN_FLAG_DGRAM_WAIT_FOR_1RTT_SHIFT, + XQC_CONN_FLAG_LOCAL_TP_UPDATED = 1ULL << XQC_CONN_FLAG_LOCAL_TP_UPDATED_SHIFT, + XQC_CONN_FLAG_PMTUD_PROBING = 1ULL << XQC_CONN_FLAG_PMTUD_PROBING_SHIFT, + XQC_CONN_FLAG_NO_DGRAM_NOTIFIED = 1ULL << XQC_CONN_FLAG_NO_DGRAM_NOTIFIED_SHIFT, + XQC_CONN_FLAG_DGRAM_MSS_NOTIFY = 1ULL << XQC_CONN_FLAG_DGRAM_MSS_NOTIFY_SHIFT, + XQC_CONN_FLAG_MP_WAIT_SCID = 1ULL << XQC_CONN_FLAG_MP_WAIT_SCID_SHIFT, + XQC_CONN_FLAG_MP_WAIT_DCID = 1ULL << XQC_CONN_FLAG_MP_WAIT_DCID_SHIFT, + XQC_CONN_FLAG_MP_READY_NOTIFY = 1ULL << XQC_CONN_FLAG_MP_READY_NOTIFY_SHIFT, } xqc_conn_flag_t; @@ -187,6 +209,8 @@ typedef struct { uint64_t active_connection_id_limit; uint64_t no_crypto; uint64_t enable_multipath; + xqc_multipath_version_t multipath_version; + uint16_t max_datagram_frame_size; } xqc_trans_settings_t; @@ -227,10 +251,11 @@ typedef struct { } xqc_key_update_ctx_t; -typedef struct { - xqc_path_info_t path_info[XQC_MAX_PATHS_COUNT]; - size_t path_cnt; -} xqc_conn_path_history_t; +typedef struct xqc_ping_record_s { + xqc_list_head_t list; + uint8_t notified; + uint32_t ref_cnt; +} xqc_ping_record_t; struct xqc_connection_s { @@ -299,6 +324,8 @@ struct xqc_connection_s { xqc_app_proto_callbacks_t app_proto_cbs; void *proto_data; + void *dgram_data; + xqc_list_head_t undecrypt_packet_in[XQC_ENC_LEV_MAX]; /* buffer for reordered packets */ uint32_t undecrypt_count[XQC_ENC_LEV_MAX]; @@ -331,13 +358,10 @@ struct xqc_connection_s { xqc_path_ctx_t *conn_initial_path; xqc_list_head_t conn_paths_list; uint64_t validating_path_id; - uint64_t should_ack_path_id; /* 此参数必须跟随 XQC_CONN_FLAG_SHOULD_ACK 系列标志位被设置 */ uint32_t create_path_count; uint32_t validated_path_count; uint32_t active_path_count; - uint32_t in_use_active_path_count; - uint8_t switch_initial; - + const xqc_scheduler_callback_t *scheduler_callback; void *scheduler; @@ -361,8 +385,64 @@ struct xqc_connection_s { xqc_conn_pkt_filter_callback_pt pkt_filter_cb; void *pkt_filter_cb_user_data; - /* history path */ - xqc_conn_path_history_t *history_path; + /* for datagram */ + uint64_t next_dgram_id; + xqc_list_head_t dgram_0rtt_buffer_list; + uint16_t dgram_mss; + + struct { + uint32_t total_dgram; + uint32_t hp_dgram; + uint32_t hp_red_dgram; + uint32_t hp_red_dgram_mp; + uint32_t timer_red_dgram; + } dgram_stats; + + struct { + uint64_t send_bytes; + uint64_t reinjected_bytes; + uint64_t recv_bytes; + } stream_stats; + + xqc_gp_timer_id_t dgram_probe_timer; + xqc_var_buf_t *last_dgram; + + /* min pkt_out_size across all paths */ + size_t pkt_out_size; + size_t max_pkt_out_size; + size_t probing_pkt_out_size; + uint32_t probing_cnt; + + /* pending ping notification */ + xqc_list_head_t ping_notification_list; + + /* cc blocking stats */ + uint32_t sched_cc_blocked; + uint32_t send_cc_blocked; + + /* receved pkts stats */ + struct { + xqc_pkt_type_t pkt_types[3]; + xqc_frame_type_bit_t pkt_frames[3]; + uint32_t pkt_size[3]; + uint32_t pkt_udp_size[3]; + uint64_t pkt_err[3]; + xqc_usec_t pkt_timestamp[3]; + xqc_packet_number_t pkt_pn[3]; + uint8_t curr_index; + uint32_t conn_rcvd_pkts; + uint32_t conn_udp_pkts; + } rcv_pkt_stats; + + struct { + xqc_pkt_type_t pkt_types[3]; + xqc_frame_type_bit_t pkt_frames[3]; + uint32_t pkt_size[3]; + xqc_usec_t pkt_timestamp[3]; + xqc_packet_number_t pkt_pn[3]; + uint8_t curr_index; + uint32_t conn_sent_pkts; + } snd_pkt_stats; }; const char *xqc_conn_flag_2_str(xqc_conn_flag_t conn_flag); @@ -396,7 +476,7 @@ void xqc_conn_transmit_pto_probe_packets(xqc_connection_t *conn); void xqc_conn_transmit_pto_probe_packets_batch(xqc_connection_t *conn); void xqc_conn_retransmit_lost_packets(xqc_connection_t *conn); void xqc_conn_retransmit_lost_packets_batch(xqc_connection_t *conn); -xqc_int_t xqc_path_send_ping_to_probe(xqc_path_ctx_t *path, xqc_pkt_num_space_t pns); +xqc_int_t xqc_path_send_ping_to_probe(xqc_path_ctx_t *path, xqc_pkt_num_space_t pns, xqc_path_specified_flag_t flag); void xqc_path_send_one_or_two_ack_elicit_pkts(xqc_path_ctx_t *path, xqc_pkt_num_space_t pns); void xqc_conn_send_one_ack_eliciting_pkt(xqc_connection_t *conn, xqc_pkt_num_space_t pns); @@ -470,11 +550,21 @@ xqc_conn_should_ack(xqc_connection_t *conn) } /* process an UDP datagram */ +#ifdef XQC_NO_PID_PACKET_PROCESS xqc_int_t xqc_conn_process_packet(xqc_connection_t *c, const unsigned char *packet_in_buf, size_t packet_in_size, xqc_usec_t recv_time); +#else +xqc_int_t xqc_conn_process_packet(xqc_connection_t *c, const unsigned char *packet_in_buf, + size_t packet_in_size, uint64_t path_id, xqc_usec_t recv_time); +#endif -void xqc_conn_process_packet_recved_path(xqc_connection_t *conn, xqc_cid_t *scid, +#ifdef XQC_NO_PID_PACKET_PROCESS +void xqc_conn_process_packet_recved_path(xqc_connection_t *conn, xqc_cid_t *scid, size_t packet_in_size, xqc_usec_t recv_time); +#else +void xqc_conn_process_packet_recved_path(xqc_connection_t *conn, xqc_cid_t *scid, + uint64_t path_id, size_t packet_in_size, xqc_usec_t recv_time); +#endif xqc_int_t xqc_conn_check_handshake_complete(xqc_connection_t *conn); @@ -519,8 +609,11 @@ void xqc_conn_update_stream_stats_on_sent(xqc_connection_t *conn, xqc_packet_out */ xqc_usec_t xqc_conn_get_max_pto(xqc_connection_t *conn); +void xqc_conn_ptmud_probing(xqc_connection_t *conn); + /* 用于流控 */ -xqc_usec_t xqc_conn_get_min_srtt(xqc_connection_t *conn); +xqc_usec_t xqc_conn_get_min_srtt(xqc_connection_t *conn, xqc_bool_t available_only); +xqc_usec_t xqc_conn_get_max_srtt(xqc_connection_t *conn); void xqc_conn_check_app_limit(xqc_connection_t *conn); @@ -530,15 +623,47 @@ void xqc_conn_closing(xqc_connection_t *conn); void xqc_conn_closing_notify(xqc_connection_t *conn); -void xqc_conn_record_histroy_path(xqc_connection_t *conn, xqc_path_ctx_t *path); - xqc_int_t xqc_conn_send_path_challenge(xqc_connection_t *conn, xqc_path_ctx_t *path); +int xqc_conn_buff_0rtt_datagram(xqc_connection_t *conn, void *data, size_t data_len, uint64_t dgram_id, xqc_data_qos_level_t qos_level); + +void xqc_conn_destroy_0rtt_datagram_buffer_list(xqc_connection_t *conn); +void xqc_conn_resend_0rtt_datagram(xqc_connection_t *conn); + +xqc_gp_timer_id_t xqc_conn_register_gp_timer(xqc_connection_t *conn, char *timer_name, xqc_gp_timer_timeout_pt cb, void *user_data); + +void xqc_conn_unregister_gp_timer(xqc_connection_t *conn, xqc_gp_timer_id_t gp_timer_id); + +xqc_int_t xqc_conn_gp_timer_set(xqc_connection_t *conn, xqc_gp_timer_id_t gp_timer_id, xqc_usec_t expire_time); + +xqc_int_t xqc_conn_gp_timer_unset(xqc_connection_t *conn, xqc_gp_timer_id_t gp_timer_id); + +xqc_int_t xqc_conn_gp_timer_get_info(xqc_connection_t *conn, xqc_gp_timer_id_t gp_timer_id, xqc_bool_t *is_set, xqc_usec_t *expire_time); + + void xqc_conn_schedule_packets_to_paths(xqc_connection_t *conn); static inline xqc_uint_t xqc_conn_get_mss(xqc_connection_t *conn) { - return conn->conn_settings.max_pkt_out_size + XQC_ACK_SPACE; + return conn->pkt_out_size + XQC_ACK_SPACE; } +xqc_int_t xqc_conn_handle_stateless_reset(xqc_connection_t *conn, + const uint8_t *sr_token); + +xqc_int_t xqc_conn_handle_deprecated_stateless_reset(xqc_connection_t *conn, + const xqc_cid_t *scid); + +void xqc_conn_try_to_update_mss(xqc_connection_t *conn); + +void xqc_conn_get_stats_internal(xqc_connection_t *conn, xqc_conn_stats_t *stats); + +xqc_ping_record_t* xqc_conn_create_ping_record(xqc_connection_t *conn); + +void xqc_conn_destroy_ping_record(xqc_ping_record_t *pr); + +void xqc_conn_destroy_ping_notification_list(xqc_connection_t *conn); + +xqc_int_t xqc_conn_send_ping_internal(xqc_connection_t *conn, void *ping_user_data, xqc_bool_t notify); + #endif /* _XQC_CONN_H_INCLUDED_ */ diff --git a/src/transport/xqc_datagram.c b/src/transport/xqc_datagram.c new file mode 100644 index 000000000..e39ec96d1 --- /dev/null +++ b/src/transport/xqc_datagram.c @@ -0,0 +1,491 @@ +/** + * @copyright Copyright (c) 2022, Alibaba Group Holding Limited + */ + +#include "src/transport/xqc_datagram.h" +#include "src/transport/xqc_conn.h" +#include "src/transport/xqc_packet_parser.h" +#include "src/common/xqc_log.h" +#include "src/transport/xqc_send_ctl.h" +#include "src/transport/xqc_utils.h" +#include "src/transport/xqc_engine.h" + + +xqc_datagram_0rtt_buffer_t* +xqc_datagram_create_0rtt_buffer(void *data, size_t data_len, + uint64_t dgram_id, xqc_data_qos_level_t qos_level) +{ + xqc_datagram_0rtt_buffer_t *buffer = xqc_malloc(sizeof(xqc_datagram_0rtt_buffer_t)); + if (buffer == NULL) { + return NULL; + } + + buffer->iov.iov_base = NULL; + + if (data_len > 0) { + buffer->iov.iov_base = xqc_malloc(data_len); + if (buffer->iov.iov_base == NULL) { + xqc_free(buffer); + return NULL; + } + xqc_memcpy(buffer->iov.iov_base, data, data_len); + } + + buffer->iov.iov_len = data_len; + buffer->dgram_id = dgram_id; + buffer->qos_level = qos_level; + xqc_init_list_head(&buffer->list); + return buffer; +} + +void +xqc_datagram_destroy_0rtt_buffer(xqc_datagram_0rtt_buffer_t* buffer) +{ + if (buffer) { + if (buffer->iov.iov_base) { + xqc_free(buffer->iov.iov_base); + } + xqc_free(buffer); + } +} + +void +xqc_datagram_record_mss(xqc_connection_t *conn) +{ + size_t udp_payload_limit = 0, dgram_frame_limit = 0, mtu_limit = 0; + size_t quic_header_size, headroom; + size_t old_mss = conn->dgram_mss; + + if (conn->conn_flag & XQC_CONN_FLAG_CAN_SEND_1RTT) { + quic_header_size = xqc_short_packet_header_size(conn->dcid_set.current_dcid.cid_len, XQC_PKTNO_BITS); + + } else { + if (conn->conn_type == XQC_CONN_TYPE_CLIENT) { + quic_header_size = xqc_long_packet_header_size(conn->dcid_set.current_dcid.cid_len, conn->scid_set.user_scid.cid_len, 0, XQC_PKTNO_BITS, XQC_PTYPE_0RTT); + + } else { + conn->dgram_mss = 0; + goto end; + } + } + + headroom = XQC_ACK_SPACE + XQC_TLS_AEAD_OVERHEAD_MAX_LEN + quic_header_size + XQC_DATAGRAM_HEADER_BYTES; + if (conn->remote_settings.max_udp_payload_size >= headroom) { + udp_payload_limit = conn->remote_settings.max_udp_payload_size - headroom; + + } else { + udp_payload_limit = 0; + } + + headroom = quic_header_size + XQC_DATAGRAM_HEADER_BYTES; + if (conn->pkt_out_size >= headroom) { + mtu_limit = conn->pkt_out_size - headroom; + + } else { + mtu_limit = 0; + } + + dgram_frame_limit = conn->remote_settings.max_datagram_frame_size >= XQC_DATAGRAM_HEADER_BYTES ? + conn->remote_settings.max_datagram_frame_size - XQC_DATAGRAM_HEADER_BYTES : + 0; + + conn->dgram_mss = xqc_min(xqc_min(dgram_frame_limit, udp_payload_limit), mtu_limit); +end: + if (conn->dgram_mss > old_mss) { + conn->conn_flag |= XQC_CONN_FLAG_DGRAM_MSS_NOTIFY; + + } else { + if ((conn->conn_flag & XQC_CONN_FLAG_CAN_SEND_1RTT) + && (conn->dgram_mss == 0) + && ~(conn->conn_flag & XQC_CONN_FLAG_NO_DGRAM_NOTIFIED)) + { + conn->conn_flag |= XQC_CONN_FLAG_DGRAM_MSS_NOTIFY; + conn->conn_flag |= XQC_CONN_FLAG_NO_DGRAM_NOTIFIED; + } + } + + if ((conn->conn_flag & XQC_CONN_FLAG_DGRAM_MSS_NOTIFY) + && conn->app_proto_cbs.dgram_cbs.datagram_mss_updated_notify + && (conn->conn_flag & XQC_CONN_FLAG_UPPER_CONN_EXIST) + && conn->dgram_data) + { + conn->conn_flag &= ~XQC_CONN_FLAG_DGRAM_MSS_NOTIFY; + conn->app_proto_cbs.dgram_cbs.datagram_mss_updated_notify(conn, conn->dgram_mss, conn->dgram_data); + } +} + + +/* + * @brief the API to get the max length of the data that can be sent + * via a single call of xqc_datagram_send + * + * @param conn the connection handle + * @return 0 = the peer does not support datagram, >0 = the max length + */ +size_t +xqc_datagram_get_mss(xqc_connection_t *conn) +{ + return conn->dgram_mss; +} + +/* + * @brief the API to send a datagram over the QUIC connection + * + * @param conn the connection handle + * @param data the data to be sent + * @param data_len the length of the data + * @param *dgram_id the pointer to return the id the datagram + * @return <0 = error (-XQC_EAGAIN, -XQC_CLOSING, -XQC_EDGRAM_TOO_LARGE, ...), + * 0 success + */ +xqc_int_t xqc_datagram_send(xqc_connection_t *conn, void *data, + size_t data_len, uint64_t *dgram_id, xqc_data_qos_level_t qos_level) +{ + if (conn == NULL) { + return -XQC_EPARAM; + } + + if (data == NULL && data_len != 0) { + return -XQC_EPARAM; + } + + if (conn->conn_state >= XQC_CONN_STATE_CLOSING) { + xqc_conn_log(conn, XQC_LOG_INFO, "|conn closing, cannot send datagram|size:%ud|", data_len); + return -XQC_CLOSING; + } + + if (conn->remote_settings.max_datagram_frame_size == 0) { + if (conn->conn_flag & XQC_CONN_FLAG_CAN_SEND_1RTT) { + xqc_conn_log(conn, XQC_LOG_INFO, "|does not support datagram|size:%ud|", data_len); + return -XQC_EDGRAM_NOT_SUPPORTED; + } else { + /*may receive max_datagram_frame_size later */ + xqc_log(conn->log, XQC_LOG_DEBUG, "|waiting_for_max_datagram_frame_size_from_peer|"); + conn->conn_flag |= XQC_CONN_FLAG_DGRAM_WAIT_FOR_1RTT; + return -XQC_EAGAIN; + } + } + + if (conn->dgram_mss < data_len) { + xqc_log(conn->log, XQC_LOG_INFO, "|datagram_is_too_large|data_len:%ud|", + data_len); + return -XQC_EDGRAM_TOO_LARGE; + } + + /* max_datagram_frame_size > 0 */ + + int ret; + xqc_pkt_type_t pkt_type = XQC_PTYPE_SHORT_HEADER; + int support_0rtt = xqc_conn_is_ready_to_send_early_data(conn); + uint64_t dg_id; + + if (!(conn->conn_flag & XQC_CONN_FLAG_CAN_SEND_1RTT)) { + if ((conn->conn_type == XQC_CONN_TYPE_CLIENT) + && (conn->conn_state == XQC_CONN_STATE_CLIENT_INITIAL_SENT) + && support_0rtt) + { + pkt_type = XQC_PTYPE_0RTT; + conn->conn_flag |= XQC_CONN_FLAG_HAS_0RTT; + + } else { + xqc_log(conn->log, XQC_LOG_DEBUG, "|does_not_support_0rtt_when_sending_datagram|"); + conn->conn_flag |= XQC_CONN_FLAG_DGRAM_WAIT_FOR_1RTT; + return -XQC_EAGAIN; + } + } + + if (!xqc_send_queue_can_write(conn->conn_send_queue)) { + conn->conn_send_queue->sndq_full = XQC_TRUE; + xqc_log(conn->log, XQC_LOG_DEBUG, "|too many packets used|ctl_packets_used:%ud|", conn->conn_send_queue->sndq_packets_used); + return -XQC_EAGAIN; + } + + if (pkt_type == XQC_PTYPE_0RTT && conn->zero_rtt_count >= XQC_PACKET_0RTT_MAX_COUNT) { + conn->conn_flag |= XQC_CONN_FLAG_DGRAM_WAIT_FOR_1RTT; + xqc_log(conn->log, XQC_LOG_DEBUG, "|too many 0rtt packets|zero_rtt_count:%ud|", conn->zero_rtt_count); + return -XQC_EAGAIN; + } + + xqc_conn_check_app_limit(conn); + + ret = xqc_write_datagram_frame_to_packet(conn, pkt_type, data, data_len, &dg_id, XQC_FALSE, qos_level); + + if (ret < 0) { + xqc_log(conn->log, XQC_LOG_ERROR, "|write_datagram_frame_to_packet_error|"); + XQC_CONN_ERR(conn, TRA_INTERNAL_ERROR); + return ret; + } + + /* 0RTT failure requires fallback to 1RTT, save the original send data */ + if (pkt_type == XQC_PTYPE_0RTT) { + /* buffer 0RTT packet */ + ret = xqc_conn_buff_0rtt_datagram(conn, data, data_len, dg_id, qos_level); + if (ret < 0) { + xqc_log(conn->log, XQC_LOG_ERROR, "|unable_to_buffer_0rtt_datagram_data_error|"); + XQC_CONN_ERR(conn, TRA_INTERNAL_ERROR); + return ret; + } + } + + if (dgram_id) { + *dgram_id = dg_id; + } + + if (!(conn->conn_flag & XQC_CONN_FLAG_TICKING)) { + if (0 == xqc_conns_pq_push(conn->engine->conns_active_pq, conn, conn->last_ticked_time)) { + conn->conn_flag |= XQC_CONN_FLAG_TICKING; + } + } + + if (conn->conn_settings.datagram_redundant_probe + && conn->dgram_probe_timer >= 0 + && qos_level <= XQC_DATA_QOS_NORMAL + && conn->last_dgram + && data_len <= conn->last_dgram->buf_len) + { + xqc_var_buf_clear(conn->last_dgram); + xqc_var_buf_save_data(conn->last_dgram, data, data_len); + xqc_conn_gp_timer_set(conn, conn->dgram_probe_timer, xqc_monotonic_timestamp() + conn->conn_settings.datagram_redundant_probe); + xqc_log(conn->log, XQC_LOG_DEBUG, "|start_dgram_probe_timer|data_len:%z|", data_len); + } + + /* call main logic to send packets out */ + xqc_engine_main_logic_internal(conn->engine); + + return XQC_OK; +} + +/* + * @brief the API to send a datagram over the QUIC connection + * + * @param conn the connection handle + * @param iov multiple data buffers need to be sent + * @param *dgram_id the pointer to return the list of dgram_id + * @param iov_size the size of iov list + * @param *sent_cnt the number of successfully sent datagrams + * @param *sent_bytes the total bytes of successfully sent datagrams + * @return <0 = error (-XQC_EAGAIN, -XQC_CLOSING, -XQC_EDGRAM_NOT_SUPPORTED, -XQC_EDGRAM_TOO_LARGE, ...), + * 0 success + */ +xqc_int_t +xqc_datagram_send_multiple(xqc_connection_t *conn, + struct iovec *iov, uint64_t *dgram_id_list, size_t iov_size, + size_t *sent_cnt, size_t *sent_bytes, xqc_data_qos_level_t qos_level) +{ + return xqc_datagram_send_multiple_internal(conn, iov, dgram_id_list, iov_size, sent_cnt, sent_bytes, qos_level, XQC_FALSE); +} + +xqc_int_t +xqc_datagram_send_multiple_internal(xqc_connection_t *conn, + struct iovec *iov, uint64_t *dgram_id_list, size_t iov_size, + size_t *sent_cnt, size_t *sent_bytes, xqc_data_qos_level_t qos_level, + xqc_bool_t use_supplied_dgram_id) +{ + if (sent_cnt) { + *sent_cnt = 0; + } + + if (sent_bytes) { + *sent_bytes = 0; + } + + if (conn == NULL || iov == NULL || iov_size == 0 || sent_cnt == NULL || sent_bytes == NULL) { + return -XQC_EPARAM; + } + + if (use_supplied_dgram_id && dgram_id_list == NULL) { + return -XQC_EPARAM; + } + + if (conn->conn_state >= XQC_CONN_STATE_CLOSING) { + xqc_conn_log(conn, XQC_LOG_INFO, "|conn closing, cannot send datagram|"); + return -XQC_CLOSING; + } + + if (conn->remote_settings.max_datagram_frame_size == 0) { + if (conn->conn_flag & XQC_CONN_FLAG_CAN_SEND_1RTT) { + xqc_conn_log(conn, XQC_LOG_INFO, "|does not support datagram|"); + return -XQC_EDGRAM_NOT_SUPPORTED; + } else { + /*may receive max_datagram_frame_size later */ + conn->conn_flag |= XQC_CONN_FLAG_DGRAM_WAIT_FOR_1RTT; + xqc_log(conn->log, XQC_LOG_DEBUG, "|waiting_for_max_datagram_frame_size_from_peer|"); + return -XQC_EAGAIN; + } + } + + int ret = XQC_OK; + xqc_pkt_type_t pkt_type = XQC_PTYPE_SHORT_HEADER; + int support_0rtt = xqc_conn_is_ready_to_send_early_data(conn); + + if (!(conn->conn_flag & XQC_CONN_FLAG_CAN_SEND_1RTT)) { + if ((conn->conn_type == XQC_CONN_TYPE_CLIENT) + && (conn->conn_state == XQC_CONN_STATE_CLIENT_INITIAL_SENT) + && support_0rtt) + { + pkt_type = XQC_PTYPE_0RTT; + conn->conn_flag |= XQC_CONN_FLAG_HAS_0RTT; + + } else { + conn->conn_flag |= XQC_CONN_FLAG_DGRAM_WAIT_FOR_1RTT; + xqc_log(conn->log, XQC_LOG_DEBUG, "|does_not_support_0rtt_when_sending_datagram|"); + return -XQC_EAGAIN; + } + } + + int i; + void *data; + size_t data_len; + uint8_t check_applimit = 1; + uint64_t dgram_id; + + for (i = 0; i < iov_size; *sent_cnt = ++i) { + data = iov[i].iov_base; + data_len = iov[i].iov_len; + + if (data == NULL && data_len != 0) { + ret = -XQC_EPARAM; + break; + } + + if (conn->dgram_mss < data_len) { + xqc_log(conn->log, XQC_LOG_INFO, "|datagram_is_too_large|data_len:%ud|", + data_len); + return -XQC_EDGRAM_TOO_LARGE; + } + + if (!xqc_send_queue_can_write(conn->conn_send_queue)) { + conn->conn_send_queue->sndq_full = XQC_TRUE; + xqc_log(conn->log, XQC_LOG_DEBUG, "|too many packets used|ctl_packets_used:%ud|", conn->conn_send_queue->sndq_packets_used); + ret = -XQC_EAGAIN; + break; + } + + if (pkt_type == XQC_PTYPE_0RTT && conn->zero_rtt_count >= XQC_PACKET_0RTT_MAX_COUNT) { + conn->conn_flag |= XQC_CONN_FLAG_DGRAM_WAIT_FOR_1RTT; + xqc_log(conn->log, XQC_LOG_DEBUG, "|too many 0rtt packets|zero_rtt_count:%ud|", conn->zero_rtt_count); + ret = -XQC_EAGAIN; + break; + } + + if (check_applimit) { + xqc_conn_check_app_limit(conn); + check_applimit = 0; + } + + if (use_supplied_dgram_id) { + dgram_id = dgram_id_list[i]; + } + + ret = xqc_write_datagram_frame_to_packet(conn, pkt_type, data, data_len, + &dgram_id, use_supplied_dgram_id, + qos_level); + + if (ret < 0) { + xqc_log(conn->log, XQC_LOG_ERROR, "|write_datagram_frame_to_packet_error|"); + XQC_CONN_ERR(conn, TRA_INTERNAL_ERROR); + return ret; + } + + /* 0RTT failure requires fallback to 1RTT, save the original send data */ + if (pkt_type == XQC_PTYPE_0RTT) { + /* buffer 0RTT packet */ + ret = xqc_conn_buff_0rtt_datagram(conn, data, data_len, dgram_id, qos_level); + if (ret < 0) { + xqc_log(conn->log, XQC_LOG_ERROR, "|unable_to_buffer_0rtt_datagram_data_error|"); + XQC_CONN_ERR(conn, TRA_INTERNAL_ERROR); + return ret; + } + } + + if (dgram_id_list) { + dgram_id_list[i] = dgram_id; + } + + *sent_bytes += data_len; + } + + if (*sent_cnt > 0) { + if (!(conn->conn_flag & XQC_CONN_FLAG_TICKING)) { + if (0 == xqc_conns_pq_push(conn->engine->conns_active_pq, conn, conn->last_ticked_time)) { + conn->conn_flag |= XQC_CONN_FLAG_TICKING; + } + } + + data = iov[*sent_cnt - 1].iov_base; + data_len = iov[*sent_cnt - 1].iov_len; + + if (conn->conn_settings.datagram_redundant_probe + && conn->dgram_probe_timer >= 0 + && qos_level <= XQC_DATA_QOS_NORMAL + && conn->last_dgram + && data_len <= conn->last_dgram->buf_len) + { + xqc_var_buf_clear(conn->last_dgram); + xqc_var_buf_save_data(conn->last_dgram, data, data_len); + xqc_conn_gp_timer_set(conn, conn->dgram_probe_timer, xqc_monotonic_timestamp() + conn->conn_settings.datagram_redundant_probe); + xqc_log(conn->log, XQC_LOG_DEBUG, "|start_dgram_probe_timer|data_len:%z|", data_len); + } + + /* call main logic to send packets out */ + xqc_engine_main_logic_internal(conn->engine); + } + + return ret < 0 ? ret : XQC_OK; +} +void +xqc_datagram_set_user_data(xqc_connection_t *conn, void *dgram_data) +{ + conn->dgram_data = dgram_data; + /* notify to the upper layer */ + xqc_datagram_record_mss(conn); +} + +void * +xqc_datagram_get_user_data(xqc_connection_t *conn) +{ + return conn->dgram_data; +} + +void +xqc_datagram_notify_write(xqc_connection_t *conn) +{ + if (conn->remote_settings.max_datagram_frame_size > 0 + && (conn->conn_flag & XQC_CONN_FLAG_UPPER_CONN_EXIST)) + { + if (conn->app_proto_cbs.dgram_cbs.datagram_write_notify) { + conn->app_proto_cbs.dgram_cbs.datagram_write_notify(conn, conn->dgram_data); + } + } +} + +xqc_int_t +xqc_datagram_notify_loss(xqc_connection_t *conn, xqc_packet_out_t *po) +{ + if (conn->app_proto_cbs.dgram_cbs.datagram_lost_notify + && (conn->conn_flag & XQC_CONN_FLAG_UPPER_CONN_EXIST)) + { + return conn->app_proto_cbs.dgram_cbs.datagram_lost_notify(conn, po->po_dgram_id, conn->dgram_data); + } + return 0; +} + +void +xqc_datagram_notify_ack(xqc_connection_t *conn, xqc_packet_out_t *po) +{ + /* if it is already acked, do not notify */ + if (po->po_acked + || (po->po_origin && po->po_origin->po_acked)) + { + xqc_log(conn->log, XQC_LOG_DEBUG, + "|datagram already notified|dgram_id:%ui|", po->po_dgram_id); + return; + } + if (conn->app_proto_cbs.dgram_cbs.datagram_acked_notify + && (conn->conn_flag & XQC_CONN_FLAG_UPPER_CONN_EXIST)) + { + xqc_log(conn->log, XQC_LOG_DEBUG, + "|notify datagram acked to app|dgram_id:%ui|", po->po_dgram_id); + conn->app_proto_cbs.dgram_cbs.datagram_acked_notify(conn, po->po_dgram_id, conn->dgram_data); + } +} \ No newline at end of file diff --git a/src/transport/xqc_datagram.h b/src/transport/xqc_datagram.h new file mode 100644 index 000000000..f25749791 --- /dev/null +++ b/src/transport/xqc_datagram.h @@ -0,0 +1,39 @@ +/** + * @copyright Copyright (c) 2022, Alibaba Group Holding Limited + */ + +#ifndef _XQC_DATAGRAM_H_INCLUDED_ +#define _XQC_DATAGRAM_H_INCLUDED_ + +#include +#include +#include "src/common/xqc_list.h" +#include "src/transport/xqc_conn.h" +#include "src/transport/xqc_frame_parser.h" + +typedef struct xqc_datagram_0rtt_buffer_s { + xqc_list_head_t list; + struct iovec iov; + uint64_t dgram_id; + xqc_data_qos_level_t qos_level; +} xqc_datagram_0rtt_buffer_t; + +xqc_datagram_0rtt_buffer_t* xqc_datagram_create_0rtt_buffer(void *data, + size_t data_len, uint64_t dgram_id, xqc_data_qos_level_t qos_level); + +void xqc_datagram_destroy_0rtt_buffer(xqc_datagram_0rtt_buffer_t* buffer); + +void xqc_datagram_record_mss(xqc_connection_t *conn); + +void xqc_datagram_notify_write(xqc_connection_t *conn); + +xqc_int_t xqc_datagram_notify_loss(xqc_connection_t *conn, xqc_packet_out_t *po); + +void xqc_datagram_notify_ack(xqc_connection_t *conn, xqc_packet_out_t *po); + +xqc_int_t xqc_datagram_send_multiple_internal(xqc_connection_t *conn, + struct iovec *iov, uint64_t *dgram_id_list, size_t iov_size, + size_t *sent_cnt, size_t *sent_bytes, xqc_data_qos_level_t qos_level, + xqc_bool_t use_supplied_dgram_id); + +#endif \ No newline at end of file diff --git a/src/transport/xqc_defs.h b/src/transport/xqc_defs.h index 71a275d16..ef36e09af 100644 --- a/src/transport/xqc_defs.h +++ b/src/transport/xqc_defs.h @@ -38,8 +38,14 @@ #define XQC_CONN_MAX_CRYPTO_DATA_TOTAL_LEN (10*1024*1024) -/* length of stateless reset token */ -#define XQC_STATELESS_RESET_TOKENLEN 16 + +/* xquic will not send stateless reset to packets which are smaller than + XQC_STATELESS_RESET_PKT_MIN_LEN */ +#define XQC_STATELESS_RESET_PKT_MIN_LEN 21 +#define XQC_STATELESS_RESET_PKT_MAX_LEN 43 + +/* xquic will */ +#define XQC_STATELESS_RESET_PKT_SUBTRAHEND 2 /* max token length supported by xquic */ #define XQC_MAX_TOKEN_LEN 256 diff --git a/src/transport/xqc_engine.c b/src/transport/xqc_engine.c index 5616cb757..5c138cc14 100644 --- a/src/transport/xqc_engine.c +++ b/src/transport/xqc_engine.c @@ -21,8 +21,12 @@ #include "src/transport/xqc_wakeup_pq.h" #include "src/transport/xqc_utils.h" #include "src/transport/xqc_timer.h" +#include "src/transport/xqc_datagram.h" #include "src/http3/xqc_h3_conn.h" #include "src/tls/xqc_tls.h" +#include "src/transport/xqc_datagram.h" +#include "src/transport/xqc_reinjection.h" +#include "src/transport/xqc_packet_out.h" extern const xqc_qpack_ins_cb_t xqc_h3_qpack_ins_cb; @@ -44,6 +48,7 @@ xqc_config_t default_client_config = { .reset_token_key = {0}, .reset_token_keylen = 0, .sendmmsg_on = 0, + .enable_h3_ext = 0, }; @@ -64,6 +69,7 @@ xqc_config_t default_server_config = { .reset_token_key = {0}, .reset_token_keylen = 0, .sendmmsg_on = 0, + .enable_h3_ext = 0, }; @@ -125,6 +131,7 @@ xqc_set_config(xqc_config_t *dst, const xqc_config_t *src) dst->cfg_log_timestamp = src->cfg_log_timestamp; dst->cfg_log_level_name = src->cfg_log_level_name; dst->sendmmsg_on = src->sendmmsg_on; + dst->enable_h3_ext = src->enable_h3_ext; return XQC_OK; } @@ -444,11 +451,17 @@ xqc_engine_create(xqc_engine_type_t engine_type, if (engine->conns_hash == NULL) { goto fail; } + engine->conns_hash_dcid = xqc_engine_conns_hash_create(engine->config); if (engine->conns_hash_dcid == NULL) { goto fail; } + engine->conns_hash_sr_token = xqc_engine_conns_hash_create(engine->config); + if (engine->conns_hash_sr_token == NULL) { + goto fail; + } + engine->conns_active_pq = xqc_engine_conns_pq_create(engine->config); if (engine->conns_active_pq == NULL) { goto fail; @@ -589,6 +602,11 @@ xqc_engine_destroy(xqc_engine_t *engine) engine->conns_hash_dcid = NULL; } + if (engine->conns_hash_sr_token) { + xqc_engine_conns_hash_destroy(engine->conns_hash_sr_token); + engine->conns_hash_sr_token = NULL; + } + if (engine->tls_ctx) { xqc_tls_ctx_destroy(engine->tls_ctx); } @@ -604,17 +622,30 @@ xqc_engine_destroy(xqc_engine_t *engine) xqc_int_t xqc_engine_send_reset(xqc_engine_t *engine, xqc_cid_t *dcid, const struct sockaddr *peer_addr, socklen_t peer_addrlen, - const struct sockaddr *local_addr, socklen_t local_addrlen, void *user_data) + const struct sockaddr *local_addr, socklen_t local_addrlen, + size_t input_pkt_size, void *user_data) { - unsigned char buf[XQC_PACKET_OUT_SIZE]; - xqc_int_t size = xqc_gen_reset_packet(dcid, buf, - engine->config->reset_token_key, - engine->config->reset_token_keylen); + unsigned char buf[XQC_PACKET_OUT_BUF_CAP]; + xqc_int_t size; + size_t max_sr_pkt_len; + xqc_stateless_reset_pt stateless_cb; + + max_sr_pkt_len = input_pkt_size - XQC_STATELESS_RESET_PKT_SUBTRAHEND; + if (max_sr_pkt_len < XQC_STATELESS_RESET_PKT_MIN_LEN) { + /* XQUIC will not send SR to a packet smaller than 21 bytes to avoid + Stateless Reset Looping */ + return XQC_OK; + } + + max_sr_pkt_len = xqc_min(max_sr_pkt_len, XQC_STATELESS_RESET_PKT_MAX_LEN); + size = xqc_gen_reset_packet(dcid, buf, engine->config->reset_token_key, + engine->config->reset_token_keylen, + max_sr_pkt_len, engine->rand_generator); if (size < 0) { return size; } - xqc_stateless_reset_pt stateless_cb = engine->transport_cbs.stateless_reset; + stateless_cb = engine->transport_cbs.stateless_reset; if (stateless_cb) { size = (xqc_int_t)stateless_cb(buf, (size_t)size, peer_addr, peer_addrlen, local_addr, local_addrlen, user_data); @@ -650,6 +681,7 @@ xqc_engine_process_conn(xqc_connection_t *conn, xqc_usec_t now) conn, xqc_conn_state_2_str(conn->conn_state), xqc_conn_flag_2_str(conn->conn_flag), now); int ret; + xqc_bool_t wait_scid, wait_dcid; xqc_conn_timer_expire(conn, now); @@ -692,7 +724,21 @@ xqc_engine_process_conn(xqc_connection_t *conn, xqc_usec_t now) if (conn->conn_flag & XQC_CONN_FLAG_CAN_SEND_1RTT) { xqc_process_read_streams(conn); if (xqc_send_queue_can_write(conn->conn_send_queue)) { - xqc_process_write_streams(conn); + if (conn->conn_send_queue->sndq_full) { + if (xqc_send_queue_release_enough_space(conn->conn_send_queue)) { + conn->conn_send_queue->sndq_full = XQC_FALSE; + xqc_process_write_streams(conn); + xqc_datagram_notify_write(conn); + } + + } else { + xqc_process_write_streams(conn); + if (conn->conn_flag & XQC_CONN_FLAG_DGRAM_WAIT_FOR_1RTT) { + xqc_datagram_notify_write(conn); + conn->conn_flag &= ~XQC_CONN_FLAG_DGRAM_WAIT_FOR_1RTT; + } + } + } else { xqc_log(conn->log, XQC_LOG_DEBUG, "|xqc_send_queue_can_write false|"); } @@ -700,19 +746,10 @@ xqc_engine_process_conn(xqc_connection_t *conn, xqc_usec_t now) XQC_CHECK_IMMEDIATE_CLOSE(); if (xqc_conn_should_ack(conn)) { - if (conn->enable_multipath == XQC_CONN_MULTIPATH_MULTIPLE_PNS) { - ret = xqc_write_ack_mp_to_packets(conn); - if (ret) { - xqc_log(conn->log, XQC_LOG_ERROR, "|xqc_write_ack_mp_to_packets error|"); - XQC_CONN_ERR(conn, TRA_INTERNAL_ERROR); - } - - } else { - ret = xqc_write_ack_to_packets(conn); - if (ret) { - xqc_log(conn->log, XQC_LOG_ERROR, "|xqc_write_ack_to_packets error|"); - XQC_CONN_ERR(conn, TRA_INTERNAL_ERROR); - } + ret = xqc_write_ack_or_mp_ack_to_packets(conn); + if (ret) { + xqc_log(conn->log, XQC_LOG_ERROR, "|xqc_write_ack_or_mp_ack_to_packets error|"); + XQC_CONN_ERR(conn, TRA_INTERNAL_ERROR); } } XQC_CHECK_IMMEDIATE_CLOSE(); @@ -724,19 +761,37 @@ xqc_engine_process_conn(xqc_connection_t *conn, xqc_usec_t now) } #endif + if (conn->enable_multipath) { + + if (conn->conn_flag & XQC_CONN_FLAG_MP_WAIT_SCID) { + if (conn->conn_flag & XQC_CONN_FLAG_NEW_CID_ACKED) { + conn->conn_flag &= ~XQC_CONN_FLAG_MP_WAIT_SCID; + conn->conn_flag &= ~XQC_CONN_FLAG_NEW_CID_ACKED; + conn->conn_flag |= XQC_CONN_FLAG_MP_READY_NOTIFY; + } + } + + if (conn->conn_flag & XQC_CONN_FLAG_MP_WAIT_DCID + && !(conn->conn_flag & XQC_CONN_FLAG_MP_WAIT_SCID)) + { + conn->conn_flag &= ~XQC_CONN_FLAG_MP_WAIT_DCID; + conn->conn_flag |= XQC_CONN_FLAG_MP_READY_NOTIFY; + } + } + /* for multi-path */ - if ((conn->conn_flag & XQC_CONN_FLAG_NEW_CID_RECEIVED) + if ((conn->conn_flag & XQC_CONN_FLAG_MP_READY_NOTIFY) && xqc_conn_check_unused_cids(conn) == XQC_OK) { if (conn->transport_cbs.ready_to_create_path_notify) { conn->transport_cbs.ready_to_create_path_notify(&conn->scid_set.user_scid, xqc_conn_get_user_data(conn)); } - conn->conn_flag &= ~XQC_CONN_FLAG_NEW_CID_RECEIVED; + conn->conn_flag &= ~XQC_CONN_FLAG_MP_READY_NOTIFY; } if (XQC_UNLIKELY(conn->conn_flag & XQC_CONN_FLAG_PING)) { - ret = xqc_write_ping_to_packet(conn, NULL, XQC_FALSE); + ret = xqc_conn_send_ping_internal(conn, NULL, XQC_FALSE); if (ret) { xqc_log(conn->log, XQC_LOG_ERROR, "|xqc_write_ping_to_packet error|"); XQC_CONN_ERR(conn, TRA_INTERNAL_ERROR); @@ -752,6 +807,11 @@ xqc_engine_process_conn(xqc_connection_t *conn, xqc_usec_t now) } } + /* PMTUD probing */ + if (XQC_UNLIKELY(conn->conn_flag & XQC_CONN_FLAG_PMTUD_PROBING)) { + xqc_conn_ptmud_probing(conn); + } + end: conn->packet_need_process_count = 0; conn->conn_flag &= ~XQC_CONN_FLAG_NEED_RUN; @@ -787,7 +847,7 @@ xqc_engine_main_logic(xqc_engine_t *engine) } engine->eng_flag |= XQC_ENG_FLAG_RUNNING; - xqc_log(engine->log, XQC_LOG_DEBUG, "|"); + xqc_log(engine->log, XQC_LOG_DEBUG, "|BEGIN|"); xqc_usec_t now = xqc_monotonic_timestamp(); xqc_connection_t *conn; @@ -863,6 +923,11 @@ xqc_engine_main_logic(xqc_engine_t *engine) xqc_conn_send_packets(conn); } + if (conn->conn_settings.mp_enable_reinjection & XQC_REINJ_UNACK_AFTER_SEND) { + xqc_conn_reinject_unack_packets(conn, XQC_REINJ_UNACK_AFTER_SEND); + xqc_conn_send_packets(conn); + } + if (XQC_UNLIKELY(conn->conn_state == XQC_CONN_STATE_CLOSED)) { conn->conn_flag &= ~XQC_CONN_FLAG_TICKING; if (!(engine->eng_flag & XQC_ENG_FLAG_NO_DESTROY)) { @@ -928,19 +993,137 @@ xqc_engine_main_logic(xqc_engine_t *engine) } engine->eng_flag &= ~XQC_ENG_FLAG_RUNNING; + + xqc_log(engine->log, XQC_LOG_DEBUG, "|END|"); return; } + +xqc_int_t +xqc_engine_handle_stateless_reset(xqc_engine_t *engine, + const unsigned char *buf, size_t buf_size, xqc_usec_t recv_time, + xqc_connection_t **c) +{ + xqc_int_t ret; + const uint8_t *sr_token; + xqc_connection_t *conn; + uint64_t hash; + xqc_str_t str; + + ret = -XQC_ERROR; + + /* parse stateless reset token from packet */ + sr_token = NULL; + ret = xqc_packet_parse_stateless_reset(buf, buf_size, &sr_token); + if (XQC_OK != ret) { + xqc_log(engine->log, XQC_LOG_DEBUG, "|not a stateless reset pkt"); + return ret; + } + + if (NULL == sr_token) { + return -XQC_ERROR; + } + + hash = xqc_hash_string(sr_token, XQC_STATELESS_RESET_TOKENLEN); + str.data = (unsigned char *)sr_token; + str.len = XQC_STATELESS_RESET_TOKENLEN; + + /* try to find connection with sr_token */ + conn = xqc_str_hash_find(engine->conns_hash_sr_token, hash, str); + if (NULL == conn) { + /* can't find connection with sr_token */ + xqc_log(engine->log, XQC_LOG_DEBUG, "|can't find conn with sr|sr:%s", + xqc_sr_token_str(sr_token)); + return -XQC_ERROR; + } + + *c = conn; + ret = xqc_conn_handle_stateless_reset(conn, sr_token); + if (XQC_OK != ret) { + /* sr_token state not match between engine and connection */ + xqc_log(conn->log, XQC_LOG_ERROR, "|sr token state mismatch|"); + return -XQC_ESTATE; + } + + return XQC_OK; +} + + +#ifdef XQC_COMPAT_GENERATE_SR_PKT +xqc_int_t +xqc_engine_handle_deprecated_stateless_reset(xqc_engine_t *engine, + const unsigned char *buf, size_t buf_size, const xqc_cid_t *scid, + xqc_usec_t recv_time, xqc_connection_t **c) +{ + xqc_connection_t *conn; + xqc_int_t ret; + + /* compat with the original stateless reset mechanism */ + if (!xqc_is_deprecated_reset_packet((xqc_cid_t *)scid, buf, buf_size, + engine->config->reset_token_key, + engine->config->reset_token_keylen)) + { + return -XQC_ERROR; + } + + /* reset is associated with peer's cid */ + conn = xqc_engine_conns_hash_find(engine, scid, 'd'); + if (NULL == conn) { + return -XQC_ERROR; + } + + *c = conn; + ret = xqc_conn_handle_deprecated_stateless_reset(conn, scid); + + return ret; +} +#endif + +xqc_int_t +xqc_engine_process_sr_pkt(xqc_engine_t *engine, const unsigned char *buf, + size_t buf_size, const xqc_cid_t *cid, xqc_usec_t recv_time, + xqc_connection_t **c) +{ + xqc_int_t ret; + + /* try handle the unknown packet as standard Stateless Reset */ + ret = xqc_engine_handle_stateless_reset(engine, buf, buf_size, + recv_time, c); + if (XQC_OK == ret) { + return XQC_OK; + } + +#ifdef XQC_COMPAT_GENERATE_SR_PKT + /* if not a standard Stateless Reset packet */ + ret = xqc_engine_handle_deprecated_stateless_reset(engine, buf, buf_size, + cid, recv_time, c); + if (XQC_OK == ret) { + return XQC_OK; + } +#endif + + return ret; +} + /** * Pass received UDP packet payload into xquic engine. * @param recv_time UDP packet received time in microsecond */ +#ifdef XQC_NO_PID_PACKET_PROCESS xqc_int_t xqc_engine_packet_process(xqc_engine_t *engine, const unsigned char *packet_in_buf, size_t packet_in_size, const struct sockaddr *local_addr, socklen_t local_addrlen, const struct sockaddr *peer_addr, socklen_t peer_addrlen, xqc_usec_t recv_time, void *user_data) +#else +xqc_int_t +xqc_engine_packet_process(xqc_engine_t *engine, + const unsigned char *packet_in_buf, size_t packet_in_size, + const struct sockaddr *local_addr, socklen_t local_addrlen, + const struct sockaddr *peer_addr, socklen_t peer_addrlen, + uint64_t path_id, xqc_usec_t recv_time, void *user_data) +#endif { xqc_int_t ret; xqc_connection_t *conn = NULL; @@ -958,60 +1141,56 @@ xqc_engine_packet_process(xqc_engine_t *engine, conn = xqc_engine_conns_hash_find(engine, &scid, 's'); - /* server creates connection when receiving a initial packet*/ - if (XQC_UNLIKELY(conn == NULL - && engine->eng_type == XQC_ENGINE_SERVER - && XQC_PACKET_IS_LONG_HEADER(packet_in_buf) - && (XQC_PACKET_LONG_HEADER_GET_TYPE(packet_in_buf) == XQC_PTYPE_INIT - || XQC_PACKET_LONG_HEADER_GET_TYPE(packet_in_buf) == XQC_PTYPE_0RTT) - && (local_addr != NULL && peer_addr != NULL))) - { - conn = xqc_conn_server_create(engine, local_addr, local_addrlen, - peer_addr, peer_addrlen, &dcid, &scid, - &default_conn_settings, user_data); - if (conn == NULL) { - xqc_log(engine->log, XQC_LOG_ERROR, "|fail to create connection|"); - return -XQC_ECREATE_CONN; - } - } - + /* can't find a connection by the cid from the packet */ if (XQC_UNLIKELY(conn == NULL)) { - if (!xqc_is_reset_packet(&scid, packet_in_buf, packet_in_size, - engine->config->reset_token_key, - engine->config->reset_token_keylen)) - { - if (xqc_engine_schedule_reset(engine, peer_addr, peer_addrlen, recv_time) != XQC_OK) { - return -XQC_ECONN_NFOUND; - } - xqc_log(engine->log, XQC_LOG_STATS, "|fail to find connection, send reset|size:%uz|scid:%s|", - packet_in_size, xqc_scid_str(&scid)); - ret = xqc_engine_send_reset(engine, &scid, peer_addr, peer_addrlen, - local_addr, local_addrlen, user_data); - if (ret) { - xqc_log(engine->log, XQC_LOG_ERROR, "|fail to send reset|"); + + if (XQC_PACKET_IS_LONG_HEADER(packet_in_buf)) { + /* server creates connection when receiving a initial/0-rtt packet */ + if (engine->eng_type == XQC_ENGINE_SERVER + && (XQC_PACKET_LONG_HEADER_GET_TYPE(packet_in_buf) == XQC_PTYPE_INIT + || XQC_PACKET_LONG_HEADER_GET_TYPE(packet_in_buf) == XQC_PTYPE_0RTT) + && (local_addr != NULL && peer_addr != NULL)) + { + conn = xqc_conn_server_create(engine, local_addr, local_addrlen, + peer_addr, peer_addrlen, &dcid, &scid, + &default_conn_settings, user_data); + if (conn == NULL) { + xqc_log(engine->log, XQC_LOG_ERROR, "|fail to create connection|"); + return -XQC_ECREATE_CONN; + } } } else { - /* reset is associated with peer's cid */ - conn = xqc_engine_conns_hash_find(engine, &scid, 'd'); - if (conn) { - xqc_log(engine->log, XQC_LOG_WARN, "|====>|receive reset, enter draining|size:%uz|scid:%s|state:%s|flags:%s", - packet_in_size, xqc_scid_str(&scid), xqc_conn_state_2_str(conn->conn_state), xqc_conn_flag_2_str(conn->conn_flag)); - if (conn->conn_state < XQC_CONN_STATE_DRAINING) { - conn->conn_state = XQC_CONN_STATE_DRAINING; - conn->conn_err = XQC_ESTATELESS_RESET; /* remember reset */ - xqc_conn_closing(conn); - xqc_send_queue_drop_packets(conn); - xqc_usec_t pto = xqc_conn_get_max_pto(conn); - if (!xqc_timer_is_set(&conn->conn_timer_manager, XQC_TIMER_CONN_DRAINING)) { - xqc_timer_set(&conn->conn_timer_manager, XQC_TIMER_CONN_DRAINING, recv_time, 3 * pto); - } - } + /* stateless reset is pretended to be a short header packet */ + ret = xqc_engine_process_sr_pkt(engine, packet_in_buf, + packet_in_size, &scid, recv_time, + &conn); + if (ret == XQC_OK && NULL != conn) { + /* SR processed */ goto after_process; } - xqc_log(engine->log, XQC_LOG_WARN, "|fail to find connection, exit|size:%uz|scid:%s|", - packet_in_size, xqc_scid_str(&scid)); + + xqc_log(engine->log, XQC_LOG_DEBUG, "|not a stateless reset pkt, " + "will try send stateless reset pkt"); + } + } + + /* can't find a conneciton, send stateless reset */ + if (NULL == conn) { + if (xqc_engine_schedule_reset(engine, peer_addr, peer_addrlen, recv_time) != XQC_OK) { + return -XQC_ECONN_NFOUND; } + + xqc_log(engine->log, XQC_LOG_STATS, "|fail to find connection, send " + "reset|size:%uz|scid:%s|", packet_in_size, xqc_scid_str(&scid)); + + ret = xqc_engine_send_reset(engine, &scid, peer_addr, peer_addrlen, + local_addr, local_addrlen, packet_in_size, + user_data); + if (ret) { + xqc_log(engine->log, XQC_LOG_ERROR, "|fail to send reset|"); + } + return -XQC_ECONN_NFOUND; } @@ -1021,8 +1200,15 @@ xqc_engine_packet_process(xqc_engine_t *engine, conn, packet_in_size, xqc_conn_state_2_str(conn->conn_state), recv_time); if (XQC_UNLIKELY(conn->local_addrlen == 0)) { - xqc_memcpy(conn->local_addr, local_addr, local_addrlen); - conn->local_addrlen = local_addrlen; + ret = xqc_memcpy_with_cap(conn->local_addr, sizeof(conn->local_addr), + local_addr, local_addrlen); + if (ret == XQC_OK) { + conn->local_addrlen = local_addrlen; + + } else { + xqc_log(conn->log, XQC_LOG_ERROR, + "|local addr too large|addr_len:%d|", (int)local_addrlen); + } xqc_log_event(conn->log, CON_CONNECTION_STARTED, conn, XQC_LOG_LOCAL_EVENT); } @@ -1038,12 +1224,19 @@ xqc_engine_packet_process(xqc_engine_t *engine, && !xqc_timer_is_set(&path->path_send_ctl->path_timer_manager, XQC_TIMER_NAT_REBINDING)) { /* set rebinding_addr & send PATH_CHALLENGE */ - xqc_memcpy(path->rebinding_addr, peer_addr, peer_addrlen); - path->rebinding_addrlen = peer_addrlen; + ret = xqc_memcpy_with_cap(path->rebinding_addr, sizeof(path->rebinding_addr), + peer_addr, peer_addrlen); + if (ret == XQC_OK) { + path->rebinding_addrlen = peer_addrlen; + } else { + xqc_log(conn->log, XQC_LOG_ERROR, + "|REBINDING|peer addr too large|addr_len:%d|", (int)peer_addrlen); + } + ret = xqc_conn_send_path_challenge(conn, path); if (ret == XQC_OK) { - xqc_log(conn->log, XQC_LOG_INFO, "|REBINDING|path:%ui|send PATH_CHALLENGE|", path->path_id); + xqc_log(conn->log, XQC_LOG_INFO, "|REBINDING|path:%ui|send PATH_CHALLENGE|addr:%s|", path->path_id, xqc_path_addr_str(path)); path->rebinding_count++; xqc_usec_t pto = xqc_conn_get_max_pto(conn); xqc_timer_set(&path->path_send_ctl->path_timer_manager, @@ -1066,7 +1259,14 @@ xqc_engine_packet_process(xqc_engine_t *engine, } /* process packets */ +#ifdef XQC_NO_PID_PACKET_PROCESS ret = xqc_conn_process_packet(conn, packet_in_buf, packet_in_size, recv_time); +#else + ret = xqc_conn_process_packet(conn, packet_in_buf, packet_in_size, path_id, recv_time); +#endif + + conn->rcv_pkt_stats.conn_udp_pkts++; + if (ret) { xqc_log(engine->log, XQC_LOG_ERROR, "|fail to process packets|conn:%p|ret:%d|", conn, ret); XQC_CONN_ERR(conn, TRA_FRAME_ENCODING_ERROR); @@ -1090,8 +1290,11 @@ xqc_engine_packet_process(xqc_engine_t *engine, goto after_process; } } - +#ifdef XQC_NO_PID_PACKET_PROCESS xqc_conn_process_packet_recved_path(conn, &scid, packet_in_size, recv_time); +#else + xqc_conn_process_packet_recved_path(conn, &scid, path_id, packet_in_size, recv_time); +#endif xqc_timer_set(&conn->conn_timer_manager, XQC_TIMER_CONN_IDLE, recv_time, xqc_conn_get_idle_timeout(conn) * 1000); @@ -1124,6 +1327,12 @@ xqc_engine_packet_process(xqc_engine_t *engine, } + + + + + + uint8_t xqc_engine_config_get_cid_len(xqc_engine_t *engine) { diff --git a/src/transport/xqc_engine.h b/src/transport/xqc_engine.h index 1226c8973..6f1a6b974 100644 --- a/src/transport/xqc_engine.h +++ b/src/transport/xqc_engine.h @@ -46,6 +46,7 @@ typedef struct xqc_engine_s { xqc_config_t *config; xqc_str_hash_table_t *conns_hash; /* scid */ xqc_str_hash_table_t *conns_hash_dcid; /* For reset packet */ + xqc_str_hash_table_t *conns_hash_sr_token; /* For stateless reset */ xqc_pq_t *conns_active_pq; /* In process */ xqc_wakeup_pq_t *conns_wait_wakeup_pq; /* Need wakeup after next tick time */ uint8_t reset_sent_cnt[XQC_RESET_CNT_ARRAY_LEN]; /* remote addr hash */ diff --git a/src/transport/xqc_frame.c b/src/transport/xqc_frame.c index b5cf53a18..d3a41fd80 100644 --- a/src/transport/xqc_frame.c +++ b/src/transport/xqc_frame.c @@ -45,6 +45,7 @@ static const char * const frame_type_2_str[XQC_FRAME_NUM] = { [XQC_FRAME_ACK_MP] = "ACK_MP", [XQC_FRAME_PATH_ABANDON] = "PATH_ABANDON", [XQC_FRAME_PATH_STATUS] = "PATH_STATUS", + [XQC_FRAME_DATAGRAM] = "DATAGRAM", [XQC_FRAME_Extension] = "Extension", }; @@ -94,6 +95,7 @@ xqc_insert_stream_frame(xqc_connection_t *conn, xqc_stream_t *stream, xqc_stream unsigned char inserted = 0; xqc_list_head_t *pos; xqc_stream_frame_t *frame; + xqc_list_for_each_reverse(pos, &stream->stream_data_in.frames_tailq) { frame = xqc_list_entry(pos, xqc_stream_frame_t, sf_list); @@ -148,10 +150,9 @@ xqc_insert_stream_frame(xqc_connection_t *conn, xqc_stream_t *stream, xqc_stream pos = new_frame->sf_list.next; xqc_list_for_each_from(pos, &stream->stream_data_in.frames_tailq) { frame = xqc_list_entry(pos, xqc_stream_frame_t, sf_list); - if (stream->stream_data_in.merged_offset_end >= frame->data_offset - && stream->stream_data_in.merged_offset_end < frame->data_offset + frame->data_length) - { - stream->stream_data_in.merged_offset_end = frame->data_offset + frame->data_length; + if (stream->stream_data_in.merged_offset_end >= frame->data_offset) { + stream->stream_data_in.merged_offset_end = xqc_max(frame->data_offset + frame->data_length, + stream->stream_data_in.merged_offset_end); xqc_log(conn->log, XQC_LOG_DEBUG, "|merge right|merged_offset_end:%ui|offset:%ui|len:%ud|", stream->stream_data_in.merged_offset_end, frame->data_offset, frame->data_length); } else { @@ -264,14 +265,59 @@ xqc_process_frames(xqc_connection_t *conn, xqc_packet_in_t *packet_in) case 0x1e: ret = xqc_process_handshake_done_frame(conn, packet_in); break; + case 0x30 ... 0x31: + ret = xqc_process_datagram_frame(conn, packet_in); + break; case 0xbaba00 ... 0xbaba01: - ret = xqc_process_ack_mp_frame(conn, packet_in); + if (conn->conn_settings.multipath_version == XQC_MULTIPATH_04) { + ret = xqc_process_ack_mp_frame(conn, packet_in); + } else { + xqc_log(conn->log, XQC_LOG_ERROR, "|receive wrong mp version mp_ack frame or cannot process frame in mp version 04|"); + ret = -XQC_EMP_INVALID_MP_VERTION; + } + break; + case 0x15228c00 ... 0x15228c01: + if (conn->conn_settings.multipath_version == XQC_MULTIPATH_05) { + ret = xqc_process_ack_mp_frame(conn, packet_in); + + } else { + xqc_log(conn->log, XQC_LOG_ERROR, "|receive wrong mp version mp_ack frame or cannot process frame in mp version 05|"); + ret = -XQC_EMP_INVALID_MP_VERTION; + } break; case 0xbaba05: - ret = xqc_process_path_abandon_frame(conn, packet_in); + if (conn->conn_settings.multipath_version == XQC_MULTIPATH_04) { + ret = xqc_process_path_abandon_frame(conn, packet_in); + } else { + xqc_log(conn->log, XQC_LOG_ERROR, "|receive wrong mp version path_abandon frame or cannot process frame in mp version 04|"); + ret = -XQC_EMP_INVALID_MP_VERTION; + } + break; + case 0x15228c05: + if (conn->conn_settings.multipath_version == XQC_MULTIPATH_05) { + ret = xqc_process_path_abandon_frame(conn, packet_in); + + } else { + xqc_log(conn->log, XQC_LOG_ERROR, "|receive wrong mp version path_abandon frame or cannot process frame in mp version 05|"); + ret = -XQC_EMP_INVALID_MP_VERTION; + } break; case 0xbaba06: - ret = xqc_process_path_status_frame(conn, packet_in); + if (conn->conn_settings.multipath_version == XQC_MULTIPATH_04) { + ret = xqc_process_path_status_frame(conn, packet_in); + } else { + xqc_log(conn->log, XQC_LOG_ERROR, "|receive wrong mp version path_status frame or cannot process frame in mp version 04|"); + ret = -XQC_EMP_INVALID_MP_VERTION; + } + break; + case 0x15228c06: + if (conn->conn_settings.multipath_version == XQC_MULTIPATH_05) { + ret = xqc_process_path_status_frame(conn, packet_in); + + } else { + xqc_log(conn->log, XQC_LOG_ERROR, "|receive wrong mp version path_status frame or cannot process frame in mp version 05|"); + ret = -XQC_EMP_INVALID_MP_VERTION; + } break; default: xqc_log(conn->log, XQC_LOG_ERROR, "|unknown frame type|"); @@ -288,6 +334,17 @@ xqc_process_frames(xqc_connection_t *conn, xqc_packet_in_t *packet_in) return -XQC_ESYS; } } + + xqc_path_ctx_t *path = xqc_conn_find_path_by_path_id(conn, packet_in->pi_path_id); + if (path != NULL + && (packet_in->pi_frame_types & XQC_FRAME_BIT_DATAGRAM)) + { + path->path_send_ctl->ctl_dgram_recv_count++; + if (packet_in->pi_flag & XQC_PIF_REINJECTED_REPLICA) { + path->path_send_ctl->ctl_reinj_dgram_recv_count++; + } + } + return XQC_OK; } @@ -310,12 +367,13 @@ xqc_process_padding_frame(xqc_connection_t *conn, xqc_packet_in_t *packet_in) xqc_int_t xqc_process_stream_frame(xqc_connection_t *conn, xqc_packet_in_t *packet_in) { - xqc_int_t ret = 0; + xqc_int_t ret; + xqc_stream_id_t stream_id; + xqc_stream_type_t stream_type; + xqc_stream_t *stream = NULL; + xqc_stream_frame_t *stream_frame; - xqc_stream_id_t stream_id; - xqc_stream_type_t stream_type; - xqc_stream_t *stream = NULL; - xqc_stream_frame_t *stream_frame = xqc_calloc(1, sizeof(xqc_stream_frame_t)); + stream_frame = xqc_calloc(1, sizeof(xqc_stream_frame_t)); if (stream_frame == NULL) { xqc_log(conn->log, XQC_LOG_ERROR, "|xqc_calloc error|"); return -XQC_EMALLOC; @@ -329,8 +387,8 @@ xqc_process_stream_frame(xqc_connection_t *conn, xqc_packet_in_t *packet_in) stream_type = xqc_get_stream_type(stream_id); - xqc_log(conn->log, XQC_LOG_DEBUG, "|offset:%ui|data_length:%ud|fin:%ud|stream_id:%ui|", - stream_frame->data_offset, stream_frame->data_length, stream_frame->fin, stream_id); + xqc_log(conn->log, XQC_LOG_DEBUG, "|offset:%ui|data_length:%ud|fin:%ud|stream_id:%ui|path:%ui|", + stream_frame->data_offset, stream_frame->data_length, stream_frame->fin, stream_id, packet_in->pi_path_id); stream = xqc_find_stream_by_id(stream_id, conn->streams_hash); if (!stream) { @@ -349,11 +407,12 @@ xqc_process_stream_frame(xqc_connection_t *conn, xqc_packet_in_t *packet_in) } } + conn->stream_stats.recv_bytes += stream_frame->data_length; + + xqc_stream_path_metrics_on_recv(conn, stream, packet_in); + if (packet_in->pi_path_id < XQC_MAX_PATHS_COUNT) { stream->paths_info[packet_in->pi_path_id].path_recv_bytes += stream_frame->data_length; - if (packet_in->pi_flag & XQC_PIF_REINJECTED_REPLICA) { - stream->paths_info[packet_in->pi_path_id].path_recv_reinject_bytes += stream_frame->data_length; - } } if (stream->stream_state_recv >= XQC_RECV_STREAM_ST_RESET_RECVD) { @@ -371,8 +430,9 @@ xqc_process_stream_frame(xqc_connection_t *conn, xqc_packet_in_t *packet_in) } if (stream_frame->fin) { - if (stream->stream_data_in.stream_length > 0 - && stream->stream_data_in.stream_length != stream_frame->data_offset + stream_frame->data_length) { + if (stream->stream_data_in.stream_determined + && stream->stream_data_in.stream_length != stream_frame->data_offset + stream_frame->data_length) + { xqc_log(conn->log, XQC_LOG_ERROR, "|final size changed|stream_id:%ui|", stream_id); XQC_CONN_ERR(conn, TRA_FINAL_SIZE_ERROR); ret = -XQC_EPROTO; @@ -384,13 +444,14 @@ xqc_process_stream_frame(xqc_connection_t *conn, xqc_packet_in_t *packet_in) } stream->stream_data_in.stream_length = stream_frame->data_offset + stream_frame->data_length; + stream->stream_data_in.stream_determined = XQC_TRUE; if (stream->stream_state_recv == XQC_RECV_STREAM_ST_RECV) { xqc_stream_recv_state_update(stream, XQC_RECV_STREAM_ST_SIZE_KNOWN); } } - if (stream->stream_data_in.stream_length > 0 + if (stream->stream_data_in.stream_determined && stream_frame->data_offset + stream_frame->data_length > stream->stream_data_in.stream_length) { xqc_log(conn->log, XQC_LOG_ERROR, "|exceed final size|stream_id:%ui|", stream_id); @@ -399,6 +460,21 @@ xqc_process_stream_frame(xqc_connection_t *conn, xqc_packet_in_t *packet_in) goto error; } + /* if stream is discarded, drop all data */ + if (stream->stream_flag & XQC_STREAM_FLAG_DISCARDED) { + xqc_log(conn->log, XQC_LOG_DEBUG, "|stream[%ui] data discarded|" + "offset:%ui|len:%ui", stream->stream_id, + stream_frame->data_offset, + stream_frame->data_length); + + /* if all data is discarded, try to close the stream */ + if (stream_frame->fin) { + xqc_stream_close_discarded_stream(stream); + } + + goto free; + } + ret = xqc_insert_stream_frame(conn, stream, stream_frame); if (ret == -XQC_EDUP_FRAME) { ret = XQC_OK; @@ -431,8 +507,8 @@ xqc_process_stream_frame(xqc_connection_t *conn, xqc_packet_in_t *packet_in) return -XQC_EPROTO; } - if (stream->stream_data_in.stream_length == stream->stream_data_in.merged_offset_end - && stream->stream_data_in.stream_length > 0) + if (stream->stream_data_in.stream_determined + && stream->stream_data_in.stream_length == stream->stream_data_in.merged_offset_end) { if (stream->stream_state_recv == XQC_RECV_STREAM_ST_SIZE_KNOWN) { xqc_stream_recv_state_update(stream, XQC_RECV_STREAM_ST_DATA_RECVD); @@ -446,12 +522,8 @@ xqc_process_stream_frame(xqc_connection_t *conn, xqc_packet_in_t *packet_in) xqc_stream_ready_to_read(stream); } - xqc_stream_path_metrics_on_recv(conn, stream, packet_in); if (packet_in->pi_path_id < XQC_MAX_PATHS_COUNT) { stream->paths_info[packet_in->pi_path_id].path_recv_effective_bytes += stream_frame->data_length; - if (packet_in->pi_flag & XQC_PIF_REINJECTED_REPLICA) { - stream->paths_info[packet_in->pi_path_id].path_recv_effective_reinject_bytes += stream_frame->data_length; - } } xqc_log(conn->log, XQC_LOG_DEBUG, "|stream_length:%ui|merged_offset_end:%ui|stream_id:%ui|", @@ -587,16 +659,11 @@ xqc_process_ack_frame(xqc_connection_t *conn, xqc_packet_in_t *packet_in) xqc_log_event(conn->log, TRA_PACKETS_ACKED, packet_in, ack_info.ranges[i].high, ack_info.ranges[i].low); } - if (conn->enable_multipath == XQC_CONN_MULTIPATH_SINGLE_PNS) { - ret = xqc_send_ctl_on_ack_received_spns(conn, &ack_info, packet_in->pkt_recv_time); - - } else { - /* 对端还不支持MP,或还未握手确认时,使用 initial path */ - xqc_path_ctx_t *path = conn->conn_initial_path; - xqc_pn_ctl_t *pn_ctl = xqc_get_pn_ctl(conn, path); - ret = xqc_send_ctl_on_ack_received(path->path_send_ctl, pn_ctl, conn->conn_send_queue, - &ack_info, packet_in->pkt_recv_time); - } + /* 对端还不支持MP,或还未握手确认时,使用 initial path */ + xqc_path_ctx_t *path = conn->conn_initial_path; + xqc_pn_ctl_t *pn_ctl = xqc_get_pn_ctl(conn, path); + ret = xqc_send_ctl_on_ack_received(path->path_send_ctl, pn_ctl, conn->conn_send_queue, + &ack_info, packet_in->pkt_recv_time, 1); if (ret != XQC_OK) { xqc_log(conn->log, XQC_LOG_ERROR, "|xqc_send_ctl_on_ack_received error|"); @@ -639,7 +706,8 @@ xqc_process_new_conn_id_frame(xqc_connection_t *conn, xqc_packet_in_t *packet_in return ret; } - xqc_log(conn->log, XQC_LOG_DEBUG, "|new_conn_id|%s|", xqc_scid_str(&new_conn_cid)); + xqc_log(conn->log, XQC_LOG_DEBUG, "|new_conn_id|%s|sr_token:%s", + xqc_scid_str(&new_conn_cid), xqc_sr_token_str(new_conn_cid.sr_token)); if (retire_prior_to > new_conn_cid.cid_seq_num) { /* @@ -705,9 +773,24 @@ xqc_process_new_conn_id_frame(xqc_connection_t *conn, xqc_packet_in_t *packet_in return XQC_OK; } - ret = xqc_insert_conns_hash(conn->engine->conns_hash_dcid, conn, &new_conn_cid); + /* insert into dcid-connection hash, for processing the deprecated stateless + reset packet */ + ret = xqc_insert_conns_hash(conn->engine->conns_hash_dcid, conn, + new_conn_cid.cid_buf, new_conn_cid.cid_len); + if (ret < 0) { + xqc_log(conn->log, XQC_LOG_ERROR, + "|insert new_cid into conns_hash_dcid failed|"); + return ret; + } + + /* insert into sr_token-connection hash, for processing stateless reset + packet */ + ret = xqc_insert_conns_hash(conn->engine->conns_hash_sr_token, conn, + new_conn_cid.sr_token, + XQC_STATELESS_RESET_TOKENLEN); if (ret < 0) { - xqc_log(conn->log, XQC_LOG_ERROR, "|insert new_cid into conns_hash_dcid failed|"); + xqc_log(conn->log, XQC_LOG_ERROR, + "|insert new_cid into conns_hash_sr_token failed|"); return ret; } @@ -735,7 +818,7 @@ xqc_process_retire_conn_id_frame(xqc_connection_t *conn, xqc_packet_in_t *packet return ret; } - if (seq_num >= conn->scid_set.largest_scid_seq_num) { + if (seq_num > conn->scid_set.largest_scid_seq_num) { /* * Receipt of a RETIRE_CONNECTION_ID frame containing a sequence number * greater than any previously sent to the peer MUST be treated as a @@ -780,11 +863,11 @@ xqc_process_retire_conn_id_frame(xqc_connection_t *conn, xqc_packet_in_t *packet xqc_log(conn->log, XQC_LOG_DEBUG, "|switch scid to %ui|", conn->scid_set.user_scid.cid_seq_num); } - /* TODO: 如果对应 “Avtive” Path 则需要替换 CID */ - xqc_path_ctx_t *path = xqc_conn_find_path_by_scid(conn, &inner_cid->cid); - if (path != NULL) { - xqc_log(conn->log, XQC_LOG_DEBUG, "|path:%ui|state:%d|", path->path_id, path->path_state); - } + /* TODO: 如果对应 “Active” Path 则需要替换 CID */ + // xqc_path_ctx_t *path = xqc_conn_find_path_by_scid(conn, &inner_cid->cid); + // if (path != NULL) { + // xqc_log(conn->log, XQC_LOG_DEBUG, "|path:%ui|state:%d|", path->path_id, path->path_state); + // } return XQC_OK; } @@ -869,6 +952,8 @@ xqc_process_reset_stream_frame(xqc_connection_t *conn, xqc_packet_in_t *packet_i xqc_log(conn->log, XQC_LOG_DEBUG, "|stream_id:%ui|stream_state_recv:%d|stream_state_send:%d|", stream->stream_id, stream->stream_state_recv, stream->stream_state_send); + xqc_stream_closing(stream, err_code); + if (stream->stream_state_send < XQC_SEND_STREAM_ST_RESET_SENT) { xqc_send_queue_drop_stream_frame_packets(conn, stream_id); xqc_write_reset_stream_to_packet(conn, stream, err_code, stream->stream_send_offset); @@ -1007,7 +1092,7 @@ xqc_process_stream_data_blocked_frame(xqc_connection_t *conn, xqc_packet_in_t *p stream->stream_flow_ctl.fc_max_stream_data_can_recv = stream->stream_data_in.next_read_offset + stream->stream_flow_ctl.fc_stream_recv_window_size; - ret = xqc_write_max_stream_data_to_packet(conn, stream_id, stream->stream_flow_ctl.fc_max_stream_data_can_recv); + ret = xqc_write_max_stream_data_to_packet(conn, stream_id, stream->stream_flow_ctl.fc_max_stream_data_can_recv, XQC_PTYPE_SHORT_HEADER); if (ret != XQC_OK) { xqc_log(conn->log, XQC_LOG_ERROR, "|xqc_write_max_stream_data_to_packet error|"); return ret; @@ -1039,7 +1124,8 @@ xqc_process_streams_blocked_frame(xqc_connection_t *conn, xqc_packet_in_t *packe return XQC_OK; } - new_max_streams = stream_limit + conn->local_settings.max_streams_bidi; + new_max_streams = xqc_min(stream_limit + conn->local_settings.max_streams_bidi, + conn->conn_flow_ctl.fc_max_streams_bidi_can_recv + conn->local_settings.max_streams_bidi); conn->conn_flow_ctl.fc_max_streams_bidi_can_recv = new_max_streams; } else { @@ -1048,7 +1134,8 @@ xqc_process_streams_blocked_frame(xqc_connection_t *conn, xqc_packet_in_t *packe return XQC_OK; } - new_max_streams = stream_limit + conn->local_settings.max_streams_uni; + new_max_streams = xqc_min(stream_limit + conn->local_settings.max_streams_uni, + conn->conn_flow_ctl.fc_max_streams_uni_can_recv + conn->local_settings.max_streams_uni); conn->conn_flow_ctl.fc_max_streams_uni_can_recv = new_max_streams; } @@ -1196,6 +1283,49 @@ xqc_process_new_token_frame(xqc_connection_t *conn, xqc_packet_in_t *packet_in) return XQC_OK; } +xqc_int_t +xqc_process_datagram_frame(xqc_connection_t *conn, xqc_packet_in_t *packet_in) +{ + /* does not support datagram */ + if (conn->local_settings.max_datagram_frame_size == 0) { + xqc_log(conn->log, XQC_LOG_ERROR, + "|the endpoint does not support datagram but receives a DATAGRAM frame|"); + XQC_CONN_ERR(conn, TRA_PROTOCOL_VIOLATION); + return -XQC_EPROTO; + } + + unsigned char *data_buffer = NULL; + size_t data_len = 0; + + xqc_int_t ret = xqc_parse_datagram_frame(packet_in, conn, &data_buffer, &data_len); + if (ret == -XQC_EPROTO) { + xqc_log(conn->log, XQC_LOG_ERROR, + "|the endpoint receives a DATAGRAM frame larger than max_datagram_frame_size|" + "max_datagram_frame_size:%ud|frame_size:%ud|", + conn->local_settings.max_datagram_frame_size, + data_len + XQC_DATAGRAM_HEADER_BYTES); + XQC_CONN_ERR(conn, TRA_PROTOCOL_VIOLATION); + return ret; + } + if (ret != XQC_OK) { + xqc_log(conn->log, XQC_LOG_ERROR, "|xqc_parse_datagram_frame error|"); + return ret; + } + + /* @TODO: datagram read callback */ + if (data_len > 0) { + if (conn->app_proto_cbs.dgram_cbs.datagram_read_notify + && (conn->conn_flag & XQC_CONN_FLAG_UPPER_CONN_EXIST)) + { + conn->app_proto_cbs.dgram_cbs.datagram_read_notify(conn, conn->dgram_data, data_buffer, data_len, xqc_monotonic_timestamp() - packet_in->pkt_recv_time); + xqc_log(conn->log, XQC_LOG_DEBUG, "|xqc_datagram_read|data_len:%z|", data_len); + } + } + + return ret; +} + + xqc_int_t xqc_process_handshake_done_frame(xqc_connection_t *conn, xqc_packet_in_t *packet_in) @@ -1231,21 +1361,40 @@ xqc_process_path_challenge_frame(xqc_connection_t *conn, xqc_packet_in_t *packet return ret; } - xqc_path_ctx_t *path = xqc_conn_find_path_by_scid(conn, &packet_in->pi_pkt.pkt_dcid); + //TODO: MPQUIC fix migration + xqc_path_ctx_t *path = NULL; + if (conn->enable_multipath) { + path = xqc_conn_find_path_by_scid(conn, &packet_in->pi_pkt.pkt_dcid); + + } else { + path = conn->conn_initial_path; + } + if (path == NULL) { - /* try to create new path */ - path = xqc_conn_create_path_inner(conn, &packet_in->pi_pkt.pkt_dcid, NULL); - if (path == NULL) { - xqc_log(conn->log, XQC_LOG_ERROR, "|xqc_conn_create_path_inner err|"); - return -XQC_EMP_CREATE_PATH; - } + if (conn->conn_type == XQC_CONN_TYPE_SERVER) { + /* try to create new path */ + path = xqc_conn_create_path_inner(conn, &packet_in->pi_pkt.pkt_dcid, NULL, XQC_APP_PATH_STATUS_AVAILABLE); + if (path == NULL) { + xqc_log(conn->log, XQC_LOG_ERROR, "|xqc_conn_create_path_inner err|"); + return -XQC_EMP_CREATE_PATH; + } + packet_in->pi_path_id = path->path_id; + conn->validating_path_id = path->path_id; + conn->conn_flag |= XQC_CONN_FLAG_RECV_NEW_PATH; - conn->validating_path_id = path->path_id; - conn->conn_flag |= XQC_CONN_FLAG_RECV_NEW_PATH; + } else { + xqc_log(conn->log, XQC_LOG_ERROR, + "|no path to challenge|dcid:%s|path_id:%ui|", + xqc_dcid_str(&packet_in->pi_pkt.pkt_dcid), + packet_in->pi_path_id); + return XQC_OK; + } } - xqc_log(conn->log, XQC_LOG_DEBUG, "|path:%ui|state:%d|RECV path_challenge_data:%s|", - path->path_id, path->path_state, path_challenge_data); + xqc_log(conn->log, XQC_LOG_DEBUG, + "|path:%ui|state:%d|RECV path_challenge_data:%s|cid:%s|", + path->path_id, path->path_state, + path_challenge_data, xqc_dcid_str(&packet_in->pi_pkt.pkt_dcid)); ret = xqc_write_path_response_frame_to_packet(conn, path, path_challenge_data); if (ret != XQC_OK) { @@ -1269,12 +1418,22 @@ xqc_process_path_response_frame(xqc_connection_t *conn, xqc_packet_in_t *packet_ return ret; } - xqc_path_ctx_t *path = xqc_conn_find_path_by_scid(conn, &packet_in->pi_pkt.pkt_dcid); - if (path == NULL) { - xqc_log(conn->log, XQC_LOG_ERROR, "|can't find path|pkt_dcid:%s|", xqc_scid_str(&packet_in->pi_pkt.pkt_dcid)); - return -XQC_EMP_PATH_NOT_FOUND; - } + //TODO: MPQUIC fix migration + xqc_path_ctx_t *path = NULL; + if (conn->enable_multipath) { + path = xqc_conn_find_path_by_scid(conn, &packet_in->pi_pkt.pkt_dcid); + if (path == NULL) { + xqc_log(conn->log, XQC_LOG_ERROR, + "|ingnore path response|pkt_dcid:%s|path_id:%ui|", + xqc_scid_str(&packet_in->pi_pkt.pkt_dcid), + packet_in->pi_path_id); + return XQC_OK; + } + } else { + path = conn->conn_initial_path; + } + xqc_log(conn->log, XQC_LOG_DEBUG, "|path:%ui|state:%d|RECV path_response_data:%s|", path->path_id, path->path_state, path_response_data); @@ -1335,29 +1494,37 @@ xqc_process_ack_mp_frame(xqc_connection_t *conn, xqc_packet_in_t *packet_in) xqc_int_t ret; xqc_ack_info_t ack_info; - uint64_t path_id; - ret = xqc_parse_ack_mp_frame(packet_in, conn, &path_id, &ack_info); + uint64_t dcid_seq_num; + ret = xqc_parse_ack_mp_frame(packet_in, conn, &dcid_seq_num, &ack_info); if (ret != XQC_OK) { xqc_log(conn->log, XQC_LOG_ERROR, "|xqc_parse_ack_mp_frame error|"); return ret; } - xqc_path_ctx_t *path = xqc_conn_find_path_by_path_id(conn, path_id); - if (path == NULL) { - xqc_log(conn->log, XQC_LOG_INFO, "|ignore unknown path|path:%ui|", path_id); + xqc_path_ctx_t *path_to_be_acked = xqc_conn_find_path_by_dcid_seq(conn, dcid_seq_num); + if (path_to_be_acked == NULL) { + xqc_log(conn->log, XQC_LOG_INFO, "|ignore unknown path|dcid_seq:%ui|", dcid_seq_num); return XQC_OK; } + if (path_to_be_acked->path_id != packet_in->pi_path_id) { + xqc_log(conn->log, XQC_LOG_DEBUG, + "|ACK_MP received on a different path|ack_path_id:%ui|recv_path_id:%ui|", + path_to_be_acked->path_id, + packet_in->pi_path_id); + } + for (int i = 0; i < ack_info.n_ranges; i++) { - xqc_log(conn->log, XQC_LOG_DEBUG, "|path:%ui|high:%ui|low:%ui|pkt_pns:%d|", path->path_id, + xqc_log(conn->log, XQC_LOG_DEBUG, "|path:%ui|high:%ui|low:%ui|pkt_pns:%d|", path_to_be_acked->path_id, ack_info.ranges[i].high, ack_info.ranges[i].low, packet_in->pi_pkt.pkt_pns); xqc_log_event(conn->log, TRA_PACKETS_ACKED, packet_in, ack_info.ranges[i].high, ack_info.ranges[i].low); } - xqc_pn_ctl_t *pn_ctl = xqc_get_pn_ctl(conn, path); + xqc_pn_ctl_t *pn_ctl = xqc_get_pn_ctl(conn, path_to_be_acked); - ret = xqc_send_ctl_on_ack_received(path->path_send_ctl, pn_ctl, conn->conn_send_queue, - &ack_info, packet_in->pkt_recv_time); + ret = xqc_send_ctl_on_ack_received(path_to_be_acked->path_send_ctl, pn_ctl, conn->conn_send_queue, + &ack_info, packet_in->pkt_recv_time, + path_to_be_acked->path_id == packet_in->pi_path_id); if (ret != XQC_OK) { xqc_log(conn->log, XQC_LOG_ERROR, "|xqc_send_ctl_on_ack_received error|"); return ret; @@ -1371,40 +1538,21 @@ xqc_process_path_abandon_frame(xqc_connection_t *conn, xqc_packet_in_t *packet_i { xqc_int_t ret = XQC_ERROR; - uint64_t path_id_type; - uint64_t path_id_content; + uint64_t dcid_seq_num; uint64_t error_code; - ret = xqc_parse_path_abandon_frame(packet_in, &path_id_type, &path_id_content, &error_code); + ret = xqc_parse_path_abandon_frame(packet_in, &dcid_seq_num, &error_code); if (ret != XQC_OK) { xqc_log(conn->log, XQC_LOG_ERROR, "|xqc_parse_path_abandon_frame error|"); return ret; } - xqc_path_ctx_t *path = NULL; - - switch (path_id_type) { - case 0x00: - path = xqc_conn_find_path_by_path_id(conn, path_id_content); - break; - - case 0x01: - path = xqc_conn_find_path_by_dcid(conn, xqc_get_cid_by_seq(&conn->dcid_set.cid_set, path_id_content)); - break; - - case 0x02: - path = xqc_conn_find_path_by_scid(conn, &packet_in->pi_pkt.pkt_dcid); - break; - - default: - xqc_log(conn->log, XQC_LOG_DEBUG, "|invalid path_id_type:%ui|", path_id_type); - return -XQC_EILLPKT; /* ignore */ - } + xqc_path_ctx_t *path = xqc_conn_find_path_by_dcid_seq(conn, dcid_seq_num); if (path == NULL) { xqc_log(conn->log, XQC_LOG_WARN, - "|invalid path_id|path_id_type:%ui|path_id_content:%ui|pi_path_id:%ui|", - path_id_type, path_id_content, packet_in->pi_path_id); + "|invalid path|dcid_seq_num:%ui|path_id:%ui|", + dcid_seq_num, packet_in->pi_path_id); return XQC_OK; /* ignore */ } @@ -1426,8 +1574,6 @@ xqc_process_path_abandon_frame(xqc_connection_t *conn, xqc_packet_in_t *packet_i } } - xqc_set_path_state(path, XQC_PATH_STATE_DRAINING); - xqc_log(conn->log, XQC_LOG_DEBUG, "|path:%ui|state:%d|err_code:%ui|", path->path_id, path->path_state, error_code); return XQC_OK; @@ -1438,52 +1584,35 @@ xqc_process_path_status_frame(xqc_connection_t *conn, xqc_packet_in_t *packet_in { xqc_int_t ret = XQC_ERROR; - uint64_t path_id_type; - uint64_t path_id_content; + uint64_t dcid_seq_num; uint64_t path_status_seq_num; uint64_t path_status; - ret = xqc_parse_path_status_frame(packet_in, &path_id_type, &path_id_content, &path_status_seq_num, &path_status); + ret = xqc_parse_path_status_frame(packet_in, &dcid_seq_num, &path_status_seq_num, &path_status); if (ret != XQC_OK) { xqc_log(conn->log, XQC_LOG_ERROR, "|xqc_parse_path_status_frame error|"); return ret; } - xqc_path_ctx_t *path = NULL; - - switch (path_id_type) { - case 0x00: - path = xqc_conn_find_path_by_scid(conn, xqc_get_cid_by_seq(&conn->scid_set.cid_set, path_id_content)); - break; - - case 0x01: - path = xqc_conn_find_path_by_dcid(conn, xqc_get_cid_by_seq(&conn->dcid_set.cid_set, path_id_content)); - break; - - case 0x02: - path = xqc_conn_find_path_by_scid(conn, &packet_in->pi_pkt.pkt_dcid); - break; - - default: - xqc_log(conn->log, XQC_LOG_DEBUG, "|invalid path_id_type:%ui|", path_id_type); - return -XQC_EILLPKT; /* ignore */ - } + xqc_path_ctx_t *path = xqc_conn_find_path_by_dcid_seq(conn, dcid_seq_num); if (path == NULL) { xqc_log(conn->log, XQC_LOG_WARN, - "|invalid path_id|path_id_type:%ui|path_id_content:%ui|pi_path_id:%ui|", - path_id_type, path_id_content, packet_in->pi_path_id); + "|invalid path|dcid_seq_num:%ui|pi_path_id:%ui|", + dcid_seq_num, packet_in->pi_path_id); return XQC_OK; /* ignore */ } - xqc_usec_t now = xqc_monotonic_timestamp(); if (path_status_seq_num > path->app_path_status_recv_seq_num) { path->app_path_status_recv_seq_num = path_status_seq_num; - xqc_set_application_path_status(path, path_status, now); + path->next_app_path_state = path_status; + + if (path->path_state < XQC_PATH_STATE_ACTIVE) { + path->path_flag |= XQC_PATH_FLAG_RECV_STATUS; - xqc_log(conn->log, XQC_LOG_DEBUG, - "|path:%ui|path_id_type:%ui|path_id_content:%ui|app_path_status:%d|seq_num:%ui|tra_path_status:%d|", - path->path_id, path_id_type, path_id_content, path->app_path_status, path_status_seq_num, path->tra_path_status); + } else { + xqc_set_application_path_status(path, path->next_app_path_state, XQC_FALSE); + } } return XQC_OK; diff --git a/src/transport/xqc_frame.h b/src/transport/xqc_frame.h index b6bf1901f..a4ef5736f 100644 --- a/src/transport/xqc_frame.h +++ b/src/transport/xqc_frame.h @@ -31,6 +31,7 @@ typedef enum { XQC_FRAME_ACK_MP, XQC_FRAME_PATH_ABANDON, XQC_FRAME_PATH_STATUS, + XQC_FRAME_DATAGRAM, XQC_FRAME_Extension, XQC_FRAME_NUM, } xqc_frame_type_t; @@ -59,6 +60,7 @@ typedef enum { XQC_FRAME_BIT_ACK_MP = 1 << XQC_FRAME_ACK_MP, XQC_FRAME_BIT_PATH_ABANDON = 1 << XQC_FRAME_PATH_ABANDON, XQC_FRAME_BIT_PATH_STATUS = 1 << XQC_FRAME_PATH_STATUS, + XQC_FRAME_BIT_DATAGRAM = 1 << XQC_FRAME_DATAGRAM, XQC_FRAME_BIT_Extension = 1 << XQC_FRAME_Extension, XQC_FRAME_BIT_NUM = 1 << XQC_FRAME_NUM, } xqc_frame_type_bit_t; @@ -91,7 +93,7 @@ typedef enum { * PING and PADDING frames contain no information, so lost PING or * PADDING frames do not require repair */ -#define XQC_NEED_REPAIR(types) ((types) & ~(XQC_FRAME_BIT_ACK| XQC_FRAME_BIT_PADDING | XQC_FRAME_BIT_PING | XQC_FRAME_BIT_CONNECTION_CLOSE)) +#define XQC_NEED_REPAIR(types) ((types) & ~(XQC_FRAME_BIT_ACK| XQC_FRAME_BIT_PADDING | XQC_FRAME_BIT_PING | XQC_FRAME_BIT_CONNECTION_CLOSE | XQC_FRAME_BIT_DATAGRAM)) const char *xqc_frame_type_2_str(xqc_frame_type_bit_t type_bit); @@ -150,4 +152,6 @@ xqc_int_t xqc_process_path_abandon_frame(xqc_connection_t *conn, xqc_packet_in_t xqc_int_t xqc_process_path_status_frame(xqc_connection_t *conn, xqc_packet_in_t *packet_in); +xqc_int_t xqc_process_datagram_frame(xqc_connection_t *conn, xqc_packet_in_t *packet_in); + #endif /* _XQC_FRAME_H_INCLUDED_ */ diff --git a/src/transport/xqc_frame_parser.c b/src/transport/xqc_frame_parser.c index 01b5d58b8..95e0b4f34 100644 --- a/src/transport/xqc_frame_parser.c +++ b/src/transport/xqc_frame_parser.c @@ -14,6 +14,88 @@ #include "src/transport/xqc_packet_parser.h" #include "src/transport/xqc_reinjection.h" +/** + * generate datagram frame + */ +xqc_int_t xqc_gen_datagram_frame(xqc_packet_out_t *packet_out, + const unsigned char *payload, size_t size) +{ + if (packet_out == NULL) { + return -XQC_EPARAM; + } + + unsigned char *dst_buf = packet_out->po_buf + packet_out->po_used_size; + size_t dst_buf_len = packet_out->po_buf_size - packet_out->po_used_size; + unsigned char *p = dst_buf + 1; + + if ((size + 1 + XQC_DATAGRAM_LENGTH_FIELD_BYTES) > dst_buf_len) { + return -XQC_ENOBUF; + } + + xqc_vint_write(p, size, XQC_DATAGRAM_LENGTH_FIELD_BYTES - 1, XQC_DATAGRAM_LENGTH_FIELD_BYTES); + p += XQC_DATAGRAM_LENGTH_FIELD_BYTES; + + if (size > 0) { + xqc_memcpy(p, payload, size); + } + + p += size; + + dst_buf[0] = 0x31; + + packet_out->po_frame_types |= XQC_FRAME_BIT_DATAGRAM; + packet_out->po_used_size += p - dst_buf; + + return XQC_OK; +} + +xqc_int_t xqc_parse_datagram_frame(xqc_packet_in_t *packet_in, xqc_connection_t *conn, + unsigned char **buffer, size_t *size) +{ + uint64_t length; + int vlen = 0; + const unsigned char *p = packet_in->pos; + const unsigned char *end = packet_in->last; + /* skip frame type */ + const unsigned char first_byte = *p++; + xqc_bool_t has_length = (first_byte & 0x1); + + if (has_length) { + /*currently, the length of length field is 2 bytes*/ + vlen = xqc_vint_read(p, end, &length); + if (vlen < 0) { + return -XQC_EVINTREAD; + } + + p += vlen; + + if (length > (end - p)) { + return -XQC_EILLEGAL_FRAME; + } + + } else { + length = end - p; + } + + /* recv a DATAGRAM frame larger than max_datagram_frame_size */ + if ((length + 1 + vlen) > conn->local_settings.max_datagram_frame_size) { + return -XQC_EPROTO; + } + + *size = length; + *buffer = (unsigned char *)p; + + p += length; + + packet_in->pos = (unsigned char *)p; + packet_in->pi_frame_types |= XQC_FRAME_BIT_DATAGRAM; + + xqc_log_event(conn->log, TRA_FRAMES_PROCESSED, XQC_FRAME_DATAGRAM, length); + + return XQC_OK; +} + + ssize_t @@ -230,10 +312,10 @@ xqc_parse_stream_frame(xqc_packet_in_t *packet_in, xqc_connection_t *conn, return -XQC_EVINTREAD; } + p += vlen; if (length > end - p) { return -XQC_EILLEGAL_FRAME; } - p += vlen; frame->data_length = length; } else { @@ -280,7 +362,7 @@ xqc_parse_stream_frame(xqc_packet_in_t *packet_in, xqc_connection_t *conn, */ ssize_t xqc_gen_crypto_frame(xqc_packet_out_t *packet_out, uint64_t offset, - const unsigned char *payload, size_t payload_size, size_t *written_size) + const unsigned char *payload, uint64_t payload_size, size_t *written_size) { unsigned char *dst_buf = packet_out->po_buf + packet_out->po_used_size; size_t dst_buf_len = packet_out->po_buf_size - packet_out->po_used_size; @@ -365,12 +447,22 @@ xqc_parse_crypto_frame(xqc_packet_in_t *packet_in, xqc_connection_t *conn, } void -xqc_gen_padding_frame(xqc_packet_out_t *packet_out) +xqc_gen_padding_frame(xqc_connection_t *conn, xqc_packet_out_t *packet_out) { - if (packet_out->po_used_size < XQC_PACKET_INITIAL_MIN_LENGTH) { + size_t total_len = XQC_PACKET_INITIAL_MIN_LENGTH; + + if (conn->conn_settings.enable_pmtud) { + if ((packet_out->po_frame_types & (XQC_FRAME_BIT_PATH_CHALLENGE | XQC_FRAME_BIT_PATH_RESPONSE)) + || (packet_out->po_flag & XQC_POF_PMTUD_PROBING)) + { + total_len = packet_out->po_buf_size + XQC_ACK_SPACE; + } + } + + if (packet_out->po_used_size < total_len) { packet_out->po_padding = packet_out->po_buf + packet_out->po_used_size; - memset(packet_out->po_padding, 0, XQC_PACKET_INITIAL_MIN_LENGTH - packet_out->po_used_size); - packet_out->po_used_size = XQC_PACKET_INITIAL_MIN_LENGTH; + memset(packet_out->po_padding, 0, total_len - packet_out->po_used_size); + packet_out->po_used_size = total_len; packet_out->po_frame_types |= XQC_FRAME_BIT_PADDING; } } @@ -567,142 +659,6 @@ xqc_gen_ack_frame(xqc_connection_t *conn, xqc_packet_out_t *packet_out, xqc_usec return dst_buf - begin; } -ssize_t -xqc_gen_ack_frame_for_spns(xqc_connection_t *conn, xqc_packet_out_t *packet_out, xqc_usec_t now, int ack_delay_exponent, - xqc_recv_record_t *recv_record, xqc_usec_t largest_pkt_recv_time, int *has_gap, - xqc_packet_number_t *largest_ack, xqc_packet_number_t must_ack) -{ - unsigned char *dst_buf = packet_out->po_buf + packet_out->po_used_size; - size_t dst_buf_len = packet_out->po_buf_size - packet_out->po_used_size + XQC_ACK_SPACE; - - xqc_packet_number_t lagest_recv, prev_low; - xqc_usec_t ack_delay; - - const unsigned char *begin = dst_buf; - const unsigned char *end = dst_buf + dst_buf_len; - unsigned char *p_range_count; - unsigned range_count = 0, first_ack_range, gap, acks, gap_bits, acks_bits, need; - - xqc_list_head_t *pos, *next; - xqc_pktno_range_node_t *range_node; - xqc_pktno_range_node_t *first_range = NULL; - xqc_list_head_t *pos_lowest_range; - xqc_bool_t cover_lowest_range = 0; - - /* 根据传入的最大ack来写ack frame */ - pos = xqc_recv_record_get_pos(recv_record, *largest_ack); - if (pos == NULL) { - xqc_log(conn->log, XQC_LOG_WARN, "|get_largest_ack_pos error|largest_ack:%ui|record_largest:%ui|", *largest_ack, xqc_recv_record_largest(recv_record)); - return -XQC_ENULLPTR; - } - pos_lowest_range = xqc_recv_record_get_pos(recv_record, must_ack); - if (pos_lowest_range == NULL) { - xqc_log(conn->log, XQC_LOG_WARN, "|get_lowest_range_pos error|must_ack:%ui|", must_ack); - return -XQC_ENULLPTR; - } - - first_range = xqc_list_entry(pos, xqc_pktno_range_node_t, list); - if (first_range == NULL) { - xqc_log(conn->log, XQC_LOG_ERROR, "|first_range empty|"); - return -XQC_ENULLPTR; - } - - ack_delay = (now - largest_pkt_recv_time); - lagest_recv = first_range->pktno_range.high; - first_ack_range = lagest_recv - first_range->pktno_range.low; - prev_low = first_range->pktno_range.low; - - xqc_log(conn->log, XQC_LOG_DEBUG, "|lagest_recv:%ui|ack_delay:%ui|first_ack_range:%ud|largest_pkt_recv_time:%ui|", - lagest_recv, ack_delay, first_ack_range, largest_pkt_recv_time); - - ack_delay = ack_delay >> ack_delay_exponent; - - unsigned lagest_recv_bits = xqc_vint_get_2bit(lagest_recv); - unsigned ack_delay_bits = xqc_vint_get_2bit(ack_delay); - unsigned first_ack_range_bits = xqc_vint_get_2bit(first_ack_range); - - need = 1 /* type */ - + xqc_vint_len(lagest_recv_bits) - + xqc_vint_len(ack_delay_bits) - + 1 /* range_count */ - + xqc_vint_len(first_ack_range_bits); - - if (dst_buf + need > end) { - return -XQC_ENOBUF; - } - - *dst_buf++ = 0x02; - - xqc_vint_write(dst_buf, lagest_recv, lagest_recv_bits, xqc_vint_len(lagest_recv_bits)); - dst_buf += xqc_vint_len(lagest_recv_bits); - - *largest_ack = lagest_recv; - - xqc_vint_write(dst_buf, ack_delay, ack_delay_bits, xqc_vint_len(ack_delay_bits)); - dst_buf += xqc_vint_len(ack_delay_bits); - - p_range_count = dst_buf; - dst_buf += 1; /* max range_count 63, 1 byte */ - - xqc_vint_write(dst_buf, first_ack_range, first_ack_range_bits, xqc_vint_len(first_ack_range_bits)); - dst_buf += xqc_vint_len(first_ack_range_bits); - - int is_first = 1; - xqc_list_for_each_from(pos, &recv_record->list_head) { /* from second node */ - range_node = xqc_list_entry(pos, xqc_pktno_range_node_t, list); - - xqc_log(conn->log, XQC_LOG_DEBUG, "|high:%ui|low:%ui|pkt_pns:%d|", - range_node->pktno_range.high, range_node->pktno_range.low, packet_out->po_pkt.pkt_pns); - - if (pos_lowest_range == NULL || pos_lowest_range == pos) { - cover_lowest_range = 1; - } - - if (is_first) { - is_first = 0; - continue; - } - - gap = prev_low - range_node->pktno_range.high - 2; - acks = range_node->pktno_range.high - range_node->pktno_range.low; - - gap_bits = xqc_vint_get_2bit(gap); - acks_bits = xqc_vint_get_2bit(acks); - - need = xqc_vint_len(gap_bits) + xqc_vint_len(acks_bits); - if (dst_buf + need > end) { - return -XQC_ENOBUF; - } - - xqc_vint_write(dst_buf, gap, gap_bits, xqc_vint_len(gap_bits)); - dst_buf += xqc_vint_len(gap_bits); - - xqc_vint_write(dst_buf, acks, acks_bits, xqc_vint_len(acks_bits)); - dst_buf += xqc_vint_len(acks_bits); - - prev_low = range_node->pktno_range.low; - - ++range_count; - if (range_count >= XQC_MAX_ACK_RANGE_CNT - 1) { - break; - } - if (cover_lowest_range == 1 && range_count >= XQC_DEFAULT_ACK_RANGE_CNT - 1) { - break; - } - } - - if (range_count > 0) { - *has_gap = 1; - - } else { - *has_gap = 0; - } - xqc_vint_write(p_range_count, range_count, 0, 1); - - packet_out->po_frame_types |= XQC_FRAME_BIT_ACK; - return dst_buf - begin; -} - /** * parse ack frame to ack_info */ @@ -721,7 +677,12 @@ xqc_parse_ack_frame(xqc_packet_in_t *packet_in, xqc_connection_t *conn, xqc_ack_ unsigned n_ranges = 0; /* the range cnt stored */ - ack_info->path_id = packet_in->pi_path_id; + /* + * mpquic draft-04: If the multipath extension has been successfully + * negotiated, ACK frames in 1-RTT packets acknowledge packets sent + * with the Connection ID having sequence number 0. + */ + ack_info->dcid_seq_num = 0; ack_info->pns = packet_in->pi_pkt.pkt_pns; vlen = xqc_vint_read(p, end, &largest_acked); @@ -1518,7 +1479,8 @@ xqc_parse_handshake_done_frame(xqc_packet_in_t *packet_in, xqc_connection_t *con * Figure 39: NEW_CONNECTION_ID Frame Format * */ ssize_t -xqc_gen_new_conn_id_frame(xqc_packet_out_t *packet_out, xqc_cid_t *new_cid, uint64_t retire_prior_to, char *key, size_t keylen) +xqc_gen_new_conn_id_frame(xqc_packet_out_t *packet_out, xqc_cid_t *new_cid, + uint64_t retire_prior_to, const uint8_t *sr_token) { unsigned char *dst_buf = packet_out->po_buf + packet_out->po_used_size; const unsigned char *begin = dst_buf; @@ -1550,9 +1512,10 @@ xqc_gen_new_conn_id_frame(xqc_packet_out_t *packet_out, xqc_cid_t *new_cid, uint xqc_memcpy(dst_buf, new_cid->cid_buf, new_cid->cid_len); dst_buf += new_cid->cid_len; - xqc_gen_reset_token(new_cid, stateless_reset_token, XQC_STATELESS_RESET_TOKENLEN, key, keylen); - xqc_memcpy(dst_buf, stateless_reset_token, XQC_STATELESS_RESET_TOKENLEN); - dst_buf += XQC_STATELESS_RESET_TOKENLEN; + if (sr_token) { + xqc_memcpy(dst_buf, sr_token, XQC_STATELESS_RESET_TOKENLEN); + dst_buf += XQC_STATELESS_RESET_TOKENLEN; + } packet_out->po_frame_types |= XQC_FRAME_BIT_NEW_CONNECTION_ID; @@ -1581,7 +1544,6 @@ xqc_parse_new_conn_id_frame(xqc_packet_in_t *packet_in, xqc_cid_t *new_cid, uint const unsigned char first_byte = *p++; int vlen; - unsigned char stateless_reset_token[XQC_STATELESS_RESET_TOKENLEN]; /* Sequence Number (i) */ vlen = xqc_vint_read(p, end, &new_cid->cid_seq_num); @@ -1617,7 +1579,7 @@ xqc_parse_new_conn_id_frame(xqc_packet_in_t *packet_in, xqc_cid_t *new_cid, uint if (p + XQC_STATELESS_RESET_TOKENLEN > end) { return -XQC_EPROTO; } - xqc_memcpy(stateless_reset_token, p, XQC_STATELESS_RESET_TOKENLEN); + xqc_memcpy(new_cid->sr_token, p, XQC_STATELESS_RESET_TOKENLEN); p += XQC_STATELESS_RESET_TOKENLEN; packet_in->pos = p; @@ -1727,6 +1689,9 @@ xqc_parse_path_challenge_frame(xqc_packet_in_t *packet_in, unsigned char *data) const unsigned char *end = packet_in->last; const unsigned char first_byte = *p++; + if (p + XQC_PATH_CHALLENGE_DATA_LEN > end) { + return -XQC_EVINTREAD; + } xqc_memcpy(data, p, XQC_PATH_CHALLENGE_DATA_LEN); p += XQC_PATH_CHALLENGE_DATA_LEN; @@ -1784,6 +1749,10 @@ xqc_parse_path_response_frame(xqc_packet_in_t *packet_in, unsigned char *data) const unsigned char *end = packet_in->last; const unsigned char first_byte = *p++; + if (p + XQC_PATH_CHALLENGE_DATA_LEN > end) { + return -XQC_EVINTREAD; + } + xqc_memcpy(data, p, XQC_PATH_CHALLENGE_DATA_LEN); p += XQC_PATH_CHALLENGE_DATA_LEN; @@ -1795,11 +1764,11 @@ xqc_parse_path_response_frame(xqc_packet_in_t *packet_in, unsigned char *data) } /* - * https://datatracker.ietf.org/doc/html/draft-ietf-quic-multipath#section-10.2 + * https://datatracker.ietf.org/doc/html/draft-ietf-quic-multipath-05#name-ack_mp-frame * * ACK_MP Frame { - * Type (i) = TBD-00..TBD-01 (experiments use 0xbaba00..0xbaba01), - * Packet Number Space Identifier (i), + * Type (i) = TBD-00..TBD-01 , + * Destination Connection ID Sequence Number (i), * Largest Acknowledged (i), * ACK Delay (i), * ACK Range Count (i), @@ -1812,12 +1781,22 @@ xqc_parse_path_response_frame(xqc_packet_in_t *packet_in, unsigned char *data) */ ssize_t -xqc_gen_ack_mp_frame(xqc_connection_t *conn, uint64_t path_id, +xqc_gen_ack_mp_frame(xqc_connection_t *conn, uint64_t dcid_seq, xqc_packet_out_t *packet_out, xqc_usec_t now, int ack_delay_exponent, xqc_recv_record_t *recv_record, xqc_usec_t largest_pkt_recv_time, int *has_gap, xqc_packet_number_t *largest_ack) { - uint64_t frame_type = 0xbaba00; + uint64_t frame_type; + if (conn->conn_settings.multipath_version == XQC_MULTIPATH_04) { + frame_type = 0xbaba00; + + } else if (conn->conn_settings.multipath_version == XQC_MULTIPATH_05) { + frame_type = 0x15228c00; + + } else { + return -XQC_EMP_INVALID_MP_VERTION; + } + unsigned char *dst_buf = packet_out->po_buf + packet_out->po_used_size; size_t dst_buf_len = packet_out->po_buf_size - packet_out->po_used_size + XQC_ACK_SPACE; @@ -1862,13 +1841,13 @@ xqc_gen_ack_mp_frame(xqc_connection_t *conn, uint64_t path_id, ack_delay = ack_delay >> ack_delay_exponent; unsigned frame_type_bits = xqc_vint_get_2bit(frame_type); - unsigned path_id_bits = xqc_vint_get_2bit(path_id); + unsigned dcid_seq_bits = xqc_vint_get_2bit(dcid_seq); unsigned lagest_recv_bits = xqc_vint_get_2bit(lagest_recv); unsigned ack_delay_bits = xqc_vint_get_2bit(ack_delay); unsigned first_ack_range_bits = xqc_vint_get_2bit(first_ack_range); need = + xqc_vint_len(frame_type_bits) - + xqc_vint_len(path_id_bits) + + xqc_vint_len(dcid_seq_bits) + xqc_vint_len(lagest_recv_bits) + xqc_vint_len(ack_delay_bits) + 1 /* range_count */ @@ -1881,8 +1860,8 @@ xqc_gen_ack_mp_frame(xqc_connection_t *conn, uint64_t path_id, xqc_vint_write(dst_buf, frame_type, frame_type_bits, xqc_vint_len(frame_type_bits)); dst_buf += xqc_vint_len(frame_type_bits); - xqc_vint_write(dst_buf, path_id, path_id_bits, xqc_vint_len(path_id_bits)); - dst_buf += xqc_vint_len(path_id_bits); + xqc_vint_write(dst_buf, dcid_seq, dcid_seq_bits, xqc_vint_len(dcid_seq_bits)); + dst_buf += xqc_vint_len(dcid_seq_bits); xqc_vint_write(dst_buf, lagest_recv, lagest_recv_bits, xqc_vint_len(lagest_recv_bits)); dst_buf += xqc_vint_len(lagest_recv_bits); @@ -1950,7 +1929,7 @@ xqc_gen_ack_mp_frame(xqc_connection_t *conn, uint64_t path_id, xqc_int_t xqc_parse_ack_mp_frame(xqc_packet_in_t *packet_in, xqc_connection_t *conn, - uint64_t *path_id, xqc_ack_info_t *ack_info) + uint64_t *dcid_seq_num, xqc_ack_info_t *ack_info) { unsigned char *p = packet_in->pos; const unsigned char *end = packet_in->last; @@ -1970,13 +1949,13 @@ xqc_parse_ack_mp_frame(xqc_packet_in_t *packet_in, xqc_connection_t *conn, } p += vlen; - vlen = xqc_vint_read(p, end, path_id); + vlen = xqc_vint_read(p, end, dcid_seq_num); if (vlen < 0) { return -XQC_EVINTREAD; } p += vlen; - ack_info->path_id = *path_id; + ack_info->dcid_seq_num = *dcid_seq_num; ack_info->pns = packet_in->pi_pkt.pkt_pns; vlen = xqc_vint_read(p, end, &largest_acked); @@ -2048,47 +2027,48 @@ xqc_parse_ack_mp_frame(xqc_packet_in_t *packet_in, xqc_connection_t *conn, /* - * https://datatracker.ietf.org/doc/html/draft-ietf-quic-multipath#section-10.1 + * https://datatracker.ietf.org/doc/html/draft-ietf-quic-multipath-05#name-path_abandon-frame * * PATH_ABANDON Frame { - * Type (i) = TBD-03 (experiments use 0xbaba05), - * Path Identifier (..), + * Type (i) = TBD-03, + * DCID Sequence Number (i), * Error Code (i), * Reason Phrase Length (i), * Reason Phrase (..), * } * - * Figure 4: PATH_ABANDON Frame Format - * - * Path Identifier { - * Identifier Type (i) = 0x00..0x02, - * [Path Identifier Content (i)], - * } - * - * Figure 5: Path Identifier Format + * Figure 6: PATH_ABANDON Frame Format */ ssize_t -xqc_gen_path_abandon_frame(xqc_packet_out_t *packet_out, - uint64_t path_id_type, uint64_t path_id_content, uint64_t error_code) +xqc_gen_path_abandon_frame(xqc_connection_t *conn, xqc_packet_out_t *packet_out, + uint64_t dcid_seq_num, uint64_t error_code) { unsigned char *dst_buf = packet_out->po_buf + packet_out->po_used_size; const unsigned char *begin = dst_buf; unsigned need = 0; + uint64_t frame_type; + + if (conn->conn_settings.multipath_version == XQC_MULTIPATH_04) { + frame_type = 0xbaba05; + + } else if (conn->conn_settings.multipath_version == XQC_MULTIPATH_05) { + frame_type = 0x15228c05; + + } else { + return -XQC_EMP_INVALID_MP_VERTION; + } - uint64_t frame_type = 0xbaba05; uint64_t reason_len = 0; uint8_t *reason = NULL; unsigned frame_type_bits = xqc_vint_get_2bit(frame_type); - unsigned path_id_type_bits = xqc_vint_get_2bit(path_id_type); - unsigned path_id_content_bits = xqc_vint_get_2bit(path_id_content); + unsigned dcid_seq_num_bits = xqc_vint_get_2bit(dcid_seq_num); unsigned error_code_bits = xqc_vint_get_2bit(error_code); unsigned reason_len_bits = xqc_vint_get_2bit(reason_len); need = xqc_vint_len(frame_type_bits) - + xqc_vint_len(path_id_type_bits) - + xqc_vint_len(path_id_content_bits) + + xqc_vint_len(dcid_seq_num_bits) + xqc_vint_len(error_code_bits) + xqc_vint_len(reason_len_bits) + reason_len; @@ -2102,16 +2082,9 @@ xqc_gen_path_abandon_frame(xqc_packet_out_t *packet_out, xqc_vint_write(dst_buf, frame_type, frame_type_bits, xqc_vint_len(frame_type_bits)); dst_buf += xqc_vint_len(frame_type_bits); - /* Path Identifier Type (i) */ - xqc_vint_write(dst_buf, path_id_type, path_id_type_bits, xqc_vint_len(path_id_type_bits)); - dst_buf += xqc_vint_len(path_id_type_bits); - - /* Path Identifier Content (i) */ - /* If Identifier Type is 2, the Path Identifier Content MUST be empty */ - if (path_id_type != 0x02) { - xqc_vint_write(dst_buf, path_id_content, path_id_content_bits, xqc_vint_len(path_id_content_bits)); - dst_buf += xqc_vint_len(path_id_content_bits); - } + /* DCID Sequence Number (i) */ + xqc_vint_write(dst_buf, dcid_seq_num, dcid_seq_num_bits, xqc_vint_len(dcid_seq_num_bits)); + dst_buf += xqc_vint_len(dcid_seq_num_bits); /* Error Code (i) */ xqc_vint_write(dst_buf, error_code, error_code_bits, xqc_vint_len(error_code_bits)); @@ -2134,12 +2107,13 @@ xqc_gen_path_abandon_frame(xqc_packet_out_t *packet_out, xqc_int_t xqc_parse_path_abandon_frame(xqc_packet_in_t *packet_in, - uint64_t *path_id_type, uint64_t *path_id_content, uint64_t *error_code) + uint64_t *dcid_seq_num, uint64_t *error_code) { unsigned char *p = packet_in->pos; const unsigned char *end = packet_in->last; int vlen; + uint64_t reason_len = 0; uint64_t frame_type = 0; vlen = xqc_vint_read(p, end, &frame_type); /* get frame_type */ @@ -2148,23 +2122,13 @@ xqc_parse_path_abandon_frame(xqc_packet_in_t *packet_in, } p += vlen; - /* Path Identifier Type (i) */ - vlen = xqc_vint_read(p, end, path_id_type); + /* DCID Sequence Number (i) */ + vlen = xqc_vint_read(p, end, dcid_seq_num); if (vlen < 0) { return -XQC_EVINTREAD; } p += vlen; - /* Path Identifier Content (i) */ - /* If Identifier Type is 2, the Path Identifier Content MUST be empty */ - if (*path_id_type != 0x02) { - vlen = xqc_vint_read(p, end, path_id_content); - if (vlen < 0) { - return -XQC_EVINTREAD; - } - p += vlen; - } - /* Error Code (i) */ vlen = xqc_vint_read(p, end, error_code); if (vlen < 0) { @@ -2172,18 +2136,17 @@ xqc_parse_path_abandon_frame(xqc_packet_in_t *packet_in, } p += vlen; - // /* Reason Phrase Length (i) */ - // vlen = xqc_vint_read(p, end, reason_len); - // if (vlen < 0) { - // return -XQC_EVINTREAD; - // } - // p += vlen; + /* Reason Phrase Length (i) */ + vlen = xqc_vint_read(p, end, &reason_len); + if (vlen < 0) { + return -XQC_EVINTREAD; + } + p += vlen; - // /* Reason Phrase (..) */ - // if (*reason_len > 0) { - // xqc_memcpy(reason, p, *reason_len); - // p += *reason_len; - // } + /* Reason Phrase (..) */ + if (reason_len > 0) { + p += reason_len; + } packet_in->pos = p; @@ -2194,11 +2157,11 @@ xqc_parse_path_abandon_frame(xqc_packet_in_t *packet_in, /* - * https://datatracker.ietf.org/doc/html/draft-ietf-quic-multipath-02#section-12.2 + * https://datatracker.ietf.org/doc/html/draft-ietf-quic-multipath-05#name-path_status-frame * * PATH_STATUS Frame { - * Type (i) = TBD-03 (experiments use 0xbaba06), - * Path Identifier (..), + * Type (i) = TBD-03, + * DCID Sequence Number (i), * Path Status sequence number (i), * Path Status (i), * } @@ -2207,25 +2170,33 @@ xqc_parse_path_abandon_frame(xqc_packet_in_t *packet_in, */ ssize_t -xqc_gen_path_status_frame(xqc_packet_out_t *packet_out, - uint64_t path_id_type, uint64_t path_id_content, +xqc_gen_path_status_frame(xqc_connection_t *conn, + xqc_packet_out_t *packet_out, + uint64_t dcid_seq_num, uint64_t path_status_seq_num, uint64_t path_status) { unsigned char *dst_buf = packet_out->po_buf + packet_out->po_used_size; const unsigned char *begin = dst_buf; unsigned need = 0; - uint64_t frame_type = 0xbaba06; + uint64_t frame_type; + if (conn->conn_settings.multipath_version == XQC_MULTIPATH_04) { + frame_type = 0xbaba06; + + } else if (conn->conn_settings.multipath_version == XQC_MULTIPATH_05) { + frame_type = 0x15228c06; + + } else { + return -XQC_EMP_INVALID_MP_VERTION; + } unsigned frame_type_bits = xqc_vint_get_2bit(frame_type); - unsigned path_id_type_bits = xqc_vint_get_2bit(path_id_type); - unsigned path_id_content_bits = xqc_vint_get_2bit(path_id_content); + unsigned dcid_seq_num_bits = xqc_vint_get_2bit(dcid_seq_num); unsigned path_status_seq_num_bits = xqc_vint_get_2bit(path_status_seq_num); unsigned path_status_bits = xqc_vint_get_2bit(path_status); need = xqc_vint_len(frame_type_bits) - + xqc_vint_len(path_id_type_bits) - + xqc_vint_len(path_id_content_bits) + + xqc_vint_len(dcid_seq_num_bits) + xqc_vint_len(path_status_seq_num_bits) + xqc_vint_len(path_status_bits); @@ -2238,16 +2209,9 @@ xqc_gen_path_status_frame(xqc_packet_out_t *packet_out, xqc_vint_write(dst_buf, frame_type, frame_type_bits, xqc_vint_len(frame_type_bits)); dst_buf += xqc_vint_len(frame_type_bits); - /* Path Identifier Type (i) */ - xqc_vint_write(dst_buf, path_id_type, path_id_type_bits, xqc_vint_len(path_id_type_bits)); - dst_buf += xqc_vint_len(path_id_type_bits); - - /* Path Identifier Content (i) */ - /* If Identifier Type is 2, the Path Identifier Content MUST be empty */ - if (path_id_type != 0x02) { - xqc_vint_write(dst_buf, path_id_content, path_id_content_bits, xqc_vint_len(path_id_content_bits)); - dst_buf += xqc_vint_len(path_id_content_bits); - } + /* DCID Sequence Number (i) */ + xqc_vint_write(dst_buf, dcid_seq_num, dcid_seq_num_bits, xqc_vint_len(dcid_seq_num_bits)); + dst_buf += xqc_vint_len(dcid_seq_num_bits); /* Path Status sequence number (i) */ xqc_vint_write(dst_buf, path_status_seq_num, path_status_seq_num_bits, xqc_vint_len(path_status_seq_num_bits)); @@ -2265,7 +2229,7 @@ xqc_gen_path_status_frame(xqc_packet_out_t *packet_out, xqc_int_t xqc_parse_path_status_frame(xqc_packet_in_t *packet_in, - uint64_t *path_id_type, uint64_t *path_id_content, + uint64_t *dcid_seq_num, uint64_t *path_status_seq_num, uint64_t *path_status) { unsigned char *p = packet_in->pos; @@ -2280,23 +2244,13 @@ xqc_parse_path_status_frame(xqc_packet_in_t *packet_in, } p += vlen; - /* Path Identifier Type (i) */ - vlen = xqc_vint_read(p, end, path_id_type); + /* DCID Sequence Number (i) */ + vlen = xqc_vint_read(p, end, dcid_seq_num); if (vlen < 0) { return -XQC_EVINTREAD; } p += vlen; - /* Path Identifier Content (i) */ - /* If Identifier Type is 2, the Path Identifier Content MUST be empty */ - if (*path_id_type != 0x02) { - vlen = xqc_vint_read(p, end, path_id_content); - if (vlen < 0) { - return -XQC_EVINTREAD; - } - p += vlen; - } - /* Path Status sequence number (i) */ vlen = xqc_vint_read(p, end, path_status_seq_num); if (vlen < 0) { @@ -2316,4 +2270,4 @@ xqc_parse_path_status_frame(xqc_packet_in_t *packet_in, packet_in->pi_frame_types |= XQC_FRAME_BIT_PATH_STATUS; return XQC_OK; -} \ No newline at end of file +} diff --git a/src/transport/xqc_frame_parser.h b/src/transport/xqc_frame_parser.h index f960a1c25..a663c6ed3 100644 --- a/src/transport/xqc_frame_parser.h +++ b/src/transport/xqc_frame_parser.h @@ -12,7 +12,17 @@ #include "src/transport/xqc_recv_record.h" #define XQC_PATH_CHALLENGE_DATA_LEN 8 +#define XQC_DATAGRAM_LENGTH_FIELD_BYTES 2 +#define XQC_DATAGRAM_HEADER_BYTES (XQC_DATAGRAM_LENGTH_FIELD_BYTES + 1) +/** + * generate datagram frame + */ +xqc_int_t xqc_gen_datagram_frame(xqc_packet_out_t *packet_out, + const unsigned char *payload, size_t size); + +xqc_int_t xqc_parse_datagram_frame(xqc_packet_in_t *packet_in, xqc_connection_t *conn, + unsigned char **buffer, size_t *size); /** * generate stream frame @@ -27,11 +37,11 @@ xqc_int_t xqc_parse_stream_frame(xqc_packet_in_t *packet_in, xqc_connection_t *c xqc_stream_frame_t *frame, xqc_stream_id_t *stream_id); ssize_t xqc_gen_crypto_frame(xqc_packet_out_t *packet_out, uint64_t offset, - const unsigned char *payload, size_t payload_size, size_t *written_size); + const unsigned char *payload, uint64_t payload_size, size_t *written_size); xqc_int_t xqc_parse_crypto_frame(xqc_packet_in_t *packet_in, xqc_connection_t *conn, xqc_stream_frame_t * frame); -void xqc_gen_padding_frame(xqc_packet_out_t *packet_out); +void xqc_gen_padding_frame(xqc_connection_t *conn, xqc_packet_out_t *packet_out); xqc_int_t xqc_parse_padding_frame(xqc_packet_in_t *packet_in, xqc_connection_t *conn); @@ -42,9 +52,6 @@ xqc_int_t xqc_parse_ping_frame(xqc_packet_in_t *packet_in, xqc_connection_t *con ssize_t xqc_gen_ack_frame(xqc_connection_t *conn, xqc_packet_out_t *packet_out, xqc_usec_t now, int ack_delay_exponent, xqc_recv_record_t *recv_record, xqc_usec_t largest_pkt_recv_time, int *has_gap, xqc_packet_number_t *largest_ack); -ssize_t xqc_gen_ack_frame_for_spns(xqc_connection_t *conn, xqc_packet_out_t *packet_out, xqc_usec_t now, int ack_delay_exponent, - xqc_recv_record_t *recv_record, xqc_usec_t largest_pkt_recv_time, int *has_gap, xqc_packet_number_t *largest_ack, xqc_packet_number_t must_ack); - xqc_int_t xqc_parse_ack_frame(xqc_packet_in_t *packet_in, xqc_connection_t *conn, xqc_ack_info_t *ack_info); ssize_t xqc_gen_conn_close_frame(xqc_packet_out_t *packet_out, uint64_t err_code, int is_app, int frame_type); @@ -95,7 +102,8 @@ ssize_t xqc_gen_handshake_done_frame(xqc_packet_out_t *packet_out); xqc_int_t xqc_parse_handshake_done_frame(xqc_packet_in_t *packet_in, xqc_connection_t *conn); -ssize_t xqc_gen_new_conn_id_frame(xqc_packet_out_t *packet_out, xqc_cid_t *new_cid, uint64_t retire_prior_to, char *key, size_t keylen); +ssize_t xqc_gen_new_conn_id_frame(xqc_packet_out_t *packet_out, xqc_cid_t *new_cid, + uint64_t retire_prior_to, const uint8_t *sr_token); xqc_int_t xqc_parse_new_conn_id_frame(xqc_packet_in_t *packet_in, xqc_cid_t *new_cid, uint64_t *retire_prior_to, xqc_connection_t *conn); @@ -115,20 +123,21 @@ ssize_t xqc_gen_ack_mp_frame(xqc_connection_t *conn, uint64_t path_id, xqc_packe int ack_delay_exponent, xqc_recv_record_t *recv_record, xqc_usec_t largest_pkt_recv_time, int *has_gap, xqc_packet_number_t *largest_ack); xqc_int_t xqc_parse_ack_mp_frame(xqc_packet_in_t *packet_in, xqc_connection_t *conn, - uint64_t *path_id, xqc_ack_info_t *ack_info); + uint64_t *dcid_seq_num, xqc_ack_info_t *ack_info); -ssize_t xqc_gen_path_abandon_frame(xqc_packet_out_t *packet_out, - uint64_t path_id_type, uint64_t path_id_content, uint64_t error_code); +ssize_t xqc_gen_path_abandon_frame(xqc_connection_t *conn, + xqc_packet_out_t *packet_out, uint64_t dcid_seq_num, uint64_t error_code); xqc_int_t xqc_parse_path_abandon_frame(xqc_packet_in_t *packet_in, - uint64_t *path_id_type, uint64_t *path_id_content, uint64_t *error_code); + uint64_t *dcid_seq_num, uint64_t *error_code); -ssize_t xqc_gen_path_status_frame(xqc_packet_out_t *packet_out, - uint64_t path_id_type, uint64_t path_id_content, +ssize_t xqc_gen_path_status_frame(xqc_connection_t *conn, + xqc_packet_out_t *packet_out, + uint64_t dcid_seq_num, uint64_t path_status_seq_num, uint64_t path_status); xqc_int_t xqc_parse_path_status_frame(xqc_packet_in_t *packet_in, - uint64_t *path_id_type, uint64_t *path_id_content, + uint64_t *dcid_seq_num, uint64_t *path_status_seq_num, uint64_t *path_status); #endif /*_XQC_FRAME_PARSER_H_INCLUDED_*/ diff --git a/src/transport/xqc_multipath.c b/src/transport/xqc_multipath.c index 90abbef19..2a4c6c776 100644 --- a/src/transport/xqc_multipath.c +++ b/src/transport/xqc_multipath.c @@ -12,6 +12,8 @@ #include "src/transport/xqc_wakeup_pq.h" #include "src/transport/xqc_packet_out.h" #include "src/transport/xqc_reinjection.h" +#include "src/transport/xqc_frame_parser.h" +#include "src/transport/xqc_datagram.h" #include "src/common/xqc_common.h" #include "src/common/xqc_malloc.h" @@ -82,8 +84,6 @@ xqc_path_create(xqc_connection_t *conn, xqc_cid_t *scid, xqc_cid_t *dcid) path->path_state = XQC_PATH_STATE_INIT; path->parent_conn = conn; - - path->tra_path_status = XQC_TRA_PATH_STATUS_IN_USE; path->app_path_status = XQC_APP_PATH_STATUS_AVAILABLE; path->app_path_status_send_seq_num = 0; path->app_path_status_recv_seq_num = 0; @@ -133,8 +133,11 @@ xqc_path_create(xqc_connection_t *conn, xqc_cid_t *scid, xqc_cid_t *dcid) xqc_cid_copy(&(path->path_dcid), dcid); } - path->path_id = path->path_scid.cid_seq_num; + //TODO: MPQUIC fix migration + path->path_id = conn->create_path_count; path->path_create_time = xqc_monotonic_timestamp(); + path->curr_pkt_out_size = conn->pkt_out_size; + path->path_max_pkt_out_size = conn->max_pkt_out_size; /* insert path to conn_paths_list */ xqc_list_add_tail(&path->path_list, &conn->conn_paths_list); @@ -188,7 +191,7 @@ xqc_path_init(xqc_path_ctx_t *path, xqc_connection_t *conn) } /* write path challenge frame & send immediately */ - ret = xqc_write_path_challenge_frame_to_packet(conn, path); + ret = xqc_write_path_challenge_frame_to_packet(conn, path, path->app_path_status == XQC_APP_PATH_STATUS_STANDBY); if (ret != XQC_OK) { xqc_log(conn->log, XQC_LOG_ERROR, "|xqc_write_path_challenge_frame_to_packet error|%d|", ret); return ret; @@ -197,8 +200,13 @@ xqc_path_init(xqc_path_ctx_t *path, xqc_connection_t *conn) xqc_set_path_state(path, XQC_PATH_STATE_VALIDATING); } - xqc_log(conn->engine->log, XQC_LOG_DEBUG, "|path:%ui|dcid:%s|scid:%s|state:%d|app_path_status:%d|tra_path_status:%d|", - path->path_id, xqc_dcid_str(&path->path_dcid), xqc_scid_str(&path->path_scid), path->path_state, path->app_path_status, path->tra_path_status); + xqc_log(conn->log, XQC_LOG_DEBUG, + "|path:%ui|conn_addr:%s|cp_addr_len:%d|path_addr:%s|pp_addr_len:%d|", + path->path_id, xqc_conn_addr_str(conn), conn->peer_addrlen, + xqc_path_addr_str(path), path->peer_addrlen); + + xqc_log(conn->engine->log, XQC_LOG_DEBUG, "|path:%ui|dcid:%s|scid:%s|state:%d|", + path->path_id, xqc_dcid_str(&path->path_dcid), xqc_scid_str(&path->path_scid), path->path_state); return XQC_OK; } @@ -212,18 +220,45 @@ xqc_path_move_unack_packets_from_conn(xqc_path_ctx_t *path, xqc_connection_t *co xqc_list_head_t *pos, *next; xqc_packet_out_t *po = NULL; uint64_t closing_path_id = path->path_id; + xqc_int_t repair_dgram = 0; xqc_list_for_each_safe(pos, next, &conn->conn_send_queue->sndq_unacked_packets[XQC_PNS_APP_DATA]) { po = xqc_list_entry(pos, xqc_packet_out_t, po_list); + repair_dgram = 0; - if (xqc_send_ctl_indirectly_ack_po(conn, po)) { + if (xqc_send_ctl_indirectly_ack_or_drop_po(conn, po)) { continue; } if (po->po_path_id == closing_path_id) { if (po->po_flag & XQC_POF_IN_FLIGHT) { xqc_send_ctl_decrease_inflight(conn, po); - xqc_send_queue_copy_to_lost(po, conn->conn_send_queue); + + if (po->po_frame_types & XQC_FRAME_BIT_DATAGRAM) { + path->path_send_ctl->ctl_lost_dgram_cnt++; + repair_dgram = xqc_datagram_notify_loss(conn, po); + if (conn->conn_settings.datagram_force_retrans_on) { + repair_dgram = XQC_DGRAM_RETX_ASKED_BY_APP; + } + } + + if (XQC_NEED_REPAIR(po->po_frame_types) + || repair_dgram == XQC_DGRAM_RETX_ASKED_BY_APP) + { + xqc_send_queue_copy_to_lost(po, conn->conn_send_queue); + + } else { + /* for datagram, we should remove all copies in the unacked list */ + if (po->po_frame_types & XQC_FRAME_BIT_DATAGRAM) { + xqc_send_ctl_on_dgram_dropped(conn, po); + xqc_send_queue_maybe_remove_unacked(po, conn->conn_send_queue, NULL); + + } else { + /* if a packet needs no retransmission, we remove it. */ + xqc_send_queue_remove_unacked(po, conn->conn_send_queue); + xqc_send_queue_insert_free(po, &conn->conn_send_queue->sndq_free_packets, conn->conn_send_queue); + } + } } } } @@ -240,17 +275,9 @@ xqc_set_path_state(xqc_path_ctx_t *path, xqc_path_state_t dst_state) if (path->path_state == XQC_PATH_STATE_ACTIVE) { conn->active_path_count--; - - if (path->tra_path_status == XQC_TRA_PATH_STATUS_IN_USE) { - conn->in_use_active_path_count--; - } } else if (dst_state == XQC_PATH_STATE_ACTIVE) { conn->active_path_count++; - - if (path->tra_path_status == XQC_TRA_PATH_STATUS_IN_USE) { - conn->in_use_active_path_count++; - } } path->path_state = dst_state; @@ -259,13 +286,14 @@ xqc_set_path_state(xqc_path_ctx_t *path, xqc_path_state_t dst_state) xqc_int_t xqc_path_immediate_close(xqc_path_ctx_t *path) { - if (path->path_state >= XQC_PATH_STATE_DRAINING) { + if (path->path_state >= XQC_PATH_STATE_CLOSING) { return XQC_OK; } xqc_connection_t *conn = path->parent_conn; - - xqc_int_t ret = xqc_write_path_abandon_frame_to_packet(conn, path); + xqc_int_t ret = XQC_OK; + + ret = xqc_write_path_abandon_frame_to_packet(conn, path); if (ret != XQC_OK) { xqc_log(conn->log, XQC_LOG_ERROR, "|xqc_write_path_abandon_frame_to_packet error|ret:%d|", ret); } @@ -279,6 +307,11 @@ xqc_path_immediate_close(xqc_path_ctx_t *path) /* 将已经分配到该路径但还未发送的包 放回原路径级别队列进行重新分配 (区分 lost/pto/send) */ xqc_path_send_buffer_clear(conn, path, NULL, type); } + + /* try to update MSS */ + if (conn->conn_settings.enable_pmtud) { + xqc_conn_try_to_update_mss(conn); + } xqc_usec_t now = xqc_monotonic_timestamp(); xqc_usec_t pto = xqc_conn_get_max_pto(conn); @@ -315,25 +348,10 @@ xqc_path_closed(xqc_path_ctx_t *path) return XQC_OK; } -void -xqc_path_abandon_acked(xqc_connection_t *conn, uint64_t path_id) -{ - xqc_path_ctx_t *path = xqc_conn_find_path_by_path_id(conn, path_id); - if (path == NULL) { - return; - } - - if (path->path_state < XQC_PATH_STATE_CLOSED) { - xqc_log(conn->log, XQC_LOG_DEBUG, "|close path:%ui|", path->path_id); - xqc_path_closed(path); - } -} - - /** * Check whether the connection supports multi-path or not. * @param conn connection context - * @return enable_multipath 0:not support, 1:SPNS, 2:MPNS + * @return enable_multipath 0:not support, 1:MPNS */ xqc_multipath_mode_t xqc_conn_enable_multipath(xqc_connection_t *conn) @@ -341,19 +359,62 @@ xqc_conn_enable_multipath(xqc_connection_t *conn) if ((conn->local_settings.enable_multipath == 1) && (conn->remote_settings.enable_multipath == 1)) { + if (conn->dcid_set.current_dcid.cid_len == 0 + || conn->scid_set.user_scid.cid_len == 0) + { + xqc_log(conn->log, XQC_LOG_ERROR, + "|mutlipath is not possible for connections" + " with zero-length DCID|"); + if (conn->conn_settings.multipath_version == XQC_MULTIPATH_04) { + XQC_CONN_ERR(conn, TRA_TRANSPORT_PARAMETER_ERROR); + + } else { + XQC_CONN_ERR(conn, TRA_MP_PROTOCOL_VIOLATION_05); + } + return XQC_CONN_NOT_SUPPORT_MULTIPATH; + } return XQC_CONN_MULTIPATH_MULTIPLE_PNS; } - return XQC_CONN_NOT_SUPPORT_MULTIPATH; +} +xqc_multipath_version_t +xqc_conn_multipath_version_negotiation(xqc_connection_t *conn) +{ + if (xqc_conn_is_current_mp_version_supported(conn->remote_settings.multipath_version) == XQC_OK && + conn->local_settings.multipath_version == conn->remote_settings.multipath_version) + { + xqc_log(conn->log, XQC_LOG_DEBUG, + "|multipath version negotiation succeed on multipath 0%d|", conn->remote_settings.multipath_version); + return conn->remote_settings.multipath_version; + } + return XQC_ERR_MULTIPATH_VERSION; } +xqc_int_t +xqc_conn_is_current_mp_version_supported(xqc_multipath_version_t mp_version) +{ + xqc_int_t ret; + switch (mp_version) { + case XQC_MULTIPATH_04: + ret = XQC_OK; + break; + case XQC_MULTIPATH_05: + ret = XQC_OK; + break; + default: + ret = -XQC_EMP_INVALID_MP_VERTION; + break; + } + return ret; +} xqc_int_t -xqc_conn_create_path(xqc_engine_t *engine, const xqc_cid_t *scid, uint64_t *new_path_id) +xqc_conn_create_path(xqc_engine_t *engine, const xqc_cid_t *scid, uint64_t *new_path_id, int path_status) { xqc_connection_t *conn = NULL; xqc_path_ctx_t *path = NULL; + xqc_app_path_status_t ps_inner = XQC_APP_PATH_STATUS_AVAILABLE; conn = xqc_engine_conns_hash_find(engine, scid, 's'); if (!conn) { @@ -373,12 +434,24 @@ xqc_conn_create_path(xqc_engine_t *engine, const xqc_cid_t *scid, uint64_t *new_ /* must have at least one available unused scid & dcid */ if (xqc_conn_check_unused_cids(conn) != XQC_OK) { + if (conn->dcid_set.cid_set.unused_cnt == 0) { + conn->conn_flag |= XQC_CONN_FLAG_MP_WAIT_DCID; + } + + if (conn->scid_set.cid_set.unused_cnt == 0) { + conn->conn_flag |= XQC_CONN_FLAG_MP_WAIT_SCID; + } + xqc_log(conn->log, XQC_LOG_WARN, "|don't have available cid for new path|"); return -XQC_EMP_NO_AVAIL_PATH_ID; } - path = xqc_conn_create_path_inner(conn, NULL, NULL); + if (path_status == XQC_APP_PATH_STATUS_STANDBY) { + ps_inner = XQC_APP_PATH_STATUS_STANDBY; + } + + path = xqc_conn_create_path_inner(conn, NULL, NULL, ps_inner); if (path == NULL) { xqc_log(conn->log, XQC_LOG_ERROR, "|xqc_path_create error|"); return -XQC_EMP_CREATE_PATH; @@ -440,6 +513,14 @@ xqc_conn_close_path(xqc_engine_t *engine, const xqc_cid_t *scid, uint64_t closed return ret; } + if (!(conn->conn_flag & XQC_CONN_FLAG_TICKING)) { + if (0 == xqc_conns_pq_push(conn->engine->conns_active_pq, conn, conn->last_ticked_time)) { + conn->conn_flag |= XQC_CONN_FLAG_TICKING; + } + } + + xqc_engine_main_logic_internal(engine); + return XQC_OK; } @@ -450,7 +531,8 @@ xqc_conn_init_paths_list(xqc_connection_t *conn) conn->conn_initial_path = xqc_conn_create_path_inner(conn, &conn->scid_set.user_scid, - &conn->dcid_set.current_dcid); + &conn->dcid_set.current_dcid, + XQC_APP_PATH_STATUS_AVAILABLE); if (conn->conn_initial_path == NULL) { xqc_log(conn->log, XQC_LOG_ERROR, "|xqc_conn_create_path_inner fail|"); return -XQC_EMP_CREATE_PATH; @@ -471,6 +553,22 @@ xqc_conn_destroy_paths_list(xqc_connection_t *conn) } } +xqc_path_ctx_t * +xqc_conn_find_path_by_dcid_seq(xqc_connection_t *conn, uint64_t dcid_seq_num) +{ + xqc_path_ctx_t *path = NULL; + xqc_list_head_t *pos, *next; + + xqc_list_for_each_safe(pos, next, &conn->conn_paths_list) { + path = xqc_list_entry(pos, xqc_path_ctx_t, path_list); + + if (path->path_dcid.cid_seq_num == dcid_seq_num) { + return path; + } + } + + return NULL; +} xqc_path_ctx_t * @@ -532,7 +630,7 @@ xqc_conn_find_path_by_dcid(xqc_connection_t *conn, xqc_cid_t *dcid) xqc_path_ctx_t * xqc_conn_create_path_inner(xqc_connection_t *conn, - xqc_cid_t *scid, xqc_cid_t *dcid) + xqc_cid_t *scid, xqc_cid_t *dcid, xqc_app_path_status_t path_status) { xqc_int_t ret = XQC_ERROR; xqc_path_ctx_t *path = NULL; @@ -543,6 +641,8 @@ xqc_conn_create_path_inner(xqc_connection_t *conn, return NULL; } + path->app_path_status = path_status; + ret = xqc_path_init(path, conn); if (ret != XQC_OK) { xqc_log(conn->log, XQC_LOG_ERROR, "|xqc_path_init error|%d|", ret); @@ -595,36 +695,66 @@ xqc_request_path_metrics_print(xqc_connection_t *conn, xqc_h3_stream_t *h3_strea stats->mp_default_path_send_weight = 1.0; stats->mp_default_path_recv_weight = 1.0; - if (!conn->enable_multipath) { - return; - } + stats->mp_standby_path_send_weight = 0.0; + stats->mp_standby_path_recv_weight = 0.0; - if (conn->validated_path_count < 2) { - return; - } + int available_path_cnt = 0, standby_path_cnt = 0; - int aggregate_cnt = 0; - uint64_t all_path_send_bytes = 0, all_path_recv_bytes = 0; + uint64_t aggregate_send_bytes = 0, aggregate_recv_bytes = 0; + uint64_t standby_path_send_bytes = 0, standby_path_recv_bytes = 0; - for (int i = 0; i < XQC_MAX_PATHS_COUNT; ++i) { - if (h3_stream->paths_info[i].path_pkt_recv_count > 0 - || h3_stream->paths_info[i].path_pkt_send_count > 0) - { - aggregate_cnt++; + xqc_list_head_t *pos, *next; + xqc_path_ctx_t *path; + xqc_list_for_each_safe(pos, next, &conn->conn_paths_list) { + path = xqc_list_entry(pos, xqc_path_ctx_t, path_list); - all_path_send_bytes += h3_stream->paths_info[i].path_send_bytes; - all_path_recv_bytes += h3_stream->paths_info[i].path_recv_bytes; + if (path->path_id < XQC_MAX_PATHS_COUNT + && path->path_id == h3_stream->paths_info[path->path_id].path_id) + { + uint64_t send_bytes = h3_stream->paths_info[path->path_id].path_send_bytes; + uint64_t recv_bytes = h3_stream->paths_info[path->path_id].path_recv_bytes; + + h3_stream->paths_info[path->path_id].path_srtt = path->path_send_ctl->ctl_srtt; + h3_stream->paths_info[path->path_id].path_app_status = path->app_path_status; + + if (send_bytes > 0 || recv_bytes > 0) { + aggregate_send_bytes += send_bytes; + aggregate_recv_bytes += recv_bytes; + + if (path->app_path_status == XQC_APP_PATH_STATUS_STANDBY) { + standby_path_cnt++; + standby_path_send_bytes += send_bytes; + standby_path_recv_bytes += recv_bytes; + } else { + available_path_cnt++; + } + } } } - stats->mp_state = (aggregate_cnt > 1) ? 1 : 2; + if (conn->enable_multipath && conn->active_path_count >= 2) { + if ((available_path_cnt > 0) && (standby_path_cnt > 0)) { + stats->mp_state = 1; + + } else if ((available_path_cnt == 0) && (standby_path_cnt > 0)) { + stats->mp_state = 2; + + } else if ((available_path_cnt > 0) && (standby_path_cnt == 0)) { + stats->mp_state = 3; + } + + } else { + stats->mp_state = 0; + } - if (all_path_send_bytes != 0) { - stats->mp_default_path_send_weight = (float)(h3_stream->paths_info[XQC_INITIAL_PATH_ID].path_send_bytes) / all_path_send_bytes; + if (aggregate_send_bytes != 0) { + stats->mp_standby_path_send_weight = (float)(standby_path_send_bytes) / aggregate_send_bytes; + stats->mp_default_path_send_weight = 1.0 - stats->mp_standby_path_send_weight; } - if (all_path_recv_bytes != 0) { - stats->mp_default_path_recv_weight = (float)(h3_stream->paths_info[XQC_INITIAL_PATH_ID].path_recv_bytes) / all_path_recv_bytes; + if (aggregate_recv_bytes != 0) { + stats->mp_standby_path_recv_weight = (float)(standby_path_recv_bytes) / aggregate_recv_bytes; + stats->mp_default_path_recv_weight = 1.0 - stats->mp_standby_path_recv_weight; } } @@ -693,7 +823,9 @@ void xqc_stream_path_metrics_on_send(xqc_connection_t *conn, xqc_packet_out_t *po) { for (int i = 0; i < XQC_MAX_STREAM_FRAME_IN_PO; i++) { - if (po->po_stream_frames[i].ps_is_used == 1) { + if (po->po_stream_frames[i].ps_is_used == 1 + && po->po_stream_frames[i].ps_is_reset == 0) + { xqc_stream_t * stream = xqc_find_stream_by_id(po->po_stream_frames[i].ps_stream_id, conn->streams_hash); if (stream != NULL && po->po_path_id < XQC_MAX_PATHS_COUNT) { @@ -705,6 +837,11 @@ xqc_stream_path_metrics_on_send(xqc_connection_t *conn, xqc_packet_out_t *po) stream->paths_info[po->po_path_id].path_send_reinject_bytes += po->po_used_size; } } + + conn->stream_stats.send_bytes += po->po_stream_frames[i].ps_length; + if (po->po_flag & XQC_POF_REINJECTED_REPLICA) { + conn->stream_stats.reinjected_bytes += po->po_stream_frames[i].ps_length; + } } } } @@ -737,7 +874,7 @@ xqc_path_metrics_print(xqc_connection_t *conn, char *buff, unsigned buff_size) // TODO 关于数据抓取说明 ret = snprintf(buff + cursor, buff_size - cursor, "#%"PRIu64"-%d-%d-%"PRIu64"-%.4f-%"PRIu32"-%"PRIu32"-%"PRIu32"-%"PRIu32"-%"PRIu32"" - "-%"PRIu32"-%"PRIu32"-%"PRIu32"-%"PRIu64"-%"PRIu64"-%"PRIu32"-%"PRIu32"-%"PRIu32, + "-%"PRIu32"-%"PRIu32"-%"PRIu32"-%"PRIu64"-%"PRIu64"-%"PRIu32"-%"PRIu32, path->path_id, path->path_state, path->app_path_status, @@ -754,42 +891,13 @@ xqc_path_metrics_print(xqc_connection_t *conn, char *buff, unsigned buff_size) path->path_send_ctl->ctl_bytes_send, path->path_send_ctl->ctl_bytes_recv, path->standby_probe_count, - path->app_path_status_changed_count, - path->tra_path_status_changed_count); + path->app_path_status_changed_count); cursor += ret; } } } -void -xqc_h3s_path_metrics_print(xqc_h3_stream_t *h3_stream, char *buff, unsigned buff_size) -{ - int cursor = 0; - int ret = 0; - - for (int i = 0; i < XQC_MAX_PATHS_COUNT; ++i) { - - if (h3_stream->paths_info[i].path_id == XQC_MAX_UINT64_VALUE) { - continue; - } - - if (cursor >= (buff_size - 100)) { // enough space - break; - } - - ret = snprintf(buff + cursor, buff_size - cursor, - "#%"PRIu64"-%"PRIu64"-%"PRIu64"-%"PRIu64"-%"PRIu64, - h3_stream->paths_info[i].path_id, - h3_stream->paths_info[i].path_pkt_send_count, - h3_stream->paths_info[i].path_pkt_recv_count, - h3_stream->paths_info[i].path_send_bytes, - h3_stream->paths_info[i].path_send_reinject_bytes); - - cursor += ret; - } -} - void xqc_path_send_buffer_append(xqc_path_ctx_t *path, xqc_packet_out_t *packet_out, xqc_list_head_t *head) @@ -837,11 +945,7 @@ xqc_path_send_buffer_clear(xqc_connection_t *conn, xqc_path_ctx_t *path, xqc_lis packet_out = xqc_list_entry(pos, xqc_packet_out_t, po_list); xqc_path_send_buffer_remove(path, packet_out); - if (packet_out->po_flag & XQC_POF_REINJECTED_REPLICA) { - xqc_disassociate_packet_with_reinjection(packet_out->po_reinj_origin, packet_out); - xqc_send_queue_insert_free(packet_out, &send_queue->sndq_free_packets, send_queue); - - } else if (head != NULL) { + if (head != NULL) { /* remove from path scheduled buffer & add to the head of conn send queue */ xqc_send_queue_move_to_head(&packet_out->po_list, head); @@ -943,48 +1047,65 @@ xqc_conn_server_init_path_addr(xqc_connection_t *conn, uint64_t path_id, return -XQC_EMP_PATH_STATE_ERROR; } - xqc_list_head_t *pos, *next; - xqc_path_ctx_t *active_path = NULL; - struct sockaddr *existed_addr = NULL; - xqc_list_for_each_safe(pos, next, &conn->conn_paths_list) { - active_path = xqc_list_entry(pos, xqc_path_ctx_t, path_list); - if (active_path->path_state != XQC_PATH_STATE_ACTIVE) { - continue; - } + if (local_addr && local_addrlen > 0) { + ret = xqc_memcpy_with_cap(path->local_addr, sizeof(path->local_addr), local_addr, local_addrlen); + if (ret == XQC_OK) { + path->local_addrlen = local_addrlen; + path->addr_str_len = 0; - /* check if ip address is same with sub-connections created */ - if (xqc_is_same_addr(peer_addr, (struct sockaddr *)active_path->peer_addr)) { - xqc_write_path_abandon_frame_to_packet(conn, path); - xqc_log(conn->engine->log, XQC_LOG_STATS, "|MP|path:%ui|conn:%s|cannot activate this path, due to the same IP|curIP:%s|conflictIP:%s|", - path_id, xqc_conn_addr_str(conn), - xqc_peer_addr_str((struct sockaddr*)peer_addr, conn->peer_addrlen), - xqc_local_addr_str((struct sockaddr*)active_path->peer_addr, active_path->peer_addrlen)); - return XQC_OK; + } else { + xqc_log(conn->log, XQC_LOG_ERROR, + "|local addr too large|addr_len:%d|", (int)local_addrlen); + return -XQC_ENOBUF; } - } - - if (local_addr && local_addrlen > 0) { - xqc_memcpy(path->local_addr, local_addr, local_addrlen); - path->local_addrlen = local_addrlen; + } if (peer_addr && peer_addrlen > 0) { - xqc_memcpy(path->peer_addr, peer_addr, peer_addrlen); - path->peer_addrlen = peer_addrlen; - } + ret = xqc_memcpy_with_cap(path->peer_addr, sizeof(path->peer_addr), peer_addr, peer_addrlen); + if (ret == XQC_OK) { + path->peer_addrlen = peer_addrlen; + path->addr_str_len = 0; - /* notify and create the path context for user layer */ - if (path_id != XQC_INITIAL_PATH_ID - && conn->transport_cbs.path_created_notify) - { - ret = conn->transport_cbs.path_created_notify(conn, &conn->scid_set.user_scid, - path->path_id, xqc_conn_get_user_data(conn)); - if (ret != XQC_OK) { - xqc_log(conn->log, XQC_LOG_WARN, "|path_created_notify fail|path:%ui|", path->path_id); - return ret; + } else { + xqc_log(conn->log, XQC_LOG_ERROR, + "|peer addr too large|addr_len:%d|", (int)peer_addrlen); + return -XQC_ENOBUF; } } + if (path_id != XQC_INITIAL_PATH_ID) { + xqc_list_head_t *pos, *next; + xqc_path_ctx_t *active_path = NULL; + struct sockaddr *existed_addr = NULL; + xqc_list_for_each_safe(pos, next, &conn->conn_paths_list) { + active_path = xqc_list_entry(pos, xqc_path_ctx_t, path_list); + if (active_path->path_state != XQC_PATH_STATE_ACTIVE) { + continue; + } + + /* check if ip address is same with sub-connections created */ + if (xqc_is_same_addr(peer_addr, (struct sockaddr *)active_path->peer_addr)) { + xqc_path_immediate_close(path); + xqc_log(conn->engine->log, XQC_LOG_STATS, "|MP|path:%ui|conn:%s|cannot activate this path, due to the same IP|curIP:%s|conflictIP:%s|", + path_id, xqc_conn_addr_str(conn), + xqc_peer_addr_str((struct sockaddr*)peer_addr, conn->peer_addrlen), + xqc_local_addr_str((struct sockaddr*)active_path->peer_addr, active_path->peer_addrlen)); + return XQC_OK; + } + } + + /* notify and create the path context for user layer */ + if (conn->transport_cbs.path_created_notify) { + ret = conn->transport_cbs.path_created_notify(conn, &conn->scid_set.user_scid, + path->path_id, xqc_conn_get_user_data(conn)); + if (ret != XQC_OK) { + xqc_log(conn->log, XQC_LOG_WARN, "|path_created_notify fail|path:%ui|", path->path_id); + return ret; + } + } + } + xqc_log(conn->engine->log, XQC_LOG_STATS, "|path:%ui|%s|", path_id, xqc_path_addr_str(path)); return XQC_OK; @@ -1006,6 +1127,11 @@ xqc_conn_client_init_path_addr(xqc_connection_t *conn) path->local_addrlen = conn->local_addrlen; } + xqc_log(conn->log, XQC_LOG_DEBUG, + "|path:%ui|conn_addr:%s|cp_addr_len:%d|path_addr:%s|pp_addr_len:%d|", + path->path_id, xqc_conn_addr_str(conn), conn->peer_addrlen, + xqc_path_addr_str(path), path->peer_addrlen); + return XQC_OK; } @@ -1025,14 +1151,29 @@ xqc_path_validate(xqc_path_ctx_t *path) xqc_set_path_state(path, XQC_PATH_STATE_ACTIVE); path->parent_conn->validated_path_count++; + xqc_log(conn->log, XQC_LOG_DEBUG, + "|path validated|path_id:%ui|validated_path_count:%ud|", + path->path_id, path->parent_conn->validated_path_count); + if (path->path_flag & XQC_PATH_FLAG_SEND_STATUS) { path->path_flag &= ~XQC_PATH_FLAG_SEND_STATUS; - - xqc_int_t ret = xqc_write_path_status_frame_to_packet(conn, path); + xqc_int_t ret = xqc_set_application_path_status(path, path->next_app_path_state, XQC_TRUE); if (ret != XQC_OK) { - xqc_log(conn->log, XQC_LOG_ERROR, "|xqc_write_path_status_frame_to_packet error|%d|", ret); + xqc_log(conn->log, XQC_LOG_ERROR, "|xqc_write_path_status_frame_to_packet error|"); } } + + if (path->path_flag & XQC_PATH_FLAG_RECV_STATUS) { + path->path_flag &= ~XQC_PATH_FLAG_RECV_STATUS; + xqc_set_application_path_status(path, path->next_app_path_state, XQC_FALSE); + } + + /* PMTUD: launch probing immediately */ + if (conn->conn_settings.enable_pmtud) { + conn->probing_cnt = 0; + conn->conn_flag |= XQC_CONN_FLAG_PMTUD_PROBING; + xqc_timer_unset(&conn->conn_timer_manager, XQC_TIMER_PMTUD_PROBING); + } } } @@ -1098,7 +1239,6 @@ xqc_path_record_info(xqc_path_ctx_t *path, xqc_path_info_t *path_info) path_info->path_id = path->path_id; path_info->path_state = (uint8_t)path->path_state; path_info->app_path_status = (uint8_t)path->app_path_status; - path_info->tra_path_status = (uint8_t)path->tra_path_status; path_info->path_bytes_send = path->path_send_ctl->ctl_bytes_send; path_info->path_bytes_recv = path->path_send_ctl->ctl_bytes_recv; @@ -1109,39 +1249,16 @@ xqc_path_record_info(xqc_path_ctx_t *path, xqc_path_info_t *path_info) path_info->standby_probe_count = path->standby_probe_count; path_info->app_path_status_changed_count = path->app_path_status_changed_count; - path_info->tra_path_status_changed_count = path->tra_path_status_changed_count; -} - -size_t -xqc_path_info_print(xqc_path_info_t *path_info, char **buff, size_t *buff_size) -{ - int ret = 0; - - if (*buff == NULL || *buff_size <= 0) { - goto end; - } - - ret = snprintf(*buff, *buff_size, "#%"PRIu64"-%d-%d-%d-%"PRIu64"-%"PRIu64"-%"PRIu64"-%"PRIu64"-%d-%d-%d", - path_info->path_id, - path_info->path_state, - path_info->app_path_status, - path_info->tra_path_status, - path_info->path_bytes_send, - path_info->path_bytes_recv, - path_info->path_create_time, - path_info->path_destroy_time, - path_info->standby_probe_count, - path_info->app_path_status_changed_count, - path_info->tra_path_status_changed_count); - if (ret <= 0) { - goto end; - } - - *buff += ret; - *buff_size -= ret; -end: - return ret; + path_info->pkt_recv_cnt = path->path_send_ctl->ctl_recv_count; + path_info->pkt_send_cnt = path->path_send_ctl->ctl_send_count; + path_info->dgram_recv_cnt = path->path_send_ctl->ctl_dgram_recv_count; + path_info->dgram_send_cnt = path->path_send_ctl->ctl_dgram_send_count; + path_info->red_dgram_recv_cnt = path->path_send_ctl->ctl_reinj_dgram_recv_count; + path_info->red_dgram_send_cnt = path->path_send_ctl->ctl_reinj_dgram_send_count; + path_info->srtt = path->path_send_ctl->ctl_srtt; + path_info->loss_cnt = path->path_send_ctl->ctl_lost_count; + path_info->tlp_cnt = path->path_send_ctl->ctl_tlp_count; } xqc_bool_t @@ -1152,58 +1269,86 @@ xqc_path_is_full(xqc_path_ctx_t *path) uint64_t cwnd = ctl->ctl_cong_callback->xqc_cong_ctl_get_cwnd(ctl->ctl_cong); return (bytes_on_path + xqc_conn_get_mss(path->parent_conn)) > cwnd; } -void -xqc_set_application_path_status(xqc_path_ctx_t *path, xqc_app_path_status_t status, xqc_usec_t now) + +xqc_int_t +xqc_set_application_path_status(xqc_path_ctx_t *path, xqc_app_path_status_t status, xqc_bool_t is_tx) { - if (path->app_path_status == status) { - return; - } + if (path->app_path_status != status + && status > XQC_APP_PATH_STATUS_NONE + && status < XQC_APP_PATH_STATUS_MAX) + { + xqc_app_path_status_t last_status = path->app_path_status; + xqc_connection_t *conn = path->parent_conn; - xqc_log(path->parent_conn->log, XQC_LOG_DEBUG, "|path:%ui|app_path_status:%d->%d|", path->path_id, path->app_path_status, status); - path->app_path_status = status; + path->app_path_status = status; - path->app_path_status_changed_count++; - path->last_app_path_status_changed_time = now; - xqc_set_transport_path_status(path, (xqc_tra_path_status_t)status, now); -} - -void xqc_set_transport_path_status(xqc_path_ctx_t *path, xqc_tra_path_status_t status, xqc_usec_t now) -{ - if (path->tra_path_status == status) { - return; + if (is_tx) { + xqc_int_t ret = xqc_write_path_status_frame_to_packet(conn, path); + if (ret != XQC_OK) { + path->app_path_status = last_status; + xqc_log(conn->log, XQC_LOG_ERROR, "|xqc_write_path_status_frame_to_packet error|%d|", ret); + return ret; + } + } + xqc_log(conn->log, XQC_LOG_DEBUG, "|path:%ui|app_path_status:%d->%d|", path->path_id, last_status, status); + path->app_path_status_changed_count++; + path->last_app_path_status_changed_time = xqc_monotonic_timestamp(); + + if (status == XQC_APP_PATH_STATUS_FROZEN) { + xqc_path_move_unack_packets_from_conn(path, conn); + for (xqc_send_type_t type = 0; type < XQC_SEND_TYPE_N; type++) { + xqc_path_send_buffer_clear(conn, path, NULL, type); + } + } } + + return XQC_OK; +} - xqc_connection_t *conn = path->parent_conn; - if (path->path_state == XQC_PATH_STATE_ACTIVE) { +xqc_int_t +xqc_conn_mark_path_standby(xqc_engine_t *engine, const xqc_cid_t *cid, uint64_t path_id) +{ + xqc_connection_t *conn = NULL; + xqc_path_ctx_t *path = NULL; - if ((path->tra_path_status == XQC_TRA_PATH_STATUS_IN_USE) - && (status != XQC_TRA_PATH_STATUS_IN_USE)) - { - conn->in_use_active_path_count--; + conn = xqc_engine_conns_hash_find(engine, cid, 's'); + if (!conn) { + xqc_log(engine->log, XQC_LOG_ERROR, "|can not find connection|"); + return -XQC_ECONN_NFOUND; + } + if (conn->conn_state >= XQC_CONN_STATE_CLOSING) { + return -XQC_CLOSING; + } - } else if ((path->tra_path_status != XQC_TRA_PATH_STATUS_IN_USE) - && (status == XQC_TRA_PATH_STATUS_IN_USE)) - { - conn->in_use_active_path_count++; - } + /* check mp-support */ + if (!conn->enable_multipath) { + xqc_log(engine->log, XQC_LOG_WARN, + "|Multipath is not supported in connection|%p|", conn); + return -XQC_EMP_NOT_SUPPORT_MP; + } + /* find path */ + path = xqc_conn_find_path_by_path_id(conn, path_id); + if (path == NULL) { + xqc_log(engine->log, XQC_LOG_WARN, + "|path is not found by path_id in connection|%p|%ui|", + conn, path_id); + return -XQC_EMP_PATH_NOT_FOUND; } - xqc_log(conn->log, XQC_LOG_DEBUG, "|path:%ui|tra_path_status:%d->%d|", path->path_id, path->tra_path_status, status); - path->tra_path_status = status; + path->next_app_path_state = XQC_APP_PATH_STATUS_STANDBY; - path->tra_path_status_changed_count++; - path->last_tra_path_status_changed_time = now; + if (path->path_state < XQC_PATH_STATE_ACTIVE) { + path->path_flag |= XQC_PATH_FLAG_SEND_STATUS; + return XQC_OK; + } - xqc_send_ctl_t *send_ctl = path->path_send_ctl; - send_ctl->ctl_pto_count_since_last_tra_path_status_changed = 0; - send_ctl->ctl_send_count_at_last_tra_path_status_changed_time = send_ctl->ctl_send_count; + return xqc_set_application_path_status(path, path->next_app_path_state, XQC_TRUE); } - -xqc_int_t -xqc_conn_mark_path_standby(xqc_engine_t *engine, const xqc_cid_t *cid, uint64_t path_id) +xqc_int_t +xqc_conn_mark_path_frozen(xqc_engine_t *engine, const xqc_cid_t *cid, uint64_t path_id) { xqc_connection_t *conn = NULL; xqc_path_ctx_t *path = NULL; @@ -1233,21 +1378,23 @@ xqc_conn_mark_path_standby(xqc_engine_t *engine, const xqc_cid_t *cid, uint64_t return -XQC_EMP_PATH_NOT_FOUND; } - xqc_usec_t now = xqc_monotonic_timestamp(); - xqc_set_application_path_status(path, XQC_APP_PATH_STATUS_STANDBY, now); + /* do not freeze the only active path */ + if (path->path_state == XQC_PATH_STATE_ACTIVE + && conn->active_path_count == 1) + { + xqc_log(conn->log, XQC_LOG_WARN, + "|can not freeze the only active path|path:%ui", path_id); + return -XQC_EMP_NO_ACTIVE_PATH; + } + + path->next_app_path_state = XQC_APP_PATH_STATUS_FROZEN; if (path->path_state < XQC_PATH_STATE_ACTIVE) { path->path_flag |= XQC_PATH_FLAG_SEND_STATUS; return XQC_OK; } - xqc_int_t ret = xqc_write_path_status_frame_to_packet(conn, path); - if (ret != XQC_OK) { - xqc_log(conn->log, XQC_LOG_ERROR, "|xqc_write_path_status_frame_to_packet error|%d|", ret); - return ret; - } - - return XQC_OK; + return xqc_set_application_path_status(path, path->next_app_path_state, XQC_TRUE); } @@ -1282,21 +1429,14 @@ xqc_conn_mark_path_available(xqc_engine_t *engine, const xqc_cid_t *cid, uint64_ return -XQC_EMP_PATH_NOT_FOUND; } - xqc_usec_t now = xqc_monotonic_timestamp(); - xqc_set_application_path_status(path, XQC_APP_PATH_STATUS_AVAILABLE, now); + path->next_app_path_state = XQC_APP_PATH_STATUS_AVAILABLE; if (path->path_state < XQC_PATH_STATE_ACTIVE) { path->path_flag |= XQC_PATH_FLAG_SEND_STATUS; return XQC_OK; } - xqc_int_t ret = xqc_write_path_status_frame_to_packet(conn, path); - if (ret != XQC_OK) { - xqc_log(conn->log, XQC_LOG_ERROR, "|xqc_write_path_status_frame_to_packet error|%d|", ret); - return ret; - } - - return XQC_OK; + return xqc_set_application_path_status(path, path->next_app_path_state, XQC_TRUE); } @@ -1305,43 +1445,13 @@ xqc_path_standby_probe(xqc_path_ctx_t *path) { xqc_connection_t *conn = path->parent_conn; - xqc_int_t ret = xqc_path_send_ping_to_probe(path, XQC_PNS_APP_DATA); + xqc_int_t ret = xqc_path_send_ping_to_probe(path, XQC_PNS_APP_DATA, + XQC_PATH_SPECIFIED_BY_PQP); if (ret != XQC_OK) { - xqc_log(conn->log, XQC_LOG_ERROR, "|xqc_path_send_ping_to_probe error|path:%ui|ret:%d|", path->path_id, ret); return ret; } + xqc_log(conn->log, XQC_LOG_DEBUG, "|PING|path:%ui|", path->path_id); path->standby_probe_count++; - xqc_log(conn->log, XQC_LOG_DEBUG, "|conn:%p|path:%ui|PING|conn_state:%s|", conn, path->path_id, xqc_conn_state_2_str(conn->conn_state)); - return XQC_OK; -} - -xqc_path_stats_t -xqc_path_get_stats(xqc_engine_t *engine, const xqc_cid_t *cid, uint64_t path_id) -{ - xqc_connection_t *conn; - xqc_path_ctx_t *path; - xqc_path_stats_t path_stats; - xqc_memzero(&path_stats, sizeof(path_stats)); - conn = xqc_engine_conns_hash_find(engine, cid, 's'); - if (!conn) { - xqc_log(engine->log, XQC_LOG_ERROR, "|can not find connection|"); - return path_stats; - } - if (conn->conn_state >= XQC_CONN_STATE_CLOSING) { - return path_stats; - } - path = xqc_conn_find_path_by_path_id(conn, path_id); - if (path == NULL) { - xqc_log(engine->log, XQC_LOG_WARN, "|path is not found by path_id in connection|"); - return path_stats; - } - xqc_send_ctl_t *send_ctl = path->path_send_ctl; - path_stats.last_tra_path_status_changed_time = path->last_tra_path_status_changed_time; - path_stats.send_count_since_last_tra_path_status_changed = - send_ctl->ctl_send_count - send_ctl->ctl_send_count_at_last_tra_path_status_changed_time; - path_stats.pto_count_since_last_tra_path_status_changed = send_ctl->ctl_pto_count_since_last_tra_path_status_changed; - path_stats.get_stats_success = 1; - return path_stats; -} +} \ No newline at end of file diff --git a/src/transport/xqc_multipath.h b/src/transport/xqc_multipath.h index 4db5e1931..7ddb220bb 100644 --- a/src/transport/xqc_multipath.h +++ b/src/transport/xqc_multipath.h @@ -18,8 +18,7 @@ /* enable multipath */ typedef enum { XQC_CONN_NOT_SUPPORT_MULTIPATH = 0, /* 00 */ - XQC_CONN_MULTIPATH_SINGLE_PNS = 1, /* 01 */ - XQC_CONN_MULTIPATH_MULTIPLE_PNS = 2, /* 10 */ + XQC_CONN_MULTIPATH_MULTIPLE_PNS = 1, /* 01 */ } xqc_multipath_mode_t; /* path state */ @@ -27,22 +26,24 @@ typedef enum { XQC_PATH_STATE_INIT = 0, /* initial state */ XQC_PATH_STATE_VALIDATING = 1, /* PATH_CHALLENGE sent/received on new path */ XQC_PATH_STATE_ACTIVE = 2, /* PATH_RESPONSE received */ - XQC_PATH_STATE_CLOSING = 3, /* PATH_ABANDONED sent */ - XQC_PATH_STATE_DRAINING = 4, /* PATH_ABANDONED received */ - XQC_PATH_STATE_CLOSED = 5, /* PATH_ABANDONED acked or draining timeout */ + XQC_PATH_STATE_CLOSING = 3, /* PATH_ABANDONED sent or received */ + XQC_PATH_STATE_CLOSED = 4, /* PATH_ABANDONED acked or draining timeout */ } xqc_path_state_t; + /* application layer path status */ typedef enum { + /* max */ + XQC_APP_PATH_STATUS_NONE, /* suggest that no traffic should be sent on that path if another path is available */ XQC_APP_PATH_STATUS_STANDBY = 1, /* allow the peer to use its own logic to split traffic among available paths */ XQC_APP_PATH_STATUS_AVAILABLE = 2, + /* freeze a path */ + XQC_APP_PATH_STATUS_FROZEN = 3, + /* max */ + XQC_APP_PATH_STATUS_MAX, } xqc_app_path_status_t; -/* transport layer path status */ -typedef enum { - XQC_TRA_PATH_STATUS_BACKUP = 1, - XQC_TRA_PATH_STATUS_IN_USE = 2, -} xqc_tra_path_status_t; + /* path close mode: passive & proactive */ typedef enum { @@ -60,8 +61,20 @@ typedef enum { typedef enum { XQC_PATH_FLAG_SEND_STATUS = 1 << 0, + XQC_PATH_FLAG_RECV_STATUS = 1 << 1, + XQC_PATH_FLAG_SOCKET_ERROR = 1 << 2, } xqc_path_flag_t; +typedef enum { + XQC_PATH_SPECIFIED_BY_ACK = 1 << 0, /* ack */ + XQC_PATH_SPECIFIED_BY_PCPR = 1 << 1, /* path challenge & response */ + XQC_PATH_SPECIFIED_BY_PTO = 1 << 2, /* PTO probe */ + XQC_PATH_SPECIFIED_BY_REINJ = 1 << 3, /* reinjection on a specific path */ + XQC_PATH_SPECIFIED_BY_PTMUD = 1 << 4, /* PMTUD Probe */ + XQC_PATH_SPECIFIED_BY_KAP = 1 << 5, /* Keepalive Probe */ + XQC_PATH_SPECIFIED_BY_PQP = 1 << 6, /* Path Quality Probe */ +} xqc_path_specified_flag_t; + /* path context */ struct xqc_path_ctx_s { @@ -69,6 +82,7 @@ struct xqc_path_ctx_s { uint64_t path_id; /* path identifier */ xqc_cid_t path_scid; xqc_cid_t path_dcid; + xqc_cid_t path_last_scid; /* Path_address: 4-tuple */ unsigned char peer_addr[sizeof(struct sockaddr_in6)], @@ -94,15 +108,13 @@ struct xqc_path_ctx_s { xqc_path_flag_t path_flag; - /* transport layer path status */ - xqc_tra_path_status_t tra_path_status; /* application layer path status, sync via PATH_STATUS frame */ xqc_app_path_status_t app_path_status; + xqc_app_path_status_t next_app_path_state; uint64_t app_path_status_send_seq_num; uint64_t app_path_status_recv_seq_num; xqc_usec_t last_app_path_status_changed_time; - xqc_usec_t last_tra_path_status_changed_time; /* Path cc & ack tracking */ xqc_send_ctl_t *path_send_ctl; @@ -125,7 +137,10 @@ struct xqc_path_ctx_s { /* path backup mode */ uint32_t standby_probe_count; uint32_t app_path_status_changed_count; - uint32_t tra_path_status_changed_count; + + /* PTMUD */ + size_t curr_pkt_out_size; + size_t path_max_pkt_out_size; }; /* 埋点路径信息 */ @@ -134,7 +149,6 @@ typedef struct { uint64_t path_id; uint8_t path_state; uint8_t app_path_status; - uint8_t tra_path_status; uint64_t path_bytes_send; uint64_t path_bytes_recv; @@ -142,13 +156,18 @@ typedef struct { uint64_t path_create_time; uint64_t path_destroy_time; - uint64_t update_rtt_count; - uint64_t mean_latest_rtt; - uint32_t cv_latest_rtt; + uint64_t srtt; + uint32_t loss_cnt; + uint32_t tlp_cnt; + uint32_t pkt_send_cnt; + uint32_t pkt_recv_cnt; + uint32_t dgram_send_cnt; + uint32_t dgram_recv_cnt; + uint32_t red_dgram_send_cnt; + uint32_t red_dgram_recv_cnt; uint32_t standby_probe_count; uint32_t app_path_status_changed_count; - uint32_t tra_path_status_changed_count; } xqc_path_info_t; @@ -160,6 +179,9 @@ xqc_int_t xqc_generate_path_challenge_data(xqc_connection_t *conn, xqc_path_ctx_ /* check mp support */ xqc_multipath_mode_t xqc_conn_enable_multipath(xqc_connection_t *conn); +/* check multipath version negotiation */ +xqc_multipath_version_t xqc_conn_multipath_version_negotiation(xqc_connection_t *conn); + /* init path_list & initial path for connection */ xqc_int_t xqc_conn_init_paths_list(xqc_connection_t *conn); @@ -170,7 +192,8 @@ void xqc_path_schedule_buf_destroy(xqc_path_ctx_t *path); void xqc_path_schedule_buf_pre_destroy(xqc_send_queue_t *send_queue, xqc_path_ctx_t *path); /* create path inner */ -xqc_path_ctx_t *xqc_conn_create_path_inner(xqc_connection_t *conn, xqc_cid_t *scid, xqc_cid_t *dcid); +xqc_path_ctx_t *xqc_conn_create_path_inner(xqc_connection_t *conn, + xqc_cid_t *scid, xqc_cid_t *dcid, xqc_app_path_status_t path_status); /* server update client addr when recv path_challenge frame */ xqc_int_t xqc_conn_server_init_path_addr(xqc_connection_t *conn, uint64_t path_id, @@ -187,19 +210,17 @@ xqc_int_t xqc_path_immediate_close(xqc_path_ctx_t *path); /* path state: "ACTIVE/CLOSING/DRAINING" -> "CLOSED" */ xqc_int_t xqc_path_closed(xqc_path_ctx_t *path); -void xqc_path_abandon_acked(xqc_connection_t *conn, uint64_t path_id); - /* find path */ xqc_path_ctx_t *xqc_conn_find_path_by_path_id(xqc_connection_t *conn, uint64_t path_id); xqc_path_ctx_t *xqc_conn_find_path_by_scid(xqc_connection_t *conn, xqc_cid_t *scid); xqc_path_ctx_t *xqc_conn_find_path_by_dcid(xqc_connection_t *conn, xqc_cid_t *dcid); +xqc_path_ctx_t *xqc_conn_find_path_by_dcid_seq(xqc_connection_t *conn, uint64_t dcid_seq); void xqc_path_send_buffer_append(xqc_path_ctx_t *path, xqc_packet_out_t *packet_out, xqc_list_head_t *head); void xqc_path_send_buffer_remove(xqc_path_ctx_t *path, xqc_packet_out_t *packet_out); void xqc_path_send_buffer_clear(xqc_connection_t *conn, xqc_path_ctx_t *path, xqc_list_head_t *head, xqc_send_type_t send_type); -void xqc_set_application_path_status(xqc_path_ctx_t *path, xqc_app_path_status_t status, xqc_usec_t now); -void xqc_set_transport_path_status(xqc_path_ctx_t *path, xqc_tra_path_status_t status, xqc_usec_t now); +xqc_int_t xqc_set_application_path_status(xqc_path_ctx_t *path, xqc_app_path_status_t status, xqc_bool_t is_tx); /* path statistics */ void xqc_conn_path_metrics_print(xqc_connection_t *conn, xqc_conn_stats_t *stats); @@ -210,18 +231,17 @@ void xqc_stream_path_metrics_on_send(xqc_connection_t *conn, xqc_packet_out_t *p void xqc_stream_path_metrics_on_recv(xqc_connection_t *conn, xqc_stream_t *stream, xqc_packet_in_t *pi); void xqc_path_metrics_print(xqc_connection_t *conn, char *buff, unsigned buff_size); -void xqc_h3s_path_metrics_print(xqc_h3_stream_t *h3_stream, char *buff, unsigned buff_size); xqc_msec_t xqc_path_get_idle_timeout(xqc_path_ctx_t *path); void xqc_path_validate(xqc_path_ctx_t *path); +xqc_int_t xqc_conn_is_current_mp_version_supported(xqc_multipath_version_t mp_version); + xqc_bool_t xqc_path_is_initial_path(xqc_path_ctx_t *path); void xqc_path_record_info(xqc_path_ctx_t *path, xqc_path_info_t *path_info); -size_t xqc_path_info_print(xqc_path_info_t *path_info, char **buff, size_t *buff_size); - xqc_bool_t xqc_path_is_full(xqc_path_ctx_t *path); xqc_int_t xqc_path_standby_probe(xqc_path_ctx_t *path); diff --git a/src/transport/xqc_pacing.c b/src/transport/xqc_pacing.c index b15f25128..0d34f06bd 100644 --- a/src/transport/xqc_pacing.c +++ b/src/transport/xqc_pacing.c @@ -141,7 +141,7 @@ xqc_pacing_can_write(xqc_pacing_t *pacing, uint32_t total_bytes) } uint64_t delay = xqc_pacing_time_until_send(pacing, total_bytes); - xqc_log(send_ctl->ctl_conn->log, XQC_LOG_DEBUG, "|pacing_delay: %ui!", delay); + xqc_log(send_ctl->ctl_conn->log, XQC_LOG_DEBUG, "|pacing_delay: %ui|", delay); if (delay != 0) { xqc_timer_update(&send_ctl->path_timer_manager, XQC_TIMER_PACING, xqc_monotonic_timestamp(), delay); diff --git a/src/transport/xqc_packet.c b/src/transport/xqc_packet.c index 20d4b4561..308cb4c55 100644 --- a/src/transport/xqc_packet.c +++ b/src/transport/xqc_packet.c @@ -194,7 +194,7 @@ xqc_packet_decrypt_single(xqc_connection_t *c, xqc_packet_in_t *packet_in) } else { xqc_log(c->log, XQC_LOG_WARN, "|decrypt data error, return|%d|pkt_type:%s|pkt_num:%ui|", ret, xqc_pkt_type_2_str(packet_in->pi_pkt.pkt_type), packet_in->pi_pkt.pkt_num); - ret = -XQC_EILLPKT; + ret = -XQC_EDECRYPT; /* don't close connection, just drop the packet */ } return ret; diff --git a/src/transport/xqc_packet.h b/src/transport/xqc_packet.h index d688ac702..b7789613a 100644 --- a/src/transport/xqc_packet.h +++ b/src/transport/xqc_packet.h @@ -11,7 +11,9 @@ #define XQC_ACK_SPACE 16 #define XQC_QUIC_MIN_MSS 1200 -#define XQC_MSS (XQC_QUIC_MIN_MSS + XQC_ACK_SPACE) +/* 1500 - 40 (IPv6) - 8 (UDP) - 16 (ACK) - 16 (AEAD)*/ +#define XQC_QUIC_MAX_MSS 1420 +#define XQC_MSS (XQC_QUIC_MAX_MSS + XQC_ACK_SPACE) /* max 0rtt packet count sent by client */ #define XQC_PACKET_0RTT_MAX_COUNT 64 diff --git a/src/transport/xqc_packet_out.c b/src/transport/xqc_packet_out.c index a4df30406..433428e2a 100644 --- a/src/transport/xqc_packet_out.c +++ b/src/transport/xqc_packet_out.c @@ -14,6 +14,9 @@ #include "src/transport/xqc_utils.h" #include "src/transport/xqc_engine.h" #include "src/transport/xqc_multipath.h" +#include "src/transport/xqc_datagram.h" +#include "src/transport/xqc_reinjection.h" +#include "src/transport/xqc_packet_out.h" xqc_packet_out_t * @@ -25,12 +28,12 @@ xqc_packet_out_create(size_t po_buf_size) goto error; } - packet_out->po_buf = xqc_malloc(po_buf_size + XQC_PACKET_OUT_EXT_SPACE); + packet_out->po_buf = xqc_malloc(XQC_PACKET_OUT_BUF_CAP); if (!packet_out->po_buf) { goto error; } - packet_out->po_buf_cap = po_buf_size + XQC_PACKET_OUT_EXT_SPACE; + packet_out->po_buf_cap = XQC_PACKET_OUT_BUF_CAP; packet_out->po_buf_size = po_buf_size; return packet_out; @@ -43,6 +46,98 @@ xqc_packet_out_create(size_t po_buf_size) return NULL; } + + xqc_bool_t + xqc_packet_out_on_specific_path(xqc_connection_t *conn, + xqc_packet_out_t *po, xqc_path_ctx_t **path) +{ + xqc_bool_t ret = XQC_FALSE; + if (po->po_path_flag) { + + if (po->po_path_flag == XQC_PATH_SPECIFIED_BY_ACK + && conn->conn_settings.mp_ack_on_any_path + && po->po_frame_types & XQC_FRAME_BIT_ACK_MP) + { + /* + * if the path is specified only due to ACK_MP, + * we force the packet to be re-scheduled. + */ + po->po_path_flag &= ~XQC_PATH_SPECIFIED_BY_ACK; + *path = NULL; + return XQC_FALSE; + } + + *path = xqc_conn_find_path_by_path_id(conn, po->po_path_id); + + /* no packets can be sent on a closing/closed/frozen path */ + if ((*path == NULL) + || ((*path)->path_state >= XQC_PATH_STATE_CLOSING) + || (*path)->app_path_status == XQC_APP_PATH_STATUS_FROZEN) + { + po->po_path_flag &= ~(XQC_PATH_SPECIFIED_BY_ACK | XQC_PATH_SPECIFIED_BY_PTO | XQC_PATH_SPECIFIED_BY_REINJ); + + if (po->po_path_flag) { + if ((*path == NULL) + || ((*path)->path_state >= XQC_PATH_STATE_CLOSING)) + { + /* if the packet can not be rescheduled and the path is closed, we remove it. */ + xqc_send_queue_remove_send(&po->po_list); + xqc_send_queue_insert_free(po, &conn->conn_send_queue->sndq_free_packets, conn->conn_send_queue); + } + ret = XQC_TRUE; + } + + *path = NULL; + + } else { + ret = XQC_TRUE; + } + } + return ret; +} + +xqc_bool_t +xqc_packet_out_can_attach_ack(xqc_packet_out_t *po, + xqc_path_ctx_t *path, xqc_pkt_type_t pkt_type) +{ + if (po->po_pkt.pkt_type != pkt_type) { + return XQC_FALSE; + } + + if (po->po_frame_types & (XQC_FRAME_BIT_ACK | XQC_FRAME_BIT_ACK_MP)) { + return XQC_FALSE; + } + + if (path->path_flag && path->path_id != po->po_path_id) { + return XQC_FALSE; + } + + return XQC_TRUE; +} + +xqc_bool_t +xqc_packet_out_can_pto_probe(xqc_packet_out_t *po, uint64_t path_id) +{ + if ((po->po_path_flag & (XQC_PATH_SPECIFIED_BY_PCPR | XQC_PATH_SPECIFIED_BY_REINJ | XQC_PATH_SPECIFIED_BY_PTMUD)) + && path_id != po->po_path_id) + { + return XQC_FALSE; + } + return XQC_TRUE; +} + +void +xqc_packet_out_remove_ack_frame(xqc_packet_out_t *po) +{ + if (po->po_frame_types & XQC_FRAME_BIT_ACK + || po->po_frame_types & XQC_FRAME_BIT_ACK_MP) + { + po->po_used_size = po->po_ack_offset; + po->po_frame_types &= ~(XQC_FRAME_BIT_ACK | XQC_FRAME_BIT_ACK_MP); + po->po_path_flag &= ~(XQC_PATH_SPECIFIED_BY_ACK); + } +} + void xqc_packet_out_copy(xqc_packet_out_t *dst, xqc_packet_out_t *src) { @@ -71,7 +166,6 @@ xqc_packet_out_copy(xqc_packet_out_t *dst, xqc_packet_out_t *src) dst->po_user_data = src->po_user_data; dst->po_path_id = src->po_path_id; - dst->po_abandon_path_id = src->po_abandon_path_id; dst->po_flag &= ~XQC_POF_IN_UNACK_LIST; dst->po_flag &= ~XQC_POF_IN_PATH_BUF_LIST; @@ -92,7 +186,7 @@ xqc_packet_out_get(xqc_send_queue_t *send_queue) xqc_send_queue_remove_free(pos, send_queue); unsigned char *tmp = packet_out->po_buf; - buf_size = packet_out->po_buf_size; + buf_size = send_queue->sndq_conn->pkt_out_size; buf_cap = packet_out->po_buf_cap; memset(packet_out, 0, sizeof(xqc_packet_out_t)); packet_out->po_buf = tmp; @@ -101,7 +195,7 @@ xqc_packet_out_get(xqc_send_queue_t *send_queue) return packet_out; } - packet_out = xqc_packet_out_create(send_queue->pkt_out_size); + packet_out = xqc_packet_out_create(send_queue->sndq_conn->pkt_out_size); if (!packet_out) { return NULL; } @@ -198,7 +292,6 @@ xqc_write_new_packet(xqc_connection_t *conn, xqc_pkt_type_t pkt_type) } packet_out->po_path_id = XQC_INITIAL_PATH_ID; - packet_out->po_abandon_path_id = XQC_MAX_UINT64_VALUE; if (packet_out->po_used_size == 0) { ret = xqc_write_packet_header(conn, packet_out); @@ -232,7 +325,6 @@ xqc_write_packet(xqc_connection_t *conn, xqc_pkt_type_t pkt_type, unsigned need) } packet_out->po_path_id = XQC_INITIAL_PATH_ID; - packet_out->po_abandon_path_id = XQC_MAX_UINT64_VALUE; if (packet_out->po_used_size == 0) { ret = xqc_write_packet_header(conn, packet_out); @@ -266,7 +358,6 @@ xqc_write_packet_for_stream(xqc_connection_t *conn, xqc_pkt_type_t pkt_type, uns } packet_out->po_path_id = XQC_INITIAL_PATH_ID; - packet_out->po_abandon_path_id = XQC_MAX_UINT64_VALUE; if (packet_out->po_used_size == 0) { ret = xqc_write_packet_header(conn, packet_out); @@ -292,37 +383,8 @@ xqc_write_ack_to_one_packet(xqc_connection_t *conn, xqc_packet_out_t *packet_out xqc_usec_t now = xqc_monotonic_timestamp(); xqc_path_ctx_t *path = conn->conn_initial_path; - - if (conn->enable_multipath == XQC_CONN_MULTIPATH_SINGLE_PNS - && conn->conn_flag & (XQC_CONN_FLAG_SHOULD_ACK_INIT << pns)) - { - path = xqc_conn_find_path_by_path_id(conn, conn->should_ack_path_id); - if (path == NULL) { - xqc_log(conn->log, XQC_LOG_ERROR, "|xqc_conn_find_path_by_path_id error|should_ack_path_id:%ui|", conn->should_ack_path_id); - path = conn->conn_initial_path; - } - } - xqc_pn_ctl_t *pn_ctl = xqc_get_pn_ctl(conn, path); - if (conn->enable_multipath == XQC_CONN_MULTIPATH_SINGLE_PNS) { - xqc_log(conn->log, XQC_LOG_DEBUG, "|gen_ack_frame|path:%ui|path_largest_recv:%ui|path_must_ack:%ui|", - path->path_id, path->path_send_ctl->ctl_largest_received[pns], path->path_send_ctl->ctl_unack_received[pns]); - if (path->path_send_ctl->ctl_largest_received[pns] < path->path_send_ctl->ctl_unack_received[pns]) { - xqc_log(conn->log, XQC_LOG_WARN, "|!|"); - } - - largest_ack = path->path_send_ctl->ctl_largest_received[pns]; - - ret = xqc_gen_ack_frame_for_spns(conn, packet_out, now, conn->local_settings.ack_delay_exponent, - &pn_ctl->ctl_recv_record[pns], path->path_send_ctl->ctl_largest_recv_time[pns], - &has_gap, &largest_ack, path->path_send_ctl->ctl_unack_received[pns]); - if (ret >= 0) { - path->path_send_ctl->ctl_unack_received[pns] = XQC_MAX_UINT64_VALUE; - goto done; - } - } - ret = xqc_gen_ack_frame(conn, packet_out, now, conn->local_settings.ack_delay_exponent, &pn_ctl->ctl_recv_record[pns], path->path_send_ctl->ctl_largest_recv_time[pns], &has_gap, &largest_ack); @@ -330,7 +392,6 @@ xqc_write_ack_to_one_packet(xqc_connection_t *conn, xqc_packet_out_t *packet_out goto error; } -done: xqc_log(conn->log, XQC_LOG_DEBUG, "|ack_size:%ui|path:%ui|path_largest_recv:%ui|frame_largest_recv:%ui|", ret, path->path_id, path->path_send_ctl->ctl_largest_received[pns], xqc_recv_record_largest(&pn_ctl->ctl_recv_record[pns])); @@ -338,7 +399,7 @@ xqc_write_ack_to_one_packet(xqc_connection_t *conn, xqc_packet_out_t *packet_out packet_out->po_used_size += ret; packet_out->po_largest_ack = largest_ack; - packet_out->po_is_path_specified = XQC_TRUE; + packet_out->po_path_flag |= XQC_PATH_SPECIFIED_BY_ACK; packet_out->po_path_id = path->path_id; path->path_send_ctl->ctl_ack_eliciting_pkt[pns] = 0; @@ -357,99 +418,142 @@ xqc_write_ack_to_one_packet(xqc_connection_t *conn, xqc_packet_out_t *packet_out return ret; } -int -xqc_write_ack_to_packets(xqc_connection_t *conn) +xqc_int_t +xqc_write_ack_or_mp_ack_to_one_packet(xqc_connection_t *conn, xqc_packet_out_t *packet_out, xqc_pkt_num_space_t pns, xqc_path_ctx_t *path, xqc_bool_t is_mp_ack) +{ + int ret; + if (is_mp_ack) { + ret = xqc_write_ack_mp_to_one_packet(conn, path, packet_out, pns); + } else { + ret = xqc_write_ack_to_one_packet(conn, packet_out, pns); + } + return ret; +} + +xqc_int_t +xqc_write_ack_or_mp_ack_to_packets(xqc_connection_t *conn) { XQC_DEBUG_PRINT xqc_pkt_num_space_t pns; xqc_packet_out_t *packet_out; xqc_pkt_type_t pkt_type; xqc_list_head_t *pos, *next; + xqc_bool_t is_mp_ack = 0; /* Send ack in default case */ int ret; - + + xqc_path_ctx_t *path; + xqc_list_head_t *path_pos, *path_next; + for (pns = 0; pns < XQC_PNS_N; ++pns) { + + /* If there's no such pns in the connection, continue the loop */ if (!(conn->conn_flag & (XQC_CONN_FLAG_SHOULD_ACK_INIT << pns))) { continue; } - if (pns == XQC_PNS_HSK) { - pkt_type = XQC_PTYPE_HSK; - - } else if (pns == XQC_PNS_INIT) { - pkt_type = XQC_PTYPE_INIT; + xqc_list_for_each_safe(path_pos, path_next, &conn->conn_paths_list) { + path = xqc_list_entry(path_pos, xqc_path_ctx_t, path_list); + if (path->path_state < XQC_PATH_STATE_VALIDATING) { + continue; + } - } else { - pkt_type = XQC_PTYPE_SHORT_HEADER; - } + /* Check if any ack should be sent in current path */ + xqc_pn_ctl_t *pn_ctl = xqc_get_pn_ctl(conn, path); + xqc_pktno_range_node_t *first_range = NULL; + xqc_list_head_t *tmp_pos, *tmp_next; + xqc_list_for_each_safe(tmp_pos, tmp_next, &pn_ctl->ctl_recv_record[pns].list_head) { + first_range = xqc_list_entry(tmp_pos, xqc_pktno_range_node_t, list); + break; + } + if (first_range == NULL) { + continue; + } - /* TODO: 暂时让SPNS的ACK全部write new packet,等待能解决发ACK包时path不可用问题的新方案 */ - if (conn->enable_multipath == XQC_CONN_MULTIPATH_SINGLE_PNS) { - goto write_new; - } + if (pns == XQC_PNS_HSK) { + pkt_type = XQC_PTYPE_HSK; + } else if (pns == XQC_PNS_INIT) { + pkt_type = XQC_PTYPE_INIT; + } else { + pkt_type = XQC_PTYPE_SHORT_HEADER; + } + /* Acknowledgements of Initial and Handshake packets MUST be carried using ACK frames */ + if (pkt_type > XQC_PTYPE_HSK && conn->enable_multipath == XQC_CONN_MULTIPATH_MULTIPLE_PNS) { + is_mp_ack = 1; + } + path_buffer: - xqc_list_for_each_safe(pos, next, &conn->conn_initial_path->path_schedule_buf[XQC_SEND_TYPE_NORMAL]) { - packet_out = xqc_list_entry(pos, xqc_packet_out_t, po_list); - if (packet_out->po_pkt.pkt_type == pkt_type) { - ret = xqc_write_ack_to_one_packet(conn, packet_out, pns); - if (ret == -XQC_ENOBUF) { - xqc_log(conn->log, XQC_LOG_DEBUG, "|xqc_write_ack_to_one_packet try conn buffer|"); - goto write_new; - - } else if (ret == XQC_OK) { - goto done; - - } else { - return ret; + /* Try to attach ack or mp_ack to packet_out in path_buffer */ + xqc_list_for_each_safe(pos, next, &path->path_schedule_buf[XQC_SEND_TYPE_NORMAL]) { + packet_out = xqc_list_entry(pos, xqc_packet_out_t, po_list); + + if (xqc_packet_out_can_attach_ack(packet_out, path, pkt_type)) { + ret = xqc_write_ack_or_mp_ack_to_one_packet(conn, packet_out, pns, path, is_mp_ack); + if (ret == -XQC_ENOBUF) { + xqc_log(conn->log, XQC_LOG_DEBUG, "|xqc_write_ack_or_mp_ack_to_one_packet try new packet|"); + goto write_new; + } else if (ret == XQC_OK) { + goto done; + } else { + return ret; + } } - } - goto write_new; - } + goto write_new; + } conn_buffer: - xqc_list_for_each_safe(pos, next, &conn->conn_send_queue->sndq_send_packets) { - packet_out = xqc_list_entry(pos, xqc_packet_out_t, po_list); - if (packet_out->po_pkt.pkt_type == pkt_type && !packet_out->po_is_path_specified) { - ret = xqc_write_ack_to_one_packet(conn, packet_out, pns); - if (ret == -XQC_ENOBUF) { - xqc_log(conn->log, XQC_LOG_DEBUG, "|xqc_write_ack_to_one_packet try new packet|"); + if (!is_mp_ack) { + xqc_list_for_each_safe(pos, next, &conn->conn_send_queue->sndq_send_packets) { + packet_out = xqc_list_entry(pos, xqc_packet_out_t, po_list); + + if (xqc_packet_out_can_attach_ack(packet_out, path, pkt_type)) { + ret = xqc_write_ack_to_one_packet(conn, packet_out, pns); + if (ret == -XQC_ENOBUF) { + xqc_log(conn->log, XQC_LOG_DEBUG, "|xqc_write_ack_to_one_packet try new packet|"); + goto write_new; + } else if (ret == XQC_OK) { + goto done; + } else { + return ret; + } + } goto write_new; - - } else if (ret == XQC_OK) { - goto done; - - } else { - return ret; } } - goto write_new; - } write_new: - packet_out = xqc_write_new_packet(conn, pkt_type); - if (packet_out == NULL) { - xqc_log(conn->log, XQC_LOG_ERROR, "|xqc_write_new_packet error|"); - return -XQC_EWRITE_PKT; - } - - ret = xqc_write_ack_to_one_packet(conn, packet_out, pns); - if (ret != XQC_OK) { - xqc_log(conn->log, XQC_LOG_ERROR, "|xqc_write_ack_to_one_packet error|ret:%d|", ret); - return ret; - } + packet_out = xqc_write_new_packet(conn, pkt_type); + if (packet_out == NULL) { + xqc_log(conn->log, XQC_LOG_ERROR, "|xqc_write_new_packet error|"); + return -XQC_EWRITE_PKT; + } + ret = xqc_write_ack_or_mp_ack_to_one_packet(conn, packet_out, pns, path, is_mp_ack); + if (ret != XQC_OK) { + xqc_log(conn->log, XQC_LOG_ERROR, "|xqc_write_ack_or_mp_ack_to_one_packet write to new packet error|ret:%d|", ret); + return ret; + } pure_ack: - /* send ack packet first */ - xqc_send_queue_move_to_high_pri(&packet_out->po_list, conn->conn_send_queue); + /* send ack packet first */ + xqc_send_queue_move_to_high_pri(&packet_out->po_list, conn->conn_send_queue); + done: - xqc_log(conn->log, XQC_LOG_DEBUG, "|pns:%d|", pns); + xqc_log(conn->log, XQC_LOG_DEBUG, "|path:%ui|pns:%d|", path->path_id, pns); + + /* Ack frame should only be sent on initial path */ + if (!is_mp_ack) { + break; + } + } } return XQC_OK; } + int -xqc_write_ping_to_packet(xqc_connection_t *conn, void *po_user_data, xqc_bool_t notify) +xqc_write_ping_to_packet(xqc_connection_t *conn, xqc_path_ctx_t *path, + void *po_user_data, xqc_bool_t notify, xqc_ping_record_t *pr) { ssize_t ret; xqc_packet_out_t *packet_out; @@ -476,6 +580,15 @@ xqc_write_ping_to_packet(xqc_connection_t *conn, void *po_user_data, xqc_bool_t */ if (notify) { packet_out->po_flag |= XQC_POF_NOTIFY; + if (pr) { + packet_out->po_pr = pr; + pr->ref_cnt++; + } + } + + if (path) { + packet_out->po_path_id = path->path_id; + packet_out->po_path_flag |= XQC_PATH_SPECIFIED_BY_KAP; } conn->conn_flag &= ~XQC_CONN_FLAG_PING; @@ -488,6 +601,50 @@ xqc_write_ping_to_packet(xqc_connection_t *conn, void *po_user_data, xqc_bool_t return ret; } + +int +xqc_write_pmtud_ping_to_packet(xqc_path_ctx_t *path, + size_t probing_size, xqc_pkt_type_t pkt_type) +{ + int ret; + xqc_packet_out_t *packet_out; + xqc_connection_t *conn = path->parent_conn; + + packet_out = xqc_write_new_packet(conn, pkt_type); + if (packet_out == NULL) { + xqc_log(conn->log, XQC_LOG_ERROR, "|xqc_write_new_packet error|"); + return -XQC_EWRITE_PKT; + } + + packet_out->po_buf_size = probing_size; + if (packet_out->po_buf_size > packet_out->po_buf_cap + || packet_out->po_buf_size < packet_out->po_used_size) + { + xqc_log(conn->log, XQC_LOG_ERROR, "|invalid PMTUD probing size|"); + ret = -XQC_EPMTUD_PROBING_SIZE; + goto error; + } + + ret = xqc_gen_ping_frame(packet_out); + if (ret < 0) { + xqc_log(conn->log, XQC_LOG_ERROR, "|xqc_gen_ping_frame error|"); + goto error; + } + + packet_out->po_used_size += ret; + packet_out->po_path_id = path->path_id; + packet_out->po_path_flag |= XQC_PATH_SPECIFIED_BY_PTMUD; + packet_out->po_flag |= XQC_POF_PMTUD_PROBING; + packet_out->po_max_pkt_out_size = conn->max_pkt_out_size; + + xqc_send_queue_move_to_high_pri(&packet_out->po_list, conn->conn_send_queue); + return XQC_OK; + +error: + xqc_maybe_recycle_packet_out(packet_out, conn); + return ret; +} + int xqc_write_conn_close_to_packet(xqc_connection_t *conn, uint64_t err_code) { @@ -581,7 +738,7 @@ xqc_write_stop_sending_to_packet(xqc_connection_t *conn, xqc_stream_t *stream, return XQC_OK; } - packet_out = xqc_write_new_packet(conn, XQC_PTYPE_SHORT_HEADER); + packet_out = xqc_write_new_packet(conn, XQC_PTYPE_NUM); if (packet_out == NULL) { xqc_log(conn->log, XQC_LOG_ERROR, "|xqc_write_new_packet error|"); return -XQC_EWRITE_PKT; @@ -720,12 +877,12 @@ xqc_write_max_data_to_packet(xqc_connection_t *conn, uint64_t max_data) } int -xqc_write_max_stream_data_to_packet(xqc_connection_t *conn, xqc_stream_id_t stream_id, uint64_t max_stream_data) +xqc_write_max_stream_data_to_packet(xqc_connection_t *conn, xqc_stream_id_t stream_id, uint64_t max_stream_data, xqc_pkt_type_t pkt_type) { ssize_t ret = XQC_OK; xqc_packet_out_t *packet_out; - packet_out = xqc_write_new_packet(conn, XQC_PTYPE_SHORT_HEADER); + packet_out = xqc_write_new_packet(conn, pkt_type); if (packet_out == NULL) { xqc_log(conn->log, XQC_LOG_ERROR, "|xqc_write_new_packet error|"); return -XQC_EWRITE_PKT; @@ -822,10 +979,57 @@ xqc_write_new_token_to_packet(xqc_connection_t *conn) int xqc_write_stream_frame_to_packet(xqc_connection_t *conn, xqc_stream_t *stream, xqc_pkt_type_t pkt_type, uint8_t fin, - const unsigned char *payload, size_t payload_size, size_t *send_data_written) + const unsigned char *payload, size_t payload_size, + size_t *send_data_written) { xqc_packet_out_t *packet_out; int n_written; + + /* increase recv window */ + xqc_usec_t max_srtt = 0; + uint64_t old_fc_win = 0; + uint64_t available_window; + + if (conn->conn_settings.enable_stream_rate_limit + && stream->stream_send_offset == 0 + && stream->stream_type == XQC_CLI_BID) + { + available_window = stream->stream_flow_ctl.fc_max_stream_data_can_recv - stream->stream_data_in.next_read_offset; + old_fc_win = stream->stream_flow_ctl.fc_stream_recv_window_size; + + if (stream->recv_rate_bytes_per_sec) { + /* set window according to the rate limit */ + max_srtt = xqc_conn_get_max_srtt(conn); + stream->stream_flow_ctl.fc_stream_recv_window_size = stream->recv_rate_bytes_per_sec * max_srtt / 1000000; + stream->stream_flow_ctl.fc_stream_recv_window_size = xqc_max(conn->conn_settings.init_recv_window, stream->stream_flow_ctl.fc_stream_recv_window_size); + stream->stream_flow_ctl.fc_stream_recv_window_size = xqc_min(XQC_MAX_RECV_WINDOW, stream->stream_flow_ctl.fc_stream_recv_window_size); + xqc_log(conn->log, XQC_LOG_DEBUG, + "|initial_fc_credit_update|stream:%ui|rate:%ui|srtt:%ui|old_fc_win:%ui|fc_win:%ui|", + stream->stream_id, + stream->recv_rate_bytes_per_sec, max_srtt, + old_fc_win, stream->stream_flow_ctl.fc_stream_recv_window_size); + + } else { + /* set window to XQC_MAX_RECV_WINDOW */ + stream->stream_flow_ctl.fc_stream_recv_window_size = XQC_MAX_RECV_WINDOW; + xqc_log(conn->log, XQC_LOG_DEBUG, + "|initial_fc_credit_update|stream:%ui|no_rate_limit|old_fc_win:%ui|fc_win:%ui|", + stream->stream_id, + old_fc_win, stream->stream_flow_ctl.fc_stream_recv_window_size); + } + + if (stream->stream_flow_ctl.fc_stream_recv_window_size > available_window) { + stream->stream_flow_ctl.fc_max_stream_data_can_recv += (stream->stream_flow_ctl.fc_stream_recv_window_size - available_window); + xqc_log(conn->log, XQC_LOG_DEBUG, + "|initial_fc_credit_update|stream:%ui|new_max_data:%ui|stream_max_recv_offset:%ui|next_read_offset:%ui|window_size:%ui|pkt_type:%d|", + stream->stream_id, + stream->stream_flow_ctl.fc_max_stream_data_can_recv, stream->stream_max_recv_offset, + stream->stream_data_in.next_read_offset, stream->stream_flow_ctl.fc_stream_recv_window_size, + pkt_type); + xqc_write_max_stream_data_to_packet(conn, stream->stream_id, stream->stream_flow_ctl.fc_max_stream_data_can_recv, pkt_type); + } + } + /* We need 25 bytes for stream frame header at most, and left bytes for stream data. * It's a trade-off value, bigger need bytes for higher payload rate. */ const unsigned need = 50; @@ -849,13 +1053,6 @@ xqc_write_stream_frame_to_packet(xqc_connection_t *conn, packet_out->po_stream_id = stream->stream_id; packet_out->po_stream_offset = stream->stream_send_offset; - if (stream->stream_mp_usage_schedule == 0) { - packet_out->po_flag |= XQC_POF_NOT_SCHEDULE; - } - if (stream->stream_mp_usage_reinject == 0) { - packet_out->po_flag |= XQC_POF_NOT_REINJECT; - } - if (fin && *send_data_written == payload_size) { stream->stream_flag |= XQC_STREAM_FLAG_FIN_WRITE; stream->stream_stats.local_fin_write_time = xqc_monotonic_timestamp(); @@ -867,6 +1064,57 @@ xqc_write_stream_frame_to_packet(xqc_connection_t *conn, return XQC_OK; } +int +xqc_write_datagram_frame_to_packet(xqc_connection_t *conn, xqc_pkt_type_t pkt_type, + const unsigned char *data, size_t data_len, uint64_t *dgram_id, xqc_bool_t use_supplied_dgram_id, + xqc_data_qos_level_t qos_level) +{ + xqc_packet_out_t *packet_out; + packet_out = xqc_write_new_packet(conn, pkt_type); + if (packet_out == NULL) { + return -XQC_EWRITE_PKT; + } + + int ret; + ret = xqc_gen_datagram_frame(packet_out, data, data_len); + + if (ret < 0) { + xqc_maybe_recycle_packet_out(packet_out, conn); + return ret; + } + + if (use_supplied_dgram_id) { + packet_out->po_dgram_id = *dgram_id; + + } else { + packet_out->po_dgram_id = conn->next_dgram_id++; + } + + if (dgram_id) { + *dgram_id = packet_out->po_dgram_id; + } + + if (pkt_type == XQC_PTYPE_0RTT) { + conn->zero_rtt_count++; + } + + if (qos_level > XQC_DATA_QOS_HIGH) { + if (qos_level == XQC_DATA_QOS_PROBING) { + /* must reinject the packet on a different path */ + packet_out->po_flag |= XQC_POF_REINJECT_DIFF_PATH; + packet_out->po_flag |= XQC_POF_QOS_PROBING; + + } else { + packet_out->po_flag |= XQC_POF_NOT_REINJECT; + } + + } else { + packet_out->po_flag |= XQC_POF_QOS_HIGH; + } + + return XQC_OK; +} + /* [Transport] 12.4, HANDSHAKE_DONE only send in 1-RTT packet */ int @@ -893,10 +1141,10 @@ xqc_write_handshake_done_frame_to_packet(xqc_connection_t *conn) xqc_int_t xqc_write_new_conn_id_frame_to_packet(xqc_connection_t *conn, uint64_t retire_prior_to) { - xqc_int_t ret = XQC_ERROR; - xqc_packet_out_t *packet_out = NULL; - - xqc_cid_t new_conn_cid; + xqc_int_t ret = XQC_ERROR; + xqc_packet_out_t *packet_out = NULL; + xqc_cid_t new_conn_cid; + uint8_t sr_token[XQC_STATELESS_RESET_TOKENLEN]; /* only reserve bits for server side */ ++conn->scid_set.largest_scid_seq_num; @@ -907,6 +1155,11 @@ xqc_write_new_conn_id_frame_to_packet(xqc_connection_t *conn, uint64_t retire_pr return -XQC_EGENERATE_CID; } + /* generate stateless reset token */ + xqc_gen_reset_token(&new_conn_cid, sr_token, XQC_STATELESS_RESET_TOKENLEN, + conn->engine->config->reset_token_key, + conn->engine->config->reset_token_keylen); + /* insert to scid_set & add scid_unused_cnt */ ret = xqc_cid_set_insert_cid(&conn->scid_set.cid_set, &new_conn_cid, XQC_CID_UNUSED, conn->remote_settings.active_connection_id_limit); @@ -918,7 +1171,8 @@ xqc_write_new_conn_id_frame_to_packet(xqc_connection_t *conn, uint64_t retire_pr return ret; } - ret = xqc_insert_conns_hash(conn->engine->conns_hash, conn, &new_conn_cid); + ret = xqc_insert_conns_hash(conn->engine->conns_hash, conn, + new_conn_cid.cid_buf, new_conn_cid.cid_len); if (ret < 0) { xqc_log(conn->log, XQC_LOG_ERROR, "|insert new_cid into conns_hash failed|"); return ret; @@ -931,16 +1185,16 @@ xqc_write_new_conn_id_frame_to_packet(xqc_connection_t *conn, uint64_t retire_pr } ret = xqc_gen_new_conn_id_frame(packet_out, &new_conn_cid, retire_prior_to, - conn->engine->config->reset_token_key, - conn->engine->config->reset_token_keylen); + sr_token); if (ret < 0) { xqc_log(conn->log, XQC_LOG_ERROR, "|xqc_gen_new_conn_id_frame error|"); goto error; } packet_out->po_used_size += ret; - xqc_log(conn->log, XQC_LOG_DEBUG, "|gen_new_scid:%s|seq_num:%ui|", - xqc_scid_str(&new_conn_cid), new_conn_cid.cid_seq_num); + xqc_log(conn->log, XQC_LOG_DEBUG, "|gen_new_scid|cid:%s|sr_token:%s|seq_num:%ui", + xqc_scid_str(&new_conn_cid), xqc_sr_token_str(new_conn_cid.sr_token), + new_conn_cid.cid_seq_num); xqc_send_queue_move_to_high_pri(&packet_out->po_list, conn->conn_send_queue); return XQC_OK; @@ -957,12 +1211,14 @@ xqc_write_retire_conn_id_frame_to_packet(xqc_connection_t *conn, uint64_t seq_nu xqc_int_t ret = XQC_ERROR; /* select new current_dcid to replace the cid to be retired */ - if (seq_num == conn->dcid_set.current_dcid.cid_seq_num) { + if (seq_num == conn->dcid_set.current_dcid.cid_seq_num) { + // TODO: DCID changes ret = xqc_get_unused_cid(&conn->dcid_set.cid_set, &conn->dcid_set.current_dcid); if (ret != XQC_OK) { xqc_log(conn->log, XQC_LOG_ERROR, "|conn don't have available dcid|"); return ret; - } + } + xqc_datagram_record_mss(conn); } xqc_log(conn->log, XQC_LOG_DEBUG, "|get_new_dcid:%s|seq_num:%ui|", xqc_dcid_str(&conn->dcid_set.current_dcid), conn->dcid_set.current_dcid.cid_seq_num); @@ -987,7 +1243,8 @@ xqc_write_retire_conn_id_frame_to_packet(xqc_connection_t *conn, uint64_t seq_nu xqc_int_t -xqc_write_path_challenge_frame_to_packet(xqc_connection_t *conn, xqc_path_ctx_t *path) +xqc_write_path_challenge_frame_to_packet(xqc_connection_t *conn, + xqc_path_ctx_t *path, xqc_bool_t attach_path_status) { xqc_int_t ret = XQC_ERROR; @@ -1005,9 +1262,27 @@ xqc_write_path_challenge_frame_to_packet(xqc_connection_t *conn, xqc_path_ctx_t packet_out->po_used_size += ret; - packet_out->po_is_path_specified = XQC_TRUE; + packet_out->po_path_flag |= XQC_PATH_SPECIFIED_BY_PCPR; packet_out->po_path_id = path->path_id; + if (attach_path_status) { + path->app_path_status_send_seq_num++; + //TODO: MPQUIC fix migration + ret = xqc_gen_path_status_frame(conn, packet_out, path->path_scid.cid_seq_num, + path->app_path_status_send_seq_num, (uint64_t)path->app_path_status); + if (ret < 0) { + /* ignore */ + xqc_log(conn->log, XQC_LOG_ERROR, "|xqc_gen_path_status_frame error|%d|", ret); + + } else { + xqc_log(conn->log, XQC_LOG_DEBUG, + "|initial_path_status|status:%d|frames:%s|", + path->app_path_status, + xqc_frame_type_2_str(packet_out->po_frame_types)); + packet_out->po_used_size += ret; + } + } + xqc_send_queue_move_to_high_pri(&packet_out->po_list, conn->conn_send_queue); xqc_log(conn->log, XQC_LOG_DEBUG, "|path:%ui|", path->path_id); @@ -1038,7 +1313,7 @@ xqc_write_path_response_frame_to_packet(xqc_connection_t *conn, xqc_path_ctx_t * packet_out->po_used_size += ret; - packet_out->po_is_path_specified = XQC_TRUE; + packet_out->po_path_flag |= XQC_PATH_SPECIFIED_BY_PCPR; packet_out->po_path_id = path->path_id; xqc_send_queue_move_to_high_pri(&packet_out->po_list, conn->conn_send_queue); @@ -1053,96 +1328,6 @@ xqc_write_path_response_frame_to_packet(xqc_connection_t *conn, xqc_path_ctx_t * } -int -xqc_write_ack_mp_to_packets(xqc_connection_t *conn) -{ - XQC_DEBUG_PRINT - xqc_pkt_num_space_t pns; - xqc_packet_out_t *packet_out; - xqc_pkt_type_t pkt_type; - xqc_list_head_t *pos, *next; - - int ret; - - xqc_path_ctx_t *path; - xqc_list_head_t *path_pos, *path_next; - - for (pns = 0; pns < XQC_PNS_N; ++pns) { - if (!(conn->conn_flag & (XQC_CONN_FLAG_SHOULD_ACK_INIT << pns))) { - continue; - } - - xqc_list_for_each_safe(path_pos, path_next, &conn->conn_paths_list) { - - path = xqc_list_entry(path_pos, xqc_path_ctx_t, path_list); - if (path->path_state < XQC_PATH_STATE_VALIDATING) { - continue; - } - xqc_pn_ctl_t *pn_ctl = xqc_get_pn_ctl(conn, path); - - xqc_pktno_range_node_t *first_range = NULL; - xqc_list_head_t *tmp_pos, *tmp_next; - xqc_list_for_each_safe(tmp_pos, tmp_next, &pn_ctl->ctl_recv_record[pns].list_head) { - first_range = xqc_list_entry(tmp_pos, xqc_pktno_range_node_t, list); - break; - } - if (first_range == NULL) { - continue; - } - - if (pns == XQC_PNS_HSK) { - pkt_type = XQC_PTYPE_HSK; - - } else if (pns == XQC_PNS_INIT) { - pkt_type = XQC_PTYPE_INIT; - - } else { - pkt_type = XQC_PTYPE_SHORT_HEADER; - } - -path_buffer: - xqc_list_for_each_safe(pos, next, &path->path_schedule_buf[XQC_SEND_TYPE_NORMAL]) { - packet_out = xqc_list_entry(pos, xqc_packet_out_t, po_list); - if (packet_out->po_pkt.pkt_type == pkt_type) { - ret = xqc_write_ack_mp_to_one_packet(conn, path, packet_out, pns); - if (ret == -XQC_ENOBUF) { - xqc_log(conn->log, XQC_LOG_DEBUG, "|xqc_write_ack_to_one_packet try new packet|"); - goto write_new; - - } else if (ret == XQC_OK) { - goto done; - - } else { - return ret; - } - } - goto write_new; - } - -write_new: - /* 指定 MP_ACK 从原路径发送 */ - packet_out = xqc_write_new_packet(conn, pkt_type); - if (packet_out == NULL) { - xqc_log(conn->log, XQC_LOG_ERROR, "|xqc_write_new_packet error|"); - return -XQC_EWRITE_PKT; - } - - ret = xqc_write_ack_mp_to_one_packet(conn, path, packet_out, pns); - if (ret != XQC_OK) { - xqc_log(conn->log, XQC_LOG_ERROR, "|xqc_write_ack_mp_to_one_packet error|ret:%d|", ret); - return ret; - } -pure_ack: - /* send ack packet first */ - xqc_send_queue_move_to_high_pri(&packet_out->po_list, conn->conn_send_queue); -done: - xqc_log(conn->log, XQC_LOG_DEBUG, "|path:%ui|pns:%d|", path->path_id, pns); - - } - } - return XQC_OK; -} - int xqc_write_ack_mp_to_one_packet(xqc_connection_t *conn, xqc_path_ctx_t *path, xqc_packet_out_t *packet_out, xqc_pkt_num_space_t pns) @@ -1154,7 +1339,8 @@ xqc_write_ack_mp_to_one_packet(xqc_connection_t *conn, xqc_path_ctx_t *path, xqc_pn_ctl_t *pn_ctl = xqc_get_pn_ctl(conn, path); - ret = xqc_gen_ack_mp_frame(conn, path->path_id, packet_out, now, + //TODO: MPQUIC fix migration + ret = xqc_gen_ack_mp_frame(conn, path->path_scid.cid_seq_num, packet_out, now, conn->local_settings.ack_delay_exponent, &pn_ctl->ctl_recv_record[packet_out->po_pkt.pkt_pns], path->path_send_ctl->ctl_largest_recv_time[pns], @@ -1168,7 +1354,7 @@ xqc_write_ack_mp_to_one_packet(xqc_connection_t *conn, xqc_path_ctx_t *path, packet_out->po_used_size += ret; packet_out->po_largest_ack = largest_ack; - packet_out->po_is_path_specified = XQC_TRUE; + packet_out->po_path_flag |= XQC_PATH_SPECIFIED_BY_ACK; packet_out->po_path_id = path->path_id; path->path_send_ctl->ctl_ack_eliciting_pkt[pns] = 0; @@ -1199,23 +1385,22 @@ xqc_write_path_abandon_frame_to_packet(xqc_connection_t *conn, xqc_path_ctx_t *p return -XQC_EWRITE_PKT; } - uint64_t path_id_type = 0x00; - uint64_t path_id_content = path->path_id; + /* dcid_seq_num = path->scid.cid_seq_num */ + //TODO: MPQUIC fix migration + uint64_t dcid_seq_num = path->path_scid.cid_seq_num; - ret = xqc_gen_path_abandon_frame(packet_out, path_id_type, path_id_content, 0); + ret = xqc_gen_path_abandon_frame(conn, packet_out, dcid_seq_num, 0); if (ret < 0) { xqc_log(conn->log, XQC_LOG_ERROR, "|xqc_gen_path_abandon_frame error|%d|", ret); goto error; } - packet_out->po_abandon_path_id = path->path_id; - packet_out->po_used_size += ret; xqc_send_queue_move_to_high_pri(&packet_out->po_list, conn->conn_send_queue); - xqc_log(conn->log, XQC_LOG_DEBUG, "|path:%ui|path_id_type:%ui|path_id_content:%ui|", - path->path_id, path_id_type, path_id_content); + xqc_log(conn->log, XQC_LOG_DEBUG, "|path:%ui|dcid_seq_num:%ui|", + path->path_id, dcid_seq_num); return XQC_OK; @@ -1236,7 +1421,8 @@ xqc_write_path_status_frame_to_packet(xqc_connection_t *conn, xqc_path_ctx_t *pa } path->app_path_status_send_seq_num++; - ret = xqc_gen_path_status_frame(packet_out, 0, path->path_dcid.cid_seq_num, + //TODO: MPQUIC fix migration + ret = xqc_gen_path_status_frame(conn, packet_out, path->path_scid.cid_seq_num, path->app_path_status_send_seq_num, (uint64_t)path->app_path_status); if (ret < 0) { xqc_log(conn->log, XQC_LOG_ERROR, "|xqc_gen_path_status_frame error|%d|", ret); @@ -1244,12 +1430,8 @@ xqc_write_path_status_frame_to_packet(xqc_connection_t *conn, xqc_path_ctx_t *pa } packet_out->po_used_size += ret; - xqc_send_queue_move_to_high_pri(&packet_out->po_list, conn->conn_send_queue); - xqc_log(conn->log, XQC_LOG_DEBUG, "|path:%ui|path_status_seq_num:%ui|app_path_status:%d|tra_path_status:%d|", - path->path_id, path->app_path_status_send_seq_num, path->app_path_status, path->tra_path_status); - return XQC_OK; error: diff --git a/src/transport/xqc_packet_out.h b/src/transport/xqc_packet_out.h index ea9e75cd3..b57baa499 100644 --- a/src/transport/xqc_packet_out.h +++ b/src/transport/xqc_packet_out.h @@ -16,8 +16,10 @@ * datagrams larger than the smallest allowed maximum datagram size. */ /* without XQC_EXTRA_SPACE & XQC_ACK_SPACE */ +#define XQC_MAX_PACKET_OUT_SIZE XQC_QUIC_MAX_MSS #define XQC_PACKET_OUT_SIZE XQC_QUIC_MIN_MSS #define XQC_PACKET_OUT_EXT_SPACE (XQC_TLS_AEAD_OVERHEAD_MAX_LEN + XQC_ACK_SPACE) +#define XQC_PACKET_OUT_BUF_CAP (XQC_MAX_PACKET_OUT_SIZE + XQC_PACKET_OUT_EXT_SPACE) #define XQC_MAX_STREAM_FRAME_IN_PO 3 @@ -37,6 +39,11 @@ typedef enum { XQC_POF_IN_UNACK_LIST = 1 << 12, /* FIXED: reset when copy */ XQC_POF_NOT_SCHEDULE = 1 << 13, XQC_POF_NOT_REINJECT = 1 << 14, + XQC_POF_DROPPED_DGRAM = 1 << 15, + XQC_POF_REINJECT_DIFF_PATH = 1 << 16, + XQC_POF_PMTUD_PROBING = 1 << 17, + XQC_POF_QOS_HIGH = 1 << 18, + XQC_POF_QOS_PROBING = 1 << 19, } xqc_packet_out_flag_t; typedef struct xqc_po_stream_frame_s { @@ -89,19 +96,35 @@ typedef struct xqc_packet_out_s { /* how many packets have been lost when the packet is sent */ uint32_t po_lost; + /* only meaningful if it contains a DATAGRAM frame */ + uint64_t po_dgram_id; + /* Multipath */ - xqc_bool_t po_is_path_specified; + uint8_t po_path_flag; uint64_t po_path_id; - uint64_t po_abandon_path_id; unsigned int po_cc_size; /* TODO: check cc size != send size */ /* Reinjection */ uint64_t po_stream_offset; uint64_t po_stream_id; - xqc_packet_out_t *po_reinj_origin; - xqc_packet_out_t *po_reinj_replica; + + /* PMTUD Probing */ + size_t po_max_pkt_out_size; + + /* ping notification */ + xqc_ping_record_t *po_pr; } xqc_packet_out_t; +xqc_bool_t xqc_packet_out_on_specific_path(xqc_connection_t *conn, + xqc_packet_out_t *po, xqc_path_ctx_t **path); + +xqc_bool_t xqc_packet_out_can_attach_ack(xqc_packet_out_t *po, + xqc_path_ctx_t *path, xqc_pkt_type_t pkt_type); + +xqc_bool_t xqc_packet_out_can_pto_probe(xqc_packet_out_t *po, uint64_t path_id); + +void xqc_packet_out_remove_ack_frame(xqc_packet_out_t *po); + xqc_packet_out_t *xqc_packet_out_create(size_t po_buf_cap); void xqc_packet_out_copy(xqc_packet_out_t *dst, xqc_packet_out_t *src); @@ -123,11 +146,15 @@ xqc_packet_out_t *xqc_write_packet_for_stream(xqc_connection_t *conn, xqc_pkt_ty int xqc_write_packet_header(xqc_connection_t *conn, xqc_packet_out_t *packet_out); -int xqc_write_ack_to_packets(xqc_connection_t *conn); +xqc_int_t xqc_write_ack_or_mp_ack_to_packets(xqc_connection_t *conn); + +xqc_int_t xqc_write_ack_or_mp_ack_to_one_packet(xqc_connection_t *conn, xqc_packet_out_t *packet_out, + xqc_pkt_num_space_t pns, xqc_path_ctx_t *path, xqc_bool_t is_mp_ack); int xqc_write_ack_to_one_packet(xqc_connection_t *conn, xqc_packet_out_t *packet_out, xqc_pkt_num_space_t pns); -int xqc_write_ping_to_packet(xqc_connection_t *conn, void *po_user_data, xqc_bool_t notify); +int xqc_write_ping_to_packet(xqc_connection_t *conn, xqc_path_ctx_t *path, + void *po_user_data, xqc_bool_t notify, xqc_ping_record_t *pr); int xqc_write_conn_close_to_packet(xqc_connection_t *conn, uint64_t err_code); @@ -143,7 +170,8 @@ int xqc_write_streams_blocked_to_packet(xqc_connection_t *conn, uint64_t stream_ int xqc_write_max_data_to_packet(xqc_connection_t *conn, uint64_t max_data); -int xqc_write_max_stream_data_to_packet(xqc_connection_t *conn, xqc_stream_id_t stream_id, uint64_t max_stream_data); +int xqc_write_max_stream_data_to_packet(xqc_connection_t *conn, +xqc_stream_id_t stream_id, uint64_t max_stream_data, xqc_pkt_type_t xqc_pkt_type); int xqc_write_max_streams_to_packet(xqc_connection_t *conn, uint64_t max_stream, int bidirectional); @@ -152,19 +180,22 @@ int xqc_write_new_token_to_packet(xqc_connection_t *conn); int xqc_write_stream_frame_to_packet(xqc_connection_t *conn, xqc_stream_t *stream, xqc_pkt_type_t pkt_type, uint8_t fin, const unsigned char *payload, size_t payload_size, size_t *send_data_written); +int xqc_write_datagram_frame_to_packet(xqc_connection_t *conn, xqc_pkt_type_t pkt_type, + const unsigned char *data, size_t data_len, uint64_t *dgram_id, xqc_bool_t use_supplied_dgram_id, + xqc_data_qos_level_t qos_level); + int xqc_write_handshake_done_frame_to_packet(xqc_connection_t *conn); xqc_int_t xqc_write_new_conn_id_frame_to_packet(xqc_connection_t *conn, uint64_t retire_prior_to); xqc_int_t xqc_write_retire_conn_id_frame_to_packet(xqc_connection_t *conn, uint64_t seq_num); -xqc_int_t xqc_write_path_challenge_frame_to_packet(xqc_connection_t *conn, xqc_path_ctx_t *path); +xqc_int_t xqc_write_path_challenge_frame_to_packet(xqc_connection_t *conn, xqc_path_ctx_t *path, + xqc_bool_t attach_path_status); xqc_int_t xqc_write_path_response_frame_to_packet(xqc_connection_t *conn, xqc_path_ctx_t *path, unsigned char *path_response_data); -int xqc_write_ack_mp_to_packets(xqc_connection_t *conn); - int xqc_write_ack_mp_to_one_packet(xqc_connection_t *conn, xqc_path_ctx_t *path, xqc_packet_out_t *packet_out, xqc_pkt_num_space_t pns); @@ -172,5 +203,7 @@ xqc_int_t xqc_write_path_abandon_frame_to_packet(xqc_connection_t *conn, xqc_pat xqc_int_t xqc_write_path_status_frame_to_packet(xqc_connection_t *conn, xqc_path_ctx_t *path); +int xqc_write_pmtud_ping_to_packet(xqc_path_ctx_t *path, size_t probing_size, xqc_pkt_type_t pkt_type); + #endif /* _XQC_PACKET_OUT_H_INCLUDED_ */ diff --git a/src/transport/xqc_packet_parser.c b/src/transport/xqc_packet_parser.c index 664317742..a55ed8e28 100644 --- a/src/transport/xqc_packet_parser.c +++ b/src/transport/xqc_packet_parser.c @@ -17,7 +17,7 @@ #include "src/transport/xqc_utils.h" #include "src/transport/xqc_send_ctl.h" #include "src/transport/xqc_defs.h" -#include "src/http3/xqc_h3_conn.h" +#include "src/common/xqc_random.h" #define xqc_packet_number_bits2len(b) ((b) + 1) @@ -241,6 +241,7 @@ xqc_packet_parse_short_header(xqc_connection_t *c, xqc_packet_in_t *packet_in) unsigned char *pos = packet_in->pos; unsigned char *end = packet_in->last; xqc_packet_t *packet = &packet_in->pi_pkt; + xqc_path_ctx_t *path = NULL; uint8_t cid_len = c->scid_set.user_scid.cid_len; packet_in->pi_pkt.pkt_type = XQC_PTYPE_SHORT_HEADER; @@ -261,22 +262,53 @@ xqc_packet_parse_short_header(xqc_connection_t *c, xqc_packet_in_t *packet_in) xqc_uint_t spin_bit = (pos[0] & 0x20) >> 5; pos += 1; - /* TODO: remove custom logic and disable spin bit */ - if (spin_bit == 0x00) { - packet_in->pi_flag |= XQC_PIF_REINJECTED_REPLICA; - } - /* check dcid */ xqc_cid_set(&(packet->pkt_dcid), pos, cid_len); pos += cid_len; if (xqc_conn_check_dcid(c, &(packet->pkt_dcid)) != XQC_OK) { - /* log & ignore */ - xqc_log(c->log, XQC_LOG_ERROR, "|parse short header|invalid destination cid, pkt dcid: %s, conn scid: %s|", + /* log & ignore, the pkt might be corrupted or stateless reset */ + xqc_log(c->log, XQC_LOG_WARN, "|parse short header|invalid destination cid, pkt dcid: %s, conn scid: %s|", xqc_dcid_str(&packet->pkt_dcid), xqc_scid_str(&c->scid_set.user_scid)); return -XQC_EILLPKT; } - packet_in->pi_path_id = packet->pkt_dcid.cid_seq_num; + //TODO: MPQUIC fix migration + if (c->enable_multipath) { + /* try to find the path */ + path = xqc_conn_find_path_by_scid(c, &packet_in->pi_pkt.pkt_dcid); +#ifndef XQC_NO_PID_PACKET_PROCESS + /* Note: to handle the case in which the server changes CID upon NAT rebinding */ + if (path == NULL && packet_in->pi_path_id != XQC_UNKNOWN_PATH_ID) { + path = xqc_conn_find_path_by_path_id(c, packet_in->pi_path_id); + + if (path == NULL) { + xqc_log(c->log, XQC_LOG_ERROR, "|can not find path|dcid:%s|path_id:%ui|", + xqc_dcid_str(&packet_in->pi_pkt.pkt_dcid), packet_in->pi_path_id); + return -XQC_EILLPKT; + } + + /* update the cid to a newer one */ + if (xqc_cid_is_equal(&path->path_last_scid, &packet_in->pi_pkt.pkt_dcid) != XQC_OK) { + xqc_cid_copy(&path->path_last_scid, &path->path_scid); + xqc_cid_copy(&path->path_scid, &packet_in->pi_pkt.pkt_dcid); + xqc_log(c->log, XQC_LOG_DEBUG, "|update_path_scid|%s->%s|", + xqc_scid_str(&path->path_last_scid), + xqc_dcid_str(&path->path_scid)); + } + } +#endif + + } else { + path = c->conn_initial_path; + } + + if (path != NULL) { + packet_in->pi_path_id = path->path_id; + + } else { + packet_in->pi_path_id = XQC_UNKNOWN_PATH_ID; + } + xqc_log(c->log, XQC_LOG_DEBUG, "|parse short header|path:%ui|pkt_dcid:%s|spin_bit:%ud|", packet_in->pi_path_id, xqc_scid_str(&(packet->pkt_dcid)), spin_bit); @@ -331,14 +363,23 @@ xqc_short_packet_update_dcid(xqc_packet_out_t *packet_out, xqc_cid_t dcid) } /* TODO: remove custom logic and disable spin bit */ -void xqc_short_packet_update_custom_spin_bit(xqc_packet_out_t *packet_out) +void xqc_packet_update_reserved_bits(xqc_packet_out_t *packet_out) { if (packet_out->po_pkt.pkt_type == XQC_PTYPE_SHORT_HEADER && packet_out->po_flag & XQC_POF_REINJECTED_REPLICA) { unsigned char *dst_buf = packet_out->po_buf; - dst_buf[0] &= (~(1<<5)); + /* reserved bits 10 */ + dst_buf[0] |= (1 << 4); } + + if (packet_out->po_pkt.pkt_type == XQC_PTYPE_0RTT + && packet_out->po_flag & XQC_POF_REINJECTED_REPLICA) + { + /* reserved bits 10 */ + unsigned char *dst_buf = packet_out->po_buf; + dst_buf[0] |= (1 << 3); + } } int @@ -594,6 +635,14 @@ xqc_packet_encrypt_buf(xqc_connection_t *conn, xqc_packet_out_t *packet_out, uint8_t *dst_pktno = dst_header + (pktno - header); uint8_t *dst_payload = dst_header + header_len; uint8_t *dst_end = dst_payload; + xqc_path_ctx_t *path = xqc_conn_find_path_by_path_id(conn, packet_out->po_path_id); + + if (path == NULL) { + XQC_CONN_ERR(conn, TRA_INTERNAL_ERROR); + xqc_log(conn->log, XQC_LOG_ERROR, "|path:%ui|does not exist|", + packet_out->po_path_id); + return XQC_EMP_PATH_NOT_FOUND; + } /* copy header to dest */ xqc_memcpy(dst_header, header, header_len); @@ -609,8 +658,9 @@ xqc_packet_encrypt_buf(xqc_connection_t *conn, xqc_packet_out_t *packet_out, } /* do packet protection */ - uint32_t nonce_path_id = (conn->enable_multipath == XQC_CONN_MULTIPATH_MULTIPLE_PNS) ? - (uint32_t)packet_out->po_path_id : 0; + //TODO: MPQUIC fix migration + uint32_t nonce_path_id = (conn->enable_multipath) ? + (uint32_t)path->path_dcid.cid_seq_num : 0; ret = xqc_tls_encrypt_payload(conn->tls, level, packet_out->po_pkt.pkt_num, nonce_path_id, dst_header, header_len, payload, payload_len, @@ -712,23 +762,43 @@ xqc_packet_decrypt(xqc_connection_t *conn, xqc_packet_in_t *packet_in) size_t header_len = packet_in->pi_pkt.pkt_num_offset + pktno_len; uint8_t *payload = header + header_len; size_t payload_len = packet_in->pi_pkt.length - pktno_len; + uint8_t reserved_bits = 0; + + if (packet_in->pi_pkt.pkt_type == XQC_PTYPE_0RTT) { + reserved_bits = (header[0] & 0x0c) >> 2; + } else if (packet_in->pi_pkt.pkt_type == XQC_PTYPE_SHORT_HEADER) { + reserved_bits = (header[0] & 0x18) >> 3; + } + + if (reserved_bits == 0x02 && conn->conn_settings.marking_reinjection) { + packet_in->pi_flag |= XQC_PIF_REINJECTED_REPLICA; + } /* parse packet number from header */ xqc_packet_number_t truncated_pn; xqc_packet_parse_packet_number(pktno, pktno_len, &truncated_pn); /* decode packet number */ - xqc_path_ctx_t *path = xqc_conn_find_path_by_path_id(conn, packet_in->pi_path_id); - if (path == NULL) { - path = conn->conn_initial_path; + // TODO: MPQUIC fix migration + xqc_packet_number_t largest_pn = 0; + if (packet_in->pi_path_id != XQC_UNKNOWN_PATH_ID) { + xqc_path_ctx_t *path = xqc_conn_find_path_by_path_id(conn, packet_in->pi_path_id); + if (path == NULL) { + xqc_log(conn->log, XQC_LOG_ERROR, + "|canno find the path|path_id:%ui|", + packet_in->pi_path_id); + return -XQC_EMP_PATH_NOT_FOUND; + } + xqc_pn_ctl_t *pn_ctl = xqc_get_pn_ctl(conn, path); + xqc_pkt_num_space_t pns = packet_in->pi_pkt.pkt_pns; + largest_pn = xqc_recv_record_largest(&pn_ctl->ctl_recv_record[pns]); } - xqc_pn_ctl_t *pn_ctl = xqc_get_pn_ctl(conn, path); - - xqc_pkt_num_space_t pns = packet_in->pi_pkt.pkt_pns; - xqc_packet_number_t largest_pn = xqc_recv_record_largest(&pn_ctl->ctl_recv_record[pns]); + packet_in->pi_pkt.pkt_num = xqc_packet_decode_packet_number(largest_pn, truncated_pn, pktno_len * 8); + xqc_log(conn->log, XQC_LOG_DEBUG, "|largest_pn:%ui|", largest_pn); + /* check key phase, determine weather to update read keys */ xqc_uint_t key_phase = XQC_PACKET_SHORT_HEADER_KEY_PHASE(header); if (packet_in->pi_pkt.pkt_type == XQC_PTYPE_SHORT_HEADER && level == XQC_ENC_LEV_1RTT @@ -744,8 +814,9 @@ xqc_packet_decrypt(xqc_connection_t *conn, xqc_packet_in_t *packet_in) } /* decrypt packet payload */ - uint32_t nonce_path_id = (conn->enable_multipath == XQC_CONN_MULTIPATH_MULTIPLE_PNS) ? - (uint32_t)packet_in->pi_path_id : 0; + // TODO: MPQUIC fix migration + uint32_t nonce_path_id = (conn->enable_multipath) ? + (uint32_t)packet_in->pi_pkt.pkt_dcid.cid_seq_num : 0; ret = xqc_tls_decrypt_payload(conn->tls, level, packet_in->pi_pkt.pkt_num, nonce_path_id, header, header_len, payload, payload_len, @@ -1214,6 +1285,8 @@ xqc_packet_parse_long_header(xqc_connection_t *c, xqc_packet_t *packet = &packet_in->pi_pkt; xqc_int_t ret = XQC_ERROR; + packet_in->pi_path_id = XQC_INITIAL_PATH_ID; + if (XQC_BUFF_LEFT_SIZE(pos, end) < XQC_PACKET_LONG_HEADER_PREFIX_LENGTH + 2) { return -XQC_EILLPKT; } @@ -1357,15 +1430,21 @@ xqc_gen_reset_token(xqc_cid_t *cid, unsigned char *token, int token_len, char *k Figure 6: Stateless Reset Packet */ xqc_int_t -xqc_gen_reset_packet(xqc_cid_t *cid, unsigned char *dst_buf, char *key, size_t keylen) +xqc_gen_reset_packet(xqc_cid_t *cid, unsigned char *dst_buf, char *key, + size_t keylen, size_t max_len, xqc_random_generator_t *rand_generator) { + static const char sr_first_byte_mask = 0x3f; + static const char sr_fixed_bits = 0x40; + const unsigned char *begin = dst_buf; - const int unpredictable_len = 23; - int padding_len; - unsigned char token[XQC_RESET_TOKEN_LEN] = {0}; + size_t unpredictable_len; + unsigned char token[XQC_RESET_TOKEN_LEN] = {0}; +#ifdef XQC_COMPAT_GENERATE_SR_PKT + int padding_len; dst_buf[0] = 0x40; dst_buf++; + unpredictable_len = 23; if (cid->cid_len > 0) { memcpy(dst_buf, cid->cid_buf, cid->cid_len); @@ -1383,6 +1462,17 @@ xqc_gen_reset_packet(xqc_cid_t *cid, unsigned char *dst_buf, char *key, size_t k memset(dst_buf, 0, padding_len); dst_buf += padding_len; +#else + /* write unpredictable bits */ + unpredictable_len = max_len - XQC_RESET_TOKEN_LEN; + xqc_get_random(rand_generator, dst_buf, unpredictable_len); + + /* write fixed bits */ + dst_buf[0] = (dst_buf[0] & sr_first_byte_mask) | sr_fixed_bits; + dst_buf += unpredictable_len; +#endif + + /* write sr token */ xqc_gen_reset_token(cid, token, XQC_RESET_TOKEN_LEN, key, keylen); memcpy(dst_buf, token, sizeof(token)); dst_buf += sizeof(token); @@ -1390,8 +1480,22 @@ xqc_gen_reset_packet(xqc_cid_t *cid, unsigned char *dst_buf, char *key, size_t k return dst_buf - begin; } +xqc_int_t +xqc_packet_parse_stateless_reset(const unsigned char *buf, size_t buf_size, + const uint8_t **sr_token) +{ + if (buf_size <= XQC_STATELESS_RESET_PKT_MIN_LEN) { + return -XQC_EILLPKT; + } + + *sr_token = buf + buf_size - XQC_STATELESS_RESET_TOKENLEN; + return XQC_OK; +} + +#ifdef XQC_COMPAT_GENERATE_SR_PKT int -xqc_is_reset_packet(xqc_cid_t *cid, const unsigned char *buf, unsigned buf_size, char *key, size_t keylen) +xqc_is_deprecated_reset_packet(xqc_cid_t *cid, const unsigned char *buf, + unsigned buf_size, char *key, size_t keylen) { if (XQC_PACKET_IS_LONG_HEADER(buf)) { return 0; @@ -1412,3 +1516,4 @@ xqc_is_reset_packet(xqc_cid_t *cid, const unsigned char *buf, unsigned buf_size, } return 0; } +#endif \ No newline at end of file diff --git a/src/transport/xqc_packet_parser.h b/src/transport/xqc_packet_parser.h index ecbcefbec..a90b4db60 100644 --- a/src/transport/xqc_packet_parser.h +++ b/src/transport/xqc_packet_parser.h @@ -31,7 +31,7 @@ void xqc_short_packet_update_key_phase(xqc_packet_out_t *packet_out, xqc_uint_t void xqc_short_packet_update_dcid(xqc_packet_out_t *packet_out, xqc_cid_t dcid); -void xqc_short_packet_update_custom_spin_bit(xqc_packet_out_t *packet_out); +void xqc_packet_update_reserved_bits(xqc_packet_out_t *packet_out); int xqc_gen_long_packet_header(xqc_packet_out_t *packet_out, const unsigned char *dcid, unsigned char dcid_len, @@ -59,9 +59,15 @@ xqc_int_t xqc_packet_parse_retry(xqc_connection_t *c, xqc_packet_in_t *packet_in xqc_int_t xqc_packet_parse_version_negotiation(xqc_connection_t *c, xqc_packet_in_t *packet_in); -xqc_int_t xqc_gen_reset_packet(xqc_cid_t *cid, unsigned char *dst_buf, char *key, size_t keylen); +xqc_int_t xqc_gen_reset_packet(xqc_cid_t *cid, unsigned char *dst_buf, + char *key, size_t keylen, size_t max_len, + xqc_random_generator_t *rand_generator); -int xqc_is_reset_packet(xqc_cid_t *cid, const unsigned char *buf, unsigned buf_size, char *key, size_t keylen); +#ifdef XQC_COMPAT_GENERATE_SR_PKT +/* deprecated stateless reset of older versions of xquic */ +int xqc_is_deprecated_reset_packet(xqc_cid_t *cid, const unsigned char *buf, + unsigned buf_size, char *key, size_t keylen); +#endif xqc_int_t xqc_packet_decrypt(xqc_connection_t *conn, xqc_packet_in_t *packet_in); @@ -72,4 +78,7 @@ xqc_int_t xqc_packet_encrypt_buf(xqc_connection_t *conn, xqc_packet_out_t *packe void xqc_gen_reset_token(xqc_cid_t *cid, unsigned char *token, int token_len, char *key, size_t keylen); +xqc_int_t xqc_packet_parse_stateless_reset(const unsigned char *buf, + size_t buf_size, const uint8_t **sr_token); + #endif /* _XQC_PACKET_PARSER_H_INCLUDED_ */ diff --git a/src/transport/xqc_recv_record.c b/src/transport/xqc_recv_record.c index 39111cca1..630d98267 100644 --- a/src/transport/xqc_recv_record.c +++ b/src/transport/xqc_recv_record.c @@ -240,24 +240,6 @@ xqc_recv_record_largest(xqc_recv_record_t *recv_record) } } -xqc_list_head_t * -xqc_recv_record_get_pos(xqc_recv_record_t *recv_record, xqc_packet_number_t packet_number) -{ - xqc_pktno_range_node_t *pnode = NULL; - xqc_list_head_t *pos, *next; - xqc_list_for_each_safe(pos, next, &recv_record->list_head) { - pnode = xqc_list_entry(pos, xqc_pktno_range_node_t, list); - - if (packet_number > pnode->pktno_range.high) { - return NULL; - } - else if (packet_number >= pnode->pktno_range.low) { - return pos; - } - } - return NULL; -} - void xqc_maybe_should_ack(xqc_connection_t *conn, xqc_path_ctx_t *path, xqc_pn_ctl_t *pn_ctl, xqc_pkt_num_space_t pns, int out_of_order, xqc_usec_t now) { @@ -283,18 +265,20 @@ xqc_maybe_should_ack(xqc_connection_t *conn, xqc_path_ctx_t *path, xqc_pn_ctl_t xqc_send_ctl_t *send_ctl = path->path_send_ctl; - if (send_ctl->ctl_ack_eliciting_pkt[pns] >= 2 + if (send_ctl->ctl_ack_eliciting_pkt[pns] >= conn->conn_settings.ack_frequency || (pns <= XQC_PNS_HSK && send_ctl->ctl_ack_eliciting_pkt[pns] >= 1) || (out_of_order && send_ctl->ctl_ack_eliciting_pkt[pns] >= 1)) { conn->conn_flag |= XQC_CONN_FLAG_SHOULD_ACK_INIT << pns; - conn->should_ack_path_id = path->path_id; xqc_timer_unset(&send_ctl->path_timer_manager, XQC_TIMER_ACK_INIT + pns); xqc_log(conn->log, XQC_LOG_DEBUG, "|yes|path:%ui|out_of_order:%d|ack_eliciting_pkt:%ud|" - "pns:%d|flag:%s|", path->path_id, out_of_order, send_ctl->ctl_ack_eliciting_pkt[pns], - pns, xqc_conn_flag_2_str(conn->conn_flag)); + "pns:%d|flag:%s|ack_freq:%ud|", + path->path_id, out_of_order, + send_ctl->ctl_ack_eliciting_pkt[pns], + pns, xqc_conn_flag_2_str(conn->conn_flag), + conn->conn_settings.ack_frequency); } else if (send_ctl->ctl_ack_eliciting_pkt[pns] > 0 && !xqc_timer_is_set(&send_ctl->path_timer_manager, XQC_TIMER_ACK_INIT + pns)) diff --git a/src/transport/xqc_recv_record.h b/src/transport/xqc_recv_record.h index 37c01bf00..af1091508 100644 --- a/src/transport/xqc_recv_record.h +++ b/src/transport/xqc_recv_record.h @@ -30,10 +30,10 @@ typedef struct xqc_recv_record_s { } xqc_recv_record_t; #define XQC_MAX_ACK_RANGE_CNT 64 -#define XQC_DEFAULT_ACK_RANGE_CNT 4 + typedef struct xqc_ack_info_s { xqc_pkt_num_space_t pns; - uint64_t path_id; + uint64_t dcid_seq_num; unsigned n_ranges; /* must > 0 */ xqc_pktno_range_t ranges[XQC_MAX_ACK_RANGE_CNT]; xqc_usec_t ack_delay; @@ -63,8 +63,6 @@ xqc_pkt_range_status xqc_recv_record_add(xqc_recv_record_t *recv_record, xqc_pac xqc_packet_number_t xqc_recv_record_largest(xqc_recv_record_t *recv_record); -xqc_list_head_t *xqc_recv_record_get_pos(xqc_recv_record_t *recv_record, xqc_packet_number_t packet_number); - void xqc_maybe_should_ack(xqc_connection_t *conn, xqc_path_ctx_t *path, xqc_pn_ctl_t *pn_ctl, xqc_pkt_num_space_t pns, int out_of_order, xqc_usec_t now); int xqc_ack_sent_record_init(xqc_ack_sent_record_t *record); diff --git a/src/transport/xqc_reinjection.c b/src/transport/xqc_reinjection.c index e4a153eb9..ab07de1a2 100644 --- a/src/transport/xqc_reinjection.c +++ b/src/transport/xqc_reinjection.c @@ -23,64 +23,24 @@ #include "xquic/xqc_errno.h" -xqc_bool_t -xqc_packet_can_reinject(xqc_packet_out_t *packet_out) -{ - if (!(packet_out->po_frame_types & XQC_FRAME_BIT_STREAM)) { - return XQC_FALSE; - } - - if (packet_out->po_flag & XQC_POF_NOT_REINJECT) { - return XQC_FALSE; - } - - if (packet_out->po_is_path_specified) { - return XQC_FALSE; - } - - if (XQC_MP_PKT_REINJECTED(packet_out)) { - return XQC_FALSE; - } - - /* DO NOT reinject non-inflight packets (those were old copies of retx pkts) */ - if (!(packet_out->po_flag & XQC_POF_IN_FLIGHT)) { - return XQC_FALSE; - } - - return XQC_TRUE; -} - - void xqc_associate_packet_with_reinjection(xqc_packet_out_t *reinj_origin, xqc_packet_out_t *reinj_replica) { if (reinj_origin) { reinj_origin->po_flag |= XQC_POF_REINJECTED_ORIGIN; + reinj_origin->po_path_flag |= XQC_PATH_SPECIFIED_BY_REINJ; + if (reinj_origin->po_origin) { + reinj_origin->po_origin->po_flag |= XQC_POF_REINJECTED_ORIGIN; + reinj_origin->po_origin->po_path_flag |= XQC_PATH_SPECIFIED_BY_REINJ; + } } if (reinj_replica) { reinj_replica->po_flag |= XQC_POF_REINJECTED_REPLICA; reinj_replica->po_origin = reinj_origin->po_origin ? reinj_origin->po_origin : reinj_origin; reinj_replica->po_origin->po_origin_ref_cnt++; - - } -} - -void -xqc_disassociate_packet_with_reinjection(xqc_packet_out_t *reinj_origin, - xqc_packet_out_t *reinj_replica) -{ - if (reinj_origin) { - reinj_origin->po_flag &= ~XQC_POF_REINJECTED_ORIGIN; - } - - if (reinj_replica) { - reinj_replica->po_flag &= ~XQC_POF_REINJECTED_REPLICA; - if (reinj_replica->po_origin) { - reinj_replica->po_origin->po_origin_ref_cnt--; - reinj_replica->po_origin = NULL; - } + reinj_replica->po_path_flag |= XQC_PATH_SPECIFIED_BY_REINJ; } } @@ -108,10 +68,10 @@ xqc_packet_out_replicate(xqc_packet_out_t *dst, xqc_packet_out_t *src) dst->po_user_data = src->po_user_data; } -ssize_t +xqc_int_t xqc_conn_try_reinject_packet(xqc_connection_t *conn, xqc_packet_out_t *packet_out) { - xqc_path_ctx_t *path = conn->scheduler_callback->xqc_scheduler_get_path(conn->scheduler, conn, packet_out, 1, 1); + xqc_path_ctx_t *path = conn->scheduler_callback->xqc_scheduler_get_path(conn->scheduler, conn, packet_out, 1, 1, NULL); if (path == NULL) { xqc_log(conn->log, XQC_LOG_DEBUG, "|MP|REINJ|fail to schedule a path|reinject|"); return -XQC_EMP_SCHEDULE_PATH; @@ -125,9 +85,14 @@ xqc_conn_try_reinject_packet(xqc_connection_t *conn, xqc_packet_out_t *packet_ou } xqc_packet_out_replicate(po_copy, packet_out); + xqc_packet_out_remove_ack_frame(po_copy); + + /* update path_flag */ + if (po_copy->po_path_flag & XQC_PATH_SPECIFIED_BY_PTO) { + po_copy->po_path_flag &= ~XQC_PATH_SPECIFIED_BY_PTO; + } xqc_associate_packet_with_reinjection(packet_out, po_copy); - po_copy->po_reinj_origin = packet_out; xqc_send_queue_insert_send(po_copy, &send_queue->sndq_send_packets, send_queue); xqc_path_send_buffer_append(path, po_copy, &path->path_reinj_tmp_buf); @@ -142,64 +107,20 @@ xqc_conn_try_reinject_packet(xqc_connection_t *conn, xqc_packet_out_t *packet_ou return XQC_OK; } -void -xqc_conn_reinject_unack_packets_by_deadline(xqc_connection_t *conn) -{ - xqc_list_head_t *pos, *next; - xqc_packet_out_t *packet_out; - - xqc_usec_t now = xqc_monotonic_timestamp(); - xqc_usec_t min_srtt = xqc_conn_get_min_srtt(conn); - - double factor = conn->conn_settings.reinj_flexible_deadline_srtt_factor; - double flexible = factor * min_srtt; - uint64_t hard = conn->conn_settings.reinj_hard_deadline; - uint64_t lower_bound = conn->conn_settings.reinj_deadline_lower_bound; - double deadline = xqc_max(xqc_min(flexible, (double)hard), (double)lower_bound); - xqc_log(conn->log, XQC_LOG_DEBUG, "|deadline:%f|factor:%.4f|min_srtt:%ui|flexible:%f|hard:%ui|lower_bound:%ui|", - deadline, factor, min_srtt, flexible, hard, lower_bound); - - xqc_list_for_each_safe(pos, next, &conn->conn_send_queue->sndq_unacked_packets[XQC_PNS_APP_DATA]) { - packet_out = xqc_list_entry(pos, xqc_packet_out_t, po_list); - - if (xqc_packet_can_reinject(packet_out) != XQC_TRUE) { - continue; - } - - if ((double)(now - packet_out->po_sent_time) < deadline) { - break; - } - - if (xqc_conn_try_reinject_packet(conn, packet_out) != XQC_OK) { - continue; - } - - xqc_log(conn->log, XQC_LOG_DEBUG, "|MP|REINJ|reinject unacked packets|" - "pkt_num:%ui|size:%ud|pkt_type:%s|frame:%s|", - packet_out->po_pkt.pkt_num, packet_out->po_used_size, - xqc_pkt_type_2_str(packet_out->po_pkt.pkt_type), - xqc_frame_type_2_str(packet_out->po_frame_types)); - - } - - xqc_path_ctx_t *path; - xqc_list_for_each_safe(pos, next, &conn->conn_paths_list) { - path = xqc_list_entry(pos, xqc_path_ctx_t, path_list); - xqc_list_splice_init(&path->path_reinj_tmp_buf, - &path->path_schedule_buf[XQC_SEND_TYPE_NORMAL]); - } -} void -xqc_conn_reinject_unack_packets_by_capacity(xqc_connection_t *conn) +xqc_conn_reinject_unack_packets(xqc_connection_t *conn, xqc_reinjection_mode_t mode) { xqc_list_head_t *pos, *next; xqc_packet_out_t *packet_out; xqc_list_for_each_safe(pos, next, &conn->conn_send_queue->sndq_unacked_packets[XQC_PNS_APP_DATA]) { packet_out = xqc_list_entry(pos, xqc_packet_out_t, po_list); - if (xqc_packet_can_reinject(packet_out)) { - + if (conn->reinj_callback + && conn->reinj_callback->xqc_reinj_ctl_can_reinject + && conn->reinj_callback->xqc_reinj_ctl_can_reinject(conn->reinj_ctl, packet_out, mode)) + { + if (xqc_conn_try_reinject_packet(conn, packet_out) != XQC_OK) { continue; } @@ -218,4 +139,4 @@ xqc_conn_reinject_unack_packets_by_capacity(xqc_connection_t *conn) xqc_list_splice_tail_init(&path->path_reinj_tmp_buf, &path->path_schedule_buf[XQC_SEND_TYPE_NORMAL]); } -} +} \ No newline at end of file diff --git a/src/transport/xqc_reinjection.h b/src/transport/xqc_reinjection.h index cbf5e4c2f..ae33d9536 100644 --- a/src/transport/xqc_reinjection.h +++ b/src/transport/xqc_reinjection.h @@ -18,26 +18,14 @@ #define XQC_MP_FIRST_FRAME_OFFSET 128 * 1024 /* 128k */ #define XQC_MP_PKT_REINJECTED(po) (po->po_flag & (XQC_POF_REINJECTED_ORIGIN | XQC_POF_REINJECTED_REPLICA)) -typedef enum { - XQC_REINJ_UNACK_BY_CAPACITY = 1 << 0, - XQC_REINJ_UNACK_BY_DEADLINE = 1 << 1, -} xqc_reinjection_mode_t; - -xqc_bool_t xqc_packet_can_reinject(xqc_packet_out_t *packet_out); - void xqc_associate_packet_with_reinjection(xqc_packet_out_t *reinj_origin, xqc_packet_out_t *reinj_replica); -void xqc_disassociate_packet_with_reinjection(xqc_packet_out_t *reinj_origin, - xqc_packet_out_t *reinj_replica); +void xqc_conn_reinject_unack_packets(xqc_connection_t *conn, + xqc_reinjection_mode_t mode); -/** - * 在发送完原始数据包后,尝试发送重注入数据包,不确保成功发送 - */ -ssize_t xqc_conn_try_reinject_packet(xqc_connection_t *conn, xqc_packet_out_t *packet_out); - -void xqc_conn_reinject_unack_packets_by_deadline(xqc_connection_t *conn); -void xqc_conn_reinject_unack_packets_by_capacity(xqc_connection_t *conn); +xqc_int_t xqc_conn_try_reinject_packet(xqc_connection_t *conn, + xqc_packet_out_t *packet_out); #endif /* XQC_REINJECTION_H */ diff --git a/src/transport/xqc_send_ctl.c b/src/transport/xqc_send_ctl.c index c6f7a4490..206abda09 100644 --- a/src/transport/xqc_send_ctl.c +++ b/src/transport/xqc_send_ctl.c @@ -18,9 +18,60 @@ #include "src/congestion_control/xqc_sample.h" #include "src/transport/xqc_pacing.h" #include "src/transport/xqc_utils.h" +#include "src/transport/xqc_datagram.h" +#include "src/transport/xqc_reinjection.h" + +int +xqc_send_ctl_may_remove_unacked_dgram(xqc_connection_t *conn, xqc_packet_out_t *po) +{ + // it is only called from loss detection function + // po must not be inflight + // po must be in unacked list + po->po_flag |= XQC_POF_DROPPED_DGRAM; + if (po->po_origin == NULL) { + if (po->po_origin_ref_cnt == 0) { + xqc_send_queue_remove_unacked(po, conn->conn_send_queue); + xqc_send_queue_insert_free(po, &conn->conn_send_queue->sndq_free_packets, conn->conn_send_queue); + return 1; + } + return 0; + } + + po->po_origin->po_flag |= XQC_POF_DROPPED_DGRAM; + if (po->po_origin->po_origin_ref_cnt >= 1) { + po->po_origin->po_origin_ref_cnt--; + } + + if (po->po_origin->po_origin_ref_cnt == 0) { + // po_origin must be in unacked list && po_origin must be ahead of po in unacked list + // if po_origin is still inflight, it will be removed when it is detected as lost + if (!(po->po_origin->po_flag & XQC_POF_IN_FLIGHT)) { + xqc_send_queue_remove_unacked(po->po_origin, conn->conn_send_queue); + xqc_send_queue_insert_free(po->po_origin, &conn->conn_send_queue->sndq_free_packets, conn->conn_send_queue); + } + } + // remove po: it must not be inflight + xqc_send_queue_remove_unacked(po, conn->conn_send_queue); + xqc_send_queue_insert_free(po, &conn->conn_send_queue->sndq_free_packets, conn->conn_send_queue); + + return 1; +} + +void +xqc_send_ctl_on_dgram_dropped(xqc_connection_t *conn, xqc_packet_out_t *po) +{ + if (po->po_flag & XQC_POF_IN_FLIGHT) { + xqc_send_ctl_decrease_inflight(conn, po); + } + + po->po_flag |= XQC_POF_DROPPED_DGRAM; + if (po->po_origin) { + po->po_origin->po_flag |= XQC_POF_DROPPED_DGRAM; + } +} int -xqc_send_ctl_indirectly_ack_po(xqc_connection_t *conn, xqc_packet_out_t *packet_out) +xqc_send_ctl_indirectly_ack_or_drop_po(xqc_connection_t *conn, xqc_packet_out_t *packet_out) { xqc_path_ctx_t *path = xqc_conn_find_path_by_path_id(conn, packet_out->po_path_id); if (path == NULL) { @@ -40,6 +91,17 @@ xqc_send_ctl_indirectly_ack_po(xqc_connection_t *conn, xqc_packet_out_t *packet_ xqc_send_queue_maybe_remove_unacked(packet_out, send_queue, path); return XQC_TRUE; } + + if (packet_out->po_frame_types & XQC_FRAME_BIT_DATAGRAM) { + if ((packet_out->po_flag & XQC_POF_DROPPED_DGRAM) + || (packet_out->po_origin && (packet_out->po_origin->po_flag & XQC_POF_DROPPED_DGRAM))) + { + xqc_send_ctl_on_dgram_dropped(conn, packet_out); + xqc_send_queue_maybe_remove_unacked(packet_out, send_queue, path); + return XQC_TRUE; + } + } + return XQC_FALSE; } @@ -59,19 +121,17 @@ xqc_send_ctl_create(xqc_path_ctx_t *path) send_ctl->ctl_conn = conn; send_ctl->ctl_pto_count = 0; - send_ctl->ctl_pto_count_since_last_tra_path_status_changed = 0; send_ctl->ctl_minrtt = XQC_MAX_UINT32_VALUE; send_ctl->ctl_srtt = XQC_kInitialRtt * 1000; send_ctl->ctl_rttvar = XQC_kInitialRtt * 1000 / 2; + send_ctl->ctl_latest_rtt = 0; send_ctl->ctl_max_bytes_in_flight = 0; - send_ctl->ctl_reordering_packet_threshold = XQC_kPacketThreshold; + send_ctl->ctl_reordering_packet_threshold = conn->conn_settings.loss_detection_pkt_thresh; send_ctl->ctl_reordering_time_threshold_shift = XQC_kTimeThresholdShift; for (size_t i = 0; i < XQC_PNS_N; i++) { - xqc_sent_record_init(&send_ctl->ctl_sent_record[i]); send_ctl->ctl_largest_acked[i] = XQC_MAX_UINT64_VALUE; send_ctl->ctl_largest_received[i] = XQC_MAX_UINT64_VALUE; - send_ctl->ctl_unack_received[i] = XQC_MAX_UINT64_VALUE; send_ctl->ctl_time_of_last_sent_ack_eliciting_packet[i] = 0; send_ctl->ctl_loss_time[i] = 0; } @@ -133,7 +193,6 @@ xqc_send_ctl_destroy(xqc_send_ctl_t *send_ctl) /* 从上到下4个pns的遍历,全都不一样 */ for (xqc_pkt_num_space_t pns = 0; pns < XQC_PNS_N; ++pns) { - xqc_sent_record_release(&send_ctl->ctl_sent_record[pns]); send_ctl->ctl_bytes_ack_eliciting_inflight[pns] = 0; } @@ -147,7 +206,6 @@ xqc_send_ctl_reset(xqc_send_ctl_t *send_ctl) xqc_path_ctx_t *path = send_ctl->ctl_path; send_ctl->ctl_pto_count = 0; - send_ctl->ctl_pto_count_since_last_tra_path_status_changed = 0; send_ctl->ctl_minrtt = XQC_MAX_UINT32_VALUE; send_ctl->ctl_srtt = XQC_kInitialRtt * 1000; send_ctl->ctl_rttvar = XQC_kInitialRtt * 1000 / 2; @@ -156,10 +214,8 @@ xqc_send_ctl_reset(xqc_send_ctl_t *send_ctl) send_ctl->ctl_reordering_time_threshold_shift = XQC_kTimeThresholdShift; for (size_t i = 0; i < XQC_PNS_N; i++) { - xqc_sent_record_init(&send_ctl->ctl_sent_record[i]); send_ctl->ctl_largest_acked[i] = XQC_MAX_UINT64_VALUE; send_ctl->ctl_largest_received[i] = XQC_MAX_UINT64_VALUE; - send_ctl->ctl_unack_received[i] = XQC_MAX_UINT64_VALUE; send_ctl->ctl_time_of_last_sent_ack_eliciting_packet[i] = 0; send_ctl->ctl_loss_time[i] = 0; } @@ -267,10 +323,6 @@ xqc_pn_ctl_destroy(xqc_pn_ctl_t *pn_ctl) xqc_pn_ctl_t * xqc_get_pn_ctl(xqc_connection_t *conn, xqc_path_ctx_t *path) { - if (conn->enable_multipath == XQC_CONN_MULTIPATH_SINGLE_PNS) { - return conn->conn_initial_path->path_pn_ctl; - } - return path->path_pn_ctl; } @@ -394,7 +446,7 @@ xqc_send_packet_cwnd_allows(xqc_send_ctl_t *send_ctl, /* packet with high priority first */ if (!xqc_send_ctl_can_send(send_ctl, packet_out, schedule_bytes)) { xqc_log(conn->log, XQC_LOG_DEBUG, - "|blocked by congestion control|"); + "|blocked by congestion control|po_sz:%ud|", packet_out->po_used_size); return XQC_FALSE; } } @@ -484,7 +536,7 @@ xqc_send_ctl_increase_inflight(xqc_connection_t *conn, xqc_packet_out_t *packet_ { xqc_path_ctx_t *path = xqc_conn_find_path_by_path_id(conn, packet_out->po_path_id); if (path == NULL) { - xqc_log(conn->log, XQC_LOG_WARN, "|can't find path by id|%L|", packet_out->po_path_id); + xqc_log(conn->log, XQC_LOG_WARN, "|can't find path by id|%ui|", packet_out->po_path_id); return; } @@ -503,7 +555,7 @@ xqc_send_ctl_decrease_inflight(xqc_connection_t *conn, xqc_packet_out_t *packet_ { xqc_path_ctx_t *path = xqc_conn_find_path_by_path_id(conn, packet_out->po_path_id); if (path == NULL) { - xqc_log(conn->log, XQC_LOG_WARN, "|can't find path by id|%L|", packet_out->po_path_id); + xqc_log(conn->log, XQC_LOG_WARN, "|can't find path by id|%ui|", packet_out->po_path_id); return; } @@ -530,7 +582,6 @@ xqc_send_ctl_on_pns_discard(xqc_send_ctl_t *send_ctl, xqc_pkt_num_space_t pns) send_ctl->ctl_time_of_last_sent_ack_eliciting_packet[pns] = 0; send_ctl->ctl_loss_time[pns] = 0; send_ctl->ctl_pto_count = 0; - send_ctl->ctl_pto_count_since_last_tra_path_status_changed = 0; xqc_log(send_ctl->ctl_conn->log, XQC_LOG_INFO, "|xqc_send_ctl_set_loss_detection_timer on discard pns:%ud", pns); xqc_send_ctl_set_loss_detection_timer(send_ctl); } @@ -563,14 +614,6 @@ xqc_send_ctl_on_packet_sent(xqc_send_ctl_t *send_ctl, xqc_pn_ctl_t *pn_ctl, xqc_ { xqc_pkt_num_space_t pns = packet_out->po_pkt.pkt_pns; - if (send_ctl->ctl_conn->enable_multipath == XQC_CONN_MULTIPATH_SINGLE_PNS) { - int ret = xqc_sent_record_add(&send_ctl->ctl_sent_record[pns], packet_out->po_pkt.pkt_num, packet_out->po_sent_time); - if (ret != XQC_OK) { - xqc_log(send_ctl->ctl_conn->log, XQC_LOG_ERROR, "|xqc_sent_record_add error|path:%ui|pkt_num:%ui|", - send_ctl->ctl_path->path_id, packet_out->po_pkt.pkt_num); - } - } - xqc_sample_on_sent(packet_out, send_ctl, now); xqc_packet_number_t orig_pktnum = packet_out->po_origin ? packet_out->po_origin->po_pkt.pkt_num : 0; @@ -670,10 +713,38 @@ xqc_send_ctl_on_packet_sent(xqc_send_ctl_t *send_ctl, xqc_pn_ctl_t *pn_ctl, xqc_ ++send_ctl->ctl_lost_count; packet_out->po_flag &= ~XQC_POF_LOST; - } else if (packet_out->po_flag & XQC_POF_TLP) { + } + + if (packet_out->po_flag & XQC_POF_TLP) { ++send_ctl->ctl_tlp_count; packet_out->po_flag &= ~XQC_POF_TLP; } + + /* record dgram stats */ + if (packet_out->po_frame_types & XQC_FRAME_BIT_DATAGRAM) { + send_ctl->ctl_conn->dgram_stats.total_dgram++; + send_ctl->ctl_dgram_send_count++; + + if (packet_out->po_flag & XQC_POF_REINJECTED_REPLICA) { + send_ctl->ctl_reinj_dgram_send_count++; + } + + if (packet_out->po_flag & XQC_POF_QOS_HIGH) { + send_ctl->ctl_conn->dgram_stats.hp_dgram++; + if (packet_out->po_flag & XQC_POF_REINJECTED_REPLICA) { + send_ctl->ctl_conn->dgram_stats.hp_red_dgram++; + if (packet_out->po_origin + && packet_out->po_path_id != packet_out->po_origin->po_path_id) + { + send_ctl->ctl_conn->dgram_stats.hp_red_dgram_mp++; + } + } + } + if (packet_out->po_flag & XQC_POF_QOS_PROBING) { + send_ctl->ctl_conn->dgram_stats.timer_red_dgram++; + } + } + ++send_ctl->ctl_send_count; xqc_stream_path_metrics_on_send(send_ctl->ctl_conn, packet_out); @@ -691,81 +762,11 @@ xqc_send_ctl_on_packet_sent(xqc_send_ctl_t *send_ctl, xqc_pn_ctl_t *pn_ctl, xqc_ } -void -xqc_send_ctl_maybe_update_rtt_spns(xqc_connection_t *conn, xqc_ack_info_t *const ack_info, xqc_usec_t ack_recv_time) -{ - xqc_path_ctx_t *path = xqc_conn_find_path_by_path_id(conn, ack_info->path_id); - if (path == NULL) { - xqc_log(conn->log, XQC_LOG_ERROR, "|xqc_conn_find_path_by_path_id error|"); - return; - } - - xqc_send_ctl_t *send_ctl = path->path_send_ctl; - xqc_pkt_num_space_t pns = ack_info->pns; - xqc_sent_record_t *sent_record = &send_ctl->ctl_sent_record[pns]; - xqc_packet_number_node_t *pn_node = NULL; - - /* TODO: 没有考虑 ack_eliciting, 是bug,会出现 largest_ack 是 non-ack-eliciting,收发两端不匹配 */ - int ret = xqc_sent_record_get_largest_pn_in_ack(sent_record, ack_info, &pn_node); - if (ret != XQC_OK || pn_node == NULL) { - xqc_log(conn->log, XQC_LOG_DEBUG, "|xqc_sent_record_get_largest_pn_in_ack no result|"); - return; - } - - xqc_log(conn->log, XQC_LOG_DEBUG, - "|conn:%p|path:%ui|largest_pn_in_ack:%ui|path_largest_ack:%ui|path_latest_rtt_ack:%ui|", - conn, ack_info->path_id, pn_node->pkt_num, send_ctl->ctl_largest_acked[pns], sent_record->latest_rtt_pn); - - if (pn_node->pkt_num > sent_record->latest_rtt_pn || - sent_record->latest_rtt_pn == XQC_MAX_UINT64_VALUE) - { - /* 更新 ctl_latest_rtt */ - send_ctl->ctl_latest_rtt = ack_recv_time - pn_node->pkt_sent_time; - /* 更新rtt */ - xqc_send_ctl_update_rtt(send_ctl, &send_ctl->ctl_latest_rtt, ack_info->ack_delay); - ++send_ctl->ctl_update_latest_rtt_count; - /* 更新 latest_rtt_pn */ - sent_record->latest_rtt_pn = pn_node->pkt_num; - } -} - - -/* on ack received */ -int -xqc_send_ctl_on_ack_received_spns(xqc_connection_t *conn, xqc_ack_info_t *const ack_info, xqc_usec_t ack_recv_time) -{ - xqc_send_queue_t *send_queue = conn->conn_send_queue; - xqc_path_ctx_t *path = NULL; - xqc_list_head_t *pos, *next; - xqc_packet_out_t *first_unacked_po = NULL; - xqc_pkt_num_space_t pns = ack_info->pns; - - xqc_list_for_each_safe(pos, next, &conn->conn_paths_list) { - path = xqc_list_entry(pos, xqc_path_ctx_t, path_list); - - int ret = xqc_send_ctl_on_ack_received(path->path_send_ctl, xqc_get_pn_ctl(conn, path), - send_queue, ack_info, ack_recv_time); - if (ret != XQC_OK) { - xqc_log(conn->log, XQC_LOG_ERROR, "|on_ack_received error|path_id:%ui|", path->path_id); - return ret; - } - } - - xqc_send_ctl_maybe_update_rtt_spns(conn, ack_info, ack_recv_time); - - /* 移除不再需要记录的pn */ - xqc_list_for_each_safe(pos, next, &conn->conn_paths_list) { - path = xqc_list_entry(pos, xqc_path_ctx_t, path_list); - xqc_sent_record_del(&path->path_send_ctl->ctl_sent_record[pns]); - } - return XQC_OK; -} - /** * OnAckReceived */ int -xqc_send_ctl_on_ack_received(xqc_send_ctl_t *send_ctl, xqc_pn_ctl_t *pn_ctl, xqc_send_queue_t *send_queue, xqc_ack_info_t *const ack_info, xqc_usec_t ack_recv_time) +xqc_send_ctl_on_ack_received(xqc_send_ctl_t *send_ctl, xqc_pn_ctl_t *pn_ctl, xqc_send_queue_t *send_queue, xqc_ack_info_t *const ack_info, xqc_usec_t ack_recv_time, xqc_bool_t ack_on_same_path) { /* ack info里包含的packet不一定会是send_ctl这条路径发出的 */ /* info里的largest ack 不一定是send_ctl这条路径的largest ack */ @@ -785,7 +786,6 @@ xqc_send_ctl_on_ack_received(xqc_send_ctl_t *send_ctl, xqc_pn_ctl_t *pn_ctl, xqc xqc_usec_t spurious_loss_sent_time = 0; unsigned char need_del_record = 0; int stream_frame_acked = 0; - unsigned char same_path_rtt = ack_info->path_id == send_ctl->ctl_path->path_id? 1 : 0; xqc_packet_number_t largest_acked_ack = xqc_ack_sent_record_on_ack(&pn_ctl->ack_sent_record[pns], ack_info); if (largest_acked_ack > pn_ctl->ctl_largest_acked_ack[pns]) { @@ -876,6 +876,10 @@ xqc_send_ctl_on_ack_received(xqc_send_ctl_t *send_ctl, xqc_pn_ctl_t *pn_ctl, xqc } } + if (packet_out->po_frame_types & XQC_FRAME_BIT_DATAGRAM) { + xqc_datagram_notify_ack(conn, packet_out); + } + xqc_send_ctl_on_packet_acked(send_ctl, packet_out, ack_recv_time, 1); xqc_send_queue_maybe_remove_unacked(packet_out, send_queue, NULL); @@ -894,13 +898,11 @@ xqc_send_ctl_on_ack_received(xqc_send_ctl_t *send_ctl, xqc_pn_ctl_t *pn_ctl, xqc return XQC_OK; } - if (update_largest_ack && has_ack_eliciting && same_path_rtt == 1) { + if (update_largest_ack && has_ack_eliciting && ack_on_same_path) { /* 更新 ctl_latest_rtt */ send_ctl->ctl_latest_rtt = ack_recv_time - send_ctl->ctl_largest_acked_sent_time[pns]; /* 更新rtt */ xqc_send_ctl_update_rtt(send_ctl, &send_ctl->ctl_latest_rtt, ack_info->ack_delay); - /* 更新 latest_rtt_pn */ - send_ctl->ctl_sent_record[pns].latest_rtt_pn = send_ctl->ctl_largest_acked[pns]; } /* TODO: ECN */ @@ -929,7 +931,6 @@ xqc_send_ctl_on_ack_received(xqc_send_ctl_t *send_ctl, xqc_pn_ctl_t *pn_ctl, xqc */ if (xqc_conn_peer_complete_address_validation(conn)) { send_ctl->ctl_pto_count = 0; - send_ctl->ctl_pto_count_since_last_tra_path_status_changed = 0; } xqc_log(conn->log, XQC_LOG_DEBUG, "|xqc_send_ctl_set_loss_detection_timer|acked|pto_count:%ud|", send_ctl->ctl_pto_count); @@ -1071,6 +1072,8 @@ xqc_send_ctl_latest_rtt_tracking(xqc_send_ctl_t *send_ctl, xqc_usec_t *latest_rt void xqc_send_ctl_update_rtt(xqc_send_ctl_t *send_ctl, xqc_usec_t *latest_rtt, xqc_usec_t ack_delay) { + xqc_connection_t *conn = send_ctl->ctl_conn; + xqc_log(send_ctl->ctl_conn->log, XQC_LOG_DEBUG, "|before update rtt|conn:%p|srtt:%ui|rttvar:%ui|minrtt:%ui|latest_rtt:%ui|ack_delay:%ui|", send_ctl->ctl_conn, send_ctl->ctl_srtt, send_ctl->ctl_rttvar, send_ctl->ctl_minrtt, *latest_rtt, ack_delay); @@ -1093,7 +1096,9 @@ xqc_send_ctl_update_rtt(xqc_send_ctl_t *send_ctl, xqc_usec_t *latest_rtt, xqc_us /* Adjust for ack delay if it's plausible. */ xqc_usec_t adjusted_rtt = *latest_rtt; - if (*latest_rtt >= (send_ctl->ctl_minrtt + ack_delay)) { + if (adjusted_rtt > ack_delay + && (adjusted_rtt + 1000) >= (send_ctl->ctl_minrtt + ack_delay)) + { adjusted_rtt -= ack_delay; } @@ -1185,7 +1190,7 @@ xqc_send_ctl_detect_lost(xqc_send_ctl_t *send_ctl, xqc_send_queue_t *send_queue, loss_delay += loss_delay >> send_ctl->ctl_reordering_time_threshold_shift; /* Minimum time of kGranularity before packets are deemed lost. */ - loss_delay = xqc_max(loss_delay, XQC_kGranularity); + loss_delay = xqc_max(loss_delay, XQC_kGranularity * 1000); /* Packets sent before this time are deemed lost. */ xqc_usec_t lost_send_time = now - loss_delay; @@ -1194,9 +1199,18 @@ xqc_send_ctl_detect_lost(xqc_send_ctl_t *send_ctl, xqc_send_queue_t *send_queue, /* 若 lost_pn == XQC_MAX_UINT64_VALUE, 无丢包 */ xqc_packet_number_t lost_pn = xqc_send_ctl_get_lost_sent_pn(send_ctl, pns); + xqc_int_t repair_dgram = 0; + xqc_reinjection_mode_t mode = conn->conn_settings.mp_enable_reinjection & XQC_REINJ_UNACK_BEFORE_SCHED; + xqc_list_for_each_safe(pos, next, &send_queue->sndq_unacked_packets[pns]) { po = xqc_list_entry(pos, xqc_packet_out_t, po_list); + repair_dgram = 0; + + if (xqc_send_ctl_indirectly_ack_or_drop_po(conn, po)) { + continue; + } + if (po->po_path_id != send_ctl->ctl_path->path_id) { continue; } @@ -1206,24 +1220,52 @@ xqc_send_ctl_detect_lost(xqc_send_ctl_t *send_ctl, xqc_send_queue_t *send_queue, break; } - if (xqc_send_ctl_indirectly_ack_po(conn, po)) { - continue; - } - /* Mark packet as lost, or set time when it should be marked. */ if (po->po_sent_time <= lost_send_time || (lost_pn != XQC_MAX_UINT64_VALUE && po->po_pkt.pkt_num <= lost_pn)) { if (po->po_flag & XQC_POF_IN_FLIGHT) { + + /* reinjection */ + if (conn->enable_multipath + && conn->reinj_callback + && conn->reinj_callback->xqc_reinj_ctl_can_reinject + && conn->reinj_callback->xqc_reinj_ctl_can_reinject(conn->reinj_ctl, po, mode)) + { + if (xqc_conn_try_reinject_packet(conn, po) == XQC_OK) { + xqc_log(conn->log, XQC_LOG_DEBUG, "|MP|REINJ|reinject lost packets|" + "pkt_num:%ui|size:%ud|pkt_type:%s|frame:%s|", + po->po_pkt.pkt_num, po->po_used_size, + xqc_pkt_type_2_str(po->po_pkt.pkt_type), + xqc_frame_type_2_str(po->po_frame_types)); + } + } + xqc_send_ctl_decrease_inflight(conn, po); - /* if a packet don't need to be repair, don't retransmit it */ - if (!XQC_NEED_REPAIR(po->po_frame_types)) { - xqc_send_queue_remove_unacked(po, send_queue); - xqc_send_queue_insert_free(po, &send_queue->sndq_free_packets, send_queue); + if (po->po_frame_types & XQC_FRAME_BIT_DATAGRAM) { + send_ctl->ctl_lost_dgram_cnt++; + repair_dgram = xqc_datagram_notify_loss(conn, po); + if (conn->conn_settings.datagram_force_retrans_on) { + repair_dgram = XQC_DGRAM_RETX_ASKED_BY_APP; + } + } - } else { + if (XQC_NEED_REPAIR(po->po_frame_types) + || repair_dgram == XQC_DGRAM_RETX_ASKED_BY_APP) + { xqc_send_queue_copy_to_lost(po, send_queue); + + } else { + if (po->po_frame_types & XQC_FRAME_BIT_DATAGRAM) { + xqc_send_ctl_on_dgram_dropped(conn, po); + xqc_send_queue_maybe_remove_unacked(po, conn->conn_send_queue, NULL); + + } else { + /* remove the packet that does not need retransmission */ + xqc_send_queue_remove_unacked(po, send_queue); + xqc_send_queue_insert_free(po, &send_queue->sndq_free_packets, send_queue); + } } lost_n++; @@ -1315,7 +1357,7 @@ xqc_bool_t xqc_send_ctl_in_persistent_congestion(xqc_send_ctl_t *send_ctl, xqc_packet_out_t *largest_lost, xqc_usec_t now) { if (send_ctl->ctl_pto_count >= XQC_CONSECUTIVE_PTO_THRESH) { - xqc_usec_t duration = (send_ctl->ctl_srtt + xqc_max(send_ctl->ctl_rttvar << 2, XQC_kGranularity) + xqc_usec_t duration = (send_ctl->ctl_srtt + xqc_max(send_ctl->ctl_rttvar << 2, XQC_kGranularity * 1000) + send_ctl->ctl_conn->remote_settings.max_ack_delay * 1000) * XQC_kPersistentCongestionThreshold; if (now - largest_lost->po_sent_time > duration) { return XQC_TRUE; @@ -1402,6 +1444,27 @@ xqc_send_ctl_cc_on_ack(xqc_send_ctl_t *send_ctl, xqc_packet_out_t *acked_packet, #endif } +void +xqc_send_ctl_on_pmtud_ping_acked(xqc_send_ctl_t *send_ctl, + xqc_packet_out_t *po) +{ + xqc_connection_t *conn = send_ctl->ctl_conn; + xqc_path_ctx_t *path = xqc_conn_find_path_by_path_id(conn, po->po_path_id); + + if (path == NULL || path->path_state >= XQC_PATH_STATE_CLOSING) { + xqc_log(conn->log, XQC_LOG_WARN, + "|PMTUD probe acked on invalid path|path:%ui|", + po->po_path_id); + } + + if (po->po_buf_size > path->curr_pkt_out_size) { + /* update path MTU */ + path->curr_pkt_out_size = po->po_buf_size; + path->path_max_pkt_out_size = po->po_max_pkt_out_size; + xqc_conn_try_to_update_mss(conn); + } +} + /** * OnPacketAcked */ @@ -1412,6 +1475,7 @@ xqc_send_ctl_on_packet_acked(xqc_send_ctl_t *send_ctl, xqc_stream_t *stream; xqc_packet_out_t *packet_out = acked_packet; xqc_connection_t *conn = send_ctl->ctl_conn; + xqc_bool_t notify_ping; if ((conn->conn_type == XQC_CONN_TYPE_SERVER) && (acked_packet->po_frame_types & XQC_FRAME_BIT_HANDSHAKE_DONE)) { conn->conn_flag |= XQC_CONN_FLAG_HANDSHAKE_DONE_ACKED; @@ -1435,19 +1499,30 @@ xqc_send_ctl_on_packet_acked(xqc_send_ctl_t *send_ctl, if (conn->app_proto_cbs.conn_cbs.conn_ping_acked && (packet_out->po_flag & XQC_POF_NOTIFY)) { - conn->app_proto_cbs.conn_cbs.conn_ping_acked(conn, &conn->scid_set.user_scid, + notify_ping = XQC_TRUE; + if (packet_out->po_pr && packet_out->po_pr->notified) { + notify_ping = XQC_FALSE; + } + + if (notify_ping) { + conn->app_proto_cbs.conn_cbs.conn_ping_acked(conn, &conn->scid_set.user_scid, packet_out->po_user_data, conn->user_data, conn->proto_data); + if (packet_out->po_pr) { + packet_out->po_pr->notified = XQC_TRUE; + } + } + + } + /* PTMUD probing success */ + if (packet_out->po_flag & XQC_POF_PMTUD_PROBING) { + xqc_send_ctl_on_pmtud_ping_acked(send_ctl, packet_out); } } /* TODO: fix NEW_CID_RECEIVED */ if (packet_out->po_frame_types & XQC_FRAME_BIT_NEW_CONNECTION_ID) { packet_out->po_frame_types &= ~XQC_FRAME_BIT_NEW_CONNECTION_ID; - conn->conn_flag |= XQC_CONN_FLAG_NEW_CID_RECEIVED; - } - - if (packet_out->po_frame_types & XQC_FRAME_BIT_PATH_ABANDON) { - xqc_path_abandon_acked(conn, packet_out->po_abandon_path_id); + conn->conn_flag |= XQC_CONN_FLAG_NEW_CID_ACKED; } if (do_cc) { @@ -1469,10 +1544,19 @@ xqc_send_ctl_get_pto_time_and_space(xqc_send_ctl_t *send_ctl, xqc_usec_t now, xq xqc_usec_t pto_timeout = XQC_MAX_UINT64_VALUE; xqc_connection_t *c = send_ctl->ctl_conn; xqc_int_t pto_cnt = send_ctl->ctl_pto_count; + double backoff = xqc_send_ctl_pow_x(c->conn_settings.pto_backoff_factor, send_ctl->ctl_pto_count); + + /* set a cap to avoid PTO timeout overflow */ + backoff = xqc_min(backoff, 1 << 16); /* get pto duration */ xqc_usec_t duration = (send_ctl->ctl_srtt - + xqc_max(4 * send_ctl->ctl_rttvar, XQC_kGranularity * 1000)) * xqc_send_ctl_pow(pto_cnt); + + xqc_max(4 * send_ctl->ctl_rttvar, XQC_kGranularity * 1000)) * backoff; + + xqc_log(c->log, XQC_LOG_DEBUG, + "|srtt:%ud|rtt_var:%ud|pto_duration:%ud|backoff_factor:%.2f|backoff:%.2f|pto_cnt:%d|max_ack_delay:%d|", + send_ctl->ctl_srtt, send_ctl->ctl_rttvar, duration, + c->conn_settings.pto_backoff_factor, backoff, pto_cnt, c->remote_settings.max_ack_delay); /* Arm PTO from now when there are no inflight packets */ if (send_ctl->ctl_bytes_in_flight == 0) { @@ -1506,7 +1590,7 @@ xqc_send_ctl_get_pto_time_and_space(xqc_send_ctl_t *send_ctl, xqc_usec_t now, xq break; } - duration += c->remote_settings.max_ack_delay * 1000 * xqc_send_ctl_pow(send_ctl->ctl_pto_count); + duration += c->remote_settings.max_ack_delay * 1000 * backoff; } t = send_ctl->ctl_time_of_last_sent_ack_eliciting_packet[pns] + duration; @@ -1682,237 +1766,20 @@ xqc_send_ctl_get_lost_sent_pn(xqc_send_ctl_t *send_ctl, xqc_pkt_num_space_t pns) xqc_packet_number_t threshold = send_ctl->ctl_reordering_packet_threshold; xqc_packet_number_t lost_pn = XQC_MAX_UINT64_VALUE; /* pkt num从0开始 */ - if (send_ctl->ctl_conn->enable_multipath == XQC_CONN_MULTIPATH_SINGLE_PNS) { - /* Single pns */ - int ret = xqc_sent_record_lost_sent_pn(&send_ctl->ctl_sent_record[pns], largest_acked, threshold, &lost_pn); - if (ret != XQC_OK) { - xqc_log(send_ctl->ctl_conn->log, XQC_LOG_ERROR, "|xqc_sent_record_lost_sent_pn error|path:%ui|largest_acked:%ui|threshold:%ui|", - send_ctl->ctl_path->path_id, largest_acked, threshold); - } - } - else { - /* Multiple pns & Single path */ - if (largest_acked >= threshold) { - lost_pn = largest_acked - threshold; - } + /* Multiple pns & Single path */ + if (largest_acked >= threshold) { + lost_pn = largest_acked - threshold; } + xqc_log(send_ctl->ctl_conn->log, XQC_LOG_DEBUG, + "|largest_acked:%ui|lost_pn:%ui|thresh:%ui|", + largest_acked, lost_pn, threshold); + return lost_pn; } xqc_packet_number_t xqc_send_ctl_get_pkt_num_gap(xqc_send_ctl_t *send_ctl, xqc_pkt_num_space_t pns, xqc_packet_number_t front, xqc_packet_number_t back) { - xqc_packet_number_t gap = 0; - - if (send_ctl->ctl_conn->enable_multipath == XQC_CONN_MULTIPATH_SINGLE_PNS) { - /* Single pns */ - int ret = xqc_sent_record_pn_gap(&send_ctl->ctl_sent_record[pns], front, back, &gap); - if (ret != XQC_OK) { - gap = 0; - xqc_log(send_ctl->ctl_conn->log, XQC_LOG_ERROR, "|xqc_sent_record_pn_gap error|path:%ui|front:%ui|back:%ui|", - send_ctl->ctl_path->path_id, front, back); - } - } - else { - /* Multiple pns & Single path */ - gap = back - front; - } - - return gap; -} - -void -xqc_sent_record_init(xqc_sent_record_t *sent_record) -{ - xqc_memzero(sent_record, sizeof(xqc_sent_record_t)); - xqc_init_list_head(&sent_record->sent_pn_list); - sent_record->latest_rtt_pn = XQC_MAX_UINT64_VALUE; -} - -void -xqc_sent_record_release(xqc_sent_record_t *sent_record) -{ - xqc_list_head_t *pos, *next; - xqc_packet_number_node_t *pnode; - xqc_list_for_each_safe(pos, next, &sent_record->sent_pn_list) { - pnode = xqc_list_entry(pos, xqc_packet_number_node_t, pn_list); - xqc_list_del_init(pos); - xqc_free(pnode); - } -} - -xqc_int_t -xqc_sent_record_add(xqc_sent_record_t *sent_record, xqc_packet_number_t pkt_num, xqc_usec_t sent_time) -{ - xqc_packet_number_node_t *new_pn_node = NULL; - xqc_packet_number_node_t *largest_pn_node = NULL; - - /* 检查pn单调递增 */ - if (!xqc_list_empty(&sent_record->sent_pn_list)) { - largest_pn_node = xqc_list_entry(sent_record->sent_pn_list.prev, xqc_packet_number_node_t, pn_list); - if (largest_pn_node->pkt_num >= pkt_num) { - return XQC_ERROR; - } - } - - new_pn_node = xqc_calloc(1, sizeof(xqc_packet_number_node_t)); - if (new_pn_node == NULL) { - return XQC_ERROR; - } - - new_pn_node->pkt_num = pkt_num; - new_pn_node->pkt_sent_time = sent_time; - - xqc_list_add_tail(&new_pn_node->pn_list, &sent_record->sent_pn_list); - return XQC_OK; -} - -void -xqc_sent_record_del(xqc_sent_record_t *sent_record) -{ - if (sent_record->latest_rtt_pn == XQC_MAX_UINT64_VALUE) { - return; - } - - xqc_list_head_t *head = &sent_record->sent_pn_list; - xqc_list_head_t *pos, *next; - xqc_packet_number_node_t *pn_node = NULL; - - xqc_list_for_each_safe(pos, next, head) { - pn_node = xqc_list_entry(pos, xqc_packet_number_node_t, pn_list); - - if (pn_node->pkt_num < sent_record->latest_rtt_pn) { - xqc_list_del_init(pos); - xqc_free(pn_node); - } - else { - break; - } - } -} - -xqc_int_t -xqc_sent_record_lost_sent_pn(xqc_sent_record_t *sent_record, xqc_packet_number_t largest_acked, xqc_packet_number_t threshold, xqc_packet_number_t *lost_pn) -{ - xqc_list_head_t *head = &sent_record->sent_pn_list; - xqc_list_head_t *pos, *next; - xqc_list_head_t *pos_thrs; - xqc_packet_number_node_t *pn_node = NULL; - xqc_packet_number_node_t *lost_node = NULL; - *lost_pn = XQC_MAX_UINT64_VALUE; - - xqc_list_for_each_safe(pos_thrs, next, head) { - if (--threshold == 0) { - break; - } - } - - /* largest_acked 之前的 pn数量小于 threshold */ - pn_node = xqc_list_entry(pos_thrs, xqc_packet_number_node_t, pn_list); - if (threshold > 0 || pn_node->pkt_num >= largest_acked) { - return XQC_OK; - } - - pos = head->next; - pos_thrs = pos_thrs->next; - xqc_list_for_each_from(pos_thrs, head) { - pn_node = xqc_list_entry(pos_thrs, xqc_packet_number_node_t, pn_list); - if (pn_node->pkt_num >= largest_acked) { - break; - } - pos = pos->next; - } - - /* largest_acked不存在sent record里*/ - if (pn_node->pkt_num != largest_acked) { - return XQC_ERROR; - } - - lost_node = xqc_list_entry(pos, xqc_packet_number_node_t, pn_list); - *lost_pn = lost_node->pkt_num; - return XQC_OK; -} - -/** Example - * sent record: {1, 3, 5, 7, 9} - * front = 3, back = 7 - * gap = 2 - */ - -xqc_int_t -xqc_sent_record_pn_gap(xqc_sent_record_t *sent_record, xqc_packet_number_t front, xqc_packet_number_t back, xqc_packet_number_t *gap) -{ - xqc_list_head_t *head = &sent_record->sent_pn_list; - xqc_list_head_t *pos, *next; - xqc_packet_number_node_t *pn_node = NULL; - *gap = 0; - - xqc_list_for_each_safe(pos, next, head) { - pn_node = xqc_list_entry(pos, xqc_packet_number_node_t, pn_list); - if (pn_node->pkt_num >= front) { - break; - } - } - - /* front 不存在 sent record 里*/ - if (pn_node->pkt_num != front) { - return XQC_ERROR; - } - - xqc_list_for_each_from(pos, head) { - pn_node = xqc_list_entry(pos, xqc_packet_number_node_t, pn_list); - if (pn_node->pkt_num >= back) { - break; - } - *gap += 1; - } - - /* back 不存在 sent record 里*/ - if (pn_node->pkt_num != back) { - return XQC_ERROR; - } - - return XQC_OK; -} - -xqc_int_t -xqc_sent_record_get_largest_pn_in_ack(xqc_sent_record_t *sent_record, xqc_ack_info_t *const ack_info, xqc_packet_number_node_t **largest_pn_node) -{ - xqc_list_head_t *head = &sent_record->sent_pn_list; - xqc_list_head_t *pos, *next; - xqc_packet_number_node_t *pn_node = NULL; - *largest_pn_node = NULL; - xqc_packet_number_t largest_ack = ack_info->ranges[0].high; - xqc_pktno_range_t *range = &ack_info->ranges[ack_info->n_ranges - 1]; - - xqc_list_for_each_safe(pos, next, head) { - pn_node = xqc_list_entry(pos, xqc_packet_number_node_t, pn_list); - - if (pn_node->pkt_num > largest_ack) { - break; - } - - while (pn_node->pkt_num > range->high && range != ack_info->ranges) { - --range; - } - - if (pn_node->pkt_num >= range->low) { - *largest_pn_node = pn_node; - } - } - - return XQC_OK; -} - -/* -void -xqc_sent_record_log(xqc_send_ctl_t *send_ctl, xqc_packet_out_t *packet_out) -{ - xqc_pkt_num_space_t pns = packet_out->po_pkt.pkt_pns; - xqc_packet_number_node_t *first_node = xqc_list_entry(send_ctl->ctl_sent_record[pns].sent_pn_list.next, xqc_packet_number_node_t, pn_list); - xqc_packet_number_node_t *last_node = xqc_list_entry(send_ctl->ctl_sent_record[pns].sent_pn_list.prev, xqc_packet_number_node_t, pn_list); - xqc_log(send_ctl->ctl_conn->log, XQC_LOG_DEBUG, "|path:%ui|pkt_num:%ui|record_smallest_pn:%ui|record_largest_pn:%ui|", - send_ctl->ctl_path->path_id, packet_out->po_pkt.pkt_num, first_node->pkt_num, last_node->pkt_num); + return back - front; } -*/ \ No newline at end of file diff --git a/src/transport/xqc_send_ctl.h b/src/transport/xqc_send_ctl.h index 1475d0344..13a895c24 100644 --- a/src/transport/xqc_send_ctl.h +++ b/src/transport/xqc_send_ctl.h @@ -12,6 +12,7 @@ #include "src/transport/xqc_send_queue.h" #include "src/transport/xqc_timer.h" #include "src/transport/xqc_multipath.h" +#include #define XQC_kPacketThreshold 3 #define XQC_kTimeThresholdShift 3 @@ -28,6 +29,7 @@ /* 2^n */ #define xqc_send_ctl_pow(n) (1 << n) +#define xqc_send_ctl_pow_x(x, n) (fabs(x - 2) < 1e-7) ? xqc_send_ctl_pow(n) : pow(x, n) #define XQC_DEFAULT_RECORD_INTERVAL (100000) /* 100ms record interval */ @@ -65,29 +67,10 @@ typedef struct xqc_pn_ctl_s { } xqc_pn_ctl_t; - -/* node of path packet number list (single pns only) */ -typedef struct xqc_packet_number_node_s { - xqc_packet_number_t pkt_num; - xqc_usec_t pkt_sent_time; - xqc_list_head_t pn_list; -} xqc_packet_number_node_t; - - -typedef struct xqc_sent_record_s { - xqc_list_head_t sent_pn_list; /* xqc_packet_number_node_t */ - - /* the latest rtt was updated when this packet was acked in a same-path round trip */ - xqc_packet_number_t latest_rtt_pn; -} xqc_sent_record_t; - - typedef struct xqc_send_ctl_s { xqc_connection_t *ctl_conn; xqc_path_ctx_t *ctl_path; - xqc_sent_record_t ctl_sent_record[XQC_PNS_N]; - /* largest packet number of the acked packets in packet_out */ xqc_packet_number_t ctl_largest_acked[XQC_PNS_N]; @@ -102,7 +85,6 @@ typedef struct xqc_send_ctl_s { /* Ack-eliciting Packets received since last ack sent */ uint32_t ctl_ack_eliciting_pkt[XQC_PNS_N]; - xqc_packet_number_t ctl_unack_received[XQC_PNS_N]; xqc_usec_t ctl_loss_time[XQC_PNS_N]; @@ -123,16 +105,21 @@ typedef struct xqc_send_ctl_s { xqc_timer_manager_t path_timer_manager; unsigned ctl_pto_count; - unsigned ctl_pto_count_since_last_tra_path_status_changed; unsigned ctl_send_count; unsigned ctl_lost_count; unsigned ctl_tlp_count; unsigned ctl_spurious_loss_count; - unsigned ctl_send_count_at_last_tra_path_status_changed_time; + unsigned ctl_lost_dgram_cnt; unsigned ctl_recv_count; + /* for QUIC datagrams */ + uint32_t ctl_dgram_send_count; + uint32_t ctl_dgram_recv_count; + uint32_t ctl_reinj_dgram_send_count; + uint32_t ctl_reinj_dgram_recv_count; + uint32_t ctl_max_bytes_in_flight; uint8_t ctl_is_cwnd_limited; @@ -173,7 +160,12 @@ xqc_send_ctl_calc_pto(xqc_send_ctl_t *send_ctl) + send_ctl->ctl_conn->local_settings.max_ack_delay * 1000; } -int xqc_send_ctl_indirectly_ack_po(xqc_connection_t *conn, xqc_packet_out_t *po); + +void xqc_send_ctl_on_dgram_dropped(xqc_connection_t *conn, xqc_packet_out_t *po); + +int xqc_send_ctl_may_remove_unacked_dgram(xqc_connection_t *conn, xqc_packet_out_t *po); + +int xqc_send_ctl_indirectly_ack_or_drop_po(xqc_connection_t *conn, xqc_packet_out_t *po); xqc_send_ctl_t *xqc_send_ctl_create(xqc_path_ctx_t *path); @@ -205,9 +197,7 @@ void xqc_send_ctl_on_pns_discard(xqc_send_ctl_t *send_ctl, xqc_pkt_num_space_t p void xqc_send_ctl_on_packet_sent(xqc_send_ctl_t *send_ctl, xqc_pn_ctl_t *pn_ctl, xqc_packet_out_t *packet_out, xqc_usec_t now, ssize_t sent); -int xqc_send_ctl_on_ack_received_spns(xqc_connection_t *conn, xqc_ack_info_t *const ack_info, xqc_usec_t ack_recv_time); - -int xqc_send_ctl_on_ack_received (xqc_send_ctl_t *send_ctl, xqc_pn_ctl_t *pn_ctl, xqc_send_queue_t *send_queue, xqc_ack_info_t *const ack_info, xqc_usec_t ack_recv_time); +int xqc_send_ctl_on_ack_received (xqc_send_ctl_t *send_ctl, xqc_pn_ctl_t *pn_ctl, xqc_send_queue_t *send_queue, xqc_ack_info_t *const ack_info, xqc_usec_t ack_recv_time, xqc_bool_t ack_on_same_path); void xqc_send_ctl_on_dgram_received(xqc_send_ctl_t *send_ctl, size_t dgram_size); @@ -264,29 +254,4 @@ xqc_packet_number_t xqc_send_ctl_get_lost_sent_pn(xqc_send_ctl_t *send_ctl, xqc_ xqc_packet_number_t xqc_send_ctl_get_pkt_num_gap(xqc_send_ctl_t *send_ctl, xqc_pkt_num_space_t pns, xqc_packet_number_t front, xqc_packet_number_t back); - -/* sent record (single pns only) */ -void xqc_sent_record_init(xqc_sent_record_t *sent_record); - -void xqc_sent_record_release(xqc_sent_record_t *sent_record); - -/* 决定发包路径后,把pn添加到path里 */ -xqc_int_t xqc_sent_record_add(xqc_sent_record_t *sent_record, xqc_packet_number_t pkt_num, xqc_usec_t sent_time); - -/* 在ack received响应之后,移除已全部确认的前排pn */ -void xqc_sent_record_del(xqc_sent_record_t *sent_record); - -/* 计算视作丢包的pn */ -xqc_int_t xqc_sent_record_lost_sent_pn(xqc_sent_record_t *sent_record, xqc_packet_number_t largest_acked, xqc_packet_number_t threshold, xqc_packet_number_t *lost_pn); - -/* 计算两个pn的间隔 */ -xqc_int_t xqc_sent_record_pn_gap(xqc_sent_record_t *sent_record, xqc_packet_number_t front, xqc_packet_number_t back, xqc_packet_number_t *gap); - -/* 找到ack info里属于sent record的最大pn */ -xqc_int_t xqc_sent_record_get_largest_pn_in_ack(xqc_sent_record_t *sent_record, xqc_ack_info_t *const ack_info, xqc_packet_number_node_t **largest_pn_node); - -/* Temp */ -/* void xqc_sent_record_log(xqc_send_ctl_t *send_ctl, xqc_packet_out_t *packet_out); */ - - #endif /* _XQC_SEND_CTL_H_INCLUDED_ */ diff --git a/src/transport/xqc_send_queue.c b/src/transport/xqc_send_queue.c index 58f679bf8..082224416 100644 --- a/src/transport/xqc_send_queue.c +++ b/src/transport/xqc_send_queue.c @@ -7,6 +7,8 @@ #include "src/transport/xqc_multipath.h" #include "src/transport/xqc_send_ctl.h" #include "src/transport/xqc_stream.h" +#include "src/transport/xqc_reinjection.h" +#include "src/transport/xqc_conn.h" xqc_send_queue_t * @@ -34,8 +36,6 @@ xqc_send_queue_create(xqc_connection_t *conn) send_queue->sndq_packets_used_max = XQC_SNDQ_PACKETS_USED_MAX; } - send_queue->pkt_out_size = conn->conn_settings.max_pkt_out_size; - send_queue->sndq_conn = conn; send_queue->sndq_packets_in_unacked_list = 0; @@ -226,6 +226,15 @@ xqc_send_queue_remove_lost(xqc_list_head_t *pos) void xqc_send_queue_insert_free(xqc_packet_out_t *po, xqc_list_head_t *head, xqc_send_queue_t *send_queue) { + if (po->po_pr) { + if (po->po_pr->ref_cnt <= 1) { + xqc_conn_destroy_ping_record(po->po_pr); + + } else { + po->po_pr->ref_cnt--; + po->po_pr = NULL; + } + } xqc_list_add_tail(&po->po_list, head); send_queue->sndq_packets_free++; send_queue->sndq_packets_used--; @@ -338,32 +347,10 @@ xqc_send_queue_copy_to_lost(xqc_packet_out_t *packet_out, xqc_send_queue_t *send } xqc_packet_out_copy(new_po, packet_out); + xqc_packet_out_remove_ack_frame(new_po); - /* 重注入包应在原路径重传,其余情况可以自由调度 */ - if (packet_out->po_flag & (XQC_POF_REINJECTED_ORIGIN | XQC_POF_REINJECTED_REPLICA)) { - new_po->po_is_path_specified = XQC_TRUE; - } - - int ret; - - if ((new_po->po_ack_offset > 0) && (new_po->po_frame_types & XQC_FRAME_BIT_ACK)) { - new_po->po_frame_types &= ~XQC_FRAME_BIT_ACK; - new_po->po_used_size = new_po->po_ack_offset; - ret = xqc_write_ack_to_one_packet(conn, new_po, new_po->po_pkt.pkt_pns); - if (ret < 0) { - xqc_log(conn->log, XQC_LOG_WARN, "|xqc_write_ack_to_one_packet error|"); - } - - } else if ((new_po->po_ack_offset > 0) && (new_po->po_frame_types & XQC_FRAME_BIT_ACK_MP)) { - new_po->po_frame_types &= ~XQC_FRAME_BIT_ACK_MP; - new_po->po_used_size = new_po->po_ack_offset; - xqc_path_ctx_t *path = xqc_conn_find_path_by_path_id(conn, new_po->po_path_id); - if (path != NULL) { - ret = xqc_write_ack_mp_to_one_packet(conn, path, new_po, new_po->po_pkt.pkt_pns); - if (ret < 0) { - xqc_log(conn->log, XQC_LOG_WARN, "|xqc_write_ack_mp_to_one_packet error|"); - } - } + if (new_po->po_path_flag & XQC_PATH_SPECIFIED_BY_PTO) { + new_po->po_path_flag &= ~XQC_PATH_SPECIFIED_BY_PTO; } xqc_send_queue_insert_lost(&new_po->po_list, &send_queue->sndq_lost_packets); @@ -383,29 +370,11 @@ xqc_send_queue_copy_to_probe(xqc_packet_out_t *packet_out, xqc_send_queue_t *sen } xqc_packet_out_copy(new_po, packet_out); + xqc_packet_out_remove_ack_frame(new_po); - new_po->po_is_path_specified = XQC_TRUE; + new_po->po_path_flag |= XQC_PATH_SPECIFIED_BY_PTO; new_po->po_path_id = path->path_id; - int ret; - - if ((new_po->po_ack_offset > 0) && (new_po->po_frame_types & XQC_FRAME_BIT_ACK)) { - new_po->po_frame_types &= ~XQC_FRAME_BIT_ACK; - new_po->po_used_size = new_po->po_ack_offset; - ret = xqc_write_ack_to_one_packet(conn, new_po, new_po->po_pkt.pkt_pns); - if (ret < 0) { - xqc_log(conn->log, XQC_LOG_WARN, "|xqc_write_ack_to_one_packet error|"); - } - - } else if ((new_po->po_ack_offset > 0) && (new_po->po_frame_types & XQC_FRAME_BIT_ACK_MP)) { - new_po->po_frame_types &= ~XQC_FRAME_BIT_ACK_MP; - new_po->po_used_size = new_po->po_ack_offset; - ret = xqc_write_ack_mp_to_one_packet(conn, path, new_po, new_po->po_pkt.pkt_pns); - if (ret < 0) { - xqc_log(conn->log, XQC_LOG_WARN, "|xqc_write_ack_mp_to_one_packet error|"); - } - } - xqc_send_queue_insert_probe(&new_po->po_list, &send_queue->sndq_pto_probe_packets); send_queue->sndq_packets_used++; packet_out->po_flag |= XQC_POF_RETRANSED; diff --git a/src/transport/xqc_send_queue.h b/src/transport/xqc_send_queue.h index dd59ca133..596dd1afa 100644 --- a/src/transport/xqc_send_queue.h +++ b/src/transport/xqc_send_queue.h @@ -5,7 +5,8 @@ #include "src/transport/xqc_conn.h" -#define XQC_SNDQ_PACKETS_USED_MAX 16000 +#define XQC_SNDQ_PACKETS_USED_MAX 18000 +#define XQC_SNDQ_RELEASE_ENOUGH_SPACE_TH 10 /* 1 / 10*/ typedef struct xqc_send_queue_s { @@ -27,7 +28,7 @@ typedef struct xqc_send_queue_s { uint64_t sndq_packets_free; uint64_t sndq_packets_used_max; - size_t pkt_out_size; + xqc_bool_t sndq_full; } xqc_send_queue_t; @@ -41,6 +42,12 @@ xqc_send_queue_can_write(xqc_send_queue_t *send_queue) return XQC_FALSE; } +static inline xqc_bool_t +xqc_send_queue_release_enough_space(xqc_send_queue_t *send_queue) +{ + return (send_queue->sndq_packets_used_max - send_queue->sndq_packets_used) + >= (send_queue->sndq_packets_used_max / XQC_SNDQ_RELEASE_ENOUGH_SPACE_TH); +} uint64_t xqc_send_queue_get_unsent_packets_num(xqc_send_queue_t *send_queue); diff --git a/src/transport/xqc_stream.c b/src/transport/xqc_stream.c index 1fefab496..d2517c594 100644 --- a/src/transport/xqc_stream.c +++ b/src/transport/xqc_stream.c @@ -134,6 +134,44 @@ xqc_stream_maybe_need_close(xqc_stream_t *stream) } } +void +xqc_stream_close_discarded_stream(xqc_stream_t *stream) +{ + xqc_timer_manager_t *timer_manager; + xqc_usec_t now; + xqc_usec_t pto; + xqc_usec_t new_expire; + + if (stream->stream_flag & XQC_STREAM_FLAG_NEED_CLOSE) { + return; + } + + xqc_log(stream->stream_conn->log, XQC_LOG_DEBUG, + "|stream_id:%ui|stream_type:%d|", + stream->stream_id, stream->stream_type); + + stream->stream_flag |= XQC_STREAM_FLAG_NEED_CLOSE; + + now = xqc_monotonic_timestamp(); + pto = xqc_conn_get_max_pto(stream->stream_conn); + new_expire = now + 3 * pto; + + timer_manager = &stream->stream_conn->conn_timer_manager; + + if ((timer_manager->timer[XQC_TIMER_STREAM_CLOSE].timer_is_set + && new_expire < timer_manager->timer[XQC_TIMER_STREAM_CLOSE].expire_time) + || !timer_manager->timer[XQC_TIMER_STREAM_CLOSE].timer_is_set) + { + xqc_timer_set(timer_manager, XQC_TIMER_STREAM_CLOSE, now, 3 * pto); + } + + stream->stream_close_time = new_expire; + + xqc_list_add_tail(&stream->closing_stream_list, &stream->stream_conn->conn_closing_streams); + xqc_stream_shutdown_read(stream); + xqc_stream_shutdown_write(stream); +} + xqc_stream_t * xqc_find_stream_by_id(xqc_stream_id_t stream_id, xqc_id_hash_table_t *streams_hash) { @@ -149,8 +187,8 @@ xqc_stream_set_flow_ctl(xqc_stream_t *stream) xqc_connection_t *conn = stream->stream_conn; if ((remote_settings->max_stream_data_bidi_remote - & remote_settings->max_stream_data_bidi_local - & remote_settings->max_stream_data_uni) == 0) + && remote_settings->max_stream_data_bidi_local + && remote_settings->max_stream_data_uni) == XQC_FALSE) { remote_settings = &stream->stream_conn->local_settings; } @@ -214,6 +252,42 @@ xqc_stream_set_flow_ctl(xqc_stream_t *stream) } } + +void +xqc_stream_update_flow_ctl(xqc_stream_t *stream) +{ + xqc_trans_settings_t *remote_settings = &stream->stream_conn->remote_settings; + xqc_connection_t *conn = stream->stream_conn; + + if (conn->conn_type == XQC_CONN_TYPE_CLIENT) { + if (stream->stream_type == XQC_CLI_BID) { + stream->stream_flow_ctl.fc_max_stream_data_can_send = remote_settings->max_stream_data_bidi_remote; + + } else if (stream->stream_type == XQC_SVR_BID) { + /* + * in server transport parameters, + * this applies to streams with the least significant two bits set to 0x1 + */ + stream->stream_flow_ctl.fc_max_stream_data_can_send = remote_settings->max_stream_data_bidi_local; + + } else { + stream->stream_flow_ctl.fc_max_stream_data_can_send = remote_settings->max_stream_data_uni; + } + + } else { /* conn->conn_type == XQC_CONN_TYPE_SERVER */ + if (stream->stream_type == XQC_CLI_BID) { + stream->stream_flow_ctl.fc_max_stream_data_can_send = remote_settings->max_stream_data_bidi_local; + + } else if (stream->stream_type == XQC_SVR_BID) { + stream->stream_flow_ctl.fc_max_stream_data_can_send = remote_settings->max_stream_data_bidi_remote; + + } else { + stream->stream_flow_ctl.fc_max_stream_data_can_send = remote_settings->max_stream_data_uni; + } + } +} + + uint64_t xqc_stream_get_init_max_stream_data(xqc_stream_t *stream) { @@ -243,8 +317,12 @@ int xqc_stream_do_send_flow_ctl(xqc_stream_t *stream) { int ret = XQC_OK; + + xqc_log(stream->stream_conn->log, XQC_LOG_DEBUG, "|conn_flow_ctl|window:%ui|", + stream->stream_conn->conn_flow_ctl.fc_max_data_can_send - stream->stream_conn->conn_flow_ctl.fc_data_sent); + /* connection level */ - if (stream->stream_conn->conn_flow_ctl.fc_data_sent + XQC_PACKET_OUT_SIZE > stream->stream_conn->conn_flow_ctl.fc_max_data_can_send) { + if (stream->stream_conn->conn_flow_ctl.fc_data_sent + stream->stream_conn->pkt_out_size > stream->stream_conn->conn_flow_ctl.fc_max_data_can_send) { xqc_log(stream->stream_conn->log, XQC_LOG_INFO, "|xqc_stream_send|exceed max_data:%ui|", stream->stream_conn->conn_flow_ctl.fc_max_data_can_send); @@ -254,7 +332,7 @@ xqc_stream_do_send_flow_ctl(xqc_stream_t *stream) } /* stream level */ - if (stream->stream_send_offset + XQC_PACKET_OUT_SIZE > stream->stream_flow_ctl.fc_max_stream_data_can_send) { + if (stream->stream_send_offset + stream->stream_conn->pkt_out_size > stream->stream_flow_ctl.fc_max_stream_data_can_send) { xqc_log(stream->stream_conn->log, XQC_LOG_INFO, "|xqc_stream_send|exceed max_stream_data:%ui|", stream->stream_flow_ctl.fc_max_stream_data_can_send); @@ -273,51 +351,94 @@ xqc_stream_do_recv_flow_ctl(xqc_stream_t *stream) xqc_usec_t now = xqc_monotonic_timestamp(); /* increase recv window */ - xqc_usec_t min_srtt = xqc_conn_get_min_srtt(conn); + xqc_usec_t min_srtt = xqc_conn_get_min_srtt(conn, 0); + xqc_usec_t max_srtt = 0; + uint64_t old_fc_win = 0; /* stream level */ - uint64_t available_window - = stream->stream_flow_ctl.fc_max_stream_data_can_recv - stream->stream_data_in.next_read_offset; + uint64_t available_window = stream->stream_flow_ctl.fc_max_stream_data_can_recv - stream->stream_data_in.next_read_offset; + if (available_window < stream->stream_flow_ctl.fc_stream_recv_window_size / 2) { - if (stream->stream_flow_ctl.fc_last_window_update_time == 0) { - /* first update window */ + + if (!stream->recv_rate_bytes_per_sec) { + if (stream->stream_flow_ctl.fc_last_window_update_time + && (now - stream->stream_flow_ctl.fc_last_window_update_time < 2 * min_srtt)) + { + stream->stream_flow_ctl.fc_stream_recv_window_size + = xqc_min(stream->stream_flow_ctl.fc_stream_recv_window_size * 2, XQC_MAX_RECV_WINDOW); + } + + } else { + + if (!max_srtt) { + max_srtt = xqc_conn_get_max_srtt(conn); + } - } else if (now - stream->stream_flow_ctl.fc_last_window_update_time < 2 * min_srtt) { - stream->stream_flow_ctl.fc_stream_recv_window_size - = xqc_min(stream->stream_flow_ctl.fc_stream_recv_window_size * 2, XQC_MAX_RECV_WINDOW); + old_fc_win = stream->stream_flow_ctl.fc_stream_recv_window_size; + stream->stream_flow_ctl.fc_stream_recv_window_size = stream->recv_rate_bytes_per_sec * max_srtt / 1000000; + stream->stream_flow_ctl.fc_stream_recv_window_size = xqc_max(conn->conn_settings.init_recv_window, stream->stream_flow_ctl.fc_stream_recv_window_size); + stream->stream_flow_ctl.fc_stream_recv_window_size = xqc_min(XQC_MAX_RECV_WINDOW, stream->stream_flow_ctl.fc_stream_recv_window_size); + xqc_log(conn->log, XQC_LOG_DEBUG, + "|stream_level|fc_win_update|old_fc_win:%ui|fc_win:%ui|", + old_fc_win, stream->stream_flow_ctl.fc_stream_recv_window_size); } + stream->stream_flow_ctl.fc_last_window_update_time = now; - stream->stream_flow_ctl.fc_max_stream_data_can_recv - += (stream->stream_flow_ctl.fc_stream_recv_window_size - available_window); - xqc_log(conn->log, XQC_LOG_DEBUG, - "|xqc_write_max_stream_data_to_packet|new_max_data:%ui|stream_max_recv_offset:%ui|next_read_offset:%ui|window_size:%ui|", - stream->stream_flow_ctl.fc_max_stream_data_can_recv, stream->stream_max_recv_offset, - stream->stream_data_in.next_read_offset, stream->stream_flow_ctl.fc_stream_recv_window_size); - xqc_write_max_stream_data_to_packet(conn, stream->stream_id, stream->stream_flow_ctl.fc_max_stream_data_can_recv); + + if (stream->stream_flow_ctl.fc_stream_recv_window_size > available_window) { + stream->stream_flow_ctl.fc_max_stream_data_can_recv += (stream->stream_flow_ctl.fc_stream_recv_window_size - available_window); + xqc_log(conn->log, XQC_LOG_DEBUG, + "|xqc_write_max_stream_data_to_packet|new_max_data:%ui|stream_max_recv_offset:%ui|next_read_offset:%ui|window_size:%ui|", + stream->stream_flow_ctl.fc_max_stream_data_can_recv, stream->stream_max_recv_offset, + stream->stream_data_in.next_read_offset, stream->stream_flow_ctl.fc_stream_recv_window_size); + xqc_write_max_stream_data_to_packet(conn, stream->stream_id, stream->stream_flow_ctl.fc_max_stream_data_can_recv, XQC_PTYPE_SHORT_HEADER); + } } /* connection level */ - available_window - = conn->conn_flow_ctl.fc_max_data_can_recv - conn->conn_flow_ctl.fc_data_read; + available_window = conn->conn_flow_ctl.fc_max_data_can_recv - conn->conn_flow_ctl.fc_data_read; + if (available_window < conn->conn_flow_ctl.fc_recv_windows_size / 2) { - if (conn->conn_flow_ctl.fc_last_window_update_time == 0) { - /* first update window */ - } else if (now - conn->conn_flow_ctl.fc_last_window_update_time < 2 * min_srtt) { - conn->conn_flow_ctl.fc_recv_windows_size - = xqc_min(conn->conn_flow_ctl.fc_recv_windows_size * 2, XQC_MAX_RECV_WINDOW); + if (!conn->conn_settings.recv_rate_bytes_per_sec) { + + if (conn->conn_flow_ctl.fc_last_window_update_time + && (now - conn->conn_flow_ctl.fc_last_window_update_time < 2 * min_srtt)) + { + conn->conn_flow_ctl.fc_recv_windows_size + = xqc_min(conn->conn_flow_ctl.fc_recv_windows_size * 2, XQC_MAX_RECV_WINDOW); + } + + if (conn->conn_flow_ctl.fc_recv_windows_size < 1.5 * stream->stream_flow_ctl.fc_stream_recv_window_size) { + conn->conn_flow_ctl.fc_recv_windows_size = (uint64_t)(1.5 * stream->stream_flow_ctl.fc_stream_recv_window_size); + } + + } else { + + if (!max_srtt) { + max_srtt = xqc_conn_get_max_srtt(conn); + } + + old_fc_win = conn->conn_flow_ctl.fc_recv_windows_size; + conn->conn_flow_ctl.fc_recv_windows_size = conn->conn_settings.recv_rate_bytes_per_sec * max_srtt / 1000000; + conn->conn_flow_ctl.fc_recv_windows_size = xqc_max(XQC_MIN_RECV_WINDOW, conn->conn_flow_ctl.fc_recv_windows_size); + conn->conn_flow_ctl.fc_recv_windows_size = xqc_min(XQC_MAX_RECV_WINDOW, conn->conn_flow_ctl.fc_recv_windows_size); + xqc_log(conn->log, XQC_LOG_DEBUG, + "|conn_level|fc_win_update|old_fc_win:%ui|fc_win:%ui|", + old_fc_win, conn->conn_flow_ctl.fc_recv_windows_size); + } + conn->conn_flow_ctl.fc_last_window_update_time = now; - if (conn->conn_flow_ctl.fc_recv_windows_size < 1.5 * stream->stream_flow_ctl.fc_stream_recv_window_size) { - conn->conn_flow_ctl.fc_recv_windows_size = (uint64_t)(1.5 * stream->stream_flow_ctl.fc_stream_recv_window_size); + + if (conn->conn_flow_ctl.fc_recv_windows_size > available_window) { + conn->conn_flow_ctl.fc_max_data_can_recv += (conn->conn_flow_ctl.fc_recv_windows_size - available_window); + xqc_log(conn->log, XQC_LOG_DEBUG, + "|xqc_write_max_data_to_packet|new_max_data:%ui|fc_data_recved:%ui|fc_data_read:%ui|window_size:%ui|", + conn->conn_flow_ctl.fc_max_data_can_recv, conn->conn_flow_ctl.fc_data_recved, + conn->conn_flow_ctl.fc_data_read, conn->conn_flow_ctl.fc_recv_windows_size); + xqc_write_max_data_to_packet(conn, conn->conn_flow_ctl.fc_max_data_can_recv); } - conn->conn_flow_ctl.fc_max_data_can_recv - += (conn->conn_flow_ctl.fc_recv_windows_size - available_window); - xqc_log(conn->log, XQC_LOG_DEBUG, - "|xqc_write_max_data_to_packet|new_max_data:%ui|fc_data_recved:%ui|fc_data_read:%ui|window_size:%ui|", - conn->conn_flow_ctl.fc_max_data_can_recv, conn->conn_flow_ctl.fc_data_recved, - conn->conn_flow_ctl.fc_data_read, conn->conn_flow_ctl.fc_recv_windows_size); - xqc_write_max_data_to_packet(conn, conn->conn_flow_ctl.fc_max_data_can_recv); } return XQC_OK; @@ -377,7 +498,8 @@ xqc_stream_do_create_flow_ctl(xqc_connection_t *conn, xqc_stream_id_t stream_id, } xqc_stream_t * -xqc_stream_create(xqc_engine_t *engine, const xqc_cid_t *cid, void *user_data) +xqc_stream_create(xqc_engine_t *engine, const xqc_cid_t *cid, xqc_stream_settings_t *settings, + void *user_data) { xqc_connection_t *conn; xqc_stream_t *stream; @@ -389,7 +511,7 @@ xqc_stream_create(xqc_engine_t *engine, const xqc_cid_t *cid, void *user_data) return NULL; } - stream = xqc_create_stream_with_conn(conn, XQC_UNDEFINE_STREAM_ID, XQC_CLI_BID, user_data); + stream = xqc_create_stream_with_conn(conn, XQC_UNDEFINE_STREAM_ID, XQC_CLI_BID, settings, user_data); if (!stream) { xqc_log(engine->log, XQC_LOG_ERROR, "|xqc_create_stream_with_conn error|"); return NULL; @@ -424,14 +546,16 @@ xqc_stream_create_with_direction(xqc_connection_t *conn, /* create stream */ return xqc_create_stream_with_conn(conn, XQC_UNDEFINE_STREAM_ID, type, - user_data); + NULL, user_data); } xqc_stream_t * xqc_create_stream_with_conn(xqc_connection_t *conn, xqc_stream_id_t stream_id, - xqc_stream_type_t stream_type, void *user_data) + xqc_stream_type_t stream_type, xqc_stream_settings_t *settings, void *user_data) { + xqc_int_t ret; + if (conn->conn_state >= XQC_CONN_STATE_CLOSING) { xqc_log(conn->log, XQC_LOG_ERROR, "|conn closing, cannot create stream|type:%d|state:%d|flag:%s|", conn->conn_type, conn->conn_state, xqc_conn_flag_2_str(conn->conn_flag)); @@ -493,8 +617,23 @@ xqc_create_stream_with_conn(xqc_connection_t *conn, xqc_stream_id_t stream_id, xqc_stream_ready_to_write(stream); } + stream->recv_rate_bytes_per_sec = 0; + + if (settings) { + if (conn->conn_settings.enable_stream_rate_limit + && stream->stream_type == XQC_CLI_BID) + { + stream->recv_rate_bytes_per_sec = settings->recv_rate_bytes_per_sec; + } + } + if (stream->stream_if->stream_create_notify) { - stream->stream_if->stream_create_notify(stream, stream->user_data); + ret = stream->stream_if->stream_create_notify(stream, stream->user_data); + if (XQC_OK != ret) { + xqc_log(conn->log, XQC_LOG_WARN, "|stream create notify error|" + "|stream_id:%ui", stream->stream_id); + stream->stream_flag |= XQC_STREAM_FLAG_DISCARDED; + } } return stream; @@ -535,7 +674,9 @@ xqc_destroy_stream(xqc_stream_t *stream) xqc_log(stream->stream_conn->log, XQC_LOG_DEBUG, "|send_state:%d|recv_state:%d|stream_id:%ui|stream_type:%d|", stream->stream_state_send, stream->stream_state_recv, stream->stream_id, stream->stream_type); - if (stream->stream_if->stream_close_notify) { + if (stream->stream_if->stream_close_notify + && !(stream->stream_flag & XQC_STREAM_FLAG_DISCARDED)) + { stream->stream_if->stream_close_notify(stream, stream->user_data); } @@ -630,6 +771,18 @@ xqc_stream_close(xqc_stream_t *stream) XQC_CONN_ERR(conn, TRA_INTERNAL_ERROR); } + /* A STOP_SENDING frame can be sent for streams in the "Recv" or "Size + Known" states */ + if (stream->stream_state_recv == XQC_RECV_STREAM_ST_RECV + || stream->stream_state_recv == XQC_RECV_STREAM_ST_SIZE_KNOWN) + { + ret = xqc_write_stop_sending_to_packet(conn, stream, H3_REQUEST_CANCELLED); + if (ret < 0) { + xqc_log(conn->log, XQC_LOG_ERROR, "|xqc_write_stop_sending_to_packet error|%d|", ret); + XQC_CONN_ERR(conn, TRA_INTERNAL_ERROR); + } + } + if (!(conn->conn_flag & XQC_CONN_FLAG_TICKING)) { if (0 == xqc_conns_pq_push(conn->engine->conns_active_pq, conn, conn->last_ticked_time)) { conn->conn_flag |= XQC_CONN_FLAG_TICKING; @@ -679,7 +832,7 @@ xqc_passive_create_stream(xqc_connection_t *conn, xqc_stream_id_t stream_id, voi } } - xqc_stream_t *stream = xqc_create_stream_with_conn(conn, stream_id, 0, user_data); + xqc_stream_t *stream = xqc_create_stream_with_conn(conn, stream_id, 0, NULL, user_data); if (stream == NULL) { xqc_log(conn->log, XQC_LOG_ERROR, "|xqc_create_stream_with_conn error|stream_id:%ui|", stream_id); XQC_CONN_ERR(conn, TRA_INTERNAL_ERROR); @@ -689,6 +842,46 @@ xqc_passive_create_stream(xqc_connection_t *conn, xqc_stream_id_t stream_id, voi return stream; } + +xqc_int_t +xqc_stream_update_settings(xqc_stream_t *stream, + xqc_stream_settings_t *settings) +{ + xqc_connection_t *conn = NULL; + xqc_usec_t max_srtt = 0; + uint64_t old_fc_win = 0, new_offset = 0; + + if (stream && settings + && settings->recv_rate_bytes_per_sec) + { + conn = stream->stream_conn; + if (conn->conn_settings.enable_stream_rate_limit) { + stream->recv_rate_bytes_per_sec = settings->recv_rate_bytes_per_sec; + max_srtt = xqc_conn_get_max_srtt(conn); + old_fc_win = stream->stream_flow_ctl.fc_stream_recv_window_size; + stream->stream_flow_ctl.fc_stream_recv_window_size = stream->recv_rate_bytes_per_sec * max_srtt / 1000000; + stream->stream_flow_ctl.fc_stream_recv_window_size = xqc_max(conn->conn_settings.init_recv_window, stream->stream_flow_ctl.fc_stream_recv_window_size); + stream->stream_flow_ctl.fc_stream_recv_window_size = xqc_min(XQC_MAX_RECV_WINDOW, stream->stream_flow_ctl.fc_stream_recv_window_size); + xqc_log(conn->log, XQC_LOG_DEBUG, + "|fc_win_update|old_fc_win:%ui|fc_win:%ui|", + old_fc_win, stream->stream_flow_ctl.fc_stream_recv_window_size); + new_offset = stream->stream_data_in.next_read_offset + stream->stream_flow_ctl.fc_stream_recv_window_size; + + if(new_offset > stream->stream_flow_ctl.fc_max_stream_data_can_recv) { + stream->stream_flow_ctl.fc_max_stream_data_can_recv = new_offset; + xqc_log(conn->log, XQC_LOG_DEBUG, + "|new_max_data:%ui|stream_max_recv_offset:%ui|next_read_offset:%ui|window_size:%ui|", + stream->stream_flow_ctl.fc_max_stream_data_can_recv, stream->stream_max_recv_offset, + stream->stream_data_in.next_read_offset, stream->stream_flow_ctl.fc_stream_recv_window_size); + xqc_write_max_stream_data_to_packet(conn, stream->stream_id, stream->stream_flow_ctl.fc_max_stream_data_can_recv, XQC_PTYPE_NUM); + } + return XQC_OK; + } + } + + return -XQC_EPARAM; +} + xqc_int_t xqc_read_crypto_stream(xqc_stream_t *stream) { @@ -829,10 +1022,6 @@ xqc_crypto_stream_send(xqc_stream_t *stream, while (stream->stream_send_offset < send_data_num) { unsigned int header_size = xqc_crypto_frame_header_size(stream->stream_send_offset, buf->data_len - offset); - - int need = 0; - need = ((buf->data_len - offset + header_size) > XQC_PACKET_OUT_SIZE) ? - (header_size + MIN_CRYPTO_FRAME_SIZE) : (buf->data_len - offset + header_size); packet_out = xqc_write_new_packet(c, pkt_type); if (packet_out == NULL) { return -XQC_EWRITE_PKT; @@ -1096,8 +1285,8 @@ xqc_stream_recv(xqc_stream_t *stream, unsigned char *recv_buf, size_t recv_buf_s } - if (stream->stream_data_in.stream_length > 0 - && stream->stream_data_in.next_read_offset == stream->stream_data_in.stream_length) + if (stream->stream_data_in.stream_determined + && stream->stream_data_in.next_read_offset == stream->stream_data_in.stream_length) { *fin = 1; stream->stream_stats.peer_fin_read_time = xqc_monotonic_timestamp(); @@ -1167,6 +1356,7 @@ xqc_stream_send(xqc_stream_t *stream, unsigned char *send_data, size_t send_data stream->stream_flag |= XQC_STREAM_FLAG_HAS_0RTT; } else { + xqc_log(conn->log, XQC_LOG_DEBUG, "|blocked by no 0RTT support|"); ret = -XQC_EAGAIN; goto do_buff; } @@ -1183,6 +1373,7 @@ xqc_stream_send(xqc_stream_t *stream, unsigned char *send_data, size_t send_data } if (!xqc_send_queue_can_write(conn->conn_send_queue)) { + conn->conn_send_queue->sndq_full = XQC_TRUE; xqc_log(conn->log, XQC_LOG_DEBUG, "|too many packets used|sndq_packets_used:%ud|", conn->conn_send_queue->sndq_packets_used); ret = -XQC_EAGAIN; goto do_buff; @@ -1332,6 +1523,9 @@ xqc_stream_write_buffed_data_to_packets(xqc_stream_t *stream) return ret; } offset += send_data_written; + xqc_log(conn->log, XQC_LOG_DEBUG, + "|resend 1RTT stream packets|stream_id:%ui|offset:%uz|fin:%d|", + stream->stream_id, offset, fin); if (fin_only) { break; } @@ -1351,6 +1545,7 @@ xqc_process_write_streams(xqc_connection_t *conn) xqc_int_t ret; xqc_stream_t *stream; xqc_list_head_t *pos, *next; + int cnt = 0; xqc_list_for_each_safe(pos, next, &conn->conn_write_streams) { stream = xqc_list_entry(pos, xqc_stream_t, write_stream_list); @@ -1361,8 +1556,8 @@ xqc_process_write_streams(xqc_connection_t *conn) stream->stream_id, stream->stream_conn); continue; } - xqc_log(conn->log, XQC_LOG_DEBUG, "|stream_write_notify|flag:%d|stream_id:%ui|conn:%p|", - stream->stream_flag, stream->stream_id, stream->stream_conn); + xqc_log(conn->log, XQC_LOG_DEBUG, "|stream_write_notify|flag:%d|stream_id:%ui|conn:%p|cnt:%d|", + stream->stream_flag, stream->stream_id, stream->stream_conn, cnt++); if (stream->stream_if->stream_write_notify == NULL) { xqc_log(conn->log, XQC_LOG_ERROR, "|stream_write_notify is NULL|flag:%d|stream_id:%ui|conn:%p|", stream->stream_flag, stream->stream_id, stream->stream_conn); @@ -1533,3 +1728,12 @@ xqc_stream_set_multipath_usage(xqc_stream_t *stream, uint8_t schedule, uint8_t r stream->stream_mp_usage_schedule = schedule; stream->stream_mp_usage_reinject = reinject; } + +void +xqc_stream_closing(xqc_stream_t *stream, xqc_int_t err) +{ + if (stream->stream_if->stream_closing_notify) { + stream->stream_if->stream_closing_notify(stream, err, + stream->user_data); + } +} diff --git a/src/transport/xqc_stream.h b/src/transport/xqc_stream.h index 2cebdbf69..e68de280e 100644 --- a/src/transport/xqc_stream.h +++ b/src/transport/xqc_stream.h @@ -35,6 +35,8 @@ typedef enum { XQC_STREAM_FLAG_NEED_CLOSE = 1 << 5, XQC_STREAM_FLAG_FIN_WRITE = 1 << 6, XQC_STREAM_FLAG_CLOSED = 1 << 7, + XQC_STREAM_FLAG_UNEXPECTED = 1 << 8, + XQC_STREAM_FLAG_DISCARDED = 1 << 9, /* stream create_notify with error, all stream data will be discarded */ } xqc_stream_flag_t; typedef enum { @@ -81,6 +83,7 @@ typedef struct xqc_stream_data_in_s { uint64_t merged_offset_end; /* [0,end) */ uint64_t next_read_offset; /* next offset in stream */ uint64_t stream_length; + xqc_bool_t stream_determined; } xqc_stream_data_in_t; @@ -146,6 +149,8 @@ struct xqc_stream_s { xqc_path_metrics_t paths_info[XQC_MAX_PATHS_COUNT]; uint8_t stream_mp_usage_schedule; uint8_t stream_mp_usage_reinject; + + uint64_t recv_rate_bytes_per_sec; }; static inline xqc_stream_type_t @@ -167,7 +172,7 @@ xqc_stream_is_uni(xqc_stream_id_t stream_id) } xqc_stream_t *xqc_create_stream_with_conn (xqc_connection_t *conn, xqc_stream_id_t stream_id, - xqc_stream_type_t stream_type, void *user_data); + xqc_stream_type_t stream_type, xqc_stream_settings_t *settings, void *user_data); void xqc_destroy_stream(xqc_stream_t *stream); @@ -193,14 +198,14 @@ xqc_stream_t *xqc_find_stream_by_id(xqc_stream_id_t stream_id, xqc_id_hash_table void xqc_stream_set_flow_ctl(xqc_stream_t *stream); +void xqc_stream_update_flow_ctl(xqc_stream_t *stream); + int xqc_stream_do_send_flow_ctl(xqc_stream_t *stream); int xqc_stream_do_recv_flow_ctl(xqc_stream_t *stream); int xqc_stream_do_create_flow_ctl(xqc_connection_t *conn, xqc_stream_id_t stream_id, xqc_stream_type_t stream_type); -uint64_t xqc_stream_get_init_max_stream_data(xqc_stream_t *stream); - xqc_stream_t *xqc_passive_create_stream(xqc_connection_t *conn, xqc_stream_id_t stream_id, void *user_data); xqc_stream_t *xqc_create_crypto_stream(xqc_connection_t *conn, xqc_encrypt_level_t encrypt_level, void *user_data); @@ -234,6 +239,9 @@ xqc_stream_recv_state_update(xqc_stream_t *stream, xqc_recv_stream_state_t state void xqc_stream_set_multipath_usage(xqc_stream_t *stream, uint8_t schedule, uint8_t reinject); +void xqc_stream_closing(xqc_stream_t *stream, xqc_int_t err); + +void xqc_stream_close_discarded_stream(xqc_stream_t *stream); #endif /* _XQC_STREAM_H_INCLUDED_ */ diff --git a/src/transport/xqc_timer.c b/src/transport/xqc_timer.c index 4100946bd..cc4e66e1d 100644 --- a/src/transport/xqc_timer.c +++ b/src/transport/xqc_timer.c @@ -27,6 +27,7 @@ static const char * const timer_type_2_str[XQC_TIMER_N] = { [XQC_TIMER_RETIRE_CID] = "RETIRE_CID", [XQC_TIMER_LINGER_CLOSE] = "LINGER_CLOSE", [XQC_TIMER_KEY_UPDATE] = "KEY_UPDATE", + [XQC_TIMER_PMTUD_PROBING] = "PMTUD_PROBING", }; const char * @@ -44,7 +45,6 @@ xqc_timer_ack_timeout(xqc_timer_type_t type, xqc_usec_t now, void *user_data) xqc_connection_t *conn = send_ctl->ctl_conn; xqc_pkt_num_space_t pns = type - XQC_TIMER_ACK_INIT; conn->conn_flag |= XQC_CONN_FLAG_SHOULD_ACK_INIT << pns; - conn->should_ack_path_id = send_ctl->ctl_path->path_id; xqc_log(conn->log, XQC_LOG_DEBUG, "|pns:%d|path:%ui|", pns, send_ctl->ctl_path->path_id); } @@ -102,7 +102,6 @@ xqc_timer_loss_detection_timeout(xqc_timer_type_t type, xqc_usec_t now, void *us } send_ctl->ctl_pto_count++; - send_ctl->ctl_pto_count_since_last_tra_path_status_changed++; xqc_log(conn->log, XQC_LOG_DEBUG, "|xqc_send_ctl_set_loss_detection_timer|PTO|conn:%p|pto_count:%ud", conn, send_ctl->ctl_pto_count); xqc_send_ctl_set_loss_detection_timer(send_ctl); @@ -143,8 +142,13 @@ xqc_timer_path_idle_timeout(xqc_timer_type_t type, xqc_usec_t now, void *user_da return; } + if (path->path_state < XQC_PATH_STATE_CLOSING) { + xqc_log(conn->log, XQC_LOG_DEBUG, "|closing path:%ui|", path->path_id); + xqc_path_immediate_close(path); + } + if (path->path_state < XQC_PATH_STATE_CLOSED) { - xqc_log(conn->log, XQC_LOG_DEBUG, "|close path:%ui|", path->path_id); + xqc_log(conn->log, XQC_LOG_DEBUG, "|closed path:%ui|", path->path_id); xqc_path_closed(path); } @@ -243,8 +247,11 @@ xqc_timer_retire_cid_timeout(xqc_timer_type_t type, xqc_usec_t now, void *user_d /* MP关闭主路后如果删除对应的cid映射,对外接口通过engine和cid无法找到conn,暂时注释掉 */ /* TODO: 1. MP切换主路后通知上层更换cid; 2. 重新设计接口,改用conn而不是engine和cid */ // /* switch state to REMOVED & delete from cid_set */ - // if (xqc_find_conns_hash(conn->engine->conns_hash, conn, &inner_cid->cid)) { - // xqc_remove_conns_hash(conn->engine->conns_hash, conn, &inner_cid->cid); + // if (xqc_find_conns_hash(conn->engine->conns_hash, conn, + // inner_cid->cid.cid_buf, inner_cid->cid.cid_len)) + // { + // xqc_remove_conns_hash(conn->engine->conns_hash, conn, + // inner_cid->cid.cid_buf, inner_cid->cid.cid_len); // } ret = xqc_cid_switch_to_next_state(&conn->scid_set.cid_set, inner_cid, XQC_CID_REMOVED); @@ -253,6 +260,12 @@ xqc_timer_retire_cid_timeout(xqc_timer_type_t type, xqc_usec_t now, void *user_d return; } + xqc_log(conn->log, XQC_LOG_DEBUG, + "|retired->removed|cid:%s|seq:%ui|len:%d|", + xqc_scid_str(&inner_cid->cid), + inner_cid->cid.cid_seq_num, + inner_cid->cid.cid_len); + // xqc_list_del(pos); // xqc_free(inner_cid); @@ -298,6 +311,13 @@ xqc_timer_key_update_timeout(xqc_timer_type_t type, xqc_usec_t now, void *user_d xqc_tls_discard_old_1rtt_keys(conn->tls); } +void +xqc_timer_pmtud_probing_timeout(xqc_timer_type_t type, xqc_usec_t now, void *user_data) +{ + xqc_connection_t *conn = (xqc_connection_t *)user_data; + conn->conn_flag |= XQC_CONN_FLAG_PMTUD_PROBING; +} + /* timer callbacks end */ @@ -361,8 +381,92 @@ xqc_timer_init(xqc_timer_manager_t *manager, xqc_log_t *log, void *user_data) } else if (type == XQC_TIMER_KEY_UPDATE) { timer->timeout_cb = xqc_timer_key_update_timeout; timer->user_data = user_data; + + } else if (type == XQC_TIMER_PMTUD_PROBING) { + timer->timeout_cb = xqc_timer_pmtud_probing_timeout; + timer->user_data = user_data; } } + + /* init gp timer list */ + xqc_init_list_head(&manager->gp_timer_list); + manager->next_gp_timer_id = 0; +} + +xqc_gp_timer_id_t xqc_timer_register_gp_timer(xqc_timer_manager_t *manager, + char *timer_name, xqc_gp_timer_timeout_pt cb, void *user_data) +{ + if (timer_name == NULL + || manager == NULL + || cb == NULL) + { + return -XQC_EPARAM; + } + + if (manager->next_gp_timer_id == XQC_GP_TIMER_ID_MAX) { + return XQC_ERROR; + } + + xqc_gp_timer_t *timer = (xqc_gp_timer_t*)xqc_calloc(1, sizeof(xqc_gp_timer_t)); + if (timer == NULL) { + return -XQC_EMALLOC; + } + + size_t name_len = strnlen(timer_name, 1024); + timer->name = (char*)xqc_calloc(1, name_len + 1); + if (timer->name == NULL) { + xqc_free(timer); + return -XQC_EMALLOC; + } + xqc_memcpy(timer->name, timer_name, name_len); + + timer->timer_is_set = XQC_FALSE; + timer->id = manager->next_gp_timer_id++; + timer->timeout_cb = cb; + timer->user_data = user_data; + + xqc_list_add_tail(&timer->list, &manager->gp_timer_list); + return timer->id; +} + +xqc_int_t +xqc_timer_unregister_gp_timer(xqc_timer_manager_t *manager, xqc_gp_timer_id_t gp_timer_id) +{ + if (!manager || gp_timer_id >= manager->next_gp_timer_id) { + return -XQC_EPARAM; + } + + xqc_list_head_t *pos, *next; + xqc_gp_timer_t *gp_timer; + + xqc_list_for_each_safe(pos, next, &manager->gp_timer_list) { + gp_timer = xqc_list_entry(pos, xqc_gp_timer_t, list); + if (gp_timer->id == gp_timer_id) { + xqc_timer_destroy_gp_timer(gp_timer); + return XQC_OK; + } + } + return XQC_ERROR; +} + +void +xqc_timer_destroy_gp_timer(xqc_gp_timer_t *gp_timer) +{ + xqc_list_del_init(&gp_timer->list); + xqc_free(gp_timer->name); + xqc_free(gp_timer); +} + +void +xqc_timer_destroy_gp_timer_list(xqc_timer_manager_t *manager) +{ + xqc_list_head_t *pos, *next; + xqc_gp_timer_t *gp_timer; + + xqc_list_for_each_safe(pos, next, &manager->gp_timer_list) { + gp_timer = xqc_list_entry(pos, xqc_gp_timer_t, list); + xqc_timer_destroy_gp_timer(gp_timer); + } } /* diff --git a/src/transport/xqc_timer.h b/src/transport/xqc_timer.h index e09ca4fad..f2604c02a 100644 --- a/src/transport/xqc_timer.h +++ b/src/transport/xqc_timer.h @@ -4,6 +4,7 @@ #include "src/common/xqc_time.h" #include "src/transport/xqc_packet.h" #include "src/transport/xqc_packet_in.h" +#include "src/common/xqc_list.h" /* * A connection will time out if no packets are sent or received for a @@ -16,6 +17,8 @@ */ #define XQC_PING_TIMEOUT 15000 +#define XQC_PMTUD_START_DELAY 10000 + typedef enum xqc_timer_level { XQC_PATH_LEVEL_TIMER, XQC_CONN_LEVEL_TIMER, @@ -42,15 +45,21 @@ typedef enum xqc_timer_type { XQC_TIMER_RETIRE_CID, XQC_TIMER_LINGER_CLOSE, XQC_TIMER_KEY_UPDATE, + XQC_TIMER_PMTUD_PROBING, XQC_TIMER_N, } xqc_timer_type_t; +typedef int32_t xqc_gp_timer_id_t; + +#define XQC_GP_TIMER_ID_MAX (0x7fffffff) /* timer timeout callback */ typedef void (*xqc_timer_timeout_pt)(xqc_timer_type_t type, xqc_usec_t now, void *user_data); +typedef void (*xqc_gp_timer_timeout_pt)(xqc_gp_timer_id_t gp_timer_id, xqc_usec_t now, void *user_data); + typedef struct xqc_timer_s { uint8_t timer_is_set; xqc_usec_t expire_time; @@ -60,11 +69,105 @@ typedef struct xqc_timer_s { void *user_data; } xqc_timer_t; +/* general purpose timer */ +typedef struct xqc_gp_timer_s { + xqc_list_head_t list; + xqc_gp_timer_id_t id; + xqc_bool_t timer_is_set; + xqc_usec_t expire_time; + + /* callback function and user_data */ + xqc_gp_timer_timeout_pt timeout_cb; + void *user_data; + char *name; +} xqc_gp_timer_t; + typedef struct xqc_timer_manager_s { xqc_timer_t timer[XQC_TIMER_N]; xqc_log_t *log; + /* general purpose timer */ + xqc_list_head_t gp_timer_list; + xqc_gp_timer_id_t next_gp_timer_id; } xqc_timer_manager_t; +/* APIs for gp timer */ +xqc_gp_timer_id_t xqc_timer_register_gp_timer(xqc_timer_manager_t *manager, + char *timer_name, xqc_gp_timer_timeout_pt cb, void *user_data); + +xqc_int_t xqc_timer_unregister_gp_timer(xqc_timer_manager_t *manager, xqc_gp_timer_id_t gp_timer_id); + +void xqc_timer_destroy_gp_timer(xqc_gp_timer_t *gp_timer); + +void xqc_timer_destroy_gp_timer_list(xqc_timer_manager_t *manager); + +static inline xqc_int_t +xqc_timer_gp_timer_set(xqc_timer_manager_t *manager, xqc_gp_timer_id_t gp_timer_id, xqc_usec_t expire_time) +{ + if (!manager || gp_timer_id >= manager->next_gp_timer_id) { + return -XQC_EPARAM; + } + + xqc_list_head_t *pos, *next; + xqc_gp_timer_t *gp_timer; + + xqc_list_for_each_safe(pos, next, &manager->gp_timer_list) { + gp_timer = xqc_list_entry(pos, xqc_gp_timer_t, list); + if (gp_timer->id == gp_timer_id) { + gp_timer->expire_time = expire_time; + gp_timer->timer_is_set = XQC_TRUE; + xqc_log(manager->log, XQC_LOG_DEBUG, "|gp_timer_set|id:%d|name:%s|expire_time:%ui|", + gp_timer->id, gp_timer->name, gp_timer->expire_time); + return XQC_OK; + } + } + return XQC_ERROR; +} + +static inline xqc_int_t +xqc_timer_gp_timer_unset(xqc_timer_manager_t *manager, xqc_gp_timer_id_t gp_timer_id) +{ + if (!manager || gp_timer_id >= manager->next_gp_timer_id) { + return -XQC_EPARAM; + } + + xqc_list_head_t *pos, *next; + xqc_gp_timer_t *gp_timer; + + xqc_list_for_each_safe(pos, next, &manager->gp_timer_list) { + gp_timer = xqc_list_entry(pos, xqc_gp_timer_t, list); + if (gp_timer->id == gp_timer_id) { + gp_timer->expire_time = 0; + gp_timer->timer_is_set = XQC_FALSE; + xqc_log(manager->log, XQC_LOG_DEBUG, "|gp_timer_unset|id:%d|name:%s|expire_time:%ui|", + gp_timer->id, gp_timer->name, gp_timer->expire_time); + return XQC_OK; + } + } + return XQC_ERROR; +} + +static inline xqc_int_t +xqc_timer_gp_timer_get_info(xqc_timer_manager_t *manager, xqc_gp_timer_id_t gp_timer_id, xqc_bool_t *is_set, xqc_usec_t *expire_time) +{ + if (!manager || gp_timer_id >= manager->next_gp_timer_id) { + return -XQC_EPARAM; + } + + xqc_list_head_t *pos, *next; + xqc_gp_timer_t *gp_timer; + + xqc_list_for_each_safe(pos, next, &manager->gp_timer_list) { + gp_timer = xqc_list_entry(pos, xqc_gp_timer_t, list); + if (gp_timer->id == gp_timer_id) { + *is_set = gp_timer->timer_is_set; + *expire_time = gp_timer->expire_time; + xqc_log(manager->log, XQC_LOG_DEBUG, "|gp_timer_get_info|id:%d|name:%s|is_set:%d|expire_time:%ui|", + gp_timer->id, gp_timer->name, gp_timer->timer_is_set, gp_timer->expire_time); + return XQC_OK; + } + } + return XQC_ERROR; +} const char *xqc_timer_type_2_str(xqc_timer_type_t timer_type); @@ -149,6 +252,22 @@ xqc_timer_expire(xqc_timer_manager_t *manager, xqc_usec_t now) } } } + + /* expire gp timer */ + xqc_list_head_t *pos, *next; + xqc_gp_timer_t *gp_timer; + + xqc_list_for_each_safe(pos, next, &manager->gp_timer_list) { + gp_timer = xqc_list_entry(pos, xqc_gp_timer_t, list); + if (gp_timer->timer_is_set && gp_timer->expire_time <= now) { + xqc_log(manager->log, XQC_LOG_DEBUG, "|gp_timer_expire|id:%d|name:%s|expire_time:%ui|now:%ui|", + gp_timer->id, gp_timer->name, gp_timer->expire_time, now); + gp_timer->timeout_cb(gp_timer->id, now, gp_timer->user_data); + if (gp_timer->expire_time <= now) { + xqc_timer_gp_timer_unset(manager, gp_timer->id); + } + } + } } /* diff --git a/src/transport/xqc_transport_params.c b/src/transport/xqc_transport_params.c index 11438fef5..ab54839c8 100644 --- a/src/transport/xqc_transport_params.c +++ b/src/transport/xqc_transport_params.c @@ -157,9 +157,22 @@ xqc_transport_params_calc_length(const xqc_transport_params_t *params, } if (params->enable_multipath) { - len += xqc_put_varint_len(XQC_TRANSPORT_PARAM_ENABLE_MULTIPATH) + - xqc_put_varint_len(xqc_put_varint_len(params->enable_multipath)) + - xqc_put_varint_len(params->enable_multipath); + if (params->multipath_version == XQC_MULTIPATH_05) { + /* enable_multipath (-draft05) is zero-length transport parameter */ + len += xqc_put_varint_len(XQC_TRANSPORT_PARAM_ENABLE_MULTIPATH_05) + + xqc_put_varint_len(0); + + } else { + len += xqc_put_varint_len(XQC_TRANSPORT_PARAM_ENABLE_MULTIPATH_04) + + xqc_put_varint_len(xqc_put_varint_len(params->enable_multipath)) + + xqc_put_varint_len(params->enable_multipath); + } + } + + if (params->max_datagram_frame_size) { + len += xqc_put_varint_len(XQC_TRANSPORT_PARAM_MAX_DATAGRAM_FRAME_SIZE) + + xqc_put_varint_len(xqc_put_varint_len(params->max_datagram_frame_size)) + + xqc_put_varint_len(params->max_datagram_frame_size); } return len; @@ -315,14 +328,23 @@ xqc_encode_transport_params(const xqc_transport_params_t *params, p = xqc_cpymem(p, params->retry_source_connection_id.cid_buf, params->retry_source_connection_id.cid_len); } + if (params->max_datagram_frame_size) { + p = xqc_put_varint_param(p, XQC_TRANSPORT_PARAM_MAX_DATAGRAM_FRAME_SIZE, + params->max_datagram_frame_size); + } + if (params->no_crypto) { p = xqc_put_varint_param(p, XQC_TRANSPORT_PARAM_NO_CRYPTO, params->no_crypto); } if (params->enable_multipath) { - p = xqc_put_varint_param(p, XQC_TRANSPORT_PARAM_ENABLE_MULTIPATH, - params->enable_multipath); + if (params->multipath_version == XQC_MULTIPATH_05) { + p = xqc_put_zero_length_param(p, XQC_TRANSPORT_PARAM_ENABLE_MULTIPATH_05); + + } else { + p = xqc_put_varint_param(p, XQC_TRANSPORT_PARAM_ENABLE_MULTIPATH_04, params->enable_multipath); + } } if ((size_t)(p - out) != len) { @@ -565,7 +587,26 @@ static xqc_int_t xqc_decode_enable_multipath(xqc_transport_params_t *params, xqc_transport_params_type_t exttype, const uint8_t *p, const uint8_t *end, uint64_t param_type, uint64_t param_len) { - XQC_DECODE_VINT_VALUE(¶ms->enable_multipath, p, end); + if (param_type == XQC_TRANSPORT_PARAM_ENABLE_MULTIPATH_05) { + /* enable_multipath param is a zero-length value, presentation means enable */ + params->enable_multipath = 1; + params->multipath_version = XQC_MULTIPATH_05; + return XQC_OK; + } else if (param_type == XQC_TRANSPORT_PARAM_ENABLE_MULTIPATH_04) { + if (params->multipath_version > XQC_MULTIPATH_04) { + return XQC_OK; + } + params->multipath_version = XQC_MULTIPATH_04; + XQC_DECODE_VINT_VALUE(¶ms->enable_multipath, p, end); + } + return XQC_OK; +} + +static xqc_int_t +xqc_decode_max_datagram_frame_size(xqc_transport_params_t *params, xqc_transport_params_type_t exttype, + const uint8_t *p, const uint8_t *end, uint64_t param_type, uint64_t param_len) +{ + XQC_DECODE_VINT_VALUE(¶ms->max_datagram_frame_size, p, end); } @@ -594,11 +635,12 @@ xqc_trans_param_decode_func xqc_trans_param_decode_func_list[] = { xqc_decode_retry_scid, xqc_decode_no_crypto, xqc_decode_enable_multipath, + xqc_decode_max_datagram_frame_size, }; /* convert param_type to param's index in xqc_trans_param_decode_func_list */ -xqc_int_t +uint64_t xqc_trans_param_get_index(uint64_t param_type) { switch (param_type) { @@ -624,10 +666,14 @@ xqc_trans_param_get_index(uint64_t param_type) case XQC_TRANSPORT_PARAM_NO_CRYPTO: return XQC_TRANSPORT_PARAM_PROTOCOL_MAX; - - case XQC_TRANSPORT_PARAM_ENABLE_MULTIPATH: + + case XQC_TRANSPORT_PARAM_ENABLE_MULTIPATH_04: + case XQC_TRANSPORT_PARAM_ENABLE_MULTIPATH_05: return XQC_TRANSPORT_PARAM_PROTOCOL_MAX + 1; + case XQC_TRANSPORT_PARAM_MAX_DATAGRAM_FRAME_SIZE: + return XQC_TRANSPORT_PARAM_PROTOCOL_MAX + 2; + default: break; } @@ -665,7 +711,7 @@ xqc_decode_one_transport_param(xqc_transport_params_t *params, * read param value, note: some parameters are allowed to be zero-length, * for example, disable_active_migration. */ - xqc_int_t param_index = xqc_trans_param_get_index(param_type); + uint64_t param_index = xqc_trans_param_get_index(param_type); if (param_index != XQC_TRANSPORT_PARAM_UNKNOWN) { xqc_int_t ret = xqc_trans_param_decode_func_list[param_index](params, exttype, p, end, param_type, param_len); @@ -716,8 +762,10 @@ xqc_decode_transport_params(xqc_transport_params_t *params, params->retry_source_connection_id.cid_len = 0; params->no_crypto = 0; + params->max_datagram_frame_size = 0; params->enable_multipath = 0; + params->multipath_version = XQC_ERR_MULTIPATH_VERSION; while (p < end) { ret = xqc_decode_one_transport_param(params, exttype, &p, end); @@ -781,6 +829,10 @@ xqc_read_transport_params(char *tp_data, size_t tp_data_len, xqc_transport_param } else if (strncmp(p, "max_ack_delay=", xqc_lengthof("max_ack_delay=")) == 0) { p += xqc_lengthof("max_ack_delay="); params->max_ack_delay = strtoul(p, NULL, 10); + + } else if (strncmp(p, "max_datagram_frame_size=", xqc_lengthof("max_datagram_frame_size=")) == 0) { + p += xqc_lengthof("max_datagram_frame_size="); + params->max_datagram_frame_size = strtoul(p, NULL, 10); } p = strchr(p, '\n'); @@ -797,23 +849,51 @@ xqc_read_transport_params(char *tp_data, size_t tp_data_len, xqc_transport_param ssize_t xqc_write_transport_params(char *tp_buf, size_t cap, const xqc_transport_params_t *params) { - ssize_t tp_data_len = snprintf(tp_buf, cap, "initial_max_streams_bidi=%"PRIu64"\n" + char dgram_tp_str[256] = ""; + ssize_t tp_data_len = 0; + + if (params->max_datagram_frame_size) { + tp_data_len = snprintf(dgram_tp_str, 256, + "max_datagram_frame_size=%"PRIu64"\n", + params->max_datagram_frame_size); + if (tp_data_len < 0) { + return -XQC_ESYS; + } + } + + + tp_data_len = snprintf(tp_buf, cap, "initial_max_streams_bidi=%"PRIu64"\n" "initial_max_streams_uni=%"PRIu64"\n" "initial_max_stream_data_bidi_local=%"PRIu64"\n" "initial_max_stream_data_bidi_remote=%"PRIu64"\n" "initial_max_stream_data_uni=%"PRIu64"\n" "initial_max_data=%"PRIu64"\n" - "max_ack_delay=%"PRIu64"\n", + "max_ack_delay=%"PRIu64"\n" + "%s", params->initial_max_streams_bidi, params->initial_max_streams_uni, params->initial_max_stream_data_bidi_local, params->initial_max_stream_data_bidi_remote, params->initial_max_stream_data_uni, params->initial_max_data, - params->max_ack_delay); + params->max_ack_delay, + dgram_tp_str); + if (tp_data_len < 0) { return -XQC_ESYS; } return tp_data_len; } + +void +xqc_init_transport_params(xqc_transport_params_t *params) +{ + xqc_memzero(params, sizeof(xqc_transport_params_t)); + + /* like xqc_conn_init_trans_settings. */ + params->max_ack_delay = XQC_DEFAULT_MAX_ACK_DELAY; + params->ack_delay_exponent = XQC_DEFAULT_ACK_DELAY_EXPONENT; + params->max_udp_payload_size = XQC_DEFAULT_MAX_UDP_PAYLOAD_SIZE; + params->active_connection_id_limit = XQC_DEFAULT_ACTIVE_CONNECTION_ID_LIMIT; +} diff --git a/src/transport/xqc_transport_params.h b/src/transport/xqc_transport_params.h index 08dc650b5..98dde9522 100644 --- a/src/transport/xqc_transport_params.h +++ b/src/transport/xqc_transport_params.h @@ -24,6 +24,7 @@ #define XQC_MAX_TRANSPORT_PARAM_BUF_LEN 512 + /** * @brief transport parameter type */ @@ -62,10 +63,15 @@ typedef enum { /* upper limit of params defined in [Transport] */ XQC_TRANSPORT_PARAM_PROTOCOL_MAX, + /* max datagram frame size */ + XQC_TRANSPORT_PARAM_MAX_DATAGRAM_FRAME_SIZE = 0x0020, + /* do no cryption on 0-RTT and 1-RTT packets */ XQC_TRANSPORT_PARAM_NO_CRYPTO = 0x1000, + /* multipath quic attributes */ - XQC_TRANSPORT_PARAM_ENABLE_MULTIPATH = 0xbabf, + XQC_TRANSPORT_PARAM_ENABLE_MULTIPATH_04 = 0x0f739bbc1b666d04, + XQC_TRANSPORT_PARAM_ENABLE_MULTIPATH_05 = 0x0f739bbc1b666d05, /* upper limit of params defined by xquic */ XQC_TRANSPORT_PARAM_UNKNOWN, @@ -111,6 +117,13 @@ typedef struct { xqc_cid_t retry_source_connection_id; uint8_t retry_source_connection_id_present; + /* + * support for datagram (RFC 9221). + * default: 0, not supported + * special: 65535, accept datagram frames with any length in a QUIC packet + */ + uint64_t max_datagram_frame_size; + /** * no_crypto is a self-defined experimental transport parameter by xquic, xquic will do no * encryption on 0-RTT or 1-RTT packets if no_crypto is set to be 1. @@ -123,13 +136,17 @@ typedef struct { /** * enable_multipath is a self-defined experimental transport parameter by xquic, which will * enable multipath quic if enable_multipath is set to be 1. - * https://datatracker.ietf.org/doc/html/draft-ietf-quic-multipath-02#section-3 + + * https://datatracker.ietf.org/doc/html/draft-ietf-quic-multipath-05#section-3 * enable_multipath is designed to be effective only on current connection and do not apply to * future connections, storing this parameter and recover on future connections is prohibited. * NOTICE: enable_multipath MIGHT be modified or removed as it is not an official parameter */ uint64_t enable_multipath; + + xqc_multipath_version_t multipath_version; + } xqc_transport_params_t; @@ -165,5 +182,7 @@ xqc_int_t xqc_read_transport_params(char *tp_data, size_t tp_data_len, ssize_t xqc_write_transport_params(char *tp_buf, size_t cap, const xqc_transport_params_t *params); +void xqc_init_transport_params(xqc_transport_params_t *params); + #endif /* XQC_TRANSPORT_PARAMS_H_ */ \ No newline at end of file diff --git a/src/transport/xqc_utils.c b/src/transport/xqc_utils.c index 490a92690..b1ae87e1b 100644 --- a/src/transport/xqc_utils.c +++ b/src/transport/xqc_utils.c @@ -36,14 +36,15 @@ xqc_conns_pq_top(xqc_pq_t *pq) } int -xqc_insert_conns_hash(xqc_str_hash_table_t *conns_hash, xqc_connection_t *conn, xqc_cid_t *cid) +xqc_insert_conns_hash(xqc_str_hash_table_t *conns_hash, xqc_connection_t *conn, + const uint8_t *data, size_t len) { - uint64_t hash = xqc_hash_string(cid->cid_buf, cid->cid_len); + uint64_t hash = xqc_hash_string(data, len); xqc_str_hash_element_t c = { .str = { - .data = cid->cid_buf, - .len = cid->cid_len + .data = (unsigned char *)data, + .len = len }, .hash = hash, .value = conn @@ -56,12 +57,13 @@ xqc_insert_conns_hash(xqc_str_hash_table_t *conns_hash, xqc_connection_t *conn, } int -xqc_remove_conns_hash(xqc_str_hash_table_t *conns_hash, xqc_connection_t *conn, xqc_cid_t *cid) +xqc_remove_conns_hash(xqc_str_hash_table_t *conns_hash, xqc_connection_t *conn, + const uint8_t *data, size_t len) { - uint64_t hash = xqc_hash_string(cid->cid_buf, cid->cid_len); + uint64_t hash = xqc_hash_string(data, len); xqc_str_t str = { - .data = cid->cid_buf, - .len = cid->cid_len, + .data = (unsigned char *)data, + .len = len, }; if (xqc_str_hash_delete(conns_hash, hash, str)) { @@ -93,12 +95,13 @@ xqc_insert_conns_addr_hash(xqc_str_hash_table_t *conns_hash, xqc_connection_t *c void * -xqc_find_conns_hash(xqc_str_hash_table_t *conns_hash, xqc_connection_t *conn, xqc_cid_t *cid) +xqc_find_conns_hash(xqc_str_hash_table_t *conns_hash, xqc_connection_t *conn, + const uint8_t *data, size_t len) { - uint64_t hash = xqc_hash_string(cid->cid_buf, cid->cid_len); + uint64_t hash = xqc_hash_string(data, len); xqc_str_t str = { - .data = cid->cid_buf, - .len = cid->cid_len, + .data = (unsigned char *)data, + .len = len, }; return xqc_str_hash_find(conns_hash, hash, str); diff --git a/src/transport/xqc_utils.h b/src/transport/xqc_utils.h index b4e947c6e..0a4be3770 100644 --- a/src/transport/xqc_utils.h +++ b/src/transport/xqc_utils.h @@ -19,10 +19,13 @@ void xqc_conns_pq_pop(xqc_pq_t *pq); xqc_conns_pq_elem_t *xqc_conns_pq_top(xqc_pq_t *pq); -int xqc_insert_conns_hash(xqc_str_hash_table_t *conns_hash, xqc_connection_t *conn, xqc_cid_t *cid); +int xqc_insert_conns_hash(xqc_str_hash_table_t *conns_hash, + xqc_connection_t *conn, const uint8_t *data, size_t len); -int xqc_remove_conns_hash(xqc_str_hash_table_t *conns_hash, xqc_connection_t *conn, xqc_cid_t *cid); +int xqc_remove_conns_hash(xqc_str_hash_table_t *conns_hash, + xqc_connection_t *conn, const uint8_t *data, size_t len); -void *xqc_find_conns_hash(xqc_str_hash_table_t *conns_hash, xqc_connection_t *conn, xqc_cid_t *cid); +void *xqc_find_conns_hash(xqc_str_hash_table_t *conns_hash, + xqc_connection_t *conn, const uint8_t *data, size_t len); #endif /* _XQC_UTILS_H_INCLUDED_ */ diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index e72200eeb..80673e074 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -37,8 +37,9 @@ if(HAVE_CUNIT) ${UNIT_TEST_DIR}/xqc_prefixed_str_test.c ${UNIT_TEST_DIR}/xqc_cid_test.c ${UNIT_TEST_DIR}/xqc_id_hash_test.c - ${UNIT_TEST_DIR}/xqc_sent_record_test.c ${UNIT_TEST_DIR}/xqc_retry_test.c + ${UNIT_TEST_DIR}/xqc_datagram_test.c + ${UNIT_TEST_DIR}/xqc_h3_ext_test.c ) diff --git a/tests/test_client.c b/tests/test_client.c index 920e149fd..536e04083 100644 --- a/tests/test_client.c +++ b/tests/test_client.c @@ -20,6 +20,7 @@ #include #include #include +#include int printf_null(const char *format, ...) @@ -55,6 +56,19 @@ printf_null(const char *format, ...) typedef struct user_conn_s user_conn_t; + +#define XQC_TEST_DGRAM_BATCH_SZ 32 + +typedef struct user_datagram_block_s { + unsigned char *recv_data; + unsigned char *data; + size_t data_len; + size_t data_sent; + size_t data_recv; + size_t data_lost; + size_t dgram_lost; + uint32_t dgram_id; +} user_dgram_blk_t; typedef struct client_ctx_s { xqc_engine_t *engine; struct event *ev_engine; @@ -67,26 +81,33 @@ typedef struct client_ctx_s { } client_ctx_t; typedef struct user_stream_s { - xqc_stream_t *stream; - xqc_h3_request_t *h3_request; - user_conn_t *user_conn; - uint64_t send_offset; - int header_sent; - int header_recvd; - char *send_body; - size_t send_body_len; - size_t send_body_max; - char *recv_body; - size_t recv_body_len; - FILE *recv_body_fp; - int recv_fin; - xqc_msec_t start_time; - xqc_msec_t first_frame_time; /* first frame download time */ - xqc_msec_t last_read_time; - int abnormal_count; - int body_read_notify_cnt; - xqc_msec_t last_recv_log_time; - uint64_t recv_log_bytes; + xqc_stream_t *stream; + xqc_h3_request_t *h3_request; + user_conn_t *user_conn; + uint64_t send_offset; + int header_sent; + int header_recvd; + char *send_body; + size_t send_body_len; + size_t send_body_max; + char *recv_body; + size_t recv_body_len; + FILE *recv_body_fp; + int recv_fin; + xqc_msec_t start_time; + xqc_msec_t first_frame_time; /* first frame download time */ + xqc_msec_t last_read_time; + int abnormal_count; + int body_read_notify_cnt; + xqc_msec_t last_recv_log_time; + uint64_t recv_log_bytes; + + xqc_h3_ext_bytestream_t *h3_ext_bs; + struct event *ev_bytestream_timer; + + int snd_times; + int rcv_times; + } user_stream_t; typedef struct user_conn_s { @@ -111,9 +132,23 @@ typedef struct user_conn_s { struct event *ev_path; struct event *ev_epoch; + struct event *ev_request; + + int h3; + + user_dgram_blk_t *dgram_blk; + size_t dgram_mss; + uint8_t dgram_not_supported; + int dgram_retry_in_hs_cb; + + xqc_connection_t *quic_conn; + xqc_h3_conn_t *h3_conn; client_ctx_t *ctx; - int cur_stream_num; + int cur_stream_num; + + uint64_t black_hole_start_time; + int tracked_pkt_cnt; } user_conn_t; #define XQC_DEMO_INTERFACE_MAX_LEN 64 @@ -154,11 +189,21 @@ int g_conn_num = 100; int g_process_num = 2; int g_test_qch_mode = 0; int g_random_cid = 0; +xqc_data_qos_level_t g_dgram_qos_level; xqc_conn_settings_t *g_conn_settings; +unsigned char *sock_op_buffer[2000]; +size_t sock_op_buffer_len = 0; +size_t dgram1_size = 0; +size_t dgram2_size = 0; + +int dgram_drop_pkt1 = 0; client_ctx_t ctx; struct event_base *eb; +int g_send_dgram; +int g_max_dgram_size; int g_req_cnt; +int g_bytestream_cnt; int g_req_max; int g_send_body_size; int g_send_body_size_defined; @@ -166,6 +211,7 @@ int g_send_body_size_from_cdf; cdf_entry_t *cdf_list; int cdf_list_size; int g_req_paral = 1; +int g_recovery = 0; int g_save_body; int g_read_body; int g_echo_check; @@ -175,13 +221,17 @@ int g_is_get; uint64_t g_last_sock_op_time; //currently, the maximum used test case id is 19 //please keep this comment updated if you are adding more test cases. :-D +//99 for pure fin +//2XX for datagram testcases +//3XX for h3 ext bytestream testcases +//4XX for conn_settings configuration int g_test_case; int g_ipv6; int g_no_crypt; int g_conn_timeout = 1; int g_conn_abs_timeout = 0; -int g_path_timeout = 700000; /* 300ms */ -int g_epoch_timeout = 200000; /* us */ +int g_path_timeout = 5000000; /* 5s */ +int g_epoch_timeout = 1000000; /* us */ char g_write_file[256]; char g_read_file[256]; char g_log_path[256]; @@ -193,6 +243,7 @@ char g_headers[MAX_HEADER][256]; int g_header_cnt = 0; int g_ping_id = 1; int g_enable_multipath = 0; +xqc_multipath_version_t g_multipath_version = XQC_MULTIPATH_04; int g_enable_reinjection = 0; int g_verify_cert = 0; int g_verify_cert_allow_self_sign = 0; @@ -201,6 +252,10 @@ int g_epoch = 0; int g_cur_epoch = 0; int g_mp_backup_mode = 0; int g_mp_request_accelerate = 0; +double g_copa_ai = 1.0; +double g_copa_delta = 0.05; +int g_pmtud_on = 0; +int g_mp_ping_on = 0; char g_header_key[MAX_HEADER_KEY_LEN]; char g_header_value[MAX_HEADER_VALUE_LEN]; @@ -220,18 +275,10 @@ char test_long_value[XQC_TEST_LONG_HEADER_LEN] = {'\0'}; int hsk_completed = 0; -static uint64_t last_recv_ts = 0; - -static void xqc_client_socket_event_callback(int fd, short what, void *arg); -static void xqc_client_timeout_callback(int fd, short what, void *arg); -static void xqc_client_abs_timeout_callback(int, short, void*); - -/* 用于路径增删debug */ -static void xqc_client_path_callback(int fd, short what, void *arg); -static void xqc_client_epoch_callback(int fd, short what, void *arg); +int g_periodically_request = 0; +static uint64_t last_recv_ts = 0; -static void xqc_client_timeout_multi_process_callback(int fd, short what, void *arg); /* CDF file format: N (N lines) @@ -312,6 +359,391 @@ now() } +static void xqc_client_socket_event_callback(int fd, short what, void *arg); +static void xqc_client_timeout_callback(int fd, short what, void *arg); +static void xqc_client_abs_timeout_callback(int, short, void*); +static void xqc_client_bytestream_timeout_callback(int, short, void*); + +/* 用于路径增删debug */ +static void xqc_client_path_callback(int fd, short what, void *arg); +static void xqc_client_epoch_callback(int fd, short what, void *arg); + +/* */ + +static void +xqc_client_datagram_send(user_conn_t *user_conn) +{ + if (user_conn->dgram_not_supported) { + // exit + printf("[dgram]|peer_does_not_support_datagram|\n"); + xqc_conn_close(ctx.engine, &user_conn->cid); + return; + } + + // try to send 0rtt datagram while the client does not have 0rtt transport parameters + if (g_test_case == 202) { + if (user_conn->dgram_mss == 0) { + user_conn->dgram_mss = 1000; + } + } + + if (user_conn->dgram_mss == 0) { + user_conn->dgram_retry_in_hs_cb = 1; + printf("[dgram]|waiting_for_max_datagram_frame_size_from_peer|please_retry_in_hs_callback|\n"); + return; + } + + user_dgram_blk_t *dgram_blk = user_conn->dgram_blk; + int ret; + + if (g_send_dgram == 1) { + if (g_test_case == 203 && user_conn->dgram_mss) { + g_test_case = -1; + user_conn->dgram_mss++; + } + uint64_t dgram_id; + while (dgram_blk->data_sent < dgram_blk->data_len) { + size_t dgram_size = dgram_blk->data_len - dgram_blk->data_sent; + if (dgram_size > user_conn->dgram_mss) { + dgram_size = user_conn->dgram_mss; + } + dgram_blk->data[dgram_blk->data_sent] = 0x31; + ret = xqc_datagram_send(user_conn->quic_conn, dgram_blk->data + dgram_blk->data_sent, dgram_size, &dgram_id, g_dgram_qos_level); + if (ret == -XQC_EAGAIN) { + printf("[dgram]|retry_datagram_send_later|\n"); + return; + } else if (ret == -XQC_EDGRAM_TOO_LARGE ) { + printf("[dgram]|trying_to_send_an_oversized_datagram|recorded_mss:%zu|send_size:%zu|current_mss:%zu|\n", user_conn->dgram_mss, dgram_size, xqc_datagram_get_mss(user_conn->quic_conn)); + xqc_conn_close(ctx.engine, &user_conn->cid); + return; + } else if (ret < 0) { + printf("[dgram]|send_datagram_error|err_code:%d|\n", ret); + xqc_conn_close(ctx.engine, &user_conn->cid); + return; + } + // printf("[dgram]|send_one_datagram|id:%"PRIu64"|size:%zu|\n", dgram_id, dgram_size); + dgram_blk->data_sent += dgram_size; + + if (g_epoch) { + break; + } + } + } else if (g_send_dgram == 2) { + struct iovec iov[XQC_TEST_DGRAM_BATCH_SZ]; + uint64_t dgram_id_list[XQC_TEST_DGRAM_BATCH_SZ]; + size_t bytes_in_batch = 0; + int batch_cnt = 0; + while ((dgram_blk->data_sent + bytes_in_batch) < dgram_blk->data_len) { + if (batch_cnt == 1) { + if (g_test_case == 203 && user_conn->dgram_mss) { + g_test_case = -1; + user_conn->dgram_mss++; + } + } + size_t dgram_size = dgram_blk->data_len - dgram_blk->data_sent - bytes_in_batch; + size_t succ_sent = 0, succ_sent_bytes = 0; + if (dgram_size > user_conn->dgram_mss) { + dgram_size = user_conn->dgram_mss; + } + iov[batch_cnt].iov_base = dgram_blk->data + dgram_blk->data_sent + bytes_in_batch; + iov[batch_cnt].iov_len = dgram_size; + dgram_blk->data[dgram_blk->data_sent + bytes_in_batch] = 0x31; + bytes_in_batch += dgram_size; + batch_cnt++; + if ((bytes_in_batch + dgram_blk->data_sent) == dgram_blk->data_len + || batch_cnt == XQC_TEST_DGRAM_BATCH_SZ) + { + ret = xqc_datagram_send_multiple(user_conn->quic_conn, iov, dgram_id_list, batch_cnt, &succ_sent, &succ_sent_bytes, g_dgram_qos_level); + if (ret == -XQC_EDGRAM_TOO_LARGE) { + printf("[dgram]|trying_to_send_an_oversized_datagram|recorded_mss:%zu|send_size:%zu|current_mss:%zu|\n", user_conn->dgram_mss, iov[succ_sent].iov_len, xqc_datagram_get_mss(user_conn->quic_conn)); + printf("[dgram]|partially_sent_pkts_in_a_batch|cnt:%zu|\n", succ_sent); + xqc_conn_close(ctx.engine, &user_conn->cid); + return; + } else if (ret < 0 && ret != -XQC_EAGAIN) { + printf("[dgram]|send_datagram_multiple_error|err_code:%d|\n", ret); + xqc_conn_close(ctx.engine, &user_conn->cid); + return; + } + + // for (int i = 0; i < succ_sent; i++) { + // printf("[dgram]|send_one_datagram|id:%"PRIu64"|size:%zu|\n", dgram_id_list[i], iov[i].iov_len); + // } + + // printf("[dgram]|datagrams_sent_in_a_batch|cnt:%zu|size:%zu|\n", succ_sent, succ_sent_bytes); + + dgram_blk->data_sent += succ_sent_bytes; + + if (ret == -XQC_EAGAIN) { + printf("[dgram]|retry_datagram_send_multiple_later|\n"); + return; + } + + bytes_in_batch = 0; + batch_cnt = 0; + } + } + + } +} + +static void +xqc_client_datagram_mss_updated_callback(xqc_connection_t *conn, size_t mss, void *user_data) +{ + user_conn_t *user_conn = (user_conn_t*)user_data; + user_conn->dgram_mss = mss; + printf("[dgram]|mss_callback|updated_mss:%zu|\n", mss); +} + +static void +xqc_client_datagram_read_callback(xqc_connection_t *conn, void *user_data, const void *data, size_t data_len, uint64_t dgram_ts) +{ + user_conn_t *user_conn = (user_conn_t*)user_data; + if (g_echo_check) { + memcpy(user_conn->dgram_blk->recv_data + user_conn->dgram_blk->data_recv, data, data_len); + } + user_conn->dgram_blk->data_recv += data_len; + //printf("[dgram]|read_data|size:%zu|recv_time:%"PRIu64"|\n", data_len, dgram_ts); + if (g_test_case == 206 && g_no_crypt) { + if (dgram1_size == 0) { + dgram1_size = data_len; + } else { + if (dgram2_size == 0) { + dgram2_size = data_len; + } + } + } +} + +static void +xqc_client_datagram_write_callback(xqc_connection_t *conn, void *user_data) +{ + user_conn_t *user_conn = (user_conn_t*)user_data; + if (g_send_dgram) { + printf("[dgram]|dgram_write|\n"); + xqc_client_datagram_send(user_conn); + } +} + +static void +xqc_client_datagram_acked_callback(xqc_connection_t *conn, uint64_t dgram_id, void *user_data) +{ + user_conn_t *user_conn = (user_conn_t*)user_data; + if (g_test_case == 207) { + printf("[dgram]|dgram_acked|dgram_id:%"PRIu64"|\n", dgram_id); + g_test_case = -1; + } +} + +static int +xqc_client_datagram_lost_callback(xqc_connection_t *conn, uint64_t dgram_id, void *user_data) +{ + //printf("[dgram]|dgram_lost|dgram_id:%"PRIu64"|\n", dgram_id); + user_conn_t *user_conn = (user_conn_t*)user_data; + //user_conn->dgram_blk->data_lost += data_len; + user_conn->dgram_blk->dgram_lost++; + if (g_test_case == 205 && g_no_crypt) { + printf("[dgram]|dgram_lost|dgram_id:%"PRIu64"|\n", dgram_id); + } + return 0; +} + + +static void +xqc_client_h3_ext_datagram_send(user_conn_t *user_conn) +{ + if (user_conn->dgram_not_supported) { + // exit + printf("[h3-dgram]|peer_does_not_support_datagram|\n"); + // xqc_h3_conn_close(ctx.engine, &user_conn->cid); + return; + } + + // try to send 0rtt datagram while the client does not have 0rtt transport parameters + if (g_test_case == 202) { + if (user_conn->dgram_mss == 0) { + user_conn->dgram_mss = 1000; + } + } + + if (user_conn->dgram_mss == 0) { + user_conn->dgram_retry_in_hs_cb = 1; + printf("[h3-dgram]|waiting_for_max_datagram_frame_size_from_peer|please_retry_in_hs_callback|\n"); + return; + } + + user_dgram_blk_t *dgram_blk = user_conn->dgram_blk; + int ret; + + if (g_send_dgram == 1) { + if (g_test_case == 203 && user_conn->dgram_mss) { + g_test_case = -1; + user_conn->dgram_mss++; + } + uint64_t dgram_id; + while (dgram_blk->data_sent < dgram_blk->data_len) { + size_t dgram_size = dgram_blk->data_len - dgram_blk->data_sent; + if (dgram_size > user_conn->dgram_mss) { + dgram_size = user_conn->dgram_mss; + } + + if (g_epoch) { + if (dgram_blk->data_len - dgram_blk->data_sent < 13) { + dgram_blk->data_sent = dgram_blk->data_len; + break; + } + dgram_blk->data[dgram_blk->data_sent] = 0x32; + *(uint32_t*)(dgram_blk->data + dgram_blk->data_sent + 1) = dgram_blk->dgram_id++; + *(uint64_t*)(dgram_blk->data + dgram_blk->data_sent + 5) = now(); + + } else { + dgram_blk->data[dgram_blk->data_sent] = 0x31; + } + + ret = xqc_h3_ext_datagram_send(user_conn->h3_conn, dgram_blk->data + dgram_blk->data_sent, dgram_size, &dgram_id, g_dgram_qos_level); + if (ret == -XQC_EAGAIN) { + printf("[h3-dgram]|retry_datagram_send_later|\n"); + return; + } else if (ret == -XQC_EDGRAM_TOO_LARGE ) { + printf("[h3-dgram]|trying_to_send_an_oversized_datagram|recorded_mss:%zu|send_size:%zu|current_mss:%zu|\n", user_conn->dgram_mss, dgram_size, xqc_h3_ext_datagram_get_mss(user_conn->h3_conn)); + xqc_h3_conn_close(ctx.engine, &user_conn->cid); + return; + } else if (ret < 0) { + printf("[h3-dgram]|send_datagram_error|err_code:%d|\n", ret); + xqc_h3_conn_close(ctx.engine, &user_conn->cid); + return; + } + //printf("[dgram]|send_one_datagram|id:%"PRIu64"|size:%zu|\n", dgram_id, dgram_size); + dgram_blk->data_sent += dgram_size; + + if (g_epoch) { + break; + } + } + } else if (g_send_dgram == 2) { + struct iovec iov[XQC_TEST_DGRAM_BATCH_SZ]; + uint64_t dgram_id_list[XQC_TEST_DGRAM_BATCH_SZ]; + size_t bytes_in_batch = 0; + int batch_cnt = 0; + while ((dgram_blk->data_sent + bytes_in_batch) < dgram_blk->data_len) { + if (batch_cnt == 1) { + if (g_test_case == 203 && user_conn->dgram_mss) { + g_test_case = -1; + user_conn->dgram_mss++; + } + } + size_t dgram_size = dgram_blk->data_len - dgram_blk->data_sent - bytes_in_batch; + size_t succ_sent = 0, succ_sent_bytes = 0; + if (dgram_size > user_conn->dgram_mss) { + dgram_size = user_conn->dgram_mss; + } + iov[batch_cnt].iov_base = dgram_blk->data + dgram_blk->data_sent + bytes_in_batch; + iov[batch_cnt].iov_len = dgram_size; + dgram_blk->data[dgram_blk->data_sent + bytes_in_batch] = 0x31; + bytes_in_batch += dgram_size; + batch_cnt++; + if ((bytes_in_batch + dgram_blk->data_sent) == dgram_blk->data_len + || batch_cnt == XQC_TEST_DGRAM_BATCH_SZ) + { + ret = xqc_h3_ext_datagram_send_multiple(user_conn->h3_conn, iov, dgram_id_list, batch_cnt, &succ_sent, &succ_sent_bytes, g_dgram_qos_level); + if (ret == -XQC_EDGRAM_TOO_LARGE) { + printf("[h3-dgram]|trying_to_send_an_oversized_datagram|recorded_mss:%zu|send_size:%zu|current_mss:%zu|\n", user_conn->dgram_mss, iov[succ_sent].iov_len, xqc_h3_ext_datagram_get_mss(user_conn->h3_conn)); + printf("[h3-dgram]|partially_sent_pkts_in_a_batch|cnt:%zu|\n", succ_sent); + xqc_h3_conn_close(ctx.engine, &user_conn->cid); + return; + } else if (ret < 0 && ret != -XQC_EAGAIN) { + printf("[h3-dgram]|send_datagram_multiple_error|err_code:%d|\n", ret); + xqc_h3_conn_close(ctx.engine, &user_conn->cid); + return; + } + + // for (int i = 0; i < succ_sent; i++) { + // printf("[dgram]|send_one_datagram|id:%"PRIu64"|size:%zu|\n", dgram_id_list[i], iov[i].iov_len); + // } + + // printf("[dgram]|datagrams_sent_in_a_batch|cnt:%zu|size:%zu|\n", succ_sent, succ_sent_bytes); + + dgram_blk->data_sent += succ_sent_bytes; + + if (ret == -XQC_EAGAIN) { + printf("[h3-dgram]|retry_datagram_send_multiple_later|\n"); + return; + } + + bytes_in_batch = 0; + batch_cnt = 0; + } + } + + } +} + + +static void +xqc_client_h3_ext_datagram_mss_updated_callback(xqc_h3_conn_t *conn, size_t mss, void *user_data) +{ + user_conn_t *user_conn = (user_conn_t*)user_data; + user_conn->dgram_mss = mss; + printf("[h3-dgram]|callback|updated_mss:%zu|\n", mss); +} + +static void +xqc_client_h3_ext_datagram_read_callback(xqc_h3_conn_t *conn, const void *data, size_t data_len, void *user_data, uint64_t ts) +{ + user_conn_t *user_conn = (user_conn_t*)user_data; + if (g_echo_check) { + memcpy(user_conn->dgram_blk->recv_data + user_conn->dgram_blk->data_recv, data, data_len); + } + user_conn->dgram_blk->data_recv += data_len; + // printf("[h3-dgram]|read_data|size:%zu|recv_time:%"PRIu64"|\n", data_len, ts); + if (g_test_case == 206 && g_no_crypt) { + if (dgram1_size == 0) { + dgram1_size = data_len; + } else { + if (dgram2_size == 0) { + dgram2_size = data_len; + } + } + } +} + +static void +xqc_client_h3_ext_datagram_write_callback(xqc_h3_conn_t *conn, void *user_data) +{ + user_conn_t *user_conn = (user_conn_t*)user_data; + if (g_send_dgram) { + printf("[h3-dgram]|dgram_write|\n"); + xqc_client_h3_ext_datagram_send(user_conn); + } +} + +static void +xqc_client_h3_ext_datagram_acked_callback(xqc_h3_conn_t *conn, uint64_t dgram_id, void *user_data) +{ + user_conn_t *user_conn = (user_conn_t*)user_data; + if (g_test_case == 207) { + printf("[h3-dgram]|dgram_acked|dgram_id:%"PRIu64"|\n", dgram_id); + g_test_case = -1; + } + // printf("[h3-dgram]|latest_rtt:%"PRIu64"|\n", xqc_conn_get_lastest_rtt(ctx.engine, &user_conn->cid)); +} + +static int +xqc_client_h3_ext_datagram_lost_callback(xqc_h3_conn_t *conn, uint64_t dgram_id, void *user_data) +{ + //printf("[dgram]|dgram_lost|dgram_id:%"PRIu64"|\n", dgram_id); + user_conn_t *user_conn = (user_conn_t*)user_data; + user_conn->dgram_blk->data_lost += 0; + user_conn->dgram_blk->dgram_lost++; + if (g_test_case == 205 && g_no_crypt) { + printf("[h3-dgram]|dgram_lost|dgram_id:%"PRIu64"|\n", dgram_id); + } + return 0; +} + + +static void xqc_client_timeout_multi_process_callback(int fd, short what, void *arg); + + void xqc_client_set_event_timer(xqc_msec_t wake_after, void *user_data) { @@ -441,14 +873,6 @@ xqc_client_write_socket(const unsigned char *buf, size_t size, ssize_t res = 0; int fd = user_conn->fd; - if (g_test_case == 41) { - /* delay short header packet to make server idle timeout */ - if ((buf[0] & 0xC0) == 0x40) { - sleep(2); - g_test_case = -1; - } - } - /* COPY to run corruption test cases */ unsigned char send_buf[XQC_PACKET_TMP_BUF_LEN]; size_t send_buf_size = 0; @@ -502,12 +926,77 @@ xqc_client_write_socket(const unsigned char *buf, size_t size, printf("test case 23, corrupt byte[15]\n"); } + // drop the first datagram packet + if ((g_test_case == 205 || g_test_case == 206) && g_no_crypt && !dgram_drop_pkt1) { + int header_type = send_buf[0] & 0x80; + if (header_type == 0x80) { + // long header: 29B + 3B (frame header) + int lp_type = send_buf[0] & 0x30; + if (lp_type == 0x10) { + //0RTT pkt + if (send_buf[29] == 0x31) { + //datagram frame + if (g_test_case == 206) { + //hold data & swap the order with the next one + //swap 1st & 2nd dgram + memcpy(sock_op_buffer, send_buf, send_buf_size); + sock_op_buffer_len = send_buf_size; + } + dgram_drop_pkt1 = 1; + return send_buf_size; + } + } + } else { + // short header: 13B + 3B (frame header) + if (send_buf[13] == 0x31) { + //datagram frame + if (g_test_case == 206) { + //hold data & swap the order with the next one + //swap 1st & 2nd dgram + memcpy(sock_op_buffer, send_buf, send_buf_size); + sock_op_buffer_len = send_buf_size; + } + dgram_drop_pkt1 = 1; + return send_buf_size; + } + } + } + res = sendto(fd, send_buf, send_buf_size, 0, peer_addr, peer_addrlen); if (res < 0) { printf("xqc_client_write_socket err %zd %s\n", res, strerror(errno)); if (errno == EAGAIN) { res = XQC_SOCKET_EAGAIN; } + if (errno == EMSGSIZE) { + res = send_buf_size; + } + } + + if (sock_op_buffer_len) { + int header_type = send_buf[0] & 0x80; + int frame_type = -1; + if (header_type == 0x80) { + // long header: 29B + 3B (frame header) + int lp_type = send_buf[0] & 0x30; + if (lp_type == 0x10) { + //0RTT pkt + frame_type = send_buf[29]; + } + } else { + frame_type = send_buf[13]; + } + if (frame_type == 0x31) { + int tmp = sendto(fd, sock_op_buffer, sock_op_buffer_len, 0, peer_addr, peer_addrlen); + if (tmp < 0) { + res = tmp; + printf("xqc_client_write_socket err %zd %s\n", res, strerror(errno)); + if (errno == EAGAIN) { + res = XQC_SOCKET_EAGAIN; + } + } + sock_op_buffer_len = 0; + } } } while ((res < 0) && (errno == EINTR)); @@ -544,6 +1033,66 @@ xqc_client_write_socket_ex(uint64_t path_id, ssize_t res; int fd = 0; + + /* test stateless reset after handshake completed */ + if (g_test_case == 41) { + if (hsk_completed && ((buf[0] & 0xC0) == 0x40)) { + if (user_conn->tracked_pkt_cnt < 2) { + /* send 10 pkts first */ + user_conn->tracked_pkt_cnt++; + + } else { + /* delay short header packet to make server idle timeout */ + uint64_t nowtime = now(); + + if (user_conn->black_hole_start_time == 0) { + user_conn->black_hole_start_time = nowtime; + printf(" blackhole start: %"PRIu64"\n", nowtime); + } + + if (nowtime - user_conn->black_hole_start_time <= 3000000) { + return size; + } + + /* keep blackhole more than 2 seconds, turn off */ + printf(" blackhole end: %"PRIu64"\n", nowtime); + g_test_case = -1; + } + } + } + + /* test stateless rset during handshake */ + if (g_test_case == 45) { + uint64_t nowtime = now(); + if (user_conn->black_hole_start_time == 0) { + + if ((((buf[0] & 0x80) == 0x80) && ((buf[0] & 0x30) >> 4) == 2) + || ((buf[0] & 0xC0) == 0x40)) + { + if (user_conn->black_hole_start_time == 0) { + user_conn->black_hole_start_time = nowtime; + printf(" blackhole start: %"PRIu64"\n", nowtime); + } + + printf("... block:%zu\n", size); + return size; + } + + } else { + /* drop all client's packet */ + + if (nowtime - user_conn->black_hole_start_time <= 10000000) { + printf("... block:%zu\n", size); + return size; + } + + /* keep blackhole more than 2 seconds, turn off */ + printf(" blackhole end: %"PRIu64"\n", nowtime); + g_test_case = -1; + + } + } + /* get path fd */ fd = xqc_client_get_path_fd_by_id(user_conn, path_id); @@ -578,7 +1127,7 @@ xqc_client_write_socket_ex(uint64_t path_id, if (g_test_case == 103 && path_id == 0 && g_client_path[0].send_size > g_send_body_size/10) { fd = g_client_path[0].rebinding_path_fd; } - else if (g_test_case == 104 && path_id == 1 && g_client_path[1].send_size > g_send_body_size/10) { + else if (g_test_case == 104 && path_id == 1 && g_client_path[1].send_size > 10240) { fd = g_client_path[1].rebinding_path_fd; } } @@ -613,15 +1162,81 @@ xqc_client_write_socket_ex(uint64_t path_id, printf("test case 23, corrupt byte[15]\n"); } + // drop the first datagram packet + if ((g_test_case == 205 || g_test_case == 206) && g_no_crypt && !dgram_drop_pkt1) { + int header_type = send_buf[0] & 0x80; + if (header_type == 0x80) { + // long header: 29B + 3B (frame header) + int lp_type = send_buf[0] & 0x30; + if (lp_type == 0x10) { + //0RTT pkt + if (send_buf[29] == 0x31) { + //datagram frame + if (g_test_case == 206) { + //hold data & swap the order with the next one + //swap 1st & 2nd dgram + memcpy(sock_op_buffer, send_buf, send_buf_size); + sock_op_buffer_len = send_buf_size; + } + dgram_drop_pkt1 = 1; + return send_buf_size; + } + } + } else { + // short header: 13B + 3B (frame header) + if (send_buf[13] == 0x31) { + //datagram frame + if (g_test_case == 206) { + //hold data & swap the order with the next one + //swap 1st & 2nd dgram + memcpy(sock_op_buffer, send_buf, send_buf_size); + sock_op_buffer_len = send_buf_size; + } + dgram_drop_pkt1 = 1; + return send_buf_size; + } + } + } + res = sendto(fd, send_buf, send_buf_size, 0, peer_addr, peer_addrlen); if (res < 0) { - printf("xqc_client_write_socket_ex path:%lu err %zd %s\n", path_id, res, strerror(errno)); + printf("xqc_client_write_socket_ex path:%"PRIu64" err %zd %s %zu\n", path_id, res, strerror(errno), send_buf_size); if (errno == EAGAIN) { res = XQC_SOCKET_EAGAIN; } else { res = XQC_SOCKET_ERROR; } + if (errno == EMSGSIZE) { + res = send_buf_size; + } + } + + if (sock_op_buffer_len) { + int header_type = send_buf[0] & 0x80; + int frame_type = -1; + if (header_type == 0x80) { + // long header: 29B + 3B (frame header) + int lp_type = send_buf[0] & 0x30; + if (lp_type == 0x10) { + //0RTT pkt + frame_type = send_buf[29]; + } + } else { + frame_type = send_buf[13]; + } + if (frame_type == 0x31) { + int tmp = sendto(fd, sock_op_buffer, sock_op_buffer_len, 0, peer_addr, peer_addrlen); + if (tmp < 0) { + res = tmp; + printf("xqc_client_write_socket err %zd %s\n", res, strerror(errno)); + if (errno == EAGAIN) { + res = XQC_SOCKET_EAGAIN; + } + } + sock_op_buffer_len = 0; + } } + } while ((res < 0) && (errno == EINTR)); return res; @@ -734,6 +1349,7 @@ xqc_client_bind_to_interface(int fd, memset(&ifr, 0x00, sizeof(ifr)); strncpy(ifr.ifr_name, interface_name, sizeof(ifr.ifr_name) - 1); +#if !defined(__APPLE__) // #if (XQC_TEST_MP) printf("fd: %d. bind to nic: %s\n", fd, interface_name); if (setsockopt(fd, SOL_SOCKET, SO_BINDTODEVICE, (char *)&ifr, sizeof(ifr)) < 0) { @@ -741,6 +1357,7 @@ xqc_client_bind_to_interface(int fd, return XQC_ERROR; } // #endif +#endif return XQC_OK; } @@ -775,6 +1392,11 @@ xqc_client_create_socket(int type, goto err; } +#if !defined(__APPLE__) + int val = IP_PMTUDISC_DO; + setsockopt(fd, IPPROTO_IP, IP_MTU_DISCOVER, &val, sizeof(val)); +#endif + g_last_sock_op_time = now(); if (interface != NULL @@ -907,7 +1529,7 @@ xqc_client_user_conn_multi_process_create(client_ctx_t *ctx, const char *server_ { user_conn_t *user_conn = calloc(1, sizeof(user_conn_t)); /* use HTTP3? */ - user_conn->h3 = transport ? 0 : 1; + user_conn->h3 = transport; user_conn->ctx = ctx; user_conn->ev_timeout = event_new(ctx->eb, -1, 0, xqc_client_timeout_multi_process_callback, user_conn); @@ -941,7 +1563,7 @@ xqc_client_user_conn_create(const char *server_addr, int server_port, user_conn_t *user_conn = calloc(1, sizeof(user_conn_t)); /* use HTTP3? */ - user_conn->h3 = transport ? 0 : 1; + user_conn->h3 = transport; user_conn->ev_timeout = event_new(eb, -1, 0, xqc_client_timeout_callback, user_conn); /* set connection timeout */ @@ -965,6 +1587,7 @@ xqc_client_user_conn_create(const char *server_addr, int server_port, event_add(user_conn->ev_abs_timeout, &tv); } + user_conn->conn_create_time = now(); int ip_type = (g_ipv6 ? AF_INET6 : AF_INET); @@ -994,14 +1617,14 @@ xqc_client_create_conn_socket(user_conn_t *user_conn) void xqc_client_set_path_debug_timer(user_conn_t *user_conn) { - if (g_debug_path) { + if (g_debug_path || g_test_case == 110) { if (user_conn->ev_path == NULL) { user_conn->ev_path = event_new(eb, -1, 0, xqc_client_path_callback, user_conn); } struct timeval tv; - tv.tv_sec = 0; - tv.tv_usec = g_path_timeout; + tv.tv_sec = g_path_timeout / 1000000; + tv.tv_usec = g_path_timeout % 1000000; event_add(user_conn->ev_path, &tv); } } @@ -1039,6 +1662,13 @@ xqc_client_conn_create_notify(xqc_connection_t *conn, const xqc_cid_t *cid, void user_conn_t *user_conn = (user_conn_t *)user_data; xqc_conn_set_alp_user_data(conn, user_conn); + user_conn->dgram_mss = xqc_datagram_get_mss(conn); + user_conn->quic_conn = conn; + + if (g_test_case == 200 || g_test_case == 201) { + printf("[dgram-200]|0RTT|initial_mss:%zu|\n", user_conn->dgram_mss); + } + printf("xqc_conn_is_ready_to_send_early_data:%d\n", xqc_conn_is_ready_to_send_early_data(conn)); return 0; } @@ -1048,7 +1678,7 @@ xqc_client_conn_close_notify(xqc_connection_t *conn, const xqc_cid_t *cid, void { DEBUG; - user_conn_t *user_conn = (user_conn_t *)conn_proto_data; + user_conn_t *user_conn = (user_conn_t *)user_data; client_ctx_t *p_ctx; if (g_test_qch_mode) { @@ -1057,12 +1687,22 @@ xqc_client_conn_close_notify(xqc_connection_t *conn, const xqc_cid_t *cid, void p_ctx = &ctx; } + xqc_int_t err = xqc_conn_get_errno(conn); + printf("should_clear_0rtt_ticket, conn_err:%d, clear_0rtt_ticket:%d\n", err, xqc_conn_should_clear_0rtt_ticket(err)); + xqc_conn_stats_t stats = xqc_conn_get_stats(p_ctx->engine, cid); - printf("send_count:%u, lost_count:%u, tlp_count:%u, recv_count:%u, srtt:%"PRIu64" early_data_flag:%d, conn_err:%d, mp_state:%d, ack_info:%s\n", - stats.send_count, stats.lost_count, stats.tlp_count, stats.recv_count, stats.srtt, stats.early_data_flag, stats.conn_err, stats.mp_state, stats.ack_info); + printf("send_count:%u, lost_count:%u, tlp_count:%u, recv_count:%u, srtt:%"PRIu64" early_data_flag:%d, conn_err:%d, mp_state:%d, ack_info:%s, alpn:%s\n", + stats.send_count, stats.lost_count, stats.tlp_count, stats.recv_count, stats.srtt, stats.early_data_flag, stats.conn_err, stats.mp_state, stats.ack_info, stats.alpn); printf("conn_info: \"%s\"\n", stats.conn_info); + if (!g_test_qch_mode) { + printf("[dgram]|recv_dgram_bytes:%zu|sent_dgram_bytes:%zu|lost_dgram_bytes:%zu|lost_cnt:%zu|\n", + user_conn->dgram_blk->data_recv, user_conn->dgram_blk->data_sent, + user_conn->dgram_blk->data_lost, user_conn->dgram_blk->dgram_lost); + } + + if (g_test_qch_mode) { if (p_ctx->cur_conn_num == 0) { event_base_loopbreak(p_ctx->eb); @@ -1106,14 +1746,32 @@ xqc_client_conn_handshake_finished(xqc_connection_t *conn, void *user_data, void DEBUG; user_conn_t *user_conn = (user_conn_t *) user_data; if (!g_test_qch_mode) { - xqc_conn_send_ping(ctx.engine, &user_conn->cid, NULL); - xqc_conn_send_ping(ctx.engine, &user_conn->cid, &g_ping_id); + if (!g_mp_ping_on) { + xqc_conn_send_ping(ctx.engine, &user_conn->cid, NULL); + xqc_conn_send_ping(ctx.engine, &user_conn->cid, &g_ping_id); + } printf("====>DCID:%s\n", xqc_dcid_str_by_scid(ctx.engine, &user_conn->cid)); printf("====>SCID:%s\n", xqc_scid_str(&user_conn->cid)); } hsk_completed = 1; + + user_conn->dgram_mss = xqc_datagram_get_mss(conn); + if (user_conn->dgram_mss == 0) { + user_conn->dgram_not_supported = 1; + if (g_test_case == 204) { + user_conn->dgram_not_supported = 0; + user_conn->dgram_mss = 1000; + } + } + if (g_test_case == 200 || g_test_case == 201) { + printf("[dgram-200]|1RTT|updated_mss:%zu|\n", user_conn->dgram_mss); + } + + if (g_send_dgram && user_conn->dgram_retry_in_hs_cb) { + xqc_client_datagram_send(user_conn); + } } int @@ -1149,6 +1807,13 @@ xqc_client_h3_conn_create_notify(xqc_h3_conn_t *conn, const xqc_cid_t *cid, void xqc_h3_conn_set_settings(conn, &settings); } + user_conn->dgram_mss = xqc_h3_ext_datagram_get_mss(conn); + user_conn->h3_conn = conn; + + if (g_test_case == 200 || g_test_case == 201) { + printf("[h3-dgram-200]|0RTT|initial_mss:%zu|\n", user_conn->dgram_mss); + } + printf("xqc_h3_conn_is_ready_to_send_early_data:%d\n", xqc_h3_conn_is_ready_to_send_early_data(conn)); return 0; } @@ -1169,9 +1834,19 @@ xqc_client_h3_conn_close_notify(xqc_h3_conn_t *conn, const xqc_cid_t *cid, void p_ctx = &ctx; } + xqc_int_t err = xqc_h3_conn_get_errno(conn); + printf("should_clear_0rtt_ticket, conn_err:%d, clear_0rtt_ticket:%d\n", err, xqc_conn_should_clear_0rtt_ticket(err)); + xqc_conn_stats_t stats = xqc_conn_get_stats(p_ctx->engine, cid); - printf("send_count:%u, lost_count:%u, tlp_count:%u, recv_count:%u, srtt:%"PRIu64" early_data_flag:%d, conn_err:%d, mp_state:%d, ack_info:%s, conn_info:%s\n", - stats.send_count, stats.lost_count, stats.tlp_count, stats.recv_count, stats.srtt, stats.early_data_flag, stats.conn_err, stats.mp_state, stats.ack_info, stats.conn_info); + printf("send_count:%u, lost_count:%u, tlp_count:%u, recv_count:%u, srtt:%"PRIu64" early_data_flag:%d, conn_err:%d, mp_state:%d, ack_info:%s, alpn:%s, conn_info:%s\n", + stats.send_count, stats.lost_count, stats.tlp_count, stats.recv_count, stats.srtt, stats.early_data_flag, stats.conn_err, stats.mp_state, stats.ack_info, stats.alpn, stats.conn_info); + + if (!g_test_qch_mode) { + printf("[h3-dgram]|recv_dgram_bytes:%zu|sent_dgram_bytes:%zu|lost_dgram_bytes:%zu|lost_cnt:%zu|\n", + user_conn->dgram_blk->data_recv, user_conn->dgram_blk->data_sent, + user_conn->dgram_blk->data_lost, user_conn->dgram_blk->dgram_lost); + } + if (g_test_qch_mode) { if (p_ctx->cur_conn_num == 0) { @@ -1195,8 +1870,10 @@ xqc_client_h3_conn_handshake_finished(xqc_h3_conn_t *h3_conn, void *user_data) p_ctx = &ctx; } - xqc_h3_conn_send_ping(p_ctx->engine, &user_conn->cid, NULL); - xqc_h3_conn_send_ping(p_ctx->engine, &user_conn->cid, &g_ping_id); + if (!g_mp_ping_on) { + xqc_h3_conn_send_ping(p_ctx->engine, &user_conn->cid, NULL); + xqc_h3_conn_send_ping(p_ctx->engine, &user_conn->cid, &g_ping_id); + } xqc_conn_stats_t stats = xqc_conn_get_stats(p_ctx->engine, &user_conn->cid); printf("0rtt_flag:%d\n", stats.early_data_flag); @@ -1209,6 +1886,23 @@ xqc_client_h3_conn_handshake_finished(xqc_h3_conn_t *h3_conn, void *user_data) printf("====>SCID:%s\n", xqc_scid_str(&user_conn->cid)); hsk_completed = 1; + + user_conn->dgram_mss = xqc_h3_ext_datagram_get_mss(h3_conn); + if (user_conn->dgram_mss == 0) { + user_conn->dgram_not_supported = 1; + if (g_test_case == 204) { + user_conn->dgram_not_supported = 0; + user_conn->dgram_mss = 1000; + } + } + + if (g_test_case == 200 || g_test_case == 201) { + printf("[h3-dgram-200]|1RTT|updated_mss:%zu|\n", user_conn->dgram_mss); + } + + if (g_send_dgram && user_conn->dgram_retry_in_hs_cb) { + xqc_client_h3_ext_datagram_send(user_conn); + } } void @@ -1244,6 +1938,11 @@ xqc_client_stream_send(xqc_stream_t *stream, void *user_data) static int send_cnt = 0; printf("|xqc_client_stream_send|cnt:%d|\n", ++send_cnt); + if (g_test_case == 99) { + xqc_stream_send(stream, NULL, 0, 1); + return 0; + } + ssize_t ret; user_stream_t *user_stream = (user_stream_t *) user_data; @@ -1476,6 +2175,176 @@ xqc_client_stream_close_notify(xqc_stream_t *stream, void *user_data) return 0; } +int +xqc_client_bytestream_send(xqc_h3_ext_bytestream_t *h3_bs, user_stream_t *user_stream) +{ + if (user_stream->start_time == 0) { + user_stream->start_time = now(); + } + ssize_t ret = 0; + char content_len[10]; + + if (g_test_case == 302 || g_test_case == 310) { + //send pure fin on bytestream + xqc_h3_ext_bytestream_finish(h3_bs); + return 0; + } + + if (g_test_case == 303) { + //send pure fin on bytestream + xqc_h3_ext_bytestream_send(h3_bs, NULL, 0, 1, g_dgram_qos_level); + return 0; + } + + if (user_stream->send_body == NULL) { + user_stream->send_body_max = MAX_BUF_SIZE; + user_stream->send_body_len = g_send_body_size > user_stream->send_body_max ? user_stream->send_body_max : g_send_body_size; + user_stream->send_body = malloc(user_stream->send_body_len); + char *p = user_stream->send_body; + for (int i = 0; i < g_send_body_size; i++) { + *p++ = rand(); + } + + if (user_stream->send_body == NULL) { + printf("send_body malloc error\n"); + return -1; + } + } + + int fin = 1; + if (g_test_case == 304 || g_test_case == 305 + || g_test_case == 311 || g_test_case == 312 + || g_test_case == 313 || g_test_case == 314) { + //do not send fin with data + fin = 0; + // will send fin in a timer + } + + /* send body */ + if (user_stream->send_offset < user_stream->send_body_len) { + ret = xqc_h3_ext_bytestream_send(h3_bs, user_stream->send_body + user_stream->send_offset, user_stream->send_body_len - user_stream->send_offset, fin, g_dgram_qos_level); + + if (ret == -XQC_EAGAIN) { + printf("xqc_h3_ext_bytestream_send eagain %zd\n", ret); + return 0; + + } else if (ret < 0) { + printf("xqc_h3_ext_bytestream_send error %zd\n", ret); + return 0; + + } else { + user_stream->snd_times++; + user_stream->send_offset += ret; + // printf("[bytestream]|send:%"PRIu64"|\n", user_stream->send_offset); + } + } + + return 0; +} + +int xqc_h3_ext_bytestream_create_callback(xqc_h3_ext_bytestream_t *h3_ext_bs, + void *bs_user_data) +{ + user_stream_t *user_stream = (user_stream_t*)bs_user_data; + struct timeval tv; + if (g_test_case == 304 || g_test_case == 305) { + user_stream->ev_bytestream_timer = event_new(eb, -1, 0, xqc_client_bytestream_timeout_callback, user_stream); + tv.tv_sec = 0; + tv.tv_usec = 100000; //100ms + event_add(user_stream->ev_bytestream_timer, &tv); + } + return 0; +} + +int xqc_h3_ext_bytestream_close_callback(xqc_h3_ext_bytestream_t *h3_ext_bs, + void *bs_user_data) +{ + //print stats + xqc_h3_ext_bytestream_stats_t stats = xqc_h3_ext_bytestream_get_stats(h3_ext_bs); + user_stream_t *user_stream = (user_stream_t*)bs_user_data; + + printf("[bytestream]|bytes_sent:%zu|bytes_rcvd:%zu|recv_fin:%d|snd_times:%d|rcv_times:%d|\n", stats.bytes_sent, stats.bytes_rcvd, user_stream->recv_fin, user_stream->snd_times, user_stream->rcv_times); + + //check content + if (g_echo_check) { + if (user_stream->send_body && user_stream->recv_body && !memcmp(user_stream->send_body, user_stream->recv_body, user_stream->send_body_len)) { + printf("[bytestream]|same_content:yes|\n"); + + } else { + printf("[bytestream]|same_content:no|\n"); + } + } + + if (user_stream->send_body) { + free(user_stream->send_body); + } + + if (user_stream->recv_body) { + free(user_stream->recv_body); + } + + if (g_test_case == 304 || g_test_case == 305) { + if (user_stream->ev_bytestream_timer) { + event_free(user_stream->ev_bytestream_timer); + } + } + + free(user_stream); + + return 0; +} + +int xqc_h3_ext_bytestream_read_callback(xqc_h3_ext_bytestream_t *h3_ext_bs, + const void *data, size_t data_len, uint8_t fin, void *bs_user_data, uint64_t data_recv_time) +{ + user_stream_t *user_stream = (user_stream_t*)bs_user_data; + int ret = 0; + + if (user_stream->recv_body == NULL) { + user_stream->recv_body = calloc(1, user_stream->send_body_len); + user_stream->recv_body_len = 0; + user_stream->recv_fin = 0; + } + + if (data_len > 0) { + memcpy(user_stream->recv_body + user_stream->recv_body_len, data, data_len); + user_stream->recv_body_len += data_len; + } + + if (!user_stream->recv_fin) { + user_stream->recv_fin = fin; + } + + if (g_test_case == 311 || g_test_case == 312 || g_test_case == 313 || g_test_case == 314) { + user_stream->send_offset = 0; + user_stream->recv_body_len = 0; + user_stream->recv_fin = 0; + user_stream->send_body_len = 1000; + g_test_case = 0; + xqc_client_bytestream_send(h3_ext_bs, user_stream); + } + + user_stream->rcv_times++; + + printf("[bytestream]|stream_id:%"PRIu64"|data_len:%zu|fin:%d|recv_time:%"PRIu64"|\n", xqc_h3_ext_bytestream_id(h3_ext_bs), data_len, fin, data_recv_time); + + return 0; +} + +int xqc_h3_ext_bytestream_write_callback(xqc_h3_ext_bytestream_t *h3_ext_bs, + void *bs_user_data) +{ + user_stream_t *us = bs_user_data; + int ret; + // printf("[bytestream]|write callback|\n"); + ret = xqc_client_bytestream_send(h3_ext_bs, us); + if (ret == -XQC_EAGAIN) { + ret = 0; + printf("[bytestream]|write blocked|\n"); + } + return 0; +} + void xqc_client_request_send_fin_only(int fd, short what, void *arg) { @@ -1492,6 +2361,11 @@ xqc_client_request_send_fin_only(int fd, short what, void *arg) int xqc_client_request_send(xqc_h3_request_t *h3_request, user_stream_t *user_stream) { + if (g_test_case == 99) { + xqc_h3_request_finish(h3_request); + return 0; + } + if (user_stream->start_time == 0) { user_stream->start_time = now(); } @@ -1745,6 +2619,11 @@ xqc_client_request_send(xqc_h3_request_t *h3_request, user_stream_t *user_stream fin = 0; } + if (g_test_case == 109) { + xqc_stream_settings_t settings = {.recv_rate_bytes_per_sec = 100000000}; + xqc_h3_request_update_settings(h3_request, &settings); + } + /* send body */ if (user_stream->send_offset < user_stream->send_body_len) { @@ -1771,6 +2650,7 @@ xqc_client_request_send(xqc_h3_request_t *h3_request, user_stream_t *user_stream } ret = xqc_h3_request_send_body(h3_request, user_stream->send_body + user_stream->send_offset, user_stream->send_body_len - user_stream->send_offset, fin); if (ret == -XQC_EAGAIN) { + printf("xqc_h3_request_send_body eagain %zd\n", ret); return 0; } else if (ret < 0) { @@ -1822,6 +2702,22 @@ xqc_client_request_send(xqc_h3_request_t *h3_request, user_stream_t *user_stream printf("xqc_h3_request_send_body sent:%zd, offset=%"PRIu64", fin=1\n", ret, user_stream->send_offset); } } + + if (g_test_case == 50) { + ret = xqc_h3_request_send_body(h3_request, user_stream->send_body, 10, 0); + printf("xqc_h3_request_send_body sent:%zd\n", ret); + } + + if (g_test_case == 51) { + ret = xqc_h3_request_send_headers(h3_request, &headers, 1); + printf("xqc_h3_request_send_headers sent:%zd\n", ret); + } + + if (g_test_case == 52) { + ret = xqc_h3_request_finish(h3_request); + } + + return 0; } @@ -1845,6 +2741,7 @@ xqc_client_request_write_notify(xqc_h3_request_t *h3_request, void *user_data) return -1; } + printf("request write notify!:%"PRIu64"\n", xqc_h3_stream_id(h3_request)); ret = xqc_client_request_send(h3_request, user_stream); return ret; } @@ -1873,7 +2770,7 @@ xqc_client_request_read_notify(xqc_h3_request_t *h3_request, xqc_request_notify_ .iov_len = sizeof("value") }, }; - + xqc_http_headers_t headers = { .headers = &header, .count = 1, @@ -2054,11 +2951,12 @@ xqc_client_request_close_notify(xqc_h3_request_t *h3_request, void *user_data) xqc_request_stats_t stats; stats = xqc_h3_request_get_stats(h3_request); printf("send_body_size:%zu, recv_body_size:%zu, send_header_size:%zu, recv_header_size:%zu, recv_fin:%d, err:%d, " - "mp_state:%d, path0_send_weight:%.2f, path0_recv_weight:%.2f, stream_info:%s\n", + "mp_state:%d, cellular_send_weight:%.2f, cellular_recv_weight:%.2f, stream_info:%s\n", stats.send_body_size, stats.recv_body_size, stats.send_header_size, stats.recv_header_size, user_stream->recv_fin, stats.stream_err, - stats.mp_state, stats.mp_default_path_send_weight, stats.mp_default_path_recv_weight, + stats.mp_state, + stats.mp_standby_path_send_weight, stats.mp_standby_path_recv_weight, stats.stream_info); if (g_echo_check) { @@ -2084,7 +2982,7 @@ xqc_client_request_close_notify(xqc_h3_request_t *h3_request, void *user_data) if (g_req_cnt < g_req_max) { user_stream = calloc(1, sizeof(user_stream_t)); user_stream->user_conn = user_conn; - user_stream->h3_request = xqc_h3_request_create(p_ctx->engine, &user_conn->cid, user_stream); + user_stream->h3_request = xqc_h3_request_create(p_ctx->engine, &user_conn->cid, NULL, user_stream); if (user_stream->h3_request == NULL) { printf("xqc_h3_request_create error\n"); free(user_stream); @@ -2111,6 +3009,15 @@ xqc_client_request_close_notify(xqc_h3_request_t *h3_request, void *user_data) return 0; } +void +xqc_client_request_closing_notify(xqc_h3_request_t *h3_request, + xqc_int_t err, void *h3s_user_data) +{ + user_stream_t *user_stream = (user_stream_t *)h3s_user_data; + + printf("***** request closing notify triggered\n"); +} + void xqc_client_socket_write_handler(user_conn_t *user_conn) { @@ -2130,8 +3037,12 @@ xqc_client_socket_read_handler(user_conn_t *user_conn, int fd) { //DEBUG; + xqc_int_t ret; ssize_t recv_size = 0; ssize_t recv_sum = 0; + uint64_t path_id = XQC_UNKNOWN_PATH_ID; + xqc_user_path_t *path; + int i; client_ctx_t *p_ctx; if (g_test_qch_mode) { @@ -2140,6 +3051,13 @@ xqc_client_socket_read_handler(user_conn_t *user_conn, int fd) p_ctx = &ctx; } + for (i = 0; i < g_multi_interface_cnt; i++) { + path = &g_client_path[i]; + if (path->path_fd == fd || path->rebinding_path_fd == fd) { + path_id = path->path_id; + } + } + #ifdef __linux__ int batch = 0; if (batch) { @@ -2176,12 +3094,20 @@ xqc_client_socket_read_handler(user_conn_t *user_conn, int fd) for (int i = 0; i < retval; i++) { recv_sum += msgs[i].msg_len; - if (xqc_engine_packet_process(p_ctx->engine, iovecs[i].iov_base, msgs[i].msg_len, +#ifdef XQC_NO_PID_PACKET_PROCESS + ret = xqc_engine_packet_process(p_ctx->engine, iovecs[i].iov_base, msgs[i].msg_len, + user_conn->local_addr, user_conn->local_addrlen, + user_conn->peer_addr, user_conn->peer_addrlen, + (xqc_msec_t)recv_time, user_conn); +#else + ret = xqc_engine_packet_process(p_ctx->engine, iovecs[i].iov_base, msgs[i].msg_len, user_conn->local_addr, user_conn->local_addrlen, user_conn->peer_addr, user_conn->peer_addrlen, - (xqc_msec_t)recv_time, user_conn) != XQC_OK) + path_id, (xqc_msec_t)recv_time, user_conn); +#endif + if (ret != XQC_OK) { - printf("xqc_server_read_handler: packet process err\n"); + printf("xqc_server_read_handler: packet process err, ret: %d\n", ret); return; } } @@ -2266,13 +3192,19 @@ xqc_client_socket_read_handler(user_conn_t *user_conn, int fd) continue; } } - - if (xqc_engine_packet_process(p_ctx->engine, packet_buf, recv_size, +#ifdef XQC_NO_PID_PACKET_PROCESS + ret = xqc_engine_packet_process(p_ctx->engine, packet_buf, recv_size, user_conn->local_addr, user_conn->local_addrlen, user_conn->peer_addr, user_conn->peer_addrlen, - (xqc_msec_t)recv_time, user_conn) != XQC_OK) - { - printf("xqc_client_read_handler: packet process err\n"); + (xqc_msec_t)recv_time, user_conn); +#else + ret = xqc_engine_packet_process(p_ctx->engine, packet_buf, recv_size, + user_conn->local_addr, user_conn->local_addrlen, + user_conn->peer_addr, user_conn->peer_addrlen, + path_id, (xqc_msec_t)recv_time, user_conn); +#endif + if (ret != XQC_OK) { + printf("xqc_client_read_handler: packet process err, ret: %d\n", ret); return; } @@ -2341,6 +3273,39 @@ xqc_client_abs_timeout_callback(int fd, short what, void *arg) } } +static void +xqc_client_bytestream_timeout_callback(int fd, short what, void *arg) +{ + user_stream_t *user_stream = (user_stream_t *) arg; + int rc = 0; + printf("xqc_client_bytestream_timeout_callback\n"); + if (user_stream->send_offset >= (user_stream->send_body_len)) { + rc = 1; + } + if (g_test_case == 304) { + if (rc == 1) { + printf("xqc_client_bytestream_timeout_callback send fin\n"); + xqc_h3_ext_bytestream_finish(user_stream->h3_ext_bs); + } else { + struct timeval tv; + tv.tv_sec = 0; + tv.tv_usec = 100000; + event_add(user_stream->ev_bytestream_timer, &tv); + } + + } else if (g_test_case == 305) { + if (rc == 1) { + printf("xqc_client_bytestream_timeout_callback close stream\n"); + xqc_h3_ext_bytestream_close(user_stream->h3_ext_bs); + } else { + struct timeval tv; + tv.tv_sec = 0; + tv.tv_usec = 100000; + event_add(user_stream->ev_bytestream_timer, &tv); + } + } +} + static void xqc_client_timeout_multi_process_callback(int fd, short what, void *arg) { user_conn_t *user_conn = (user_conn_t *) arg; @@ -2357,6 +3322,33 @@ static void xqc_client_timeout_multi_process_callback(int fd, short what, void * printf("xqc_conn_close, %d connetion rest\n", ctx->cur_conn_num); } +static void +xqc_client_request_callback(int fd, short what, void *arg) +{ + user_conn_t *user_conn = (user_conn_t *) arg; + int rc; + client_ctx_t *ctx = user_conn->ctx; + + printf("--- xqc_client_request_callback\n"); + + user_stream_t *user_stream = calloc(1, sizeof(user_stream_t)); + user_stream->user_conn = user_conn; + if (user_conn->h3 == 0 || user_conn->h3 == 2) { + user_stream->h3_request = xqc_h3_request_create(ctx->engine, &user_conn->cid, NULL, user_stream); + if (user_stream->h3_request == NULL) { + printf("xqc_h3_request_create error\n"); + return; + } + + xqc_client_request_send(user_stream->h3_request, user_stream); + } + + struct timeval tv; + tv.tv_sec = 10; + tv.tv_usec = 0; + int ret = event_add(user_conn->ev_request, &tv); +} + static void xqc_client_timeout_callback(int fd, short what, void *arg) { @@ -2395,7 +3387,7 @@ xqc_client_timeout_callback(int fd, short what, void *arg) memset(user_stream, 0, sizeof(user_stream_t)); user_stream->user_conn = user_conn; printf("gtest 15: restart from idle!\n"); - user_stream->stream = xqc_stream_create(ctx.engine, &(user_conn->cid), user_stream); + user_stream->stream = xqc_stream_create(ctx.engine, &(user_conn->cid), NULL, user_stream); if (user_stream->stream == NULL) { printf("xqc_stream_create error\n"); goto conn_close; @@ -2410,6 +3402,19 @@ xqc_client_timeout_callback(int fd, short what, void *arg) } conn_close: + if (g_send_dgram && g_echo_check) { + if (g_test_case == 206 && g_no_crypt) { + // swap the first & 2nd dgram + printf("%zu %zu\n", dgram1_size, dgram2_size); + if (dgram1_size && dgram2_size) { + //printf("%x %x\n", user_conn->dgram_blk->recv_data[0], user_conn->dgram_blk->recv_data[dgram1_size]); + memcpy(sock_op_buffer, user_conn->dgram_blk->recv_data, dgram1_size); + memmove(user_conn->dgram_blk->recv_data, user_conn->dgram_blk->recv_data + dgram1_size, dgram2_size); + memcpy(user_conn->dgram_blk->recv_data + dgram2_size, sock_op_buffer, dgram1_size); + } + } + printf("[dgram]|echo_check|same_content:%s|\n", !memcmp(user_conn->dgram_blk->data, user_conn->dgram_blk->recv_data, user_conn->dgram_blk->data_len) ? "yes" : "no"); + } printf("xqc_client_timeout_callback | conn_close\n"); rc = xqc_conn_close(ctx.engine, &user_conn->cid); if (rc) { @@ -2428,6 +3433,9 @@ xqc_client_path_callback(int fd, short what, void *arg) // 判断conn状态 // TODO + if (g_test_case == 110) { + g_test_case = -1; + } int b_add_path = 0; @@ -2438,7 +3446,7 @@ xqc_client_path_callback(int fd, short what, void *arg) b_add_path = 1; uint64_t path_id = 0; - int ret = xqc_conn_create_path(ctx.engine, &(user_conn->cid), &path_id); + int ret = xqc_conn_create_path(ctx.engine, &(user_conn->cid), &path_id, 0); if (ret < 0) { printf("not support mp, xqc_conn_create_path err = %d\n", ret); @@ -2469,30 +3477,88 @@ static void xqc_client_epoch_callback(int fd, short what, void *arg) { user_conn_t *user_conn = (user_conn_t *) arg; + int ret; g_cur_epoch++; printf("|xqc_client_epoch_callback|epoch:%d|\n", g_cur_epoch); - for (int i = 0; i < g_req_paral; i++) { - g_req_cnt++; - user_stream_t *user_stream = calloc(1, sizeof(user_stream_t)); - user_stream->user_conn = user_conn; - user_stream->last_recv_log_time = now(); - user_stream->recv_log_bytes = 0; - if (user_conn->h3) { - user_stream->h3_request = xqc_h3_request_create(ctx.engine, &user_conn->cid, user_stream); - if (user_stream->h3_request == NULL) { - printf("xqc_h3_request_create error\n"); - continue; + if (g_send_dgram) { + if (user_conn->h3 == 1) { + xqc_client_datagram_send(user_conn); + + } else if (user_conn->h3 == 2) { + xqc_client_h3_ext_datagram_send(user_conn); + } + + } else { + for (int i = 0; i < g_req_paral; i++) { + g_req_cnt++; + user_stream_t *user_stream = calloc(1, sizeof(user_stream_t)); + user_stream->user_conn = user_conn; + user_stream->last_recv_log_time = now(); + user_stream->recv_log_bytes = 0; + if (user_conn->h3 == 0 || user_conn->h3 == 2) { + user_stream->h3_request = xqc_h3_request_create(ctx.engine, &user_conn->cid, NULL, user_stream); + if (user_stream->h3_request == NULL) { + printf("xqc_h3_request_create error\n"); + continue; + } + xqc_client_request_send(user_stream->h3_request, user_stream); + } else { + user_stream->stream = xqc_stream_create(ctx.engine, &user_conn->cid, NULL, user_stream); + if (user_stream->stream == NULL) { + printf("xqc_stream_create error\n"); + continue; + } + xqc_client_stream_send(user_stream->stream, user_stream); } - xqc_client_request_send(user_stream->h3_request, user_stream); + } + } + + if (g_mp_ping_on) { + if (user_conn->h3 == 1) { + xqc_conn_send_ping(ctx.engine, &user_conn->cid, NULL); + xqc_conn_send_ping(ctx.engine, &user_conn->cid, &g_ping_id); + } else { - user_stream->stream = xqc_stream_create(ctx.engine, &user_conn->cid, user_stream); - if (user_stream->stream == NULL) { - printf("xqc_stream_create error\n"); - continue; - } - xqc_client_stream_send(user_stream->stream, user_stream); + xqc_h3_conn_send_ping(ctx.engine, &user_conn->cid, NULL); + xqc_h3_conn_send_ping(ctx.engine, &user_conn->cid, &g_ping_id); + } + } + + if (g_test_case == 107 && !g_recovery) { + /* freeze the first path */ + ret = xqc_conn_mark_path_frozen(ctx.engine, &(user_conn->cid), 0); + if (ret < 0) { + printf("xqc_conn_mark_path_frozen err = %d\n", ret); + } + g_recovery = g_cur_epoch + 2; + } + + if (g_test_case == 108 && !g_recovery) { + /* freeze the second path */ + ret = xqc_conn_mark_path_frozen(ctx.engine, &(user_conn->cid), 1); + if (ret < 0) { + printf("xqc_conn_mark_path_frozen err = %d\n", ret); + } + g_recovery = g_cur_epoch + 2; + } + + if (g_test_case == 107 && g_recovery == g_cur_epoch) { + g_test_case = -1; + /* freeze the first path */ + ret = xqc_conn_mark_path_standby(ctx.engine, &(user_conn->cid), 0); + if (ret < 0) { + printf("xqc_conn_mark_path_standby err = %d\n", ret); + } + } + + if (g_test_case == 108 && g_recovery == g_cur_epoch) { + g_test_case = -1; + /* freeze the second path */ + ret = xqc_conn_mark_path_standby(ctx.engine, &(user_conn->cid), 1); + if (ret < 0) { + printf("xqc_conn_mark_path_standby err = %d\n", ret); } } @@ -2583,7 +3649,7 @@ xqc_client_close_keylog_file(client_ctx_t *ctx) void -xqc_keylog_cb(const char *line, void *user_data) +xqc_keylog_cb(const xqc_cid_t *scid, const char *line, void *user_data) { client_ctx_t *ctx = (client_ctx_t*)user_data; if (ctx->keylog_fd <= 0) { @@ -2591,6 +3657,8 @@ xqc_keylog_cb(const char *line, void *user_data) return; } + printf("scid:%s\n", xqc_scid_str(scid)); + int write_len = write(ctx->keylog_fd, line, strlen(line)); if (write_len < 0) { printf("write keys failed, errno: %d\n", errno); @@ -2624,44 +3692,48 @@ xqc_client_ready_to_create_path(const xqc_cid_t *cid, return; } - for (int i = 0; i < g_multi_interface_cnt; i++) { - if (g_client_path[i].is_in_used == 1) { - continue; - } - - int ret = xqc_conn_create_path(ctx.engine, &(user_conn->cid), &path_id); - - if (ret < 0) { - printf("not support mp, xqc_conn_create_path err = %d\n", ret); - return; - } - - printf("***** create a new path. index: %d, path_id: %" PRIu64 "\n", i, path_id); - g_client_path[i].path_id = path_id; - g_client_path[i].is_in_used = 1; + if (g_test_case != 110) { + for (int i = 0; i < g_multi_interface_cnt; i++) { + if (g_client_path[i].is_in_used == 1) { + continue; + } + + int ret = xqc_conn_create_path(ctx.engine, &(user_conn->cid), &path_id, 0); - if (g_test_case == 104) { - ret = xqc_conn_mark_path_standby(ctx.engine, &(user_conn->cid), 0); if (ret < 0) { - printf("xqc_conn_mark_path_standby err = %d\n", ret); + printf("not support mp, xqc_conn_create_path err = %d\n", ret); + return; } - ret = xqc_conn_mark_path_available(ctx.engine, &(user_conn->cid), 1); - if (ret < 0) { - printf("xqc_conn_mark_path_standby err = %d\n", ret); + + printf("***** create a new path. index: %d, path_id: %" PRIu64 "\n", i, path_id); + g_client_path[i].path_id = path_id; + g_client_path[i].is_in_used = 1; + + if (g_test_case == 104) { + ret = xqc_conn_mark_path_standby(ctx.engine, &(user_conn->cid), 0); + if (ret < 0) { + printf("xqc_conn_mark_path_standby err = %d\n", ret); + } + ret = xqc_conn_mark_path_available(ctx.engine, &(user_conn->cid), 1); + if (ret < 0) { + printf("xqc_conn_mark_path_available err = %d\n", ret); + } } - } - if (g_mp_backup_mode) { - ret = xqc_conn_mark_path_standby(ctx.engine, &(user_conn->cid), path_id); - if (ret < 0) { - printf("xqc_conn_mark_path_standby err = %d\n", ret); + if (g_mp_backup_mode) { + ret = xqc_conn_mark_path_standby(ctx.engine, &(user_conn->cid), path_id); + if (ret < 0) { + printf("xqc_conn_mark_path_standby err = %d\n", ret); + } } - } - xqc_client_set_path_debug_timer(user_conn); + xqc_client_set_path_debug_timer(user_conn); + } + + } else { + xqc_client_set_path_debug_timer(user_conn); } - } static void xqc_client_concurrent_callback(int fd, short what, void *arg){ @@ -2682,6 +3754,7 @@ static void xqc_client_concurrent_callback(int fd, short what, void *arg){ .h3_request_close_notify = xqc_client_request_close_notify, .h3_request_read_notify = xqc_client_request_read_notify, .h3_request_write_notify = xqc_client_request_write_notify, + .h3_request_closing_notify = xqc_client_request_closing_notify, } }; @@ -2733,10 +3806,14 @@ static void xqc_client_concurrent_callback(int fd, short what, void *arg){ conn_ssl_config.transport_parameter_data = NULL; const xqc_cid_t *cid; - if (user_conn->h3) { + if (user_conn->h3 == 0) { cid = xqc_h3_connect(ctx->engine, g_conn_settings, user_conn->token, user_conn->token_len, g_host, g_no_crypt, &conn_ssl_config, user_conn->peer_addr, user_conn->peer_addrlen, user_conn); + } else if (user_conn->h3 == 2) { + cid = xqc_connect(ctx->engine, g_conn_settings, user_conn->token, user_conn->token_len, + g_host, g_no_crypt, &conn_ssl_config, user_conn->peer_addr, + user_conn->peer_addrlen, XQC_DEFINED_ALPN_H3_EXT, user_conn); } else { cid = xqc_connect(ctx->engine, g_conn_settings, user_conn->token, user_conn->token_len, "127.0.0.1", g_no_crypt, &conn_ssl_config, user_conn->peer_addr, @@ -2751,13 +3828,12 @@ static void xqc_client_concurrent_callback(int fd, short what, void *arg){ ctx->cur_conn_num++; memcpy(&user_conn->cid, cid, sizeof(*cid)); - while (user_conn->cur_stream_num < g_req_paral) { user_stream_t *user_stream = calloc(1, sizeof(user_stream_t)); user_stream->user_conn = user_conn; - if (user_conn->h3) { - user_stream->h3_request = xqc_h3_request_create(ctx->engine, cid, user_stream); + if (user_conn->h3 == 0 || user_conn->h3 == 2) { + user_stream->h3_request = xqc_h3_request_create(ctx->engine, cid, NULL, user_stream); if (user_stream->h3_request == NULL) { printf("xqc_h3_request_create error\n"); continue; @@ -2767,9 +3843,9 @@ static void xqc_client_concurrent_callback(int fd, short what, void *arg){ } user_conn->cur_stream_num++; - } + } return; } @@ -2868,7 +3944,7 @@ void usage(int argc, char *argv[]) { " -c Congestion Control Algorithm. r:reno b:bbr c:cubic B:bbr2 bbr+ bbr2+\n" " -C Pacing on.\n" " -t Connection timeout. Default 3 seconds.\n" -" -T Transport layer. No HTTP3.\n" +" -T Transport protocol: 0 H3 (default), 1 Transport layer, 2 H3-ext.\n" " -1 Force 1RTT.\n" " -s Body size to send.\n" " -F Abs_timeout to close conn. >=0.\n" @@ -2885,6 +3961,7 @@ void usage(int argc, char *argv[]) { " -N No encryption\n" " -6 IPv6\n" " -M Enable multi-path on. |\n" +" -v Multipath Version Negotiation.\n" " -i Multi-path interface. e.g. -i interface1 -i interface2.\n" " -R Enable reinjection. Default is 0, no reinjection.\n" " -V Force cert verification. 0: don't allow self-signed cert. 1: allow self-signed cert.\n" @@ -2898,12 +3975,15 @@ void usage(int argc, char *argv[]) { " -J Random CID. default is 0.\n" " -Q Multipath backup path standby, set backup_mode on(1). default backup_mode is 0(off).\n" " -A Multipath request accelerate on. default is 0(off).\n" +" -y multipath backup path standby.\n" +" -z periodically send request.\n" , prog); } int main(int argc, char *argv[]) { g_req_cnt = 0; + g_bytestream_cnt = 0; g_req_max = 1; g_send_body_size = 1024*1024; g_send_body_size_defined = 0; @@ -2919,7 +3999,13 @@ int main(int argc, char *argv[]) { g_test_case = 0; g_ipv6 = 0; g_no_crypt = 0; + g_max_dgram_size = 0; + g_send_dgram = 0; g_req_paral = 1; + g_copa_ai = 1.0; + g_copa_delta = 0.05; + g_dgram_qos_level = XQC_DATA_QOS_HIGH; + g_pmtud_on = 0; char server_addr[64] = TEST_SERVER_ADDR; g_server_addr = server_addr; @@ -2930,14 +4016,37 @@ int main(int argc, char *argv[]) { int pacing_on = 0; int transport = 0; int use_1rtt = 0; + uint64_t rate_limit = 0; strcpy(g_log_path, "./clog"); srand(0); //fix the random seed + int long_opt_index; + + const struct option long_opts[] = { + {"copa_delta", required_argument, &long_opt_index, 1}, + {"copa_ai_unit", required_argument, &long_opt_index, 2}, + {"epoch_timeout", required_argument, &long_opt_index, 3}, + {"dgram_qos", required_argument, &long_opt_index, 4}, + {"pmtud", required_argument, &long_opt_index, 5}, + {"mp_ping", required_argument, &long_opt_index, 6}, + {"rate_limit", required_argument, &long_opt_index, 7}, + {0, 0, 0, 0} + }; + int ch = 0; - while ((ch = getopt(argc, argv, "a:p:P:n:c:Ct:T1s:w:r:l:Ed:u:H:h:Gx:6NMR:i:V:q:o:fe:F:D:b:B:J:QA")) != -1) { + while ((ch = getopt_long(argc, argv, "a:p:P:n:c:Ct:T:1s:w:r:l:Ed:u:H:h:Gx:6NMR:i:V:v:q:o:fe:F:D:b:B:J:Q:U:Ayz", long_opts, NULL)) != -1) { switch (ch) { + case 'U': + printf("option send_datagram 0 (off), 1 (on), 2(on + batch): %s\n", optarg); + g_send_dgram = atoi(optarg); + break; + case 'Q': + /* max_datagram_frame_size */ + printf("option max_datagram_frame_size: %s\n", optarg); + g_max_dgram_size = atoi(optarg); + break; case 'a': /* Server addr. */ printf("option addr :%s\n", optarg); snprintf(server_addr, sizeof(server_addr), optarg); @@ -2978,8 +4087,8 @@ int main(int argc, char *argv[]) { break; case 'T': /* Transport layer. No HTTP3. */ printf("option transport :%s\n", "on"); - transport = 1; - g_transport = 1; + transport = atoi(optarg); + g_transport = transport; break; case '1': /* Force 1RTT. */ printf("option 1RTT :%s\n", "on"); @@ -3068,6 +4177,16 @@ int main(int argc, char *argv[]) { printf("option enable multi-path: %s\n", optarg); g_enable_multipath = 1; break; + + case 'v': /* Negotiate multipath version. 4: Multipath-04. 5: Multipath-05*/ + printf("option multipath version: %s\n", optarg); + if (atoi(optarg) == 4) { + g_multipath_version = XQC_MULTIPATH_04; + + } else if (atoi(optarg) == 5) { + g_multipath_version = XQC_MULTIPATH_05; + } + break; case 'R': printf("option enable reinjection: %s\n", "on"); g_enable_reinjection = atoi(optarg); @@ -3118,7 +4237,7 @@ int main(int argc, char *argv[]) { printf("random cid:%s\n", optarg); g_random_cid = atoi(optarg); break; - case 'Q': + case 'y': printf("option multipath backup path standby :%s\n", "on"); g_mp_backup_mode = 1; break; @@ -3126,6 +4245,76 @@ int main(int argc, char *argv[]) { printf("option multipath request accelerate :%s\n", "on"); g_mp_request_accelerate = 1; break; + case 'z': + printf("option periodically send request :%s\n", "on"); + g_periodically_request = 1; + break; + /* long options */ + case 0: + + switch (long_opt_index) + { + case 1: /* copa_delta */ + g_copa_delta = atof(optarg); + if (g_copa_delta <= 0 || g_copa_delta > 0.5) { + printf("option g_copa_delta must be in (0, 0.5]\n"); + exit(0); + } else { + printf("option g_copa_delta: %.4lf\n", g_copa_delta); + } + break; + + case 2: /* copa_ai_unit */ + + g_copa_ai = atof(optarg); + if (g_copa_ai < 1.0) { + printf("option g_copa_ai must be greater than 1.0\n"); + exit(0); + } else { + printf("option g_copa_ai: %.4lf\n", g_copa_ai); + } + break; + + case 3: + g_epoch_timeout = atoi(optarg); + if (g_epoch_timeout <= 0) { + printf("invalid epoch_timeout!\n"); + exit(0); + } else { + printf("option g_epoch_timeout: %d\n", g_epoch_timeout); + } + break; + + case 4: + g_dgram_qos_level = atoi(optarg); + if (g_dgram_qos_level < XQC_DATA_QOS_HIGHEST || g_dgram_qos_level > XQC_DATA_QOS_LOWEST) { + printf("invalid qos level!\n"); + exit(0); + } else { + printf("option g_dgram_qos_level: %d\n", g_dgram_qos_level); + } + break; + + case 5: + g_pmtud_on = atoi(optarg); + printf("option g_pmtud_on: %d\n", g_pmtud_on); + break; + + case 6: + g_mp_ping_on = atoi(optarg); + printf("option g_mp_ping_on: %d\n", g_mp_ping_on); + break; + + case 7: + rate_limit = atoi(optarg); + printf("option rate_limit: %"PRIu64" Bps\n", rate_limit); + break; + + default: + break; + } + + break; default: printf("other option :%c\n", ch); @@ -3139,6 +4328,10 @@ int main(int argc, char *argv[]) { memset(g_header_value, 'v', sizeof(g_header_value)); memset(&ctx, 0, sizeof(ctx)); + if (g_test_case == 44) { + xqc_log_enable(XQC_FALSE); + } + xqc_client_open_keylog_file(&ctx); xqc_client_open_log_file(&ctx); @@ -3185,7 +4378,7 @@ int main(int argc, char *argv[]) { } #endif } -#ifndef XQC_DISABLE_RENO +#ifdef XQC_ENABLE_RENO else if (c_cong_ctl == 'r') { cong_ctrl = xqc_reno_cb; } @@ -3204,9 +4397,21 @@ int main(int argc, char *argv[]) { } #endif } +#endif +#ifdef XQC_ENABLE_UNLIMITED + else if (c_cong_ctl == 'u') { + cong_ctrl = xqc_unlimited_cc_cb; + + } +#endif +#ifdef XQC_ENABLE_COPA + else if (c_cong_ctl == 'P') { + cong_ctrl = xqc_copa_cb; + + } #endif else { - printf("unknown cong_ctrl, option is b, r, c, B, bbr+, bbr2+\n"); + printf("unknown cong_ctrl, option is b, r, c, B, bbr+, bbr2+, u\n"); return -1; } printf("congestion control flags: %x\n", cong_flags); @@ -3219,13 +4424,57 @@ int main(int argc, char *argv[]) { .customize_on = 1, .init_cwnd = 32, .cc_optimization_flags = cong_flags, + .copa_delta_ai_unit = g_copa_ai, + .copa_delta_base = g_copa_delta, }, - //.so_sndbuf = 1024*1024, - .proto_version = XQC_VERSION_V1, .spurious_loss_detect_on = 0, .keyupdate_pkt_threshold = 0, + .max_datagram_frame_size = g_max_dgram_size, .enable_multipath = g_enable_multipath, + .multipath_version = g_multipath_version, + .marking_reinjection = 1, + .mp_ping_on = g_mp_ping_on, + .recv_rate_bytes_per_sec = rate_limit, }; + + xqc_stream_settings_t stream_settings = { .recv_rate_bytes_per_sec = 0 }; + + if (g_test_case == 109) { + conn_settings.enable_stream_rate_limit = 1; + stream_settings.recv_rate_bytes_per_sec = 500000; + } + + if (g_test_case == 400) { + //low_delay + conn_settings = xqc_conn_get_conn_settings_template(XQC_CONN_SETTINGS_LOW_DELAY); + } + + if (g_pmtud_on) { + conn_settings.enable_pmtud = 1; + } + + conn_settings.pacing_on = pacing_on; + conn_settings.proto_version = XQC_VERSION_V1; + conn_settings.max_datagram_frame_size = g_max_dgram_size; + conn_settings.enable_multipath = g_enable_multipath; + + if (g_test_case == 208) { + conn_settings.datagram_redundancy = 1; + } + + if (g_test_case == 209) { + conn_settings.datagram_redundant_probe = 30000; + } + + if (g_test_case == 210) { + conn_settings.datagram_redundancy = 2; + } + + if (g_test_case == 211) { + conn_settings.datagram_redundancy = 2; + conn_settings.datagram_redundant_probe = 30000; + } + g_conn_settings = &conn_settings; xqc_config_t config; @@ -3233,6 +4482,11 @@ int main(int argc, char *argv[]) { return -1; } + + if (transport == 2 && g_test_case != 315) { + config.enable_h3_ext = 1; + } + switch(c_log_level) { case 'e': config.cfg_log_level = XQC_LOG_ERROR; break; case 'i': config.cfg_log_level = XQC_LOG_INFO; break; @@ -3279,6 +4533,10 @@ int main(int argc, char *argv[]) { conn_settings.max_pkt_out_size = 1400; } + if (g_test_case == 201) { + conn_settings.max_pkt_out_size = 1216; + } + if (g_test_qch_mode) { pid_t pid; int i; @@ -3329,7 +4587,21 @@ int main(int argc, char *argv[]) { .h3_request_close_notify = xqc_client_request_close_notify, .h3_request_read_notify = xqc_client_request_read_notify, .h3_request_write_notify = xqc_client_request_write_notify, - } + .h3_request_closing_notify = xqc_client_request_closing_notify, + }, + .h3_ext_dgram_cbs = { + .dgram_read_notify = xqc_client_h3_ext_datagram_read_callback, + .dgram_write_notify = xqc_client_h3_ext_datagram_write_callback, + .dgram_acked_notify = xqc_client_h3_ext_datagram_acked_callback, + .dgram_lost_notify = xqc_client_h3_ext_datagram_lost_callback, + .dgram_mss_updated_notify = xqc_client_h3_ext_datagram_mss_updated_callback, + }, + .h3_ext_bs_cbs = { + .bs_read_notify = xqc_h3_ext_bytestream_read_callback, + .bs_write_notify = xqc_h3_ext_bytestream_write_callback, + .bs_create_notify = xqc_h3_ext_bytestream_create_callback, + .bs_close_notify = xqc_h3_ext_bytestream_close_callback, + }, }; /* init http3 context */ @@ -3351,6 +4623,13 @@ int main(int argc, char *argv[]) { .stream_write_notify = xqc_client_stream_write_notify, .stream_read_notify = xqc_client_stream_read_notify, .stream_close_notify = xqc_client_stream_close_notify, + }, + .dgram_cbs = { + .datagram_write_notify = xqc_client_datagram_write_callback, + .datagram_read_notify = xqc_client_datagram_read_callback, + .datagram_acked_notify = xqc_client_datagram_acked_callback, + .datagram_lost_notify = xqc_client_datagram_lost_callback, + .datagram_mss_updated_notify = xqc_client_datagram_mss_updated_callback, } }; @@ -3393,15 +4672,35 @@ int main(int argc, char *argv[]) { } /* enable_reinjection */ - if (g_enable_reinjection) { - conn_settings.reinj_ctl_callback = xqc_xlink_reinj_ctl_cb; - conn_settings.mp_enable_reinjection = g_enable_reinjection; + if (g_enable_reinjection == 1) { + conn_settings.reinj_ctl_callback = xqc_default_reinj_ctl_cb; + conn_settings.mp_enable_reinjection = 1; + + } else if (g_enable_reinjection == 2) { + conn_settings.reinj_ctl_callback = xqc_deadline_reinj_ctl_cb; + conn_settings.mp_enable_reinjection = 2; + + } else if (g_enable_reinjection == 3) { + conn_settings.reinj_ctl_callback = xqc_dgram_reinj_ctl_cb; + conn_settings.mp_enable_reinjection = 4; + conn_settings.scheduler_callback = xqc_rap_scheduler_cb; + } else if (g_enable_reinjection == 4) { + conn_settings.reinj_ctl_callback = xqc_dgram_reinj_ctl_cb; + conn_settings.mp_enable_reinjection = 4; + conn_settings.scheduler_callback = xqc_rap_scheduler_cb; + conn_settings.datagram_redundant_probe = 30000; } if (g_mp_backup_mode) { conn_settings.scheduler_callback = xqc_backup_scheduler_cb; } + if (g_test_case == 501) { + conn_settings.scheduler_callback = xqc_backup_scheduler_cb; + conn_settings.mp_enable_reinjection = 0; + conn_settings.standby_path_probe_timeout = 500; + } + unsigned char token[XQC_MAX_TOKEN_LEN]; int token_len = XQC_MAX_TOKEN_LEN; token_len = xqc_client_read_token(token, token_len); @@ -3440,11 +4739,20 @@ int main(int argc, char *argv[]) { const xqc_cid_t *cid; - if (user_conn->h3) { + + printf("conn type: %d\n", user_conn->h3); + + user_conn->ctx = &ctx; + + if (user_conn->h3 == 0) { if (g_test_case == 7) {user_conn->token_len = -1;} /* create connection fail */ cid = xqc_h3_connect(ctx.engine, &conn_settings, user_conn->token, user_conn->token_len, g_host, g_no_crypt, &conn_ssl_config, user_conn->peer_addr, user_conn->peer_addrlen, user_conn); + } else if (user_conn->h3 == 2) { + cid = xqc_connect(ctx.engine, &conn_settings, user_conn->token, user_conn->token_len, + g_host, g_no_crypt, &conn_ssl_config, user_conn->peer_addr, + user_conn->peer_addrlen, XQC_DEFINED_ALPN_H3_EXT, user_conn); } else { if (g_test_case == 43) { /* try a alpn not supported by server */ @@ -3468,41 +4776,151 @@ int main(int argc, char *argv[]) { /* copy cid to its own memory space to prevent crashes caused by internal cid being freed */ memcpy(&user_conn->cid, cid, sizeof(*cid)); - if (g_test_case != 500) { + user_conn->dgram_blk = calloc(1, sizeof(user_dgram_blk_t)); + user_conn->dgram_blk->data_sent = 0; + user_conn->dgram_blk->data_recv = 0; + user_conn->dgram_blk->dgram_id = 1; + if (user_conn->quic_conn) { + printf("[dgram]|prepare_dgram_user_data|\n"); + xqc_datagram_set_user_data(user_conn->quic_conn, user_conn); + + } + + if (user_conn->h3_conn) { + printf("[h3-dgram]|prepare_dgram_user_data|\n"); + xqc_h3_ext_datagram_set_user_data(user_conn->h3_conn, user_conn); + + } + + if (g_test_case == 501) { + goto skip_data; + } + + if (g_test_case >= 300 && g_test_case < 400 && user_conn->h3 == 2) { + // for h3 bytestream testcases + // send h3 requests + if (g_test_case != 306 && g_test_case != 307 && g_test_case != 308 && g_test_case != 310 + && g_test_case != 311 && g_test_case != 312 && g_test_case != 313) + { + for (int i = 0; i < g_req_paral; i++) { + g_req_cnt++; + user_stream_t *user_stream = calloc(1, sizeof(user_stream_t)); + user_stream->user_conn = user_conn; + user_stream->last_recv_log_time = now(); + user_stream->recv_log_bytes = 0; + user_stream->h3_request = xqc_h3_request_create(ctx.engine, cid, NULL, user_stream); + if (user_stream->h3_request == NULL) { + printf("xqc_h3_request_create error\n"); + continue; + } + + xqc_client_request_send(user_stream->h3_request, user_stream); + + } + } + + // open bytestreams and send data for (int i = 0; i < g_req_paral; i++) { - g_req_cnt++; + g_bytestream_cnt++; user_stream_t *user_stream = calloc(1, sizeof(user_stream_t)); user_stream->user_conn = user_conn; user_stream->last_recv_log_time = now(); user_stream->recv_log_bytes = 0; - if (user_conn->h3) { - if (g_test_case == 11) { /* create stream fail */ - xqc_cid_t tmp; - xqc_h3_request_create(ctx.engine, &tmp, user_stream); - continue; - } + user_stream->h3_ext_bs = xqc_h3_ext_bytestream_create(ctx.engine, cid, user_stream); + if (user_stream->h3_ext_bs == NULL) { + printf("xqc_h3_ext_bytestream_create error\n"); + continue; + } - user_stream->h3_request = xqc_h3_request_create(ctx.engine, cid, user_stream); - if (user_stream->h3_request == NULL) { - printf("xqc_h3_request_create error\n"); - continue; + xqc_client_bytestream_send(user_stream->h3_ext_bs, user_stream); + + } + + // prepare to send datagrams + if (g_test_case != 306 && g_test_case != 307 && g_test_case != 308 && g_test_case != 310 + && g_test_case != 311 && g_test_case != 312 && g_test_case != 313) + { + if (g_send_dgram) { + user_conn->dgram_blk->data = calloc(1, g_send_body_size); + user_conn->dgram_blk->data[0] = 0x1; + user_conn->dgram_blk->data_len = g_send_body_size; + if (g_echo_check) { + user_conn->dgram_blk->recv_data = calloc(1, g_send_body_size << 4); + user_conn->dgram_blk->recv_data[0] = 0x2; } + xqc_client_h3_ext_datagram_send(user_conn); + } + } - xqc_client_request_send(user_stream->h3_request, user_stream); + } else if (!g_send_dgram && g_test_case != 500) { - } else { - user_stream->stream = xqc_stream_create(ctx.engine, cid, user_stream); - if (user_stream->stream == NULL) { - printf("xqc_stream_create error\n"); - continue; + if (g_periodically_request) { + user_conn->ev_request = event_new(eb, -1, 0, xqc_client_request_callback, user_conn); + + /* request once every 1 second */ + struct timeval tv; + tv.tv_sec = 0; + tv.tv_usec = 500000; + int ret = event_add(user_conn->ev_request, &tv); + if (ret != 0) { + printf("[ERROR] add request event ret: %d\n", ret); + } + + + printf("----------- g_periodically_request: %d\n", g_periodically_request); + + } else { + + for (int i = 0; i < g_req_paral; i++) { + g_req_cnt++; + user_stream_t *user_stream = calloc(1, sizeof(user_stream_t)); + user_stream->user_conn = user_conn; + user_stream->last_recv_log_time = now(); + user_stream->recv_log_bytes = 0; + if (user_conn->h3 == 0 || user_conn->h3 == 2) { + if (g_test_case == 11) { /* create stream fail */ + xqc_cid_t tmp; + xqc_h3_request_create(ctx.engine, &tmp, NULL, user_stream); + continue; + } + + user_stream->h3_request = xqc_h3_request_create(ctx.engine, cid, &stream_settings, user_stream); + if (user_stream->h3_request == NULL) { + printf("xqc_h3_request_create error\n"); + continue; + } + + xqc_client_request_send(user_stream->h3_request, user_stream); + + } else { + user_stream->stream = xqc_stream_create(ctx.engine, cid, NULL, user_stream); + if (user_stream->stream == NULL) { + printf("xqc_stream_create error\n"); + continue; + } + printf("[qperf]|ts:%"PRIu64"|test_start|\n", now()); + xqc_client_stream_send(user_stream->stream, user_stream); } - printf("[qperf]|ts:%"PRIu64"|test_start|\n", now()); - xqc_client_stream_send(user_stream->stream, user_stream); } } last_recv_ts = now(); + + } else { + user_conn->dgram_blk->data = calloc(1, g_send_body_size); + user_conn->dgram_blk->data_len = g_send_body_size; + if (g_echo_check) { + user_conn->dgram_blk->recv_data = calloc(1, g_send_body_size << 4); + } + if (user_conn->h3 == 2) { + xqc_client_h3_ext_datagram_send(user_conn); + + } else if (user_conn->h3 == 1) { + xqc_client_datagram_send(user_conn); + } + } +skip_data: event_base_dispatch(eb); @@ -3513,6 +4931,16 @@ int main(int argc, char *argv[]) { } event_free(user_conn->ev_timeout); + if (user_conn->dgram_blk) { + if (user_conn->dgram_blk->data) { + free(user_conn->dgram_blk->data); + } + if (user_conn->dgram_blk->recv_data) { + free(user_conn->dgram_blk->recv_data); + } + free(user_conn->dgram_blk); + } + free(user_conn->peer_addr); free(user_conn->local_addr); free(user_conn); diff --git a/tests/test_server.c b/tests/test_server.c index 0632740d9..faca4c431 100644 --- a/tests/test_server.c +++ b/tests/test_server.c @@ -16,6 +16,8 @@ #include #include #include +#include +#include #define XQC_FIRST_OCTET 1 int @@ -37,6 +39,18 @@ printf_null(const char *format, ...) #define XQC_MAX_LOG_LEN 2048 +#define XQC_TEST_DGRAM_BATCH_SZ 32 + +typedef struct user_datagram_block_s { + unsigned char *data; + size_t data_len; + size_t to_send_size; + size_t data_sent; + size_t data_recv; + size_t data_lost; + size_t dgram_lost; +} user_dgram_blk_t; + typedef struct xqc_quic_lb_ctx_s { uint8_t sid_len; @@ -50,17 +64,24 @@ typedef struct xqc_quic_lb_ctx_s { typedef struct user_stream_s { - xqc_stream_t *stream; - xqc_h3_request_t *h3_request; - uint64_t send_offset; - int header_sent; - int header_recvd; - char *send_body; - size_t send_body_len; - size_t send_body_max; - char *recv_body; - size_t recv_body_len; - FILE *recv_body_fp; + xqc_stream_t *stream; + xqc_h3_request_t *h3_request; + uint64_t send_offset; + int header_sent; + int header_recvd; + char *send_body; + size_t send_body_len; + size_t send_body_max; + char *recv_body; + size_t recv_body_len; + FILE *recv_body_fp; + xqc_h3_ext_bytestream_t *h3_ext_bs; + int recv_fin; + int echo_fin; + + int snd_times; + int rcv_times; + struct event *ev_timeout; } user_stream_t; typedef struct user_conn_s { @@ -68,6 +89,13 @@ typedef struct user_conn_s { struct sockaddr_in6 peer_addr; socklen_t peer_addrlen; xqc_cid_t cid; + + user_dgram_blk_t *dgram_blk; + size_t dgram_mss; + uint8_t dgram_not_supported; + + xqc_connection_t *quic_conn; + xqc_h3_conn_t *h3_conn; } user_conn_t; typedef struct xqc_server_ctx_s { @@ -89,6 +117,10 @@ typedef struct { xqc_server_ctx_t ctx; struct event_base *eb; +xqc_data_qos_level_t g_dgram_qos_level; +int g_pmtud_on; +int g_send_dgram; +int g_max_dgram_size; int g_send_body_size_from_cdf; cdf_entry_t *cdf_list; int cdf_list_size; @@ -98,15 +130,20 @@ int g_send_body_size_defined; int g_save_body; int g_read_body; int g_spec_url; +//99 pure fin int g_test_case; int g_ipv6; int g_batch=0; int g_lb_cid_encryption_on = 0; int g_enable_multipath = 0; +// xqc_multipath_version_t g_multipath_version = XQC_MULTIPATH_05; int g_enable_reinjection = 0; int g_spec_local_addr = 0; int g_mpshell = 0; int g_endless_sending = 0; +double g_copa_ai = 1.0; +double g_copa_delta = 0.05; +int g_enable_h3_ext = 1; int g_mp_backup_mode = 0; char g_write_file[256]; char g_read_file[256]; @@ -125,6 +162,7 @@ static uint64_t last_snd_ts; #define XQC_TEST_LONG_HEADER_LEN 32769 char test_long_value[XQC_TEST_LONG_HEADER_LEN] = {'\0'}; + /* CDF file format: N (N lines) @@ -204,6 +242,364 @@ now() return ul; } + +static void +xqc_server_datagram_send(user_conn_t *user_conn) +{ + if (user_conn->dgram_not_supported) { + // exit + printf("[dgram]|peer_does_not_support_datagram|\n"); + xqc_conn_close(ctx.engine, &user_conn->cid); + return; + } + + user_dgram_blk_t *dgram_blk = user_conn->dgram_blk; + int ret; + + if (g_send_dgram == 1) { + uint64_t dgram_id; + while (dgram_blk->data_sent < dgram_blk->to_send_size) { + size_t dgram_size = dgram_blk->to_send_size - dgram_blk->data_sent; + if (dgram_size > user_conn->dgram_mss) { + dgram_size = user_conn->dgram_mss; + } + dgram_blk->data[dgram_blk->data_sent] = 0x31; + ret = xqc_datagram_send(user_conn->quic_conn, dgram_blk->data + dgram_blk->data_sent, dgram_size, &dgram_id, g_dgram_qos_level); + if (ret == -XQC_EAGAIN) { + printf("[dgram]|retry_datagram_send_later|\n"); + return; + } else if (ret == -XQC_EDGRAM_TOO_LARGE) { + printf("[dgram]|trying_to_send_an_oversized_datagram|recorded_mss:%zu|send_size:%zu|current_mss:%zu|\n", user_conn->dgram_mss, dgram_size, xqc_datagram_get_mss(user_conn->quic_conn)); + xqc_conn_close(ctx.engine, &user_conn->cid); + return; + } else if (ret < 0) { + printf("[dgram]|send_datagram_error|err_code:%d|\n", ret); + xqc_conn_close(ctx.engine, &user_conn->cid); + return; + } + //printf("[dgram]|send_one_datagram|id:%"PRIu64"|size:%zu|\n", dgram_id, dgram_size); + dgram_blk->data_sent += dgram_size; + } + } else if (g_send_dgram == 2) { + struct iovec iov[XQC_TEST_DGRAM_BATCH_SZ]; + uint64_t dgram_id_list[XQC_TEST_DGRAM_BATCH_SZ]; + size_t bytes_in_batch = 0; + int batch_cnt = 0; + while ((dgram_blk->data_sent + bytes_in_batch) < dgram_blk->to_send_size) { + size_t dgram_size = dgram_blk->to_send_size - dgram_blk->data_sent - bytes_in_batch; + size_t succ_sent = 0, succ_sent_bytes = 0; + if (dgram_size > user_conn->dgram_mss) { + dgram_size = user_conn->dgram_mss; + } + iov[batch_cnt].iov_base = dgram_blk->data + dgram_blk->data_sent + bytes_in_batch; + iov[batch_cnt].iov_len = dgram_size; + dgram_blk->data[dgram_blk->data_sent + bytes_in_batch] = 0x31; + bytes_in_batch += dgram_size; + batch_cnt++; + if ((bytes_in_batch + dgram_blk->data_sent) == dgram_blk->to_send_size + || batch_cnt == XQC_TEST_DGRAM_BATCH_SZ) + { + ret = xqc_datagram_send_multiple(user_conn->quic_conn, iov, dgram_id_list, batch_cnt, &succ_sent, &succ_sent_bytes, g_dgram_qos_level); + if (ret == -XQC_EDGRAM_TOO_LARGE) { + printf("[dgram]|trying_to_send_an_oversized_datagram|recorded_mss:%zu|send_size:%zu|current_mss:%zu|\n", user_conn->dgram_mss, iov[succ_sent].iov_len, xqc_datagram_get_mss(user_conn->quic_conn)); + xqc_conn_close(ctx.engine, &user_conn->cid); + return; + } else if (ret < 0 && ret != -XQC_EAGAIN) { + printf("[dgram]|send_datagram_multiple_error|err_code:%d|\n", ret); + xqc_conn_close(ctx.engine, &user_conn->cid); + return; + } + + // for (int i = 0; i < succ_sent; i++) { + // printf("[dgram]|send_one_datagram|id:%"PRIu64"|size:%zu|\n", dgram_id_list[i], iov[i].iov_len); + // } + + // printf("[dgram]|datagrams_sent_in_a_batch|cnt:%zu|size:%zu|\n", succ_sent, succ_sent_bytes); + + dgram_blk->data_sent += succ_sent_bytes; + + if (ret == -XQC_EAGAIN) { + printf("[dgram]|retry_datagram_send_multiple_later|\n"); + return; + } + + bytes_in_batch = 0; + batch_cnt = 0; + } + } + + } +} + +static void +xqc_server_datagram_mss_updated_callback(xqc_connection_t *conn, + size_t mss, void *user_data) +{ + user_conn_t *user_conn = (user_conn_t*)user_data; + if (user_conn->dgram_mss == 0) { + if (g_test_case == 200 || g_test_case == 201) { + printf("[dgram-200]|1RTT|initial_mss:%zu|\n", mss); + } + + } else { + printf("[dgram]|1RTT|updated_mss:%zu|\n", mss); + } + + user_conn->dgram_mss = mss; + + if (!user_conn->dgram_not_supported && user_conn->dgram_mss == 0) { + user_conn->dgram_not_supported = 1; + } +} + +static void +xqc_server_datagram_read_callback(xqc_connection_t *conn, void *user_data, const void *data, size_t data_len, uint64_t dgram_ts) +{ + user_conn_t *user_conn = (user_conn_t*)user_data; + + if (g_send_dgram) { + if (g_echo) { + uint64_t dgram_id; + int ret; + if (user_conn->dgram_blk->data_recv + data_len > user_conn->dgram_blk->data_len) { + //expand buffer size + size_t new_len = (user_conn->dgram_blk->data_recv + data_len) << 1; + unsigned char *new_data = calloc(1, new_len); + memcpy(new_data, user_conn->dgram_blk->data, user_conn->dgram_blk->data_recv); + if (user_conn->dgram_blk->data) { + free(user_conn->dgram_blk->data); + } + user_conn->dgram_blk->data = new_data; + user_conn->dgram_blk->data_len = new_len; + } + memcpy(user_conn->dgram_blk->data + user_conn->dgram_blk->data_recv, data, data_len); + user_conn->dgram_blk->data_recv += data_len; + user_conn->dgram_blk->to_send_size = user_conn->dgram_blk->data_recv; + + } else { + user_conn->dgram_blk->data_recv += data_len; + } + } + + if (g_send_dgram){ + if (user_conn->dgram_blk->data_sent < user_conn->dgram_blk->data_len) { + xqc_server_datagram_send(user_conn); + } + } +} + +static void +xqc_server_datagram_write_callback(xqc_connection_t *conn, void *user_data) +{ + user_conn_t *user_conn = (user_conn_t*)user_data; + if (g_send_dgram) { + xqc_server_datagram_send(user_conn); + } +} + +static void +xqc_server_datagram_acked_callback(xqc_connection_t *conn, uint64_t dgram_id, void *user_data) +{ + +} + +static int +xqc_server_datagram_lost_callback(xqc_connection_t *conn, uint64_t dgram_id, void *user_data) +{ + user_conn_t *user_conn = (user_conn_t*)user_data; + //user_conn->dgram_blk->data_lost += data_len; + user_conn->dgram_blk->dgram_lost++; + return 0; +} + + +static void +xqc_server_h3_ext_datagram_send(user_conn_t *user_conn) +{ + if (user_conn->dgram_not_supported) { + // exit + printf("[h3-dgram]|peer_does_not_support_datagram|\n"); + xqc_h3_conn_close(ctx.engine, &user_conn->cid); + return; + } + + user_dgram_blk_t *dgram_blk = user_conn->dgram_blk; + int ret; + + if (g_send_dgram == 1) { + uint64_t dgram_id; + while (dgram_blk->data_sent < dgram_blk->to_send_size) { + size_t dgram_size = dgram_blk->to_send_size - dgram_blk->data_sent; + if (dgram_size > user_conn->dgram_mss) { + dgram_size = user_conn->dgram_mss; + } + dgram_blk->data[dgram_blk->data_sent] = 0x31; + ret = xqc_h3_ext_datagram_send(user_conn->h3_conn, dgram_blk->data + dgram_blk->data_sent, dgram_size, &dgram_id, g_dgram_qos_level); + if (ret == -XQC_EAGAIN) { + printf("[h3-dgram]|retry_datagram_send_later|\n"); + return; + } else if (ret == -XQC_EDGRAM_TOO_LARGE ) { + printf("[h3-dgram]|trying_to_send_an_oversized_datagram|recorded_mss:%zu|send_size:%zu|current_mss:%zu|\n", user_conn->dgram_mss, dgram_size, xqc_h3_ext_datagram_get_mss(user_conn->h3_conn)); + xqc_h3_conn_close(ctx.engine, &user_conn->cid); + return; + } else if (ret < 0) { + printf("[h3-dgram]|send_datagram_error|err_code:%d|\n", ret); + xqc_h3_conn_close(ctx.engine, &user_conn->cid); + return; + } + //printf("[dgram]|send_one_datagram|id:%"PRIu64"|size:%zu|\n", dgram_id, dgram_size); + dgram_blk->data_sent += dgram_size; + } + } else if (g_send_dgram == 2) { + struct iovec iov[XQC_TEST_DGRAM_BATCH_SZ]; + uint64_t dgram_id_list[XQC_TEST_DGRAM_BATCH_SZ]; + size_t bytes_in_batch = 0; + int batch_cnt = 0; + while ((dgram_blk->data_sent + bytes_in_batch) < dgram_blk->to_send_size) { + size_t dgram_size = dgram_blk->to_send_size - dgram_blk->data_sent - bytes_in_batch; + size_t succ_sent = 0, succ_sent_bytes = 0; + if (dgram_size > user_conn->dgram_mss) { + dgram_size = user_conn->dgram_mss; + } + iov[batch_cnt].iov_base = dgram_blk->data + dgram_blk->data_sent + bytes_in_batch; + iov[batch_cnt].iov_len = dgram_size; + dgram_blk->data[dgram_blk->data_sent + bytes_in_batch] = 0x31; + bytes_in_batch += dgram_size; + batch_cnt++; + if ((bytes_in_batch + dgram_blk->data_sent) == dgram_blk->to_send_size + || batch_cnt == XQC_TEST_DGRAM_BATCH_SZ) + { + ret = xqc_h3_ext_datagram_send_multiple(user_conn->h3_conn, iov, dgram_id_list, batch_cnt, &succ_sent, &succ_sent_bytes, g_dgram_qos_level); + if (ret == -XQC_EDGRAM_TOO_LARGE) { + printf("[h3-dgram]|trying_to_send_an_oversized_datagram|recorded_mss:%zu|send_size:%zu|current_mss:%zu|\n", user_conn->dgram_mss, iov[succ_sent].iov_len, xqc_h3_ext_datagram_get_mss(user_conn->h3_conn)); + xqc_h3_conn_close(ctx.engine, &user_conn->cid); + return; + } else if (ret < 0 && ret != -XQC_EAGAIN) { + printf("[h3-dgram]|send_datagram_multiple_error|err_code:%d|\n", ret); + xqc_h3_conn_close(ctx.engine, &user_conn->cid); + return; + } + + // for (int i = 0; i < succ_sent; i++) { + // printf("[dgram]|send_one_datagram|id:%"PRIu64"|size:%zu|\n", dgram_id_list[i], iov[i].iov_len); + // } + + // printf("[dgram]|datagrams_sent_in_a_batch|cnt:%zu|size:%zu|\n", succ_sent, succ_sent_bytes); + + dgram_blk->data_sent += succ_sent_bytes; + + if (ret == -XQC_EAGAIN) { + printf("[h3-dgram]|retry_datagram_send_multiple_later|\n"); + return; + } + + bytes_in_batch = 0; + batch_cnt = 0; + } + } + + } +} + +static void +xqc_server_h3_ext_datagram_mss_updated_callback(xqc_h3_conn_t *conn, size_t mss, void *user_data) +{ + user_conn_t *user_conn = (user_conn_t*)user_data; + if (user_conn->dgram_mss == 0) { + if (g_test_case == 200 || g_test_case == 201) { + printf("[h3-dgram-200]|1RTT|initial_mss:%zu|\n", mss); + } + + } else { + printf("[h3-dgram]|1RTT|updated_mss:%zu|\n", mss); + } + + user_conn->dgram_mss = mss; + + if (!user_conn->dgram_not_supported && user_conn->dgram_mss == 0) { + user_conn->dgram_not_supported = 1; + } +} + +static void +xqc_server_h3_ext_datagram_read_callback(xqc_h3_conn_t *conn, const void *data, size_t data_len, void *user_data, uint64_t ts) +{ + user_conn_t *user_conn = (user_conn_t*)user_data; + + uint8_t dgram_type; + uint32_t dgram_id; + uint64_t timestamp; + + if (data_len >= 13) { + dgram_type = *(uint8_t*)data; + dgram_id = *(uint32_t*)(data + 1); + timestamp = *(uint64_t*)(data + 5); + + if (dgram_type == 0x32) { + printf("[h3-dgram-benchmark]|dgram_id:%u|time:%"PRIu64"|\n", dgram_id, now() - timestamp); + } + } + + + + if (g_send_dgram) { + if (g_echo) { + uint64_t dgram_id; + int ret; + if (user_conn->dgram_blk->data_recv + data_len > user_conn->dgram_blk->data_len) { + //expand buffer size + size_t new_len = (user_conn->dgram_blk->data_recv + data_len) << 1; + unsigned char *new_data = calloc(1, new_len); + memcpy(new_data, user_conn->dgram_blk->data, user_conn->dgram_blk->data_recv); + if (user_conn->dgram_blk->data) { + free(user_conn->dgram_blk->data); + } + user_conn->dgram_blk->data = new_data; + user_conn->dgram_blk->data_len = new_len; + } + memcpy(user_conn->dgram_blk->data + user_conn->dgram_blk->data_recv, data, data_len); + user_conn->dgram_blk->data_recv += data_len; + user_conn->dgram_blk->to_send_size = user_conn->dgram_blk->data_recv; + + } else { + user_conn->dgram_blk->data_recv += data_len; + } + } + + // printf("recv:%zd, to_send:%zd, data_len: %zd, sent: %zd\n", user_conn->dgram_blk->data_recv, user_conn->dgram_blk->to_send_size, user_conn->dgram_blk->data_len, user_conn->dgram_blk->data_sent); + + if (g_send_dgram){ + if (user_conn->dgram_blk->data_sent < user_conn->dgram_blk->to_send_size) { + xqc_server_h3_ext_datagram_send(user_conn); + } + } +} + +static void +xqc_server_h3_ext_datagram_write_callback(xqc_h3_conn_t *conn, void *user_data) +{ + user_conn_t *user_conn = (user_conn_t*)user_data; + printf("h3 datagram write notify!\n"); + if (g_send_dgram) { + xqc_server_h3_ext_datagram_send(user_conn); + } +} + +static void +xqc_server_h3_ext_datagram_acked_callback(xqc_h3_conn_t *conn, uint64_t dgram_id, void *user_data) +{ + +} + +static int +xqc_server_h3_ext_datagram_lost_callback(xqc_h3_conn_t *conn, uint64_t dgram_id, void *user_data) +{ + user_conn_t *user_conn = (user_conn_t*)user_data; + user_conn->dgram_blk->data_lost += 0; + user_conn->dgram_blk->dgram_lost++; + return 0; +} + void xqc_server_set_event_timer(xqc_msec_t wake_after, void *user_data) { @@ -255,6 +651,23 @@ int xqc_server_conn_create_notify(xqc_connection_t *conn, const xqc_cid_t *cid, void *user_data, void *conn_proto_data) { DEBUG; + user_conn_t *user_conn = (user_conn_t*)user_data; + + user_conn->quic_conn = conn; + user_conn->dgram_blk = calloc(1, sizeof(user_dgram_blk_t)); + user_conn->dgram_blk->data_recv = 0; + user_conn->dgram_blk->data_sent = 0; + + xqc_datagram_set_user_data(conn, user_conn); + + if (g_send_dgram) { + user_conn->dgram_blk->data = calloc(1, g_send_body_size); + user_conn->dgram_blk->data_len = g_send_body_size; + if (!g_echo) { + user_conn->dgram_blk->to_send_size = g_send_body_size; + } + } + return 0; } @@ -262,10 +675,21 @@ int xqc_server_conn_close_notify(xqc_connection_t *conn, const xqc_cid_t *cid, void *user_data, void *conn_proto_data) { DEBUG; - user_conn_t *user_conn = (user_conn_t *)conn_proto_data; + user_conn_t *user_conn = (user_conn_t *)user_data; xqc_conn_stats_t stats = xqc_conn_get_stats(ctx.engine, cid); - printf("send_count:%u, lost_count:%u, tlp_count:%u, recv_count:%u, srtt:%"PRIu64" early_data_flag:%d, conn_err:%d, ack_info:%s\n", - stats.send_count, stats.lost_count, stats.tlp_count, stats.recv_count, stats.srtt, stats.early_data_flag, stats.conn_err, stats.ack_info); + printf("send_count:%u, lost_count:%u, lost_dgram_count:%u, tlp_count:%u, recv_count:%u, srtt:%"PRIu64" early_data_flag:%d, conn_err:%d, ack_info:%s, alpn:%s\n", + stats.send_count, stats.lost_count, stats.lost_dgram_count, stats.tlp_count, stats.recv_count, stats.srtt, stats.early_data_flag, stats.conn_err, stats.ack_info, stats.alpn); + + printf("[dgram]|recv_dgram_bytes:%zu|sent_dgram_bytes:%zu|lost_dgram_bytes:%zu|lost_cnt:%zu|\n", + user_conn->dgram_blk->data_recv, user_conn->dgram_blk->data_sent, + user_conn->dgram_blk->data_lost, user_conn->dgram_blk->dgram_lost); + + if (user_conn->dgram_blk) { + if (user_conn->dgram_blk->data) { + free(user_conn->dgram_blk->data); + } + free(user_conn->dgram_blk); + } free(user_conn); @@ -282,6 +706,7 @@ xqc_server_conn_handshake_finished(xqc_connection_t *conn, void *user_data, void { DEBUG; user_conn_t *user_conn = (user_conn_t *) user_data; + printf("datagram_mss:%zd\n", xqc_datagram_get_mss(conn)); } void @@ -349,7 +774,7 @@ xqc_server_stream_send(xqc_stream_t *stream, void *user_data) return 0; } else { - printf("xqc_stream_send sent_bytes=%"PRIu64"\n", ret); + printf("xqc_stream_send sent_bytes=%zd\n", ret); } } else { ret = xqc_stream_send(stream, user_stream->send_body + user_stream->send_offset, user_stream->send_body_len - user_stream->send_offset, 1); @@ -385,6 +810,14 @@ xqc_server_stream_create_notify(xqc_stream_t *stream, void *user_data) user_stream->stream = stream; xqc_stream_set_user_data(stream, user_stream); + if (g_test_case == 99) { + xqc_stream_send(stream, NULL, 0, 1); + } + + if (g_test_case == 15) { + return -1; + } + return 0; } @@ -482,6 +915,22 @@ xqc_server_h3_conn_create_notify(xqc_h3_conn_t *h3_conn, const xqc_cid_t *cid, v /* user_conn_t *user_conn = (xqc_server_ctx_t*)conn_user_data; */ user_conn_t *user_conn = calloc(1, sizeof(user_conn_t)); + + user_conn->h3_conn = h3_conn; + user_conn->dgram_blk = calloc(1, sizeof(user_dgram_blk_t)); + user_conn->dgram_blk->data_recv = 0; + user_conn->dgram_blk->data_sent = 0; + + xqc_h3_ext_datagram_set_user_data(h3_conn, user_conn); + + if (g_send_dgram) { + user_conn->dgram_blk->data = calloc(1, g_send_body_size); + user_conn->dgram_blk->data_len = g_send_body_size; + if (!g_echo) { + user_conn->dgram_blk->to_send_size = user_conn->dgram_blk->data_len; + } + } + xqc_h3_conn_set_user_data(h3_conn, user_conn); xqc_h3_conn_get_peer_addr(h3_conn, (struct sockaddr *)&user_conn->peer_addr, @@ -498,8 +947,19 @@ xqc_server_h3_conn_close_notify(xqc_h3_conn_t *h3_conn, const xqc_cid_t *cid, vo DEBUG; user_conn_t *user_conn = (user_conn_t*)conn_user_data; xqc_conn_stats_t stats = xqc_conn_get_stats(ctx.engine, cid); - printf("send_count:%u, lost_count:%u, tlp_count:%u, recv_count:%u, srtt:%"PRIu64" early_data_flag:%d, conn_err:%d, ack_info:%s, conn_info:%s\n", - stats.send_count, stats.lost_count, stats.tlp_count, stats.recv_count, stats.srtt, stats.early_data_flag, stats.conn_err, stats.ack_info, stats.conn_info); + printf("send_count:%u, lost_count:%u, tlp_count:%u, recv_count:%u, srtt:%"PRIu64" early_data_flag:%d, conn_err:%d, ack_info:%s, conn_info:%s, alpn:%s\n", + stats.send_count, stats.lost_count, stats.tlp_count, stats.recv_count, stats.srtt, stats.early_data_flag, stats.conn_err, stats.ack_info, stats.conn_info, stats.alpn); + + printf("[h3-dgram]|recv_dgram_bytes:%zu|sent_dgram_bytes:%zu|lost_dgram_bytes:%zu|lost_cnt:%zu|\n", + user_conn->dgram_blk->data_recv, user_conn->dgram_blk->data_sent, + user_conn->dgram_blk->data_lost, user_conn->dgram_blk->dgram_lost); + + if (user_conn->dgram_blk) { + if (user_conn->dgram_blk->data) { + free(user_conn->dgram_blk->data); + } + free(user_conn->dgram_blk); + } free(user_conn); @@ -518,6 +978,20 @@ xqc_server_h3_conn_handshake_finished(xqc_h3_conn_t *h3_conn, void *conn_user_da user_conn_t *user_conn = (user_conn_t *)conn_user_data; xqc_conn_stats_t stats = xqc_conn_get_stats(ctx.engine, &user_conn->cid); printf("0rtt_flag:%d\n", stats.early_data_flag); + printf("h3_datagram_mss:%zd\n", xqc_h3_ext_datagram_get_mss(h3_conn)); + + + /* pretend to create a server-inited http3 stream */ + if (g_test_case == 17) { + xqc_stream_t * stream = xqc_stream_create_with_direction( + xqc_h3_conn_get_xqc_conn(h3_conn), XQC_STREAM_BIDI, NULL); + printf("--- server create stream\n"); + + unsigned char szbuf[4096] = {0}; + xqc_stream_send(stream, szbuf, 4096, 1); + + } + } void @@ -536,6 +1010,138 @@ xqc_server_h3_conn_update_cid_notify(xqc_h3_conn_t *h3_conn, const xqc_cid_t *re } +int +xqc_server_bytestream_send(xqc_h3_ext_bytestream_t *h3_bs, user_stream_t *user_stream) +{ + int ret = 0; + /* echo bytestream */ + if (user_stream->send_offset < user_stream->recv_body_len || (!user_stream->echo_fin && user_stream->recv_fin)) { + ret = xqc_h3_ext_bytestream_send(h3_bs, user_stream->send_body + user_stream->send_offset, user_stream->recv_body_len - user_stream->send_offset, user_stream->recv_fin, g_dgram_qos_level); + + if (ret == -XQC_EAGAIN) { + return ret; + + } else if (ret < 0) { + printf("xqc_h3_ext_bytestream_send error %d\n", ret); + return ret; + + } else { + user_stream->snd_times++; + user_stream->send_offset += ret; + if (user_stream->recv_fin && user_stream->send_offset == user_stream->recv_body_len) { + user_stream->echo_fin = 1; + } + } + } + + return 0; +} + +int xqc_h3_ext_bytestream_create_callback(xqc_h3_ext_bytestream_t *h3_ext_bs, + void *bs_user_data) +{ + user_stream_t *user_stream = calloc(1, sizeof(user_stream_t)); + user_stream->h3_ext_bs = h3_ext_bs; + xqc_h3_ext_bytestream_set_user_data(h3_ext_bs, user_stream); + + if (user_stream->send_body == NULL) { + user_stream->send_body_max = MAX_BUF_SIZE; + user_stream->send_body_len = user_stream->send_body_max; + user_stream->send_body = malloc(user_stream->send_body_len); + user_stream->send_offset = 0; + user_stream->recv_body_len = 0; + user_stream->recv_fin = 0; + } + + if (g_test_case == 99) { + xqc_h3_ext_bytestream_finish(h3_ext_bs); + } + + printf("[bytestream]| stream: %"PRIu64" create callback|\n", xqc_h3_ext_bytestream_id(h3_ext_bs)); + + return 0; +} + +int xqc_h3_ext_bytestream_close_callback(xqc_h3_ext_bytestream_t *h3_ext_bs, + void *bs_user_data) +{ + //print stats + xqc_h3_ext_bytestream_stats_t stats = xqc_h3_ext_bytestream_get_stats(h3_ext_bs); + user_stream_t *user_stream = (user_stream_t*)bs_user_data; + + printf("[bytestream]|bytes_sent:%zu|bytes_rcvd:%zu|recv_fin:%d|snd_times:%d|rcv_times:%d|\n", stats.bytes_sent, stats.bytes_rcvd, user_stream->recv_fin, user_stream->snd_times, user_stream->rcv_times); + + if (user_stream->send_body) { + free(user_stream->send_body); + } + + if (user_stream->recv_body) { + free(user_stream->recv_body); + } + + free(user_stream); + return 0; +} + +int xqc_h3_ext_bytestream_read_callback(xqc_h3_ext_bytestream_t *h3_ext_bs, + const void *data, size_t data_len, uint8_t fin, void *bs_user_data, uint64_t data_recv_time) +{ + user_stream_t *user_stream = (user_stream_t*)bs_user_data; + int ret = 0, sent = 0; + + user_stream->recv_body_len = 0; + user_stream->recv_fin = 0; + user_stream->send_offset = 0; + + if (data_len > 0) { + memcpy(user_stream->send_body + user_stream->recv_body_len, data, data_len); + user_stream->recv_body_len += data_len; + } + + if (!user_stream->recv_fin) { + user_stream->recv_fin = fin; + } + + user_stream->rcv_times++; + + printf("[bytestream]|stream_id:%"PRIu64"|data_len:%zu|fin:%d|recv_time:%"PRIu64"|\n", + xqc_h3_ext_bytestream_id(h3_ext_bs), data_len, fin, data_recv_time); + + sent = xqc_server_bytestream_send(h3_ext_bs, user_stream); + if (sent < 0 && sent != -XQC_EAGAIN) { + //something went wrong + printf("xqc_server_bytestream_send error: %d\n", ret); + if (!(sent == -XQC_H3_BYTESTREAM_FIN_SENT && g_test_case == 99)) { + xqc_h3_ext_bytestream_close(h3_ext_bs); + } + } + + return 0; +} + +int xqc_h3_ext_bytestream_write_callback(xqc_h3_ext_bytestream_t *h3_ext_bs, + void *bs_user_data) +{ + user_stream_t *us = bs_user_data; + int ret; + printf("[bytestream]|write callback|\n"); + ret = xqc_server_bytestream_send(h3_ext_bs, us); + if (ret == -XQC_EAGAIN) { + ret = 0; + printf("[bytestream]|write blocked|\n"); + } + return 0; +} + +void +xqc_client_h3_send_pure_fin(int fd, short what, void *arg) +{ + user_stream_t *user_stream = arg; + xqc_h3_request_finish(user_stream->h3_request); +} + + + #define MAX_HEADER 100 int @@ -595,12 +1201,23 @@ xqc_server_request_send(xqc_h3_request_t *h3_request, user_stream_t *user_stream }; int header_only = 0; + int send_fin = 1; + if (g_echo && user_stream->recv_body_len == 0) { header_only = 1; } + if (g_test_case == 100) { + user_stream->ev_timeout = event_new(eb, -1, 0, xqc_client_h3_send_pure_fin, user_stream); + struct timeval tv; + tv.tv_sec = 1; + tv.tv_usec = 0; + event_add(user_stream->ev_timeout, &tv); + send_fin = 0; + } + if (user_stream->header_sent == 0) { - ret = xqc_h3_request_send_headers(h3_request, &headers, header_only); + ret = xqc_h3_request_send_headers(h3_request, &headers, header_only && send_fin); if (ret < 0) { printf("xqc_h3_request_send_headers error %zd\n", ret); return ret; @@ -654,7 +1271,7 @@ xqc_server_request_send(xqc_h3_request_t *h3_request, user_stream_t *user_stream if (user_stream->send_offset < user_stream->send_body_len) { ret = xqc_h3_request_send_body(h3_request, user_stream->send_body + user_stream->send_offset, - user_stream->send_body_len - user_stream->send_offset, 1); + user_stream->send_body_len - user_stream->send_offset, send_fin); if (ret < 0) { printf("xqc_h3_request_send_body error %zd\n", ret); return 0; @@ -687,6 +1304,17 @@ xqc_server_request_create_notify(xqc_h3_request_t *h3_request, void *strm_user_d user_stream->h3_request = h3_request; xqc_h3_request_set_user_data(h3_request, user_stream); + if (g_test_case == 99) { + xqc_h3_request_finish(h3_request); + } + + + if (g_test_case == 14) { + xqc_h3_request_close(h3_request); + return -1; + } + + return 0; } @@ -695,6 +1323,13 @@ xqc_server_request_close_notify(xqc_h3_request_t *h3_request, void *user_data) { DEBUG; user_stream_t *user_stream = (user_stream_t*)user_data; + + if (g_test_case == 100) { + if (user_stream->ev_timeout) { + event_free(user_stream->ev_timeout); + } + } + free(user_stream->send_body); free(user_stream->recv_body); free(user_stream); @@ -959,6 +1594,7 @@ xqc_server_socket_read_handler(xqc_server_ctx_t *ctx) ssize_t recv_size = 0; unsigned char packet_buf[XQC_PACKET_TMP_BUF_LEN]; uint64_t recv_time; + xqc_int_t ret; #ifdef __linux__ int batch = 0; /* packets are not necessarily on the same connection */ @@ -995,11 +1631,17 @@ xqc_server_socket_read_handler(xqc_server_ctx_t *ctx) uint64_t recv_time = now(); for (int i = 0; i < retval; i++) { recv_sum += msgs[i].msg_len; - +#ifdef XQC_NO_PID_PACKET_PROCESS + if (xqc_engine_packet_process(ctx->engine, iovecs[i].iov_base, msgs[i].msg_len, + (struct sockaddr *) (&ctx->local_addr), ctx->local_addrlen, + (struct sockaddr *) (&pa[i]), peer_addrlen, + (xqc_msec_t)recv_time, NULL) != XQC_OK) +#else if (xqc_engine_packet_process(ctx->engine, iovecs[i].iov_base, msgs[i].msg_len, (struct sockaddr *) (&ctx->local_addr), ctx->local_addrlen, (struct sockaddr *) (&pa[i]), peer_addrlen, - (xqc_msec_t) recv_time, NULL) != XQC_OK) + XQC_UNKNOWN_PATH_ID, (xqc_msec_t)recv_time, NULL) != XQC_OK) +#endif { printf("xqc_server_read_handler: packet process err\n"); return; @@ -1038,12 +1680,21 @@ xqc_server_socket_read_handler(xqc_server_ctx_t *ctx) //printf("xqc_server_read_handler recv_size=%zd, recv_time=%llu, now=%llu, recv_total=%d\n", recv_size, recv_time, now(), ++g_recv_total); /*printf("peer_ip: %s, peer_port: %d\n", inet_ntoa(ctx->peer_addr.sin_addr), ntohs(ctx->peer_addr.sin_port)); printf("local_ip: %s, local_port: %d\n", inet_ntoa(ctx->local_addr.sin_addr), ntohs(ctx->local_addr.sin_port));*/ - if (xqc_engine_packet_process(ctx->engine, packet_buf, recv_size, - (struct sockaddr *) (&ctx->local_addr), ctx->local_addrlen, - (struct sockaddr *) (&peer_addr), peer_addrlen, - (xqc_msec_t) recv_time, NULL) != XQC_OK) + +#ifdef XQC_NO_PID_PACKET_PROCESS + ret = xqc_engine_packet_process(ctx->engine, packet_buf, recv_size, + (struct sockaddr *) (&ctx->local_addr), ctx->local_addrlen, + (struct sockaddr *) (&peer_addr), peer_addrlen, + (xqc_msec_t) recv_time, NULL); +#else + ret = xqc_engine_packet_process(ctx->engine, packet_buf, recv_size, + (struct sockaddr *) (&ctx->local_addr), ctx->local_addrlen, + (struct sockaddr *) (&peer_addr), peer_addrlen, + XQC_UNKNOWN_PATH_ID, (xqc_msec_t) recv_time, NULL); +#endif + if (ret != XQC_OK) { - printf("xqc_server_read_handler: packet process err\n"); + printf("xqc_server_read_handler: packet process err: %d\n", ret); return; } } while (recv_size > 0); @@ -1307,14 +1958,15 @@ xqc_server_close_keylog_file(xqc_server_ctx_t *ctx) void -xqc_keylog_cb(const char *line, void *user_data) +xqc_keylog_cb(const xqc_cid_t *scid, const char *line, void *user_data) { xqc_server_ctx_t *ctx = (xqc_server_ctx_t*)user_data; if (ctx->keylog_fd <= 0) { printf("write keys error!\n"); return; } - + + printf("scid:%s\n", xqc_scid_str(scid)); int write_len = write(ctx->keylog_fd, line, strlen(line)); if (write_len < 0) { printf("write keys failed, errno: %d\n", errno); @@ -1404,7 +2056,10 @@ void usage(int argc, char *argv[]) { " -K load balance id encryption key\n" " -o Output log file path, default ./slog\n" " -m Set mpshell on.\n" +" -y Multipath backup path standby.\n" " -Q Multipath backup path standby, set backup_mode on(1). default backup_mode is 0(off).\n" +" -H Disable h3_ext.\n" +" -U Send_datagram 0 (off), 1 (on), 2(on + batch).\n" , prog); } @@ -1424,6 +2079,13 @@ int main(int argc, char *argv[]) { g_read_body = 0; g_spec_url = 0; g_ipv6 = 0; + g_max_dgram_size = 0; + g_send_dgram = 0; + g_copa_ai = 1.0; + g_copa_delta = 0.05; + g_enable_h3_ext = 1; + g_dgram_qos_level = XQC_DATA_QOS_HIGH; + g_pmtud_on = 0; char server_addr[64] = TEST_ADDR; int server_port = TEST_PORT; @@ -1436,9 +2098,32 @@ int main(int argc, char *argv[]) { //ensure the random sequence is the same for every test srand(0); + int long_opt_index; + + const struct option long_opts[] = { + {"copa_delta", required_argument, &long_opt_index, 1}, + {"copa_ai_unit", required_argument, &long_opt_index, 2}, + {"dgram_qos", required_argument, &long_opt_index, 3}, + {"pmtud", required_argument, &long_opt_index, 4}, + {0, 0, 0, 0} + }; + int ch = 0; - while ((ch = getopt(argc, argv, "a:p:ec:Cs:w:r:l:u:x:6bS:MR:o:EK:mLQ")) != -1) { + while ((ch = getopt_long(argc, argv, "a:p:ec:Cs:w:r:l:u:x:6bS:MR:o:EK:mLQ:U:yH", long_opts, NULL)) != -1) { switch (ch) { + case 'H': + printf("option disable h3_ext\n"); + g_enable_h3_ext = 0; + break; + case 'U': + printf("option send_datagram 0 (off), 1 (on), 2(on + batch): %s\n", optarg); + g_send_dgram = atoi(optarg); + break; + case 'Q': + /* max_datagram_frame_size */ + printf("option max_datagram_frame_size: %s\n", optarg); + g_max_dgram_size = atoi(optarg); + break; case 'a': printf("option addr :%s\n", optarg); snprintf(server_addr, sizeof(server_addr), optarg); @@ -1528,7 +2213,7 @@ int main(int argc, char *argv[]) { g_sid_len = strlen(g_sid); break; case 'M': - printf("option enable multi-path: %s\n", optarg); + printf("option enable multi-path: %s\n", "on"); g_enable_multipath = 1; break; case 'R': @@ -1553,10 +2238,57 @@ int main(int argc, char *argv[]) { /* mpshell 限定 */ g_mpshell = 1; break; - case 'Q': + case 'y': printf("option multipath backup path standby :%s\n", "on"); g_mp_backup_mode = 1; break; + /* long options */ + case 0: + + switch (long_opt_index) + { + case 1: /* copa_delta */ + g_copa_delta = atof(optarg); + if (g_copa_delta <= 0 || g_copa_delta > 0.5) { + printf("option g_copa_delta must be in (0, 0.5]\n"); + exit(0); + } else { + printf("option g_copa_delta: %.4lf\n", g_copa_delta); + } + break; + + case 2: /* copa_ai_unit */ + + g_copa_ai = atof(optarg); + if (g_copa_ai < 1.0) { + printf("option g_copa_ai must be greater than 1.0\n"); + exit(0); + } else { + printf("option g_copa_ai: %.4lf\n", g_copa_ai); + } + break; + + case 3: + g_dgram_qos_level = atoi(optarg); + if (g_dgram_qos_level < XQC_DATA_QOS_HIGHEST || g_dgram_qos_level > XQC_DATA_QOS_LOWEST) { + printf("invalid qos level!\n"); + exit(0); + } else { + printf("option g_dgram_qos_level: %d\n", g_dgram_qos_level); + } + break; + + case 4: + g_pmtud_on = atoi(optarg); + printf("option g_pmtud_on: %d\n", g_pmtud_on); + break; + + default: + break; + } + + break; + default: printf("other option :%c\n", ch); @@ -1623,7 +2355,7 @@ int main(int argc, char *argv[]) { } #endif } -#ifndef XQC_DISABLE_RENO +#ifdef XQC_ENABLE_RENO else if (c_cong_ctl == 'r') { cong_ctrl = xqc_reno_cb; } @@ -1642,8 +2374,20 @@ int main(int argc, char *argv[]) { #endif } #endif +#ifdef XQC_ENABLE_UNLIMITED + else if (c_cong_ctl == 'u') { + cong_ctrl = xqc_unlimited_cc_cb; + + } +#endif +#ifdef XQC_ENABLE_COPA + else if (c_cong_ctl == 'P') { + cong_ctrl = xqc_copa_cb; + + } +#endif else { - printf("unknown cong_ctrl, option is b, r, c\n"); + printf("unknown cong_ctrl, option is b, r, c, u\n"); return -1; } printf("congestion control flags: %x\n", cong_flags); @@ -1655,11 +2399,21 @@ int main(int argc, char *argv[]) { .customize_on = 1, .init_cwnd = 32, .cc_optimization_flags = cong_flags, + .copa_delta_ai_unit = g_copa_ai, + .copa_delta_base = g_copa_delta, }, .enable_multipath = g_enable_multipath, + // .multipath_version = g_multipath_version, .spurious_loss_detect_on = 0, + .max_datagram_frame_size = g_max_dgram_size, + // .datagram_force_retrans_on = 1, + .marking_reinjection = 1, }; + if (g_pmtud_on) { + conn_settings.enable_pmtud = 1; + } + if (g_test_case == 6) { conn_settings.idle_time_out = 10000; } @@ -1670,9 +2424,18 @@ int main(int argc, char *argv[]) { } /* enable_reinjection */ - if (g_enable_reinjection) { - conn_settings.reinj_ctl_callback = xqc_xlink_reinj_ctl_cb; - conn_settings.mp_enable_reinjection = g_enable_reinjection; + if (g_enable_reinjection == 1) { + conn_settings.reinj_ctl_callback = xqc_default_reinj_ctl_cb; + conn_settings.mp_enable_reinjection = 1; + + } else if (g_enable_reinjection == 2) { + conn_settings.reinj_ctl_callback = xqc_deadline_reinj_ctl_cb; + conn_settings.mp_enable_reinjection = 2; + + } else if (g_enable_reinjection == 3) { + conn_settings.reinj_ctl_callback = xqc_dgram_reinj_ctl_cb; + conn_settings.mp_enable_reinjection = 4; + conn_settings.scheduler_callback = xqc_rap_scheduler_cb; } if (g_mp_backup_mode) { @@ -1685,7 +2448,28 @@ int main(int argc, char *argv[]) { /* test reset, destroy connection as soon as possible */ if (g_test_case == 13) { - conn_settings.idle_time_out = 1; + conn_settings.idle_time_out = 1000; + } + + if (g_test_case == 201) { + conn_settings.max_pkt_out_size = 1216; + } + + if (g_test_case == 208) { + conn_settings.datagram_redundancy = 1; + } + + if (g_test_case == 209) { + conn_settings.datagram_redundant_probe = 30000; + } + + if (g_test_case == 210) { + conn_settings.datagram_redundancy = 2; + } + + if (g_test_case == 211) { + conn_settings.datagram_redundancy = 2; + conn_settings.datagram_redundant_probe = 30000; } xqc_server_set_conn_settings(&conn_settings); @@ -1695,6 +2479,8 @@ int main(int argc, char *argv[]) { return -1; } + config.enable_h3_ext = g_enable_h3_ext; + switch(c_log_level) { case 'e': config.cfg_log_level = XQC_LOG_ERROR; break; case 'i': config.cfg_log_level = XQC_LOG_INFO; break; @@ -1750,7 +2536,20 @@ int main(int argc, char *argv[]) { .h3_request_read_notify = xqc_server_request_read_notify, .h3_request_create_notify = xqc_server_request_create_notify, .h3_request_close_notify = xqc_server_request_close_notify, - } + }, + .h3_ext_dgram_cbs = { + .dgram_read_notify = xqc_server_h3_ext_datagram_read_callback, + .dgram_write_notify = xqc_server_h3_ext_datagram_write_callback, + .dgram_acked_notify = xqc_server_h3_ext_datagram_acked_callback, + .dgram_lost_notify = xqc_server_h3_ext_datagram_lost_callback, + .dgram_mss_updated_notify = xqc_server_h3_ext_datagram_mss_updated_callback, + }, + .h3_ext_bs_cbs = { + .bs_read_notify = xqc_h3_ext_bytestream_read_callback, + .bs_write_notify = xqc_h3_ext_bytestream_write_callback, + .bs_create_notify = xqc_h3_ext_bytestream_create_callback, + .bs_close_notify = xqc_h3_ext_bytestream_close_callback, + }, }; /* register transport callbacks */ @@ -1765,7 +2564,14 @@ int main(int argc, char *argv[]) { .stream_read_notify = xqc_server_stream_read_notify, .stream_create_notify = xqc_server_stream_create_notify, .stream_close_notify = xqc_server_stream_close_notify, - } + }, + .dgram_cbs = { + .datagram_acked_notify = xqc_server_datagram_acked_callback, + .datagram_lost_notify = xqc_server_datagram_lost_callback, + .datagram_read_notify = xqc_server_datagram_read_callback, + .datagram_write_notify = xqc_server_datagram_write_callback, + .datagram_mss_updated_notify = xqc_server_datagram_mss_updated_callback, + }, }; diff --git a/tests/unittest/main.c b/tests/unittest/main.c index 931b139f8..56b337ab3 100644 --- a/tests/unittest/main.c +++ b/tests/unittest/main.c @@ -14,7 +14,6 @@ #include "xqc_common_test.h" #include "xqc_vint_test.h" #include "xqc_recv_record_test.h" -#include "xqc_sent_record_test.h" #include "xqc_reno_test.h" #include "xqc_cubic_test.h" #include "xqc_packet_test.h" @@ -37,6 +36,8 @@ #include "xqc_cid_test.h" #include "xqc_id_hash_test.h" #include "xqc_retry_test.h" +#include "xqc_datagram_test.h" +#include "xqc_h3_ext_test.h" static int xqc_init_suite(void) { return 0; } @@ -60,8 +61,6 @@ main() } if (!CU_add_test(pSuite, "xqc_test_get_random", xqc_test_get_random) - || !CU_add_test(pSuite, "xqc_test_sent_record", xqc_test_sent_record) - || !CU_add_test(pSuite, "xqc_test_sent_record_get_largest_pn_in_ack", xqc_test_sent_record_get_largest_pn_in_ack) || !CU_add_test(pSuite, "xqc_test_engine_create", xqc_test_engine_create) || !CU_add_test(pSuite, "xqc_test_conn_create", xqc_test_conn_create) || !CU_add_test(pSuite, "xqc_test_pq", xqc_test_pq) @@ -97,6 +96,8 @@ main() || !CU_add_test(pSuite, "xqc_cid_test", xqc_test_cid) || !CU_add_test(pSuite, "xqc_test_id_hash", xqc_test_id_hash) || !CU_add_test(pSuite, "xqc_test_retry", xqc_test_retry) + || !CU_add_test(pSuite, "xqc_test_receive_invalid_dgram", xqc_test_receive_invalid_dgram) + || !CU_add_test(pSuite, "xqc_test_h3_ext_frame", xqc_test_h3_ext_frame) /* ADD TESTS HERE */) { CU_cleanup_registry(); diff --git a/tests/unittest/xqc_datagram_test.c b/tests/unittest/xqc_datagram_test.c new file mode 100644 index 000000000..8a3b3a77d --- /dev/null +++ b/tests/unittest/xqc_datagram_test.c @@ -0,0 +1,49 @@ +/** + * @copyright Copyright (c) 2022, Alibaba Group Holding Limited + */ + +#include "xqc_datagram_test.h" +#include +#include "src/transport/xqc_conn.h" +#include "src/transport/xqc_engine.h" +#include "src/transport/xqc_frame.h" +#include "src/transport/xqc_frame_parser.h" +#include "src/transport/xqc_packet_out.h" +#include "src/transport/xqc_packet_in.h" +#include "xqc_common_test.h" + +void +xqc_test_receive_invalid_dgram() +{ + xqc_int_t ret; + + xqc_connection_t *conn = test_engine_connect(); + CU_ASSERT(conn != NULL); + + const unsigned char payload[100]; + + xqc_packet_out_t *packet_out; + packet_out = xqc_write_new_packet(conn, XQC_PTYPE_SHORT_HEADER); + CU_ASSERT(packet_out != NULL); + + ret = xqc_gen_datagram_frame(packet_out, payload, (size_t)100); + CU_ASSERT(ret == XQC_OK); + + xqc_packet_in_t pkt_in; + pkt_in.pos = packet_out->po_payload; + pkt_in.last = packet_out->po_buf + packet_out->po_used_size; + conn->local_settings.max_datagram_frame_size = 0; + + ret = xqc_process_datagram_frame(conn, &pkt_in); + CU_ASSERT(ret == -XQC_EPROTO); + + conn->local_settings.max_datagram_frame_size = 50; + ret = xqc_process_datagram_frame(conn, &pkt_in); + CU_ASSERT(ret == -XQC_EPROTO); + + conn->local_settings.max_datagram_frame_size = 120; + ret = xqc_process_datagram_frame(conn, &pkt_in); + CU_ASSERT(ret == XQC_OK); + + xqc_engine_destroy(conn->engine); +} \ No newline at end of file diff --git a/tests/unittest/xqc_datagram_test.h b/tests/unittest/xqc_datagram_test.h new file mode 100644 index 000000000..a162d5042 --- /dev/null +++ b/tests/unittest/xqc_datagram_test.h @@ -0,0 +1,10 @@ +/** + * @copyright Copyright (c) 2022, Alibaba Group Holding Limited + */ + +#ifndef _XQC_DATAGRAM_TEST_H_INCLUDED_ +#define _XQC_DATAGRAM_TEST_H_INCLUDED_ + +void xqc_test_receive_invalid_dgram(); + +#endif /* _XQC_CUBIC_TEST_H_INCLUDED_ */ diff --git a/tests/unittest/xqc_engine_test.c b/tests/unittest/xqc_engine_test.c index 8143e07ff..003354d86 100644 --- a/tests/unittest/xqc_engine_test.c +++ b/tests/unittest/xqc_engine_test.c @@ -45,11 +45,19 @@ xqc_test_engine_packet_process() xqc_msec_t recv_time = xqc_monotonic_timestamp(); +#ifdef XQC_NO_PID_PACKET_PROCESS xqc_int_t rc = xqc_engine_packet_process(engine, XQC_TEST_LONG_HEADER_PACKET_B, sizeof(XQC_TEST_LONG_HEADER_PACKET_B) - 1, (struct sockaddr *)(&local_addr), local_addrlen, (struct sockaddr *)(&peer_addr), peer_addrlen, recv_time, NULL); +#else + xqc_int_t rc = xqc_engine_packet_process(engine, XQC_TEST_LONG_HEADER_PACKET_B, + sizeof(XQC_TEST_LONG_HEADER_PACKET_B) - 1, + (struct sockaddr *)(&local_addr), local_addrlen, + (struct sockaddr *)(&peer_addr), peer_addrlen, + XQC_UNKNOWN_PATH_ID, recv_time, NULL); +#endif //CU_ASSERT(rc == XQC_OK); /* get connection */ @@ -68,10 +76,17 @@ xqc_test_engine_packet_process() conn->conn_flag |= XQC_CONN_FLAG_HANDSHAKE_COMPLETED; recv_time = xqc_monotonic_timestamp(); +#ifdef XQC_NO_PID_PACKET_PROCESS rc = xqc_engine_packet_process(engine, XQC_TEST_SHORT_HEADER_PACKET_A, sizeof(XQC_TEST_SHORT_HEADER_PACKET_A) - 1, (struct sockaddr *)&local_addr, local_addrlen, (struct sockaddr *)&peer_addr, peer_addrlen, recv_time, NULL); +#else + rc = xqc_engine_packet_process(engine, XQC_TEST_SHORT_HEADER_PACKET_A, + sizeof(XQC_TEST_SHORT_HEADER_PACKET_A) - 1, + (struct sockaddr *)&local_addr, local_addrlen, + (struct sockaddr *)&peer_addr, peer_addrlen, XQC_UNKNOWN_PATH_ID, recv_time, NULL); +#endif //CU_ASSERT(rc == XQC_OK); xqc_engine_destroy(engine); diff --git a/tests/unittest/xqc_h3_ext_test.c b/tests/unittest/xqc_h3_ext_test.c new file mode 100644 index 000000000..0da581d0e --- /dev/null +++ b/tests/unittest/xqc_h3_ext_test.c @@ -0,0 +1,87 @@ +/** + * @copyright Copyright (c) 2022, Alibaba Group Holding Limited + */ + +#include +#include +#include +#include "src/http3/frame/xqc_h3_frame.h" +#include "src/http3/xqc_h3_conn.h" +#include "src/http3/xqc_h3_stream.h" +#include "src/http3/xqc_h3_ext_bytestream.h" + +#include "xqc_common_test.h" + + +ssize_t +xqc_test_h3_ext_frame_parse(const char *p, size_t sz, xqc_h3_frame_pctx_t *state) +{ + ssize_t offset = 0; + while (offset < sz) { + ssize_t len = rand() % sz + 1; + ssize_t ret = xqc_h3_frm_parse(p + offset, len, state); + if (ret < 0) { + return ret; + } + if (ret == 0) { + return offset; + } + offset += ret; + if (state->state == XQC_H3_FRM_STATE_END) { + return offset; + } + } + return XQC_ERROR; +} + +void +xqc_test_h3_ext_frame() +{ + uint64_t push_id = 10; + xqc_h3_ext_frame_bidi_stream_type_t stream_type; + + xqc_list_head_t send_buf; + xqc_init_list_head(&send_buf); + + xqc_h3_frame_pctx_t pctx; + memset(&pctx, 0, sizeof(xqc_h3_frame_pctx_t)); + ssize_t processed; + + /* write */ + /* write bidi_stream_type frame */ + xqc_int_t ret = xqc_h3_ext_frm_write_bidi_stream_type(&send_buf, XQC_H3_BIDI_STREAM_TYPE_REQUEST, XQC_TRUE); + CU_ASSERT(ret == XQC_OK); + /* write bidi_stream_type frame */ + ret = xqc_h3_ext_frm_write_bidi_stream_type(&send_buf, XQC_H3_BIDI_STREAM_TYPE_BYTESTREAM, XQC_TRUE); + CU_ASSERT(ret == XQC_OK); + + xqc_var_buf_t *buf = xqc_var_buf_create(XQC_VAR_BUF_INIT_SIZE); + xqc_list_head_t *pos, *next; + xqc_list_for_each_safe(pos, next, &send_buf) { + xqc_list_buf_t *list_buf = xqc_list_entry(pos, xqc_list_buf_t, list_head); + xqc_var_buf_t *data_buf = list_buf->buf; + xqc_var_buf_save_data(buf, data_buf->data, data_buf->data_len); + + xqc_list_del(&list_buf->list_head); + xqc_var_buf_free(data_buf); + xqc_free(list_buf); + } + + /* parse */ + /* parse bidi_stream_type frame */ + processed = xqc_test_h3_ext_frame_parse(buf->data + buf->consumed_len, buf->data_len - buf->consumed_len, &pctx); + CU_ASSERT(processed > 0); + CU_ASSERT(pctx.state == XQC_H3_FRM_STATE_END); + CU_ASSERT(pctx.frame.frame_payload.stream_type.stream_type.vi == XQC_H3_BIDI_STREAM_TYPE_REQUEST); + buf->consumed_len += processed; + xqc_h3_frm_reset_pctx(&pctx); + /* parse bidi_stream_type frame */ + processed = xqc_test_h3_ext_frame_parse(buf->data + buf->consumed_len, buf->data_len - buf->consumed_len, &pctx); + CU_ASSERT(processed > 0); + CU_ASSERT(pctx.state == XQC_H3_FRM_STATE_END); + CU_ASSERT(pctx.frame.frame_payload.stream_type.stream_type.vi == XQC_H3_BIDI_STREAM_TYPE_BYTESTREAM); + buf->consumed_len += processed; + xqc_h3_frm_reset_pctx(&pctx); + + xqc_var_buf_free(buf); +} \ No newline at end of file diff --git a/tests/unittest/xqc_h3_ext_test.h b/tests/unittest/xqc_h3_ext_test.h new file mode 100644 index 000000000..ae4312484 --- /dev/null +++ b/tests/unittest/xqc_h3_ext_test.h @@ -0,0 +1,10 @@ +/** + * @copyright Copyright (c) 2022, Alibaba Group Holding Limited + */ + +#ifndef XQUIC_XQC_H3_EXT_TEST_H +#define XQUIC_XQC_H3_EXT_TEST_H + +void xqc_test_h3_ext_frame(); + +#endif //XQUIC_XQC_H3_EXT_TEST_H diff --git a/tests/unittest/xqc_h3_test.c b/tests/unittest/xqc_h3_test.c index 0675253e3..74f48f8ee 100644 --- a/tests/unittest/xqc_h3_test.c +++ b/tests/unittest/xqc_h3_test.c @@ -430,7 +430,7 @@ xqc_test_stream() xqc_connection_t *conn = test_engine_connect(); CU_ASSERT(conn != NULL); - xqc_stream_t *stream = xqc_create_stream_with_conn(conn, XQC_UNDEFINE_STREAM_ID, XQC_CLI_UNI, NULL); + xqc_stream_t *stream = xqc_create_stream_with_conn(conn, XQC_UNDEFINE_STREAM_ID, XQC_CLI_UNI, NULL, NULL); CU_ASSERT(stream != NULL); xqc_h3_conn_t *h3c = xqc_h3_conn_create(conn, NULL); diff --git a/tests/unittest/xqc_process_frame_test.c b/tests/unittest/xqc_process_frame_test.c index 265d05549..5f33bb270 100644 --- a/tests/unittest/xqc_process_frame_test.c +++ b/tests/unittest/xqc_process_frame_test.c @@ -12,7 +12,6 @@ char XQC_TEST_ILL_FRAME_1[] = {0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; char XQC_TEST_ZERO_LEN_NEW_TOKEN_FRAME[] = {0x07, 0x00}; -char XQC_TEST_OVERAGE_STREAM_BLOCKED_FRAME[] = {0x16, 0xD0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01}; void @@ -32,11 +31,6 @@ xqc_test_process_frame() ret = xqc_process_frames(conn, &packet_in); CU_ASSERT(ret == -XQC_EPROTO); - packet_in.pos = XQC_TEST_OVERAGE_STREAM_BLOCKED_FRAME; - packet_in.last = packet_in.pos + sizeof(XQC_TEST_OVERAGE_STREAM_BLOCKED_FRAME); - ret = xqc_process_frames(conn, &packet_in); - CU_ASSERT(ret == -XQC_EPARAM); - xqc_engine_destroy(conn->engine); } diff --git a/tests/unittest/xqc_reno_test.c b/tests/unittest/xqc_reno_test.c index f1c1e5f64..d97f962d5 100644 --- a/tests/unittest/xqc_reno_test.c +++ b/tests/unittest/xqc_reno_test.c @@ -20,7 +20,7 @@ print_reno(xqc_new_reno_t *reno) void xqc_test_reno() { -#ifdef XQC_DISABLE_RENO +#ifndef XQC_ENABLE_RENO return; #endif xqc_msec_t now = xqc_monotonic_timestamp(); diff --git a/tests/unittest/xqc_sent_record_test.c b/tests/unittest/xqc_sent_record_test.c deleted file mode 100644 index e81e76a67..000000000 --- a/tests/unittest/xqc_sent_record_test.c +++ /dev/null @@ -1,107 +0,0 @@ -/** - * @copyright Copyright (c) 2022, Alibaba Group Holding Limited - */ - -#include "xqc_sent_record_test.h" -#include "src/transport/xqc_send_ctl.h" -#include "src/common/xqc_list.h" -#include - - -void -xqc_test_sent_record() -{ - int pn_array[10] = {1, 13, 66, 98, 101, 102, 233, 234, 235}; - int i = 0; - int ret; - xqc_sent_record_t sent_record; - - xqc_sent_record_init(&sent_record); - - for (i = 0; i < 9; i++) { - xqc_sent_record_add(&sent_record, pn_array[i], 0); - } - - xqc_list_head_t *pos, *next; - xqc_packet_number_node_t *pnode; - i = 0; - xqc_list_for_each_safe(pos, next, &sent_record.sent_pn_list) { - pnode = xqc_list_entry(pos, xqc_packet_number_node_t, pn_list); - // printf("|pn[%d]:%d|", i, (int)pnode->pkt_num); - CU_ASSERT(pn_array[i++] == pnode->pkt_num); - } - - - xqc_packet_number_t gap; - - ret = xqc_sent_record_pn_gap(&sent_record, 13, 102, &gap); - CU_ASSERT(XQC_OK == ret && 4 == gap); - - ret = xqc_sent_record_pn_gap(&sent_record, 14, 102, &gap); - CU_ASSERT(XQC_ERROR == ret); - - ret = xqc_sent_record_pn_gap(&sent_record, 66, 103, &gap); - CU_ASSERT(XQC_ERROR == ret); - - ret = xqc_sent_record_pn_gap(&sent_record, 235, 236, &gap); - CU_ASSERT(XQC_ERROR == ret); - - ret = xqc_sent_record_pn_gap(&sent_record, 98, 98, &gap); - CU_ASSERT(XQC_OK == ret && 0 == gap); - - - xqc_packet_number_t lost_sent_pn; - - ret = xqc_sent_record_lost_sent_pn(&sent_record, 66, 5, &lost_sent_pn); - CU_ASSERT(XQC_OK == ret && XQC_MAX_UINT64_VALUE == lost_sent_pn); - - ret = xqc_sent_record_lost_sent_pn(&sent_record, 101, 5, &lost_sent_pn); - CU_ASSERT(XQC_OK == ret && XQC_MAX_UINT64_VALUE == lost_sent_pn); - - ret = xqc_sent_record_lost_sent_pn(&sent_record, 234, 5, &lost_sent_pn); - // printf("|lost_sent_pn:%d|", (int)lost_sent_pn); - CU_ASSERT(XQC_OK == ret && 66 == lost_sent_pn); - - ret = xqc_sent_record_lost_sent_pn(&sent_record, 105, 5, &lost_sent_pn); - CU_ASSERT(XQC_ERROR == ret && XQC_MAX_UINT64_VALUE == lost_sent_pn); - - sent_record.latest_rtt_pn = 67; - xqc_sent_record_del(&sent_record); - - ret = xqc_sent_record_lost_sent_pn(&sent_record, 234, 5, &lost_sent_pn); - CU_ASSERT(XQC_OK == ret && XQC_MAX_UINT64_VALUE == lost_sent_pn); - - ret = xqc_sent_record_lost_sent_pn(&sent_record, 236, 5, &lost_sent_pn); - CU_ASSERT(XQC_ERROR == ret && XQC_MAX_UINT64_VALUE == lost_sent_pn); - - xqc_sent_record_release(&sent_record); -} - - - -void -xqc_test_sent_record_get_largest_pn_in_ack() -{ - xqc_ack_info_t ack_info; - ack_info.n_ranges = 2; - ack_info.ranges[0].high = 10; - ack_info.ranges[0].low = 8; - ack_info.ranges[1].high = 6; - ack_info.ranges[1].low = 2; - - int pn_array[10] = {5, 6, 8, 11, 12, 102, 233, 234, 235}; - - xqc_sent_record_t sent_record; - xqc_sent_record_init(&sent_record); - - for (int i = 0; i < 9; i++) { - xqc_sent_record_add(&sent_record, pn_array[i], 0); - } - - xqc_packet_number_node_t *largest_pn_node = NULL; - - int ret = xqc_sent_record_get_largest_pn_in_ack(&sent_record, &ack_info, &largest_pn_node); - CU_ASSERT(XQC_OK == ret && 8 == largest_pn_node->pkt_num); - - xqc_sent_record_release(&sent_record); -} \ No newline at end of file diff --git a/tests/unittest/xqc_sent_record_test.h b/tests/unittest/xqc_sent_record_test.h deleted file mode 100644 index 97934b26b..000000000 --- a/tests/unittest/xqc_sent_record_test.h +++ /dev/null @@ -1,12 +0,0 @@ -/** - * @copyright Copyright (c) 2022, Alibaba Group Holding Limited - */ - -#ifndef _XQC_SENT_RECORD_TEST_H_INCLUDED_ -#define _XQC_SENT_RECORD_TEST_H_INCLUDED_ - -void xqc_test_sent_record(); - -void xqc_test_sent_record_get_largest_pn_in_ack(); - -#endif /* _XQC_SENT_RECORD_TEST_H_INCLUDED_ */ diff --git a/xqc_build.sh b/xqc_build.sh index 7901772e7..6b1fcabc2 100755 --- a/xqc_build.sh +++ b/xqc_build.sh @@ -53,7 +53,13 @@ if [ x"$platform" == xios ] ; then -DGCOV=OFF -DCMAKE_TOOLCHAIN_FILE=${IOS_CMAKE_TOOLCHAIN} -DENABLE_BITCODE=0 - -DXQC_NO_SHARED=1" + -DXQC_NO_SHARED=1 + -DXQC_COMPAT_GENERATE_SR_PKT=1 + -DXQC_ENABLE_RENO=0 + -DXQC_ENABLE_BBR2=0 + -DXQC_ENABLE_COPA=0 + -DXQC_ENABLE_UNLIMITED=0 + -DXQC_ENABLE_MP_INTEROP=0" elif [ x"$platform" == xandroid ] ; then if [ x"$ANDROID_NDK" == x ] ; then @@ -72,11 +78,15 @@ elif [ x"$platform" == xandroid ] ; then -DCMAKE_TOOLCHAIN_FILE=$ANDROID_NDK/build/cmake/android.toolchain.cmake -DANDROID_STL=c++_shared -DANDROID_NATIVE_API_LEVEL=android-19 - -DXQC_DISABLE_RENO=OFF - -DXQC_ENABLE_BBR2=ON + -DXQC_ENABLE_RENO=OFF + -DXQC_ENABLE_BBR2=OFF + -DXQC_ENABLE_COPA=OFF + -DXQC_ENABLE_UNLIMITED=OFF + -DXQC_ENABLE_MP_INTEROP=OFF -DXQC_DISABLE_LOG=OFF - -DXQC_ONLY_ERROR_LOG=ON" -else + -DXQC_ONLY_ERROR_LOG=ON + -DXQC_COMPAT_GENERATE_SR_PKT=ON" +else echo "no support platform" exit 0 fi diff --git a/xqc_configure.h.in b/xqc_configure.h.in index c6ca83e53..05d7691cf 100644 --- a/xqc_configure.h.in +++ b/xqc_configure.h.in @@ -1,4 +1,10 @@ - /* the configured options and settings for xquic */ #define xquic_VERSION_MAJOR @xquic_VERSION_MAJOR@ #define xquic_VERSION_MINOR @xquic_VERSION_MINOR@ + +#cmakedefine XQC_ENABLE_BBR2 +#cmakedefine XQC_ENABLE_RENO +#cmakedefine XQC_ENABLE_COPA +#cmakedefine XQC_ENABLE_UNLIMITED +#cmakedefine XQC_ENABLE_MP_INTEROP +#cmakedefine XQC_NO_PID_PACKET_PROCESS \ No newline at end of file From 0ba058d5167413b41c2b13781ff35ba5eb95024a Mon Sep 17 00:00:00 2001 From: Huanhuan Zheng <140379011+cherylsy@users.noreply.github.com> Date: Tue, 24 Oct 2023 11:40:42 +0800 Subject: [PATCH 4/6] [~] Add mp option when running ./demo_server (#349) --- interop/run_endpoint.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interop/run_endpoint.sh b/interop/run_endpoint.sh index 190a1218f..4011d4fe2 100644 --- a/interop/run_endpoint.sh +++ b/interop/run_endpoint.sh @@ -97,7 +97,7 @@ elif [ "$ROLE" == "server" ]; then #cp -r /www /logs - ARGS="-l d -L "$LOG_DIR/server.log" -p 443 -D "/www" -k $SSLKEYLOGFILE" + ARGS="-l d -L "$LOG_DIR/server.log" -p 443 -D "/www" -k $SSLKEYLOGFILE -M" echo "./demo_server $ARGS" ./demo_server $ARGS fi From cba880e3bd57032586f69383472427a7abd5ed2c Mon Sep 17 00:00:00 2001 From: Huanhuan Zheng <140379011+cherylsy@users.noreply.github.com> Date: Wed, 25 Oct 2023 19:50:19 +0800 Subject: [PATCH 5/6] [~]enable gen new_conn_id (#352) --- src/transport/xqc_conn.c | 6 +++--- src/transport/xqc_engine.c | 3 +-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/transport/xqc_conn.c b/src/transport/xqc_conn.c index 3cf8efef7..63c1da34f 100644 --- a/src/transport/xqc_conn.c +++ b/src/transport/xqc_conn.c @@ -323,17 +323,17 @@ xqc_conn_init_trans_settings(xqc_connection_t *conn) /* set local default setting values */ ls->max_streams_bidi = 128; - ls->max_stream_data_bidi_remote = 16 * 1024 * 1024; + ls->max_stream_data_bidi_remote = XQC_MAX_RECV_WINDOW; if (conn->conn_settings.enable_stream_rate_limit) { ls->max_stream_data_bidi_local = conn->conn_settings.init_recv_window; } else { - ls->max_stream_data_bidi_local = 16 * 1024 * 1024; + ls->max_stream_data_bidi_local = XQC_MAX_RECV_WINDOW; } ls->max_streams_uni = 128; - ls->max_stream_data_uni = 16 * 1024 * 1024; + ls->max_stream_data_uni = XQC_MAX_RECV_WINDOW; if (conn->conn_settings.recv_rate_bytes_per_sec) { ls->max_data = conn->conn_settings.recv_rate_bytes_per_sec * XQC_FC_INIT_RTT / 1000000; diff --git a/src/transport/xqc_engine.c b/src/transport/xqc_engine.c index 5c138cc14..779d880e7 100644 --- a/src/transport/xqc_engine.c +++ b/src/transport/xqc_engine.c @@ -754,12 +754,11 @@ xqc_engine_process_conn(xqc_connection_t *conn, xqc_usec_t now) } XQC_CHECK_IMMEDIATE_CLOSE(); -#if 0 + ret = xqc_conn_try_add_new_conn_id(conn, 0); if (ret) { xqc_log(conn->log, XQC_LOG_ERROR, "|xqc_conn_try_add_new_conn_id error|"); } -#endif if (conn->enable_multipath) { From 32967424163d4c23a5ea6c93bc224ea3b7a2fb92 Mon Sep 17 00:00:00 2001 From: Huanhuan Zheng <140379011+cherylsy@users.noreply.github.com> Date: Thu, 2 Nov 2023 14:05:09 +0800 Subject: [PATCH 6/6] [~]merge interop & Solve conflicts (#358) [~] merge interop branch to main branch --- README.md | 5 +++-- docs/Platforms.md | 17 +++++++++++++---- docs/images/dingtalk_group.jpg | Bin 216989 -> 0 bytes docs/images/dingtalk_group.png | Bin 0 -> 251106 bytes scripts/case_test.sh | 26 ++++++++++++++++---------- 5 files changed, 32 insertions(+), 16 deletions(-) delete mode 100644 docs/images/dingtalk_group.jpg create mode 100644 docs/images/dingtalk_group.png diff --git a/README.md b/README.md index a6fba543a..c5480c2a7 100644 --- a/README.md +++ b/README.md @@ -65,7 +65,7 @@ To run test cases, you need ## QuickStart Guide -XQUIC supports both BabaSSL and BoringSSL. +XQUIC can be built with Tongsuo(BabaSSL) or BoringSSL. ### Build with BoringSSL @@ -86,6 +86,7 @@ SSL_PATH_STR="${PWD}" SSL_INC_PATH_STR="${PWD}/include" SSL_LIB_PATH_STR="${PWD}/build/ssl/libssl.a;${PWD}/build/crypto/libcrypto.a" cd ../.. +## Note: if you don’t have golang in your environment, please install [golang](https://go.dev/doc/install) first. # build XQUIC with BoringSSL git submodule update --init --recursive @@ -160,4 +161,4 @@ Feel free to contact us in the following ways: * Dingtalk group: 34059705 * slack channel: #xquic in quicdev group - dingtalk group + dingtalk group diff --git a/docs/Platforms.md b/docs/Platforms.md index 50a5a7652..73ddf1497 100644 --- a/docs/Platforms.md +++ b/docs/Platforms.md @@ -35,20 +35,29 @@ make -j You can use the cmake variables `-DPLATFORM=mac` to build XQUIC on macOS. +Remember you still need to install BoringSSL or Tongsuo(BabaSSL) first, please follow to the guidance [get and build BoringSSL](https://github.com/alibaba/xquic#build-with-boringssl) / [get and build BabaSSL](https://github.com/alibaba/xquic#build-with-babassl). + ```bash -# build XQUIC with BabaSSL + +# build XQUIC with BoringSSL + git submodule update --init --recursive mkdir build; cd build -cmake -DPLATFORM=mac .. +cmake -DPLATFORM=mac -DSSL_TYPE=${SSL_TYPE_STR} -DSSL_PATH=${SSL_PATH_STR} -DSSL_INC_PATH=${SSL_INC_PATH_STR} -DSSL_LIB_PATH=${SSL_LIB_PATH_STR} -DXQC_SUPPORT_SENDMMSG_BUILD=0 .. make -j -# build XQUIC with BoringSSL + +# build XQUIC with BabaSSL git submodule update --init --recursive mkdir build; cd build -cmake -DPLATFORM=mac -DSSL_TYPE=${SSL_TYPE_STR} -DSSL_PATH=${SSL_PATH_STR} -DSSL_INC_PATH=${SSL_INC_PATH_STR} -DSSL_LIB_PATH=${SSL_LIB_PATH_STR} .. +cmake -DPLATFORM=mac -DXQC_SUPPORT_SENDMMSG_BUILD=0 .. make -j + ``` +Troubleshooting: +> Note: sendmmsg is not supported on MacOS, make sure you add -DXQC\_SUPPORT\_SENDMMSG\_BUILD=0 to turn off the feature + ## Windows Release ```bash diff --git a/docs/images/dingtalk_group.jpg b/docs/images/dingtalk_group.jpg deleted file mode 100644 index 99c40284a40ae3b4cb70cab1f051ecd1a8004521..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 216989 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�!}Z(BHsfJhUh7entwnjkGIN)Z78Dbk_>A|N15Ktdv2x`2RyfC>nT5PFAz z^xg@*7wHKlL{hx_ciwx(J?HYyB+q{Kv(}nx&bg%QM0gYH+43PT zo)nU5>g<*dO5ndoXxekH0iQAlQ7P69VYHh@{1S0kVerfItk^jhse0|BEz3BSrg}$L zZR=3_ZL+F`StNMSI1FZjwItIL2o{5U{R*xvj1yYjKdd`8o=%)>Uv3oBbf^penZyp3 z5XaM3lRlx};g@i3q+CF>6AERA-NFZc35a@JcQ?VHfA^Ae_LTB$NcKcBn+~-xWen)F zpw?B%%3P+Kk3^BDFx0{*r^9oYY}svC;^JK3gyvC@HU;0=kZ0e z;3c_li#}6twaSpdrX6D0$RNV270= zfW5VvmW5S7FvvyN7>zEE-TSeSHL}VR8%j{s8;->I#w+ ztENIux@X@E9fG`tELU+XUc`t+60hW5T5E%Jju5|GXW{=lfysykVwd$;wqRV*n-@(Mn%hoK&pgM0JYwQK@k^` zc!#@v8=V=kgmbAsq9nD@uFP%y@r=)T?X&S|jgzZ~1e{z^8?N9J9RSE^BOs0#K#1Gs zN5+7WoD1

4Q$ZJC^&%t;EtMQ=UEhNwM<$>jGS_zaYC^xtMD2{NClys?SPEaR`@f zlp8c$k!YIL9=q~7>QVi8OHb`{4D&@VNX91Tm)%VVo`7z9HyT_k=^dN{K1BvFkLUrn zuK`291T#GvFFqN(rf^q3+K{@eCPjDe8t-9# z{J4JM?RC)dGiop0 za0D*-BGdhA@-H5DGxNynHpzEHK67c}ORth?b4!W$Do72Mt#BEvGOo1YUQ4Zuf_|U| z?oj4Ol~bydht5V{@@JCN9Af1YYPlS7VzLEnm~#YH`Pi9wm3rL1jD(u)?o`M)Ee=rr zeV8xP)I26do8^ynYP)%O3p~je;JXC)1)zNt=WSu4m=6_wo!N7@%bq`XNY=l;p8O{B zjGOp_RE+`JKK_cGx1`A#sV&4113#8aMzkrOLO|qk z@`b4{`0p6-FYkH$vqupyU8X4C9vRO?mJg9uD8Q&L=u_;19OmP_+H)`^ja)t4fp5HGk+86Xdx zNrU+<5Bt@x3BLMl(3iUK{uZQI?`y)PG0um&yiX*5#8FYc1@VE9oBia=K?FNHA{@J! zOOPfqC!J>V7@-oR&=mu@v$JiUvQjTz{{{U_pjPEv*|-S`*MSs(t7M}IwnRPB+m35@ zXH~j*VuQpcsz?`1MH-_wnQd>Uc69}S2Qq}*zoaHTPDid-zJ)wN;l*j9PIn=!c)C`# znvkC}>~3xysLJsJ0cX?j{AAm%p8Mro7kPoA0Z1i*%5Mf?OiFE`cFCcu(_6>te2*o) z*|@&wv=;;MMJ}pxFc&&R!Y*ufb4HDSV_~taNw+s?cT}xoA1YI zeHM_$4Bg?Q_&o~dvfwW$&RiS|a5UxzER?SvUU9uIOz1Yly3~Uc)4d?u*4Cax=0sc$ z`Z-6A92Ob5|Co#=n4hk&tk2d$~u*2JiJP78hc5axYbyyiRI=FLW z%lP=`^Q2xG{*BMi#a;~r_QZh~FO%;gTp`m!Yv4V)hD$s(aF-WJpoD5lD{IHQCEm2SmU-t+TC1kU!j#H?)k@JxaSSdB`@skUFR^dHI!ToY0z-6YTD z)<)#+ITr=!{Mv2v4yYuv)FGYd(^va@2t0S|_}gymFM8I3<+$K)AZ9CC`N ziY|)}uC5xR8nK{I#09jBJQI>%(ejkbZl(?ZjgY?Yq1@dJRoc68E+nv&IMzAjSpxzb?$ zV@DjSQ9G_sc^O)3xxepBaC*_gJmwB#6AK@XIFQ|vJ%8JL+%l0=3c8|Q>oHV+bJ&$c zBqhNt-IpQLG_7RS=8%IDqWX%2AD3X<+ar;D?*YK6iH@pu7_b8ENW}X%?*l12q=gfR zIzZw>jD=n#QUb=Fh*_0YbL1~%)Y0S461#RUHRY2MdLEhgjT@u9Ta<}uD1XSx8IURe z{^W+jGrRo-by}_&?*UH3FaAk!n<1-b?|h51pZ5(-;TB3t94!7L}LnlG)wj%REnaWs%S z?g2NON*nJ_xDR_e+QRhW8r`IC(7uywwyejukk#96o+9nkmkhjx_#Ntz6PN#uR?+Q8 z9&34#o8@+gf-lF`nP%oTI2uEG8E|ArVLb(rr8Z3=7zo(8vW;CL-K zi#$Qnw$Y!x+3$Eyp+Z;pa&FkeCUgA+@ealEvk6$QOHpxq`}|2&+lT2r>i9UV`P{Sj z>yMz|l2a)Z3k*^-TFO3lZu7^2*2>A@{l}VeK8#yT;sfmS6l{ijlf(D!AB(k72N_QB zmnoQ8-qvZh(IdFAyOWte_7+N;Z$K0VkEI_eaupfDe)vwsdq3{Hk-50Hts%CCN?24H z6~IzQecK*(R#*G(KRu%(8KAY=m>5ph!1~Ad{ROeSxF#KXNmhwHQh|Tn(|aW{h%}2y zq#zoNE6Ih|Xk8ok`&||6K^JWoSI%{{--UBIWapCdE*NAKvW1V_LECaKR(|hGS4oy+ zeW)NFp5x;_QmU>u;{KmlKtRe37c`PPbt7HawXSMT4=#Euu4g7DkyBX2dv3AS zVax*KZp4l zB=dTL?(*;+-H3bTWxy7bTV-1@`g)Rh&fLMP{Vwf>Yy{L>rr7MPU{@F|mzp&mY;4;? z%J=l0`(4#|NNcCa4Y&Ljr)wmMjGWwBni1gon( z;fP-NNNRYbC`d_aRRhCFQoEki#M_we#AkI`?fwj^)pMKFrUeFDT*D)1Bg!iqex}yA zouGNX?1_>=UOV5-#rG}}!i~R63ENKYLnChi`DfT{g^VRZ>u`^fboX-=bZ_lY?v$UR`wij z{C%=&bHVy5H``GI0HGo-rrk~vRtiTWJ0 zm6*blv8wp8%j&Y3R{`DPT%+gN{xeh*!f7}JN-zC%k}NvZfcs`CjXorNgKw%*^gEEg zZZ>?>5`)oVc7Lidg|E0S+dr9*S~ z_E}A$&#*O0t`!n#%#}!xpU7Da^oHCJY@uP!(q&eWcG5RIpOL#8_;H{%=4LKVlUqq( zJm)Cuw8G_$@4?gU+$!g{azKm^E;JTiQ|wM9j#LLRMyxI|;A3RYWecb_)!wbsx9_OM zVEEi{&oGl69_MVN&QFC0(U;QX@lRBCp(^;|mGlm16h}!>YYOKR^)cMw$B^!ubH`%# z{vgdkwLlL#=7W*%{jvjp_Kf+wBJVeU^9qZ%HV8EPjVk?3lE27JOIZR_!JOf;P_^|@ zhS(kHsVF*J51K{b)YtyfUy!e35PD*fAD^|H$ncvPpZ;j_sR0+-_rXuj{(bHQD-wkJ z$cyWSX!bq*uKj&?&)@LEb}iuzu2OcZ1~yX@%jC2KE!3ue*E206 zN`D0a>!mOAEt!>wOLDln#5~tW&#rNr`e8ZZ6@(usFH@H)q*1(DVyJE~c$O!T1~0np z7~#y5VlqW79M1?px}7AQvD2l@ANd3X;EA6CH28I$ zNKDb$j7K*Hx#MXy=DUy3YZ<8yVBc&v4YhH5mvzuf-)AyHP)KG#aB3-FmO4<=mbjuL zK{NK+WkVA=r9b%>JA)mqs^pU5N9XU{uj7rYz_cz?5{$pLz)9H<7taV()z;g6m^)_6 zR9{3Ku+^#25y0|W2?URUM{MW5L*(TCpkUfIDT5%_-i5%SUPCy z1wVKs)eFk<4jN@I`COIqev5GK`SR&3dTWWuYJe-mc(#H=t_fj=adD;rOI_Ts^VjKx zc~VBPujA>{y+yr~?iR#U(ks5r(^#hb1Dr{rF7)I%VBePiqoDZLuf0lulLSQnJFbBdTu@3RouJmO-wVh2Dy*|=cvRGv#e~_Ydavd&? zV0i)|*dTg~VE{r`t`7YR8i!3#5SRZIT>c$JWZ$|Qlo+OrRi7xDy140m+A5 zNy9V|eiLf;%}i-sZVo&tY~AVYp0z0gXs+ooP(hj0`EN{j3`}Lsj(WKB+LcL}>TIEh zaR#3LE|P=vMQ_-Ag~8h+Bh;sC-T1Rrn$Q*wyal0a>55;LJB{}E!=^J!BW7yqJ4pZ6 zU2LrE4!=T;r2d!#F8eRYV*s{>*p~`+$AG)8iHs->v+P`VnWauF+_7Yj-XNCUJ>)ei zLP}hFnZGXKkTzTYnQ7OxPVU_jUCTA19l)E(?HHs+cf3)}kfkbF^^6^gyMj!7$ho#_ zqeVX|r=hsQz|@|Z@a0B{e}UDUwPB!-kb&mF`@2Qs213b0M^_$aL<=VyD~~;->4jq2 zfE%_&VzHz73!(sa!YOeSK1Zg+7^3z|R>9#wQm$lRN26EUG-B%lL=@U<;nZY1Gi5Ip-fBCtyBdVJcLm;tC||##%S4onXEc_% za}WfpeBvOyl^isI`hR`e(-Kw+;v2vxfU${bEq89X9iX~3*?pbcY2m7$^s93m-?AIg zc#TWBl*_Imzk-TA%3@-6o%KDk*>Q2e<(AFpD}2#+j8od?J<@&8 z%%%S>y^8}9pNp|(;(D$gpgC>!$ysZk#Z}dmr3zmnLJC6d(1b{XfSd=2VSV|<1}41t0Np| zxQa61mJOe;%?q7Ydulfa|BW5E@h9fgxF9UbjWTXI%s!iN8PRr!d>y7wDyUupn7^NJ zxb!zYX>$fOF;nVy$4}L%5o*J6^~aA?2TJ!_l+7W^Aox0Z9SqmHJ!sGmDTQDCce;OTH4b)(WvUct!O z-zZK2tGv*z1=w7?MZdb2A8(-irF2n=^;UjPvS<1enjO!s;n|cGIiNxf9mKZVPJWrF zGU#DGNv%w^tA6Zkh`IOjwm@xyfv-7LA_#pfhVcY|OJE5AdeeSTU}->e0lQi%8kg4W z#||rj7`%%}m=%TielQOOdHWo{zf+Hll4~+wwS(RrgUJ=Uo>^LJy?cJ+M)JUs&1`T% z*uc}cE~|st{+3uV#KSJ?Ms!Oky@Z>7gXgC%6-)$y+6yYWo`r7U!*`>at%QR zUb43kpWC>kP;fNV3cd^AqB&Y@FdM-5vi0!l?{cw-u_<*M@y`zv3$uJ@JY|Znz-q%C zTz>1AhnTh!-3gZfqnbU0;~+qhJB+CwU1eg2X6(5OS1V|{QGax#qSNteaS0WwbMNQ+ z^+i>7sSr^9Kt;*Z&}z#Fgkx@$nsWltAP@D9vG9k@^lb;$V)~!=tA-WQEx-*{$338O zlX`E4U?=DR(3MqL-D}YxeXe2(%M^~>oNJwu6p4X+%r<^BCsj&gDqsaVpu2wl7I6z) zISj;bD)JbLUYab0XPG_k*KbU-(rDIU3w27SjHc;3i2G7E|5N8S-)-a;VB>|v+N>sa z7|$W}V{NgKl|O!O^11Gp$okP`l*D8O-VKbN|HhVmfBltL45@Nli4TIiLkiy*6LqXj zZ_T+5H*ZXy4~r3va0>K0 z6k~bFjz_lTR4J=3=klvU9$qy+W2R_$E+`0xZVK0`VPnWpiz1&8%Ykc>&>?h2u zCTixF2F}bF605z03qC$!q0zW-51wMVenwsrtkX!b0b|4wuiuAMTV)PAt$VG*!?TOO}BwIy@?MBZE#1-%_3Y^F@)KE&B8+0}e@c%p*E6 z@o_8j7pQ$sa*1plxNt0Dp8~sO1n`Ez+vs?Q6+3hcA#a@7T>=%h}xal!aJ0$_X$rLMM9UnYOL+G;WAwK8*ZTB+tJ zp6C%T&8&TG@ZGad^xpuBF=proR81lAssYXiPeo9`53eqTh@!!^BlkjTInvAfY0eEv z&6S7uLTLYpyo9xX0=r@kXM*7xwZyBjoohHBGSKG^ZzQuzB6=?N$!YbIvfVcSg1-N$ zarQV^5U0fPkomq4m;g40TrH4uMi%@9^_mm+flGBQW}-TN5?DVB5oBd7xQztW(NM7`0DlzvD1Y>asjYgeK0u(v)smk*3J_N6QBDarHB2a6{HmUDqkSh+P<87g9aA+~~6> z^=?dq7RzLXZQhE9xz>~!u%|0WR9PI>$p=r1jlc%aEt4!67j$LqY}4nHA<6?4)~WK| z9crdqsgK5|`mV&8FV>)Kpk}Q^ume*#QEr7PCV56=Gn+O2hFT^o6XCn4r|bjXqx@5x z$zt;%E~qT!VQdHV%C}k@CoJ0vLBsVzc)J!FOD}LU3@L0n%`9_4+r<7ce zH&a#`8f)UUd>mVD(2?UhsA%UXGOAdO!7Rqw#ZQHbL&~$?Gkfke6Bpic_2QyXuyGsB zgPPh*B?aZqhNeOB9+}2c`ItXwE67!%4^~Vc)98A`@;oZac2Rgllt1*I?W} z-#df;g5or!u%4Zn8&m0?CEED`uae4_X@-4;QZV5>WdNpdb%-Hw0paqdk5~S2adytGD-;>CjWt)X}h=x(gfcnA+MA z_a}ZvLfj`zI6nR)Ki8^YehZ->?zZSoqo*{QPn3UujmdlRUwMIigDebc0sA-eHERtd zJ2#r6VkZZJ)!aiVrE_QR^7{5>pIhz^?=8_^C(>g~+s=$KD2RH!Bz`EW|Fa#l z3A0%E=xXMo^ zTzLI`H$qcY_!w&54g<#xY`I<@-4z1}lQ-c0-8mu~84T=L)6MLv(CQ_m)8TmGg2B3HJ~+%jpN`DP{dMpd-Zmvbte zCB8;S*k?eM)rsbRhH~rDrftVtEm{`Ku;!Hb!}UGSY{^=E@aE2do#XbW6XM-V9|XRp z))=0q`~|5CZOcU@;qQYly1dSz{b=iQy}L$*tS}=I8%i;6q~Al78cQ#L+Yn{(Nu|VV z*tkrKx@7G_eesy{nYINU-(yxpfLD%|6H)%$uVBZ*x+a&ppwK9t9W8n7rqGkVzBgBN zFIukLB-@xqSl-ln(Qht$Ml#+-(g&cKvYwc$wwZl5SoolBW60sz|BS?HVoz~y^{>_^ zgD7Ie!N03wS$_iLlK0I^ge1h?Cp;gSOlg#rfx37Q7_J$ETK){JM(k7i z0ZIX;5On+ObEH38LE~WYcKVfVZqt@&bbsXjx)%$}fzTa0fFA$S@;{$&jS zbAN6B3~=ZOU+Q(^+fR)i_q2=76t?;PWx!~uDe?Io$8R9>;gb-ci0LK>LCGUmVH{(= zh*@ZR#S8e2QJU#**`@n()OqyxHHyDZr95EQfu=p|VA-*yzfPGzDnmU>jLfZGipUGI z2kJbz*s8h=rDhM-1!?}|#Rt7fTeqy%zwmz-&XbVIt$gvBX&u>xz!-t&VwZJeAXQB? z`SZK>dY+X#WR7M*!@{CzZk`X%7#@nm%4AxkaAGYOd$9`3tJP$xZNOL?{~Vt=$;L9fu|tUg2%Q!m5Oqi-99{SnMEIj zgD~U$?_l8}MR=PIoV@EBosx@bwXZ8n@!ulNC4aY`UgWhrjwToYi*XGu9ssp&!qV(5 zd4Xd!)MdA?8Z_pp2OG_O`r~@^GSZjwpymXzGTY(dxorU~i*|_?AMmz^O+s$q2&GuL z=qYx|uS&HQb(30sG0m);wjFuDZq$7bkKja~Fb z;$F?pp+>Sp;YSAxNY_oFbJW+j8k9J9QWlwy+`q)&-oiuFh*-S7xr#dv{f3i(jJR$Ws0_^#griDs*zU$+$_iU+|Tmu{RSsLmEX-;ce*#f zN4@zZ?>oPbeK$Z_)G@R*rdqI7gt>^ zCk(KqY4?NXK9O&06QmJsItXV%eYLEpHG-POQqqoM!X-Yb z^HYGTouuwLJx9LGN4V>DW3B6)DOxT=%|4*!+$xz$^Eg-_g-pq<7-YG>wWkT>+-tx@ zOyqSekTJ*s60;8duks~hUH86amdXo0j{1`^%~GOB^$aEXB=&aqe8-RufI6w6$9pnsbN|-sw2SfgDrbQV3uET`Xris+7y}yOzv{-WNiRE9;3= zH`HeM>ndH9&=>O4H2?5kw*Z8Y0eTz{A$W0}Jc7xh*e~Poa@)s5^_n^z>7HJ{65B7N zIepHJrxu&y@8)e7m#>=}PssrjeE0l{;DZ3I?11Qxr5>=sA9rnE9i&TKbeVla#LU(N zvM7QTo=)%GQM_jCPEba)DYt;gL)YZ$@FA`#_~H(e1w+{kukTdy+wUt=SZ-hW^PuOW znfgXF&wJfusS|~NGsS=;=T57WO^pG5EImHzFGvqrd_IL7K!$-a51>(CDzer#fX7lN zt})m=oH*HS+^w$6QN2iy9vP&Qko;2i^_i~btwDI^2n93?jCq8Zl_InH?=1wal(6jf zVj&&cqDjtGt9tvF|AGpQ)QRQS-!b4o(?_I@Inp z+Dz}Tnhxt}LF_&5|+ zG8$HYjg8$FqAAwf<&{tBb@Dresvz1lnp8^RW4X0$_{vpvPc_XOhbCB$e4Pg_UQ*?? z54#DqibDLm)r_h%;4!8Uh%-Q-wWr53dW< zP@sC#@}}t}>ZW0w87ZF3OYWuYO2RX)22Ef+zNSl}Z*jT*TzPQYWs}j-YGLEa`UUN` zt*P_3Ae36l+h9(VCm1h&6{d==%GPdYzUPO#BqR6qMZs7V&kZ?|{;oBO^*du>$w=Xt zn9kSe-QoRMEDGl_ig19w12bswtSvFw8asTAzubMlVq(9rQD1PzldrBeL7U%^E|SXo zi@qudz0EwT^nA*!!|A0CT2zfqwj7oZ&USkPW*>SRXfs=LWV z`H_?6Om^{^HqL;Q0=WGP5+U^%Xjp0CB+}0&#J_0A@_w<*K;M@u7mZkixu!E-vUHRS zXPz7V1>G4rHJIZwFdHv9I5??wkrB;gO*ZiNs?HR9a-T(2L{Hf37VlxZCnuzXDJq8t zJ8Ufmxqn?S>c%oRebiCi7~IUtO6gk7)hOG@P5bgt3AoK$(~8feg28{{}e9`q7MWwGgS& z+*iX(QL>wHh1ApkolxOl%7pU;R?mYx2CinW26upCSQhYR?U|oKSGO#t;*xjLcjnVV zX^k^CBITt!ABNQ%thtciYEhAYp(@4paF`t}8cev#n|W0si%H`-WEj(G{5p>SW{XEt`I)g!24+93QHh5Y+3I(K!X zGl?4bYJvcH8kpA-J)B5XQ9jyEyZgsp1hZc8N9vkS&9)iKoqUHk11)CY4~pp16}?+e z%p-3vhus1XqFa}M2{RqJjJj@lus}>RDz?Odg2!z9{`AtaU!cIv+SKNh9x(cJco{-u zrNKQTeN`?7)8ctL6ahc(hYRc5YVr9sXU64pWfl)HH!uIx)3Zp9L2PG2txC+P zyQ#d}Q_Y16ZPDE4a2E%=pKa@@Zx{G!kBT0CW}m0Pp*uYJenaUsC|AMnT0~R&#)v9U zy%4W_LLF-3>i>~0x|__ur$R_0%FsZaFnf&KiCtxiH`{;pcxfJwvJw;39DN`h%A9k9 zCq4QCV)%M1{bh0yEN~fe31);zC|rhI+xBu(qb}!+IT&-F6S>vab^m>9H_&40yP zsul5kczSJlYwvpz9wVtV(DFCf*kOkzgJX3Yq7*5W>Kj|1W4F0CG`%j?srW z@zIVNNZZSlJ?9S|#Yt`S67|6GZvc@H5-;l6>NzX6I#h^F?3&$*maZ;&Wc{<;<;=p% z8nEBLm1SXb)7Hs>s$%QY8fDD+*_vfrA_WYA_xS;n#iu++XUY~`DUWZeUr7^X;2r4= ztGN60{j1wt7F7BdDKT<5e^Rj)EldO0HZK6i#ENMBs?}#~OQG8;9gn+I|A%2I*K*Eh zP6C(RJ2GS%K@!7)%|2vcl%_Fz3O3f(Ps?!NoUcLU*@(<;9kIZAe~El478AZBXhjy< zEq>Z4D&*<f{rX!$NXE z5Me-~4NUw&bjZyrE~DSIFu1`l0qsmVW;zp*$>xqjX@pZl%x`zgKStL4g__aGF(_Uz zk(i|d#E(7C2B4Q693MOijGgF1Fv8V{0%$Uu&5RY^!tj+1#rMbCBT6`cJX{~Mwhj_o zI>LcJzUoHa?lARX@5Gg0?)imHmH*`Ck7SoAg-=a%Df9FQ$+y2} z0R%Vg-(<#+$bR07*v0TI-MWa=te?ig+ECAGU=V)s634MsTecFY{mbnMK<@89R zd2kRfZxzPs9HvO`MeaF0E8%eqD0z0}*Y&((v5AQjGrgkm%ErkER+9(+rT$i;#0bc# zuc16VFWBP!S0hkyU&2XY9cjA_4;<;To-lvNnYoq!=6Kr%QHJa|+=FmJi7FGEGC2+3 zHm7sYuaE=!L@M=C$hEd12v;IW{AH~ zqgc&qnyBbcAPXb(U{AspTRZ?0qx1IG^8UO4kI`y7&x;NYSpmi*C8sW9f~3!G%s~r1 z%y^tEL)04Auyd}K6*ZPUV7qzb!4YL_>u-B%%J9a`=9lpyvyE0BWC+F7{)_X0S%N~k zid1F%{*S{i=Lzn{%<*l2ZH}LxVZVz_ma&XQi}NZfPQzgp!R@Bt)*@Q(av<_6;l@|N zle_jIx}VQ;^p$%VjjQ3c#1~jp1Q2sg-S6Orrglv~u~yb3v1Wklg}7$x`@!ws+5SL^ zUHvnMzWWtJ*%TM_#bE$wpdusLYuXsAlFwH`c8xVC?ye6B{riR^~Z z67}%NCuuF$Ft1AjZmILvyrpH*cwh7gJ5TmI2V;WTm)P^Fq8{T@RwbUNwn;tR_;n#{ ze9+IXvvHX@Vf_TzN4(!x&crNjFIweZ&xD)HAA; zs^K$J>31~8hBesP?jzT$1Ap!4>-Q%%CtO9QNbZ{`R(LCA zl7X#%2+Z7l4SLzdUaazu!^9K)%UkMTPG7B^*07V(t`^38UdI)gQIbGp$DL}`$!~{T z!sACz@3B7O`FDispWnOyIq3iB(gAZaIw0iz|1&51&!)iteR%6%Nsi)QivdG1r7@dM z1&y+`H&Pe+bF$v9!<8tW{RfTwh5w7MFG@056XQu$T2yucHXX@CGt3^y*T*YBb?lMqldu&t`q z+sjxM{&=JB58aynLl)_I`thk97tE#*AoM5U0|^>L&j~UezEh4XCz!@W;#%IzrVWbK zN8sZzjbu$7%ewBpu0pJOTbex;!3;TwU5)A-Fl4!@-)24Zig#Iun)um)u}|hG-Vc)wly@x3;`>Um zx{(7Zn~Qq!>pC>{UdJk56ZB8Ggch}!;Y0Sk*tqUa6}1!Y;mD=|>sW=p<023l&#QFT znLamY-txC>e4dLnbry9>o^?S%LBoE6D%!Q^9HF>aryADrKnr^BiTCtQYE4DrQse2L z+)NB2WSTg~rZEi3((l{1KY*^I@&gg=;Lw~ac&C@b<6j6C`Mc4wo1nhnJAXlb!BMM* zL60wrX7@C{m~L*a?9(~yOPI|$<~d(`D2lK}~vVlAQ0 zpL}XW%@{L+6>ELxYYuPV(N+lpvZQ6VfDo|rWqAn7^sDi-1n)Q7VZ(DdK zXZ&Rn!Y;1+_QcHJfZTtn3$#6PZzuO)jsU_%z8VnNWVveN)v%s6=k6ZoWLMYAMG@^} zE=-@+!&^uCLKcN-P5gX<$1WF)`u1-fb>%%F`fH>J+=Rbvt!ZcdT|&9XSs>mDX@ zsN2q5);sC_jm8Eq*6UL0jl5LmT*3qZK0^WAv6#~=5-w0L@W%0d*A+~R| zSp&CEx#W@GRlmK@I0yPIrkwxMbor0o^Z(aen;D1?1(_A6VU6OyyPXI9#`(KHrCKx% zTvg3>I1m!P!qwohr{)u?RLHuX@>)kVy6?JoXyfn3Tq)v1Y&Os`hhFW)1LSX%M84 ztScC$W5Fr#0pM6ZbUUHXN)$<(jA?XTelO05Rg}G<1{o)%O5>Sl;^vW9JcGNre zvT1zjDuiw~Y$M=+$NguxSA#?%2z*daGp^~@1HVCTf6c+X5RkY+8S55zsP-uQlHLJ- zQey88TZQ_Cycps-RxAu=wmf*dYKh&2L(OnD_tam$;_5*@@uy~+AnIXBSg&f@wK1kN zM~FgNMinqVW4vi%4IlyQ3h4rGNECh+%R*$qLbmXO+eU} z9_b?Lzo5J{wh@=vdx_oeJUyEfpS^4fWT_q;%5~>tPsbwVlv`9|OeXW5v1@1NUky2@ z<`5~?rlZO#T2wcZ9O6TMZ6K3C6QS4$ys#4;3yiSnydaTw)ygwD(Mbt*ykj*b3H^lxI~Buu^}q##|JD#BfLar+Q4u?O`30Y1#<|G)sC%=(Y% z>f_qU>J5w&{eySV2Bz_kHWzV`=}%H5>B(VHEw}wbqRR{aM46`~@Xb zt>zY_lhbb7fEkr~m60=BcScvc2!OGh?uD&yPBqhzYI%#9MP*h)h6atWgqf6D}X zc%!*rp+lqi-G+N7+uXqy%FBV>Qw`uKd%k8&;#(qQ#a(|b?eKni=&VI}3Xg^!YpOJB zQc~9~t%-9AZ0(->%09cMRLATdMgsk$!A3_G)fCAPQbxPs-?H4^(w39Dw^aQt>hn%6 zm0z~DWjxhT+S9b5XY~Oafk~IY?;eLfN&wibo5ZIWq3)Jz*oDImWC-^>_q~IXG*nq5 z&EByh-$4IS-Jh&a*Ln0stSCbUp)hv*E;iP*B600`t;^GuIf=X9+KmWfWoua83pE@`fOwVm+!VIWN z@?Gdnke8FXF8oM*0=m!w$2Vi4UEDg2H0|I>Zr1a}m}=FF{XT-v#-`XsV5@^i<)%!w z;;Ow4M_gQCggf}fzo5sNLP zmk+X+ku>TyJ2_XaN_{gzE^qG4vv{|2DkSS`fYw!97xBs%kENB8lBvp)1%!J*<6}`n zT@zTPqWru=T&)1bPJncvDusFQuk4P1!!~HlIik~;w);)@-uY&34TI5WWOw9>4(Gl` zxHm1d(^%4Nx_HAgCYQG9@mG8ynM15i)WDPZYf_Qb^$xWU!N&dX>HimdZypzO`~8nk zk}1V#wUh=SwAr$ziBw2h&`@e@Ns_Upq~_HfiIP&{mSrN{MQWtYl3wj0l(uQXv{EX~ zv`nv>nU~)g?%PfE{(Ro|_kF)Vzu))$`~2bIG1E1#<+`qO&hwn}Jm(5fI~V+R?$iSN z3dT601)Nw_ftQ~ zSh*|;6v%0wS(>k0G^vBNobrmi^h{lQT71Tx8LdYdhdmKorlY>^e*DB_X|>A; zBTJ#oOXiErVvNqcoW&Wv5NQvQYAxEEctrbEck$BRFyrn;%g3I)uiSdoY@czf;{mt) zt*4hBZrJxow^r}b1Fg_oV6G}BEO~MLSCRYgW?RPz6T|M_m*CW+{ zI#(q3{RGIIoa%X&LLcM8b-6UxdG{jTy}hz(;r^0g$2ISC+V4s*%y4PA`(pIf@MZ_S z*tcGrxaEzDtxb*?rj^5be_ZyXw>uVc2u*q(coR*azcR=7Q@#Mr0m;M~_$L;@b z{{LU_g#NxJ?*Ca+I^1^n7pD2=ljQ!+aMXX^wEqj!{1@wp6P-;n{g!B&Ze2TG;r!Il zkf)!7wUoXC5lFSmgWE#2SB-jyG}o8ff^;kPcPkGgv~#44_x;y+1zgE~RD> z@b9kTLURC_Z+`fx%7|WNGl3~b5RP!pB3L9P1vKNM#Uku7(8X?LIo^tcY!HMu57Pxa zJt#3U;|HUCQ09Uvd?D}_ZjmkDO#bO+KRiW{gjZnn54R%!^bFhetT!P)-Yoiur_h%A z1XZy=-U<2fAx!@FegAiT|93x&MgM>0_kZ=(kE-!MTtDyrnMr>1;rT{t#w@iR(*!Ng zMV*}z_Ag$}#jqs>YX}2|fkH@n22|ihlpZZl2+%%yz+f=R|AT?M`rS{&;c z8S|mFLyl=x`Z?hK5`kIU98Bg8pw-T}KYb$f4;l+em!e4o5#ts*1*z{v9u$j4zX#q` z*5u{Da$1PweuCcU%mL*LAWHi#^8iNDUdi5p|M#CZbDZc3Qg;)Pg0pgpC%wZI>puiF zj9G@iu1I{>Ywo&c$E?_ql2UIwo_vZr!G706uy!=(6*g)@-MesZB}3b@pt7rbO-Y5i z=RJj`x?9$?O{jYCTtR({v+fesD|DDiBQQ{FS}0}c)KP{`n#6#}gkc1ShYX#&p=(@1 zZ0l9-2NK_)m5v^=#ztgTIkky2WTiv?ZIA2P!h{yHqcyl2`FD98g}G?JV<^uRt$Y94 zyfS=KTFPljwf%RE_Ue9gtKRWTx_Xa9UxE@fOn3-;ULWF1k<(`esn1Kz$S7T?Z+1g# zX+KyUaA{)E`B{>c|6qe2z-YLvqF{k%dY&Eq^wCZ^EOU8+&C&F;F%Qb} zb~G)K3j8Ez)SCsx)ww3~7b_Gz(38m0Fzgxl9&7um7g*=feId0+>Mh#?4n^5}uPseY zSes#*{Pa=j_OxGT`Nq)~bc|nwy{+zn@hb;w7l`9j*18u|&9=TbKng`QrSL2{rE>T_#P6 zE>`w4n^7b)d6%ln)@jB?a?ORx{ZMN?Xwm;O#GAHL%=St{ASGYZ#i2dI++y06?ZXa5 zng0^Mk}h*($<9n#^=I7$!Ldtgu@WMCp*3weql}zV>;^%pIJ65Y46pT7fI=fKSWK?P(;!E`9?soFu4F#CezhY7FFU+3Tru=)B- zc6z!xCi5%f5KGLh9NhX)vvAIqw4`5d+)bLgJ<`4Q^n~Tf9kkUY-_C9E?hveJlo?8jBD|&OJjDj( z(=l-_>Eka{YkO_8dbK3XWY5TZ-9}Fi#%dn(iLYb*+tkS3e;{7{GGw6&6Ud+@!QMKJ z29F4P3O96qAsm_{+Rm+f>fFF2xn#~LD_zr(YVMi1bE}cU$Oz-O?J>Sf4C*d)8mS%? zpU7iPlY*_owI=&9AFx8)@X5keaPC~(KB~&0_xBKwe*lY@ZwtV!@ele z)4ju+MIS_<&8R_TDXy%JV$8P2Hu*Qmu^eY0{1lav&)r`81uI?^_;d#n2Jk?wkMx>d z;`L~cuhT-#Fvs1~*DXO_UXUkKcu76Nl8?Z^&+MIdinYFJ>QwdSg#0OB}*tenwl$Mo< zP&O6H1X7UPv@eAHAp_%}&)kR(1nVGp>-}rv(ka7Uu6EFE|Br{A2q>Ue7d4XFzYuh< zkn)42Qp_qx2Je%5WBx(P|DIRFNgS1#v z>--rgY;jxhF@V5)N*Q&Ni_=dHbhWue=Xw!$=pH zyXXp}od&mmlSJ)Rr4?Nk%E#gxLZF*y5(8e*G@n-PMi_3QVV(tJp=9O~O!Xzeg*exNgx`WznMX4aNks z0SA^b@J?YXMQVKx!H#|GxBz~WEG&To3;0ga_~b`|-AIWaY>M%)N`)}c>mLkE|EC7B zA~SV?C31nQmzU&e)^IxPsE_SlmF6Au&2)DjUX`|Mk6I}7uen6+A;Sb2!W>)XQwBQ0 zD7DOIch}QO5aBohzLKK!h0vCH^$Q`Y$cX$jwO_mwVJ5=2K}*H)HWHBl0^WyR(0WItiN5SzN}?+Vyw|>oFKDHCA*X zg%WUM(wkI@+U>>1mbu?spjoZEp<3hY=F_V^>jFU#>YHdug(A{(a6>QCg!_CcSM_QI z_y!IfopXxZ&o1cg;dBHwGUdG2h@!n+8ZAe?mlcsWW*m3>IQschlFvN}@)_0@IEwc? ztolOODT(`-{JB6jZ;6ZVeT*_W*i5|+cL7Bxhe;y!q_g-e2i472w?2HdWxb;LuQ!(D zr0p_=^6q5+?&?pP{=#2FT&in^PCXD#X2iH)JjpF2-7U0!yAp z?wBpElQu2Wf112rBTRc;*qW_}rroKNukocUY3qZ-S)>EX8fc=&h@FtY0=0upBU_$D z_^qP7l6`zAEvtLL>2i&viO#7o2jE{I?V2=M_+|TRz z$GAb7Wyre!)p3JV+OW^p2L;i|^a4v?ay1-|*fZ)4CDlW8b%BYlhgfA=#6Vrj}lsVEkvaXa8oA>F=Hi^!$UiT*O6R2wmf!K3aX$c@{BfRq>qG@UiX^A}T2#^?an7vueJ|Bx?#7hUA9p)}Xeuxn<92ya}@ zEgtAN@`Z4_l$xX5hX@tTa7*+0kD_Cb0B5+&k`#=&707aTciZKr)6pI;l`-0Jt(yNP zV10*Y5j>(~m8jRq=P09BX7f&mNkf~2Dg?VrsXr*31vWi*Etqfd7la-!t=B`o9rDFr z7^Z5>Q#sk-YK+vsGv#bdvz_IX7U5)+<*a(~!VT9wz7!@I(k5M)!Dp?Am8Zo^_TZQHQNOL}c({bos?y7g28SL)0U8Bk263rB$QI~zUda};Uu z&_Ri=dFtbMRDU_G1|Rj#b&Ds?3yO1`Eb!`hKkCqpa*5Z}He?9s2vZ&gpjZ(Bp+KWc zbEK(=F%4nM(nYh+XLhpAAa&7`Oz$M(g*ParJ@#wqteK~)rq7)kW}2V3yPi6oXGAU> zqufg7#xk^#lI!tEof1cAG3X=V_|qXnreZ62+vJ$UPgUfe=EcC$Fj7BcVs4;lzn+L?ovkgWYC)8aCH2fQWgU zB)ZTcaK4MxM#D$XNOMUlKsg&OI#?JDj+{)+BonIOburU4cO4Vf!yk) zclKyo+w+@g^0`vwHf!>>nQl$W++*2w6868pKot(T3R%FbX>G{UJqgHZ@yD+ zJyr8o#}ZA=PWJ93iI!Rbw+0a?petsfU;xFC6sc;8{1C zEaC{gUg3?%99#*XhdkfH3laIVke(w{{;MOrllA8I>*5eqZza>#}17aURgoF8*`i1ZckYBoG8_ic=Ac8Gu+{)9#-%%0> zj4uRgQ^*j!oKP{Ge^Wy2YNQ=SvKLdDx(uXn5eE+|qRFz~k)dgOzd;BAxNII^sUMUO z`Lj)wmAZ4?WZ!OEmanvK>y`7$n+~tlj8c-&QT|A5B;|x21R?jMwDQULoDggqWi+Mb zU@5{~1Rs4@s0Vfr`fkw2e{x6xoERMCdE@0Lb+vN0-sMg^PusiLZjxIT{9;XU>i!a9 z9Z9Itjl0B(o`Pi*O0YPcn9fJb(@cJ!NQ(;%%IycoD)4C6{=>ifHE%wle7$!_OI7?p z@K3Rhe}xeJcacW_BG~FA{fV{QzhbTI-eIv8OI{SSmR8-_u#`B`6S4V-zkC1mafUJf zGGGxe@p;2L@fi3$oMf4&{O%pQ<~LEdh!4#@Hsfr#?g}P8un=6q!g5Y$diUKPiCrBY zv~H*x?u<68`HX%{{zBlG#=kLAG5kK-C$SAbK@s)rd#WTMS`U&v`F5CPb%2e_ge3eO zGwcZDNvE^@V@$gT$3XNW*$0uP+1zCO;V0UF(q?Qb?I|3& zOzt1;4O!j9Wg)n~GD8|NgK$!sd^o;gIXv8bZRWrPRx2O_1ftU}l%9FC zG6?sHy?z~d*&pIh;;_gVxIF{|eh!Ol`d%zZ<Qc7vh10cO=Z5cfgr z(%+b<+~Fn{8C?mmeoY|fD^cnibCp#X}TkQ7~aWd-2#9N89ZuRHkoQXl*oFhNy9Jqv(hI``32+pQCBznnL-f z3Lp8771JMDYa#S^=h9yuYBqVx?)pe8_Yfm#izDK-^y`FRhlvsS9_P>9a$#DacK=?* z6$j_pKrc|nIwX5{Qy8V%!+dWQL>W%`m4yPh(vD$OJ{E%b`V}G16I~GgTsx%Y&R)s> z;{pe^kdFvj%IrSWm&d<&N!pngcsh`v1?Qj>7&@4dx*7j^I`|(iwaN`gBY#F$Y|OGBeJq^JCU%D zjSei0f2~z&_|<#{{BAz4f?1&nM%denX6+yUe+4?Sr zv%BrS5Q>!`NN9qnQ@A!tn?y+&UU4IQ`Par0g06$%>fM z1OC6V;0u`a?^uCBpmz2N)ncpA~hF? zV`qMDpdy<*i6Q|insF&}JOn`Nt}?rhtB(MJJ?!1JPEn+Gn~aZ*5*qcE=cMa`XM(a= zo$953ys&@_@yxm6cxESlb{_#(feuia6^lEWo#{NX9gF{j)ek9-zBjO=3?XvsgI=db z$&y_m%6+34K2G{>QS==H2}$3FM}HUU%`H)+PiDAu_4F`C(c|bf`NhKN zR{kq>q<^s<;+`7`UjY-A*aZ>V&B+$NQ2XvHrgi)s2qNImXbmjH*JzC#Yn%u+M$`Mu z=|~lN2Wef-PG_HCKn%T$PE(+?)5_7F>n)O)*Aobj-a$F^kDEW`bA&z?xBywk`m>pF zdAd-(7`HfS16u+Xeqh=p(H_=wCZ@p@UL6HD*a4`bXSM#oD=3vF~;u3`v0 z7P!nplM_Bl5)EYTD{?Yhq=T6L2?%| zifKI-LJB->ErL&b>bkn5hd!qD*Z?`8{bpH;z@&Q2)5qF}Ofv)T5+qygelL!r4V|45 zH^dOmi2n|fE(1s+Fr=y0^?IsUZkAWLtNQ55#2&4W#)_6s3$mx0PKnSyrKUhsE_!36 z4v#fvp1>G+vxY~+Qa1#E8soqNpbW~2t!~zoE+84*w-oB7|bGg>+gVIwnrS-c^yaKW*{aQD`x4kbm zJP-hkNt=;yYv-Opi zIY;ZVHP{8)#g!vctN73(A-c-QqIJ1Y#)1-6kKpTj9}!3(&uBC3nllRzWrf)4> zlk-gD*9WI1&JZUyu|X#f*-={kunu3dFF$!Hq#`)anx2K8G*lE_FgU@&c4bd8jJRW$ zdtm3Iw4~Fscg<{=d+c$X#*_y1q8FZv4h|;<6TB$dg5?gz4VV4*({I`b9q&~=8n##> z;^M<+Q@ATx?RYKUMRcR4W*S4y(>pBa3t?_8V%evKJ;yW$1%%T6SRlzfU1Sfly2C9- zqrNa8Tbk27CcOtD)o2hikbkL?qL@K-a#F~qkGkV|p}=>}t$X(fgoA|1T7uN?OG_KP zvJhw!`@VSeS3T<*87n$B9vPT32jAv8Oj;^VviGsLd($IC#ZxmIHz=c;@e+ev602_e zZT90U-IzD6Y1HVtbLVcj#z8x1<6$I_M-?h9uA^l$W$VnDZ$`f{^&)ZhJ zC08ja{LqBu4x1n;LyL>xh75|WfwJSVtOiZcai3xKIpFY+Ey!D1vkCk62!jsNSV`_1 z-*4>>+3aNGA?JlJ#b0mGrtTH@#%;RiS%~L+1y-+GBn6AUq3g+Dq5)p-!#?uC1c$H1 zJiyoKVI{?hiT;xPU=#f6pdZ)9dS}rgP|UO9xvS6r=NYP%7;L@0ZLhj!SyaW*+NhCf?0NT+)<`^idro?ZhJG_`h}vV(ho5<+ zp=vKPkie6N6VQtHt)vC0lShJwIOVH8jI6AzDX7uXnsrImQ)&94sL^qE=MvyF`Bqz4 zd-Obt&DIp&FbhFBDPXF$#0!*IjVyfhTIh^;p|P{CgVJ+p4kUfh+XyzDmEGy@Xdt@k zuCuEEPVermZDQ-*+S=(h?S@g1+Ui-(HV5Z>vOYARah+o0ANk>qIIG3M0|R{x#fWN% zviATfcj5NccS1T*iu;}!<-=0<(XYbhmyZ}*AbSKD9Vl>|G4yb;OdypI2#Vj}j!KKhE0w^dNuw}a(=V@-ur z_!?075Qt;zTM8!Vf;rx_dElK9*_Xz(f6W|2>F6Aw)=5G92}aSEc`t zKk`)K|Bujr?6{$|^vAmNMbary$8y$dQ;M%=tNEH`Hs7EAYl7@=t7Z9%u<7%#S+0LG zZYl!YwDF(7O(B2crr+Hm{r?^}NrJ?PnL@+kCgKZnT4!G81fiPfinkMQeAeQi9f_^; zj4Hbqy3Vrco<@$yCi-~=;s=rXR!-Vw(O4^_P6{xRJPiQUkW3bE@3&+^c4!^qica+2 z$)%nGV5MSgcs2eL-!^?-kgB_$QrVm(&(@ziSx)Vp4F?;_KXG>&DO$s-|HyX+g?Ac* zEwgLXRY31Dw6{7ep;s>6@P#mkU>~rz?gIPn9gR;?$ZS-BQuiAtjU0ea@eE&8Ds+&u z^v3lXO4cGr!@?;SwD1wg0}{|Lg@6`n)dbwF80NBSB9>IsNt)X;jC zf~EIW;RsFige#Ww0rlQ|K@t)G@il05gXR(e&>J+6(GBrd!)3yCqNq$MnrB(VrCX10 zB#e#MdGI3i_LS90tFzpUh_(joE@j$KOiTb!eBE$4{(?F33&Caf(JQ#_URJ!}nFk4X z%+GGGnmHxYJ8k0YH=yxP{W=htS7L zh`dI}o7|1~x{s$Q?V8{r-VE#-=q33fT~vdqfXtTDMRGvcTI=$iu=Sfe)zq|U?iA4& z69`iM!Qz60SoNX%F}3}8!x+athYEwK3)3S{6&t+POB~fYedo)AWXb83G~u!$K6s}$ z7tw{JM`F;~1CXmBQ{CH;o1ViVwq}mxI^22iCgs;WTi$q=(NP_b&T8@!E5RBz>jg8D zsO{a*;3ZLS67y>Bcuy?@i(Yiad2YuPhYFO+n zD{qZAoV6AQv??Ey48l9-rtFyaVP3&v^@#r2w5PDqe+J(SX)9uqw<5;l>G8dI+cNbr z>K)z7@seOy*HP`YTEB1M*3=UkLmt zIB(L&yq+DBVWEus$InT~({dhZ;y3qD_cXtyk}=*i6D5iGn{9Rh0s28JPOJ?zIk98T zE7{NqS~s69F;I8yJOyYV2CtF7bqgy{n?*pP?2nSUqlI0tC5$0Acv-M_&AH&l33Ibf z;=j0+z*}n7Ik-|$-YC|I#7`j~-tSX1=w2e>u~tK6*DqPm^{;#*7%2C8WK(MG`KLuU zGG^=fWvY1ktDB7&VEp-qZBOZ-VIIv|%KA*cmzgv2&t zKIydKSSG0L+x6`?IcJI8`q$Xd&ZjCt}P{8V+enStGu2V4{ zv`|9$pa?v@HKOFGh`OMHxu?!WSo38M_|_p(@<_G~Wq)h)ni;%vGp299XPE6)a*bTa zDmF*4fvXTqKWc{*mQy-e-EcIoVh#n4n5oulAo{0-sA17LVQ<^ir=*<5yR1(f{N=N* z)mjZHD#7X3Yh$%;f@ayu$VYo-zJfe&A4TZkb74@p=&3reZb;{$qJY*RA}rO3rjpEKtaW4Q&EKT6YIYJ9SxJ!?Qqc!7NIP$~zgc_Y+SE zm;{>Oj3F7LJ5-QgWt1-zZ2fR>#OJUz5x>?QoNs{;`Al9fNzf*26C_|Oz7VEIXM>Q- zJOK2i1({0Pcy9|3ScFl8ju*Pdd9UPsJUt_`vUk<#2B(Z0a|;$qqjT;{Hd~iwz8O}h zQ$gX#bIbT%+}oL>G3sw=mv0vz?6Xu{*R81SJ8Jp0@Hb}4?F%8r3nC<5I`Dt>U+KZp zS&Ec(80o_#6^+P6(9Il>A~5j6pK-?v`j7#09dA8xMsX2fwJrn6Zz)A1a#hS=6x!p} z9deG+wa#D%cqmsnA_efoXFGgPCH3z7A@27F;h6MkIWAo$4n^KYsN1c%T0jfA;UfrGHoP@85z;hhtwQ8aEJ6>0}^v zZb(*NxPHc@-r)T3XVKb02mNIwJ}q#7I2LwQPbg2V-~S*ZpkToFJ;;L9(_%q+F?Kiv z$js{cBmox{Zf4Ygl08e|Hk19)9)o@4zWT3Jtrrwoq{R~yUJk^%HVfQLpcM;}dy%1n zl<(U!iBWw{_>-IYjBHcCC6)7@SrMBpZ~0D~ zCZn;pSmGD*b%s6`k4x8ci}DR;R(P8qYBa}UDs+E)w0qp0(Pc{y6JOQp3-n<6rbEFb z%pVYQ{+Pgc?p1y;=CFfHYorF^6I?v#KCV#?S3N(3x;Yah!$$eI3PMF_V%c|)s$+;qehi|cFEU56)zGFYeLcZgz>&A6LU>@GI3R;P z;Y0iSR8IBAdga(W8maZ*C-&k=7Z;h%{`Hpsg~>s?C5ww_d&R-_LHu1KGI;uJnGh%~ z35kUyaUZO@YXN1i9yZ4^B$FbX!DcA7sVgOyCFqw2t9!=mIoj2aa0V6OJG!@kVRnOc{?GP1p` z0cMgdA*a#-NQ~VSFgW^15T*Y-u2bAl2nL*^F(0#A3Al?UjP&%wzKJTv^1 z4+V!yirWVX4WBfGh@=M)5e&pcr6w#HG_~2X;Od)DnREvq+?V2gnpT`zYy3y%fAp-UP4dAN%AytwP0DBSW!NIf3s&|;m}Ix=Zabbr`A z=jrp>t66!I@LXhYI5C3Y{(c0v-B5@3+u8b{D{}MpT|Q)=9QWK>0}je+u_0sASUJ^G z*OG#JC7!N?LsFg=3Mw$wpIrsK71Cc{V_;UqhLE41Fk~138?0bx|AZ0}28te2_E^aB zH(^hlxox>Mw`)>UUtb;5ddW#T`?iP8!;!o$HlHfa3BtbJ5VB63R%b&c1r!Q|Dff+U z_Yu%N<50Q1qF4f|#pgmZP%hSmvxlu|@OO=SQ1|gx;4ruAz^e80?G@M7?CLNr*pt z%C_|2UV>Bqvo0$32=680(n;LMRjLuZ%&7IWCF=0jB+7eMjldG?4G`tyo5a@n9_RDo zF4!jX1^DP@-uQaACddyQ{X!sCy{q-yTDv~b`O3*PmjYhcYS2geKfjA|2RZ{OJsPc$ z$`b)&QW%QmL*RH<0fn#f6b$YwN4gRD4K09D$261^rKY&jYFzUTr#&v|wNzu(?nzER zXJD25g)lm4$_31p^Yd=V?EINg&>zIN7F{_?24(cBFqcKz7rw8OJ-+n)l|5GHCYfy3 z^0kz7^I2}9Bqni4@H{$-1s1O8gV-D`%QKne()x--Xdu1++{Sq(veTBUJR>qMq0_)U z>E$RPEoJ~ZIt5^_^!dY;A`lqu7?t15qjiW9YZ=eq&nfjaDG_vcP<#Hkd#;N@N&mV$ zx4O_rKoouV$9l5rEx)ljAbR|b&heFW_oIU5ixqUAEZ+{xz2ohLO|CBvjC^|0%jbPc z@@2o7+Gm^B^Uf)rzj=g~r;qGJxfY!M1!DTxBM*qg*pxtx)$jCk<>6>(;xZd?Y!T8h z?1iXcf)1r;Q;LM};ZPzPJEyk~rt1i9sgXtNgcoSlPLU7=+L8i~PVnAXpFqu}DonUl zp|dB=*>T<3(3={qk?#Q#h_mN@bA&pV%tB$Jq@Ghgjfa``0FBn;Sv5|%KLO@lYjrnSdwZ$e|ENN|G zlihvDgn+E!fKCELssS7e5p?yVfl>w3Di5v~4o$<5#o#^;cR^??;Q-e2l#emkYZlmR z+N)Q9Gc*8vwPgZ$71#biw6ehk`dV-R;+g@(9T2Mw;akALr4+F4V4|f50z_X3(1$R5 z*vk#*gRLisQVjG2j_*BJHRgKFC^%=ZbaLq$&4U7A*Qs5EqqVeaDF7_t3Am<~{YyGL z@T>=bfza3*d^%yIB&C`fXe8SQd&`T_3hYzN_Um3e+EKUehA zhRz-<3H;c>wFd-b1B^uLSe!A8DV}Xc+!f4|^n{OWRziE2>6LZS8|o$H)1zJioNZWB zxiV`z=_4&SUEBvka}1Y*7mQg9n^^&UXiodxo=9k)*9g9C;6D=IjIkS7m^PBDC+m52 zGqv>b#ef~(24Wi*%5g}s=lS}_|ax?Vu5U!=(L`Kb| z3UILusGD98k0(HWq`X z1sq12gorRYKIiF)Y%;lrj?Fk7%1JKH8)HaQ*|+Rez!v-XnewJdFGo%teScTzPE8)` zy%%nW)+KSejtn@9e51g+bz2bu@BO^p1VMbmZ|hs@8TRqp`Yz$OPPv}j*_|tYPNK{W znba#HBI3}N^4HK7=WA%|-9PV>pP;l0xUL%k-1A&lVV2SF3u7JHLZ(gcy0hC-cTs55 zh~+w-`92)59RND)hdK0_Wo#nFBKUh?)Ke@+7n=%oD}4FH;7zONdR7{r4*q>ZXmT&XA8& z9RJMn8k$?!x0k}TLjZQv>iOvd?2zcbj6K9VkR?=v*SlEIA#6$cqk_(eoW7XDUBfxU zQDSLg5wTcKL1}HiZp}5T3~jrGPYn0nS^j9sqA{Dl>SQr2Z?ps%A0xJ=oqa#o-t|Dt zN`N4!TFU1sb zB4L`eV9Gu;U@=ABo6=zJFtg_UfW3y&o8+Xu6HA@?Ao!e2&^$wHB;lBO z0|_rCcg?1ZGRE}TydZB=EuBY6qT*2P`S(gwqYituPqNcv$!ij0b~)KRBpmjjw&aT|00$M7rAQ3PR6|iW zt9|j{x&z^HD`K3t;*Sm%`q=(=q1`Oh`PuhEAO8=?GrEN#BivMm>wC{D$(-408?wta z(MWU6?&Vh`+SlYGvdW@J{7;t0MG5|jAtjK%TgV@o9w`$2Q)!17g`6Z(AnIC6SJ(!j zx%4*y0qXCjE8g_MJ$KIYTjs_Ja2*&6%SuL`X*DoK9%X3H#J!R)#32emhMr<%m@AHC zbdx$jk&bNR#kSw!eyDBGJ7cicHQs(3&1Cnq(~C^U%t;>gT=SPSG;!H*yv#!^XAzSz zW*=*m#kJ#BS1M=yKcK{Tzf;v$)QsW4@1Jv1fkSlhM=137d@WY>G6 z3+tZ?*WFeiHrgXUYgpkgA{1@v$eaqV243cMZ*Vbcfc+CKu-HpUr2+Ihlc`?%q0}v* zGv>j(_1RS^`|jCK89VmVz%g+pEA%q>lVBn`5Jd_=_mgWqa?l6L#iu(m*52Ias+s3< zkoT+F%xnp(BcWcOS%F{u1X;?THos{&QhJYOBQs=~nNZn&jb$~vp3l@fl{$HuNoCFF z-cv~_$Fd5n=J+q2X#_`~0H^?9`dzV+#ER|ex8(azHKD}rNA;FfuKCFQu}|CtTd-Wm zG~U?ox}8pu1=VtgU+PNuO6^=TVZLPP)AS*5V;5ZSrz$)&WDWtp;;jV;Bm{G6U@E#e zPD4!R5o~h)F%`mQil3@4YL&5Sl+Vkhk+B)%~uCzgL>cG5V zefj<37}O8Y&9#G7Ey-td(>pw%rkp6$@S0o@@`WJ%g%I)K_-8V5wQK?$H>g_iuU6cw zViZv(q;PzhY8gv;8>tV%ty13D9p@9xBu#jOM z^vwMa!NIvr7o%9tcN9zhStRfSd!tCLWbqzI{sF4wNM*a$Ze$yt2!?Own|A^q>2Z`AkoKb_~hjQXJ&aQ84( zq~&hI*|P727-C=k)z6JjL7Rm>3rqQ@<obIYI}-;UZjd#*i!0hF-CHxGO@%{VstEQgW4u6KfHkyXY7= zEzg+3Wrbf!4U0%ZQX1HnR-g({fW{$nA1DRsUzH%#%M$;O|6 zP&AY43x$ep{M*>lMi?Qa^7>9syU%5x((CATSXWPzQRs-u~#*5=u7B8VBM0f%M$=1LxU@%vM6Vn~52Ujx*R8kA2L+l}>*&<4KjX`G>F89-VLh_EFfPIj^N(++SkA z{*W3g1P2uS%4K*PgM>Vv#YIGZ`_Z0tP|62^vO%vX^2a6$_A-i@5BhRK#(GO(uUOBc zvEuB?oFbbPbC*Pw+2vuI)=qk`c;w+Rj1(#_K;Vr$ebE{? zaG?~9u9-zh9N%y(C=L!6`*TT%hsW|Nq?SpjC*u~tqoGv@feb#$hICU;&^W38k7r?0}<2~ zyIaSpkqoBm$Y;`S56l$e<<5_lPAr8`P=Aal5v{v{q(Tpy8r1?r11`&P|Yr&=W zwom=$Kl7S=FDs-VbN?_ZAq7I{dhI899$8E{s{AySB1PGE;sX~AqSKozcBTa`&ah3n zzFA)QOfs%W9v?}efpnFcg) z^nbfu;}~vu;n2RY+m`d@KPh;zs6eJAHd0gvZ}1xqga&b0vhR3`&;*&b31Trmf&(}> ztB?mT(5t>&KpQKRY&cHueP=gI<-JPsjVbvv%_s}vBz?Ui@q>{6Tk&%$NEtFgzZWO5 z>gjdNAF@R7C%}c@l0;Eb4^6{lq*8s4UMUFB)uGU0+9*vSUsS^alUev`zy81L{u6j zic3`~{ysCH7-tCaTx2?2DLc5+{zGfg5kvi92{8YBl)z_)V4QFR*CV^Q#x)z2_xADJ zzffo9)|v(<)6h6i2jUCg@aL1IyqRDv98hi#2beMevr+B{Ft2C6GG(lp)yo?e15-u< zv&^@7*S1WQa>@3P>g*DOZryU5m%ei0fZE5wB1=? z#3<(|1Gj=xmw8e>xsIcfo7 z%+ZRwZC|kX$b-k4mnHTzTR$pT!Yb1nqR>OX7C?NDyr&8umJoYH0#m$_GLo{Nb~1B{ z=z69^8=d9ZmVZ{w%l+D=u3GH`xu69nweQ9~T&)>S3j-?EPXOiKkosf(L?iMGN-i8h zZ`l!04|_;mmF;dRovVC<8E03#AgJv~+L9*QUB3NGMxgOpNB7nRtKN+xev+~QL=#!Ne*tLTz8HD6uf!Tq3hmp*M( zdFB6B?saSP>&<6Ix4oYCiZP8k47=Kj2uwq(fMPPfdl1xC3`6NB-+<% zvt+rv9_~{eW*ns+oFgdHo2^a; ziHtzSK1yx`Bx(v^YnbCuQfFg85m#p+xNZpMf>gKRfo1{l@J@8Gzr}3^dv^%s0+feB zJ*5y5MBH8g`Ur|1W@>~>L3zKW3m2l|0zeZDKyujpba0bepeE$9xB&3BC-Q|b$`&Tm z0T~Nm=C3{SRqtb12bY$QN(m3uhsb$0npU{`P)#$_Qc}e8|5@{r%BJ%RZM9#%G$P1_ z)1Hfp+sg#2R^%lz$LjNK_e=?1);LlTZ#EVz0CrriH>i-LlYp&CPq98Ll;JCOK%q7Q zh1%n6(PXoZ(dl!OFnR+)k|oSdT!Fl0b*R=`#Jmd0)f>IlF|TCX%9!fg7aq=eR;1u3 zZ2*hZ*#cEI7dmS_>NE>D3V1nZTyHT6Q?x5TDtKygLpA8c#M^-;HA`(bPYH=Zwk?D% z0W5Ye+A>$wy zG*sx+fD%-WU|Q7R402;}jhRKIfpUVKYufCawX4gj*M}EO-#TN4`BV)JjX>c;C^G(> zF_~M~pqHor2pQq&YQ-B-9igbI>Y_W+sXP5x)}d=^d!Ep-JxGlzeil7L;o2V(JCyBx zU;A;=z>{dpye{s?Nqbr@A%xgo?gAXjDJ_I6!M&;t!KZ}``EB1skXKp5sn~cPGskW< zIL3hn^7mZq+~P*rU%fi}R^Z$Xqdqzjc3-1jWb;8^Na=vi`>EGQoFtT~zPD@rLQu-k z^6{BpTbmtMn-R8j^xc~UYc+kP5NAFoCB2=W_C}IN&aFE|>pyEhfS|NoarXCrQmM4{ zKZ0pU^{j=GM$zq~?{;O|Rs8m3XXNA*ixn528|*X*v)S}OVgCFjtfD}0ky|LZv=7wE zF$0la4oH286YFCR8r3>0{UZ(~ut=VC8t(vW)Gzfu%Y3LdkEffs zxv83KpOU*C7ZvWiAb%%&I9mW@kkihho8p@2ldufwWT6Svo_--z7dO6V^2~G6*&LF7 z?FmnPRgW;UvX7fLkCmBzF>3^xVmE7q>~`Cl85$#jjx$GhrK`&(xv*g2U9VY^STdF* zSnBPKV%Hm<)anI9_VBwuT@Fz@8~ggjr_!X6<9?lTJ_583_a$`GT?~q198#Co2-uVL zc|IJfyqvFI;?9!8i!vpl^ZBktSL5ueZWYlF2CF6AZ-0?Bj*;3MtYk&J=9%0`%wb7< zGLOdC`8AII-Yqk>=pAVATXk-YoN|?#0!hDUsBJ-SeC>dFUH!7S1u_c{zJyMP+UDS4 zECYF{wPDr2_!fq(OQ20)D0_DE-O7$4Ql3xD+4S&jsS!~Ro&+8r>8ci$v}xzT`Afw? z)>)DrG)(jI*@Yw-2Ad(t7Fx$;$GD^=Z@Y5f!0s)#&Z!R%TcZ8lC$Icv#ub=TfW(B0q+ zFCY&MKuHY1~bP!+C)29t%tFp(?VsHO*IJ@ULi7@f9N6d2XkE9*iii5n?ENe+;fY0`ZQvD z`Y-0sHA3$OMo$m(B#cennK7s|lW>H{za+X*9?Fpy8cH|ftG?m{N%418z9@XR_K)un z(&AxqD=Ay540Geh{2Y|XmN#0J@r$s+>!Mx?W6kyo*EI`DypW3Bf{cnyz`}-QCDnjm5 zEetPmRM}Dv%KOyoygIorgqO>$S?tGo>hr4o~*QiM6B z2!$vV)rg9cra~w=?b>83YqFM|8Ot~`Gw1a?Mk~6$&wc;y`+0qz=a25!ZLTrS`7For zKDPIJ9{mFiSiLUegL1riNLGV@>u^m#Jk%Hx5calFF9`9=e*ETq$+$S`lLBrN*Ppkh z1odKKyQn|~=m=DROQDG-CJD=C?|!(?jP3nkmD5=Frw-JCSOS(!J60>l+w(@|k`wLY zgc#fvh+!%hqRkhelsy+sCVDZ(;ZyApX&trLHl@j>3y(ZJ@y2*gsL8q;HJ|1zc$<67 zSSOhF0wQBwRy2N&uL7euw*Y=062`W0=alo<2)C)2W5WCjBcKDXm|e-ZEabrlW%?Mr zF#!1m)ThIKjwIHbbc9iRbD^ex+344UJgddF)&ij&r37J(BY}qO@E-*u`cHNvh4Mhk zV@mq9VR$=IPjOR)@*UY2(y6M@Y(^gW0s)sze+4I4hl+pIbPsOFol$-djSEXY@;i-w zMHt&eqn+DQD5E<4^`-e!@g&&&Xp*#^(jn^O=D>ba`D_a(|E9mHr+o1&Q*GLEWVV>p zeenh~*pC3+oRyJYkSkCOJ}Ty;`3dQ)SL4!?o+oh^kkctCXpXKLHZ}42vq!0ZoY!Wa z4Wh(pKIkuc6U2h=fged;O_%505C@9tc~XaPZ{vgVy7MjdhJ9iK}BR8fqlW zL$(FvdYMx5yE_ZmYZ~+dx#l1Pc7h>mXLjB?yJgsm` zGWSjFLTn2bb-sU%RveXz{kWvLpp_bdvQpB4GPV&YV-MHzwj)Ml*7|r!1MC`irQOtl z9Fr+JruU2<6ioWqwkZC6)*`93GhEgzEiztK2xe~Yf3P<{PhhE%M5E$u|0}0286YQt zv3A%^rEf&G~gs!rAdsoh`q;=5Fmt}V2`edvylxA|7@Eh{D%tc-aysJeQ9s%s`t zb-@n@;fFlr^su?F;_c++%?fgyIc zToaw%U2g~E$CtcOn|#VT^q@|#ahs`uzKnWOw#O@(N4isi_z^!9n)$W1pnS-TlXat z)|VR&cP*EmwfmxWC{%gD{xB6gk0ssIkZnSva^OkaYN{uBBysD&&{&&T7L~On(T0Bk zpLu^wPNsbZG^ymjb>g}Gx_6BqPWWr=eiUX>7C$RJ;kuCWlE~ntgr);KCb&FQ8U4&_ zt7?QUOL~wiwM5`_Pr)bgG?K9G4R0bGBw9C{C`BB5-EJd(VCH!7na)}~(;C9UyTXwt z@e@LV+VjtU2t+r62Oi7YDqxy*0H#?)PwvkxfDG2oWlfhkyGzQn79#aC5(Vz$%{*HF z!^cU5p{iFLOeK$DgN-SpwvZnIH=A-K?f}Komh+vw-yURCeV- zL;tbm%MXqf!u?f_Q_G0FdELM`tI|rwL?H7aLsdXB^?CC3q%rs_`U@9Oa9myF6EAwgNvrg9aw(<}0A*8b?jC0GhJ>U=h8Lan#{gkTm>9 zhemj& zK+)}g?-yHDc#}7-YvRYAc?5BEO?jW<4@(B2Dj3ccdJ3yuyJeZcJVo> zA$N{OM}Ew??rQdphh2Ae@S3Mu3*CXh^_ORQUyCVgoYx^YAXU_@r97h;%eIqmB5S_w zp~S4rs*H==@;-pu<6MC>Sz?fa3$203xd@mUe||{xPtOJV10(hkXl94)E3kPMX0}^~ zV9Ywsq3c3%JNJi>dn;+&n@P`pa{*cD^WD@i+q-U>Y_sSX%yPriXHQEPHu>%7oMmLR z?rNNx$jcc$;><>i_C~SX1F0eA;1Pr>ggBKikWZ1suCUINXm2IC`v5K0k(1^KSZ;vfJVl5_>GTV>9UHM}rtd6ri z+bVeT*d8(}JWmJ22PvG)Y_J)zcCj&7GJE|O`Q#M#k+=n0?wK2=U5%Rud7)#{gri}j z;)hL>j9;}K8hbYAz2Gje=xr5l^H|PH;13)|;Sb0$4S!vW8)CVrpIOnJ7w8429Z|lk zqtu%+w;K@%iXQW+!-1ZM9$-+ShQ>d(`303j1lz0)Ks` z6FS1Tj`XzxO3Gj8Oz#Km(&Cqap}C2HjaW^;45jBykoML@M!h1$t3@&uXWS%Lo!@3V z`v8d*SaWO@si$l{9;Fss#1r`N-C+ODYf4UE{Q1hQ(5g?%uHS3ZRuxnFP&S;LX6dkS zrs@34625o=MU^U9{_4v8dXags9sDr`9_DSR(9b_qvULoL!Jc4 zot5H?i+boCjPmPedOTAKYjE(tDZ=6;kEyX83{A3jaFlVcoE3b)Y-`7=T!M@Gj>I3` z;M7W8v_pbBM>lOlEm5;){XMcu(h7$PMa~94g*k!sT{jhEAFW(5VX>-k3*~GrjFgiE zBjpd<7{fE|X9ax&@Htf{wQ+>CmG%rawC5gP_F_-jCoN0Mjc82K z5BacB9h`g%eh}zKAs5Ms$(=&=4xWD#6m^V`Hw21ee#XJvjj8s<8y~OjnX$K{jaW~8 zj<7PQPbhqu0y2(85*z3H;RR^D2fCjzs1lCw*$}o!iEx!6!=qCQF%j5C&G>&LCruMU z_E0M4O^j5a=^H~~8(pMeRk4!<5^LaOF zb5O1XihIHw#vj^`{-=3lAQ%`ikj%_wU}}_3F}7#QvWe__5Q7E*s>_EsN?tqbBH`#9Oq@TfT1g3vpt#??sXpCRv$N%oDX|8moK zyGj!$s*y969~}J2jCwxYdFGD5%b>F3KTho##<*JveUTG+l-sS3#qAIGtHtg!Xo(Ge zv`NpyTvDG@$?On;4%o}AUK|9#tjqJskK}pblx&nMcE{Zx*ce8Qt5Uyda16;>{U(mk zUuXMJEXIA^gJEIr-TKUPn*e3oebRqL0#_={;3$ZwY&p68C7fRKA3*^V55 zDj~JLCB=rK_0c6B$zGVvNJl_jZ|JR83N^@rgMEQp89UU z;6v=;%&xEz9qGUc`-7*fNMwirP!-10Ihy>7e-jQClIR6DCD;xZ+++^KwpIBD8|(DU z-B~f9Da1=GrdBd}f5~@%F<;wD0SpQ;xc3--Fw%0<7BT_31Vmx`NPL|%2yYyA*g1$7 zNr*1Pn-I9^LOpUmzEAeUdaajd9KTHIktc5mCve6Mjlhr6Z5VKOn?2f65^$YEQ7?q+ z4a2PCdFxs4g6`$0B&MOsemkn8XD=H|iYK`6<@jOMcn0Ii*6N%BRxd^Q{^cImZMCDX zDDvACH5&d!g|XLBpgDx?(Wq3xeZMduakK@=7NWl*_fa$<8Q9=KzIb1)544E`S(obv zGMQCG3JcA)SDG%fA{k67 zcbC3EO!PYH{5oOHjyi_^ca(zIdg5J3(Mix44^htx1*6#)~P;g(Xzgz5Z;H9De z1w~F(_IW-4q&^ei>^k*>*e<;TYM<!Tuki!v6P#@t zfibt9;zj=})cM4XBgUZzpB@c9GfiVgRnA}2VnH`%iFpeBEr7+NwbBAj!cwH~mf9dB z_RnkLZ;(D&D5Hx&t}-t!mSx17WXZ8-kuGXdr?w1+rVLccebj!mAbHUfg&9ZNrif?G zo_!MOzY>~?KP*52PyWO!j}-zEZYz8dc&KRLT#!;l^hy7vr{u|$r->F5}$`?Lt3&O_XwrRgB|iqx{<2_zwvKb`q3eL6jZEQ%bah_q4@nK|5oE#*FLpd>U zxIB14uuFj7xiR;nr>v^-(DPt6=Z{h)YgvJgBF*t2k39qd^bm9Sa3ugMs%82Vwn(>^ z&YBqKu#MYk@rzHS_$+%JcCl%{l%qcB(~F<9i$AA)db&xx$rR4504@I$D6w8&m_OO7 zmQ&_Su6u$cNd+a^-@y~(D3aqRdj^!;l zmchm9c=r4p@$1X&tAi2-4AuGu@M)!7b=cgkKzc0?xMpf3@f{JHJxu!+7~+8*!X6x{ zJ8v>S%*~0MLpopn>`q>^qwSo_>K(1h^j#MptQ+TdJY;%%*ZrY2?%rCfMvXBYfEJ|kUo$((zjvs* zzz6n45+4iRCfM7d{jDdClrX62XPqBSYuq!cV`Yfem@=Hc`!)o@-)N1^=D+w^6@L6J z;R%S5{vLrzh$>=Tc|KK+x0IEBBkIlFLt&;;>W&VxUvER4eF-WoCLovkp4uc5e@q$~ zVsfZ5v#SVjT!#0eGjEY?HL6VVsY&tyb=X<9sHBOz;ir<7b1YS>mhn<5uTE09+PuKE zyb!{r5i`|E+!|`0SjP*lt=l&KW%d4c#GCWX{WWyzxCSP#ExEKuO8!XsF89Xp!8b!U z82Rl2M-q$SE)N&DM8l@?%P>Xuo7`H~Hj2C{R}U}RRBa0fq?55Tz*Ir{MP^F!EKQNy zi=|U8HCdO24>tbc@O;km1D5Oh0`ikuzK9Own9evNY!Env7n*;2fq);Oj|aT7gLR7U z^#JWVRz9$k_Oo3Go#}*PbL2akS@}lkn14Or$nx@5R?cEOvo~6A?I(tR*bqBSJX0;z z(D1amK%*yfKiN*whUVP&Liq67*n$VCS!}2R!npB;C^XFZ+a0}}uV_mrWj_*wo7;>( zEALCLn|`c22Nx^SF^ZiJ0$0`!$5mH(5f|)@5H3~mBeWm-ak_?)S%Ahw1_?x&zL8m6 z5yFIr$kvC=D*q;Evt7Wv(=O0376{t7EdnD9QSaqt>;Y~lV{p5id!f}%c62?-JAH-Q zvJdG~oYRf#OOk@7T#LV|9Q`_Izv2#0vH2^Z_blE3*<3r)Xp#gkIQ(Ws3CKdtq4wPI zw){}=d-EEIJjn~75)JlyfB%f35B=RV6Woa-u_2(R0!FSzwQECRheo5n z5oZr(U>jrQY@V4r>D})2^5LysuO-j-PMVsYrEgA2Va>U?V4z{{#?t8M%VNdfa(}>(D`3@{%iMI@#^r^W>EW)1g}c zQW2x=*iWU4-w413)Z#z>3I8Wb7o~jOZyzNTUv{--UMKvI>KL*6|L@7i)4E6Xs%Q3h zGZIL7S5X_L|IKMw>e8!DSBk96m&U1F*$=!U_*bzhH8-ya@iR6HYE8W9YPc^{I^z`UK%kVS>AW-Ez9K8;K@*C1^zS zK*OMOO+F6@ma-WmN#D5*;Ps7aOd1lBb1HCUgBYe?At&fI@0dD|nGiC) zIN`Hc^$-%8lX0oAA))nTc%*}9(??A*SK0{L{HEf2XoJEpuzGo(+})Wh$&Dou z^-bZ6@G|X^ml|h%aH__e%}PsdranM1N6tNHu-yV60zPDVB+&!fw13ROm%|)9wc-GM zDgG8=t#^gec6+1EdA)_E>@n&GH047I?pJCWQWnn*?7lXB(Q{oj&ibKFkr_}j{-z`` zdOW#f4rQrI;IdD*ZQ9gW4tWU6pp%kIS1)thh|1hbo*LgYuzE>M!r|WQ=^wwQH7+Y4 zAi7R4P4GLBi1}xPw~V_D79fU*OG2O%Q1+3;+8m)#5iZ9M?MjnZ@tcF4CMQH}F-c0c z3`Uo-CmN}h-)+<U~DEauu<;1%U`+0>`3=NJv2csq5qQr#{W^pL9S{6@t?!gb?R znGaBFX65NdP6EboVtuN$q7t=|&L!6#I<%qA=bHJEqBrHIb`^=eN)(7H4gX%?3diex zAh+BDeCr8TqcIU=$_(fONOoWnWpMscc;ZwP>pZRp`3KLx+UOHlH<=CIScLS;lK49* zi==hKkkIFk8JdlnLYRYr@dElb=H%m#eF}?;W$B0SO?G_YQTV=S>hO@QmTd zQ@ibagFZL=h?eELBj;==2P>OrxTp5Iwq&I~W) z02dWCZn)+qu9sC6y!*m|lzF_>vjWUpQ{JP$ZuKI#)%v8rMvH`4!2oWOG}>vy-cs6N zknZ_O$L-TlkF6d$|dz)%$y51MAB9#YqNLoZ_J%A;eeTawO_L7i$(gR9pym3JVIR= z?D((MQP2RB9lH=Y0-SiN8>G3jcRH`9h?zDPetZ}==jAg?1i;ko{W%jQZQkU6ntn+5 zD>h09gkCt}Ug(iA<+;%;ukwbAjEPb2!?Ne5HQqcX^fj zr+E{>#MjuG)dFKo+(8Gmrb3QiZ*t(*&s*T|i@Yeg69NdyYv;008Q@J^8G|5{^J1I% z%6e&cH}m@4vvb}^fC0O$y?XH}+1^Wf9|!ViQIPr9;6M%Iax!)vm>k4#C-#d!km3yG zJ0a)!Z^>w~3ad;SH&7GGGZ_tg?#Uq95dFS9O)wNRl zOjN){_yN?vPvFO3U{92z^GF;C=L%?rM=`PV-lX(qKF5@ziy#RWWJ?#K3e_ymBPs^cN2@MU5r-8$n#r_tjh9467F`SUuhVKNUHmNU4^=Ck7@sZ|A{<6(6{9 zV_;TeK`-4eC6_>V`zadv3uAvT7mUc<4t2K5HINc*@(!Uk0BZ&Za(xr?%4F8=FizSz z){wGvdtmpa3a})RM29GJEK-lHFy~Kt1#o2_%Cmb$20@2ICr~*o&WM%8TNM7WE%|7b z@A%00gm*_H9)CW5u}G|Ix7;+!mz-d0#f)Wx@2?Ey^>|M3ZHxuNwC*@R1`p2tCQQ49 z0>XAA1cf~1?Z@U>ecy6{R98Hg^r*kQ(-)A4N@d!%n~G4|cYXbiYwh^uD{8oFUpnMe zhmg_snee$o$mlB!7YXR3p^X&c|55CuRjscC*LM-f>&PTI={Ko+i~k<}Ge9$!FK+ zz>Ts+V&R*qF|d^_wN>r7n`R+Tn##HZOKFIgQ+pflB6W6PZ3uMghYJ)8xb6CJr$GmB z!v6e+Xmd88(Top}01mIkB}m*|`8vn60YVlt!5Vvc62o?ExhCA|6|eee=HiI8*sx#Mvr4v zH~$R87e%=UB}qU{IrcN(TW%R3w(X2(wgklstZ zP%D?F!ZNAvieoRy>oHck5ItYBbDT?naLOvL=`MSzB7iPFBKs+D58lHLOQ0aXLWyO* zC1I|C9dMDzgA80q8v}etbmRw-!MbdP{hSLfkigfALjOYQyU0&ecz+Yx5NvF1qtU44 z=o=iB4i(5(pHIPGGJ5|e+=K5YgZR_;^LF#S@D=njWMWcb)$Drpr0e)ZcH$Q<#WmuL zRDmQx$hmO-u@ykg_d;$EAkm1!Ma51ybb_(aTVaEXGND!BGyhL-Bi9$BN##7umVr%0 z>-9-j<$mkpM}F|XgK_xw@jtg=P=#ZIX*5!>x%q)d^j6YK9_QNByKM&4kmy%9E{Kl$1{>(gE#{w8SYkdpx_3piPy1hCt({Wm;51`D2fV;J z#gPIM%+l&WO!<80apj(MPgfjN@FlgzedNyzf@7|X(p+68H<(Oca(MruzX_&R7hXYq zp_DO_ERT1WRm+nd;8AHR5OSy!XA7ezHLVj-8 z_Un_*kdp^^Hh5SNne8ee@*cHIIdDQm zUTlj06IwTQ_+2Ox16w$%=>qpg55vt@t?j61CvzOwNH%$_74=O#ZBf>v1!eJ5CtaR> z{jtu`yXZ}ep@i#Wa9#pNr*!lk`mA<%%L-4`G9|#d>4;wjBcr^Z2(|ukkGZPY@eI0B zk(OHLy4|f$CzthjCG6AAof@~NIVcF&VEfF!5Jz6VQP+$>XKbckC9?)dl`=TePv(tr zU`OLhjoevCAr9Zl!xRT`Tz3Qdo>68Y{Wc#aC}`w!XwtN)AM&#*yKGEFqAVGStM_JJ zU9@Zeb$+Qn$<%x_%{%pfkexpT3#o}T^`$EzoYIjpSfHOcW}ayA58Op1XF7tR_6u70C{}s3N4L(2p1)u3nI5<9*DYeS7p*g=>-fcC@|0ZMZ@5T(q-3vMnspTU?E$bGb4yqwK_Y|(UWv6drTPY0X z(*Mi?4E8Z|IdlL~&odHi@+G^c^VUOg@wxw8SFNRN1!_T1P_jqj_yoOHiJA1ZT3-`>SkIrH zA99Fw-5?Y$214O)pp{%5aSCO8lk$xFdXQDllMw?QBgcm6+Q783ps%SUZMoy*TPLlb zn>mUkNGQ)wY3kg1hWY?vo=i*feplx*35|YTwnq$K;#mOkG=abT`L8SvV>lr{e!U3{ z?^F8Afr*I|Yuj9#M2rSyoOH(be{%Y9G0*9!>jt3_c!aEQ?#I9E~bSIaeMM?pf`p z)<&!>XO&S0m(#kDAFg`pkgGOp z_D1Wjl9R-;11Gei6LgGiV&;a_zwt?uOqgDk;G=%kc;@ze6}#)>2>4z5@2UXX{dnnt z__#{$?CQKn#>lMGdm7So>txbmng@sc>Upt zwx_oxP3pU6OJJ9TUOI6HARv%0eE&!M`+teS`wvz3EIE!1PllU|Pi3ie9;kDW#n8=X zjY|&SNm;V%(CZXTSuD@N3mvCf>%-<5vUSg&kJuERQGKD>r>=MPf{3Tx zvzNVmr^X4x73DeLm!raNA$4vf-fn`sx`ErM=N|nw7wr*M+;*&o*~aC($4XhiZqrfT zNc2L*-8S!SD4a~&tRHG|zLIbH`W_%u3Pus&0#S{{J` z6V#E!Yc0%M{Yd3XPC(I#=PzhGFxPmC9Thpf6wS~j_l>4XB`i`U5Tt@>&7oT!Z1)zg zD;l$T{r#Puv9EN-4KB{-81nCEtyI-gB%ZhU$SGHSVlZpRyY0n!+s{aNiboQ!$8mM} zo=8(MeJWD1h}<`q`I&}KY2?74j-ow;xM3`0x(yJxv`@nF(X$LO;QS~Qanr^k8u;4T zbd{n9W}mo}^k;sO5iV;cZCO`yc3x%+kdXxxQ#FxnYx?hphq;fXkwX>cnHJYlS>uyv z4_jZ&^omIgnR{^^LBMUWY)0cJkChgJX1`I)y|n&k=wfnR99f!ooK5DOC(gV-9zfLTy9T%aBNKOCK-=)fY>kJ=c2eoG-Mp^0IquuP4KOnA)iUCd{W}y1is8} zfJ6ipn5R=~3#?$)fkQb%PGp?Zo9AZ5%7t_WPbKQrN~<;t|FqDuM++`Jn-{!OZu_yf z1Zd+I9!!f$KLlNldHk#7mOCv}j08&}Z?5?_p{kl=|G^=$^hCGMvpKmR-uN{7YeXJA zW$i&AzS0!x+im{SgKqVlBD(dGi=5o+(BzP9`>*UFL-JadK190`$oFC*eaFos%BxU0 z@_vV^ty{6Mh8u?2^<~MrN$+A#DIc42@$+sav-uh4GRK;WKrf{u7sBFVEuj1y1M=k_ zysb02x=e>OP-pUY<8GKq> zIApL%Y`{-v$x>S9igb`h=_RY)@Ao~pLt|l-_Zr1X!XhWd#-1&3B(W-L6U6wkFC-y^ zs1q=-2)CYA`x1y9cBnjy`&oHGc)&u~Cs0-eI^ul|$ml{oegUbAqmpnb9)*8H6AFYd z?0DowY<_(8hN7J{2AZB)0s1@cFIqM>fcCWnjBKmUoOW&5ds*jQd)AB~EUy=Q2&gl`Z!}_uOAh8IykTF+T{kHQo4H`uhgFUJ$e{ zbVHa2z$Gmc+x#yNEpCWY*5A z^_uFCZ^JULn;xM+qi_`SO~;E@Q<*{0=5Q~ICVhD)xDy?e><@yqYS`2=Le z_JNVIW@`20xJz&eKBQ-)on<&rSlWJW)Y#11txLCjRDfaB9Ss+?()_fSOb|5|jwc`o zWz7pl9jI1h#B&vimAwDM`&vd_LVc3SAFK{E^DB-p=^6%mooP8$Vi!-CP$y#h@aZNS zUTk80)NfeVw6$iZ@mRA?=ue;ug!xwJs^Pg@Dqm3@dL_Ef!}@R#tN;vmY28pk6Ffii;OxGLcO|jt<(uPk!Ol+Cp^Y=Ci3b`)vL%GgBsRrQLGz@&6C*q7|TaD#ENRw`0$hZAvyTbHJvM4RW zck;4mX@^<?!}YuZYGa zQo&aQ)oBa>jOq)u=z;KT#GRJwCv_ZPnKYmowN*>97&7VVnB^uG(<||0iuUP%TMl!Q z6347l(yUeXyPtZHu)kkZh`*=&y4zj>IYc9oJN6UUqfn;Cs6)HUAqTRmoKtLVk2Hh& z8nFgZfA@QKr`-#ZlG3yvr3Du(I2`M9%Xa}VPdPzacFRvPcyngVL$Wnmb<(&gNKcV= z@j;H~0+lA2eAYJdC_kqv{N?n&Xs22SM$&%+`}ZQ$n(WUQrzb{#N0-H{*$%lY5HFpr z(eWA2OOq6RIv?&?ys>94)^y;&#|PpsTWD<>BmIF828MC;cmy5N9hqk7voS^BbC!>B zKASPM=HZJNqbSSOrB&-M8!GQq79fKe6Laa-n*{PB6N$JTmH!N}fc~a0K-(w54y-?) z#lUuuSdZOItifa@^TS?p?ZE^_9KZ4W;YQGS^B*M(X-F#~fyX`m6CTOphRPb`4?S{rGk#Lm6 zTOOXpCz54vB9EB}ONj!!4ih!xkfNyre-nfto*zeO9(u>H*U0sg*yenpZ@&PvbFLf- zz~2TK7=k@HkT?%dPiJ00_WC;*l%NNQ=U340Ar3Z9qTzAMTh|A@JnvW^;xmJ9;C=vV zM>bw)%M$oMXCULi-X_G$g@a&?HZ}JZa)nj_(a0C(tJ8c(uy)TXt(*fnA*Bv!tfpdH0M`n1 zk*LiulRP}tOz1m}|MfS)A@J);)ED8%ApEiB92aUHsgn&h?-W}D&vJmrjVHJvf6zh4 zEog(GunWtPSC8zdoaqo3tX-o|dhq^-%-CqHldd*wrdm$#kt<1oj$zU-H4Ksbn*I;P!)s1yE}w-ZW9_f|Ep~sQs)BAI4Nc$%ukF6F8JO> zVprWDP7O8&@WT~_kfSAmn)TslCB2odg}hQ`BmA=qk-nYGdb^*7oG|&jtj8bZh@lq$ z&phPO#Rk^9oCCAVE^ECgyGN6{mp%KPUk2CnxW1y+XwKuM@U?f=I}A!yB6VvK;OBrt z!P*hOI(rL0CNZ_I{p!Q$sUorJGI0;xDxF^ks|xoV@l!L*U-uzNzuEN3%?=~~PaEv# ze(ZZ!vz_@-}Ll$V4@lE+9lo-4}#& zsxeVI4`=(EJfB7oHP*}WP%wa@9bfErFW>+Bufvbdakxi0iV;?C1kN9f@ z;LiSiCXYG^&$9UBe;a>&lTg}k!gXC4dl*>W*FSz4Qyk(mn=RL0KI&SoGB^$^5bA7+ zpIu7DCX$API!E5!x-g!+7`~bzNp24S@O;aF>M6%X!IdR^IPrqA4cm31`sEQ`iqrH@ z8!pWKn7HFzi6iwKjG_ug7R}V-k1%a4Y0Io!7+tsRb_W)Xr&*7-R`JcOWZy=^VAEM_ zFwRSR2!yVUYN8$s{uw|aiV4{fnhd%uG_Q+qgu;-GN?@`;%h3+dUDbFq* znjC#%^U9%3hj@nFgCcM(ekI4Uh}haR$b0J3YI$Opd1?a)E)H!XviZCP4wx~yVQVx0 z@*6I-OS^G-^%rY0RH*S{aK^)y)vP0_Kz{R;%$T;x16zcGAFmIn=+6p1B)lbE0Zhy9 zY0&g9OdoPx^wMN)^zlHT`h$9x&nFVAg6)aXTlOrg;$NS{Xxkk0a-2caP`K0Ue6Ih}=0`mG$;W z@vFGXxOthP4S%hg?o$Kh;d2RDwS5Xt06zNwBD_z)0ZG^0X7soy>492dDUh%3V|ulB zGceVFz_@m5Qf)wzx>BBNyRlX2{-U)dc@vtX+l+Im=`D zpjzaJ*8+!q6IEeGRH8=W4f74v{5v*UcmF0lZ#FD$ZcULYb01QT%$$le{Vq~7#A!aF zrNq2n@fZII7|?0ezo#Zz`=J~YZIM16+RiY+DMuOxmu#oyR> zh5r7pQRnsmC#g9EqY3p6+-V(1UrlUzdp}(t{6Cn_rZP7ORIP6W;t@$x3HTlXeo?C? zAAA|b;CN!krCtIHCx6-EM=xi5oV!XiZR3kzhW2n>od16za#p#{OypmHa6fn)dHA#K&z(>GqXAD^V}^C!W+|6EiJMmGl*!3% z68YJ_!Y|_~38Ev&B7LXMt6%?52VW(&rSe0>_hu%JEXP^+x$yyn?O$(GSYCo&9g`&n-QP4*ONvR=)m|ij#T*V z8SE!|B0c6hGohPrr2}6I7(R>v41Z^neX#AfL-wPzFseAkyTmMmeS;yF8%iO+b65^= zaq(-nZ7ptwu-CSkZQ6bHOX<_E#SbEmJ!w`vlS}W0v!((LU3e!_r-SfJ=}=P=h#ATZ zi5a4UQb5r(M0vmC261gEdT{z;cQPe8=~6oM7dH6YcYRiJ_A zNmBT7>M)xjG&;4zm1Z^;{)T(L!(xE9ZcsN1Hu?#BL{YD}?+p`9{1V8OR3wUp<2$q#*SVQ4C z>!3Apb|*7D7mI&d;nzEV^l_7<)srRNL#MTrQ5})bpU9K}JMf6Z%vXE_^_vjrc3lG4 zkKqq$#G6GIgezKMxNttCB+FMB5D>-MNiiWN8Dx1B2XYWm8FZM5p9+Ybfw&0~|rw^2c;hBoUC=utn_;#Kqemwx15`)V;qd zwoP7sv;UZu&QKet!U*|g`wZxMe`rJ%4129QPP2EBw6t1%8A5m$SY&!+)3!dq;3GYd zW*-anwss2EomH@N9P_-r0Igz7D><@je|KHI!1d=!}T zr;+5yG)C3d4T4(OitL7k$s$QlV@^$Tywg{|d6L<cua zN64+aK(~aYpq80IIm?(r7L+pAVG;Gx+Br8>$`6}0a5tqV`ME~;noO`Vo^G-zqs`P^ z8Y*nZ1x0I3C|Zvp;$^es<-5~Bkz7~k7q5(WAcd?tp6BaQ;*P=Ni zT`8rc;dyYrama%@+fO?u-d>co{gm8xg}kF8ZlfPH9nR^f8n>)aUp{Xsxf8CUf!b~` zrC$LkJcGi`-vmGzgYzu9aWVM4-14_@UrV8s0`%KZ;FOJr0;e#*ogsaM{GnRd9U6*} z;B(}uON#YLv82b;Jf^?8E>aVvdOw>m`w~5K=sy@#R8-l9Zn7<#xL|VG%g6as333xf<@Bc!L@dD0jL2h& z(6~Cx(T&O;KxSkO9S_26MtwlQpqPi--g_+;CxR8iB~9+_z4%gJk?~i`Ol9 zcg+pw>JeI?P6&5&lg2D^v(4y6xjLcr)3g2ISm{+DTZ1MdD7TlVtm$u7x(2`fZhi zc3$Ozeuz495dD_E60jx^fUI_;J^fk=-#OlG8B05xSf#NMth9%w$!@WYm)AbM7MbWf z^Ww{rb&_)WqU(gi@ki}{Zf^U71_8>Lil0RVLvrK34as%GkQ{;E#e6kHq96Vx48mxU z7~C%uwfqx(7o3MYL8sG;5!!E```bt&F_;(@tDFCv93 z{i%i`k9?y=i514`uzTEHcuBZT4l3JPMw;l9q8^{LG6mWvV#fIdKiW>vBaBIbiqeQ{ z`59D?QD}P0$Fs`O!R2uU5Ls98FVls2N?0zJjIC)P1E3LaO`pSFnsg|^s!hJE_~Wj^ znQbM;vl12!%uT;A$}sIhh7o@O8g~JVZh<-0Ie3#2t%fS%heDr?zcJU>e#t7^Noy2> zZeN-_-$Uw>nATVSp#l35MdM9kLv{MP!mL`qL5Ca{z1hwOvvbsA3ihZMbfz8uy0;AK z^!M_PF7e~vfyY@f+)iErv<`%|B5qp+gte!Du$DfsNvvK_hprRLWO$RHO5Z>vIFn6_ zoD{U4AF^;=x#QNQyyBPZLk+~np2>w8J>TdLqGOQs8KEJ>>{yBhYW%^VD><^yLuhu9 z+#v%lBFX|HvwsaY>p$2Iy+G6a1{Y^@Qu%i@->7P~6Mde26xj9Lsnm@_hMnj^U|x@n zBm(Rm>C=iwS>|*bP$9hvt2Dx+8I5+4UvK&m2CZ>L(oiE)h@g?lO`r|r!pPCM$dJL+ z_?Pj?8orQx;Yfcnsy?F3cvDk;gtM$L$>jzl1V;CJ{`15ZbvphDS7)o7K@U);VxWz3 zanLO8VCY@Nh*O6?o0du*| zX8#ZCQ=;AJ&!sF5aqzWh6+8RQ6Hqlb>rd|Pe%QaVvS0D5g=<8Z?VO-j3%Z&Yvs#ww-Fy_0-{BR{A+rmEmpdi zwdRDjFno4W=>hGD5?ecZDnD(=nAe>y+uTl#?Ej;NSg#na^jR>H!MO%|V}fh-t8`Go zT213eTmpC&+(q?^s;O53iG?>J|l{mEy7=uQ@~dxGAm1*vG_wG1@ zpL-El7D3|4C3BKfg7yS+H&o9YhZKd~svBUwH5oZf1B?g`*$8WcMx|>5`Fvt62@8I+ z$=x#NB->8jetPvRg{~`t_GIDX)5cZ9u!RvFW}52n!HuRNUhdr^5=SqT;8vj*&}5^0 z3QTt{P35lS%}Gr-o24gly`r`C_>sBS_HGg<&iouevFLTcT*CwdP2@ovTu1{e_ZXh6 z9ao;;C)7nEkq>G3P!rEzyu~ihxwa=_t7oPCw(6W&6m^B`M^}FKcXkKjwsv!#j2cHD zBXc+JaQ@QR5c1-EcjT*sPit+iXP;U8(sG#?`Gk2}3~$BCp+Poj3IKFVo>~*&`I!Se z&-8hJ6UyCJLWU$1bp^&PC^f<)Q#NS~dJsMP%*J1Ds-@MM*cv)3zn^G;W79K6${bc5`MOlpjA%h~&*|`S*el!ZhY^?Wi!L`sVbUe8(iki@!U0aDZ z+J~J`nIXD@t z-uWbbi_*<6yd_p&pA?WWP3c1p6tZO@U6fi+d}-ak`c| z+N+MH{JM5O`LiB(TY~0T6sL&d!9*i;GyEn6faKZSW!mHZNQC>zP(>9rL_x zoAs5#Smzti3&4=Fsw91`^Q?7^Wr-(Gp1Dg|By>XcSPWB&)=e34DZhxw3Jp>kNZ9=3 z&YcbJin?_8(t%&UV^o%(K1njUAC1NtPnl-JvEX zXOV?%sYJKgE;0GHTiD`zcG_?MsM>$Hm&$k#bj#q7bl(h(#qpPb8wuml8p(n1RWx$Q zRg}D=Sa-v=N2d2z*@fEKH%wFsQCvd2a_Wr|EgMiU3Te)v>q{THzL2zo>kCRMe?-{Q z7}Pm*d7vo9Qy`wvn`59;wM*XEIqK6^nXiw%H}^uTrIOl5z(OzcvOY{qu{HGN9?VVyO1D%wm= z#ca5oW5MJ7FaXFY@BOr@vr;v|m1dyMQ zb9UQsTrq>=He%;^(-$Q?GKq84%$n1hTDZWy{q3(q(>io$tR`|15}nW?qD+`+Xg$F>ojv zjq*tq-F3|!CW1k%yL#f5HI~kWnJ%@3j+NB9|FUk~7sEF)g$rmU4%Lm&|d~wdZOyf!Q{bytR#NX{dLTh0Rzli2w=7#F)yFQRR_B@Ar&1Um< zCjwoF4xW%5SKC3x4ixrjV5Vt>oy1O3--}usZiKeAgB7ib_`|6EAKKnL9?JdyA0AOE zTget_Iw4BZrj1OSkV=b^B~u|JX%mIHM97{>I#eX0#AGkYmA#TBVnU3q5R(}(E@tLB z_iLO}C-nK<-~0Fd+<){q#~8D`->-dnJ{$jzG-!0DMb*f|81^tvojJ{m^q>k;)n_!Z z=P__QpL_v-)e67zozsI}9%iNrR|;Zb#E z9T2fp7dErKC-&4Yltvn^kNKPwL&uV)JY0S|N6%kFp%10T>KNLV?7>XI@dLp}lX ziurX-Y@tkq)FVx<#`?-qVPZo$RAFi6ypF6N=#2ZTg#i^tz+*9>!W5~mcqV@!eofq# zHIV~*7WG}IRFC8P`sL#$^rkv?jB*6o;4yBzz#!o{3a+JE&jZ;+d`wRRAl0*FY$|5q z!jkVOt#R{A>F-SEY%rfu&>*kND4}t%Qf*Tjk=eme#{=(xhteS9+T?9W!94g{|01~q z1OtI9ke?4B&W4k<=Rt&huj?v*IqnelQMa5eag!kXj*eEaOPBER#Z2I4OeJtpI9EPI z-*_-A(^i=J;Wc=ij>Koy{eewQ>H+ZmOUvfLCUtbl z4G7+|5RBnL2ZgeMFBwWh`b1Q5KlM@1SczB3|HGRvg^%9)HD=HVqMd>pfZoW1R>Wk> z3zOJVd_Y6**C$~7ej(=gDvVjLuELsj-|3j0ywCbzOM^KV=xTOzD_4~Ape_rY)i3o1ey`6T)ZiK~B3(&dn5$s(^Gu?8`_}D9ctq!I)_}DAu z=$o^Ey>jM`oiwKiQjNcfnNeTOaj0`21UN9!AW;C|UbnUa@XI3DtUkaoV+yv%$0+DZ zVAkz7bKPp1ir2(-HVJyN-tf}i0%Q03OG7v*b&fR*o|;yz(Xlgt0bBpuvM~n zp{KZuot)bJS(;qsM`t@}C+r-P`;}^jSeiEe`o#wHX8S3oMvVTKW zHaK-_C}G9O*`+b3OP^PMa-qK7p+-oJDWE7hPj|~PU~MU4-$<}5Ijy96>Mw#uM)3Dw z+6u^0hDtz6^XnD{D1ls?&G<;DVh3G-V>xgPKrz2PTKY(qyX5tDwjild1GJd7ge#i- zV+^xfkWmtLO5|*0MfIpxPC!$Elp%6rM3et|`~U(3 z;y-w(h*o$Bnm#m!XV%~LvNUfD&&;AfQ+XlanFZxI*!I3ibsW;i=XoX|@At>R7fFV( zSjR8Ffx9K(wx~lDrlK1I$YXO@#FE#P1x8Quy~EdKIjUSW{IVmB$5OIZ$f!XT)H&k6 z*NzXEQc)00Z^+r>G7k7VEsLa-cd@4$zAU%w2yRE+j=^6o=-F3%9Vx86?Ysx+oc9t! zwG_%iE(qSgSrD3Q@z0cji2$aq$(=2{BTyBJYg8zhU5wm1t??51ulO z8=X6Lq5z6Tn$5NJ-zi_-y?9R0!5eQSFJ6CwcnI=Av4(Ro{RuNn=LJU4U-eN2^wES5 zRRmQEx|$sOQ&I=FtbIBce}AeAe{R}p`!CB1 z05|*lxO{I%XV|wavZqwKSgczXeB|30wqAcQ&Cu%1Z)zIz@;AaOt7P_v5@mF@vbK<~ zp6xNFTCV=maPr{h`R=E_PeYcl`v28rSxvqsw|oV{*H^qu)V3EO+1e8li`+dg0wmi8 z4^z6m+ctn$qMfR)sCBlay&j`loxGqy@Pw>I5^t78(kiHd8Yy{d_aMf-62bTs`Fss- z+E66EJmk(dz>@$hszjdTGlXeF=bk`?z_5JHU@rsN32VXY5$R>P!Y8*SBnge|yBfod zGJTbX0pS-IYA;<~Dkw>&1E2qitRB?;1c-sjMIr!N86lnJl!XXQ#F@7^6b)N~7op>w zJ-IAoQlvZ+Ryo%En8HTi(kLEaLejN?w9;6W5ZYDaiqO; zLFxR74&*@-i0k=diL6FuSfd3#Lq8F(0-kxD&8%a@t1EjY;+86#X9X53f44n^W7j(P ziuTYPv%_cVfl)rw(Lby~8hpHjfNy{x7S!(-7~yDIyaz1-^A=89Q_csq9wRkO? zdbq`^`Pp!a#6Qxu?;pSM1VW94L}S=7;P2S8Awz{%pkj-7VF2%?+uN>(hS;z~EI%4q zvt2W15maoQntb?hfaJuRka$S_`|p1-r25}NwjHxIlWIe>|Af{(Ib<~Tr%hrM09NWN znLmO;fb9N>-k$b1%t)0pztJz0#k;S&g6qboSr$F9VX3**OE=mqjgsO3wJn4ZuV62? zA*v9F=W6@|GAIodDts#R=1XDx1iB;gJTJsfVjljEG|gE@pksuwOl$7Y;$?MDIvtb- zTGXnin3;5`xto5$e;Mlg*xafzjZkUrec-~(8FSaoJ4db&B{I!O4Hwo~hh(C&Y)u65Alah;59&B_ z0g5O|WuPFhDdjY^dREURyl3;?=tF6@T9I;dE}KSby-CirD`p($w6D4R$UeO1o@&&t z9Sz5yhuh9-UJ)U9d9FbA)7Njlpnv@5K}Ii9bPWIW^v9jveNuye+r5`e*H(QWTpqh; zU|Y9ZK`ZBO%EwH-0@csTQ}lHs`b}Vne+K0}+|nMz8_kdiJ;D?&7J>9<@3SnU632Ct z2Hczrn`YSUzjL2KzCatOgFqn^g6W@d2wr?P+_nKaAe>L(ZiXg7IKUK-P&{+~+;VgUzT{?YUIVca=hR>&s4Xr&7DMCwA^*C2sQ((z9fFQ$0Xb zFYg1(_;{qBeH~8GlWPx<`!j@#iC5R=9xlCm##Qlp%FTY|dp3dZW{3u^@ZH$UnLWAB z>_v){=X>gAW|@D#hiSW_c9HqHCCw+!CFJQCnIeRQNd@_7&uW;;-&}9jgvN0o{$sZJd>e~;tggwHY%OGf; zdYE?ym+P)4iM#N$#{k?{^yFxWW%2>sx9#5Zr-1tN-QpV#CUSKx*p@IVNxn*AN@_w* zRZ%q_2U}Jxl=wnpGk5XG-(x>XgDOHy7hfAumLJ1pfuc`x}a> z(itkP#tp6=di`*C%MX8YpvhaZcVzEu z4f2dPTR80=ZU3@hc;B=cOMN4i-uC*CYxu)=&w=LyIYAnoi6MUxrhvEUFd2J9^Or{s z@V37nTmM?0$bJBKKn1(OTmp!}`rL_+kUu#6_C*g2CcaYmh_>F?^L5mf@yFdA##eVq z_cPV28gbV*E0;>b*orBK1!5Qh6o6FfnKByNqN)yo1} zy%0GGMu?mb6Rs4dL55&qK9sJoyL%YyP>8T^@u^oK!@!<0aNQcPZ{JgCkkBM zxRnhP0p51nZ%5B3vPAHE5^`r`d- zxYUpsVElLz^wECRH40SUp1k`K=dzXL6H@~BVN58tHs zbBL4c`yN0ypf6CQC|}lAi6%OpJwcK4+qmZQ{p^sqRpv%b5b+qResR`D7{^Vl%bSH~=SNe}lEa{F6Q`y}wWv%vUG zD)%1X`N4Hh3|d?O4M3~#F8vP9F z_3Cs;euS{WNA3Vow1nG)Y5zq~u`ZyqWnj(YeL1<=_NOr;;8uJZ3n1|L#U@X-(!gRc zHq13y|0_u$r0HBEre^#06ee0SP_QQX!R_LTlO^rCCmk0w8VE6qvJe+7E<~UOzrw_B zB8t$O5s?4C@1@lcU%=IUS;H9Yb$`C}PiH%~&hJVGj%_kI8oa!pJadfgVM6tEk8qbT zUwz|#RcNv@$jh>;lm zqfk2(^Q3_@;PB$}W^k5q?O%@Zle`Go&|!4%&B3=?*Ns=|Zx`~jl`FhA#7vM9fRUJ@ z&!{arIu-MQW#@%paI(7SQixOk6*Px*d5GBH(1x4d-0B25a)QklYBNnqq+mW8gCnEh4$o_Bqt z?a<42_m?Fs-M3h9jc^1F_7x(#W2eV%owuU2`M@TU*Tci0J5T7})(#{~Tm8(5fm0P3 zALRE=u8Vhf6*yyyG|T!!W5fny0`S=%R@4IGgzLQp{wl znMt2C7&0ip-HO+cGn_+FTe@NalhZEBj+756iFbWmzi)N;$k|nDLg|kxP1w|Xy+i14 z<^%~m@Y3FAC=G}RV$+Z9B(+&Lew8IPwdjYlHm41pFv-OeEETG8b!TCC}F3*N!r#GW}bxM0t>~8hmsXQ;h`!n5B&iuaT4=<(8jlFM?##3gz%Zp)4~U zf-FnuEMx&kek=)kkwk&U5&`O9)l2?77CDK;+}(5)IVPexw~_K;sm=_|K$}cYE$4jW zOpT{!7L$jKQYl%SE^^gG4mlXKApUwsVDEq-8yDrJ=ZiIPm+_*r*5x_1yC2>yw#7(Y zDPJ!;K+$T;MgbRM2%@DmN{7$o0H5pM3eO(la$s&GWud$|FSp{@16)p+4KY%jE8rLa z6C`A)rDX1M2?m(iZT)*d0L3nlB#>h#Fk(w>t}3eeBtw*SXeof9}y^ev3rk zd3s6nF4HD0qxztLGrRt?)=-#F&)1Z4d*`d<<-i&E;PEc0&^|n`<-PG ze0t=4W!w1l$mIPuH#^uN&P&tR>gk;}B<=V<3bq~Sk)hXT6Q2`c(LVp)^aH|O?8IFh z5B$ybBIUi@Ew$wvA_S3r+ovLP>;9!}Ab2)(RDFzO-(~rSxb(}ooqLr-%RQLpdB)Xo z!=i(S7G29-$F^M%2xDw~A#@%yF-*r%2DXt;DNz77ID;Rss#^gu;B8`tCPRz4wN;`( zLc~KE%W01$i(}NH>||$8>(foP-Uq%u+346p`sMb>?;;sISUzxKN#KPN+}@5$1e`&U z^RU1?S&>BpZi#LAY2rB#^;TJAR^LgAAgEAcXIq7luahP#K`_^p7n?teJL`sVP`m$C z#-L2y*65?!*6(I_T=*aQAI=1!m@ImaNRDBR3a!Hob(lX-Zbb`7k``_cO05=#N~P7N zSe&oT+!r4z<-YI+X4*y^Z`;m;1KdR%*{JCsGt}4xjple5$@U_JAp&?Yn%VD@=M;-& z6jP?=r`Ke<_qxA$)Vw@xv*_jKs`Mmf+Mv4x<2z@Op6D4vmWZ)vpi~HNx}4UaYAJC< zW&Uy)v)R>;2b{pAe%dx*UbmQ_8Dz|v4phd0^J9RpgT}f68Pjp}S9=e)*;vj#kG&k+0alPnS2W~oBCeX5O~cMZ7e1CP-(K>qFL{#oXQe0l)^%_Dok z5s(Bw0bUBEW_Mxjw*=WtXBx#^iJ6rzMya~w?~5yUf47gilUr9&ShT!I?%ujr&i5D4 zdbD63yH%De^F6W1nB$H|a5!Vp|3A|cYJ$vZ{Dp-47d&4bmo~op&XVx)uPPi#)-zI7}bR{N4K`zKnUhZE6TPN$i*)`|s#E-jNE-u_EsXkEIHB~V$rYT@DIPv&l2{90*Y+_Qb`fw83oyrr_=t6+6D#$D6c;e?AuPxj9vq@9ZGZ8*_Hk(2N~wf}PGEEnej>#+*5t*xG{p z)lYBitY0eGY3!_4^pF!U_uP<>{OFfJsJeo89}i>TM zC2X+0&_bq{0w@+NK~kBF(ignw9LD@DUFGyzk~!NLJ3?%v;{h%AfxX(a2l!?GMc0az z_?sp8rRLdFIPeXZ^4Oi0 zeTzL@7!@zWTzpa4ef@#GCN|f1v=X#|3zByO>Qz;DNW%=IFwcm6vv&0dUW~3%uKAWH zxzpk|&rVvvyUT+em&*%$d_L?EP!)Xi=-clC4DDySk{`z~|hsyr^2dOY{=$uCRZ zUKE;@$LISkyo;s5s^JCD9k2ZeON&SKSWq)qitTS@I~o{n*mcV=1nCOom_9&kdXVp3 z587tA|j7#;GgpJP0a^_Bn1{{kA6Mm+Ci{z++hm2W=^fyAET$Q~WLIvH^kR3oR zXeNPs6GZKRW3FZqsiA34HDw`rbKBh=yN){?Y}y;{bbQ6_V*-Uc4rE`SUa4)`lgM30 z^d%0ib0A>~5`d*cuE^oaOuLxb-{`p0$2dI2dc!=SC)?t>s-o{Kn@WKiq~i(n`^Z`z z*O>*~T3A1lB$YTBwH3#R-o#SQW7Lv2HGdImg;W|h-sh$?o%0DEqLw&g`%xKE_wG8B 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%0D9J>1mGJnKkBeQKvPYg3#H9i?Hu;is3V|C&*15V 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 diff --git a/docs/images/dingtalk_group.png b/docs/images/dingtalk_group.png new file mode 100644 index 0000000000000000000000000000000000000000..a6197ca2122490198a2478df92bc93df198d80c2 GIT binary patch literal 251106 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Μ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 literal 0 HcmV?d00001 diff --git a/scripts/case_test.sh b/scripts/case_test.sh index 083e83219..9782ab3f6 100755 --- a/scripts/case_test.sh +++ b/scripts/case_test.sh @@ -1856,10 +1856,10 @@ clear_log echo -e "timer_based_dgram_probe...\c" ./test_client -l d -T 1 -x 209 -s 1000 -U 1 -Q 65535 -x 209 > stdlog killall test_server -cli_res1=`grep "|recv_dgram_bytes:3000|sent_dgram_bytes:1000|" stdlog` -svr_res=`grep -a "|recv_dgram_bytes:2000|sent_dgram_bytes:2000|" svr_stdlog` -errlog=`grep_err_log` -if [ -n "$cli_res1" ] && [ -n "$svr_res" ] && [ -z "$errlog" ]; then +cli_res1=(`grep "|recv_dgram_bytes:" stdlog | egrep -o ':[0-9]+' | egrep -o '[0-9]+'`) +svr_res=(`grep "|recv_dgram_bytes:" svr_stdlog | egrep -o ':[0-9]+' | egrep -o '[0-9]+'`) +if [ ${cli_res1[0]} -ge 3000 ] && [ ${cli_res1[1]} -ge 1000 ] \ + && [ ${svr_res[0]} -ge 2000 ] && [ ${svr_res[1]} -ge 2000 ]; then echo ">>>>>>>> pass:1" case_print_result "timer_based_dgram_probe" "pass" else @@ -3461,12 +3461,15 @@ echo -e "h3_ext_finish_bytestream_during_transmission...\c" ./test_client -l d -T 2 -s 102400 -U 1 -Q 65535 -E -x 304 > stdlog cli_res1=`grep ">>>>>>>> pass:1" stdlog | wc -l` cli_res2=`grep "\[dgram\]|echo_check|same_content:yes|" stdlog` -cli_res3=`grep "\[h3-dgram\]|recv_dgram_bytes:102400|sent_dgram_bytes:102400|lost_dgram_bytes:0|lost_cnt:0|" stdlog` -cli_res4=`grep "\[bytestream\]|bytes_sent:102400|bytes_rcvd:102400|recv_fin:1|" stdlog` +cli_res3=(`grep "\[h3-dgram\]|recv_dgram_bytes:" stdlog | egrep -o ':[0-9]+' | egrep -o '[0-9]+'`) +cli_res4=(`grep "\[bytestream\]|bytes_sent:" stdlog | egrep -o ':[0-9]+' | egrep -o '[0-9]+'`) cli_res5=`grep "\[bytestream\]|same_content:yes|" stdlog | wc -l` cli_res6=`grep "send pure fin" clog` errlog=`grep_err_log | grep -v "send data after fin sent"` -if [ "$cli_res1" == "1" ] && [ -n "$cli_res2" ] && [ -n "$cli_res3" ] && [ -n "$cli_res4" ] && [ "$cli_res5" == "1" ] && [ -n "$cli_res6" ] && [ -z "$errlog" ]; then +if [ "$cli_res1" == "1" ] && [ -n "$cli_res2" ] \ + && [ ${cli_res3[0]} -ge 102400 ] && [ ${cli_res3[1]} -ge 102400 ] \ + && [ ${cli_res4[0]} -ge 102400 ] && [ ${cli_res4[1]} -ge 102400 ] \ + && [ "$cli_res5" == "1" ] && [ -n "$cli_res6" ] && [ -z "$errlog" ]; then echo ">>>>>>>> pass:1" case_print_result "h3_ext_finish_bytestream_during_transmission" "pass" else @@ -3481,12 +3484,15 @@ echo -e "h3_ext_close_bytestream_during_transmission...\c" ./test_client -l d -T 2 -s 102400 -U 1 -Q 65535 -E -x 305 > stdlog cli_res1=`grep ">>>>>>>> pass:1" stdlog | wc -l` cli_res2=`grep "\[dgram\]|echo_check|same_content:yes|" stdlog` -cli_res3=`grep "\[h3-dgram\]|recv_dgram_bytes:102400|sent_dgram_bytes:102400|lost_dgram_bytes:0|lost_cnt:0|" stdlog` -cli_res4=`grep "\[bytestream\]|bytes_sent:102400|.*|recv_fin:0|" stdlog` +cli_res3=(`grep "\[h3-dgram\]|recv_dgram_bytes:" stdlog | egrep -o ':[0-9]+' | egrep -o '[0-9]+'`) +cli_res4=(`grep "\[bytestream\]|bytes_sent:" stdlog | egrep -o ':[0-9]+' | egrep -o '[0-9]+'`) cli_res5=`grep "\[bytestream\]|same_content:.*|" stdlog | wc -l` cli_res6=`grep "xqc_h3_ext_bytestream_close|success" clog` errlog=`grep_err_log | grep -v "xqc_h3_stream_process_data|xqc_stream_recv"` -if [ "$cli_res1" == "1" ] && [ -n "$cli_res2" ] && [ -n "$cli_res3" ] && [ -n "$cli_res4" ] && [ "$cli_res5" == "1" ] && [ -n "$cli_res6" ] && [ -z "$errlog" ]; then +if [ "$cli_res1" == "1" ] && [ -n "$cli_res2" ] \ + && [ ${cli_res3[0]} -ge 102400 ] && [ ${cli_res3[1]} -ge 102400 ] \ + && [ ${cli_res4[0]} -ge 102400 ] \ + && [ "$cli_res5" == "1" ] && [ -n "$cli_res6" ] && [ -z "$errlog" ]; then echo ">>>>>>>> pass:1" case_print_result "h3_ext_close_bytestream_during_transmission" "pass" else