diff --git a/CMakeLists.txt b/CMakeLists.txt index 6de2f267f..88de858be 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -12,7 +12,6 @@ OPTION(LSQUIC_BIN "Compile example binaries that use the library" ON) OPTION(LSQUIC_TESTS "Compile library unit tests" ON) OPTION(LSQUIC_SHARED_LIB "Compile as shared librarry" OFF) OPTION(LSQUIC_DEVEL "Compile in development mode" OFF) -OPTION(LSQUIC_WEBTRANSPORT "Enable WebTransport support" OFF) INCLUDE(GNUInstallDirs) INCLUDE(CheckSymbolExists) @@ -82,9 +81,6 @@ IF (LSQUIC_DEVEL) SET(MY_CMAKE_FLAGS "${MY_CMAKE_FLAGS} -DLSQUIC_DEVEL=1") ENDIF() -IF (LSQUIC_WEBTRANSPORT) - SET(MY_CMAKE_FLAGS "${MY_CMAKE_FLAGS} -DLSQUIC_WEBTRANSPORT_SERVER_SUPPORT=1") -ENDIF() IF(LSQUIC_PROFILE EQUAL 1) SET(MY_CMAKE_FLAGS "${MY_CMAKE_FLAGS} -g -pg") @@ -438,6 +434,7 @@ ENDIF() INSTALL(FILES include/lsquic.h + include/lsquic_wt.h include/lsquic_types.h include/lsxpack_header.h DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/lsquic diff --git a/bin/CMakeLists.txt b/bin/CMakeLists.txt index c856bd045..7ccee5473 100644 --- a/bin/CMakeLists.txt +++ b/bin/CMakeLists.txt @@ -23,7 +23,7 @@ IF(MSVC) LIST(APPEND LIBS ${GETOPT_LIB}) ENDIF() -add_executable(http_server http_server.c prog.c test_common.c test_cert.c) +add_executable(http_server http_server.c devious_baton.c prog.c test_common.c test_cert.c) IF(NOT MSVC) # TODO: port MD5 server and client to Windows add_executable(md5_server md5_server.c prog.c test_common.c test_cert.c) add_executable(md5_client md5_client.c prog.c test_common.c test_cert.c) @@ -32,6 +32,9 @@ add_executable(echo_server echo_server.c prog.c test_common.c test_cert.c) add_executable(echo_client echo_client.c prog.c test_common.c test_cert.c) add_executable(duck_server duck_server.c prog.c test_common.c test_cert.c) add_executable(duck_client duck_client.c prog.c test_common.c test_cert.c) +add_executable(bat_server bat_server.c prog.c test_common.c test_cert.c) +add_executable(bat_client bat_client.c prog.c test_common.c test_cert.c) +add_executable(baton_client baton_client.c devious_baton.c prog.c test_common.c test_cert.c) add_executable(perf_client perf_client.c prog.c test_common.c test_cert.c) add_executable(perf_server perf_server.c prog.c test_common.c test_cert.c) @@ -67,6 +70,9 @@ TARGET_LINK_LIBRARIES(echo_server ${LIBS}) TARGET_LINK_LIBRARIES(echo_client ${LIBS}) TARGET_LINK_LIBRARIES(duck_server ${LIBS}) TARGET_LINK_LIBRARIES(duck_client ${LIBS}) +TARGET_LINK_LIBRARIES(bat_server ${LIBS}) +TARGET_LINK_LIBRARIES(bat_client ${LIBS}) +TARGET_LINK_LIBRARIES(baton_client ${LIBS}) TARGET_LINK_LIBRARIES(perf_client ${LIBS}) TARGET_LINK_LIBRARIES(perf_server ${LIBS}) diff --git a/bin/bat_client.c b/bin/bat_client.c new file mode 100644 index 000000000..541ec09e4 --- /dev/null +++ b/bin/bat_client.c @@ -0,0 +1,570 @@ +/* Copyright (c) 2017 - 2026 LiteSpeed Technologies Inc. See LICENSE. */ +/* + * bat_client.c -- Simple BAT (HTTP Datagram Bat) client for HTTP/3. + * + * BAT (Bidirectional Attestation Test) is a simple echo protocol for testing + * HTTP Datagram implementations (RFC 9297). It uses Extended CONNECT to + * establish a stream context, then sends HTTP Datagrams via QUIC DATAGRAM + * frames and optionally via Capsule Protocol. + * + * This example demonstrates: + * - Setting up Extended CONNECT with :protocol and Capsule-Protocol headers + * - Enabling HTTP Datagram support via lsquic_stream_set_http_dg_capsules() + * - Sending datagrams via on_http_dg_write() callback + * - Receiving datagrams via on_http_dg_read() callback + * - Using both QUIC DATAGRAM and Capsule transport modes + * + * See docs/draft-tikhonov-httpbis-bat-00.txt for BAT protocol specification. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifndef WIN32 +#include +#else +#include "vc_compat.h" +#include "getopt.h" +#endif + +#include + +#include "lsquic.h" +#include "test_common.h" +#include "prog.h" + +#define TOOL_LOG_PREFIX "bat_client" +#include "tool_log.h" +#include "lsxpack_header.h" + +#define BAT_PROTOCOL "bat-00" +#define BAT_PATH "/bat" +#define BAT_PAYLOAD "bat-ping" + +struct hset_elem +{ + STAILQ_ENTRY(hset_elem) next; + size_t nalloc; + struct lsxpack_header xhdr; +}; + +STAILQ_HEAD(hset, hset_elem); + +struct lsquic_conn_ctx +{ + struct prog *prog; + lsquic_stream_t *stream; + int have_stream; + int headers_sent; + int response_seen; + int response_ok; + int datagram_sent; + int datagram_echoed; + int capsule_sent; + int capsule_echoed; + int want_capsule; + const char *path; + const unsigned char *payload; + size_t payload_sz; + unsigned char *capsule_payload; + size_t capsule_payload_sz; +}; + +static void * +hset_create (void *UNUSED_hsi_ctx, lsquic_stream_t *UNUSED_stream, + int UNUSED_is_push_promise) +{ + struct hset *hset; + + if ((hset = malloc(sizeof(*hset)))) + { + STAILQ_INIT(hset); + return hset; + } + else + return NULL; +} + +static struct lsxpack_header * +hset_prepare_decode (void *hset_p, struct lsxpack_header *xhdr, + size_t req_space) +{ + struct hset *const hset = hset_p; + struct hset_elem *el; + char *buf; + + if (0 == req_space) + req_space = 0x100; + + if (req_space > LSXPACK_MAX_STRLEN) + { + LSQ_WARN("requested space for header is too large: %zd bytes", + req_space); + return NULL; + } + + if (!xhdr) + { + buf = malloc(req_space); + if (!buf) + { + LSQ_WARN("cannot allocate buf of %zd bytes", req_space); + return NULL; + } + el = malloc(sizeof(*el)); + if (!el) + { + LSQ_WARN("cannot allocate hset_elem"); + free(buf); + return NULL; + } + STAILQ_INSERT_TAIL(hset, el, next); + lsxpack_header_prepare_decode(&el->xhdr, buf, 0, req_space); + el->nalloc = req_space; + } + else + { + el = (struct hset_elem *) ((char *) xhdr + - offsetof(struct hset_elem, xhdr)); + if (req_space <= el->nalloc) + { + LSQ_ERROR("requested space is smaller than already allocated"); + return NULL; + } + if (req_space < el->nalloc * 2) + req_space = el->nalloc * 2; + buf = realloc(el->xhdr.buf, req_space); + if (!buf) + { + LSQ_WARN("cannot reallocate hset buf"); + return NULL; + } + el->xhdr.buf = buf; + el->xhdr.val_len = req_space; + el->nalloc = req_space; + } + + return &el->xhdr; +} + +static int +hset_add_header (void *UNUSED_hset_p, struct lsxpack_header *UNUSED_xhdr) +{ + return 0; +} + +static void +hset_destroy (void *hset_p) +{ + struct hset *hset = hset_p; + struct hset_elem *el, *next; + + for (el = STAILQ_FIRST(hset); el; el = next) + { + next = STAILQ_NEXT(el, next); + free(el->xhdr.buf); + free(el); + } + free(hset); +} + +static const struct lsquic_hset_if header_bypass_api = +{ + .hsi_create_header_set = hset_create, + .hsi_prepare_decode = hset_prepare_decode, + .hsi_process_header = hset_add_header, + .hsi_discard_header_set = hset_destroy, +}; + +static void +send_headers (struct lsquic_conn_ctx *ctx) +{ + struct header_buf hbuf; + struct lsxpack_header headers_arr[6]; + const char *hostname = ctx->prog->prog_hostname; + unsigned h_idx = 0; + + if (!hostname) + hostname = "localhost"; + +#define V(v) (v), strlen(v) + hbuf.off = 0; + /* Extended CONNECT request per RFC 9220 (HTTP/3) */ + header_set_ptr(&headers_arr[h_idx++], &hbuf, V(":method"), V("CONNECT")); + /* :protocol header identifies the protocol (BAT in this case) */ + header_set_ptr(&headers_arr[h_idx++], &hbuf, V(":protocol"), V(BAT_PROTOCOL)); + header_set_ptr(&headers_arr[h_idx++], &hbuf, V(":scheme"), V("https")); + header_set_ptr(&headers_arr[h_idx++], &hbuf, V(":path"), V(ctx->path)); + header_set_ptr(&headers_arr[h_idx++], &hbuf, V(":authority"), V(hostname)); + /* Capsule-Protocol: ?1 header per RFC 9297 enables HTTP Datagrams */ + header_set_ptr(&headers_arr[h_idx++], &hbuf, V("capsule-protocol"), V("?1")); +#undef V + + lsquic_http_headers_t headers = { + .count = h_idx, + .headers = headers_arr, + }; + if (0 != lsquic_stream_send_headers(ctx->stream, &headers, 0)) + { + LSQ_ERROR("cannot send headers: %s", strerror(errno)); + exit(1); + } + ctx->headers_sent = 1; +} + +static int +parse_status (struct hset *hset) +{ + const struct hset_elem *el; + const char *name; + const char *value; + + STAILQ_FOREACH(el, hset, next) + { + name = lsxpack_header_get_name(&el->xhdr); + if (el->xhdr.name_len == sizeof(":status") - 1 + && 0 == memcmp(name, ":status", sizeof(":status") - 1)) + { + value = lsxpack_header_get_value(&el->xhdr); + return el->xhdr.val_len > 0 && value[0] == '2'; + } + } + + return 0; +} + +static lsquic_conn_ctx_t * +bat_client_on_new_conn (void *stream_if_ctx, lsquic_conn_t *conn) +{ + struct lsquic_conn_ctx *ctx = stream_if_ctx; + LSQ_NOTICE("created a new connection"); + lsquic_conn_make_stream(conn); + return ctx; +} + +static void +bat_client_on_conn_closed (lsquic_conn_t *conn) +{ + struct lsquic_conn_ctx *ctx = lsquic_conn_get_ctx(conn); + LSQ_NOTICE("Connection closed, stop client"); + prog_stop(ctx->prog); + free(ctx->capsule_payload); + ctx->capsule_payload = NULL; + lsquic_conn_set_ctx(conn, NULL); +} + +static lsquic_stream_ctx_t * +bat_client_on_new_stream (void *stream_if_ctx, lsquic_stream_t *stream) +{ + struct lsquic_conn_ctx *ctx = stream_if_ctx; + if (ctx->have_stream) + { + LSQ_WARN("extra stream opened: closing"); + lsquic_stream_close(stream); + return NULL; + } + ctx->stream = stream; + ctx->have_stream = 1; + lsquic_stream_wantwrite(stream, 1); + lsquic_stream_wantread(stream, 1); + return NULL; +} + +static void +bat_client_on_write (lsquic_stream_t *stream, lsquic_stream_ctx_t *UNUSED_st_h) +{ + struct lsquic_conn_ctx *ctx = lsquic_stream_conn(stream) + ? lsquic_conn_get_ctx(lsquic_stream_conn(stream)) + : NULL; + if (!ctx || ctx->headers_sent) + return; + + LSQ_NOTICE("sending BAT CONNECT request headers"); + send_headers(ctx); + ctx->headers_sent = 1; + if (0 != lsquic_stream_flush(stream)) + LSQ_ERROR("cannot flush CONNECT headers: %s", strerror(errno)); + lsquic_stream_wantread(stream, 1); + lsquic_stream_wantwrite(stream, 0); + return; +} + +static void +bat_client_on_read (lsquic_stream_t *stream, lsquic_stream_ctx_t *UNUSED_st_h) +{ + struct lsquic_conn_ctx *ctx = lsquic_conn_get_ctx(lsquic_stream_conn(stream)); + struct hset *hset; + int ok; + size_t max_payload; + if (ctx->response_seen) + return; + + /* Read response headers from Extended CONNECT */ + hset = lsquic_stream_get_hset(stream); + if (!hset) + { + LSQ_ERROR("could not get header set from stream"); + lsquic_conn_abort(lsquic_stream_conn(stream)); + return; + } + + /* Check for 2xx status code */ + ok = parse_status(hset); + hset_destroy(hset); + ctx->response_seen = 1; + ctx->response_ok = ok; + lsquic_stream_wantread(stream, 1); + + if (!ok) + { + LSQ_ERROR("server rejected BAT CONNECT"); + lsquic_conn_close(lsquic_stream_conn(stream)); + return; + } + + LSQ_NOTICE("received BAT CONNECT response"); + + /* Enable HTTP Datagram capsule processing for capsule-carried datagrams. */ + if (0 != lsquic_stream_set_http_dg_capsules(stream, 1)) + LSQ_WARN("cannot enable HTTP Datagram capsules: %s", strerror(errno)); + + /* Check if HTTP Datagrams were negotiated */ + max_payload = lsquic_stream_get_max_http_dg_size(stream); + if (max_payload == 0) + { + LSQ_ERROR("HTTP datagrams not negotiated"); + lsquic_conn_close(lsquic_stream_conn(stream)); + return; + } + + /* Prepare to test capsule mode with payload larger than QUIC DATAGRAM MTU */ + ctx->want_capsule = 1; + { + struct lsquic_conn_info info; + if (0 == lsquic_conn_get_info(lsquic_stream_conn(stream), &info) + && info.lci_pmtu > 0) + /* Create payload larger than PMTU to force capsule mode */ + ctx->capsule_payload_sz = (size_t) info.lci_pmtu + 1; + else + ctx->capsule_payload_sz = 0; + } + ctx->capsule_payload = NULL; + + /* Request HTTP Datagram write callback */ + if (lsquic_stream_want_http_dg_write(stream, 1) < 0) + LSQ_ERROR("want_http_dg_write failed"); +} + +static int +bat_client_on_http_dg_write (lsquic_stream_t *stream, lsquic_stream_ctx_t *h, + size_t max_quic_payload, + lsquic_http_dg_consume_f consume) +{ + struct lsquic_conn_ctx *ctx = lsquic_conn_get_ctx(lsquic_stream_conn(stream)); + + (void) h; + + if (!ctx->response_ok || !ctx->have_stream) + return -1; + + if (!ctx->datagram_sent) + { + /* First send: small payload via QUIC DATAGRAM frame + * Use LSQUIC_HTTP_DG_SEND_DATAGRAM to force QUIC DATAGRAM transport */ + if (0 != consume(stream, ctx->payload, ctx->payload_sz, + LSQUIC_HTTP_DG_SEND_DATAGRAM)) + return -1; + ctx->datagram_sent = 1; + LSQ_INFO("sent BAT datagram payload of %zu bytes", ctx->payload_sz); + /* Re-arm write callback for capsule send */ + if (ctx->capsule_payload && !ctx->capsule_sent) + { + lsquic_stream_want_http_dg_write(stream, 0); + lsquic_stream_want_http_dg_write(stream, 1); + } + } + else if (ctx->want_capsule && !ctx->capsule_sent) + { + /* Prepare large payload to force Capsule Protocol transport */ + if (!ctx->capsule_payload) + { + size_t want_sz; + + if (ctx->capsule_payload_sz > 0) + want_sz = ctx->capsule_payload_sz; + else + { + if (max_quic_payload == SIZE_MAX) + { + LSQ_ERROR("max QUIC datagram payload too large"); + return -1; + } + want_sz = max_quic_payload + 1; + } + + if (want_sz <= max_quic_payload && max_quic_payload < SIZE_MAX) + want_sz = max_quic_payload + 1; + + ctx->capsule_payload_sz = want_sz; + ctx->capsule_payload = malloc(ctx->capsule_payload_sz); + if (!ctx->capsule_payload) + { + LSQ_ERROR("cannot allocate capsule payload"); + return -1; + } + memset(ctx->capsule_payload, 'C', ctx->capsule_payload_sz); + } + + /* Send large payload with DEFAULT mode - library will automatically + * use Capsule Protocol since payload > max_quic_payload */ + if (0 != consume(stream, ctx->capsule_payload, + ctx->capsule_payload_sz, + LSQUIC_HTTP_DG_SEND_DEFAULT)) + return -1; + ctx->capsule_sent = 1; + LSQ_NOTICE("sent BAT capsule payload of %zu bytes", + ctx->capsule_payload_sz); + } + else + { + /* All datagrams sent, disable write callbacks */ + lsquic_stream_want_http_dg_write(stream, 0); + return 0; + } + + if (!ctx->capsule_payload || ctx->capsule_sent) + lsquic_stream_want_http_dg_write(stream, 0); + return 0; +} + +static void +bat_client_on_http_dg_read (lsquic_stream_t *stream, lsquic_stream_ctx_t *h, + const void *buf, size_t bufsz) +{ + struct lsquic_conn_ctx *ctx = lsquic_conn_get_ctx(lsquic_stream_conn(stream)); + + (void) h; + + LSQ_NOTICE("received BAT datagram payload of %zu bytes", bufsz); + + /* BAT protocol echoes datagrams back, verify we got what we sent */ + if (bufsz == ctx->payload_sz + && 0 == memcmp(buf, ctx->payload, ctx->payload_sz)) + { + LSQ_INFO("received expected BAT datagram echo"); + ctx->datagram_echoed = 1; + if (ctx->want_capsule && !ctx->capsule_sent) + lsquic_stream_want_http_dg_write(stream, 1); + } + else if (ctx->capsule_payload + && bufsz == ctx->capsule_payload_sz + && 0 == memcmp(buf, ctx->capsule_payload, ctx->capsule_payload_sz)) + { + LSQ_NOTICE("received expected BAT capsule echo"); + ctx->capsule_echoed = 1; + } + else + { + LSQ_NOTICE("unexpected BAT payload"); + lsquic_conn_abort(lsquic_stream_conn(stream)); + return; + } + + /* Both echoes received, test complete */ + if (ctx->datagram_echoed + && (!ctx->want_capsule || ctx->capsule_echoed)) + lsquic_conn_close(lsquic_stream_conn(stream)); +} + +static void +bat_client_on_close (lsquic_stream_t *stream, lsquic_stream_ctx_t *UNUSED_st_h) +{ + (void) stream; +} + +const struct lsquic_stream_if bat_client_stream_if = { + .on_new_conn = bat_client_on_new_conn, + .on_conn_closed = bat_client_on_conn_closed, + .on_new_stream = bat_client_on_new_stream, + .on_read = bat_client_on_read, + .on_write = bat_client_on_write, + .on_http_dg_write = bat_client_on_http_dg_write, + .on_http_dg_read = bat_client_on_http_dg_read, + .on_close = bat_client_on_close, +}; + +static void +usage (const char *prog) +{ + const char *const slash = strrchr(prog, '/'); + if (slash) + prog = slash + 1; + LSQ_NOTICE( +"Usage: %s [opts]\n" +"\n" +"Options:\n" + , prog); +} + +int +main (int argc, char **argv) +{ + int opt, s; + struct sport_head sports; + struct prog prog; + struct lsquic_conn_ctx ctx; + const char *const *alpns; + + memset(&ctx, 0, sizeof(ctx)); + ctx.path = BAT_PATH; + ctx.payload = (const unsigned char *) BAT_PAYLOAD; + ctx.payload_sz = sizeof(BAT_PAYLOAD) - 1; + + TAILQ_INIT(&sports); + prog_init(&prog, LSENG_HTTP, &sports, &bat_client_stream_if, &ctx); + prog.prog_settings.es_http_datagrams = 1; + prog.prog_api.ea_hsi_if = &header_bypass_api; + prog.prog_api.ea_hsi_ctx = NULL; + alpns = lsquic_get_h3_alpns(prog.prog_settings.es_versions); + prog.prog_api.ea_alpn = alpns[0]; + + ctx.prog = &prog; + + while (-1 != (opt = getopt(argc, argv, PROG_OPTS "h"))) + { + switch (opt) { + case 'h': + usage(argv[0]); + prog_print_common_options(&prog, stdout); + exit(0); + default: + if (0 != prog_set_opt(&prog, opt, optarg)) + exit(1); + } + } + + if (0 != prog_prep(&prog)) + { + LSQ_ERROR("could not prep"); + exit(EXIT_FAILURE); + } + if (0 != prog_connect(&prog, NULL, 0)) + { + LSQ_ERROR("could not connect"); + exit(EXIT_FAILURE); + } + + LSQ_DEBUG("entering event loop"); + + s = prog_run(&prog); + prog_cleanup(&prog); + + exit(0 == s ? EXIT_SUCCESS : EXIT_FAILURE); +} diff --git a/bin/bat_server.c b/bin/bat_server.c new file mode 100644 index 000000000..6eb698403 --- /dev/null +++ b/bin/bat_server.c @@ -0,0 +1,528 @@ +/* Copyright (c) 2017 - 2026 LiteSpeed Technologies Inc. See LICENSE. */ +/* + * bat_server.c -- Simple BAT (HTTP Datagram Bat) server for HTTP/3. + * + * BAT (Bidirectional Attestation Test) is a simple echo protocol for testing + * HTTP Datagram implementations (RFC 9297). The server accepts Extended CONNECT + * requests with the :protocol header, then echoes back any HTTP Datagrams it + * receives. + * + * This example demonstrates: + * - Validating Extended CONNECT request headers (server side) + * - Enabling HTTP Datagram support via lsquic_stream_set_http_dg_capsules() + * - Receiving datagrams via on_http_dg_read() callback + * - Queuing and sending datagrams via on_http_dg_write() callback + * - Handling both QUIC DATAGRAM and Capsule Protocol transparently + * + * See docs/draft-tikhonov-httpbis-bat-00.txt for BAT protocol specification. + */ + +#include +#include +#include +#include +#include +#include +#include +#include + +#ifndef WIN32 +#include +#else +#include "vc_compat.h" +#include "getopt.h" +#endif + +#include + +#include "lsquic.h" +#include "../src/liblsquic/lsquic_hash.h" +#include "test_common.h" +#include "test_cert.h" +#include "prog.h" + +#define TOOL_LOG_PREFIX "bat_server" +#include "tool_log.h" +#include "lsxpack_header.h" + +#define BAT_PROTOCOL "bat-00" + +struct hset_elem +{ + STAILQ_ENTRY(hset_elem) next; + size_t nalloc; + struct lsxpack_header xhdr; +}; + +struct bat_pending +{ + STAILQ_ENTRY(bat_pending) next; + unsigned char *buf; + size_t sz; +}; + +STAILQ_HEAD(bat_pending_head, bat_pending); + +STAILQ_HEAD(hset, hset_elem); + +struct lsquic_stream_ctx +{ + lsquic_stream_t *stream; + int headers_read; + int headers_ok; + int response_sent; + struct bat_pending_head pending_dg; +}; + +static void * +hset_create (void *UNUSED_hsi_ctx, lsquic_stream_t *UNUSED_stream, + int UNUSED_is_push_promise) +{ + struct hset *hset; + + if ((hset = malloc(sizeof(*hset)))) + { + STAILQ_INIT(hset); + return hset; + } + else + return NULL; +} + +static struct lsxpack_header * +hset_prepare_decode (void *hset_p, struct lsxpack_header *xhdr, + size_t req_space) +{ + struct hset *const hset = hset_p; + struct hset_elem *el; + char *buf; + + if (0 == req_space) + req_space = 0x100; + + if (req_space > LSXPACK_MAX_STRLEN) + { + LSQ_WARN("requested space for header is too large: %zd bytes", + req_space); + return NULL; + } + + if (!xhdr) + { + buf = malloc(req_space); + if (!buf) + { + LSQ_WARN("cannot allocate buf of %zd bytes", req_space); + return NULL; + } + el = malloc(sizeof(*el)); + if (!el) + { + LSQ_WARN("cannot allocate hset_elem"); + free(buf); + return NULL; + } + STAILQ_INSERT_TAIL(hset, el, next); + lsxpack_header_prepare_decode(&el->xhdr, buf, 0, req_space); + el->nalloc = req_space; + } + else + { + el = (struct hset_elem *) ((char *) xhdr + - offsetof(struct hset_elem, xhdr)); + if (req_space <= el->nalloc) + { + LSQ_ERROR("requested space is smaller than already allocated"); + return NULL; + } + if (req_space < el->nalloc * 2) + req_space = el->nalloc * 2; + buf = realloc(el->xhdr.buf, req_space); + if (!buf) + { + LSQ_WARN("cannot reallocate hset buf"); + return NULL; + } + el->xhdr.buf = buf; + el->xhdr.val_len = req_space; + el->nalloc = req_space; + } + + return &el->xhdr; +} + +static int +hset_add_header (void *UNUSED_hset_p, struct lsxpack_header *UNUSED_xhdr) +{ + return 0; +} + +static void +hset_destroy (void *hset_p) +{ + struct hset *hset = hset_p; + struct hset_elem *el, *next; + + for (el = STAILQ_FIRST(hset); el; el = next) + { + next = STAILQ_NEXT(el, next); + free(el->xhdr.buf); + free(el); + } + free(hset); +} + +static const struct lsquic_hset_if header_bypass_api = +{ + .hsi_create_header_set = hset_create, + .hsi_prepare_decode = hset_prepare_decode, + .hsi_process_header = hset_add_header, + .hsi_discard_header_set = hset_destroy, +}; + +static int +parse_request_headers (struct hset *hset, int *ok, int *unsupported) +{ + const struct hset_elem *el; + const char *name; + const char *value; + int have_method = 0; + int have_protocol = 0; + int have_capsule = 0; + int method_ok = 0; + int protocol_ok = 0; + int capsule_ok = 0; + + *ok = 0; + *unsupported = 0; + + /* BAT protocol validation per draft-tikhonov-httpbis-bat-00: + * - :method must be CONNECT + * - :protocol must be "bat-00" (or other version) + * - capsule-protocol must be "?1" (RFC 9297) + */ + STAILQ_FOREACH(el, hset, next) + { + name = lsxpack_header_get_name(&el->xhdr); + value = lsxpack_header_get_value(&el->xhdr); + if (el->xhdr.name_len == sizeof(":method") - 1 + && 0 == memcmp(name, ":method", sizeof(":method") - 1)) + { + have_method = 1; + if (el->xhdr.val_len == sizeof("CONNECT") - 1 + && 0 == memcmp(value, "CONNECT", + sizeof("CONNECT") - 1)) + method_ok = 1; + else + *unsupported = 1; + } + else if (el->xhdr.name_len == sizeof(":protocol") - 1 + && 0 == memcmp(name, ":protocol", sizeof(":protocol") - 1)) + { + have_protocol = 1; + if (el->xhdr.val_len == sizeof(BAT_PROTOCOL) - 1 + && 0 == memcmp(value, BAT_PROTOCOL, + sizeof(BAT_PROTOCOL) - 1)) + protocol_ok = 1; + else + *unsupported = 1; + } + else if (el->xhdr.name_len == sizeof("capsule-protocol") - 1 + && 0 == memcmp(name, "capsule-protocol", + sizeof("capsule-protocol") - 1)) + { + have_capsule = 1; + /* "?1" is structured field boolean true per RFC 9297 */ + if (el->xhdr.val_len == sizeof("?1") - 1 + && 0 == memcmp(value, "?1", sizeof("?1") - 1)) + capsule_ok = 1; + else + *unsupported = 1; + } + } + + if (have_method && have_protocol && have_capsule) + *ok = method_ok && protocol_ok && capsule_ok; + else + *ok = 0; + + return 0; +} + +static int +send_response (lsquic_stream_t *stream, const char *status, int fin) +{ + struct header_buf hbuf; + struct lsxpack_header headers_arr[1]; + + hbuf.off = 0; + header_set_ptr(&headers_arr[0], &hbuf, ":status", 7, + status, strlen(status)); + lsquic_http_headers_t headers = { + .count = 1, + .headers = headers_arr, + }; + if (0 != lsquic_stream_send_headers(stream, &headers, fin)) + { + LSQ_ERROR("cannot send headers: %s", strerror(errno)); + return -1; + } + return 0; +} + +static lsquic_conn_ctx_t * +bat_server_on_new_conn (void *UNUSED_stream_if_ctx, lsquic_conn_t *conn) +{ + LSQ_NOTICE("New BAT connection established"); + return NULL; +} + +static void +bat_server_on_conn_closed (lsquic_conn_t *conn) +{ + LSQ_NOTICE("BAT connection closed"); + lsquic_conn_set_ctx(conn, NULL); +} + +static lsquic_stream_ctx_t * +bat_server_on_new_stream (void *UNUSED_stream_if_ctx, lsquic_stream_t *stream) +{ + struct lsquic_stream_ctx *st; + st = calloc(1, sizeof(*st)); + if (!st) + { + LSQ_ERROR("cannot allocate stream context"); + lsquic_stream_close(stream); + return NULL; + } + st->stream = stream; + STAILQ_INIT(&st->pending_dg); + lsquic_stream_wantread(stream, 1); + return st; +} + +static void +bat_server_on_read (lsquic_stream_t *stream, lsquic_stream_ctx_t *st_h) +{ + struct hset *hset; + int ok = 0; + int unsupported = 0; + + if (st_h->headers_read) + return; + + st_h->headers_read = 1; + + /* Retrieve and validate Extended CONNECT request headers */ + hset = lsquic_stream_get_hset(stream); + if (!hset) + { + LSQ_ERROR("could not get header set from stream"); + lsquic_stream_close(stream); + return; + } + + parse_request_headers(hset, &ok, &unsupported); + hset_destroy(hset); + + if (!ok) + { + /* Send 400 Bad Request or 501 Not Implemented */ + send_response(stream, unsupported ? "501" : "400", 1); + lsquic_stream_close(stream); + return; + } + st_h->headers_ok = 1; + LSQ_NOTICE("accepted BAT CONNECT request"); + + /* Enable HTTP Datagram capsule processing for capsule-carried datagrams. */ + if (0 != lsquic_stream_set_http_dg_capsules(stream, 1)) + LSQ_WARN("cannot enable HTTP Datagram capsules: %s", strerror(errno)); + + lsquic_stream_wantread(stream, 1); + /* Trigger response write. */ + lsquic_stream_wantwrite(stream, 1); +} + +static void +bat_server_on_write (lsquic_stream_t *stream, lsquic_stream_ctx_t *st_h) +{ + if (!st_h->headers_ok) + return; + + if (!st_h->response_sent && 0 != send_response(stream, "200", 0)) + { + lsquic_stream_close(stream); + return; + } + + if (!st_h->response_sent) + { + st_h->response_sent = 1; + if (0 != lsquic_stream_flush(stream)) + LSQ_ERROR("cannot flush BAT response: %s", strerror(errno)); + } + lsquic_stream_wantwrite(stream, 0); +} + +static void +bat_server_on_close (lsquic_stream_t *UNUSED_stream, + lsquic_stream_ctx_t *st_h) +{ + struct bat_pending *pending; + + while ((pending = STAILQ_FIRST(&st_h->pending_dg))) + { + STAILQ_REMOVE_HEAD(&st_h->pending_dg, next); + free(pending->buf); + free(pending); + } + free(st_h); +} + +static int +bat_server_on_http_dg_write (lsquic_stream_t *stream, lsquic_stream_ctx_t *st_h, + size_t UNUSED_max_quic_payload, + lsquic_http_dg_consume_f consume) +{ + struct bat_pending *pending; + + /* Get next queued datagram to echo back */ + pending = STAILQ_FIRST(&st_h->pending_dg); + if (!pending) + return -1; + + /* Send using DEFAULT mode - library will automatically choose + * QUIC DATAGRAM or Capsule based on payload size and availability */ + if (0 != consume(stream, pending->buf, pending->sz, + LSQUIC_HTTP_DG_SEND_DEFAULT)) + return -1; + + LSQ_NOTICE("sending BAT datagram echo of %zu bytes", pending->sz); + + /* Remove from queue */ + STAILQ_REMOVE_HEAD(&st_h->pending_dg, next); + free(pending->buf); + free(pending); + + /* Disable write callback if queue is empty */ + if (STAILQ_EMPTY(&st_h->pending_dg)) + lsquic_stream_want_http_dg_write(st_h->stream, 0); + return 0; +} + +static void +bat_server_on_http_dg_read (lsquic_stream_t *stream, lsquic_stream_ctx_t *st_h, + const void *buf, size_t bufsz) +{ + struct bat_pending *pending; + + if (!st_h->headers_ok) + return; + + LSQ_NOTICE("received BAT datagram payload of %zu bytes", bufsz); + + /* BAT protocol: echo back any received datagram. + * Queue it for sending via on_http_dg_write callback */ + pending = malloc(sizeof(*pending)); + if (!pending) + { + LSQ_WARN("cannot allocate pending entry"); + return; + } + + /* Must copy payload - buffer is only valid during callback */ + pending->buf = malloc(bufsz); + if (!pending->buf) + { + free(pending); + LSQ_ERROR("cannot allocate datagram buffer"); + return; + } + memcpy(pending->buf, buf, bufsz); + pending->sz = bufsz; + + /* Add to queue and request write callback */ + STAILQ_INSERT_TAIL(&st_h->pending_dg, pending, next); + lsquic_stream_want_http_dg_write(st_h->stream, 1); +} + +const struct lsquic_stream_if bat_server_stream_if = { + .on_new_conn = bat_server_on_new_conn, + .on_conn_closed = bat_server_on_conn_closed, + .on_new_stream = bat_server_on_new_stream, + .on_read = bat_server_on_read, + .on_write = bat_server_on_write, + .on_close = bat_server_on_close, + .on_http_dg_write = bat_server_on_http_dg_write, + .on_http_dg_read = bat_server_on_http_dg_read, +}; + +static void +usage (const char *prog) +{ + const char *const slash = strrchr(prog, '/'); + if (slash) + prog = slash + 1; + printf( +"Usage: %s [opts]\n" +"\n" +"Options:\n" + , prog); +} + +int +main (int argc, char **argv) +{ + int opt, s; + struct prog prog; + struct sport_head sports; + const char *const *alpn; + + TAILQ_INIT(&sports); + prog_init(&prog, LSENG_SERVER|LSENG_HTTP, &sports, + &bat_server_stream_if, NULL); + /* Enable HTTP Datagram support (RFC 9297) */ + prog.prog_settings.es_http_datagrams = 1; + /* Use header bypass API for efficient header processing */ + prog.prog_api.ea_hsi_if = &header_bypass_api; + prog.prog_api.ea_hsi_ctx = NULL; + + while (-1 != (opt = getopt(argc, argv, PROG_OPTS "h"))) + { + switch (opt) { + case 'h': + usage(argv[0]); + prog_print_common_options(&prog, stdout); + exit(0); + default: + if (0 != prog_set_opt(&prog, opt, optarg)) + exit(1); + } + } + + alpn = lsquic_get_h3_alpns(prog.prog_settings.es_versions); + while (*alpn) + { + if (0 == add_alpn(*alpn)) + ++alpn; + else + { + LSQ_ERROR("cannot add ALPN %s", *alpn); + exit(EXIT_FAILURE); + } + } + + if (0 != prog_prep(&prog)) + { + LSQ_ERROR("could not prep"); + exit(EXIT_FAILURE); + } + + LSQ_DEBUG("entering event loop"); + + s = prog_run(&prog); + prog_cleanup(&prog); + + exit(0 == s ? EXIT_SUCCESS : EXIT_FAILURE); +} diff --git a/bin/baton_client.c b/bin/baton_client.c new file mode 100644 index 000000000..c48d1d413 --- /dev/null +++ b/bin/baton_client.c @@ -0,0 +1,225 @@ +/* Copyright (c) 2017 - 2026 LiteSpeed Technologies Inc. See LICENSE. */ +/* + * baton_client.c -- Devious Baton WebTransport client + */ + +#include +#include +#include +#include +#include +#include +#include + +#ifndef WIN32 +#include +#else +#include "vc_compat.h" +#include "getopt.h" +#endif + +#include + +#include "lsquic.h" +#include "lsquic_wt.h" +#include "devious_baton.h" +#include "test_common.h" +#include "prog.h" + +#define TOOL_LOG_PREFIX "baton_client" +#include "tool_log.h" + + +static int +parse_close_spec (const char *spec, unsigned *after_n_batons, + const char **reason) +{ + char *end; + unsigned long val; + const char *colon; + + if (!spec || !*spec) + return -1; + + colon = strchr(spec, ':'); + if (!colon) + { + *after_n_batons = 1; + *reason = spec; + return 0; + } + + if (colon == spec) + { + *after_n_batons = 1; + *reason = spec + 1; + return 0; + } + + errno = 0; + val = strtoul(spec, &end, 10); + if (0 == errno && end == colon && val > 0 && val <= UINT_MAX) + { + *after_n_batons = (unsigned) val; + *reason = colon + 1; + return 0; + } + + *after_n_batons = 1; + *reason = spec; + return 0; +} + + +static void +usage (const char *prog) +{ + const char *const slash = strrchr(prog, '/'); + if (slash) + prog = slash + 1; + LSQ_NOTICE( +"Usage: %s [opts]\n" +"\n" +"Options:\n" +" -b value Initial baton value (1-255)\n" +" -c count Number of parallel batons\n" +" -U count Burst WT datagrams to queue via write callback\n" +" -M policy WT datagram queue-full policy: fail|oldest|newest\n" +" -u count Max queued WT datagrams per session (0 = library default)\n" +" -v bytes Max queued WT datagram bytes per session (0 = library default)\n" +" -P path CONNECT path base (default: " DEVIOUS_BATON_PATH ")\n" +" -p bytes Padding length for baton messages\n" +" -E spec Close WT session after Nth received baton: N:reason or reason\n" + , prog); +} + + +int +main (int argc, char **argv) +{ + int opt, s; + struct sport_head sports; + struct prog prog; + struct devious_baton_app app; + const char *const *alpns; + + TAILQ_INIT(&sports); + prog_init(&prog, LSENG_HTTP, &sports, + devious_baton_stream_if(), &app); + prog.prog_settings.es_http_datagrams = 1; + prog.prog_settings.es_webtransport = 1; + prog.prog_settings.es_reset_stream_at = 1; + prog.prog_settings.es_max_webtransport_sessions = 1; + prog.prog_settings.es_init_max_streams_uni = 64; + prog.prog_settings.es_init_max_stream_data_bidi_remote = 100000; + prog.prog_api.ea_hsi_if = devious_baton_hset_if(); + prog.prog_api.ea_hsi_ctx = NULL; + + alpns = lsquic_get_h3_alpns(prog.prog_settings.es_versions); + prog.prog_api.ea_alpn = alpns[0]; + + devious_baton_app_init(&app, &prog, 0); + + while (-1 != (opt = getopt(argc, argv, PROG_OPTS "b:c:U:M:u:v:P:p:E:h"))) + { + switch (opt) { + case 'b': + app.baton = (unsigned) atoi(optarg); + break; + case 'c': + app.count = (unsigned) atoi(optarg); + break; + case 'U': + app.dg_burst_count = (unsigned) atoi(optarg); + break; + case 'M': + if (0 == strcmp(optarg, "fail")) + app.dg_drop_policy = LSQWT_DG_FAIL_EAGAIN; + else if (0 == strcmp(optarg, "oldest")) + app.dg_drop_policy = LSQWT_DG_DROP_OLDEST; + else if (0 == strcmp(optarg, "newest")) + app.dg_drop_policy = LSQWT_DG_DROP_NEWEST; + else + { + LSQ_ERROR("unknown datagram policy `%s'", optarg); + exit(1); + } + break; + case 'u': + { + char *end; + unsigned long val; + + errno = 0; + val = strtoul(optarg, &end, 10); + if (errno || *end || val > UINT_MAX) + { + LSQ_ERROR("invalid queue count `%s'", optarg); + exit(1); + } + app.dgq_max_count = (unsigned) val; + break; + } + case 'v': + { + char *end; + unsigned long long val; + + errno = 0; + val = strtoull(optarg, &end, 10); + if (errno || *end || val > SIZE_MAX) + { + LSQ_ERROR("invalid queue bytes `%s'", optarg); + exit(1); + } + app.dgq_max_bytes = (size_t) val; + break; + } + case 'P': + app.path_base = optarg; + break; + case 'p': + app.padding_len = (unsigned) atoi(optarg); + break; + case 'E': + if (0 != parse_close_spec(optarg, &app.close_after_n_batons, + &app.close_reason)) + { + LSQ_ERROR("invalid close spec `%s'", optarg); + exit(1); + } + break; + case 'h': + usage(argv[0]); + prog_print_common_options(&prog, stdout); + exit(0); + default: + if (0 != prog_set_opt(&prog, opt, optarg)) + exit(1); + } + } + + if (0 != devious_baton_build_path(&app)) + { + LSQ_ERROR("cannot build path"); + exit(EXIT_FAILURE); + } + + if (0 != prog_prep(&prog)) + { + LSQ_ERROR("could not prep"); + exit(EXIT_FAILURE); + } + if (0 != prog_connect(&prog, NULL, 0)) + { + LSQ_ERROR("could not connect"); + exit(EXIT_FAILURE); + } + + LSQ_DEBUG("entering event loop"); + + s = prog_run(&prog); + prog_cleanup(&prog); + + exit(0 == s ? EXIT_SUCCESS : EXIT_FAILURE); +} diff --git a/bin/devious_baton.c b/bin/devious_baton.c new file mode 100644 index 000000000..8e647c191 --- /dev/null +++ b/bin/devious_baton.c @@ -0,0 +1,2246 @@ +/* Copyright (c) 2017 - 2026 LiteSpeed Technologies Inc. See LICENSE. */ +/* + * devious_baton.c -- Devious Baton WebTransport example logic + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "lsquic.h" +#include "lsquic_wt.h" +#include "devious_baton.h" +#include "test_common.h" +#include "prog.h" +#include "lsxpack_header.h" + +#define TOOL_LOG_PREFIX "devious_baton" +#include "tool_log.h" +#include "../src/liblsquic/lsquic_varint.h" + + +enum devious_baton_stream_kind +{ + DB_STREAM_CONTROL = 1, + DB_STREAM_BATON = 2, +}; + + +enum devious_baton_stream_flags +{ + DBSF_HAVE_BATON = 1 << 0, + DBSF_MESSAGE_DONE = 1 << 1, + DBSF_RESET_SENT = 1 << 2, + DBSF_CLOSED = 1 << 3, + DBSF_PEER_RESET_SEEN = 1 << 4, + DBSF_PEER_STOP_SEND_SEEN = 1 << 5, + DBSF_FINISHED = 1 << 6, + DBSF_PEER_FIN = 1 << 7, +}; + +struct devious_baton_session +{ + TAILQ_ENTRY(devious_baton_session) next_session; + struct lsquic_wt_session *wt_sess; + struct devious_baton_app cfg; + unsigned active_batons; + unsigned active_streams; + unsigned dg_burst_pending; + unsigned dg_burst_sent; + unsigned dg_burst_failed; + unsigned received_batons; + signed char closed; +}; + + +struct devious_baton_conn +{ + struct devious_baton_app *app; + struct prog *prog; + struct lsquic_stream *control_stream; + const char *connect_protocol; + int headers_sent; + int response_seen; + int response_ok; + signed char retried_with_legacy_protocol; +}; + + +struct devious_baton_stream +{ + enum devious_baton_stream_kind dbs_kind; + struct devious_baton_conn *dbs_conn; + struct devious_baton_session *dbs_session; + struct lsquic_stream *dbs_stream; + unsigned char *dbs_buf; + size_t dbs_buf_len; + size_t dbs_buf_cap; + unsigned char dbs_baton_to_send; + enum devious_baton_stream_flags dbs_flags; + enum lsquic_wt_stream_dir dbs_dir; + enum lsquic_wt_stream_initiator dbs_initiator; +}; + + +TAILQ_HEAD(db_session_head, devious_baton_session); + +static struct db_session_head s_sessions = + TAILQ_HEAD_INITIALIZER(s_sessions); + +static const char * +db_role (const struct devious_baton_session *sess) +{ + return sess->cfg.is_server ? "server" : "client"; +} + + +static const char * +db_dir (enum lsquic_wt_stream_dir dir) +{ + return dir == LSQWT_UNI ? "uni" : "bidi"; +} + + +static const char * +db_initiator (enum lsquic_wt_stream_initiator initiator) +{ + return initiator == LSQWT_SERVER ? "server" : "client"; +} + + +static void +db_log_receive (const struct devious_baton_stream *st, unsigned char baton) +{ + if (st->dbs_stream) + LSQ_INFO("%s received baton %u on %s stream %"PRIu64 + " (%s-initiated)", db_role(st->dbs_session), baton, + db_dir(st->dbs_dir), (uint64_t) lsquic_stream_id(st->dbs_stream), + db_initiator(st->dbs_initiator)); + else + LSQ_INFO("%s received baton %u in datagram", db_role(st->dbs_session), + baton); +} + + + +struct hset_elem +{ + STAILQ_ENTRY(hset_elem) next; + size_t nalloc; + struct lsxpack_header xhdr; +}; + + +STAILQ_HEAD(hset, hset_elem); + + +static struct devious_baton_session * +find_session (struct lsquic_wt_session *sess) +{ + struct devious_baton_session *it; + + TAILQ_FOREACH(it, &s_sessions, next_session) + if (it->wt_sess == sess) + return it; + + return NULL; +} + + +static void +remove_session (struct devious_baton_session *sess) +{ + if (!sess) + return; + + if (sess->wt_sess) + TAILQ_REMOVE(&s_sessions, sess, next_session); + + free(sess); +} + + +static void +maybe_remove_closed_session (struct devious_baton_session *sess) +{ + if (sess && sess->closed && 0 == sess->active_streams) + remove_session(sess); +} + + +static void +maybe_close_session (struct devious_baton_session *sess) +{ + if (!sess) + return; + + if (sess->active_batons == 0) + { + struct lsquic_conn *conn; + + LSQ_INFO("%s has no active batons left, closing session", db_role(sess)); + conn = lsquic_wt_session_conn(sess->wt_sess); + lsquic_wt_close(sess->wt_sess, 0, NULL, 0); + if (conn) + lsquic_conn_close(conn); + } +} + + +static void +db_finish_stream (struct devious_baton_stream *st, int dec_active_batons, + const char *reason) +{ + struct devious_baton_session *sess; + + if (!st || st->dbs_kind != DB_STREAM_BATON + || (st->dbs_flags & DBSF_FINISHED)) + return; + + st->dbs_flags |= DBSF_FINISHED; + sess = st->dbs_session; + if (!sess) + return; + + if (dec_active_batons && sess->active_batons) + { + --sess->active_batons; + LSQ_INFO("%s %s on %s stream %"PRIu64"; active batons now %u", + db_role(sess), reason, db_dir(st->dbs_dir), + (uint64_t) lsquic_stream_id(st->dbs_stream), + sess->active_batons); + maybe_close_session(sess); + } +} + + +static void +db_close_baton_stream (struct devious_baton_stream *st) +{ + if (!st || st->dbs_kind != DB_STREAM_BATON || !st->dbs_stream + || (st->dbs_flags & DBSF_CLOSED)) + return; + + st->dbs_flags |= DBSF_CLOSED; + lsquic_stream_close(st->dbs_stream); +} + + +static int +db_maybe_reset_stream (struct devious_baton_stream *st, uint64_t error_code, + const char *error_name, const char *reason) +{ + if (!st || st->dbs_kind != DB_STREAM_BATON || !st->dbs_stream) + return -1; + + if (st->dbs_flags & DBSF_CLOSED) + { + LSQ_INFO("%s not sending RESET_STREAM(%s) on %s stream %"PRIu64 + ": already closed", db_role(st->dbs_session), error_name, + db_dir(st->dbs_dir), + (uint64_t) lsquic_stream_id(st->dbs_stream)); + return 0; + } + + if (st->dbs_flags & DBSF_RESET_SENT) + { + LSQ_INFO("%s not sending duplicate RESET_STREAM(%s) on %s stream " + "%"PRIu64, db_role(st->dbs_session), error_name, + db_dir(st->dbs_dir), + (uint64_t) lsquic_stream_id(st->dbs_stream)); + return 0; + } + + LSQ_INFO("%s sending RESET_STREAM(%s) on %s stream %"PRIu64" (%s)", + db_role(st->dbs_session), error_name, db_dir(st->dbs_dir), + (uint64_t) lsquic_stream_id(st->dbs_stream), reason); + if (0 != lsquic_wt_stream_reset(st->dbs_stream, error_code)) + { + LSQ_WARN("%s cannot send RESET_STREAM(%s) on %s stream %"PRIu64 + ": %s", db_role(st->dbs_session), error_name, + db_dir(st->dbs_dir), + (uint64_t) lsquic_stream_id(st->dbs_stream), strerror(errno)); + return -1; + } + + st->dbs_flags |= DBSF_RESET_SENT; + st->dbs_flags &= ~DBSF_HAVE_BATON; + lsquic_stream_wantwrite(st->dbs_stream, 0); + return 0; +} + + +static int +parse_uint (const char *value, unsigned *out) +{ + char *end; + unsigned long v; + + if (!value || !*value) + return -1; + + errno = 0; + v = strtoul(value, &end, 10); + if (errno || *end != '\0' || v > UINT_MAX) + return -1; + + *out = (unsigned) v; + return 0; +} + + +static int +parse_query (const char *query, struct devious_baton_app *cfg, + char *err_buf, size_t err_sz) +{ + char *dup, *tok, *name, *value, *eq; + unsigned val; + + if (!query || !*query) + return 0; + + dup = strdup(query); + if (!dup) + { + snprintf(err_buf, err_sz, "cannot parse query"); + return -1; + } + + for (tok = strtok(dup, "&"); tok; tok = strtok(NULL, "&")) + { + eq = strchr(tok, '='); + if (!eq) + continue; + *eq = '\0'; + name = tok; + value = eq + 1; + + if (0 == strcmp(name, "version")) + { + if (0 != parse_uint(value, &val)) + { + snprintf(err_buf, err_sz, "invalid version"); + free(dup); + return -1; + } + cfg->version = val; + } + else if (0 == strcmp(name, "baton")) + { + if (0 != parse_uint(value, &val) || val == 0 || val > 255) + { + snprintf(err_buf, err_sz, "invalid baton"); + free(dup); + return -1; + } + cfg->baton = val; + } + else if (0 == strcmp(name, "count")) + { + if (0 != parse_uint(value, &val) || val == 0) + { + snprintf(err_buf, err_sz, "invalid count"); + free(dup); + return -1; + } + cfg->count = val; + } + } + + free(dup); + return 0; +} + + +static int +dup_header_value (const char *value, size_t value_len, char **out) +{ + char *buf; + + buf = malloc(value_len + 1); + if (!buf) + return -1; + + memcpy(buf, value, value_len); + buf[value_len] = '\0'; + *out = buf; + return 0; +} + + +static int +is_supported_connect_protocol (const char *value, size_t value_len) +{ + return (value_len == sizeof(WEBTRANSPORT_H3_CONNECT_PROTOCOL) - 1 + && 0 == memcmp(value, WEBTRANSPORT_H3_CONNECT_PROTOCOL, + sizeof(WEBTRANSPORT_H3_CONNECT_PROTOCOL) - 1)) + || (value_len == sizeof(WEBTRANSPORT_CONNECT_PROTOCOL) - 1 + && 0 == memcmp(value, WEBTRANSPORT_CONNECT_PROTOCOL, + sizeof(WEBTRANSPORT_CONNECT_PROTOCOL) - 1)); +} + + +static int +dup_first_sf_string (const char *value, size_t value_len, char **out) +{ + char *buf; + size_t in_off, out_off; + + for (in_off = 0; in_off < value_len; ++in_off) + if (value[in_off] == '"') + break; + if (in_off >= value_len) + return -1; + + ++in_off; + buf = malloc(value_len - in_off + 1); + if (!buf) + return -1; + + out_off = 0; + while (in_off < value_len) + { + if (value[in_off] == '"') + { + buf[out_off] = '\0'; + *out = buf; + return 0; + } + + if (value[in_off] == '\\') + { + ++in_off; + if (in_off >= value_len) + break; + } + + buf[out_off++] = value[in_off++]; + } + + free(buf); + return -1; +} + + +static void +free_connect_info (struct lsquic_wt_connect_info *info) +{ + if (!info) + return; + + free((char *) info->wtci_authority); + free((char *) info->wtci_path); + free((char *) info->wtci_origin); + free((char *) info->wtci_protocol); + + info->wtci_authority = NULL; + info->wtci_path = NULL; + info->wtci_origin = NULL; + info->wtci_protocol = NULL; +} + + +static int +parse_path (const char *path, struct devious_baton_app *cfg, + char *err_buf, size_t err_sz) +{ + size_t path_len = 0; + size_t path_base_len; + if (!path) + { + snprintf(err_buf, err_sz, "invalid path"); + return -1; + } + + path_len = strlen(path); + path_base_len = sizeof(DEVIOUS_BATON_PATH) - 1; + if (path_len < path_base_len + || 0 != memcmp(path, DEVIOUS_BATON_PATH, path_base_len)) + { + snprintf(err_buf, err_sz, "invalid path"); + return -1; + } + + if (path_len > path_base_len && path[path_base_len] != '?') + { + snprintf(err_buf, err_sz, "invalid path"); + return -1; + } + + cfg->version = 0; + cfg->count = 1; + cfg->baton = 0; + + if (path_len > path_base_len && path[path_base_len] == '?') + { + if (0 != parse_query(path + path_base_len + 1, cfg, + err_buf, err_sz)) + return -1; + } + + if (cfg->version != 0) + { + snprintf(err_buf, err_sz, "unsupported version"); + return -1; + } + + if (cfg->count > cfg->max_count) + { + snprintf(err_buf, err_sz, "count too large"); + return -1; + } + + if (cfg->baton == 0) + cfg->baton = 1 + (rand() % 255); + + return 0; +} + + +static int +parse_request (struct hset *hset, struct devious_baton_app *cfg, + struct lsquic_wt_connect_info *info, + char *err_buf, size_t err_sz) +{ + const struct hset_elem *el; + const char *name; + const char *value; + char *path = NULL; + char *scheme = NULL; + char *authority = NULL; + char *origin = NULL; + char *protocol = NULL; + int have_method = 0; + int have_connect_protocol = 0; + int have_scheme = 0; + int method_ok = 0; + int connect_protocol_ok = 0; + int scheme_ok = 0; + int ok = 0; + int rv = -1; + + STAILQ_FOREACH(el, hset, next) + { + name = lsxpack_header_get_name(&el->xhdr); + value = lsxpack_header_get_value(&el->xhdr); + + if (el->xhdr.name_len == sizeof(":method") - 1 + && 0 == memcmp(name, ":method", sizeof(":method") - 1)) + { + have_method = 1; + method_ok = (el->xhdr.val_len == sizeof("CONNECT") - 1 + && 0 == memcmp(value, "CONNECT", sizeof("CONNECT") - 1)); + } + else if (el->xhdr.name_len == sizeof(":protocol") - 1 + && 0 == memcmp(name, ":protocol", sizeof(":protocol") - 1)) + { + have_connect_protocol = 1; + connect_protocol_ok = + is_supported_connect_protocol(value, el->xhdr.val_len); + } + else if (el->xhdr.name_len == sizeof(":scheme") - 1 + && 0 == memcmp(name, ":scheme", sizeof(":scheme") - 1)) + { + char *new_scheme; + + have_scheme = 1; + scheme_ok = el->xhdr.val_len == sizeof("https") - 1 + && 0 == memcmp(value, "https", sizeof("https") - 1); + if (0 != dup_header_value(value, el->xhdr.val_len, &new_scheme)) + { + snprintf(err_buf, err_sz, "cannot copy scheme"); + goto end; + } + free(scheme); + scheme = new_scheme; + } + else if (el->xhdr.name_len == sizeof("wt-available-protocols") - 1 + && 0 == memcmp(name, "wt-available-protocols", + sizeof("wt-available-protocols") - 1)) + { + free(protocol); + protocol = NULL; + if (0 == dup_first_sf_string(value, el->xhdr.val_len, &protocol)) + LSQ_DEBUG("client offered WT protocol \"%s\"", protocol); + else + { + LSQ_DEBUG("ignore malformed WT-Available-Protocols"); + } + } + else if (el->xhdr.name_len == sizeof(":path") - 1 + && 0 == memcmp(name, ":path", sizeof(":path") - 1)) + { + char *new_path; + + if (0 != dup_header_value(value, el->xhdr.val_len, &new_path)) + { + snprintf(err_buf, err_sz, "cannot copy path"); + goto end; + } + free(path); + path = new_path; + } + else if (el->xhdr.name_len == sizeof(":authority") - 1 + && 0 == memcmp(name, ":authority", + sizeof(":authority") - 1)) + { + char *new_authority; + + if (0 != dup_header_value(value, el->xhdr.val_len, + &new_authority)) + { + snprintf(err_buf, err_sz, "cannot copy authority"); + goto end; + } + free(authority); + authority = new_authority; + } + else if (el->xhdr.name_len == sizeof("origin") - 1 + && 0 == memcmp(name, "origin", sizeof("origin") - 1)) + { + char *new_origin; + + if (0 != dup_header_value(value, el->xhdr.val_len, &new_origin)) + { + snprintf(err_buf, err_sz, "cannot copy origin"); + goto end; + } + free(origin); + origin = new_origin; + } + } + + if (have_method && have_connect_protocol && have_scheme) + ok = method_ok && connect_protocol_ok && scheme_ok + && authority != NULL; + + if (!ok) + { + snprintf(err_buf, err_sz, "invalid CONNECT request"); + goto end; + } + + if (0 != parse_path(path, cfg, err_buf, err_sz)) + goto end; + + if (info) + { + memset(info, 0, sizeof(*info)); + info->wtci_authority = authority; + info->wtci_path = path; + info->wtci_origin = origin; + info->wtci_protocol = protocol; + info->wtci_draft = 0; + authority = NULL; + path = NULL; + origin = NULL; + protocol = NULL; + } + + rv = 0; + + end: + free(scheme); + free(authority); + free(path); + free(origin); + free(protocol); + return rv; +} + + +static int +build_path (struct devious_baton_app *cfg) +{ + int n; + + n = snprintf(cfg->path_buf, sizeof(cfg->path_buf), + "%s?version=%u&count=%u&baton=%u", + cfg->path_base ? cfg->path_base : DEVIOUS_BATON_PATH, + cfg->version, cfg->count, cfg->baton); + if (n < 0 || (size_t) n >= sizeof(cfg->path_buf)) + return -1; + + cfg->path = cfg->path_buf; + return 0; +} + + +static int +send_headers (struct devious_baton_conn *conn) +{ + struct header_buf hbuf; + struct lsxpack_header headers_arr[5]; + const char *connect_protocol; + const char *hostname; + unsigned h_idx; + + h_idx = 0; + connect_protocol = conn->connect_protocol + ? conn->connect_protocol + : WEBTRANSPORT_H3_CONNECT_PROTOCOL; + hostname = conn->prog->prog_hostname ? conn->prog->prog_hostname + : "localhost"; + +#define V(v) (v), strlen(v) + hbuf.off = 0; + header_set_ptr(&headers_arr[h_idx++], &hbuf, V(":method"), V("CONNECT")); + header_set_ptr(&headers_arr[h_idx++], &hbuf, V(":protocol"), + connect_protocol, + strlen(connect_protocol)); + header_set_ptr(&headers_arr[h_idx++], &hbuf, V(":scheme"), V("https")); + header_set_ptr(&headers_arr[h_idx++], &hbuf, V(":authority"), + hostname, strlen(hostname)); + header_set_ptr(&headers_arr[h_idx++], &hbuf, V(":path"), + conn->app->path, + strlen(conn->app->path)); +#undef V + + lsquic_http_headers_t headers = { + .count = h_idx, + .headers = headers_arr, + }; + + if (0 != lsquic_stream_send_headers(conn->control_stream, &headers, 0)) + { + LSQ_ERROR("cannot send CONNECT headers: %s", strerror(errno)); + return -1; + } + + return 0; +} + + +static int +parse_status (struct hset *hset, unsigned *status_code) +{ + const struct hset_elem *el; + const char *name; + const char *value; + unsigned code; + + if (status_code) + *status_code = 0; + + STAILQ_FOREACH(el, hset, next) + { + name = lsxpack_header_get_name(&el->xhdr); + if (el->xhdr.name_len == sizeof(":status") - 1 + && 0 == memcmp(name, ":status", sizeof(":status") - 1)) + { + value = lsxpack_header_get_value(&el->xhdr); + if (el->xhdr.val_len != 3 + || value[0] < '0' || value[0] > '9' + || value[1] < '0' || value[1] > '9' + || value[2] < '0' || value[2] > '9') + return 0; + code = (unsigned) (value[0] - '0') * 100 + + (unsigned) (value[1] - '0') * 10 + + (unsigned) (value[2] - '0'); + if (status_code) + *status_code = code; + return value[0] == '2'; + } + } + + return 0; +} + + +static void +log_hset_debug (const char *prefix, const struct hset *hset) +{ + const struct hset_elem *el; + const char *name; + const char *value; + + STAILQ_FOREACH(el, hset, next) + { + name = lsxpack_header_get_name(&el->xhdr); + value = lsxpack_header_get_value(&el->xhdr); + LSQ_DEBUG("%s header `%.*s': `%.*s'", prefix, + (int) el->xhdr.name_len, name, (int) el->xhdr.val_len, value); + } +} + + +static int +dup_wt_protocol (const struct hset *hset, char **out) +{ + const struct hset_elem *el; + const char *name; + const char *value; + + *out = NULL; + STAILQ_FOREACH(el, hset, next) + { + name = lsxpack_header_get_name(&el->xhdr); + if (el->xhdr.name_len == sizeof("wt-protocol") - 1 + && 0 == memcmp(name, "wt-protocol", sizeof("wt-protocol") - 1)) + { + value = lsxpack_header_get_value(&el->xhdr); + return dup_first_sf_string(value, el->xhdr.val_len, out); + } + } + + return 1; +} + + +static int +buf_append (struct devious_baton_stream *st, const unsigned char *buf, + size_t len) +{ + size_t need; + size_t new_cap; + unsigned char *new_buf; + + if (len > SIZE_MAX - st->dbs_buf_len) + { + errno = EOVERFLOW; + return -1; + } + + need = st->dbs_buf_len + len; + if (need <= st->dbs_buf_cap) + { + memcpy(st->dbs_buf + st->dbs_buf_len, buf, len); + st->dbs_buf_len += len; + return 0; + } + + new_cap = st->dbs_buf_cap ? st->dbs_buf_cap : 64; + while (new_cap < need) + { + if (new_cap > SIZE_MAX / 2) + { + new_cap = need; + break; + } + new_cap *= 2; + } + + new_buf = realloc(st->dbs_buf, new_cap); + if (!new_buf) + return -1; + + st->dbs_buf = new_buf; + st->dbs_buf_cap = new_cap; + memcpy(st->dbs_buf + st->dbs_buf_len, buf, len); + st->dbs_buf_len += len; + return 0; +} + + +static int +parse_baton_message (struct devious_baton_stream *st, unsigned char *baton, + size_t *consumed) +{ + uint64_t padding_len; + const unsigned char *p; + const unsigned char *end; + int r; + + p = st->dbs_buf; + end = st->dbs_buf + st->dbs_buf_len; + + r = lsquic_varint_read(p, end, &padding_len); + if (r < 0) + return 0; + + if (padding_len > SIZE_MAX - (size_t) r - 1) + return -1; + + if (st->dbs_buf_len < (size_t) r + (size_t) padding_len + 1) + return 0; + + *baton = st->dbs_buf[r + padding_len]; + *consumed = (size_t) r + (size_t) padding_len + 1; + return 1; +} + + +static int +write_baton_message (struct devious_baton_session *sess, unsigned char baton, + unsigned char **out, size_t *out_len) +{ + uint64_t padding_len; + size_t varint_len; + size_t need; + unsigned char *buf; + + padding_len = sess->cfg.padding_len; + varint_len = (size_t) vint_size(padding_len); + if (varint_len > SIZE_MAX - 1 + || (size_t) padding_len > SIZE_MAX - varint_len - 1) + { + errno = EOVERFLOW; + return -1; + } + need = varint_len + (size_t) padding_len + 1; + + buf = malloc(need); + if (!buf) + return -1; + + vint_write(buf, padding_len, vint_val2bits(padding_len), varint_len); + if (padding_len) + memset(buf + varint_len, 0, padding_len); + buf[varint_len + padding_len] = baton; + + *out = buf; + *out_len = need; + return 0; +} + + +static void +send_datagram_if_needed (struct devious_baton_session *sess, + unsigned char baton) +{ + unsigned char *buf; + size_t len; + int send_dg; + + if (sess->cfg.is_server) + send_dg = (baton % 7) == 0; + else + send_dg = (baton % 7) == 1; + + if (!send_dg) + return; + + if (0 != write_baton_message(sess, baton, &buf, &len)) + return; + + if (0 > lsquic_wt_send_datagram(sess->wt_sess, buf, len)) + LSQ_DEBUG("cannot send datagram: %s", strerror(errno)); + else + LSQ_INFO("%s sent datagram carrying baton %u (%zu bytes)", + db_role(sess), baton, len); + + free(buf); +} + + +static const char * +db_dg_policy (unsigned policy) +{ + switch ((enum lsquic_wt_dg_drop_policy) policy) + { + case LSQWT_DG_FAIL_EAGAIN: + return "fail"; + case LSQWT_DG_DROP_OLDEST: + return "drop-oldest"; + case LSQWT_DG_DROP_NEWEST: + return "drop-newest"; + default: + return "unknown"; + } +} + + +static void +db_send_burst_datagram (struct devious_baton_session *bsess) +{ + unsigned char baton; + unsigned char *buf; + size_t len; + ssize_t nw; + + while (bsess->dg_burst_pending > 0) + { + baton = (unsigned char) (bsess->cfg.baton + bsess->dg_burst_sent); + if (0 == baton) + baton = 1; + if (0 != write_baton_message(bsess, baton, &buf, &len)) + { + LSQ_WARN("%s cannot allocate burst datagram payload", + db_role(bsess)); + break; + } + nw = lsquic_wt_send_datagram(bsess->wt_sess, buf, len); + free(buf); + + if (nw < 0) + { + ++bsess->dg_burst_failed; + if (errno == EAGAIN + && bsess->cfg.dg_drop_policy == LSQWT_DG_FAIL_EAGAIN) + break; + } + else + ++bsess->dg_burst_sent; + + --bsess->dg_burst_pending; + } +} + + +static int +stream_is_readable_by_us (const struct devious_baton_session *sess, + const struct devious_baton_stream *st) +{ + enum lsquic_wt_stream_initiator self_init; + + if (st->dbs_dir == LSQWT_BIDI) + return 1; + + self_init = sess->cfg.is_server ? LSQWT_SERVER : LSQWT_CLIENT; + return st->dbs_initiator != self_init; +} + + +static int +queue_baton (struct devious_baton_session *sess, struct devious_baton_stream *st, + unsigned char baton) +{ + LSQ_INFO("%s queue baton %u on %s stream %"PRIu64" (%s-initiated)", + db_role(sess), baton, db_dir(st->dbs_dir), + (uint64_t) lsquic_stream_id(st->dbs_stream), + db_initiator(st->dbs_initiator)); + st->dbs_baton_to_send = baton; + st->dbs_flags |= DBSF_HAVE_BATON; + lsquic_stream_wantwrite(st->dbs_stream, 1); + if (stream_is_readable_by_us(sess, st)) + lsquic_stream_wantread(st->dbs_stream, 1); + return 0; +} + + +static int +open_stream_and_queue (struct devious_baton_session *sess, + enum lsquic_wt_stream_dir dir, unsigned char baton) +{ + struct lsquic_stream *stream; + struct devious_baton_stream *st; + + if (dir == LSQWT_UNI) + stream = lsquic_wt_open_uni(sess->wt_sess); + else + stream = lsquic_wt_open_bidi(sess->wt_sess); + + if (!stream) + { + LSQ_WARN("%s cannot open %s stream for baton %u", db_role(sess), + db_dir(dir), baton); + return -1; + } + + LSQ_INFO("%s opened %s stream %"PRIu64" for baton %u", db_role(sess), + db_dir(dir), (uint64_t) lsquic_stream_id(stream), baton); + + st = (struct devious_baton_stream *) lsquic_wt_stream_get_ctx(stream); + if (!st) + return -1; + + queue_baton(sess, st, baton); + return 0; +} + + +static int +handle_baton (struct devious_baton_stream *st, unsigned char baton) +{ + struct devious_baton_session *sess; + enum lsquic_wt_stream_dir out_dir; + enum lsquic_wt_stream_initiator self_init; + unsigned char next_baton; + + sess = st->dbs_session; + db_log_receive(st, baton); + + ++sess->received_batons; + if (sess->cfg.close_reason + && sess->received_batons == sess->cfg.close_after_n_batons) + { + LSQ_INFO("%s closing session after baton %u with reason `%s'", + db_role(sess), sess->received_batons, sess->cfg.close_reason); + lsquic_wt_close(sess->wt_sess, DEVIOUS_BATON_SESS_ERR_BRUH, + sess->cfg.close_reason, + strlen(sess->cfg.close_reason)); + return 0; + } + + send_datagram_if_needed(sess, baton); + + if (baton == 0) + { + LSQ_INFO("%s got terminal baton 0", db_role(sess)); + db_finish_stream(st, 1, "received terminal baton"); + return 0; + } + + next_baton = (unsigned char) (baton + 1); + LSQ_INFO("%s increment baton %u -> %u", db_role(sess), baton, next_baton); + self_init = sess->cfg.is_server ? LSQWT_SERVER : LSQWT_CLIENT; + + if (st->dbs_dir == LSQWT_UNI) + { + LSQ_INFO("%s responds to uni baton on new bidi stream", db_role(sess)); + out_dir = LSQWT_BIDI; + if (0 != open_stream_and_queue(sess, out_dir, next_baton)) + return -1; + db_finish_stream(st, 0, "handed off baton to new bidi stream"); + } + else if (st->dbs_initiator != self_init) + { + LSQ_INFO("%s responds on same peer-initiated bidi stream %"PRIu64, + db_role(sess), (uint64_t) lsquic_stream_id(st->dbs_stream)); + queue_baton(sess, st, next_baton); + } + else + { + LSQ_INFO("%s responds to self-initiated bidi baton on new uni stream", + db_role(sess)); + out_dir = LSQWT_UNI; + if (0 != open_stream_and_queue(sess, out_dir, next_baton)) + return -1; + db_finish_stream(st, 0, "handed off baton to new uni stream"); + } + + return 0; +} + + +static void +consume_baton_data (struct devious_baton_stream *st, int fin) +{ + unsigned char baton; + size_t consumed; + int r; + + if (st->dbs_flags & DBSF_MESSAGE_DONE) + return; + + r = parse_baton_message(st, &baton, &consumed); + if (r == 0) + { + if (fin) + { + if (0 == st->dbs_buf_len) + { + st->dbs_flags |= DBSF_MESSAGE_DONE; + return; + } + + LSQ_WARN("%s got FIN before full baton message; closing session", + db_role(st->dbs_session)); + lsquic_wt_close(st->dbs_session->wt_sess, + DEVIOUS_BATON_SESS_ERR_BRUH, + "got FIN before full baton message", + sizeof("got FIN before full baton message") - 1); + } + return; + } + + if (r < 0) + { + LSQ_WARN("%s got malformed baton message; closing session", + db_role(st->dbs_session)); + lsquic_wt_close(st->dbs_session->wt_sess, + DEVIOUS_BATON_SESS_ERR_BRUH, + "got malformed baton message", + sizeof("got malformed baton message") - 1); + return; + } + + if (st->dbs_buf_len != consumed) + { + LSQ_WARN("%s got trailing bytes in baton message; closing session", + db_role(st->dbs_session)); + lsquic_wt_close(st->dbs_session->wt_sess, + DEVIOUS_BATON_SESS_ERR_BRUH, + "got trailing bytes in baton message", + sizeof("got trailing bytes in baton message") - 1); + return; + } + + st->dbs_flags |= DBSF_MESSAGE_DONE; + st->dbs_buf_len = 0; + if (0 != handle_baton(st, baton)) + lsquic_wt_close(st->dbs_session->wt_sess, + DEVIOUS_BATON_SESS_ERR_DA_YAMN, + "could not handle baton", + sizeof("could not handle baton") - 1); +} + + +static void +consume_baton_datagram (struct devious_baton_session *sess, const void *buf, + size_t len) +{ + struct devious_baton_stream st; + unsigned char baton; + size_t consumed; + int r; + + memset(&st, 0, sizeof(st)); + st.dbs_session = sess; + st.dbs_buf = (unsigned char *) buf; + st.dbs_buf_len = len; + + r = parse_baton_message(&st, &baton, &consumed); + if (r <= 0 || consumed != len) + { + LSQ_WARN("%s got malformed/incomplete datagram baton; closing session", + db_role(sess)); + lsquic_wt_close(sess->wt_sess, DEVIOUS_BATON_SESS_ERR_BRUH, + "got malformed or incomplete datagram baton", + sizeof("got malformed or incomplete datagram baton") - 1); + return; + } + + /* Per spec, datagram batons are observational: log only, do not reply. */ + db_log_receive(&st, baton); +} + + +static void +maybe_close_baton_stream (struct devious_baton_stream *st) +{ + if (!st || st->dbs_kind != DB_STREAM_BATON || !st->dbs_stream) + return; + + if ((st->dbs_flags & DBSF_MESSAGE_DONE) + && (st->dbs_flags & DBSF_PEER_FIN) + && !(st->dbs_flags & DBSF_HAVE_BATON)) + db_close_baton_stream(st); +} + + +static void +drain_baton_stream_after_message (struct devious_baton_stream *st) +{ + unsigned char buf[256]; + ssize_t nread; + + nread = lsquic_stream_read(st->dbs_stream, buf, sizeof(buf)); + if (nread > 0) + { + LSQ_WARN("%s got trailing bytes in baton stream; closing session", + db_role(st->dbs_session)); + lsquic_wt_close(st->dbs_session->wt_sess, + DEVIOUS_BATON_SESS_ERR_BRUH, + "got trailing bytes in baton stream", + sizeof("got trailing bytes in baton stream") - 1); + return; + } + + if (nread == 0) + { + st->dbs_flags |= DBSF_PEER_FIN; + lsquic_stream_wantread(st->dbs_stream, 0); + maybe_close_baton_stream(st); + return; + } + + if (errno != EWOULDBLOCK) + db_close_baton_stream(st); +} + + +static lsquic_wt_session_ctx_t * +db_wti_on_session_open (void *ctx, struct lsquic_wt_session *sess, + const struct lsquic_wt_connect_info *UNUSED_info) +{ + struct devious_baton_app *cfg; + struct devious_baton_session *bsess; + + cfg = ctx; + bsess = calloc(1, sizeof(*bsess)); + if (!bsess) + return NULL; + + bsess->wt_sess = sess; + bsess->cfg = *cfg; + bsess->active_batons = cfg->count; + TAILQ_INSERT_TAIL(&s_sessions, bsess, next_session); + LSQ_INFO("%s opened devious baton session %"PRIu64 + " (count=%u, initial baton=%u, padding=%u)", db_role(bsess), + (uint64_t) lsquic_wt_session_id(sess), bsess->cfg.count, + bsess->cfg.baton, bsess->cfg.padding_len); + bsess->dg_burst_pending = bsess->cfg.dg_burst_count; + if (bsess->dg_burst_pending > 0) + { + LSQ_INFO("%s enabling WT datagram write callback with burst=%u" + " policy=%s", db_role(bsess), bsess->dg_burst_pending, + db_dg_policy(bsess->cfg.dg_drop_policy)); + (void) lsquic_wt_want_datagram_write(sess, 1); + } + + if (bsess->cfg.is_server) + { + unsigned i; + for (i = 0; i < bsess->cfg.count; ++i) + { + LSQ_INFO("server starts baton exchange %u/%u with baton %u", + i + 1, bsess->cfg.count, bsess->cfg.baton); + if (0 != open_stream_and_queue(bsess, LSQWT_UNI, + (unsigned char) bsess->cfg.baton)) + { + lsquic_wt_close(sess, DEVIOUS_BATON_SESS_ERR_DA_YAMN, + "could not open initial baton stream", + sizeof("could not open initial baton stream") - 1); + break; + } + } + } + + return (lsquic_wt_session_ctx_t *) bsess; +} + + +static void +db_wti_on_session_rejected (void *ctx, + const struct lsquic_wt_connect_info *info, + unsigned status, const char *reason, + size_t reason_len) +{ + const struct devious_baton_app *cfg; + + cfg = ctx; + LSQ_INFO("%s WT session rejected (status=%u, path=%s, reason=%.*s)", + cfg && cfg->is_server ? "server" : "client", status, + info && info->wtci_path ? info->wtci_path : "", + (int) reason_len, reason ? reason : ""); +} + + +static void +db_wti_on_session_close (struct lsquic_wt_session *sess, + struct lsquic_wt_session_ctx *sctx, + uint64_t UNUSED_code, + const char *UNUSED_reason, + size_t UNUSED_reason_len) +{ + struct devious_baton_session *bsess; + struct lsquic_conn *conn; + + bsess = sctx ? (struct devious_baton_session *) sctx + : find_session(sess); + if (bsess) + { + LSQ_INFO("%s closed devious baton session %"PRIu64 + " (active batons left: %u)", db_role(bsess), + (uint64_t) lsquic_wt_session_id(sess), bsess->active_batons); + bsess->closed = 1; + if (!bsess->cfg.is_server) + { + conn = lsquic_wt_session_conn(sess); + if (conn) + lsquic_conn_close(conn); + } + } + maybe_remove_closed_session(bsess); +} + + +static lsquic_stream_ctx_t * +db_on_wt_stream (struct lsquic_wt_session *sess, struct lsquic_stream *stream, + enum lsquic_wt_stream_dir dir) +{ + struct devious_baton_session *bsess; + struct devious_baton_stream *st; + + bsess = find_session(sess); + if (!bsess) + return NULL; + + st = calloc(1, sizeof(*st)); + if (!st) + return NULL; + + st->dbs_kind = DB_STREAM_BATON; + st->dbs_session = bsess; + st->dbs_stream = stream; + st->dbs_dir = dir; + st->dbs_initiator = lsquic_wt_stream_initiator(stream); + ++bsess->active_streams; + LSQ_INFO("%s accepted %s stream %"PRIu64" (%s-initiated)", + db_role(bsess), db_dir(st->dbs_dir), + (uint64_t) lsquic_stream_id(stream), + db_initiator(st->dbs_initiator)); + if (stream_is_readable_by_us(bsess, st)) + lsquic_stream_wantread(stream, 1); + return (lsquic_stream_ctx_t *) st; +} + + +static lsquic_stream_ctx_t * +db_wti_on_uni_stream (struct lsquic_wt_session *sess, + struct lsquic_stream *stream) +{ + return db_on_wt_stream(sess, stream, LSQWT_UNI); +} + + +static lsquic_stream_ctx_t * +db_wti_on_bidi_stream (struct lsquic_wt_session *sess, + struct lsquic_stream *stream) +{ + return db_on_wt_stream(sess, stream, LSQWT_BIDI); +} + + +static void +db_on_wt_datagram (struct lsquic_wt_session *sess, const void *buf, size_t len) +{ + struct devious_baton_session *bsess; + + if (!sess || !buf || len == 0) + return; + + bsess = find_session(sess); + if (!bsess) + return; + + LSQ_INFO("%s received datagram (%zu bytes)", db_role(bsess), len); + consume_baton_datagram(bsess, buf, len); +} + + +static int +db_wti_on_datagram_write (struct lsquic_wt_session *sess, + size_t UNUSED_max_dg_size) +{ + struct devious_baton_session *bsess; + + bsess = find_session(sess); + if (!bsess) + return 0; + + if (bsess->dg_burst_pending == 0) + { + (void) lsquic_wt_want_datagram_write(sess, 0); + return 0; + } + + db_send_burst_datagram(bsess); + if (bsess->dg_burst_pending == 0) + { + (void) lsquic_wt_want_datagram_write(sess, 0); + LSQ_INFO("%s burst datagram send complete: sent=%u failed=%u" + " policy=%s", db_role(bsess), bsess->dg_burst_sent, + bsess->dg_burst_failed, db_dg_policy(bsess->cfg.dg_drop_policy)); + } + + return 0; +} + + +static void +db_on_wt_stream_fin (struct lsquic_stream *stream, + struct lsquic_stream_ctx *sctx) +{ + struct devious_baton_stream *st; + + st = (struct devious_baton_stream *) sctx; + if (!st || st->dbs_kind != DB_STREAM_BATON) + return; + + LSQ_INFO("%s got FIN on %s stream %"PRIu64, db_role(st->dbs_session), + db_dir(st->dbs_dir), (uint64_t) lsquic_stream_id(stream)); + st->dbs_flags |= DBSF_PEER_FIN; + consume_baton_data(st, 1); + maybe_close_baton_stream(st); +} + + +static void +db_wti_on_stream_reset (struct lsquic_stream *stream, + struct lsquic_stream_ctx *sctx, + uint64_t error_code) +{ + struct devious_baton_stream *st; + + st = (struct devious_baton_stream *) sctx; + if (!st || st->dbs_kind != DB_STREAM_BATON) + return; + + st->dbs_flags |= DBSF_PEER_RESET_SEEN; + LSQ_INFO("%s got RESET_STREAM on %s stream %"PRIu64" with code %"PRIu64, + db_role(st->dbs_session), db_dir(st->dbs_dir), + (uint64_t) lsquic_stream_id(stream), error_code); + if (st->dbs_dir == LSQWT_BIDI) + db_maybe_reset_stream(st, DEVIOUS_BATON_STREAM_ERR_WHATEVER, + "WHATEVER", "received RESET_STREAM on bidirectional stream"); + if (stream_is_readable_by_us(st->dbs_session, st)) + lsquic_stream_wantread(stream, 0); + lsquic_stream_wantwrite(stream, 0); + db_finish_stream(st, 1, "got reset"); +} + + + +static void +db_wti_on_stop_sending (struct lsquic_stream *stream, + struct lsquic_stream_ctx *sctx, + uint64_t error_code) +{ + struct devious_baton_stream *st; + + st = (struct devious_baton_stream *) sctx; + if (!st || st->dbs_kind != DB_STREAM_BATON) + return; + + st->dbs_flags |= DBSF_PEER_STOP_SEND_SEEN; + LSQ_INFO("%s got STOP_SENDING on %s stream %"PRIu64 + " with code %"PRIu64, + db_role(st->dbs_session), db_dir(st->dbs_dir), + (uint64_t) lsquic_stream_id(stream), error_code); + db_maybe_reset_stream(st, DEVIOUS_BATON_STREAM_ERR_WHATEVER, + "WHATEVER", "received STOP_SENDING"); + if (stream_is_readable_by_us(st->dbs_session, st)) + lsquic_stream_wantread(stream, 0); + lsquic_stream_wantwrite(stream, 0); + db_finish_stream(st, 1, "got STOP_SENDING"); +} + +static void on_read (struct lsquic_stream *stream, + struct lsquic_stream_ctx *st_h); +static void on_write (struct lsquic_stream *stream, + struct lsquic_stream_ctx *st_h); +static void on_close (struct lsquic_stream *stream, + struct lsquic_stream_ctx *st_h); + + +static uint64_t +db_ss_code (struct lsquic_stream *UNUSED_stream, + struct lsquic_stream_ctx *UNUSED_sctx) +{ + return DEVIOUS_BATON_STREAM_ERR_IDC; +} + + +static const struct lsquic_webtransport_if wt_if = +{ + .wti_on_stream_read = on_read, + .wti_on_stream_write = on_write, + .wti_on_stream_close = on_close, + .wti_on_stream_ss_code = db_ss_code, + .wti_on_session_open = db_wti_on_session_open, + .wti_on_session_rejected = db_wti_on_session_rejected, + .wti_on_session_close = db_wti_on_session_close, + .wti_on_uni_stream = db_wti_on_uni_stream, + .wti_on_bidi_stream = db_wti_on_bidi_stream, + .wti_on_datagram_read = db_on_wt_datagram, + .wti_on_datagram_write = db_wti_on_datagram_write, + .wti_on_stream_fin = db_on_wt_stream_fin, + .wti_on_stream_reset = db_wti_on_stream_reset, + .wti_on_stop_sending = db_wti_on_stop_sending, +}; + + +static void +apply_wt_datagram_params (struct lsquic_wt_accept_params *params, + const struct devious_baton_app *app) +{ + params->wtap_datagram_drop_policy = (enum lsquic_wt_dg_drop_policy) + app->dg_drop_policy; + params->wtap_datagram_send_mode = LSQUIC_HTTP_DG_SEND_DEFAULT; + params->wtap_max_datagram_queue_count = app->dgq_max_count; + params->wtap_max_datagram_queue_bytes = app->dgq_max_bytes; +} + + +static void * +hset_create (void *UNUSED_hsi_ctx, struct lsquic_stream *UNUSED_stream, + int UNUSED_is_push_promise) +{ + struct hset *hset; + + if ((hset = malloc(sizeof(*hset)))) + { + STAILQ_INIT(hset); + return hset; + } + else + return NULL; +} + + +static struct lsxpack_header * +hset_prepare_decode (void *hset_p, struct lsxpack_header *xhdr, + size_t req_space) +{ + struct hset *const hset = hset_p; + struct hset_elem *el; + char *buf; + + if (0 == req_space) + req_space = 0x100; + + if (req_space > LSXPACK_MAX_STRLEN) + { + LSQ_WARN("requested space for header is too large: %zd bytes", + req_space); + return NULL; + } + + if (!xhdr) + { + buf = malloc(req_space); + if (!buf) + { + LSQ_WARN("cannot allocate buf of %zd bytes", req_space); + return NULL; + } + el = malloc(sizeof(*el)); + if (!el) + { + LSQ_WARN("cannot allocate hset_elem"); + free(buf); + return NULL; + } + STAILQ_INSERT_TAIL(hset, el, next); + lsxpack_header_prepare_decode(&el->xhdr, buf, 0, req_space); + el->nalloc = req_space; + } + else + { + el = (struct hset_elem *) ((char *) xhdr + - offsetof(struct hset_elem, xhdr)); + if (req_space <= el->nalloc) + { + LSQ_ERROR("requested space is smaller than already allocated"); + return NULL; + } + if (el->nalloc > SIZE_MAX / 2) + { + LSQ_WARN("requested hset buffer growth overflows"); + return NULL; + } + if (req_space < el->nalloc * 2) + req_space = el->nalloc * 2; + buf = realloc(el->xhdr.buf, req_space); + if (!buf) + { + LSQ_WARN("cannot reallocate hset buf"); + return NULL; + } + el->xhdr.buf = buf; + el->xhdr.val_len = req_space; + el->nalloc = req_space; + } + + return &el->xhdr; +} + + +static int +hset_add_header (void *UNUSED_hset_p, struct lsxpack_header *UNUSED_xhdr) +{ + return 0; +} + + +static void +hset_destroy (void *hset_p) +{ + struct hset *hset = hset_p; + struct hset_elem *el, *next; + + for (el = STAILQ_FIRST(hset); el; el = next) + { + next = STAILQ_NEXT(el, next); + free(el->xhdr.buf); + free(el); + } + free(hset); +} + + +static const struct lsquic_hset_if header_bypass_api = +{ + .hsi_create_header_set = hset_create, + .hsi_prepare_decode = hset_prepare_decode, + .hsi_process_header = hset_add_header, + .hsi_discard_header_set = hset_destroy, +}; + + +static struct devious_baton_stream * +alloc_control_ctx (struct devious_baton_conn *conn, struct lsquic_stream *stream) +{ + struct devious_baton_stream *st; + + st = calloc(1, sizeof(*st)); + if (!st) + return NULL; + + st->dbs_kind = DB_STREAM_CONTROL; + st->dbs_conn = conn; + st->dbs_stream = stream; + return st; +} + + +static void +process_control_server (struct devious_baton_stream *st) +{ + struct hset *hset; + struct devious_baton_app cfg; + struct devious_baton_conn *conn; + struct lsquic_wt_accept_params params; + struct lsquic_wt_connect_info info; + char err_buf[128]; + int ok; + + conn = st->dbs_conn; + + hset = lsquic_stream_get_hset(st->dbs_stream); + if (!hset) + { + LSQ_ERROR("could not get header set from stream"); + lsquic_stream_close(st->dbs_stream); + return; + } + + cfg = *conn->app; + ok = parse_request(hset, &cfg, &info, err_buf, sizeof(err_buf)); + if (0 != ok) + { + hset_destroy(hset); + lsquic_wt_reject(st->dbs_stream, 400, err_buf, strlen(err_buf)); + lsquic_stream_close(st->dbs_stream); + return; + } + + LSQ_INFO("server accepted CONNECT for %s (count=%u, baton=%u, version=%u)", + cfg.path ? cfg.path : "", cfg.count, cfg.baton, cfg.version); + info.wtci_draft = lsquic_wt_peer_draft(lsquic_stream_conn(st->dbs_stream)); + + memset(¶ms, 0, sizeof(params)); + params.wtap_status = 200; + params.wtap_wt_if = &wt_if; + params.wtap_wt_if_ctx = &cfg; + params.wtap_connect_info = &info; + apply_wt_datagram_params(¶ms, &cfg); + if (0 != lsquic_wt_accept(st->dbs_stream, ¶ms)) + { + free_connect_info(&info); + hset_destroy(hset); + lsquic_stream_close(st->dbs_stream); + return; + } + + free_connect_info(&info); + hset_destroy(hset); + + if (0 != lsquic_stream_flush(st->dbs_stream)) + LSQ_ERROR("cannot flush response: %s", strerror(errno)); + + st->dbs_flags |= DBSF_MESSAGE_DONE; + +} + + +static int +retry_connect_with_legacy_protocol (struct devious_baton_stream *st) +{ + struct devious_baton_conn *conn; + struct lsquic_conn *lconn; + + conn = st->dbs_conn; + if (!conn || conn->app->is_server) + return 0; + + if (conn->retried_with_legacy_protocol) + return 0; + + if (!conn->connect_protocol + || 0 != strcmp(conn->connect_protocol, WEBTRANSPORT_H3_CONNECT_PROTOCOL)) + return 0; + + conn->retried_with_legacy_protocol = 1; + conn->connect_protocol = WEBTRANSPORT_CONNECT_PROTOCOL; + conn->headers_sent = 0; + conn->response_seen = 0; + conn->response_ok = 0; + conn->control_stream = NULL; + + LSQ_INFO("CONNECT failed; retrying with protocol `%s'", + conn->connect_protocol); + lsquic_stream_wantread(st->dbs_stream, 0); + lsquic_stream_wantwrite(st->dbs_stream, 0); + lconn = lsquic_stream_conn(st->dbs_stream); + lsquic_stream_close(st->dbs_stream); + lsquic_conn_make_stream(lconn); + return 1; +} + + +static void +process_control_client (struct devious_baton_stream *st) +{ + struct hset *hset; + struct lsquic_wt_connect_info info; + struct lsquic_wt_accept_params params; + const char *hostname; + char *protocol; + int rv; + unsigned status_code; + + if (st->dbs_conn->response_seen) + { + if (!st->dbs_conn->response_ok) + lsquic_stream_wantread(st->dbs_stream, 0); + return; + } + + hset = lsquic_stream_get_hset(st->dbs_stream); + if (!hset) + { + LSQ_ERROR("could not get header set from stream"); + lsquic_conn_abort(lsquic_stream_conn(st->dbs_stream)); + return; + } + + protocol = NULL; + st->dbs_conn->response_seen = 1; + st->dbs_conn->response_ok = parse_status(hset, &status_code); + log_hset_debug("CONNECT response", hset); + rv = dup_wt_protocol(hset, &protocol); + if (0 == rv) + LSQ_INFO("server selected WT protocol \"%s\"", protocol); + else if (rv < 0) + LSQ_DEBUG("ignore malformed WT-Protocol"); + hset_destroy(hset); + + if (!st->dbs_conn->response_ok) + { + if (retry_connect_with_legacy_protocol(st)) + { + free(protocol); + return; + } + + if (status_code) + LSQ_ERROR("CONNECT failed with status %u", status_code); + else + LSQ_ERROR("CONNECT failed: missing or invalid :status"); + free(protocol); + lsquic_stream_wantread(st->dbs_stream, 0); + lsquic_conn_abort(lsquic_stream_conn(st->dbs_stream)); + return; + } + + LSQ_INFO("client received successful CONNECT response with status %u", + status_code); + + hostname = st->dbs_conn->prog->prog_hostname + ? st->dbs_conn->prog->prog_hostname + : "localhost"; + memset(&info, 0, sizeof(info)); + info.wtci_authority = hostname; + info.wtci_path = st->dbs_conn->app->path; + info.wtci_protocol = protocol; + info.wtci_draft = lsquic_wt_peer_draft(lsquic_stream_conn(st->dbs_stream)); + + memset(¶ms, 0, sizeof(params)); + params.wtap_wt_if = &wt_if; + params.wtap_wt_if_ctx = st->dbs_conn->app; + params.wtap_connect_info = &info; + apply_wt_datagram_params(¶ms, st->dbs_conn->app); + if (0 != lsquic_wt_accept(st->dbs_stream, ¶ms)) + { + free(protocol); + lsquic_conn_abort(lsquic_stream_conn(st->dbs_stream)); + return; + } + + free(protocol); + st->dbs_flags |= DBSF_MESSAGE_DONE; + +} + + +static void +drain_control_stream (struct devious_baton_stream *st) +{ + unsigned char buf[256]; + ssize_t nread; + + for (;;) + { + nread = lsquic_stream_read(st->dbs_stream, buf, sizeof(buf)); + if (nread > 0) + continue; + if (nread == 0) + { + lsquic_stream_close(st->dbs_stream); + return; + } + if (errno == EWOULDBLOCK) + return; + lsquic_stream_close(st->dbs_stream); + return; + } +} + + +static void +on_read (struct lsquic_stream *stream, struct lsquic_stream_ctx *st_h) +{ + struct devious_baton_stream *st; + struct devious_baton_conn *conn; + unsigned char buf[2048]; + ssize_t nread; + + st = (struct devious_baton_stream *) st_h; + conn = (struct devious_baton_conn *) + lsquic_conn_get_ctx(lsquic_stream_conn(stream)); + + if (!st) + { + if (conn && conn->app->is_server + && !lsquic_stream_is_webtransport_client_bidi_stream(stream)) + { + st = alloc_control_ctx(conn, stream); + if (!st) + { + lsquic_stream_close(stream); + return; + } + lsquic_stream_set_ctx(stream, (lsquic_stream_ctx_t *) st); + } + else + return; + } + + if (st->dbs_kind == DB_STREAM_CONTROL) + { + if (st->dbs_flags & DBSF_MESSAGE_DONE) + { + drain_control_stream(st); + return; + } + + if (conn->app->is_server) + process_control_server(st); + else + process_control_client(st); + return; + } + + if (st->dbs_flags & DBSF_CLOSED) + return; + + if (st->dbs_flags & DBSF_MESSAGE_DONE) + { + drain_baton_stream_after_message(st); + return; + } + + while (1) + { + nread = lsquic_stream_read(stream, buf, sizeof(buf)); + if (nread > 0) + { + if (0 != buf_append(st, buf, (size_t) nread)) + { + lsquic_wt_close(st->dbs_session->wt_sess, + DEVIOUS_BATON_SESS_ERR_BRUH, + "could not buffer baton stream data", + sizeof("could not buffer baton stream data") - 1); + return; + } + } + else if (nread == 0) + { + st->dbs_flags |= DBSF_PEER_FIN; + consume_baton_data(st, 1); + lsquic_stream_wantread(stream, 0); + maybe_close_baton_stream(st); + return; + } + else if (errno == EWOULDBLOCK) + break; + else + break; + } + + consume_baton_data(st, 0); +} + + +static void +on_write (struct lsquic_stream *stream, struct lsquic_stream_ctx *st_h) +{ + struct devious_baton_stream *st; + struct devious_baton_conn *conn; + enum lsquic_wt_stream_initiator self_init; + unsigned char *msg; + size_t msg_len; + ssize_t nwritten; + + st = (struct devious_baton_stream *) st_h; + conn = (struct devious_baton_conn *) + lsquic_conn_get_ctx(lsquic_stream_conn(stream)); + + if (!st) + return; + + if (st->dbs_kind == DB_STREAM_CONTROL) + { + if (conn && !conn->app->is_server) + { + if (!conn->headers_sent) + { + conn->control_stream = stream; + if (0 != send_headers(conn)) + { + lsquic_conn_abort(lsquic_stream_conn(stream)); + return; + } + LSQ_INFO("client sent CONNECT request for %s using protocol `%s'", + conn->app->path, conn->connect_protocol); + conn->headers_sent = 1; + if (0 != lsquic_stream_flush(stream)) + { + LSQ_ERROR("cannot flush CONNECT headers: %s", + strerror(errno)); + lsquic_conn_abort(lsquic_stream_conn(stream)); + return; + } + } + lsquic_stream_wantwrite(stream, 0); + } + return; + } + + if (st->dbs_flags & DBSF_CLOSED) + return; + + if (!(st->dbs_flags & DBSF_HAVE_BATON)) + return; + + if (0 != write_baton_message(st->dbs_session, st->dbs_baton_to_send, + &msg, &msg_len)) + return; + + LSQ_INFO("%s sending baton %u on %s stream %"PRIu64, + db_role(st->dbs_session), st->dbs_baton_to_send, + db_dir(st->dbs_dir), + (uint64_t) lsquic_stream_id(stream)); + nwritten = lsquic_stream_write(stream, msg, msg_len); + free(msg); + + if (nwritten == (ssize_t) msg_len) + { + LSQ_INFO("%s sent baton %u on %s stream %"PRIu64, + db_role(st->dbs_session), st->dbs_baton_to_send, + db_dir(st->dbs_dir), + (uint64_t) lsquic_stream_id(stream)); + st->dbs_flags &= ~DBSF_HAVE_BATON; + lsquic_stream_shutdown(stream, 1); + lsquic_stream_wantwrite(stream, 0); + self_init = st->dbs_session->cfg.is_server + ? LSQWT_SERVER : LSQWT_CLIENT; + if (st->dbs_dir == LSQWT_UNI || st->dbs_initiator != self_init) + db_finish_stream(st, 0, "completed baton write"); + if (st->dbs_dir == LSQWT_UNI + && !stream_is_readable_by_us(st->dbs_session, st)) + db_close_baton_stream(st); + maybe_close_baton_stream(st); + } +} + + +static void +on_close (struct lsquic_stream *UNUSED_stream, + struct lsquic_stream_ctx *st_h) +{ + struct devious_baton_stream *st; + struct devious_baton_session *bsess; + + st = (struct devious_baton_stream *) st_h; + if (!st) + return; + + bsess = st->dbs_session; + if (st->dbs_kind == DB_STREAM_BATON && bsess && bsess->active_streams > 0) + { + --bsess->active_streams; + maybe_remove_closed_session(bsess); + } + + free(st->dbs_buf); + free(st); +} + + +static struct lsquic_conn_ctx * +on_new_conn (void *stream_if_ctx, struct lsquic_conn *conn) +{ + struct devious_baton_app *app; + struct devious_baton_conn *ctx; + + app = stream_if_ctx; + ctx = calloc(1, sizeof(*ctx)); + if (!ctx) + return NULL; + + ctx->app = app; + ctx->prog = app->prog; + ctx->connect_protocol = WEBTRANSPORT_H3_CONNECT_PROTOCOL; + + if (!app->is_server) + lsquic_conn_make_stream(conn); + + return (struct lsquic_conn_ctx *) ctx; +} + + +static void +on_conn_closed (struct lsquic_conn *conn) +{ + struct devious_baton_conn *ctx; + + ctx = (struct devious_baton_conn *) lsquic_conn_get_ctx(conn); + if (ctx && ctx->prog) + prog_stop(ctx->prog); + + free(ctx); + lsquic_conn_set_ctx(conn, NULL); +} + + +static struct lsquic_stream_ctx * +on_new_stream (void *stream_if_ctx, struct lsquic_stream *stream) +{ + struct devious_baton_app *app; + struct devious_baton_conn *conn; + struct devious_baton_stream *st; + + app = stream_if_ctx; + conn = (struct devious_baton_conn *) + lsquic_conn_get_ctx(lsquic_stream_conn(stream)); + + if (!conn || app->is_server) + return NULL; + + if (conn->control_stream) + return NULL; + + st = alloc_control_ctx(conn, stream); + if (!st) + { + lsquic_stream_close(stream); + return NULL; + } + + conn->control_stream = stream; + lsquic_stream_wantwrite(stream, 1); + lsquic_stream_wantread(stream, 1); + return (struct lsquic_stream_ctx *) st; +} + + +static const struct lsquic_stream_if devious_baton_stream_if_impl = +{ + .on_new_conn = on_new_conn, + .on_conn_closed = on_conn_closed, + .on_new_stream = on_new_stream, + .on_read = on_read, + .on_write = on_write, + .on_close = on_close, +}; + +void +devious_baton_app_init (struct devious_baton_app *app, struct prog *prog, + int is_server) +{ + memset(app, 0, sizeof(*app)); + app->prog = prog; + app->is_server = is_server; + app->version = 0; + app->count = 1; + app->baton = 1; + app->padding_len = 0; + app->dg_burst_count = 0; + app->dg_drop_policy = LSQWT_DG_FAIL_EAGAIN; + app->dgq_max_count = 0; + app->dgq_max_bytes = 0; + app->max_count = 100; + app->close_after_n_batons = 0; + app->close_reason = NULL; + app->path_base = DEVIOUS_BATON_PATH; + app->path = DEVIOUS_BATON_PATH; + + if (!is_server) + devious_baton_build_path(app); +} + + +int +devious_baton_build_path (struct devious_baton_app *app) +{ + return build_path(app); +} + + +int +devious_baton_accept (struct lsquic_stream *stream, + const struct lsquic_wt_connect_info *info, + const struct devious_baton_app *app, + char *err_buf, size_t err_sz) +{ + struct devious_baton_app cfg; + struct lsquic_wt_accept_params params; + + if (!stream || !info || !app) + { + if (err_buf && err_sz) + snprintf(err_buf, err_sz, "invalid arguments"); + return -1; + } + + cfg = *app; + if (0 != parse_path(info->wtci_path, &cfg, err_buf, err_sz)) + { + lsquic_wt_reject(stream, 400, err_buf ? err_buf : NULL, + err_buf ? strlen(err_buf) : 0); + lsquic_stream_close(stream); + return -1; + } + + memset(¶ms, 0, sizeof(params)); + params.wtap_status = 200; + params.wtap_wt_if = &wt_if; + params.wtap_wt_if_ctx = &cfg; + params.wtap_connect_info = info; + apply_wt_datagram_params(¶ms, &cfg); + if (0 != lsquic_wt_accept(stream, ¶ms)) + { + if (err_buf && err_sz) + snprintf(err_buf, err_sz, "cannot accept WebTransport"); + return -1; + } + + if (0 != lsquic_stream_flush(stream)) + LSQ_ERROR("cannot flush response: %s", strerror(errno)); + return 0; +} + + +const struct lsquic_stream_if * +devious_baton_stream_if (void) +{ + return &devious_baton_stream_if_impl; +} + + +const struct lsquic_hset_if * +devious_baton_hset_if (void) +{ + return &header_bypass_api; +} diff --git a/bin/devious_baton.h b/bin/devious_baton.h new file mode 100644 index 000000000..9a8cece6e --- /dev/null +++ b/bin/devious_baton.h @@ -0,0 +1,73 @@ +/* Copyright (c) 2017 - 2026 LiteSpeed Technologies Inc. See LICENSE. */ +#ifndef DEVIOUS_BATON_H +#define DEVIOUS_BATON_H + +#include + +struct lsquic_hset_if; +struct lsquic_stream_if; +struct lsquic_stream; +struct lsquic_wt_connect_info; +struct prog; + +#define WEBTRANSPORT_H3_CONNECT_PROTOCOL "webtransport-h3" +#define WEBTRANSPORT_CONNECT_PROTOCOL "webtransport" +#define DEVIOUS_BATON_PATH "/webtransport/devious-baton" + + +enum devious_baton_stream_error +{ + DEVIOUS_BATON_STREAM_ERR_IDC = 0x01, + DEVIOUS_BATON_STREAM_ERR_WHATEVER = 0x02, + DEVIOUS_BATON_STREAM_ERR_I_LIED = 0x03, +}; + + +enum devious_baton_session_error +{ + DEVIOUS_BATON_SESS_ERR_DA_YAMN = 0x01, + DEVIOUS_BATON_SESS_ERR_BRUH = 0x02, + DEVIOUS_BATON_SESS_ERR_SUS = 0x03, + DEVIOUS_BATON_SESS_ERR_BORED = 0x04, +}; + + +struct devious_baton_app +{ + struct prog *prog; + int is_server; + unsigned version; + unsigned count; + unsigned baton; + unsigned padding_len; + unsigned dg_burst_count; + unsigned dg_drop_policy; + unsigned dgq_max_count; + size_t dgq_max_bytes; + unsigned max_count; + unsigned close_after_n_batons; + const char *close_reason; + const char *path_base; + const char *path; + char path_buf[256]; +}; + +void +devious_baton_app_init (struct devious_baton_app *, struct prog *, int); + +int +devious_baton_build_path (struct devious_baton_app *); + +const struct lsquic_stream_if * +devious_baton_stream_if (void); + +const struct lsquic_hset_if * +devious_baton_hset_if (void); + +int +devious_baton_accept (struct lsquic_stream *, + const struct lsquic_wt_connect_info *, + const struct devious_baton_app *, + char *err_buf, size_t err_sz); + +#endif diff --git a/bin/duck_client.c b/bin/duck_client.c index 40987bcc4..fc31fa300 100644 --- a/bin/duck_client.c +++ b/bin/duck_client.c @@ -31,7 +31,8 @@ #include "test_common.h" #include "prog.h" -#include "../src/liblsquic/lsquic_logger.h" +#define TOOL_LOG_PREFIX "duck_client" +#include "tool_log.h" /* Expected request and response of the siduck protocol */ #define REQUEST "quack" diff --git a/bin/duck_server.c b/bin/duck_server.c index c57406824..33c1c9e29 100644 --- a/bin/duck_server.c +++ b/bin/duck_server.c @@ -25,7 +25,8 @@ #include "test_cert.h" #include "prog.h" -#include "../src/liblsquic/lsquic_logger.h" +#define TOOL_LOG_PREFIX "duck_server" +#include "tool_log.h" static lsquic_conn_ctx_t * diff --git a/bin/echo_client.c b/bin/echo_client.c index 1597f52f5..35cf01d6e 100644 --- a/bin/echo_client.c +++ b/bin/echo_client.c @@ -31,7 +31,8 @@ #include "test_common.h" #include "prog.h" -#include "../src/liblsquic/lsquic_logger.h" +#define TOOL_LOG_PREFIX "echo_client" +#include "tool_log.h" struct lsquic_conn_ctx; diff --git a/bin/echo_server.c b/bin/echo_server.c index 2d8bd1159..8af76aeff 100644 --- a/bin/echo_server.c +++ b/bin/echo_server.c @@ -24,7 +24,8 @@ #include "test_cert.h" #include "prog.h" -#include "../src/liblsquic/lsquic_logger.h" +#define TOOL_LOG_PREFIX "echo_server" +#include "tool_log.h" struct lsquic_conn_ctx; diff --git a/bin/http_client.c b/bin/http_client.c index 56ccb489a..07b32bee2 100644 --- a/bin/http_client.c +++ b/bin/http_client.c @@ -43,7 +43,8 @@ #include "test_common.h" #include "prog.h" -#include "../src/liblsquic/lsquic_logger.h" +#define TOOL_LOG_PREFIX "http_client" +#include "tool_log.h" #include "../src/liblsquic/lsquic_int_types.h" #include "../src/liblsquic/lsquic_util.h" #include "../src/liblsquic/lsquic_hash.h" @@ -86,6 +87,11 @@ static int s_discard_response; */ static long s_abandon_early; +/* DRR proof mode defaults: mixed POST body and HTTP datagrams. */ +#define DRR_PROOF_DEFAULT_PATH "/cgi-bin/md5sum.cgi" +#define DRR_PROOF_STREAM_CHUNK 16384 +#define DRR_PROOF_DGRAM_CHUNK 1200 + struct sample_stats { unsigned n; @@ -195,6 +201,7 @@ struct http_client_ctx { unsigned hcc_n_open_conns; unsigned hcc_reset_after_nbytes; unsigned hcc_retire_cid_after_nbytes; + unsigned hcc_drr_proof_secs; const char *hcc_download_dir; char *hcc_sess_resume_file_name; @@ -203,6 +210,8 @@ struct http_client_ctx { HCC_SKIP_SESS_RESUME = (1 << 0), HCC_SEEN_FIN = (1 << 1), HCC_ABORT_ON_INCOMPLETE = (1 << 2), + HCC_DRR_PROOF = (1 << 3), + HCC_METHOD_SET = (1 << 4), } hcc_flags; const char *hcc_test_type; struct prog *prog; @@ -224,6 +233,7 @@ struct lsquic_conn_ctx { */ enum { CH_SESSION_RESUME_SAVED = 1 << 0, + CH_HTTP_CAP_DATAGRAMS = 1 << 1, } ch_flags; }; @@ -448,6 +458,18 @@ http_client_on_hsk_done (lsquic_conn_t *conn, enum lsquic_hsk_status status) } +static void +http_client_on_http_caps (lsquic_conn_t *conn, const struct lsquic_http_caps *caps) +{ + lsquic_conn_ctx_t *conn_h = lsquic_conn_get_ctx(conn); + + if (caps->lhc_flags & LSQUIC_HTTP_CAP_DATAGRAMS) + conn_h->ch_flags |= CH_HTTP_CAP_DATAGRAMS; + else + conn_h->ch_flags &= ~CH_HTTP_CAP_DATAGRAMS; +} + + /* Now only used for gQUIC and will be going away after that */ static void http_client_on_sess_resume_info (lsquic_conn_t *conn, const unsigned char *buf, @@ -503,19 +525,72 @@ struct lsquic_stream_ctx { ABANDON = 1 << 2, /* Abandon reading from stream after sh_stop bytes * have been read. */ + SH_DG_WANT_WRITE = 1 << 3, } sh_flags; lsquic_time_t sh_created; lsquic_time_t sh_ttfb; + lsquic_time_t sh_proof_deadline; size_t sh_stop; /* Stop after reading this many bytes if ABANDON is set */ size_t sh_nread; /* Number of bytes read from stream using one of * lsquic_stream_read* functions. */ + size_t sh_stream_gen_bytes; + size_t sh_dgram_gen_bytes; unsigned count; FILE *download_fh; struct lsquic_reader reader; + unsigned char sh_dgram_payload[DRR_PROOF_DGRAM_CHUNK]; }; +static int +stream_in_drr_proof (const lsquic_stream_ctx_t *st_h) +{ + return !!(st_h->client_ctx->hcc_flags & HCC_DRR_PROOF); +} + + +static int +drr_proof_expired (const lsquic_stream_ctx_t *st_h) +{ + return stream_in_drr_proof(st_h) + && lsquic_time_now() >= st_h->sh_proof_deadline; +} + + +static void +stop_drr_proof_writes (lsquic_stream_t *stream, lsquic_stream_ctx_t *st_h) +{ + if (st_h->sh_flags & SH_DG_WANT_WRITE) + { + (void) lsquic_stream_want_http_dg_write(stream, 0); + st_h->sh_flags &= ~SH_DG_WANT_WRITE; + } + lsquic_stream_shutdown(stream, 1); + lsquic_stream_wantread(stream, 1); +} + + +static void +start_drr_proof_datagram_write (lsquic_stream_t *stream, lsquic_stream_ctx_t *st_h) +{ + lsquic_conn_ctx_t *conn_h; + + if (st_h->sh_flags & SH_DG_WANT_WRITE) + return; + + conn_h = lsquic_conn_get_ctx(lsquic_stream_conn(stream)); + if (!(conn_h->ch_flags & CH_HTTP_CAP_DATAGRAMS)) + return; + + if (0 == lsquic_stream_want_http_dg_write(stream, 1)) + st_h->sh_flags |= SH_DG_WANT_WRITE; + else + LSQ_WARN("cannot enable HTTP datagram write for stream %"PRIu64": %s", + lsquic_stream_id(stream), strerror(errno)); +} + + static lsquic_stream_ctx_t * http_client_on_new_stream (void *stream_if_ctx, lsquic_stream_t *stream) { @@ -533,6 +608,12 @@ http_client_on_new_stream (void *stream_if_ctx, lsquic_stream_t *stream) st_h->client_ctx = stream_if_ctx; /* Internal helper (not in lsquic.h); example-only timing. */ st_h->sh_created = lsquic_time_now(); + if (stream_in_drr_proof(st_h)) + { + st_h->sh_proof_deadline = st_h->sh_created + + (lsquic_time_t) st_h->client_ctx->hcc_drr_proof_secs * 1000000; + memset(st_h->sh_dgram_payload, 'D', sizeof(st_h->sh_dgram_payload)); + } if (st_h->client_ctx->hcc_cur_pe) { st_h->client_ctx->hcc_cur_pe = TAILQ_NEXT( @@ -609,6 +690,8 @@ send_headers (lsquic_stream_ctx_t *st_h) hostname = st_h->client_ctx->prog->prog_hostname; hbuf.off = 0; struct lsxpack_header headers_arr[10]; + const int has_request_body = !!st_h->client_ctx->payload + || stream_in_drr_proof(st_h); #define V(v) (v), strlen(v) header_set_ptr(&headers_arr[h_idx++], &hbuf, V(":method"), V(st_h->client_ctx->method)); header_set_ptr(&headers_arr[h_idx++], &hbuf, V(":scheme"), V("https")); @@ -629,17 +712,19 @@ send_headers (lsquic_stream_ctx_t *st_h) sprintf(pfv, "i=?0"); header_set_ptr(&headers_arr[h_idx++], &hbuf, V("priority"), V(pfv)); } - if (st_h->client_ctx->payload) + if (has_request_body) { header_set_ptr(&headers_arr[h_idx++], &hbuf, V("content-type"), V("application/octet-stream")); - header_set_ptr(&headers_arr[h_idx++], &hbuf, V("content-length"), V( st_h->client_ctx->payload_size)); + if (st_h->client_ctx->payload) + header_set_ptr(&headers_arr[h_idx++], &hbuf, V("content-length"), + V(st_h->client_ctx->payload_size)); } lsquic_http_headers_t headers = { .count = h_idx, .headers = headers_arr, }; if (0 != lsquic_stream_send_headers(st_h->stream, &headers, - st_h->client_ctx->payload == NULL)) + !has_request_body)) { LSQ_ERROR("cannot send headers: %s", strerror(errno)); exit(1); @@ -677,6 +762,34 @@ display_cert_chain (lsquic_conn_t *conn) } +static int +http_client_on_http_dg_write (lsquic_stream_t *stream, lsquic_stream_ctx_t *st_h, + size_t max_quic_payload, lsquic_http_dg_consume_f consume_datagram) +{ + size_t payload_sz; + + if (!stream_in_drr_proof(st_h)) + return -1; + + if (drr_proof_expired(st_h)) + { + stop_drr_proof_writes(stream, st_h); + return -1; + } + + payload_sz = MIN(max_quic_payload, sizeof(st_h->sh_dgram_payload)); + if (0 == payload_sz) + return -1; + + if (0 != consume_datagram(stream, st_h->sh_dgram_payload, payload_sz, + LSQUIC_HTTP_DG_SEND_DATAGRAM)) + return -1; + + st_h->sh_dgram_gen_bytes += payload_sz; + return 0; +} + + static void http_client_on_write (lsquic_stream_t *stream, lsquic_stream_ctx_t *st_h) { @@ -684,7 +797,32 @@ http_client_on_write (lsquic_stream_t *stream, lsquic_stream_ctx_t *st_h) if (st_h->sh_flags & HEADERS_SENT) { - if (st_h->client_ctx->payload && test_reader_size(st_h->reader.lsqr_ctx) > 0) + if (stream_in_drr_proof(st_h)) + { + unsigned char buf[DRR_PROOF_STREAM_CHUNK]; + + if (drr_proof_expired(st_h)) + { + stop_drr_proof_writes(stream, st_h); + return; + } + + memset(buf, 'S', sizeof(buf)); + while ((nw = lsquic_stream_write(stream, buf, sizeof(buf))) > 0) + st_h->sh_stream_gen_bytes += (size_t) nw; + + if (nw < 0 && errno != EWOULDBLOCK) + { + LSQ_ERROR("write error: %s", strerror(errno)); + exit(1); + } + + start_drr_proof_datagram_write(stream, st_h); + lsquic_stream_wantwrite(stream, 1); + return; + } + else if (st_h->client_ctx->payload + && test_reader_size(st_h->reader.lsqr_ctx) > 0) { nw = lsquic_stream_writef(stream, &st_h->reader); if (nw < 0) @@ -712,6 +850,8 @@ http_client_on_write (lsquic_stream_t *stream, lsquic_stream_ctx_t *st_h) { st_h->sh_flags |= HEADERS_SENT; send_headers(st_h); + if (stream_in_drr_proof(st_h)) + start_drr_proof_datagram_write(stream, st_h); } } @@ -956,6 +1096,9 @@ http_client_on_close (lsquic_stream_t *stream, lsquic_stream_ctx_t *st_h) (client_ctx->hcc_cc_reqs_per_conn - conn_h->ch_n_cc_streams))); create_streams(client_ctx, conn_h); } + if (stream_in_drr_proof(st_h)) + LSQ_INFO("DRR proof stream stats: stream-bytes=%zu, dgram-bytes=%zu", + st_h->sh_stream_gen_bytes, st_h->sh_dgram_gen_bytes); if (st_h->reader.lsqr_ctx) destroy_lsquic_reader_ctx(st_h->reader.lsqr_ctx); if (st_h->download_fh) @@ -971,8 +1114,9 @@ static struct lsquic_stream_if http_client_if = { .on_read = http_client_on_read, .on_write = http_client_on_write, .on_close = http_client_on_close, + .on_http_dg_write = http_client_on_http_dg_write, .on_hsk_done = http_client_on_hsk_done, - .on_hset_in = http_client_on_hset_in, + .on_http_caps = http_client_on_http_caps, }; @@ -1085,6 +1229,8 @@ usage (const char *prog) " -Q ALPN Use hq ALPN. Specify, for example, \"h3-29\".\n" " -J TEST Run test. Available tests:\n" " goaway - call lsquic_conn_going_away() after handshake\n" +" -Y SECS DRR proof mode: send POST stream body and HTTP datagrams\n" +" as fast as possible for SECS seconds, then stop writes.\n" , prog); } @@ -1743,6 +1889,7 @@ main (int argc, char **argv) break; case 'M': client_ctx.method = optarg; + client_ctx.hcc_flags |= HCC_METHOD_SET; break; case 'r': client_ctx.hcc_total_n_reqs = atoi(optarg); @@ -1855,6 +2002,15 @@ main (int argc, char **argv) prog.prog_api.ea_alpn = optarg; prog.prog_api.ea_stream_if = &hq_client_if; break; + case 'Y': + client_ctx.hcc_drr_proof_secs = atoi(optarg); + if (0 == client_ctx.hcc_drr_proof_secs) + { + LSQ_ERROR("invalid DRR proof duration `%s'", optarg); + exit(1); + } + client_ctx.hcc_flags |= HCC_DRR_PROOF; + break; common_opts: default: if (0 != prog_set_opt(&prog, opt, optarg)) @@ -1869,6 +2025,11 @@ main (int argc, char **argv) if (client_ctx.qif_file) { + if (client_ctx.hcc_flags & HCC_DRR_PROOF) + { + LSQ_ERROR("DRR proof mode is incompatible with QIF mode"); + exit(1); + } client_ctx.qif_fh = fopen(client_ctx.qif_file, "r"); if (!client_ctx.qif_fh) { @@ -1884,8 +2045,43 @@ main (int argc, char **argv) } else if (TAILQ_EMPTY(&client_ctx.hcc_path_elems)) { - fprintf(stderr, "Specify at least one path using -p option\n"); - exit(1); + if (client_ctx.hcc_flags & HCC_DRR_PROOF) + { + pe = calloc(1, sizeof(*pe)); + if (!pe) + { + perror("calloc"); + exit(1); + } + pe->path = DRR_PROOF_DEFAULT_PATH; + TAILQ_INSERT_TAIL(&client_ctx.hcc_path_elems, pe, next_pe); + } + else + { + fprintf(stderr, "Specify at least one path using -p option\n"); + exit(1); + } + } + + if (client_ctx.hcc_flags & HCC_DRR_PROOF) + { + if (client_ctx.payload) + { + LSQ_ERROR("DRR proof mode is incompatible with -P payload"); + exit(1); + } + if (!((prog.prog_engine_flags & LSENG_HTTP) + && prog.prog_api.ea_stream_if == &http_client_if)) + { + LSQ_ERROR("DRR proof mode requires HTTP/3 mode"); + exit(1); + } + if (!(client_ctx.hcc_flags & HCC_METHOD_SET)) + client_ctx.method = "POST"; + prog.prog_settings.es_http_datagrams = 1; + LSQ_INFO("DRR proof mode enabled for %u second%s", + client_ctx.hcc_drr_proof_secs, + client_ctx.hcc_drr_proof_secs == 1 ? "" : "s"); } /* Internal helper (not in lsquic.h); example-only timing. */ diff --git a/bin/http_server.c b/bin/http_server.c index 664de68ff..d73610bbf 100644 --- a/bin/http_server.c +++ b/bin/http_server.c @@ -7,6 +7,7 @@ #include #include +#include #include #include #include @@ -29,8 +30,10 @@ #include #include "lsquic.h" +#include "lsquic_wt.h" #include "../src/liblsquic/lsquic_hash.h" #include "lsxpack_header.h" +#include "devious_baton.h" #include "test_config.h" #include "test_common.h" #include "test_cert.h" @@ -44,7 +47,8 @@ #endif #endif -#include "../src/liblsquic/lsquic_logger.h" +#define TOOL_LOG_PREFIX "http_server" +#include "tool_log.h" #include "../src/liblsquic/lsquic_int_types.h" #include "../src/liblsquic/lsquic_util.h" @@ -305,6 +309,8 @@ struct server_ctx { unsigned max_conn; unsigned n_conn; unsigned n_current_conns; + struct devious_baton_app baton_app; + int enable_devious_baton; unsigned delay_resp_sec; uint64_t max_pacing_rate; }; @@ -431,7 +437,7 @@ struct md5sum_ctx struct req { enum method { - UNSET, GET, POST, UNSUPPORTED, + UNSET, GET, POST, CONNECT, UNSUPPORTED, } method; enum { HAVE_XHDR = 1 << 0, @@ -440,10 +446,16 @@ struct req PH_AUTHORITY = 1 << 0, PH_METHOD = 1 << 1, PH_PATH = 1 << 2, + PH_PROTOCOL = 1 << 3, + PH_SCHEME = 1 << 4, } pseudo_headers; char *path; + char *scheme; char *method_str; char *authority_str; + char *connect_protocol; + char *protocol; + char *origin; char *qif_str; size_t qif_sz; struct lsxpack_header @@ -480,6 +492,7 @@ struct lsquic_stream_ctx { SH_HEADERS_SENT = (1 << 0), SH_DELAYED = (1 << 1), SH_HEADERS_READ = (1 << 2), + SH_WT_HANDOFF = (1 << 3), } flags; struct lsquic_reader reader; int file_fd; /* Used by pwritev */ @@ -1078,6 +1091,7 @@ http_server_on_close (lsquic_stream_t *stream, lsquic_stream_ctx_t *st_h) } + const struct lsquic_stream_if http_server_if = { .on_new_conn = http_server_on_new_conn, .on_conn_closed = http_server_on_conn_closed, @@ -1321,6 +1335,147 @@ static const char INDEX_HTML[] = ; +struct wt_connect_handler +{ + const char *path_prefix; + int (*handler)(struct lsquic_stream *, struct lsquic_stream_ctx *); +}; + + +static int +path_prefix_matches (const char *path, const char *prefix) +{ + size_t len; + + if (!path || !prefix) + return 0; + + len = strlen(prefix); + return 0 == strncmp(path, prefix, len) + && (path[len] == '\0' || path[len] == '?'); +} + + +static int +is_supported_connect_protocol (const char *connect_protocol) +{ + return 0 == strcmp(connect_protocol, WEBTRANSPORT_H3_CONNECT_PROTOCOL) + || 0 == strcmp(connect_protocol, WEBTRANSPORT_CONNECT_PROTOCOL); +} + + +static int +baton_connect_handler (struct lsquic_stream *stream, + struct lsquic_stream_ctx *st_h) +{ + struct lsquic_wt_connect_info info; + struct req *req; + char err_buf[128]; + int rv; + + req = st_h->req; + if (!req) + return 1; + + memset(&info, 0, sizeof(info)); + info.wtci_authority = req->authority_str; + info.wtci_path = req->path; + info.wtci_origin = req->origin; + info.wtci_protocol = req->protocol; + info.wtci_draft = lsquic_wt_peer_draft(lsquic_stream_conn(stream)); + + rv = devious_baton_accept(stream, &info, &st_h->server_ctx->baton_app, + err_buf, sizeof(err_buf)); + if (0 == rv) + { + st_h->flags |= SH_WT_HANDOFF; + interop_server_hset_destroy(req); + st_h->req = NULL; + } + + return 1; +} + + +static const struct wt_connect_handler wt_connect_handlers[] = +{ + { + .path_prefix = DEVIOUS_BATON_PATH, + .handler = baton_connect_handler, + }, +}; + + +static int +handle_connect_request (struct lsquic_stream *stream, + struct lsquic_stream_ctx *st_h) +{ + const struct wt_connect_handler *handler; + struct req *req; + char err_buf[128]; + + req = st_h->req; + if (!req) + return 0; + + if (!req->connect_protocol) + { + snprintf(err_buf, sizeof(err_buf), "Protocol is not specified"); + lsquic_wt_reject(stream, 400, err_buf, strlen(err_buf)); + lsquic_stream_close(stream); + return 1; + } + + if (!is_supported_connect_protocol(req->connect_protocol)) + { + snprintf(err_buf, sizeof(err_buf), "Unsupported CONNECT protocol"); + lsquic_wt_reject(stream, 400, err_buf, strlen(err_buf)); + lsquic_stream_close(stream); + return 1; + } + + if (!req->scheme) + { + snprintf(err_buf, sizeof(err_buf), "Scheme is not specified"); + lsquic_wt_reject(stream, 400, err_buf, strlen(err_buf)); + lsquic_stream_close(stream); + return 1; + } + + if (0 != strcmp(req->scheme, "https")) + { + snprintf(err_buf, sizeof(err_buf), "Unsupported CONNECT scheme"); + lsquic_wt_reject(stream, 400, err_buf, strlen(err_buf)); + lsquic_stream_close(stream); + return 1; + } + + if (!req->path) + { + snprintf(err_buf, sizeof(err_buf), "Path is not specified"); + lsquic_wt_reject(stream, 400, err_buf, strlen(err_buf)); + lsquic_stream_close(stream); + return 1; + } + + if (st_h->server_ctx->enable_devious_baton) + for (handler = wt_connect_handlers; + handler < wt_connect_handlers + + sizeof(wt_connect_handlers) + / sizeof(wt_connect_handlers[0]); ++handler) + if (path_prefix_matches(req->path, handler->path_prefix)) + { + return handler->handler(stream, st_h); + } + + snprintf(err_buf, sizeof(err_buf), "No WebTransport handler for %s", + req->path); + lsquic_wt_reject(stream, 404, err_buf, strlen(err_buf)); + lsquic_stream_close(stream); + return 1; +} + + static size_t read_md5 (void *ctx, const unsigned char *buf, size_t sz, int fin) { @@ -1362,6 +1517,9 @@ http_server_interop_on_read (lsquic_stream_t *stream, lsquic_stream_ctx_t *st_h) char md5str[ sizeof(md5sum) * 2 + 1 ]; char byte[1]; + if (st_h->flags & SH_WT_HANDOFF) + return; + if (!(st_h->flags & SH_HEADERS_READ)) { st_h->flags |= SH_HEADERS_READ; @@ -1374,6 +1532,11 @@ http_server_interop_on_read (lsquic_stream_t *stream, lsquic_stream_ctx_t *st_h) ERROR_RESP(400, "Path is not specified"); else if (st_h->req->method == UNSUPPORTED) ERROR_RESP(501, "Method %s is not supported", st_h->req->method_str); + else if (st_h->req->method == CONNECT) + { + handle_connect_request(stream, st_h); + return; + } else if (!(map = find_handler(st_h->req->method, st_h->req->path, matches))) ERROR_RESP(404, "No handler found for method: %s; path: %s", st_h->req->method_str, st_h->req->path); @@ -1459,7 +1622,8 @@ http_server_interop_on_read (lsquic_stream_t *stream, lsquic_stream_ctx_t *st_h) if (nw < 0) { LSQ_ERROR("could not read from stream for MD5: %s", strerror(errno)); - exit(1); + lsquic_stream_close(stream); + return; } if (nw == 0) st_h->interop_u.md5c.done = 1; @@ -1486,8 +1650,9 @@ http_server_interop_on_read (lsquic_stream_t *stream, lsquic_stream_ctx_t *st_h) st_h->interop_u.vhc.req_body = malloc(st_h->req->qif_sz); if (!st_h->interop_u.vhc.req_body) { - perror("malloc"); - exit(1); + LSQ_ERROR("cannot allocate request body buffer"); + lsquic_stream_close(stream); + return; } } need = st_h->req->qif_sz - st_h->interop_u.vhc.req_sz; @@ -1497,7 +1662,7 @@ http_server_interop_on_read (lsquic_stream_t *stream, lsquic_stream_ctx_t *st_h) st_h->interop_u.vhc.req_body + st_h->interop_u.vhc.req_sz, need); if (nw > 0) - st_h->interop_u.vhc.req_sz += need; + st_h->interop_u.vhc.req_sz += (size_t) nw; else if (nw == 0) { LSQ_WARN("request body too short (does not match headers)"); @@ -1507,7 +1672,8 @@ http_server_interop_on_read (lsquic_stream_t *stream, lsquic_stream_ctx_t *st_h) else { LSQ_ERROR("error reading from stream"); - exit(1); + lsquic_stream_close(stream); + return; } } else @@ -1718,6 +1884,9 @@ http_server_interop_on_write (lsquic_stream_t *stream, lsquic_stream_ctx_t *st_h struct resp *resp; ssize_t nw; + if (st_h->flags & SH_WT_HANDOFF) + return; + switch (st_h->interop_handler) { case IOH_ERROR: @@ -1791,9 +1960,14 @@ usage (const char *prog) " -p FILE Push request with this path\n" " -w SIZE Write immediately (LSWS mode). Argument specifies maximum\n" " size of the immediate write.\n" +#if HAVE_REGEX +" -B Enable Devious Baton WebTransport handler (interop mode)\n" +" -u count With -B: max queued WT datagrams per session (0 = default)\n" +" -v bytes With -B: max queued WT datagram bytes per session (0 = default)\n" +#endif #if HAVE_PREADV " -P SIZE Use preadv(2) to read from disk and lsquic_stream_pwritev() to\n" -" write to stream. Positive SIZE indicate maximum value per\n" + " write to stream. Positive SIZE indicate maximum value per\n" " write; negative means always use remaining file size.\n" " Incompatible with -w.\n" #endif @@ -1804,13 +1978,61 @@ usage (const char *prog) } +static int +dup_first_sf_string (const char *value, size_t value_len, char **out) +{ + char *buf; + size_t in_off, out_off; + + for (in_off = 0; in_off < value_len; ++in_off) + if (value[in_off] == '"') + break; + if (in_off >= value_len) + return -1; + + ++in_off; + buf = malloc(value_len - in_off + 1); + if (!buf) + return -1; + + out_off = 0; + while (in_off < value_len) + { + if (value[in_off] == '"') + { + buf[out_off] = '\0'; + *out = buf; + return 0; + } + + if (value[in_off] == '\\') + { + ++in_off; + if (in_off >= value_len) + break; + } + + buf[out_off++] = value[in_off++]; + } + + free(buf); + return -1; +} + + static void * interop_server_hset_create (void *hsi_ctx, lsquic_stream_t *stream, int is_push_promise) { struct req *req; + (void) hsi_ctx; + (void) stream; + (void) is_push_promise; + req = malloc(sizeof(struct req)); + if (!req) + return NULL; memset(req, 0, offsetof(struct req, decode_buf)); return req; @@ -1857,7 +2079,8 @@ interop_server_hset_add_header (void *hset_p, struct lsxpack_header *xhdr) if (!xhdr) { - if (req->pseudo_headers == (PH_AUTHORITY|PH_METHOD|PH_PATH)) + if ((req->pseudo_headers & (PH_AUTHORITY|PH_METHOD|PH_PATH)) + == (PH_AUTHORITY|PH_METHOD|PH_PATH)) return 0; else { @@ -1872,13 +2095,27 @@ interop_server_hset_add_header (void *hset_p, struct lsxpack_header *xhdr) name_len = xhdr->name_len; value_len = xhdr->val_len; - req->qif_str = realloc(req->qif_str, - req->qif_sz + name_len + value_len + 2); - if (!req->qif_str) + if (name_len > SIZE_MAX - value_len - 2 + || req->qif_sz > SIZE_MAX - (name_len + value_len + 2)) { - LSQ_ERROR("malloc failed"); + LSQ_ERROR("header accumulation size overflow"); return -1; } + + { + char *new_qif_str; + size_t new_qif_sz; + + new_qif_sz = req->qif_sz + name_len + value_len + 2; + new_qif_str = realloc(req->qif_str, new_qif_sz); + if (!new_qif_str) + { + LSQ_ERROR("malloc failed"); + return -1; + } + req->qif_str = new_qif_str; + } + memcpy(req->qif_str + req->qif_sz, name, name_len); req->qif_str[req->qif_sz + name_len] = '\t'; memcpy(req->qif_str + req->qif_sz + name_len + 1, value, value_len); @@ -1907,12 +2144,36 @@ interop_server_hset_add_header (void *hset_p, struct lsxpack_header *xhdr) req->method = GET; else if (0 == strcmp(req->method_str, "POST")) req->method = POST; + else if (0 == strcmp(req->method_str, "CONNECT")) + req->method = CONNECT; else req->method = UNSUPPORTED; req->pseudo_headers |= PH_METHOD; return 0; } + if (9 == name_len && 0 == strncmp(name, ":protocol", 9)) + { + if (req->connect_protocol) + return 1; + req->connect_protocol = strndup(value, value_len); + if (!req->connect_protocol) + return -1; + req->pseudo_headers |= PH_PROTOCOL; + return 0; + } + + if (7 == name_len && 0 == strncmp(name, ":scheme", 7)) + { + if (req->scheme) + return 1; + req->scheme = strndup(value, value_len); + if (!req->scheme) + return -1; + req->pseudo_headers |= PH_SCHEME; + return 0; + } + if (10 == name_len && 0 == strncmp(name, ":authority", 10)) { req->authority_str = strndup(value, value_len); @@ -1922,6 +2183,30 @@ interop_server_hset_add_header (void *hset_p, struct lsxpack_header *xhdr) return 0; } + if (6 == name_len && 0 == strncmp(name, "origin", 6)) + { + if (req->origin) + return 1; + req->origin = strndup(value, value_len); + if (!req->origin) + return -1; + return 0; + } + + if (name_len == sizeof("wt-available-protocols") - 1 + && 0 == strncmp(name, "wt-available-protocols", + sizeof("wt-available-protocols") - 1)) + { + if (req->protocol) + { + free(req->protocol); + req->protocol = NULL; + } + if (0 != dup_first_sf_string(value, value_len, &req->protocol)) + LSQ_DEBUG("ignore malformed WT-Available-Protocols"); + return 0; + } + return 0; } @@ -1932,8 +2217,12 @@ interop_server_hset_destroy (void *hset_p) struct req *req = hset_p; free(req->qif_str); free(req->path); + free(req->scheme); free(req->method_str); free(req->authority_str); + free(req->connect_protocol); + free(req->protocol); + free(req->origin); free(req); } @@ -1967,8 +2256,9 @@ main (int argc, char **argv) prog_init(&prog, LSENG_SERVER|LSENG_HTTP, &server_ctx.sports, &http_server_if, &server_ctx); + devious_baton_app_init(&server_ctx.baton_app, &prog, 1); - while (-1 != (opt = getopt(argc, argv, PROG_OPTS "y:Y:n:p:r:w:P:x:h" + while (-1 != (opt = getopt(argc, argv, PROG_OPTS "y:Y:n:p:r:w:P:x:Bu:v:h" #if HAVE_OPEN_MEMSTREAM "Q:" #endif @@ -2014,6 +2304,50 @@ main (int argc, char **argv) case 'x': server_ctx.max_pacing_rate = strtoull(optarg, NULL, 10); break; +#if HAVE_REGEX + case 'B': + server_ctx.enable_devious_baton = 1; + prog.prog_settings.es_http_datagrams = 1; + prog.prog_settings.es_webtransport = 1; + prog.prog_settings.es_reset_stream_at = 1; + prog.prog_settings.es_init_max_streams_uni = 64; + if (0 == prog.prog_settings.es_init_max_stream_data_bidi_local) + prog.prog_settings.es_init_max_stream_data_bidi_local = 100000; + if (0 == prog.prog_settings.es_max_webtransport_sessions) + prog.prog_settings.es_max_webtransport_sessions = + LSQUIC_DF_MAX_WEBTRANSPORT_SESSIONS; + break; + case 'u': + { + char *end; + unsigned long val; + + errno = 0; + val = strtoul(optarg, &end, 10); + if (errno || *end || val > UINT_MAX) + { + LSQ_ERROR("invalid queue count `%s'", optarg); + exit(1); + } + server_ctx.baton_app.dgq_max_count = (unsigned) val; + break; + } + case 'v': + { + char *end; + unsigned long long val; + + errno = 0; + val = strtoull(optarg, &end, 10); + if (errno || *end || val > SIZE_MAX) + { + LSQ_ERROR("invalid queue bytes `%s'", optarg); + exit(1); + } + server_ctx.baton_app.dgq_max_bytes = (size_t) val; + break; + } +#endif case 'h': usage(argv[0]); prog_print_common_options(&prog, stdout); @@ -2032,7 +2366,21 @@ main (int argc, char **argv) } } - if (!server_ctx.document_root) + if (server_ctx.enable_devious_baton) + { +#if HAVE_REGEX + if (server_ctx.document_root) + LSQ_NOTICE("Devious Baton enabled: ignoring document root"); + init_map_regexes(); + prog.prog_api.ea_stream_if = &interop_http_server_if; + prog.prog_api.ea_hsi_if = &header_bypass_api; + prog.prog_api.ea_hsi_ctx = NULL; +#else + LSQ_ERROR("Devious Baton requires regex support"); + exit(EXIT_FAILURE); +#endif + } + else if (!server_ctx.document_root) { #if HAVE_REGEX LSQ_NOTICE("Document root is not set: start in Interop Mode"); diff --git a/bin/md5_client.c b/bin/md5_client.c index df9561e50..c70dba921 100644 --- a/bin/md5_client.c +++ b/bin/md5_client.c @@ -29,7 +29,8 @@ #include "test_common.h" #include "prog.h" -#include "../src/liblsquic/lsquic_logger.h" +#define TOOL_LOG_PREFIX "md5_client" +#include "tool_log.h" #include "../src/liblsquic/lsquic_int_types.h" #include "../src/liblsquic/lsquic_varint.h" #include "../src/liblsquic/lsquic_hq.h" diff --git a/bin/md5_server.c b/bin/md5_server.c index b72baff64..3b657012a 100644 --- a/bin/md5_server.c +++ b/bin/md5_server.c @@ -23,7 +23,8 @@ #include "test_cert.h" #include "prog.h" -#include "../src/liblsquic/lsquic_logger.h" +#define TOOL_LOG_PREFIX "md5_server" +#include "tool_log.h" static int g_really_calculate_md5 = 1; diff --git a/bin/perf_client.c b/bin/perf_client.c index f434cd427..af22e644d 100644 --- a/bin/perf_client.c +++ b/bin/perf_client.c @@ -30,7 +30,8 @@ #include "test_common.h" #include "prog.h" -#include "../src/liblsquic/lsquic_logger.h" +#define TOOL_LOG_PREFIX "perf_client" +#include "tool_log.h" #include "../src/liblsquic/lsquic_byteswap.h" struct scenario diff --git a/bin/perf_server.c b/bin/perf_server.c index addf84caa..816f58c7d 100644 --- a/bin/perf_server.c +++ b/bin/perf_server.c @@ -30,7 +30,8 @@ #include "prog.h" #include "../src/liblsquic/lsquic_byteswap.h" -#include "../src/liblsquic/lsquic_logger.h" +#define TOOL_LOG_PREFIX "perf_server" +#include "tool_log.h" static uint64_t s_max_pacing_rate = 0; diff --git a/bin/prog.c b/bin/prog.c index 0facdcdd4..4f45e2af0 100644 --- a/bin/prog.c +++ b/bin/prog.c @@ -30,7 +30,8 @@ #include "../src/liblsquic/lsquic_hash.h" #include "../src/liblsquic/lsquic_int_types.h" #include "../src/liblsquic/lsquic_util.h" -#include "../src/liblsquic/lsquic_logger.h" +#define TOOL_LOG_PREFIX "prog" +#include "tool_log.h" #include "test_config.h" #include "test_cert.h" @@ -220,6 +221,11 @@ prog_print_common_options (const struct prog *prog, FILE *out) fprintf(out, " -k Connect UDP socket. Only meant to be used with clients\n" " to pick up ICMP errors.\n" +" -o NAME=VAL Set engine option. Can be specified more than once.\n" +" Write scheduler options:\n" +" write_sched_strategy=0|1 # 0=fixed, 1=DRR\n" +" write_datagram_prio=0..4\n" +" write_datagram_share=0.0..1.0\n" " -i USECS Clock granularity in microseconds. Defaults to %u.\n", LSQUIC_DF_CLOCK_GRANULARITY ); diff --git a/bin/test_cert.c b/bin/test_cert.c index 8fbf4ac59..40aaa9839 100644 --- a/bin/test_cert.c +++ b/bin/test_cert.c @@ -11,7 +11,8 @@ #include "lsquic_types.h" #include "lsquic.h" -#include "../src/liblsquic/lsquic_logger.h" +#define TOOL_LOG_PREFIX "test_cert" +#include "tool_log.h" #include "../src/liblsquic/lsquic_hash.h" #include "test_cert.h" diff --git a/bin/test_common.c b/bin/test_common.c index 20c037626..53c6bb456 100644 --- a/bin/test_common.c +++ b/bin/test_common.c @@ -46,7 +46,8 @@ #include "prog.h" #include "lsxpack_header.h" -#include "../src/liblsquic/lsquic_logger.h" +#define TOOL_LOG_PREFIX "test_common" +#include "tool_log.h" #define MAX(a, b) ((a) > (b) ? (a) : (b)) #define MIN(a, b) ((a) < (b) ? (a) : (b)) @@ -2211,7 +2212,24 @@ set_engine_option (struct lsquic_engine_settings *settings, return 0; } break; + case 19: + if (0 == strncmp(name, "write_datagram_prio", 19)) + { + settings->es_write_datagram_prio = atoi(val); + return 0; + } + break; case 20: + if (0 == strncmp(name, "write_sched_strategy", 20)) + { + settings->es_write_sched_strategy = atoi(val); + return 0; + } + if (0 == strncmp(name, "write_datagram_share", 20)) + { + settings->es_write_datagram_share = atof(val); + return 0; + } if (0 == strncmp(name, "max_header_list_size", 20)) { settings->es_max_header_list_size = atoi(val); diff --git a/bin/test_common.h b/bin/test_common.h index 5306b9229..3f00ebeb6 100644 --- a/bin/test_common.h +++ b/bin/test_common.h @@ -6,6 +6,10 @@ #ifndef TEST_COMMON_H #define TEST_COMMON_H 1 +#ifndef WIN32 +# include +#endif + #if __linux__ # include /* For IFNAMSIZ */ #endif diff --git a/bin/tool_log.h b/bin/tool_log.h new file mode 100644 index 000000000..efc2f4283 --- /dev/null +++ b/bin/tool_log.h @@ -0,0 +1,58 @@ +#ifndef BIN_TOOL_LOG_H +#define BIN_TOOL_LOG_H + +#ifndef TOOL_LOG_PREFIX +#define TOOL_LOG_PREFIX "tool" +#endif + +#include +#include + +#include "../src/liblsquic/lsquic_logger.h" + + +static inline void +tool_log_prefixed (enum lsq_log_level level, const char *fmt, ...) +{ + char prefixed_fmt[2048]; + int off; + va_list ap; + + off = snprintf(prefixed_fmt, sizeof(prefixed_fmt), "%s: ", TOOL_LOG_PREFIX); + if (off < 0 || (size_t) off >= sizeof(prefixed_fmt)) + off = 0; + + if ((size_t) off < sizeof(prefixed_fmt)) + snprintf(prefixed_fmt + off, sizeof(prefixed_fmt) - (size_t) off, + "%s", fmt); + + va_start(ap, fmt); + lsquic_logger_log0v(level, prefixed_fmt, ap); + va_end(ap); +} + + +#define TOOL_LOG(level, ...) do { \ + if (LSQ_LOG_ENABLED_EXT(level, LSQLM_NOMODULE)) \ + tool_log_prefixed(level, __VA_ARGS__); \ +} while (0) + +#undef LSQ_DEBUG +#undef LSQ_INFO +#undef LSQ_NOTICE +#undef LSQ_WARN +#undef LSQ_ERROR +#undef LSQ_ALERT +#undef LSQ_CRIT +#undef LSQ_EMERG + +#define LSQ_DEBUG(...) TOOL_LOG(LSQ_LOG_DEBUG, __VA_ARGS__) +#define LSQ_INFO(...) TOOL_LOG(LSQ_LOG_INFO, __VA_ARGS__) +#define LSQ_NOTICE(...) TOOL_LOG(LSQ_LOG_NOTICE, __VA_ARGS__) +#define LSQ_WARN(...) TOOL_LOG(LSQ_LOG_WARN, __VA_ARGS__) +#define LSQ_ERROR(...) TOOL_LOG(LSQ_LOG_ERROR, __VA_ARGS__) +#define LSQ_ALERT(...) TOOL_LOG(LSQ_LOG_ALERT, __VA_ARGS__) +#define LSQ_CRIT(...) TOOL_LOG(LSQ_LOG_CRIT, __VA_ARGS__) +#define LSQ_EMERG(...) TOOL_LOG(LSQ_LOG_EMERG, __VA_ARGS__) + +#endif diff --git a/docs/apiref.rst b/docs/apiref.rst index 41d710515..1cb9967a2 100644 --- a/docs/apiref.rst +++ b/docs/apiref.rst @@ -304,6 +304,8 @@ optional members. .. _apiref-engine-settings: +.. _engine-settings: + Engine Settings --------------- @@ -848,10 +850,55 @@ settings structure: .. member:: int es_datagrams - Enable datagrams extension. Allowed values are 0 and 1. + Enable QUIC datagram extension (RFC 9221). Allowed values are 0 and 1. + + When enabled, provides connection-level unreliable datagram support via + :member:`lsquic_stream_if.on_datagram` and :member:`lsquic_stream_if.on_dg_write` + callbacks. This is independent of HTTP Datagrams (see :member:`es_http_datagrams`). Default value is :macro:`LSQUIC_DF_DATAGRAMS` + .. member:: int es_http_datagrams + + Enable HTTP Datagram support (RFC 9297) for HTTP/3. Allowed values are 0 and 1. + + When enabled, HTTP Datagrams can be sent via QUIC DATAGRAM frames or + Capsule Protocol. Provides stream-associated datagram support via + :member:`lsquic_stream_if.on_http_dg_write` and :member:`lsquic_stream_if.on_http_dg_read` + callbacks. + + This is independent of raw QUIC datagrams (:member:`es_datagrams`). You can + enable HTTP Datagrams without enabling raw QUIC datagrams; in that case, only + the Capsule Protocol will be used. + + See :ref:`apiref-http-datagrams` for complete documentation. + + Default value is :macro:`LSQUIC_DF_HTTP_DATAGRAMS` + + .. member:: unsigned es_http_dg_max_capsule_read_size + + Maximum buffer size for reading HTTP Datagram payloads via Capsule Protocol. + + This is a per-stream limit that applies only to capsule-based transport, + not QUIC DATAGRAM frames. A value of zero means capsule payloads will not + be accepted. + + See :ref:`apiref-http-datagrams` for details. + + Default value is :macro:`LSQUIC_DF_HTTP_DG_MAX_CAPSULE_READ_SIZE` (10KB) + + .. member:: unsigned es_http_dg_max_capsule_write_size + + Maximum buffer size for writing HTTP Datagram payloads via Capsule Protocol. + + This is a per-stream limit that applies only to capsule-based transport, + not QUIC DATAGRAM frames. A value of zero means capsule payloads will not + be sent. + + See :ref:`apiref-http-datagrams` for details. + + Default value is :macro:`LSQUIC_DF_HTTP_DG_MAX_CAPSULE_WRITE_SIZE` (10KB) + .. member:: int es_optimistic_nat If set to true, changes in peer port are assumed to be due to a @@ -917,6 +964,45 @@ settings structure: Default value is :macro:`LSQUIC_DF_CHECK_TP_SANITY` + .. member:: int es_webtransport + + Enable WebTransport support. Allowed values are 0 and 1. + + Default value is :macro:`LSQUIC_DF_WEBTRANSPORT_SERVER` + + .. member:: unsigned es_max_webtransport_sessions + + Maximum number of concurrent WebTransport sessions per connection. + + Current WebTransport support on this branch is limited to a single + session per connection while per-session WT flow control remains + deferred. + + Default value is :macro:`LSQUIC_DF_MAX_WEBTRANSPORT_SESSIONS` + + .. member:: unsigned es_write_sched_strategy + + Outbound write scheduler strategy. + + - 0: fixed priority class dispatch (:member:`LSQWSS_FIXED`) + - 1: deficit round robin class dispatch (:member:`LSQWSS_DRR`) + + Default value is :macro:`LSQUIC_DF_WRITE_SCHED_STRATEGY` + + .. member:: unsigned char es_write_datagram_prio + + Datagram class position in fixed scheduler mode. + Lower values are dispatched earlier. + + Default value is :macro:`LSQUIC_DF_WRITE_DATAGRAM_PRIO` + + .. member:: float es_write_datagram_share + + DATAGRAM share used by DRR scheduler. + Must be in [0.0, 1.0]. + + Default value is :macro:`LSQUIC_DF_WRITE_DATAGRAM_SHARE` + To initialize the settings structure to library defaults, use the following convenience function: @@ -1167,6 +1253,30 @@ out of date. Please check your :file:`lsquic.h` for actual values.* Transport parameter sanity checks are performed by default. +.. macro:: LSQUIC_DF_WEBTRANSPORT_SERVER + + WebTransport support is disabled by default. + +.. macro:: LSQUIC_DF_MAX_WEBTRANSPORT_SESSIONS + + Default max concurrent WebTransport sessions per connection is 1. + +.. macro:: LSQUIC_DF_WRITE_SCHED_STRATEGY + + Default write scheduler strategy is fixed priority. + +.. macro:: LSQUIC_DF_WRITE_DATAGRAM_PRIO + + Default datagram class position in fixed scheduler mode is 2. + +.. macro:: LSQUIC_WRITE_WEIGHT_MAX + + Internal DRR weight cap used to derive stream/datagram quanta. + +.. macro:: LSQUIC_DF_WRITE_DATAGRAM_SHARE + + Default DATAGRAM share in DRR mode. + Receiving Packets ----------------- @@ -1293,6 +1403,36 @@ Stream Callback Interface The stream callback interface structure lists the callbacks used by the engine to communicate with the user code: +.. type:: enum lsquic_http_cap_flags + + Effective HTTP capability flags delivered via + :member:`lsquic_stream_if.on_http_caps`. + + .. member:: LSQUIC_HTTP_CAP_DATAGRAMS + + HTTP Datagrams are negotiated and usable. + + .. member:: LSQUIC_HTTP_CAP_CONNECT_PROTOCOL + + Extended CONNECT is negotiated and usable. + + .. member:: LSQUIC_HTTP_CAP_WEBTRANSPORT + + WebTransport is negotiated and usable. + + This is a best-effort capability bit. It can be set in + compatibility mode for draft-14 peers, or for peers that negotiate + the transport pieces needed by the current implementation while + omitting ``reset_stream_at`` or WT initial flow-control settings. + +.. type:: struct lsquic_http_caps + + Effective HTTP capabilities after peer SETTINGS are processed. + + .. member:: uint32_t lhc_flags + + Bitmask of :type:`lsquic_http_cap_flags`. + .. type:: struct lsquic_stream_if .. member:: lsquic_conn_ctx_t *(*on_new_conn)(void *stream_if_ctx, lsquic_conn_t *) @@ -1371,6 +1511,13 @@ the engine to communicate with the user code: This callback is optional. + .. member:: void (*on_http_caps)(lsquic_conn_t *c, const struct lsquic_http_caps *caps) + + Called when peer HTTP SETTINGS are processed and effective HTTP + capabilities are known. + + This callback is optional. + .. member:: void (*on_goaway_received)(lsquic_conn_t *) This is called when our side received GOAWAY frame. After this, @@ -2139,6 +2286,18 @@ LSQUIC provides a mechanism to set and retrieve per-connection parameters at run These parameters allow fine-grained control over connection behavior beyond what is available through engine settings. +.. type:: enum lsquic_write_sched_strategy + + Write scheduler strategy. + + .. member:: LSQWSS_FIXED + + Fixed-priority class dispatch. + + .. member:: LSQWSS_DRR + + Deficit round-robin class dispatch. + .. type:: enum lsquic_conn_param Connection parameter identifiers for use with :func:`lsquic_conn_set_param()` @@ -2182,6 +2341,53 @@ available through engine settings. **Note:** :func:`lsquic_conn_get_info()` enables the sampler if needed. + .. member:: LSQCP_WT_PEER_SETTINGS_RECEIVED + + Whether peer HTTP/3 SETTINGS has been received. + + **Type:** ``uint64_t`` (0 or 1, get-only) + + .. member:: LSQCP_WT_PEER_SUPPORTS + + Whether peer currently satisfies WebTransport requirements. + + This reflects the implementation's effective policy, including the + compatibility mode described above. + + **Type:** ``uint64_t`` (0 or 1, get-only) + + .. member:: LSQCP_WT_PEER_DRAFT + + WebTransport draft version inferred from peer SETTINGS. + + **Type:** ``uint64_t`` (get-only) + + .. member:: LSQCP_WT_PEER_CONNECT_PROTOCOL + + Whether peer advertised ``SETTINGS_ENABLE_CONNECT_PROTOCOL=1``. + + **Type:** ``uint64_t`` (0 or 1, get-only) + + .. member:: LSQCP_WRITE_SCHED_STRATEGY + + Set or query write scheduler strategy at runtime. + + **Type:** ``enum lsquic_write_sched_strategy`` + + .. member:: LSQCP_WRITE_DATAGRAM_PRIO + + Set or query datagram class position in fixed scheduler mode. + + **Type:** ``unsigned`` + + **Note:** valid only when strategy is :member:`LSQWSS_FIXED`. + + .. member:: LSQCP_WRITE_DATAGRAM_SHARE + + Set or query DATAGRAM share for DRR mode. + + **Type:** ``float`` in [0.0, 1.0] + .. function:: int lsquic_conn_set_param (lsquic_conn_t *conn, enum lsquic_conn_param param, const void *value, size_t value_len) Set a connection parameter. @@ -2222,6 +2428,17 @@ available through engine settings. lsquic_conn_set_param(conn, LSQCP_ENABLE_BW_SAMPLER, &on, sizeof(on)); + **Example - Switching to DRR and setting DATAGRAM share:** + + :: + + enum lsquic_write_sched_strategy strategy = LSQWSS_DRR; + float dg_share = 0.30f; + lsquic_conn_set_param(conn, LSQCP_WRITE_SCHED_STRATEGY, + &strategy, sizeof(strategy)); + lsquic_conn_set_param(conn, LSQCP_WRITE_DATAGRAM_SHARE, + &dg_share, sizeof(dg_share)); + **Note:** For :member:`LSQCP_MAX_PACING_RATE`, pacing must be enabled via :member:`lsquic_engine_settings.es_pace_packets` for this parameter to have any effect. @@ -2253,6 +2470,16 @@ available through engine settings. printf("Max rate: %lu bytes/sec\\n", max_rate); } + **Example - Reading DATAGRAM share for DRR mode:** + + :: + + float dg_share = 0.f; + size_t len = sizeof(dg_share); + if (lsquic_conn_get_param(conn, LSQCP_WRITE_DATAGRAM_SHARE, + &dg_share, &len) == 0) + printf("Datagram share: %.2f\\n", dg_share); + Miscellaneous Stream Functions ------------------------------ @@ -2629,3 +2856,342 @@ and Set minimum datagram size. This is the minumum value of the buffer passed to the :member:`lsquic_stream_if.on_dg_write` callback. Returns 0 on success and -1 on error. + +.. _apiref-http-datagrams: + +HTTP Datagrams +-------------- + +lsquic supports `HTTP Datagrams (RFC 9297) `_ +for HTTP/3. HTTP Datagrams provide a mechanism for multiplexed, potentially unreliable +application data over HTTP connections. + +Overview +~~~~~~~~ + +HTTP Datagrams can be transported via two mechanisms: + +1. **QUIC DATAGRAM frames** (RFC 9221) - unreliable, unordered, low-latency delivery +2. **Capsule Protocol** (RFC 9297, Section 3) - reliable, ordered delivery over QUIC streams + +The library can automatically choose the appropriate transport based on negotiation +and payload size, or the application can explicitly control which mechanism to use. + +HTTP Datagrams are typically used with Extended CONNECT requests (RFC 8441 for HTTP/2, +RFC 9220 for HTTP/3) that establish a stream context. Each datagram is associated with +a specific stream via a context ID. + +Key Differences from QUIC Datagrams +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +LSQUIC supports both raw QUIC datagrams (:member:`lsquic_engine_settings.es_datagrams`) +and HTTP Datagrams (:member:`lsquic_engine_settings.es_http_datagrams`). These are +distinct features: + +- **QUIC DATAGRAM extension datagrams** are connection-level, unreliable datagrams without stream association +- **HTTP Datagrams** are stream-associated and can use either QUIC DATAGRAM frames or + the Capsule Protocol for transport + +You can enable HTTP Datagrams without enabling raw QUIC datagrams. In this case, only +the Capsule Protocol will be used for HTTP Datagram transport. + +Configuration +~~~~~~~~~~~~~ + +Enable HTTP Datagram support by setting :member:`lsquic_engine_settings.es_http_datagrams` +to 1. You can also configure buffer sizes for capsule-based transport: + +- :member:`lsquic_engine_settings.es_http_dg_max_capsule_read_size` +- :member:`lsquic_engine_settings.es_http_dg_max_capsule_write_size` + +See the :ref:`Engine Settings ` section for details on these +parameters. + +Provide callbacks for sending and receiving HTTP Datagrams: + +- :member:`lsquic_stream_if.on_http_dg_write` - called when ready to send +- :member:`lsquic_stream_if.on_http_dg_read` - called when datagram received + +Send Modes +~~~~~~~~~~ + +HTTP Datagrams can be sent using three modes (``enum lsquic_http_dg_send_mode``): + +**LSQUIC_HTTP_DG_SEND_DEFAULT** - Automatic mode (recommended) + +The library chooses the transport automatically: + +- Uses QUIC DATAGRAM frames if negotiated and payload fits within MTU +- Falls back to Capsule Protocol for oversized payloads or if QUIC datagrams unavailable + +This mode provides the best balance of performance and reliability. + +**LSQUIC_HTTP_DG_SEND_DATAGRAM** - QUIC DATAGRAM only + +Forces use of QUIC DATAGRAM frames: + +- Fails if QUIC datagrams were not negotiated +- Fails if payload exceeds ``max_quic_payload`` +- Provides UDP-like semantics: unreliable, unordered, low latency +- No head-of-line blocking + +Use when you need unreliable delivery and can handle packet loss. + +**LSQUIC_HTTP_DG_SEND_CAPSULE** - Capsule Protocol only + +Forces use of capsule-based transport over QUIC streams: + +- Provides TCP-like semantics: reliable, ordered delivery +- Subject to head-of-line blocking within the stream +- Subject to stream flow control and :member:`es_http_dg_max_capsule_write_size` + +Use when reliability is required or for testing capsule implementations. + +Sending HTTP Datagrams +~~~~~~~~~~~~~~~~~~~~~~~ + +To send HTTP Datagrams: + +1. Call :func:`lsquic_stream_want_http_dg_write` with ``is_want=1`` to indicate + readiness to send +2. Library invokes :member:`lsquic_stream_if.on_http_dg_write` callback +3. In callback, call the ``consume_datagram`` function pointer once with your payload + +.. function:: int lsquic_stream_want_http_dg_write (lsquic_stream_t *stream, int is_want) + + Control whether the stream is eligible to supply HTTP Datagram payloads. + + :param stream: Stream from Extended CONNECT request + :param is_want: 1 to enable write readiness, 0 to disable + :return: Previous value of the flag, or -1 on error + + When enabled, the library will invoke :member:`lsquic_stream_if.on_http_dg_write` + when ready to send a datagram. Disable after sending to avoid unnecessary callbacks. + +**Callback signature:** + +.. c:member:: int (*on_http_dg_write)(lsquic_stream_t *s, lsquic_stream_ctx_t *h, size_t max_quic_payload, lsquic_http_dg_consume_f consume_datagram) + + Called when HTTP Datagram is ready to be written. + + :param s: Stream context (from Extended CONNECT) + :param h: User stream context (from :member:`on_new_stream`) + :param max_quic_payload: Maximum payload size for QUIC DATAGRAM frame. + Payloads larger than this will automatically use Capsule Protocol + when ``LSQUIC_HTTP_DG_SEND_DEFAULT`` is used. This value may change + due to PMTU changes or stream ID growth. + :param consume_datagram: Function to call with payload data. Must be + called exactly once per callback invocation. + :return: 0 on success, -1 on error + + **Important:** Call ``consume_datagram`` exactly once with your payload data. + If you have no data to send, return -1 without calling ``consume_datagram``. + + The ``consume_datagram`` function has this signature:: + + typedef int (*lsquic_http_dg_consume_f)( + lsquic_stream_t *stream, + const void *buf, /* payload data */ + size_t sz, /* payload size */ + enum lsquic_http_dg_send_mode mode); + + It returns 0 on success, -1 on failure (e.g., buffer full, negotiation failed). + +**Example:** + +.. code-block:: c + + static int + my_on_http_dg_write(lsquic_stream_t *stream, lsquic_stream_ctx_t *ctx, + size_t max_quic_payload, + lsquic_http_dg_consume_f consume) + { + unsigned char payload[] = "Hello via HTTP Datagram"; + + /* Use automatic mode - library chooses transport */ + if (0 != consume(stream, payload, sizeof(payload) - 1, + LSQUIC_HTTP_DG_SEND_DEFAULT)) + return -1; /* Send failed */ + + /* Disable further write callbacks until we have more data */ + lsquic_stream_want_http_dg_write(stream, 0); + return 0; + } + +Receiving HTTP Datagrams +~~~~~~~~~~~~~~~~~~~~~~~~~ + +To receive HTTP Datagrams: + +1. After successful Extended CONNECT response, call + :func:`lsquic_stream_set_http_dg_capsules` with ``enable=1`` +2. Library parses incoming capsule datagrams and invokes + :member:`lsquic_stream_if.on_http_dg_read` callback + +.. function:: int lsquic_stream_set_http_dg_capsules (lsquic_stream_t *stream, int enable) + + Enable or disable HTTP Datagram capsule processing on this stream. + + :param stream: Stream from Extended CONNECT request + :param enable: 1 to enable, 0 to disable + :return: 0 on success, -1 on error (sets errno) + + When enabled, the library: + + - Takes over the stream's :member:`on_read` callback to parse capsule framing + - Delivers capsule-carried HTTP Datagram payloads via :member:`on_http_dg_read` + - May suppress :member:`on_write` callbacks while flushing pending capsules + + Call this function after receiving a successful Extended CONNECT response + that includes the ``Capsule-Protocol: ?1`` header. + + **Note:** Calling this function is required to receive capsule-carried + HTTP Datagrams; QUIC DATAGRAM frame delivery does not depend on it. + +**Callback signature:** + +.. c:member:: void (*on_http_dg_read)(lsquic_stream_t *s, lsquic_stream_ctx_t *h, const void *buf, size_t sz) + + Called when an HTTP Datagram payload is received. + + :param s: Stream context + :param h: User stream context + :param buf: Pointer to datagram payload data + :param sz: Size of payload in bytes + + For QUIC DATAGRAM frames, this callback can be invoked during + :func:`lsquic_engine_packet_in`. For capsule-carried datagrams, it is + invoked during stream read dispatch. The buffer is only valid during the + callback; copy the data if you need it after the callback returns. + + This callback is optional. If not set, received HTTP Datagrams are silently + discarded. + +**Example:** + +.. code-block:: c + + static void + my_on_http_dg_read(lsquic_stream_t *stream, lsquic_stream_ctx_t *ctx, + const void *buf, size_t bufsz) + { + /* Process payload quickly - we're in packet processing */ + printf("Received HTTP Datagram: %zu bytes\n", bufsz); + + /* Copy data if needed after callback returns */ + if (ctx->need_to_save) { + ctx->saved = malloc(bufsz); + if (ctx->saved) + memcpy(ctx->saved, buf, bufsz); + } + } + +Payload Size Limits +~~~~~~~~~~~~~~~~~~~ + +.. function:: size_t lsquic_stream_get_max_http_dg_size (lsquic_stream_t *stream) + + Get maximum HTTP Datagram payload size for QUIC DATAGRAM frames. + + :param stream: Stream with HTTP Datagram context + :return: Maximum payload size in bytes, or 0 if HTTP Datagrams not negotiated + + This returns the maximum payload that can be sent via QUIC DATAGRAM frames + on this stream. The size accounts for: + + - QUIC DATAGRAM frame overhead + - HTTP Datagram context ID overhead (Quarter Stream ID encoding) + - Current path MTU + + **Returns 0 if:** + + - HTTP Datagrams were not negotiated during handshake + - QUIC datagrams are not available + + Use this to check if HTTP Datagrams are available, and to determine if + your payload will fit in a QUIC DATAGRAM frame. + + **Note:** This value may change during the connection due to PMTU changes + or stream ID growth (which affects Quarter Stream ID encoding size). + Check before each send, or use ``LSQUIC_HTTP_DG_SEND_DEFAULT`` to let the + library handle size-based fallback automatically. + + Payloads larger than this value must be sent via Capsule Protocol, which + is subject to stream flow control and :member:`es_http_dg_max_capsule_write_size`. + +Extended CONNECT Setup +~~~~~~~~~~~~~~~~~~~~~~ + +HTTP Datagrams require an Extended CONNECT request with specific headers: + +**Client side:** + +.. code-block:: c + + struct lsxpack_header headers[] = { + {":method", "CONNECT"}, + {":protocol", "your-protocol"}, /* e.g., "webtransport" or "bat-00" */ + {":scheme", "https"}, + {":path", "/your-path"}, + {":authority", "example.com"}, + {"capsule-protocol", "?1"}, /* Required for Capsule Protocol */ + }; + + lsquic_stream_send_headers(stream, &headers, 0); /* fin=0 */ + +**Server side:** + +Validate the request headers and respond with 200: + +.. code-block:: c + + /* Validate :method is CONNECT, :protocol matches, capsule-protocol is ?1 */ + + struct lsxpack_header response[] = { + {":status", "200"}, + }; + + lsquic_stream_send_headers(stream, &response, 0); /* fin=0 */ + + /* Enable capsule processing */ + lsquic_stream_set_http_dg_capsules(stream, 1); + +The ``Capsule-Protocol: ?1`` header (RFC 9297) indicates support for the +Capsule Protocol. Some protocols require it even when primarily using +QUIC DATAGRAM frames; check your protocol specification. + +Complete Example +~~~~~~~~~~~~~~~~ + +See the BAT (Bidirectional Attestation Test) examples in ``bin/bat_client.c`` +and ``bin/bat_server.c`` for complete, working implementations. BAT is a simple +echo protocol defined in ``docs/draft-tikhonov-httpbis-bat-00.txt`` specifically +for testing HTTP Datagram implementations. + +Troubleshooting +~~~~~~~~~~~~~~~ + +**Datagrams not being sent:** + +- Check that :member:`es_http_datagrams` is enabled +- Verify :func:`lsquic_stream_get_max_http_dg_size` returns non-zero +- Ensure you called :func:`lsquic_stream_want_http_dg_write` +- Check that Extended CONNECT succeeded with 200 response + +**Datagrams not being received:** + +- If using capsule fallback, verify :func:`lsquic_stream_set_http_dg_capsules` was called +- Check that :member:`on_http_dg_read` callback is set +- Ensure ``Capsule-Protocol: ?1`` header was included in CONNECT request + +**Payload size errors:** + +- Query :func:`lsquic_stream_get_max_http_dg_size` for current limit +- Use ``LSQUIC_HTTP_DG_SEND_DEFAULT`` for automatic capsule fallback +- Increase :member:`es_http_dg_max_capsule_write_size` if using large capsules + +**Performance issues:** + +- Use ``LSQUIC_HTTP_DG_SEND_DATAGRAM`` for latency-sensitive, loss-tolerant data +- Use ``LSQUIC_HTTP_DG_SEND_CAPSULE`` for data that must be reliably delivered +- Be aware capsules can cause head-of-line blocking on their stream diff --git a/docs/draft-frindell-webtrans-devious-baton-00.txt b/docs/draft-frindell-webtrans-devious-baton-00.txt new file mode 100644 index 000000000..5899c2240 --- /dev/null +++ b/docs/draft-frindell-webtrans-devious-baton-00.txt @@ -0,0 +1,448 @@ + + + + +WebTransport A. Frindell +Internet-Draft Meta +Intended status: Informational 10 July 2023 +Expires: 11 January 2024 + + + Devious Baton Protocol for Exercising WebTransport + draft-frindell-webtrans-devious-baton-00 + +Abstract + + This document describes a simple protocol that can be used to + exercise the functionality provided by WebTransport. The protocol + passes a "baton" between endpoints, using both unidirectional and + bidirectional streams. + +About This Document + + This note is to be removed before publishing as an RFC. + + The latest revision of this draft can be found at + https://afrind.github.io/draft-frindell-webtrans-devious-baton/draft- + frindell-webtrans-devious-baton.html. Status information for this + document may be found at https://datatracker.ietf.org/doc/draft- + frindell-webtrans-devious-baton/. + + Discussion of this document takes place on the WebTransport Working + Group mailing list (mailto:webtransport@ietf.org), which is archived + at https://mailarchive.ietf.org/arch/browse/webtransport/. Subscribe + at https://www.ietf.org/mailman/listinfo/webtransport/. + + Source for this draft and an issue tracker can be found at + https://github.com/afrind/draft-frindell-webtrans-devious-baton. + +Status of This Memo + + This Internet-Draft is submitted in full conformance with the + provisions of BCP 78 and BCP 79. + + Internet-Drafts are working documents of the Internet Engineering + Task Force (IETF). Note that other groups may also distribute + working documents as Internet-Drafts. The list of current Internet- + Drafts is at https://datatracker.ietf.org/drafts/current/. + + Internet-Drafts are draft documents valid for a maximum of six months + and may be updated, replaced, or obsoleted by other documents at any + time. It is inappropriate to use Internet-Drafts as reference + material or to cite them other than as "work in progress." + + + +Frindell Expires 11 January 2024 [Page 1] + +Internet-Draft devious-baton July 2023 + + + This Internet-Draft will expire on 11 January 2024. + +Copyright Notice + + Copyright (c) 2023 IETF Trust and the persons identified as the + document authors. All rights reserved. + + This document is subject to BCP 78 and the IETF Trust's Legal + Provisions Relating to IETF Documents (https://trustee.ietf.org/ + license-info) in effect on the date of publication of this document. + Please review these documents carefully, as they describe your rights + and restrictions with respect to this document. Code Components + extracted from this document must include Revised BSD License text as + described in Section 4.e of the Trust Legal Provisions and are + provided without warranty as described in the Revised BSD License. + +Table of Contents + + 1. Introduction . . . . . . . . . . . . . . . . . . . . . . . . 2 + 2. Conventions and Definitions . . . . . . . . . . . . . . . . . 3 + 3. Session Establishment . . . . . . . . . . . . . . . . . . . . 3 + 3.1. Query Parameters . . . . . . . . . . . . . . . . . . . . 3 + 3.2. Protocol Version . . . . . . . . . . . . . . . . . . . . 4 + 4. Protocol Behavior . . . . . . . . . . . . . . . . . . . . . . 4 + 4.1. Setup . . . . . . . . . . . . . . . . . . . . . . . . . . 4 + 4.2. Processing a Baton Message . . . . . . . . . . . . . . . 4 + 4.3. Baton message . . . . . . . . . . . . . . . . . . . . . . 5 + 4.4. Datagrams . . . . . . . . . . . . . . . . . . . . . . . . 6 + 4.5. Session Closure . . . . . . . . . . . . . . . . . . . . . 6 + 4.6. Error Handling . . . . . . . . . . . . . . . . . . . . . 6 + 4.6.1. Stream Error Codes . . . . . . . . . . . . . . . . . 6 + 4.6.2. Session Error Codes . . . . . . . . . . . . . . . . . 7 + 5. Security Considerations . . . . . . . . . . . . . . . . . . . 7 + 6. IANA Considerations . . . . . . . . . . . . . . . . . . . . . 7 + 7. Normative References . . . . . . . . . . . . . . . . . . . . 7 + Acknowledgments . . . . . . . . . . . . . . . . . . . . . . . . . 8 + Author's Address . . . . . . . . . . . . . . . . . . . . . . . . 8 + +1. Introduction + + WebTransport offers applications the ability to send and receive data + over bidirectional and unidirectional streams, as well as send and + received datagrams. The Devious Baton protocol is an application + that can be used to test the full suite of functionality in a + WebTransport implementation and demonstrate interoperability. + + + + + + +Frindell Expires 11 January 2024 [Page 2] + +Internet-Draft devious-baton July 2023 + + + The protocol works by passing a "baton" -- a one byte integer -- + between endpoints using streams. A receiving endpoint increments the + baton value modulo 256 and sends it to the peer until the baton's + value reaches 0. + +2. Conventions and Definitions + + The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", + "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and + "OPTIONAL" in this document are to be interpreted as described in + BCP 14 [RFC2119] [RFC8174] when, and only when, they appear in all + capitals, as shown here. + + Client: The endpoint that initiates the WebTransport session + + Server: The endpoint that did not initiate the WebTransport session + + Devious Baton Session: A single WebTransport session initiated as + described in Section 3 + +3. Session Establishment + + The client initiates a WebTransport session as defined in [OVERVIEW]. + The protocol can be used by any endpoint, but for interoperability it + is RECOMMENDED that the URL path be /webtransport/devious-baton. + +3.1. Query Parameters + + The behavior of the protocol can be configured by parameters + indicated by the client. These parameters are transmitted in query + parameters in the session establishment URL. Sending parameters is + optional but omission of a parameter requires the server to interpret + that as the default value. + + The server MUST support the following optional query parameters: + + * version - an integer specifying the draft version of Devious Baton + the client intends to use + + If the version is invalid or the server does not support the + specified version, it MUST reject the WebTransport session with a 4xx + status code. The default value is 0. + + * baton - an integer between 1 and 255, inclusive, which the server + will use as the initial baton value for all batons. + + + + + + +Frindell Expires 11 January 2024 [Page 3] + +Internet-Draft devious-baton July 2023 + + + If the baton value is invalid, the server MUST reject the + WebTransport session with a 4xx status code. There is no default - + if unspecified the server chooses a random baton value between 1 and + 255, inclusive. + + * count - a positive integer specifying how many batons will be sent + in parallel + + The default value is 1. If the client asks for more batons than the + server is capable of sending, the server MUST reject the WebTransport + session with a 4xx status code. + +3.2. Protocol Version + + This draft defines Devious Baton protocol version 0. + +4. Protocol Behavior + +4.1. Setup + + Upon successful negotiation of a WebTransport session to the Devious + Baton endpoint, the server opens a unidirectional stream for each + baton. If there is insufficient stream credit to open a + unidirectional stream, the server MUST close the WebTransport session + with the DA_YAMN session error code. The server sends a Baton + message with the initial baton value on each stream and closes it. + +4.2. Processing a Baton Message + + When either endpoint receives a Baton message on a stream, it takes + the following actions: + + * If the value of the baton is 0, the endpoint decrements the number + of active batons by one. + + * If the value of the baton is not 0, the endpoint MUST send a new + Baton message with a baton value equal to the incoming baton value + + 1 modulo 256. The new Baton message is sent on a stream, + decribed below. + + * After sending the Baton message, the endpoint MUST send a FIN on + the stream. + + The endpoint selects the outgoing Baton message stream based on how + the incoming Baton message arrived. + + + + + + +Frindell Expires 11 January 2024 [Page 4] + +Internet-Draft devious-baton July 2023 + + + * If the incoming Baton message arrived on a unidirectional stream, + the endpoint opens a bidirectional stream and sends the outgoing + Baton message on it. + + * If the Baton message arrived on a peer-initiated bidirectional + stream, the endpoint sends the outgoing Baton message on that + stream. + + * If the Baton message arrived on a self-initiated bidirectional + stream, the endpoint opens a unidirectional stream and sends the + outgoing Baton message on it. + + If an endpoint receives a baton message with an unexpected value, it + MAY close the WebTransport session with the SUS session error code. + + If the endpoint has insufficient stream credit to open the correct + type of stream, it MUST close the WebTransport session with the + DA_YAMN session error code. + + If the endpoint has insufficient flow control credit to send the + Baton message, it SHOULD send as much as limits allow, and wait for + additional credit. The endpoint SHOULD close the WebTransport + session with the BORED session error code if the peer takes too long + to grant credit. + +4.3. Baton message + + Baton Message { + padding length(i) + padding(...) + baton(1) + } + + To allow for exercising of long streams and flow control, the Baton + message begins with an aribtrary amount of padding. padding length + specifies the number of bytes of padding. The padding field contains + padding length octets of padding. The receiver ignores the bytes + themselves so they can be any value, for example 0 or random data. + + baton contains the current value of the baton. It is a single byte + to enforce the modulo 256 arithmetic. + + + + + + + + + + +Frindell Expires 11 January 2024 [Page 5] + +Internet-Draft devious-baton July 2023 + + +4.4. Datagrams + + When a client endpoint receives a Baton message with a baton value = + 1 modulo 7, it sends a datagram with an identical Baton message. + When a server endpoint receives a Baton message with a baton value = + 0 modulo 7, it sends a datagram with an identical Baton message. + Note that a Baton message in a datagram MUST use a padding value + small enough such that the entire Baton message fits in a single + datagram. + +4.5. Session Closure + + Each endpoint tracks the number of active batons. It is initally + equal to the client's count parameter. Each time a baton exchange + completes or is reset, the number of active batons is decreased by 1. + When the number of active batons reaches 0, the endpoint MUST close + the WebTransport session with no error. + + To close a Devious Baton Session with an error, the endpoint + initiating the close sends a CLOSE_WEBTRANSPORT_SESSION capsule with + the specified session error code. To close the session without an + error, the endpoint initiating the close sends a FIN on the CONNECT + stream. + +4.6. Error Handling + + If an endpoint receives a gracefully closed stream or datagram with + an incomplete Baton message, it MUST close the WebTransport session + with the BRUH session error code. + + Either endpoint can send a STOP_SENDING or RESET_STREAM on an open + stream. STOP_SENDING MUST use the IDC stream error code. Upon + receipt of a STOP_SENDING on a stream, or a RESET_STREAM on a + bidirectional stream, the endpoint MUST send a RESET_STREAM for that + stream with the WHATEVER stream error code unless it has already + closed the stream. A RESET_STREAM sent spontaneously MUST use the + I_LIED stream error code. + + If an endpoint gets tired of waiting for the next Baton message, it + MAY close the WebTransport session with the BORED error code. + +4.6.1. Stream Error Codes + + The following error codes can be sent in RESET_STREAM and + STOP_SENDING frames. + + + + + + +Frindell Expires 11 January 2024 [Page 6] + +Internet-Draft devious-baton July 2023 + + + +==========+======+================================+ + | Name | Code | Description | + +==========+======+================================+ + | IDC | 0x01 | I don't care about this stream | + +----------+------+--------------------------------+ + | WHATEVER | 0x02 | The peer asked for this | + +----------+------+--------------------------------+ + | I_LIED | 0x03 | Spontaneous reset | + +----------+------+--------------------------------+ + + Table 1: Stream Error Codes + +4.6.2. Session Error Codes + + The following error codes can be sent in the + CLOSE_WEBTRANSPORT_SESSION capsule. + + +=========+======+=================================+ + | Name | Code | Description | + +=========+======+=================================+ + | DA_YAMN | 0x01 | There is insufficient stream | + | | | credit to continue the protocol | + +---------+------+---------------------------------+ + | BRUH | 0x02 | Received a malformed Baton | + | | | message | + +---------+------+---------------------------------+ + | SUS | 0x03 | Received an unexpected Baton | + | | | message | + +---------+------+---------------------------------+ + | BORED | 0x04 | Got tired of waiting for the | + | | | next message | + +---------+------+---------------------------------+ + + Table 2: Session Error Codes + +5. Security Considerations + + There are not believed to be any further security considerations + beyond those presented in QUIC Transport. + +6. IANA Considerations + + This document has no IANA actions. + +7. Normative References + + + + + + +Frindell Expires 11 January 2024 [Page 7] + +Internet-Draft devious-baton July 2023 + + + [OVERVIEW] Vasiliev, V., "The WebTransport Protocol Framework", Work + in Progress, Internet-Draft, draft-ietf-webtrans-overview- + 05, 24 January 2023, + . + + [RFC2119] Bradner, S., "Key words for use in RFCs to Indicate + Requirement Levels", BCP 14, RFC 2119, + DOI 10.17487/RFC2119, March 1997, + . + + [RFC8174] Leiba, B., "Ambiguity of Uppercase vs Lowercase in RFC + 2119 Key Words", BCP 14, RFC 8174, DOI 10.17487/RFC8174, + May 2017, . + +Acknowledgments + + Martin Thomson, Christian Huitema and Lucas Pardue contributed ideas + to this protocol. David Schinazi suggested the name Devious Baton. + + Error code naming inspiration by middle schoolers everywhere, but + specifically James Frindell. + +Author's Address + + Alan Frindell + Meta + Email: afrind@meta.com + + + + + + + + + + + + + + + + + + + + + + + +Frindell Expires 11 January 2024 [Page 8] diff --git a/docs/draft-ietf-quic-reliable-stream-reset-07.txt b/docs/draft-ietf-quic-reliable-stream-reset-07.txt new file mode 100644 index 000000000..e20e8498a --- /dev/null +++ b/docs/draft-ietf-quic-reliable-stream-reset-07.txt @@ -0,0 +1,504 @@ + + + + +QUIC M. Seemann +Internet-Draft +Intended status: Standards Track 奥一穂 (K. Oku) +Expires: 17 December 2025 Fastly + 15 June 2025 + + + QUIC Stream Resets with Partial Delivery + draft-ietf-quic-reliable-stream-reset-07 + +Abstract + + QUIC defines a RESET_STREAM frame to abort sending on a stream. When + a sender resets a stream, it also stops retransmitting STREAM frames + for this stream in the event of packet loss. On the receiving side, + there is no guarantee that any data sent on that stream is delivered. + + This document defines a new QUIC frame, the RESET_STREAM_AT frame, + that allows resetting a stream, while guaranteeing delivery of stream + data up to a certain byte offset. + +About This Document + + This note is to be removed before publishing as an RFC. + + The latest revision of this draft can be found at + https://quicwg.github.io/reliable-stream-reset/draft-ietf-quic- + reliable-stream-reset.html. Status information for this document may + be found at https://datatracker.ietf.org/doc/draft-ietf-quic- + reliable-stream-reset/. + + Discussion of this document takes place on the QUIC Working Group + mailing list (mailto:quic@ietf.org), which is archived at + https://mailarchive.ietf.org/arch/browse/quic/. Subscribe at + https://www.ietf.org/mailman/listinfo/quic/. + + Source for this draft and an issue tracker can be found at + https://github.com/quicwg/reliable-stream-reset. + +Status of This Memo + + This Internet-Draft is submitted in full conformance with the + provisions of BCP 78 and BCP 79. + + Internet-Drafts are working documents of the Internet Engineering + Task Force (IETF). Note that other groups may also distribute + working documents as Internet-Drafts. The list of current Internet- + Drafts is at https://datatracker.ietf.org/drafts/current/. + + + +Seemann & Oku Expires 17 December 2025 [Page 1] + +Internet-Draft QUIC Stream Resets with Partial Delivery June 2025 + + + Internet-Drafts are draft documents valid for a maximum of six months + and may be updated, replaced, or obsoleted by other documents at any + time. It is inappropriate to use Internet-Drafts as reference + material or to cite them other than as "work in progress." + + This Internet-Draft will expire on 17 December 2025. + +Copyright Notice + + Copyright (c) 2025 IETF Trust and the persons identified as the + document authors. All rights reserved. + + This document is subject to BCP 78 and the IETF Trust's Legal + Provisions Relating to IETF Documents (https://trustee.ietf.org/ + license-info) in effect on the date of publication of this document. + Please review these documents carefully, as they describe your rights + and restrictions with respect to this document. Code Components + extracted from this document must include Revised BSD License text as + described in Section 4.e of the Trust Legal Provisions and are + provided without warranty as described in the Revised BSD License. + +Table of Contents + + 1. Introduction . . . . . . . . . . . . . . . . . . . . . . . . 3 + 2. Conventions and Definitions . . . . . . . . . . . . . . . . . 3 + 3. Transport Parameter . . . . . . . . . . . . . . . . . . . . . 4 + 4. RESET_STREAM_AT Frame . . . . . . . . . . . . . . . . . . . . 4 + 5. Resetting Streams . . . . . . . . . . . . . . . . . . . . . . 5 + 5.1. Sending RESET_STREAM_AT after FIN . . . . . . . . . . . . 5 + 5.2. Multiple RESET_STREAM_AT / RESET_STREAM frames . . . . . 6 + 5.3. Stream States . . . . . . . . . . . . . . . . . . . . . . 6 + 6. Implementation Guidance . . . . . . . . . . . . . . . . . . . 7 + 7. Security Considerations . . . . . . . . . . . . . . . . . . . 7 + 8. IANA Considerations . . . . . . . . . . . . . . . . . . . . . 7 + 8.1. QUIC Transport Parameter . . . . . . . . . . . . . . . . 8 + 8.2. QUIC Frame Types . . . . . . . . . . . . . . . . . . . . 8 + 9. References . . . . . . . . . . . . . . . . . . . . . . . . . 8 + 9.1. Normative References . . . . . . . . . . . . . . . . . . 8 + 9.2. Informative References . . . . . . . . . . . . . . . . . 9 + Acknowledgments . . . . . . . . . . . . . . . . . . . . . . . . . 9 + Authors' Addresses . . . . . . . . . . . . . . . . . . . . . . . 9 + + + + + + + + + + +Seemann & Oku Expires 17 December 2025 [Page 2] + +Internet-Draft QUIC Stream Resets with Partial Delivery June 2025 + + +1. Introduction + + QUIC version 1 ([RFC9000]) allows streams to be reset. When a stream + is reset, the sender doesn't retransmit stream data for the + respective stream. On the receiving side, the QUIC stack is free to + surface the stream reset to the application immediately, without + providing any stream data it has received for that stream. + + Some applications running on top of QUIC use bytes at the beginning + of the stream to communicate critical information related to that + stream. For example, WebTransport ([WEBTRANSPORT]) uses a variable- + length encoded integer to associate a stream with a particular + WebTransport session. + + Since QUIC does not provide guaranteed delivery of steam data for + reset streams, it is possible that a receiver is unable to read + critical information. In the example above, a reset stream can cause + the receiver to fail to associate incoming streams with their + respective subcomponent of the application. Therefore, it is + desirable that the receiver can rely on the delivery of critical + information to applications, even if the QUIC stream is reset before + data is read by the application. + + Another use case is relaying data from an external data source. When + a relay is sending data being read from an external source and + encounters an error, it might want to use a stream reset to signal + that error, while at the same time guaranteeing that all data + received from the source is delivered to the peer. + + This document extends QUIC with a variant of stream resets that + reliably delivers the beginning of a stream up to a sender-specified + offset, communicated using the RESET_STREAM_AT frame. It can be + considered a form of range-based partial reliability. As a variant + of reset, application protocols continue to treat this stream + function as an abrupt termination; see Section 2.4 of [RFC9000]. + +2. Conventions and Definitions + + The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", + "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and + "OPTIONAL" in this document are to be interpreted as described in + BCP 14 [RFC2119] [RFC8174] when, and only when, they appear in all + capitals, as shown here. + + + + + + + + +Seemann & Oku Expires 17 December 2025 [Page 3] + +Internet-Draft QUIC Stream Resets with Partial Delivery June 2025 + + +3. Transport Parameter + + Support for receiving RESET_STREAM_AT frames is advertised by sending + the reset_stream_at (0x17f7586d2cb571) transport parameter + (Section 7.4 of [RFC9000]) with an empty value. An implementation + that understands this transport parameter MUST treat the receipt of a + non-empty value as a connection error of type + TRANSPORT_PARAMETER_ERROR. + + When using 0-RTT, both endpoints MUST remember the value of this + transport parameter. This allows use of this extension in 0-RTT + packets. When the server accepts 0-RTT data, the server MUST NOT + disable this extension on the resumed connection. + +4. RESET_STREAM_AT Frame + + Conceptually, the RESET_STREAM_AT frame is a RESET_STREAM frame with + an added Reliable Size field. + + RESET_STREAM_AT Frame { + Type (i) = 0x24, + Stream ID (i), + Application Protocol Error Code (i), + Final Size (i), + Reliable Size (i), + } + + Figure 1: RESET_STREAM_AT Frame Format + + The RESET_STREAM_AT frame contains the following fields: + + Stream ID: A variable-length integer encoding of the stream ID of + the stream being terminated. + + Application Protocol Error Code: A variable-length integer + containing the application protocol error code (Section 20.2 of + [RFC9000]) that indicates why the stream is being closed. + + Final Size: A variable-length integer indicating the final size of + the stream by the sender, in units of bytes; see Section 4.5 of + [RFC9000]. + + Reliable Size: A variable-length integer indicating the amount of + data that needs to be delivered to the application even though the + stream is reset. + + + + + + +Seemann & Oku Expires 17 December 2025 [Page 4] + +Internet-Draft QUIC Stream Resets with Partial Delivery June 2025 + + + If the Reliable Size is larger than the Final Size, the receiver MUST + close the connection with a connection error of type + FRAME_ENCODING_ERROR. + + RESET_STREAM_AT frames are ack-eliciting, and MUST only be sent in + the application data packet number space. When lost, they MUST be + retransmitted, unless the stream state has transitioned to "Data + Recvd" or "Reset Recvd" due to transmission and acknowledgement of + other frames (see Section 5.2). + +5. Resetting Streams + + A sender that wants to reset a stream but also deliver some bytes to + the receiver sends a RESET_STREAM_AT frame with the Reliable Size + field specifying the amount of data to be delivered. + + When using a RESET_STREAM_AT frame, the initiator MUST guarantee + reliable delivery of stream data of at least Reliable Size bytes. If + STREAM frames containing data up to that byte offset are lost, the + initiator MUST retransmit this data, as described in Section 13.3 of + [RFC9000]. Data sent beyond that byte offset SHOULD NOT be + retransmitted. + + As described in Section 3.2 of [RFC9000], a stream reset signal might + be suppressed or withheld, and the same applies to a stream reset + signal carried in a RESET_STREAM_AT frame. Similarly, the Reliable + Size of the RESET_STREAM_AT frame does not prevent a QUIC stack from + delivering data beyond the specified offset to the receiving + application. + + Note that a Reliable Size value of zero is valid. A RESET_STREAM_AT + frame with this value is logically equivalent to a RESET_STREAM frame + (Section 3.2 of [RFC9000]). When resetting a stream without the + intent to deliver any data to the receiver, the sender MAY use either + RESET_STREAM or RESET_STREAM_AT with a Reliable Size of zero. + + As stated in Section 4.5 of [RFC9000], the final size for a stream + cannot change once it is known. If a frame is received indicating a + change in the final size for the stream, an endpoint SHOULD respond + with an error of type FINAL_SIZE_ERROR. + +5.1. Sending RESET_STREAM_AT after FIN + + Similar to how it is possible to send a RESET_STREAM frame after a + STREAM frame carrying the FIN bit, it is possible to send a + RESET_STREAM_AT frame after a STREAM frame carrying the FIN bit. + + + + + +Seemann & Oku Expires 17 December 2025 [Page 5] + +Internet-Draft QUIC Stream Resets with Partial Delivery June 2025 + + + Due to packet reordering, it is possible for a receiver to receive + the RESET_STREAM_AT frame before receiving the STREAM frame carrying + the FIN bit. + +5.2. Multiple RESET_STREAM_AT / RESET_STREAM frames + + The initiator MAY send multiple RESET_STREAM_AT frames for the same + stream in order to reduce the Reliable Size. It MAY also send a + RESET_STREAM frame, which is equivalent to sending a RESET_STREAM_AT + frame with a Reliable Size of 0. When reducing the Reliable Size, + the sender MUST retransmit the RESET_STREAM_AT frame carrying the + smallest Reliable Size as well as stream data up to that size, until + all acknowledgements for the stream data and the RESET_STREAM_AT + frame are received. + + When sending multiple RESET_STREAM_AT or RESET_STREAM frames for the + same stream, the initiator MUST NOT increase the Reliable Size. + + When receiving a RESET_STREAM_AT frame with a lower Reliable Size, + the receiver only needs to provide data up the lower Reliable Size to + the application. It MUST NOT expect the sender to deliver any data + beyond that byte offset. + + Reordering of packets might lead to a RESET_STREAM_AT frame with a + higher Reliable Size being received after a RESET_STREAM_AT frame + with a lower Reliable Size. The receiver MUST ignore any + RESET_STREAM_AT frame that increases the Reliable Size. + + When sending another RESET_STREAM_AT, RESET_STREAM or STREAM frame + carrying a FIN bit for the same stream, the initiator MUST NOT change + the Application Error Code or the Final Size. If the receiver + detects a change in those fields, it MUST close the connection with a + connection error of type STREAM_STATE_ERROR. + + While multiple RESET_STREAM_AT frames can reduce Reliable Size, some + applications might need to ensure that a minimum amount of data is + always delivered on a stream. Application protocols can establish + rules for streams that ensure that Reliable Size is not reduced below + a certain threshold if that is necessary to ensure correct operation + of the protocol. + +5.3. Stream States + + In terms of stream state transitions (Section 3 of [RFC9000]), the + effect of a RESET_STREAM_AT frame is equivalent to that of the FIN + bit. Both the RESET_STREAM_AT frame and the FIN bit on a STREAM + frame serve the same role: signaling the amount of data to be + delivered. + + + +Seemann & Oku Expires 17 December 2025 [Page 6] + +Internet-Draft QUIC Stream Resets with Partial Delivery June 2025 + + + On the sending side, when the first RESET_STREAM_AT frame is sent, + the sending part of the stream enters the "Data Sent" state. Once + the RESET_STREAM_AT frame carrying the smallest Reliable Size and all + stream data up to that byte offset have been acknowledged, the + sending part of the stream enters the "Data Recvd" state. The + transition from "Data Sent" to "Data Recvd" happens immediately if + the application resets a stream and all bytes up to the specified + Reliable Size have already been sent and acknowledged. Conversely, + the transition might take multiple network roundtrips or require + additional flow control credit issued by the receiver. + + On the receiving side, when a RESET_STREAM_AT frame is received, the + receiving part of the stream enters the "Size Known" state. Once all + data up to the smallest Reliable Size have been received, it enters + the "Data Recvd" state. Similarly to the sending side, transition + from "Size Known" to "Data Recvd" might happen immediately or involve + issuance of additional flow control credit. + +6. Implementation Guidance + + In terms of transport machinery, the RESET_STREAM_AT frame is more + akin to the FIN bit than to the RESET_STREAM frame (see Section 5.3). + By sending a RESET_STREAM_AT frame, the sender commits to delivering + all bytes up to the Reliable Size. + + To the endpoints, the main differences from closing a stream by using + the FIN bit are: + + * the offset up to which the sender commits to sending might be + smaller than Final Size, + + * this offset might get reduced by subsequent RESET_STREAM_AT + frames, and + + * the closure is accompanied by an error code. + +7. Security Considerations + + As the RESET_STREAM_AT frame is an extension to the stream machinery + defined in QUIC version 1, the security considerations of [RFC9000] + apply accordingly. Specifically, given that RESET_STREAM_AT frames + indicate the offset up to which data is reliably transmitted, + endpoints SHOULD remain vigilant against resource commitment and + exhaustion attacks even after sending or receiving RESET_STREAM_AT + frames, until the stream reaches the terminal state. + +8. IANA Considerations + + + + +Seemann & Oku Expires 17 December 2025 [Page 7] + +Internet-Draft QUIC Stream Resets with Partial Delivery June 2025 + + +8.1. QUIC Transport Parameter + + This document registers the reset_stream_at transport parameter in + the "QUIC Transport Parameters" registry established in Section 22.3 + of [RFC9000]. The following fields are registered: + + Value: 0x17f7586d2cb571 + + Parameter Name: reset_stream_at + + Status: Provisional (note that, prior to publication, the value will + be replaced by a new value lower than 64) + + Specification: This document + + Change Controller: IETF (iesg@ietf.org) + + Contact: QUIC Working Group (quic@ietf.org) + +8.2. QUIC Frame Types + + This document registers one new value in the "QUIC Frame Types" + registry established in Section 22.4 of [RFC9000]. The following + fields are registered: + + Value: 0x24 + + Frame Type Name: RESET_STREAM_AT + + Status: Provisional (will become Permanent once this document is + approved) + + Specification: This document + + Change Controller: IETF (iesg@ietf.org) + + Contact: QUIC Working Group (quic@ietf.org) + +9. References + +9.1. Normative References + + [RFC2119] Bradner, S., "Key words for use in RFCs to Indicate + Requirement Levels", BCP 14, RFC 2119, + DOI 10.17487/RFC2119, March 1997, + . + + + + + +Seemann & Oku Expires 17 December 2025 [Page 8] + +Internet-Draft QUIC Stream Resets with Partial Delivery June 2025 + + + [RFC8174] Leiba, B., "Ambiguity of Uppercase vs Lowercase in RFC + 2119 Key Words", BCP 14, RFC 8174, DOI 10.17487/RFC8174, + May 2017, . + + [RFC9000] Iyengar, J., Ed. and M. Thomson, Ed., "QUIC: A UDP-Based + Multiplexed and Secure Transport", RFC 9000, + DOI 10.17487/RFC9000, May 2021, + . + +9.2. Informative References + + [WEBTRANSPORT] + Frindell, A., Kinnear, E., and V. Vasiliev, "WebTransport + over HTTP/3", Work in Progress, Internet-Draft, draft- + ietf-webtrans-http3-12, 3 March 2025, + . + +Acknowledgments + + TODO acknowledge. + +Authors' Addresses + + Marten Seemann + Email: martenseemann@gmail.com + + + Kazuho Oku + Fastly + Email: kazuhooku@gmail.com + + Additional contact information: + + 奥一穂 + Fastly + + + + + + + + + + + + + + + +Seemann & Oku Expires 17 December 2025 [Page 9] diff --git a/docs/draft-ietf-webtrans-http3-15.txt b/docs/draft-ietf-webtrans-http3-15.txt new file mode 100644 index 000000000..07209223e --- /dev/null +++ b/docs/draft-ietf-webtrans-http3-15.txt @@ -0,0 +1,2240 @@ + + + + +WEBTRANS A. Frindell +Internet-Draft Facebook +Intended status: Standards Track E. Kinnear +Expires: 3 September 2026 Apple Inc. + V. Vasiliev + Google + 2 March 2026 + + + WebTransport over HTTP/3 + draft-ietf-webtrans-http3-15 + +Abstract + + WebTransport [OVERVIEW] is a protocol framework that enables + application clients constrained by the Web security model to + communicate with a remote application server using a secure + multiplexed transport. This document describes a WebTransport + protocol that is based on HTTP/3 [HTTP3] and provides support for + unidirectional streams, bidirectional streams, and datagrams, all + multiplexed within the same HTTP/3 connection. + +About This Document + + This note is to be removed before publishing as an RFC. + + The latest revision of this draft can be found at https://ietf-wg- + webtrans.github.io/draft-ietf-webtrans-http3/#go.draft-ietf-webtrans- + http3.html. Status information for this document may be found at + https://datatracker.ietf.org/doc/draft-ietf-webtrans-http3/. + + Discussion of this document takes place on the WebTransport Working + Group mailing list (mailto:webtransport@ietf.org), which is archived + at https://mailarchive.ietf.org/arch/browse/webtransport/. Subscribe + at https://www.ietf.org/mailman/listinfo/webtransport/. + + Source for this draft and an issue tracker can be found at + https://github.com/ietf-wg-webtrans/draft-ietf-webtrans-http3. + +Status of This Memo + + This Internet-Draft is submitted in full conformance with the + provisions of BCP 78 and BCP 79. + + Internet-Drafts are working documents of the Internet Engineering + Task Force (IETF). Note that other groups may also distribute + working documents as Internet-Drafts. The list of current Internet- + Drafts is at https://datatracker.ietf.org/drafts/current/. + + + +Frindell, et al. Expires 3 September 2026 [Page 1] + +Internet-Draft WebTransport-H3 March 2026 + + + Internet-Drafts are draft documents valid for a maximum of six months + and may be updated, replaced, or obsoleted by other documents at any + time. It is inappropriate to use Internet-Drafts as reference + material or to cite them other than as "work in progress." + + This Internet-Draft will expire on 3 September 2026. + +Copyright Notice + + Copyright (c) 2026 IETF Trust and the persons identified as the + document authors. All rights reserved. + + This document is subject to BCP 78 and the IETF Trust's Legal + Provisions Relating to IETF Documents (https://trustee.ietf.org/ + license-info) in effect on the date of publication of this document. + Please review these documents carefully, as they describe your rights + and restrictions with respect to this document. Code Components + extracted from this document must include Revised BSD License text as + described in Section 4.e of the Trust Legal Provisions and are + provided without warranty as described in the Revised BSD License. + +Table of Contents + + 1. Introduction . . . . . . . . . . . . . . . . . . . . . . . . 3 + 1.1. Terminology . . . . . . . . . . . . . . . . . . . . . . . 3 + 2. Overview . . . . . . . . . . . . . . . . . . . . . . . . . . 4 + 2.1. QUIC, WebTransport, and HTTP/3 . . . . . . . . . . . . . 4 + 2.1.1. Minimizing Implementation Complexity . . . . . . . . 5 + 2.1.2. Capsule-Based WebTransport over HTTP/3 . . . . . . . 5 + 2.2. Protocol Overview . . . . . . . . . . . . . . . . . . . . 6 + 3. Session Establishment . . . . . . . . . . . . . . . . . . . . 7 + 3.1. Establishing a WebTransport-Capable HTTP/3 Connection . . 7 + 3.2. Creating a New Session . . . . . . . . . . . . . . . . . 9 + 3.3. Application Protocol Negotiation . . . . . . . . . . . . 10 + 3.4. Prioritization . . . . . . . . . . . . . . . . . . . . . 11 + 4. WebTransport Features . . . . . . . . . . . . . . . . . . . . 12 + 4.1. Transport Properties . . . . . . . . . . . . . . . . . . 13 + 4.2. Unidirectional streams . . . . . . . . . . . . . . . . . 13 + 4.3. Bidirectional Streams . . . . . . . . . . . . . . . . . . 13 + 4.4. Resetting Data Streams . . . . . . . . . . . . . . . . . 14 + 4.5. Datagrams . . . . . . . . . . . . . . . . . . . . . . . . 15 + 4.6. Buffering Incoming Streams and Datagrams . . . . . . . . 16 + 4.7. Interaction with the HTTP/3 GOAWAY frame . . . . . . . . 16 + 4.8. Use of Keying Material Exporters . . . . . . . . . . . . 17 + 5. Flow Control . . . . . . . . . . . . . . . . . . . . . . . . 18 + 5.1. Negotiating the Use of Flow Control . . . . . . . . . . . 18 + 5.2. Limiting the Number of Simultaneous Sessions . . . . . . 19 + 5.3. Limiting the Number of Streams Within a Session . . . . . 19 + + + +Frindell, et al. Expires 3 September 2026 [Page 2] + +Internet-Draft WebTransport-H3 March 2026 + + + 5.4. Data Limits . . . . . . . . . . . . . . . . . . . . . . . 20 + 5.5. Flow Control SETTINGS . . . . . . . . . . . . . . . . . . 21 + 5.5.1. SETTINGS_WT_INITIAL_MAX_STREAMS_UNI . . . . . . . . . 21 + 5.5.2. SETTINGS_WT_INITIAL_MAX_STREAMS_BIDI . . . . . . . . 21 + 5.5.3. SETTINGS_WT_INITIAL_MAX_DATA . . . . . . . . . . . . 22 + 5.6. Flow Control Capsules . . . . . . . . . . . . . . . . . . 22 + 5.6.1. Flow Control and Intermediaries . . . . . . . . . . . 22 + 5.6.2. WT_MAX_STREAMS Capsule . . . . . . . . . . . . . . . 23 + 5.6.3. WT_STREAMS_BLOCKED Capsule . . . . . . . . . . . . . 24 + 5.6.4. WT_MAX_DATA Capsule . . . . . . . . . . . . . . . . . 25 + 5.6.5. WT_DATA_BLOCKED Capsule . . . . . . . . . . . . . . . 26 + 6. Session Termination . . . . . . . . . . . . . . . . . . . . . 26 + 7. Considerations for Future Versions . . . . . . . . . . . . . 28 + 7.1. Negotiating the Draft Version . . . . . . . . . . . . . . 28 + 8. Security Considerations . . . . . . . . . . . . . . . . . . . 29 + 9. IANA Considerations . . . . . . . . . . . . . . . . . . . . . 30 + 9.1. Upgrade Token Registration . . . . . . . . . . . . . . . 30 + 9.2. HTTP/3 SETTINGS Parameter Registration . . . . . . . . . 30 + 9.3. Frame Type Registration . . . . . . . . . . . . . . . . . 31 + 9.4. Stream Type Registration . . . . . . . . . . . . . . . . 32 + 9.5. HTTP/3 Error Code Registration . . . . . . . . . . . . . 32 + 9.6. Capsule Types . . . . . . . . . . . . . . . . . . . . . . 34 + 9.7. Protocol Negotiation HTTP Header Fields . . . . . . . . . 36 + 10. References . . . . . . . . . . . . . . . . . . . . . . . . . 36 + 10.1. Normative References . . . . . . . . . . . . . . . . . . 36 + 10.2. Informative References . . . . . . . . . . . . . . . . . 38 + Index . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39 + Authors' Addresses . . . . . . . . . . . . . . . . . . . . . . . 39 + +1. Introduction + + HTTP/3 [HTTP3] is a protocol defined on top of QUIC [RFC9000] that + can multiplex HTTP requests over a QUIC connection. This document + defines a mechanism for multiplexing non-HTTP data with HTTP/3 in a + manner that conforms with the WebTransport protocol requirements and + semantics [OVERVIEW]. Using the mechanism described here, multiple + WebTransport instances, or sessions, can be multiplexed + simultaneously with regular HTTP traffic on the same HTTP/3 + connection. + +1.1. Terminology + + The keywords "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", + "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and + "OPTIONAL" in this document are to be interpreted as described in BCP + 14 [RFC2119] [RFC8174] when, and only when, they appear in all + capitals, as shown here. + + + + +Frindell, et al. Expires 3 September 2026 [Page 3] + +Internet-Draft WebTransport-H3 March 2026 + + + This document follows terminology defined in Section 1.2 of + [OVERVIEW]. Note that this document distinguishes between a + WebTransport server and an HTTP/3 server. An HTTP/3 server is the + server that terminates HTTP/3 connections; a WebTransport server is + an application that accepts WebTransport sessions, which can be + accessed via an HTTP/3 server. An application client is user or + developer-provided code, often untrusted, that utilizes the interface + offered by the WebTransport client to communicate with an application + server. The application server uses the interface offered by the + WebTransport server to accept incoming WebTransport sessions. + +2. Overview + +2.1. QUIC, WebTransport, and HTTP/3 + + QUIC version 1 [RFC9000] is a secure transport protocol with flow + control and congestion control. QUIC supports application data + exchange via streams; reliable and ordered byte streams that can be + multiplexed. Stream independence can mitigate head-of-line blocking. + While QUIC provides streams as a transport service, it is + unopinionated about their usage. The applicability of streams is + described by section 4 of [RFC9308]. + + HTTP is an application-layer protocol, defined by "HTTP Semantics" + [HTTP]. HTTP/3 is the application mapping for QUIC, defined in + [RFC9114]. It describes how QUIC streams are used to carry control + data or HTTP request and response message sequences in the form of + frames and describes details of stream and connection lifecycle + management. HTTP/3 offers two features in addition to HTTP + Semantics: QPACK header compression [RFC9208] and Server Push + Section 4.6 of [RFC9114]. + + WebTransport session establishment involves interacting at the HTTP + layer with a resource. For Web user agents and other WebTransport + clients, this interaction is important for security reasons, + especially to ensure that the resource is willing to use + WebTransport. + + Although WebTransport requires HTTP for its handshake, when HTTP/3 is + in use, HTTP is not used for anything else related to an established + session. Instead, QUIC streams begin with a sequence of header bytes + that links them to the established session. The remainder of the + stream is the body, which carries the payload supplied by the + application using WebTransport. This process is similar to + WebSockets over HTTP/1.1 [ORIGIN], where access to the underlying + byte stream is enabled after both sides have completed an initial + handshake. + + + + +Frindell, et al. Expires 3 September 2026 [Page 4] + +Internet-Draft WebTransport-H3 March 2026 + + + The layering of QUIC, HTTP/3, and WebTransport is shown in Figure 1. + Once a WebTransport session is established, applications have nearly + direct access to QUIC. + + ,--------------------------------, + | WebTransport | + ,----------------,---------------, + | HTTP Semantics | | + | and | | + | Session Setup | Nearly direct | + ,----------------, | + | HTTP/3 | | + ,----------------`---------------, + | QUIC | + `--------------------------------' + + Figure 1: WebTransport Layering + +2.1.1. Minimizing Implementation Complexity + + WebTransport has minimal interaction with HTTP and HTTP/3. Clients + and servers can constrain their use of features to only those + required to complete a WebTransport handshake: + + * Generating/parsing the request method, host, path, protocol, + optional Origin header field, and perhaps some extra header + fields. + + * Generating/parsing the response status code and possibly some + extra header fields. + + A WebTransport endpoint, whether a client or a server, can likely + perform several of its HTTP-level requirements using bytestring + comparisons. + + While HTTP/3 encodes HTTP messages using QPACK, this complexity can + be minimized. When receiving, a WebTransport endpoint can disable + dynamic decompression entirely but must always support static + decompression and Huffman decoding. When sending, endpoints can opt + to never use dynamic compression, static compression, or Huffman + encoding. + +2.1.2. Capsule-Based WebTransport over HTTP/3 + + WebTransport over HTTP/3 as defined in this document provides the + best performance by using native QUIC streams and datagrams. + Endpoints SHOULD always use this protocol when using WebTransport + over an HTTP/3 connection. + + + +Frindell, et al. Expires 3 September 2026 [Page 5] + +Internet-Draft WebTransport-H3 March 2026 + + + However, it is also possible to use WebTransport over a single HTTP/3 + stream using the capsule-based protocol defined in [WEBTRANS-H2]. + The two protocols are distinguished by their upgrade tokens: this + document uses the "webtransport-h3" token (Section 9.1), while + [WEBTRANS-H2] uses the "webtransport" token. + + The capsule-based protocol can be useful for intermediaries that + proxy WebTransport sessions between HTTP/2 and HTTP/3 connections, as + it avoids the need to translate between the two wire formats. It can + also be useful in deployment environments such as data centers where + existing routing infrastructure supports forwarding streams but does + not support the HTTP/3 extensions required by this document. + + Endpoints that use the capsule-based protocol over HTTP/3 lose the + benefits of stream independence, as all WebTransport streams within a + session share a single QUIC stream and are subject to head-of-line + blocking. Datagrams sent using the capsule-based protocol are also + retransmitted by QUIC, and therefore do not provide unreliable + delivery. + +2.2. Protocol Overview + + WebTransport servers in general are identified by a pair of authority + value and path value (defined in [RFC3986] Sections 3.2 and 3.3 + respectively). + + When an HTTP/3 connection is established, the server sends a + SETTINGS_WT_ENABLED setting to indicate support for WebTransport over + HTTP/3. This process also negotiates the use of additional HTTP/3 + extensions to enable both endpoints to open WebTransport streams. + + WebTransport sessions are initiated inside a given HTTP/3 connection + by the client, who sends an extended CONNECT request [RFC9220]. If + the server accepts the request, a WebTransport session is + established. The resulting stream will be further referred to as a + _CONNECT stream_, and its stream ID is used to uniquely identify a + given WebTransport session within the connection. The ID of the + CONNECT stream that established a given WebTransport session will be + further referred to as a _Session ID_. + + After the session is established, the endpoints can exchange data + using the following mechanisms: + + * A client can create a bidirectional stream and transfer its + ownership to WebTransport by providing a special signal in the + first bytes. + + + + + +Frindell, et al. Expires 3 September 2026 [Page 6] + +Internet-Draft WebTransport-H3 March 2026 + + + * A server can create a bidirectional stream and transfer its + ownership to WebTransport by providing a special signal in the + first bytes. + + * Both client and server can create a unidirectional stream using a + special stream type. + + * Both client and server can send datagrams using HTTP Datagrams + [HTTP-DATAGRAM]. + + A WebTransport session is terminated when the CONNECT stream that + created it is closed. + +3. Session Establishment + +3.1. Establishing a WebTransport-Capable HTTP/3 Connection + + A WebTransport-Capable HTTP/3 connection requires the server to + signal support for WebTransport over HTTP/3 using a setting. Clients + also signal support by using the "webtransport-h3" upgrade token in + extended CONNECT requests when establishing sessions (see + Section 9.1). + + This document defines a SETTINGS_WT_ENABLED setting that WebTransport + servers use to indicate their support for WebTransport. The default + value for the SETTINGS_WT_ENABLED setting is "0", meaning that the + server does not support WebTransport. Clients MUST NOT attempt to + establish WebTransport sessions until they have received the setting + indicating WebTransport support from the server. + + WebTransport over HTTP/3 uses extended CONNECT in HTTP/3 as described + in [RFC9220], which defines the SETTINGS_ENABLE_CONNECT_PROTOCOL + setting. + + WebTransport over HTTP/3 requires support for HTTP/3 datagrams and + the Capsule Protocol, and both the client and the server indicate + support for HTTP/3 datagrams by sending a SETTINGS_H3_DATAGRAM + setting value set to 1 in their SETTINGS frame (see Section 2.1.1 of + [HTTP-DATAGRAM]). + + WebTransport over HTTP/3 also requires support for QUIC datagrams. + To indicate support, both the client and the server send a + max_datagram_frame_size transport parameter with a value greater than + 0 (see Section 3 of [QUIC-DATAGRAM]). + + + + + + + +Frindell, et al. Expires 3 September 2026 [Page 7] + +Internet-Draft WebTransport-H3 March 2026 + + + WebTransport over HTTP/3 relies on the RESET_STREAM_AT frame defined + in [RESET-STREAM-AT]. To indicate support, both the client and the + server enable the extension by sending an empty reset_stream_at + transport parameter as described in Section 3 of [RESET-STREAM-AT]. + + In summary, servers supporting WebTransport over HTTP/3 send: + + * A SETTINGS_WT_ENABLED setting with a value greater than "0" + + * A SETTINGS_ENABLE_CONNECT_PROTOCOL setting with a value of "1" + + * A SETTINGS_H3_DATAGRAM setting with a value of 1 + + * A max_datagram_frame_size transport parameter with a value greater + than 0 + + * An empty reset_stream_at transport parameter + + Clients supporting WebTransport over HTTP/3 send: + + * A SETTINGS_H3_DATAGRAM setting with a value of 1 + + * A max_datagram_frame_size transport parameter with a value greater + than 0 + + * An empty reset_stream_at transport parameter + + [[RFC editor: please remove the following paragraph before + publication.]] + + For draft versions of WebTransport only, clients MUST also send + SETTINGS_WT_ENABLED with the draft-specific codepoint to allow the + server to identify the client's supported version; see Section 7.1. + + Servers should note that CONNECT requests to establish new + WebTransport sessions, in addition to other messages, can arrive + before the client's SETTINGS are received (see Section 4.6). If the + server receives SETTINGS that do not have correct values for every + required setting, or transport parameters that do not have correct + values for every required transport parameter, the server MUST treat + all established and newly incoming WebTransport sessions as + malformed, as described in Section 4.1.2 of [HTTP3]. + + A client MUST NOT establish WebTransport sessions if the server's + SETTINGS do not have correct values for every required setting or if + the server's transport parameters do not have correct values for + every required transport parameter. If a client does not wish to use + the connection for purposes other than WebTransport when the + + + +Frindell, et al. Expires 3 September 2026 [Page 8] + +Internet-Draft WebTransport-H3 March 2026 + + + requirements for WebTransport are not met, the client MAY close the + HTTP/3 connection with a WT_REQUIREMENTS_NOT_MET error code to aid in + debugging. + + [[RFC editor: please remove the following paragraph before + publication.]] + + For draft versions of WebTransport only, the server MUST NOT process + any incoming WebTransport requests until the client's SETTINGS have + been received; see Section 7.1. + +3.2. Creating a New Session + + As WebTransport sessions are established over HTTP/3, they are + identified using the https URI scheme (Section 4.2.2 of [HTTP]). + + In order to create a new WebTransport session, a WebTransport client + sends an HTTP extended CONNECT request. In this request: + + * The :protocol pseudo-header field ([RFC8441]) MUST be set to + webtransport-h3. + + * The :scheme field MUST be https. + + * Both the :authority and the :path value MUST be set; these fields + identify the desired WebTransport server resource. + + * If the WebTransport session is coming from a browser client, an + Origin header [RFC6454] MUST be provided within the request. + Otherwise, the header is OPTIONAL. + + Upon receiving an extended CONNECT request with a :protocol field set + to webtransport-h3, the HTTP/3 server can check if it has a + WebTransport server associated with the specified :authority and + :path values. If it does not, it SHOULD reply with status code 404 + (Section 15.5.5 of [HTTP]). When the request contains the Origin + header, the WebTransport server MUST verify the Origin header to + ensure that the specified origin is allowed to access the server in + question. If the verification fails, the WebTransport server SHOULD + reply with status code 403 (Section 15.5.4 of [HTTP]). If all checks + pass, the WebTransport server MAY accept the session by replying with + a 2xx series status code, as defined in Section 15.3 of [HTTP]. + + From the client's perspective, a WebTransport session is established + when the client receives a 2xx response. From the server's + perspective, a session is established once it sends a 2xx response. + + + + + +Frindell, et al. Expires 3 September 2026 [Page 9] + +Internet-Draft WebTransport-H3 March 2026 + + + The server may reply with a 3xx response, indicating a redirection + (Section 15.4 of [HTTP]). The WebTransport client MUST NOT + automatically follow such redirects, as it potentially could have + already sent data for the WebTransport session in question; it MAY + notify the application client about the redirect. + + Clients cannot initiate WebTransport in 0-RTT packets, as the CONNECT + method is not considered safe (see Section 10.9 of [HTTP3]). + However, WebTransport-related SETTINGS parameters may be retained + from the previous session as described in Section 7.2.4.2 of [HTTP3]. + If the server accepts 0-RTT, the server MUST NOT reduce the limit of + maximum open WebTransport sessions, or other initial flow control + values, from the values negotiated during the previous session; such + change would be deemed incompatible, and MUST result in a + H3_SETTINGS_ERROR connection error. + + The "webtransport-h3" HTTP Upgrade Token uses the Capsule Protocol as + defined in [HTTP-DATAGRAM]. The Capsule Protocol is negotiated when + the server sends a 2xx response. The capsule-protocol header field + Section 3.4 of [HTTP-DATAGRAM] is not required by WebTransport and + can safely be ignored by WebTransport endpoints. + +3.3. Application Protocol Negotiation + + WebTransport over HTTP/3 offers a protocol negotiation mechanism, + similar to the TLS Application-Layer Protocol Negotiation (ALPN) + extension [RFC7301]; the intent is to simplify porting existing + protocols that use QUIC and rely on this functionality. + + The client MAY include a WT-Available-Protocols header field in the + CONNECT request. The WT-Available-Protocols field enumerates the + possible protocols in preference order, with the most preferred + protocol listed first. If the server receives such a header, it MAY + include a WT-Protocol field in a successful (2xx) response. If it + does, the server MUST include a single choice from the client's list + in that field. Servers MAY reject the request if the client did not + include a suitable protocol. + + Both WT-Available-Protocols and WT-Protocol are Structured Fields + [FIELDS]. WT-Available-Protocols is a List. WT-Protocol is defined + as an Item. In both cases, the only valid value type is a String. + Any value type other than String MUST be treated as an error that + causes the entire field to be ignored as recommended in [FIELDS], + allowing application protocol negotiation to remain optional. No + semantics are defined for parameters on either field; parameters MUST + be ignored. + + + + + +Frindell, et al. Expires 3 September 2026 [Page 10] + +Internet-Draft WebTransport-H3 March 2026 + + + A server that requires application protocol negotiation MAY reject + the session if the WT-Available-Protocols header field is absent or + if it is malformed and therefore ignored. + + A client that requires application protocol negotiation MUST close + the WebTransport session with a WT_ALPN_ERROR error code if the + server does not include a WT-Protocol header field, or if it is + malformed and therefore ignored, in a successful response. + + If the client sends a WT-Available-Protocols header field and the + server responds with a WT-Protocol header field, the value in the WT- + Protocol response header field MUST be one of the values listed in + WT-Available-Protocols of the request. If the client receives a WT- + Protocol value that was not included in its WT-Available-Protocols + list, the client MUST close the WebTransport session with a + WT_ALPN_ERROR error code. + + The semantics of individual values used in WT-Available-Protocols and + WT-Protocol are determined by the WebTransport resource in question + and are not required to be registered in IANA's "ALPN Protocol IDs" + registry. + +3.4. Prioritization + + WebTransport sessions are initiated using extended CONNECT. While + Section 11 of [RFC9218] describes how extensible priorities can be + applied to data sent on a CONNECT stream, WebTransport extends the + types of data that are exchanged in relation to the request and + response, which requires additional considerations. + + WebTransport CONNECT requests and responses MAY contain the Priority + header field (Section 5 of [RFC9218]); clients MAY reprioritize by + sending PRIORITY_UPDATE frames (Section 7 of [RFC9218]). In + extension to [RFC9218], it is RECOMMENDED that clients and servers + apply the scheduling guidance in both Section 9 of [RFC9218] and + Section 10 of [RFC9218] for all data that they send in the enclosing + WebTransport session, including Capsules, WebTransport streams and + datagrams. WebTransport does not provide any priority signaling + mechanism for streams and datagrams within a WebTransport session; + such mechanisms can be defined by application protocols using + WebTransport. It is RECOMMENDED that such mechanisms only affect + scheduling within a session and not scheduling of other data on the + same HTTP/3 connection. + + + + + + + + +Frindell, et al. Expires 3 September 2026 [Page 11] + +Internet-Draft WebTransport-H3 March 2026 + + + The client/server priority merging guidance in Section 8 of [RFC9218] + also applies to WebTransport sessions. For example, a client that + receives a response Priority header field could alter its view of a + WebTransport session priority and alter the scheduling of outgoing + data as a result. + + Endpoints that prioritize WebTransport sessions need to consider how + they interact with other sessions or requests on the same HTTP/3 + connection. + +4. WebTransport Features + + WebTransport over HTTP/3 provides the following features described in + [OVERVIEW]: unidirectional streams, bidirectional streams, and + datagrams, all of which can be initiated by either endpoint. + Protocols designed for use with WebTransport over HTTP/3 are + constrained to these features. The Capsule Protocol is an + implementation detail of WebTransport over HTTP/3 and is not a + WebTransport feature. + + Session IDs are used to demultiplex streams and datagrams belonging + to different WebTransport sessions. On the wire, session IDs are + encoded using the QUIC variable length integer scheme described in + [RFC9000]. + + The client MAY optimistically open unidirectional and bidirectional + streams, as well as send datagrams, on a session for which it has + sent the CONNECT request, even if it has not yet received the + server's response to the request. On the server side, opening + streams and sending datagrams is possible as soon as the CONNECT + request has been received. + + Session IDs are derived from the stream ID of the CONNECT stream that + established the session and therefore MUST always correspond to a + client-initiated bidirectional stream, as defined in Section 2.1 of + [RFC9000]. If an endpoint receives a session ID on a unidirectional + stream, bidirectional stream, or datagram that does not correspond to + a client-initiated bidirectional stream ID, the endpoint MUST close + the connection with an H3_ID_ERROR error code. Session IDs that + correspond to closed sessions are not considered invalid for the + purposes of this check; endpoints handle data for closed sessions as + described in Section 6. + + + + + + + + + +Frindell, et al. Expires 3 September 2026 [Page 12] + +Internet-Draft WebTransport-H3 March 2026 + + +4.1. Transport Properties + + The WebTransport framework [OVERVIEW] defines a set of optional + transport properties that clients can use to determine the presence + of features which might allow additional optimizations beyond the + common set of properties available via all WebTransport protocols. + + Below are details about support in WebTransport over HTTP/3 for the + properties defined by the WebTransport framework. + + Unreliable Delivery: WebTransport over HTTP/3 supports unreliable + delivery. Resetting a stream results in lost stream data no + longer being retransmitted. WebTransport over HTTP/3 also + supports datagrams, which are not retransmitted. + + Pooling: WebTransport over HTTP/3 provides optional support for + pooling. Endpoints that do not support pooling can reply to + CONNECT requests with a header indicating a rate limit policy with + a quota of "1" ([I-D.ietf-httpapi-ratelimit-headers]). + +4.2. Unidirectional streams + + WebTransport endpoints can initiate unidirectional streams. The + HTTP/3 unidirectional stream type SHALL be 0x54. The body of the + stream SHALL be the stream type, followed by the session ID, encoded + as a variable-length integer, followed by the user-specified stream + data (Figure 2). + + Unidirectional Stream { + Stream Type (i) = 0x54, + Session ID (i), + User-Specified Stream Data (..) + } + + Figure 2: Unidirectional WebTransport stream format + +4.3. Bidirectional Streams + + All client-initiated bidirectional streams are reserved by HTTP/3 as + request streams, which are a sequence of HTTP/3 frames with a variety + of rules (see Sections 4.1 and 6.1 of [HTTP3]). + + WebTransport extends HTTP/3 to allow clients to declare and to use + alternative request stream rules. Once a client receives settings + indicating WebTransport support (Section 3.1), it MUST send a special + signal value, encoded as a variable-length integer, as the first + bytes of each bidirectional WebTransport stream it initiates to + indicate how the remaining bytes on the stream are used. + + + +Frindell, et al. Expires 3 September 2026 [Page 13] + +Internet-Draft WebTransport-H3 March 2026 + + + WebTransport extends HTTP/3 by defining rules for all server- + initiated bidirectional streams. Once a server receives an incoming + CONNECT request establishing a WebTransport session (Section 3.1), it + can open a bidirectional stream for use with that session and MUST + send a special signal value, encoded as a variable-length integer, as + the first bytes of the stream in order to indicate how the remaining + bytes on the stream are used. + + Clients and servers use the signal value 0x41 to open a bidirectional + WebTransport stream. Following this is the associated session ID, + encoded as a variable-length integer; the rest of the stream is the + application payload of the WebTransport stream (Figure 3). + + Bidirectional Stream { + Signal Value (i) = 0x41, + Session ID (i), + Stream Body (..) + } + + Figure 3: Bidirectional WebTransport stream format + + This document reserves the special signal value 0x41 as a WT_STREAM + frame type. While it is registered as an HTTP/3 frame type to avoid + collisions, WT_STREAM lacks length and is not a proper HTTP/3 frame; + it is an extension of HTTP/3 frame syntax that MUST be supported by + any peer negotiating WebTransport. Endpoints that implement this + extension are also subject to additional frame handling requirements. + Endpoints MUST NOT send WT_STREAM as a frame type on HTTP/3 streams + other than the very first bytes of a request stream. Receiving this + frame type in any other circumstances MUST be treated as a connection + error of type H3_FRAME_ERROR. + +4.4. Resetting Data Streams + + A WebTransport endpoint may send a RESET_STREAM or a STOP_SENDING + frame for a WebTransport data stream. Those signals are propagated + by the WebTransport implementation to the application. + + A WebTransport application MUST provide an error code for those + operations. Since WebTransport shares the error code space with + HTTP/3, WebTransport application errors for streams are limited to an + unsigned 32-bit integer, assuming values between 0x00000000 and + 0xffffffff. WebTransport implementations MUST remap those error + codes into the error range reserved for WT_APPLICATION_ERROR, where + 0x00000000 corresponds to 0x52e4a40fa8db, and 0xffffffff corresponds + to 0x52e5ac983162. Note that there are codepoints inside that range + of form "0x1f * N + 0x21" that are reserved by Section 8.1 of + [HTTP3]; those have to be skipped when mapping the error codes (i.e., + + + +Frindell, et al. Expires 3 September 2026 [Page 14] + +Internet-Draft WebTransport-H3 March 2026 + + + the two HTTP/3 error codepoints adjacent to a reserved codepoint + would map to two adjacent WebTransport application error codepoints). + An example pseudocode can be seen in Figure 4. + + first = 0x52e4a40fa8db + last = 0x52e5ac983162 + + def webtransport_code_to_http_code(n): + return first + n + floor(n / 0x1e) + + def http_code_to_webtransport_code(h): + assert(first <= h <= last) + assert((h - 0x21) % 0x1f != 0) + shifted = h - first + return shifted - floor(shifted / 0x1f) + + Figure 4: Pseudocode for converting between WebTransport + application errors and HTTP/3 error codes + + WebTransport data streams are associated with sessions through a + header at the beginning of the stream; resetting a stream might + result in that data being discarded when using a RESET_STREAM frame. + To prevent this, WebTransport implementations MUST use the + RESET_STREAM_AT frame [RESET-STREAM-AT] with a Reliable Size set to + at least the size of the WebTransport header when resetting a + WebTransport data stream. This ensures reliable delivery of the ID + field associating the data stream with a WebTransport session. + + WebTransport endpoints MUST forward the error code for a stream + associated with a known session to the application that owns that + session; similarly, intermediaries MUST reset such streams with a + corresponding error code when receiving a reset from their peer. If + a RESET_STREAM or STOP_SENDING frame is received with an error code + outside the range reserved for WT_APPLICATION_ERROR, the stream is + still considered reset, but the error code is not mapped to a + WebTransport application error code. The WebTransport implementation + SHOULD deliver this to the application as a stream reset with no + application error code. + +4.5. Datagrams + + Datagrams can be sent using HTTP Datagrams. The WebTransport + datagram payload is sent unmodified in the "HTTP Datagram Payload" + field of an HTTP Datagram (Section 2.1 of [HTTP-DATAGRAM]). Note + that the payload field directly follows the Quarter Stream ID field, + which is at the start of the QUIC DATAGRAM frame payload and refers + to the CONNECT stream that established the WebTransport session. + + + + +Frindell, et al. Expires 3 September 2026 [Page 15] + +Internet-Draft WebTransport-H3 March 2026 + + +4.6. Buffering Incoming Streams and Datagrams + + In WebTransport over HTTP/3, the client MUST wait for receipt of the + server's SETTINGS frame before establishing any WebTransport sessions + by sending CONNECT requests using the WebTransport upgrade token (see + Section 3.1). This ensures that the client will always know what + versions of WebTransport can be used on a given HTTP/3 connection. + + Clients can, however, send a SETTINGS frame, multiple WebTransport + CONNECT requests, WebTransport data streams, and WebTransport + datagrams all within a single flight. As those can arrive out of + order, a WebTransport server could be put into a situation where it + receives a stream or a datagram without a corresponding session. + Similarly, a client may receive a server-initiated stream or a + datagram before receiving the CONNECT response headers from the + server. + + To handle this case, WebTransport endpoints SHOULD buffer streams and + datagrams until they can be associated with an established session. + To avoid resource exhaustion, endpoints MUST limit the number of + buffered streams and datagrams. When the number of buffered streams + is exceeded, a stream SHALL be closed by sending a RESET_STREAM and/ + or STOP_SENDING with the WT_BUFFERED_STREAM_REJECTED error code. + When the number of buffered datagrams is exceeded, a datagram SHALL + be dropped. It is up to an implementation to choose what stream or + datagram to discard. + +4.7. Interaction with the HTTP/3 GOAWAY frame + + HTTP/3 defines a graceful shutdown mechanism (Section 5.2 of [HTTP3]) + that allows a peer to send a GOAWAY frame indicating that it will no + longer accept any new incoming requests or pushes. + + A client receiving GOAWAY cannot initiate CONNECT requests for new + WebTransport sessions on that HTTP/3 connection; it must open a new + HTTP/3 connection to initiate new WebTransport sessions with the same + peer. + + An HTTP/3 GOAWAY frame is also a signal to applications to initiate + shutdown for all WebTransport sessions. To shut down a single + WebTransport session, either endpoint can send a WT_DRAIN_SESSION + (0x78ae) capsule. + + WT_DRAIN_SESSION Capsule { + Type (i) = WT_DRAIN_SESSION, + Length (i) = 0 + } + + + + +Frindell, et al. Expires 3 September 2026 [Page 16] + +Internet-Draft WebTransport-H3 March 2026 + + + Figure 5: WT_DRAIN_SESSION Capsule Format + + After sending or receiving either a WT_DRAIN_SESSION capsule or a + HTTP/3 GOAWAY frame, an endpoint MAY continue using the session and + MAY open new WebTransport streams. The signal is intended for the + application using WebTransport, which is expected to attempt to + gracefully terminate the session as soon as possible. + + The WT_DRAIN_SESSION capsule is useful when an end-to-end + WebTransport session passes through an intermediary. For example, + when the backend shuts down, it sends a GOAWAY to the intermediary. + The intermediary can convert this signal to a WT_DRAIN_SESSION + capsule on the client-facing session, without impacting other + requests or sessions carried on that connection. + +4.8. Use of Keying Material Exporters + + WebTransport over HTTP/3 supports the use of TLS keying material + exporters Section 7.5 of [RFC8446]. Since the underlying QUIC + connection may be shared by multiple WebTransport sessions, + WebTransport defines a mechanism for deriving a TLS exporter that + separates keying material for different sessions. If the application + requests an exporter for a given WebTransport session with a + specified label and context, the resulting exporter SHALL be a TLS + exporter as defined in Section 7.5 of [RFC8446] with the label set to + "EXPORTER-WebTransport" and the context set to the serialization of + the "WebTransport Exporter Context" struct as defined below. + + WebTransport Exporter Context { + WebTransport Session ID (64), + WebTransport Application-Supplied Exporter Label Length (8), + WebTransport Application-Supplied Exporter Label (8..), + WebTransport Application-Supplied Exporter Context Length (8), + WebTransport Application-Supplied Exporter Context (..) + } + + Figure 6: WebTransport Exporter Context struct + + A TLS exporter API might permit the context field to be omitted. In + this case, as with TLS 1.3, the WebTransport Application-Supplied + Exporter Context becomes zero-length if omitted. + + + + + + + + + + +Frindell, et al. Expires 3 September 2026 [Page 17] + +Internet-Draft WebTransport-H3 March 2026 + + +5. Flow Control + + Flow control governs the amount of resources that can be consumed or + data that can be sent. When using WebTransport over HTTP/3, + endpoints can limit the number of sessions that a peer can create on + a single HTTP/3 connection and the number of streams that a peer can + create within a session. Endpoints can also limit the amount of data + that can be consumed by each session and by each stream within a + session. + + WebTransport over HTTP/3 provides a connection-level limit that + governs the number of sessions that can be created on an HTTP/3 + connection (see Section 5.2). It also provides session-level limits + that govern the number of streams that can be created in a session + and limit the amount of data that can be exchanged across all streams + in each session (see Section 5.3). + + The underlying QUIC connection provides connection and stream level + flow control. The QUIC connection data limit defines the total + amount of data that can be sent across all WebTransport sessions and + other non-WebTransport streams. A QUIC stream's data limit controls + the amount of data that can be sent on that stream, WebTransport or + otherwise (see Section 4 of [RFC9000]). + +5.1. Negotiating the Use of Flow Control + + A WebTransport endpoint that allows a WebTransport session to share + an underlying transport connection with other WebTransport sessions + MUST enable flow control. This prevents an application from + consuming excessive resources on a single session and starving + traffic for other sessions (see Section 8). + + Flow control is enabled when both endpoints declare their intent to + use flow control by taking any of the following actions: + + * Sending SETTINGS_WT_INITIAL_MAX_STREAMS_UNI with any value other + than "0". + + * Sending SETTINGS_WT_INITIAL_MAX_STREAMS_BIDI with any value other + than "0". + + * Sending SETTINGS_WT_INITIAL_MAX_DATA with any value other than + "0". + + If both endpoints take at least one of these actions, flow control is + enabled, and the limits described in the entirety of Section 5 apply. + + + + + +Frindell, et al. Expires 3 September 2026 [Page 18] + +Internet-Draft WebTransport-H3 March 2026 + + + Flow control can be enabled regardless of the number of WebTransport + sessions a server supports. + + If flow control is not enabled, clients MUST NOT attempt to establish + more than one simultaneous WebTransport session. A server that + receives more than one session on an underlying transport connection + when flow control is not enabled MUST reset the excessive CONNECT + streams with a H3_REQUEST_REJECTED status (see Section 5.2). + + Also, if flow control is not enabled, an endpoint MUST ignore receipt + of any flow control capsules (see Section 5.6), since the peer might + not have received SETTINGS at the time they were sent or packets + might have been reordered. + +5.2. Limiting the Number of Simultaneous Sessions + + Servers SHOULD limit the rate of incoming WebTransport sessions on + HTTP/3 connections to prevent excessive consumption of resources. To + do so, they have multiple mechanisms available: + + * The H3_REQUEST_REJECTED error code defined in Section 8.1 of + [HTTP3] indicates to the receiving HTTP/3 stack that the request + was not processed in any way. + + * HTTP status code 429 indicates that the request was rejected due + to rate limiting [RFC6585]. Unlike the previous method, this + signal is directly propagated to the application. + + WebTransport servers can use rate limit header fields in responses to + CONNECT requests to signal quota policies and service limits to + WebTransport clients (see [I-D.ietf-httpapi-ratelimit-headers]). + This provides hints to clients about how many sessions they can + reasonably expect to be able to open. An endpoint that does not + support pooling and flow control MUST NOT accept more than one + incoming WebTransport session at a time. + +5.3. Limiting the Number of Streams Within a Session + + The WT_MAX_STREAMS capsule (Section 5.6.2) establishes a limit on the + number of streams within a WebTransport session. Like the QUIC + MAX_STREAMS frame (Section 19.11 of [RFC9000]), this capsule has two + types that provide separate limits for unidirectional and + bidirectional streams that a peer initiates. + + + + + + + + +Frindell, et al. Expires 3 September 2026 [Page 19] + +Internet-Draft WebTransport-H3 March 2026 + + + Note that the CONNECT stream for the session is not included in + either the bidirectional or the unidirectional stream limits; the + number of CONNECT streams a client can open is limited by QUIC flow + control's stream limits and any rate limit that a WebTransport server + enforces. + + The session-level stream limit applies in addition to the QUIC + MAX_STREAMS frame, which provides a connection-level stream limit. + New streams can only be created within the session if both the + stream- and the connection-level limit permit, see Section 4.6 of + [RFC9000] for details on how QUIC stream limits are applied. + + Unlike the QUIC MAX_STREAMS frame, there is no simple relationship + between the value in this frame and stream IDs in QUIC STREAM frames. + This especially applies if there are other users of streams on the + connection. + + The WT_STREAMS_BLOCKED capsule (Section 5.6.3) can be sent to + indicate that an endpoint was unable to create a stream due to the + session-level stream limit. + + Note that enforcing this limit requires reliable resets for stream + headers so that both endpoints can agree on the number of streams + that are open. + +5.4. Data Limits + + The WT_MAX_DATA capsule (Section 5.6.4) establishes a limit on the + amount of data that can be sent within a WebTransport session. This + limit counts all data that is sent on streams of the corresponding + type, excluding the stream header (see Section 4.2 and Section 4.3). + The stream header is excluded from this limit so that this limit does + not prevent the sending of information that is essential in linking + new streams to a specific WebTransport session. + + For streams that were reset, implementing WT_MAX_DATA requires that + the QUIC stack provide the WebTransport implementation with + information about the final size of streams; see Section 4.5 of + [RFC9000]. This guarantees that both endpoints agree on how much + WebTransport session flow control credit was consumed by the sender + on that stream. + + The WT_DATA_BLOCKED capsule (Section 5.6.5) can be sent to indicate + that an endpoint was unable to send data due to a limit set by the + WT_MAX_DATA capsule. + + + + + + +Frindell, et al. Expires 3 September 2026 [Page 20] + +Internet-Draft WebTransport-H3 March 2026 + + + Because WebTransport over HTTP/3 uses a native QUIC stream for each + WebTransport stream, per-stream data limits are provided by QUIC + natively (see Section 4.1 of [RFC9000]). The WT_MAX_STREAM_DATA and + WT_STREAM_DATA_BLOCKED capsules (Sections 6.6 and 6.9 of + [I-D.ietf-webtrans-http2]) are not used and so are prohibited. + Endpoints MUST treat receipt of a WT_MAX_STREAM_DATA or a + WT_STREAM_DATA_BLOCKED capsule as a session error. + +5.5. Flow Control SETTINGS + + Initial flow control limits can be exchanged via HTTP/3 SETTINGS + (Section 9.2) by providing non-zero values for + + * WT_MAX_STREAMS via SETTINGS_WT_INITIAL_MAX_STREAMS_UNI and + SETTINGS_WT_INITIAL_MAX_STREAMS_BIDI + + * WT_MAX_DATA via SETTINGS_WT_INITIAL_MAX_DATA + +5.5.1. SETTINGS_WT_INITIAL_MAX_STREAMS_UNI + + The SETTINGS_WT_INITIAL_MAX_STREAMS_UNI setting indicates the initial + value for the unidirectional max stream limit, otherwise communicated + by the WT_MAX_STREAMS capsule (see Section 5.6.2). The default value + for the SETTINGS_WT_INITIAL_MAX_STREAMS_UNI setting is "0", + indicating that the endpoint needs to send WT_MAX_STREAMS capsules on + each individual WebTransport session before its peer is allowed to + create any unidirectional streams within that session. + + Note that this limit applies to all WebTransport sessions that use + the HTTP/3 connection on which this SETTING is sent. + +5.5.2. SETTINGS_WT_INITIAL_MAX_STREAMS_BIDI + + The SETTINGS_WT_INITIAL_MAX_STREAMS_BIDI setting indicates the + initial value for the bidirectional max stream limit, otherwise + communicated by the WT_MAX_STREAMS capsule (see Section 5.6.2). The + default value for the SETTINGS_WT_INITIAL_MAX_STREAMS_BIDI setting is + "0", indicating that the endpoint needs to send WT_MAX_STREAMS + capsules on each individual WebTransport session before its peer is + allowed to create any bidirectional streams within that session. + + Note that this limit applies to all WebTransport sessions that use + the HTTP/3 connection on which this SETTING is sent. + + + + + + + + +Frindell, et al. Expires 3 September 2026 [Page 21] + +Internet-Draft WebTransport-H3 March 2026 + + +5.5.3. SETTINGS_WT_INITIAL_MAX_DATA + + The SETTINGS_WT_INITIAL_MAX_DATA setting indicates the initial value + for the session data limit, otherwise communicated by the WT_MAX_DATA + capsule (see Section 5.6.4). The default value for the + SETTINGS_WT_INITIAL_MAX_DATA setting is "0", indicating that the + endpoint needs to send a WT_MAX_DATA capsule within each session + before its peer is allowed to send any stream data within that + session. + + Note that this limit applies to all WebTransport sessions that use + the HTTP/3 connection on which this SETTING is sent. + +5.6. Flow Control Capsules + + WebTransport over HTTP/3 uses several capsules for flow control, and + all of these capsules define special intermediary handling as + described in Section 3.2 of [HTTP-DATAGRAM]. These capsules, + referred to as the "flow control capsules", are WT_MAX_DATA, + WT_MAX_STREAMS, WT_DATA_BLOCKED, and WT_STREAMS_BLOCKED. + + An endpoint MUST NOT wait for a WT_DATA_BLOCKED or WT_STREAMS_BLOCKED + capsule before sending a WT_MAX_DATA or WT_MAX_STREAMS capsule; doing + so could result in the sender being blocked for at least an entire + round trip. Endpoints SHOULD send WT_MAX_DATA and WT_MAX_STREAMS + capsules as they consume data or close streams (similar to the + mechanism used in QUIC, see Section 4.2 of [RFC9000]). + +5.6.1. Flow Control and Intermediaries + + Because flow control in WebTransport is hop-by-hop and does not + provide an end-to-end signal, intermediaries MUST consume flow + control signals and express their own flow control limits to the next + hop. The intermediary can send these signals via HTTP/3 flow control + messages, HTTP/2 flow control messages, or as WebTransport flow + control capsules, where appropriate. Intermediaries are responsible + for storing any data for which they advertise flow control credit if + that data cannot be immediately forwarded to the next hop. + + In practice, an intermediary that translates flow control signals + between similar WebTransport protocols, such as between two HTTP/3 + connections, can often simply reexpress the same limits received on + one connection directly on the other connection. + + An intermediary that does not want to be responsible for storing data + that cannot be immediately sent on its translated connection can + ensure that it does not advertise a higher flow control limit on one + connection than the corresponding limit on the translated connection. + + + +Frindell, et al. Expires 3 September 2026 [Page 22] + +Internet-Draft WebTransport-H3 March 2026 + + +5.6.2. WT_MAX_STREAMS Capsule + + An HTTP capsule [HTTP-DATAGRAM] called WT_MAX_STREAMS is introduced + to inform the peer of the cumulative number of streams of a given + type it is permitted to open. A WT_MAX_STREAMS capsule with a type + of 0x190B4D3F applies to bidirectional streams, and a WT_MAX_STREAMS + capsule with a type of 0x190B4D40 applies to unidirectional streams. + + Note that, because Maximum Streams is a cumulative value representing + the total allowed number of streams, including previously closed + streams, endpoints repeatedly send new WT_MAX_STREAMS capsules with + increasing Maximum Streams values as streams are opened. + + WT_MAX_STREAMS Capsule { + Type (i) = 0x190B4D3F..0x190B4D40, + Length (i), + Maximum Streams (i), + } + + Figure 7: WT_MAX_STREAMS Capsule Format + + WT_MAX_STREAMS capsules contain the following field: + + Maximum Streams: A count of the cumulative number of streams of the + corresponding type that can be opened over the lifetime of the + session. This value cannot exceed 2^60, as it is not possible to + encode stream IDs larger than 2^62-1. Receipt of a capsule with a + Maximum Streams value larger than this limit MUST be treated as an + HTTP/3 error of type H3_DATAGRAM_ERROR. + + An endpoint MUST NOT open more streams than permitted by the current + stream limit set by its peer. For instance, a server that receives a + unidirectional stream limit of 3 is permitted to open streams 3, 7, + and 11, but not stream 15. + + Note that this limit includes streams that have been closed as well + as those that are open. + + If an endpoint receives an incoming stream for a session that would + exceed the advertised Maximum Streams value, it MUST close the + WebTransport session with a WT_FLOW_CONTROL_ERROR error code. + + Unlike in QUIC, where MAX_STREAMS frames can be delivered in any + order, WT_MAX_STREAMS capsules are sent on the WebTransport session's + connect stream and are delivered in order. If an endpoint receives a + WT_MAX_STREAMS capsule with a Maximum Streams value less than a + previously received value, it MUST close the WebTransport session + with a WT_FLOW_CONTROL_ERROR error code. + + + +Frindell, et al. Expires 3 September 2026 [Page 23] + +Internet-Draft WebTransport-H3 March 2026 + + + The WT_MAX_STREAMS capsule defines special intermediary handling, as + described in Section 3.2 of [HTTP-DATAGRAM]. Intermediaries MUST + consume WT_MAX_STREAMS capsules for flow control purposes and MUST + generate and send appropriate flow control signals for their limits. + + Initial values for these limits MAY be communicated by sending non- + zero values for SETTINGS_WT_INITIAL_MAX_STREAMS_UNI and + SETTINGS_WT_INITIAL_MAX_STREAMS_BIDI. + +5.6.3. WT_STREAMS_BLOCKED Capsule + + A sender SHOULD send a WT_STREAMS_BLOCKED capsule (type=0x190B4D43 or + 0x190B4D44) when it wishes to open a stream but is unable to do so + due to the maximum stream limit set by its peer. A + WT_STREAMS_BLOCKED capsule of type 0x190B4D43 is used to indicate + reaching the bidirectional stream limit, and a WT_STREAMS_BLOCKED + capsule of type 0x190B4D44 is used to indicate reaching the + unidirectional stream limit. + + A WT_STREAMS_BLOCKED capsule does not open the stream, but informs + the peer that a new stream was needed and the stream limit prevented + the creation of the stream. A sender is not required to send + WT_STREAMS_BLOCKED capsules, however WT_STREAMS_BLOCKED capsules can + be used as input to tuning of flow control algorithms and for + debugging purposes. + + WT_STREAMS_BLOCKED Capsule { + Type (i) = 0x190B4D43..0x190B4D44, + Length (i), + Maximum Streams (i), + } + + Figure 8: WT_STREAMS_BLOCKED Capsule Format + + WT_STREAMS_BLOCKED capsules contain the following field: + + Maximum Streams: A variable-length integer indicating the maximum + number of streams allowed at the time the capsule was sent. This + value cannot exceed 2^60, as it is not possible to encode stream + IDs larger than 2^62-1. + + The WT_STREAMS_BLOCKED capsule defines special intermediary handling, + as described in Section 3.2 of [HTTP-DATAGRAM]. Intermediaries MUST + consume WT_STREAMS_BLOCKED capsules for flow control purposes and + MUST generate and send appropriate flow control signals for their + limits. + + + + + +Frindell, et al. Expires 3 September 2026 [Page 24] + +Internet-Draft WebTransport-H3 March 2026 + + +5.6.4. WT_MAX_DATA Capsule + + An HTTP capsule [HTTP-DATAGRAM] called WT_MAX_DATA (type=0x190B4D3D) + is introduced to inform the peer of the maximum amount of data that + can be sent on the WebTransport session as a whole. + + This limit counts all data that is sent on streams of the + corresponding type, excluding the stream header (see Section 4.2 and + Section 4.3). For streams that were reset, implementing WT_MAX_DATA + requires that the QUIC stack provide the WebTransport implementation + with information about the final size of streams (see Section 4.5 of + [RFC9000]). + + WT_MAX_DATA Capsule { + Type (i) = 0x190B4D3D, + Length (i), + Maximum Data (i), + } + + Figure 9: WT_MAX_DATA Capsule Format + + WT_MAX_DATA capsules contain the following field: + + Maximum Data: A variable-length integer indicating the maximum + amount of data that can be sent on the entire session, in units of + bytes. + + The sum of the lengths of Stream Body data sent on all streams + associated with this session MUST NOT exceed the Maximum Data value + advertised by a receiver. Note that capsules sent on the CONNECT + stream, and the Signal Value, Stream Type, and Session ID fields, are + not included in this limit. If an endpoint receives Stream Body data + in excess of this limit, it MUST close the WebTransport session with + a WT_FLOW_CONTROL_ERROR error code. + + Unlike in QUIC, where MAX_DATA frames can be delivered in any order, + WT_MAX_DATA capsules are sent on the WebTransport session's connect + stream and are delivered in order. If an endpoint receives a + WT_MAX_DATA capsule with a Maximum Data value less than a previously + received value, it MUST close the WebTransport session with a + WT_FLOW_CONTROL_ERROR error code. + + The WT_MAX_DATA capsule defines special intermediary handling, as + described in Section 3.2 of [HTTP-DATAGRAM]. Intermediaries MUST + consume WT_MAX_DATA capsules for flow control purposes and MUST + generate and send appropriate flow control signals for their limits + (see Section 5.6.1). + + + + +Frindell, et al. Expires 3 September 2026 [Page 25] + +Internet-Draft WebTransport-H3 March 2026 + + + The initial value for this limit MAY be communicated by sending a + non-zero value for SETTINGS_WT_INITIAL_MAX_DATA. + +5.6.5. WT_DATA_BLOCKED Capsule + + A sender SHOULD send a WT_DATA_BLOCKED capsule (type=0x190B4D41) when + it wishes to send data but is unable to do so due to WebTransport + session-level flow control. A sender is not required to send + WT_DATA_BLOCKED capsules, however WT_DATA_BLOCKED capsules can be + used as input to tuning of flow control algorithms and for debugging + purposes. + + WT_DATA_BLOCKED Capsule { + Type (i) = 0x190B4D41, + Length (i), + Maximum Data (i), + } + + Figure 10: WT_DATA_BLOCKED Capsule Format + + WT_DATA_BLOCKED capsules contain the following field: + + Maximum Data: A variable-length integer indicating the session-level + limit at which blocking occurred. + + The WT_DATA_BLOCKED capsule defines special intermediary handling, as + described in Section 3.2 of [HTTP-DATAGRAM]. Intermediaries MUST + consume WT_DATA_BLOCKED capsules for flow control purposes and MUST + generate and send appropriate flow control signals for their limits + (see Section 5.6.1). + +6. Session Termination + + A WebTransport session over HTTP/3 is considered terminated when + either of the following conditions is met: + + * the CONNECT stream is closed, either cleanly or abruptly, on + either side; or + + * a WT_CLOSE_SESSION capsule is either sent or received. + + Upon learning that the session has been terminated, the endpoint MUST + reset the send side and abort reading on the receive side of all + unidirectional and bidirectional streams associated with the session + (see Section 2.4 of [RFC9000]) using the WT_SESSION_GONE error code; + it MUST NOT send any new datagrams or open any new streams. + + + + + +Frindell, et al. Expires 3 September 2026 [Page 26] + +Internet-Draft WebTransport-H3 March 2026 + + + To terminate a session with a detailed error message, an application + MAY provide such a message for the WebTransport endpoint to send in + an HTTP capsule [HTTP-DATAGRAM] of type WT_CLOSE_SESSION (0x2843). + The format of the capsule SHALL be as follows: + + WT_CLOSE_SESSION Capsule { + Type (i) = WT_CLOSE_SESSION, + Length (i), + Application Error Code (32), + Application Error Message (..8192), + } + + Figure 11: WT_CLOSE_SESSION Capsule Format + + WT_CLOSE_SESSION has the following fields: + + Application Error Code: A 32-bit error code provided by the + application closing the session. + + Application Error Message: A UTF-8 encoded error message string + provided by the application closing the session. The message + takes up the remainder of the capsule, and its length MUST NOT + exceed 1024 bytes. + + Note that the Application Error Code field does not mirror the Error + Code field in QUIC's CONNECTION_CLOSE frame (Section 19.19 of + [RFC9000]) because WebTransport application errors use a subset of + the HTTP/3 Error Code space and need to fit within those bounds, see + Section 4.4. + + An endpoint that sends a WT_CLOSE_SESSION capsule MUST immediately + send a FIN on the CONNECT Stream. The endpoint MAY also send a + STOP_SENDING with error code WT_SESSION_GONE to indicate it is no + longer reading from the CONNECT stream. The recipient MUST either + close or reset the stream in response. If any additional stream data + is received on the CONNECT stream after receiving a WT_CLOSE_SESSION + capsule, the stream MUST be reset with code H3_MESSAGE_ERROR. + + Cleanly terminating a CONNECT stream without a WT_CLOSE_SESSION + capsule SHALL be semantically equivalent to terminating it with a + WT_CLOSE_SESSION capsule that has an error code of 0 and an empty + error string. + + In some scenarios, an endpoint might want to send a WT_CLOSE_SESSION + with detailed close information and then immediately close the + underlying QUIC connection. If the endpoint were to do both of those + simultaneously, the peer could potentially receive the + CONNECTION_CLOSE before receiving the WT_CLOSE_SESSION, thus never + + + +Frindell, et al. Expires 3 September 2026 [Page 27] + +Internet-Draft WebTransport-H3 March 2026 + + + receiving the application error data contained in the latter. To + avoid this, the endpoint SHOULD wait until all CONNECT streams have + been closed by the peer before sending the CONNECTION_CLOSE; this + gives WT_CLOSE_SESSION properties similar to that of the QUIC + CONNECTION_CLOSE mechanism as a best-effort mechanism of delivering + application close metadata. + +7. Considerations for Future Versions + + Future versions of WebTransport that change the syntax of the CONNECT + requests used to establish WebTransport sessions will need to modify + the upgrade token used to identify WebTransport, allowing servers to + offer multiple versions simultaneously (see Section 9.1). + + Servers that support future incompatible versions of WebTransport + signal that support by changing the codepoint used for the + SETTINGS_WT_ENABLED setting (see Section 9.2). Clients can select + the associated upgrade token, if applicable, to use when establishing + a new session, ensuring that servers will always know the syntax in + use for every incoming request. + + Changes to future stream formats require changes to the + Unidirectional Stream type (see Section 4.2) and Bidirectional Stream + signal value (see Section 4.3) to allow recipients of incoming frames + to determine the WebTransport version, and corresponding wire format, + used for the session associated with that stream. + +7.1. Negotiating the Draft Version + + [[RFC editor: please remove this section before publication.]] + + The wire format aspects of the protocol are negotiated by changing + the codepoint used for the SETTINGS_WT_ENABLED setting. Each draft + version defines a distinct codepoint for SETTINGS_WT_ENABLED. Both + the client and the server MUST send SETTINGS_WT_ENABLED with the + codepoint corresponding to their supported draft version. An + endpoint that supports multiple draft versions sends a + SETTINGS_WT_ENABLED value for each supported version, as each version + uses a different setting identifier. The highest version supported + by both endpoints is selected. + + Because data streams can arrive at the server before the CONNECT + request that establishes the associated session, and the wire format + of the stream header depends on the negotiated version, the server + needs to know the client's version before processing any incoming + WebTransport streams. For this reason, the server MUST NOT process + any incoming WebTransport requests until the client's SETTINGS have + been received. + + + +Frindell, et al. Expires 3 September 2026 [Page 28] + +Internet-Draft WebTransport-H3 March 2026 + + +8. Security Considerations + + WebTransport over HTTP/3 satisfies all of the security requirements + imposed by [OVERVIEW] on WebTransport protocols, thus providing a + secure framework for client-server communication in cases when the + application is potentially untrusted. + + WebTransport over HTTP/3 requires explicit opt-in through the use of + an HTTP/3 setting; this avoids potential protocol confusion attacks + by ensuring the HTTP/3 server explicitly supports it. It also + requires the use of the Origin header for browser traffic, providing + the server with the ability to deny access to Web-based applications + that do not originate from a trusted origin. + + Just like HTTP traffic going over HTTP/3, WebTransport pools traffic + to different origins within a single connection. Different origins + imply different trust domains, meaning that the implementations have + to treat each transport as potentially hostile towards others on the + same connection. One potential attack is a resource exhaustion + attack: since all of the WebTransport sessions share both congestion + control and flow control context, a single application aggressively + using up those resources can cause other sessions to stall. A + WebTransport endpoint MUST implement flow control mechanisms if it + allows a WebTransport session to share the transport connection with + other WebTransport sessions. WebTransport endpoints SHOULD implement + a fairness scheme that ensures that each session that shares a + transport connection gets a reasonable share of controlled resources; + this applies both to sending data and to opening new streams. + + An application could attempt to exhaust resources by opening too many + WebTransport sessions at once. In cases when the application is + untrusted, a WebTransport client SHOULD limit the number of outgoing + sessions it will open. + + Note that the security considerations of HTTP/3 [HTTP3] apply to + WebTransport over HTTP/3. In particular, the denial-of-service + considerations in Section 10.5 of [HTTP3] are relevant. WebTransport + extends HTTP/3 with additional features that have legitimate uses but + can become a burden when they are used unnecessarily or to excess. + + + + + + + + + + + + +Frindell, et al. Expires 3 September 2026 [Page 29] + +Internet-Draft WebTransport-H3 March 2026 + + + WebTransport introduces new interaction modes that permit either + endpoint to send streams and datagrams to its peer. This is + particularly novel for clients, which previously had limited exposure + to unsolicited server-initiated traffic beyond server push (see + Section 4.6 of [HTTP3]). An endpoint that does not monitor use of + these features exposes itself to a risk of denial-of-service attack. + Implementations SHOULD track the use of WebTransport features, such + as the number of incoming streams and datagrams, and set limits on + their use. An endpoint MAY treat activity that is suspicious as a + connection error of type H3_EXCESSIVE_LOAD. + +9. IANA Considerations + + This document registers an upgrade token (Section 9.1), HTTP/3 + settings (Section 9.2), an HTTP/3 stream type (Section 9.4), an + HTTP/3 error code (Section 9.5), and an HTTP header field + (Section 9.7). + +9.1. Upgrade Token Registration + + The following entry is added to the "Hypertext Transfer Protocol + (HTTP) Upgrade Token Registry" registry established by Section 16.7 + of [HTTP]. + + The "webtransport-h3" label identifies HTTP/3 used as a protocol for + WebTransport: + + Value: webtransport-h3 + + Description: WebTransport over HTTP/3 + + Reference: This document + +9.2. HTTP/3 SETTINGS Parameter Registration + + The following entry is added to the "HTTP/3 Settings" registry + established by [HTTP3]: + + Setting Name: SETTINGS_WT_ENABLED + + Value: 0x2c7cf000 + + Default: 0 + + Reference: This document + + Change Controller: IETF + + + + +Frindell, et al. Expires 3 September 2026 [Page 30] + +Internet-Draft WebTransport-H3 March 2026 + + + Contact: WebTransport Working Group webtransport@ietf.org + (mailto:webtransport@ietf.org) + + Setting Name: SETTINGS_WT_INITIAL_MAX_STREAMS_UNI + + Value: 0x2b64 + + Default: 0 + + Reference: This document + + Change Controller: IETF + + Contact: WebTransport Working Group webtransport@ietf.org + (mailto:webtransport@ietf.org) + + Setting Name: SETTINGS_WT_INITIAL_MAX_STREAMS_BIDI + + Value: 0x2b65 + + Default: 0 + + Reference: This document + + Change Controller: IETF + + Contact: WebTransport Working Group webtransport@ietf.org + (mailto:webtransport@ietf.org) + + Setting Name: SETTINGS_WT_INITIAL_MAX_DATA + + Value: 0x2b61 + + Default: 0 + + Reference: This document + + Change Controller: IETF + + Contact: WebTransport Working Group webtransport@ietf.org + (mailto:webtransport@ietf.org) + +9.3. Frame Type Registration + + The following entry is added to the "HTTP/3 Frame Type" registry + established by [HTTP3]: + + + + + +Frindell, et al. Expires 3 September 2026 [Page 31] + +Internet-Draft WebTransport-H3 March 2026 + + + The WT_STREAM frame is reserved for the purpose of avoiding collision + with WebTransport HTTP/3 extensions: + + Value: 0x41 + + Frame Type: WT_STREAM + + Reference: This document + + Change Controller: IETF + + Contact: WebTransport Working Group webtransport@ietf.org + (mailto:webtransport@ietf.org) + +9.4. Stream Type Registration + + The following entry is added to the "HTTP/3 Stream Type" registry + established by [HTTP3]: + + The "WebTransport stream" type allows unidirectional streams to be + used by WebTransport: + + Value: 0x54 + + Stream Type: WebTransport stream + + Reference: This document + + Sender: Both + + Change Controller: IETF + + Contact: WebTransport Working Group webtransport@ietf.org + (mailto:webtransport@ietf.org) + +9.5. HTTP/3 Error Code Registration + + The following entries are added to the "HTTP/3 Error Code" registry + established by [HTTP3]: + + Name: WT_BUFFERED_STREAM_REJECTED + + Value: 0x3994bd84 + + Description: WebTransport data stream rejected due to lack of + associated session. + + Reference: This document. + + + +Frindell, et al. Expires 3 September 2026 [Page 32] + +Internet-Draft WebTransport-H3 March 2026 + + + Change Controller: IETF + + Contact: WebTransport Working Group webtransport@ietf.org + (mailto:webtransport@ietf.org) + + Name: WT_SESSION_GONE + + Value: 0x170d7b68 + + Description: WebTransport data stream aborted because the associated + WebTransport session has been closed. Also used to indicate that + the endpoint is no longer reading from the CONNECT stream. + + Specification: This document. + + Name: WT_FLOW_CONTROL_ERROR + + Value: 0x045d4487 + + Description: WebTransport session aborted because a flow control + error was encountered. + + Reference: This document. + + Change Controller: IETF + + Contact: WebTransport Working Group webtransport@ietf.org + (mailto:webtransport@ietf.org) + + Name: WT_ALPN_ERROR + + Value: 0x0817b3dd + + Description: WebTransport session aborted because application + protocol negotiation failed. + + Reference: This document. + + Change Controller: IETF + + Contact: WebTransport Working Group webtransport@ietf.org + (mailto:webtransport@ietf.org) + + Name: WT_REQUIREMENTS_NOT_MET + + Value: 0x212c0d48 + + Description: HTTP/3 connection closed because the features required + + + +Frindell, et al. Expires 3 September 2026 [Page 33] + +Internet-Draft WebTransport-H3 March 2026 + + + for WebTransport are not supported. Either the client or server + is missing required SETTINGS or transport parameters needed for + WebTransport. + + Reference: This document. + + Change Controller: IETF + + Contact: WebTransport Working Group webtransport@ietf.org + (mailto:webtransport@ietf.org) + + In addition, the following range of entries is registered: + + Name: WT_APPLICATION_ERROR + + Value: 0x52e4a40fa8db to 0x52e5ac983162 inclusive, with the + exception of the codepoints of form 0x1f * N + 0x21. + + Description: WebTransport application error codes. + + Reference: This document. + + Change Controller: IETF + + Contact: WebTransport Working Group webtransport@ietf.org + (mailto:webtransport@ietf.org) + +9.6. Capsule Types + + The following entries are added to the "HTTP Capsule Types" registry + established by [HTTP-DATAGRAM]: + + The WT_CLOSE_SESSION capsule. + + Value: 0x2843 + Capsule Type: WT_CLOSE_SESSION + Status: permanent + Reference: This document + Change Controller: IETF + Contact: WebTransport Working Group webtransport@ietf.org + (mailto:webtransport@ietf.org) + Notes: None + + The WT_DRAIN_SESSION capsule. + + Value: 0x78ae + Capsule Type: WT_DRAIN_SESSION + Status: provisional (when this document is approved this will become + + + +Frindell, et al. Expires 3 September 2026 [Page 34] + +Internet-Draft WebTransport-H3 March 2026 + + + permanent) + Reference: This document + Change Controller: IETF + Contact: WebTransport Working Group webtransport@ietf.org + (mailto:webtransport@ietf.org) + Notes: None + + The WT_MAX_STREAMS capsule: + + Value: 0x190B4D3F..0x190B4D40 + Capsule Type: WT_MAX_STREAMS + Status: permanent + Reference: This document + Change Controller: IETF + Contact: WebTransport Working Group webtransport@ietf.org + (mailto:webtransport@ietf.org) + Notes: None + + The WT_STREAMS_BLOCKED capsule: + + Value: 0x190B4D43..0x190B4D44 + Capsule Type: WT_STREAMS_BLOCKED + Status: permanent + Reference: This document + Change Controller: IETF + Contact: WebTransport Working Group webtransport@ietf.org + (mailto:webtransport@ietf.org) + Notes: None + + The WT_MAX_DATA capsule: + + Value: 0x190B4D3D + Capsule Type: WT_MAX_DATA + Status: permanent + Reference: This document + Change Controller: IETF + Contact: WebTransport Working Group webtransport@ietf.org + (mailto:webtransport@ietf.org) + Notes: None + + The WT_DATA_BLOCKED capsule: + + Value: 0x190B4D41 + Capsule Type: WT_DATA_BLOCKED + Status: permanent + Reference: This document + Change Controller: IETF + Contact: WebTransport Working Group webtransport@ietf.org + + + +Frindell, et al. Expires 3 September 2026 [Page 35] + +Internet-Draft WebTransport-H3 March 2026 + + + (mailto:webtransport@ietf.org) + Notes: None + +9.7. Protocol Negotiation HTTP Header Fields + + The following HTTP header fields are used for negotiating a protocol + (Section 3.3). These are added to the "HTTP Field Name" registry + established in Section 18.4 of [HTTP]: + + The WT-Available-Protocols field: + + Field Name: WT-Available-Protocols + Status: permanent + Structured Type: List + Reference: Section 3.3 + Comments: None + + The WT-Protocol field: + + Field Name: WT-Protocol + Status: permanent + Structured Type: Item + Reference: Section 3.3 + Comments: None + +10. References + +10.1. Normative References + + [FIELDS] Nottingham, M. and P. Kamp, "Structured Field Values for + HTTP", RFC 9651, DOI 10.17487/RFC9651, September 2024, + . + + [HTTP] Fielding, R., Ed., Nottingham, M., Ed., and J. Reschke, + Ed., "HTTP Semantics", STD 97, RFC 9110, + DOI 10.17487/RFC9110, June 2022, + . + + [HTTP-DATAGRAM] + Schinazi, D. and L. Pardue, "HTTP Datagrams and the + Capsule Protocol", RFC 9297, DOI 10.17487/RFC9297, August + 2022, . + + [HTTP3] Bishop, M., Ed., "HTTP/3", RFC 9114, DOI 10.17487/RFC9114, + June 2022, . + + + + + + +Frindell, et al. Expires 3 September 2026 [Page 36] + +Internet-Draft WebTransport-H3 March 2026 + + + [OVERVIEW] Kinnear, E. and V. Vasiliev, "The WebTransport Protocol + Framework", Work in Progress, Internet-Draft, draft-ietf- + webtrans-overview-11, 20 October 2025, + . + + [QUIC-DATAGRAM] + Pauly, T., Kinnear, E., and D. Schinazi, "An Unreliable + Datagram Extension to QUIC", RFC 9221, + DOI 10.17487/RFC9221, March 2022, + . + + [RESET-STREAM-AT] + Seemann, M. and K. Oku, "QUIC Stream Resets with Partial + Delivery", Work in Progress, Internet-Draft, draft-ietf- + quic-reliable-stream-reset-07, 14 June 2025, + . + + [RFC2119] Bradner, S., "Key words for use in RFCs to Indicate + Requirement Levels", BCP 14, RFC 2119, + DOI 10.17487/RFC2119, March 1997, + . + + [RFC3986] Berners-Lee, T., Fielding, R., and L. Masinter, "Uniform + Resource Identifier (URI): Generic Syntax", STD 66, + RFC 3986, DOI 10.17487/RFC3986, January 2005, + . + + [RFC6454] Barth, A., "The Web Origin Concept", RFC 6454, + DOI 10.17487/RFC6454, December 2011, + . + + [RFC6585] Nottingham, M. and R. Fielding, "Additional HTTP Status + Codes", RFC 6585, DOI 10.17487/RFC6585, April 2012, + . + + [RFC8174] Leiba, B., "Ambiguity of Uppercase vs Lowercase in RFC + 2119 Key Words", BCP 14, RFC 8174, DOI 10.17487/RFC8174, + May 2017, . + + [RFC8441] McManus, P., "Bootstrapping WebSockets with HTTP/2", + RFC 8441, DOI 10.17487/RFC8441, September 2018, + . + + [RFC8446] Rescorla, E., "The Transport Layer Security (TLS) Protocol + Version 1.3", RFC 8446, DOI 10.17487/RFC8446, August 2018, + . + + + +Frindell, et al. Expires 3 September 2026 [Page 37] + +Internet-Draft WebTransport-H3 March 2026 + + + [RFC9000] Iyengar, J., Ed. and M. Thomson, Ed., "QUIC: A UDP-Based + Multiplexed and Secure Transport", RFC 9000, + DOI 10.17487/RFC9000, May 2021, + . + + [RFC9114] Bishop, M., Ed., "HTTP/3", RFC 9114, DOI 10.17487/RFC9114, + June 2022, . + + [RFC9218] Oku, K. and L. Pardue, "Extensible Prioritization Scheme + for HTTP", RFC 9218, DOI 10.17487/RFC9218, June 2022, + . + + [RFC9220] Hamilton, R., "Bootstrapping WebSockets with HTTP/3", + RFC 9220, DOI 10.17487/RFC9220, June 2022, + . + +10.2. Informative References + + [I-D.ietf-httpapi-ratelimit-headers] + Polli, R., Ruiz, A. M., and D. Miller, "RateLimit header + fields for HTTP", Work in Progress, Internet-Draft, draft- + ietf-httpapi-ratelimit-headers-10, 27 September 2025, + . + + [I-D.ietf-webtrans-http2] + Frindell, A., Kinnear, E., Pauly, T., Thomson, M., + Vasiliev, V., and G. Xie, "WebTransport over HTTP/2", Work + in Progress, Internet-Draft, draft-ietf-webtrans-http2-13, + 20 October 2025, . + + [ORIGIN] Fette, I. and A. Melnikov, "The WebSocket Protocol", + RFC 6455, DOI 10.17487/RFC6455, December 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, . + + [RFC9208] Melnikov, A., "IMAP QUOTA Extension", RFC 9208, + DOI 10.17487/RFC9208, March 2022, + . + + [RFC9308] Kühlewind, M. and B. Trammell, "Applicability of the QUIC + Transport Protocol", RFC 9308, DOI 10.17487/RFC9308, + September 2022, . + + + +Frindell, et al. Expires 3 September 2026 [Page 38] + +Internet-Draft WebTransport-H3 March 2026 + + + [WEBTRANS-H2] + Frindell, A., Kinnear, E., Pauly, T., Thomson, M., + Vasiliev, V., and G. Xie, "WebTransport over HTTP/2", Work + in Progress, Internet-Draft, draft-ietf-webtrans-http2-13, + 20 October 2025, . + +Index + + W + + W + + WT_DATA_BLOCKED Section 5.4, Paragraph 3; Section 5.6, + Paragraph 1; Section 5.6, Paragraph 2; Section 5.6.5, + Paragraph 1; Section 5.6.5, Paragraph 3; Section 5.6.5, + Paragraph 5; Section 9.6, Paragraph 13.4.1 + WT_MAX_DATA Section 5.4, Paragraph 1; Section 5.4, Paragraph + 2; Section 5.4, Paragraph 3; Section 5.5, Paragraph 2.2.1; + Section 5.5.3, Paragraph 1; Section 5.6, Paragraph 1; + Section 5.6, Paragraph 2; Section 5.6.4, Paragraph 1; + Section 5.6.4, Paragraph 2; Section 5.6.4, Paragraph 4; + Section 5.6.4, Paragraph 7; Section 5.6.4, Paragraph 8; + Section 9.6, Paragraph 11.4.1 + WT_MAX_STREAMS Section 5.3, Paragraph 1; Section 5.5, + Paragraph 2.1.1; Section 5.5.1, Paragraph 1; Section 5.5.2, + Paragraph 1; Section 5.6, Paragraph 1; Section 5.6, + Paragraph 2; Section 5.6.2, Paragraph 1; Section 5.6.2, + Paragraph 2; Section 5.6.2, Paragraph 4; Section 5.6.2, + Paragraph 9; Section 5.6.2, Paragraph 10; Section 9.6, + Paragraph 7.4.1 + WT_STREAMS_BLOCKED Section 5.3, Paragraph 5; Section 5.6, + Paragraph 1; Section 5.6, Paragraph 2; Section 5.6.3, + Paragraph 1; Section 5.6.3, Paragraph 2; Section 5.6.3, + Paragraph 4; Section 5.6.3, Paragraph 6; Section 9.6, + Paragraph 9.4.1 + +Authors' Addresses + + Alan Frindell + Facebook + Email: afrind@fb.com + + + Eric Kinnear + Apple Inc. + Email: ekinnear@apple.com + + + + +Frindell, et al. Expires 3 September 2026 [Page 39] + +Internet-Draft WebTransport-H3 March 2026 + + + Victor Vasiliev + Google + Email: vasilvv@google.com + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Frindell, et al. Expires 3 September 2026 [Page 40] diff --git a/docs/draft-tikhonov-httpbis-bat-00.txt b/docs/draft-tikhonov-httpbis-bat-00.txt new file mode 100644 index 000000000..4f8c4e987 --- /dev/null +++ b/docs/draft-tikhonov-httpbis-bat-00.txt @@ -0,0 +1,394 @@ +HTTP D. Tikhonov +Internet-Draft USA +Intended status: Experimental January 17, 2026 +Expires: July 20, 2026 + + + HTTP Datagram Bat: A Simple Testing Protocol for HTTP Datagrams + draft-tikhonov-httpbis-bat-00 + +Abstract + + This document describes a simple application protocol for testing + implementations of HTTP Datagrams (RFC 9297). BAT (Bidirectional + Attestation Test) defines a basic echo service that uses HTTP + Datagrams over an HTTP CONNECT request, providing a straightforward + method for verifying interoperability of HTTP Datagram + implementations across different HTTP versions. + +Status of This Memo + + This Internet-Draft is submitted in full conformance with the + provisions of BCP 78 and BCP 79. + + Internet-Drafts are working documents of the Internet Engineering + Task Force (IETF). Note that other groups may also distribute + working documents as Internet-Drafts. The list of current Internet- + Drafts is at https://datatracker.ietf.org/drafts/current/. + + Internet-Drafts are draft documents valid for a maximum of six months + and may be updated, replaced, or obsoleted by other documents at any + time. It is inappropriate to use Internet-Drafts as reference + material or to cite them other than as "work in progress." + + This Internet-Draft will expire on July 20, 2026. + +Copyright Notice + + Copyright (c) 2026 IETF Trust and the persons identified as the + document authors. All rights reserved. + + This document is subject to BCP 78 and the IETF Trust's Legal + Provisions Relating to IETF Documents + (https://trustee.ietf.org/license-info) in effect on the date of + publication of this document. Please review these documents + carefully, as they describe your rights and restrictions with respect + to this document. Code Components extracted from this document must + include Revised BSD License text as described in Section 4.e of the + Trust Legal Provisions and are provided without warranty as described + in the Revised BSD License. + + + +Tikhonov Expires July 20, 2026 [Page 1] + +Internet-Draft BAT January 2026 + + +Table of Contents + + 1. Introduction . . . . . . . . . . . . . . . . . . . . . . . . 2 + 1.1. Notational Conventions . . . . . . . . . . . . . . . . . 3 + 2. Protocol Overview . . . . . . . . . . . . . . . . . . . . . . 3 + 3. Establishing a BAT Session . . . . . . . . . . . . . . . . . 3 + 3.1. Extended CONNECT Request . . . . . . . . . . . . . . . . 4 + 3.2. Response . . . . . . . . . . . . . . . . . . . . . . . . 4 + 4. Protocol Behavior . . . . . . . . . . . . . . . . . . . . . . 4 + 4.1. Client Behavior . . . . . . . . . . . . . . . . . . . . . 4 + 4.2. Server Behavior . . . . . . . . . . . . . . . . . . . . . 5 + 5. Error Handling . . . . . . . . . . . . . . . . . . . . . . . 5 + 6. Security Considerations . . . . . . . . . . . . . . . . . . . 5 + 7. IANA Considerations . . . . . . . . . . . . . . . . . . . . . 6 + 7.1. Registration of the BAT Protocol . . . . . . . . . . . . 6 + 8. References . . . . . . . . . . . . . . . . . . . . . . . . . 6 + 8.1. Normative References . . . . . . . . . . . . . . . . . . 6 + 8.2. Informative References . . . . . . . . . . . . . . . . . 7 + Appendix A. Acknowledgements . . . . . . . . . . . . . . . . . . 7 + Author's Address . . . . . . . . . . . . . . . . . . . . . . . . 7 + +1. Introduction + + HTTP Datagrams [RFC9297] provide a mechanism for conveying + multiplexed, potentially unreliable datagrams inside HTTP + connections. HTTP Datagrams can use QUIC DATAGRAM frames [RFC9221] + for unreliable delivery in HTTP/3 [RFC9114]. This document defines + BAT only for HTTP/3. + + While RFC 9297 defines the framework for HTTP Datagrams, testing + interoperability between implementations requires a simple + application protocol with well-defined semantics. This document + defines BAT (Bidirectional Attestation Test), a minimal echo service + that operates over HTTP CONNECT requests and uses HTTP Datagrams for + bidirectional communication. + + BAT provides implementers with: + + o A simple protocol to verify HTTP Datagram support in HTTP/3 + + o A mechanism to test QUIC DATAGRAM frames (in HTTP/3) + + o A foundation for progressive testing of more complex HTTP Datagram + applications + + + + +Tikhonov Expires July 20, 2026 [Page 2] + +Internet-Draft BAT January 2026 + + + BAT is designed as a testing and development tool. It is not + intended for production use or as a general-purpose echo service. + This version of BAT is defined only for HTTP/3. + +1.1. Notational Conventions + + The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", + "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and + "OPTIONAL" in this document are to be interpreted as described in + BCP 14 [RFC2119] [RFC8174] when, and only when, they appear in all + capitals, as shown here. + +2. Protocol Overview + + BAT is built on top of HTTP CONNECT extended requests and HTTP + Datagrams. The protocol operates as follows: + + 1. Client sends an extended CONNECT request to establish a BAT + session + + 2. Server accepts the request with a 2xx response + + 3. Client sends HTTP Datagrams with arbitrary payload data + + 4. Server echoes each received HTTP Datagram back to the client with + the same payload + + 5. Either endpoint may close the connection at any time + + The protocol supports concurrent datagrams and does not impose + ordering requirements on responses. + +3. Establishing a BAT Session + + A BAT session is established using an extended CONNECT request. + The protocol is identified using the ":protocol" pseudo-header field + with the value "bat". + + Only implementations of the final, published RFC can identify + themselves as "bat". Until such an RFC exists, implementations + MUST NOT identify themselves using this string. + + *RFC Editor's Note:* Please remove the following prior to + publication of a final version of this document. + + Implementations of draft versions of the protocol MUST add the string + "-" and the corresponding draft number to the identifier. For + example, draft-tikhonov-httpbis-bat-00 is identified using the + string "bat-00". + + + +Tikhonov Expires July 20, 2026 [Page 3] + +Internet-Draft BAT January 2026 + + +3.1. Extended CONNECT Request + + To establish a BAT session, a client sends an extended CONNECT + request [RFC8441] [RFC9220] with the following properties: + + o The ":method" pseudo-header field is set to "CONNECT" + + o The ":protocol" pseudo-header field is set to "bat-00" + + o The ":scheme" and ":path" pseudo-header fields SHOULD be included + and set to appropriate values + + o The ":authority" pseudo-header field contains the server's + authority + + Example request (HTTP/3 format): + + :method = CONNECT + :protocol = bat-00 + :scheme = https + :path = /bat + :authority = example.com + +3.2. Response + + If the server supports BAT and the request is acceptable, it + responds with a 2xx status code (typically 200). The server MUST + negotiate HTTP Datagrams as specified in Section 2 of [RFC9297]. + + For HTTP/3, this requires the SETTINGS_H3_DATAGRAM setting. + + If SETTINGS_H3_DATAGRAM is not negotiated with a value of 1, the + server MUST reject the request with a non-2xx response. A server + that does not support BAT or HTTP Datagrams SHOULD respond with + status code 501 (Not Implemented). A client MUST treat a 2xx + response without negotiated SETTINGS_H3_DATAGRAM as a protocol + error and close the connection. + + Example successful response: + + :status = 200 + +4. Protocol Behavior + + HTTP Datagrams in BAT are sent using QUIC DATAGRAM frames [RFC9221] + as specified in Section 2.1 of [RFC9297]. QUIC DATAGRAM frames MUST + be negotiated via the SETTINGS_H3_DATAGRAM setting. When an HTTP + Datagram payload is larger than the maximum size allowed by QUIC + DATAGRAM frames, endpoints MAY send DATAGRAM capsules using the + Capsule Protocol [RFC9297] on the CONNECT stream. Endpoints MUST NOT + send any other data on the CONNECT stream after the headers. + +4.1. Client Behavior + + Once the BAT session is established, the client MAY send HTTP + Datagrams with arbitrary payload data. The client MUST send HTTP + Datagrams using QUIC DATAGRAM frames when the payload fits within the + maximum QUIC DATAGRAM size. When the payload exceeds that limit, the + client MAY send a DATAGRAM capsule on the CONNECT stream. The client + MUST NOT send any other data on the CONNECT stream after the headers. + The client SHOULD track which datagrams have been sent to correlate + with received echoes. + + + + +Tikhonov Expires July 20, 2026 [Page 4] + +Internet-Draft BAT January 2026 + + + The client SHOULD implement a timeout mechanism to detect lost + datagrams (particularly relevant when using QUIC DATAGRAM frames in + HTTP/3, which provide unreliable delivery). + +4.2. Server Behavior + + The server MUST echo back each received HTTP Datagram with the exact + same payload to the client. The server MUST NOT modify the payload + contents. + + The server MUST send echoed HTTP Datagrams using QUIC DATAGRAM frames + when the payload fits within the maximum QUIC DATAGRAM size. If the + payload does not fit, the server MUST send a DATAGRAM capsule instead. + + The server SHOULD process datagrams as quickly as possible to + minimize round-trip latency measurements. + + If the server receives malformed HTTP Datagrams, it SHOULD terminate + the request stream as specified in Section 2 of [RFC9297]. For + HTTP/3, this means aborting the stream with error code + H3_DATAGRAM_ERROR (0x33). + +5. Error Handling + + Implementations MUST follow the error handling procedures defined in + [RFC9297]. + + Clients MUST NOT send any data on the CONNECT stream itself other + than DATAGRAM capsules as described in Section 4. + + If the server receives stream data (as opposed to HTTP Datagrams) + after accepting the BAT connection, it MUST treat this as a + stream error. + + Either endpoint may close the connection at any time. When using + HTTP/3, graceful shutdown should use the GOAWAY frame mechanism. + +6. Security Considerations + + BAT is intended as a testing protocol and inherits the security + considerations of HTTP Datagrams [RFC9297] and HTTP CONNECT + [RFC9110]. + + Servers SHOULD implement rate limiting and maximum datagram size + restrictions to prevent resource exhaustion attacks. + + Servers SHOULD implement authentication and authorization mechanisms + before accepting BAT connections, as the protocol could be + abused for reflection attacks or bandwidth amplification. + + + +Tikhonov Expires July 20, 2026 [Page 5] + +Internet-Draft BAT January 2026 + + + The echo behavior means that any payload sent by a client will be + reflected back. Applications should be aware that this could leak + information if not properly isolated in testing environments. + + BAT does not provide confidentiality, integrity, or + authentication of the datagram payload itself beyond what is provided + by the underlying HTTP connection (typically TLS). + +7. IANA Considerations + +7.1. Registration of the BAT Protocol + + This document registers the "bat" protocol in the "HTTP Upgrade + Tokens" registry established by [RFC9110]. + + The "bat" label identifies the BAT protocol defined in this + document: + + Protocol: BAT + + Identification: bat + + Specification: This document + + Expected Usage: LIMITED USE + + Security Considerations: See Section 6 of this document + +8. References + +8.1. Normative References + + [RFC2119] Bradner, S., "Key words for use in RFCs to Indicate + Requirement Levels", BCP 14, RFC 2119, + DOI 10.17487/RFC2119, March 1997, + . + + [RFC8174] Leiba, B., "Ambiguity of Uppercase vs Lowercase in RFC + 2119 Key Words", BCP 14, RFC 8174, DOI 10.17487/RFC8174, + May 2017, . + + [RFC8441] McManus, P., "Bootstrapping WebSockets with HTTP/2", + RFC 8441, DOI 10.17487/RFC8441, September 2018, + . + + [RFC9110] Fielding, R., Ed., Nottingham, M., Ed., and J. Reschke, + Ed., "HTTP Semantics", STD 97, RFC 9110, + DOI 10.17487/RFC9110, June 2022, + + + +Tikhonov Expires July 20, 2026 [Page 6] + +Internet-Draft BAT January 2026 + + + . + + [RFC9114] Bishop, M., Ed., "HTTP/3", RFC 9114, DOI 10.17487/RFC9114, + June 2022, . + + [RFC9220] Hamilton, R., "Bootstrapping WebSockets with HTTP/3", + RFC 9220, DOI 10.17487/RFC9220, June 2022, + . + + [RFC9221] Pauly, T., Kinnear, E., and D. Schinazi, "An Unreliable + Datagram Extension to QUIC", RFC 9221, + DOI 10.17487/RFC9221, March 2022, + . + + [RFC9297] Schinazi, D. and L. Pardue, "HTTP Datagrams and the + Capsule Protocol", RFC 9297, DOI 10.17487/RFC9297, August + 2022, . + +8.2. Informative References + + None. + +Appendix A. Acknowledgements + + This protocol design is inspired by SiDUCK [draft-pardue-quic- + siduck-00] by Lucas Pardue, which provides a similar testing protocol + for QUIC DATAGRAM frames. + + Thanks to the authors of RFC 9297 for defining the HTTP Datagram + framework. + +Author's Address + + Dmitri Tikhonov + USA + + Email: dtikhonov@example.com + + + + + +Tikhonov Expires July 20, 2026 [Page 7] diff --git a/docs/index.rst b/docs/index.rst index 626dfc7fa..86c4fce69 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -63,6 +63,7 @@ Contents gettingstarted tutorial + webtransport apiref devel internals diff --git a/docs/internals.rst b/docs/internals.rst index be218c495..97e2e6993 100644 --- a/docs/internals.rst +++ b/docs/internals.rst @@ -961,6 +961,20 @@ ERROR should never happen -- this indicates a bug or maybe failure to allocate memory -- and so the connection is aborted in that case. If everything is OK, the while loop goes on. +When RESET_STREAM_AT reliable delivery is enabled for a stream (see +``lsquic_stream_set_reliable_size``), we must ensure that the reliable +prefix bytes are never placed into buffered packets. While buffered +packets are convenient, they can be elided wholesale when a stream is +reset. To avoid dropping the reliable prefix, the stream write path +asks the send controller for a scheduled packet while the stream send +offset is below the reliable size. After the reliable prefix is +packetized, normal buffering resumes. + +Note that the reliable size is currently stored as a uint8_t and a +value of zero means RESET_STREAM_AT is not used. This keeps the feature +scoped to small WebTransport headers and does not support a reliable +size of zero as described by the draft. + The ``seen_ok`` check is used to place the connection on the tickable list on the first successfully packetized STREAM frame. This is so that if the packet is buffered (meaning that the writing is occurring outside of diff --git a/docs/tutorial.rst b/docs/tutorial.rst index 7a19a0656..66eb8510d 100644 --- a/docs/tutorial.rst +++ b/docs/tutorial.rst @@ -932,7 +932,7 @@ is parsed to see which setting to alter. while (/* getopt */) { - case 'o': /* For example: -o version=h3-27 -o cc_algo=2 */ + case 'o': /* Example: -o version=h3-27 -o cc_algo=2 */ if (!settings_initialized) { lsquic_engine_init_settings(&settings, cert_file || key_file ? LSENG_SERVER : 0); @@ -962,6 +962,12 @@ buffer is used to log a configuration error. Finally, the settings struct is pointed to by the engine API struct before the engine constructor is called. +The example programs also expose write scheduler settings via ``-o``: + +- ``-o write_sched_strategy=0|1`` (0=fixed, 1=DRR) +- ``-o write_datagram_prio=0..4`` (fixed mode) +- ``-o write_datagram_share=0.0..1.0`` (DRR mode) + Logging ======= diff --git a/docs/webtransport.rst b/docs/webtransport.rst new file mode 100644 index 000000000..f3e3fb5b1 --- /dev/null +++ b/docs/webtransport.rst @@ -0,0 +1,411 @@ +************ +WebTransport +************ + +.. highlight:: c + +This document describes how to use LSQUIC's WebTransport support as an +application guide. It focuses on the control flow and lifecycle of a +WebTransport session. For low-level HTTP Datagram details, see +:ref:`apiref-http-datagrams`. For engine settings, see +:ref:`apiref-engine-settings`. + +Current Status +============== + +WebTransport support is usable today, with the following +important limits: + +- HTTP/3 only. +- One WebTransport session per QUIC connection. +- Per-session WebTransport flow control is deferred. +- Compatibility mode may accept some draft-14 peers and some partial + draft-15 peers. + +If your application needs WebTransport datagrams, enable both +WebTransport and HTTP Datagrams in engine settings. + +Required Includes +================= + +Include both headers: + +:: + + #include "lsquic.h" + #include "lsquic_wt.h" + +Recommended Engine Settings +=========================== + +At minimum, enable WebTransport itself: + +:: + + settings.es_webtransport = 1; + +Applications using WebTransport must also use: + +:: + + settings.es_http_datagrams = 1; + settings.es_reset_stream_at = 1; + settings.es_max_webtransport_sessions = 1; + +``es_http_datagrams`` is required for WebTransport datagrams. +``es_reset_stream_at`` is required by the current WebTransport support. +``es_max_webtransport_sessions`` must be set to 1. + +WebTransport Model In LSQUIC +============================ + +LSQUIC treats a WebTransport session as something that takes over an HTTP/3 +Extended CONNECT stream. + +The important points are: + +- Your application first sees the CONNECT stream as a normal HTTP stream. +- Your application either rejects the request using ``lsquic_wt_reject()`` + or hands the stream to WebTransport using ``lsquic_wt_accept()``. +- Once ``lsquic_wt_accept()`` returns 0, the CONNECT stream belongs to the + WebTransport layer. Application code must stop using that stream for + normal I/O or readiness management. +- ``lsquic_wt_accept()`` does not return a session handle. Instead, it + transfers ownership of the CONNECT stream to WebTransport. +- The session becomes usable in ``wti_on_session_open()``. +- If acceptance later fails, ``wti_on_session_rejected()`` is called. +- If a usable session later ends, ``wti_on_session_close()`` is called. + +This matters because peer HTTP/3 SETTINGS may not be known yet when the +CONNECT request arrives. LSQUIC handles that internally. Your application +does not need to retry acceptance later. + +The WebTransport Callback Table +=============================== + +WebTransport uses a separate callback table: + +:: + + static const struct lsquic_webtransport_if wt_if = { + .wti_on_session_open = on_wt_session_open, + .wti_on_session_rejected = on_wt_session_rejected, + .wti_on_session_close = on_wt_session_close, + .wti_on_uni_stream = on_wt_uni_stream, + .wti_on_bidi_stream = on_wt_bidi_stream, + .wti_on_stream_read = on_wt_stream_read, + .wti_on_stream_write = on_wt_stream_write, + .wti_on_stream_close = on_wt_stream_close, + .wti_on_stream_ss_code = on_wt_stream_ss_code, + .wti_on_datagram_read = on_wt_datagram_read, + .wti_on_datagram_write = on_wt_datagram_write, + .wti_on_stream_fin = on_wt_stream_fin, + .wti_on_stream_reset = on_wt_stream_reset, + .wti_on_stop_sending = on_wt_stop_sending, + }; + +Most applications will use only a subset at first: + +- session open / rejected / close; +- unidirectional and bidirectional stream creation; +- stream read / write / close; and +- datagram read / write if datagrams are needed. + +Server-Side Flow +================ + +The server starts with a normal HTTP/3 CONNECT request. + +1. Parse the request headers. +2. Apply application policy. +3. If the request is not acceptable, call ``lsquic_wt_reject()``. +4. If the request is acceptable, fill ``struct lsquic_wt_accept_params`` and + call ``lsquic_wt_accept()``. +5. Stop touching the CONNECT stream after successful acceptance. +6. Wait for ``wti_on_session_open()``. + +A minimal acceptance flow looks like this: + +:: + + static int + handle_wt_connect(lsquic_stream_t *stream, + const struct lsquic_wt_connect_info *info) + { + struct lsquic_wt_accept_params params; + + if (!path_is_allowed(info->wtci_path)) + { + lsquic_wt_reject(stream, 404, + "no handler for this path", + sizeof("no handler for this path") - 1); + lsquic_stream_close(stream); + return -1; + } + + memset(¶ms, 0, sizeof(params)); + params.wtap_status = 200; + params.wtap_wt_if = &wt_if; + params.wtap_wt_if_ctx = &app_ctx; + params.wtap_connect_info = info; + + if (0 != lsquic_wt_accept(stream, ¶ms)) + return -1; + + return 0; + } + +A few notes: + +- ``wtap_wt_if`` points to the WebTransport callback table. +- ``wtap_wt_if_ctx`` is passed to ``wti_on_session_open()`` and + ``wti_on_session_rejected()``. +- ``wtap_connect_info`` lets the WebTransport layer keep request metadata. +- ``wtap_status`` defaults to 200 when zero; setting it explicitly makes the + code easier to read. +- ``wtap_extra_resp_headers`` can add extra response headers to the CONNECT + 2xx response. +- ``wtap_sess_ctx`` can supply a fixed session context if the application + does not want to allocate one in ``wti_on_session_open()``. + +``struct lsquic_wt_accept_params`` also lets the application set per-session +WT datagram queue limits, default queue-full policy, and default datagram +send mode. + +Request Validation +------------------ + +Application code is still responsible for request validation before calling +``lsquic_wt_accept()``. In practice this means checking: + +- ``:method`` is ``CONNECT``; +- ``:protocol`` is a supported WebTransport protocol token; +- ``:scheme`` is ``https``; +- ``:authority`` and ``:path`` are present; +- any application-specific path or protocol constraints; and +- ``Origin`` policy, if you use one. + +Origin validation is intentionally application-owned. + +Client-Side Flow +================ + +The client also starts with a normal HTTP/3 stream. + +1. Create a stream. +2. Send Extended CONNECT headers. +3. Read the response headers. +4. If the response is not successful, treat it as a CONNECT failure. +5. If the response is successful, fill ``struct lsquic_wt_accept_params`` and + call ``lsquic_wt_accept()`` on the control stream. +6. Wait for ``wti_on_session_open()``. + +A minimal client-side CONNECT request includes: + +- ``:method = CONNECT`` +- ``:protocol = webtransport`` +- ``:scheme = https`` +- ``:authority = ...`` +- ``:path = ...`` + +After the response is accepted, the client hands the control stream to the +WT layer exactly like the server does. + +The important symmetry is this: once the CONNECT response is successful, +both peers use the same WebTransport session callbacks. + +Session Open, Rejection, and Close +================================== + +There are three distinct application-visible outcomes: + +Session opened +-------------- + +``wti_on_session_open()`` is called when the session becomes usable. +Return a session context pointer from this callback if you want one +associated with the session. + +A typical callback looks like this: + +:: + + static lsquic_wt_session_ctx_t * + on_wt_session_open(void *ctx, lsquic_wt_session_t *sess, + const struct lsquic_wt_connect_info *info) + { + struct my_wt_session *s; + + s = calloc(1, sizeof(*s)); + if (!s) + return NULL; + + s->sess = sess; + s->path = info->wtci_path; + return (lsquic_wt_session_ctx_t *) s; + } + +Session rejected +---------------- + +``wti_on_session_rejected()`` is called when LSQUIC took ownership of the +CONNECT stream but the session never became usable. + +This includes deferred-accept failure cases, for example when peer HTTP/3 +capabilities are resolved later and WebTransport cannot be enabled. + +The callback receives: + +- the application context passed via ``wtap_wt_if_ctx``; +- request metadata via ``wtci_*`` fields; and +- an HTTP status code, or 0 if no response was sent. + +Session closed +-------------- + +``wti_on_session_close()`` is called only for sessions that opened. + +Call ``lsquic_wt_close()`` to close a live session with an application error +code and optional reason bytes: + +:: + + lsquic_wt_close(sess, 0, NULL, 0); + +or: + +:: + + lsquic_wt_close(sess, MY_APP_ERR_BAD_STATE, + reason_buf, reason_len); + +The close callback runs when the session is actually finished, not when close +starts. + +Working With WT Streams +======================= + +Open outgoing WT streams using: + +- ``lsquic_wt_open_uni()`` +- ``lsquic_wt_open_bidi()`` + +Incoming peer-initiated streams are delivered using: + +- ``wti_on_uni_stream()`` +- ``wti_on_bidi_stream()`` + +These callbacks should allocate and return the usual ``lsquic_stream_ctx_t`` +for the new WT data stream. + +After that, ordinary stream callbacks apply: + +- ``wti_on_stream_read()`` +- ``wti_on_stream_write()`` +- ``wti_on_stream_close()`` + +Optional stream lifecycle callbacks are also available: + +- ``wti_on_stream_fin()`` +- ``wti_on_stream_reset()`` +- ``wti_on_stop_sending()`` + +WT stream metadata helpers are available when the application needs them: + +- ``lsquic_wt_session_from_stream()`` +- ``lsquic_wt_stream_get_ctx()`` +- ``lsquic_wt_stream_dir()`` +- ``lsquic_wt_stream_initiator()`` +- ``lsquic_stream_is_webtransport_session()`` +- ``lsquic_stream_is_webtransport_client_bidi_stream()`` +- ``lsquic_stream_get_webtransport_session_stream_id()`` + +If your application needs to terminate a WT data stream explicitly, use: + +- ``lsquic_wt_stream_reset()`` +- ``lsquic_wt_stream_stop_sending()`` + +Working With WT Datagrams +========================= + +WT datagrams are session-scoped and use the HTTP Datagram machinery +internally. That means ``es_http_datagrams`` must be enabled. + +The simplest send path is: + +:: + + if (0 > lsquic_wt_send_datagram(sess, buf, len)) + ; /* queue full, session closing, or datagrams unavailable */ + +If the application wants callback-driven sending, enable datagram write +interest: + +:: + + lsquic_wt_want_datagram_write(sess, 1); + +That causes ``wti_on_datagram_write()`` to be called when the session should +try to send. The callback receives the current maximum datagram size. + +Incoming datagrams arrive in ``wti_on_datagram_read()``. + +If the application needs explicit queue-full policy or send mode, use +``lsquic_wt_send_datagram_ex()``. Otherwise, ``lsquic_wt_send_datagram()`` +uses the session defaults. + +The default send mode is ``LSQUIC_HTTP_DG_SEND_DEFAULT``. In that mode, +LSQUIC may send via QUIC DATAGRAM frames or fall back to Capsules, depending +on negotiated capability and payload size. See :ref:`apiref-http-datagrams` +for transport details. + +Capability Queries +================== + +LSQUIC exposes a few connection-level helpers: + +- ``lsquic_wt_peer_settings_received()`` +- ``lsquic_wt_peer_supports()`` +- ``lsquic_wt_peer_draft()`` +- ``lsquic_wt_peer_connect_protocol()`` + +These are useful for diagnostics and policy, but a typical accept path does +not need to pre-check them before calling ``lsquic_wt_accept()``. + +If you want notification when effective HTTP capabilities become known, use +the ordinary stream callback table's ``on_http_caps`` callback. The +``LSQUIC_HTTP_CAP_WEBTRANSPORT`` bit is best-effort on this branch and may be +set in compatibility mode. + +Practical Rules +=============== + +Do not: + +- keep using the CONNECT stream after ``lsquic_wt_accept()`` returns 0; +- assume that ``lsquic_wt_accept()`` means the session is already open; +- assume WT datagrams require QUIC DATAGRAM support, because Capsule fallback + may be used; or +- assume more than one WT session can be active on a connection. + +Do: + +- validate request policy before accepting on the server; +- treat ``wti_on_session_open()`` as the point where the session becomes + usable; +- handle ``wti_on_session_rejected()`` separately from + ``wti_on_session_close()``; and +- enable HTTP Datagrams if you plan to use WT datagrams. + +Worked Example +============== + +The Devious Baton example in the ``bin`` directory is the best in-tree WT +example today: + +- ``bin/http_server.c`` shows CONNECT request handling. +- ``bin/devious_baton.c`` shows both server-side and client-side WT handoff, + stream callbacks, datagram callbacks, and session close handling. +- ``bin/baton_client.c`` exercises the client path. + +Use those files as the concrete companion to this guide. diff --git a/include/lsquic.h b/include/lsquic.h index 6d97ae92a..1299638e2 100644 --- a/include/lsquic.h +++ b/include/lsquic.h @@ -160,6 +160,43 @@ enum lsquic_hsk_status * process events. * */ + +/** + * HTTP Datagram send mode controls transport selection. + * See apiref.rst for detailed explanation of each mode. + */ +enum lsquic_http_dg_send_mode +{ + /* Automatic: use QUIC DATAGRAM if available and fits, else Capsule */ + LSQUIC_HTTP_DG_SEND_DEFAULT = 0, + /* Force QUIC DATAGRAM only (unreliable, unordered, low latency) */ + LSQUIC_HTTP_DG_SEND_DATAGRAM, + /* Force Capsule Protocol only (reliable, ordered) */ + LSQUIC_HTTP_DG_SEND_CAPSULE, +}; + +enum lsquic_write_sched_strategy +{ + LSQWSS_FIXED, + LSQWSS_DRR, +}; + +enum lsquic_http_cap_flags +{ + LSQUIC_HTTP_CAP_DATAGRAMS = 1 << 0, + LSQUIC_HTTP_CAP_CONNECT_PROTOCOL = 1 << 1, + LSQUIC_HTTP_CAP_WEBTRANSPORT = 1 << 2, +}; + +struct lsquic_http_caps +{ + uint32_t lhc_flags; +}; + +typedef int (*lsquic_http_dg_consume_f)(lsquic_stream_t *s, const void *buf, + size_t sz, + enum lsquic_http_dg_send_mode mode); + struct lsquic_stream_if { /** @@ -191,15 +228,39 @@ struct lsquic_stream_if { /* Called when datagram is ready to be written */ ssize_t (*on_dg_write)(lsquic_conn_t *c, void *, size_t); /* Called when datagram is read from a packet. This callback is required - * when es_datagrams is true. Take care to process it quickly, as this - * is called during lsquic_engine_packet_in(). + * when es_datagrams is true and HTTP Datagrams are not enabled. Take care + * to process it quickly, as this is called during + * lsquic_engine_packet_in(). */ void (*on_datagram)(lsquic_conn_t *, const void *buf, size_t); + /** + * Called when HTTP Datagram (RFC 9297) is ready to be written. + * Must call consume_datagram exactly once to supply the payload. + * Return 0 on success, -1 on error. + * max_quic_payload is the maximum size that fits in a QUIC DATAGRAM frame. + * See apiref.rst "HTTP Datagrams" section for details and examples. + */ + int (*on_http_dg_write)(lsquic_stream_t *s, lsquic_stream_ctx_t *h, + size_t max_quic_payload, lsquic_http_dg_consume_f consume_datagram); + /** + * Called when an HTTP Datagram (RFC 9297) payload is received. + * For QUIC DATAGRAM frames, this can be invoked during packet processing. + * For capsule-carried datagrams, it is invoked during stream read events. + * Process payload quickly; buffer is only valid during callback. + */ + void (*on_http_dg_read)(lsquic_stream_t *s, lsquic_stream_ctx_t *h, + const void *buf, size_t); /* This callback in only called in client mode */ /** * When handshake is completed, this optional callback is called. */ void (*on_hsk_done)(lsquic_conn_t *c, enum lsquic_hsk_status s); + /** + * Called when peer HTTP SETTINGS are processed and effective HTTP + * capabilities are known. + * This callback is optional. + */ + void (*on_http_caps)(lsquic_conn_t *c, const struct lsquic_http_caps *); /** * When client receives a token in NEW_TOKEN frame, this callback is called. * The callback is optional. @@ -243,6 +304,16 @@ struct lsquic_stream_if { void (*on_hset_in) (lsquic_stream_t *s, lsquic_stream_ctx_t *h); }; + +struct lsquic_http_dg_if +{ + int (*on_http_dg_write)(lsquic_stream_t *s, lsquic_stream_ctx_t *h, + size_t max_quic_payload, lsquic_http_dg_consume_f consume_datagram); + void (*on_http_dg_read)(lsquic_stream_t *s, lsquic_stream_ctx_t *h, + const void *buf, size_t); +}; + + struct ssl_ctx_st; struct ssl_st; struct ssl_session_st; @@ -414,6 +485,9 @@ typedef struct ssl_ctx_st * (*lsquic_lookup_cert_f)( /** Turn on timestamp extension by default */ #define LSQUIC_DF_TIMESTAMPS 1 +/** Turn off RESET_STREAM_AT extension by default */ +#define LSQUIC_DF_RESET_STREAM_AT 0 + /** default anti-amplification factor is 3 */ #define LSQUIC_DF_AMP_FACTOR 3 @@ -429,6 +503,15 @@ typedef struct ssl_ctx_st * (*lsquic_lookup_cert_f)( /** Turn off datagram extension by default */ #define LSQUIC_DF_DATAGRAMS 0 +/** Turn off HTTP Datagrams by default */ +#define LSQUIC_DF_HTTP_DATAGRAMS 0 + +/** Default maximum HTTP Datagram capsule read buffer size. */ +#define LSQUIC_DF_HTTP_DG_MAX_CAPSULE_READ_SIZE (10 * 1024) + +/** Default maximum HTTP Datagram capsule write buffer size. */ +#define LSQUIC_DF_HTTP_DG_MAX_CAPSULE_WRITE_SIZE (10 * 1024) + /** Assume optimistic NAT by default. */ #define LSQUIC_DF_OPTIMISTIC_NAT 1 @@ -477,13 +560,21 @@ typedef struct ssl_ctx_st * (*lsquic_lookup_cert_f)( /** Transport parameter sanity checks are performed by default. */ #define LSQUIC_DF_CHECK_TP_SANITY 1 -#if LSQUIC_WEBTRANSPORT_SERVER_SUPPORT -/** Turn off webtransport extension for server by default */ +/** Turn off WebTransport extension by default. */ #define LSQUIC_DF_WEBTRANSPORT_SERVER 0 -/** Default allowed server webtransport streams count */ -#define LSQUIC_DF_MAX_WEBTRANSPORT_SERVER_STREAMS 10 -#endif +/** Default allowed WebTransport sessions count per connection. */ +#define LSQUIC_DF_MAX_WEBTRANSPORT_SESSIONS 1 + +/** Default write scheduler strategy. */ +#define LSQUIC_DF_WRITE_SCHED_STRATEGY LSQWSS_FIXED + +/** Default datagram class priority in fixed scheduler. */ +#define LSQUIC_DF_WRITE_DATAGRAM_PRIO 2 + +/** Default DRR datagram share. */ +#define LSQUIC_DF_WRITE_DATAGRAM_SHARE 0.30f + struct lsquic_engine_settings { /** * This is a bit mask wherein each bit corresponds to a value in @@ -971,6 +1062,13 @@ struct lsquic_engine_settings { */ int es_timestamps; + /** + * Enable RESET_STREAM_AT extension. Allowed values are 0 and 1. + * + * Default value is @ref LSQUIC_DF_RESET_STREAM_AT + */ + int es_reset_stream_at; + /** * Maximum packet size we are willing to receive. This is sent to * peer in transport parameters: the library does not enforce this @@ -1041,6 +1139,33 @@ struct lsquic_engine_settings { */ int es_datagrams; + /** + * Enable HTTP Datagram support (RFC 9297) for HTTP/3. + * HTTP Datagrams can be sent via QUIC DATAGRAM frames or Capsule Protocol. + * Different from es_datagrams (raw QUIC datagrams); see apiref.rst. + * + * Default value is @ref LSQUIC_DF_HTTP_DATAGRAMS + */ + int es_http_datagrams; + + /** + * Maximum buffer size for reading encapsulated HTTP Datagram payloads. + * Per-stream limit for Capsule Protocol payloads (not QUIC DATAGRAMs). + * Zero means capsule payloads are not accepted. + * + * Default value is @ref LSQUIC_DF_HTTP_DG_MAX_CAPSULE_READ_SIZE + */ + unsigned es_http_dg_max_capsule_read_size; + + /** + * Maximum buffer size for writing encapsulated HTTP Datagram payloads. + * Per-stream limit for Capsule Protocol payloads (not QUIC DATAGRAMs). + * Zero means capsule payloads are not allowed. + * + * Default value is @ref LSQUIC_DF_HTTP_DG_MAX_CAPSULE_WRITE_SIZE + */ + unsigned es_http_dg_max_capsule_write_size; + /** * If set to true, changes in peer port are assumed to be due to a * benign NAT rebinding and path characteristics -- MTU, RTT, and @@ -1168,21 +1293,39 @@ struct lsquic_engine_settings { */ uint8_t es_preferred_address[24]; -#if LSQUIC_WEBTRANSPORT_SERVER_SUPPORT /** - * Enable datagram extension for http3 server. Allowed values are 0 and 1. + * Enable WebTransport support for this endpoint. Allowed values are 0 and 1. + * + * Default value is @ref LSQUIC_DF_WEBTRANSPORT_SERVER. + */ + int es_webtransport; + + /** + * Maximum number of WebTransport sessions allowed for a connection. * - * Default value is @ref LSQUIC_DF_WEBTRANSPORT_SERVER + * Default value is @ref LSQUIC_DF_MAX_WEBTRANSPORT_SESSIONS. */ - int es_webtransport_server; + unsigned es_max_webtransport_sessions; /** - * Maximum number of webtransport streams allowed by server for a connection. + * Outbound write scheduler strategy. * - * Default value is @ref LSQUIC_DF_MAX_WEBTRANSPORT_SERVER_STREAMS. + * Default value is @ref LSQUIC_DF_WRITE_SCHED_STRATEGY. */ - unsigned es_max_webtransport_server_streams; -#endif + unsigned es_write_sched_strategy; + + /** + * Datagram class priority in fixed scheduler mode. + * Lower value means earlier dispatch. + * + * Default value is @ref LSQUIC_DF_WRITE_DATAGRAM_PRIO. + */ + unsigned char es_write_datagram_prio; + + /** + * Datagram share used by DRR scheduler. Valid range is [0.0, 1.0]. + */ + float es_write_datagram_share; }; /* Initialize `settings' to default values */ @@ -1614,9 +1757,13 @@ lsquic_conn_close (lsquic_conn_t *); /** * Set whether you want to read from stream. If @param is_want is true, * @ref on_read() will be called when there is readable data in the - * stream. If @param is false, @ref on_read() will not be called. + * stream. If @param is_want is false, @ref on_read() will not be called. * - * Returns previous value of this flag. + * Returns previous value of this flag. On error, returns -1 and sets errno: + * + * EBADF The read side is shut down. + * EINVAL Reading from this stream is not allowed (for example, + * locally-initiated unidirectional IETF stream). */ int lsquic_stream_wantread (lsquic_stream_t *s, int is_want); @@ -1920,19 +2067,9 @@ lsquic_stream_set_http_prio (lsquic_stream_t *, */ lsquic_conn_t * lsquic_stream_conn(const lsquic_stream_t *s); -#if LSQUIC_WEBTRANSPORT_SERVER_SUPPORT -void -lsquic_stream_set_webtransport_session(lsquic_stream_t *s); - -int -lsquic_stream_is_webtransport_session (const lsquic_stream_t *s); - -int -lsquic_stream_is_webtransport_client_bidi_stream(const lsquic_stream_t *s); - +/* Record reliable size for RESET_STREAM_AT. Returns 0 on success. */ int -lsquic_stream_get_webtransport_session_stream_id(const lsquic_stream_t *s); -#endif +lsquic_stream_set_reliable_size (lsquic_stream_t *s, size_t sz); /** Get connection ID */ const lsquic_cid_t * @@ -1960,6 +2097,46 @@ lsquic_conn_get_min_datagram_size (lsquic_conn_t *); int lsquic_conn_set_min_datagram_size (lsquic_conn_t *, size_t sz); +/** + * Register per-stream HTTP Datagram callbacks. These take precedence over + * engine-wide on_http_dg_* callbacks. Pass NULL to clear. + * Returns 0 on success, -1 on error (sets errno). + */ +int +lsquic_stream_set_http_dg_if (lsquic_stream_t *, + const struct lsquic_http_dg_if *); + +/** + * Control whether stream is eligible to supply HTTP Datagram payloads. + * Call with is_want=1 to enable on_http_dg_write() callbacks. + * Returns previous value, or -1 on error. + * See apiref.rst "HTTP Datagrams" for usage examples. + */ +int +lsquic_stream_want_http_dg_write (lsquic_stream_t *, int is_want); + +/** + * Enable HTTP Datagram capsule processing on this stream. + * Required to receive capsule-carried HTTP Datagrams. + * Library takes over on_read to parse capsule framing; delivers payloads + * via on_http_dg_read(). May suppress on_write while flushing capsules. + * Call after successful Extended CONNECT response with Capsule-Protocol: ?1. + * Returns 0 on success, -1 on error (sets errno). + */ +int +lsquic_stream_set_http_dg_capsules (lsquic_stream_t *, int enable); + +/** + * Get maximum HTTP Datagram payload size for QUIC DATAGRAM frames. + * Returns 0 if HTTP Datagrams were not negotiated. + * Accounts for QUIC DATAGRAM overhead, context ID, and current PMTU. + * Value may change due to PMTU or stream ID growth; use SEND_DEFAULT for + * automatic fallback. + */ +size_t +lsquic_stream_get_max_http_dg_size (lsquic_stream_t *); + + struct lsquic_logger_if { /* Return number of bytes written/consumed; return value is ignored. */ int (*log_buf)(void *logger_ctx, const char *buf, size_t len); @@ -2166,6 +2343,7 @@ enum lsquic_conn_param * Pacing must be turned on via es_pace_packets in lsquic_engine_settings. */ LSQCP_MAX_PACING_RATE = 1, + /** * If you plan to call lsquic_conn_get_info(), set this value to true. * @@ -2182,6 +2360,48 @@ enum lsquic_conn_param * lsquic_conn_get_info() enables sampling if needed for functionality. */ LSQCP_ENABLE_BW_SAMPLER = 2, + + /** + * Whether peer HTTP/3 SETTINGS frame has been received. + * Type: uint64_t (0 or 1) + */ + LSQCP_WT_PEER_SETTINGS_RECEIVED, + + /** + * Whether peer currently satisfies WebTransport requirements. + * Type: uint64_t (0 or 1) + */ + LSQCP_WT_PEER_SUPPORTS, + + /** + * Draft version implied by peer's WebTransport SETTINGS codepoint. + * Type: uint64_t + */ + LSQCP_WT_PEER_DRAFT, + + /** + * Whether peer advertised SETTINGS_ENABLE_CONNECT_PROTOCOL=1. + * Type: uint64_t (0 or 1) + */ + LSQCP_WT_PEER_CONNECT_PROTOCOL, + + /** + * Outbound write scheduler strategy. + * Type: enum lsquic_write_sched_strategy + */ + LSQCP_WRITE_SCHED_STRATEGY, + + /** + * Datagram class priority in fixed scheduler mode. + * Type: unsigned + */ + LSQCP_WRITE_DATAGRAM_PRIO, + + /** + * DATAGRAM share in DRR mode. + * Type: float in [0.0, 1.0] + */ + LSQCP_WRITE_DATAGRAM_SHARE, }; struct lsquic_conn_info diff --git a/include/lsquic_wt.h b/include/lsquic_wt.h new file mode 100644 index 000000000..0f0b7fa41 --- /dev/null +++ b/include/lsquic_wt.h @@ -0,0 +1,283 @@ +/* Copyright (c) 2017 - 2026 LiteSpeed Technologies Inc. See LICENSE. */ + +#ifndef __LSQUIC_WT_H__ +#define __LSQUIC_WT_H__ + +/** + * @file + * WebTransport public API. Include lsquic.h before this file for type + * definitions. + */ + +#ifdef __cplusplus +extern "C" { +#endif + +/** Opaque WebTransport session handle. */ +typedef struct lsquic_wt_session lsquic_wt_session_t; + +/** WebTransport session context returned by callbacks. */ +typedef struct lsquic_wt_session_ctx lsquic_wt_session_ctx_t; + +enum lsquic_wt_stream_dir +{ + LSQWT_UNI, + LSQWT_BIDI, +}; + +enum lsquic_wt_stream_initiator +{ + LSQWT_CLIENT, + LSQWT_SERVER, +}; + +enum lsquic_wt_dg_drop_policy +{ + LSQWT_DG_FAIL_EAGAIN, + LSQWT_DG_DROP_OLDEST, + LSQWT_DG_DROP_NEWEST, +}; + +struct lsquic_wt_connect_info +{ + const char *wtci_authority; + const char *wtci_path; + const char *wtci_origin; /* optional */ + const char *wtci_protocol; /* application protocol, if present */ + unsigned wtci_draft; /* negotiated WebTransport draft version, if known */ +}; + +#define LSQUIC_WTAP_STATUS_DEFAULT 200 +#define LSQUIC_WTAP_MAX_DATAGRAM_QUEUE_COUNT_DEFAULT 64 +#define LSQUIC_WTAP_MAX_DATAGRAM_QUEUE_BYTES_DEFAULT (256 * 1024) +#define LSQUIC_WTAP_DATAGRAM_DROP_POLICY_DEFAULT LSQWT_DG_FAIL_EAGAIN +#define LSQUIC_WTAP_DATAGRAM_SEND_MODE_DEFAULT LSQUIC_HTTP_DG_SEND_DEFAULT + +struct lsquic_wt_accept_params +{ + /* Optional extra headers for CONNECT 2xx response. */ + const struct lsquic_http_headers *wtap_extra_resp_headers; + + /* Response status; default LSQUIC_WTAP_STATUS_DEFAULT; must be 2xx. */ + unsigned wtap_status; + + /* Per-session WebTransport callbacks. */ + const struct lsquic_webtransport_if *wtap_wt_if; + + /* Passed to wti_on_session_open and wti_on_session_rejected. */ + void *wtap_wt_if_ctx; + + /* Optional parsed CONNECT metadata. */ + const struct lsquic_wt_connect_info *wtap_connect_info; + + /* Optional fixed session ctx (if wti_on_session_open is not used). */ + lsquic_wt_session_ctx_t *wtap_sess_ctx; + + /* Queue item count limit; default when zero. */ + unsigned wtap_max_datagram_queue_count; + + /* Queue bytes limit; default when zero. */ + size_t wtap_max_datagram_queue_bytes; + + /* Default queue-full policy; default LSQUIC_WTAP_DATAGRAM_DROP_POLICY_DEFAULT. */ + enum lsquic_wt_dg_drop_policy wtap_datagram_drop_policy; + + /* Default send mode; default LSQUIC_WTAP_DATAGRAM_SEND_MODE_DEFAULT. */ + enum lsquic_http_dg_send_mode wtap_datagram_send_mode; +}; + +struct lsquic_webtransport_if +{ + /* Session became usable after WT took ownership of CONNECT stream. */ + lsquic_wt_session_ctx_t * + (*wti_on_session_open) (void *ctx, lsquic_wt_session_t *, + const struct lsquic_wt_connect_info *info); + + /* WT-owned CONNECT was rejected before a session became usable. + * Status is 0 if no HTTP response was sent. + */ + void + (*wti_on_session_rejected) (void *ctx, + const struct lsquic_wt_connect_info *info, + unsigned status, const char *reason, + size_t reason_len); + + /* Session closed (normal or error path). */ + void + (*wti_on_session_close) (lsquic_wt_session_t *, lsquic_wt_session_ctx_t *, + uint64_t code, const char *reason, + size_t reason_len); + + /* New peer-initiated WT unidirectional stream. */ + lsquic_stream_ctx_t * + (*wti_on_uni_stream) (lsquic_wt_session_t *, lsquic_stream_t *); + + /* New peer-initiated WT bidirectional stream. */ + lsquic_stream_ctx_t * + (*wti_on_bidi_stream) (lsquic_wt_session_t *, lsquic_stream_t *); + + /* Stream readable callback for WT data streams. */ + void + (*wti_on_stream_read) (lsquic_stream_t *, lsquic_stream_ctx_t *); + + /* Stream writeable callback for WT data streams. */ + void + (*wti_on_stream_write) (lsquic_stream_t *, lsquic_stream_ctx_t *); + + /* Stream close callback for WT data streams. */ + void + (*wti_on_stream_close) (lsquic_stream_t *, lsquic_stream_ctx_t *); + + /* Supplies STOP_SENDING code for outgoing STOP_SENDING frame. */ + uint64_t + (*wti_on_stream_ss_code) (lsquic_stream_t *, lsquic_stream_ctx_t *); + + /* Received WT datagram payload. */ + void + (*wti_on_datagram_read) (lsquic_wt_session_t *, const void *buf, + size_t len); + + /* Datagram write interest callback; app should enqueue/send now. */ + int + (*wti_on_datagram_write) (lsquic_wt_session_t *, + size_t max_datagram_size); + + /* FIN observed on stream. */ + void + (*wti_on_stream_fin) (lsquic_stream_t *, lsquic_stream_ctx_t *); + + /* RESET_STREAM observed on stream. */ + void + (*wti_on_stream_reset) (lsquic_stream_t *, lsquic_stream_ctx_t *, + uint64_t error_code); + + /* STOP_SENDING observed on stream. */ + void + (*wti_on_stop_sending) (lsquic_stream_t *, lsquic_stream_ctx_t *, + uint64_t error_code); +}; + +/** + * Accept WebTransport CONNECT. + * + * Applications are responsible for validating wtci_origin, if present, + * before accepting the session. A return value of 0 means ownership of + * the CONNECT stream has transferred to WT. The session may become usable + * immediately or later via wti_on_session_open(), or it may be rejected via + * wti_on_session_rejected(). + */ +int +lsquic_wt_accept (lsquic_stream_t *connect_stream, + const struct lsquic_wt_accept_params *params); + +/** Reject WebTransport CONNECT with non-2xx status. */ +int +lsquic_wt_reject (lsquic_stream_t *connect_stream, + unsigned status, const char *reason, size_t reason_len); + +/** Close a WebTransport session with an application error code. */ +int +lsquic_wt_close (lsquic_wt_session_t *sess, uint64_t code, + const char *reason, size_t reason_len); + +/** Query the QUIC connection that owns this session. */ +lsquic_conn_t * +lsquic_wt_session_conn (lsquic_wt_session_t *sess); + +/** Return the stream ID of the CONNECT control stream. */ +lsquic_stream_id_t +lsquic_wt_session_id (lsquic_wt_session_t *sess); + +/** Return whether peer HTTP/3 SETTINGS have been received. */ +int +lsquic_wt_peer_settings_received (lsquic_conn_t *conn); + +/** + * Return whether peer currently supports WebTransport on this connection. + * + * This is a best-effort capability check. On this branch it may become + * true in compatibility mode for draft-14 peers, or for peers that + * negotiate the core transport pieces but omit reset_stream_at or WT + * initial flow-control settings. + */ +int +lsquic_wt_peer_supports (lsquic_conn_t *conn); + +/** Return peer WebTransport draft version for this connection, if known. */ +unsigned +lsquic_wt_peer_draft (lsquic_conn_t *conn); + +/** Return whether peer enabled CONNECT protocol via HTTP/3 SETTINGS. */ +int +lsquic_wt_peer_connect_protocol (lsquic_conn_t *conn); + +/** Open a WebTransport unidirectional stream. */ +lsquic_stream_t * +lsquic_wt_open_uni (lsquic_wt_session_t *sess); + +/** Open a WebTransport bidirectional stream. */ +lsquic_stream_t * +lsquic_wt_open_bidi (lsquic_wt_session_t *sess); + +/** Map a WT stream back to its session. */ +lsquic_wt_session_t * +lsquic_wt_session_from_stream (lsquic_stream_t *stream); + +/** Return whether this is the CONNECT control stream for a WT session. */ +int +lsquic_stream_is_webtransport_session (const lsquic_stream_t *stream); + +/** Return whether this is a switched client-initiated WT bidirectional stream. */ +int +lsquic_stream_is_webtransport_client_bidi_stream (const lsquic_stream_t *stream); + +/** Get the CONNECT stream ID associated with a switched WT stream. */ +int +lsquic_stream_get_webtransport_session_stream_id ( + const lsquic_stream_t *stream, + lsquic_stream_id_t *stream_id); + +/** Return WT stream context (set by WT callbacks). */ +lsquic_stream_ctx_t * +lsquic_wt_stream_get_ctx (lsquic_stream_t *stream); + +/** Query WT stream direction. */ +enum lsquic_wt_stream_dir +lsquic_wt_stream_dir (const lsquic_stream_t *stream); + +/** Query WT stream initiator. */ +enum lsquic_wt_stream_initiator +lsquic_wt_stream_initiator (const lsquic_stream_t *stream); + +/** Send a WT datagram in session context. */ +ssize_t +lsquic_wt_send_datagram (lsquic_wt_session_t *sess, + const void *buf, size_t len); + +/** Send a WT datagram with explicit queue-full policy. */ +ssize_t +lsquic_wt_send_datagram_ex (lsquic_wt_session_t *sess, + const void *buf, size_t len, enum lsquic_wt_dg_drop_policy policy, + enum lsquic_http_dg_send_mode mode); + +/** Control WT datagram write callback interest. */ +int +lsquic_wt_want_datagram_write (lsquic_wt_session_t *sess, int is_want); + +/** Maximum datagram size for this session. */ +size_t +lsquic_wt_max_datagram_size (const lsquic_wt_session_t *sess); + +/** Reset a WT stream with an application error code. */ +int +lsquic_wt_stream_reset (lsquic_stream_t *stream, uint64_t error_code); + +/** Send STOP_SENDING on a WT stream with an application error code. */ +int +lsquic_wt_stream_stop_sending (lsquic_stream_t *stream, uint64_t error_code); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/reset_stream_at_send_plan.md b/reset_stream_at_send_plan.md new file mode 100644 index 000000000..1d8318a09 --- /dev/null +++ b/reset_stream_at_send_plan.md @@ -0,0 +1,59 @@ +RESET_STREAM_AT send-side implementation plan (options) + +Decision constraints +- Feature flag in engine settings; default OFF. +- Only send RESET_STREAM_AT if peer advertised reset_stream_at transport parameter. +- Focus is WebTransport: RESET_STREAM_AT is used for WT only. +- For WT streams, we intentionally allow retraction beyond the WT header; + reliable_size MUST be at least the WT header size, per the I-D. + (Comment to keep in code verbatim.) + +Option A: WebTransport-only, header-sized reliable_size +- Track per-WT stream header size. +- On reset of a WT data stream, send RESET_STREAM_AT with + reliable_size = wt_header_size (and final_size = final offset). +- Non-WT streams continue using RST_STREAM. + +Pros: matches WT requirement, minimal logic. +Cons: does not support partial-reliability beyond header for other protocols. + +Option B: WebTransport-only, header-sized reliable_size + optional override +- Same as Option A, but allow app/API to override reliable_size + (>= wt_header_size) for WT streams only. +- Default remains header size if no override. + +Pros: still WT-focused, allows future tuning without rework. +Cons: requires API surface + app wiring. + +Option C: Always send RESET_STREAM_AT when enabled (IETF only) +- If flag ON + peer TP present, use RESET_STREAM_AT for all IETF streams. +- For WT streams: reliable_size = wt_header_size. +- For non-WT streams: reliable_size = 0 (full retraction) or final_size + (no retraction) — needs a policy choice. + +Pros: simple single path in sender. +Cons: affects non-WT behavior; policy ambiguity. + +Common work items (all options) +- Settings flag (default OFF) and peer-support check. +- New send flag/state to choose RESET_STREAM_AT vs RST_STREAM in frame writer. +- Store send-side final_size, reliable_size, error_code for retransmission. +- Update STOP_SENDING handling to choose RESET_STREAM_AT when enabled. +- Update ACK handling for RESET_STREAM_AT frames (clear send flag on ack). +- Tests: verify correct frame chosen, correct fields, ack clears send flag, + and fallback to RST_STREAM when disabled or peer doesn’t support. + +Open questions +- Where/how to store WT header size (stream field vs per-session map)? +- How to identify WT streams in core (flag on stream vs external hook)? +- Should we allow RESET_STREAM_AT only when WT support is compiled in? + +Commit. Then: + +I like Option A best. Who know where WT I-D will go, let's make it easy to change. + +Answers to open questions: +- WT header size should be stored as uint8_t in stream. Small integer because + the maximum varint length will fit there. Find a good place in struct lsquic_stream +- Flag on stream. +- WT support is always compiled in. Instead, send RESET_STREAM_AT for WT streams only. diff --git a/security-audit-findings-2026-04-11.txt b/security-audit-findings-2026-04-11.txt new file mode 100644 index 000000000..ccdbaeee2 --- /dev/null +++ b/security-audit-findings-2026-04-11.txt @@ -0,0 +1,226 @@ +LSQUIC WebTransport Follow-up Findings +Date: 2026-04-11 +Base commit: a2e3c27 + +1. WT uni-stream remap can orphan the stream on allocation failure. + Files: src/liblsquic/lsquic_wt.c, src/liblsquic/lsquic_stream.c + Sites: wt_uni_on_read(), lsquic_stream_set_stream_if(), wt_on_new_stream() + Summary: The code frees the old pending-uni context before switching the + stream interface. The switch immediately invokes the new on_new_stream + callback. If that callback cannot allocate the WT stream context, the stream + is left switched with a NULL context and no longer has its old pending state. + +2. WT client-bidi remap has the same unchecked failure mode. + Files: src/liblsquic/lsquic_wt.c, src/liblsquic/lsquic_stream.c + Sites: wt_on_client_bidi_stream(), lsquic_stream_set_stream_if(), + wt_on_new_stream() + Summary: A failed WT on_new_stream allocation after interface switching can + leave a bidi stream attached to a session without a valid WT stream context. + +3. Pending WT uni replay can orphan streams on allocation failure. + Files: src/liblsquic/lsquic_wt.c, src/liblsquic/lsquic_stream.c + Sites: wt_replay_pending_streams(), lsquic_stream_set_stream_if(), + wt_on_new_stream() + Summary: Replay frees the old wt_uni_read_ctx before switching to the WT + stream interface. If the immediate on_new_stream call fails, the replayed + stream loses both the old and new context. + +4. Pending WT bidi replay can orphan streams on allocation failure. + Files: src/liblsquic/lsquic_wt.c, src/liblsquic/lsquic_stream.c + Sites: wt_replay_pending_streams(), lsquic_stream_set_stream_if(), + wt_on_new_stream() + Summary: Bidi replay has the same unchecked switch-to-WT failure mode as the + uni replay path. + +5. wt_copy_extra_resp_headers() lacks overflow checks and has a zero-buffer UB + path. + File: src/liblsquic/lsquic_wt.c + Site: wt_copy_extra_resp_headers() + Summary: The aggregate size and allocation math are unchecked, and the copy + loop can dereference buf + off even when bufsz is zero. + +6. wt_send_response() lacks checked allocation math for extra headers. + File: src/liblsquic/lsquic_wt.c + Site: wt_send_response() + Summary: The headers array allocation uses 1 + extra_count without overflow + protection and then copies extra headers blindly. + +7. devious_baton write_baton_message() lacks overflow checks. + File: bin/devious_baton.c + Site: write_baton_message() + Summary: The code computes varint_len + padding_len + 1 without guarding + against size_t overflow or absurdly large allocations from attacker-fed + configuration. + +8. devious_baton header decode growth can overflow. + File: bin/devious_baton.c + Site: hset_prepare_decode() + Summary: Doubling el->nalloc without overflow checks can wrap req_space + before reallocating the decode buffer. + +9. http_server interop header accumulation uses unchecked growth math and a + lossy realloc. + File: bin/http_server.c + Site: interop_server_hset_add_header() + Summary: req->qif_sz + name_len + value_len + 2 is unchecked for overflow, + and assigning realloc() directly to req->qif_str can lose the old buffer on + OOM. + +10. http_server verification HEAD path exits the whole process on allocation + failure. + File: bin/http_server.c + Site: idle_on_write() + Summary: malloc failure for req_body calls exit(1), making memory pressure a + remote process-kill DoS if the sample server is deployed. + +11. http_server verification-body reader over-advances on short reads. + File: bin/http_server.c + Site: http_server_interop_on_read() + Summary: The verification endpoint increments req_sz by the requested byte + count instead of the number of bytes actually read. A partial stream read + can therefore skip unread body bytes, compare uninitialized memory against + the header block, and desynchronize verification-state bookkeeping. + +12. http_server interop read failures still terminate the whole process. + File: bin/http_server.c + Site: http_server_interop_on_read() + Summary: The MD5 and verification-body handlers call exit(1) when a stream + read returns an error. A peer-induced per-stream failure can therefore kill + the entire sample server instead of being contained to the affected stream. + +13. WT datagram queue allocators can wrap their element size calculation. + File: src/liblsquic/lsquic_wt.c + Sites: wt_dgq_enqueue(), wt_in_dgq_enqueue() + Summary: Both outgoing and incoming WT datagram queues allocate elements as + sizeof(elem) + len without overflow checks. A very large datagram length on + the public send path, or an unexpectedly large queued incoming length, can + wrap the allocation size before memcpy and corrupt the heap. + +14. WT local stream opens can return broken streams after init failure. + File: src/liblsquic/lsquic_wt.c + Sites: wt_on_new_stream(), lsquic_wt_open_uni(), lsquic_wt_open_bidi() + Summary: If WT stream-context allocation fails during local stream creation, + the dynamic onnew context leaks and the open helper can still receive a + stream object whose WT initialization never completed. The caller can end up + with a half-initialized outgoing WT stream instead of a clean ENOMEM error. + +15. WT response-header helpers mishandle pathological header counts. + File: src/liblsquic/lsquic_wt.c + Sites: wt_copy_extra_resp_headers(), wt_send_response() + Summary: A negative copied-header count is currently treated as “no headers” + instead of invalid input, and `wt_send_response()` allows `extra->count == + INT_MAX`, which overflows the one-added header total when cast back to the + `int` count field passed to stream header sending. + +16. WT datagram-write failures can leave stale write-interest state behind. + File: src/liblsquic/lsquic_wt.c + Sites: lsquic_wt_send_datagram_ex(), lsquic_wt_want_datagram_write() + Summary: The code arms HTTP-datagram write interest before queueing or + before confirming the underlying stream accepted the request. If enqueue or + arm operations fail, the session can retain stale datagram-write flags or a + needless armed write callback, causing inconsistent WT datagram state. + +17. Incoming WT uni-stream classification does not recover from init failure. + Files: src/liblsquic/lsquic_full_conn_ietf.c, src/liblsquic/lsquic_wt.c + Sites: apply_uni_stream_class(), wt_uni_on_new() + Summary: The unidirectional stream classifier switches a peer-created + stream to the WT uni handler without checking whether the WT on_new + callback actually created a read context. An allocation failure leaves the + stream half-switched with a NULL WT context on a remote ingress path. + +18. WT readers ignore stream read failures and leave broken streams live. + File: src/liblsquic/lsquic_wt.c + Sites: wt_control_on_read(), wt_uni_on_read() + Summary: The CONNECT-stream wrapper and WT uni-stream reader return + immediately on negative `lsquic_stream_readf()` results. A read error does + not tear down the affected WT stream, which can strand the session or keep + a broken incoming WT stream around instead of closing it promptly. + +19. Truncated WT uni session IDs are misparsed as session 0. + File: src/liblsquic/lsquic_wt.c + Sites: wt_uni_readf(), wt_uni_on_read() + Summary: If a peer ends a WT uni stream in the middle of the session-id + varint, the parser marks the read context "done" without producing a valid + session ID. The zero-initialized `sess_id` is then treated as real input, + so the stream can be buffered or associated as if it belonged to session + 0 instead of being rejected as malformed. + +20. Partial capsule-type varints at EOF are accepted as clean stream end. + File: src/liblsquic/lsquic_stream.c + Site: lsquic_http_dg_capsule_readf() + Summary: The HTTP Datagram capsule reader only reports a truncated capsule + at EOF after it has advanced past the capsule-type field. If the peer ends + the stream in the middle of the capsule-type varint, the parser treats the + bytes as a clean EOF instead of aborting with HEC_DATAGRAM_ERROR. WT + CONNECT streams in capsule mode inherit this malformed-input bypass. + +21. WT settings validation allows impossible multi-session capability. + Files: src/liblsquic/lsquic_engine.c, src/liblsquic/lsquic_hcso_writer.c, + src/liblsquic/lsquic_wt.c + Sites: lsquic_engine_check_settings(), wt_get_session_limit(), + lsquic_hcso_writer_write_settings() + Summary: Current WT support on this branch only implements a single + session per connection, but engine validation accepts + `es_max_webtransport_sessions > 1` and SETTINGS then advertises that + larger value to peers. The accept path still hard-limits the effective + session count to one, so the library can overstate WT capability and + reject a peer-visible second session that it previously advertised. + +22. WT accept allows non-2xx CONNECT responses to become opened sessions. + Files: include/lsquic_wt.h, src/liblsquic/lsquic_wt.c + Sites: lsquic_wt_accept(), wt_open_session() + Summary: The public WT API documents `wtap_extra_resp_headers` as extra + headers for the CONNECT 2xx response and reserves `lsquic_wt_reject()` + for non-2xx outcomes, but `lsquic_wt_accept()` accepted any `wtap_status` + in the generic `100..999` range. The open path then sent that status on + the wire and still marked the WT session opened internally, creating a + protocol/state mismatch where a 3xx/4xx/5xx CONNECT response could still + trigger `wti_on_session_open()`. + +23. WT reject accepts 2xx status codes. + File: src/liblsquic/lsquic_wt.c + Site: lsquic_wt_reject() + Summary: The public WT API documents `lsquic_wt_reject()` as the + non-2xx path for rejecting a WebTransport CONNECT request, but the + implementation accepted any status code that `wt_send_response()` + would format. A caller could therefore "reject" a CONNECT stream + with a 2xx response, violating the API contract and emitting a + protocol-confusing success status from the explicit reject path. + +25. WT write handlers treat stream write errors as mere backpressure. + File: src/liblsquic/lsquic_wt.c + Sites: wt_on_write(), wt_control_on_write() + Summary: The WT data-stream prefix writer and CONNECT close-capsule writer + both return immediately on `lsquic_stream_write() <= 0`. In core stream + code, a negative return indicates an actual write-side stream error, not + just blocked output. WT therefore leaves the affected stream/session live + after a write failure instead of aborting it, which can strand an outgoing + WT stream prefix or a closing control stream indefinitely. + +26. WT stream reset APIs accept the CONNECT control stream. + File: src/liblsquic/lsquic_wt.c + Sites: lsquic_wt_stream_reset(), lsquic_wt_stream_stop_sending() + Summary: Both public WT app-error stream helpers only check whether a + stream has a WT session attachment. The CONNECT control stream is attached + to its WT session too, so callers can currently send RESET_STREAM or + STOP_SENDING with WT application error codes on the control stream instead + of using the WT close-session path. That is the wrong protocol surface for + closing a WebTransport session and should be rejected at the API boundary. + +27. WT accept allows client-side streams. + Files: docs/webtransport.rst, src/liblsquic/lsquic_wt.c + Site: lsquic_wt_accept() + Summary: The public WT acceptance flow is documented as the server-side + handoff for an incoming Extended CONNECT request, but `lsquic_wt_accept()` + never checked that the supplied control stream was server-side. A caller + could therefore pass a client-side stream and start binding WT session + state to the wrong endpoint role instead of getting a clean `EINVAL`. + +29. WT accept does not reject streams whose response headers already started. + File: src/liblsquic/lsquic_wt.c + Site: lsquic_wt_accept() + Summary: `lsquic_wt_reject()` already refuses CONNECT streams once response + header emission has started, but `lsquic_wt_accept()` did not. A caller + could therefore hand a stream to WT after application code had already + started the HTTP response, and WT would still bind session state internally + even though it could no longer send its own CONNECT 2xx handshake cleanly. diff --git a/security-audit-plan.md b/security-audit-plan.md new file mode 100644 index 000000000..1f255a0ef --- /dev/null +++ b/security-audit-plan.md @@ -0,0 +1,129 @@ +# LSQUIC WebTransport Security Audit Plan + +## 1. Threat model and assumptions + +This is a plan, not a findings report. + +- Highest expected CVE yield is in post-handshake WebTransport code, not in generic QUIC parsing. The WT implementation in `src/liblsquic/lsquic_wt.c` mixes stream wrappers, session objects, callback handoff, queued datagrams, and deferred acceptance in one ownership domain. +- Likely remote unauthenticated bugs are limited to the QUIC/HTTP3 ingress that gates WT: packet/frame parsing and stream classification in `src/liblsquic/lsquic_full_conn_ietf.c`, `src/liblsquic/lsquic_stream.c`, and related packet code. +- Likely remote authenticated/post-handshake bugs are the main target: CONNECT handling, WT session creation, WT_CLOSE capsule handling, uni/bidi stream binding, HTTP Datagram delivery, and teardown/finalization. +- Local/config/example issues matter if operators expose the sample server: `bin/http_server.c` and `bin/devious_baton.c` parse attacker-controlled WT requests and payloads. + +## 2. Attack surface map + +1. Unauthenticated UDP ingress: QUIC packet/frame parsing and connection promotion. Path: `lsquic_packet_in.c` -> mini/full connection parsers -> `src/liblsquic/lsquic_full_conn_ietf.c`. Review only enough to understand how DATAGRAM/STREAM input reaches WT. +2. Post-handshake capability negotiation: peer SETTINGS and transport capability synthesis. Path: `on_setting`/`on_settings_frame` -> `update_peer_wt_support` in `src/liblsquic/lsquic_full_conn_ietf.c`. +3. WT CONNECT control stream: app accepts CONNECT, then `lsquic_wt_accept` installs capsule/datagram handlers and wraps the original stream callbacks in `src/liblsquic/lsquic_wt.c`. +4. Peer-created WT streams: +- Uni: `apply_uni_stream_class` -> `lsquic_wt_uni_stream_if` -> `wt_uni_on_read`. +- Client bidi: `hq_filter_readable` -> `cp_on_hq_switch_stream` -> `wt_on_client_bidi_stream`. +5. QUIC DATAGRAM to WT datagram path: `process_datagram_frame` -> `process_http_dg_frame` -> `lsquic_wt_on_http_dg_read`. +6. Teardown path: CLOSE capsule, control stream EOF/reset, stream destroy hooks, buffered replay, and session finalization in `wt_control_on_*`, `wt_on_stream_destroy`, `wt_session_maybe_finalize`. + +## 3. Prioritized hotspots + +### 1. WT session lifecycle and control-stream wrapper + +- Files/functions: `src/liblsquic/lsquic_wt.c` `lsquic_wt_accept`, `wt_wrap_control_stream`, `wt_control_on_read`, `wt_control_on_write`, `wt_control_on_close`, `wt_control_on_reset`, `wt_on_close_capsule`, `wt_on_stream_destroy`, `wt_session_maybe_finalize`, `wt_destroy_session` +- Why it matters: this is the ownership nexus for session object lifetime, callback redirection, close capsule state, and final teardown. +- Likely bug classes: UAF, double free, stale callback context, double-close/finalize, null deref, assertion reachable from peer-driven teardown ordering. +- Exposure: remote authenticated/post-handshake. + +### 2. Pending WT streams and datagram replay + +- Files/functions: `src/liblsquic/lsquic_wt.c` `wt_evaluate_accept`, `wt_buffer_or_reject_stream`, `wt_replay_pending_streams`, `wt_replay_pending_datagrams`, `wt_count_pending_streams`, `wt_close_data_streams`, `wt_in_dgq_enqueue`, `wt_dgq_enqueue` +- Why it matters: attacker-controlled ordering can create streams/datagrams before session open, then drive replay or reject/close races. +- Likely bug classes: memory/resource exhaustion, count/size underflow, queue leaks, stale `wt_uni_read_ctx` use, wrong-session binding. +- Exposure: remote authenticated/post-handshake. + +### 3. WT capability negotiation and stream classification + +- Files/functions: `src/liblsquic/lsquic_full_conn_ietf.c` `update_peer_wt_support`, `on_setting`, `apply_uni_stream_class`, `unicla_on_read`; `src/liblsquic/lsquic_stream.c` `hq_filter_readable`; `src/liblsquic/lsquic_wt.c` `wt_on_client_bidi_stream` +- Why it matters: this decides when arbitrary peer input is reinterpreted as WT traffic and bound to a session. +- Likely bug classes: state machine bypass, protocol desync, invalid stream/session association, assert/crash on duplicate or reordered transitions. +- Exposure: remote authenticated; limited unauthenticated relevance via malformed stream/frame delivery. + +### 4. QUIC DATAGRAM and HTTP Datagram demux into WT + +- Files/functions: `src/liblsquic/lsquic_full_conn_ietf.c` `process_http_dg_frame`, `process_datagram_frame`; `src/liblsquic/lsquic_wt.c` `lsquic_wt_on_http_dg_read`, `lsquic_wt_on_http_dg_write`, `wt_dgq_send_one` +- Why it matters: datagrams are attacker-sized, unordered, and mapped by qsid to stream/session state. +- Likely bug classes: parser confusion, integer truncation in qsid mapping, wrong-stream delivery, queue exhaustion, oversized datagram handling bugs. +- Exposure: remote authenticated/post-handshake. + +### 5. Example/server-side WT request parsing + +- Files/functions: `bin/http_server.c` `handle_connect_request`, `baton_connect_handler`; `bin/devious_baton.c` `parse_request`, `parse_path`, `parse_query`, `buf_append`, `consume_baton_data` +- Why it matters: many deployments copy example code; this code allocates and parses attacker-controlled path/query/datagram/stream data. +- Likely bug classes: heap overflow via growth math, memory leaks, request parsing confusion, excessive allocation/DoS. +- Exposure: local/configuration/example, but remotely reachable if the sample server is deployed. + +## 4. Audit techniques by hotspot + +### 1. Lifecycle/control wrapper + +- Evidence that confirms a real vuln: ASan/UAF on control-stream close/reset after app callbacks; double free on repeated close paths; crash or callback after `wts_if`/session destroy. +- Dynamic checks: targeted harness that drives `accept -> open -> close capsule -> control FIN/reset -> stream destroy` in all permutations; inject allocation failures in `wt_copy_*`, `wt_queue_close_capsule`, `wt_wrap_control_stream`. + +### 2. Pending replay and buffering + +- Evidence: queued counts/bytes exceed configured limits, leaked pending streams/datagrams across reject/close, replay binds stream to wrong session, free-after-replay of `wt_uni_read_ctx`. +- Dynamic checks: harness for datagrams/uni/bidi streams arriving before accept/open; corpus cases that hit `WT_MAX_PENDING_STREAMS`, queue byte ceilings, drop-oldest/newest behavior, and close during replay. + +### 3. Negotiation and classification + +- Evidence: `CP_WEBTRANSPORT` becomes set without full prerequisites; malformed uni stream type or bidi switch frame crashes or binds incorrectly; duplicate SETTINGS/control streams turn into asserts instead of clean aborts. +- Dynamic checks: packet-driven tests for reordered SETTINGS, draft-14 vs draft-15, CONNECT protocol missing/present, partial varints on uni classifier, duplicate close/session advertisements. + +### 4. Datagram demux + +- Evidence: invalid qsid reaches wrong stream, datagram before stream existence causes unexpected object creation, oversized datagram causes overflow or stale queue state. +- Dynamic checks: fuzz `process_datagram_frame` with valid/invalid DATAGRAM frames and quarter-stream IDs; replay seeds for nonexistent stream IDs, max-size qsid, zero-length payloads, repeated bursts. + +### 5. Example code + +- Evidence: realloc growth overflow, unbounded memory growth, malformed path/query/datagram causing crash or persistent leak in `http_server`/`devious_baton`. +- Dynamic checks: run sample server under ASan/UBSan/LSan with malformed CONNECT headers, large query strings, fragmented baton messages, and datagram floods. + +Concrete invariants/assertions to check during review: + +- `CP_WEBTRANSPORT` implies `CP_H3_PEER_SETTINGS` and `CP_HTTP_DATAGRAMS`, and client mode also implies `CP_CONNECT_PROTOCOL`. +- Each live WT session is unique by `wts_stream_id`. +- `wts_n_streams` reaches zero exactly once before `wt_destroy_session`. +- No callback runs after `wts_if` is nulled in `wt_fire_session_close_cb`. +- Pending stream count never exceeds 64 and queue bytes/count never exceed configured maxima. +- A control stream sees at most one WT_CLOSE capsule and no data after it. +- A stream is never rebound from one session to another. + +## 5. Fuzzing and sanitizer plan + +- Sanitizers: keep existing Debug+clang ASan path from `CMakeLists.txt`, but add explicit ASan+UBSan+LSan CI jobs for WT tests. Use MSan only for isolated unit/fuzzer harnesses that do not depend on full BoringSSL paths. TSan is lower priority. +- Existing coverage gaps: +- `tests/test_wt.c` covers helper/state routines but is not packet-driven and does not exercise real ingress/classification/replay end to end. +- `tests/test_h3_framing.c` has AFL-style fuzz-driver support, but it is generic HTTP/3 framing, not WT-specific. +- Existing fuzzing under `src/liblsquic/ls-qpack/fuzz/` is QPACK-only. +- There is no dedicated WT corpus or harness for CLOSE capsules, uni-stream session IDs, HQ switch frames, or DATAGRAM demux. +- New fuzz targets: +- WT control stream harness around `lsquic_wt_accept` plus capsule parsing and close/reset interleavings. +- Uni-stream classifier harness for `HQUST_WEBTRANSPORT` plus partial/fragmented session-id varints. +- DATAGRAM harness for `process_datagram_frame` / `process_http_dg_frame` / `lsquic_wt_on_http_dg_read`. +- Negotiation harness for SETTINGS permutations feeding `on_setting` and `update_peer_wt_support`. +- Corpus seeds: +- Valid draft-14 and draft-15 handshakes with and without CONNECT protocol. +- Valid WT_CLOSE capsule, duplicate CLOSE, truncated CLOSE, oversized reason, unknown capsules. +- Valid/invalid quarter stream IDs, nonexistent stream IDs, max varint IDs. +- Streams/datagrams arriving before session open, during accept-pending, and after session close. +- Pending-stream floods that hit 63, 64, and 65 queued streams. +- AFL++ is available in this environment, so `afl-fuzz` and companion `afl-*` tools should be used for the WT-specific harnesses once they exist. + +## 6. Concrete step-by-step audit schedule + +1. Read `lsquic_wt.c` top-to-bottom once, but annotate only lifetime edges first: accept, wrap, close, destroy, replay, destroy-on-stream callbacks. +2. Review `lsquic_full_conn_ietf.c` only at WT-relevant ingress points: SETTINGS, WT support synthesis, uni-stream classification, DATAGRAM demux. +3. Review `lsquic_stream.c:hq_filter_readable` to confirm how switch-to-WT bidi streams can be triggered and whether partial-frame/error states can mis-sequence the handoff. +4. Build a small packet/state matrix and execute it under ASan+UBSan: `SETTINGS reorder`, `CONNECT pending`, `uni before accept`, `bidi before open`, `datagram before open`, `close/reset during replay`. +5. Fuzz the three WT-specific targets above before spending time on generic QUIC/QPACK code. WT should dominate the budget. +6. Only after WT-specific review stabilizes, spot-check related generic code for prerequisite bypasses: stream creation limits, DATAGRAM frame parsing, and hash/stream lookup ownership. +7. Audit `bin/http_server.c` and `bin/devious_baton.c` last, and only treat findings there as lower-priority unless the deployment clearly exposes those programs. +8. Any confirmed issue should immediately get a minimized regression in `tests/test_wt.c` or a new dedicated WT harness, not just a prose note. + +Expected order by CVE yield: `lsquic_wt.c` lifecycle -> pending replay/queues -> SETTINGS/capability/classification -> DATAGRAM demux -> example server code. diff --git a/src/liblsquic/CMakeLists.txt b/src/liblsquic/CMakeLists.txt index 335c70f07..54f8dcfb7 100644 --- a/src/liblsquic/CMakeLists.txt +++ b/src/liblsquic/CMakeLists.txt @@ -82,6 +82,7 @@ SET(lsquic_STAT_SRCS lsquic_util.c lsquic_varint.c lsquic_version.c + lsquic_wt.c ) IF(NOT MSVC) @@ -161,4 +162,3 @@ install( DESTINATION share/lsquic NAMESPACE lsquic:: FILE lsquic-targets.cmake) - diff --git a/src/liblsquic/lsquic_conn.c b/src/liblsquic/lsquic_conn.c index d3d723ee7..d8f80da8d 100644 --- a/src/liblsquic/lsquic_conn.c +++ b/src/liblsquic/lsquic_conn.c @@ -1,6 +1,8 @@ /* Copyright (c) 2017 - 2026 LiteSpeed Technologies Inc. See LICENSE. */ #include +#include #include +#include #include #include @@ -9,7 +11,14 @@ #include "lsquic.h" #include "lsquic_int_types.h" #include "lsquic_hash.h" +#include "lsquic_conn_flow.h" +#include "lsquic_rtt.h" +#include "lsquic_hq.h" +#include "lsquic_varint.h" +#include "lsquic_sfcw.h" +#include "lsquic_stream.h" #include "lsquic_conn.h" +#include "lsquic_conn_public.h" #include "lsquic_packet_common.h" #include "lsquic_packet_gquic.h" #include "lsquic_packet_in.h" @@ -307,6 +316,14 @@ lsquic_conn_get_min_datagram_size (struct lsquic_conn *lconn) } +void +lsquic_conn_http_dg_streams_updated (lsquic_conn_t *lconn) +{ + if (lconn->cn_if && lconn->cn_if->ci_http_dg_streams_updated) + lconn->cn_if->ci_http_dg_streams_updated(lconn); +} + + #if LSQUIC_CONN_STATS void lsquic_conn_stats_diff (const struct conn_stats *cumulative_stats, @@ -370,3 +387,150 @@ lsquic_conn_get_param (lsquic_conn_t *lconn, enum lsquic_conn_param param, return -1; } + +struct http_dg_handler +{ + struct lsquic_hash_elem hgh_hash_el; + lsquic_stream_id_t hgh_stream_id; + const struct lsquic_http_dg_if + *hgh_if; +}; + + +static struct http_dg_handler * +hgh_find (struct lsquic_hash *hash, lsquic_stream_id_t stream_id) +{ + struct lsquic_hash_elem *el; + + if (!hash) + return NULL; + + el = lsquic_hash_find(hash, &stream_id, sizeof(stream_id)); + if (!el) + return NULL; + + return lsquic_hashelem_getdata(el); +} + + +const struct lsquic_http_dg_if * +lsquic_conn_http_dg_get_if (struct lsquic_conn_public *conn_pub, + lsquic_stream_id_t stream_id) +{ + struct http_dg_handler *handler; + + if (!conn_pub) + return NULL; + + handler = hgh_find(conn_pub->http_dg_handlers, stream_id); + if (!handler) + return NULL; + + return handler->hgh_if; +} + + +static int +hgh_insert (struct lsquic_conn_public *conn_pub, + lsquic_stream_id_t stream_id, + const struct lsquic_http_dg_if *dg_if) +{ + struct http_dg_handler *handler; + struct lsquic_hash_elem *el; + + if (!conn_pub->http_dg_handlers) + { + conn_pub->http_dg_handlers = lsquic_hash_create(); + if (!conn_pub->http_dg_handlers) + { + errno = ENOMEM; + return -1; + } + } + + handler = hgh_find(conn_pub->http_dg_handlers, stream_id); + if (handler) + { + handler->hgh_if = dg_if; + return 0; + } + + handler = calloc(1, sizeof(*handler)); + if (!handler) + { + errno = ENOMEM; + return -1; + } + + handler->hgh_stream_id = stream_id; + handler->hgh_if = dg_if; + + el = lsquic_hash_insert(conn_pub->http_dg_handlers, &handler->hgh_stream_id, + sizeof(handler->hgh_stream_id), handler, &handler->hgh_hash_el); + if (!el) + { + free(handler); + errno = ENOMEM; + return -1; + } + + return 0; +} + + +static void +hgh_remove (struct lsquic_conn_public *conn_pub, lsquic_stream_id_t stream_id) +{ + struct lsquic_hash_elem *el; + struct http_dg_handler *handler; + + if (!conn_pub || !conn_pub->http_dg_handlers) + return; + + el = lsquic_hash_find(conn_pub->http_dg_handlers, &stream_id, + sizeof(stream_id)); + if (!el) + return; + + handler = lsquic_hashelem_getdata(el); + lsquic_hash_erase(conn_pub->http_dg_handlers, el); + free(handler); +} + + +int +lsquic_conn_http_dg_set_if (struct lsquic_conn_public *conn_pub, + lsquic_stream_id_t stream_id, + const struct lsquic_http_dg_if *dg_if) +{ + if (!conn_pub) + { + errno = EINVAL; + return -1; + } + + if (!dg_if) + { + hgh_remove(conn_pub, stream_id); + return 0; + } + + return hgh_insert(conn_pub, stream_id, dg_if); +} + + +void +lsquic_conn_http_dg_cleanup (struct lsquic_conn_public *conn_pub) +{ + struct lsquic_hash_elem *el; + + if (!conn_pub || !conn_pub->http_dg_handlers) + return; + + for (el = lsquic_hash_first(conn_pub->http_dg_handlers); el; + el = lsquic_hash_next(conn_pub->http_dg_handlers)) + free(lsquic_hashelem_getdata(el)); + + lsquic_hash_destroy(conn_pub->http_dg_handlers); + conn_pub->http_dg_handlers = NULL; +} diff --git a/src/liblsquic/lsquic_conn.h b/src/liblsquic/lsquic_conn.h index b1e0ec10c..2d0946cb0 100644 --- a/src/liblsquic/lsquic_conn.h +++ b/src/liblsquic/lsquic_conn.h @@ -24,12 +24,14 @@ #endif struct lsquic_conn; +struct lsquic_conn_public; struct lsquic_engine_public; struct lsquic_packet_out; struct lsquic_packet_in; struct sockaddr; struct parse_funcs; struct attq_elem; +struct lsquic_http_dg_if; #if LSQUIC_CONN_STATS struct conn_stats; #endif @@ -191,6 +193,16 @@ struct conn_iface void (*ci_make_stream) (struct lsquic_conn *); + struct lsquic_stream * + (*ci_make_bidi_stream_with_if) (struct lsquic_conn *, + const struct lsquic_stream_if *, + void *stream_if_ctx); + + struct lsquic_stream * + (*ci_make_uni_stream_with_if) (struct lsquic_conn *, + const struct lsquic_stream_if *, + void *stream_if_ctx); + void (*ci_abort) (struct lsquic_conn *); @@ -290,6 +302,14 @@ struct conn_iface size_t (*ci_get_min_datagram_size) (struct lsquic_conn *); + /* Optional method */ + size_t + (*ci_get_max_datagram_size) (struct lsquic_conn *); + + /* Optional method: called when HTTP datagram streams queue changes */ + void + (*ci_http_dg_streams_updated) (struct lsquic_conn *); + /* Optional method */ void (*ci_early_data_failed) (struct lsquic_conn *); @@ -315,6 +335,21 @@ struct conn_iface (*ci_user_stream_progress) (struct lsquic_conn *); }; +void +lsquic_conn_http_dg_streams_updated (lsquic_conn_t *lconn); + +const struct lsquic_http_dg_if * +lsquic_conn_http_dg_get_if (struct lsquic_conn_public *, + lsquic_stream_id_t stream_id); + +int +lsquic_conn_http_dg_set_if (struct lsquic_conn_public *, + lsquic_stream_id_t stream_id, + const struct lsquic_http_dg_if *); + +void +lsquic_conn_http_dg_cleanup (struct lsquic_conn_public *); + #define LSCONN_CCE_BITS 3 #define LSCONN_MAX_CCES (1 << LSCONN_CCE_BITS) diff --git a/src/liblsquic/lsquic_conn_public.h b/src/liblsquic/lsquic_conn_public.h index 6601b1b37..1c07faee7 100644 --- a/src/liblsquic/lsquic_conn_public.h +++ b/src/liblsquic/lsquic_conn_public.h @@ -10,12 +10,16 @@ #ifndef LSQUIC_CONN_PUBLIC_H #define LSQUIC_CONN_PUBLIC_H 1 +#include + struct lsquic_conn; struct lsquic_engine_public; struct lsquic_mm; struct lsquic_hash; struct headers_stream; struct lsquic_send_ctl; +struct lsquic_wt_session; +struct lsquic_stream; #if LSQUIC_CONN_STATS struct conn_stats; #endif @@ -27,8 +31,10 @@ struct lsquic_conn_public { struct lsquic_streams_tailq sending_streams, /* Send RST_STREAM, BLOCKED, and WUF frames */ read_streams, write_streams, /* Send STREAM frames */ - service_streams; + service_streams, + http_dg_streams; /* Send HTTP Datagrams */ struct lsquic_hash *all_streams; + struct lsquic_hash *http_dg_handlers; struct lsquic_cfcw cfcw; struct lsquic_conn_cap conn_cap; struct lsquic_rtt_stats rtt_stats; @@ -48,12 +54,25 @@ struct lsquic_conn_public { } u; enum { CP_STREAM_UNBLOCKED = 1 << 0, /* Set when a stream becomes unblocked */ + CP_HTTP_DATAGRAMS = 1 << 1, /* HTTP Datagram support negotiated */ + CP_WEBTRANSPORT = 1 << 2, /* WebTransport support negotiated */ + CP_H3_PEER_SETTINGS = 1 << 3, /* Peer SETTINGS frame received */ + CP_CONNECT_PROTOCOL = 1 << 4, /* Peer enabled CONNECT protocol */ } cp_flags; + uint64_t cp_wt_peer_draft; + int (*cp_get_ss_code) (const struct lsquic_stream *, uint64_t *); + void (*cp_on_stream_destroy) (struct lsquic_stream *); + int (*cp_is_hq_switch_frame) (struct lsquic_stream *, uint64_t, + uint64_t); + void (*cp_on_hq_switch_stream) (struct lsquic_stream *, + lsquic_stream_id_t); + void (*cp_on_http_caps_change) (struct lsquic_conn_public *); struct lsquic_send_ctl *send_ctl; #if LSQUIC_CONN_STATS struct conn_stats *conn_stats; #endif const struct network_path *path; + TAILQ_HEAD(, lsquic_wt_session) wt_sessions; #if LSQUIC_EXTRA_CHECKS unsigned long stream_frame_bytes; unsigned wtp_level; /* wtp: Write To Packets */ diff --git a/src/liblsquic/lsquic_enc_sess_ietf.c b/src/liblsquic/lsquic_enc_sess_ietf.c index 9339dc38e..768987c4b 100644 --- a/src/liblsquic/lsquic_enc_sess_ietf.c +++ b/src/liblsquic/lsquic_enc_sess_ietf.c @@ -646,6 +646,8 @@ gen_trans_params (struct enc_sess_iquic *enc_sess, unsigned char *buf, } if (!settings->es_allow_migration) params.tp_set |= 1 << TPI_DISABLE_ACTIVE_MIGRATION; + if (settings->es_reset_stream_at) + params.tp_set |= 1 << TPI_RESET_STREAM_AT; if (settings->es_ql_bits) { params.tp_loss_bits = settings->es_ql_bits - 1; @@ -663,7 +665,7 @@ gen_trans_params (struct enc_sess_iquic *enc_sess, unsigned char *buf, params.tp_numerics[TPI_TIMESTAMPS] = TS_GENERATE_THEM; params.tp_set |= 1 << TPI_TIMESTAMPS; } - if (settings->es_datagrams) + if (settings->es_datagrams || settings->es_http_datagrams) { if (params.tp_set & (1 << TPI_MAX_UDP_PAYLOAD_SIZE)) params.tp_numerics[TPI_MAX_DATAGRAM_FRAME_SIZE] @@ -3530,6 +3532,8 @@ lsquic_enc_sess_ietf_gen_quic_ctx ( } if (!settings->es_allow_migration) params.tp_set |= 1 << TPI_DISABLE_ACTIVE_MIGRATION; + if (settings->es_reset_stream_at) + params.tp_set |= 1 << TPI_RESET_STREAM_AT; if (settings->es_ql_bits) { params.tp_loss_bits = settings->es_ql_bits - 1; @@ -3547,7 +3551,7 @@ lsquic_enc_sess_ietf_gen_quic_ctx ( params.tp_numerics[TPI_TIMESTAMPS] = TS_GENERATE_THEM; params.tp_set |= 1 << TPI_TIMESTAMPS; } - if (settings->es_datagrams) + if (settings->es_datagrams || settings->es_http_datagrams) { if (params.tp_set & (1 << TPI_MAX_UDP_PAYLOAD_SIZE)) params.tp_numerics[TPI_MAX_DATAGRAM_FRAME_SIZE] diff --git a/src/liblsquic/lsquic_engine.c b/src/liblsquic/lsquic_engine.c index 3d50c6075..1468274c0 100644 --- a/src/liblsquic/lsquic_engine.c +++ b/src/liblsquic/lsquic_engine.c @@ -323,10 +323,9 @@ lsquic_engine_init_settings (struct lsquic_engine_settings *settings, settings->es_ping_period = 0; settings->es_noprogress_timeout = LSQUIC_DF_NOPROGRESS_TIMEOUT_SERVER; -#if LSQUIC_WEBTRANSPORT_SERVER_SUPPORT - settings->es_webtransport_server = LSQUIC_DF_WEBTRANSPORT_SERVER; - settings->es_max_webtransport_server_streams = LSQUIC_DF_MAX_WEBTRANSPORT_SERVER_STREAMS; -#endif + settings->es_webtransport = LSQUIC_DF_WEBTRANSPORT_SERVER; + settings->es_max_webtransport_sessions + = LSQUIC_DF_MAX_WEBTRANSPORT_SESSIONS; } else { @@ -386,9 +385,16 @@ lsquic_engine_init_settings (struct lsquic_engine_settings *settings, settings->es_spin = LSQUIC_DF_SPIN; settings->es_delayed_acks = LSQUIC_DF_DELAYED_ACKS; settings->es_timestamps = LSQUIC_DF_TIMESTAMPS; + settings->es_reset_stream_at = LSQUIC_DF_RESET_STREAM_AT; settings->es_grease_quic_bit = LSQUIC_DF_GREASE_QUIC_BIT; settings->es_mtu_probe_timer = LSQUIC_DF_MTU_PROBE_TIMER; settings->es_dplpmtud = LSQUIC_DF_DPLPMTUD; + settings->es_datagrams = LSQUIC_DF_DATAGRAMS; + settings->es_http_datagrams = LSQUIC_DF_HTTP_DATAGRAMS; + settings->es_http_dg_max_capsule_read_size = + LSQUIC_DF_HTTP_DG_MAX_CAPSULE_READ_SIZE; + settings->es_http_dg_max_capsule_write_size = + LSQUIC_DF_HTTP_DG_MAX_CAPSULE_WRITE_SIZE; settings->es_cc_algo = LSQUIC_DF_CC_ALGO; settings->es_cc_rtt_thresh = LSQUIC_DF_CC_RTT_THRESH; settings->es_enable_bw_sampler = LSQUIC_DF_ENABLE_BW_SAMPLER; @@ -408,6 +414,9 @@ lsquic_engine_init_settings (struct lsquic_engine_settings *settings, settings->es_check_tp_sanity = LSQUIC_DF_CHECK_TP_SANITY; settings->es_amp_factor = LSQUIC_DF_AMP_FACTOR; settings->es_send_verneg = LSQUIC_DF_SEND_VERNEG; + settings->es_write_sched_strategy = LSQUIC_DF_WRITE_SCHED_STRATEGY; + settings->es_write_datagram_prio = LSQUIC_DF_WRITE_DATAGRAM_PRIO; + settings->es_write_datagram_share = LSQUIC_DF_WRITE_DATAGRAM_SHARE; } @@ -506,35 +515,69 @@ lsquic_engine_check_settings (const struct lsquic_engine_settings *settings, "the allowed maximum of %u", (unsigned) MAX_OUT_BATCH_SIZE); return -1; } - - if (settings->es_max_delayed_0rtt_packets > UCHAR_MAX) + if(settings->es_webtransport) { - if (err_buf) - snprintf(err_buf, err_buf_sz, "max delayed 0-RTT packet count " - "is greater than the allowed maximum of %u", - (unsigned) UCHAR_MAX); - return -1; - } -#if LSQUIC_WEBTRANSPORT_SERVER_SUPPORT - if(settings->es_webtransport_server) - { - if(!(flags & ENG_SERVER)) + if (!settings->es_http_datagrams) { if (err_buf) - snprintf(err_buf, err_buf_sz, "server webtransport support enabled, but " - "ENG_SERVER flag is not set"); + snprintf(err_buf, err_buf_sz, "webtransport support enabled, " + "but HTTP datagrams are disabled"); return -1; } - if(settings->es_max_webtransport_server_streams == 0) + if(settings->es_max_webtransport_sessions == 0) { if (err_buf) - snprintf(err_buf, err_buf_sz, "server webtransport support enabled, but " + snprintf(err_buf, err_buf_sz, "webtransport support enabled, but " "webtransport sessions count is 0"); return -1; } + + if (settings->es_max_webtransport_sessions > 1) + { + if (err_buf) + snprintf(err_buf, err_buf_sz, "webtransport support enabled, " + "but current WT implementation " + "only supports 1 session per " + "connection"); + return -1; + } + + if (!settings->es_reset_stream_at) + { + if (err_buf) + snprintf(err_buf, err_buf_sz, "webtransport support enabled, " + "but reset_stream_at is disabled"); + return -1; + } } -#endif + + if (settings->es_write_sched_strategy > LSQWSS_DRR) + { + if (err_buf) + snprintf(err_buf, err_buf_sz, "invalid write scheduler strategy: " + "%u", settings->es_write_sched_strategy); + return -1; + } + + if (settings->es_write_datagram_prio >= 5) + { + if (err_buf) + snprintf(err_buf, err_buf_sz, "invalid datagram write priority: " + "%u", settings->es_write_datagram_prio); + return -1; + } + + if (settings->es_write_datagram_share != settings->es_write_datagram_share + || settings->es_write_datagram_share < 0.f + || settings->es_write_datagram_share > 1.f) + { + if (err_buf) + snprintf(err_buf, err_buf_sz, "%s", + "write datagram share must be in [0.0, 1.0]"); + return -1; + } + return 0; } @@ -3584,5 +3627,3 @@ lsquic_engine_retire_cid (struct lsquic_engine_public *enpub, conn->cn_cces_mask &= ~(1u << cce_idx); LSQ_DEBUGC("retire CID %"CID_FMT, CID_BITS(&cce->cce_cid)); } - - diff --git a/src/liblsquic/lsquic_ev_log.c b/src/liblsquic/lsquic_ev_log.c index 006a35c17..a31ef0480 100644 --- a/src/liblsquic/lsquic_ev_log.c +++ b/src/liblsquic/lsquic_ev_log.c @@ -481,10 +481,10 @@ lsquic_ev_log_generated_stop_waiting_frame (const lsquic_cid_t *cid, void lsquic_ev_log_generated_stop_sending_frame (const lsquic_cid_t *cid, - lsquic_stream_id_t stream_id, uint16_t error_code) + lsquic_stream_id_t stream_id, uint64_t error_code) { LCID("generated STOP_SENDING frame; stream ID: %"PRIu64"; error code: " - "%"PRIu16, stream_id, error_code); + "%"PRIu64, stream_id, error_code); } diff --git a/src/liblsquic/lsquic_ev_log.h b/src/liblsquic/lsquic_ev_log.h index af7d1a81a..a2d918811 100644 --- a/src/liblsquic/lsquic_ev_log.h +++ b/src/liblsquic/lsquic_ev_log.h @@ -260,7 +260,7 @@ lsquic_ev_log_generated_stop_waiting_frame (const lsquic_cid_t *, void lsquic_ev_log_generated_stop_sending_frame (const lsquic_cid_t *, - lsquic_stream_id_t, uint16_t); + lsquic_stream_id_t, uint64_t); #define EV_LOG_GENERATED_STOP_SENDING_FRAME(...) do { \ if (LSQ_LOG_ENABLED_EXT(LSQ_LOG_DEBUG, LSQLM_EVENT)) \ diff --git a/src/liblsquic/lsquic_full_conn.c b/src/liblsquic/lsquic_full_conn.c index a2a078414..3bd3dbea0 100644 --- a/src/liblsquic/lsquic_full_conn.c +++ b/src/liblsquic/lsquic_full_conn.c @@ -676,6 +676,8 @@ new_conn_common (lsquic_cid_t cid, struct lsquic_engine_public *enpub, TAILQ_INIT(&conn->fc_pub.read_streams); TAILQ_INIT(&conn->fc_pub.write_streams); TAILQ_INIT(&conn->fc_pub.service_streams); + TAILQ_INIT(&conn->fc_pub.http_dg_streams); + TAILQ_INIT(&conn->fc_pub.wt_sessions); STAILQ_INIT(&conn->fc_stream_ids_to_reset); lsquic_conn_cap_init(&conn->fc_pub.conn_cap, LSQUIC_MIN_FCW); lsquic_alarmset_init(&conn->fc_alset, &conn->fc_conn); @@ -1129,6 +1131,7 @@ full_conn_ci_destroy (lsquic_conn_t *lconn) lsquic_hash_erase(conn->fc_pub.all_streams, el); lsquic_stream_destroy(stream); } + lsquic_conn_http_dg_cleanup(&conn->fc_pub); lsquic_hash_destroy(conn->fc_pub.all_streams); if (conn->fc_flags & FC_CREATED_OK) conn->fc_stream_ifs[STREAM_IF_STD].stream_if @@ -1447,6 +1450,24 @@ full_conn_ci_make_stream (struct lsquic_conn *lconn) } } +static struct lsquic_stream * +full_conn_ci_make_bidi_stream_with_if (struct lsquic_conn *UNUSED_lconn, + const struct lsquic_stream_if *UNUSED_stream_if, + void *UNUSED_stream_if_ctx) +{ + errno = ENOSYS; + return NULL; +} + +static struct lsquic_stream * +full_conn_ci_make_uni_stream_with_if (struct lsquic_conn *UNUSED_lconn, + const struct lsquic_stream_if *UNUSED_stream_if, + void *UNUSED_stream_if_ctx) +{ + errno = ENOSYS; + return NULL; +} + static lsquic_stream_t * find_stream_by_id (struct full_conn *conn, lsquic_stream_id_t stream_id) @@ -2249,6 +2270,7 @@ static process_frame_f const process_frames[N_QUIC_FRAMES] = [QUIC_FRAME_PADDING] = process_padding_frame, [QUIC_FRAME_PING] = process_ping_frame, [QUIC_FRAME_RST_STREAM] = process_rst_stream_frame, + [QUIC_FRAME_RESET_STREAM_AT] = process_invalid_frame, [QUIC_FRAME_STOP_WAITING] = process_stop_waiting_frame, [QUIC_FRAME_STREAM] = process_stream_frame, [QUIC_FRAME_WINDOW_UPDATE] = process_window_update_frame, @@ -4446,6 +4468,8 @@ static const struct conn_iface full_conn_iface = { .ci_is_push_enabled = full_conn_ci_is_push_enabled, .ci_is_tickable = full_conn_ci_is_tickable, .ci_make_stream = full_conn_ci_make_stream, + .ci_make_bidi_stream_with_if = full_conn_ci_make_bidi_stream_with_if, + .ci_make_uni_stream_with_if = full_conn_ci_make_uni_stream_with_if, .ci_n_avail_streams = full_conn_ci_n_avail_streams, .ci_n_pending_streams = full_conn_ci_n_pending_streams, .ci_next_packet_to_send = full_conn_ci_next_packet_to_send, diff --git a/src/liblsquic/lsquic_full_conn_ietf.c b/src/liblsquic/lsquic_full_conn_ietf.c index a0ac7ed4e..66bcc76b2 100644 --- a/src/liblsquic/lsquic_full_conn_ietf.c +++ b/src/liblsquic/lsquic_full_conn_ietf.c @@ -7,6 +7,7 @@ #include #include #define _USE_MATH_DEFINES /* Need this for M_E on Windows */ +#include #include #include #include @@ -80,6 +81,7 @@ #include "ls-sfparser.h" #include "lsquic_qpack_exp.h" + #define LSQUIC_LOGGER_MODULE LSQLM_CONN #define LSQUIC_LOG_CONN_ID lsquic_conn_log_cid(&conn->ifc_conn) #include "lsquic_logger.h" @@ -96,6 +98,21 @@ #define MIN(a, b) ((a) < (b) ? (a) : (b)) #define MAX(a, b) ((a) > (b) ? (a) : (b)) +/* IETF QUIC push promise does not contain stream ID. This means that, unlike + * in GQUIC, one cannot create a stream immediately and pass it to the client. + * We may have to add a special API for IETF push promises. That's in the + * future: right now, we punt it. + */ +#define CLIENT_PUSH_SUPPORT 0 + +#define DRR_DEFICIT_CAP_MULTIPLIER 4 +/* Keep DRR quantum/deficit bursts bounded while retaining share granularity. */ +#define LSQUIC_WRITE_WEIGHT_MAX 100 + +struct ietf_full_conn; +static void update_peer_wt_support (struct ietf_full_conn *conn); + + /* IMPORTANT: Keep values of IFC_SERVER and IFC_HTTP same as LSENG_SERVER * and LSENG_HTTP. @@ -144,14 +161,17 @@ enum ifull_conn_flags enum more_flags { MF_VALIDATE_PATH = 1 << 0, - /* HOLE */ /* <- Hole! Reuse me! */ + MF_HTTP_DATAGRAMS = 1 << 1, MF_CHECK_MTU_PROBE = 1 << 2, MF_IGNORE_MISSING = 1 << 3, MF_CONN_CLOSE_PACK = 1 << 4, /* CONNECTION_CLOSE has been packetized */ MF_SEND_WRONG_COUNTS= 1 << 5, /* Send wrong ECN counts to peer */ - MF_WANT_DATAGRAM_WRITE = 1 << 6, + MF_WANT_DATAGRAM_WRITE = 1 << 6, /* Effective: conn or HTTP datagrams */ MF_DOING_0RTT = 1 << 7, MF_HAVE_HCSI = 1 << 8, /* Have HTTP Control Stream Incoming */ + MF_WANT_CONN_DG_WRITE = 1 << 9, /* Connection-level datagrams */ + MF_RESET_STREAM_AT = 1 << 10, /* RESET_STREAM_AT support is enabled */ + MF_PEER_RESET_STREAM_AT = 1 << 11, /* Peer supports RESET_STREAM_AT */ }; @@ -273,7 +293,7 @@ struct stream_id_to_ss { STAILQ_ENTRY(stream_id_to_ss) sits_next; lsquic_stream_id_t sits_stream_id; - enum http_error_code sits_error_code; + uint64_t sits_error_code; }; struct http_ctl_stream_in @@ -395,6 +415,35 @@ static const struct prio_iter_if ext_prio_iter_if = { lsquic_hpi_cleanup, }; +struct ietf_full_conn; +typedef int (*write_dispatch_f)(struct ietf_full_conn *); +static int do_write_buffered_high_prio (struct ietf_full_conn *conn); +static int do_write_events_high_prio (struct ietf_full_conn *conn); +static int do_write_buffered_other_prio (struct ietf_full_conn *conn); +static int do_write_events_low_prio (struct ietf_full_conn *conn); + +/* The functions for writing STREAM frames are ordered here by priority */ +static write_dispatch_f const write_stream_funcs[] = { + do_write_buffered_high_prio, + do_write_events_high_prio, + do_write_buffered_other_prio, + do_write_events_low_prio, +}; + +/* In addition to the stream-writing functions above, there is a function + * for writing datagrams. Together, they are put together in an array + * for the fixed (the "F" in "FW" below) writing strategy. + */ +#define ALL_FW_COUNT ((unsigned) (sizeof(write_stream_funcs) \ + / sizeof(write_stream_funcs[0]) + 1)) + +enum drr_write_class +{ + DWSC_STREAM, + DWSC_DATAGRAM, + DWSC_N_CLASSES, +}; + struct ietf_full_conn { @@ -437,7 +486,9 @@ struct ietf_full_conn lsquic_packno_t ifc_max_ack_packno[N_PNS]; lsquic_packno_t ifc_max_non_probing; struct { - uint64_t max_stream_send; + uint64_t max_stream_send_bidi_local; + uint64_t max_stream_send_bidi_remote; + uint64_t max_stream_send_uni; uint8_t ack_exp; } ifc_cfg; int (*ifc_process_incoming_packet)( @@ -458,6 +509,19 @@ struct ietf_full_conn struct { uint64_t header_table_size, qpack_blocked_streams; + uint64_t wt_max_sessions; + uint64_t wt_initial_max_data; + uint64_t wt_initial_max_streams_uni; + uint64_t wt_initial_max_streams_bidi; + unsigned wt_draft; + signed char wt_max_sessions_seen; + signed char wt_enabled; + signed char wt_enabled_seen; + signed char wt_initial_max_data_seen; + signed char wt_initial_max_streams_uni_seen; + signed char wt_initial_max_streams_bidi_seen; + signed char enable_connect_protocol; + signed char enable_connect_protocol_seen; } ifc_peer_hq_settings; struct dcid_elem *ifc_dces[MAX_IETF_CONN_DCIDS]; TAILQ_HEAD(, dcid_elem) ifc_to_retire; @@ -489,6 +553,7 @@ struct ietf_full_conn unsigned ifc_max_ack_freq_seqno; /* Incoming */ unsigned short ifc_min_dg_sz, ifc_max_dg_sz; + float ifc_write_datagram_share; lsquic_time_t ifc_last_live_update; struct conn_path ifc_paths[N_PATHS]; union { @@ -512,11 +577,28 @@ struct ietf_full_conn lsquic_time_t ifc_idle_to; lsquic_time_t ifc_ping_period; lsquic_time_t ifc_last_tick; + write_dispatch_f ifc_write_dispatch; + union { + struct { + write_dispatch_f ifwf_do_write[ALL_FW_COUNT]; + unsigned ifwf_count; + } fixed; + struct { + uint64_t ifwdrr_budget; + uint64_t ifwdrr_budget_start; + uint64_t ifwdrr_deficit[DWSC_N_CLASSES]; + unsigned ifwdrr_blocked_accum; + unsigned ifwdrr_weight[DWSC_N_CLASSES]; + unsigned char ifwdrr_next_class; + unsigned char ifwdrr_budget_active; + } drr; + } ifc_write_sched; struct lsquic_hash *ifc_bpus; uint64_t ifc_last_max_data_off_sent; struct packet_tolerance_stats ifc_pts; #if LSQUIC_CONN_STATS + uint64_t ifc_write_class_sent_bytes[DWSC_N_CLASSES]; struct conn_stats ifc_stats, *ifc_last_stats; #endif @@ -592,6 +674,38 @@ packet_tolerance_alarm_expired (enum alarm_id al_id, void *ctx, static int init_http (struct ietf_full_conn *); +static void +set_write_datagram_priority (struct ietf_full_conn *, unsigned prio); + +static void +init_write_sched_fixed (struct ietf_full_conn *conn); + +static void +init_write_sched_fixed_default (struct ietf_full_conn *conn); + +static void +init_write_sched_drr (struct ietf_full_conn *conn); + +static void +apply_write_sched_settings (struct ietf_full_conn *conn); + +static void +set_write_sched_strategy (struct ietf_full_conn *conn, unsigned strategy); + +static void +set_write_datagram_share (struct ietf_full_conn *conn, float share); + +static void +set_drr_weights_from_share (struct ietf_full_conn *conn, float share); + +static int +do_write_drr (struct ietf_full_conn *conn); + +#if LSQUIC_CONN_STATS +static void +log_write_sched_stats (const struct ietf_full_conn *conn); +#endif + static unsigned highest_bit_set (unsigned sz) { @@ -987,6 +1101,10 @@ static const struct crypto_stream_if crypto_stream_if = static const struct lsquic_stream_if *unicla_if_ptr; +const struct lsquic_stream_if *lsquic_wt_uni_stream_if (void); +#if LSQUIC_TEST +void lsquic_wt_test_set_fail_stream_ctx_alloc (unsigned count); +#endif static lsquic_stream_id_t @@ -1022,6 +1140,36 @@ avail_streams_count (const struct ietf_full_conn *conn, int server, } } +static void +queue_streams_blocked_frame (struct ietf_full_conn *conn, enum stream_dir sd); + + +static int +is_our_stream_id (const struct ietf_full_conn *conn, lsquic_stream_id_t stream_id) +{ + const unsigned is_server = !!(conn->ifc_flags & IFC_SERVER); + return (1u & stream_id) == is_server; +} + + +static uint64_t +max_send_off_for_stream (const struct ietf_full_conn *conn, + lsquic_stream_id_t stream_id) +{ + const enum stream_dir sd = (stream_id >> SD_SHIFT) & 1; + if (sd == SD_UNI) + { + if (is_our_stream_id(conn, stream_id)) + return conn->ifc_cfg.max_stream_send_uni; + else + return 0; + } + else if (is_our_stream_id(conn, stream_id)) + return conn->ifc_cfg.max_stream_send_bidi_remote; + else + return conn->ifc_cfg.max_stream_send_bidi_local; +} + /* If `priority' is negative, this means that the stream is critical */ static int @@ -1082,6 +1230,7 @@ create_bidi_stream_out (struct ietf_full_conn *conn) struct lsquic_stream *stream; lsquic_stream_id_t stream_id; enum stream_ctor_flags flags; + uint64_t max_send_off; flags = SCF_IETF|SCF_DI_AUTOSWITCH; if (conn->ifc_enpub->enp_settings.es_rw_once) @@ -1096,11 +1245,15 @@ create_bidi_stream_out (struct ietf_full_conn *conn) } stream_id = generate_stream_id(conn, SD_BIDI); + max_send_off = max_send_off_for_stream(conn, stream_id); + LSQ_DEBUG("create outgoing bidi stream %"PRIu64 + ": recv_window=%u, send_limit=%"PRIu64, stream_id, + conn->ifc_settings->es_init_max_stream_data_bidi_local, max_send_off); stream = lsquic_stream_new(stream_id, &conn->ifc_pub, conn->ifc_enpub->enp_stream_if, conn->ifc_enpub->enp_stream_if_ctx, conn->ifc_settings->es_init_max_stream_data_bidi_local, - conn->ifc_cfg.max_stream_send, flags); + max_send_off, flags); if (!stream) return -1; if (!lsquic_hash_insert(conn->ifc_pub.all_streams, &stream->id, @@ -1113,6 +1266,121 @@ create_bidi_stream_out (struct ietf_full_conn *conn) return 0; } +static struct lsquic_stream * +create_bidi_stream_out_with_if (struct ietf_full_conn *conn, + const struct lsquic_stream_if *stream_if, void *stream_if_ctx) +{ + struct lsquic_stream *stream; + lsquic_stream_id_t stream_id; + enum stream_ctor_flags flags; + uint64_t max_send_off; + + if (0 == avail_streams_count(conn, conn->ifc_flags & IFC_SERVER, SD_BIDI)) + { + queue_streams_blocked_frame(conn, SD_BIDI); + errno = ENOSPC; + return NULL; + } + + flags = SCF_IETF|SCF_DI_AUTOSWITCH; + if (conn->ifc_enpub->enp_settings.es_rw_once) + flags |= SCF_DISP_RW_ONCE; + if (conn->ifc_enpub->enp_settings.es_delay_onclose) + flags |= SCF_DELAY_ONCLOSE; + + stream_id = generate_stream_id(conn, SD_BIDI); + max_send_off = max_send_off_for_stream(conn, stream_id); + LSQ_DEBUG("create outgoing bidi stream %"PRIu64 + ": recv_window=%u, send_limit=%"PRIu64, stream_id, + conn->ifc_settings->es_init_max_stream_data_bidi_local, max_send_off); + stream = lsquic_stream_new(stream_id, &conn->ifc_pub, + stream_if, stream_if_ctx, + conn->ifc_settings->es_init_max_stream_data_bidi_local, + max_send_off, flags); + if (!stream) + return NULL; + if (!lsquic_hash_insert(conn->ifc_pub.all_streams, &stream->id, + sizeof(stream->id), stream, &stream->sm_hash_el)) + { + lsquic_stream_destroy(stream); + return NULL; + } + lsquic_stream_call_on_new(stream); + return stream; +} + +static struct lsquic_stream * +create_uni_stream_out_with_if (struct ietf_full_conn *conn, + const struct lsquic_stream_if *stream_if, void *stream_if_ctx) +{ + struct lsquic_stream *stream; + lsquic_stream_id_t stream_id; + enum stream_ctor_flags flags; + + if (0 == avail_streams_count(conn, conn->ifc_flags & IFC_SERVER, SD_UNI)) + { + queue_streams_blocked_frame(conn, SD_UNI); + errno = ENOSPC; + return NULL; + } + + flags = SCF_IETF; + if (conn->ifc_enpub->enp_settings.es_rw_once) + flags |= SCF_DISP_RW_ONCE; + if (conn->ifc_enpub->enp_settings.es_delay_onclose) + flags |= SCF_DELAY_ONCLOSE; + + stream_id = generate_stream_id(conn, SD_UNI); + stream = lsquic_stream_new(stream_id, &conn->ifc_pub, + stream_if, stream_if_ctx, 0, conn->ifc_max_stream_data_uni, + flags); + if (!stream) + return NULL; + if (!lsquic_hash_insert(conn->ifc_pub.all_streams, &stream->id, + sizeof(stream->id), stream, &stream->sm_hash_el)) + { + lsquic_stream_destroy(stream); + return NULL; + } + lsquic_stream_call_on_new(stream); + return stream; +} + + +static struct lsquic_stream * +create_push_stream (struct ietf_full_conn *conn) +{ + struct lsquic_stream *stream; + lsquic_stream_id_t stream_id; + enum stream_ctor_flags flags; + + assert((conn->ifc_flags & (IFC_SERVER|IFC_HTTP)) == (IFC_SERVER|IFC_HTTP)); + + flags = SCF_IETF|SCF_HTTP; + if (conn->ifc_enpub->enp_settings.es_rw_once) + flags |= SCF_DISP_RW_ONCE; + if (conn->ifc_enpub->enp_settings.es_delay_onclose) + flags |= SCF_DELAY_ONCLOSE; + + stream_id = generate_stream_id(conn, SD_UNI); + LSQ_DEBUG("create outgoing push stream %"PRIu64 + ": recv_window=0, send_limit=%"PRIu64, stream_id, + conn->ifc_max_stream_data_uni); + stream = lsquic_stream_new(stream_id, &conn->ifc_pub, + conn->ifc_enpub->enp_stream_if, + conn->ifc_enpub->enp_stream_if_ctx, + 0, conn->ifc_max_stream_data_uni, flags); + if (!stream) + return NULL; + if (!lsquic_hash_insert(conn->ifc_pub.all_streams, &stream->id, + sizeof(stream->id), stream, &stream->sm_hash_el)) + { + lsquic_stream_destroy(stream); + return NULL; + } + return stream; +} + /* This function looks through the SCID array searching for an available * slot. If it finds an available slot it will @@ -1236,6 +1504,8 @@ ietf_full_conn_init (struct ietf_full_conn *conn, TAILQ_INIT(&conn->ifc_pub.read_streams); TAILQ_INIT(&conn->ifc_pub.write_streams); TAILQ_INIT(&conn->ifc_pub.service_streams); + TAILQ_INIT(&conn->ifc_pub.http_dg_streams); + TAILQ_INIT(&conn->ifc_pub.wt_sessions); STAILQ_INIT(&conn->ifc_stream_ids_to_ss); TAILQ_INIT(&conn->ifc_to_retire); conn->ifc_n_to_retire = 0; @@ -1275,6 +1545,20 @@ ietf_full_conn_init (struct ietf_full_conn *conn, conn->ifc_peer_hq_settings.header_table_size = HQ_DF_QPACK_MAX_TABLE_CAPACITY; conn->ifc_peer_hq_settings.qpack_blocked_streams = HQ_DF_QPACK_BLOCKED_STREAMS; + conn->ifc_peer_hq_settings.wt_max_sessions = 0; + conn->ifc_peer_hq_settings.wt_max_sessions_seen = 0; + conn->ifc_peer_hq_settings.wt_draft = 0; + conn->ifc_peer_hq_settings.wt_enabled = 0; + conn->ifc_peer_hq_settings.wt_enabled_seen = 0; + conn->ifc_peer_hq_settings.wt_initial_max_data = 0; + conn->ifc_peer_hq_settings.wt_initial_max_data_seen = 0; + conn->ifc_peer_hq_settings.wt_initial_max_streams_uni = 0; + conn->ifc_peer_hq_settings.wt_initial_max_streams_uni_seen = 0; + conn->ifc_peer_hq_settings.wt_initial_max_streams_bidi = 0; + conn->ifc_peer_hq_settings.wt_initial_max_streams_bidi_seen = 0; + conn->ifc_peer_hq_settings.enable_connect_protocol = 0; + conn->ifc_peer_hq_settings.enable_connect_protocol_seen = 0; + conn->ifc_pub.cp_wt_peer_draft = 0; conn->ifc_flags = flags | IFC_FIRST_TICK; conn->ifc_max_ack_packno[PNS_INIT] = IQUIC_INVALID_PACKNO; @@ -1294,6 +1578,9 @@ ietf_full_conn_init (struct ietf_full_conn *conn, conn->ifc_pii = &ext_prio_iter_if; else conn->ifc_pii = &orig_prio_iter_if; + if (conn->ifc_settings->es_reset_stream_at) + conn->ifc_mflags |= MF_RESET_STREAM_AT; + init_write_sched_fixed_default(conn); return 0; } @@ -1364,7 +1651,14 @@ lsquic_ietf_full_conn_client_new (struct lsquic_engine_public *enpub, enpub->enp_settings.es_max_streams_in << SIT_SHIFT; if (flags & IFC_HTTP) - conn->ifc_max_streams_in[SD_UNI] = 3; + { + unsigned max_uni_streams = 3; + if (enpub->enp_settings.es_support_push && CLIENT_PUSH_SUPPORT) + max_uni_streams = MAX(max_uni_streams, enpub->enp_settings.es_max_streams_in); + if (enpub->enp_settings.es_init_max_streams_uni) + max_uni_streams = MAX(max_uni_streams, enpub->enp_settings.es_init_max_streams_uni); + conn->ifc_max_streams_in[SD_UNI] = max_uni_streams; + } else conn->ifc_max_streams_in[SD_UNI] = enpub->enp_settings.es_max_streams_in; conn->ifc_max_allowed_stream_id[SIT_UNI_SERVER] @@ -2443,7 +2737,7 @@ generate_max_stream_data_frame (struct ietf_full_conn *conn, static int generate_stop_sending_frame_by_id (struct ietf_full_conn *conn, - lsquic_stream_id_t stream_id, enum http_error_code error_code) + lsquic_stream_id_t stream_id, uint64_t error_code) { struct lsquic_packet_out *packet_out; size_t need; @@ -2465,7 +2759,7 @@ generate_stop_sending_frame_by_id (struct ietf_full_conn *conn, return -1; } LSQ_DEBUG("generated %d-byte STOP_SENDING frame (stream id: %"PRIu64", " - "error code: %u)", w, stream_id, error_code); + "error code: %"PRIu64")", w, stream_id, error_code); EV_LOG_GENERATED_STOP_SENDING_FRAME(LSQUIC_LOG_CONN_ID, stream_id, error_code); if (0 != lsquic_packet_out_add_frame(packet_out, conn->ifc_pub.mm, 0, @@ -2486,7 +2780,9 @@ static int generate_stop_sending_frame (struct ietf_full_conn *conn, struct lsquic_stream *stream) { - if (0 == generate_stop_sending_frame_by_id(conn, stream->id, HEC_NO_ERROR)) + const uint64_t error_code = stream->sm_ss_code; + + if (0 == generate_stop_sending_frame_by_id(conn, stream->id, error_code)) { lsquic_stream_ss_frame_sent(stream); return 1; @@ -2521,7 +2817,51 @@ generate_stop_sending_frames (struct ietf_full_conn *conn, lsquic_time_t now) } -/* Return true if generated, false otherwise */ +static int +generate_reset_stream_at_frame (struct ietf_full_conn *conn, + struct lsquic_stream *stream, uint64_t reliable_size) +{ + lsquic_packet_out_t *packet_out; + unsigned need; + int sz; + + need = conn->ifc_conn.cn_pf->pf_reset_stream_at_frame_size(stream->id, + stream->tosend_off, reliable_size, stream->error_code); + packet_out = get_writeable_packet(conn, need); + if (!packet_out) + { + LSQ_DEBUG("cannot get writeable packet for RESET_STREAM_AT frame"); + return 0; + } + sz = conn->ifc_conn.cn_pf->pf_gen_reset_stream_at_frame( + packet_out->po_data + packet_out->po_data_sz, + lsquic_packet_out_avail(packet_out), stream->id, + stream->tosend_off, reliable_size, stream->error_code); + if (sz < 0) + { + ABORT_ERROR("gen_reset_stream_at_frame failed"); + return 0; + } + if (0 != lsquic_packet_out_add_stream(packet_out, conn->ifc_pub.mm, stream, + QUIC_FRAME_RESET_STREAM_AT, packet_out->po_data_sz, sz)) + { + ABORT_ERROR("adding frame to packet failed: %d", errno); + return 0; + } + lsquic_send_ctl_incr_pack_sz(&conn->ifc_send_ctl, packet_out, sz); + packet_out->po_frame_types |= 1 << QUIC_FRAME_RESET_STREAM_AT; + lsquic_stream_rst_frame_sent(stream); + LSQ_DEBUG("wrote RESET_STREAM_AT: stream %"PRIu64"; final %"PRIu64"; " + "reliable %"PRIu64"; error code %"PRIu64, stream->id, + stream->tosend_off, reliable_size, stream->error_code); + EV_LOG_CONN_EVENT(LSQUIC_LOG_CONN_ID, "generated RESET_STREAM_AT: stream " + "%"PRIu64"; final %"PRIu64"; reliable %"PRIu64"; error code %"PRIu64, + stream->id, stream->tosend_off, reliable_size, stream->error_code); + + return 1; +} + + static int generate_rst_stream_frame (struct ietf_full_conn *conn, struct lsquic_stream *stream) @@ -2566,12 +2906,33 @@ generate_rst_stream_frame (struct ietf_full_conn *conn, } +/* Return true if generated, false otherwise */ +static int +generate_stream_reset_frame (struct ietf_full_conn *conn, + struct lsquic_stream *stream) +{ + uint64_t reliable_size; + enum quic_frame_type frame_type; + + frame_type = lsquic_stream_get_reset_frame_type(stream, &reliable_size); + if (frame_type == QUIC_FRAME_RST_STREAM) + return generate_rst_stream_frame(conn, stream); + else if (conn->ifc_mflags & MF_PEER_RESET_STREAM_AT) + return generate_reset_stream_at_frame(conn, stream, reliable_size); + else + { + LSQ_WARN("peer does not support RESET_STREAM_AT, " + "sending RST_STREAM"); + return generate_rst_stream_frame(conn, stream); + } +} + + static int is_our_stream (const struct ietf_full_conn *conn, const struct lsquic_stream *stream) { - const unsigned is_server = !!(conn->ifc_flags & IFC_SERVER); - return (1 & stream->id) == is_server; + return is_our_stream_id(conn, stream->id); } @@ -2846,7 +3207,7 @@ process_stream_ready_to_send (struct ietf_full_conn *conn, stream); } if (stream->sm_qflags & SMQF_SEND_RST) - r &= generate_rst_stream_frame(conn, stream); + r &= generate_stream_reset_frame(conn, stream); if (stream->sm_qflags & SMQF_SEND_STOP_SENDING) r &= generate_stop_sending_frame(conn, stream); return r; @@ -2883,6 +3244,29 @@ ietf_full_conn_ci_write_ack (struct lsquic_conn *lconn, generate_ack_frame_for_pns(conn, packet_out, PNS_APP, lsquic_time_now()); } +static void +update_datagram_want (struct ietf_full_conn *conn) +{ + int want; + + want = (conn->ifc_mflags & MF_WANT_CONN_DG_WRITE) + || ((conn->ifc_pub.cp_flags & CP_HTTP_DATAGRAMS) + && !TAILQ_EMPTY(&conn->ifc_pub.http_dg_streams)); + + if (want) + { + if (!(conn->ifc_mflags & MF_WANT_DATAGRAM_WRITE)) + { + conn->ifc_mflags |= MF_WANT_DATAGRAM_WRITE; + if (lsquic_send_ctl_can_send(&conn->ifc_send_ctl)) + lsquic_engine_add_conn_to_tickable(conn->ifc_enpub, + &conn->ifc_conn); + } + } + else + conn->ifc_mflags &= ~MF_WANT_DATAGRAM_WRITE; +} + static int ietf_full_conn_ci_want_datagram_write (struct lsquic_conn *lconn, int is_want) @@ -2892,16 +3276,12 @@ ietf_full_conn_ci_want_datagram_write (struct lsquic_conn *lconn, int is_want) if (conn->ifc_flags & IFC_DATAGRAMS) { - old = !!(conn->ifc_mflags & MF_WANT_DATAGRAM_WRITE); + old = !!(conn->ifc_mflags & MF_WANT_CONN_DG_WRITE); if (is_want) - { - conn->ifc_mflags |= MF_WANT_DATAGRAM_WRITE; - if (lsquic_send_ctl_can_send (&conn->ifc_send_ctl)) - lsquic_engine_add_conn_to_tickable(conn->ifc_enpub, - &conn->ifc_conn); - } + conn->ifc_mflags |= MF_WANT_CONN_DG_WRITE; else - conn->ifc_mflags &= ~MF_WANT_DATAGRAM_WRITE; + conn->ifc_mflags &= ~MF_WANT_CONN_DG_WRITE; + update_datagram_want(conn); LSQ_DEBUG("turn %s \"want datagram write\" flag", is_want ? "on" : "off"); return old; @@ -2911,6 +3291,26 @@ ietf_full_conn_ci_want_datagram_write (struct lsquic_conn *lconn, int is_want) } +static void +ietf_full_conn_ci_http_dg_streams_updated (struct lsquic_conn *lconn) +{ + struct ietf_full_conn *conn = (struct ietf_full_conn *) lconn; + + update_datagram_want(conn); +} + + +static size_t +ietf_full_conn_ci_get_max_datagram_size (struct lsquic_conn *lconn) +{ + struct ietf_full_conn *conn = (struct ietf_full_conn *) lconn; + + if (conn->ifc_flags & IFC_DATAGRAMS) + return conn->ifc_max_dg_sz; + else + return 0; +} + static void ietf_full_conn_ci_client_call_on_new (struct lsquic_conn *lconn) { @@ -3100,6 +3500,7 @@ ietf_full_conn_ci_destroy (struct lsquic_conn *lconn) lsquic_hash_erase(conn->ifc_pub.all_streams, el); lsquic_stream_destroy(stream); } + lsquic_conn_http_dg_cleanup(&conn->ifc_pub); if (conn->ifc_flags & IFC_HTTP) { lsquic_qdh_cleanup(&conn->ifc_qdh); @@ -3146,6 +3547,8 @@ ietf_full_conn_ci_destroy (struct lsquic_conn *lconn) } lsquic_hash_destroy(conn->ifc_pub.all_streams); #if LSQUIC_CONN_STATS + if (conn->ifc_flags & IFC_CREATED_OK) + log_write_sched_stats(conn); if (conn->ifc_flags & IFC_CREATED_OK) { LSQ_NOTICE("# ticks: %lu", conn->ifc_stats.n_ticks); @@ -3489,6 +3892,9 @@ apply_trans_params (struct ietf_full_conn *conn, lsquic_send_ctl_do_ql_bits(&conn->ifc_send_ctl); } + if (params->tp_set & (1 << TPI_RESET_STREAM_AT)) + conn->ifc_mflags |= MF_PEER_RESET_STREAM_AT; + if (params->tp_init_max_streams_bidi > (1ull << 60) || params->tp_init_max_streams_uni > (1ull << 60)) { @@ -3525,7 +3931,15 @@ apply_trans_params (struct ietf_full_conn *conn, el = lsquic_hash_next(conn->ifc_pub.all_streams)) { stream = lsquic_hashelem_getdata(el); - if (is_our_stream(conn, stream)) + const enum stream_dir sd = (stream->id >> SD_SHIFT) & 1; + if (sd == SD_UNI) + { + if (is_our_stream(conn, stream)) + limit = params->tp_init_max_stream_data_uni; + else + limit = 0; + } + else if (is_our_stream(conn, stream)) limit = params->tp_init_max_stream_data_bidi_remote; else limit = params->tp_init_max_stream_data_bidi_local; @@ -3537,12 +3951,12 @@ apply_trans_params (struct ietf_full_conn *conn, } } - if (conn->ifc_flags & IFC_SERVER) - conn->ifc_cfg.max_stream_send + conn->ifc_cfg.max_stream_send_bidi_local = params->tp_init_max_stream_data_bidi_local; - else - conn->ifc_cfg.max_stream_send + conn->ifc_cfg.max_stream_send_bidi_remote = params->tp_init_max_stream_data_bidi_remote; + conn->ifc_cfg.max_stream_send_uni + = params->tp_init_max_stream_data_uni; conn->ifc_cfg.ack_exp = params->tp_ack_delay_exponent; switch ((!!conn->ifc_settings->es_idle_timeout << 1) @@ -3601,17 +4015,20 @@ apply_trans_params (struct ietf_full_conn *conn, LSQ_DEBUG("timestamps enabled: will send TIMESTAMP frames"); conn->ifc_flags |= IFC_TIMESTAMPS; } - if (conn->ifc_settings->es_datagrams - && (params->tp_set & (1 << TPI_MAX_DATAGRAM_FRAME_SIZE)) - && params->tp_numerics[TPI_MAX_DATAGRAM_FRAME_SIZE] > 0) + if ((conn->ifc_settings->es_datagrams + || conn->ifc_settings->es_http_datagrams) + && (params->tp_set & (1 << TPI_MAX_DATAGRAM_FRAME_SIZE))) { LSQ_DEBUG("datagrams enabled"); conn->ifc_flags |= IFC_DATAGRAMS; - conn->ifc_max_dg_sz = - params->tp_numerics[TPI_MAX_DATAGRAM_FRAME_SIZE] > USHRT_MAX - ? USHRT_MAX : params->tp_numerics[TPI_MAX_DATAGRAM_FRAME_SIZE]; + if (params->tp_numerics[TPI_MAX_DATAGRAM_FRAME_SIZE] <= USHRT_MAX) + conn->ifc_max_dg_sz = params->tp_numerics[TPI_MAX_DATAGRAM_FRAME_SIZE]; + else + conn->ifc_max_dg_sz = USHRT_MAX; } + update_peer_wt_support(conn); + conn->ifc_pub.max_peer_ack_usec = params->tp_max_ack_delay * 1000; if ((params->tp_set & (1 << TPI_MAX_UDP_PAYLOAD_SIZE)) @@ -3708,12 +4125,8 @@ init_http (struct ietf_full_conn *conn) if (0 != lsquic_hcso_write_settings(&conn->ifc_hcso, conn->ifc_settings->es_max_header_list_size, dyn_table_size, max_risked_streams, conn->ifc_flags & IFC_SERVER -#if LSQUIC_WEBTRANSPORT_SERVER_SUPPORT , - conn->ifc_settings->es_webtransport_server, - conn->ifc_settings->es_max_webtransport_server_streams -#endif - )) + conn->ifc_settings->es_webtransport)) { ABORT_WARN("cannot write SETTINGS"); return -1; @@ -3790,6 +4203,7 @@ handshake_ok (struct lsquic_conn *lconn) : lsquic_tp_to_str)(params, buf, sizeof(buf)), buf)); if (0 != apply_trans_params(conn, params)) return -1; + apply_write_sched_settings(conn); dce = get_new_dce(conn); if (!dce) @@ -4196,31 +4610,79 @@ maybe_conn_flush_special_streams (struct ietf_full_conn *conn) } -static int -write_is_possible (struct ietf_full_conn *conn) +static uint64_t +write_budget_used (const struct ietf_full_conn *conn) { - const lsquic_packet_out_t *packet_out; - - packet_out = lsquic_send_ctl_last_scheduled(&conn->ifc_send_ctl, PNS_APP, - CUR_NPATH(conn), 0); - return (packet_out && lsquic_packet_out_avail(packet_out) > 10) - || lsquic_send_ctl_can_send(&conn->ifc_send_ctl); + if (conn->ifc_send_ctl.sc_bytes_scheduled + > conn->ifc_write_sched.drr.ifwdrr_budget_start) + return conn->ifc_send_ctl.sc_bytes_scheduled + - conn->ifc_write_sched.drr.ifwdrr_budget_start; + else + return 0; } static void -process_streams_write_events (struct ietf_full_conn *conn, int high_prio) +write_budget_set (struct ietf_full_conn *conn, uint64_t budget) { - struct lsquic_stream *stream; - union prio_iter pi; + conn->ifc_write_sched.drr.ifwdrr_budget = budget; + conn->ifc_write_sched.drr.ifwdrr_budget_start = + conn->ifc_send_ctl.sc_bytes_scheduled; + conn->ifc_write_sched.drr.ifwdrr_budget_active = 1; +} - conn->ifc_pii->pii_init(&pi, TAILQ_FIRST(&conn->ifc_pub.write_streams), - TAILQ_LAST(&conn->ifc_pub.write_streams, lsquic_streams_tailq), - (uintptr_t) &TAILQ_NEXT((lsquic_stream_t *) NULL, next_write_stream), - &conn->ifc_pub, - high_prio ? "write-high" : "write-low", NULL, NULL); - if (high_prio) +static void +write_budget_clear (struct ietf_full_conn *conn) +{ + conn->ifc_write_sched.drr.ifwdrr_budget_active = 0; + conn->ifc_write_sched.drr.ifwdrr_budget = 0; + conn->ifc_write_sched.drr.ifwdrr_budget_start = 0; +} + + +static int +write_budget_is_exhausted (const struct ietf_full_conn *conn) +{ + return conn->ifc_write_dispatch == do_write_drr + && conn->ifc_write_sched.drr.ifwdrr_budget_active + && write_budget_used(conn) >= conn->ifc_write_sched.drr.ifwdrr_budget; +} + + +static int +write_is_possible_unbudgeted (struct ietf_full_conn *conn) +{ + const lsquic_packet_out_t *packet_out; + + packet_out = lsquic_send_ctl_last_scheduled(&conn->ifc_send_ctl, PNS_APP, + CUR_NPATH(conn), 0); + return (packet_out && lsquic_packet_out_avail(packet_out) > 10) + || lsquic_send_ctl_can_send(&conn->ifc_send_ctl); +} + + +static int +write_is_possible (struct ietf_full_conn *conn) +{ + return !write_budget_is_exhausted(conn) + && write_is_possible_unbudgeted(conn); +} + + +static void +process_streams_write_events (struct ietf_full_conn *conn, int high_prio) +{ + struct lsquic_stream *stream; + union prio_iter pi; + + conn->ifc_pii->pii_init(&pi, TAILQ_FIRST(&conn->ifc_pub.write_streams), + TAILQ_LAST(&conn->ifc_pub.write_streams, lsquic_streams_tailq), + (uintptr_t) &TAILQ_NEXT((lsquic_stream_t *) NULL, next_write_stream), + &conn->ifc_pub, + high_prio ? "write-high" : "write-low", NULL, NULL); + + if (high_prio) conn->ifc_pii->pii_drop_non_high(&pi); else conn->ifc_pii->pii_drop_high(&pi); @@ -5142,7 +5604,7 @@ find_stream_by_id (struct ietf_full_conn *conn, lsquic_stream_id_t stream_id) static void maybe_schedule_ss_for_stream (struct ietf_full_conn *conn, - lsquic_stream_id_t stream_id, enum http_error_code error_code) + lsquic_stream_id_t stream_id, uint64_t error_code) { struct stream_id_to_ss *sits; @@ -5181,12 +5643,14 @@ new_stream (struct ietf_full_conn *conn, lsquic_stream_id_t stream_id, void *stream_ctx; struct lsquic_stream *stream; unsigned initial_window; + uint64_t max_send_off; const int call_on_new = flags & SCF_CALL_ON_NEW; + const enum stream_dir sd = (stream_id >> SD_SHIFT) & 1; flags &= ~SCF_CALL_ON_NEW; flags |= SCF_DI_AUTOSWITCH|SCF_IETF; - if ((conn->ifc_flags & IFC_HTTP) && ((stream_id >> SD_SHIFT) & 1) == SD_UNI) + if ((conn->ifc_flags & IFC_HTTP) && sd == SD_UNI) { iface = unicla_if_ptr; stream_ctx = conn; @@ -5212,16 +5676,22 @@ new_stream (struct ietf_full_conn *conn, lsquic_stream_id_t stream_id, } } - if (((stream_id >> SD_SHIFT) & 1) == SD_UNI) + if (sd == SD_UNI) initial_window = conn->ifc_enpub->enp_settings .es_init_max_stream_data_uni; else initial_window = conn->ifc_enpub->enp_settings .es_init_max_stream_data_bidi_remote; + max_send_off = max_send_off_for_stream(conn, stream_id); + LSQ_DEBUG("create incoming %sdirectional stream %"PRIu64 + ": recv_window=%u, send_limit=%"PRIu64, + sd == SD_BIDI ? "bi" : "uni", stream_id, initial_window, + max_send_off); + stream = lsquic_stream_new(stream_id, &conn->ifc_pub, iface, stream_ctx, initial_window, - conn->ifc_cfg.max_stream_send, flags); + max_send_off, flags); if (stream) { if (conn->ifc_bpus) @@ -5343,6 +5813,81 @@ process_rst_stream_frame (struct ietf_full_conn *conn, } +static unsigned +process_reset_stream_at_frame (struct ietf_full_conn *conn, + struct lsquic_packet_in *packet_in, const unsigned char *p, size_t len) +{ + lsquic_stream_id_t stream_id; + uint64_t final_size, reliable_size, error_code; + lsquic_stream_t *stream; + int call_on_new; + int parsed_len; + + if (!(conn->ifc_mflags & MF_RESET_STREAM_AT)) + { + ABORT_QUIETLY(0, TEC_PROTOCOL_VIOLATION, + "Received unexpected RESET_STREAM_AT frame (not negotiated)"); + return 0; + } + + parsed_len = conn->ifc_conn.cn_pf->pf_parse_reset_stream_at_frame(p, len, + &stream_id, &error_code, &final_size, + &reliable_size); + if (parsed_len < 0) + { + ABORT_QUIETLY(0, TEC_FRAME_ENCODING_ERROR, + "cannot parse RESET_STREAM_AT"); + return 0; + } + + LSQ_DEBUG("Got RESET_STREAM_AT; stream: %"PRIu64"; final: %"PRIu64"; " + "reliable: %"PRIu64, stream_id, final_size, reliable_size); + + if (conn_is_send_only_stream(conn, stream_id)) + { + ABORT_QUIETLY(0, TEC_STREAM_STATE_ERROR, + "received RESET_STREAM_AT on send-only stream %"PRIu64, stream_id); + return 0; + } + + call_on_new = 0; + stream = find_stream_by_id(conn, stream_id); + if (!stream) + { + if (conn_is_stream_closed(conn, stream_id)) + { + LSQ_DEBUG("got reset_stream_at frame for closed stream %"PRIu64, + stream_id); + return parsed_len; + } + if (!is_peer_initiated(conn, stream_id)) + { + ABORT_ERROR("received reset for never-initiated stream %"PRIu64, + stream_id); + return 0; + } + + stream = new_stream(conn, stream_id, 0); + if (!stream) + { + ABORT_ERROR("cannot create new stream: %s", strerror(errno)); + return 0; + } + ++call_on_new; + } + + if (0 != lsquic_stream_reset_stream_at_in(stream, final_size, + reliable_size, error_code)) + { + ABORT_ERROR("received invalid RESET_STREAM_AT"); + return 0; + } + if (call_on_new) + lsquic_stream_call_on_new(stream); + return parsed_len; +} + + static unsigned process_stop_sending_frame (struct ietf_full_conn *conn, struct lsquic_packet_in *packet_in, const unsigned char *p, size_t len) @@ -5599,6 +6144,16 @@ process_crypto_frame (struct ietf_full_conn *conn, } +static int +http3_server_bidi_stream_forbidden (const struct ietf_full_conn *conn, + lsquic_stream_id_t stream_id) +{ + return (conn->ifc_flags & (IFC_SERVER|IFC_HTTP)) == IFC_HTTP + && SIT_BIDI_SERVER == (stream_id & SIT_MASK) + && !(conn->ifc_pub.cp_flags & CP_WEBTRANSPORT); +} + + static unsigned process_stream_frame (struct ietf_full_conn *conn, struct lsquic_packet_in *packet_in, const unsigned char *p, size_t len) @@ -5633,8 +6188,7 @@ process_stream_frame (struct ietf_full_conn *conn, return 0; } - if ((conn->ifc_flags & (IFC_SERVER|IFC_HTTP)) == IFC_HTTP - && SIT_BIDI_SERVER == (stream_frame->stream_id & SIT_MASK)) + if (http3_server_bidi_stream_forbidden(conn, stream_frame->stream_id)) { ABORT_QUIETLY(1, HEC_STREAM_CREATION_ERROR, "HTTP/3 server " "is not allowed to initiate bidirectional streams (got " @@ -5960,8 +6514,7 @@ process_max_stream_data_frame (struct ietf_full_conn *conn, { if (is_peer_initiated(conn, stream_id)) { - if ((conn->ifc_flags & (IFC_SERVER|IFC_HTTP)) == IFC_HTTP - && SIT_BIDI_SERVER == (stream_id & SIT_MASK)) + if (http3_server_bidi_stream_forbidden(conn, stream_id)) { ABORT_QUIETLY(1, HEC_STREAM_CREATION_ERROR, "HTTP/3 server " "is not allowed to initiate bidirectional streams (got " @@ -6603,6 +7156,97 @@ process_timestamp_frame (struct ietf_full_conn *conn, return 0; } +static unsigned +process_http_dg_frame (struct ietf_full_conn *conn, + const unsigned char *data, size_t data_sz, + unsigned parsed_len) +{ + const unsigned char *p = data; + const unsigned char *end = p + data_sz; + uint64_t qsid; + int nread; + lsquic_stream_id_t stream_id; + struct lsquic_stream *stream; + struct lsquic_hash_elem *el; + const struct lsquic_http_dg_if *dg_if; + + if (!(conn->ifc_mflags & MF_HTTP_DATAGRAMS) + || !conn->ifc_settings->es_http_datagrams) + { + ABORT_QUIETLY(1, HEC_DATAGRAM_ERROR, + "HTTP Datagram received without SETTINGS_H3_DATAGRAM"); + return 0; + } + + /* RFC 9297, Section 2.1: Quarter Stream ID is a varint. */ + nread = vint_read(p, end, &qsid); + if (nread <= 0) + { + ABORT_QUIETLY(1, HEC_DATAGRAM_ERROR, + "invalid HTTP Datagram Quarter Stream ID"); + return 0; + } + + if (qsid > VINT_MAX_VALUE / 4) + { + ABORT_QUIETLY(1, HEC_DATAGRAM_ERROR, + "HTTP Datagram Quarter Stream ID too large"); + return 0; + } + + /* RFC 9297, Section 2.1: Quarter Stream ID maps to stream ID * 4. */ + stream_id = (lsquic_stream_id_t) (qsid * 4); + LSQ_DEBUG("HTTP Datagram qsid=%"PRIu64" stream=%"PRIu64, qsid, stream_id); + /* RFC 9297, Section 2.1: Quarter Stream ID refers to client-initiated + * bidirectional request streams (stream IDs divisible by 4). + */ + if ((stream_id & SIT_MASK) != SIT_BIDI_CLIENT) + { + ABORT_QUIETLY(1, HEC_DATAGRAM_ERROR, + "HTTP Datagram stream ID %"PRIu64" is invalid", stream_id); + return 0; + } + + el = lsquic_hash_find(conn->ifc_pub.all_streams, &stream_id, + sizeof(stream_id)); + if (!el) + { + ABORT_QUIETLY(1, HEC_DATAGRAM_ERROR, + "HTTP Datagram for stream %"PRIu64" not found", stream_id); + return 0; + } + + stream = lsquic_hashelem_getdata(el); + LSQ_DEBUG("HTTP Datagram stream %"PRIu64" ptr=%p", + stream_id, (void *) stream); + dg_if = lsquic_conn_http_dg_get_if(&conn->ifc_pub, stream_id); + if (dg_if && dg_if->on_http_dg_read) + { + lsquic_stream_hist_http_dg_recv(stream); + EV_LOG_CONN_EVENT(LSQUIC_LOG_CONN_ID, + "RX HTTP DATAGRAM: stream=%"PRIu64" size=%zu", + stream_id, (size_t) (end - (p + nread))); + LSQ_DEBUG("HTTP Datagram deliver to stream %"PRIu64, stream_id); + dg_if->on_http_dg_read(stream, stream->st_ctx, + p + nread, end - (p + nread)); + } + else if (stream->stream_if && stream->stream_if->on_http_dg_read) + { + lsquic_stream_hist_http_dg_recv(stream); + EV_LOG_CONN_EVENT(LSQUIC_LOG_CONN_ID, + "RX HTTP DATAGRAM: stream=%"PRIu64" size=%zu", + stream_id, (size_t) (end - (p + nread))); + LSQ_DEBUG("HTTP Datagram deliver to stream %"PRIu64, stream_id); + stream->stream_if->on_http_dg_read(stream, stream->st_ctx, + p + nread, end - (p + nread)); + } + else + LSQ_DEBUG("HTTP Datagram stream %"PRIu64" has no callback", + stream_id); + + return parsed_len; +} + static unsigned process_datagram_frame (struct ietf_full_conn *conn, @@ -6626,8 +7270,13 @@ process_datagram_frame (struct ietf_full_conn *conn, EV_LOG_CONN_EVENT(LSQUIC_LOG_CONN_ID, "%zd-byte DATAGRAM", data_sz); LSQ_DEBUG("%zd-byte DATAGRAM", data_sz); + LSQ_DEBUG("process DATAGRAM: ifc_flags=0x%X", conn->ifc_flags); - conn->ifc_enpub->enp_stream_if->on_datagram(&conn->ifc_conn, data, data_sz); + if ((conn->ifc_flags & IFC_HTTP) + && (conn->ifc_pub.cp_flags & CP_HTTP_DATAGRAMS)) + return process_http_dg_frame(conn, data, data_sz, parsed_len); + else + conn->ifc_enpub->enp_stream_if->on_datagram(&conn->ifc_conn, data, data_sz); return parsed_len; } @@ -6642,6 +7291,7 @@ static process_frame_f const process_frames[N_QUIC_FRAMES] = { [QUIC_FRAME_PADDING] = process_padding_frame, [QUIC_FRAME_RST_STREAM] = process_rst_stream_frame, + [QUIC_FRAME_RESET_STREAM_AT] = process_reset_stream_at_frame, [QUIC_FRAME_CONNECTION_CLOSE] = process_connection_close_frame, [QUIC_FRAME_MAX_DATA] = process_max_data_frame, [QUIC_FRAME_MAX_STREAM_DATA] = process_max_stream_data_frame, @@ -8106,7 +8756,145 @@ ietf_full_conn_ci_set_min_datagram_size (struct lsquic_conn *lconn, } +struct http_dg_consume_ctx +{ + unsigned char *payload_buf; + size_t payload_buf_sz; + size_t payload_sz; + int consumed; + int encapsulated; +}; + +static int +http_dg_consume (lsquic_stream_t *stream, const void *buf, size_t sz, + enum lsquic_http_dg_send_mode mode) +{ + struct http_dg_consume_ctx *ctx; + + ctx = stream->sm_http_dg_consume_ctx; + if (!ctx || ctx->consumed) + { + errno = EINVAL; + return -1; + } + + if (mode != LSQUIC_HTTP_DG_SEND_CAPSULE && sz <= ctx->payload_buf_sz) + { + memcpy(ctx->payload_buf, buf, sz); + ctx->payload_sz = sz; + ctx->consumed = 1; + lsquic_stream_hist_http_dg_send(stream); + EV_LOG_CONN_EVENT(lsquic_conn_log_cid(stream->conn_pub->lconn), + "TX HTTP DATAGRAM: stream=%"PRIu64" size=%zu", + stream->id, sz); + return 0; + } + + if (mode == LSQUIC_HTTP_DG_SEND_DATAGRAM) + { + errno = EMSGSIZE; + return -1; + } + + if (0 != lsquic_stream_http_dg_queue_capsule(stream, buf, sz)) + return -1; + + ctx->consumed = 1; + ctx->encapsulated = 1; + EV_LOG_CONN_EVENT(lsquic_conn_log_cid(stream->conn_pub->lconn), + "TX HTTP DG capsule: stream=%"PRIu64" size=%zu", + stream->id, sz); + return 0; +} + /* Return true if datagram was written, false otherwise */ +static ssize_t +http_dg_write_cb (struct lsquic_conn *lconn, void *buf, size_t sz) +{ + struct ietf_full_conn *conn = (struct ietf_full_conn *) lconn; + struct lsquic_stream *stream; + struct http_dg_consume_ctx ctx; + const struct lsquic_http_dg_if *dg_if; + int (*on_http_dg_write)(lsquic_stream_t *s, lsquic_stream_ctx_t *h, + size_t max_quic_payload, + lsquic_http_dg_consume_f consume_datagram); + uint64_t qsid; + uint64_t bits; + size_t vlen; + int rc; + + if (!(conn->ifc_pub.cp_flags & CP_HTTP_DATAGRAMS)) + return -1; + + stream = TAILQ_FIRST(&conn->ifc_pub.http_dg_streams); + if (!stream) + return -1; + dg_if = lsquic_conn_http_dg_get_if(&conn->ifc_pub, stream->id); + if (dg_if && dg_if->on_http_dg_write) + on_http_dg_write = dg_if->on_http_dg_write; + else if (stream->stream_if && stream->stream_if->on_http_dg_write) + on_http_dg_write = stream->stream_if->on_http_dg_write; + else + return -1; + if (lsquic_stream_http_dg_capsule_pending(stream)) + { + errno = EAGAIN; + return -1; + } + + /* RFC 9297, Section 2.1: Quarter Stream ID is stream ID / 4. */ + qsid = stream->id / 4; + bits = vint_val2bits(qsid); + vlen = 1u << bits; + if (vlen >= sz) + { + errno = EMSGSIZE; + return -1; + } + + memset(&ctx, 0, sizeof(ctx)); + ctx.payload_buf = (unsigned char *) buf + vlen; + ctx.payload_buf_sz = sz - vlen; + + assert(!stream->sm_http_dg_consume_ctx); + stream->sm_http_dg_consume_ctx = &ctx; + rc = on_http_dg_write(stream, stream->st_ctx, + ctx.payload_buf_sz, http_dg_consume); + stream->sm_http_dg_consume_ctx = NULL; + + if (!ctx.consumed) + { + if (rc != 0) + return -1; + errno = EAGAIN; + return -1; + } + + if (stream->sm_qflags & SMQF_WANT_HTTP_DG) + { + TAILQ_REMOVE(&conn->ifc_pub.http_dg_streams, stream, + next_http_dg_stream); + TAILQ_INSERT_TAIL(&conn->ifc_pub.http_dg_streams, stream, + next_http_dg_stream); + } + + if (ctx.encapsulated) + { + errno = EAGAIN; + return -1; + } + + if (ctx.payload_sz > ctx.payload_buf_sz) + { + errno = EMSGSIZE; + return -1; + } + + vint_write(buf, qsid, bits, vlen); + return (ssize_t) (vlen + ctx.payload_sz); +} + + static int write_datagram (struct ietf_full_conn *conn) { @@ -8119,16 +8907,29 @@ write_datagram (struct ietf_full_conn *conn) if (!packet_out) return 0; - w = conn->ifc_conn.cn_pf->pf_gen_datagram_frame( - packet_out->po_data + packet_out->po_data_sz, - lsquic_packet_out_avail(packet_out), conn->ifc_min_dg_sz, - conn->ifc_max_dg_sz, - conn->ifc_enpub->enp_stream_if->on_dg_write, &conn->ifc_conn); + if (!TAILQ_EMPTY(&conn->ifc_pub.http_dg_streams) + && (conn->ifc_pub.cp_flags & CP_HTTP_DATAGRAMS)) + w = conn->ifc_conn.cn_pf->pf_gen_datagram_frame( + packet_out->po_data + packet_out->po_data_sz, + lsquic_packet_out_avail(packet_out), conn->ifc_min_dg_sz, + conn->ifc_max_dg_sz, + http_dg_write_cb, &conn->ifc_conn); + else + w = conn->ifc_conn.cn_pf->pf_gen_datagram_frame( + packet_out->po_data + packet_out->po_data_sz, + lsquic_packet_out_avail(packet_out), conn->ifc_min_dg_sz, + conn->ifc_max_dg_sz, + conn->ifc_enpub->enp_stream_if->on_dg_write, &conn->ifc_conn); if (w < 0) { - LSQ_DEBUG("could not generate DATAGRAM frame"); + if (errno != EAGAIN) + LSQ_DEBUG("could not generate DATAGRAM frame"); return 0; } + if (!TAILQ_EMPTY(&conn->ifc_pub.http_dg_streams) + && (conn->ifc_pub.cp_flags & CP_HTTP_DATAGRAMS)) + EV_LOG_CONN_EVENT(LSQUIC_LOG_CONN_ID, + "TX HTTP DATAGRAM: size=%d", w); if (0 != lsquic_packet_out_add_frame(packet_out, conn->ifc_pub.mm, 0, QUIC_FRAME_DATAGRAM, packet_out->po_data_sz, w)) { @@ -8170,49 +8971,446 @@ ietf_full_conn_ci_user_stream_progress (struct lsquic_conn *lconn) } -static enum tick_st -ietf_full_conn_ci_tick (struct lsquic_conn *lconn, lsquic_time_t now) +static int +do_write_buffered_some_prio (struct ietf_full_conn *conn, + enum buf_packet_type bpt) { - struct ietf_full_conn *conn = (struct ietf_full_conn *) lconn; - int have_delayed_packets, s; - enum tick_st tick = 0; - unsigned n; + int s = lsquic_send_ctl_schedule_buffered(&conn->ifc_send_ctl, bpt); + conn->ifc_flags |= (s < 0) << IFC_BIT_ERROR; + return !write_is_possible(conn); +} -#define CLOSE_IF_NECESSARY() do { \ - if (conn->ifc_flags & IFC_IMMEDIATE_CLOSE_FLAGS) \ - { \ - tick |= immediate_close(conn); \ - goto close_end; \ - } \ -} while (0) -#define RETURN_IF_OUT_OF_PACKETS() do { \ - if (!lsquic_send_ctl_can_send(&conn->ifc_send_ctl)) \ - { \ - if (0 == lsquic_send_ctl_n_scheduled(&conn->ifc_send_ctl)) \ - { \ - LSQ_DEBUG("used up packet allowance, quiet now (line %d)", \ - __LINE__); \ - tick |= TICK_QUIET; \ - } \ - else \ - { \ - LSQ_DEBUG("used up packet allowance, sending now (line %d)",\ - __LINE__); \ - tick |= TICK_SEND; \ - } \ - goto end; \ - } \ -} while (0) +static int +do_write_buffered_high_prio (struct ietf_full_conn *conn) +{ + return do_write_buffered_some_prio(conn, BPT_HIGHEST_PRIO); +} - CONN_STATS(n_ticks, 1); - CLOSE_IF_NECESSARY(); +static int +do_write_buffered_other_prio (struct ietf_full_conn *conn) +{ + return do_write_buffered_some_prio(conn, BPT_OTHER_PRIO); +} - if (conn->ifc_flags & IFC_HAVE_SAVED_ACK) - { - (void) /* If there is an error, we'll fail shortly */ - process_ack(conn, &conn->ifc_ack, conn->ifc_saved_ack_received, now); + +static int +do_write_events_some_prio (struct ietf_full_conn *conn, int high_prio) +{ + if (!TAILQ_EMPTY(&conn->ifc_pub.write_streams)) + { + process_streams_write_events(conn, high_prio); + return !write_is_possible(conn); + } + else + return 0; +} + + +static int +do_write_events_high_prio (struct ietf_full_conn *conn) +{ + return do_write_events_some_prio(conn, 1); +} + + +static int +do_write_events_low_prio (struct ietf_full_conn *conn) +{ + return do_write_events_some_prio(conn, 0); +} + + +static int +do_write_datagram (struct ietf_full_conn *conn) +{ + while ((conn->ifc_mflags & MF_WANT_DATAGRAM_WRITE) && write_datagram(conn)) + if (!write_is_possible(conn)) + return 1; + return 0; +} + + +static int +do_write_stream (struct ietf_full_conn *conn) +{ + const size_t count = sizeof(write_stream_funcs) / sizeof(write_stream_funcs[0]); + const write_dispatch_f *const end = write_stream_funcs + count; + const write_dispatch_f *f; + + for (f = write_stream_funcs; f < end; ++f) + if ((*f)(conn)) + return 1; + + return 0; +} + + +static write_dispatch_f const write_dispatch_by_drr_class[DWSC_N_CLASSES] = { + [DWSC_STREAM] = do_write_stream, + [DWSC_DATAGRAM] = do_write_datagram, +}; + + +#if LSQUIC_CONN_STATS +static const char *const write_sched_class_str[DWSC_N_CLASSES] = { + [DWSC_STREAM] = "STREAM", + [DWSC_DATAGRAM] = "DATAGRAM", +}; +#endif + + +static unsigned +drr_next_blocked_class (struct ietf_full_conn *conn) +{ + const unsigned stream_weight = + conn->ifc_write_sched.drr.ifwdrr_weight[DWSC_STREAM]; + const unsigned datagram_weight = + conn->ifc_write_sched.drr.ifwdrr_weight[DWSC_DATAGRAM]; + const unsigned total = stream_weight + datagram_weight; + + if (0 == total || 0 == datagram_weight) + return DWSC_STREAM; + if (0 == stream_weight) + return DWSC_DATAGRAM; + + conn->ifc_write_sched.drr.ifwdrr_blocked_accum = + (conn->ifc_write_sched.drr.ifwdrr_blocked_accum + + datagram_weight) % total; + + if (conn->ifc_write_sched.drr.ifwdrr_blocked_accum < datagram_weight) + return DWSC_DATAGRAM; + else + return DWSC_STREAM; +} + + +static int +do_write_fixed (struct ietf_full_conn *conn) +{ +#if LSQUIC_CONN_STATS + uint64_t scheduled_before, scheduled_after, used; + enum drr_write_class class_id; +#endif + unsigned i; + + for (i = 0; i < conn->ifc_write_sched.fixed.ifwf_count; ++i) + { +#if LSQUIC_CONN_STATS + scheduled_before = conn->ifc_send_ctl.sc_bytes_scheduled; +#endif + if (conn->ifc_write_sched.fixed.ifwf_do_write[i](conn)) + return 1; +#if LSQUIC_CONN_STATS + scheduled_after = conn->ifc_send_ctl.sc_bytes_scheduled; + if (scheduled_after > scheduled_before) + used = scheduled_after - scheduled_before; + else + used = 0; + if (do_write_datagram == conn->ifc_write_sched.fixed.ifwf_do_write[i]) + class_id = DWSC_DATAGRAM; + else + class_id = DWSC_STREAM; + conn->ifc_write_class_sent_bytes[class_id] += used; +#endif + } + return 0; +} + + +static int +do_write_drr (struct ietf_full_conn *conn) +{ + uint64_t deficit_cap, quantum, scheduled_before, scheduled_after, used; + const unsigned class_count = DWSC_N_CLASSES; + unsigned i, mtu, class_id, start_class; + int stop, budget_exhausted; + + if (CUR_NPATH(conn)->np_pack_size) + mtu = CUR_NPATH(conn)->np_pack_size; + else + mtu = 1200; + + for (i = 0; i < class_count; ++i) + { + if (0 == conn->ifc_write_sched.drr.ifwdrr_weight[i]) + { + conn->ifc_write_sched.drr.ifwdrr_deficit[i] = 0; + continue; + } + quantum = (uint64_t) mtu * conn->ifc_write_sched.drr.ifwdrr_weight[i]; + deficit_cap = quantum * DRR_DEFICIT_CAP_MULTIPLIER; + if (conn->ifc_write_sched.drr.ifwdrr_deficit[i] > deficit_cap - quantum) + conn->ifc_write_sched.drr.ifwdrr_deficit[i] = deficit_cap; + else + conn->ifc_write_sched.drr.ifwdrr_deficit[i] += quantum; + } + + start_class = conn->ifc_write_sched.drr.ifwdrr_next_class; + for (i = 0; i < class_count; ++i) + { + class_id = (start_class + i) % class_count; + if (conn->ifc_write_sched.drr.ifwdrr_deficit[class_id] < mtu) + continue; + scheduled_before = conn->ifc_send_ctl.sc_bytes_scheduled; + write_budget_set(conn, conn->ifc_write_sched.drr.ifwdrr_deficit[class_id]); + stop = write_dispatch_by_drr_class[class_id](conn); + budget_exhausted = write_budget_is_exhausted(conn); + write_budget_clear(conn); + scheduled_after = conn->ifc_send_ctl.sc_bytes_scheduled; + if (scheduled_after > scheduled_before) + used = scheduled_after - scheduled_before; + else + used = 0; +#if LSQUIC_CONN_STATS + conn->ifc_write_class_sent_bytes[class_id] += used; +#endif + if (used >= conn->ifc_write_sched.drr.ifwdrr_deficit[class_id]) + conn->ifc_write_sched.drr.ifwdrr_deficit[class_id] = 0; + else + conn->ifc_write_sched.drr.ifwdrr_deficit[class_id] -= used; + if (stop) + { + if (!budget_exhausted) + { + conn->ifc_write_sched.drr.ifwdrr_next_class = + drr_next_blocked_class(conn); + return 1; + } + } + conn->ifc_write_sched.drr.ifwdrr_next_class = (class_id + 1) + % class_count; + } + + return 0; +} + + +#if LSQUIC_CONN_STATS +static void +log_write_sched_stats (const struct ietf_full_conn *conn) +{ + enum drr_write_class class_id; + uint64_t total = 0; + + for (class_id = 0; class_id < DWSC_N_CLASSES; ++class_id) + { + LSQ_NOTICE("write scheduler class bytes: class=%s bytes=%"PRIu64, + write_sched_class_str[class_id], + conn->ifc_write_class_sent_bytes[class_id]); + total += conn->ifc_write_class_sent_bytes[class_id]; + } + + /* There are only two classes for now and we are interested in the + * datagram share -- log it explicitly for ease: + */ + if (total) + LSQ_NOTICE("datagram target share: %.3f; actual share: %.3f", + conn->ifc_write_datagram_share, + conn->ifc_write_class_sent_bytes[DWSC_DATAGRAM] / (float) total); +} +#endif + + +static void +set_write_datagram_priority (struct ietf_full_conn *conn, unsigned prio) +{ + unsigned src, dst; + + if (prio >= ALL_FW_COUNT) + { + LSQ_WARN("invalid write datagram priority %u: ignore", prio); + return; + } + + for (src = 0, dst = 0; dst < ALL_FW_COUNT; ++dst) + if (dst == prio) + conn->ifc_write_sched.fixed.ifwf_do_write[dst] = do_write_datagram; + else + conn->ifc_write_sched.fixed.ifwf_do_write[dst] + = write_stream_funcs[src++]; + assert(src == sizeof(write_stream_funcs) / sizeof(write_stream_funcs[0])); + conn->ifc_write_sched.fixed.ifwf_count = ALL_FW_COUNT; +} + + +/* Optimization: if datagrams are not supported, do not call that function */ +static void +init_do_write_stream_only (struct ietf_full_conn *conn) +{ + set_write_datagram_priority(conn, ALL_FW_COUNT - 1); + conn->ifc_write_sched.fixed.ifwf_count = ALL_FW_COUNT - 1; +} + + +static int +can_write_datagrams (const struct ietf_full_conn *conn) +{ + return (conn->ifc_flags & IFC_DATAGRAMS) + || (conn->ifc_pub.cp_flags & CP_HTTP_DATAGRAMS); +} + + +static unsigned +get_write_datagram_priority (const struct ietf_full_conn *conn) +{ + unsigned i; + + for (i = 0; i < ALL_FW_COUNT; ++i) + if (conn->ifc_write_sched.fixed.ifwf_do_write[i] == do_write_datagram) + return i; + return ALL_FW_COUNT; +} + + +static void +set_write_datagram_share (struct ietf_full_conn *conn, float share) +{ + if (share != share || share < 0.f || share > 1.f) + { + LSQ_WARN("invalid datagram share %.3f", share); + return; + } + + conn->ifc_write_datagram_share = share; + + if (conn->ifc_write_dispatch == do_write_drr) + set_drr_weights_from_share(conn, share); +} + + +static void +set_drr_weights_from_share (struct ietf_full_conn *conn, float share) +{ + unsigned datagram_weight, stream_weight; + + datagram_weight = (unsigned) (share * LSQUIC_WRITE_WEIGHT_MAX + 0.5f); + if (datagram_weight > LSQUIC_WRITE_WEIGHT_MAX) + datagram_weight = LSQUIC_WRITE_WEIGHT_MAX; + stream_weight = LSQUIC_WRITE_WEIGHT_MAX - datagram_weight; + + conn->ifc_write_sched.drr.ifwdrr_weight[DWSC_STREAM] + = stream_weight; + conn->ifc_write_sched.drr.ifwdrr_weight[DWSC_DATAGRAM] + = datagram_weight; + conn->ifc_write_sched.drr.ifwdrr_blocked_accum = 0; +} + + +static void +init_write_sched_fixed (struct ietf_full_conn *conn) +{ + if (can_write_datagrams(conn)) + set_write_datagram_priority(conn, conn->ifc_settings->es_write_datagram_prio); + else + init_do_write_stream_only(conn); + conn->ifc_write_datagram_share = conn->ifc_settings->es_write_datagram_share; + conn->ifc_write_dispatch = do_write_fixed; +} + + +static void +init_write_sched_fixed_default (struct ietf_full_conn *conn) +{ + if (can_write_datagrams(conn)) + set_write_datagram_priority(conn, LSQUIC_DF_WRITE_DATAGRAM_PRIO); + else + init_do_write_stream_only(conn); + conn->ifc_write_datagram_share = LSQUIC_DF_WRITE_DATAGRAM_SHARE; + conn->ifc_write_dispatch = do_write_fixed; +} + + +static void +init_write_sched_drr (struct ietf_full_conn *conn) +{ + memset(&conn->ifc_write_sched.drr, 0, sizeof(conn->ifc_write_sched.drr)); + conn->ifc_write_datagram_share = conn->ifc_settings->es_write_datagram_share; + set_drr_weights_from_share(conn, conn->ifc_write_datagram_share); + conn->ifc_write_dispatch = do_write_drr; +} + + +static void +apply_write_sched_settings (struct ietf_full_conn *conn) +{ + if (conn->ifc_settings->es_write_sched_strategy == LSQWSS_DRR) + init_write_sched_drr(conn); + else + init_write_sched_fixed(conn); +} + + +static void +set_write_sched_strategy (struct ietf_full_conn *conn, + enum lsquic_write_sched_strategy strategy) +{ + enum lsquic_write_sched_strategy current_strategy; + + if (conn->ifc_write_dispatch == do_write_fixed) + current_strategy = LSQWSS_FIXED; + else + current_strategy = LSQWSS_DRR; + + if (strategy == current_strategy) + return; + else if (strategy == LSQWSS_FIXED) + conn->ifc_write_dispatch = do_write_fixed; + else if (strategy == LSQWSS_DRR) + { + memset(&conn->ifc_write_sched.drr, 0, sizeof(conn->ifc_write_sched.drr)); + set_drr_weights_from_share(conn, conn->ifc_write_datagram_share); + conn->ifc_write_dispatch = do_write_drr; + } + else + LSQ_WARN("invalid write scheduler strategy %d", strategy); +} + + +static enum tick_st +ietf_full_conn_ci_tick (struct lsquic_conn *lconn, lsquic_time_t now) +{ + struct ietf_full_conn *conn = (struct ietf_full_conn *) lconn; + int have_delayed_packets, s; + enum tick_st tick = 0; + unsigned n; + +#define CLOSE_IF_NECESSARY() do { \ + if (conn->ifc_flags & IFC_IMMEDIATE_CLOSE_FLAGS) \ + { \ + tick |= immediate_close(conn); \ + goto close_end; \ + } \ +} while (0) + +#define RETURN_IF_OUT_OF_PACKETS() do { \ + if (!lsquic_send_ctl_can_send(&conn->ifc_send_ctl)) \ + { \ + if (0 == lsquic_send_ctl_n_scheduled(&conn->ifc_send_ctl)) \ + { \ + LSQ_DEBUG("used up packet allowance, quiet now (line %d)", \ + __LINE__); \ + tick |= TICK_QUIET; \ + } \ + else \ + { \ + LSQ_DEBUG("used up packet allowance, sending now (line %d)",\ + __LINE__); \ + tick |= TICK_SEND; \ + } \ + goto end; \ + } \ +} while (0) + + CONN_STATS(n_ticks, 1); + + CLOSE_IF_NECESSARY(); + + if (conn->ifc_flags & IFC_HAVE_SAVED_ACK) + { + (void) /* If there is an error, we'll fail shortly */ + process_ack(conn, &conn->ifc_ack, conn->ifc_saved_ack_received, now); conn->ifc_flags &= ~IFC_HAVE_SAVED_ACK; } @@ -8341,30 +9539,9 @@ ietf_full_conn_ci_tick (struct lsquic_conn *lconn, lsquic_time_t now) maybe_conn_flush_special_streams(conn); - s = lsquic_send_ctl_schedule_buffered(&conn->ifc_send_ctl, BPT_HIGHEST_PRIO); - conn->ifc_flags |= (s < 0) << IFC_BIT_ERROR; - if (!write_is_possible(conn)) + if (conn->ifc_write_dispatch(conn)) goto end_write; - while ((conn->ifc_mflags & MF_WANT_DATAGRAM_WRITE) && write_datagram(conn)) - if (!write_is_possible(conn)) - goto end_write; - - if (!TAILQ_EMPTY(&conn->ifc_pub.write_streams)) - { - process_streams_write_events(conn, 1); - if (!write_is_possible(conn)) - goto end_write; - } - - s = lsquic_send_ctl_schedule_buffered(&conn->ifc_send_ctl, BPT_OTHER_PRIO); - conn->ifc_flags |= (s < 0) << IFC_BIT_ERROR; - if (!write_is_possible(conn)) - goto end_write; - - if (!TAILQ_EMPTY(&conn->ifc_pub.write_streams)) - process_streams_write_events(conn, 0); - lsquic_send_ctl_maybe_app_limited(&conn->ifc_send_ctl, CUR_NPATH(conn)); end_write: @@ -8586,6 +9763,36 @@ ietf_full_conn_ci_make_stream (struct lsquic_conn *lconn) } } +static struct lsquic_stream * +ietf_full_conn_ci_make_bidi_stream_with_if (struct lsquic_conn *lconn, + const struct lsquic_stream_if *stream_if, void *stream_if_ctx) +{ + struct ietf_full_conn *const conn = (struct ietf_full_conn *) lconn; + + if (!handshake_done_or_doing_sess_resume(conn)) + { + errno = EAGAIN; + return NULL; + } + + return create_bidi_stream_out_with_if(conn, stream_if, stream_if_ctx); +} + +static struct lsquic_stream * +ietf_full_conn_ci_make_uni_stream_with_if (struct lsquic_conn *lconn, + const struct lsquic_stream_if *stream_if, void *stream_if_ctx) +{ + struct ietf_full_conn *const conn = (struct ietf_full_conn *) lconn; + + if (!handshake_done_or_doing_sess_resume(conn)) + { + errno = EAGAIN; + return NULL; + } + + return create_uni_stream_out_with_if(conn, stream_if, stream_if_ctx); +} + static void ietf_full_conn_ci_internal_error (struct lsquic_conn *lconn, @@ -8801,6 +10008,9 @@ ietf_full_conn_ci_set_param (lsquic_conn_t *lconn, enum lsquic_conn_param param, { struct ietf_full_conn *conn = (struct ietf_full_conn *) lconn; uint64_t rate; + enum lsquic_write_sched_strategy strategy; + unsigned datagram_prio; + float datagram_share; int enable_bw_sampler; switch (param) @@ -8820,6 +10030,33 @@ ietf_full_conn_ci_set_param (lsquic_conn_t *lconn, enum lsquic_conn_param param, LSQ_INFO("bw sampler %s", enable_bw_sampler ? "enabled" : "disabled"); return 0; + case LSQCP_WRITE_SCHED_STRATEGY: + if (value_len != sizeof(strategy)) + return -1; + memcpy(&strategy, value, sizeof(strategy)); + if (strategy > LSQWSS_DRR) + return -1; + set_write_sched_strategy(conn, strategy); + return 0; + case LSQCP_WRITE_DATAGRAM_PRIO: + if (value_len != sizeof(datagram_prio)) + return -1; + if (conn->ifc_write_dispatch != do_write_fixed) + return -1; + memcpy(&datagram_prio, value, sizeof(datagram_prio)); + if (datagram_prio >= ALL_FW_COUNT) + return -1; + set_write_datagram_priority(conn, datagram_prio); + return 0; + case LSQCP_WRITE_DATAGRAM_SHARE: + if (value_len != sizeof(datagram_share)) + return -1; + memcpy(&datagram_share, value, sizeof(datagram_share)); + if (datagram_share != datagram_share + || datagram_share < 0.f || datagram_share > 1.f) + return -1; + set_write_datagram_share(conn, datagram_share); + return 0; default: return -1; } @@ -8831,17 +10068,48 @@ ietf_full_conn_ci_get_param (lsquic_conn_t *lconn, enum lsquic_conn_param param, void *value, size_t *value_len) { struct ietf_full_conn *conn = (struct ietf_full_conn *) lconn; - uint64_t rate; + uint64_t u64; + enum lsquic_write_sched_strategy strategy; + unsigned datagram_prio; + float datagram_share; int enable_bw_sampler; switch (param) { case LSQCP_MAX_PACING_RATE: - if (*value_len < sizeof(uint64_t)) + if (*value_len < sizeof(u64)) + return -1; + u64 = conn->ifc_send_ctl.sc_max_pacing_rate; + memcpy(value, &u64, sizeof(u64)); + *value_len = sizeof(u64); + return 0; + case LSQCP_WT_PEER_SETTINGS_RECEIVED: + if (*value_len < sizeof(u64)) + return -1; + u64 = !!(conn->ifc_pub.cp_flags & CP_H3_PEER_SETTINGS); + memcpy(value, &u64, sizeof(u64)); + *value_len = sizeof(u64); + return 0; + case LSQCP_WT_PEER_SUPPORTS: + if (*value_len < sizeof(u64)) + return -1; + u64 = !!(conn->ifc_pub.cp_flags & CP_WEBTRANSPORT); + memcpy(value, &u64, sizeof(u64)); + *value_len = sizeof(u64); + return 0; + case LSQCP_WT_PEER_DRAFT: + if (*value_len < sizeof(u64)) return -1; - rate = conn->ifc_send_ctl.sc_max_pacing_rate; - memcpy(value, &rate, sizeof(rate)); - *value_len = sizeof(rate); + u64 = conn->ifc_pub.cp_wt_peer_draft; + memcpy(value, &u64, sizeof(u64)); + *value_len = sizeof(u64); + return 0; + case LSQCP_WT_PEER_CONNECT_PROTOCOL: + if (*value_len < sizeof(u64)) + return -1; + u64 = !!(conn->ifc_pub.cp_flags & CP_CONNECT_PROTOCOL); + memcpy(value, &u64, sizeof(u64)); + *value_len = sizeof(u64); return 0; case LSQCP_ENABLE_BW_SAMPLER: if (*value_len < sizeof(int)) @@ -8851,6 +10119,34 @@ ietf_full_conn_ci_get_param (lsquic_conn_t *lconn, enum lsquic_conn_param param, memcpy(value, &enable_bw_sampler, sizeof(enable_bw_sampler)); *value_len = sizeof(enable_bw_sampler); return 0; + case LSQCP_WRITE_SCHED_STRATEGY: + if (*value_len < sizeof(strategy)) + return -1; + if (conn->ifc_write_dispatch == do_write_drr) + strategy = LSQWSS_DRR; + else + strategy = LSQWSS_FIXED; + memcpy(value, &strategy, sizeof(strategy)); + *value_len = sizeof(strategy); + return 0; + case LSQCP_WRITE_DATAGRAM_PRIO: + if (*value_len < sizeof(datagram_prio)) + return -1; + if (conn->ifc_write_dispatch != do_write_fixed) + return -1; + datagram_prio = get_write_datagram_priority(conn); + if (datagram_prio >= ALL_FW_COUNT) + return -1; + memcpy(value, &datagram_prio, sizeof(datagram_prio)); + *value_len = sizeof(datagram_prio); + return 0; + case LSQCP_WRITE_DATAGRAM_SHARE: + if (*value_len < sizeof(datagram_share)) + return -1; + datagram_share = conn->ifc_write_datagram_share; + memcpy(value, &datagram_share, sizeof(datagram_share)); + *value_len = sizeof(datagram_share); + return 0; default: return -1; } @@ -8923,6 +10219,8 @@ ietf_full_conn_ci_log_stats (struct lsquic_conn *lconn) .ci_early_data_failed = ietf_full_conn_ci_early_data_failed, \ .ci_get_engine = ietf_full_conn_ci_get_engine, \ .ci_get_min_datagram_size= ietf_full_conn_ci_get_min_datagram_size, \ + .ci_get_max_datagram_size= ietf_full_conn_ci_get_max_datagram_size, \ + .ci_get_stream_by_id = NULL, \ .ci_get_path = ietf_full_conn_ci_get_path, \ .ci_going_away = ietf_full_conn_ci_going_away, \ .ci_hsk_done = ietf_full_conn_ci_hsk_done, \ @@ -8930,6 +10228,8 @@ ietf_full_conn_ci_log_stats (struct lsquic_conn *lconn) .ci_is_push_enabled = ietf_full_conn_ci_is_push_enabled, \ .ci_is_tickable = ietf_full_conn_ci_is_tickable, \ .ci_make_stream = ietf_full_conn_ci_make_stream, \ + .ci_make_bidi_stream_with_if = ietf_full_conn_ci_make_bidi_stream_with_if, \ + .ci_make_uni_stream_with_if = ietf_full_conn_ci_make_uni_stream_with_if, \ .ci_mtu_probe_acked = ietf_full_conn_ci_mtu_probe_acked, \ .ci_n_avail_streams = ietf_full_conn_ci_n_avail_streams, \ .ci_n_pending_streams = ietf_full_conn_ci_n_pending_streams, \ @@ -8944,6 +10244,7 @@ ietf_full_conn_ci_log_stats (struct lsquic_conn *lconn) .ci_stateless_reset = ietf_full_conn_ci_stateless_reset, \ .ci_tick = ietf_full_conn_ci_tick, \ .ci_tls_alert = ietf_full_conn_ci_tls_alert, \ + .ci_http_dg_streams_updated = ietf_full_conn_ci_http_dg_streams_updated, \ .ci_want_datagram_write = ietf_full_conn_ci_want_datagram_write, \ .ci_user_stream_progress = ietf_full_conn_ci_user_stream_progress, \ .ci_write_ack = ietf_full_conn_ci_write_ack @@ -9034,6 +10335,135 @@ on_max_push_id (void *ctx, uint64_t push_id) } +static int +local_webtransport_enabled (const struct ietf_full_conn *conn) +{ + return conn->ifc_settings->es_webtransport != 0; +} + + +static void +update_peer_wt_support (struct ietf_full_conn *conn) +{ + int had_support; + int supports; + int peer_settings_received; + int peer_connect_protocol; + int peer_wt_enabled; + int peer_quic_datagrams; + int peer_reset_stream_at; + + peer_settings_received = !!(conn->ifc_flags & IFC_HAVE_PEER_SET); + if (peer_settings_received) + conn->ifc_pub.cp_flags |= CP_H3_PEER_SETTINGS; + else + conn->ifc_pub.cp_flags &= ~CP_H3_PEER_SETTINGS; + + peer_connect_protocol = + conn->ifc_peer_hq_settings.enable_connect_protocol_seen + && conn->ifc_peer_hq_settings.enable_connect_protocol; + if (peer_connect_protocol) + conn->ifc_pub.cp_flags |= CP_CONNECT_PROTOCOL; + else + conn->ifc_pub.cp_flags &= ~CP_CONNECT_PROTOCOL; + + conn->ifc_pub.cp_wt_peer_draft = conn->ifc_peer_hq_settings.wt_draft; + peer_wt_enabled = + (conn->ifc_peer_hq_settings.wt_max_sessions_seen + && conn->ifc_peer_hq_settings.wt_max_sessions > 0) + || (conn->ifc_peer_hq_settings.wt_enabled_seen + && conn->ifc_peer_hq_settings.wt_enabled); + peer_quic_datagrams = !!(conn->ifc_flags & IFC_DATAGRAMS); + peer_reset_stream_at = !!(conn->ifc_mflags & MF_PEER_RESET_STREAM_AT); + had_support = !!(conn->ifc_pub.cp_flags & CP_WEBTRANSPORT); + + /* + * Strict draft-15 support requires peer SETTINGS, negotiated HTTP + * Datagrams and QUIC DATAGRAM, plus peer WT advertisement. Client-side + * CONNECT also requires SETTINGS_ENABLE_CONNECT_PROTOCOL=1. + * + * Compatibility policy on this branch: + * - still enable WT for draft-14 peers that advertise WT via + * WT_MAX_SESSIONS and negotiate the transport pieces above; + * - still enable WT when peers omit reset_stream_at TP or WT initial + * settings, but warn and treat this as compatibility mode. + * + * This keeps draft-14 / partial draft-15 interop working while WT flow + * control remains deferred and the implementation supports one WT + * session per connection. + */ + supports = peer_settings_received + && local_webtransport_enabled(conn) + && peer_wt_enabled + && (conn->ifc_pub.cp_flags & CP_HTTP_DATAGRAMS) + && peer_quic_datagrams; + if (!(conn->ifc_flags & IFC_SERVER)) + supports = supports && peer_connect_protocol; + + if (supports) + conn->ifc_pub.cp_flags |= CP_WEBTRANSPORT; + else + conn->ifc_pub.cp_flags &= ~CP_WEBTRANSPORT; + + if (supports && !had_support) + { + if (!peer_reset_stream_at) + LSQ_WARN("peer missing reset_stream_at TP: enabling WT in " + "compatibility mode"); + if (!conn->ifc_peer_hq_settings.wt_initial_max_data_seen + || !conn->ifc_peer_hq_settings.wt_initial_max_streams_uni_seen + || !conn->ifc_peer_hq_settings.wt_initial_max_streams_bidi_seen) + LSQ_WARN("peer missing one or more WT initial settings: " + "enabling WT in compatibility mode"); + } + + LSQ_DEBUG("peer WT: settings=%d, local=%d, wt_max_seen=%d, wt_max=%"PRIu64 + ", wt_enabled_seen=%d, wt_enabled=%d, wt_max_data_seen=%d, wt_max_uni_seen=%d, " + "wt_max_bidi_seen=%d, draft=%u, connect_seen=%d, connect=%d, " + "h3_dgram=%d, quic_dgram=%d, reset_at=%d " + "=> support=%d", + peer_settings_received, + local_webtransport_enabled(conn), + conn->ifc_peer_hq_settings.wt_max_sessions_seen, + conn->ifc_peer_hq_settings.wt_max_sessions, + conn->ifc_peer_hq_settings.wt_enabled_seen, + conn->ifc_peer_hq_settings.wt_enabled, + conn->ifc_peer_hq_settings.wt_initial_max_data_seen, + conn->ifc_peer_hq_settings.wt_initial_max_streams_uni_seen, + conn->ifc_peer_hq_settings.wt_initial_max_streams_bidi_seen, + conn->ifc_peer_hq_settings.wt_draft, + conn->ifc_peer_hq_settings.enable_connect_protocol_seen, + peer_connect_protocol, + !!(conn->ifc_pub.cp_flags & CP_HTTP_DATAGRAMS), + peer_quic_datagrams, + peer_reset_stream_at, + !!(conn->ifc_pub.cp_flags & CP_WEBTRANSPORT)); + + if (conn->ifc_pub.cp_on_http_caps_change) + conn->ifc_pub.cp_on_http_caps_change(&conn->ifc_pub); +} + + +static void +notify_http_caps (struct ietf_full_conn *conn) +{ + struct lsquic_http_caps caps; + + if (!conn->ifc_enpub->enp_stream_if->on_http_caps) + return; + + caps.lhc_flags = 0; + if (conn->ifc_pub.cp_flags & CP_HTTP_DATAGRAMS) + caps.lhc_flags |= LSQUIC_HTTP_CAP_DATAGRAMS; + if (conn->ifc_pub.cp_flags & CP_CONNECT_PROTOCOL) + caps.lhc_flags |= LSQUIC_HTTP_CAP_CONNECT_PROTOCOL; + if (conn->ifc_pub.cp_flags & CP_WEBTRANSPORT) + caps.lhc_flags |= LSQUIC_HTTP_CAP_WEBTRANSPORT; + + conn->ifc_enpub->enp_stream_if->on_http_caps(&conn->ifc_conn, &caps); +} + + static void on_settings_frame (void *ctx) { @@ -9079,7 +10509,9 @@ on_settings_frame (void *ctx) queue_streams_blocked_frame(conn, SD_UNI); LSQ_DEBUG("cannot create QPACK encoder stream due to unidir limit"); } + update_peer_wt_support(conn); maybe_create_delayed_streams(conn); + notify_http_caps(conn); } @@ -9087,6 +10519,7 @@ static void on_setting (void *ctx, uint64_t setting_id, uint64_t value) { struct ietf_full_conn *const conn = ctx; + int enable; switch (setting_id) { @@ -9102,6 +10535,79 @@ on_setting (void *ctx, uint64_t setting_id, uint64_t value) LSQ_DEBUG("Peer's SETTINGS_MAX_HEADER_LIST_SIZE=%"PRIu64"; " "we ignore it", value); break; + case HQSID_H3_DATAGRAM_ENABLED: + if (value > 1) + { + ABORT_QUIETLY(1, HEC_SETTINGS_ERROR, + "invalid SETTINGS_H3_DATAGRAM value %"PRIu64, value); + return; + } + enable = value == 1; + if (enable) + conn->ifc_mflags |= MF_HTTP_DATAGRAMS; + else + conn->ifc_mflags &= ~MF_HTTP_DATAGRAMS; + if (conn->ifc_settings->es_http_datagrams && enable) + conn->ifc_pub.cp_flags |= CP_HTTP_DATAGRAMS; + else + conn->ifc_pub.cp_flags &= ~CP_HTTP_DATAGRAMS; + if (conn->ifc_write_dispatch == do_write_fixed && enable) + set_write_datagram_priority(conn, + conn->ifc_settings->es_write_datagram_prio); + update_datagram_want(conn); + update_peer_wt_support(conn); + LSQ_DEBUG("Peer's SETTINGS_H3_DATAGRAM=%"PRIu64, value); + break; + case HQSID_ENABLE_CONNECT_PROTOCOL: + if (value > 1) + { + ABORT_QUIETLY(1, HEC_SETTINGS_ERROR, + "invalid SETTINGS_ENABLE_CONNECT_PROTOCOL value %"PRIu64, + value); + return; + } + conn->ifc_peer_hq_settings.enable_connect_protocol_seen = 1; + conn->ifc_peer_hq_settings.enable_connect_protocol = value == 1; + update_peer_wt_support(conn); + LSQ_DEBUG("Peer's SETTINGS_ENABLE_CONNECT_PROTOCOL=%"PRIu64, value); + break; + case HQSID_WT_MAX_SESSIONS: + conn->ifc_peer_hq_settings.wt_max_sessions_seen = 1; + conn->ifc_peer_hq_settings.wt_max_sessions = value; + if (conn->ifc_peer_hq_settings.wt_draft < 14) + conn->ifc_peer_hq_settings.wt_draft = 14; + if (!local_webtransport_enabled(conn) && value > 0) + LSQ_DEBUG("peer enabled WT while local endpoint has WT disabled"); + update_peer_wt_support(conn); + LSQ_DEBUG("Peer's SETTINGS_WT_MAX_SESSIONS=%"PRIu64, value); + break; + case HQSID_WT_INITIAL_MAX_DATA: + conn->ifc_peer_hq_settings.wt_initial_max_data_seen = 1; + conn->ifc_peer_hq_settings.wt_initial_max_data = value; + update_peer_wt_support(conn); + LSQ_DEBUG("Peer's SETTINGS_WT_INITIAL_MAX_DATA=%"PRIu64, value); + break; + case HQSID_WT_INITIAL_MAX_STREAMS_UNI: + conn->ifc_peer_hq_settings.wt_initial_max_streams_uni_seen = 1; + conn->ifc_peer_hq_settings.wt_initial_max_streams_uni = value; + update_peer_wt_support(conn); + LSQ_DEBUG("Peer's SETTINGS_WT_INITIAL_MAX_STREAMS_UNI=%"PRIu64, value); + break; + case HQSID_WT_INITIAL_MAX_STREAMS_BIDI: + conn->ifc_peer_hq_settings.wt_initial_max_streams_bidi_seen = 1; + conn->ifc_peer_hq_settings.wt_initial_max_streams_bidi = value; + update_peer_wt_support(conn); + LSQ_DEBUG("Peer's SETTINGS_WT_INITIAL_MAX_STREAMS_BIDI=%"PRIu64, value); + break; + case HQSID_WT_ENABLED: + conn->ifc_peer_hq_settings.wt_enabled_seen = 1; + conn->ifc_peer_hq_settings.wt_enabled = value > 0; + conn->ifc_peer_hq_settings.wt_draft = 15; + if (!local_webtransport_enabled(conn) && value > 0) + LSQ_DEBUG("peer enabled WT while local endpoint has WT disabled"); + update_peer_wt_support(conn); + LSQ_DEBUG("Peer's SETTINGS_WT_ENABLED=%"PRIu64, value); + break; default: LSQ_DEBUG("received unknown SETTING 0x%"PRIX64"=0x%"PRIX64 "; ignore it", setting_id, value); @@ -9552,6 +11058,54 @@ static const struct lsquic_stream_if hcsi_if = }; +#if LSQUIC_TEST +static unsigned s_ietf_test_failed_wt_uni_switch_close; +static int s_ietf_test_stub_wt_uni_switch_close; +#endif + +static void +close_failed_wt_uni_switch (struct lsquic_stream *stream) +{ +#if LSQUIC_TEST + if (s_ietf_test_stub_wt_uni_switch_close) + { + ++s_ietf_test_failed_wt_uni_switch_close; + return; + } +#endif + lsquic_stream_close(stream); +} + + +static int +switch_wt_uni_stream_if (struct ietf_full_conn *conn, + struct lsquic_stream *stream) +{ + const struct lsquic_stream_if *old_if; + lsquic_stream_ctx_t *old_ctx; + void *old_onnew_arg; + + old_if = stream->stream_if; + old_ctx = stream->st_ctx; + old_onnew_arg = stream->sm_onnew_arg; + lsquic_stream_set_stream_if(stream, lsquic_wt_uni_stream_if(), conn); + if (!stream->st_ctx) + { + stream->stream_if = old_if; + stream->st_ctx = old_ctx; + stream->sm_onnew_arg = old_onnew_arg; + if (0 == errno) + errno = ENOMEM; + LSQ_WARN("cannot initialize incoming WT uni stream %"PRIu64, + stream->id); + close_failed_wt_uni_switch(stream); + return -1; + } + + return 0; +} + + static void abort_push_stream (struct ietf_full_conn *conn) { @@ -9624,6 +11178,10 @@ apply_uni_stream_class (struct ietf_full_conn *conn, lsquic_stream_close(stream); } break; + case HQUST_WEBTRANSPORT: + LSQ_DEBUG("Incoming WebTransport stream ID: %"PRIu64, stream->id); + (void) switch_wt_uni_stream_if(conn, stream); + break; case HQUST_PUSH: abort_push_stream(conn); lsquic_stream_close(stream); @@ -9748,6 +11306,110 @@ static const struct lsquic_stream_if unicla_if = static const struct lsquic_stream_if *unicla_if_ptr = &unicla_if; +#if LSQUIC_TEST +int +lsquic_ietf_test_wt_support (unsigned is_server, + unsigned peer_settings_received, + unsigned local_webtransport, + unsigned http_datagrams, + unsigned quic_datagrams, + unsigned connect_protocol, + unsigned wt_max_sessions_seen, + uint64_t wt_max_sessions, + unsigned wt_enabled_seen, + unsigned wt_enabled, + unsigned wt_initial_max_data_seen, + unsigned wt_initial_max_streams_uni_seen, + unsigned wt_initial_max_streams_bidi_seen, + unsigned peer_reset_stream_at, + unsigned draft, + unsigned *supports, + unsigned *peer_wt_draft) +{ + struct ietf_full_conn conn; + struct lsquic_engine_settings settings; + + memset(&conn, 0, sizeof(conn)); + memset(&settings, 0, sizeof(settings)); + + settings.es_webtransport = local_webtransport != 0; + conn.ifc_settings = &settings; + + if (is_server) + conn.ifc_flags |= IFC_SERVER; + if (peer_settings_received) + conn.ifc_flags |= IFC_HAVE_PEER_SET; + if (quic_datagrams) + conn.ifc_flags |= IFC_DATAGRAMS; + if (http_datagrams) + conn.ifc_pub.cp_flags |= CP_HTTP_DATAGRAMS; + if (peer_reset_stream_at) + conn.ifc_mflags |= MF_PEER_RESET_STREAM_AT; + + conn.ifc_peer_hq_settings.enable_connect_protocol_seen = + connect_protocol != 0; + conn.ifc_peer_hq_settings.enable_connect_protocol = + connect_protocol != 0; + conn.ifc_peer_hq_settings.wt_max_sessions_seen = wt_max_sessions_seen != 0; + conn.ifc_peer_hq_settings.wt_max_sessions = wt_max_sessions; + conn.ifc_peer_hq_settings.wt_enabled_seen = wt_enabled_seen != 0; + conn.ifc_peer_hq_settings.wt_enabled = wt_enabled != 0; + conn.ifc_peer_hq_settings.wt_initial_max_data_seen = + wt_initial_max_data_seen != 0; + conn.ifc_peer_hq_settings.wt_initial_max_streams_uni_seen = + wt_initial_max_streams_uni_seen != 0; + conn.ifc_peer_hq_settings.wt_initial_max_streams_bidi_seen = + wt_initial_max_streams_bidi_seen != 0; + conn.ifc_peer_hq_settings.wt_draft = draft; + + update_peer_wt_support(&conn); + + if (supports) + *supports = !!(conn.ifc_pub.cp_flags & CP_WEBTRANSPORT); + if (peer_wt_draft) + *peer_wt_draft = conn.ifc_pub.cp_wt_peer_draft; + + return 0; +} + + +int +lsquic_ietf_test_wt_uni_switch_failure (int *restored_if, int *restored_ctx, + int *close_attempted) +{ + struct ietf_full_conn conn; + struct lsquic_stream stream; + int rc; + + memset(&conn, 0, sizeof(conn)); + memset(&stream, 0, sizeof(stream)); + conn.ifc_pub.lconn = &conn.ifc_conn; + stream.id = 2; + stream.conn_pub = &conn.ifc_pub; + stream.stream_if = unicla_if_ptr; + stream.sm_onnew_arg = &conn; + stream.st_ctx = (lsquic_stream_ctx_t *) &conn; + stream.stream_flags = STREAM_ONNEW_DONE; + s_ietf_test_failed_wt_uni_switch_close = 0; + s_ietf_test_stub_wt_uni_switch_close = 1; + lsquic_wt_test_set_fail_stream_ctx_alloc(1); + errno = 0; + rc = switch_wt_uni_stream_if(&conn, &stream); + lsquic_wt_test_set_fail_stream_ctx_alloc(0); + s_ietf_test_stub_wt_uni_switch_close = 0; + + if (restored_if) + *restored_if = stream.stream_if == unicla_if_ptr + && stream.sm_onnew_arg == &conn; + if (restored_ctx) + *restored_ctx = stream.st_ctx == (lsquic_stream_ctx_t *) &conn; + if (close_attempted) + *close_attempted = s_ietf_test_failed_wt_uni_switch_close == 1; + + return rc != 0 && errno == ENOMEM ? 0 : -1; +} +#endif + void lsquic_ietf_full_conn_test_push_disabled (unsigned results[5]) { diff --git a/src/liblsquic/lsquic_hcso_writer.c b/src/liblsquic/lsquic_hcso_writer.c index 099b2525d..6f93805ec 100644 --- a/src/liblsquic/lsquic_hcso_writer.c +++ b/src/liblsquic/lsquic_hcso_writer.c @@ -16,11 +16,17 @@ #include "lsquic_varint.h" #include "lsquic_hq.h" #include "lsquic_hash.h" +#include "lsquic_malo.h" +#include "lsquic_conn_flow.h" +#include "lsquic_rtt.h" #include "lsquic_stream.h" #include "lsquic_frab_list.h" #include "lsquic_byteswap.h" +#include "lsquic_mm.h" #include "lsquic_hcso_writer.h" #include "lsquic_conn.h" +#include "lsquic_conn_public.h" +#include "lsquic_engine_public.h" #define LSQUIC_LOGGER_MODULE LSQLM_HCSO_WRITER #define LSQUIC_LOG_CONN_ID \ @@ -121,39 +127,34 @@ int lsquic_hcso_write_settings (struct hcso_writer *writer, unsigned max_header_list_size, unsigned dyn_table_size, unsigned max_risked_streams, - int is_server -#if LSQUIC_WEBTRANSPORT_SERVER_SUPPORT - , int webtransport_server, - unsigned max_webtransport_server_streams -#endif - ) + int is_server, + int wt_enabled) { + const struct lsquic_engine_public *enpub; unsigned char *p; unsigned bits; int was_empty; -#ifdef NDEBUG -# define frame_size_len 1 -#else - /* Need to use two bytes for frame length, as randomization may require - * more than 63 bytes. + enum { + HCSO_SETTINGS_FRAME_SIZE_LEN = 2, + HCSO_MAX_SETTINGS = 10, + HCSO_MAX_VARINT_LEN = VINT_MAX_SIZE, + }; + /* Use fixed worst-case sizing: WT setting IDs are larger than 1-byte + * varints, and fuzzing can randomize setting IDs to full varint width. */ -# define frame_size_len 2 -#endif - unsigned char buf[1 /* Frame type */ + /* Frame size */ frame_size_len - /* There are maximum seven settings that need to be written out and - * each value can be encoded in maximum 8 bytes: - */ - + 7 * ( -#ifdef NDEBUG - 1 /* Each setting needs 1-byte varint number, */ -#else - 8 /* but it can be up to 8 bytes when randomized */ -#endif - + 8) ]; + unsigned char buf[1 /* frame type */ + + HCSO_SETTINGS_FRAME_SIZE_LEN + + HCSO_MAX_SETTINGS * (HCSO_MAX_VARINT_LEN + HCSO_MAX_VARINT_LEN)]; + + assert(writer); + assert(writer->how_stream); + assert(writer->how_stream->conn_pub); + assert(writer->how_stream->conn_pub->enpub); + enpub = writer->how_stream->conn_pub->enpub; p = buf; *p++ = HQFT_SETTINGS; - p += frame_size_len; + p += HCSO_SETTINGS_FRAME_SIZE_LEN; if (max_header_list_size != HQ_DF_MAX_HEADER_LIST_SIZE) { @@ -188,55 +189,81 @@ lsquic_hcso_write_settings (struct hcso_writer *writer, p += 1 << bits; } -#if LSQUIC_WEBTRANSPORT_SERVER_SUPPORT - if (is_server && webtransport_server && max_webtransport_server_streams) + if (wt_enabled) { - /* Write out SETTINGS_ENABLE_WEBTRANSPORT */ -#define SETTINGS_ENABLE_WEBTRANSPORT (0x2b603742) -#define SETTINGS_ENABLE_WEBTRANSPORT_VALUE (1) - bits = hcso_setting_type2bits(writer, SETTINGS_ENABLE_WEBTRANSPORT); - vint_write(p, SETTINGS_ENABLE_WEBTRANSPORT, bits, 1 << bits); + uint64_t wt_max_sessions; + + wt_max_sessions = enpub->enp_settings.es_max_webtransport_sessions; + if (wt_max_sessions == 0) + wt_max_sessions = 1; + + /* We currently map WT initial settings to connection-level engine + * limits. This mapping is valid as long as we support exactly one + * WebTransport session per connection. + */ + /* Write out SETTINGS_WT_ENABLED */ + bits = hcso_setting_type2bits(writer, HQSID_WT_ENABLED); + vint_write(p, HQSID_WT_ENABLED, bits, 1 << bits); p += 1 << bits; - bits = vint_val2bits(SETTINGS_ENABLE_WEBTRANSPORT_VALUE); - vint_write(p, SETTINGS_ENABLE_WEBTRANSPORT_VALUE, bits, 1 << bits); + bits = vint_val2bits(wt_max_sessions); + vint_write(p, wt_max_sessions, bits, 1 << bits); p += 1 << bits; - /* Write out SETTINGS_ENABLE_WEBTRANSPORT */ -#define WEBTRANSPORT_MAX_SESSIONS (0x2b603743) - bits = hcso_setting_type2bits(writer, WEBTRANSPORT_MAX_SESSIONS); - vint_write(p, WEBTRANSPORT_MAX_SESSIONS, bits, 1 << bits); + /* Write out draft-14 compatibility setting WT_MAX_SESSIONS. */ + bits = hcso_setting_type2bits(writer, HQSID_WT_MAX_SESSIONS); + vint_write(p, HQSID_WT_MAX_SESSIONS, bits, 1 << bits); p += 1 << bits; - bits = vint_val2bits(max_webtransport_server_streams); - vint_write(p, max_webtransport_server_streams, bits, 1 << bits); + bits = vint_val2bits(wt_max_sessions); + vint_write(p, wt_max_sessions, bits, 1 << bits); p += 1 << bits; - /* Write out H3_DATAGRAM_ENABLED */ -#define H3_DATAGRAM_ENABLED (0x33) -#define H3_DATAGRAM_ENABLED_VALUE (1) - bits = hcso_setting_type2bits(writer, H3_DATAGRAM_ENABLED); - vint_write(p, H3_DATAGRAM_ENABLED, bits, 1 << bits); + bits = hcso_setting_type2bits(writer, HQSID_WT_INITIAL_MAX_DATA); + vint_write(p, HQSID_WT_INITIAL_MAX_DATA, bits, 1 << bits); + p += 1 << bits; + bits = vint_val2bits(enpub->enp_settings.es_init_max_data); + vint_write(p, enpub->enp_settings.es_init_max_data, bits, 1 << bits); + p += 1 << bits; + + bits = hcso_setting_type2bits(writer, HQSID_WT_INITIAL_MAX_STREAMS_UNI); + vint_write(p, HQSID_WT_INITIAL_MAX_STREAMS_UNI, bits, 1 << bits); p += 1 << bits; - bits = vint_val2bits(H3_DATAGRAM_ENABLED_VALUE); - vint_write(p, H3_DATAGRAM_ENABLED_VALUE, bits, 1 << bits); + bits = vint_val2bits(enpub->enp_settings.es_init_max_streams_uni); + vint_write(p, enpub->enp_settings.es_init_max_streams_uni, bits, 1 << bits); p += 1 << bits; - /* Write out SETTINGS_ENABLE_CONNECT_PROTOCOL */ -#define SETTINGS_ENABLE_CONNECT_PROTOCOL (0x8) -#define SETTINGS_ENABLE_CONNECT_PROTOCOL_VALUE (1) - bits = hcso_setting_type2bits(writer, SETTINGS_ENABLE_CONNECT_PROTOCOL); - vint_write(p, SETTINGS_ENABLE_CONNECT_PROTOCOL, bits, 1 << bits); + bits = hcso_setting_type2bits(writer, HQSID_WT_INITIAL_MAX_STREAMS_BIDI); + vint_write(p, HQSID_WT_INITIAL_MAX_STREAMS_BIDI, bits, 1 << bits); p += 1 << bits; - bits = vint_val2bits(SETTINGS_ENABLE_CONNECT_PROTOCOL_VALUE); - vint_write(p, SETTINGS_ENABLE_CONNECT_PROTOCOL_VALUE, bits, 1 << bits); + bits = vint_val2bits(enpub->enp_settings.es_init_max_streams_bidi); + vint_write(p, enpub->enp_settings.es_init_max_streams_bidi, bits, 1 << bits); p += 1 << bits; + + if (is_server) + { + /* Write out SETTINGS_ENABLE_CONNECT_PROTOCOL */ + bits = hcso_setting_type2bits(writer, + HQSID_ENABLE_CONNECT_PROTOCOL); + vint_write(p, HQSID_ENABLE_CONNECT_PROTOCOL, bits, 1 << bits); + p += 1 << bits; + bits = vint_val2bits(1); + vint_write(p, 1, bits, 1 << bits); + p += 1 << bits; + } } -#endif -#ifdef NDEBUG - buf[1] = p - buf - 2; -#else - vint_write(buf + 1, p - buf - 3, 1, 2); -#endif + if (enpub->enp_settings.es_http_datagrams) + { + /* Write out H3_DATAGRAM_ENABLED */ + bits = hcso_setting_type2bits(writer, HQSID_H3_DATAGRAM_ENABLED); + vint_write(p, HQSID_H3_DATAGRAM_ENABLED, bits, 1 << bits); + p += 1 << bits; + bits = vint_val2bits(1); + vint_write(p, 1, bits, 1 << bits); + p += 1 << bits; + } + + vint_write(buf + 1, p - buf - 1 - HCSO_SETTINGS_FRAME_SIZE_LEN, 1, + HCSO_SETTINGS_FRAME_SIZE_LEN); was_empty = lsquic_frab_list_empty(&writer->how_fral); diff --git a/src/liblsquic/lsquic_hcso_writer.h b/src/liblsquic/lsquic_hcso_writer.h index d29195729..539d59313 100644 --- a/src/liblsquic/lsquic_hcso_writer.h +++ b/src/liblsquic/lsquic_hcso_writer.h @@ -23,11 +23,8 @@ struct hcso_writer int lsquic_hcso_write_settings (struct hcso_writer *, - unsigned, unsigned, unsigned, int -#if LSQUIC_WEBTRANSPORT_SERVER_SUPPORT - ,int, unsigned -#endif - ); + unsigned, unsigned, unsigned, int, + int); int lsquic_hcso_write_goaway (struct hcso_writer *, lsquic_stream_id_t); diff --git a/src/liblsquic/lsquic_hq.h b/src/liblsquic/lsquic_hq.h index f97c831d3..9315cb7a8 100644 --- a/src/liblsquic/lsquic_hq.h +++ b/src/liblsquic/lsquic_hq.h @@ -18,6 +18,10 @@ enum hq_frame_type HQFT_PUSH_PROMISE = 5, HQFT_GOAWAY = 7, HQFT_MAX_PUSH_ID = 0xD, + /* WebTransport signal value carried where frame type is otherwise parsed. + * Per draft-ietf-webtrans-http3, this is not a regular HTTP/3 frame. + */ + HQFT_WT_STREAM = 0x41, /* These made me expand shf_frame_type to 4 bytes from 1. If at some * point we have to support a frame that is wider than 4 byte, it will * be time to bite the bullet and use our own enum for these types @@ -37,8 +41,15 @@ enum hq_frame_type enum hq_setting_id { HQSID_QPACK_MAX_TABLE_CAPACITY = 1, + HQSID_ENABLE_CONNECT_PROTOCOL = 8, HQSID_MAX_HEADER_LIST_SIZE = 6, HQSID_QPACK_BLOCKED_STREAMS = 7, + HQSID_H3_DATAGRAM_ENABLED = 0x33, + HQSID_WT_MAX_SESSIONS = 0x14E9CD29, + HQSID_WT_ENABLED = 0x2C7CF000, + HQSID_WT_INITIAL_MAX_DATA = 0x2B61, + HQSID_WT_INITIAL_MAX_STREAMS_UNI = 0x2B64, + HQSID_WT_INITIAL_MAX_STREAMS_BIDI = 0x2B65, }; /* As of 12/18/2018: */ @@ -56,6 +67,7 @@ enum hq_uni_stream_type HQUST_PUSH = 1, HQUST_QPACK_ENC = 2, HQUST_QPACK_DEC = 3, + HQUST_WEBTRANSPORT = 0x54, }; @@ -63,6 +75,7 @@ enum hq_uni_stream_type */ enum http_error_code { + HEC_DATAGRAM_ERROR = 0x33, HEC_NO_ERROR = 0x100, HEC_GENERAL_PROTOCOL_ERROR = 0x101, HEC_INTERNAL_ERROR = 0x102, @@ -83,6 +96,9 @@ enum http_error_code HEC_QPACK_DECOMPRESSION_FAILED = 0x200, HEC_QPACK_ENCODER_STREAM_ERROR = 0x201, HEC_QPACK_DECODER_STREAM_ERROR = 0x202, + /* [draft-ietf-webtrans-http3-15], Section 6 */ + HEC_WT_SESSION_GONE = 0x170D7B68, + HEC_WT_BUFFERED_STREAM_REJECTED = 0x3994BD84, }; diff --git a/src/liblsquic/lsquic_logger.c b/src/liblsquic/lsquic_logger.c index 3599b2aa9..eb4cc687d 100644 --- a/src/liblsquic/lsquic_logger.c +++ b/src/liblsquic/lsquic_logger.c @@ -97,6 +97,7 @@ enum lsq_log_level lsq_log_levels[N_LSQUIC_LOGGER_MODULES] = { [LSQLM_BW_SAMPLER] = LSQ_LOG_WARN, [LSQLM_PACKET_RESIZE] = LSQ_LOG_WARN, [LSQLM_CONN_STATS] = LSQ_LOG_WARN, + [LSQLM_WT] = LSQ_LOG_WARN, }; const char *const lsqlm_to_str[N_LSQUIC_LOGGER_MODULES] = { @@ -142,6 +143,7 @@ const char *const lsqlm_to_str[N_LSQUIC_LOGGER_MODULES] = { [LSQLM_BW_SAMPLER] = "bw-sampler", [LSQLM_PACKET_RESIZE] = "packet-resize", [LSQLM_CONN_STATS] = "conn-stats", + [LSQLM_WT] = "wt", }; const char *const lsq_loglevel2str[N_LSQUIC_LOG_LEVELS] = { @@ -383,6 +385,17 @@ lsquic_logger_log1 (enum lsq_log_level log_level, void lsquic_logger_log0 (enum lsq_log_level log_level, const char *fmt, ...) +{ + va_list ap; + + va_start(ap, fmt); + lsquic_logger_log0v(log_level, fmt, ap); + va_end(ap); +} + + +void +lsquic_logger_log0v (enum lsq_log_level log_level, const char *fmt, va_list ap) { const int saved_errno = errno; size_t len = 0; @@ -402,10 +415,7 @@ lsquic_logger_log0 (enum lsq_log_level log_level, const char *fmt, ...) if (FORMAT_PROBLEM(lb, len, max)) goto end; len += lb; - va_list ap; - va_start(ap, fmt); lb = vsnprintf(buf + len, max - len, fmt, ap); - va_end(ap); if (lb > 0 && (size_t) lb >= max - len && max - len >= TRUNC_SZ) { len = max - TRUNC_SZ; diff --git a/src/liblsquic/lsquic_logger.h b/src/liblsquic/lsquic_logger.h index 8442755bd..1ca7b69e8 100644 --- a/src/liblsquic/lsquic_logger.h +++ b/src/liblsquic/lsquic_logger.h @@ -90,6 +90,7 @@ enum lsquic_logger_module { LSQLM_BW_SAMPLER, LSQLM_PACKET_RESIZE, LSQLM_CONN_STATS, + LSQLM_WT, N_LSQUIC_LOGGER_MODULES }; @@ -187,6 +188,9 @@ lsquic_logger_log0 (enum lsq_log_level, const char *format, ...) __attribute__((format(printf, 2, 3))) #endif ; + +void +lsquic_logger_log0v (enum lsq_log_level, const char *format, va_list ap); # define LSQ_LOG0(level, ...) do { \ if (LSQ_LOG_ENABLED(level)) \ lsquic_logger_log0(level, __VA_ARGS__); \ diff --git a/src/liblsquic/lsquic_mini_conn.c b/src/liblsquic/lsquic_mini_conn.c index 54168e31a..57fa4e95b 100644 --- a/src/liblsquic/lsquic_mini_conn.c +++ b/src/liblsquic/lsquic_mini_conn.c @@ -647,6 +647,7 @@ static process_frame_f const process_frames[N_QUIC_FRAMES] = [QUIC_FRAME_PADDING] = process_padding_frame, [QUIC_FRAME_PING] = process_ping_frame, [QUIC_FRAME_RST_STREAM] = process_rst_stream_frame, + [QUIC_FRAME_RESET_STREAM_AT] = process_invalid_frame, [QUIC_FRAME_STOP_WAITING] = process_stop_waiting_frame, [QUIC_FRAME_STREAM] = process_stream_frame, [QUIC_FRAME_WINDOW_UPDATE] = process_window_update_frame, diff --git a/src/liblsquic/lsquic_mini_conn_ietf.c b/src/liblsquic/lsquic_mini_conn_ietf.c index 971951679..a207c09d7 100644 --- a/src/liblsquic/lsquic_mini_conn_ietf.c +++ b/src/liblsquic/lsquic_mini_conn_ietf.c @@ -1385,6 +1385,7 @@ static unsigned (*const imico_process_frames[N_QUIC_FRAMES]) * them the same: handshake cannot proceed. */ [QUIC_FRAME_RST_STREAM] = imico_process_invalid_frame, + [QUIC_FRAME_RESET_STREAM_AT] = imico_process_invalid_frame, [QUIC_FRAME_MAX_DATA] = imico_process_invalid_frame, [QUIC_FRAME_MAX_STREAM_DATA] = imico_process_invalid_frame, [QUIC_FRAME_MAX_STREAMS] = imico_process_invalid_frame, diff --git a/src/liblsquic/lsquic_packet_common.c b/src/liblsquic/lsquic_packet_common.c index 92a290e65..3998a2419 100644 --- a/src/liblsquic/lsquic_packet_common.c +++ b/src/liblsquic/lsquic_packet_common.c @@ -42,6 +42,7 @@ const char * const frame_type_2_str[N_QUIC_FRAMES] = { [QUIC_FRAME_ACK_FREQUENCY] = "QUIC_FRAME_ACK_FREQUENCY", [QUIC_FRAME_TIMESTAMP] = "QUIC_FRAME_TIMESTAMP", [QUIC_FRAME_DATAGRAM] = "QUIC_FRAME_DATAGRAM", + [QUIC_FRAME_RESET_STREAM_AT] = "QUIC_FRAME_RESET_STREAM_AT", }; diff --git a/src/liblsquic/lsquic_packet_common.h b/src/liblsquic/lsquic_packet_common.h index 9e8e5cef6..36f638214 100644 --- a/src/liblsquic/lsquic_packet_common.h +++ b/src/liblsquic/lsquic_packet_common.h @@ -49,6 +49,7 @@ enum quic_frame_type QUIC_FRAME_ACK_FREQUENCY, /* I */ QUIC_FRAME_TIMESTAMP, /* I */ QUIC_FRAME_DATAGRAM, /* I */ + QUIC_FRAME_RESET_STREAM_AT, /* I */ N_QUIC_FRAMES }; @@ -80,6 +81,7 @@ enum quic_ft_bit { QUIC_FTBIT_ACK_FREQUENCY = 1 << QUIC_FRAME_ACK_FREQUENCY, QUIC_FTBIT_TIMESTAMP = 1 << QUIC_FRAME_TIMESTAMP, QUIC_FTBIT_DATAGRAM = 1 << QUIC_FRAME_DATAGRAM, + QUIC_FTBIT_RESET_STREAM_AT = 1 << QUIC_FRAME_RESET_STREAM_AT, }; extern const char * const frame_type_2_str[N_QUIC_FRAMES]; @@ -120,6 +122,7 @@ extern const char * const frame_type_2_str[N_QUIC_FRAMES]; QUIC_FRAME_SLEN(QUIC_FRAME_ACK_FREQUENCY) + 1 + \ QUIC_FRAME_SLEN(QUIC_FRAME_TIMESTAMP) + 1 + \ QUIC_FRAME_SLEN(QUIC_FRAME_DATAGRAM) + 1 + \ + QUIC_FRAME_SLEN(QUIC_FRAME_RESET_STREAM_AT) + 1 + \ 0 @@ -187,6 +190,7 @@ extern const char *const lsquic_pns2str[]; | QUIC_FTBIT_ACK \ | QUIC_FTBIT_PADDING \ | QUIC_FTBIT_RST_STREAM \ + | QUIC_FTBIT_RESET_STREAM_AT \ | QUIC_FTBIT_CONNECTION_CLOSE \ | QUIC_FTBIT_BLOCKED \ | QUIC_FTBIT_PING \ diff --git a/src/liblsquic/lsquic_parse.h b/src/liblsquic/lsquic_parse.h index e1abc4607..e0d70bb4f 100644 --- a/src/liblsquic/lsquic_parse.h +++ b/src/liblsquic/lsquic_parse.h @@ -163,6 +163,17 @@ struct parse_funcs int (*pf_parse_rst_frame) (const unsigned char *buf, size_t buf_len, lsquic_stream_id_t *stream_id, uint64_t *offset, uint64_t *error_code); + unsigned + (*pf_reset_stream_at_frame_size) (lsquic_stream_id_t stream_id, + uint64_t error_code, uint64_t final_size, uint64_t reliable_size); + int + (*pf_gen_reset_stream_at_frame) (unsigned char *buf, size_t buf_len, + lsquic_stream_id_t stream_id, uint64_t error_code, uint64_t final_size, + uint64_t reliable_size); + int + (*pf_parse_reset_stream_at_frame) (const unsigned char *buf, size_t buf_len, + lsquic_stream_id_t *stream_id, uint64_t *error_code, + uint64_t *final_size, uint64_t *reliable_size); int (*pf_parse_stop_sending_frame) (const unsigned char *buf, size_t buf_len, lsquic_stream_id_t *stream_id, uint64_t *error_code); diff --git a/src/liblsquic/lsquic_parse_common.c b/src/liblsquic/lsquic_parse_common.c index 9060cf491..7ead6bc67 100644 --- a/src/liblsquic/lsquic_parse_common.c +++ b/src/liblsquic/lsquic_parse_common.c @@ -344,6 +344,7 @@ const enum quic_ft_bit lsquic_legal_frames_by_level[N_LSQVER][N_ENC_LEVS] = | QUIC_FTBIT_ACK | QUIC_FTBIT_CONNECTION_CLOSE, [ENC_LEV_0RTT] = QUIC_FTBIT_PADDING | QUIC_FTBIT_PING | QUIC_FTBIT_STREAM | QUIC_FTBIT_RST_STREAM + | QUIC_FTBIT_RESET_STREAM_AT | QUIC_FTBIT_BLOCKED | QUIC_FTBIT_CONNECTION_CLOSE | QUIC_FTBIT_MAX_DATA | QUIC_FTBIT_MAX_STREAM_DATA | QUIC_FTBIT_MAX_STREAMS | QUIC_FTBIT_STREAM_BLOCKED @@ -357,6 +358,7 @@ const enum quic_ft_bit lsquic_legal_frames_by_level[N_LSQVER][N_ENC_LEVS] = [ENC_LEV_APP] = QUIC_FTBIT_CRYPTO | QUIC_FTBIT_PADDING | QUIC_FTBIT_PING | QUIC_FTBIT_ACK | QUIC_FTBIT_CONNECTION_CLOSE | QUIC_FTBIT_STREAM | QUIC_FTBIT_RST_STREAM + | QUIC_FTBIT_RESET_STREAM_AT | QUIC_FTBIT_BLOCKED | QUIC_FTBIT_MAX_DATA | QUIC_FTBIT_MAX_STREAM_DATA | QUIC_FTBIT_MAX_STREAMS | QUIC_FTBIT_STREAM_BLOCKED @@ -374,6 +376,7 @@ const enum quic_ft_bit lsquic_legal_frames_by_level[N_LSQVER][N_ENC_LEVS] = | QUIC_FTBIT_ACK | QUIC_FTBIT_CONNECTION_CLOSE, [ENC_LEV_0RTT] = QUIC_FTBIT_PADDING | QUIC_FTBIT_PING | QUIC_FTBIT_STREAM | QUIC_FTBIT_RST_STREAM + | QUIC_FTBIT_RESET_STREAM_AT | QUIC_FTBIT_BLOCKED | QUIC_FTBIT_CONNECTION_CLOSE | QUIC_FTBIT_MAX_DATA | QUIC_FTBIT_MAX_STREAM_DATA | QUIC_FTBIT_MAX_STREAMS | QUIC_FTBIT_STREAM_BLOCKED @@ -387,6 +390,7 @@ const enum quic_ft_bit lsquic_legal_frames_by_level[N_LSQVER][N_ENC_LEVS] = [ENC_LEV_APP] = QUIC_FTBIT_CRYPTO | QUIC_FTBIT_PADDING | QUIC_FTBIT_PING | QUIC_FTBIT_ACK | QUIC_FTBIT_CONNECTION_CLOSE | QUIC_FTBIT_STREAM | QUIC_FTBIT_RST_STREAM + | QUIC_FTBIT_RESET_STREAM_AT | QUIC_FTBIT_BLOCKED | QUIC_FTBIT_MAX_DATA | QUIC_FTBIT_MAX_STREAM_DATA | QUIC_FTBIT_MAX_STREAMS | QUIC_FTBIT_STREAM_BLOCKED @@ -404,6 +408,7 @@ const enum quic_ft_bit lsquic_legal_frames_by_level[N_LSQVER][N_ENC_LEVS] = | QUIC_FTBIT_ACK | QUIC_FTBIT_CONNECTION_CLOSE, [ENC_LEV_0RTT] = QUIC_FTBIT_PADDING | QUIC_FTBIT_PING | QUIC_FTBIT_STREAM | QUIC_FTBIT_RST_STREAM + | QUIC_FTBIT_RESET_STREAM_AT | QUIC_FTBIT_BLOCKED | QUIC_FTBIT_CONNECTION_CLOSE | QUIC_FTBIT_MAX_DATA | QUIC_FTBIT_MAX_STREAM_DATA | QUIC_FTBIT_MAX_STREAMS | QUIC_FTBIT_STREAM_BLOCKED @@ -417,6 +422,7 @@ const enum quic_ft_bit lsquic_legal_frames_by_level[N_LSQVER][N_ENC_LEVS] = [ENC_LEV_APP] = QUIC_FTBIT_CRYPTO | QUIC_FTBIT_PADDING | QUIC_FTBIT_PING | QUIC_FTBIT_ACK | QUIC_FTBIT_CONNECTION_CLOSE | QUIC_FTBIT_STREAM | QUIC_FTBIT_RST_STREAM + | QUIC_FTBIT_RESET_STREAM_AT | QUIC_FTBIT_BLOCKED | QUIC_FTBIT_MAX_DATA | QUIC_FTBIT_MAX_STREAM_DATA | QUIC_FTBIT_MAX_STREAMS | QUIC_FTBIT_STREAM_BLOCKED @@ -434,6 +440,7 @@ const enum quic_ft_bit lsquic_legal_frames_by_level[N_LSQVER][N_ENC_LEVS] = | QUIC_FTBIT_ACK | QUIC_FTBIT_CONNECTION_CLOSE, [ENC_LEV_0RTT] = QUIC_FTBIT_PADDING | QUIC_FTBIT_PING | QUIC_FTBIT_STREAM | QUIC_FTBIT_RST_STREAM + | QUIC_FTBIT_RESET_STREAM_AT | QUIC_FTBIT_BLOCKED | QUIC_FTBIT_MAX_DATA | QUIC_FTBIT_MAX_STREAM_DATA | QUIC_FTBIT_MAX_STREAMS | QUIC_FTBIT_STREAM_BLOCKED @@ -448,6 +455,7 @@ const enum quic_ft_bit lsquic_legal_frames_by_level[N_LSQVER][N_ENC_LEVS] = [ENC_LEV_APP] = QUIC_FTBIT_CRYPTO | QUIC_FTBIT_PADDING | QUIC_FTBIT_PING | QUIC_FTBIT_ACK | QUIC_FTBIT_CONNECTION_CLOSE | QUIC_FTBIT_STREAM | QUIC_FTBIT_RST_STREAM + | QUIC_FTBIT_RESET_STREAM_AT | QUIC_FTBIT_BLOCKED | QUIC_FTBIT_MAX_DATA | QUIC_FTBIT_MAX_STREAM_DATA | QUIC_FTBIT_MAX_STREAMS | QUIC_FTBIT_STREAM_BLOCKED diff --git a/src/liblsquic/lsquic_parse_ietf_v1.c b/src/liblsquic/lsquic_parse_ietf_v1.c index 657ee3e1c..95efd6eea 100644 --- a/src/liblsquic/lsquic_parse_ietf_v1.c +++ b/src/liblsquic/lsquic_parse_ietf_v1.c @@ -804,6 +804,103 @@ ietf_v1_parse_rst_frame (const unsigned char *buf, size_t buf_len, } +static unsigned +ietf_v1_reset_stream_at_frame_size (lsquic_stream_id_t stream_id, + uint64_t error_code, uint64_t final_size, uint64_t reliable_size) +{ + return 1 /* Type */ + + vint_size(stream_id) /* Stream ID (i) */ + + vint_size(error_code) /* Application Error Code (i) */ + + vint_size(final_size) /* Final Size (i) */ + + vint_size(reliable_size); /* Reliable Size (i) */ +} + + +static int +ietf_v1_gen_reset_stream_at_frame (unsigned char *buf, size_t buf_len, + lsquic_stream_id_t stream_id, uint64_t error_code, uint64_t final_size, + uint64_t reliable_size) +{ + unsigned vbits; + unsigned char *p; + + if (buf_len < ietf_v1_reset_stream_at_frame_size(stream_id, error_code, + final_size, reliable_size)) + return -1; + + p = buf; + + *p++ = 0x24; + /* Stream ID (i) */ + vbits = vint_val2bits(stream_id); + vint_write(p, stream_id, vbits, 1 << vbits); + p += 1 << vbits; + + /* Application Error Code (i) */ + vbits = vint_val2bits(error_code); + vint_write(p, error_code, vbits, 1 << vbits); + p += 1 << vbits; + + /* Final Size (i) */ + vbits = vint_val2bits(final_size); + vint_write(p, final_size, vbits, 1 << vbits); + p += 1 << vbits; + + /* Reliable Size (i) */ + vbits = vint_val2bits(reliable_size); + vint_write(p, reliable_size, vbits, 1 << vbits); + p += 1 << vbits; + + return p - buf; +} + + +static int +ietf_v1_parse_reset_stream_at_frame (const unsigned char *buf, size_t buf_len, + lsquic_stream_id_t *stream_id_p, uint64_t *error_code_p, + uint64_t *final_size_p, uint64_t *reliable_size_p) +{ + const unsigned char *p = buf + 1; + const unsigned char *const end = buf + buf_len; + uint64_t stream_id, error_code, final_size, reliable_size; + int r; + + /* Stream ID (i) */ + r = vint_read(p, end, &stream_id); + if (r < 0) + return r; + p += r; + + /* Application Error Code (i) */ + r = vint_read(p, end, &error_code); + if (r < 0) + return r; + p += r; + + /* Final Size (i) */ + r = vint_read(p, end, &final_size); + if (r < 0) + return r; + p += r; + + /* Reliable Size (i) */ + r = vint_read(p, end, &reliable_size); + if (r < 0) + return r; + p += r; + + if (reliable_size > final_size) + return -1; + + *stream_id_p = stream_id; + *error_code_p = error_code; + *final_size_p = final_size; + *reliable_size_p = reliable_size; + + return p - buf; +} + + static int ietf_v1_parse_stop_sending_frame (const unsigned char *buf, size_t buf_len, lsquic_stream_id_t *stream_id, uint64_t *error_code) @@ -2294,6 +2391,9 @@ const struct parse_funcs lsquic_parse_funcs_ietf_v1 = .pf_rst_frame_size = ietf_v1_rst_frame_size, .pf_gen_rst_frame = ietf_v1_gen_rst_frame, .pf_parse_rst_frame = ietf_v1_parse_rst_frame, + .pf_reset_stream_at_frame_size = ietf_v1_reset_stream_at_frame_size, + .pf_gen_reset_stream_at_frame = ietf_v1_gen_reset_stream_at_frame, + .pf_parse_reset_stream_at_frame = ietf_v1_parse_reset_stream_at_frame, .pf_connect_close_frame_size = ietf_v1_connect_close_frame_size, .pf_gen_connect_close_frame = ietf_v1_gen_connect_close_frame, .pf_parse_connect_close_frame = ietf_v1_parse_connect_close_frame, diff --git a/src/liblsquic/lsquic_parse_iquic_common.c b/src/liblsquic/lsquic_parse_iquic_common.c index 416e382c2..7715fefc4 100644 --- a/src/liblsquic/lsquic_parse_iquic_common.c +++ b/src/liblsquic/lsquic_parse_iquic_common.c @@ -278,7 +278,7 @@ const enum quic_frame_type lsquic_iquic_byte2type[0x40] = [0x21] = QUIC_FRAME_INVALID, [0x22] = QUIC_FRAME_INVALID, [0x23] = QUIC_FRAME_INVALID, - [0x24] = QUIC_FRAME_INVALID, + [0x24] = QUIC_FRAME_RESET_STREAM_AT, [0x25] = QUIC_FRAME_INVALID, [0x26] = QUIC_FRAME_INVALID, [0x27] = QUIC_FRAME_INVALID, diff --git a/src/liblsquic/lsquic_send_ctl.c b/src/liblsquic/lsquic_send_ctl.c index e2a7cfedf..513946db9 100644 --- a/src/liblsquic/lsquic_send_ctl.c +++ b/src/liblsquic/lsquic_send_ctl.c @@ -2707,7 +2707,7 @@ lsquic_send_ctl_elide_stream_frames (lsquic_send_ctl_t *ctl, lsquic_stream_id_t stream_id) { struct lsquic_packet_out *packet_out, *next; - unsigned n, adj; + unsigned adj; int dropped; dropped = 0; @@ -2740,6 +2740,17 @@ lsquic_send_ctl_elide_stream_frames (lsquic_send_ctl_t *ctl, if (dropped) lsquic_send_ctl_reset_packnos(ctl); + lsquic_send_ctl_elide_stream_frames_from_buffered(ctl, stream_id); +} + + +void +lsquic_send_ctl_elide_stream_frames_from_buffered (lsquic_send_ctl_t *ctl, + lsquic_stream_id_t stream_id) +{ + struct lsquic_packet_out *packet_out, *next; + unsigned n; + for (n = 0; n < sizeof(ctl->sc_buffered_packets) / sizeof(ctl->sc_buffered_packets[0]); ++n) { @@ -3164,14 +3175,14 @@ send_ctl_maybe_flush_decoder (struct lsquic_send_ctl *ctl, lsquic_packet_out_t * lsquic_send_ctl_get_packet_for_stream (lsquic_send_ctl_t *ctl, unsigned need_at_least, const struct network_path *path, - const struct lsquic_stream *stream) + const struct lsquic_stream *stream, int buffered_packet_ok) { enum buf_packet_type packet_type; if (lsquic_send_ctl_schedule_stream_packets_immediately(ctl)) return lsquic_send_ctl_get_writeable_packet(ctl, PNS_APP, need_at_least, path, 0, NULL); - else + else if (buffered_packet_ok) { if (!lsquic_send_ctl_has_buffered(ctl)) send_ctl_maybe_flush_decoder(ctl, stream); @@ -3179,6 +3190,8 @@ lsquic_send_ctl_get_packet_for_stream (lsquic_send_ctl_t *ctl, return send_ctl_get_buffered_packet(ctl, packet_type, need_at_least, path, stream); } + else + return NULL; } @@ -3197,7 +3210,7 @@ lsquic_sendctl_gen_stream_blocked_frame (struct lsquic_send_ctl *ctl, off = lsquic_stream_combined_send_off(stream); need = pf->pf_stream_blocked_frame_size(stream->id, off); packet_out = lsquic_send_ctl_get_packet_for_stream(ctl, need, - stream->conn_pub->path, stream); + stream->conn_pub->path, stream, 1); if (!packet_out) { LSQ_DEBUG("failed to get packet_out with lsquic_send_ctl_get_packet_for_stream"); diff --git a/src/liblsquic/lsquic_send_ctl.h b/src/liblsquic/lsquic_send_ctl.h index 67d2abf44..15633848b 100644 --- a/src/liblsquic/lsquic_send_ctl.h +++ b/src/liblsquic/lsquic_send_ctl.h @@ -243,7 +243,7 @@ lsquic_send_ctl_get_writeable_packet (lsquic_send_ctl_t *, enum packnum_space, struct lsquic_packet_out * lsquic_send_ctl_get_packet_for_stream (lsquic_send_ctl_t *, unsigned need_at_least, const struct network_path *, - const struct lsquic_stream *); + const struct lsquic_stream *, int buffered_packet_ok); int lsquic_sendctl_gen_stream_blocked_frame (struct lsquic_send_ctl *ctl, @@ -284,6 +284,10 @@ lsquic_send_ctl_turn_nstp_on (lsquic_send_ctl_t *ctl) void lsquic_send_ctl_elide_stream_frames (lsquic_send_ctl_t *, lsquic_stream_id_t); +void +lsquic_send_ctl_elide_stream_frames_from_buffered (lsquic_send_ctl_t *, + lsquic_stream_id_t); + int lsquic_send_ctl_squeeze_sched (lsquic_send_ctl_t *); diff --git a/src/liblsquic/lsquic_stream.c b/src/liblsquic/lsquic_stream.c index bed9f70b4..85fc6074e 100644 --- a/src/liblsquic/lsquic_stream.c +++ b/src/liblsquic/lsquic_stream.c @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -60,6 +61,8 @@ #include "lsquic_byteswap.h" #include "lsquic_ietf.h" #include "lsquic_hcso_writer.h" +#include "lsquic_sizes.h" +#include "lsquic_trans_params.h" #define LSQUIC_LOGGER_MODULE LSQLM_STREAM #define LSQUIC_LOG_CONN_ID lsquic_conn_log_cid(stream->conn_pub->lconn) @@ -71,6 +74,9 @@ static void drop_frames_in (lsquic_stream_t *stream); +static void +maybe_finish_reset_at (lsquic_stream_t *stream); + static void maybe_schedule_call_on_close (lsquic_stream_t *stream); @@ -80,12 +86,33 @@ stream_wantread (lsquic_stream_t *stream, int is_want); static int stream_wantwrite (lsquic_stream_t *stream, int is_want); +static int +stream_is_locally_initiated_unidir (const struct lsquic_stream *stream); + +static int +stream_is_incoming_unidir (const struct lsquic_stream *stream); + enum stream_write_options { SWO_BUFFER = 1 << 0, /* Allow buffering in sm_buf */ }; +struct capsule_parser +{ + uint64_t cp_type; + lsquic_capsule_read_f cp_cb; +}; + + +struct capsule_parsers +{ + unsigned cps_count; + unsigned cps_nalloc; + struct capsule_parser cps_elems[0]; +}; + + static ssize_t stream_write_to_packets (lsquic_stream_t *, struct lsquic_reader *, size_t, enum stream_write_options); @@ -93,9 +120,25 @@ stream_write_to_packets (lsquic_stream_t *, struct lsquic_reader *, size_t, static ssize_t save_to_buffer (lsquic_stream_t *, struct lsquic_reader *, size_t len); +static void +http_dg_capsule_on_read (lsquic_stream_t *stream, lsquic_stream_ctx_t *UNUSED_h); + +static void +http_dg_capsule_on_write (lsquic_stream_t *stream, lsquic_stream_ctx_t *UNUSED_h); + +static lsquic_capsule_read_f +stream_find_capsule_cb (struct lsquic_stream *stream, uint64_t capsule_type); + +static void +http_dg_capsule_datagram_cb (lsquic_stream_t *stream, lsquic_stream_ctx_t *h, + uint64_t capsule_type, const void *payload, size_t payload_len); + static int stream_flush (lsquic_stream_t *stream); +static void +maybe_mark_as_blocked (lsquic_stream_t *stream); + static int stream_flush_nocheck (lsquic_stream_t *stream); @@ -202,6 +245,10 @@ enum stream_history_event SHE_FLUSH = 'u', SHE_STOP_SENDIG_OUT = 'U', SHE_USER_WRITE_DATA = 'w', + SHE_HTTP_DG_RECV = 'm', + SHE_HTTP_DG_SEND = 'M', + SHE_HTTP_DG_CAPSULE_ON = 'k', + SHE_HTTP_DG_CAPSULE_OFF= 'K', SHE_SHUTDOWN_WRITE = 'W', SHE_CLOSE = 'X', SHE_DELAY_SW = 'y', @@ -236,7 +283,6 @@ sm_history_append (lsquic_stream_t *stream, enum stream_history_event sh_event) stream->sm_hist_buf); } - # define SM_HISTORY_APPEND(stream, event) sm_history_append(stream, event) # define SM_HISTORY_DUMP_REMAINING(stream) do { \ if (stream->sm_hist_idx & SM_HIST_IDX_MASK) \ @@ -249,6 +295,20 @@ sm_history_append (lsquic_stream_t *stream, enum stream_history_event sh_event) # define SM_HISTORY_DUMP_REMAINING(stream) #endif +#if LSQUIC_KEEP_STREAM_HISTORY +void +lsquic_stream_hist_http_dg_recv (lsquic_stream_t *stream) +{ + SM_HISTORY_APPEND(stream, SHE_HTTP_DG_RECV); +} + +void +lsquic_stream_hist_http_dg_send (lsquic_stream_t *stream) +{ + SM_HISTORY_APPEND(stream, SHE_HTTP_DG_SEND); +} +#endif + static int stream_inside_callback (const lsquic_stream_t *stream) @@ -358,7 +418,7 @@ stream_is_hsk (const struct lsquic_stream *stream) static struct lsquic_packet_out * stream_get_packet_for_stream_0rtt (struct lsquic_send_ctl *ctl, unsigned need_at_least, const struct network_path *path, - const struct lsquic_stream *stream) + const struct lsquic_stream *stream, int buffered_packet_ok) { struct lsquic_packet_out *packet_out; @@ -371,12 +431,12 @@ stream_get_packet_for_stream_0rtt (struct lsquic_send_ctl *ctl, ((struct lsquic_stream *) stream)->sm_get_packet_for_stream = lsquic_send_ctl_get_packet_for_stream; return lsquic_send_ctl_get_packet_for_stream(ctl, need_at_least, - path, stream); + path, stream, buffered_packet_ok); } else { packet_out = lsquic_send_ctl_get_packet_for_stream(ctl, need_at_least, - path, stream); + path, stream, buffered_packet_ok); if (packet_out) packet_out->po_header_type = HETY_0RTT; return packet_out; @@ -410,6 +470,7 @@ stream_new_common (lsquic_stream_id_t id, struct lsquic_conn_public *conn_pub, stream->conn_pub = conn_pub; stream->sm_onnew_arg = stream_if_ctx; stream->sm_write_avail = stream_write_avail_no_frames; + stream->sm_ss_code = HEC_NO_ERROR; STAILQ_INIT(&stream->sm_hq_frames); STAILQ_INIT(&stream->uh); @@ -641,6 +702,7 @@ lsquic_stream_destroy (lsquic_stream_t *stream) SM_HISTORY_APPEND(stream, SHE_ONCLOSE_CALL); stream->stream_if->on_close(stream, stream->st_ctx); } + lsquic_stream_set_http_dg_if(stream, NULL); if (stream->sm_qflags & SMQF_SENDING_FLAGS) TAILQ_REMOVE(&stream->conn_pub->sending_streams, stream, next_send_stream); if (stream->sm_qflags & SMQF_WANT_READ) @@ -649,6 +711,8 @@ lsquic_stream_destroy (lsquic_stream_t *stream) TAILQ_REMOVE(&stream->conn_pub->write_streams, stream, next_write_stream); if (stream->sm_qflags & SMQF_SERVICE_FLAGS) TAILQ_REMOVE(&stream->conn_pub->service_streams, stream, next_service_stream); + if (stream->sm_qflags & SMQF_WANT_HTTP_DG) + TAILQ_REMOVE(&stream->conn_pub->http_dg_streams, stream, next_http_dg_stream); if (stream->sm_qflags & SMQF_QPACK_DEC) lsquic_qdh_cancel_stream(stream->conn_pub->u.ietf.qdh, stream); else if ((stream->sm_bflags & (SMBF_IETF|SMBF_USE_HEADERS)) @@ -665,8 +729,18 @@ lsquic_stream_destroy (lsquic_stream_t *stream) STAILQ_REMOVE_HEAD(&stream->uh, uh_next); destroy_uh(uh, stream->conn_pub->enpub->enp_hsi_if); } + if (stream->sm_http_dg) + { + free(stream->sm_http_dg->read.buf); + free(stream->sm_http_dg->write.buf); + free(stream->sm_http_dg); + stream->sm_http_dg = NULL; + } + lsquic_stream_clear_capsule_handlers(stream); free(stream->sm_buf); free(stream->sm_header_block); + if (stream->conn_pub->cp_on_stream_destroy) + stream->conn_pub->cp_on_stream_destroy(stream); LSQ_DEBUG("destroyed stream"); SM_HISTORY_DUMP_REMAINING(stream); free(stream); @@ -692,7 +766,9 @@ stream_is_finished (struct lsquic_stream *stream) /* Can't finish stream until all "self" flags are unset: */ | SMQF_SELF_FLAGS)) && ((stream->stream_flags & STREAM_FORCE_FINISH) - || (stream->stream_flags & (STREAM_FIN_SENT |STREAM_RST_SENT))); + || (stream->stream_flags & (STREAM_FIN_SENT |STREAM_RST_SENT)) + || (stream_is_incoming_unidir(stream) + && (stream->stream_flags & STREAM_U_READ_DONE))); } @@ -849,6 +925,7 @@ stream_readable_discard (struct lsquic_stream *stream) } (void) maybe_switch_data_in(stream); + maybe_finish_reset_at(stream); return 0; /* Never readable */ } @@ -1064,6 +1141,18 @@ lsquic_stream_frame_in (lsquic_stream_t *stream, stream_frame_t *frame) goto release_packet_frame; } + if (frame->data_frame.df_fin && (stream->sm_bflags & SMBF_IETF) + && (stream->stream_flags & STREAM_RESET_AT_RECVD) + && stream->sm_reset_at_final != DF_END(frame)) + { + lconn = stream->conn_pub->lconn; + lconn->cn_if->ci_abort_error(lconn, 0, TEC_FINAL_SIZE_ERROR, + "final size %"PRIu64" from STREAM frame (id: %"PRIu64") does not " + "match RESET_STREAM_AT final size %"PRIu64, DF_END(frame), + stream->id, stream->sm_reset_at_final); + goto release_packet_frame; + } + if (frame->data_frame.df_fin && (stream->sm_bflags & SMBF_IETF) && (stream->stream_flags & STREAM_FIN_RECVD) && stream->sm_fin_off != DF_END(frame)) @@ -1154,31 +1243,178 @@ maybe_elide_stream_frames (struct lsquic_stream *stream) if (!(stream->stream_flags & STREAM_FRAMES_ELIDED)) { if (stream->n_unacked) - lsquic_send_ctl_elide_stream_frames(stream->conn_pub->send_ctl, - stream->id); + { + if (stream->sm_reset_stream_at_sz == 0) + lsquic_send_ctl_elide_stream_frames(stream->conn_pub->send_ctl, + stream->id); + else + /* Don't elide STREAM frames from scheduled packets because we + * want to ensure that the reliable bytes get delivered. + */ + lsquic_send_ctl_elide_stream_frames_from_buffered( + stream->conn_pub->send_ctl, stream->id); + } stream->stream_flags |= STREAM_FRAMES_ELIDED; } } -int -lsquic_stream_rst_in (lsquic_stream_t *stream, uint64_t offset, - uint64_t error_code) +static void +maybe_finish_reset_at (struct lsquic_stream *stream) +{ + if (!(stream->stream_flags & STREAM_RESET_AT_RECVD) + || (stream->stream_flags & STREAM_RST_RECVD) + || stream->read_offset < stream->sm_reset_at) + return; + + stream->stream_flags |= STREAM_RST_RECVD; + + if (stream->stream_if->on_reset + && !(stream->stream_flags & STREAM_ONCLOSE_DONE) + && !(stream->sm_dflags & SMDF_ONRESET0)) + { + stream->stream_if->on_reset(stream, stream->st_ctx, 0); + stream->sm_dflags |= SMDF_ONRESET0; + } + + /* Let user collect error: */ + maybe_conn_to_tickable_if_readable(stream); + + lsquic_sfcw_consume_rem(&stream->fc); + drop_frames_in(stream); + + maybe_finish_stream(stream); + maybe_schedule_call_on_close(stream); +} + + +static int +stream_reset_in_ietf (struct lsquic_stream *stream, uint64_t final_size, + uint64_t reliable_size, uint64_t error_code, + enum quic_frame_type frame_type) { struct lsquic_conn *lconn; + const char *frame_name; - if ((stream->sm_bflags & SMBF_IETF) - && (stream->stream_flags & STREAM_FIN_RECVD) - && stream->sm_fin_off != offset) + frame_name = frame_type_2_str[frame_type]; + + if (stream->stream_flags & STREAM_RESET_AT_RECVD) + { + if (stream->sm_reset_at_final != final_size + || stream->sm_reset_at_error != error_code) + { + lconn = stream->conn_pub->lconn; + lconn->cn_if->ci_abort_error(lconn, 0, + stream->sm_reset_at_final != final_size + ? TEC_FINAL_SIZE_ERROR + : TEC_STREAM_STATE_ERROR, + "%s frame changes final size or error code for stream " + "%"PRIu64" (final: %"PRIu64" vs %"PRIu64"; error: " + "%"PRIu64" vs %"PRIu64")", frame_name, stream->id, + stream->sm_reset_at_final, final_size, + stream->sm_reset_at_error, error_code); + return -1; + } + + if (reliable_size > stream->sm_reset_at) + { + LSQ_DEBUG("ignore %s increasing reliable size on stream %"PRIu64 + " (old %"PRIu64", new %"PRIu64")", + frame_name, stream->id, stream->sm_reset_at, + reliable_size); + return 0; + } + if (reliable_size < stream->sm_reset_at) + { + LSQ_DEBUG("update %s reliable size on stream %"PRIu64 + " from %"PRIu64" to %"PRIu64, frame_name, stream->id, + stream->sm_reset_at, reliable_size); + stream->sm_reset_at = reliable_size; + maybe_finish_reset_at(stream); + } + return 0; + } + + if ((stream->stream_flags & STREAM_FIN_RECVD) + && stream->sm_fin_off != final_size) { lconn = stream->conn_pub->lconn; lconn->cn_if->ci_abort_error(lconn, 0, TEC_FINAL_SIZE_ERROR, - "final size %"PRIu64" from RESET_STREAM frame (id: %"PRIu64") " - "does not match previous final size %"PRIu64, offset, + "final size %"PRIu64" from %s frame (id: %"PRIu64") does not " + "match previous final size %"PRIu64, final_size, frame_name, stream->id, stream->sm_fin_off); return -1; } + SM_HISTORY_APPEND(stream, SHE_RST_IN); + stream->stream_flags |= STREAM_RESET_AT_RECVD; + stream->sm_reset_at = reliable_size; + stream->sm_reset_at_final = final_size; + stream->sm_reset_at_error = error_code; + if (stream->sm_qflags & SMQF_WAIT_FIN_OFF) + { + stream->sm_qflags &= ~SMQF_WAIT_FIN_OFF; + LSQ_DEBUG("final offset is now known: %"PRIu64, final_size); + } + + if (lsquic_sfcw_get_max_recv_off(&stream->fc) > final_size) + { + LSQ_INFO("%s invalid: its final size %"PRIu64" is " + "smaller than that of byte following the last byte we have seen: " + "%"PRIu64, frame_name, final_size, + lsquic_sfcw_get_max_recv_off(&stream->fc)); + return -1; + } + + if (!lsquic_sfcw_set_max_recv_off(&stream->fc, final_size)) + { + LSQ_INFO("%s invalid: its final size %"PRIu64 + " violates flow control", frame_name, final_size); + return -1; + } + + maybe_finish_reset_at(stream); + return 0; +} + + +int +lsquic_stream_reset_stream_at_in (struct lsquic_stream *stream, + uint64_t final_size, uint64_t reliable_size, uint64_t error_code) +{ + assert(stream->sm_bflags & SMBF_IETF); + return stream_reset_in_ietf(stream, final_size, reliable_size, + error_code, QUIC_FRAME_RESET_STREAM_AT); +} + + +enum quic_frame_type +lsquic_stream_get_reset_frame_type (const struct lsquic_stream *stream, + uint64_t *reliable_size) +{ + if (stream->sm_reset_stream_at_sz == 0) + { + if (reliable_size) + *reliable_size = 0; + return QUIC_FRAME_RST_STREAM; + } + else + { + /* I would not worry about other users: STREAM_RESET_AT is used by + * WebTransport only. If other protocols evolve that use STREAM_RESET_AT + * and that don't want to retract and we want to implement them, we will + * worry about this later. + */ + *reliable_size = stream->sm_reset_stream_at_sz; + return QUIC_FRAME_RESET_STREAM_AT; + } +} + + +static int +stream_reset_in_gquic (struct lsquic_stream *stream, uint64_t offset, + uint64_t error_code) +{ if (stream->stream_flags & STREAM_RST_RECVD) { LSQ_DEBUG("ignore duplicate RST_STREAM frame"); @@ -1189,6 +1425,7 @@ lsquic_stream_rst_in (lsquic_stream_t *stream, uint64_t offset, /* This flag must always be set, even if we are "ignoring" it: it is * used by elision code. */ + stream->sm_rst_in_code = error_code; stream->stream_flags |= STREAM_RST_RECVD; if (lsquic_sfcw_get_max_recv_off(&stream->fc) > offset) @@ -1210,22 +1447,11 @@ lsquic_stream_rst_in (lsquic_stream_t *stream, uint64_t offset, if (stream->stream_if->on_reset && !(stream->stream_flags & STREAM_ONCLOSE_DONE)) { - if (stream->sm_bflags & SMBF_IETF) - { - if (!(stream->sm_dflags & SMDF_ONRESET0)) - { - stream->stream_if->on_reset(stream, stream->st_ctx, 0); - stream->sm_dflags |= SMDF_ONRESET0; - } - } - else - { - if ((stream->sm_dflags & (SMDF_ONRESET0|SMDF_ONRESET1)) + if ((stream->sm_dflags & (SMDF_ONRESET0|SMDF_ONRESET1)) != (SMDF_ONRESET0|SMDF_ONRESET1)) - { - stream->stream_if->on_reset(stream, stream->st_ctx, 2); - stream->sm_dflags |= SMDF_ONRESET0|SMDF_ONRESET1; - } + { + stream->stream_if->on_reset(stream, stream->st_ctx, 2); + stream->sm_dflags |= SMDF_ONRESET0|SMDF_ONRESET1; } } @@ -1235,11 +1461,8 @@ lsquic_stream_rst_in (lsquic_stream_t *stream, uint64_t offset, lsquic_sfcw_consume_rem(&stream->fc); drop_frames_in(stream); - if (!(stream->sm_bflags & SMBF_IETF)) - { - drop_buffered_data(stream); - maybe_elide_stream_frames(stream); - } + drop_buffered_data(stream); + maybe_elide_stream_frames(stream); if (stream->sm_qflags & SMQF_WAIT_FIN_OFF) { @@ -1249,7 +1472,6 @@ lsquic_stream_rst_in (lsquic_stream_t *stream, uint64_t offset, if (!(stream->stream_flags & (STREAM_RST_SENT|STREAM_SS_SENT|STREAM_FIN_SENT)) - && !(stream->sm_bflags & SMBF_IETF) && !(stream->sm_qflags & SMQF_SEND_RST)) stream_reset(stream, 7 /* QUIC_RST_ACKNOWLEDGEMENT */, 0); @@ -1262,6 +1484,18 @@ lsquic_stream_rst_in (lsquic_stream_t *stream, uint64_t offset, } +int +lsquic_stream_rst_in (struct lsquic_stream *stream, uint64_t offset, + uint64_t error_code) +{ + if (stream->sm_bflags & SMBF_IETF) + return stream_reset_in_ietf(stream, offset, 0, error_code, + QUIC_FRAME_RST_STREAM); + else + return stream_reset_in_gquic(stream, offset, error_code); +} + + void lsquic_stream_stop_sending_in (struct lsquic_stream *stream, uint64_t error_code) @@ -1273,6 +1507,7 @@ lsquic_stream_stop_sending_in (struct lsquic_stream *stream, } SM_HISTORY_APPEND(stream, SHE_STOP_SENDIG_IN); + stream->sm_ss_in_code = error_code; stream->stream_flags |= STREAM_SS_RECVD; if (stream->stream_if->on_reset && !(stream->sm_dflags & SMDF_ONRESET1) @@ -1387,6 +1622,17 @@ lsquic_stream_peer_blocked_gquic (struct lsquic_stream *stream) } +int +lsquic_stream_is_blocked (const lsquic_stream_t *stream) +{ + return (stream->blocked_off + && stream->blocked_off == stream->max_send_off) + || (0 == stream->blocked_off + && 0 == stream->max_send_off + && (stream->stream_flags & STREAM_BLOCKED_SENT)); +} + + void lsquic_stream_blocked_frame_sent (lsquic_stream_t *stream) { @@ -1491,10 +1737,17 @@ read_data_frames (struct lsquic_stream *stream, int do_filtering, { struct data_frame *data_frame; size_t nread, toread, total_nread; + ssize_t rv; int short_read, processed_frames; +#ifndef NDEBUG + assert(!(stream->sm_dflags & SMDF_READING_DATA_FRAMES)); + stream->sm_dflags |= SMDF_READING_DATA_FRAMES; +#endif + processed_frames = 0; total_nread = 0; + rv = 0; while ((data_frame = stream->data_in->di_if->di_get_frame( stream->data_in, stream->read_offset))) @@ -1529,7 +1782,10 @@ read_data_frames (struct lsquic_stream *stream, int do_filtering, stream->data_in->di_if->di_frame_done(stream->data_in, data_frame); data_frame = NULL; if (0 != maybe_switch_data_in(stream)) - return -1; + { + rv = -1; + goto end; + } if (fin) { stream->stream_flags |= STREAM_FIN_REACHED; @@ -1548,7 +1804,13 @@ read_data_frames (struct lsquic_stream *stream, int do_filtering, if (processed_frames) stream_consumed_bytes(stream); - return total_nread; + rv = total_nread; + + end: +#ifndef NDEBUG + stream->sm_dflags &= ~SMDF_READING_DATA_FRAMES; +#endif + return rv; } @@ -1668,7 +1930,10 @@ lsquic_stream_readf (struct lsquic_stream *stream, nread = stream_readf(stream, readf, ctx); if (nread >= 0) + { maybe_update_last_progress(stream); + maybe_finish_reset_at(stream); + } return nread; } @@ -1746,6 +2011,23 @@ lsquic_stream_ss_frame_sent (struct lsquic_stream *stream) static void handle_early_read_shutdown_ietf (struct lsquic_stream *stream) { + uint64_t ss_code; + + if (stream_is_locally_initiated_unidir(stream)) + { + LSQ_DEBUG("skip STOP_SENDING for locally initiated unidirectional " + "stream %"PRIu64, stream->id); + return; + } + + if (stream->sm_ss_code == HEC_NO_ERROR) + { + ss_code = stream->sm_ss_code; + if (stream->conn_pub->cp_get_ss_code + && 0 == stream->conn_pub->cp_get_ss_code(stream, &ss_code)) + stream->sm_ss_code = ss_code; + } + if (!(stream->sm_qflags & SMQF_SENDING_FLAGS)) TAILQ_INSERT_TAIL(&stream->conn_pub->sending_streams, stream, next_send_stream); @@ -1814,6 +2096,24 @@ stream_is_incoming_unidir (const struct lsquic_stream *stream) } +static int +stream_is_locally_initiated_unidir (const struct lsquic_stream *stream) +{ + enum stream_id_type sit; + + if (stream->sm_bflags & SMBF_IETF) + { + sit = stream->id & SIT_MASK; + if (stream->sm_bflags & SMBF_SERVER) + return sit == SIT_UNI_SERVER; + else + return sit == SIT_UNI_CLIENT; + } + else + return 0; +} + + static void stream_shutdown_write (lsquic_stream_t *stream) { @@ -1987,6 +2287,13 @@ lsquic_stream_received_goaway (lsquic_stream_t *stream) } +void +lsquic_stream_set_ss_code (lsquic_stream_t *stream, uint64_t error_code) +{ + stream->sm_ss_code = error_code; +} + + uint64_t lsquic_stream_read_offset (const lsquic_stream_t *stream) { @@ -2059,29 +2366,66 @@ stream_wantwrite (struct lsquic_stream *stream, int new_val) if (old_val != new_val) { if (new_val) + { maybe_put_onto_write_q(stream, SMQF_WANT_WRITE); + if (!(stream->stream_flags & STREAM_ONCLOSE_DONE) + && !lsquic_stream_is_write_reset(stream) + && 0 == lsquic_stream_write_avail(stream)) + maybe_mark_as_blocked(stream); + } else maybe_remove_from_write_q(stream, SMQF_WANT_WRITE); } return old_val; } +static int +stream_want_http_dg (struct lsquic_stream *stream, int is_want) +{ + const int old_val = !!(stream->sm_qflags & SMQF_WANT_HTTP_DG); + const int new_val = !!is_want; + + if (old_val != new_val) + { + if (new_val) + { + TAILQ_INSERT_TAIL(&stream->conn_pub->http_dg_streams, stream, + next_http_dg_stream); + stream->sm_qflags |= SMQF_WANT_HTTP_DG; + } + else + { + stream->sm_qflags &= ~SMQF_WANT_HTTP_DG; + TAILQ_REMOVE(&stream->conn_pub->http_dg_streams, stream, + next_http_dg_stream); + } + lsquic_conn_http_dg_streams_updated(stream->conn_pub->lconn); + } + + return old_val; +} + int lsquic_stream_wantread (lsquic_stream_t *stream, int is_want) { SM_HISTORY_APPEND(stream, SHE_WANTREAD_NO + !!is_want); - if (!(stream->stream_flags & STREAM_U_READ_DONE)) + + if (stream->stream_flags & STREAM_U_READ_DONE) { - if (is_want) - maybe_conn_to_tickable_if_readable(stream); - return stream_wantread(stream, is_want); + errno = EBADF; + return -1; } - else + + if (is_want && stream_is_locally_initiated_unidir(stream)) { - errno = EBADF; + errno = EINVAL; return -1; } + + if (is_want) + maybe_conn_to_tickable_if_readable(stream); + return stream_wantread(stream, is_want); } @@ -2115,6 +2459,284 @@ lsquic_stream_wantwrite (lsquic_stream_t *stream, int is_want) } +int +lsquic_stream_want_http_dg_write (lsquic_stream_t *stream, int is_want) +{ + size_t max_sz; + const struct lsquic_http_dg_if *dg_if; + + is_want = !!is_want; + + if (!is_want) + return stream_want_http_dg(stream, 0); + + dg_if = lsquic_conn_http_dg_get_if(stream->conn_pub, stream->id); + if (!((dg_if && dg_if->on_http_dg_write) + || (stream->stream_if && stream->stream_if->on_http_dg_write))) + { + errno = EINVAL; + return -1; + } + + if (!stream->conn_pub->enpub->enp_settings.es_http_datagrams + || !(stream->conn_pub->cp_flags & CP_HTTP_DATAGRAMS)) + { + errno = ENOTSUP; + return -1; + } + + if (stream->conn_pub->lconn->cn_if + && stream->conn_pub->lconn->cn_if->ci_get_max_datagram_size) + max_sz = stream->conn_pub->lconn->cn_if->ci_get_max_datagram_size( + stream->conn_pub->lconn); + else + max_sz = 0; + + if (0 == max_sz) + { + errno = ENOTSUP; + return -1; + } + + return stream_want_http_dg(stream, 1); +} + +int +lsquic_stream_set_http_dg_if (lsquic_stream_t *stream, + const struct lsquic_http_dg_if *dg_if) +{ + int rc; + + if (!stream) + { + errno = EINVAL; + return -1; + } + + if (dg_if && !dg_if->on_http_dg_read && !dg_if->on_http_dg_write) + { + errno = EINVAL; + return -1; + } + + if (!dg_if && (stream->sm_qflags & SMQF_WANT_HTTP_DG)) + (void) lsquic_stream_want_http_dg_write(stream, 0); + + rc = lsquic_conn_http_dg_set_if(stream->conn_pub, stream->id, dg_if); + return rc; +} + + +static struct capsule_parser * +stream_find_capsule_parser (struct lsquic_stream *stream, + uint64_t capsule_type, unsigned *idx) +{ + struct capsule_parsers *parsers; + unsigned i; + + parsers = stream->sm_capsule_parsers; + if (!parsers) + return NULL; + + for (i = 0; i < parsers->cps_count; ++i) + if (parsers->cps_elems[i].cp_type == capsule_type) + { + if (idx) + *idx = i; + return &parsers->cps_elems[i]; + } + + return NULL; +} + + +static lsquic_capsule_read_f +stream_find_capsule_cb (struct lsquic_stream *stream, uint64_t capsule_type) +{ + struct capsule_parser *parser; + + parser = stream_find_capsule_parser(stream, capsule_type, NULL); + return parser ? parser->cp_cb : NULL; +} + + +int +lsquic_stream_set_capsule_handler (lsquic_stream_t *stream, + uint64_t capsule_type, lsquic_capsule_read_f cb) +{ + struct capsule_parsers *parsers; + struct capsule_parsers *new_parsers; + struct capsule_parser *parser; + size_t alloc_sz; + unsigned idx, new_nalloc; + + if (!stream) + { + errno = EINVAL; + return -1; + } + + parsers = stream->sm_capsule_parsers; + parser = stream_find_capsule_parser(stream, capsule_type, &idx); + if (parser) + { + if (cb) + { + parser->cp_cb = cb; + return 0; + } + + --parsers->cps_count; + if (idx < parsers->cps_count) + parsers->cps_elems[idx] = parsers->cps_elems[parsers->cps_count]; + return 0; + } + + if (!cb) + return 0; + + if (!parsers || parsers->cps_count == parsers->cps_nalloc) + { + new_nalloc = parsers ? parsers->cps_nalloc * 2 : 2; + alloc_sz = sizeof(*parsers) + + new_nalloc * sizeof(parsers->cps_elems[0]); + new_parsers = realloc(parsers, alloc_sz); + if (!new_parsers) + { + errno = ENOMEM; + return -1; + } + if (!parsers) + new_parsers->cps_count = 0; + new_parsers->cps_nalloc = new_nalloc; + stream->sm_capsule_parsers = new_parsers; + parsers = new_parsers; + } + + parsers->cps_elems[parsers->cps_count].cp_type = capsule_type; + parsers->cps_elems[parsers->cps_count].cp_cb = cb; + ++parsers->cps_count; + return 0; +} + + +void +lsquic_stream_clear_capsule_handlers (lsquic_stream_t *stream) +{ + if (!stream) + return; + + free(stream->sm_capsule_parsers); + stream->sm_capsule_parsers = NULL; +} + + +static void +http_dg_capsule_on_read (lsquic_stream_t *stream, lsquic_stream_ctx_t *UNUSED_h); +static void +http_dg_capsule_on_write (lsquic_stream_t *stream, + lsquic_stream_ctx_t *UNUSED_h); + +int +lsquic_stream_set_http_dg_capsules (lsquic_stream_t *stream, int enable) +{ + if (!(stream->sm_bflags & SMBF_IETF) + || !(stream->sm_bflags & SMBF_USE_HEADERS)) + { + errno = ENOTSUP; + return -1; + } + + if (!stream->conn_pub->enpub->enp_settings.es_http_datagrams) + { + errno = ENOTSUP; + return -1; + } + + if (enable) + { + if (!stream->sm_http_dg) + { + stream->sm_http_dg = calloc(1, sizeof(*stream->sm_http_dg)); + if (!stream->sm_http_dg) + { + errno = ENOMEM; + return -1; + } + } + if (0 != lsquic_stream_set_capsule_handler(stream, 0x00, + http_dg_capsule_datagram_cb)) + return -1; + stream->sm_bflags |= SMBF_HTTP_DG_CAPSULES; + SM_HISTORY_APPEND(stream, SHE_HTTP_DG_CAPSULE_ON); + LSQ_DEBUG("HTTP DG capsule mode enabled on stream %"PRIu64, stream->id); + (void) lsquic_stream_wantread(stream, 1); + free(stream->sm_http_dg->read.buf); + stream->sm_http_dg->read.buf = NULL; + stream->sm_http_dg->read.state = HDC_READ_TYPE; + stream->sm_http_dg->read.type_state.pos = 0; + stream->sm_http_dg->read.len_state.pos = 0; + } + else + { + stream->sm_bflags &= ~SMBF_HTTP_DG_CAPSULES; + SM_HISTORY_APPEND(stream, SHE_HTTP_DG_CAPSULE_OFF); + LSQ_DEBUG("HTTP DG capsule mode disabled on stream %"PRIu64, stream->id); + (void) lsquic_stream_wantread(stream, 0); + (void) lsquic_stream_wantwrite(stream, 0); + (void) lsquic_stream_set_capsule_handler(stream, 0x00, NULL); + if (stream->sm_http_dg) + { + free(stream->sm_http_dg->read.buf); + stream->sm_http_dg->read.buf = NULL; + stream->sm_http_dg->read.buf_sz = 0; + stream->sm_http_dg->read.state = HDC_READ_TYPE; + free(stream->sm_http_dg->write.buf); + stream->sm_http_dg->write.buf = NULL; + stream->sm_http_dg->write.buf_sz = 0; + stream->sm_http_dg->write.offset = 0; + stream->sm_http_dg->write.header_len = 0; + } + } + + return 0; +} + + +size_t +lsquic_stream_get_max_http_dg_size (lsquic_stream_t *stream) +{ + size_t max_sz; + uint64_t qsid; + size_t overhead; + + if (!(stream->conn_pub->cp_flags & CP_HTTP_DATAGRAMS)) + return 0; + + if (stream->conn_pub->lconn->cn_if + && stream->conn_pub->lconn->cn_if->ci_get_max_datagram_size) + max_sz = stream->conn_pub->lconn->cn_if->ci_get_max_datagram_size( + stream->conn_pub->lconn); + else + max_sz = 0; + + if (0 == max_sz) + return 0; + + if (stream->id & 3) + return 0; + + qsid = stream->id / 4; + if (qsid > VINT_MAX_VALUE) + return 0; + overhead = vint_size(qsid); + if (max_sz <= overhead) + return 0; + return max_sz - overhead; +} + + + struct progress { enum stream_flags s_flags; @@ -2142,7 +2764,6 @@ progress_eq (struct progress a, struct progress b) return a.s_flags == b.s_flags && a.q_flags == b.q_flags && a.uh == b.uh; } - static void stream_dispatch_read_events_loop (lsquic_stream_t *stream) { @@ -2159,7 +2780,10 @@ stream_dispatch_read_events_loop (lsquic_stream_t *stream) progress = stream_progress(stream); size = stream->read_offset; - stream->stream_if->on_read(stream, stream->st_ctx); + if (stream->sm_bflags & SMBF_HTTP_DG_CAPSULES) + http_dg_capsule_on_read(stream, stream->st_ctx); + else + stream->stream_if->on_read(stream, stream->st_ctx); if (no_progress_limit && size == stream->read_offset && progress_eq(progress, stream_progress(stream))) @@ -2224,6 +2848,9 @@ on_write_header_wrapper (struct lsquic_stream *stream, lsquic_stream_ctx_t *h) stream->sm_hblock_sz = 0; stream_hblock_sent(stream); LSQ_DEBUG("header block written out successfully"); + if (0 != lsquic_stream_flush(stream)) + LSQ_DEBUG("cannot flush completed header block: %s", + strerror(errno)); /* TODO: if there was eos, do something else */ if (stream->sm_qflags & SMQF_WANT_WRITE) stream->stream_if->on_write(stream, h); @@ -2245,6 +2872,9 @@ static void (*select_on_write (struct lsquic_stream *stream))(struct lsquic_stream *, lsquic_stream_ctx_t *) { + if (stream->sm_http_dg + && stream->sm_http_dg->write.header_len) + return http_dg_capsule_on_write; if (SSHS_HBLOCK_SENDING != stream->sm_send_headers_state) /* Common case */ return stream->stream_if->on_write; @@ -2252,7 +2882,6 @@ static void return on_write_header_wrapper; } - static void stream_dispatch_write_events_loop (lsquic_stream_t *stream) { @@ -2297,7 +2926,10 @@ stream_dispatch_read_events_once (lsquic_stream_t *stream) { if ((stream->sm_qflags & SMQF_WANT_READ) && lsquic_stream_readable(stream)) { - stream->stream_if->on_read(stream, stream->st_ctx); + if (stream->sm_bflags & SMBF_HTTP_DG_CAPSULES) + http_dg_capsule_on_read(stream, stream->st_ctx); + else + stream->stream_if->on_read(stream, stream->st_ctx); } } @@ -2321,7 +2953,10 @@ maybe_mark_as_blocked (lsquic_stream_t *stream) used = lsquic_stream_combined_send_off(stream); if (stream->max_send_off == used) { - if (stream->blocked_off < stream->max_send_off) + if (stream->blocked_off < stream->max_send_off + || (0 == used + && !(stream->sm_qflags & SMQF_SEND_BLOCKED) + && !(stream->stream_flags & STREAM_BLOCKED_SENT))) { stream->blocked_off = used; if (!(stream->sm_qflags & SMQF_SENDING_FLAGS)) @@ -3178,7 +3813,7 @@ stream_write_to_packet_std (struct frame_gen_ctx *fg_ctx, const size_t size) unsigned stream_header_sz, need_at_least; struct lsquic_packet_out *packet_out; struct lsquic_stream *headers_stream; - int len; + int buffered_packet_ok, len; if ((stream->stream_flags & (STREAM_HEADERS_SENT|STREAM_HDRS_FLUSHED)) == STREAM_HEADERS_SENT) @@ -3215,8 +3850,11 @@ stream_write_to_packet_std (struct frame_gen_ctx *fg_ctx, const size_t size) else need_at_least += size > 0; get_packet: + buffered_packet_ok = !(stream->sm_reset_stream_at_sz != 0 + && stream->tosend_off < stream->sm_reset_stream_at_sz); packet_out = stream->sm_get_packet_for_stream(send_ctl, - need_at_least, stream->conn_pub->path, stream); + need_at_least, stream->conn_pub->path, stream, + buffered_packet_ok); if (packet_out) { len = write_stream_frame(fg_ctx, size, packet_out); @@ -3648,6 +4286,11 @@ stream_write (lsquic_stream_t *stream, struct lsquic_reader *reader, ssize_t nw; len = reader->lsqr_size(reader->lsqr_ctx); +#ifdef SSIZE_MAX + len = MIN(len, (size_t) SSIZE_MAX); +#else + len = MIN(len, (size_t) PTRDIFF_MAX); +#endif if (len == 0) return 0; @@ -3685,7 +4328,7 @@ stream_write (lsquic_stream_t *stream, struct lsquic_reader *reader, { lsquic_sendctl_gen_stream_blocked_frame(stream->conn_pub->send_ctl, stream); } - return nwritten; + return (ssize_t) nwritten; } @@ -4274,6 +4917,12 @@ stream_reset (struct lsquic_stream *stream, uint64_t error_code, int do_close) LSQ_INFO("reset, error code %"PRIu64, error_code); stream->error_code = error_code; + if (stream->tosend_off < stream->sm_reset_stream_at_sz + && !(stream->stream_flags & STREAM_U_WRITE_DONE)) + { + (void) stream_flush_nocheck(stream); + } + if (!(stream->sm_qflags & SMQF_SENDING_FLAGS)) TAILQ_INSERT_TAIL(&stream->conn_pub->sending_streams, stream, next_send_stream); @@ -4313,38 +4962,100 @@ lsquic_stream_conn (const lsquic_stream_t *stream) return stream->conn_pub->lconn; } +void +lsquic_stream_mark_session_stream (struct lsquic_stream *stream) +{ + stream->sm_bflags |= SMBF_SESSION_STREAM; +} + + +int +lsquic_stream_is_session_stream (const struct lsquic_stream *stream) +{ + return !!(stream->sm_bflags & SMBF_SESSION_STREAM); +} + -#if LSQUIC_WEBTRANSPORT_SERVER_SUPPORT void -lsquic_stream_set_webtransport_session(lsquic_stream_t *s) { - s->stream_flags |= SMBF_WEBTRANSPORT_SESSION_STREAM; +lsquic_stream_mark_switch_client_bidi (struct lsquic_stream *stream, + lsquic_stream_id_t stream_id) +{ + stream->sm_switch_stream_id = stream_id; + stream->sm_bflags |= SMBF_SWITCH_CLIENT_BIDI_STREAM; } int -lsquic_stream_is_webtransport_session(const lsquic_stream_t *s) { - return (s->stream_flags & SMBF_WEBTRANSPORT_SESSION_STREAM); +lsquic_stream_is_switch_client_bidi (const struct lsquic_stream *stream) +{ + return !!(stream->sm_bflags & SMBF_SWITCH_CLIENT_BIDI_STREAM); } int -lsquic_stream_is_webtransport_client_bidi_stream(const lsquic_stream_t *s) { - return (s->stream_flags & SMBF_WEBTRANSPORT_CLIENT_BIDI_STREAM); +lsquic_stream_get_switch_stream_id (const struct lsquic_stream *stream, + lsquic_stream_id_t *stream_id) +{ + if (!(stream->sm_bflags & SMBF_SWITCH_CLIENT_BIDI_STREAM) + || !stream_id) + return -1; + + *stream_id = stream->sm_switch_stream_id; + return 0; } int -lsquic_stream_get_webtransport_session_stream_id(const lsquic_stream_t *s) { - if(s->stream_flags & SMBF_WEBTRANSPORT_CLIENT_BIDI_STREAM) - { - return s->webtransport_session_stream_id; - } +lsquic_stream_onclose_done (const struct lsquic_stream *stream) +{ + return !!(stream->stream_flags & STREAM_ONCLOSE_DONE); +} - return -1; + +void +lsquic_stream_mark_rejected (struct lsquic_stream *stream) +{ + stream->stream_flags |= STREAM_SS_RECVD; } -#endif +void +lsquic_stream_set_reset_stream_at_size (struct lsquic_stream *s, uint8_t sz) +{ + if (!s || !sz || !(s->sm_bflags & SMBF_IETF) + || !s->conn_pub->enpub->enp_settings.es_reset_stream_at) + return; + + s->sm_reset_stream_at_sz = sz; + s->sm_bflags |= SMBF_DELAY_ONCLOSE; +} + + +int +lsquic_stream_set_reliable_size (struct lsquic_stream *s, size_t sz) +{ + const struct transport_params *params; + + if (sz > ((1ULL << (sizeof(s->sm_reset_stream_at_sz) * 8)) - 1)) + return -1; + if (!(s->sm_bflags & SMBF_IETF)) + return -1; + if (!s->conn_pub->enpub->enp_settings.es_reset_stream_at) + return -1; + if (s->tosend_off < sz) + return -1; + if (!s->conn_pub->lconn->cn_esf.i + || !s->conn_pub->lconn->cn_esf.i->esfi_get_peer_transport_params) + return -1; + + params = s->conn_pub->lconn->cn_esf.i->esfi_get_peer_transport_params( + s->conn_pub->lconn->cn_enc_session); + if (!params || !(params->tp_set & (1 << TPI_RESET_STREAM_AT))) + return -1; + + lsquic_stream_set_reset_stream_at_size(s, (uint8_t) sz); + return 0; +} int lsquic_stream_close (lsquic_stream_t *stream) @@ -4380,10 +5091,12 @@ lsquic_stream_acked (struct lsquic_stream *stream, { --stream->n_unacked; LSQ_DEBUG("ACKed; n_unacked: %u", stream->n_unacked); - if (frame_type == QUIC_FRAME_RST_STREAM) + if (frame_type == QUIC_FRAME_RST_STREAM + || frame_type == QUIC_FRAME_RESET_STREAM_AT) { SM_HISTORY_APPEND(stream, SHE_RST_ACKED); - LSQ_DEBUG("RESET that we sent has been acked by peer"); + LSQ_DEBUG("%s that we sent has been acked by peer", + frame_type_2_str[frame_type]); stream->stream_flags |= STREAM_RST_ACKED; } } @@ -4842,23 +5555,23 @@ hq_read (void *ctx, const unsigned char *buf, size_t sz, int fin) break; filter->hqfi_flags |= HQFI_FLAG_BEGIN; filter->hqfi_state = HQFI_STATE_READING_PAYLOAD; -#if LSQUIC_WEBTRANSPORT_SERVER_SUPPORT - if(stream->conn_pub->enpub->enp_settings.es_webtransport_server) { - // 4.2. Bidirectional Streams, https://datatracker.ietf.org/doc/draft-ietf-webtrans-http3/05/ -#define WEBTRANSPORT_BIDI_STREAM_TYPE (0x41) - if(filter->hqfi_type == WEBTRANSPORT_BIDI_STREAM_TYPE) { - // check webtransport_session_stream_id availability as well SMBF_WEBTRANSPORT_SESSION_STREAM - // flag for webtransport_session_stream_id stream in app code - stream->webtransport_session_stream_id = filter->hqfi_webtransport_session_id; - stream->stream_flags |= SMBF_WEBTRANSPORT_CLIENT_BIDI_STREAM; - // disable header processing as we will not have any headers for this stream anymore - stream->sm_bflags &= ~SMBF_USE_HEADERS; - filter->hqfi_type = HQFT_DATA; - filter->hqfi_left = UINT64_MAX; // set data size infinite to keep processing till stream reset - goto end; - } + if (stream->conn_pub->cp_is_hq_switch_frame + && stream->conn_pub->cp_is_hq_switch_frame(stream, + filter->hqfi_type, filter->hqfi_switch_stream_id)) + { + lsquic_stream_mark_switch_client_bidi(stream, + filter->hqfi_switch_stream_id); + lsquic_stream_set_reset_stream_at_size(stream, (uint8_t) ( + vint_size(filter->hqfi_type) + + vint_size(filter->hqfi_switch_stream_id))); + stream->stream_flags |= STREAM_SWITCH_PENDING; + /* Switch from framed parsing to raw DATA for this stream. */ + stream->sm_bflags &= ~SMBF_USE_HEADERS; + stream->sm_write_avail = stream_write_avail_no_frames; + filter->hqfi_type = HQFT_DATA; + filter->hqfi_left = UINT64_MAX; + goto end; } -#endif LSQ_DEBUG("HQ frame type 0x%"PRIX64" at offset %"PRIu64", size %"PRIu64, filter->hqfi_type, stream->read_offset + (unsigned) (p - buf), filter->hqfi_left); @@ -5038,6 +5751,14 @@ hq_filter_readable (struct lsquic_stream *stream) } } + if (stream->stream_flags & STREAM_SWITCH_PENDING) + { + stream->stream_flags &= ~STREAM_SWITCH_PENDING; + if (stream->conn_pub->cp_on_hq_switch_stream) + stream->conn_pub->cp_on_hq_switch_stream(stream, + stream->sm_switch_stream_id); + } + return hq_filter_readable_now(stream); } @@ -5278,3 +5999,459 @@ lsquic_stream_has_unacked_data (struct lsquic_stream *stream) { return stream->n_unacked > 0 || stream->sm_n_buffered > 0; } + +/* RFC 9297, Section 3.2: Datagram Capsule type is 0x00. */ +#define H3_CAPSULE_DATAGRAM 0x00 + +static void +stream_reset_http_dg_capsule (struct lsquic_stream *stream) +{ + struct http_dg_capsule_read_state *rd; + + if (!stream->sm_http_dg) + return; + + rd = &stream->sm_http_dg->read; + rd->state = HDC_READ_TYPE; + rd->type_state.pos = 0; + rd->len_state.pos = 0; + rd->type = 0; + rd->length = 0; + rd->offset = 0; + rd->buf = NULL; + rd->buf_sz = 0; + rd->cb = NULL; +} + +static void +stream_abort_http_dg_capsule (struct lsquic_stream *stream, const char *reason) +{ + struct http_dg_capsule_read_state *rd; + struct lsquic_conn *lconn = stream->conn_pub->lconn; + + if (stream->sm_http_dg) + { + rd = &stream->sm_http_dg->read; + free(rd->buf); + rd->buf = NULL; + } + lconn->cn_if->ci_abort_error(lconn, 1, HEC_DATAGRAM_ERROR, + "%s on stream %"PRIu64, reason, stream->id); +} + + +static void +(*select_on_dg_read (struct lsquic_stream *stream)) + (lsquic_stream_t *stream, lsquic_stream_ctx_t *h, const void *buf, + size_t buf_sz) +{ + const struct lsquic_http_dg_if *dg_if; + + dg_if = lsquic_conn_http_dg_get_if(stream->conn_pub, stream->id); + if (dg_if && dg_if->on_http_dg_read) + return dg_if->on_http_dg_read; + else if (stream->stream_if && stream->stream_if->on_http_dg_read) + return stream->stream_if->on_http_dg_read; + else + return NULL; +} + + +static void +http_dg_capsule_datagram_cb (lsquic_stream_t *stream, lsquic_stream_ctx_t *h, + uint64_t UNUSED_capsule_type, const void *payload, size_t payload_len) +{ + void (*on_dg_read)(lsquic_stream_t *stream, lsquic_stream_ctx_t *h, + const void *buf, size_t buf_sz); + + on_dg_read = select_on_dg_read(stream); + SM_HISTORY_APPEND(stream, SHE_HTTP_DG_RECV); + LSQ_DEBUG("HTTP DG capsule received on stream %"PRIu64" size %zu", + stream->id, payload_len); + if (on_dg_read) + on_dg_read(stream, h, payload, payload_len); + else + LSQ_DEBUG("no callback for HTTP datagram on stream " + "%"PRIu64"; drop on floor", stream->id); +} + + +#ifndef LSQUIC_TEST +static +#endif +size_t +lsquic_http_dg_capsule_readf (void *ctx, const unsigned char *buf, size_t len, int fin) +{ + struct lsquic_stream *stream = ctx; + struct http_dg_capsule_read_state *rd; + const unsigned char *p = buf; + const unsigned char *end = buf + len; + struct varint_read_state *vrs; + size_t avail, to_copy; + int s; + + if (!stream->sm_http_dg) + return 0; + + rd = &stream->sm_http_dg->read; + while (p < end) + { + switch (rd->state) + { + case HDC_READ_TYPE: + /* RFC 9297, Section 3: Capsule fields are varint-encoded. */ + vrs = &rd->type_state; + s = lsquic_varint_read_nb(&p, end, vrs); + if (s < 0) + goto end; + rd->type = vrs->val; + rd->state = HDC_READ_LENGTH; + rd->len_state.pos = 0; + break; + case HDC_READ_LENGTH: + /* RFC 9297, Section 3: Capsule length is varint-encoded. */ + vrs = &rd->len_state; + s = lsquic_varint_read_nb(&p, end, vrs); + if (s < 0) + goto end; + rd->length = vrs->val; + rd->offset = 0; + if (rd->length > SIZE_MAX) + { + stream_abort_http_dg_capsule(stream, + "capsule length too large"); + goto end; + } + rd->cb = stream_find_capsule_cb(stream, rd->type); + if (rd->cb) + { + unsigned max_sz; + + max_sz = stream->conn_pub->enpub->enp_settings + .es_http_dg_max_capsule_read_size; + if (rd->length > max_sz) + { + stream_abort_http_dg_capsule(stream, + "capsule length exceeds configured maximum"); + goto end; + } + rd->buf_sz = (size_t) rd->length; + if (rd->buf_sz) + { + rd->buf = malloc(rd->buf_sz); + if (!rd->buf) + { + stream_abort_http_dg_capsule(stream, + "cannot allocate capsule buffer"); + goto end; + } + } + } + else + LSQ_DEBUG("no callback for capsule type 0x%"PRIX64 + " on stream %"PRIu64"; drop on floor", + rd->type, stream->id); + rd->state = HDC_READ_PAYLOAD; + break; + case HDC_READ_PAYLOAD: + avail = (size_t) (end - p); + to_copy = rd->length - rd->offset; + if (to_copy > avail) + to_copy = avail; + if (rd->cb) + { + if (rd->buf_sz) + memcpy(rd->buf + rd->offset, p, to_copy); + } + rd->offset += to_copy; + p += to_copy; + if (rd->offset == rd->length) + { + if (rd->cb) + rd->cb(stream, stream->st_ctx, rd->type, + rd->buf, rd->buf_sz); + free(rd->buf); + rd->buf = NULL; + stream_reset_http_dg_capsule(stream); + } + break; + } + } + + end: + if (fin && (rd->state != HDC_READ_TYPE || rd->type_state.pos != 0)) + stream_abort_http_dg_capsule(stream, "truncated capsule"); + return len; +} + + +#if LSQUIC_TEST +static unsigned s_stream_test_http_dg_capsule_error; + + +static void +stream_test_abort_error (struct lsquic_conn *UNUSED_conn, int UNUSED_is_app, + unsigned error_code, const char *UNUSED_format, ...) +{ + s_stream_test_http_dg_capsule_error = error_code; +} + + +int +lsquic_stream_test_truncated_capsule_type_fin_aborts (unsigned *error_code) +{ + static const unsigned char truncated_type[] = { 0x40, }; + static const struct conn_iface conn_iface = { + .ci_abort_error = stream_test_abort_error, + }; + struct lsquic_conn conn = LSCONN_INITIALIZER_CIDLEN(conn, 0); + struct lsquic_conn_public conn_pub; + struct lsquic_stream stream; + struct http_dg_stream_state http_dg; + + memset(&conn_pub, 0, sizeof(conn_pub)); + memset(&stream, 0, sizeof(stream)); + memset(&http_dg, 0, sizeof(http_dg)); + conn.cn_if = &conn_iface; + conn_pub.lconn = &conn; + stream.id = 0; + stream.conn_pub = &conn_pub; + stream.sm_http_dg = &http_dg; + s_stream_test_http_dg_capsule_error = 0; + + (void) lsquic_http_dg_capsule_readf(&stream, truncated_type, + sizeof(truncated_type), 1); + + if (error_code) + *error_code = s_stream_test_http_dg_capsule_error; + return 0; +} +#endif + +static void +http_dg_capsule_on_read (lsquic_stream_t *stream, lsquic_stream_ctx_t *UNUSED_h) +{ + ssize_t nread; + + nread = lsquic_stream_readf(stream, lsquic_http_dg_capsule_readf, stream); + if (nread == 0) + { + if (stream->stream_if && stream->stream_if->on_read) + stream->stream_if->on_read(stream, stream->st_ctx); + (void) lsquic_stream_wantread(stream, 0); + } + else if (nread < 0 && errno != EWOULDBLOCK) + (void) lsquic_stream_wantread(stream, 0); +} + + +int +lsquic_stream_http_dg_capsule_pending (const struct lsquic_stream *stream) +{ + return stream->sm_http_dg + && stream->sm_http_dg->write.header_len != 0; +} + +static void +http_dg_capsule_clear_pending (struct lsquic_stream *stream) +{ + struct http_dg_capsule_write_state *wr; + + if (!stream->sm_http_dg) + return; + + wr = &stream->sm_http_dg->write; + free(wr->buf); + wr->buf = NULL; + wr->buf_sz = 0; + wr->offset = 0; + wr->header_len = 0; +} + +static int +http_dg_capsule_try_write (struct lsquic_stream *stream) +{ + struct http_dg_capsule_write_state *wr; + struct iovec iov[2]; + size_t total, off, header_len, payload_off; + int iovcnt; + ssize_t nw; + + if (!stream->sm_http_dg) + { + errno = EINVAL; + return -1; + } + + wr = &stream->sm_http_dg->write; + header_len = wr->header_len; + if (header_len == 0) + return 1; + + total = header_len + wr->buf_sz; + off = wr->offset; + if (off >= total) + return 1; + + if (off < header_len) + { + iov[0].iov_base = wr->header + off; + iov[0].iov_len = header_len - off; + iov[1].iov_base = wr->buf; + iov[1].iov_len = wr->buf_sz; + iovcnt = 2; + } + else + { + payload_off = off - header_len; + iov[0].iov_base = wr->buf + payload_off; + iov[0].iov_len = wr->buf_sz - payload_off; + iovcnt = 1; + } + + nw = lsquic_stream_writev(stream, iov, iovcnt); + if (nw < 0) + return -1; + if (nw == 0) + return 0; + + wr->offset += (size_t) nw; + if (wr->offset >= total) + return 1; + + return 0; +} + +int +lsquic_stream_http_dg_queue_capsule (struct lsquic_stream *stream, + const void *buf, size_t sz) +{ + struct http_dg_capsule_write_state *wr; + unsigned char header[16]; + unsigned max_sz; + uint64_t bits; + size_t header_len; + if (!stream->sm_http_dg) + { + stream->sm_http_dg = calloc(1, sizeof(*stream->sm_http_dg)); + if (!stream->sm_http_dg) + { + errno = ENOMEM; + return -1; + } + } + + if (lsquic_stream_http_dg_capsule_pending(stream)) + { + errno = EAGAIN; + return -1; + } + + max_sz = stream->conn_pub->enpub->enp_settings + .es_http_dg_max_capsule_write_size; + if (max_sz == 0 || sz > max_sz) + { + errno = EMSGSIZE; + return -1; + } + + if (sz > VINT_MAX_VALUE) + { + errno = EINVAL; + return -1; + } + + /* RFC 9297, Section 3.2: Capsule type and length are varint-encoded. */ + bits = vint_val2bits(H3_CAPSULE_DATAGRAM); + header_len = 1u << bits; + vint_write(header, H3_CAPSULE_DATAGRAM, bits, header_len); + bits = vint_val2bits(sz); + vint_write(header + header_len, sz, bits, 1u << bits); + header_len += 1u << bits; + + wr = &stream->sm_http_dg->write; + wr->buf = NULL; + if (sz) + { + /* Copy payload; possible optimization via SMBF_RW_ONCE remains. */ + wr->buf = malloc(sz); + if (!wr->buf) + { + errno = ENOMEM; + return -1; + } + memcpy(wr->buf, buf, sz); + } + wr->buf_sz = sz; + memcpy(wr->header, header, header_len); + wr->header_len = header_len; + wr->offset = 0; + LSQ_DEBUG("HTTP DG capsule queued on stream %"PRIu64" size %zu", + stream->id, sz); + if (lsquic_stream_wantwrite(stream, 1) != 0) + { + http_dg_capsule_clear_pending(stream); + return -1; + } + return 0; +} + +static void +http_dg_capsule_on_write (lsquic_stream_t *stream, lsquic_stream_ctx_t *UNUSED_h) +{ + int rc; + + rc = http_dg_capsule_try_write(stream); + if (rc < 0) + return; + if (rc == 0) + return; + + http_dg_capsule_clear_pending(stream); + SM_HISTORY_APPEND(stream, SHE_HTTP_DG_SEND); + LSQ_DEBUG("HTTP DG capsule flushed on stream %"PRIu64, stream->id); + (void) lsquic_stream_wantwrite(stream, 0); + (void) lsquic_stream_flush(stream); +} + + +struct lsquic_conn_public * +lsquic_stream_get_conn_public (const struct lsquic_stream *stream) +{ + return stream->conn_pub; +} + + +void * +lsquic_stream_get_attachment (const struct lsquic_stream *stream) +{ + return stream->sm_attachment; +} + + +void +lsquic_stream_set_attachment (struct lsquic_stream *stream, void *attachment) +{ + stream->sm_attachment = attachment; +} + + +int +lsquic_stream_is_server (const struct lsquic_stream *stream) +{ + return (stream->sm_bflags & SMBF_SERVER) != 0; +} + + +int +lsquic_stream_headers_state_is_begin (const struct lsquic_stream *stream) +{ + return stream->sm_send_headers_state == SSHS_BEGIN; +} + + +const struct lsquic_stream_if * +lsquic_stream_get_stream_if (const struct lsquic_stream *stream) +{ + return stream->stream_if; +} diff --git a/src/liblsquic/lsquic_stream.h b/src/liblsquic/lsquic_stream.h index 1f2fe6a49..13033afd9 100644 --- a/src/liblsquic/lsquic_stream.h +++ b/src/liblsquic/lsquic_stream.h @@ -19,9 +19,14 @@ union hblock_ctx; struct lsquic_packet_out; struct lsquic_send_ctl; struct network_path; +struct capsule_parsers; TAILQ_HEAD(lsquic_streams_tailq, lsquic_stream); +typedef void (*lsquic_capsule_read_f)(lsquic_stream_t *stream, + lsquic_stream_ctx_t *h, uint64_t capsule_type, + const void *payload, size_t payload_len); + #ifndef LSQUIC_KEEP_STREAM_HISTORY # define LSQUIC_KEEP_STREAM_HISTORY 1 @@ -100,9 +105,7 @@ struct hq_filter struct varint_read2_state hqfi_vint2_state; /* No need to copy the values: use it directly */ #define hqfi_left hqfi_vint2_state.vr2s_two -#if LSQUIC_WEBTRANSPORT_SERVER_SUPPORT -#define hqfi_webtransport_session_id hqfi_vint2_state.vr2s_two -#endif +#define hqfi_switch_stream_id hqfi_vint2_state.vr2s_two #define hqfi_type hqfi_vint2_state.vr2s_one struct varint_read_state hqfi_vint1_state; enum { @@ -123,6 +126,38 @@ struct hq_filter } hqfi_state:8; }; +struct http_dg_capsule_read_state +{ + enum { + HDC_READ_TYPE = 0, + HDC_READ_LENGTH, + HDC_READ_PAYLOAD, + } state; + struct varint_read_state type_state; + struct varint_read_state len_state; + uint64_t type; + uint64_t length; + uint64_t offset; + unsigned char *buf; + size_t buf_sz; + lsquic_capsule_read_f cb; +}; + +struct http_dg_capsule_write_state +{ + unsigned char *buf; + size_t buf_sz; + size_t offset; + unsigned char header[16]; + size_t header_len; +}; + +struct http_dg_stream_state +{ + struct http_dg_capsule_read_state read; + struct http_dg_capsule_write_state write; +}; + struct stream_filter_if { @@ -173,6 +208,7 @@ enum stream_q_flags /* The stream can reference itself, preventing its own destruction: */ #define SMQF_SELF_FLAGS SMQF_WAIT_FIN_OFF SMQF_WAIT_FIN_OFF = 1 << 11, /* Waiting for final offset: FIN or RST */ + SMQF_WANT_HTTP_DG = 1 << 12, /* Want to send HTTP Datagrams */ }; @@ -193,13 +229,11 @@ enum stream_b_flags SMBF_INCREMENTAL = 1 <<11, /* Value of the "incremental" HTTP Priority parameter */ SMBF_HPRIO_SET = 1 <<12, /* Extensible HTTP Priorities have been set once */ SMBF_DELAY_ONCLOSE= 1 <<13, /* Delay calling on_close() until peer ACKs everything */ -#if LSQUIC_WEBTRANSPORT_SERVER_SUPPORT - SMBF_WEBTRANSPORT_SESSION_STREAM = 1 <<14, /* WEBTRANSPORT session stream */ - SMBF_WEBTRANSPORT_CLIENT_BIDI_STREAM = 1 <<15, /* WEBTRANSPORT client initiated bidi stream */ -#define N_SMBF_FLAGS 16 -#else -#define N_SMBF_FLAGS 14 -#endif + SMBF_HTTP_DG_CAPSULES = 1 <<14, /* HTTP Datagram capsules are enabled */ + SMBF_SESSION_STREAM = 1 <<15, /* Extension session stream */ + SMBF_SWITCH_CLIENT_BIDI_STREAM + = 1 <<16, /* Extension switched client bidi stream */ +#define N_SMBF_FLAGS 17 }; @@ -209,6 +243,8 @@ enum stream_d_flags { SMDF_ONRESET0 = 1 << 0, /* Called on_reset(0) */ SMDF_ONRESET1 = 1 << 1, /* Called on_reset(1) */ + SMDF_READING_DATA_FRAMES + = 1 << 2, /* Internal: detect nested read_data_frames() */ }; @@ -239,10 +275,12 @@ enum stream_flags { STREAM_BLOCKED_SENT = 1 << 23, /* Stays set once a STREAM_BLOCKED frame is sent */ STREAM_RST_READ = 1 << 24, /* User code collected the error */ STREAM_DATA_RECVD = 1 << 25, /* Cache stream state calculation */ - STREAM_UNUSED26 = 1 << 26, /* Unused */ + STREAM_RESET_AT_RECVD = 1 << 26, /* Received RESET_STREAM_AT frame */ STREAM_HDRS_FLUSHED = 1 << 27, /* Only used in buffered packets mode */ STREAM_SS_RECVD = 1 << 28, /* Received STOP_SENDING frame */ STREAM_DELAYED_SW = 1 << 29, /* Delayed shutdown_write call */ + STREAM_SWITCH_PENDING + = 1 << 30, /* Defer stream-if switch out of parser */ }; @@ -264,13 +302,16 @@ struct lsquic_stream struct lsquic_conn_public *conn_pub; TAILQ_ENTRY(lsquic_stream) next_send_stream, next_read_stream, next_write_stream, next_service_stream, - next_prio_stream; + next_prio_stream, next_http_dg_stream; uint64_t tosend_off; uint64_t sm_payload; /* Not counting HQ frames */ uint64_t max_send_off; uint64_t sm_last_recv_off; uint64_t error_code; + uint64_t sm_ss_code; + uint64_t sm_rst_in_code; + uint64_t sm_ss_in_code; /* From the network, we get frames, which we keep on a list ordered * by offset. @@ -295,6 +336,10 @@ struct lsquic_stream /* We can safely use sm_hq_filter */ #define sm_uni_type_state sm_hq_filter.hqfi_vint2_state.vr2s_varint_state + struct http_dg_stream_state *sm_http_dg; + struct capsule_parsers *sm_capsule_parsers; + void *sm_http_dg_consume_ctx; + /** If @ref SMQF_WANT_FLUSH is set, flush until this offset. */ uint64_t sm_flush_to; @@ -320,6 +365,10 @@ struct lsquic_stream /* Valid if STREAM_FIN_RECVD is set: */ uint64_t sm_fin_off; + /* Valid if STREAM_RESET_AT_RECVD is set: */ + uint64_t sm_reset_at; + uint64_t sm_reset_at_final; + uint64_t sm_reset_at_error; /* A stream may be generating STREAM or CRYPTO frames */ size_t (*sm_frame_header_sz)( @@ -332,7 +381,8 @@ struct lsquic_stream struct lsquic_packet_out * (*sm_get_packet_for_stream)( struct lsquic_send_ctl *, unsigned, const struct network_path *, - const struct lsquic_stream *); + const struct lsquic_stream *, + int buffered_packet_ok); /* This element is optional */ const struct stream_filter_if *sm_sfi; @@ -368,9 +418,10 @@ struct lsquic_stream enum stream_d_flags sm_dflags:8; signed char sm_saved_want_write; signed char sm_has_frame; -#if LSQUIC_WEBTRANSPORT_SERVER_SUPPORT - lsquic_stream_id_t webtransport_session_stream_id; -#endif + lsquic_stream_id_t sm_switch_stream_id; + void *sm_attachment; + /* When non-zero, send RESET_STREAM_AT with this reliable size. */ + uint8_t sm_reset_stream_at_sz; #if LSQUIC_KEEP_STREAM_HISTORY sm_hist_idx_t sm_hist_idx; #endif @@ -381,6 +432,30 @@ struct lsquic_stream #endif }; +#if LSQUIC_KEEP_STREAM_HISTORY +void +lsquic_stream_hist_http_dg_recv (lsquic_stream_t *stream); +void +lsquic_stream_hist_http_dg_send (lsquic_stream_t *stream); +#else +#define lsquic_stream_hist_http_dg_recv(stream) do { } while (0) +#define lsquic_stream_hist_http_dg_send(stream) do { } while (0) +#endif + +int +lsquic_stream_http_dg_capsule_pending (const struct lsquic_stream *stream); + +int +lsquic_stream_http_dg_queue_capsule (struct lsquic_stream *stream, + const void *buf, size_t sz); + +int +lsquic_stream_set_capsule_handler (lsquic_stream_t *stream, + uint64_t capsule_type, lsquic_capsule_read_f cb); + +void +lsquic_stream_clear_capsule_handlers (lsquic_stream_t *stream); + enum stream_ctor_flags { @@ -453,12 +528,56 @@ lsquic_stream_frame_in (lsquic_stream_t *, struct stream_frame *frame); int lsquic_stream_uh_in (lsquic_stream_t *, struct uncompressed_headers *); +void +lsquic_stream_push_req (lsquic_stream_t *, + struct uncompressed_headers *push_req); + +void +lsquic_stream_set_reset_stream_at_size (lsquic_stream_t *s, uint8_t sz); + +int +lsquic_stream_set_reliable_size (lsquic_stream_t *s, size_t sz); + +void +lsquic_stream_mark_session_stream (struct lsquic_stream *stream); + +int +lsquic_stream_is_session_stream (const struct lsquic_stream *stream); + +void +lsquic_stream_mark_switch_client_bidi (struct lsquic_stream *stream, + lsquic_stream_id_t stream_id); + +int +lsquic_stream_is_switch_client_bidi (const struct lsquic_stream *stream); + +int +lsquic_stream_get_switch_stream_id (const struct lsquic_stream *stream, + lsquic_stream_id_t *stream_id); + +int +lsquic_stream_onclose_done (const struct lsquic_stream *stream); + +void +lsquic_stream_mark_rejected (struct lsquic_stream *stream); + int lsquic_stream_rst_in (lsquic_stream_t *, uint64_t offset, uint64_t error_code); +int +lsquic_stream_reset_stream_at_in (lsquic_stream_t *, uint64_t final_size, + uint64_t reliable_size, uint64_t error_code); + +enum quic_frame_type +lsquic_stream_get_reset_frame_type (const struct lsquic_stream *stream, + uint64_t *reliable_size); + void lsquic_stream_stop_sending_in (struct lsquic_stream *, uint64_t error_code); +void +lsquic_stream_set_ss_code (lsquic_stream_t *, uint64_t error_code); + uint64_t lsquic_stream_read_offset (const lsquic_stream_t *stream); @@ -633,12 +752,8 @@ lsquic_stream_header_is_trailer (const struct lsquic_stream *); int lsquic_stream_verify_len (struct lsquic_stream *, unsigned long long); -static inline unsigned -lsquic_stream_is_blocked (const lsquic_stream_t *stream_) -{ - return stream_->blocked_off && - stream_->blocked_off == stream_->max_send_off; -} +int +lsquic_stream_is_blocked (const lsquic_stream_t *stream); void lsquic_stream_ss_frame_sent (struct lsquic_stream *); @@ -651,4 +766,22 @@ lsquic_stream_set_pwritev_params (unsigned iovecs, unsigned frames); void lsquic_stream_drop_hset_ref (struct lsquic_stream *); +struct lsquic_conn_public * +lsquic_stream_get_conn_public (const struct lsquic_stream *); + +void * +lsquic_stream_get_attachment (const struct lsquic_stream *); + +void +lsquic_stream_set_attachment (struct lsquic_stream *, void *); + +int +lsquic_stream_is_server (const struct lsquic_stream *); + +int +lsquic_stream_headers_state_is_begin (const struct lsquic_stream *); + +const struct lsquic_stream_if * +lsquic_stream_get_stream_if (const struct lsquic_stream *); + #endif diff --git a/src/liblsquic/lsquic_trans_params.c b/src/liblsquic/lsquic_trans_params.c index bcc34e75a..cfebd7c95 100644 --- a/src/liblsquic/lsquic_trans_params.c +++ b/src/liblsquic/lsquic_trans_params.c @@ -65,12 +65,13 @@ tpi_val_2_enum (uint64_t tpi_val) case 0xDE1A: return TPI_MIN_ACK_DELAY; case 0xFF02DE1A:return TPI_MIN_ACK_DELAY_02; case 0x7158: return TPI_TIMESTAMPS; + case 0x17F7586D2CB571: return TPI_RESET_STREAM_AT; default: return INT_MAX; } } -static const unsigned enum_2_tpi_val[LAST_TPI + 1] = +static const uint64_t enum_2_tpi_val[LAST_TPI + 1] = { [TPI_ORIGINAL_DEST_CID] = 0x0, [TPI_MAX_IDLE_TIMEOUT] = 0x1, @@ -99,6 +100,7 @@ static const unsigned enum_2_tpi_val[LAST_TPI + 1] = [TPI_MIN_ACK_DELAY_02] = 0xFF02DE1A, [TPI_TIMESTAMPS] = 0x7158, [TPI_GREASE_QUIC_BIT] = 0x2AB2, + [TPI_RESET_STREAM_AT] = 0x17F7586D2CB571, }; @@ -130,6 +132,7 @@ const char * const lsquic_tpi2str[LAST_TPI + 1] = [TPI_MIN_ACK_DELAY_02] = "min_ack_delay_02", [TPI_TIMESTAMPS] = "timestamps", [TPI_GREASE_QUIC_BIT] = "grease_quic_bit", + [TPI_RESET_STREAM_AT] = "reset_stream_at", }; #define tpi2str lsquic_tpi2str @@ -462,6 +465,7 @@ lsquic_tp_encode (const struct transport_params *params, int is_server, break; case TPI_DISABLE_ACTIVE_MIGRATION: case TPI_GREASE_QUIC_BIT: + case TPI_RESET_STREAM_AT: *p++ = 0; break; #if LSQUIC_TEST_QUANTUM_READINESS @@ -601,6 +605,7 @@ lsquic_tp_decode (const unsigned char *const buf, size_t bufsz, break; case TPI_DISABLE_ACTIVE_MIGRATION: case TPI_GREASE_QUIC_BIT: + case TPI_RESET_STREAM_AT: EXPECT_LEN(0); break; case TPI_STATELESS_RESET_TOKEN: @@ -1047,6 +1052,7 @@ lsquic_tp_encode_27 (const struct transport_params *params, int is_server, break; case TPI_DISABLE_ACTIVE_MIGRATION: case TPI_GREASE_QUIC_BIT: + case TPI_RESET_STREAM_AT: *p++ = 0; break; #if LSQUIC_TEST_QUANTUM_READINESS @@ -1174,6 +1180,8 @@ lsquic_tp_decode_27 (const unsigned char *const buf, size_t bufsz, } break; case TPI_DISABLE_ACTIVE_MIGRATION: + case TPI_GREASE_QUIC_BIT: + case TPI_RESET_STREAM_AT: EXPECT_LEN(0); break; case TPI_STATELESS_RESET_TOKEN: diff --git a/src/liblsquic/lsquic_trans_params.h b/src/liblsquic/lsquic_trans_params.h index 3f5fa0dac..a84bb6ac7 100644 --- a/src/liblsquic/lsquic_trans_params.h +++ b/src/liblsquic/lsquic_trans_params.h @@ -49,6 +49,7 @@ enum transport_param_id * Empty transport parameters: */ TPI_GREASE_QUIC_BIT, + TPI_RESET_STREAM_AT, TPI_DISABLE_ACTIVE_MIGRATION, MAX_EMPTY_TPI = TPI_DISABLE_ACTIVE_MIGRATION, /* @@ -224,6 +225,8 @@ lsquic_tp_get_quantum_sz (void); | (1 << TPI_MIN_ACK_DELAY_02) \ /* [draft-huitema-quic-ts-08] does not specify, store: */ \ | (1 << TPI_TIMESTAMPS) \ + /* [draft-ietf-quic-reliable-stream-reset-07] Section 3: */ \ + | (1 << TPI_RESET_STREAM_AT) \ ) /* We always send the minimum ACK delay as 10ms; it is not configurable. diff --git a/src/liblsquic/lsquic_varint.h b/src/liblsquic/lsquic_varint.h index d9e1732d2..770ada169 100644 --- a/src/liblsquic/lsquic_varint.h +++ b/src/liblsquic/lsquic_varint.h @@ -3,6 +3,8 @@ #define LSQUIC_VARINT_H 1 #define VINT_MASK ((1 << 6) - 1) +/* QUIC varint is encoded in 1, 2, 4, or 8 bytes. */ +#define VINT_MAX_SIZE 8 #include diff --git a/src/liblsquic/lsquic_wt.c b/src/liblsquic/lsquic_wt.c new file mode 100644 index 000000000..ef1059934 --- /dev/null +++ b/src/liblsquic/lsquic_wt.c @@ -0,0 +1,5442 @@ +/* Copyright (c) 2017 - 2026 LiteSpeed Technologies Inc. See LICENSE. */ +/* + * lsquic_wt.c -- WebTransport scaffolding + */ + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "lsquic.h" +#include "lsquic_wt.h" +#include "lsquic_int_types.h" +#include "lsquic_conn_flow.h" +#include "lsquic_rtt.h" +#include "lsquic_varint.h" +#include "lsquic_hq.h" +#include "lsquic_hash.h" +#include "lsquic_mm.h" +#include "lsquic_sfcw.h" +#include "lsquic_stream.h" +#include "lsquic_engine_public.h" +#include "lsquic_conn.h" +#include "lsquic_conn_public.h" +#include "lsxpack_header.h" + +#define LSQUIC_LOGGER_MODULE LSQLM_WT +#define LSQUIC_LOG_CONN_ID (conn ? lsquic_conn_id(conn) : NULL) +#include "lsquic_logger.h" + +#define WT_SET_CONN_FROM_STREAM(stream_) \ + lsquic_conn_t *const conn = (stream_) ? lsquic_stream_conn(stream_) : NULL + +#define WT_SET_CONN_FROM_SESSION(sess_) \ + lsquic_conn_t *const conn = (sess_) ? (sess_)->wts_conn : NULL + + +struct wt_onnew_ctx +{ + struct lsquic_wt_session *sess; + unsigned char prefix[16]; + size_t prefix_len; + int is_dynamic; +}; + +struct wt_stream_ctx +{ + struct lsquic_wt_session *sess; + lsquic_stream_ctx_t *app_ctx; + uint64_t (*ss_code) (struct lsquic_stream *, + lsquic_stream_ctx_t *); + unsigned char prefix[16]; + size_t prefix_len; + size_t prefix_off; +}; + +struct wt_uni_read_ctx +{ + struct varint_read_state state; + lsquic_stream_id_t sess_id; + int done; + int malformed; +}; + +struct wt_dgq_elem +{ + TAILQ_ENTRY(wt_dgq_elem) next; + size_t len; + enum lsquic_http_dg_send_mode mode; + unsigned char buf[]; +}; + +TAILQ_HEAD(wt_dgq_head, wt_dgq_elem); + + +enum wt_session_flags +{ + WTSF_CLOSING = 1 << 0, + WTSF_ON_CLOSE_CALLED = 1 << 1, + WTSF_WANT_DG_WRITE = 1 << 2, + WTSF_CLOSE_RCVD = 1 << 3, + WTSF_CLOSE_SENT = 1 << 4, + WTSF_CLOSE_CAPSULE_PENDING = 1 << 5, + WTSF_ACCEPT_PENDING = 1 << 6, + WTSF_OPENED = 1 << 7, + WTSF_REJECTED = 1 << 8, + WTSF_FINALIZING = 1 << 9, +}; + +enum wt_capsule_type +{ + /* [draft-ietf-webtrans-http3-15], Section 5.4 */ + WT_CAPSULE_DRAIN_SESSION = 0x78AEULL, + WT_CAPSULE_MAX_DATA = 0x190B4D3DULL, + WT_CAPSULE_MAX_STREAMS_BIDI = 0x190B4D3FULL, + WT_CAPSULE_MAX_STREAMS_UNI = 0x190B4D40ULL, + WT_CAPSULE_DATA_BLOCKED = 0x190B4D41ULL, + WT_CAPSULE_STREAMS_BLOCKED_BIDI = 0x190B4D43ULL, + WT_CAPSULE_STREAMS_BLOCKED_UNI = 0x190B4D44ULL, + /* [draft-ietf-webtrans-http3-15], Section 6, Figure 11 */ + WT_CAPSULE_CLOSE_SESSION = 0x2843ULL, +}; + +struct lsquic_wt_session; + +struct wt_control_ctx +{ + const struct lsquic_stream_if *wtcc_orig_if; + lsquic_stream_ctx_t *wtcc_orig_ctx; +}; + + +struct wt_headers_copy +{ + struct lsquic_http_headers headers; + struct lsxpack_header *headers_arr; + char *buf; +}; + + +struct lsquic_wt_session +{ + TAILQ_ENTRY(lsquic_wt_session) wts_next; + struct lsquic_stream *wts_control_stream; + struct lsquic_conn *wts_conn; + struct lsquic_conn_public *wts_conn_pub; + const struct lsquic_webtransport_if + *wts_if; + void *wts_if_ctx; + lsquic_wt_session_ctx_t *wts_sess_ctx; + struct lsquic_wt_connect_info wts_info; + lsquic_stream_id_t wts_stream_id; + char *wts_authority; + char *wts_path; + char *wts_origin; + char *wts_protocol; + struct lsquic_stream_if wts_control_if; + struct wt_control_ctx wts_control_ctx; + struct lsquic_stream_if wts_data_if; + struct wt_onnew_ctx wts_onnew_ctx; + struct wt_dgq_head wts_dgq; + struct wt_dgq_head wts_in_dgq; + struct wt_headers_copy wts_extra_resp_headers; + unsigned char *wts_close_buf; + char *wts_close_reason; + size_t wts_dgq_bytes; + size_t wts_in_dgq_bytes; + size_t wts_close_buf_len; + size_t wts_close_buf_off; + size_t wts_close_reason_len; + unsigned wts_dgq_count; + unsigned wts_in_dgq_count; + unsigned wts_dgq_max_count; + size_t wts_dgq_max_bytes; + enum lsquic_wt_dg_drop_policy wts_dg_policy; + enum lsquic_http_dg_send_mode wts_dg_mode; + uint64_t wts_close_code; + unsigned wts_n_streams; + unsigned wts_accept_status; + enum wt_session_flags wts_flags; +}; + + + +struct wt_header_buf +{ + char buf[128]; + size_t off; +}; + + +#define WT_MAX_PENDING_STREAMS 64 +#define WT_APP_ERROR_MAX 0xFFFFFFFFULL +#define WT_APP_ERROR_MIN_H3 0x52E4A40FA8DBULL +#define WT_APP_ERROR_MAX_H3 0x52E5AC983162ULL +#define WT_CLOSE_REASON_MAX 1024 + +enum wt_accept_result +{ + WT_ACCEPT_OPEN, + WT_ACCEPT_PENDING, + WT_ACCEPT_REJECT, +}; + +static void wt_dgq_drop_all (struct lsquic_wt_session *sess); +static struct lsquic_wt_session * +wt_stream_get_session (const struct lsquic_stream *stream); +static void +wt_close_remote (struct lsquic_wt_session *sess, uint64_t code, + const char *reason, size_t reason_len, int close_received); +static void +wt_reject_session (struct lsquic_wt_session *sess, unsigned status, + const char *reason, size_t reason_len); +static void +wt_close_stream_with_session_gone (struct lsquic_stream *stream); +static int +wt_build_close_capsule (uint64_t code, const char *reason, size_t reason_len, + unsigned char **buf, size_t *buf_len); +static void +wt_in_dgq_drop_all (struct lsquic_wt_session *sess); +static int +wt_in_dgq_enqueue (struct lsquic_wt_session *sess, const void *buf, size_t len); +static void +wt_replay_pending_datagrams (struct lsquic_wt_session *sess); +static void +wt_destroy_session (struct lsquic_wt_session *sess); +static enum wt_accept_result +wt_evaluate_accept (struct lsquic_stream *connect_stream, + const struct lsquic_wt_session *sess, + unsigned *status, const char **reason, + size_t *reason_len); +static void +wt_on_conn_http_caps_change (struct lsquic_conn_public *conn_pub); +static void +wt_fire_session_rejected_cb (struct lsquic_wt_session *sess, unsigned status, + const char *reason, size_t reason_len); +static void +wt_fire_session_open_cb (struct lsquic_wt_session *sess); +int +lsquic_wt_close (struct lsquic_wt_session *sess, uint64_t code, + const char *reason, size_t reason_len); + +static void +wt_drop_send_state (struct lsquic_wt_session *sess) +{ + if (sess->wts_control_stream) + (void) lsquic_stream_want_http_dg_write(sess->wts_control_stream, 0); + sess->wts_flags &= ~WTSF_WANT_DG_WRITE; + wt_dgq_drop_all(sess); +} + + +/* [draft-ietf-webtrans-http3-15], Section 6 */ +static void +wt_latch_close_info (struct lsquic_wt_session *sess, uint64_t code, + const char *reason, size_t reason_len) +{ + char *copy; + + if (sess->wts_flags & WTSF_CLOSING) + return; + + if (code > WT_APP_ERROR_MAX) + { + LSQ_LOG0(LSQ_LOG_WARN, "WT close code %"PRIu64" is too large; clamp to 0x%X", + code, (unsigned) WT_APP_ERROR_MAX); + code = WT_APP_ERROR_MAX; + } + + if (!reason && reason_len > 0) + { + LSQ_LOG0(LSQ_LOG_WARN, "WT close reason is NULL with non-zero length; ignore it"); + reason_len = 0; + } + else if (reason_len > WT_CLOSE_REASON_MAX) + { + LSQ_LOG0(LSQ_LOG_WARN, "WT close reason length %zu exceeds %u; truncate", + reason_len, WT_CLOSE_REASON_MAX); + reason_len = WT_CLOSE_REASON_MAX; + } + + copy = NULL; + if (reason_len > 0) + { + copy = malloc(reason_len); + if (!copy) + { + LSQ_LOG0(LSQ_LOG_WARN, "cannot copy WT close reason; omit it"); + reason_len = 0; + } + else + memcpy(copy, reason, reason_len); + } + + free(sess->wts_close_reason); + sess->wts_close_reason = copy; + sess->wts_close_reason_len = reason_len; + sess->wts_close_code = code; +} + + +static void +wt_abort_connect_message_error (struct lsquic_stream *stream, + const char *reason) +{ + WT_SET_CONN_FROM_STREAM(stream); + + conn->cn_if->ci_abort_error(conn, 1, HEC_MESSAGE_ERROR, "%s", reason); +} + + +/* [draft-ietf-webtrans-http3-15], Section 6, Figure 11 */ +static void +wt_on_close_capsule (struct lsquic_stream *stream, const void *payload, + size_t payload_len) +{ + WT_SET_CONN_FROM_STREAM(stream); + struct lsquic_wt_session *sess; + uint64_t code; + const unsigned char *p; + + sess = wt_stream_get_session(stream); + if (!sess) + return; + + /* [draft-ietf-webtrans-http3-15], Section 6 */ + if (sess->wts_flags & WTSF_CLOSE_RCVD) + { + wt_abort_connect_message_error(stream, + "received WT_CLOSE_SESSION after session close capsule"); + return; + } + + if (payload_len < 4 || payload_len > 4 + WT_CLOSE_REASON_MAX) + { + wt_abort_connect_message_error(stream, + "malformed WT_CLOSE_SESSION capsule payload"); + return; + } + + p = payload; + code = (uint64_t) p[0] << 24 | (uint64_t) p[1] << 16 + | (uint64_t) p[2] << 8 | (uint64_t) p[3]; + LSQ_INFO("received WT_CLOSE_SESSION on stream %"PRIu64 + " for session %"PRIu64" (code=%"PRIu64", reason_len=%zu)", + lsquic_stream_id(stream), sess->wts_stream_id, code, payload_len - 4); + wt_close_remote(sess, code, (const char *) p + 4, payload_len - 4, 1); +} + + +static const char * +wt_capsule_name (uint64_t capsule_type) +{ + switch (capsule_type) + { + case WT_CAPSULE_MAX_DATA: + return "WT_MAX_DATA"; + case WT_CAPSULE_MAX_STREAMS_BIDI: + return "WT_MAX_STREAMS_BIDI"; + case WT_CAPSULE_MAX_STREAMS_UNI: + return "WT_MAX_STREAMS_UNI"; + case WT_CAPSULE_DATA_BLOCKED: + return "WT_DATA_BLOCKED"; + case WT_CAPSULE_STREAMS_BLOCKED_BIDI: + return "WT_STREAMS_BLOCKED_BIDI"; + case WT_CAPSULE_STREAMS_BLOCKED_UNI: + return "WT_STREAMS_BLOCKED_UNI"; + case WT_CAPSULE_CLOSE_SESSION: + return "WT_CLOSE_SESSION"; + default: + return NULL; + } +} + + +static void +wt_on_capsule (lsquic_stream_t *stream, lsquic_stream_ctx_t *UNUSED_h, + uint64_t capsule_type, const void *payload, size_t payload_len) +{ + WT_SET_CONN_FROM_STREAM(stream); + struct lsquic_wt_session *sess; + const char *name; + const unsigned char *p; + const unsigned char *end; + struct varint_read_state vrs; + int s; + + sess = wt_stream_get_session(stream); + if (sess && (sess->wts_flags & WTSF_CLOSE_RCVD)) + { + wt_abort_connect_message_error(stream, + "received capsule after WT_CLOSE_SESSION"); + return; + } + + if (capsule_type == WT_CAPSULE_CLOSE_SESSION) + { + wt_on_close_capsule(stream, payload, payload_len); + return; + } + + name = wt_capsule_name(capsule_type); + if (!name) + return; + + if (payload_len == 0) + { + LSQ_INFO("%s capsule on WT CONNECT stream %"PRIu64 + " has empty payload; ignoring", + name, lsquic_stream_id(stream)); + return; + } + + if (payload_len > VINT_MAX_SIZE) + { + LSQ_INFO("%s capsule on WT CONNECT stream %"PRIu64 + " has too-large payload length %zu; ignoring", + name, lsquic_stream_id(stream), payload_len); + return; + } + + p = payload; + end = (const unsigned char *) payload + payload_len; + memset(&vrs, 0, sizeof(vrs)); + s = lsquic_varint_read_nb(&p, end, &vrs); + if (0 == s && p == end) + LSQ_INFO("%s capsule on WT CONNECT stream %"PRIu64 + ": value=%"PRIu64" (ignored)", + name, lsquic_stream_id(stream), vrs.val); + else + LSQ_INFO("%s capsule on WT CONNECT stream %"PRIu64 + " has malformed payload (len=%zu); ignoring", + name, lsquic_stream_id(stream), payload_len); +} + + +static void +wt_unregister_capsule_handlers (struct lsquic_stream *stream) +{ + static const uint64_t wt_capsule_types[] = { + WT_CAPSULE_MAX_DATA, + WT_CAPSULE_MAX_STREAMS_BIDI, + WT_CAPSULE_MAX_STREAMS_UNI, + WT_CAPSULE_DATA_BLOCKED, + WT_CAPSULE_STREAMS_BLOCKED_BIDI, + WT_CAPSULE_STREAMS_BLOCKED_UNI, + WT_CAPSULE_CLOSE_SESSION, + }; + unsigned i; + + for (i = 0; i < sizeof(wt_capsule_types) / sizeof(wt_capsule_types[0]); ++i) + (void) lsquic_stream_set_capsule_handler(stream, wt_capsule_types[i], + NULL); +} + + +static int +wt_register_capsule_handlers (struct lsquic_stream *stream) +{ + static const uint64_t wt_capsule_types[] = { + WT_CAPSULE_MAX_DATA, + WT_CAPSULE_MAX_STREAMS_BIDI, + WT_CAPSULE_MAX_STREAMS_UNI, + WT_CAPSULE_DATA_BLOCKED, + WT_CAPSULE_STREAMS_BLOCKED_BIDI, + WT_CAPSULE_STREAMS_BLOCKED_UNI, + WT_CAPSULE_CLOSE_SESSION, + }; + unsigned i; + + for (i = 0; i < sizeof(wt_capsule_types) / sizeof(wt_capsule_types[0]); ++i) + if (0 != lsquic_stream_set_capsule_handler(stream, wt_capsule_types[i], + wt_on_capsule)) + { + while (i-- > 0) + (void) lsquic_stream_set_capsule_handler(stream, + wt_capsule_types[i], NULL); + return -1; + } + + return 0; +} + +static int +lsquic_wt_on_http_dg_write (struct lsquic_stream *stream, + lsquic_stream_ctx_t *UNUSED_sctx, + size_t max_quic_payload, + lsquic_http_dg_consume_f consume_datagram); + +static void +lsquic_wt_on_http_dg_read (struct lsquic_stream *stream, + lsquic_stream_ctx_t *UNUSED_sctx, + const void *buf, size_t len); +static size_t +wt_uni_readf (void *ctx, const unsigned char *buf, size_t sz, int fin); + +static const struct lsquic_http_dg_if wt_http_dg_if = +{ + .on_http_dg_write = lsquic_wt_on_http_dg_write, + .on_http_dg_read = lsquic_wt_on_http_dg_read, +}; + +static struct lsquic_wt_session * +wt_session_find (struct lsquic_conn_public *conn_pub, + lsquic_stream_id_t stream_id); + +static unsigned +wt_count_pending_streams (struct lsquic_conn_public *conn_pub); + +static int +wt_buffer_or_reject_stream (struct lsquic_stream *stream, + lsquic_stream_id_t session_id, enum lsquic_wt_stream_dir dir); + +static void +wt_replay_pending_streams (struct lsquic_wt_session *sess); + +static void +wt_drive_connect_stream (struct lsquic_stream *stream); + +static void +wt_free_extra_resp_headers (struct lsquic_wt_session *sess); + +static int +wt_copy_extra_resp_headers (struct lsquic_wt_session *sess, + const struct lsquic_http_headers *headers); + +static int +wt_send_response (struct lsquic_stream *stream, unsigned status, + const struct lsquic_http_headers *extra, + int fin); + +static int +wt_queue_close_capsule (struct lsquic_wt_session *sess, uint64_t code, + const char *reason, size_t reason_len); + +static void +wt_begin_close (struct lsquic_wt_session *sess, uint64_t code, + const char *reason, size_t reason_len); + +static void +wt_close_remote (struct lsquic_wt_session *sess, uint64_t code, + const char *reason, size_t reason_len, int close_received); + +static int +wt_dgq_enqueue (struct lsquic_wt_session *sess, const void *buf, size_t len, + enum lsquic_wt_dg_drop_policy policy, + enum lsquic_http_dg_send_mode mode); + +static void +wt_dgq_drop_all (struct lsquic_wt_session *sess); + +static void +wt_stream_bind_session (struct lsquic_wt_session *sess, + struct lsquic_stream *stream); + +static void +wt_stream_unbind_session (struct lsquic_stream *stream); + +static void +wt_on_reset_core (struct lsquic_stream *stream, struct wt_stream_ctx *wctx, + int how, struct lsquic_conn *conn); + +static int +wt_wrap_control_stream (struct lsquic_wt_session *sess, + struct lsquic_stream *stream); + +static int +wt_stream_ss_code (const struct lsquic_stream *stream, uint64_t *ss_code); + +static void +wt_on_stream_destroy (struct lsquic_stream *stream); + +static void +wt_session_maybe_finalize (struct lsquic_wt_session *sess); +static void +wt_control_on_reset (struct lsquic_stream *stream, lsquic_stream_ctx_t *sctx, + int how); + +static int +wt_is_hq_switch_frame (struct lsquic_stream *stream, uint64_t frame_type, + uint64_t frame_len); + +static void +wt_on_client_bidi_stream (struct lsquic_stream *stream, + lsquic_stream_id_t session_id); + +static int +wt_switch_to_data_if (struct lsquic_stream *stream, + struct lsquic_wt_session *sess); + +static lsquic_stream_ctx_t * +wt_on_new_stream (void *ctx, struct lsquic_stream *stream); + +static int +wt_is_reserved_h3_error_code (uint64_t h3_error_code) +{ + return h3_error_code >= WT_APP_ERROR_MIN_H3 + && h3_error_code <= WT_APP_ERROR_MAX_H3 + && ((h3_error_code - 0x21ULL) % 0x1FULL) == 0; +} + + +#ifndef LSQUIC_TEST +static +#endif +int +lsquic_wt_validate_incoming_session_id (struct lsquic_stream *stream, + lsquic_stream_id_t session_id, const char *stream_kind) +{ + WT_SET_CONN_FROM_STREAM(stream); + + if ((session_id & SIT_MASK) == SIT_BIDI_CLIENT) + return 0; + + LSQ_WARN("invalid WT %s stream session ID %"PRIu64 + " on stream %"PRIu64, stream_kind, (uint64_t) session_id, + lsquic_stream_id(stream)); + conn->cn_if->ci_abort_error(conn, 1, HEC_ID_ERROR, + "invalid WT %s stream session ID %"PRIu64 + " on stream %"PRIu64, stream_kind, (uint64_t) session_id, + lsquic_stream_id(stream)); + return -1; +} + + +#if LSQUIC_TEST +static unsigned s_wt_test_error_code; +static unsigned s_wt_test_fail_stream_ctx_alloc; +static unsigned s_wt_test_freed_dynamic_onnew; +static unsigned s_wt_test_aborted_outgoing_stream; +static int s_wt_test_dg_write_stub_active; +static int s_wt_test_dg_write_arm_result; +static unsigned s_wt_test_dg_write_arm_calls; +static unsigned s_wt_test_dg_write_disarm_calls; +static unsigned s_wt_test_read_error_close_calls; +static int s_wt_test_stub_read_error_close; +static unsigned s_wt_test_write_error_close_calls; +static int s_wt_test_stub_write_error_close; +static struct wt_test_http_dg_write_ctx *s_wt_test_http_dg_write_ctx; + + +static void +wt_test_abort_error (struct lsquic_conn *UNUSED_conn, int UNUSED_is_app, + unsigned error_code, const char *UNUSED_format, ...) +{ + s_wt_test_error_code = error_code; +} + + +int +lsquic_wt_test_validate_incoming_session_id (lsquic_stream_id_t stream_id, + lsquic_stream_id_t session_id, const char *stream_kind, + unsigned *error_code) +{ + struct lsquic_conn conn = LSCONN_INITIALIZER_CIDLEN(conn, 0); + struct lsquic_conn_public conn_pub; + struct lsquic_stream stream; + static const struct conn_iface conn_iface = { + .ci_abort_error = wt_test_abort_error, + }; + int s; + + memset(&conn_pub, 0, sizeof(conn_pub)); + memset(&stream, 0, sizeof(stream)); + TAILQ_INIT(&conn_pub.wt_sessions); + conn.cn_if = &conn_iface; + conn_pub.lconn = &conn; + stream.id = stream_id; + stream.conn_pub = &conn_pub; + s_wt_test_error_code = 0; + s = lsquic_wt_validate_incoming_session_id(&stream, session_id, + stream_kind); + if (error_code) + *error_code = s_wt_test_error_code; + return s; +} + + +void +lsquic_wt_test_set_fail_stream_ctx_alloc (unsigned count) +{ + s_wt_test_fail_stream_ctx_alloc = count; +} + + +#endif + + +static struct wt_stream_ctx * +wt_test_calloc (size_t size) +{ +#if LSQUIC_TEST + if (s_wt_test_fail_stream_ctx_alloc > 0) + { + --s_wt_test_fail_stream_ctx_alloc; + return NULL; + } +#endif + return calloc(1, size); +} + + +static struct wt_stream_ctx * +wt_stream_ctx_alloc (void) +{ + return (struct wt_stream_ctx *) wt_test_calloc(sizeof(struct wt_stream_ctx)); +} + + +static struct wt_uni_read_ctx * +wt_uni_read_ctx_alloc (void) +{ + return (struct wt_uni_read_ctx *) wt_test_calloc( + sizeof(struct wt_uni_read_ctx)); +} + + +static void +wt_close_stream_after_read_error (struct lsquic_stream *stream, + const char *stream_kind) +{ + WT_SET_CONN_FROM_STREAM(stream); + + LSQ_WARN("error reading WT %s stream %"PRIu64, stream_kind, + lsquic_stream_id(stream)); +#if LSQUIC_TEST + if (s_wt_test_stub_read_error_close) + { + ++s_wt_test_read_error_close_calls; + return; + } +#endif + lsquic_stream_close(stream); +} + + +static void +wt_close_stream_after_write_error (struct lsquic_stream *stream, + const char *stream_kind) +{ + WT_SET_CONN_FROM_STREAM(stream); + + LSQ_WARN("error writing WT %s stream %"PRIu64, stream_kind, + lsquic_stream_id(stream)); +#if LSQUIC_TEST + if (s_wt_test_stub_write_error_close) + { + ++s_wt_test_write_error_close_calls; + return; + } +#endif + lsquic_stream_close(stream); +} + + +static void +wt_free_onnew_ctx (struct wt_onnew_ctx *onnew) +{ + if (onnew && onnew->is_dynamic) + { +#if LSQUIC_TEST + ++s_wt_test_freed_dynamic_onnew; +#endif + free(onnew); + } +} + + +static int +wt_size_add (size_t *total, size_t add) +{ + if (add > SIZE_MAX - *total) + { + errno = EOVERFLOW; + return -1; + } + + *total += add; + return 0; +} + + +static struct wt_dgq_elem * +wt_dgq_elem_alloc (size_t len) +{ + size_t need; + + need = sizeof(struct wt_dgq_elem); + if (0 != wt_size_add(&need, len)) + return NULL; + + return malloc(need); +} + +static int +wt_app_error_to_h3_error (uint64_t wt_error_code, uint64_t *h3_error_code) +{ + if (!h3_error_code || wt_error_code > WT_APP_ERROR_MAX) + return -1; + + *h3_error_code = WT_APP_ERROR_MIN_H3 + wt_error_code + wt_error_code / 0x1EULL; + return 0; +} + + +static int +wt_h3_error_to_app_error (uint64_t h3_error_code, uint64_t *wt_error_code) +{ + uint64_t shifted; + + if (!wt_error_code + || h3_error_code < WT_APP_ERROR_MIN_H3 + || h3_error_code > WT_APP_ERROR_MAX_H3 + || wt_is_reserved_h3_error_code(h3_error_code)) + return -1; + + shifted = h3_error_code - WT_APP_ERROR_MIN_H3; + *wt_error_code = shifted - shifted / 0x1FULL; + return 0; +} + + +#if LSQUIC_TEST +int +lsquic_wt_test_app_error_to_h3_error (uint64_t wt_error_code, + uint64_t *h3_error_code) +{ + return wt_app_error_to_h3_error(wt_error_code, h3_error_code); +} + + +int +lsquic_wt_test_h3_error_to_app_error (uint64_t h3_error_code, + uint64_t *wt_error_code) +{ + return wt_h3_error_to_app_error(h3_error_code, wt_error_code); +} + + +lsquic_wt_session_t * +lsquic_wt_test_dgq_session_new (unsigned max_count, size_t max_bytes) +{ + lsquic_wt_session_t *sess; + + sess = calloc(1, sizeof(*sess)); + if (!sess) + return NULL; + + TAILQ_INIT(&sess->wts_dgq); + sess->wts_dgq_max_count = max_count ? max_count + : LSQUIC_WTAP_MAX_DATAGRAM_QUEUE_COUNT_DEFAULT; + sess->wts_dgq_max_bytes = max_bytes ? max_bytes + : LSQUIC_WTAP_MAX_DATAGRAM_QUEUE_BYTES_DEFAULT; + sess->wts_dg_policy = LSQUIC_WTAP_DATAGRAM_DROP_POLICY_DEFAULT; + sess->wts_dg_mode = LSQUIC_WTAP_DATAGRAM_SEND_MODE_DEFAULT; + return sess; +} + + +void +lsquic_wt_test_dgq_session_destroy (lsquic_wt_session_t *sess) +{ + if (sess) + { + wt_dgq_drop_all(sess); + free(sess); + } +} + + +int +lsquic_wt_test_dgq_enqueue (lsquic_wt_session_t *sess, const void *buf, + size_t len, + enum lsquic_wt_dg_drop_policy policy) +{ + return wt_dgq_enqueue(sess, buf, len, policy, + LSQUIC_HTTP_DG_SEND_DEFAULT); +} + + +unsigned +lsquic_wt_test_dgq_count (const lsquic_wt_session_t *sess) +{ + return sess ? sess->wts_dgq_count : 0; +} + + +size_t +lsquic_wt_test_dgq_bytes (const lsquic_wt_session_t *sess) +{ + return sess ? sess->wts_dgq_bytes : 0; +} + + +int +lsquic_wt_test_dgq_front (const lsquic_wt_session_t *sess, unsigned char *val) +{ + const struct wt_dgq_elem *elem; + + if (!sess || !val) + return -1; + + elem = TAILQ_FIRST(&sess->wts_dgq); + if (!elem) + return -1; + + *val = elem->buf[0]; + return 0; +} + + +int +lsquic_wt_test_dgq_back (const lsquic_wt_session_t *sess, unsigned char *val) +{ + const struct wt_dgq_elem *elem; + + if (!sess || !val) + return -1; + + elem = TAILQ_LAST(&sess->wts_dgq, wt_dgq_head); + if (!elem) + return -1; + + *val = elem->buf[0]; + return 0; +} + + +struct wt_test_close_result +{ + unsigned called; + uint64_t close_code; + size_t close_reason_len; +}; + + +struct wt_test_datagram_result +{ + unsigned called; + size_t len; +}; + + +struct wt_test_accept_result +{ + unsigned opened; + unsigned rejected; + unsigned status; +}; + + +static void +wt_test_on_session_close (lsquic_wt_session_t *UNUSED_sess, + lsquic_wt_session_ctx_t *sctx, uint64_t code, + const char *UNUSED_reason, size_t reason_len) +{ + struct wt_test_close_result *result; + + result = (struct wt_test_close_result *) sctx; + result->called += 1; + result->close_code = code; + result->close_reason_len = reason_len; +} + + +static lsquic_wt_session_ctx_t * +wt_test_on_session_open (void *ctx, lsquic_wt_session_t *UNUSED_sess, + const struct lsquic_wt_connect_info *UNUSED_info) +{ + struct wt_test_accept_result *result; + + result = ctx; + result->opened += 1; + return NULL; +} + + +static void +wt_test_on_session_rejected (void *ctx, + const struct lsquic_wt_connect_info *UNUSED_info, + unsigned status, const char *UNUSED_reason, + size_t UNUSED_reason_len) +{ + struct wt_test_accept_result *result; + + result = ctx; + result->rejected += 1; + result->status = status; +} + + +static void +wt_test_on_datagram_read (lsquic_wt_session_t *sess, const void *UNUSED_buf, + size_t len) +{ + struct wt_test_datagram_result *result; + + result = (struct wt_test_datagram_result *) + ((struct lsquic_wt_session *) sess)->wts_sess_ctx; + result->called += 1; + result->len = len; +} + + +static void +wt_test_on_datagram_read_and_close (lsquic_wt_session_t *sess, + const void *UNUSED_buf, size_t len) +{ + struct wt_test_datagram_result *result; + + result = (struct wt_test_datagram_result *) + ((struct lsquic_wt_session *) sess)->wts_sess_ctx; + result->called += 1; + result->len = len; + if (result->called == 1) + (void) lsquic_wt_close((struct lsquic_wt_session *) sess, 0, NULL, 0); +} + + +int +lsquic_wt_test_build_close_capsule (uint64_t code, const char *reason, + size_t reason_len, unsigned char *buf, + size_t *buf_len) +{ + unsigned char *capsule; + size_t capsule_len; + + if (code > WT_APP_ERROR_MAX + || reason_len > WT_CLOSE_REASON_MAX + || (!reason && reason_len > 0)) + { + errno = EINVAL; + return -1; + } + + if (0 != wt_build_close_capsule(code, reason, reason_len, + &capsule, &capsule_len)) + return -1; + + if (!buf_len || *buf_len < capsule_len) + { + free(capsule); + errno = ENOBUFS; + return -1; + } + + memcpy(buf, capsule, capsule_len); + *buf_len = capsule_len; + free(capsule); + return 0; +} + + +int +lsquic_wt_test_remote_close (uint64_t code, const char *reason, + size_t reason_len, unsigned *called, + uint64_t *close_code, size_t *close_reason_len, + int *is_closing, int *close_received, + int *on_close_called) +{ + struct lsquic_wt_session sess; + struct wt_test_close_result result; + struct lsquic_webtransport_if wt_if; + + memset(&sess, 0, sizeof(sess)); + memset(&result, 0, sizeof(result)); + memset(&wt_if, 0, sizeof(wt_if)); + wt_if.wti_on_session_close = wt_test_on_session_close; + sess.wts_if = &wt_if; + sess.wts_sess_ctx = (lsquic_wt_session_ctx_t *) &result; + sess.wts_n_streams = 1; + + wt_close_remote(&sess, code, reason, reason_len, 1); + + if (called) + *called = result.called; + if (close_code) + *close_code = sess.wts_close_code; + if (close_reason_len) + *close_reason_len = sess.wts_close_reason_len; + if (is_closing) + *is_closing = !!(sess.wts_flags & WTSF_CLOSING); + if (close_received) + *close_received = !!(sess.wts_flags & WTSF_CLOSE_RCVD); + if (on_close_called) + *on_close_called = !!(sess.wts_flags & WTSF_ON_CLOSE_CALLED); + + free(sess.wts_close_reason); + free(sess.wts_close_buf); + return 0; +} + +int +lsquic_wt_test_closing_rejects (unsigned *mask) +{ + struct lsquic_wt_session sess; + unsigned bits; + + memset(&sess, 0, sizeof(sess)); + sess.wts_flags = WTSF_CLOSING; + bits = 0; + + errno = 0; + if (!lsquic_wt_open_uni(&sess) && errno == EPIPE) + bits |= 1u << 0; + + errno = 0; + if (!lsquic_wt_open_bidi(&sess) && errno == EPIPE) + bits |= 1u << 1; + + errno = 0; + if (0 > lsquic_wt_send_datagram_ex(&sess, "x", 1, LSQWT_DG_FAIL_EAGAIN, + LSQUIC_HTTP_DG_SEND_DEFAULT) + && errno == EPIPE) + bits |= 1u << 2; + + errno = 0; + if (0 > lsquic_wt_want_datagram_write(&sess, 1) && errno == EPIPE) + bits |= 1u << 3; + + if (mask) + *mask = bits; + return 0; +} + + +int +lsquic_wt_test_local_close (uint64_t code, const char *reason, + size_t reason_len, int *queued_capsule, + unsigned *dgq_count) +{ + struct lsquic_wt_session sess; + unsigned char capsule_buf[WT_CLOSE_REASON_MAX + 32]; + size_t capsule_len; + static const unsigned char dg[] = { 'd', 'g', }; + + memset(&sess, 0, sizeof(sess)); + TAILQ_INIT(&sess.wts_dgq); + sess.wts_dgq_max_count = LSQUIC_WTAP_MAX_DATAGRAM_QUEUE_COUNT_DEFAULT; + sess.wts_dgq_max_bytes = LSQUIC_WTAP_MAX_DATAGRAM_QUEUE_BYTES_DEFAULT; + + assert(0 == wt_dgq_enqueue(&sess, dg, sizeof(dg), LSQWT_DG_FAIL_EAGAIN, + LSQUIC_HTTP_DG_SEND_DEFAULT)); + wt_begin_close(&sess, code, reason, reason_len); + + if (queued_capsule) + { + *queued_capsule = 0; + if (sess.wts_close_code != 0 || sess.wts_close_reason_len != 0) + { + capsule_len = sizeof(capsule_buf); + if (0 == lsquic_wt_test_build_close_capsule(sess.wts_close_code, + sess.wts_close_reason, sess.wts_close_reason_len, + capsule_buf, &capsule_len)) + *queued_capsule = 1; + } + } + + if (dgq_count) + *dgq_count = sess.wts_dgq_count; + + free(sess.wts_close_reason); + return 0; +} + + +int +lsquic_wt_test_finalize (uint64_t code, const char *reason, size_t reason_len, + unsigned *called, uint64_t *close_code, + size_t *close_reason_len, int *removed, + unsigned *dropped_datagrams) +{ + struct lsquic_wt_session *sess; + struct wt_test_close_result result; + struct lsquic_webtransport_if wt_if; + struct lsquic_conn_public conn_pub; + static const unsigned char dg[] = { 'd', 'g', }; + unsigned n_dgrams; + + sess = calloc(1, sizeof(*sess)); + if (!sess) + return -1; + + memset(&result, 0, sizeof(result)); + memset(&wt_if, 0, sizeof(wt_if)); + memset(&conn_pub, 0, sizeof(conn_pub)); + TAILQ_INIT(&conn_pub.wt_sessions); + TAILQ_INIT(&sess->wts_dgq); + + wt_if.wti_on_session_close = wt_test_on_session_close; + sess->wts_if = &wt_if; + sess->wts_sess_ctx = (lsquic_wt_session_ctx_t *) &result; + sess->wts_conn_pub = &conn_pub; + sess->wts_stream_id = 4; + sess->wts_dgq_max_count = LSQUIC_WTAP_MAX_DATAGRAM_QUEUE_COUNT_DEFAULT; + sess->wts_dgq_max_bytes = LSQUIC_WTAP_MAX_DATAGRAM_QUEUE_BYTES_DEFAULT; + wt_latch_close_info(sess, code, reason, reason_len); + sess->wts_flags |= WTSF_CLOSING | WTSF_OPENED; + assert(0 == wt_dgq_enqueue(sess, dg, sizeof(dg), LSQWT_DG_FAIL_EAGAIN, + LSQUIC_HTTP_DG_SEND_DEFAULT)); + n_dgrams = sess->wts_dgq_count; + TAILQ_INSERT_TAIL(&conn_pub.wt_sessions, sess, wts_next); + + wt_session_maybe_finalize(sess); + + if (called) + *called = result.called; + if (close_code) + *close_code = result.close_code; + if (close_reason_len) + *close_reason_len = result.close_reason_len; + if (removed) + *removed = TAILQ_EMPTY(&conn_pub.wt_sessions); + if (dropped_datagrams) + *dropped_datagrams = n_dgrams; + + return 0; +} + + +int +lsquic_wt_test_control_reset_close (unsigned *called, int *is_closing, + int *close_received) +{ + struct lsquic_wt_session sess; + struct wt_test_close_result result; + struct lsquic_webtransport_if wt_if; + struct lsquic_conn conn; + struct lsquic_conn_public conn_pub; + struct lsquic_stream stream; + + memset(&sess, 0, sizeof(sess)); + memset(&result, 0, sizeof(result)); + memset(&wt_if, 0, sizeof(wt_if)); + memset(&conn, 0, sizeof(conn)); + memset(&conn_pub, 0, sizeof(conn_pub)); + memset(&stream, 0, sizeof(stream)); + + wt_if.wti_on_session_close = wt_test_on_session_close; + sess.wts_if = &wt_if; + sess.wts_sess_ctx = (lsquic_wt_session_ctx_t *) &result; + sess.wts_n_streams = 1; + sess.wts_conn = &conn; + sess.wts_conn_pub = &conn_pub; + conn_pub.lconn = &conn; + stream.sm_attachment = &sess; + stream.conn_pub = &conn_pub; + stream.id = 0; + + wt_control_on_reset(&stream, NULL, 0); + + if (called) + *called = result.called; + if (is_closing) + *is_closing = !!(sess.wts_flags & WTSF_CLOSING); + if (close_received) + *close_received = !!(sess.wts_flags & WTSF_CLOSE_RCVD); + + free(sess.wts_close_reason); + free(sess.wts_close_buf); + return 0; +} + + +int +lsquic_wt_test_http_dg_read (int with_session, int is_control_stream, + int is_closing, unsigned *called) +{ + struct wt_test_datagram_result result; + struct lsquic_webtransport_if wt_if; + struct lsquic_wt_session sess; + struct lsquic_conn conn; + struct lsquic_conn_public conn_pub; + struct lsquic_stream stream, control_stream; + static const unsigned char dg[] = { 'd', 'g', }; + + memset(&result, 0, sizeof(result)); + memset(&wt_if, 0, sizeof(wt_if)); + memset(&sess, 0, sizeof(sess)); + memset(&conn, 0, sizeof(conn)); + memset(&conn_pub, 0, sizeof(conn_pub)); + memset(&stream, 0, sizeof(stream)); + memset(&control_stream, 0, sizeof(control_stream)); + + wt_if.wti_on_datagram_read = wt_test_on_datagram_read; + sess.wts_if = &wt_if; + sess.wts_sess_ctx = (lsquic_wt_session_ctx_t *) &result; + sess.wts_conn = &conn; + sess.wts_conn_pub = &conn_pub; + if (is_closing) + sess.wts_flags |= WTSF_CLOSING; + + conn_pub.lconn = &conn; + stream.conn_pub = &conn_pub; + control_stream.conn_pub = &conn_pub; + + if (with_session) + { + stream.sm_attachment = &sess; + sess.wts_control_stream = is_control_stream ? &stream : &control_stream; + } + + lsquic_wt_on_http_dg_read(&stream, NULL, dg, sizeof(dg)); + if (called) + *called = result.called; + return 0; +} + + +int +lsquic_wt_test_http_dg_read_bytes (const unsigned char *buf, size_t len, + unsigned flags, unsigned *called) +{ + struct wt_test_datagram_result result; + struct lsquic_webtransport_if wt_if; + struct lsquic_wt_session sess; + struct lsquic_conn conn; + struct lsquic_conn_public conn_pub; + struct lsquic_stream stream, control_stream; + + memset(&result, 0, sizeof(result)); + memset(&wt_if, 0, sizeof(wt_if)); + memset(&sess, 0, sizeof(sess)); + memset(&conn, 0, sizeof(conn)); + memset(&conn_pub, 0, sizeof(conn_pub)); + memset(&stream, 0, sizeof(stream)); + memset(&control_stream, 0, sizeof(control_stream)); + + wt_if.wti_on_datagram_read = wt_test_on_datagram_read; + sess.wts_if = &wt_if; + sess.wts_sess_ctx = (lsquic_wt_session_ctx_t *) &result; + sess.wts_conn = &conn; + sess.wts_conn_pub = &conn_pub; + if (flags & 4) + sess.wts_flags |= WTSF_CLOSING; + + conn_pub.lconn = &conn; + stream.conn_pub = &conn_pub; + control_stream.conn_pub = &conn_pub; + + if (flags & 1) + { + stream.sm_attachment = &sess; + sess.wts_control_stream = flags & 2 ? &stream : &control_stream; + } + + if (buf && len > 0) + lsquic_wt_on_http_dg_read(&stream, NULL, buf, len); + else + lsquic_wt_on_http_dg_read(&stream, NULL, "", 0); + + if (called) + *called = result.called; + return 0; +} + + +int +lsquic_wt_test_close_capsule_payload (const unsigned char *payload, + size_t payload_len, unsigned flags, + unsigned *error_code, + int *is_closing, + int *close_received, + size_t *close_reason_len) +{ + struct lsquic_wt_session sess; + struct lsquic_conn conn = LSCONN_INITIALIZER_CIDLEN(conn, 0); + struct lsquic_conn_public conn_pub; + struct lsquic_stream stream; + static const struct conn_iface conn_iface = { + .ci_abort_error = wt_test_abort_error, + }; + + memset(&sess, 0, sizeof(sess)); + memset(&conn_pub, 0, sizeof(conn_pub)); + memset(&stream, 0, sizeof(stream)); + + conn.cn_if = &conn_iface; + conn_pub.lconn = &conn; + stream.id = 0; + stream.conn_pub = &conn_pub; + stream.sm_attachment = &sess; + stream.stream_flags = STREAM_U_READ_DONE | STREAM_U_WRITE_DONE; + sess.wts_conn = &conn; + sess.wts_conn_pub = &conn_pub; + sess.wts_control_stream = &stream; + sess.wts_n_streams = 1; + sess.wts_stream_id = 4; + if (flags & 1) + sess.wts_flags |= WTSF_CLOSE_RCVD; + s_wt_test_error_code = 0; + + wt_on_close_capsule(&stream, payload ? payload + : (const unsigned char *) "", + payload_len); + + if (error_code) + *error_code = s_wt_test_error_code; + if (is_closing) + *is_closing = !!(sess.wts_flags & WTSF_CLOSING); + if (close_received) + *close_received = !!(sess.wts_flags & WTSF_CLOSE_RCVD); + if (close_reason_len) + *close_reason_len = sess.wts_close_reason_len; + + free(sess.wts_close_reason); + free(sess.wts_close_buf); + return 0; +} + + +int +lsquic_wt_test_uni_read_bytes (const unsigned char *buf, size_t len, int fin, + size_t *consumed, int *done, + lsquic_stream_id_t *session_id) +{ + struct wt_uni_read_ctx uctx; + size_t nr; + + memset(&uctx, 0, sizeof(uctx)); + nr = wt_uni_readf(&uctx, buf ? buf : (const unsigned char *) "", len, fin); + + if (consumed) + *consumed = nr; + if (done) + *done = uctx.done; + if (session_id) + *session_id = uctx.sess_id; + return 0; +} + + +int +lsquic_wt_test_uni_read_state (const unsigned char *buf, size_t len, int fin, + size_t *consumed, int *done, int *malformed, + lsquic_stream_id_t *session_id) +{ + struct wt_uni_read_ctx uctx; + size_t nr; + + memset(&uctx, 0, sizeof(uctx)); + nr = wt_uni_readf(&uctx, buf ? buf : (const unsigned char *) "", len, fin); + + if (consumed) + *consumed = nr; + if (done) + *done = uctx.done; + if (malformed) + *malformed = uctx.malformed; + if (session_id) + *session_id = uctx.sess_id; + return 0; +} + + +int +lsquic_wt_test_accept_resolution (unsigned initial_flags, unsigned final_flags, + unsigned existing_sessions, + unsigned *initial_result, + unsigned *final_result, + unsigned *opened, unsigned *rejected, + unsigned *status) +{ + struct wt_test_accept_result result; + struct lsquic_webtransport_if wt_if; + struct lsquic_engine_public enpub; + struct lsquic_conn conn; + struct lsquic_conn_public conn_pub; + struct lsquic_stream stream; + struct lsquic_wt_session sess, other; + unsigned reject_status; + const char *reject_reason; + size_t reject_reason_len; + enum wt_accept_result accept_result; + + memset(&result, 0, sizeof(result)); + memset(&wt_if, 0, sizeof(wt_if)); + memset(&enpub, 0, sizeof(enpub)); + memset(&conn, 0, sizeof(conn)); + memset(&conn_pub, 0, sizeof(conn_pub)); + memset(&stream, 0, sizeof(stream)); + memset(&sess, 0, sizeof(sess)); + memset(&other, 0, sizeof(other)); + + TAILQ_INIT(&conn_pub.wt_sessions); + wt_if.wti_on_session_open = wt_test_on_session_open; + wt_if.wti_on_session_rejected = wt_test_on_session_rejected; + enpub.enp_settings.es_webtransport = 1; + conn_pub.enpub = &enpub; + conn_pub.lconn = &conn; + conn_pub.cp_flags = initial_flags; + stream.conn_pub = &conn_pub; + sess.wts_if = &wt_if; + sess.wts_if_ctx = &result; + sess.wts_conn = &conn; + sess.wts_conn_pub = &conn_pub; + sess.wts_control_stream = &stream; + sess.wts_stream_id = 0; + + if (existing_sessions > 0) + TAILQ_INSERT_TAIL(&conn_pub.wt_sessions, &other, wts_next); + + reject_status = 500; + reject_reason = "cannot accept WebTransport"; + reject_reason_len = sizeof("cannot accept WebTransport") - 1; + accept_result = wt_evaluate_accept(&stream, &sess, &reject_status, + &reject_reason, &reject_reason_len); + if (initial_result) + *initial_result = accept_result; + + if (accept_result == WT_ACCEPT_PENDING) + { + sess.wts_flags |= WTSF_ACCEPT_PENDING; + TAILQ_INSERT_TAIL(&conn_pub.wt_sessions, &sess, wts_next); + conn_pub.cp_flags = final_flags; + reject_status = 500; + reject_reason = "cannot accept WebTransport"; + reject_reason_len = sizeof("cannot accept WebTransport") - 1; + accept_result = wt_evaluate_accept(&stream, &sess, &reject_status, + &reject_reason, + &reject_reason_len); + if (accept_result == WT_ACCEPT_OPEN) + wt_fire_session_open_cb(&sess); + else if (accept_result == WT_ACCEPT_REJECT) + wt_fire_session_rejected_cb(&sess, reject_status, reject_reason, + reject_reason_len); + TAILQ_REMOVE(&conn_pub.wt_sessions, &sess, wts_next); + } + else if (accept_result == WT_ACCEPT_OPEN) + wt_fire_session_open_cb(&sess); + else + wt_fire_session_rejected_cb(&sess, reject_status, reject_reason, + reject_reason_len); + + if (existing_sessions > 0) + TAILQ_REMOVE(&conn_pub.wt_sessions, &other, wts_next); + + if (final_result) + *final_result = accept_result; + if (opened) + *opened = result.opened; + if (rejected) + *rejected = result.rejected; + if (status) + *status = result.status; + return 0; +} + + +int +lsquic_wt_test_pending_datagram_replay (unsigned *called_before, + unsigned *called_after) +{ + struct wt_test_datagram_result result; + struct lsquic_webtransport_if wt_if; + struct lsquic_conn conn; + struct lsquic_conn_public conn_pub; + struct lsquic_stream stream; + struct lsquic_wt_session sess; + static const unsigned char dg[] = { 'd', 'g', }; + + memset(&result, 0, sizeof(result)); + memset(&wt_if, 0, sizeof(wt_if)); + memset(&conn, 0, sizeof(conn)); + memset(&conn_pub, 0, sizeof(conn_pub)); + memset(&stream, 0, sizeof(stream)); + memset(&sess, 0, sizeof(sess)); + + TAILQ_INIT(&sess.wts_in_dgq); + wt_if.wti_on_datagram_read = wt_test_on_datagram_read; + sess.wts_if = &wt_if; + sess.wts_sess_ctx = (lsquic_wt_session_ctx_t *) &result; + sess.wts_conn = &conn; + sess.wts_conn_pub = &conn_pub; + sess.wts_control_stream = &stream; + sess.wts_stream_id = 0; + sess.wts_dgq_max_count = LSQUIC_WTAP_MAX_DATAGRAM_QUEUE_COUNT_DEFAULT; + sess.wts_dgq_max_bytes = LSQUIC_WTAP_MAX_DATAGRAM_QUEUE_BYTES_DEFAULT; + sess.wts_flags |= WTSF_ACCEPT_PENDING; + stream.conn_pub = &conn_pub; + stream.sm_attachment = &sess; + + lsquic_wt_on_http_dg_read(&stream, NULL, dg, sizeof(dg)); + if (called_before) + *called_before = result.called; + + wt_fire_session_open_cb(&sess); + wt_replay_pending_datagrams(&sess); + if (called_after) + *called_after = result.called; + + wt_in_dgq_drop_all(&sess); + return 0; +} + + +int +lsquic_wt_test_pending_datagram_replay_stops_on_close (unsigned *called_after, + int *is_closing) +{ + struct wt_test_datagram_result result; + struct lsquic_webtransport_if wt_if; + struct lsquic_wt_session sess; + static const unsigned char dg1[] = { 'd', '1', }; + static const unsigned char dg2[] = { 'd', '2', }; + + memset(&result, 0, sizeof(result)); + memset(&wt_if, 0, sizeof(wt_if)); + memset(&sess, 0, sizeof(sess)); + + TAILQ_INIT(&sess.wts_in_dgq); + wt_if.wti_on_datagram_read = wt_test_on_datagram_read_and_close; + sess.wts_if = &wt_if; + sess.wts_sess_ctx = (lsquic_wt_session_ctx_t *) &result; + sess.wts_n_streams = 1; + sess.wts_dgq_max_count = LSQUIC_WTAP_MAX_DATAGRAM_QUEUE_COUNT_DEFAULT; + sess.wts_dgq_max_bytes = LSQUIC_WTAP_MAX_DATAGRAM_QUEUE_BYTES_DEFAULT; + assert(0 == wt_in_dgq_enqueue(&sess, dg1, sizeof(dg1))); + assert(0 == wt_in_dgq_enqueue(&sess, dg2, sizeof(dg2))); + + wt_replay_pending_datagrams(&sess); + + if (called_after) + *called_after = result.called; + if (is_closing) + *is_closing = !!(sess.wts_flags & WTSF_CLOSING); + + wt_in_dgq_drop_all(&sess); + free(sess.wts_close_reason); + free(sess.wts_close_buf); + return 0; +} + + +int +lsquic_wt_test_destroy_while_closing (int is_control_stream, unsigned *called, + int *removed) +{ + struct lsquic_wt_session *sess; + struct wt_test_close_result result; + struct lsquic_webtransport_if wt_if; + struct lsquic_conn_public conn_pub; + struct lsquic_stream stream; + + sess = calloc(1, sizeof(*sess)); + if (!sess) + return -1; + + memset(&result, 0, sizeof(result)); + memset(&wt_if, 0, sizeof(wt_if)); + memset(&conn_pub, 0, sizeof(conn_pub)); + memset(&stream, 0, sizeof(stream)); + + TAILQ_INIT(&conn_pub.wt_sessions); + wt_if.wti_on_session_close = wt_test_on_session_close; + sess->wts_if = &wt_if; + sess->wts_sess_ctx = (lsquic_wt_session_ctx_t *) &result; + sess->wts_conn_pub = &conn_pub; + sess->wts_stream_id = 4; + sess->wts_n_streams = 1; + sess->wts_flags = WTSF_CLOSING | WTSF_OPENED; + stream.id = is_control_stream ? 4 : 8; + stream.sm_attachment = sess; + stream.conn_pub = &conn_pub; + if (is_control_stream) + sess->wts_control_stream = &stream; + TAILQ_INSERT_TAIL(&conn_pub.wt_sessions, sess, wts_next); + + wt_on_stream_destroy(&stream); + + if (called) + *called = result.called; + if (removed) + *removed = TAILQ_EMPTY(&conn_pub.wt_sessions); + + return 0; +} + + +int +lsquic_wt_test_stream_switch_failure_restores_state (int *restored_if, + int *restored_ctx, + int *restored_session) +{ + struct lsquic_wt_session sess; + struct lsquic_conn conn; + struct lsquic_conn_public conn_pub; + struct lsquic_stream stream; + const struct lsquic_stream_if *orig_if; + lsquic_stream_ctx_t *orig_ctx; + void *orig_onnew_arg; + static const struct lsquic_stream_if old_if; + int rc; + + memset(&sess, 0, sizeof(sess)); + memset(&conn, 0, sizeof(conn)); + memset(&conn_pub, 0, sizeof(conn_pub)); + memset(&stream, 0, sizeof(stream)); + + orig_if = &old_if; + orig_ctx = (lsquic_stream_ctx_t *) (uintptr_t) 0x1234; + orig_onnew_arg = (void *) (uintptr_t) 0x5678; + + stream.id = 8; + stream.conn_pub = &conn_pub; + stream.stream_if = orig_if; + stream.st_ctx = orig_ctx; + stream.sm_onnew_arg = orig_onnew_arg; + stream.stream_flags = STREAM_ONNEW_DONE; + + sess.wts_stream_id = 4; + conn_pub.lconn = &conn; + sess.wts_data_if.on_new_stream = wt_on_new_stream; + sess.wts_onnew_ctx.sess = &sess; + + s_wt_test_fail_stream_ctx_alloc = 1; + rc = wt_switch_to_data_if(&stream, &sess); + s_wt_test_fail_stream_ctx_alloc = 0; + + if (restored_if) + *restored_if = stream.stream_if == orig_if; + if (restored_ctx) + *restored_ctx = stream.st_ctx == orig_ctx + && stream.sm_onnew_arg == orig_onnew_arg; + if (restored_session) + *restored_session = wt_stream_get_session(&stream) == NULL + && sess.wts_n_streams == 0; + + return rc == -1 ? 0 : -1; +} + + +int +lsquic_wt_test_extra_resp_header_validation (int *null_headers_rejected, + int *zero_len_ok) +{ + struct lsquic_wt_session sess; + struct lsquic_http_headers headers; + struct lsxpack_header header_arr[1]; + static const char empty[] = ""; + int rc; + + memset(&sess, 0, sizeof(sess)); + memset(&headers, 0, sizeof(headers)); + memset(header_arr, 0, sizeof(header_arr)); + + headers.count = 1; + headers.headers = NULL; + rc = wt_copy_extra_resp_headers(&sess, &headers); + if (null_headers_rejected) + *null_headers_rejected = rc != 0; + + headers.headers = header_arr; + lsxpack_header_set_offset2(&header_arr[0], empty, 0, 0, 0, 0); + rc = wt_copy_extra_resp_headers(&sess, &headers); + if (zero_len_ok) + *zero_len_ok = rc == 0 + && sess.wts_extra_resp_headers.headers.count == 1 + && sess.wts_extra_resp_headers.headers.headers != NULL; + + wt_free_extra_resp_headers(&sess); + return 0; +} + + +int +lsquic_wt_test_send_response_rejects_missing_extra_headers (int *rejected) +{ + struct lsquic_stream stream; + struct lsquic_conn conn; + struct lsquic_conn_public conn_pub; + struct lsquic_http_headers headers; + int rc; + + memset(&stream, 0, sizeof(stream)); + memset(&conn, 0, sizeof(conn)); + memset(&conn_pub, 0, sizeof(conn_pub)); + memset(&headers, 0, sizeof(headers)); + + conn_pub.lconn = &conn; + stream.conn_pub = &conn_pub; + headers.count = 1; + headers.headers = NULL; + rc = wt_send_response(&stream, 200, &headers, 0); + if (rejected) + *rejected = rc != 0; + return 0; +} + + +int +lsquic_wt_test_response_header_count_validation (int *negative_rejected, + int *overflow_rejected) +{ + struct lsquic_stream stream; + struct lsquic_conn conn; + struct lsquic_conn_public conn_pub; + struct lsquic_wt_session sess; + struct lsquic_http_headers headers; + int rc; + + memset(&stream, 0, sizeof(stream)); + memset(&conn, 0, sizeof(conn)); + memset(&conn_pub, 0, sizeof(conn_pub)); + memset(&sess, 0, sizeof(sess)); + memset(&headers, 0, sizeof(headers)); + + headers.count = -1; + rc = wt_copy_extra_resp_headers(&sess, &headers); + if (negative_rejected) + *negative_rejected = rc != 0 && errno == EINVAL; + + conn_pub.lconn = &conn; + stream.conn_pub = &conn_pub; + headers.count = INT_MAX; + headers.headers = (struct lsxpack_header *) (uintptr_t) 1; + rc = wt_send_response(&stream, 200, &headers, 0); + if (overflow_rejected) + *overflow_rejected = rc != 0 && errno == EOVERFLOW; + + return 0; +} + + +int +lsquic_wt_test_dgq_overflow_rejected (int incoming, int *overflow_rejected) +{ + struct lsquic_wt_session sess; + static const unsigned char byte = 0; + int rc; + + memset(&sess, 0, sizeof(sess)); + TAILQ_INIT(&sess.wts_dgq); + TAILQ_INIT(&sess.wts_in_dgq); + sess.wts_dgq_max_count = UINT_MAX; + sess.wts_dgq_max_bytes = SIZE_MAX; + + errno = 0; + if (incoming) + rc = wt_in_dgq_enqueue(&sess, &byte, + SIZE_MAX - sizeof(struct wt_dgq_elem) + 1); + else + rc = wt_dgq_enqueue(&sess, &byte, + SIZE_MAX - sizeof(struct wt_dgq_elem) + 1, + LSQWT_DG_FAIL_EAGAIN, + LSQUIC_HTTP_DG_SEND_DEFAULT); + + if (overflow_rejected) + *overflow_rejected = rc != 0 && errno == EOVERFLOW; + + wt_dgq_drop_all(&sess); + wt_in_dgq_drop_all(&sess); + return 0; +} + + +struct wt_test_open_fail_ctx +{ + struct lsquic_conn conn; + struct lsquic_conn_public conn_pub; + struct lsquic_stream stream; +}; + +static struct wt_test_open_fail_ctx *s_wt_test_open_fail_ctx; + +static struct lsquic_stream * +wt_test_make_stream_with_if (struct lsquic_conn *UNUSED_lconn, + const struct lsquic_stream_if *stream_if, + void *stream_if_ctx, lsquic_stream_id_t stream_id) +{ + struct wt_test_open_fail_ctx *ctx = s_wt_test_open_fail_ctx; + + memset(&ctx->stream, 0, sizeof(ctx->stream)); + ctx->stream.id = stream_id; + ctx->stream.conn_pub = &ctx->conn_pub; + ctx->stream.stream_if = stream_if; + ctx->stream.sm_onnew_arg = stream_if_ctx; + ctx->stream.stream_flags = STREAM_ONNEW_DONE; + ctx->stream.st_ctx = stream_if->on_new_stream(stream_if_ctx, &ctx->stream); + ctx->stream.conn_pub = NULL; + return &ctx->stream; +} + +static struct lsquic_stream * +wt_test_make_uni_stream_with_if (struct lsquic_conn *lconn, + const struct lsquic_stream_if *stream_if, + void *stream_if_ctx) +{ + return wt_test_make_stream_with_if(lconn, stream_if, stream_if_ctx, 2); +} + +static struct lsquic_stream * +wt_test_make_bidi_stream_with_if (struct lsquic_conn *lconn, + const struct lsquic_stream_if *stream_if, + void *stream_if_ctx) +{ + return wt_test_make_stream_with_if(lconn, stream_if, stream_if_ctx, 0); +} + +static size_t +wt_test_get_max_datagram_size (struct lsquic_conn *UNUSED_lconn) +{ + return 16; +} + +int +lsquic_wt_test_open_stream_init_failure (int bidi, int *aborted, + int *freed_dynamic_onnew) +{ + struct wt_test_open_fail_ctx ctx; + struct lsquic_wt_session sess; + static const struct conn_iface conn_iface = { + .ci_make_uni_stream_with_if = wt_test_make_uni_stream_with_if, + .ci_make_bidi_stream_with_if = wt_test_make_bidi_stream_with_if, + }; + struct lsquic_stream *stream; + + memset(&ctx, 0, sizeof(ctx)); + memset(&sess, 0, sizeof(sess)); + ctx.conn.cn_if = &conn_iface; + ctx.conn_pub.lconn = &ctx.conn; + sess.wts_conn = &ctx.conn; + sess.wts_conn_pub = &ctx.conn_pub; + sess.wts_stream_id = 4; + sess.wts_data_if.on_new_stream = wt_on_new_stream; + sess.wts_onnew_ctx.sess = &sess; + s_wt_test_open_fail_ctx = &ctx; + s_wt_test_fail_stream_ctx_alloc = 1; + s_wt_test_aborted_outgoing_stream = 0; + s_wt_test_freed_dynamic_onnew = 0; + + if (bidi) + stream = lsquic_wt_open_bidi(&sess); + else + stream = lsquic_wt_open_uni(&sess); + + s_wt_test_open_fail_ctx = NULL; + s_wt_test_fail_stream_ctx_alloc = 0; + if (aborted) + *aborted = s_wt_test_aborted_outgoing_stream == 1; + if (freed_dynamic_onnew) + *freed_dynamic_onnew = s_wt_test_freed_dynamic_onnew == 1; + + return stream == NULL && errno == ENOMEM ? 0 : -1; +} + + +int +lsquic_wt_test_datagram_write_state_rollback (int *want_flag_cleared, + int *send_disarmed) +{ + struct lsquic_wt_session sess; + struct lsquic_stream stream; + struct lsquic_conn conn; + struct lsquic_conn_public conn_pub; + static const struct conn_iface conn_iface = { + .ci_get_max_datagram_size = wt_test_get_max_datagram_size, + }; + static const unsigned char byte = 0; + ssize_t nw; + int rc; + + memset(&sess, 0, sizeof(sess)); + memset(&stream, 0, sizeof(stream)); + memset(&conn, 0, sizeof(conn)); + memset(&conn_pub, 0, sizeof(conn_pub)); + TAILQ_INIT(&sess.wts_dgq); + conn.cn_if = &conn_iface; + conn_pub.lconn = &conn; + conn_pub.cp_flags = CP_HTTP_DATAGRAMS; + stream.id = 0; + stream.conn_pub = &conn_pub; + sess.wts_control_stream = &stream; + + s_wt_test_dg_write_stub_active = 1; + s_wt_test_dg_write_arm_calls = 0; + s_wt_test_dg_write_disarm_calls = 0; + s_wt_test_dg_write_arm_result = -1; + errno = 0; + rc = lsquic_wt_want_datagram_write(&sess, 1); + if (want_flag_cleared) + *want_flag_cleared = rc != 0 + && errno == EIO + && !(sess.wts_flags & WTSF_WANT_DG_WRITE) + && s_wt_test_dg_write_arm_calls == 1 + && s_wt_test_dg_write_disarm_calls == 0; + + sess.wts_dgq_max_count = 0; + sess.wts_dgq_max_bytes = 0; + s_wt_test_dg_write_arm_calls = 0; + s_wt_test_dg_write_disarm_calls = 0; + s_wt_test_dg_write_arm_result = 0; + errno = 0; + nw = lsquic_wt_send_datagram_ex(&sess, &byte, sizeof(byte), + LSQWT_DG_FAIL_EAGAIN, + LSQUIC_HTTP_DG_SEND_DEFAULT); + if (send_disarmed) + *send_disarmed = nw < 0 + && errno == EAGAIN + && s_wt_test_dg_write_arm_calls == 1 + && s_wt_test_dg_write_disarm_calls == 1 + && sess.wts_dgq_count == 0; + + s_wt_test_dg_write_stub_active = 0; + wt_dgq_drop_all(&sess); + return 0; +} + + +enum wt_test_http_dg_write_flags +{ + WT_TEST_HTTP_DG_WRITE_PREQUEUE = 1 << 0, + WT_TEST_HTTP_DG_WRITE_WANT = 1 << 1, + WT_TEST_HTTP_DG_WRITE_CB_QUEUE = 1 << 2, + WT_TEST_HTTP_DG_WRITE_CB_FAIL = 1 << 3, + WT_TEST_HTTP_DG_WRITE_CB_CLOSE = 1 << 4, + WT_TEST_HTTP_DG_WRITE_CONSUME_FAIL = 1 << 5, + WT_TEST_HTTP_DG_WRITE_DATAGRAM_MODE = 1 << 6, +}; + + +struct wt_test_http_dg_write_ctx +{ + unsigned flags; + const unsigned char *buf; + size_t len; + unsigned callback_calls; + unsigned consume_calls; +}; + + +static int +wt_test_http_dg_consume (struct lsquic_stream *UNUSED_stream, const void *buf, + size_t len, enum lsquic_http_dg_send_mode UNUSED_mode) +{ + struct wt_test_http_dg_write_ctx *ctx = s_wt_test_http_dg_write_ctx; + + if (ctx) + ++ctx->consume_calls; + (void) buf; + (void) len; + if (ctx && (ctx->flags & WT_TEST_HTTP_DG_WRITE_CONSUME_FAIL)) + { + errno = EIO; + return -1; + } + return 0; +} + + +static int +wt_test_on_datagram_write (lsquic_wt_session_t *sess, size_t UNUSED_max_payload) +{ + struct wt_test_http_dg_write_ctx *ctx = s_wt_test_http_dg_write_ctx; + enum lsquic_http_dg_send_mode mode; + static const unsigned char zero = 0; + + if (ctx) + ++ctx->callback_calls; + + if (ctx && (ctx->flags & WT_TEST_HTTP_DG_WRITE_CB_QUEUE)) + { + mode = (ctx->flags & WT_TEST_HTTP_DG_WRITE_DATAGRAM_MODE) + ? LSQUIC_HTTP_DG_SEND_DATAGRAM + : LSQUIC_HTTP_DG_SEND_DEFAULT; + if (0 != wt_dgq_enqueue((struct lsquic_wt_session *) sess, + ctx->buf ? (const void *) ctx->buf : &zero, + ctx->len > 0 ? ctx->len : 1, LSQWT_DG_FAIL_EAGAIN, mode)) + return -1; + } + + if (ctx && (ctx->flags & WT_TEST_HTTP_DG_WRITE_CB_CLOSE)) + wt_begin_close((struct lsquic_wt_session *) sess, 0, NULL, 0); + + if (ctx && (ctx->flags & WT_TEST_HTTP_DG_WRITE_CB_FAIL)) + { + errno = EIO; + return -1; + } + + return 0; +} + + +int +lsquic_wt_test_http_dg_write_path (unsigned flags, const unsigned char *buf, + size_t len, size_t max_quic_payload, + unsigned *consume_calls, + unsigned *callback_calls, + unsigned *queued_after, + int *want_flag_set, int *is_closing, + unsigned *disarm_calls, int *saved_errno) +{ + struct wt_test_http_dg_write_ctx ctx; + struct lsquic_webtransport_if wt_if; + struct lsquic_wt_session sess; + struct lsquic_stream stream; + struct lsquic_conn conn; + struct lsquic_conn_public conn_pub; + static const struct conn_iface conn_iface = { + .ci_get_max_datagram_size = wt_test_get_max_datagram_size, + }; + static const unsigned char zero = 0; + enum lsquic_http_dg_send_mode mode; + int rc; + + memset(&ctx, 0, sizeof(ctx)); + memset(&wt_if, 0, sizeof(wt_if)); + memset(&sess, 0, sizeof(sess)); + memset(&stream, 0, sizeof(stream)); + memset(&conn, 0, sizeof(conn)); + memset(&conn_pub, 0, sizeof(conn_pub)); + TAILQ_INIT(&sess.wts_dgq); + TAILQ_INIT(&sess.wts_in_dgq); + + ctx.flags = flags; + ctx.buf = buf ? buf : &zero; + ctx.len = len > 0 ? len : 1; + wt_if.wti_on_datagram_write = wt_test_on_datagram_write; + + conn.cn_if = &conn_iface; + conn_pub.lconn = &conn; + conn_pub.cp_flags = CP_HTTP_DATAGRAMS; + stream.id = 0; + stream.conn_pub = &conn_pub; + sess.wts_control_stream = &stream; + sess.wts_conn = &conn; + sess.wts_conn_pub = &conn_pub; + sess.wts_if = &wt_if; + sess.wts_stream_id = 4; + sess.wts_dgq_max_count = UINT_MAX; + sess.wts_dgq_max_bytes = SIZE_MAX; + wt_stream_bind_session(&sess, &stream); + + mode = (flags & WT_TEST_HTTP_DG_WRITE_DATAGRAM_MODE) + ? LSQUIC_HTTP_DG_SEND_DATAGRAM + : LSQUIC_HTTP_DG_SEND_DEFAULT; + if (flags & WT_TEST_HTTP_DG_WRITE_PREQUEUE) + if (0 != wt_dgq_enqueue(&sess, ctx.buf, ctx.len, LSQWT_DG_FAIL_EAGAIN, + mode)) + return -1; + + if (flags & WT_TEST_HTTP_DG_WRITE_WANT) + sess.wts_flags |= WTSF_WANT_DG_WRITE; + + s_wt_test_dg_write_stub_active = 1; + s_wt_test_dg_write_arm_result = 0; + s_wt_test_dg_write_arm_calls = 0; + s_wt_test_dg_write_disarm_calls = 0; + s_wt_test_http_dg_write_ctx = &ctx; + + errno = 0; + rc = lsquic_wt_on_http_dg_write(&stream, NULL, max_quic_payload, + wt_test_http_dg_consume); + + if (consume_calls) + *consume_calls = ctx.consume_calls; + if (callback_calls) + *callback_calls = ctx.callback_calls; + if (queued_after) + *queued_after = sess.wts_dgq_count; + if (want_flag_set) + *want_flag_set = !!(sess.wts_flags & WTSF_WANT_DG_WRITE); + if (is_closing) + *is_closing = !!(sess.wts_flags & WTSF_CLOSING); + if (disarm_calls) + *disarm_calls = s_wt_test_dg_write_disarm_calls; + if (saved_errno) + *saved_errno = rc < 0 ? errno : 0; + + s_wt_test_http_dg_write_ctx = NULL; + s_wt_test_dg_write_stub_active = 0; + wt_dgq_drop_all(&sess); + wt_in_dgq_drop_all(&sess); + free(sess.wts_close_buf); + free(sess.wts_close_reason); + return rc; +} + + +int +lsquic_wt_test_read_error_closes_stream (int *control_closed, + int *uni_closed) +{ + struct lsquic_conn conn; + struct lsquic_conn_public conn_pub; + struct lsquic_stream stream; + + memset(&conn, 0, sizeof(conn)); + memset(&conn_pub, 0, sizeof(conn_pub)); + memset(&stream, 0, sizeof(stream)); + conn_pub.lconn = &conn; + stream.id = 0; + stream.conn_pub = &conn_pub; + s_wt_test_stub_read_error_close = 1; + s_wt_test_read_error_close_calls = 0; + + wt_close_stream_after_read_error(&stream, "control"); + if (control_closed) + *control_closed = s_wt_test_read_error_close_calls == 1; + + wt_close_stream_after_read_error(&stream, "uni"); + if (uni_closed) + *uni_closed = s_wt_test_read_error_close_calls == 2; + + s_wt_test_stub_read_error_close = 0; + return 0; +} + + +int +lsquic_wt_test_write_error_closes_stream (int *control_closed, + int *data_closed) +{ + struct lsquic_conn conn; + struct lsquic_conn_public conn_pub; + struct lsquic_stream stream; + + memset(&conn, 0, sizeof(conn)); + memset(&conn_pub, 0, sizeof(conn_pub)); + memset(&stream, 0, sizeof(stream)); + conn_pub.lconn = &conn; + stream.id = 0; + stream.conn_pub = &conn_pub; + s_wt_test_stub_write_error_close = 1; + s_wt_test_write_error_close_calls = 0; + + wt_close_stream_after_write_error(&stream, "control"); + if (control_closed) + *control_closed = s_wt_test_write_error_close_calls == 1; + + wt_close_stream_after_write_error(&stream, "data"); + if (data_closed) + *data_closed = s_wt_test_write_error_close_calls == 2; + + s_wt_test_stub_write_error_close = 0; + return 0; +} + + +int +lsquic_wt_test_control_stream_ops_rejected (unsigned *mask) +{ + struct lsquic_wt_session sess; + struct lsquic_stream stream; + unsigned bits; + + memset(&sess, 0, sizeof(sess)); + memset(&stream, 0, sizeof(stream)); + sess.wts_control_stream = &stream; + wt_stream_bind_session(&sess, &stream); + bits = 0; + + errno = 0; + if (-1 == lsquic_wt_stream_reset(&stream, 0) && errno == EINVAL) + bits |= 1u << 0; + + errno = 0; + if (-1 == lsquic_wt_stream_stop_sending(&stream, 0) && errno == EINVAL) + bits |= 1u << 1; + + if (mask) + *mask = bits; + return 0; +} + + +#endif + +int +lsquic_stream_is_webtransport_session (const struct lsquic_stream *stream) +{ + return lsquic_stream_is_session_stream(stream); +} + + +int +lsquic_stream_is_webtransport_client_bidi_stream ( + const struct lsquic_stream *stream) +{ + return lsquic_stream_is_switch_client_bidi(stream); +} + + +int +lsquic_stream_get_webtransport_session_stream_id ( + const struct lsquic_stream *stream, + lsquic_stream_id_t *stream_id) +{ + return lsquic_stream_get_switch_stream_id(stream, stream_id); +} + + +static struct lsquic_wt_session * +wt_stream_get_session (const struct lsquic_stream *stream) +{ + return lsquic_stream_get_attachment(stream); +} + + +static void +wt_stream_set_session (struct lsquic_stream *stream, + struct lsquic_wt_session *session) +{ + lsquic_stream_set_attachment(stream, session); +} + + +static int +wt_session_is_opened (const struct lsquic_wt_session *sess) +{ + return !!(sess->wts_flags & WTSF_OPENED); +} + + +static void +wt_build_prefix (unsigned char *buf, size_t *len, uint64_t first, + lsquic_stream_id_t sess_id) +{ + uint64_t bits; + size_t off; + + bits = vint_val2bits(first); + off = 1u << bits; + vint_write(buf, first, bits, off); + + bits = vint_val2bits(sess_id); + vint_write(buf + off, sess_id, bits, 1u << bits); + off += 1u << bits; + + *len = off; +} + + +static int +wt_dgq_has_room_ex (unsigned count, size_t bytes, unsigned max_count, + size_t max_bytes, size_t len) +{ + return count < max_count + && len <= max_bytes + && bytes <= max_bytes - len; +} + + +static void +wt_dgq_drop_head_ex (struct lsquic_wt_session *sess, struct wt_dgq_head *head, + unsigned *count, size_t *bytes, const char *label, + const char *reason) +{ + WT_SET_CONN_FROM_SESSION(sess); + struct wt_dgq_elem *elem; + + elem = TAILQ_FIRST(head); + if (!elem) + return; + + TAILQ_REMOVE(head, elem, next); + assert(*count > 0); + assert(*bytes >= elem->len); + --*count; + *bytes -= elem->len; + + LSQ_INFO("drop queued WT %s for session %"PRIu64 + " (%s, len=%zu, queued=%u/%zu)", label, sess->wts_stream_id, + reason ? reason : "unknown", elem->len, *count, *bytes); + + free(elem); +} + + +static void +wt_dgq_drop_head (struct lsquic_wt_session *sess, const char *reason) +{ + wt_dgq_drop_head_ex(sess, &sess->wts_dgq, &sess->wts_dgq_count, + &sess->wts_dgq_bytes, "datagram", reason); +} + + +static void +wt_in_dgq_drop_head (struct lsquic_wt_session *sess, const char *reason) +{ + wt_dgq_drop_head_ex(sess, &sess->wts_in_dgq, &sess->wts_in_dgq_count, + &sess->wts_in_dgq_bytes, "incoming datagram", reason); +} + + +static void +wt_dgq_drop_all (struct lsquic_wt_session *sess) +{ + while (!TAILQ_EMPTY(&sess->wts_dgq)) + wt_dgq_drop_head(sess, "session closed"); +} + + +static void +wt_in_dgq_drop_all (struct lsquic_wt_session *sess) +{ + while (!TAILQ_EMPTY(&sess->wts_in_dgq)) + wt_in_dgq_drop_head(sess, "session closed"); +} + + +static int +wt_dgq_arm_write (struct lsquic_wt_session *sess) +{ + WT_SET_CONN_FROM_SESSION(sess); + int rc; + +#if LSQUIC_TEST + if (s_wt_test_dg_write_stub_active) + { + ++s_wt_test_dg_write_arm_calls; + if (s_wt_test_dg_write_arm_result < 0) + errno = EIO; + return s_wt_test_dg_write_arm_result; + } +#endif + rc = lsquic_stream_want_http_dg_write(sess->wts_control_stream, 1); + if (rc < 0) + LSQ_WARN("cannot arm WT datagram write on stream %"PRIu64": %s", + lsquic_stream_id(sess->wts_control_stream), strerror(errno)); + return rc; +} + + +static int +wt_dgq_disarm_write (struct lsquic_stream *stream) +{ +#if LSQUIC_TEST + if (s_wt_test_dg_write_stub_active) + { + ++s_wt_test_dg_write_disarm_calls; + return 0; + } +#endif + return lsquic_stream_want_http_dg_write(stream, 0); +} + + +static int +wt_dgq_enqueue (struct lsquic_wt_session *sess, const void *buf, size_t len, + enum lsquic_wt_dg_drop_policy policy, + enum lsquic_http_dg_send_mode mode) +{ + WT_SET_CONN_FROM_SESSION(sess); + struct wt_dgq_elem *elem; + + if (policy == LSQWT_DG_DROP_OLDEST) + while (!wt_dgq_has_room_ex(sess->wts_dgq_count, sess->wts_dgq_bytes, + sess->wts_dgq_max_count, + sess->wts_dgq_max_bytes, len) + && !TAILQ_EMPTY(&sess->wts_dgq)) + wt_dgq_drop_head(sess, "policy=drop-oldest"); + + if (!wt_dgq_has_room_ex(sess->wts_dgq_count, sess->wts_dgq_bytes, + sess->wts_dgq_max_count, sess->wts_dgq_max_bytes, + len)) + { + if (policy == LSQWT_DG_DROP_NEWEST) + LSQ_INFO("drop newest WT datagram in session %"PRIu64 + " (len=%zu, queued=%u/%zu)", sess->wts_stream_id, len, + sess->wts_dgq_count, sess->wts_dgq_bytes); + else + LSQ_DEBUG("WT datagram queue full in session %"PRIu64 + " (len=%zu, queued=%u/%zu)", sess->wts_stream_id, len, + sess->wts_dgq_count, sess->wts_dgq_bytes); + errno = EAGAIN; + return -1; + } + + elem = wt_dgq_elem_alloc(len); + if (!elem) + return -1; + + memcpy(elem->buf, buf, len); + elem->len = len; + elem->mode = mode; + TAILQ_INSERT_TAIL(&sess->wts_dgq, elem, next); + ++sess->wts_dgq_count; + sess->wts_dgq_bytes += len; + + LSQ_DEBUG("queued WT datagram in session %"PRIu64 + " (len=%zu, queued=%u/%zu)", sess->wts_stream_id, len, + sess->wts_dgq_count, sess->wts_dgq_bytes); + return 0; +} + + +static int +wt_in_dgq_enqueue (struct lsquic_wt_session *sess, const void *buf, size_t len) +{ + WT_SET_CONN_FROM_SESSION(sess); + struct wt_dgq_elem *elem; + const enum lsquic_wt_dg_drop_policy policy = sess->wts_dg_policy; + + if (policy == LSQWT_DG_DROP_OLDEST) + while (!wt_dgq_has_room_ex(sess->wts_in_dgq_count, + sess->wts_in_dgq_bytes, + sess->wts_dgq_max_count, + sess->wts_dgq_max_bytes, len) + && !TAILQ_EMPTY(&sess->wts_in_dgq)) + wt_in_dgq_drop_head(sess, "policy=drop-oldest"); + + if (!wt_dgq_has_room_ex(sess->wts_in_dgq_count, sess->wts_in_dgq_bytes, + sess->wts_dgq_max_count, sess->wts_dgq_max_bytes, + len)) + { + LSQ_INFO("drop pending incoming WT datagram in session %"PRIu64 + " (policy=%s, len=%zu, queued=%u/%zu)", + sess->wts_stream_id, + policy == LSQWT_DG_DROP_NEWEST ? "drop-newest" + : "fail-eagain", + len, sess->wts_in_dgq_count, sess->wts_in_dgq_bytes); + errno = EAGAIN; + return -1; + } + + elem = wt_dgq_elem_alloc(len); + if (!elem) + return -1; + + memcpy(elem->buf, buf, len); + elem->len = len; + elem->mode = LSQUIC_HTTP_DG_SEND_DEFAULT; + TAILQ_INSERT_TAIL(&sess->wts_in_dgq, elem, next); + ++sess->wts_in_dgq_count; + sess->wts_in_dgq_bytes += len; + + LSQ_DEBUG("queued pending incoming WT datagram in session %"PRIu64 + " (len=%zu, queued=%u/%zu)", sess->wts_stream_id, len, + sess->wts_in_dgq_count, sess->wts_in_dgq_bytes); + return 0; +} + + +static int +wt_dgq_send_one (struct lsquic_wt_session *sess, struct lsquic_stream *stream, + size_t max_quic_payload, + lsquic_http_dg_consume_f consume_datagram) +{ + WT_SET_CONN_FROM_SESSION(sess); + struct wt_dgq_elem *elem; + int rc; + + elem = TAILQ_FIRST(&sess->wts_dgq); + if (!elem) + return 0; + + rc = consume_datagram(stream, elem->buf, elem->len, elem->mode); + if (rc != 0) + { + if (elem->mode == LSQUIC_HTTP_DG_SEND_DATAGRAM + && max_quic_payload > 0 + && elem->len > max_quic_payload) + { + LSQ_INFO("drop queued WT datagram too large for QUIC DATAGRAM " + "in session %"PRIu64" (len=%zu, max=%zu)", + sess->wts_stream_id, elem->len, max_quic_payload); + wt_dgq_drop_head(sess, "too large for QUIC DATAGRAM mode"); + return 1; + } + LSQ_WARN("WT datagram consume failed on stream %"PRIu64 + ": %s", lsquic_stream_id(stream), strerror(errno)); + return -1; + } + + TAILQ_REMOVE(&sess->wts_dgq, elem, next); + assert(sess->wts_dgq_count > 0); + assert(sess->wts_dgq_bytes >= elem->len); + --sess->wts_dgq_count; + sess->wts_dgq_bytes -= elem->len; + free(elem); + + return 1; +} + + +static void +wt_stream_bind_session (struct lsquic_wt_session *sess, + struct lsquic_stream *stream) +{ + if (!stream) + return; + + if (wt_stream_get_session(stream) == sess) + return; + + assert(!wt_stream_get_session(stream)); + wt_stream_set_session(stream, sess); + ++sess->wts_n_streams; +} + + +static void +wt_stream_unbind_session (struct lsquic_stream *stream) +{ + struct lsquic_wt_session *sess; + + if (!stream) + return; + + sess = wt_stream_get_session(stream); + if (!sess) + return; + + wt_stream_set_session(stream, NULL); + if (sess->wts_n_streams > 0) + --sess->wts_n_streams; + + if ((sess->wts_flags & WTSF_CLOSING) && sess->wts_n_streams == 0) + wt_session_maybe_finalize(sess); +} + + +static int +wt_switch_to_data_if (struct lsquic_stream *stream, + struct lsquic_wt_session *sess) +{ + WT_SET_CONN_FROM_STREAM(stream); + const struct lsquic_stream_if *orig_if; + lsquic_stream_ctx_t *orig_ctx; + void *orig_onnew_arg; + + orig_if = lsquic_stream_get_stream_if(stream); + orig_ctx = lsquic_stream_get_ctx(stream); + orig_onnew_arg = stream->sm_onnew_arg; + + lsquic_stream_set_stream_if(stream, &sess->wts_data_if, + &sess->wts_onnew_ctx); + if (lsquic_stream_get_ctx(stream)) + return 0; + + stream->stream_if = orig_if; + stream->st_ctx = orig_ctx; + stream->sm_onnew_arg = orig_onnew_arg; + errno = ENOMEM; + LSQ_WARN("failed to switch stream %"PRIu64" into WT session %"PRIu64, + lsquic_stream_id(stream), sess->wts_stream_id); + return -1; +} + + +static void +wt_abort_failed_local_stream (struct lsquic_stream *stream) +{ +#if LSQUIC_TEST + if (stream && !stream->conn_pub) + { + ++s_wt_test_aborted_outgoing_stream; + return; + } +#endif + if (stream) + (void) lsquic_stream_close(stream); +} + + +static lsquic_stream_ctx_t * +wt_on_new_stream (void *ctx, struct lsquic_stream *stream) +{ + WT_SET_CONN_FROM_STREAM(stream); + struct wt_onnew_ctx *const onnew = ctx; + struct lsquic_wt_session *sess; + struct wt_stream_ctx *wctx; + lsquic_stream_ctx_t *app_ctx; + + sess = onnew ? onnew->sess : NULL; + + wctx = wt_stream_ctx_alloc(); + if (!wctx) + { + wt_free_onnew_ctx(onnew); + LSQ_WARN("cannot allocate WT stream ctx for stream %"PRIu64, + lsquic_stream_id(stream)); + return NULL; + } + + wctx->sess = sess; + if (onnew->prefix_len) + { + memcpy(wctx->prefix, onnew->prefix, onnew->prefix_len); + wctx->prefix_len = onnew->prefix_len; + lsquic_stream_set_reset_stream_at_size(stream, (uint8_t) onnew->prefix_len); + } + + /* Mark stream as belonging to the session before calling app hooks: + * app on_new may enable reads and trigger nested readability checks. + */ + wt_stream_bind_session(sess, stream); + + app_ctx = NULL; + if (sess->wts_if) + { + if (lsquic_wt_stream_dir(stream) == LSQWT_UNI) + { + if (sess->wts_if->wti_on_uni_stream) + app_ctx = sess->wts_if->wti_on_uni_stream( + (lsquic_wt_session_t *) sess, stream); + } + else if (sess->wts_if->wti_on_bidi_stream) + app_ctx = sess->wts_if->wti_on_bidi_stream( + (lsquic_wt_session_t *) sess, stream); + } + + wctx->app_ctx = app_ctx; + if (sess->wts_if) + wctx->ss_code = sess->wts_if->wti_on_stream_ss_code; + LSQ_DEBUG("initialized WT stream %"PRIu64" in session %"PRIu64 + " (dir=%s, initiator=%s, prefix_len=%zu)", + lsquic_stream_id(stream), sess->wts_stream_id, + lsquic_wt_stream_dir(stream) == LSQWT_UNI ? "uni" : "bidi", + lsquic_wt_stream_initiator(stream) == LSQWT_SERVER + ? "server" : "client", + wctx->prefix_len); + + wt_free_onnew_ctx(onnew); + + return (lsquic_stream_ctx_t *) wctx; +} + +static void +wt_on_read (struct lsquic_stream *stream, lsquic_stream_ctx_t *sctx) +{ + WT_SET_CONN_FROM_STREAM(stream); + struct wt_stream_ctx *wctx = (struct wt_stream_ctx *) sctx; + + if (!wctx || !wctx->sess || !wctx->sess->wts_if + || !wctx->sess->wts_if->wti_on_stream_read) + { + LSQ_DEBUG("skip WT on_read for stream %"PRIu64": no callback", + lsquic_stream_id(stream)); + return; + } + + LSQ_DEBUG("dispatch WT on_read for stream %"PRIu64" session %"PRIu64, + lsquic_stream_id(stream), wctx->sess->wts_stream_id); + wctx->sess->wts_if->wti_on_stream_read(stream, wctx->app_ctx); +} + +static void +wt_on_write (struct lsquic_stream *stream, lsquic_stream_ctx_t *sctx) +{ + WT_SET_CONN_FROM_STREAM(stream); + struct wt_stream_ctx *wctx = (struct wt_stream_ctx *) sctx; + ssize_t nw; + + if (!wctx || !wctx->sess || !wctx->sess->wts_if) + { + LSQ_DEBUG("skip WT on_write for stream %"PRIu64": no stream ctx", + lsquic_stream_id(stream)); + return; + } + + while (wctx->prefix_off < wctx->prefix_len) + { + nw = lsquic_stream_write(stream, wctx->prefix + wctx->prefix_off, + wctx->prefix_len - wctx->prefix_off); + if (nw < 0) + { + wt_close_stream_after_write_error(stream, "data"); + return; + } + if (nw == 0) + { + LSQ_DEBUG("WT stream %"PRIu64" prefix write blocked/off=%zu/%zu", + lsquic_stream_id(stream), wctx->prefix_off, wctx->prefix_len); + return; + } + wctx->prefix_off += (size_t) nw; + } + + if (wctx->sess->wts_if->wti_on_stream_write) + { + LSQ_DEBUG("dispatch WT on_write for stream %"PRIu64" session %"PRIu64, + lsquic_stream_id(stream), wctx->sess->wts_stream_id); + wctx->sess->wts_if->wti_on_stream_write(stream, wctx->app_ctx); + } +} + +static void +wt_on_close (struct lsquic_stream *stream, lsquic_stream_ctx_t *sctx) +{ + WT_SET_CONN_FROM_STREAM(stream); + struct wt_stream_ctx *wctx = (struct wt_stream_ctx *) sctx; + + LSQ_DEBUG("WT stream %"PRIu64" closed", lsquic_stream_id(stream)); + if (wctx && wctx->sess && wctx->sess->wts_if + && wctx->sess->wts_if->wti_on_stream_close) + wctx->sess->wts_if->wti_on_stream_close(stream, wctx->app_ctx); + + free(wctx); +} + +static void +wt_on_reset (struct lsquic_stream *stream, lsquic_stream_ctx_t *sctx, int how) +{ + WT_SET_CONN_FROM_STREAM(stream); + struct wt_stream_ctx *wctx; + + wctx = (struct wt_stream_ctx *) sctx; + wt_on_reset_core(stream, wctx, how, conn); +} + + +static void +wt_on_reset_core (struct lsquic_stream *stream, struct wt_stream_ctx *wctx, + int how, struct lsquic_conn *conn) +{ + enum wt_reset_event_mask + { + WT_REM_STREAM_RESET = 1 << 0, + WT_REM_STOP_SENDING = 1 << 1, + }; + unsigned evmask; + uint64_t h3_error_code, wt_error_code; + + if (!stream || !wctx) + return; + if (!wctx->sess || !wctx->sess->wts_if) + return; + + switch (how) + { + case 0: + evmask = WT_REM_STREAM_RESET; + break; + case 1: + evmask = WT_REM_STOP_SENDING; + break; + case 2: + evmask = WT_REM_STREAM_RESET | WT_REM_STOP_SENDING; + break; + default: + evmask = 0; + break; + } + + if (!evmask) + { + LSQ_DEBUG("WT stream reset callback got unsupported reset kind %d", how); + return; + } + + if ((evmask & WT_REM_STREAM_RESET) + && wctx->sess->wts_if->wti_on_stream_reset) + { + h3_error_code = stream->sm_rst_in_code; + if (0 == wt_h3_error_to_app_error(h3_error_code, &wt_error_code)) + h3_error_code = wt_error_code; + wctx->sess->wts_if->wti_on_stream_reset(stream, wctx->app_ctx, + h3_error_code); + } + + if ((evmask & WT_REM_STOP_SENDING) + && wctx->sess->wts_if->wti_on_stop_sending) + { + h3_error_code = stream->sm_ss_in_code; + if (!lsquic_stream_is_rejected(stream)) + h3_error_code = stream->sm_rst_in_code; + if (0 == wt_h3_error_to_app_error(h3_error_code, &wt_error_code)) + h3_error_code = wt_error_code; + wctx->sess->wts_if->wti_on_stop_sending(stream, wctx->app_ctx, + h3_error_code); + } +} + + +static size_t +wt_control_drain_readf (void *ctx, const unsigned char *buf, size_t sz, int fin) +{ + int *saw_data = ctx; + + (void) fin; + if (sz > 0) + *saw_data = 1; + return sz; +} + + +/* [draft-ietf-webtrans-http3-15], Section 6 */ +static void +wt_control_on_read (struct lsquic_stream *stream, lsquic_stream_ctx_t *sctx) +{ + struct lsquic_wt_session *sess; + int saw_data; + ssize_t nread; + + (void) sctx; + sess = wt_stream_get_session(stream); + if (!sess) + return; + + saw_data = 0; + nread = lsquic_stream_readf(stream, wt_control_drain_readf, &saw_data); + if (nread > 0) + { + if ((sess->wts_flags & WTSF_CLOSE_RCVD) && saw_data) + wt_abort_connect_message_error(stream, + "received data after WT_CLOSE_SESSION on CONNECT stream"); + return; + } + else if (nread < 0) + { + wt_close_stream_after_read_error(stream, "control"); + return; + } + + if (0 == nread) + { + (void) lsquic_stream_shutdown(stream, 0); + if (sess->wts_flags & WTSF_ACCEPT_PENDING) + wt_reject_session(sess, 0, NULL, 0); + else + wt_close_remote(sess, 0, NULL, 0, 0); + } +} + + +static void +wt_control_on_write (struct lsquic_stream *stream, lsquic_stream_ctx_t *sctx) +{ + struct lsquic_wt_session *sess; + const struct wt_control_ctx *control_ctx; + ssize_t nw; + + sess = wt_stream_get_session(stream); + if (!sess) + return; + + control_ctx = &sess->wts_control_ctx; + if (sess->wts_flags & WTSF_CLOSE_CAPSULE_PENDING) + { + while (sess->wts_close_buf_off < sess->wts_close_buf_len) + { + nw = lsquic_stream_write(stream, + sess->wts_close_buf + sess->wts_close_buf_off, + sess->wts_close_buf_len - sess->wts_close_buf_off); + if (nw < 0) + { + wt_close_stream_after_write_error(stream, "control"); + return; + } + if (nw == 0) + return; + sess->wts_close_buf_off += (size_t) nw; + } + + free(sess->wts_close_buf); + sess->wts_close_buf = NULL; + sess->wts_close_buf_len = 0; + sess->wts_close_buf_off = 0; + sess->wts_flags &= ~WTSF_CLOSE_CAPSULE_PENDING; + sess->wts_flags |= WTSF_CLOSE_SENT; + /* + * [draft-ietf-webtrans-http3-15], Section 6 says the endpoint MAY + * send STOP_SENDING on the CONNECT stream here. Do not exercise + * this yet: incoming STOP_SENDING is processed eagerly in stream + * code, which can preempt delivery of WT_CLOSE_SESSION. + */ + if (0) + { + lsquic_stream_set_ss_code(stream, HEC_WT_SESSION_GONE); + (void) lsquic_stream_shutdown(stream, 0); + } + (void) lsquic_stream_shutdown(stream, 1); + return; + } + + if (!(sess->wts_flags & WTSF_CLOSING) + && control_ctx->wtcc_orig_if && control_ctx->wtcc_orig_if->on_write) + control_ctx->wtcc_orig_if->on_write(stream, sctx); +} + + +static lsquic_stream_ctx_t * +wt_control_on_new (void *ctx, struct lsquic_stream *stream) +{ + WT_SET_CONN_FROM_STREAM(stream); + const struct wt_control_ctx *const control_ctx = ctx; + + if (!control_ctx) + return NULL; + + LSQ_DEBUG("WT control stream %"PRIu64" switched to wrapper stream_if", + lsquic_stream_id(stream)); + return control_ctx->wtcc_orig_ctx; +} + + +static void +wt_control_on_close (struct lsquic_stream *stream, lsquic_stream_ctx_t *sctx) +{ + WT_SET_CONN_FROM_STREAM(stream); + struct lsquic_wt_session *sess; + const struct wt_control_ctx *control_ctx; + + sess = wt_stream_get_session(stream); + if (!sess) + return; + + control_ctx = &sess->wts_control_ctx; + if (control_ctx->wtcc_orig_if && control_ctx->wtcc_orig_if->on_close) + control_ctx->wtcc_orig_if->on_close(stream, sctx); + + LSQ_INFO("WT control stream %"PRIu64" closed; close session %"PRIu64, + lsquic_stream_id(stream), sess->wts_stream_id); + if (sess->wts_flags & WTSF_ACCEPT_PENDING) + wt_reject_session(sess, 0, NULL, 0); + else if (!(sess->wts_flags & WTSF_CLOSING)) + wt_close_remote(sess, 0, NULL, 0, 0); +} + + +static void +wt_control_on_reset (struct lsquic_stream *stream, lsquic_stream_ctx_t *sctx, + int how) +{ + WT_SET_CONN_FROM_STREAM(stream); + struct lsquic_wt_session *sess; + const struct wt_control_ctx *control_ctx; + + sess = wt_stream_get_session(stream); + if (!sess) + return; + + control_ctx = &sess->wts_control_ctx; + if (control_ctx->wtcc_orig_if && control_ctx->wtcc_orig_if->on_reset) + control_ctx->wtcc_orig_if->on_reset(stream, sctx, how); + + LSQ_INFO("WT control stream %"PRIu64" reset (how=%d); close session " + "%"PRIu64, lsquic_stream_id(stream), how, sess->wts_stream_id); + if (sess->wts_flags & WTSF_ACCEPT_PENDING) + wt_reject_session(sess, 0, NULL, 0); + else if (0 == how && !(sess->wts_flags & WTSF_CLOSING)) + wt_close_remote(sess, 0, NULL, 0, 0); +} + + +static int +wt_wrap_control_stream (struct lsquic_wt_session *sess, + struct lsquic_stream *stream) +{ + WT_SET_CONN_FROM_STREAM(stream); + const struct lsquic_stream_if *orig_if; + + orig_if = lsquic_stream_get_stream_if(stream); + if (!orig_if) + { + errno = EINVAL; + LSQ_WARN("cannot wrap WT control stream %"PRIu64": stream_if is NULL", + lsquic_stream_id(stream)); + return -1; + } + + sess->wts_control_ctx.wtcc_orig_if = orig_if; + sess->wts_control_ctx.wtcc_orig_ctx = lsquic_stream_get_ctx(stream); + sess->wts_control_if = *orig_if; + sess->wts_control_if.on_new_stream = wt_control_on_new; + sess->wts_control_if.on_read = wt_control_on_read; + sess->wts_control_if.on_write = wt_control_on_write; + sess->wts_control_if.on_close = wt_control_on_close; + sess->wts_control_if.on_reset = wt_control_on_reset; + + lsquic_stream_set_stream_if(stream, &sess->wts_control_if, + &sess->wts_control_ctx); + lsquic_stream_wantread(stream, 1); + return 0; +} + + +#if LSQUIC_TEST +struct wt_test_reset_result +{ + unsigned called; + uint64_t reset_code; + uint64_t stop_code; +}; + + +static void +wt_test_on_stream_reset (struct lsquic_stream *UNUSED_stream, + struct lsquic_stream_ctx *sctx, uint64_t error_code) +{ + struct wt_test_reset_result *result; + + result = (struct wt_test_reset_result *) sctx; + result->called |= 1; + result->reset_code = error_code; +} + + +static void +wt_test_on_stop_sending (struct lsquic_stream *UNUSED_stream, + struct lsquic_stream_ctx *sctx, uint64_t error_code) +{ + struct wt_test_reset_result *result; + + result = (struct wt_test_reset_result *) sctx; + result->called |= 2; + result->stop_code = error_code; +} + + +int +lsquic_wt_test_dispatch_reset (int how, int ss_received, int with_ctx, + int with_if, uint64_t rst_in_code, + uint64_t ss_in_code, unsigned *called, + uint64_t *reset_code, uint64_t *stop_code) +{ + struct wt_test_reset_result result; + struct lsquic_stream stream; + struct wt_stream_ctx wctx; + struct lsquic_wt_session sess; + struct lsquic_webtransport_if wt_if; + + memset(&result, 0, sizeof(result)); + memset(&stream, 0, sizeof(stream)); + memset(&wctx, 0, sizeof(wctx)); + memset(&sess, 0, sizeof(sess)); + memset(&wt_if, 0, sizeof(wt_if)); + + if (with_if) + { + wt_if.wti_on_stream_reset = wt_test_on_stream_reset; + wt_if.wti_on_stop_sending = wt_test_on_stop_sending; + sess.wts_if = &wt_if; + } + + stream.sm_rst_in_code = rst_in_code; + stream.sm_ss_in_code = ss_in_code; + if (ss_received) + lsquic_stream_mark_rejected(&stream); + wctx.sess = &sess; + wctx.app_ctx = (lsquic_stream_ctx_t *) &result; + + if (with_ctx) + wt_on_reset_core(&stream, &wctx, how, NULL); + else + wt_on_reset_core(&stream, NULL, how, NULL); + + if (called) + *called = result.called; + if (reset_code) + *reset_code = result.reset_code; + if (stop_code) + *stop_code = result.stop_code; + + return 0; +} +#endif + +static lsquic_stream_ctx_t * +wt_uni_on_new (void *UNUSED_ctx, struct lsquic_stream *stream) +{ + WT_SET_CONN_FROM_STREAM(stream); + struct wt_uni_read_ctx *uctx; + + uctx = wt_uni_read_ctx_alloc(); + if (!uctx) + { + errno = ENOMEM; + LSQ_WARN("cannot allocate WT uni read ctx for stream %"PRIu64, + lsquic_stream_id(stream)); + return NULL; + } + + lsquic_stream_wantread(stream, 1); + LSQ_DEBUG("initialized WT uni reader on stream %"PRIu64, + lsquic_stream_id(stream)); + return (lsquic_stream_ctx_t *) uctx; +} + +static size_t +wt_uni_readf (void *ctx, const unsigned char *buf, size_t sz, int fin) +{ + struct wt_uni_read_ctx *uctx = ctx; + const unsigned char *p = buf; + const unsigned char *const end = buf + sz; + int s; + + if (uctx->done) + return 0; + + s = lsquic_varint_read_nb(&p, end, &uctx->state); + if (s == 0) + { + uctx->sess_id = uctx->state.val; + uctx->done = 1; + return (size_t) (p - buf); + } + else if (fin) + { + uctx->done = 1; + uctx->malformed = 1; + return (size_t) (p - buf); + } + else + return (size_t) (p - buf); +} + +static void +wt_uni_on_read (struct lsquic_stream *stream, lsquic_stream_ctx_t *sctx) +{ + WT_SET_CONN_FROM_STREAM(stream); + struct wt_uni_read_ctx *uctx = (struct wt_uni_read_ctx *) sctx; + struct lsquic_wt_session *sess; + ssize_t nread; + + if (!uctx || uctx->done) + return; + + nread = lsquic_stream_readf(stream, wt_uni_readf, uctx); + if (nread < 0) + { + wt_close_stream_after_read_error(stream, "uni"); + return; + } + + if (!uctx->done) + return; + + if (uctx->malformed) + { + LSQ_INFO("unexpected FIN while reading WT uni session ID from stream " + "%"PRIu64, lsquic_stream_id(stream)); + lsquic_stream_close(stream); + return; + } + + if (0 != lsquic_wt_validate_incoming_session_id(stream, uctx->sess_id, + "uni")) + return; + + sess = wt_session_find(lsquic_stream_get_conn_public(stream), + uctx->sess_id); + if (sess && (sess->wts_flags & WTSF_CLOSING)) + { + wt_close_stream_with_session_gone(stream); + return; + } + + if (!sess || !wt_session_is_opened(sess)) + { + if (0 != wt_buffer_or_reject_stream(stream, uctx->sess_id, LSQWT_UNI)) + return; + + LSQ_INFO("buffered WT uni stream %"PRIu64" for session %"PRIu64, + lsquic_stream_id(stream), (uint64_t) uctx->sess_id); + return; + } + + if (0 != wt_switch_to_data_if(stream, sess)) + { + lsquic_stream_set_ss_code(stream, HEC_INTERNAL_ERROR); + lsquic_stream_close(stream); + return; + } + + free(uctx); + LSQ_DEBUG("mapped WT uni stream %"PRIu64" to session %"PRIu64, + lsquic_stream_id(stream), sess->wts_stream_id); +} + +static void +wt_uni_on_close (struct lsquic_stream *UNUSED_stream, + lsquic_stream_ctx_t *sctx) +{ + WT_SET_CONN_FROM_STREAM(UNUSED_stream); + LSQ_DEBUG("WT uni stream reader closed"); + free(sctx); +} + +static const struct lsquic_stream_if wt_uni_stream_if = +{ + .on_new_stream = wt_uni_on_new, + .on_read = wt_uni_on_read, + .on_close = wt_uni_on_close, +}; + +const struct lsquic_stream_if * +lsquic_wt_uni_stream_if (void) +{ + return &wt_uni_stream_if; +} + + +static int +wt_is_pending_uni_stream (const struct lsquic_stream *stream, + lsquic_stream_id_t *session_id) +{ + const struct wt_uni_read_ctx *uctx; + + if (lsquic_stream_get_stream_if(stream) != &wt_uni_stream_if) + return 0; + + if (wt_stream_get_session(stream)) + return 0; + + uctx = (const struct wt_uni_read_ctx *) lsquic_stream_get_ctx(stream); + if (!uctx || !uctx->done) + return 0; + + if (session_id) + *session_id = uctx->sess_id; + + return 1; +} + + +static int +wt_is_pending_bidi_stream (const struct lsquic_stream *stream, + lsquic_stream_id_t *session_id) +{ + lsquic_stream_id_t sid; + + if (!lsquic_stream_is_webtransport_client_bidi_stream(stream)) + return 0; + + if (wt_stream_get_session(stream)) + return 0; + + if (0 != lsquic_stream_get_webtransport_session_stream_id(stream, &sid)) + return 0; + + if (session_id) + *session_id = sid; + + return 1; +} + + +static unsigned +wt_count_pending_streams (struct lsquic_conn_public *conn_pub) +{ + struct lsquic_hash_elem *el; + struct lsquic_stream *stream; + unsigned n_pending; + + n_pending = 0; + for (el = lsquic_hash_first(conn_pub->all_streams); el; + el = lsquic_hash_next(conn_pub->all_streams)) + { + stream = lsquic_hashelem_getdata(el); + if (wt_is_pending_uni_stream(stream, NULL) + || wt_is_pending_bidi_stream(stream, NULL)) + ++n_pending; + } + + return n_pending; +} + + +static unsigned +wt_count_sessions_except (const struct lsquic_conn_public *conn_pub, + const struct lsquic_wt_session *skip) +{ + const struct lsquic_wt_session *sess; + unsigned count; + + count = 0; + TAILQ_FOREACH(sess, &conn_pub->wt_sessions, wts_next) + if (sess != skip) + ++count; + + return count; +} + + +static unsigned +wt_get_session_limit (const struct lsquic_conn_public *conn_pub, + int is_server_stream) +{ + if (is_server_stream + && 0 == conn_pub->enpub->enp_settings.es_max_webtransport_sessions) + return 0; + + /* Until WT flow control lands, draft-15 only allows one session. */ + return 1; +} + + +static enum wt_accept_result +wt_evaluate_accept (struct lsquic_stream *connect_stream, + const struct lsquic_wt_session *sess, + unsigned *status, const char **reason, + size_t *reason_len) +{ + WT_SET_CONN_FROM_STREAM(connect_stream); + struct lsquic_conn_public *conn_pub; + unsigned n_sessions, local_limit; + + conn_pub = lsquic_stream_get_conn_public(connect_stream); + if (!conn_pub) + { + errno = EINVAL; + LSQ_WARN("cannot accept WT stream %"PRIu64": no connection context", + lsquic_stream_id(connect_stream)); + return WT_ACCEPT_REJECT; + } + + n_sessions = wt_count_sessions_except(conn_pub, sess); + if (lsquic_stream_is_server(connect_stream)) + { + if (!conn_pub->enpub->enp_settings.es_webtransport) + { + LSQ_WARN("cannot accept WT stream %"PRIu64": local WT disabled", + lsquic_stream_id(connect_stream)); + if (status) + *status = 400; + if (reason) + *reason = "Peer does not support WebTransport"; + if (reason_len) + *reason_len = sizeof("Peer does not support WebTransport") - 1; + return WT_ACCEPT_REJECT; + } + + local_limit = wt_get_session_limit(conn_pub, 1); + if (local_limit > 0 && n_sessions >= local_limit) + { + LSQ_WARN("cannot accept WT stream %"PRIu64": local session " + "limit reached (%u)", lsquic_stream_id(connect_stream), + local_limit); + if (status) + *status = 429; + if (reason) + *reason = "WebTransport session limit reached"; + if (reason_len) + *reason_len = sizeof("WebTransport session limit reached") - 1; + return WT_ACCEPT_REJECT; + } + } + else + { + local_limit = wt_get_session_limit(conn_pub, 0); + if (local_limit > 0 && n_sessions >= local_limit) + { + LSQ_WARN("cannot accept WT stream %"PRIu64": no-flow-control " + "session limit reached (%u)", + lsquic_stream_id(connect_stream), local_limit); + if (status) + *status = 429; + if (reason) + *reason = "WebTransport session limit reached"; + if (reason_len) + *reason_len = sizeof("WebTransport session limit reached") - 1; + return WT_ACCEPT_REJECT; + } + } + + if (!(conn_pub->cp_flags & CP_H3_PEER_SETTINGS)) + { + LSQ_INFO("defer WT accept on stream %"PRIu64": peer SETTINGS not " + "received yet", lsquic_stream_id(connect_stream)); + return WT_ACCEPT_PENDING; + } + + if (!(conn_pub->cp_flags & CP_WEBTRANSPORT)) + { + LSQ_WARN("cannot accept WT stream %"PRIu64": peer WT support is off", + lsquic_stream_id(connect_stream)); + if (status) + *status = 400; + if (reason) + *reason = "Peer does not support WebTransport"; + if (reason_len) + *reason_len = sizeof("Peer does not support WebTransport") - 1; + return WT_ACCEPT_REJECT; + } + + if (!lsquic_stream_is_server(connect_stream) + && !(conn_pub->cp_flags & CP_CONNECT_PROTOCOL)) + { + LSQ_WARN("cannot accept WT stream %"PRIu64": peer did not enable " + "CONNECT protocol", lsquic_stream_id(connect_stream)); + if (status) + *status = 400; + if (reason) + *reason = "Peer does not support WebTransport"; + if (reason_len) + *reason_len = sizeof("Peer does not support WebTransport") - 1; + return WT_ACCEPT_REJECT; + } + + return WT_ACCEPT_OPEN; +} + + +static int +wt_buffer_or_reject_stream (struct lsquic_stream *stream, + lsquic_stream_id_t session_id, enum lsquic_wt_stream_dir dir) +{ + WT_SET_CONN_FROM_STREAM(stream); + struct lsquic_conn_public *conn_pub; + const char *stream_kind; + unsigned n_pending; + + stream_kind = dir == LSQWT_UNI ? "uni" : "bidi"; + conn_pub = lsquic_stream_get_conn_public(stream); + n_pending = wt_count_pending_streams(conn_pub); + if (n_pending >= WT_MAX_PENDING_STREAMS) + { + LSQ_WARN("pending WT stream limit reached (%u): reject %s stream " + "%"PRIu64" for session %"PRIu64, WT_MAX_PENDING_STREAMS, + stream_kind, lsquic_stream_id(stream), (uint64_t) session_id); + lsquic_stream_set_ss_code(stream, HEC_WT_BUFFERED_STREAM_REJECTED); + if (dir == LSQWT_UNI) + lsquic_stream_close(stream); + else + lsquic_stream_maybe_reset(stream, HEC_WT_BUFFERED_STREAM_REJECTED, + 1); + return -1; + } + + lsquic_stream_wantread(stream, 0); + return 0; +} + + +static void +wt_replay_pending_streams (struct lsquic_wt_session *sess) +{ + WT_SET_CONN_FROM_SESSION(sess); + struct lsquic_hash_elem *el; + struct lsquic_stream *stream; + struct wt_uni_read_ctx *uctx; + lsquic_stream_id_t session_id; + unsigned n_replayed; + + if (sess->wts_flags & WTSF_CLOSING) + return; + + n_replayed = 0; + for (el = lsquic_hash_first(sess->wts_conn_pub->all_streams); el; + el = lsquic_hash_next(sess->wts_conn_pub->all_streams)) + { + stream = lsquic_hashelem_getdata(el); + if (wt_is_pending_uni_stream(stream, &session_id) + && session_id == sess->wts_stream_id) + { + uctx = (struct wt_uni_read_ctx *) lsquic_stream_get_ctx(stream); + if (0 != wt_switch_to_data_if(stream, sess)) + { + lsquic_stream_set_ss_code(stream, HEC_INTERNAL_ERROR); + lsquic_stream_close(stream); + continue; + } + + free(uctx); + LSQ_INFO("replay buffered WT uni stream %"PRIu64" for session %"PRIu64, + lsquic_stream_id(stream), sess->wts_stream_id); + ++n_replayed; + if (sess->wts_flags & WTSF_CLOSING) + break; + } + else if (wt_is_pending_bidi_stream(stream, &session_id) + && session_id == sess->wts_stream_id) + { + LSQ_INFO("replay buffered WT bidi stream %"PRIu64 + " for session %"PRIu64, lsquic_stream_id(stream), + sess->wts_stream_id); + if (0 != wt_switch_to_data_if(stream, sess)) + { + lsquic_stream_maybe_reset(stream, HEC_INTERNAL_ERROR, 1); + continue; + } + ++n_replayed; + if (sess->wts_flags & WTSF_CLOSING) + break; + } + } + + if (n_replayed) + LSQ_INFO("replayed %u buffered WT stream%s for session %"PRIu64, + n_replayed, n_replayed == 1 ? "" : "s", sess->wts_stream_id); +} + + +static void +wt_replay_pending_datagrams (struct lsquic_wt_session *sess) +{ + struct wt_dgq_elem *elem; + + if ((sess->wts_flags & WTSF_CLOSING) + || !sess->wts_if || !sess->wts_if->wti_on_datagram_read) + return; + + while ((elem = TAILQ_FIRST(&sess->wts_in_dgq))) + { + TAILQ_REMOVE(&sess->wts_in_dgq, elem, next); + assert(sess->wts_in_dgq_count > 0); + assert(sess->wts_in_dgq_bytes >= elem->len); + --sess->wts_in_dgq_count; + sess->wts_in_dgq_bytes -= elem->len; + sess->wts_if->wti_on_datagram_read((lsquic_wt_session_t *) sess, + elem->buf, elem->len); + free(elem); + if (sess->wts_flags & WTSF_CLOSING) + break; + } +} + + +static struct lsquic_wt_session * +wt_session_find (struct lsquic_conn_public *conn_pub, + lsquic_stream_id_t stream_id) +{ + struct lsquic_wt_session *sess; + + TAILQ_FOREACH(sess, &conn_pub->wt_sessions, wts_next) + if (sess->wts_stream_id == stream_id) + return sess; + + return NULL; +} + + + +static void +wt_free_connect_info (struct lsquic_wt_session *sess) +{ + free(sess->wts_authority); + free(sess->wts_path); + free(sess->wts_origin); + free(sess->wts_protocol); + + sess->wts_authority = NULL; + sess->wts_path = NULL; + sess->wts_origin = NULL; + sess->wts_protocol = NULL; + + memset(&sess->wts_info, 0, sizeof(sess->wts_info)); +} + + +static void +wt_free_extra_resp_headers (struct lsquic_wt_session *sess) +{ + free(sess->wts_extra_resp_headers.headers_arr); + free(sess->wts_extra_resp_headers.buf); + memset(&sess->wts_extra_resp_headers, 0, sizeof(sess->wts_extra_resp_headers)); +} + + +static int +wt_copy_string (char **dst, const char *src) +{ + if (!src) + { + *dst = NULL; + return 0; + } + + *dst = strdup(src); + return *dst ? 0 : -1; +} + + +static int +wt_copy_connect_info (struct lsquic_wt_session *sess, + const struct lsquic_wt_connect_info *info) +{ + WT_SET_CONN_FROM_SESSION(sess); + if (!info) + { + memset(&sess->wts_info, 0, sizeof(sess->wts_info)); + return 0; + } + + memset(&sess->wts_info, 0, sizeof(sess->wts_info)); + sess->wts_info.wtci_draft = info->wtci_draft; + + if (0 != wt_copy_string(&sess->wts_authority, info->wtci_authority)) + goto err; + if (0 != wt_copy_string(&sess->wts_path, info->wtci_path)) + goto err; + if (0 != wt_copy_string(&sess->wts_origin, info->wtci_origin)) + goto err; + if (0 != wt_copy_string(&sess->wts_protocol, info->wtci_protocol)) + goto err; + + sess->wts_info.wtci_authority = sess->wts_authority; + sess->wts_info.wtci_path = sess->wts_path; + sess->wts_info.wtci_origin = sess->wts_origin; + sess->wts_info.wtci_protocol = sess->wts_protocol; + return 0; + + err: + LSQ_WARN("cannot copy WT CONNECT info for stream %"PRIu64, + sess->wts_stream_id); + wt_free_connect_info(sess); + return -1; +} + + +static int +wt_copy_extra_resp_headers (struct lsquic_wt_session *sess, + const struct lsquic_http_headers *headers) +{ + struct lsxpack_header *dst; + const struct lsxpack_header *src; + size_t count, bufsz, off, i, name_len, val_len; + char *buf; + const char *hdr_buf; + + wt_free_extra_resp_headers(sess); + if (!headers) + return 0; + + if (headers->count < 0) + { + errno = EINVAL; + return -1; + } + + if (headers->count == 0) + return 0; + + if (!headers->headers) + { + errno = EINVAL; + return -1; + } + + count = (size_t) headers->count; + if (count > SIZE_MAX / sizeof(*dst)) + { + errno = EOVERFLOW; + return -1; + } + + bufsz = 0; + for (i = 0; i < count; ++i) + { + src = &headers->headers[i]; + if (0 != wt_size_add(&bufsz, src->name_len) + || 0 != wt_size_add(&bufsz, src->val_len)) + return -1; + } + + dst = malloc(sizeof(*dst) * count); + if (!dst) + return -1; + + buf = bufsz ? malloc(bufsz) : NULL; + if (bufsz && !buf) + { + free(dst); + return -1; + } + + off = 0; + for (i = 0; i < count; ++i) + { + src = &headers->headers[i]; + name_len = src->name_len; + val_len = src->val_len; + hdr_buf = buf ? buf + off : ""; + if (name_len > 0) + memcpy(buf + off, lsxpack_header_get_name(src), name_len); + if (val_len > 0) + memcpy(buf + off + name_len, lsxpack_header_get_value(src), + val_len); + lsxpack_header_set_offset2(&dst[i], hdr_buf, 0, name_len, name_len, + val_len); + off += name_len + val_len; + } + + sess->wts_extra_resp_headers.headers_arr = dst; + sess->wts_extra_resp_headers.buf = buf; + sess->wts_extra_resp_headers.headers.count = (int) count; + sess->wts_extra_resp_headers.headers.headers = dst; + return 0; +} + + +static void +wt_drive_connect_stream (struct lsquic_stream *stream) +{ + WT_SET_CONN_FROM_STREAM(stream); + lsquic_stream_dispatch_write_events(stream); + if (lsquic_stream_has_data_to_flush(stream) + && 0 != lsquic_stream_flush(stream)) + LSQ_DEBUG("cannot flush WT CONNECT stream %"PRIu64": %s", + lsquic_stream_id(stream), strerror(errno)); +} + + +static void +wt_install_conn_hooks (struct lsquic_conn_public *conn_pub) +{ + conn_pub->cp_get_ss_code = wt_stream_ss_code; + conn_pub->cp_on_stream_destroy = wt_on_stream_destroy; + conn_pub->cp_is_hq_switch_frame = wt_is_hq_switch_frame; + conn_pub->cp_on_hq_switch_stream = wt_on_client_bidi_stream; + conn_pub->cp_on_http_caps_change = wt_on_conn_http_caps_change; +} + + +static void +wt_send_accept_internal_error (struct lsquic_stream *stream, int send_headers) +{ + WT_SET_CONN_FROM_STREAM(stream); + if (!send_headers || !lsquic_stream_headers_state_is_begin(stream)) + return; + + if (0 != wt_send_response(stream, 500, NULL, 1)) + LSQ_WARN("cannot send WT accept failure response on stream %"PRIu64, + lsquic_stream_id(stream)); + else + wt_drive_connect_stream(stream); +} + + +static int +wt_set_header (struct lsxpack_header *hdr, struct wt_header_buf *hbuf, + const char *name, size_t name_len, + const char *value, size_t value_len) +{ + size_t name_off; + size_t value_off; + + if (hbuf->off + name_len + value_len > sizeof(hbuf->buf)) + { + errno = ENOBUFS; + return -1; + } + + name_off = hbuf->off; + memcpy(hbuf->buf + hbuf->off, name, name_len); + hbuf->off += name_len; + + value_off = hbuf->off; + memcpy(hbuf->buf + hbuf->off, value, value_len); + hbuf->off += value_len; + + lsxpack_header_set_offset2(hdr, hbuf->buf, name_off, name_len, + value_off, value_len); + return 0; +} + + +static int +wt_send_response (struct lsquic_stream *stream, unsigned status, + const struct lsquic_http_headers *extra, + int fin) +{ + WT_SET_CONN_FROM_STREAM(stream); + struct lsxpack_header *headers_arr; + struct wt_header_buf hbuf; + char status_val[4]; + int extra_count; + size_t header_count; + int n; + int i; + + if (status < 100 || status > 999) + { + errno = EINVAL; + LSQ_WARN("invalid WT response status: %u", status); + return -1; + } + + n = snprintf(status_val, sizeof(status_val), "%u", status); + if (n <= 0 || n >= (int) sizeof(status_val)) + { + errno = EINVAL; + LSQ_WARN("could not format WT status value: %u", status); + return -1; + } + + extra_count = extra ? extra->count : 0; + if (extra_count < 0) + { + errno = EINVAL; + LSQ_WARN("invalid WT extra header count: %d", extra_count); + return -1; + } + if (extra_count >= INT_MAX) + { + errno = EOVERFLOW; + LSQ_WARN("WT extra header count overflows response total: %d", + extra_count); + return -1; + } + + if (extra_count > 0 && (!extra || !extra->headers)) + { + errno = EINVAL; + LSQ_WARN("missing WT extra response headers array"); + return -1; + } + + header_count = 1 + (size_t) extra_count; + if (header_count > SIZE_MAX / sizeof(*headers_arr)) + { + errno = EOVERFLOW; + LSQ_WARN("WT response header count overflows allocation: %zu", + header_count); + return -1; + } + + headers_arr = malloc(sizeof(*headers_arr) * header_count); + if (!headers_arr) + { + LSQ_WARN("cannot allocate WT response headers array"); + return -1; + } + + hbuf.off = 0; + if (0 != wt_set_header(&headers_arr[0], &hbuf, ":status", 7, + status_val, (size_t) n)) + { + free(headers_arr); + LSQ_WARN("cannot set WT response :status header"); + return -1; + } + + for (i = 0; i < extra_count; ++i) + headers_arr[1 + i] = extra->headers[i]; + + if (0 != lsquic_stream_send_headers(stream, + &(struct lsquic_http_headers) { + .count = (int) header_count, + .headers = headers_arr, + }, fin)) + { + LSQ_WARN("cannot send WT response headers on stream %"PRIu64 + ": %s", lsquic_stream_id(stream), strerror(errno)); + free(headers_arr); + return -1; + } + + LSQ_DEBUG("sent WT response status %u on stream %"PRIu64" (extra=%d, fin=%d)", + status, lsquic_stream_id(stream), extra_count, fin); + free(headers_arr); + + return 0; +} + + +static int +wt_stream_can_read (const struct lsquic_stream *stream) +{ + enum stream_id_type type; + + type = lsquic_stream_id(stream) & SIT_MASK; + return type == SIT_BIDI_CLIENT + || type == SIT_BIDI_SERVER + || (lsquic_stream_is_server(stream) + ? type == SIT_UNI_CLIENT + : type == SIT_UNI_SERVER); +} + + +static int +wt_stream_can_write (const struct lsquic_stream *stream) +{ + enum stream_id_type type; + + type = lsquic_stream_id(stream) & SIT_MASK; + return type == SIT_BIDI_CLIENT + || type == SIT_BIDI_SERVER + || (lsquic_stream_is_server(stream) + ? type == SIT_UNI_SERVER + : type == SIT_UNI_CLIENT); +} + + +static int +wt_status_is_2xx (unsigned status) +{ + return status >= 200 && status <= 299; +} + + +/* [draft-ietf-webtrans-http3-15], Section 6; [RFC9000], Section 2.4 */ +static void +wt_close_stream_with_session_gone (struct lsquic_stream *stream) +{ + if (wt_stream_can_read(stream)) + { + lsquic_stream_set_ss_code(stream, HEC_WT_SESSION_GONE); + (void) lsquic_stream_shutdown(stream, 0); + } + + if (wt_stream_can_write(stream)) + lsquic_stream_maybe_reset(stream, HEC_WT_SESSION_GONE, 1); +} + + +static void +wt_close_data_streams (struct lsquic_wt_session *sess) +{ + WT_SET_CONN_FROM_SESSION(sess); + struct lsquic_stream **streams, *stream; + struct lsquic_hash_elem *el; + lsquic_stream_id_t session_id; + size_t n_streams, i; + + if (!sess->wts_conn_pub || !sess->wts_conn_pub->all_streams) + return; + + n_streams = 0; + for (el = lsquic_hash_first(sess->wts_conn_pub->all_streams); el; + el = lsquic_hash_next(sess->wts_conn_pub->all_streams)) + { + stream = lsquic_hashelem_getdata(el); + if ((wt_stream_get_session(stream) == sess + && stream != sess->wts_control_stream) + || (wt_is_pending_uni_stream(stream, &session_id) + && session_id == sess->wts_stream_id) + || (wt_is_pending_bidi_stream(stream, &session_id) + && session_id == sess->wts_stream_id)) + ++n_streams; + } + + if (n_streams == 0) + return; + + streams = malloc(n_streams * sizeof(*streams)); + if (!streams) + { + LSQ_WARN("cannot allocate WT stream-close list for session %"PRIu64, + sess->wts_stream_id); + return; + } + + i = 0; + for (el = lsquic_hash_first(sess->wts_conn_pub->all_streams); el; + el = lsquic_hash_next(sess->wts_conn_pub->all_streams)) + { + stream = lsquic_hashelem_getdata(el); + if ((wt_stream_get_session(stream) == sess + && stream != sess->wts_control_stream) + || (wt_is_pending_uni_stream(stream, &session_id) + && session_id == sess->wts_stream_id) + || (wt_is_pending_bidi_stream(stream, &session_id) + && session_id == sess->wts_stream_id)) + streams[i++] = stream; + } + + for (i = 0; i < n_streams; ++i) + wt_close_stream_with_session_gone(streams[i]); + + free(streams); +} + + +static void +wt_fire_session_close_cb (struct lsquic_wt_session *sess) +{ + const struct lsquic_webtransport_if *wt_if; + lsquic_wt_session_ctx_t *sess_ctx; + + if (!(sess->wts_flags & WTSF_OPENED)) + return; + + if (sess->wts_flags & WTSF_ON_CLOSE_CALLED) + return; + + wt_if = sess->wts_if; + sess_ctx = sess->wts_sess_ctx; + sess->wts_if = NULL; + sess->wts_sess_ctx = NULL; + sess->wts_flags |= WTSF_ON_CLOSE_CALLED; + + if (wt_if && wt_if->wti_on_session_close) + wt_if->wti_on_session_close((lsquic_wt_session_t *) sess, + sess_ctx, sess->wts_close_code, sess->wts_close_reason, + sess->wts_close_reason_len); +} + + +static void +wt_fire_session_rejected_cb (struct lsquic_wt_session *sess, unsigned status, + const char *reason, size_t reason_len) +{ + if (sess->wts_if && sess->wts_if->wti_on_session_rejected) + sess->wts_if->wti_on_session_rejected(sess->wts_if_ctx, + &sess->wts_info, + status, reason, reason_len); +} + + +static void +wt_fire_session_open_cb (struct lsquic_wt_session *sess) +{ + if (sess->wts_flags & WTSF_OPENED) + return; + + sess->wts_flags |= WTSF_OPENED; + if (sess->wts_if && sess->wts_if->wti_on_session_open) + sess->wts_sess_ctx = sess->wts_if->wti_on_session_open( + sess->wts_if_ctx, (lsquic_wt_session_t *) sess, + &sess->wts_info); +} + + +/* [draft-ietf-webtrans-http3-15], Section 6, Figure 11 */ +static int +wt_build_close_capsule (uint64_t code, const char *reason, size_t reason_len, + unsigned char **buf, size_t *buf_len) +{ + unsigned char *capsule; + unsigned bits; + size_t type_len, payload_len_len, payload_len, total_len, off; + + payload_len = 4 + reason_len; + type_len = vint_size(WT_CAPSULE_CLOSE_SESSION); + payload_len_len = vint_size(payload_len); + total_len = type_len + payload_len_len + payload_len; + + capsule = malloc(total_len); + if (!capsule) + return -1; + + bits = vint_val2bits(WT_CAPSULE_CLOSE_SESSION); + vint_write(capsule, WT_CAPSULE_CLOSE_SESSION, bits, type_len); + off = type_len; + + bits = vint_val2bits(payload_len); + vint_write(capsule + off, payload_len, bits, payload_len_len); + off += payload_len_len; + + capsule[off + 0] = (unsigned char) (code >> 24); + capsule[off + 1] = (unsigned char) (code >> 16); + capsule[off + 2] = (unsigned char) (code >> 8); + capsule[off + 3] = (unsigned char) code; + off += 4; + + if (reason_len > 0) + memcpy(capsule + off, reason, reason_len); + + *buf = capsule; + *buf_len = total_len; + return 0; +} + + +static int +wt_queue_close_capsule (struct lsquic_wt_session *sess, uint64_t code, + const char *reason, size_t reason_len) +{ + if (sess->wts_flags & (WTSF_CLOSE_CAPSULE_PENDING | WTSF_CLOSE_SENT)) + return 0; + + if (0 != wt_build_close_capsule(code, reason, reason_len, + &sess->wts_close_buf, + &sess->wts_close_buf_len)) + return -1; + + sess->wts_close_buf_off = 0; + sess->wts_flags |= WTSF_CLOSE_CAPSULE_PENDING; + + if (0 != lsquic_stream_wantwrite(sess->wts_control_stream, 1)) + { + free(sess->wts_close_buf); + sess->wts_close_buf = NULL; + sess->wts_close_buf_len = 0; + sess->wts_close_buf_off = 0; + sess->wts_flags &= ~WTSF_CLOSE_CAPSULE_PENDING; + return -1; + } + + return 0; +} + + +static void +wt_destroy_session (struct lsquic_wt_session *sess) +{ + WT_SET_CONN_FROM_SESSION(sess); + + LSQ_INFO("destroy WT session %"PRIu64" (code=%"PRIu64", reason_len=%zu)", + sess->wts_stream_id, sess->wts_close_code, sess->wts_close_reason_len); + + if (sess->wts_conn_pub + && wt_session_find(sess->wts_conn_pub, sess->wts_stream_id) == sess) + { + TAILQ_REMOVE(&sess->wts_conn_pub->wt_sessions, sess, wts_next); + LSQ_DEBUG("removed WT session %"PRIu64" from connection list", + sess->wts_stream_id); + } + + wt_dgq_drop_all(sess); + wt_in_dgq_drop_all(sess); + wt_free_extra_resp_headers(sess); + free(sess->wts_close_buf); + free(sess->wts_close_reason); + wt_free_connect_info(sess); + free(sess); +} + + +static void +wt_session_maybe_finalize (struct lsquic_wt_session *sess) +{ + if (!(sess->wts_flags & WTSF_CLOSING) + || sess->wts_n_streams != 0 + || (sess->wts_flags & WTSF_FINALIZING)) + return; + + sess->wts_flags |= WTSF_FINALIZING; + wt_fire_session_close_cb(sess); + wt_destroy_session(sess); +} + + +static void +wt_begin_close (struct lsquic_wt_session *sess, uint64_t code, + const char *reason, size_t reason_len) +{ + WT_SET_CONN_FROM_SESSION(sess); + + if (sess->wts_flags & WTSF_CLOSING) + return; + + wt_latch_close_info(sess, code, reason, reason_len); + + LSQ_INFO("closing WT session %"PRIu64" (code=%"PRIu64", reason_len=%zu)", + sess->wts_stream_id, sess->wts_close_code, + sess->wts_close_reason_len); + sess->wts_flags |= WTSF_CLOSING; + wt_drop_send_state(sess); + wt_close_data_streams(sess); +} + + +/* [draft-ietf-webtrans-http3-15], Section 6 */ +static void +wt_close_remote (struct lsquic_wt_session *sess, uint64_t code, + const char *reason, size_t reason_len, int close_received) +{ + if (close_received) + sess->wts_flags |= WTSF_CLOSE_RCVD; + + wt_begin_close(sess, code, reason, reason_len); + + if (sess->wts_control_stream + && !(sess->wts_flags & WTSF_CLOSE_CAPSULE_PENDING) + && !lsquic_stream_is_closed(sess->wts_control_stream)) + (void) lsquic_stream_shutdown(sess->wts_control_stream, 1); + + wt_session_maybe_finalize(sess); +} + + +static void +wt_reject_session (struct lsquic_wt_session *sess, unsigned status, + const char *reason, size_t reason_len) +{ + WT_SET_CONN_FROM_SESSION(sess); + struct lsquic_stream *stream; + + if (sess->wts_flags & (WTSF_REJECTED | WTSF_OPENED)) + return; + + stream = sess->wts_control_stream; + sess->wts_flags |= WTSF_REJECTED | WTSF_CLOSING; + sess->wts_flags &= ~WTSF_ACCEPT_PENDING; + sess->wts_accept_status = status; + + LSQ_INFO("reject WT CONNECT stream %"PRIu64" with status %u", + sess->wts_stream_id, status); + wt_fire_session_rejected_cb(sess, status, reason, reason_len); + wt_drop_send_state(sess); + wt_in_dgq_drop_all(sess); + wt_free_extra_resp_headers(sess); + wt_close_data_streams(sess); + + if (status != 0 + && stream && lsquic_stream_is_server(stream) + && lsquic_stream_headers_state_is_begin(stream)) + { + if (0 != wt_send_response(stream, status, NULL, 1)) + LSQ_WARN("cannot send WT reject response on stream %"PRIu64, + lsquic_stream_id(stream)); + else + wt_drive_connect_stream(stream); + } + + if (stream && !lsquic_stream_is_closed(stream)) + lsquic_stream_close(stream); + + if (!stream || lsquic_stream_is_closed(stream)) + wt_session_maybe_finalize(sess); +} + + +static int +wt_open_session (struct lsquic_wt_session *sess) +{ + WT_SET_CONN_FROM_SESSION(sess); + struct lsquic_stream *stream; + + if (sess->wts_flags & (WTSF_REJECTED | WTSF_OPENED)) + return 0; + + stream = sess->wts_control_stream; + if (stream && lsquic_stream_is_server(stream) + && lsquic_stream_headers_state_is_begin(stream)) + { + if (0 != wt_send_response(stream, + sess->wts_accept_status + ? sess->wts_accept_status + : LSQUIC_WTAP_STATUS_DEFAULT, + sess->wts_extra_resp_headers.headers_arr + ? &sess->wts_extra_resp_headers.headers + : NULL, + 0)) + return -1; + wt_drive_connect_stream(stream); + } + + wt_free_extra_resp_headers(sess); + + sess->wts_flags &= ~WTSF_ACCEPT_PENDING; + wt_fire_session_open_cb(sess); + if (!(sess->wts_flags & WTSF_CLOSING)) + wt_replay_pending_streams(sess); + if (!(sess->wts_flags & WTSF_CLOSING)) + wt_replay_pending_datagrams(sess); + LSQ_INFO("accepted WT session %"PRIu64" on stream %"PRIu64, + sess->wts_stream_id, + stream ? lsquic_stream_id(stream) : sess->wts_stream_id); + return 0; +} + + +static void +wt_resolve_pending_accepts (struct lsquic_conn_public *conn_pub) +{ + struct lsquic_wt_session *sess, *next; + enum wt_accept_result result; + const char *reason; + size_t reason_len; + unsigned status; + + for (sess = TAILQ_FIRST(&conn_pub->wt_sessions); sess; sess = next) + { + next = TAILQ_NEXT(sess, wts_next); + if (!(sess->wts_flags & WTSF_ACCEPT_PENDING)) + continue; + + status = 500; + reason = "cannot accept WebTransport"; + reason_len = sizeof("cannot accept WebTransport") - 1; + result = wt_evaluate_accept(sess->wts_control_stream, sess, &status, + &reason, &reason_len); + if (result == WT_ACCEPT_PENDING) + continue; + + if (result == WT_ACCEPT_REJECT) + wt_reject_session(sess, status, reason, reason_len); + else if (0 != wt_open_session(sess)) + wt_reject_session(sess, 500, "cannot accept WebTransport", + sizeof("cannot accept WebTransport") - 1); + } +} + + +static void +wt_on_conn_http_caps_change (struct lsquic_conn_public *conn_pub) +{ + if (conn_pub) + wt_resolve_pending_accepts(conn_pub); +} + +int +lsquic_wt_accept (struct lsquic_stream *connect_stream, + const struct lsquic_wt_accept_params *params) +{ + WT_SET_CONN_FROM_STREAM(connect_stream); + struct lsquic_wt_session *sess; + struct lsquic_conn_public *conn_pub; + const struct lsquic_wt_connect_info *info; + enum wt_accept_result accept_result; + const char *reject_reason; + size_t reject_reason_len; + int send_internal_error; + int saved_errno; + + if (!connect_stream || !params) + { + errno = EINVAL; + LSQ_WARN("WT accept called with invalid arguments"); + return -1; + } + + if (!params->wtap_wt_if || !params->wtap_wt_if->wti_on_stream_read) + { + errno = EINVAL; + LSQ_WARN("WT accept called without stream read callback on stream %"PRIu64, + lsquic_stream_id(connect_stream)); + return -1; + } + + if (params->wtap_status != 0 && !wt_status_is_2xx(params->wtap_status)) + { + errno = EINVAL; + LSQ_WARN("WT accept called with non-2xx status %u on stream %"PRIu64, + params->wtap_status, lsquic_stream_id(connect_stream)); + return -1; + } + + if (wt_stream_get_session(connect_stream)) + { + errno = EALREADY; + LSQ_WARN("WT accept called for already-accepted stream %"PRIu64, + lsquic_stream_id(connect_stream)); + return -1; + } + + conn_pub = lsquic_stream_get_conn_public(connect_stream); + if (!conn_pub) + { + errno = EINVAL; + LSQ_WARN("WT accept called without conn_pub on stream %"PRIu64, + lsquic_stream_id(connect_stream)); + return -1; + } + + LSQ_INFO("accept WT CONNECT stream %"PRIu64" (server=%d, status=%u)", + lsquic_stream_id(connect_stream), lsquic_stream_is_server(connect_stream), + params->wtap_status); + send_internal_error = 0; + saved_errno = 0; + info = params->wtap_connect_info; + + sess = calloc(1, sizeof(*sess)); + if (!sess) + { + saved_errno = errno; + send_internal_error = 1; + LSQ_WARN("cannot allocate WT session for stream %"PRIu64, + lsquic_stream_id(connect_stream)); + goto err0; + } + + sess->wts_control_stream = connect_stream; + sess->wts_conn_pub = conn_pub; + sess->wts_conn = lsquic_stream_conn(connect_stream); + sess->wts_if = params->wtap_wt_if; + sess->wts_if_ctx = params->wtap_wt_if_ctx; + sess->wts_sess_ctx = params->wtap_sess_ctx; + sess->wts_stream_id = lsquic_stream_id(connect_stream); + sess->wts_accept_status = params->wtap_status + ? params->wtap_status + : LSQUIC_WTAP_STATUS_DEFAULT; + sess->wts_dgq_max_count = params->wtap_max_datagram_queue_count + ? params->wtap_max_datagram_queue_count + : LSQUIC_WTAP_MAX_DATAGRAM_QUEUE_COUNT_DEFAULT; + sess->wts_dgq_max_bytes = params->wtap_max_datagram_queue_bytes + ? params->wtap_max_datagram_queue_bytes + : LSQUIC_WTAP_MAX_DATAGRAM_QUEUE_BYTES_DEFAULT; + sess->wts_dg_policy = params->wtap_datagram_drop_policy <= LSQWT_DG_DROP_NEWEST + ? params->wtap_datagram_drop_policy + : LSQUIC_WTAP_DATAGRAM_DROP_POLICY_DEFAULT; + sess->wts_dg_mode = params->wtap_datagram_send_mode; + TAILQ_INIT(&sess->wts_dgq); + TAILQ_INIT(&sess->wts_in_dgq); + + if (0 != wt_copy_connect_info(sess, info)) + { + saved_errno = errno; + send_internal_error = 1; + goto err1; + } + + if (0 != wt_copy_extra_resp_headers(sess, params->wtap_extra_resp_headers)) + { + saved_errno = errno; + send_internal_error = 1; + goto err1; + } + + sess->wts_data_if = *sess->wts_conn_pub->enpub->enp_stream_if; + sess->wts_data_if.on_new_stream = wt_on_new_stream; + sess->wts_data_if.on_read = wt_on_read; + sess->wts_data_if.on_write = wt_on_write; + sess->wts_data_if.on_close = wt_on_close; + sess->wts_data_if.on_reset = wt_on_reset; + + sess->wts_onnew_ctx.sess = sess; + sess->wts_onnew_ctx.prefix_len = 0; + sess->wts_onnew_ctx.is_dynamic = 0; + + if (0 != lsquic_stream_set_http_dg_if(connect_stream, &wt_http_dg_if)) + { + saved_errno = errno; + send_internal_error = 1; + LSQ_WARN("cannot set WT HTTP datagram callbacks on stream %"PRIu64, + lsquic_stream_id(connect_stream)); + goto err1; + } + + if (0 != lsquic_stream_set_http_dg_capsules(connect_stream, 1)) + { + saved_errno = errno; + send_internal_error = 1; + LSQ_WARN("cannot enable WT capsule parsing on stream %"PRIu64, + lsquic_stream_id(connect_stream)); + goto err2; + } + + if (0 != wt_register_capsule_handlers(connect_stream)) + { + saved_errno = errno; + send_internal_error = 1; + LSQ_WARN("cannot register WT capsule handlers on stream %"PRIu64, + lsquic_stream_id(connect_stream)); + goto err3; + } + + if (0 != wt_wrap_control_stream(sess, connect_stream)) + { + saved_errno = errno; + send_internal_error = 1; + goto err4; + } + + wt_install_conn_hooks(conn_pub); + wt_stream_bind_session(sess, connect_stream); + lsquic_stream_mark_session_stream(connect_stream); + TAILQ_INSERT_TAIL(&sess->wts_conn_pub->wt_sessions, sess, wts_next); + + reject_reason = "cannot accept WebTransport"; + reject_reason_len = sizeof("cannot accept WebTransport") - 1; + accept_result = wt_evaluate_accept(connect_stream, sess, + &sess->wts_accept_status, + &reject_reason, &reject_reason_len); + if (accept_result == WT_ACCEPT_REJECT) + { + wt_reject_session(sess, sess->wts_accept_status, reject_reason, + reject_reason_len); + return 0; + } + + if (accept_result == WT_ACCEPT_PENDING) + { + sess->wts_flags |= WTSF_ACCEPT_PENDING; + return 0; + } + + if (0 != wt_open_session(sess)) + { + wt_reject_session(sess, 500, "cannot accept WebTransport", + sizeof("cannot accept WebTransport") - 1); + return 0; + } + + return 0; + +err4: + wt_unregister_capsule_handlers(connect_stream); +err3: + lsquic_stream_set_http_dg_capsules(connect_stream, 0); +err2: + lsquic_stream_set_http_dg_if(connect_stream, NULL); +err1: + wt_free_extra_resp_headers(sess); + wt_free_connect_info(sess); + free(sess); +err0: + if (send_internal_error) + wt_send_accept_internal_error(connect_stream, + lsquic_stream_is_server(connect_stream) + && lsquic_stream_headers_state_is_begin(connect_stream)); + if (!saved_errno) + saved_errno = errno ? errno : EIO; + errno = saved_errno; + return -1; +} + +#if LSQUIC_TEST +int +lsquic_wt_test_accept_status_validation (unsigned status, int *accepted) +{ + int valid; + + valid = status == 0 || wt_status_is_2xx(status); + if (accepted) + *accepted = valid; + return 0; +} + +int +lsquic_wt_test_reject_status_validation (unsigned status, int *accepted) +{ + int valid; + + valid = status == 0 || !wt_status_is_2xx(status); + if (accepted) + *accepted = valid; + return 0; +} + +#endif + + + +int +lsquic_wt_reject (struct lsquic_stream *connect_stream, + unsigned status, const char *UNUSED_reason, + size_t UNUSED_reason_len) +{ + WT_SET_CONN_FROM_STREAM(connect_stream); + if (!connect_stream) + { + errno = EINVAL; + LSQ_WARN("WT reject called with NULL stream"); + return -1; + } + + if (!lsquic_stream_is_server(connect_stream)) + { + errno = EINVAL; + LSQ_WARN("WT reject called on client stream %"PRIu64, + lsquic_stream_id(connect_stream)); + return -1; + } + + if (!lsquic_stream_headers_state_is_begin(connect_stream)) + { + errno = EALREADY; + LSQ_WARN("WT reject called after headers started on stream %"PRIu64, + lsquic_stream_id(connect_stream)); + return -1; + } + + if (0 == status) + status = 400; + + if (wt_status_is_2xx(status)) + { + errno = EINVAL; + LSQ_WARN("WT reject called with 2xx status %u on stream %"PRIu64, + status, lsquic_stream_id(connect_stream)); + return -1; + } + + if (0 != wt_send_response(connect_stream, status, NULL, 1)) + { + LSQ_WARN("cannot send WT reject response on stream %"PRIu64, + lsquic_stream_id(connect_stream)); + return -1; + } + + LSQ_INFO("rejected WT CONNECT stream %"PRIu64" with status %u", + lsquic_stream_id(connect_stream), status); + return 0; +} + + + +int +lsquic_wt_close (struct lsquic_wt_session *sess, uint64_t code, + const char *reason, size_t reason_len) +{ + WT_SET_CONN_FROM_SESSION(sess); + + if (sess->wts_flags & WTSF_CLOSING) + return 0; + + wt_begin_close(sess, code, reason, reason_len); + + if (!sess->wts_control_stream) + { + wt_session_maybe_finalize(sess); + return 0; + } + + if (sess->wts_close_code != 0 || sess->wts_close_reason_len != 0) + { + if (0 == wt_queue_close_capsule(sess, sess->wts_close_code, + sess->wts_close_reason, + sess->wts_close_reason_len)) + wt_drive_connect_stream(sess->wts_control_stream); + else + { + LSQ_WARN("cannot queue WT_CLOSE_SESSION for session %"PRIu64, + sess->wts_stream_id); + /* [draft-ietf-webtrans-http3-15], Section 6 */ + lsquic_stream_set_ss_code(sess->wts_control_stream, + HEC_WT_SESSION_GONE); + (void) lsquic_stream_shutdown(sess->wts_control_stream, 0); + (void) lsquic_stream_shutdown(sess->wts_control_stream, 1); + } + } + else + { + /* [draft-ietf-webtrans-http3-15], Section 6 */ + lsquic_stream_set_ss_code(sess->wts_control_stream, HEC_WT_SESSION_GONE); + (void) lsquic_stream_shutdown(sess->wts_control_stream, 0); + (void) lsquic_stream_shutdown(sess->wts_control_stream, 1); + } + + return 0; +} + + + +struct lsquic_conn * +lsquic_wt_session_conn (struct lsquic_wt_session *sess) +{ + if (!sess->wts_conn_pub) + return NULL; + + return sess->wts_conn_pub->lconn; +} + + + +lsquic_stream_id_t +lsquic_wt_session_id (struct lsquic_wt_session *sess) +{ + return sess->wts_stream_id; +} + + + +static int +wt_get_conn_u64_param (lsquic_conn_t *conn, enum lsquic_conn_param param, + uint64_t *value) +{ + size_t value_len; + + if (!conn || !value) + return -1; + + value_len = sizeof(*value); + if (0 != lsquic_conn_get_param(conn, param, value, &value_len)) + return -1; + + return value_len == sizeof(*value) ? 0 : -1; +} + + +int +lsquic_wt_peer_settings_received (lsquic_conn_t *conn) +{ + uint64_t value; + + if (0 != wt_get_conn_u64_param(conn, LSQCP_WT_PEER_SETTINGS_RECEIVED, + &value)) + return 0; + + return value != 0; +} + + +int +lsquic_wt_peer_supports (lsquic_conn_t *conn) +{ + uint64_t value; + + if (0 != wt_get_conn_u64_param(conn, LSQCP_WT_PEER_SUPPORTS, &value)) + return 0; + + return value != 0; +} + + +unsigned +lsquic_wt_peer_draft (lsquic_conn_t *conn) +{ + uint64_t value; + + if (0 != wt_get_conn_u64_param(conn, LSQCP_WT_PEER_DRAFT, &value)) + return 0; + + if (value > UINT_MAX) + return UINT_MAX; + return (unsigned) value; +} + + +int +lsquic_wt_peer_connect_protocol (lsquic_conn_t *conn) +{ + uint64_t value; + + if (0 != wt_get_conn_u64_param(conn, LSQCP_WT_PEER_CONNECT_PROTOCOL, + &value)) + return 0; + + return value != 0; +} + + +struct lsquic_stream * +lsquic_wt_open_uni (struct lsquic_wt_session *sess) +{ + WT_SET_CONN_FROM_SESSION(sess); + struct wt_onnew_ctx *onnew; + struct lsquic_conn *lconn; + struct lsquic_stream *stream; + + /* [draft-ietf-webtrans-http3-15], Section 6 */ + /* [draft-ietf-webtrans-http3-15], Section 6 */ + if (sess->wts_flags & WTSF_CLOSING) + { + errno = EPIPE; + return NULL; + } + + if (!sess->wts_conn_pub) + { + errno = EINVAL; + LSQ_WARN("WT open_uni called with invalid session"); + return NULL; + } + + lconn = sess->wts_conn_pub->lconn; + if (!lconn || !lconn->cn_if || !lconn->cn_if->ci_make_uni_stream_with_if) + { + errno = ENOSYS; + LSQ_WARN("WT open_uni unavailable for session %"PRIu64, + sess->wts_stream_id); + return NULL; + } + + onnew = calloc(1, sizeof(*onnew)); + if (!onnew) + { + LSQ_WARN("cannot allocate WT onnew ctx for uni stream in session %"PRIu64, + sess->wts_stream_id); + return NULL; + } + + onnew->sess = sess; + onnew->is_dynamic = 1; + wt_build_prefix(onnew->prefix, &onnew->prefix_len, HQUST_WEBTRANSPORT, + sess->wts_stream_id); + + stream = lconn->cn_if->ci_make_uni_stream_with_if(lconn, + &sess->wts_data_if, onnew); + if (!stream) + { + LSQ_WARN("cannot open WT uni stream in session %"PRIu64, + sess->wts_stream_id); + free(onnew); + return NULL; + } + + if (wt_stream_get_session(stream) != sess) + { + LSQ_WARN("WT uni stream %"PRIu64" failed to initialize in session " + "%"PRIu64, lsquic_stream_id(stream), sess->wts_stream_id); + wt_abort_failed_local_stream(stream); + errno = ENOMEM; + return NULL; + } + + LSQ_DEBUG("opened WT uni stream %"PRIu64" in session %"PRIu64, + lsquic_stream_id(stream), sess->wts_stream_id); + return stream; +} + + + +struct lsquic_stream * +lsquic_wt_open_bidi (struct lsquic_wt_session *sess) +{ + WT_SET_CONN_FROM_SESSION(sess); + struct wt_onnew_ctx *onnew; + struct lsquic_conn *lconn; + struct lsquic_stream *stream; + + /* [draft-ietf-webtrans-http3-15], Section 6 */ + if (sess->wts_flags & WTSF_CLOSING) + { + errno = EPIPE; + return NULL; + } + + if (!sess->wts_conn_pub) + { + errno = EINVAL; + LSQ_WARN("WT open_bidi called with invalid session"); + return NULL; + } + + lconn = sess->wts_conn_pub->lconn; + if (!lconn || !lconn->cn_if || !lconn->cn_if->ci_make_bidi_stream_with_if) + { + errno = ENOSYS; + LSQ_WARN("WT open_bidi unavailable for session %"PRIu64, + sess->wts_stream_id); + return NULL; + } + + onnew = calloc(1, sizeof(*onnew)); + if (!onnew) + { + LSQ_WARN("cannot allocate WT onnew ctx for bidi stream in session %"PRIu64, + sess->wts_stream_id); + return NULL; + } + + onnew->sess = sess; + onnew->is_dynamic = 1; + wt_build_prefix(onnew->prefix, &onnew->prefix_len, HQFT_WT_STREAM, + sess->wts_stream_id); + + stream = lconn->cn_if->ci_make_bidi_stream_with_if(lconn, + &sess->wts_data_if, onnew); + if (!stream) + { + LSQ_WARN("cannot open WT bidi stream in session %"PRIu64, + sess->wts_stream_id); + free(onnew); + return NULL; + } + + if (wt_stream_get_session(stream) != sess) + { + LSQ_WARN("WT bidi stream %"PRIu64" failed to initialize in session " + "%"PRIu64, lsquic_stream_id(stream), sess->wts_stream_id); + wt_abort_failed_local_stream(stream); + errno = ENOMEM; + return NULL; + } + + LSQ_DEBUG("opened WT bidi stream %"PRIu64" in session %"PRIu64, + lsquic_stream_id(stream), sess->wts_stream_id); + return stream; +} + + + +struct lsquic_wt_session * +lsquic_wt_session_from_stream (struct lsquic_stream *stream) +{ + if (!stream) + return NULL; + + return wt_stream_get_session(stream); +} + +lsquic_stream_ctx_t * +lsquic_wt_stream_get_ctx (struct lsquic_stream *stream) +{ + struct wt_stream_ctx *wctx; + struct lsquic_wt_session *sess; + + if (!stream || !wt_stream_get_session(stream)) + return NULL; + + sess = wt_stream_get_session(stream); + if (lsquic_stream_get_stream_if(stream) != &sess->wts_data_if) + return NULL; + + wctx = (struct wt_stream_ctx *) lsquic_stream_get_ctx(stream); + if (!wctx) + return NULL; + + return wctx->app_ctx; +} + + +static int +wt_stream_ss_code (const struct lsquic_stream *stream, + uint64_t *ss_code) +{ + struct wt_stream_ctx *wctx; + struct lsquic_wt_session *sess; + uint64_t wt_error_code; + + if (!stream || !ss_code) + return -1; + + if (lsquic_stream_onclose_done(stream)) + return -1; + + wctx = (struct wt_stream_ctx *) lsquic_stream_get_ctx(stream); + if (!wctx || !wctx->ss_code) + return -1; + + sess = wctx->sess; + if (wt_stream_get_session(stream) != sess) + return -1; + + if (lsquic_stream_get_stream_if(stream) != &sess->wts_data_if) + return -1; + + wt_error_code = wctx->ss_code((struct lsquic_stream *) stream, + wctx->app_ctx); + return wt_app_error_to_h3_error(wt_error_code, ss_code); +} + + +enum lsquic_wt_stream_dir +lsquic_wt_stream_dir (const struct lsquic_stream *stream) +{ + enum stream_id_type type; + + if (!stream) + return LSQWT_BIDI; + + type = lsquic_stream_id(stream) & SIT_MASK; + + if (type == SIT_UNI_CLIENT || type == SIT_UNI_SERVER) + return LSQWT_UNI; + else + return LSQWT_BIDI; +} + + + +enum lsquic_wt_stream_initiator +lsquic_wt_stream_initiator (const struct lsquic_stream *stream) +{ + enum stream_id_type type; + + if (!stream) + return LSQWT_CLIENT; + + type = lsquic_stream_id(stream) & SIT_MASK; + + if (type == SIT_BIDI_SERVER || type == SIT_UNI_SERVER) + return LSQWT_SERVER; + else + return LSQWT_CLIENT; +} + + + +ssize_t +lsquic_wt_send_datagram (struct lsquic_wt_session *sess, const void *buf, + size_t len) +{ + return lsquic_wt_send_datagram_ex(sess, buf, len, sess->wts_dg_policy, + sess->wts_dg_mode); +} + + +ssize_t +lsquic_wt_send_datagram_ex (struct lsquic_wt_session *sess, const void *buf, + size_t len, enum lsquic_wt_dg_drop_policy policy, + enum lsquic_http_dg_send_mode mode) +{ + WT_SET_CONN_FROM_SESSION(sess); + struct lsquic_stream *control_stream; + size_t max_sz; + int old_want; + + /* [draft-ietf-webtrans-http3-15], Section 6 */ + if (sess->wts_flags & WTSF_CLOSING) + { + errno = EPIPE; + return -1; + } + + if (!buf || len == 0 || policy > LSQWT_DG_DROP_NEWEST) + { + errno = EINVAL; + LSQ_WARN("invalid WT datagram send arguments"); + return -1; + } + + LSQ_DEBUG("queue WT datagram for session %"PRIu64": len=%zu", + sess->wts_stream_id, len); + control_stream = sess->wts_control_stream; + if (!control_stream) + { + errno = EINVAL; + LSQ_WARN("cannot send WT datagram in session %"PRIu64 + ": no control stream", sess->wts_stream_id); + return -1; + } + + max_sz = lsquic_stream_get_max_http_dg_size(control_stream); + if (max_sz == 0) + { + errno = ENOSYS; + LSQ_WARN("WT datagrams not negotiated in session %"PRIu64, + sess->wts_stream_id); + return -1; + } + + if (len > max_sz) + { + errno = EMSGSIZE; + LSQ_WARN("WT datagram too large in session %"PRIu64 + ": len=%zu, max=%zu", sess->wts_stream_id, len, max_sz); + return -1; + } + + old_want = wt_dgq_arm_write(sess); + if (old_want < 0) + return -1; + + if (0 != wt_dgq_enqueue(sess, buf, len, policy, mode)) + { + if (0 == old_want && 0 == sess->wts_dgq_count + && !(sess->wts_flags & WTSF_WANT_DG_WRITE)) + (void) wt_dgq_disarm_write(control_stream); + return -1; + } + + LSQ_DEBUG("enqueued WT datagram for session %"PRIu64" on stream %"PRIu64, + sess->wts_stream_id, lsquic_stream_id(control_stream)); + return (ssize_t) len; +} + + +int +lsquic_wt_want_datagram_write (lsquic_wt_session_t *sess, int is_want) +{ + WT_SET_CONN_FROM_SESSION(sess); + struct lsquic_stream *control_stream; + + /* [draft-ietf-webtrans-http3-15], Section 6 */ + if (is_want && (sess->wts_flags & WTSF_CLOSING)) + { + errno = EPIPE; + return -1; + } + + control_stream = sess->wts_control_stream; + if (!control_stream) + { + errno = EINVAL; + LSQ_WARN("WT datagram write interest called without control stream"); + return -1; + } + + if (is_want) + { + if (sess->wts_flags & WTSF_CLOSING) + { + errno = EPIPE; + return -1; + } + + if (0 > wt_dgq_arm_write(sess)) + return -1; + sess->wts_flags |= WTSF_WANT_DG_WRITE; + return 0; + } + else + { + sess->wts_flags &= ~WTSF_WANT_DG_WRITE; + if (sess->wts_dgq_count == 0) + return wt_dgq_disarm_write(control_stream); + return 0; + } +} + + +size_t +lsquic_wt_max_datagram_size (const struct lsquic_wt_session *sess) +{ + WT_SET_CONN_FROM_SESSION(sess); + + if (!sess->wts_control_stream) + { + LSQ_DEBUG("WT max_datagram_size unavailable: no session/control stream"); + return 0; + } + + return lsquic_stream_get_max_http_dg_size(sess->wts_control_stream); +} + + + +static int +lsquic_wt_on_http_dg_write (struct lsquic_stream *stream, + lsquic_stream_ctx_t *UNUSED_sctx, + size_t max_quic_payload, + lsquic_http_dg_consume_f consume_datagram) +{ + WT_SET_CONN_FROM_STREAM(stream); + struct lsquic_wt_session *sess; + unsigned had_queued; + int rc; + + if (!stream || !consume_datagram) + { + errno = EINVAL; + LSQ_WARN("WT HTTP datagram write callback called with invalid args"); + return -1; + } + + LSQ_DEBUG("WT HTTP datagram write callback on stream %"PRIu64 + " max_payload=%zu", lsquic_stream_id(stream), max_quic_payload); + sess = wt_stream_get_session(stream); + if (!sess || sess->wts_control_stream != stream) + { + errno = EAGAIN; + LSQ_DEBUG("WT HTTP datagram write has no control session on stream %"PRIu64, + lsquic_stream_id(stream)); + return -1; + } + + /* [draft-ietf-webtrans-http3-15], Section 6 */ + if (sess->wts_flags & WTSF_CLOSING) + { + (void) lsquic_stream_want_http_dg_write(stream, 0); + errno = EAGAIN; + return -1; + } + + had_queued = sess->wts_dgq_count; + if (had_queued) + { + rc = wt_dgq_send_one(sess, stream, max_quic_payload, consume_datagram); + if (rc < 0) + return -1; + if (rc > 0) + goto end; + } + + if ((sess->wts_flags & WTSF_WANT_DG_WRITE) + && sess->wts_if && sess->wts_if->wti_on_datagram_write) + { + rc = sess->wts_if->wti_on_datagram_write((lsquic_wt_session_t *) sess, + max_quic_payload); + if (rc < 0) + { + LSQ_WARN("WT datagram write callback failed in session %"PRIu64 + ": %s", sess->wts_stream_id, strerror(errno)); + return -1; + } + } + + if (sess->wts_dgq_count) + { + rc = wt_dgq_send_one(sess, stream, max_quic_payload, consume_datagram); + if (rc < 0) + return -1; + if (rc > 0) + goto end; + } + + (void) lsquic_stream_want_http_dg_write(stream, 0); + errno = EAGAIN; + return -1; + + end: + if (sess->wts_dgq_count == 0 && !(sess->wts_flags & WTSF_WANT_DG_WRITE)) + (void) lsquic_stream_want_http_dg_write(stream, 0); + LSQ_DEBUG("sent WT datagram on stream %"PRIu64" in session %"PRIu64 + " (queued_before=%u, queued_now=%u)", lsquic_stream_id(stream), + sess->wts_stream_id, had_queued, sess->wts_dgq_count); + return 0; +} + + +static void +lsquic_wt_on_http_dg_read (struct lsquic_stream *stream, + lsquic_stream_ctx_t *UNUSED_sctx, + const void *buf, size_t len) +{ + WT_SET_CONN_FROM_STREAM(stream); + struct lsquic_wt_session *sess; + + if (!stream || !buf || len == 0) + return; + + LSQ_DEBUG("received WT datagram on stream %"PRIu64" (len=%zu)", + lsquic_stream_id(stream), len); + sess = wt_stream_get_session(stream); + if (!sess || sess->wts_control_stream != stream) + { + LSQ_DEBUG("drop WT datagram on stream %"PRIu64 + ": no matching control session", lsquic_stream_id(stream)); + return; + } + + if (sess->wts_flags & WTSF_ACCEPT_PENDING) + { + if (0 != wt_in_dgq_enqueue(sess, buf, len)) + LSQ_DEBUG("drop pending incoming WT datagram for session %"PRIu64, + sess->wts_stream_id); + return; + } + + /* [draft-ietf-webtrans-http3-15], Section 6 */ + if (!(sess->wts_flags & WTSF_CLOSING) + && sess->wts_if && sess->wts_if->wti_on_datagram_read) + { + LSQ_DEBUG("deliver WT datagram to session %"PRIu64, + sess->wts_stream_id); + sess->wts_if->wti_on_datagram_read((lsquic_wt_session_t *) sess, + buf, len); + } +} + + + +int +lsquic_wt_stream_reset (struct lsquic_stream *stream, uint64_t error_code) +{ + struct lsquic_wt_session *sess; + uint64_t h3_error_code; + + if (!stream) + { + errno = EINVAL; + return -1; + } + + sess = wt_stream_get_session(stream); + if (!sess) + { + errno = EINVAL; + return -1; + } + + if (lsquic_stream_get_stream_if(stream) != &sess->wts_data_if) + { + errno = EINVAL; + return -1; + } + + if (0 != wt_app_error_to_h3_error(error_code, &h3_error_code)) + { + errno = EINVAL; + return -1; + } + + lsquic_stream_maybe_reset(stream, h3_error_code, 1); + return 0; +} + + + +int +lsquic_wt_stream_stop_sending (struct lsquic_stream *stream, + uint64_t error_code) +{ + struct lsquic_wt_session *sess; + uint64_t h3_error_code; + + if (!stream) + { + errno = EINVAL; + return -1; + } + + sess = wt_stream_get_session(stream); + if (!sess) + { + errno = EINVAL; + return -1; + } + + if (lsquic_stream_get_stream_if(stream) != &sess->wts_data_if) + { + errno = EINVAL; + return -1; + } + + if (0 != wt_app_error_to_h3_error(error_code, &h3_error_code)) + { + errno = EINVAL; + return -1; + } + + stream->sm_ss_code = h3_error_code; + return lsquic_stream_shutdown(stream, 0); +} + + + +static void +wt_on_stream_destroy (struct lsquic_stream *stream) +{ + WT_SET_CONN_FROM_STREAM(stream); + struct lsquic_wt_session *sess; + int is_control_stream; + int was_closing; + + if (!stream) + return; + + sess = wt_stream_get_session(stream); + if (!sess) + return; + + is_control_stream = sess->wts_control_stream == stream; + if (is_control_stream) + sess->wts_control_stream = NULL; + was_closing = !!(sess->wts_flags & WTSF_CLOSING); + LSQ_DEBUG("WT stream destroy: stream=%"PRIu64", session=%"PRIu64 + ", is_control=%d, closing=%d", lsquic_stream_id(stream), + sess->wts_stream_id, is_control_stream, + was_closing); + wt_stream_unbind_session(stream); + if (is_control_stream && !was_closing) + wt_close_remote(sess, 0, NULL, 0, 0); +} + + +static int +wt_is_hq_switch_frame (struct lsquic_stream *stream, uint64_t frame_type, + uint64_t frame_len) +{ + (void) frame_len; + + return frame_type == HQFT_WT_STREAM + && (stream->conn_pub->enpub->enp_settings.es_webtransport + || (stream->conn_pub->cp_flags & CP_WEBTRANSPORT)); +} + + +static void +wt_on_client_bidi_stream (struct lsquic_stream *stream, + lsquic_stream_id_t session_id) +{ + WT_SET_CONN_FROM_STREAM(stream); + struct lsquic_wt_session *existing; + struct lsquic_wt_session *sess; + + if (!stream) + return; + + existing = wt_stream_get_session(stream); + if (existing) + { + if (existing->wts_stream_id != session_id) + LSQ_WARN("WT stream %"PRIu64" is already bound to session %"PRIu64 + ", cannot rebind to %"PRIu64, lsquic_stream_id(stream), + existing->wts_stream_id, (uint64_t) session_id); + return; + } + + if (0 != lsquic_wt_validate_incoming_session_id(stream, session_id, + "bidi")) + return; + + LSQ_DEBUG("associate client-initiated bidi stream %"PRIu64 + " with WT session %"PRIu64, lsquic_stream_id(stream), + (uint64_t) session_id); + sess = wt_session_find(lsquic_stream_get_conn_public(stream), + session_id); + if (sess && (sess->wts_flags & WTSF_CLOSING)) + { + wt_close_stream_with_session_gone(stream); + return; + } + + if (!sess || !wt_session_is_opened(sess)) + { + if (0 != wt_buffer_or_reject_stream(stream, session_id, LSQWT_BIDI)) + return; + LSQ_INFO("buffered WT bidi stream %"PRIu64" for session %"PRIu64, + lsquic_stream_id(stream), (uint64_t) session_id); + return; + } + + LSQ_DEBUG("bound stream %"PRIu64" to WT session %"PRIu64, + lsquic_stream_id(stream), sess->wts_stream_id); + if (0 != wt_switch_to_data_if(stream, sess)) + lsquic_stream_maybe_reset(stream, HEC_INTERNAL_ERROR, 1); +} diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 23fb174a9..268d3df73 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -4,6 +4,7 @@ INCLUDE_DIRECTORIES(../src/liblsquic) ENABLE_TESTING() SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DLSQUIC_TEST=1") +target_compile_definitions(lsquic PRIVATE LSQUIC_TEST=1) IF (MSVC) SET(LIB_FLAGS "-FORCE:MULTIPLE") @@ -57,6 +58,7 @@ SET(TESTS quic_be_floats reg_pkt_headergen rst_stream_gquic_be + reset_stream_at_ietf rtt send_headers send_ctl_bw_lifecycle @@ -72,6 +74,7 @@ SET(TESTS trapa varint ver_nego + wt wuf_gquic_be ) @@ -139,6 +142,12 @@ IF(NOT CMAKE_SYSTEM_NAME STREQUAL "Windows") ADD_TEST(h3_framing_zero_size test_h3_framing -s 12) ENDIF() +ADD_EXECUTABLE(fuzz_wt fuzz_wt.c ${ADDL_SOURCES}) +TARGET_LINK_LIBRARIES(fuzz_wt ${LIBS} ${LIB_FLAGS}) +IF(MSVC) + TARGET_LINK_LIBRARIES(fuzz_wt ${GETOPT_LIB}) +ENDIF() + IF(NOT MSVC) ADD_EXECUTABLE(graph_cubic graph_cubic.c ${ADDL_SOURCES}) TARGET_LINK_LIBRARIES(graph_cubic ${LIBS}) diff --git a/tests/fuzz_wt.c b/tests/fuzz_wt.c new file mode 100644 index 000000000..e1170dcd6 --- /dev/null +++ b/tests/fuzz_wt.c @@ -0,0 +1,792 @@ +/* Copyright (c) 2017 - 2026 LiteSpeed Technologies Inc. See LICENSE. */ +#include +#include +#include +#include +#include + +#include "lsquic.h" +#include "lsquic_wt.h" + +int lsquic_wt_test_build_close_capsule (uint64_t code, const char *reason, + size_t reason_len, + unsigned char *buf, size_t *buf_len); +int lsquic_wt_test_remote_close (uint64_t code, const char *reason, + size_t reason_len, unsigned *called, + uint64_t *close_code, + size_t *close_reason_len, int *is_closing, + int *close_received, int *on_close_called); +int lsquic_wt_test_validate_incoming_session_id ( + lsquic_stream_id_t stream_id, lsquic_stream_id_t session_id, + const char *stream_kind, unsigned *error_code); +int lsquic_wt_test_http_dg_read_bytes (const unsigned char *buf, size_t len, + unsigned flags, unsigned *called); +int lsquic_wt_test_close_capsule_payload (const unsigned char *payload, + size_t payload_len, unsigned flags, + unsigned *error_code, + int *is_closing, + int *close_received, + size_t *close_reason_len); +int lsquic_wt_test_uni_read_bytes (const unsigned char *buf, size_t len, + int fin, size_t *consumed, int *done, + lsquic_stream_id_t *session_id); +int lsquic_wt_test_accept_resolution (unsigned initial_flags, + unsigned final_flags, + unsigned existing_sessions, + unsigned *initial_result, + unsigned *final_result, + unsigned *opened, + unsigned *rejected, + unsigned *status); +int lsquic_wt_test_dispatch_reset (int how, int ss_received, int with_ctx, + int with_if, uint64_t rst_in_code, + uint64_t ss_in_code, unsigned *called, + uint64_t *reset_code, uint64_t *stop_code); +int lsquic_wt_test_closing_rejects (unsigned *mask); +int lsquic_wt_test_local_close (uint64_t code, const char *reason, + size_t reason_len, int *queued_capsule, + unsigned *dgq_count); +int lsquic_wt_test_finalize (uint64_t code, const char *reason, + size_t reason_len, unsigned *called, + uint64_t *close_code, + size_t *close_reason_len, int *removed, + unsigned *dropped_datagrams); +int lsquic_wt_test_control_reset_close (unsigned *called, int *is_closing, + int *close_received); +int lsquic_wt_test_http_dg_read (int with_session, int is_control_stream, + int is_closing, unsigned *called); +int lsquic_wt_test_pending_datagram_replay (unsigned *called_before, + unsigned *called_after); +int lsquic_wt_test_pending_datagram_replay_stops_on_close ( + unsigned *called_after, int *is_closing); +int lsquic_wt_test_destroy_while_closing (int is_control_stream, + unsigned *called, int *removed); +int lsquic_wt_test_extra_resp_header_validation (int *null_headers_rejected, + int *zero_len_ok); +int lsquic_wt_test_send_response_rejects_missing_extra_headers ( + int *rejected); +int lsquic_wt_test_response_header_count_validation (int *negative_rejected, + int *overflow_rejected); +int lsquic_wt_test_dgq_overflow_rejected (int incoming, + int *overflow_rejected); +int lsquic_wt_test_open_stream_init_failure (int bidi, int *aborted, + int *freed_dynamic_onnew); +int lsquic_wt_test_datagram_write_state_rollback (int *want_flag_cleared, + int *send_disarmed); +int lsquic_wt_test_http_dg_write_path (unsigned flags, + const unsigned char *buf, size_t len, + size_t max_quic_payload, + unsigned *consume_calls, + unsigned *callback_calls, + unsigned *queued_after, + int *want_flag_set, int *is_closing, + unsigned *disarm_calls, + int *saved_errno); +int lsquic_wt_test_read_error_closes_stream (int *control_closed, + int *uni_closed); +int lsquic_wt_test_uni_read_state (const unsigned char *buf, size_t len, + int fin, size_t *consumed, int *done, + int *malformed, + lsquic_stream_id_t *session_id); +int lsquic_stream_test_truncated_capsule_type_fin_aborts ( + unsigned *error_code); +int lsquic_ietf_test_wt_support (unsigned is_server, + unsigned peer_settings_received, + unsigned local_webtransport, + unsigned http_datagrams, + unsigned quic_datagrams, + unsigned connect_protocol, + unsigned wt_max_sessions_seen, + uint64_t wt_max_sessions, + unsigned wt_enabled_seen, + unsigned wt_enabled, + unsigned wt_initial_max_data_seen, + unsigned wt_initial_max_streams_uni_seen, + unsigned wt_initial_max_streams_bidi_seen, + unsigned peer_reset_stream_at, + unsigned draft, + unsigned *supports, + unsigned *peer_wt_draft); +int lsquic_ietf_test_wt_uni_switch_failure (int *restored_if, + int *restored_ctx, + int *close_attempted); +lsquic_wt_session_t *lsquic_wt_test_dgq_session_new (unsigned max_count, + size_t max_bytes); +void lsquic_wt_test_dgq_session_destroy (lsquic_wt_session_t *sess); +int lsquic_wt_test_dgq_enqueue (lsquic_wt_session_t *sess, const void *buf, + size_t len, + enum lsquic_wt_dg_drop_policy policy); +unsigned lsquic_wt_test_dgq_count (const lsquic_wt_session_t *sess); +size_t lsquic_wt_test_dgq_bytes (const lsquic_wt_session_t *sess); +int lsquic_wt_test_dgq_front (const lsquic_wt_session_t *sess, + unsigned char *val); +int lsquic_wt_test_dgq_back (const lsquic_wt_session_t *sess, + unsigned char *val); + +#define FUZZ_WT_MAX_INPUT (64 * 1024) +#define WT_CLOSE_REASON_MAX 1024 +#define WT_TEST_CP_WEBTRANSPORT (1u << 2) +#define WT_TEST_CP_H3_PEER_SETTINGS (1u << 3) +#define WT_TEST_CP_CONNECT_PROTOCOL (1u << 4) +#define WT_TEST_HTTP_DG_WRITE_PREQUEUE (1u << 0) +#define WT_TEST_HTTP_DG_WRITE_WANT (1u << 1) +#define WT_TEST_HTTP_DG_WRITE_CB_QUEUE (1u << 2) +#define WT_TEST_HTTP_DG_WRITE_CB_FAIL (1u << 3) +#define WT_TEST_HTTP_DG_WRITE_CB_CLOSE (1u << 4) +#define WT_TEST_HTTP_DG_WRITE_CONSUME_FAIL (1u << 5) +#define WT_TEST_HTTP_DG_WRITE_DATAGRAM_MODE (1u << 6) + +static volatile uint64_t s_sink; +static int s_extended_profile; + +struct cursor +{ + const unsigned char *data; + size_t size; + size_t off; +}; + +struct wt_story_state +{ + uint64_t close_code; + lsquic_stream_id_t session_id; + unsigned peer_supports; + unsigned peer_draft; + unsigned accept_status; + size_t close_reason_len; + int uni_done; + int uni_malformed; +}; + +static unsigned +cur_u8 (struct cursor *cur) +{ + if (cur->off >= cur->size) + return 0; + return cur->data[cur->off++]; +} + +static uint64_t +cur_u64 (struct cursor *cur) +{ + uint64_t value; + unsigned i; + + value = 0; + for (i = 0; i < 8; ++i) + value |= (uint64_t) cur_u8(cur) << (i * 8); + return value; +} + +static unsigned +cur_bool (struct cursor *cur) +{ + return cur_u8(cur) & 1; +} + +static const unsigned char * +cur_chunk (struct cursor *cur, size_t *len) +{ + size_t want, avail; + + want = cur_u8(cur); + want |= (size_t) cur_u8(cur) << 8; + avail = cur->size - cur->off; + if (want > avail) + want = avail; + if (len) + *len = want; + if (want == 0) + return NULL; + cur->off += want; + return cur->data + cur->off - want; +} + +static void +fuzz_remote_close (struct cursor *cur) +{ + unsigned called; + uint64_t close_code; + int is_closing, close_received, on_close_called; + size_t reason_len, close_reason_len; + const unsigned char *reason; + + called = 0; + close_code = 0; + is_closing = close_received = on_close_called = 0; + reason = cur_chunk(cur, &reason_len); + if (reason_len > WT_CLOSE_REASON_MAX) + reason_len = WT_CLOSE_REASON_MAX; + (void) lsquic_wt_test_remote_close(cur_u64(cur), (const char *) reason, + reason_len, &called, &close_code, + &close_reason_len, &is_closing, + &close_received, &on_close_called); + s_sink ^= called + close_code + close_reason_len + is_closing + + close_received + on_close_called; +} + +static void +fuzz_http_dg_read_bytes (struct cursor *cur) +{ + unsigned called, flags; + size_t len; + const unsigned char *buf; + + called = 0; + flags = cur_u8(cur) & 7; + buf = cur_chunk(cur, &len); + (void) lsquic_wt_test_http_dg_read_bytes(buf, len, flags, &called); + s_sink ^= called + len; +} + +static void +fuzz_close_capsule_payload (struct cursor *cur) +{ + unsigned error_code, flags; + int is_closing, close_received; + size_t payload_len, close_reason_len; + const unsigned char *payload; + + error_code = 0; + is_closing = close_received = 0; + flags = cur_u8(cur) & 1; + payload = cur_chunk(cur, &payload_len); + if (payload_len > WT_CLOSE_REASON_MAX + 8) + payload_len = WT_CLOSE_REASON_MAX + 8; + (void) lsquic_wt_test_close_capsule_payload(payload, payload_len, flags, + &error_code, &is_closing, + &close_received, + &close_reason_len); + s_sink ^= error_code + is_closing + close_received + close_reason_len; +} + +static void +fuzz_dispatch_reset (struct cursor *cur) +{ + unsigned called; + uint64_t reset_code, stop_code; + + called = 0; + reset_code = 0; + stop_code = 0; + (void) lsquic_wt_test_dispatch_reset(cur_u8(cur) % 3, cur_bool(cur), + cur_bool(cur), cur_bool(cur), + cur_u64(cur), cur_u64(cur), + &called, &reset_code, &stop_code); + s_sink ^= called + reset_code + stop_code; +} + +static void +fuzz_local_close_ex (uint64_t code, const unsigned char *reason, + size_t reason_len) +{ + int queued_capsule; + unsigned dgq_count; + + queued_capsule = 0; + dgq_count = 0; + (void) lsquic_wt_test_local_close(code, (const char *) reason, reason_len, + &queued_capsule, &dgq_count); + s_sink ^= queued_capsule + dgq_count; +} + +static void +fuzz_finalize_ex (uint64_t code, const unsigned char *reason, + size_t reason_len) +{ + unsigned called, dropped_datagrams; + uint64_t close_code; + size_t close_reason_len; + int removed; + + called = 0; + dropped_datagrams = 0; + close_code = 0; + close_reason_len = 0; + removed = 0; + (void) lsquic_wt_test_finalize(code, (const char *) reason, reason_len, + &called, &close_code, &close_reason_len, + &removed, &dropped_datagrams); + s_sink ^= called + close_code + close_reason_len + removed + + dropped_datagrams; +} + +static void +fuzz_http_dg_delivery (struct cursor *cur) +{ + unsigned called; + + called = 0; + (void) lsquic_wt_test_http_dg_read(cur_bool(cur), cur_bool(cur), + cur_bool(cur), &called); + s_sink ^= called; +} + +static void +fuzz_uni_read_state (struct cursor *cur, struct wt_story_state *state) +{ + lsquic_stream_id_t session_id; + size_t len, consumed; + int done, malformed, fin; + const unsigned char *buf; + + consumed = 0; + done = 0; + malformed = 0; + session_id = 0; + fin = !!cur_bool(cur); + buf = cur_chunk(cur, &len); + (void) lsquic_wt_test_uni_read_state(buf, len, fin, &consumed, &done, + &malformed, &session_id); + if (state) + { + state->session_id = session_id; + state->uni_done = done; + state->uni_malformed = malformed; + } + s_sink ^= consumed + done + malformed + session_id; +} + +static void +fuzz_truncated_capsule_type (void) +{ + unsigned error_code; + + error_code = 0; + (void) lsquic_stream_test_truncated_capsule_type_fin_aborts(&error_code); + s_sink ^= error_code; +} + +static void +fuzz_extra_resp_header_validation (void) +{ + int null_headers_rejected, zero_len_ok; + + null_headers_rejected = 0; + zero_len_ok = 0; + (void) lsquic_wt_test_extra_resp_header_validation( + &null_headers_rejected, &zero_len_ok); + s_sink ^= null_headers_rejected + zero_len_ok; +} + +static void +fuzz_send_response_validation (void) +{ + int rejected, negative_rejected, overflow_rejected; + + rejected = negative_rejected = overflow_rejected = 0; + (void) lsquic_wt_test_send_response_rejects_missing_extra_headers( + &rejected); + (void) lsquic_wt_test_response_header_count_validation( + &negative_rejected, &overflow_rejected); + s_sink ^= rejected + negative_rejected + overflow_rejected; +} + +static unsigned +story_accept_flags (unsigned profile, unsigned supports, unsigned is_server) +{ + unsigned flags; + + flags = 0; + if (profile & 1) + flags |= WT_TEST_CP_H3_PEER_SETTINGS; + if ((profile & 2) && supports) + flags |= WT_TEST_CP_WEBTRANSPORT; + if (!is_server && (profile & 4)) + flags |= WT_TEST_CP_CONNECT_PROTOCOL; + return flags; +} + +static void +fuzz_closing_rejects (void) +{ + unsigned mask; + + mask = 0; + (void) lsquic_wt_test_closing_rejects(&mask); + s_sink ^= mask; +} + +static void +fuzz_control_reset_close (void) +{ + unsigned called; + int is_closing, close_received; + + called = 0; + is_closing = close_received = 0; + (void) lsquic_wt_test_control_reset_close(&called, &is_closing, + &close_received); + s_sink ^= called + is_closing + close_received; +} + +static void +fuzz_destroy_while_closing (struct cursor *cur) +{ + unsigned called; + int removed; + + called = 0; + removed = 0; + (void) lsquic_wt_test_destroy_while_closing(cur_u8(cur) & 1, &called, + &removed); + s_sink ^= called + removed; +} + +static void +fuzz_open_stream_init_failure (struct cursor *cur) +{ + int aborted, freed_dynamic_onnew; + + aborted = freed_dynamic_onnew = 0; + (void) lsquic_wt_test_open_stream_init_failure(cur_u8(cur) & 1, &aborted, + &freed_dynamic_onnew); + s_sink ^= aborted + freed_dynamic_onnew; +} + +static void +fuzz_datagram_write_state_rollback (void) +{ + int want_flag_cleared, send_disarmed; + + want_flag_cleared = send_disarmed = 0; + (void) lsquic_wt_test_datagram_write_state_rollback(&want_flag_cleared, + &send_disarmed); + s_sink ^= want_flag_cleared + send_disarmed; +} + +static void +fuzz_http_dg_write_path (struct cursor *cur) +{ + unsigned consume_calls, callback_calls, queued_after, disarm_calls; + int want_flag_set, is_closing, saved_errno; + size_t len; + const unsigned char *buf; + + consume_calls = callback_calls = queued_after = disarm_calls = 0; + want_flag_set = is_closing = saved_errno = 0; + buf = cur_chunk(cur, &len); + if (len > 64) + len = 64; + (void) lsquic_wt_test_http_dg_write_path(cur_u8(cur) & 0x7F, buf, len, + cur_u8(cur), + &consume_calls, &callback_calls, + &queued_after, &want_flag_set, + &is_closing, &disarm_calls, + &saved_errno); + s_sink ^= consume_calls + callback_calls + queued_after + want_flag_set + + is_closing + disarm_calls + (unsigned) saved_errno; +} + +static void +fuzz_read_error_closes_stream (void) +{ + int control_closed, uni_closed; + + control_closed = uni_closed = 0; + (void) lsquic_wt_test_read_error_closes_stream(&control_closed, + &uni_closed); + s_sink ^= control_closed + uni_closed; +} + +static void +fuzz_uni_switch_failure (void) +{ + int restored_if, restored_ctx, close_attempted; + + restored_if = restored_ctx = close_attempted = 0; + (void) lsquic_ietf_test_wt_uni_switch_failure(&restored_if, &restored_ctx, + &close_attempted); + s_sink ^= restored_if + restored_ctx + close_attempted; +} + +static void +fuzz_queue_story (struct cursor *cur) +{ + lsquic_wt_session_t *sess; + enum lsquic_wt_dg_drop_policy policy; + unsigned i, nops, count, called_before, called_after, front, back; + int is_closing, overflow_rejected; + size_t max_bytes, len, bytes; + const unsigned char *buf; + unsigned char scratch; + + sess = lsquic_wt_test_dgq_session_new(1 + (cur_u8(cur) % 8), + 1 + (cur_u8(cur) % 64)); + if (sess) + { + nops = 1 + (cur_u8(cur) % 4); + scratch = cur_u8(cur); + for (i = 0; i < nops; ++i) + { + buf = cur_chunk(cur, &len); + if (len > 32) + len = 32; + if (!buf && cur_bool(cur)) + { + buf = &scratch; + len = 1; + } + policy = (enum lsquic_wt_dg_drop_policy) (cur_u8(cur) % 3); + (void) lsquic_wt_test_dgq_enqueue(sess, + buf ? (const void *) buf : "", + len, policy); + } + + count = lsquic_wt_test_dgq_count(sess); + bytes = lsquic_wt_test_dgq_bytes(sess); + front = back = 0; + (void) lsquic_wt_test_dgq_front(sess, (unsigned char *) &front); + (void) lsquic_wt_test_dgq_back(sess, (unsigned char *) &back); + s_sink ^= count + bytes + front + back; + lsquic_wt_test_dgq_session_destroy(sess); + } + + called_before = called_after = 0; + is_closing = 0; + if (cur_bool(cur)) + (void) lsquic_wt_test_pending_datagram_replay_stops_on_close( + &called_after, &is_closing); + else + (void) lsquic_wt_test_pending_datagram_replay(&called_before, + &called_after); + overflow_rejected = 0; + (void) lsquic_wt_test_dgq_overflow_rejected(cur_bool(cur), + &overflow_rejected); + max_bytes = cur->size - cur->off; + s_sink ^= called_before + called_after + is_closing + overflow_rejected + + max_bytes; +} + +static void +fuzz_negotiation_story (struct cursor *cur, struct wt_story_state *state) +{ + unsigned supports, draft; + unsigned bits, is_server; + unsigned initial_result, final_result, opened, rejected, status; + unsigned initial_flags, final_flags; + + supports = draft = 0; + bits = cur_u8(cur); + is_server = bits & 1; + (void) lsquic_ietf_test_wt_support( + is_server, + bits & 2, + bits & 4, + bits & 8, + bits & 16, + bits & 32, + cur_bool(cur), + cur_u64(cur), + cur_bool(cur), + cur_bool(cur), + cur_bool(cur), + cur_bool(cur), + cur_bool(cur), + cur_bool(cur), + 14 + (cur_bool(cur)), + &supports, &draft); + + initial_flags = story_accept_flags(cur_u8(cur), supports, is_server); + final_flags = story_accept_flags(cur_u8(cur), supports, is_server); + if (cur_bool(cur)) + final_flags |= initial_flags; + + initial_result = final_result = opened = rejected = status = 0; + (void) lsquic_wt_test_accept_resolution(initial_flags, final_flags, + cur_u8(cur) % 3, + &initial_result, &final_result, + &opened, &rejected, &status); + if (state) + { + state->peer_supports = supports; + state->peer_draft = draft; + state->accept_status = status; + } + s_sink ^= supports + draft + initial_result + final_result + opened + + rejected + status; +} + +static void +fuzz_uni_story (struct cursor *cur, struct wt_story_state *state) +{ + unsigned error_code; + lsquic_stream_id_t stream_id, session_id; + const char *kind; + + fuzz_uni_read_state(cur, state); + session_id = state ? state->session_id : 0; + if (state && state->uni_done && !state->uni_malformed) + { + error_code = 0; + kind = cur_bool(cur) ? "bidi" : "uni"; + stream_id = session_id + (cur_bool(cur) ? 0 : 4); + (void) lsquic_wt_test_validate_incoming_session_id(stream_id, + session_id, + kind, + &error_code); + s_sink ^= error_code + stream_id; + } + + if (cur_bool(cur)) + fuzz_uni_switch_failure(); + if (cur_bool(cur)) + fuzz_open_stream_init_failure(cur); +} + +static void +fuzz_datagram_story (struct cursor *cur) +{ + fuzz_http_dg_delivery(cur); + fuzz_http_dg_read_bytes(cur); + fuzz_close_capsule_payload(cur); + fuzz_http_dg_write_path(cur); + if (cur_bool(cur)) + fuzz_truncated_capsule_type(); + if (cur_bool(cur)) + fuzz_datagram_write_state_rollback(); +} + +static void +fuzz_close_story (struct cursor *cur, struct wt_story_state *state) +{ + uint64_t code; + size_t reason_len; + const unsigned char *reason; + + code = cur_u64(cur); + reason = cur_chunk(cur, &reason_len); + if (reason_len > WT_CLOSE_REASON_MAX) + reason_len = WT_CLOSE_REASON_MAX; + if (state) + { + state->close_code = code; + state->close_reason_len = reason_len; + } + + if (0 == reason_len) + reason = (const unsigned char *) ""; + + if (0 == lsquic_wt_test_build_close_capsule(code, (const char *) reason, + reason_len, (unsigned char[WT_CLOSE_REASON_MAX + 32]){0}, + &(size_t){ WT_CLOSE_REASON_MAX + 32 })) + s_sink ^= reason_len; + fuzz_remote_close(cur); + fuzz_local_close_ex(code, reason, reason_len); + if (cur_bool(cur)) + fuzz_finalize_ex(code, reason, reason_len); + if (cur_bool(cur)) + fuzz_control_reset_close(); + if (cur_bool(cur)) + fuzz_destroy_while_closing(cur); +} + +static void +fuzz_api_story (struct cursor *cur) +{ + fuzz_extra_resp_header_validation(); + fuzz_send_response_validation(); + fuzz_closing_rejects(); + fuzz_dispatch_reset(cur); + fuzz_read_error_closes_stream(); + if (cur_bool(cur)) + fuzz_open_stream_init_failure(cur); +} + +static void +fuzz_story_sequence (struct cursor *cur, struct wt_story_state *state) +{ + fuzz_negotiation_story(cur, state); + fuzz_uni_story(cur, state); + fuzz_datagram_story(cur); + fuzz_queue_story(cur); + fuzz_close_story(cur, state); + fuzz_api_story(cur); +} + +static void +fuzz_one (const unsigned char *data, size_t size) +{ + struct cursor cur; + struct wt_story_state state; + unsigned i, nops; + + if (!data || size == 0) + return; + + memset(&state, 0, sizeof(state)); + cur.data = data; + cur.size = size; + cur.off = 1; + fuzz_story_sequence(&cur, &state); + + nops = s_extended_profile ? 2 + (data[0] & 7) : 1 + (data[0] & 3); + + for (i = 0; i < nops; ++i) + switch (cur_u8(&cur) % 6) + { + case 0: fuzz_negotiation_story(&cur, &state); break; + case 1: fuzz_uni_story(&cur, &state); break; + case 2: fuzz_datagram_story(&cur); break; + case 3: fuzz_queue_story(&cur); break; + case 4: fuzz_close_story(&cur, &state); break; + case 5: fuzz_api_story(&cur); break; + } +} + +static int +read_input (const char *path, unsigned char **buf, size_t *len) +{ + FILE *fp; + unsigned char *mem; + size_t cap, nread, off; + + fp = path ? fopen(path, "rb") : stdin; + if (!fp) + return -1; + + mem = malloc(FUZZ_WT_MAX_INPUT); + if (!mem) + { + if (path) + fclose(fp); + return -1; + } + + cap = FUZZ_WT_MAX_INPUT; + off = 0; + do + { + nread = fread(mem + off, 1, cap - off, fp); + off += nread; + } + while (nread > 0 && off < cap); + + if (path) + fclose(fp); + *buf = mem; + *len = off; + return 0; +} + +int +main (int argc, char **argv) +{ + unsigned char *buf; + size_t len; + const char *profile; + + if (argc > 2) + return 1; + + buf = NULL; + len = 0; + profile = getenv("FUZZ_WT_PROFILE"); + s_extended_profile = profile && 0 == strcmp(profile, "extended"); + if (0 != read_input(argc == 2 ? argv[1] : NULL, &buf, &len)) + return 1; + + fuzz_one(buf, len); + free(buf); + return 0; +} diff --git a/tests/fuzz_wt_corpus/seed_capsule b/tests/fuzz_wt_corpus/seed_capsule new file mode 100644 index 000000000..11dc89016 --- /dev/null +++ b/tests/fuzz_wt_corpus/seed_capsule @@ -0,0 +1 @@ +ABCDEFpayload diff --git a/tests/fuzz_wt_corpus/seed_datagram b/tests/fuzz_wt_corpus/seed_datagram new file mode 100644 index 000000000..e3d871108 --- /dev/null +++ b/tests/fuzz_wt_corpus/seed_datagram @@ -0,0 +1 @@ +datagram-seed-001 diff --git a/tests/fuzz_wt_corpus/seed_replay b/tests/fuzz_wt_corpus/seed_replay new file mode 100644 index 000000000..5473aa242 --- /dev/null +++ b/tests/fuzz_wt_corpus/seed_replay @@ -0,0 +1 @@ +WT-replay-seed-pending-datagram diff --git a/tests/fuzz_wt_corpus/seed_story b/tests/fuzz_wt_corpus/seed_story new file mode 100644 index 000000000..212f9f07a --- /dev/null +++ b/tests/fuzz_wt_corpus/seed_story @@ -0,0 +1 @@ +WT-story-seed-accept-close-dgram diff --git a/tests/fuzz_wt_corpus/seed_uni b/tests/fuzz_wt_corpus/seed_uni new file mode 100644 index 000000000..9c55edbba --- /dev/null +++ b/tests/fuzz_wt_corpus/seed_uni @@ -0,0 +1 @@ +uni-stream-seed diff --git a/tests/test_engine_ctor.c b/tests/test_engine_ctor.c index 45cdca296..ce5324679 100644 --- a/tests/test_engine_ctor.c +++ b/tests/test_engine_ctor.c @@ -12,6 +12,7 @@ main (void) struct lsquic_engine_settings settings; lsquic_engine_t *engine; unsigned versions; + char err_buf[0x100]; const unsigned flags = LSENG_SERVER; assert(0 == lsquic_global_init(LSQUIC_GLOBAL_SERVER)); @@ -33,6 +34,16 @@ main (void) engine = lsquic_engine_new(flags, &api); assert(!engine); + lsquic_engine_init_settings(&settings, flags); + settings.es_webtransport = 1; + settings.es_http_datagrams = 1; + settings.es_reset_stream_at = 1; + settings.es_max_webtransport_sessions = 2; + memset(err_buf, 0, sizeof(err_buf)); + assert(0 != lsquic_engine_check_settings(&settings, flags, + err_buf, sizeof(err_buf))); + assert(strstr(err_buf, "only supports 1 session")); + lsquic_global_cleanup(); return 0; } diff --git a/tests/test_mini_conn_delay.c b/tests/test_mini_conn_delay.c index 2560aa74d..568ee7d6a 100644 --- a/tests/test_mini_conn_delay.c +++ b/tests/test_mini_conn_delay.c @@ -1,8 +1,6 @@ /* Copyright (c) 2017 - 2022 LiteSpeed Technologies Inc. See LICENSE. */ /* Test for mini connection delayed packet data corruption bug */ -#undef LSQUIC_TEST - #include #include #include diff --git a/tests/test_reset_stream_at_ietf.c b/tests/test_reset_stream_at_ietf.c new file mode 100644 index 000000000..5fe58d0f7 --- /dev/null +++ b/tests/test_reset_stream_at_ietf.c @@ -0,0 +1,96 @@ +/* Copyright (c) 2017 - 2026 LiteSpeed Technologies Inc. See LICENSE. */ +#include +#include +#include + +#include "lsquic.h" +#include "lsquic_types.h" +#include "lsquic_parse.h" + +//static const struct parse_funcs *const pf = select_pf_by_ver(LSQVER_I001); // will not work on MSVC +#define pf ((const struct parse_funcs *const)select_pf_by_ver(LSQVER_I001)) + +struct reset_stream_at_test { + unsigned char buf[0x20]; + size_t buf_len; + lsquic_stream_id_t stream_id; + uint64_t error_code; + uint64_t final_size; + uint64_t reliable_size; +}; + +static const struct reset_stream_at_test tests[] = { + /* All fields fit in 1-byte varints */ + { + .buf = { 0x24, 0x03, 0x02, 0x07, 0x05 }, + .buf_len = 5, + .stream_id = 0x03, + .error_code = 0x02, + .final_size = 0x07, + .reliable_size = 0x05, + }, + + /* All fields use 2-byte varints (0x40) */ + { + .buf = { 0x24, + 0x40, 0x40, + 0x40, 0x40, + 0x40, 0x40, + 0x40, 0x40, + }, + .buf_len = 9, + .stream_id = 0x40, + .error_code = 0x40, + .final_size = 0x40, + .reliable_size = 0x40, + }, + + { .buf = { 0 }, } +}; + + +static void +run_parse_tests (void) +{ + const struct reset_stream_at_test *test; + for (test = tests; test->buf[0]; ++test) + { + lsquic_stream_id_t stream_id = ~0; + uint64_t error_code = ~0; + uint64_t final_size = ~0; + uint64_t reliable_size = ~0; + int sz = pf->pf_parse_reset_stream_at_frame(test->buf, test->buf_len, + &stream_id, &error_code, &final_size, + &reliable_size); + assert(sz == (int) test->buf_len); + assert(stream_id == test->stream_id); + assert(error_code == test->error_code); + assert(final_size == test->final_size); + assert(reliable_size == test->reliable_size); + } +} + + +static void +run_gen_tests (void) +{ + const struct reset_stream_at_test *test; + for (test = tests; test->buf[0]; ++test) + { + unsigned char buf[0x100]; + int sz = pf->pf_gen_reset_stream_at_frame(buf, test->buf_len, + test->stream_id, test->error_code, test->final_size, + test->reliable_size); + assert(sz == (int) test->buf_len); + assert(0 == memcmp(buf, test->buf, test->buf_len)); + } +} + + +int +main (void) +{ + run_parse_tests(); + run_gen_tests(); + return 0; +} diff --git a/tests/test_stream.c b/tests/test_stream.c index 24de0ae2c..10f40ed5d 100644 --- a/tests/test_stream.c +++ b/tests/test_stream.c @@ -2,6 +2,7 @@ #include #include #include +#include #include #include #include @@ -20,6 +21,7 @@ #include "lsquic_alarmset.h" #include "lsquic_packet_in.h" #include "lsquic_conn_flow.h" +#include "lsquic_ietf.h" #include "lsquic_rtt.h" #include "lsquic_sfcw.h" #include "lsquic_varint.h" @@ -33,7 +35,10 @@ #include "lsquic_logger.h" #include "lsquic_parse.h" #include "lsquic_conn.h" +#include "lsquic_enc_sess.h" #include "lsquic_engine_public.h" +#include "lsquic_sizes.h" +#include "lsquic_trans_params.h" #include "lsquic_cubic.h" #include "lsquic_pacer.h" #include "lsquic_senhist.h" @@ -48,6 +53,8 @@ #include "lsqpack.h" #include "lsquic_frab_list.h" #include "lsquic_qenc_hdl.h" +#include "lsquic_http1x_if.h" +#include "lsquic_qdec_hdl.h" #include "lsquic_varint.h" #include "lsquic_hq.h" #include "lsquic_data_in_if.h" @@ -182,6 +189,22 @@ static struct reset_call_ctx { int how; } s_onreset_called = { NULL, -1, }; +static struct transport_params s_reset_stream_at_params = { + .tp_set = (1 << TPI_RESET_STREAM_AT), +}; +static struct transport_params s_no_reset_stream_at_params; +static struct transport_params *s_peer_tp = &s_reset_stream_at_params; + +static struct transport_params * +test_get_peer_transport_params (enc_session_t *UNUSED_session) +{ + return s_peer_tp; +} + +static const struct enc_session_funcs_iquic test_reset_stream_at_esfi = { + .esfi_get_peer_transport_params = test_get_peer_transport_params, +}; + static void on_reset (lsquic_stream_t *stream, lsquic_stream_ctx_t *h, int how) @@ -196,6 +219,172 @@ const struct lsquic_stream_if stream_if = { .on_reset = on_reset, }; +static const struct lsquic_stream_if stream_if_no_reset = { + .on_new_stream = on_new_stream, + .on_close = on_close, + .on_reset = NULL, +}; + +struct http_dg_test_ctx { + lsquic_stream_t *stream; + unsigned read_calls; + size_t read_len; + unsigned char read_buf[64]; +}; + +static lsquic_stream_ctx_t * +http_dg_on_new_stream (void *stream_if_ctx, lsquic_stream_t *stream) +{ + struct http_dg_test_ctx *ctx = stream_if_ctx; + ctx->stream = stream; + return (lsquic_stream_ctx_t *) ctx; +} + +static void +http_dg_on_read (lsquic_stream_t *UNUSED_stream, lsquic_stream_ctx_t *UNUSED_h) +{ +} + +static void +http_dg_on_write (lsquic_stream_t *UNUSED_stream, lsquic_stream_ctx_t *UNUSED_h) +{ +} + +static void +http_dg_on_close (lsquic_stream_t *UNUSED_stream, lsquic_stream_ctx_t *UNUSED_h) +{ +} + +static int +http_dg_on_write_datagram (lsquic_stream_t *UNUSED_stream, + lsquic_stream_ctx_t *UNUSED_h, size_t UNUSED_max_quic_payload, + lsquic_http_dg_consume_f UNUSED_consume_datagram) +{ + return 0; +} + +static void +http_dg_on_read_datagram (lsquic_stream_t *UNUSED_stream, lsquic_stream_ctx_t *h, + const void *buf, size_t bufsz) +{ + struct http_dg_test_ctx *ctx = (struct http_dg_test_ctx *) h; + size_t to_copy; + + ++ctx->read_calls; + ctx->read_len = bufsz; + to_copy = bufsz; + if (to_copy > sizeof(ctx->read_buf)) + to_copy = sizeof(ctx->read_buf); + memcpy(ctx->read_buf, buf, to_copy); +} + +static void +http_capsule_test_cb (lsquic_stream_t *UNUSED_stream, lsquic_stream_ctx_t *h, + uint64_t UNUSED_capsule_type, const void *payload, size_t payload_len) +{ + struct http_dg_test_ctx *ctx = (struct http_dg_test_ctx *) h; + size_t to_copy; + + ++ctx->read_calls; + ctx->read_len = payload_len; + to_copy = payload_len; + if (to_copy > sizeof(ctx->read_buf)) + to_copy = sizeof(ctx->read_buf); + memcpy(ctx->read_buf, payload, to_copy); +} + +static const struct lsquic_stream_if http_dg_stream_if = { + .on_new_stream = http_dg_on_new_stream, + .on_read = http_dg_on_read, + .on_write = http_dg_on_write, + .on_close = http_dg_on_close, + .on_http_dg_write = http_dg_on_write_datagram, + .on_http_dg_read = http_dg_on_read_datagram, +}; + +static struct { + int called; + unsigned error_code; + char reason[128]; +} s_abort_error; + +static size_t s_max_dgram_size; + +extern size_t +lsquic_http_dg_capsule_readf (void *ctx, const unsigned char *buf, + size_t len, int fin); + +static void +abort_error (struct lsquic_conn *UNUSED_lconn, int UNUSED_is_app, + unsigned error_code, const char *format, ...) +{ + va_list ap; + + s_abort_error.called = 1; + s_abort_error.error_code = error_code; + va_start(ap, format); + vsnprintf(s_abort_error.reason, sizeof(s_abort_error.reason), format, ap); + va_end(ap); +} + +static size_t +get_max_datagram_size (struct lsquic_conn *UNUSED_lconn) +{ + return s_max_dgram_size; +} + +#define H3_CAPSULE_DATAGRAM 0x00 + +static size_t +make_capsule (unsigned char *buf, size_t bufsz, uint64_t type, + const void *payload, size_t payload_sz) +{ + uint64_t bits; + size_t len, off = 0; + + bits = vint_val2bits(type); + len = 1u << bits; + assert(off + len <= bufsz); + vint_write(buf + off, type, bits, len); + off += len; + + bits = vint_val2bits(payload_sz); + len = 1u << bits; + assert(off + len + payload_sz <= bufsz); + vint_write(buf + off, payload_sz, bits, len); + off += len; + + if (payload_sz) + memcpy(buf + off, payload, payload_sz); + + return off + payload_sz; +} + +static size_t +make_hq_data_frame (unsigned char *buf, size_t bufsz, + const void *payload, size_t payload_sz) +{ + uint64_t bits; + size_t len, off = 0; + + bits = vint_val2bits(HQFT_DATA); + len = 1u << bits; + assert(off + len <= bufsz); + vint_write(buf + off, HQFT_DATA, bits, len); + off += len; + + bits = vint_val2bits(payload_sz); + len = 1u << bits; + assert(off + len + payload_sz <= bufsz); + vint_write(buf + off, payload_sz, bits, len); + off += len; + + if (payload_sz) + memcpy(buf + off, payload, payload_sz); + + return off + payload_sz; +} + /* This does not do anything beyond just acking the packet: we do not attempt * to update the send controller to have the correct state. @@ -350,6 +539,8 @@ user_stream_progress (struct lsquic_conn *lconn) static const struct conn_iface our_conn_if = { .ci_can_write_ack = can_write_ack, + .ci_abort_error = abort_error, + .ci_get_max_datagram_size = get_max_datagram_size, .ci_get_path = get_network_path, .ci_write_ack = write_ack, .ci_user_stream_progress = user_stream_progress, @@ -377,6 +568,7 @@ init_test_objs (struct test_objs *tobjs, unsigned initial_conn_window, TAILQ_INIT(&tobjs->conn_pub.read_streams); TAILQ_INIT(&tobjs->conn_pub.write_streams); TAILQ_INIT(&tobjs->conn_pub.service_streams); + TAILQ_INIT(&tobjs->conn_pub.http_dg_streams); lsquic_cfcw_init(&tobjs->conn_pub.cfcw, &tobjs->conn_pub, initial_conn_window); lsquic_conn_cap_init(&tobjs->conn_pub.conn_cap, initial_conn_window); @@ -1064,149 +1256,673 @@ test_loc_data_rem_RST (struct test_objs *tobjs) else assert(s_onreset_called.how == 2); - ack_packet(&tobjs->send_ctl, 1); + ack_packet(&tobjs->send_ctl, 1); + + if (!(stream->sm_bflags & SMBF_IETF)) + { + assert(!TAILQ_EMPTY(&tobjs->conn_pub.sending_streams)); + assert((stream->sm_qflags & SMQF_SENDING_FLAGS) == SMQF_SEND_RST); + } + + /* Not yet closed: error needs to be collected */ + assert(TAILQ_EMPTY(&tobjs->conn_pub.service_streams)); + assert(0 == (stream->sm_qflags & SMQF_SERVICE_FLAGS)); + + n = lsquic_stream_write(stream, buf, 100); + if (stream->sm_bflags & SMBF_IETF) + assert(100 == n); /* Write successful after reset in IETF */ + else + assert(-1 == n); /* Error collected */ + s = lsquic_stream_close(stream); + assert(0 == s); /* Stream successfully closed */ + + if (stream->sm_bflags & SMBF_IETF) + assert(stream->n_unacked == 1); + + assert(!TAILQ_EMPTY(&tobjs->conn_pub.service_streams)); + assert((stream->sm_qflags & SMQF_SERVICE_FLAGS) == SMQF_CALL_ONCLOSE); + + if (!(stream->sm_bflags & SMBF_IETF)) + lsquic_stream_rst_frame_sent(stream); + + lsquic_stream_call_on_close(stream); + + assert(TAILQ_EMPTY(&tobjs->conn_pub.sending_streams)); + + if (stream->sm_bflags & SMBF_IETF) + { + /* FIN packet has not been acked yet: */ + assert(TAILQ_EMPTY(&tobjs->conn_pub.service_streams)); + /* Now ack it: */ + ack_packet(&tobjs->send_ctl, 1); + } + + assert(!TAILQ_EMPTY(&tobjs->conn_pub.service_streams)); + assert((stream->sm_qflags & SMQF_SERVICE_FLAGS) == SMQF_FREE_STREAM); + + lsquic_stream_destroy(stream); + assert(TAILQ_EMPTY(&tobjs->conn_pub.service_streams)); + + assert(200 == tobjs->conn_pub.cfcw.cf_max_recv_off); + assert(200 == tobjs->conn_pub.cfcw.cf_read_off); +} + + +/* Client: we send some data (no FIN), and remote end sends some data and + * then sends STOP_SENDING + */ +static void +test_loc_data_rem_SS (struct test_objs *tobjs) +{ + lsquic_packet_out_t *packet_out; + lsquic_stream_t *stream; + char buf_out[0x100]; + unsigned char buf[0x100]; + ssize_t n; + int s, fin; + + init_buf(buf_out, sizeof(buf_out)); + + stream = new_stream(tobjs, 345); + assert(stream->sm_bflags & SMBF_IETF); /* STOP_SENDING is IETF-only */ + n = lsquic_stream_write(stream, buf_out, 100); + assert(n == 100); + assert(0 == lsquic_send_ctl_n_scheduled(&tobjs->send_ctl)); + + s = lsquic_stream_flush(stream); + assert(1 == lsquic_send_ctl_n_scheduled(&tobjs->send_ctl)); + + n = read_from_scheduled_packets(&tobjs->send_ctl, stream->id, buf, + sizeof(buf), 0, &fin, 0); + assert(100 == n); + assert(0 == memcmp(buf_out, buf, 100)); + assert(!fin); + + /* Pretend we sent out a packet: */ + packet_out = lsquic_send_ctl_next_packet_to_send(&tobjs->send_ctl, 0); + lsquic_send_ctl_sent_packet(&tobjs->send_ctl, packet_out); + + s = lsquic_stream_frame_in(stream, new_frame_in(tobjs, 0, 100, 0)); + assert(0 == s); + s_onreset_called = (struct reset_call_ctx) { NULL, -1, }; + lsquic_stream_stop_sending_in(stream, 12345); + assert(s_onreset_called.stream == stream); + assert(s_onreset_called.how == 1); + + /* Incoming STOP_SENDING should not affect the ability to read from + * stream. + */ + unsigned char mybuf[123]; + const ssize_t nread = lsquic_stream_read(stream, mybuf, sizeof(mybuf)); + assert(nread == 100); + + ack_packet(&tobjs->send_ctl, 1); + + if (!(stream->sm_bflags & SMBF_IETF)) + { + assert(!TAILQ_EMPTY(&tobjs->conn_pub.sending_streams)); + assert((stream->sm_qflags & SMQF_SENDING_FLAGS) == SMQF_SEND_RST); + } + + /* Not yet closed: error needs to be collected */ + assert(TAILQ_EMPTY(&tobjs->conn_pub.service_streams)); + assert(0 == (stream->sm_qflags & SMQF_SERVICE_FLAGS)); + + n = lsquic_stream_write(stream, buf, 100); + assert(-1 == n); /* Error collected */ + s = lsquic_stream_close(stream); + assert(0 == s); /* Stream successfully closed */ + + assert(!TAILQ_EMPTY(&tobjs->conn_pub.service_streams)); + assert((stream->sm_qflags & SMQF_SERVICE_FLAGS) == SMQF_CALL_ONCLOSE); + + if (stream->sm_bflags & SMBF_IETF) + lsquic_stream_ss_frame_sent(stream); + lsquic_stream_rst_frame_sent(stream); + lsquic_stream_call_on_close(stream); + + assert(TAILQ_EMPTY(&tobjs->conn_pub.sending_streams)); + if (stream->sm_bflags & SMBF_IETF) + assert(stream->sm_qflags & SMQF_WAIT_FIN_OFF); + else + { + assert(!TAILQ_EMPTY(&tobjs->conn_pub.service_streams)); + assert((stream->sm_qflags & SMQF_SERVICE_FLAGS) == SMQF_FREE_STREAM); + } + + const unsigned expected_nread = stream->sm_bflags & SMBF_IETF ? 100 : 200; + lsquic_stream_destroy(stream); + assert(TAILQ_EMPTY(&tobjs->conn_pub.service_streams)); + + assert(expected_nread == tobjs->conn_pub.cfcw.cf_max_recv_off); + assert(expected_nread == tobjs->conn_pub.cfcw.cf_read_off); +} + + +static void +test_rst_stream_duplicate (struct test_objs *tobjs) +{ + lsquic_stream_t *stream; + int s; + + stream = new_stream(tobjs, 345); + + s_onreset_called = (struct reset_call_ctx) { NULL, -1, }; + s = lsquic_stream_rst_in(stream, 0, 0); + assert(0 == s); + assert(s_onreset_called.stream == stream); + if (stream->sm_bflags & SMBF_IETF) + assert(s_onreset_called.how == 0); + else + assert(s_onreset_called.how == 2); + + s_onreset_called = (struct reset_call_ctx) { NULL, -1, }; + s = lsquic_stream_rst_in(stream, 0, 0); + assert(0 == s); + assert(s_onreset_called.stream == NULL); + + lsquic_stream_destroy(stream); +} + + +static void +test_rst_stream_smaller_than_seen (struct test_objs *tobjs) +{ + lsquic_stream_t *stream; + int s; + + stream = new_stream(tobjs, 345); + s = lsquic_sfcw_set_max_recv_off(&stream->fc, 200); + assert(s); + + s_onreset_called = (struct reset_call_ctx) { NULL, -1, }; + s = lsquic_stream_rst_in(stream, 100, 0); + assert(s < 0); + if (stream->sm_bflags & SMBF_IETF) + assert(stream->stream_flags & STREAM_RESET_AT_RECVD); + else + assert(stream->stream_flags & STREAM_RST_RECVD); + assert(s_onreset_called.stream == NULL); + + lsquic_stream_destroy(stream); +} + + +static void +test_rst_stream_flow_control_violation (struct test_objs *tobjs) +{ + lsquic_stream_t *stream; + uint64_t offset; + int s; + + stream = new_stream(tobjs, 345); + offset = lsquic_sfcw_get_fc_recv_off(&stream->fc) + 1; + + s_onreset_called = (struct reset_call_ctx) { NULL, -1, }; + s = lsquic_stream_rst_in(stream, offset, 0); + assert(s < 0); + if (stream->sm_bflags & SMBF_IETF) + assert(stream->stream_flags & STREAM_RESET_AT_RECVD); + else + assert(stream->stream_flags & STREAM_RST_RECVD); + assert(s_onreset_called.stream == NULL); + + lsquic_stream_destroy(stream); +} + + +static void +test_rst_stream_final_size_mismatch (struct test_objs *tobjs) +{ + lsquic_stream_t *stream; + int s; + + stream = new_stream(tobjs, 345); + assert(stream->sm_bflags & SMBF_IETF); + stream->stream_flags |= STREAM_FIN_RECVD; + stream->sm_fin_off = 100; + memset(&s_abort_error, 0, sizeof(s_abort_error)); + + s_onreset_called = (struct reset_call_ctx) { NULL, -1, }; + s = lsquic_stream_rst_in(stream, 200, 0); + assert(s < 0); + assert(s_abort_error.called); + assert(s_abort_error.error_code == TEC_FINAL_SIZE_ERROR); + assert(s_onreset_called.stream == NULL); + + lsquic_stream_destroy(stream); +} + + +static void +test_reset_stream_at_updates_and_errors (struct test_objs *tobjs) +{ + lsquic_stream_t *stream; + int s; + + stream = new_stream(tobjs, 345); + assert(stream->sm_bflags & SMBF_IETF); + + stream->sm_qflags |= SMQF_WAIT_FIN_OFF; + stream->read_offset = 100; + + s_onreset_called = (struct reset_call_ctx) { NULL, -1, }; + s = lsquic_stream_reset_stream_at_in(stream, 100, 50, 7); + assert(0 == s); + assert(!(stream->sm_qflags & SMQF_WAIT_FIN_OFF)); + assert(stream->stream_flags & STREAM_RESET_AT_RECVD); + assert(stream->stream_flags & STREAM_RST_RECVD); + assert(s_onreset_called.stream == stream); + assert(s_onreset_called.how == 0); + + s_onreset_called = (struct reset_call_ctx) { NULL, -1, }; + s = lsquic_stream_reset_stream_at_in(stream, 100, 50, 7); + assert(0 == s); + assert(stream->sm_reset_at == 50); + assert(s_onreset_called.stream == NULL); + + s_onreset_called = (struct reset_call_ctx) { NULL, -1, }; + s = lsquic_stream_reset_stream_at_in(stream, 100, 60, 7); + assert(0 == s); + assert(stream->sm_reset_at == 50); + assert(s_onreset_called.stream == NULL); + + s = lsquic_stream_reset_stream_at_in(stream, 100, 40, 7); + assert(0 == s); + assert(stream->sm_reset_at == 40); + + memset(&s_abort_error, 0, sizeof(s_abort_error)); + s = lsquic_stream_reset_stream_at_in(stream, 101, 40, 7); + assert(s < 0); + assert(s_abort_error.called); + assert(s_abort_error.error_code == TEC_FINAL_SIZE_ERROR); + + lsquic_stream_destroy(stream); +} + +static void +test_reset_stream_at_error_code_mismatch (struct test_objs *tobjs) +{ + lsquic_stream_t *stream; + int s; + + stream = new_stream(tobjs, 345); + assert(stream->sm_bflags & SMBF_IETF); + + stream->read_offset = 100; + s = lsquic_stream_reset_stream_at_in(stream, 100, 50, 7); + assert(0 == s); + + memset(&s_abort_error, 0, sizeof(s_abort_error)); + s = lsquic_stream_reset_stream_at_in(stream, 100, 50, 8); + assert(s < 0); + assert(s_abort_error.called); + assert(s_abort_error.error_code == TEC_STREAM_STATE_ERROR); + + lsquic_stream_destroy(stream); +} + +static void +test_reset_stream_at_send_gate (struct test_objs *tobjs) +{ + lsquic_stream_t *stream; + uint64_t reliable_size; + enum quic_frame_type frame_type; + + stream = new_stream(tobjs, 345); + assert(stream->sm_bflags & SMBF_IETF); + + tobjs->eng_pub.enp_settings.es_reset_stream_at = 1; + tobjs->lconn.cn_esf.i = &test_reset_stream_at_esfi; + tobjs->lconn.cn_enc_session = (enc_session_t *) 1; + + reliable_size = 0; + frame_type = lsquic_stream_get_reset_frame_type(stream, &reliable_size); + assert(frame_type == QUIC_FRAME_RST_STREAM); + + stream->tosend_off = 6; + assert(-1 == lsquic_stream_set_reliable_size(stream, 7)); + + stream->tosend_off = 7; + assert(0 == lsquic_stream_set_reliable_size(stream, 7)); + reliable_size = 0; + frame_type = lsquic_stream_get_reset_frame_type(stream, &reliable_size); + assert(frame_type == QUIC_FRAME_RESET_STREAM_AT); + assert(7 == reliable_size); + + lsquic_stream_destroy(stream); +} + +static void +test_reset_stream_at_reliable_size_errors (struct test_objs *tobjs) +{ + lsquic_stream_t *stream; + size_t too_big; + + stream = new_stream(tobjs, 345); + assert(stream->sm_bflags & SMBF_IETF); + + too_big = (size_t) (1ULL << (sizeof(stream->sm_reset_stream_at_sz) * 8)); + assert(-1 == lsquic_stream_set_reliable_size(stream, too_big)); + + stream->tosend_off = 8; + tobjs->eng_pub.enp_settings.es_reset_stream_at = 0; + assert(-1 == lsquic_stream_set_reliable_size(stream, 7)); + + tobjs->eng_pub.enp_settings.es_reset_stream_at = 1; + assert(-1 == lsquic_stream_set_reliable_size(stream, 7)); + + tobjs->lconn.cn_esf.i = &test_reset_stream_at_esfi; + tobjs->lconn.cn_enc_session = (enc_session_t *) 1; + s_peer_tp = &s_no_reset_stream_at_params; + assert(-1 == lsquic_stream_set_reliable_size(stream, 7)); + + s_peer_tp = NULL; + assert(-1 == lsquic_stream_set_reliable_size(stream, 7)); + + s_peer_tp = &s_reset_stream_at_params; + assert(0 == lsquic_stream_set_reliable_size(stream, 7)); + + lsquic_stream_destroy(stream); +} + +static void +test_reset_stream_at_reliable_size_not_ietf (struct test_objs *tobjs) +{ + lsquic_stream_t *stream; + + stream = new_stream(tobjs, 345); + assert(!(stream->sm_bflags & SMBF_IETF)); + + stream->tosend_off = 8; + tobjs->eng_pub.enp_settings.es_reset_stream_at = 1; + tobjs->lconn.cn_esf.i = &test_reset_stream_at_esfi; + tobjs->lconn.cn_enc_session = (enc_session_t *) 1; + s_peer_tp = &s_reset_stream_at_params; + assert(-1 == lsquic_stream_set_reliable_size(stream, 7)); + + lsquic_stream_destroy(stream); +} + + +static void +test_reset_stream_at_ack (struct test_objs *tobjs) +{ + lsquic_stream_t *stream; + + stream = new_stream(tobjs, 345); + stream->n_unacked = 1; + + lsquic_stream_acked(stream, QUIC_FRAME_RESET_STREAM_AT); + + assert(0 == stream->n_unacked); + assert(stream->stream_flags & STREAM_RST_ACKED); + + lsquic_stream_destroy(stream); +} + + +static void +test_rst_stream_gquic_no_stream_reset (struct test_objs *tobjs) +{ + lsquic_stream_t *stream; + int s; + + stream = new_stream(tobjs, 345); + assert(!(stream->sm_bflags & SMBF_IETF)); + + stream->stream_flags |= STREAM_RST_SENT; + stream->sm_qflags |= SMQF_WAIT_FIN_OFF; + + s = lsquic_stream_rst_in(stream, 0, 0); + assert(0 == s); + assert(!(stream->sm_qflags & SMQF_WAIT_FIN_OFF)); + assert(!(stream->sm_qflags & SMQF_SEND_RST)); + + lsquic_stream_destroy(stream); +} + + +static void +test_shutdown_read_gquic_with_send_rst (struct test_objs *tobjs) +{ + lsquic_stream_t *stream; + int s; + + stream = new_stream(tobjs, 345); + assert(!(stream->sm_bflags & SMBF_IETF)); + + lsquic_stream_maybe_reset(stream, 0x123, 0); + assert(stream->sm_qflags & SMQF_SEND_RST); + assert(!TAILQ_EMPTY(&tobjs->conn_pub.sending_streams)); + assert(TAILQ_FIRST(&tobjs->conn_pub.sending_streams) == stream); + + s = lsquic_stream_shutdown(stream, 0); + assert(0 == s); + assert(stream->stream_flags & STREAM_U_READ_DONE); + assert(stream->sm_qflags & SMQF_SEND_RST); + + lsquic_stream_destroy(stream); +} + +static void +test_rst_stream_gquic_no_on_reset (struct test_objs *tobjs) +{ + lsquic_stream_t *stream; + int s; + + stream = new_stream(tobjs, 345); + assert(!(stream->sm_bflags & SMBF_IETF)); + stream->stream_if = &stream_if_no_reset; + + s_onreset_called = (struct reset_call_ctx) { NULL, -1, }; + s = lsquic_stream_rst_in(stream, 0, 0); + assert(0 == s); + assert(s_onreset_called.stream == NULL); + + lsquic_stream_destroy(stream); +} + +static void +test_rst_stream_gquic_on_reset_already_called (struct test_objs *tobjs) +{ + lsquic_stream_t *stream; + int s; + + stream = new_stream(tobjs, 345); + assert(!(stream->sm_bflags & SMBF_IETF)); + + stream->sm_dflags |= SMDF_ONRESET0 | SMDF_ONRESET1; + s_onreset_called = (struct reset_call_ctx) { NULL, -1, }; + s = lsquic_stream_rst_in(stream, 0, 0); + assert(0 == s); + assert(s_onreset_called.stream == NULL); + + lsquic_stream_destroy(stream); +} + + +static void +test_stop_sending_no_on_reset (struct test_objs *tobjs) +{ + lsquic_stream_t *stream; + + stream = new_stream(tobjs, 345); + assert(stream->sm_bflags & SMBF_IETF); + stream->stream_if = &stream_if_no_reset; + + s_onreset_called = (struct reset_call_ctx) { NULL, -1, }; + lsquic_stream_stop_sending_in(stream, 12345); + assert(s_onreset_called.stream == NULL); + + lsquic_stream_destroy(stream); +} + + +static void +test_stop_sending_onclose_done (struct test_objs *tobjs) +{ + lsquic_stream_t *stream; + + stream = new_stream(tobjs, 345); + assert(stream->sm_bflags & SMBF_IETF); + stream->stream_flags |= STREAM_ONCLOSE_DONE; + + s_onreset_called = (struct reset_call_ctx) { NULL, -1, }; + lsquic_stream_stop_sending_in(stream, 12345); + assert(s_onreset_called.stream == NULL); + + lsquic_stream_destroy(stream); +} + +static void +test_stop_sending_with_send_rst (struct test_objs *tobjs) +{ + lsquic_stream_t *stream; + + stream = new_stream(tobjs, 345); + assert(stream->sm_bflags & SMBF_IETF); + + stream->sm_qflags |= SMQF_SEND_RST; + TAILQ_INSERT_TAIL(&tobjs->conn_pub.sending_streams, stream, next_send_stream); + s_onreset_called = (struct reset_call_ctx) { NULL, -1, }; + lsquic_stream_stop_sending_in(stream, 12345); + assert(stream->sm_qflags & SMQF_SEND_RST); + + lsquic_stream_destroy(stream); +} + + +static void +test_stop_sending_duplicate (struct test_objs *tobjs) +{ + lsquic_stream_t *stream; + + stream = new_stream(tobjs, 345); + assert(stream->sm_bflags & SMBF_IETF); /* STOP_SENDING is IETF-only */ + + s_onreset_called = (struct reset_call_ctx) { NULL, -1, }; + lsquic_stream_stop_sending_in(stream, 12345); + assert(s_onreset_called.stream == stream); + assert(s_onreset_called.how == 1); + + s_onreset_called = (struct reset_call_ctx) { NULL, -1, }; + lsquic_stream_stop_sending_in(stream, 12345); + assert(s_onreset_called.stream == NULL); + + lsquic_stream_destroy(stream); +} + - if (!(stream->sm_bflags & SMBF_IETF)) - { - assert(!TAILQ_EMPTY(&tobjs->conn_pub.sending_streams)); - assert((stream->sm_qflags & SMQF_SENDING_FLAGS) == SMQF_SEND_RST); - } +static void +test_stop_sending_clears_sending_flags (struct test_objs *tobjs) +{ + lsquic_stream_t *stream; - /* Not yet closed: error needs to be collected */ - assert(TAILQ_EMPTY(&tobjs->conn_pub.service_streams)); - assert(0 == (stream->sm_qflags & SMQF_SERVICE_FLAGS)); + stream = new_stream(tobjs, 345); + assert(stream->sm_bflags & SMBF_IETF); /* STOP_SENDING is IETF-only */ - n = lsquic_stream_write(stream, buf, 100); - if (stream->sm_bflags & SMBF_IETF) - assert(100 == n); /* Write successful after reset in IETF */ - else - assert(-1 == n); /* Error collected */ - s = lsquic_stream_close(stream); - assert(0 == s); /* Stream successfully closed */ + stream->stream_flags |= STREAM_RST_SENT; + stream->sm_qflags |= SMQF_SEND_WUF; + TAILQ_INSERT_TAIL(&tobjs->conn_pub.sending_streams, stream, next_send_stream); - if (stream->sm_bflags & SMBF_IETF) - assert(stream->n_unacked == 1); + lsquic_stream_stop_sending_in(stream, 12345); - assert(!TAILQ_EMPTY(&tobjs->conn_pub.service_streams)); - assert((stream->sm_qflags & SMQF_SERVICE_FLAGS) == SMQF_CALL_ONCLOSE); + assert(!(stream->sm_qflags & SMQF_SEND_WUF)); + assert(!(stream->sm_qflags & SMQF_SEND_BLOCKED)); + assert(!(stream->sm_qflags & SMQF_SEND_STOP_SENDING)); + assert(!(stream->sm_qflags & SMQF_SENDING_FLAGS)); + assert(TAILQ_EMPTY(&tobjs->conn_pub.sending_streams)); - if (!(stream->sm_bflags & SMBF_IETF)) - lsquic_stream_rst_frame_sent(stream); + lsquic_stream_destroy(stream); +} - lsquic_stream_call_on_close(stream); - assert(TAILQ_EMPTY(&tobjs->conn_pub.sending_streams)); +static void +test_stream_maybe_reset_do_close (struct test_objs *tobjs) +{ + lsquic_stream_t *stream; - if (stream->sm_bflags & SMBF_IETF) - { - /* FIN packet has not been acked yet: */ - assert(TAILQ_EMPTY(&tobjs->conn_pub.service_streams)); - /* Now ack it: */ - ack_packet(&tobjs->send_ctl, 1); - } + stream = new_stream(tobjs, 345); + stream->stream_flags |= STREAM_RST_SENT | STREAM_RST_RECVD; - assert(!TAILQ_EMPTY(&tobjs->conn_pub.service_streams)); - assert((stream->sm_qflags & SMQF_SERVICE_FLAGS) == SMQF_FREE_STREAM); + lsquic_stream_maybe_reset(stream, 12345, 1); + assert(stream->stream_flags & STREAM_U_READ_DONE); lsquic_stream_destroy(stream); - assert(TAILQ_EMPTY(&tobjs->conn_pub.service_streams)); - - assert(200 == tobjs->conn_pub.cfcw.cf_max_recv_off); - assert(200 == tobjs->conn_pub.cfcw.cf_read_off); } -/* Client: we send some data (no FIN), and remote end sends some data and - * then sends STOP_SENDING - */ static void -test_loc_data_rem_SS (struct test_objs *tobjs) +test_stream_reset_qpack_and_sending_flags (struct test_objs *tobjs) { - lsquic_packet_out_t *packet_out; + struct qpack_dec_hdl qdh; lsquic_stream_t *stream; - char buf_out[0x100]; - unsigned char buf[0x100]; - ssize_t n; - int s, fin; - init_buf(buf_out, sizeof(buf_out)); + memset(&qdh, 0, sizeof(qdh)); stream = new_stream(tobjs, 345); - assert(stream->sm_bflags & SMBF_IETF); /* STOP_SENDING is IETF-only */ - n = lsquic_stream_write(stream, buf_out, 100); - assert(n == 100); - assert(0 == lsquic_send_ctl_n_scheduled(&tobjs->send_ctl)); + assert(stream->sm_bflags & SMBF_IETF); - s = lsquic_stream_flush(stream); - assert(1 == lsquic_send_ctl_n_scheduled(&tobjs->send_ctl)); + tobjs->conn_pub.u.ietf.qdh = &qdh; - n = read_from_scheduled_packets(&tobjs->send_ctl, stream->id, buf, - sizeof(buf), 0, &fin, 0); - assert(100 == n); - assert(0 == memcmp(buf_out, buf, 100)); - assert(!fin); + stream->sm_qflags |= SMQF_QPACK_DEC; + stream->sm_qflags |= SMQF_SEND_WUF; + TAILQ_INSERT_TAIL(&tobjs->conn_pub.sending_streams, stream, next_send_stream); - /* Pretend we sent out a packet: */ - packet_out = lsquic_send_ctl_next_packet_to_send(&tobjs->send_ctl, 0); - lsquic_send_ctl_sent_packet(&tobjs->send_ctl, packet_out); + lsquic_stream_maybe_reset(stream, 123, 0); - s = lsquic_stream_frame_in(stream, new_frame_in(tobjs, 0, 100, 0)); - assert(0 == s); - s_onreset_called = (struct reset_call_ctx) { NULL, -1, }; - lsquic_stream_stop_sending_in(stream, 12345); - assert(s_onreset_called.stream == stream); - assert(s_onreset_called.how == 1); + assert(stream->sm_qflags & SMQF_SEND_RST); + assert(!(stream->sm_qflags & SMQF_QPACK_DEC)); - /* Incoming STOP_SENDING should not affect the ability to read from - * stream. - */ - unsigned char mybuf[123]; - const ssize_t nread = lsquic_stream_read(stream, mybuf, sizeof(mybuf)); - assert(nread == 100); + lsquic_stream_destroy(stream); + tobjs->conn_pub.u.ietf.qdh = NULL; +} - ack_packet(&tobjs->send_ctl, 1); +static void +test_rst_frame_sent_keeps_sending_stream (struct test_objs *tobjs) +{ + lsquic_stream_t *stream; - if (!(stream->sm_bflags & SMBF_IETF)) - { - assert(!TAILQ_EMPTY(&tobjs->conn_pub.sending_streams)); - assert((stream->sm_qflags & SMQF_SENDING_FLAGS) == SMQF_SEND_RST); - } + stream = new_stream(tobjs, 345); + stream->sm_qflags |= SMQF_SEND_RST | SMQF_SEND_WUF; + TAILQ_INSERT_TAIL(&tobjs->conn_pub.sending_streams, stream, next_send_stream); - /* Not yet closed: error needs to be collected */ - assert(TAILQ_EMPTY(&tobjs->conn_pub.service_streams)); - assert(0 == (stream->sm_qflags & SMQF_SERVICE_FLAGS)); + lsquic_stream_rst_frame_sent(stream); - n = lsquic_stream_write(stream, buf, 100); - assert(-1 == n); /* Error collected */ - s = lsquic_stream_close(stream); - assert(0 == s); /* Stream successfully closed */ + assert(stream->sm_qflags & SMQF_SEND_WUF); + assert(!(stream->sm_qflags & SMQF_SEND_RST)); + assert(TAILQ_FIRST(&tobjs->conn_pub.sending_streams) == stream); - assert(!TAILQ_EMPTY(&tobjs->conn_pub.service_streams)); - assert((stream->sm_qflags & SMQF_SERVICE_FLAGS) == SMQF_CALL_ONCLOSE); + TAILQ_REMOVE(&tobjs->conn_pub.sending_streams, stream, next_send_stream); + stream->sm_qflags &= ~SMQF_SEND_WUF; + lsquic_stream_destroy(stream); +} - if (stream->sm_bflags & SMBF_IETF) - lsquic_stream_ss_frame_sent(stream); - lsquic_stream_rst_frame_sent(stream); - lsquic_stream_call_on_close(stream); +static void +test_rst_frame_sent_remove_with_next (struct test_objs *tobjs) +{ + lsquic_stream_t *stream1; + lsquic_stream_t *stream2; - assert(TAILQ_EMPTY(&tobjs->conn_pub.sending_streams)); - if (stream->sm_bflags & SMBF_IETF) - assert(stream->sm_qflags & SMQF_WAIT_FIN_OFF); - else - { - assert(!TAILQ_EMPTY(&tobjs->conn_pub.service_streams)); - assert((stream->sm_qflags & SMQF_SERVICE_FLAGS) == SMQF_FREE_STREAM); - } + stream1 = new_stream(tobjs, 345); + stream2 = new_stream(tobjs, 347); + stream1->sm_qflags |= SMQF_SEND_RST; + stream2->sm_qflags |= SMQF_SEND_WUF; + TAILQ_INSERT_TAIL(&tobjs->conn_pub.sending_streams, stream1, next_send_stream); + TAILQ_INSERT_TAIL(&tobjs->conn_pub.sending_streams, stream2, next_send_stream); - const unsigned expected_nread = stream->sm_bflags & SMBF_IETF ? 100 : 200; - lsquic_stream_destroy(stream); - assert(TAILQ_EMPTY(&tobjs->conn_pub.service_streams)); + lsquic_stream_rst_frame_sent(stream1); - assert(expected_nread == tobjs->conn_pub.cfcw.cf_max_recv_off); - assert(expected_nread == tobjs->conn_pub.cfcw.cf_read_off); + assert(TAILQ_FIRST(&tobjs->conn_pub.sending_streams) == stream2); + assert(TAILQ_NEXT(stream2, next_send_stream) == NULL); + + lsquic_stream_destroy(stream1); + lsquic_stream_destroy(stream2); } + /* We send some data and RST, receive data and FIN */ static void @@ -1603,7 +2319,30 @@ test_termination (void) { 1, 0, test_rem_data_loc_close, }, { 1, 1, test_loc_FIN_rem_RST, }, { 1, 1, test_loc_data_rem_RST, }, + { 1, 1, test_rst_stream_duplicate, }, + { 1, 1, test_rst_stream_smaller_than_seen, }, + { 1, 1, test_rst_stream_flow_control_violation, }, + { 0, 1, test_rst_stream_final_size_mismatch, }, + { 0, 1, test_reset_stream_at_updates_and_errors, }, + { 0, 1, test_reset_stream_at_error_code_mismatch, }, + { 0, 1, test_reset_stream_at_send_gate, }, + { 0, 1, test_reset_stream_at_ack, }, + { 0, 1, test_reset_stream_at_reliable_size_errors, }, + { 1, 0, test_reset_stream_at_reliable_size_not_ietf, }, + { 1, 0, test_rst_stream_gquic_no_stream_reset, }, + { 1, 0, test_shutdown_read_gquic_with_send_rst, }, + { 1, 0, test_rst_stream_gquic_no_on_reset, }, + { 1, 0, test_rst_stream_gquic_on_reset_already_called, }, + { 0, 1, test_stop_sending_no_on_reset, }, + { 0, 1, test_stop_sending_onclose_done, }, + { 0, 1, test_stop_sending_with_send_rst, }, { 0, 1, test_loc_data_rem_SS, }, + { 0, 1, test_stop_sending_duplicate, }, + { 0, 1, test_stop_sending_clears_sending_flags, }, + { 1, 1, test_stream_maybe_reset_do_close, }, + { 0, 1, test_stream_reset_qpack_and_sending_flags, }, + { 1, 1, test_rst_frame_sent_keeps_sending_stream, }, + { 1, 1, test_rst_frame_sent_remove_with_next, }, { 1, 0, test_loc_RST_rem_FIN, }, #ifndef NDEBUG { 1, 1, test_gapless_elision_beginning, }, @@ -2516,6 +3255,53 @@ test_writing_to_stream_outside_callback (void) deinit_test_objs(&tobjs); } +static void +test_reliable_prefix_bypasses_buffered (void) +{ + ssize_t nw; + struct test_objs tobjs; + lsquic_stream_t *stream; + int s; + const struct buf_packet_q *const bpq = + &tobjs.send_ctl.sc_buffered_packets[BPT_OTHER_PRIO]; + + init_test_ctl_settings(&g_ctl_settings); + g_ctl_settings.tcs_schedule_stream_packets_immediately = 0; + g_ctl_settings.tcs_bp_type = BPT_OTHER_PRIO; + + init_test_objs(&tobjs, 0x4000, 0x4000, NULL); + stream = new_stream(&tobjs, 123); + stream->sm_reset_stream_at_sz = 5; + + nw = lsquic_stream_write(stream, "ABCDE", 5); + assert(("5 bytes written correctly", nw == 5)); + s = lsquic_stream_flush(stream); + assert(0 == s); + assert(("reliable prefix not scheduled", + 0 == lsquic_send_ctl_n_scheduled(&tobjs.send_ctl))); + assert(("no buffered packets", 0 == bpq->bpq_count)); + assert(("prefix remains buffered", stream->sm_n_buffered > 0)); + + g_ctl_settings.tcs_schedule_stream_packets_immediately = 1; + s = lsquic_stream_flush(stream); + assert(0 == s); + assert(("reliable prefix scheduled", + 1 == lsquic_send_ctl_n_scheduled(&tobjs.send_ctl))); + assert(("no buffered packets", 0 == bpq->bpq_count)); + + g_ctl_settings.tcs_schedule_stream_packets_immediately = 0; + nw = lsquic_stream_write(stream, "XYZ", 3); + assert(("3 bytes written correctly", nw == 3)); + s = lsquic_stream_flush(stream); + assert(0 == s); + assert(("still 1 scheduled packet", + 1 == lsquic_send_ctl_n_scheduled(&tobjs.send_ctl))); + assert(("buffered packet created", 1 == bpq->bpq_count)); + + lsquic_stream_destroy(stream); + deinit_test_objs(&tobjs); +} + static void verify_ack (struct lsquic_packet_out *packet_out) @@ -3650,6 +4436,369 @@ test_bad_packbits_guess_1 (void) deinit_test_objs(&tobjs); } +static void +test_http_dg_capsules (void) +{ + struct test_objs tobjs; + struct http_dg_test_ctx dg_ctx; + struct lsquic_stream *stream; + enum stream_ctor_flags saved_flags = stream_ctor_flags; + const struct parse_funcs *saved_pf = g_pf; + const size_t saved_max_dgram = s_max_dgram_size; + unsigned char capsule[64]; + unsigned char hq_frame[96]; + const unsigned char payload_a[] = "DATA"; + const unsigned char payload_b[] = "ABCDE"; + size_t cap_len; + int s; + + /* Use IETF QUIC for HTTP Datagram capsule handling. */ + g_pf = select_pf_by_ver(LSQVER_I001); + stream_ctor_flags |= SCF_HTTP; + init_test_objs(&tobjs, 0x1000, 0x1000, g_pf); + tobjs.stream_if = &http_dg_stream_if; + tobjs.stream_if_ctx = &dg_ctx; + tobjs.conn_pub.cp_flags |= CP_HTTP_DATAGRAMS; + tobjs.eng_pub.enp_settings.es_http_datagrams = 1; + tobjs.eng_pub.enp_settings.es_http_dg_max_capsule_read_size = 32; + tobjs.eng_pub.enp_settings.es_http_dg_max_capsule_write_size = 32; + tobjs.eng_pub.enp_settings.es_progress_check = 1; + s_max_dgram_size = 1200; + memset(&dg_ctx, 0, sizeof(dg_ctx)); + /* Test: basic capsule read delivers payload to callback. */ + stream = new_stream(&tobjs, 4); + s = lsquic_stream_set_http_dg_capsules(stream, 1); + assert(s == 0); + stream->stream_flags |= STREAM_HAVE_UH; + stream->sm_hq_filter.hqfi_flags |= HQFI_FLAG_HEADER; + stream->sm_bflags |= SMBF_RW_ONCE; + cap_len = make_capsule(capsule, sizeof(capsule), H3_CAPSULE_DATAGRAM, + payload_a, sizeof(payload_a) - 1); + cap_len = make_hq_data_frame(hq_frame, sizeof(hq_frame), capsule, cap_len); + s = lsquic_stream_frame_in(stream, + new_frame_in_ext(&tobjs, 0, cap_len, 0, hq_frame)); + assert(s == 0); + lsquic_stream_dispatch_read_events(stream); + assert(dg_ctx.read_calls == 1); + assert(dg_ctx.read_len == sizeof(payload_a) - 1); + assert(0 == memcmp(dg_ctx.read_buf, payload_a, sizeof(payload_a) - 1)); + stream->stream_flags |= STREAM_FIN_REACHED; + lsquic_stream_destroy(stream); + + memset(&dg_ctx, 0, sizeof(dg_ctx)); + /* Test: split DATA frame (capsule) across two stream frames. */ + stream = new_stream(&tobjs, 8); + s = lsquic_stream_set_http_dg_capsules(stream, 1); + assert(s == 0); + stream->stream_flags |= STREAM_HAVE_UH; + stream->sm_hq_filter.hqfi_flags |= HQFI_FLAG_HEADER; + cap_len = make_capsule(capsule, sizeof(capsule), H3_CAPSULE_DATAGRAM, + payload_b, sizeof(payload_b) - 1); + cap_len = make_hq_data_frame(hq_frame, sizeof(hq_frame), capsule, cap_len); + s = lsquic_stream_frame_in(stream, + new_frame_in_ext(&tobjs, 0, 1, 0, hq_frame)); + assert(s == 0); + s = lsquic_stream_frame_in(stream, + new_frame_in_ext(&tobjs, 1, cap_len - 1, 0, hq_frame + 1)); + assert(s == 0); + lsquic_stream_dispatch_read_events(stream); + assert(dg_ctx.read_calls == 1); + assert(dg_ctx.read_len == sizeof(payload_b) - 1); + assert(0 == memcmp(dg_ctx.read_buf, payload_b, sizeof(payload_b) - 1)); + stream->stream_flags |= STREAM_FIN_REACHED; + lsquic_stream_destroy(stream); + + memset(&dg_ctx, 0, sizeof(dg_ctx)); + /* Test: payload completion uses the latched capsule callback. */ + stream = new_stream(&tobjs, 10); + s = lsquic_stream_set_http_dg_capsules(stream, 1); + assert(s == 0); + stream->stream_flags |= STREAM_HAVE_UH; + stream->sm_hq_filter.hqfi_flags |= HQFI_FLAG_HEADER; + stream->sm_http_dg->read.state = HDC_READ_PAYLOAD; + stream->sm_http_dg->read.type = H3_CAPSULE_DATAGRAM; + stream->sm_http_dg->read.length = sizeof(payload_b) - 1; + stream->sm_http_dg->read.offset = 0; + stream->sm_http_dg->read.buf_sz = sizeof(payload_b) - 1; + stream->sm_http_dg->read.buf = malloc(sizeof(payload_b) - 1); + assert(stream->sm_http_dg->read.buf); + stream->sm_http_dg->read.cb = http_capsule_test_cb; + s = lsquic_stream_set_capsule_handler(stream, H3_CAPSULE_DATAGRAM, NULL); + assert(s == 0); + assert(lsquic_http_dg_capsule_readf(stream, payload_b, + sizeof(payload_b) - 1, 0) == sizeof(payload_b) - 1); + assert(dg_ctx.read_calls == 1); + assert(dg_ctx.read_len == sizeof(payload_b) - 1); + assert(0 == memcmp(dg_ctx.read_buf, payload_b, sizeof(payload_b) - 1)); + assert(stream->sm_http_dg->read.state == HDC_READ_TYPE); + stream->stream_flags |= STREAM_FIN_REACHED; + lsquic_stream_destroy(stream); + + memset(&dg_ctx, 0, sizeof(dg_ctx)); + /* Test: non-datagram capsule type is ignored by http dg callback. */ + stream = new_stream(&tobjs, 12); + s = lsquic_stream_set_http_dg_capsules(stream, 1); + assert(s == 0); + stream->stream_flags |= STREAM_HAVE_UH; + stream->sm_hq_filter.hqfi_flags |= HQFI_FLAG_HEADER; + cap_len = make_capsule(capsule, sizeof(capsule), 1, payload_a, 3); + cap_len = make_hq_data_frame(hq_frame, sizeof(hq_frame), capsule, cap_len); + s = lsquic_stream_frame_in(stream, + new_frame_in_ext(&tobjs, 0, cap_len, 0, hq_frame)); + assert(s == 0); + lsquic_stream_dispatch_read_events(stream); + assert(dg_ctx.read_calls == 0); + stream->stream_flags |= STREAM_FIN_REACHED; + lsquic_stream_destroy(stream); + + tobjs.eng_pub.enp_settings.es_http_dg_max_capsule_read_size = 2; + memset(&dg_ctx, 0, sizeof(dg_ctx)); + memset(&s_abort_error, 0, sizeof(s_abort_error)); + /* Test: oversized capsule triggers H3_DATAGRAM_ERROR. */ + stream = new_stream(&tobjs, 16); + s = lsquic_stream_set_http_dg_capsules(stream, 1); + assert(s == 0); + stream->stream_flags |= STREAM_HAVE_UH; + stream->sm_hq_filter.hqfi_flags |= HQFI_FLAG_HEADER; + cap_len = make_capsule(capsule, sizeof(capsule), H3_CAPSULE_DATAGRAM, + payload_a, 3); + cap_len = make_hq_data_frame(hq_frame, sizeof(hq_frame), capsule, cap_len); + s = lsquic_stream_frame_in(stream, + new_frame_in_ext(&tobjs, 0, cap_len, 0, hq_frame)); + assert(s == 0); + lsquic_stream_dispatch_read_events(stream); + assert(s_abort_error.called); + assert(s_abort_error.error_code == HEC_DATAGRAM_ERROR); + stream->stream_flags |= STREAM_FIN_REACHED; + lsquic_stream_destroy(stream); + tobjs.eng_pub.enp_settings.es_http_dg_max_capsule_read_size = 32; + + memset(&dg_ctx, 0, sizeof(dg_ctx)); + memset(&s_abort_error, 0, sizeof(s_abort_error)); + /* Test: truncated capsule triggers H3_DATAGRAM_ERROR. */ + stream = new_stream(&tobjs, 20); + s = lsquic_stream_set_http_dg_capsules(stream, 1); + assert(s == 0); + stream->stream_flags |= STREAM_HAVE_UH; + stream->sm_hq_filter.hqfi_flags |= HQFI_FLAG_HEADER; + cap_len = make_capsule(capsule, sizeof(capsule), H3_CAPSULE_DATAGRAM, + payload_a, 4); + cap_len = make_hq_data_frame(hq_frame, sizeof(hq_frame), + capsule, cap_len - 1); + s = lsquic_stream_frame_in(stream, + new_frame_in_ext(&tobjs, 0, cap_len, 1, hq_frame)); + assert(s == 0); + lsquic_stream_dispatch_read_events(stream); + assert(s_abort_error.called); + assert(s_abort_error.error_code == HEC_DATAGRAM_ERROR); + stream->stream_flags |= STREAM_FIN_REACHED; + lsquic_stream_destroy(stream); + + /* Test: disable capsules clears pending write state. */ + stream = new_stream(&tobjs, 24); + s = lsquic_stream_set_http_dg_capsules(stream, 1); + assert(s == 0); + s = lsquic_stream_http_dg_queue_capsule(stream, payload_a, 3); + assert(s == 0); + assert(lsquic_stream_http_dg_capsule_pending(stream)); + s = lsquic_stream_set_http_dg_capsules(stream, 0); + assert(s == 0); + assert(!lsquic_stream_http_dg_capsule_pending(stream)); + assert(!(stream->sm_qflags & SMQF_WANT_WRITE)); + stream->stream_flags |= STREAM_FIN_REACHED; + lsquic_stream_destroy(stream); + + /* Test: queued capsule is flushed by write event. */ + stream = new_stream(&tobjs, 28); + s = lsquic_stream_set_http_dg_capsules(stream, 1); + assert(s == 0); + stream->stream_flags |= STREAM_HEADERS_SENT; + s = lsquic_stream_http_dg_queue_capsule(stream, payload_a, 3); + assert(s == 0); + assert(lsquic_stream_http_dg_capsule_pending(stream)); + lsquic_stream_dispatch_write_events(stream); + assert(!lsquic_stream_http_dg_capsule_pending(stream)); + assert(!(stream->sm_qflags & SMQF_WANT_WRITE)); + stream->stream_flags |= STREAM_FIN_REACHED; + lsquic_stream_destroy(stream); + + /* Test: second queued capsule returns EAGAIN. */ + stream = new_stream(&tobjs, 32); + s = lsquic_stream_set_http_dg_capsules(stream, 1); + assert(s == 0); + s = lsquic_stream_http_dg_queue_capsule(stream, payload_a, 3); + assert(s == 0); + errno = 0; + s = lsquic_stream_http_dg_queue_capsule(stream, payload_a, 3); + assert(s < 0); + assert(errno == EAGAIN); + stream->stream_flags |= STREAM_FIN_REACHED; + lsquic_stream_destroy(stream); + + tobjs.eng_pub.enp_settings.es_http_dg_max_capsule_write_size = 0; + /* Test: write size limit returns EMSGSIZE. */ + stream = new_stream(&tobjs, 36); + s = lsquic_stream_set_http_dg_capsules(stream, 1); + assert(s == 0); + errno = 0; + s = lsquic_stream_http_dg_queue_capsule(stream, payload_a, 3); + assert(s < 0); + assert(errno == EMSGSIZE); + stream->stream_flags |= STREAM_FIN_REACHED; + lsquic_stream_destroy(stream); + tobjs.eng_pub.enp_settings.es_http_dg_max_capsule_write_size = 32; + + /* Test: max HTTP datagram size reflects overhead and zero cases. */ + stream = new_stream(&tobjs, 40); + s_max_dgram_size = 5; + assert(lsquic_stream_get_max_http_dg_size(stream) == 4); + s_max_dgram_size = 1; + assert(lsquic_stream_get_max_http_dg_size(stream) == 0); + s_max_dgram_size = 0; + assert(lsquic_stream_get_max_http_dg_size(stream) == 0); + stream->stream_flags |= STREAM_FIN_REACHED; + lsquic_stream_destroy(stream); + + /* Test: max size is zero for non-client-bidi stream IDs. */ + stream = new_stream(&tobjs, 41); + s_max_dgram_size = 1200; + assert(lsquic_stream_get_max_http_dg_size(stream) == 0); + stream->stream_flags |= STREAM_FIN_REACHED; + lsquic_stream_destroy(stream); + + /* Test: want_http_dg toggles stream in queue. */ + stream = new_stream(&tobjs, 44); + /* Test: want_http_dg fails without negotiated support. */ + s_max_dgram_size = 1200; + s = lsquic_stream_want_http_dg_write(stream, 1); + assert(s == 0); + assert(stream->sm_qflags & SMQF_WANT_HTTP_DG); + assert(TAILQ_FIRST(&tobjs.conn_pub.http_dg_streams) == stream); + s = lsquic_stream_want_http_dg_write(stream, 0); + assert(s == 1); + assert(!(stream->sm_qflags & SMQF_WANT_HTTP_DG)); + assert(TAILQ_EMPTY(&tobjs.conn_pub.http_dg_streams)); + stream->stream_flags |= STREAM_FIN_REACHED; + lsquic_stream_destroy(stream); + + /* Test: queue allocates http_dg state on demand. */ + stream = new_stream(&tobjs, 48); + stream->stream_flags |= STREAM_HEADERS_SENT; + s = lsquic_stream_http_dg_queue_capsule(stream, payload_a, 3); + assert(s == 0); + assert(lsquic_stream_http_dg_capsule_pending(stream)); + stream->stream_flags |= STREAM_FIN_REACHED; + lsquic_stream_destroy(stream); + + /* Test: wantwrite failure clears pending capsule state. */ + stream = new_stream(&tobjs, 50); + stream->stream_flags |= STREAM_U_WRITE_DONE; + errno = 0; + s = lsquic_stream_http_dg_queue_capsule(stream, payload_a, 3); + assert(s < 0); + assert(errno == EBADF); + assert(!lsquic_stream_http_dg_capsule_pending(stream)); + if (stream->sm_http_dg) + { + assert(stream->sm_http_dg->write.buf == NULL); + assert(stream->sm_http_dg->write.header_len == 0); + } + stream->stream_flags |= STREAM_FIN_REACHED; + lsquic_stream_destroy(stream); + + /* Test: want_http_dg set without immediate clear. */ + stream = new_stream(&tobjs, 52); + s_max_dgram_size = 1200; + s = lsquic_stream_want_http_dg_write(stream, 1); + assert(s == 0); + assert(stream->sm_qflags & SMQF_WANT_HTTP_DG); + stream->stream_flags |= STREAM_FIN_REACHED; + lsquic_stream_destroy(stream); + + deinit_test_objs(&tobjs); + stream_ctor_flags = saved_flags; + g_pf = saved_pf; + s_max_dgram_size = saved_max_dgram; +} + +static void +test_http_dg_capsules_errors (void) +{ + struct test_objs tobjs; + struct http_dg_test_ctx dg_ctx; + struct lsquic_stream *stream; + enum stream_ctor_flags saved_flags = stream_ctor_flags; + const struct parse_funcs *saved_pf = g_pf; + const size_t saved_max_dgram = s_max_dgram_size; + int s; + + /* Use IETF QUIC for error-path coverage. */ + g_pf = select_pf_by_ver(LSQVER_I001); + stream_ctor_flags &= ~SCF_HTTP; + init_test_objs(&tobjs, 0x1000, 0x1000, g_pf); + tobjs.stream_if = &http_dg_stream_if; + tobjs.stream_if_ctx = &dg_ctx; + tobjs.eng_pub.enp_settings.es_http_datagrams = 1; + tobjs.eng_pub.enp_settings.es_progress_check = 1; + /* Test: missing HTTP headers flag rejects capsules. */ + stream = new_stream(&tobjs, 8); + errno = 0; + s = lsquic_stream_set_http_dg_capsules(stream, 1); + assert(s < 0); + assert(errno == ENOTSUP); + stream->stream_flags |= STREAM_FIN_REACHED; + lsquic_stream_destroy(stream); + deinit_test_objs(&tobjs); + stream_ctor_flags |= SCF_HTTP; + init_test_objs(&tobjs, 0x1000, 0x1000, g_pf); + tobjs.stream_if = &http_dg_stream_if; + tobjs.stream_if_ctx = &dg_ctx; + tobjs.eng_pub.enp_settings.es_http_datagrams = 0; + stream = new_stream(&tobjs, 12); + errno = 0; + s = lsquic_stream_set_http_dg_capsules(stream, 1); + assert(s < 0); + assert(errno == ENOTSUP); + stream->stream_flags |= STREAM_FIN_REACHED; + lsquic_stream_destroy(stream); + s_max_dgram_size = 1200; + tobjs.eng_pub.enp_settings.es_http_datagrams = 1; + stream = new_stream(&tobjs, 16); + tobjs.conn_pub.cp_flags &= ~CP_HTTP_DATAGRAMS; + errno = 0; + s = lsquic_stream_want_http_dg_write(stream, 1); + assert(s < 0); + assert(errno == ENOTSUP); + stream->stream_flags |= STREAM_FIN_REACHED; + lsquic_stream_destroy(stream); + stream = new_stream(&tobjs, 20); + tobjs.conn_pub.cp_flags |= CP_HTTP_DATAGRAMS; + /* Test: want_http_dg fails without datagram size. */ + s_max_dgram_size = 0; + errno = 0; + s = lsquic_stream_want_http_dg_write(stream, 1); + assert(s < 0); + assert(errno == ENOTSUP); + stream->stream_flags |= STREAM_FIN_REACHED; + lsquic_stream_destroy(stream); + /* Test: want_http_dg fails without on_http_dg_write callback. */ + tobjs.stream_if = &stream_if; + tobjs.stream_if_ctx = &test_ctx; + stream = new_stream(&tobjs, 24); + s_max_dgram_size = 1200; + tobjs.conn_pub.cp_flags |= CP_HTTP_DATAGRAMS; + errno = 0; + s = lsquic_stream_want_http_dg_write(stream, 1); + assert(s < 0); + assert(errno == EINVAL); + stream->stream_flags |= STREAM_FIN_REACHED; + lsquic_stream_destroy(stream); + deinit_test_objs(&tobjs); + stream_ctor_flags = saved_flags; + g_pf = saved_pf; + s_max_dgram_size = saved_max_dgram; +} + static void main_test_packetization (void) @@ -3688,6 +4837,7 @@ main (int argc, char **argv) int opt; lsquic_global_init(LSQUIC_GLOBAL_SERVER); + lsq_log_levels[LSQLM_STREAM] = LSQ_LOG_DEBUG; while (-1 != (opt = getopt(argc, argv, "Ahl:"))) { @@ -3712,6 +4862,7 @@ main (int argc, char **argv) test_writing_to_stream_schedule_stream_packets_immediately(); test_writing_to_stream_outside_callback(); + test_reliable_prefix_bypasses_buffered(); test_stealing_ack(); test_changing_pack_size(); test_reducing_pack_size(); @@ -3723,6 +4874,8 @@ main (int argc, char **argv) test_overlaps(); test_insert_edge_cases(); test_unexpected_http_close(); + test_http_dg_capsules(); + test_http_dg_capsules_errors(); { int idx[6]; diff --git a/tests/test_wt.c b/tests/test_wt.c new file mode 100644 index 000000000..5e3419acc --- /dev/null +++ b/tests/test_wt.c @@ -0,0 +1,1087 @@ +/* Copyright (c) 2017 - 2026 LiteSpeed Technologies Inc. See LICENSE. */ +#include +#include +#include +#include +#include +#include + +#include "lsquic.h" +#include "lsquic_wt.h" +#include "lsquic_int_types.h" +#include "lsquic_hash.h" +#include "lsquic_hq.h" +#include "lsquic_varint.h" +#include "lsquic_sfcw.h" +#include "lsquic_conn.h" +#include "lsquic_stream.h" + +#define WT_APP_ERROR_MAX 0xFFFFFFFFULL +#define WT_APP_ERROR_MIN_H3 0x52E4A40FA8DBULL +#define WT_APP_ERROR_MAX_H3 0x52E5AC983162ULL +#define WT_CLOSE_REASON_MAX 1024 +#define WT_TEST_CP_WEBTRANSPORT (1u << 2) +#define WT_TEST_CP_H3_PEER_SETTINGS (1u << 3) +#define WT_TEST_CP_CONNECT_PROTOCOL (1u << 4) +#define WT_TEST_HTTP_DG_WRITE_PREQUEUE (1u << 0) +#define WT_TEST_HTTP_DG_WRITE_WANT (1u << 1) +#define WT_TEST_HTTP_DG_WRITE_CB_QUEUE (1u << 2) +#define WT_TEST_HTTP_DG_WRITE_CB_FAIL (1u << 3) +#define WT_TEST_HTTP_DG_WRITE_CB_CLOSE (1u << 4) +#define WT_TEST_HTTP_DG_WRITE_CONSUME_FAIL (1u << 5) +#define WT_TEST_HTTP_DG_WRITE_DATAGRAM_MODE (1u << 6) + +int lsquic_wt_test_app_error_to_h3_error (uint64_t wt_error_code, + uint64_t *h3_error_code); +int lsquic_wt_test_h3_error_to_app_error (uint64_t h3_error_code, + uint64_t *wt_error_code); +int lsquic_wt_test_validate_incoming_session_id ( + lsquic_stream_id_t stream_id, lsquic_stream_id_t session_id, + const char *stream_kind, unsigned *error_code); +int lsquic_wt_test_dispatch_reset (int how, int ss_received, int with_ctx, + int with_if, uint64_t rst_in_code, + uint64_t ss_in_code, unsigned *called, + uint64_t *reset_code, uint64_t *stop_code); +int lsquic_wt_test_build_close_capsule (uint64_t code, const char *reason, + size_t reason_len, + unsigned char *buf, size_t *buf_len); +int lsquic_wt_test_remote_close (uint64_t code, const char *reason, + size_t reason_len, unsigned *called, + uint64_t *close_code, + size_t *close_reason_len, int *is_closing, + int *close_received, int *on_close_called); +int lsquic_wt_test_closing_rejects (unsigned *mask); +int lsquic_wt_test_local_close (uint64_t code, const char *reason, + size_t reason_len, int *queued_capsule, + unsigned *dgq_count); +int lsquic_wt_test_finalize (uint64_t code, const char *reason, + size_t reason_len, unsigned *called, + uint64_t *close_code, + size_t *close_reason_len, int *removed, + unsigned *dropped_datagrams); +int lsquic_wt_test_control_reset_close (unsigned *called, int *is_closing, + int *close_received); +int lsquic_wt_test_http_dg_read (int with_session, int is_control_stream, + int is_closing, unsigned *called); +int lsquic_wt_test_accept_resolution (unsigned initial_flags, + unsigned final_flags, + unsigned existing_sessions, + unsigned *initial_result, + unsigned *final_result, + unsigned *opened, + unsigned *rejected, + unsigned *status); +int lsquic_wt_test_accept_status_validation (unsigned status, int *accepted); +int lsquic_wt_test_reject_status_validation (unsigned status, int *accepted); +int lsquic_wt_test_pending_datagram_replay (unsigned *called_before, + unsigned *called_after); +int lsquic_wt_test_pending_datagram_replay_stops_on_close ( + unsigned *called_after, int *is_closing); +int lsquic_wt_test_destroy_while_closing (int is_control_stream, + unsigned *called, int *removed); +int lsquic_wt_test_stream_switch_failure_restores_state (int *restored_if, + int *restored_ctx, + int *restored_session); +int lsquic_wt_test_extra_resp_header_validation (int *null_headers_rejected, + int *zero_len_ok); +int lsquic_wt_test_send_response_rejects_missing_extra_headers ( + int *rejected); +int lsquic_wt_test_response_header_count_validation (int *negative_rejected, + int *overflow_rejected); +int lsquic_wt_test_dgq_overflow_rejected (int incoming, + int *overflow_rejected); +int lsquic_wt_test_open_stream_init_failure (int bidi, int *aborted, + int *freed_dynamic_onnew); +int lsquic_wt_test_datagram_write_state_rollback (int *want_flag_cleared, + int *send_disarmed); +int lsquic_wt_test_http_dg_write_path (unsigned flags, + const unsigned char *buf, size_t len, + size_t max_quic_payload, + unsigned *consume_calls, + unsigned *callback_calls, + unsigned *queued_after, + int *want_flag_set, int *is_closing, + unsigned *disarm_calls, + int *saved_errno); +int lsquic_wt_test_read_error_closes_stream (int *control_closed, + int *uni_closed); +int lsquic_wt_test_write_error_closes_stream (int *control_closed, + int *data_closed); +int lsquic_wt_test_control_stream_ops_rejected (unsigned *mask); +int lsquic_wt_test_uni_read_state (const unsigned char *buf, size_t len, + int fin, size_t *consumed, int *done, + int *malformed, + lsquic_stream_id_t *session_id); +int lsquic_stream_test_truncated_capsule_type_fin_aborts ( + unsigned *error_code); +int lsquic_ietf_test_wt_support (unsigned is_server, + unsigned peer_settings_received, + unsigned local_webtransport, + unsigned http_datagrams, + unsigned quic_datagrams, + unsigned connect_protocol, + unsigned wt_max_sessions_seen, + uint64_t wt_max_sessions, + unsigned wt_enabled_seen, + unsigned wt_enabled, + unsigned wt_initial_max_data_seen, + unsigned wt_initial_max_streams_uni_seen, + unsigned wt_initial_max_streams_bidi_seen, + unsigned peer_reset_stream_at, + unsigned draft, + unsigned *supports, + unsigned *peer_wt_draft); +int lsquic_ietf_test_wt_uni_switch_failure (int *restored_if, + int *restored_ctx, + int *close_attempted); +lsquic_wt_session_t *lsquic_wt_test_dgq_session_new (unsigned max_count, + size_t max_bytes); +void lsquic_wt_test_dgq_session_destroy (lsquic_wt_session_t *sess); +int lsquic_wt_test_dgq_enqueue (lsquic_wt_session_t *sess, const void *buf, + size_t len, + enum lsquic_wt_dg_drop_policy policy); +unsigned lsquic_wt_test_dgq_count (const lsquic_wt_session_t *sess); +size_t lsquic_wt_test_dgq_bytes (const lsquic_wt_session_t *sess); +int lsquic_wt_test_dgq_front (const lsquic_wt_session_t *sess, + unsigned char *val); +int lsquic_wt_test_dgq_back (const lsquic_wt_session_t *sess, + unsigned char *val); + + +enum wt_test_accept_result +{ + WT_TEST_ACCEPT_OPEN, + WT_TEST_ACCEPT_PENDING, + WT_TEST_ACCEPT_REJECT, +}; + + +static void +test_error_code_mapping (void) +{ + uint64_t h3_error_code, wt_error_code; + + assert(0 == lsquic_wt_test_app_error_to_h3_error(0, &h3_error_code)); + assert(h3_error_code == WT_APP_ERROR_MIN_H3); + + assert(0 == lsquic_wt_test_app_error_to_h3_error(0x1D, &h3_error_code)); + assert(h3_error_code == WT_APP_ERROR_MIN_H3 + 0x1D); + + assert(0 == lsquic_wt_test_app_error_to_h3_error(0x1E, &h3_error_code)); + assert(h3_error_code == WT_APP_ERROR_MIN_H3 + 0x1E + 1); + + assert(0 == lsquic_wt_test_app_error_to_h3_error(WT_APP_ERROR_MAX, + &h3_error_code)); + assert(h3_error_code == WT_APP_ERROR_MAX_H3); + + assert(0 == lsquic_wt_test_h3_error_to_app_error(WT_APP_ERROR_MIN_H3, + &wt_error_code)); + assert(wt_error_code == 0); + + assert(0 == lsquic_wt_test_h3_error_to_app_error(WT_APP_ERROR_MAX_H3, + &wt_error_code)); + assert(wt_error_code == WT_APP_ERROR_MAX); + + /* Reserved codepoint inside WT app error range: rejected. */ + assert(0 != lsquic_wt_test_h3_error_to_app_error(WT_APP_ERROR_MIN_H3 + 0x1E, + &wt_error_code)); + + assert(0 != lsquic_wt_test_app_error_to_h3_error(WT_APP_ERROR_MAX + 1, + &h3_error_code)); + assert(0 != lsquic_wt_test_h3_error_to_app_error(WT_APP_ERROR_MIN_H3 - 1, + &wt_error_code)); + assert(0 != lsquic_wt_test_h3_error_to_app_error(WT_APP_ERROR_MAX_H3 + 1, + &wt_error_code)); +} + + +struct peer_params +{ + uint64_t settings_received; + uint64_t supports; + uint64_t draft; + uint64_t connect_protocol; + int has_settings_received; + int has_supports; + int has_draft; + int has_connect_protocol; + int short_len; +}; + +static const struct peer_params *s_peer_params; + + +static int +get_param (lsquic_conn_t *conn, enum lsquic_conn_param param, void *value, + size_t *value_len) +{ + const struct peer_params *params = s_peer_params; + uint64_t out; + + (void) conn; + + if (!value || !value_len || *value_len < sizeof(uint64_t) || !params) + return -1; + + switch (param) + { + case LSQCP_WT_PEER_SETTINGS_RECEIVED: + if (!params->has_settings_received) + return -1; + out = params->settings_received; + break; + case LSQCP_WT_PEER_SUPPORTS: + if (!params->has_supports) + return -1; + out = params->supports; + break; + case LSQCP_WT_PEER_DRAFT: + if (!params->has_draft) + return -1; + out = params->draft; + break; + case LSQCP_WT_PEER_CONNECT_PROTOCOL: + if (!params->has_connect_protocol) + return -1; + out = params->connect_protocol; + break; + default: + return -1; + } + + memcpy(value, &out, sizeof(out)); + *value_len = params->short_len ? sizeof(uint32_t) : sizeof(out); + return 0; +} + + +static const struct conn_iface conn_iface = { + .ci_get_param = get_param, +}; + + +static void +test_peer_query_helpers (void) +{ + struct peer_params params = { + .settings_received = 1, + .supports = 1, + .draft = UINT_MAX + 123ULL, + .connect_protocol = 1, + .has_settings_received = 1, + .has_supports = 1, + .has_draft = 1, + .has_connect_protocol = 1, + }; + struct lsquic_conn conn = LSCONN_INITIALIZER_CIDLEN(conn, 0); + + conn.cn_if = &conn_iface; + s_peer_params = ¶ms; + + assert(lsquic_wt_peer_settings_received(&conn)); + assert(lsquic_wt_peer_supports(&conn)); + assert(lsquic_wt_peer_connect_protocol(&conn)); + assert(lsquic_wt_peer_draft(&conn) == UINT_MAX); + + params.has_supports = 0; + assert(!lsquic_wt_peer_supports(&conn)); + params.has_supports = 1; + + params.short_len = 1; + assert(!lsquic_wt_peer_settings_received(&conn)); + params.short_len = 0; + + assert(!lsquic_wt_peer_supports(NULL)); + assert(!lsquic_wt_peer_settings_received(NULL)); + assert(!lsquic_wt_peer_connect_protocol(NULL)); + assert(lsquic_wt_peer_draft(NULL) == 0); + s_peer_params = NULL; +} + +static void +test_stream_helpers (void) +{ + struct lsquic_stream stream; + struct lsquic_wt_session *sess; + + memset(&stream, 0, sizeof(stream)); + + stream.id = 0; /* client-initiated bidirectional */ + assert(lsquic_wt_stream_dir(&stream) == LSQWT_BIDI); + assert(lsquic_wt_stream_initiator(&stream) == LSQWT_CLIENT); + + stream.id = 1; /* server-initiated bidirectional */ + assert(lsquic_wt_stream_dir(&stream) == LSQWT_BIDI); + assert(lsquic_wt_stream_initiator(&stream) == LSQWT_SERVER); + + stream.id = 2; /* client-initiated unidirectional */ + assert(lsquic_wt_stream_dir(&stream) == LSQWT_UNI); + assert(lsquic_wt_stream_initiator(&stream) == LSQWT_CLIENT); + + stream.id = 3; /* server-initiated unidirectional */ + assert(lsquic_wt_stream_dir(&stream) == LSQWT_UNI); + assert(lsquic_wt_stream_initiator(&stream) == LSQWT_SERVER); + + sess = (struct lsquic_wt_session *) (uintptr_t) 0x1234; + stream.sm_attachment = sess; + assert(lsquic_wt_session_from_stream(&stream) == sess); + assert(lsquic_wt_session_from_stream(NULL) == NULL); + + assert(lsquic_wt_stream_dir(NULL) == LSQWT_BIDI); + assert(lsquic_wt_stream_initiator(NULL) == LSQWT_CLIENT); +} + +static void +test_invalid_public_api (void) +{ + struct lsquic_stream stream; + + memset(&stream, 0, sizeof(stream)); + + assert(lsquic_wt_stream_get_ctx(NULL) == NULL); + assert(lsquic_wt_stream_get_ctx(&stream) == NULL); +} + + +static void +test_incoming_session_id_validation (void) +{ + unsigned error_code; + unsigned called; + + error_code = 0; + assert(0 != lsquic_wt_test_validate_incoming_session_id(15, 1, "uni", + &error_code)); + assert(error_code == HEC_ID_ERROR); + + error_code = 0; + assert(0 != lsquic_wt_test_validate_incoming_session_id(15, 2, "bidi", + &error_code)); + assert(error_code == HEC_ID_ERROR); + + error_code = 0; + assert(0 == lsquic_wt_test_validate_incoming_session_id(15, 0, "uni", + &error_code)); + assert(0 == error_code); + + error_code = 0; + assert(0 == lsquic_wt_test_validate_incoming_session_id(15, 4, "bidi", + &error_code)); + assert(0 == error_code); + + error_code = 0; + assert(0 != lsquic_wt_test_validate_incoming_session_id(15, 3, "uni", + &error_code)); + assert(error_code == HEC_ID_ERROR); + + called = 1; + assert(0 == lsquic_wt_test_http_dg_read(0, 0, 0, &called)); + assert(0 == called); + + called = 1; + assert(0 == lsquic_wt_test_http_dg_read(1, 0, 0, &called)); + assert(0 == called); + + called = 1; + assert(0 == lsquic_wt_test_http_dg_read(1, 1, 1, &called)); + assert(0 == called); + + called = 0; + assert(0 == lsquic_wt_test_http_dg_read(1, 1, 0, &called)); + assert(1 == called); +} + +static void +test_dgq_policies (void) +{ + lsquic_wt_session_t *sess; + unsigned char v; + const unsigned char b1[2] = { 1, 1, }; + const unsigned char b2[2] = { 2, 2, }; + const unsigned char b3[2] = { 3, 3, }; + const unsigned char b4[2] = { 4, 4, }; + const unsigned char b5[2] = { 5, 5, }; + + sess = lsquic_wt_test_dgq_session_new(4, 8); + assert(sess); + assert(0 == lsquic_wt_test_dgq_enqueue(sess, b1, sizeof(b1), + LSQWT_DG_FAIL_EAGAIN)); + assert(0 == lsquic_wt_test_dgq_enqueue(sess, b2, sizeof(b2), + LSQWT_DG_FAIL_EAGAIN)); + assert(0 == lsquic_wt_test_dgq_enqueue(sess, b3, sizeof(b3), + LSQWT_DG_FAIL_EAGAIN)); + assert(0 == lsquic_wt_test_dgq_enqueue(sess, b4, sizeof(b4), + LSQWT_DG_FAIL_EAGAIN)); + errno = 0; + assert(0 != lsquic_wt_test_dgq_enqueue(sess, b5, sizeof(b5), + LSQWT_DG_FAIL_EAGAIN)); + assert(EAGAIN == errno); + assert(4 == lsquic_wt_test_dgq_count(sess)); + assert(8 == lsquic_wt_test_dgq_bytes(sess)); + assert(0 == lsquic_wt_test_dgq_front(sess, &v)); + assert(1 == v); + assert(0 == lsquic_wt_test_dgq_back(sess, &v)); + assert(4 == v); + lsquic_wt_test_dgq_session_destroy(sess); + + sess = lsquic_wt_test_dgq_session_new(4, 8); + assert(sess); + assert(0 == lsquic_wt_test_dgq_enqueue(sess, b1, sizeof(b1), + LSQWT_DG_FAIL_EAGAIN)); + assert(0 == lsquic_wt_test_dgq_enqueue(sess, b2, sizeof(b2), + LSQWT_DG_FAIL_EAGAIN)); + assert(0 == lsquic_wt_test_dgq_enqueue(sess, b3, sizeof(b3), + LSQWT_DG_FAIL_EAGAIN)); + assert(0 == lsquic_wt_test_dgq_enqueue(sess, b4, sizeof(b4), + LSQWT_DG_FAIL_EAGAIN)); + assert(0 == lsquic_wt_test_dgq_enqueue(sess, b5, sizeof(b5), + LSQWT_DG_DROP_OLDEST)); + assert(4 == lsquic_wt_test_dgq_count(sess)); + assert(8 == lsquic_wt_test_dgq_bytes(sess)); + assert(0 == lsquic_wt_test_dgq_front(sess, &v)); + assert(2 == v); + assert(0 == lsquic_wt_test_dgq_back(sess, &v)); + assert(5 == v); + lsquic_wt_test_dgq_session_destroy(sess); + + sess = lsquic_wt_test_dgq_session_new(4, 8); + assert(sess); + assert(0 == lsquic_wt_test_dgq_enqueue(sess, b1, sizeof(b1), + LSQWT_DG_FAIL_EAGAIN)); + assert(0 == lsquic_wt_test_dgq_enqueue(sess, b2, sizeof(b2), + LSQWT_DG_FAIL_EAGAIN)); + assert(0 == lsquic_wt_test_dgq_enqueue(sess, b3, sizeof(b3), + LSQWT_DG_FAIL_EAGAIN)); + assert(0 == lsquic_wt_test_dgq_enqueue(sess, b4, sizeof(b4), + LSQWT_DG_FAIL_EAGAIN)); + errno = 0; + assert(0 != lsquic_wt_test_dgq_enqueue(sess, b5, sizeof(b5), + LSQWT_DG_DROP_NEWEST)); + assert(EAGAIN == errno); + assert(4 == lsquic_wt_test_dgq_count(sess)); + assert(8 == lsquic_wt_test_dgq_bytes(sess)); + assert(0 == lsquic_wt_test_dgq_front(sess, &v)); + assert(1 == v); + assert(0 == lsquic_wt_test_dgq_back(sess, &v)); + assert(4 == v); + lsquic_wt_test_dgq_session_destroy(sess); +} + + + + +static void +test_close_capsule_and_close_state (void) +{ + char long_reason[WT_CLOSE_REASON_MAX + 9]; + unsigned char buf[64]; + unsigned called, mask, dgq_count, dropped_datagrams; + uint64_t capsule_type, payload_len, close_code; + size_t buf_len, close_reason_len; + int is_closing, close_received, on_close_called, queued_capsule, removed; + const unsigned char *p, *end; + int nr; + + memset(long_reason, 'x', sizeof(long_reason)); + buf_len = sizeof(buf); + assert(0 == lsquic_wt_test_build_close_capsule(0x12345678, "ok", 2, + buf, &buf_len)); + p = buf; + end = buf + buf_len; + nr = lsquic_varint_read(p, end, &capsule_type); + assert(nr > 0); + p += nr; + assert(capsule_type == 0x2843); + nr = lsquic_varint_read(p, end, &payload_len); + assert(nr > 0); + p += nr; + assert(payload_len == 6); + assert((size_t) (end - p) == payload_len); + close_code = (uint64_t) p[0] << 24 | (uint64_t) p[1] << 16 + | (uint64_t) p[2] << 8 | (uint64_t) p[3]; + assert(close_code == 0x12345678); + assert(0 == memcmp(p + 4, "ok", 2)); + + called = 0; + close_code = 0; + close_reason_len = 0; + is_closing = 0; + close_received = 0; + on_close_called = 0; + assert(0 == lsquic_wt_test_remote_close(0x22, "bye", 3, &called, + &close_code, &close_reason_len, &is_closing, + &close_received, &on_close_called)); + assert(called == 0); + assert(close_code == 0x22); + assert(close_reason_len == 3); + assert(is_closing); + assert(close_received); + assert(!on_close_called); + + called = 0; + close_code = 0; + close_reason_len = 0; + is_closing = 0; + close_received = 0; + on_close_called = 0; + assert(0 == lsquic_wt_test_remote_close(WT_APP_ERROR_MAX + 1, + long_reason, sizeof(long_reason), &called, + &close_code, &close_reason_len, &is_closing, + &close_received, &on_close_called)); + assert(called == 0); + assert(close_code == WT_APP_ERROR_MAX); + assert(close_reason_len == WT_CLOSE_REASON_MAX); + assert(is_closing); + assert(close_received); + assert(!on_close_called); + + queued_capsule = 0; + dgq_count = 1; + assert(0 == lsquic_wt_test_local_close(0x1234, "bye", 3, + &queued_capsule, &dgq_count)); + assert(queued_capsule); + assert(0 == dgq_count); + + queued_capsule = 1; + dgq_count = 1; + assert(0 == lsquic_wt_test_local_close(0, NULL, 0, + &queued_capsule, &dgq_count)); + assert(!queued_capsule); + assert(0 == dgq_count); + + mask = 0; + assert(0 == lsquic_wt_test_closing_rejects(&mask)); + assert(mask == 0xF); + + called = 0; + close_code = 0; + close_reason_len = 0; + removed = 0; + dropped_datagrams = 0; + assert(0 == lsquic_wt_test_finalize(0x33, "done", 4, &called, + &close_code, &close_reason_len, &removed, + &dropped_datagrams)); + assert(called == 1); + assert(close_code == 0x33); + assert(close_reason_len == 4); + assert(removed); + assert(dropped_datagrams); + + called = 0; + is_closing = 0; + close_received = 1; + assert(0 == lsquic_wt_test_control_reset_close(&called, &is_closing, + &close_received)); + assert(called == 0); + assert(is_closing); + assert(!close_received); +} + + +static void +test_reset_dispatch (void) +{ + uint64_t h3_rst, h3_ss; + uint64_t reset_code, stop_code; + unsigned called; + + assert(0 == lsquic_wt_test_app_error_to_h3_error(0x11, &h3_rst)); + assert(0 == lsquic_wt_test_app_error_to_h3_error(0x22, &h3_ss)); + + called = 0; + reset_code = 0; + stop_code = 0; + assert(0 == lsquic_wt_test_dispatch_reset(2, 1, 1, 1, h3_rst, h3_ss, + &called, &reset_code, &stop_code)); + assert(3 == called); + assert(0x11 == reset_code); + assert(0x22 == stop_code); + + called = 0; + reset_code = 0; + stop_code = 0; + assert(0 == lsquic_wt_test_dispatch_reset(1, 0, 1, 1, h3_rst, h3_ss, + &called, &reset_code, &stop_code)); + assert(2 == called); + assert(0 == reset_code); + assert(0x11 == stop_code); + + called = 0; + reset_code = 0; + stop_code = 0; + assert(0 == lsquic_wt_test_dispatch_reset(0, 1, 0, 1, h3_rst, h3_ss, + &called, &reset_code, &stop_code)); + assert(0 == called); + assert(0 == reset_code); + assert(0 == stop_code); + + called = 0; + reset_code = 0; + stop_code = 0; + assert(0 == lsquic_wt_test_dispatch_reset(2, 1, 1, 0, h3_rst, h3_ss, + &called, &reset_code, &stop_code)); + assert(0 == called); + assert(0 == reset_code); + assert(0 == stop_code); +} + + +static void +test_deferred_accept_resolution (void) +{ + unsigned initial_result, final_result, opened, rejected, status; + unsigned called_before, called_after; + + initial_result = final_result = opened = rejected = status = UINT_MAX; + assert(0 == lsquic_wt_test_accept_resolution( + WT_TEST_CP_H3_PEER_SETTINGS | WT_TEST_CP_WEBTRANSPORT + | WT_TEST_CP_CONNECT_PROTOCOL, + WT_TEST_CP_H3_PEER_SETTINGS | WT_TEST_CP_WEBTRANSPORT + | WT_TEST_CP_CONNECT_PROTOCOL, + 0, &initial_result, &final_result, + &opened, &rejected, &status)); + assert(initial_result == WT_TEST_ACCEPT_OPEN); + assert(final_result == WT_TEST_ACCEPT_OPEN); + assert(opened == 1); + assert(rejected == 0); + + initial_result = final_result = opened = rejected = status = UINT_MAX; + assert(0 == lsquic_wt_test_accept_resolution( + 0, + WT_TEST_CP_H3_PEER_SETTINGS | WT_TEST_CP_WEBTRANSPORT + | WT_TEST_CP_CONNECT_PROTOCOL, + 0, &initial_result, &final_result, + &opened, &rejected, &status)); + assert(initial_result == WT_TEST_ACCEPT_PENDING); + assert(final_result == WT_TEST_ACCEPT_OPEN); + assert(opened == 1); + assert(rejected == 0); + + initial_result = final_result = opened = rejected = status = UINT_MAX; + assert(0 == lsquic_wt_test_accept_resolution( + 0, WT_TEST_CP_H3_PEER_SETTINGS, 0, + &initial_result, &final_result, + &opened, &rejected, &status)); + assert(initial_result == WT_TEST_ACCEPT_PENDING); + assert(final_result == WT_TEST_ACCEPT_REJECT); + assert(opened == 0); + assert(rejected == 1); + assert(status == 400); + + initial_result = final_result = opened = rejected = status = UINT_MAX; + assert(0 == lsquic_wt_test_accept_resolution( + WT_TEST_CP_H3_PEER_SETTINGS | WT_TEST_CP_WEBTRANSPORT + | WT_TEST_CP_CONNECT_PROTOCOL, + WT_TEST_CP_H3_PEER_SETTINGS | WT_TEST_CP_WEBTRANSPORT + | WT_TEST_CP_CONNECT_PROTOCOL, + 1, &initial_result, &final_result, + &opened, &rejected, &status)); + assert(initial_result == WT_TEST_ACCEPT_REJECT); + assert(final_result == WT_TEST_ACCEPT_REJECT); + assert(opened == 0); + assert(rejected == 1); + assert(status == 429); + + called_before = called_after = UINT_MAX; + assert(0 == lsquic_wt_test_pending_datagram_replay(&called_before, + &called_after)); + assert(called_before == 0); + assert(called_after == 1); +} + + +static void +test_accept_status_validation (void) +{ + int accepted; + + accepted = -1; + assert(0 == lsquic_wt_test_accept_status_validation(0, &accepted)); + assert(accepted == 1); + + accepted = -1; + assert(0 == lsquic_wt_test_accept_status_validation(199, &accepted)); + assert(accepted == 0); + + accepted = -1; + assert(0 == lsquic_wt_test_accept_status_validation(200, &accepted)); + assert(accepted == 1); + + accepted = -1; + assert(0 == lsquic_wt_test_accept_status_validation(299, &accepted)); + assert(accepted == 1); + + accepted = -1; + assert(0 == lsquic_wt_test_accept_status_validation(300, &accepted)); + assert(accepted == 0); +} + + +static void +test_reject_status_validation (void) +{ + int accepted; + + accepted = -1; + assert(0 == lsquic_wt_test_reject_status_validation(0, &accepted)); + assert(accepted == 1); + + accepted = -1; + assert(0 == lsquic_wt_test_reject_status_validation(199, &accepted)); + assert(accepted == 1); + + accepted = -1; + assert(0 == lsquic_wt_test_reject_status_validation(200, &accepted)); + assert(accepted == 0); + + accepted = -1; + assert(0 == lsquic_wt_test_reject_status_validation(299, &accepted)); + assert(accepted == 0); + + accepted = -1; + assert(0 == lsquic_wt_test_reject_status_validation(300, &accepted)); + assert(accepted == 1); +} + + +static void +test_write_error_closes_stream (void) +{ + int control_closed, data_closed; + + control_closed = data_closed = 0; + assert(0 == lsquic_wt_test_write_error_closes_stream(&control_closed, + &data_closed)); + assert(control_closed); + assert(data_closed); +} + + +static void +test_control_stream_ops_rejected (void) +{ + unsigned mask; + + mask = 0; + assert(0 == lsquic_wt_test_control_stream_ops_rejected(&mask)); + assert(mask == 0x3); +} + + +static void +test_compatibility_mode_behavior (void) +{ + unsigned supports, draft; + + supports = draft = UINT_MAX; + assert(0 == lsquic_ietf_test_wt_support( + 1, /* server side */ + 1, /* peer SETTINGS received */ + 1, /* local WT enabled */ + 1, /* HTTP datagrams */ + 1, /* QUIC datagrams */ + 0, /* CONNECT protocol not needed server-side */ + 1, 1, /* draft-14 WT_MAX_SESSIONS */ + 0, 0, /* no WT_ENABLED setting */ + 0, 0, 0, /* no WT initial settings */ + 0, /* no reset_stream_at TP */ + 14, + &supports, &draft)); + assert(supports == 1); + assert(draft == 14); + + supports = draft = UINT_MAX; + assert(0 == lsquic_ietf_test_wt_support( + 0, /* client side */ + 1, 1, 1, 1, + 1, /* CONNECT protocol required and present */ + 0, 0, /* no WT_MAX_SESSIONS */ + 1, 1, /* draft-15 WT enabled */ + 0, 0, 0, /* missing WT initial settings */ + 0, /* no reset_stream_at TP */ + 15, + &supports, &draft)); + assert(supports == 1); + assert(draft == 15); + + supports = draft = UINT_MAX; + assert(0 == lsquic_ietf_test_wt_support( + 0, 1, 1, 1, 1, + 0, /* missing CONNECT protocol */ + 0, 0, + 1, 1, + 1, 1, 1, + 1, + 15, + &supports, &draft)); + assert(supports == 0); + assert(draft == 15); +} + + +static void +test_pending_replay_stops_on_close (void) +{ + unsigned called_after; + int is_closing; + + called_after = UINT_MAX; + is_closing = 0; + assert(0 == lsquic_wt_test_pending_datagram_replay_stops_on_close( + &called_after, &is_closing)); + assert(called_after == 1); + assert(is_closing); +} + + +static void +test_destroy_while_closing (void) +{ + unsigned called; + int removed; + + called = UINT_MAX; + removed = 0; + assert(0 == lsquic_wt_test_destroy_while_closing(0, &called, &removed)); + assert(called == 1); + assert(removed); + + called = UINT_MAX; + removed = 0; + assert(0 == lsquic_wt_test_destroy_while_closing(1, &called, &removed)); + assert(called == 1); + assert(removed); +} + + +static void +test_stream_switch_failure_restores_state (void) +{ + int restored_if, restored_ctx, restored_session; + + restored_if = restored_ctx = restored_session = 0; + assert(0 == lsquic_wt_test_stream_switch_failure_restores_state( + &restored_if, &restored_ctx, + &restored_session)); + assert(restored_if); + assert(restored_ctx); + assert(restored_session); +} + + +static void +test_extra_resp_header_validation (void) +{ + int null_headers_rejected, zero_len_ok, rejected; + int negative_rejected, overflow_rejected; + + null_headers_rejected = zero_len_ok = rejected = 0; + negative_rejected = overflow_rejected = 0; + assert(0 == lsquic_wt_test_extra_resp_header_validation( + &null_headers_rejected, &zero_len_ok)); + assert(null_headers_rejected); + assert(zero_len_ok); + + assert(0 == lsquic_wt_test_send_response_rejects_missing_extra_headers( + &rejected)); + assert(rejected); + + assert(0 == lsquic_wt_test_response_header_count_validation( + &negative_rejected, &overflow_rejected)); + assert(negative_rejected); + assert(overflow_rejected); +} + + +static void +test_dgq_overflow_rejected (void) +{ + int overflow_rejected; + + overflow_rejected = 0; + assert(0 == lsquic_wt_test_dgq_overflow_rejected(0, &overflow_rejected)); + assert(overflow_rejected); + + overflow_rejected = 0; + assert(0 == lsquic_wt_test_dgq_overflow_rejected(1, &overflow_rejected)); + assert(overflow_rejected); +} + + +static void +test_open_stream_init_failure (void) +{ + int aborted, freed_dynamic_onnew; + + aborted = freed_dynamic_onnew = 0; + assert(0 == lsquic_wt_test_open_stream_init_failure(0, &aborted, + &freed_dynamic_onnew)); + assert(aborted); + assert(freed_dynamic_onnew); + + aborted = freed_dynamic_onnew = 0; + assert(0 == lsquic_wt_test_open_stream_init_failure(1, &aborted, + &freed_dynamic_onnew)); + assert(aborted); + assert(freed_dynamic_onnew); +} + + +static void +test_datagram_write_state_rollback (void) +{ + int want_flag_cleared, send_disarmed; + + want_flag_cleared = send_disarmed = 0; + assert(0 == lsquic_wt_test_datagram_write_state_rollback( + &want_flag_cleared, &send_disarmed)); + assert(want_flag_cleared); + assert(send_disarmed); +} + + +static void +test_http_dg_write_path (void) +{ + static const unsigned char dg[] = { 'd', 'g', }; + unsigned consume_calls, callback_calls, queued_after, disarm_calls; + int want_flag_set, is_closing, saved_errno; + + consume_calls = callback_calls = queued_after = disarm_calls = 0; + want_flag_set = is_closing = saved_errno = 0; + assert(0 == lsquic_wt_test_http_dg_write_path( + WT_TEST_HTTP_DG_WRITE_PREQUEUE, dg, sizeof(dg), 16, + &consume_calls, &callback_calls, &queued_after, + &want_flag_set, &is_closing, &disarm_calls, &saved_errno)); + assert(consume_calls == 1); + assert(callback_calls == 0); + assert(queued_after == 0); + assert(!want_flag_set); + assert(!is_closing); + assert(saved_errno == 0); + + consume_calls = callback_calls = queued_after = disarm_calls = 0; + want_flag_set = is_closing = saved_errno = 0; + assert(0 == lsquic_wt_test_http_dg_write_path( + WT_TEST_HTTP_DG_WRITE_WANT | WT_TEST_HTTP_DG_WRITE_CB_QUEUE, + dg, sizeof(dg), 16, &consume_calls, &callback_calls, + &queued_after, &want_flag_set, &is_closing, &disarm_calls, + &saved_errno)); + assert(consume_calls == 1); + assert(callback_calls == 1); + assert(queued_after == 0); + assert(want_flag_set); + assert(!is_closing); + assert(disarm_calls == 0); + assert(saved_errno == 0); + + consume_calls = callback_calls = queued_after = disarm_calls = 0; + want_flag_set = is_closing = saved_errno = 0; + assert(-1 == lsquic_wt_test_http_dg_write_path( + WT_TEST_HTTP_DG_WRITE_WANT | WT_TEST_HTTP_DG_WRITE_CB_QUEUE + | WT_TEST_HTTP_DG_WRITE_CB_CLOSE, + dg, sizeof(dg), 16, &consume_calls, &callback_calls, + &queued_after, &want_flag_set, &is_closing, &disarm_calls, + &saved_errno)); + assert(consume_calls == 0); + assert(callback_calls == 1); + assert(queued_after == 0); + assert(!want_flag_set); + assert(is_closing); + assert(saved_errno == EAGAIN); +} + + +static void +test_wt_uni_switch_failure (void) +{ + int restored_if, restored_ctx, close_attempted; + + restored_if = restored_ctx = close_attempted = 0; + assert(0 == lsquic_ietf_test_wt_uni_switch_failure(&restored_if, + &restored_ctx, + &close_attempted)); + assert(restored_if); + assert(restored_ctx); + assert(close_attempted); +} + + +static void +test_read_error_closes_stream (void) +{ + int control_closed, uni_closed; + + control_closed = uni_closed = 0; + assert(0 == lsquic_wt_test_read_error_closes_stream(&control_closed, + &uni_closed)); + assert(control_closed); + assert(uni_closed); +} + + +static void +test_truncated_uni_session_id_is_malformed (void) +{ + static const unsigned char truncated_varint[] = { 0x40, }; + size_t consumed; + int done, malformed; + lsquic_stream_id_t session_id; + + consumed = 0; + done = malformed = 0; + session_id = 123; + assert(0 == lsquic_wt_test_uni_read_state(truncated_varint, + sizeof(truncated_varint), 1, + &consumed, &done, &malformed, + &session_id)); + assert(consumed == sizeof(truncated_varint)); + assert(done); + assert(malformed); + assert(session_id == 0); +} + + +static void +test_truncated_capsule_type_fin_aborts (void) +{ + unsigned error_code; + + error_code = 0; + assert(0 == lsquic_stream_test_truncated_capsule_type_fin_aborts( + &error_code)); + assert(error_code == HEC_DATAGRAM_ERROR); +} + + +int +main (void) +{ + test_error_code_mapping(); + test_peer_query_helpers(); + test_stream_helpers(); + test_invalid_public_api(); + test_incoming_session_id_validation(); + test_dgq_policies(); + test_close_capsule_and_close_state(); + test_deferred_accept_resolution(); + test_accept_status_validation(); + test_reject_status_validation(); + test_write_error_closes_stream(); + test_control_stream_ops_rejected(); + test_compatibility_mode_behavior(); + test_reset_dispatch(); + test_pending_replay_stops_on_close(); + test_destroy_while_closing(); + test_stream_switch_failure_restores_state(); + test_extra_resp_header_validation(); + test_dgq_overflow_rejected(); + test_open_stream_init_failure(); + test_datagram_write_state_rollback(); + test_http_dg_write_path(); + test_wt_uni_switch_failure(); + test_read_error_closes_stream(); + test_truncated_uni_session_id_is_malformed(); + test_truncated_capsule_type_fin_aborts(); + return 0; +} diff --git a/tools/run_wt_dg_matrix.sh b/tools/run_wt_dg_matrix.sh new file mode 100755 index 000000000..66369a8a1 --- /dev/null +++ b/tools/run_wt_dg_matrix.sh @@ -0,0 +1,172 @@ +#!/usr/bin/env bash + +set -euo pipefail + +usage() { + cat <<'EOF' +Usage: tools/run_wt_dg_matrix.sh [options] + +Run WebTransport datagram queue policy matrix against http_server -B. + +Options: + -s host:port Server address for baton_client and http_server bind + (default: localhost:12347) + -c certspec TLS cert spec for http_server -c + (default: localhost,server.crt,server.key) + -l level Log level for server/client (default: info) + -b value Initial baton value for client (default: 1) + -u list Datagram burst values (comma-separated, default: 1,64,80,160) + -p list Policies (comma-separated, default: fail,oldest,newest) + -t sec Per-client timeout in seconds (default: 12) + -o dir Output directory (default: /tmp/wt_dg_matrix_) + -h Show this help + +Examples: + tools/run_wt_dg_matrix.sh + tools/run_wt_dg_matrix.sh -u 64,80,160 -p fail,newest -o /tmp/wt-matrix +EOF +} + +count_pattern() { + local pattern="$1" + local file="$2" + + if command -v rg >/dev/null 2>&1; then + rg -c -- "$pattern" "$file" || true + else + grep -c -- "$pattern" "$file" || true + fi +} + +parse_csv() { + local csv="$1" + local -n out_arr=$2 + local item + + out_arr=() + IFS=',' read -r -a out_arr <<< "$csv" + for item in "${out_arr[@]}"; do + if [[ -z "$item" ]]; then + echo "empty value in list: $csv" >&2 + exit 1 + fi + done +} + +server_addr="localhost:12347" +cert_spec="localhost,server.crt,server.key" +log_level="info" +baton_value="1" +u_csv="1,64,80,160" +p_csv="fail,oldest,newest" +timeout_sec="12" +out_dir="/tmp/wt_dg_matrix_$(date +%Y%m%d_%H%M%S)" + +while getopts ":s:c:l:b:u:p:t:o:h" opt; do + case "$opt" in + s) server_addr="$OPTARG" ;; + c) cert_spec="$OPTARG" ;; + l) log_level="$OPTARG" ;; + b) baton_value="$OPTARG" ;; + u) u_csv="$OPTARG" ;; + p) p_csv="$OPTARG" ;; + t) timeout_sec="$OPTARG" ;; + o) out_dir="$OPTARG" ;; + h) + usage + exit 0 + ;; + :) + echo "missing argument for -$OPTARG" >&2 + usage + exit 1 + ;; + \?) + echo "unknown option: -$OPTARG" >&2 + usage + exit 1 + ;; + esac +done + +parse_csv "$u_csv" u_values +parse_csv "$p_csv" policies + +mkdir -p "$out_dir" +server_log="$out_dir/server.log" +summary_file="$out_dir/summary.tsv" + +cleanup() { + if [[ -n "${srv_pid:-}" ]]; then + kill "$srv_pid" 2>/dev/null || true + wait "$srv_pid" 2>/dev/null || true + fi +} +trap cleanup EXIT + +./bin/http_server -c "$cert_spec" -s "$server_addr" -B -L "$log_level" \ + >"$server_log" 2>&1 & +srv_pid=$! + +sleep 1 +if ! kill -0 "$srv_pid" 2>/dev/null; then + echo "http_server failed to start; see $server_log" >&2 + exit 1 +fi + +printf "U\tpolicy\trc\tconnect_ok\tconnect_fail\tsent\tfailed\tdrop_old\tdrop_new\tq_full\n" \ + >"$summary_file" +printf "%-4s %-7s %-3s %-10s %-12s %-6s %-7s %-8s %-8s %-6s\n" \ + "U" "POLICY" "RC" "CONNECT_OK" "CONNECT_FAIL" "SENT" "FAILED" \ + "DROP_OLD" "DROP_NEW" "Q_FULL" + +for u in "${u_values[@]}"; do + for p in "${policies[@]}"; do + case "$p" in + fail|oldest|newest) ;; + *) + echo "invalid policy: $p (expected fail|oldest|newest)" >&2 + exit 1 + ;; + esac + + client_log="$out_dir/client_U${u}_${p}.log" + if timeout "$timeout_sec" ./bin/baton_client -s "$server_addr" \ + -L "$log_level" -b "$baton_value" -U "$u" -M "$p" \ + >"$client_log" 2>&1; then + rc=0 + else + rc=$? + fi + + connect_ok="$(count_pattern "client received successful CONNECT response" "$client_log")" + connect_fail="$(count_pattern "CONNECT failed" "$client_log")" + drop_old="$(count_pattern "drop queued WT datagram" "$client_log")" + drop_new="$(count_pattern "drop newest WT datagram" "$client_log")" + q_full="$(count_pattern "WT datagram queue full" "$client_log")" + + sent="-" + failed="-" + if burst_line="$(grep -m1 'burst datagram send complete' "$client_log" 2>/dev/null)"; then + sent="$(printf "%s\n" "$burst_line" \ + | sed -n 's/.*sent=\([0-9][0-9]*\).*/\1/p')" + failed="$(printf "%s\n" "$burst_line" \ + | sed -n 's/.*failed=\([0-9][0-9]*\).*/\1/p')" + [[ -n "$sent" ]] || sent="-" + [[ -n "$failed" ]] || failed="-" + fi + + printf "%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n" \ + "$u" "$p" "$rc" "$connect_ok" "$connect_fail" "$sent" "$failed" \ + "$drop_old" "$drop_new" "$q_full" >>"$summary_file" + printf "%-4s %-7s %-3s %-10s %-12s %-6s %-7s %-8s %-8s %-6s\n" \ + "$u" "$p" "$rc" "$connect_ok" "$connect_fail" "$sent" "$failed" \ + "$drop_old" "$drop_new" "$q_full" + done +done + +echo +echo "Wrote summary: $summary_file" +echo "Server log: $server_log" +echo "Client logs: $out_dir/client_U*_*.log" + diff --git a/web_transport_devious_baton_integration_plan.md b/web_transport_devious_baton_integration_plan.md new file mode 100644 index 000000000..d8dfe2a6e --- /dev/null +++ b/web_transport_devious_baton_integration_plan.md @@ -0,0 +1,157 @@ +# WebTransport (WT) Devious Baton Integration Plan + +## Goal +Implement WebTransport support in **lsquic** and validate the API and state machine by integrating the *Devious Baton* test application into the existing lsquic HTTP client and server example programs. + +This approach deliberately avoids early integration with OpenLiteSpeed (OLS) or PHP bindings, reducing complexity and enabling faster iteration and interop testing. + +--- + +## High-Level Design Principles + +1. **WebTransport is layered on top of HTTP/3** + - WT sessions are initiated via `CONNECT` requests with `:protocol = "webtransport"`. + - The CONNECT request stream acts as the WT *control stream*. + +2. **No special WT request callback** + - lsquic does not introduce a dedicated `on_wt_connect_request` handler. + - WT requests flow through the normal HTTP request path (hset or HTTP/1.x-compatible APIs). + +3. **Application-driven accept / reject** + - The application inspects the request headers as usual. + - When the application sends a response: + - **2xx** → lsquic *promotes* the stream to a WT session. + - **non-2xx** → request is rejected; no WT session is created. + +4. **lsquic stays out of request routing logic** + - lsquic only detects WT *candidates* and manages protocol transitions. + - All policy decisions live in application code. + +--- + +## Internal lsquic Behavior + +### WT Candidate Detection +When request headers are complete on an HTTP/3 stream, lsquic internally checks: +- Method == `CONNECT` +- `:protocol == "webtransport"` + +If matched, the stream is marked as a **WT candidate**. + +### Promotion Trigger +When the application sends final response headers: +- If status is **2xx** and the stream is a WT candidate: + - The stream is promoted to a WT control stream. + - A `wt_session` object is created and bound to the stream. +- If status is **non-2xx**: + - WT candidate state is cleared; the stream completes as a normal HTTP request. + +### Required API Primitives +Minimal helper APIs needed: +- `lsquic_stream_is_wt_candidate(stream)` +- `lsquic_stream_get_wt_sess(stream)` (valid only after 2xx promotion) + +WT session APIs: +- Open WT bidi / uni streams +- Send WT datagrams +- WT callbacks: incoming streams, datagrams, session close + +--- + +## TODO (Short-Term) + +- Implement WT datagrams using existing HTTP Datagram support; wire + `lsquic_wt_send_datagram()` and `lsquic_wt_max_datagram_size()`. +- Dispatch incoming WT datagrams to the application callback + (`on_dg_datagram` / WT datagram handler). +- Update Devious Baton to exercise WT datagrams (send/receive) via the WT + datagram callback. + +--- + +## Devious Baton Integration (Server Example) + +### Hook Point +In the existing HTTP server example: +- At request-headers-complete time: + - If `lsquic_stream_is_wt_candidate(stream)` is true: + - Hand off the stream to the *baton server module*. + - Bypass normal HTTP request handling. + +### Baton Server Module Responsibilities +1. Inspect request headers and apply policy. +2. Send response: + - Accept: send `2xx`. + - Reject: send `4xx` / `5xx` and return. +3. On acceptance: + - Call `lsquic_stream_get_wt_sess(stream)`. + - Register WT callbacks. +4. Implement Devious Baton behavior: + - Handle incoming WT bidi streams. + - Process baton payloads per the test spec. + - Handle WT datagrams if required. + - Clean up on WT session close. + +### Ownership Model +Once baton handling begins: +- The stream switches from HTTP mode to WT mode. +- HTTP request/response logic must not touch the stream again. + +--- + +## Devious Baton Integration (Client Example) + +Add a new client mode, e.g.: +- `--devious-baton` + +Client flow: +1. Send CONNECT request with `:protocol=webtransport`. +2. Read response headers: + - If 2xx: WT session established. + - Else: report failure and exit. +3. Execute baton test logic: + - Open WT bidi streams. + - Send baton data. + - Receive and validate responses. + - Optionally test datagrams. + +This turns the existing examples into a self-contained interop test harness. + +### Runtime Queue Tuning (current tools) + +You can tune per-session WT datagram queue limits at runtime without code +changes: + +- `baton_client` + - `-u `: max queued WT datagrams (0 = library default) + - `-v `: max queued WT datagram bytes (0 = library default) +- `http_server -B` + - `-u `: max queued WT datagrams (0 = library default) + - `-v `: max queued WT datagram bytes (0 = library default) + +Example: + +```bash +./bin/http_server -c localhost,server.crt,server.key -s 127.0.0.1:12347 -B -u 64 -v 65536 -L info +./bin/baton_client -s localhost:12347 -u 64 -v 65536 -M oldest -U 200 -L info +``` + +--- + +## Benefits of This Approach + +- Fast validation of WT API design and state machine. +- No dependency on OLS, PHP, or language bindings. +- Clear separation between HTTP handling and WT runtime. +- Minimal public API surface required initially. +- Easy evolution toward OLS integration once semantics stabilize. + +--- + +## Future Work (Out of Scope for Now) + +- OpenLiteSpeed integration +- PHP or other language bindings +- Advanced routing, auth, or deployment concerns + +These can be layered on once WT support is proven via Devious Baton.