diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 79e7587b29b4..7f4ff2f20dd6 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -50,13 +50,13 @@ jobs: persist-credentials: false - name: 'initialize' - uses: github/codeql-action/init@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4.31.9 + uses: github/codeql-action/init@1b168cd39490f61582a9beae412bb7057a6b2c4e # v4.31.8 with: languages: actions, python queries: security-extended - name: 'perform analysis' - uses: github/codeql-action/analyze@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4.31.9 + uses: github/codeql-action/analyze@1b168cd39490f61582a9beae412bb7057a6b2c4e # v4.31.8 c: if: ${{ github.repository_owner == 'curl' || github.event_name != 'schedule' }} @@ -87,7 +87,7 @@ jobs: persist-credentials: false - name: 'initialize' - uses: github/codeql-action/init@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4.31.9 + uses: github/codeql-action/init@1b168cd39490f61582a9beae412bb7057a6b2c4e # v4.31.8 with: languages: cpp build-mode: manual @@ -131,4 +131,4 @@ jobs: fi - name: 'perform analysis' - uses: github/codeql-action/analyze@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4.31.9 + uses: github/codeql-action/analyze@1b168cd39490f61582a9beae412bb7057a6b2c4e # v4.31.8 diff --git a/.github/workflows/http3-linux.yml b/.github/workflows/http3-linux.yml index 5f85a51be780..ee34fa0ead1d 100644 --- a/.github/workflows/http3-linux.yml +++ b/.github/workflows/http3-linux.yml @@ -54,9 +54,9 @@ env: # renovate: datasource=github-tags depName=wolfSSL/wolfssl versioning=semver extractVersion=^v?(?.+)-stable$ registryUrl=https://github.com WOLFSSL_VERSION: 5.8.4 # renovate: datasource=github-tags depName=ngtcp2/nghttp3 versioning=semver registryUrl=https://github.com - NGHTTP3_VERSION: 1.14.0 + NGHTTP3_VERSION: 1.13.1 # renovate: datasource=github-tags depName=ngtcp2/ngtcp2 versioning=semver registryUrl=https://github.com - NGTCP2_VERSION: 1.19.0 + NGTCP2_VERSION: 1.18.0 # renovate: datasource=github-tags depName=nghttp2/nghttp2 versioning=semver registryUrl=https://github.com NGHTTP2_VERSION: 1.68.0 # renovate: datasource=github-tags depName=cloudflare/quiche versioning=semver registryUrl=https://github.com diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml index 2fd1817f7b52..acd34565d3a3 100644 --- a/.github/workflows/linux.yml +++ b/.github/workflows/linux.yml @@ -37,7 +37,7 @@ env: MAKEFLAGS: -j 5 CURL_CI: github CURL_TEST_MIN: 1600 - CURL_CLANG_TIDYFLAGS: '-checks=-clang-analyzer-security.insecureAPI.bzero,-clang-analyzer-optin.performance.Padding,-clang-analyzer-security.ArrayBound,-clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling,-clang-analyzer-valist.Uninitialized' + CURL_CLANG_TIDYFLAGS: '-checks=-clang-analyzer-security.insecureAPI.bzero,-clang-analyzer-security.insecureAPI.strcpy,-clang-analyzer-optin.performance.Padding,-clang-analyzer-security.ArrayBound,-clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling,-clang-analyzer-valist.Uninitialized' # renovate: datasource=github-tags depName=libressl/portable versioning=semver registryUrl=https://github.com LIBRESSL_VERSION: 4.2.1 # renovate: datasource=github-tags depName=wolfSSL/wolfssl versioning=semver extractVersion=^v?(?.+)-stable$ registryUrl=https://github.com diff --git a/.github/workflows/non-native.yml b/.github/workflows/non-native.yml index 36e87bcbf957..e1b675aa2ccb 100644 --- a/.github/workflows/non-native.yml +++ b/.github/workflows/non-native.yml @@ -52,7 +52,7 @@ jobs: with: persist-credentials: false - name: 'cmake' - uses: cross-platform-actions/action@492b0c80085400348c599edace11141a4ee73524 # v0.32.0 + uses: cross-platform-actions/action@46e8d7fb25520a8d6c64fd2b7a1192611da98eda # v0.30.0 env: MATRIX_ARCH: '${{ matrix.arch }}' with: @@ -97,7 +97,7 @@ jobs: with: persist-credentials: false - name: 'cmake' - uses: cross-platform-actions/action@492b0c80085400348c599edace11141a4ee73524 # v0.32.0 + uses: cross-platform-actions/action@46e8d7fb25520a8d6c64fd2b7a1192611da98eda # v0.30.0 env: MATRIX_ARCH: '${{ matrix.arch }}' with: @@ -139,7 +139,7 @@ jobs: include: - { build: 'autotools', arch: 'x86_64', compiler: 'clang' } - { build: 'cmake' , arch: 'x86_64', compiler: 'clang', options: '-DCMAKE_UNITY_BUILD=OFF', desc: ' !unity !runtests !examples' } - - { build: 'autotools', arch: 'arm64' , compiler: 'clang', desc: ' !examples' } + - { build: 'autotools', arch: 'arm64' , compiler: 'clang' } - { build: 'cmake' , arch: 'arm64' , compiler: 'clang' } fail-fast: false steps: @@ -147,7 +147,7 @@ jobs: with: persist-credentials: false - name: '${{ matrix.build }}' - uses: cross-platform-actions/action@492b0c80085400348c599edace11141a4ee73524 # v0.32.0 + uses: cross-platform-actions/action@46e8d7fb25520a8d6c64fd2b7a1192611da98eda # v0.30.0 env: CC: '${{ matrix.compiler }}' CURL_TEST_MIN: 1800 diff --git a/.github/workflows/sync-fork-once-daily.yaml b/.github/workflows/sync-fork-once-daily.yaml new file mode 100644 index 000000000000..d8db76853ee0 --- /dev/null +++ b/.github/workflows/sync-fork-once-daily.yaml @@ -0,0 +1,27 @@ +name: sync-fork-daily-once +on: + schedule: + - cron: '0 0 * * *' + workflow_dispatch: { } +jobs: + sync: + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + persist-credentials: true + - name: Sync fork + run: | + git config --global user.name 'github-actions[bot]' + git config --global user.email 'github-actions[bot]@users.noreply.github.com' + git remote add upstream https://github.com/curl/curl.git + git fetch upstream + git checkout ${{ github.ref_name }} + git merge --no-ff --no-commit upstream/${{ github.ref_name }} + git reset HEAD .github/workflows + git checkout -- .github/workflows + git commit -m "Sync with upstream (excluding workflows)" + git push origin ${{ github.ref_name }} diff --git a/docs/HTTP3.md b/docs/HTTP3.md index e18277cb3c87..77cf9c955c9b 100644 --- a/docs/HTTP3.md +++ b/docs/HTTP3.md @@ -352,6 +352,18 @@ those until one succeeds - just as with all other protocols. If those IP addresses contain both IPv6 and IPv4, those attempts happen, delayed, in parallel (the actual eyeballing). +## --quic-v2 + +The `--quic-v2` option instructs curl to attempt to connect to the server using exclusively QUIC version 2. +QUIC version 2 is identified by the protocol ID `0x6b3343cf`. + +If this option is used, curl attempts to establish a QUIC version 2 connection. It does not fall back to other QUIC versions (like v1) or other HTTP versions (like HTTP/2 or HTTP/1.1) if the QUIC v2 connection attempt fails or is not supported by the server. This behavior is similar to `--http3-only` but specifically targets QUIC version 2 for the QUIC layer. + +This option requires a QUIC backend library (e.g., ngtcp2, quiche, OpenSSL with QUIC support) that is capable of and configured to use QUIC version 2. If the backend does not support QUIC v2 or cannot be configured for it, using this flag might result in a connection failure or the flag having no specific effect on the QUIC version chosen (depending on the backend). + +Example: + % curl --quic-v2 https://example.com + ## Known Bugs Check out the [list of known HTTP3 bugs](https://curl.se/docs/knownbugs.html#HTTP3). diff --git a/docs/MANUAL.md b/docs/MANUAL.md index c52e231bdc47..c86652b31189 100644 --- a/docs/MANUAL.md +++ b/docs/MANUAL.md @@ -845,6 +845,15 @@ still have `foo.example.com` do it, set `NO_PROXY` to `www.example.com`. The usage of the `-x`/`--proxy` flag overrides the environment variables. +--quic-v2 + (HTTP/3) Tells curl to attempt to connect using QUIC version 2 only. + This option makes curl try to connect to the target server using the + QUIC version 2 protocol (`0x6b3343cf`). If the server does not + support QUIC version 2, the connection will fail. This option does + not fall back to other QUIC versions or other HTTP versions. + This requires a QUIC backend that supports QUIC version 2. + (Added in 8.X.Y) + ## Netrc Unix introduced the `.netrc` concept a long time ago. It is a way for a user diff --git a/docs/libcurl/curl_easy_setopt.md b/docs/libcurl/curl_easy_setopt.md index 771442b18504..42077d599767 100644 --- a/docs/libcurl/curl_easy_setopt.md +++ b/docs/libcurl/curl_easy_setopt.md @@ -882,6 +882,21 @@ Add transfer mode to URL over proxy. See CURLOPT_PROXY_TRANSFER_MODE(3) **Deprecated option** Issue an HTTP PUT request. See CURLOPT_PUT(3) +## CURLOPT_QUIC_VERSION + +Pass a long specifying the QUIC version to use. Set to `0` (default) to allow +negotiation by the QUIC library, `1` to request QUIC version 1 +(`0x00000001`), or `2` to request QUIC version 2 (`0x6b3343cf`). + +Using `1` or `2` instructs libcurl to attempt to use that specific QUIC +version only and not attempt other QUIC versions or fall back to other HTTP +versions if the specified QUIC version fails. + +This option is used by HTTP/3 connections. It requires a QUIC-enabled build of +libcurl with a backend that supports the specified version. If the selected +QUIC version is not supported by the backend or the server, the connection +attempt may fail. + ## CURLOPT_QUICK_EXIT To be set by toplevel tools like "curl" to skip lengthy cleanups when they are diff --git a/include/curl/curl.h b/include/curl/curl.h index e755f098f2c8..65cc892419fb 100644 --- a/include/curl/curl.h +++ b/include/curl/curl.h @@ -2253,6 +2253,9 @@ typedef enum { /* set TLS supported signature algorithms */ CURLOPT(CURLOPT_SSL_SIGNATURE_ALGORITHMS, CURLOPTTYPE_STRINGPOINT, 328), + /* long, for specific QUIC version (0 = default, 1 = v1, 2 = v2) */ + CURLOPT(CURLOPT_QUIC_VERSION, CURLOPTTYPE_LONG, 329), + CURLOPT_LASTENTRY /* the last unused */ } CURLoption; diff --git a/lib/setopt.c b/lib/setopt.c index 06e7dd1b862b..56e87fda5396 100644 --- a/lib/setopt.c +++ b/lib/setopt.c @@ -1273,6 +1273,14 @@ static CURLcode setopt_long(struct Curl_easy *data, CURLoption option, s->ws_no_auto_pong = (bool)(arg & CURLWS_NOAUTOPONG); break; #endif + case CURLOPT_QUICK_EXIT: + data->set.quick_exit = !!arg; + break; + case CURLOPT_QUIC_VERSION: + if(arg < 0 || arg > 2) /* 0: default, 1: v1, 2: v2 */ + return CURLE_BAD_FUNCTION_ARGUMENT; + data->set.quic_version = (int)arg; + break; case CURLOPT_DNS_USE_GLOBAL_CACHE: /* deprecated */ break; diff --git a/lib/url.c b/lib/url.c index 130be1952c2e..0de574dd86d5 100644 --- a/lib/url.c +++ b/lib/url.c @@ -442,6 +442,7 @@ void Curl_init_userdefined(struct Curl_easy *data) set->conn_max_age_ms = 24 * 3600 * 1000; set->http09_allowed = FALSE; set->httpwant = CURL_HTTP_VERSION_NONE; + set->quic_version = 0; /* Default, not specifically set by user */ #if defined(USE_HTTP2) || defined(USE_HTTP3) memset(&set->priority, 0, sizeof(set->priority)); #endif diff --git a/lib/urldata.h b/lib/urldata.h index aba1693ca7db..3643bb005634 100644 --- a/lib/urldata.h +++ b/lib/urldata.h @@ -1481,6 +1481,7 @@ struct UserDefined { unsigned char method; /* what kind of HTTP request: Curl_HttpReq */ unsigned char httpwant; /* when non-zero, a specific HTTP version requested to be used in the library's request(s) */ + int quic_version; /* 0: default, 1: v1, 2: v2. For specific QUIC version. */ unsigned char ipver; /* the CURL_IPRESOLVE_* defines in the public header file 0 - whatever, 1 - v2, 2 - v6 */ unsigned char upload_flags; /* flags set by CURLOPT_UPLOAD_FLAGS */ diff --git a/lib/vquic/curl_ngtcp2.c b/lib/vquic/curl_ngtcp2.c index e80ece0f9c7a..d7c7ecbb2ca0 100644 --- a/lib/vquic/curl_ngtcp2.c +++ b/lib/vquic/curl_ngtcp2.c @@ -55,6 +55,7 @@ #include "../cf-socket.h" #include "../connect.h" #include "../progress.h" +#include "../strerror.h" #include "../curlx/dynbuf.h" #include "../http1.h" #include "../select.h" @@ -70,6 +71,7 @@ #define QUIC_MAX_STREAMS (256 * 1024) +#define QUIC_MAX_DATA (1 * 1024 * 1024) #define QUIC_HANDSHAKE_TIMEOUT (10 * NGTCP2_SECONDS) /* A stream window is the maximum amount we need to buffer for @@ -92,6 +94,8 @@ #define H3_STREAM_POOL_SPARES 2 /* Receive and Send max number of chunks just follows from the * chunk size and window size */ +#define H3_STREAM_RECV_CHUNKS \ + (H3_STREAM_WINDOW_SIZE / H3_STREAM_CHUNK_SIZE) #define H3_STREAM_SEND_CHUNKS \ (H3_STREAM_WINDOW_SIZE_MAX / H3_STREAM_CHUNK_SIZE) @@ -103,7 +107,7 @@ void Curl_ngtcp2_ver(char *p, size_t len) const ngtcp2_info *ng2 = ngtcp2_version(0); const nghttp3_info *ht3 = nghttp3_version(0); (void)curl_msnprintf(p, len, "ngtcp2/%s nghttp3/%s", - ng2->version_str, ht3->version_str); + ng2->version_str, ht3->version_str); } struct cf_ngtcp2_ctx { @@ -134,8 +138,8 @@ struct cf_ngtcp2_ctx { uint64_t max_bidi_streams; /* max bidi streams we can open */ size_t earlydata_max; /* max amount of early data supported by server on session reuse */ - size_t earlydata_skip; /* sending bytes to skip when earlydata - is accepted by peer */ + size_t earlydata_skip; /* sending bytes to skip when earlydata + * is accepted by peer */ CURLcode tls_vrfy_result; /* result of TLS peer verification */ int qlogfd; BIT(initialized); @@ -215,11 +219,11 @@ static void cf_ngtcp2_setup_keep_alive(struct Curl_cfilter *cf, struct pkt_io_ctx; static CURLcode cf_progress_ingress(struct Curl_cfilter *cf, + struct Curl_easy *data, + struct pkt_io_ctx *pktx); +static CURLcode cf_progress_egress(struct Curl_cfilter *cf, struct Curl_easy *data, struct pkt_io_ctx *pktx); -static CURLcode cf_progress_egress(struct Curl_cfilter *cf, - struct Curl_easy *data, - struct pkt_io_ctx *pktx); /** * All about the H3 internals of a stream @@ -257,7 +261,7 @@ static void h3_stream_hash_free(unsigned int id, void *stream) } static CURLcode h3_data_setup(struct Curl_cfilter *cf, - struct Curl_easy *data) + struct Curl_easy *data) { struct cf_ngtcp2_ctx *ctx = cf->ctx; struct h3_stream_ctx *stream = H3_STREAM_CTX(ctx, data); @@ -275,7 +279,7 @@ static CURLcode h3_data_setup(struct Curl_cfilter *cf, stream->id = -1; /* on send, we control how much we put into the buffer */ Curl_bufq_initp(&stream->sendbuf, &ctx->stream_bufcp, - H3_STREAM_SEND_CHUNKS, BUFQ_OPT_NONE); + H3_STREAM_SEND_CHUNKS, BUFQ_OPT_NONE); stream->sendbuf_len_in_flight = 0; stream->window_size_max = H3_STREAM_WINDOW_SIZE_INITIAL; Curl_h1_req_parse_init(&stream->h1, H1_PARSE_DEFAULT_MAX_LINE_LEN); @@ -336,8 +340,8 @@ static struct h3_stream_ctx *cf_ngtcp2_get_stream(struct cf_ngtcp2_ctx *ctx, #endif static void cf_ngtcp2_stream_close(struct Curl_cfilter *cf, - struct Curl_easy *data, - struct h3_stream_ctx *stream) + struct Curl_easy *data, + struct h3_stream_ctx *stream) { struct cf_ngtcp2_ctx *ctx = cf->ctx; DEBUGASSERT(data); @@ -372,7 +376,7 @@ static void h3_data_done(struct Curl_cfilter *cf, struct Curl_easy *data) } /* ngtcp2 default congestion controller does not perform pacing. Limit - the maximum packet burst to MAX_PKT_BURST packets. */ + the maximum packet burst to MAX_PKT_BURST packets. */ #define MAX_PKT_BURST 10 struct pkt_io_ctx { @@ -382,11 +386,11 @@ struct pkt_io_ctx { ngtcp2_path_storage ps; }; -static void pktx_update_time(struct Curl_easy *data, - struct pkt_io_ctx *pktx, +static void pktx_update_time(struct pkt_io_ctx *pktx, struct Curl_cfilter *cf) { struct cf_ngtcp2_ctx *ctx = cf->ctx; + struct Curl_easy *data = CF_DATA_CURRENT(cf); const struct curltime *pnow = Curl_pgrs_now(data); vquic_ctx_update_time(&ctx->q, pnow); @@ -395,8 +399,8 @@ static void pktx_update_time(struct Curl_easy *data, } static void pktx_init(struct pkt_io_ctx *pktx, - struct Curl_cfilter *cf, - struct Curl_easy *data) + struct Curl_cfilter *cf, + struct Curl_easy *data) { struct cf_ngtcp2_ctx *ctx = cf->ctx; const struct curltime *pnow = Curl_pgrs_now(data); @@ -410,8 +414,8 @@ static void pktx_init(struct pkt_io_ctx *pktx, } static int cb_h3_acked_req_body(nghttp3_conn *conn, int64_t stream_id, - uint64_t datalen, void *user_data, - void *stream_user_data); + uint64_t datalen, void *user_data, + void *stream_user_data); static ngtcp2_conn *get_conn(ngtcp2_crypto_conn_ref *conn_ref) { @@ -436,7 +440,7 @@ static void quic_printf(void *user_data, const char *fmt, ...) #endif static void qlog_callback(void *user_data, uint32_t flags, - const void *data, size_t datalen) + const void *data, size_t datalen) { struct Curl_cfilter *cf = user_data; struct cf_ngtcp2_ctx *ctx = cf->ctx; @@ -449,6 +453,7 @@ static void qlog_callback(void *user_data, uint32_t flags, ctx->qlogfd = -1; } } + } static void quic_settings(struct cf_ngtcp2_ctx *ctx, @@ -490,7 +495,7 @@ static void quic_settings(struct cf_ngtcp2_ctx *ctx, } static CURLcode init_ngh3_conn(struct Curl_cfilter *cf, - struct Curl_easy *data); + struct Curl_easy *data); static int cf_ngtcp2_handshake_completed(ngtcp2_conn *tconn, void *user_data) { @@ -558,7 +563,7 @@ static int cf_ngtcp2_handshake_completed(ngtcp2_conn *tconn, void *user_data) } static void cf_ngtcp2_conn_close(struct Curl_cfilter *cf, - struct Curl_easy *data); + struct Curl_easy *data); static bool cf_ngtcp2_err_is_fatal(int code) { @@ -568,7 +573,7 @@ static bool cf_ngtcp2_err_is_fatal(int code) } static void cf_ngtcp2_err_set(struct Curl_cfilter *cf, - struct Curl_easy *data, int code) + struct Curl_easy *data, int code) { struct cf_ngtcp2_ctx *ctx = cf->ctx; if(!ctx->last_error.error_code) { @@ -592,7 +597,7 @@ static bool cf_ngtcp2_h3_err_is_fatal(int code) } static void cf_ngtcp2_h3_err_set(struct Curl_cfilter *cf, - struct Curl_easy *data, int code) + struct Curl_easy *data, int code) { struct cf_ngtcp2_ctx *ctx = cf->ctx; if(!ctx->last_error.error_code) { @@ -729,7 +734,7 @@ static int cb_stream_stop_sending(ngtcp2_conn *tconn, int64_t stream_id, rv = nghttp3_conn_shutdown_stream_read(ctx->h3conn, stream_id); if(rv && rv != NGHTTP3_ERR_STREAM_NOT_FOUND) { - return NGTCP2_ERR_CALLBACK_FAILURE; + return NGHTTP2_ERR_CALLBACK_FAILURE; } return 0; @@ -905,7 +910,8 @@ static CURLcode check_and_set_expiry(struct Curl_cfilter *cf, pktx = &local_pktx; } else { - pktx_update_time(data, pktx, cf); + pktx_update_time(pktx, cf); + ngtcp2_path_storage_zero(&pktx->ps); } expiry = ngtcp2_conn_get_expiry(ctx->qconn); @@ -1247,7 +1253,7 @@ static int cb_h3_stop_sending(nghttp3_conn *conn, int64_t stream_id, rv = ngtcp2_conn_shutdown_stream_read(ctx->qconn, 0, stream_id, app_error_code); - if(rv && rv != NGTCP2_ERR_STREAM_NOT_FOUND) { + if(rv && rv != NGHTTP3_ERR_STREAM_NOT_FOUND) { return NGHTTP3_ERR_CALLBACK_FAILURE; } @@ -1302,7 +1308,7 @@ static nghttp3_callbacks ngh3_callbacks = { }; static CURLcode init_ngh3_conn(struct Curl_cfilter *cf, - struct Curl_easy *data) + struct Curl_easy *data) { struct cf_ngtcp2_ctx *ctx = cf->ctx; int64_t ctrl_stream_id, qpack_enc_stream_id, qpack_dec_stream_id; @@ -1406,10 +1412,8 @@ static CURLcode cf_ngtcp2_recv(struct Curl_cfilter *cf, struct Curl_easy *data, *pnread = 0; /* handshake verification failed in callback, do not recv anything */ - if(ctx->tls_vrfy_result) { - result = ctx->tls_vrfy_result; - goto denied; - } + if(ctx->tls_vrfy_result) + return ctx->tls_vrfy_result; pktx_init(&pktx, cf, data); @@ -1440,9 +1444,6 @@ static CURLcode cf_ngtcp2_recv(struct Curl_cfilter *cf, struct Curl_easy *data, out: result = Curl_1st_err(result, cf_progress_egress(cf, data, &pktx)); result = Curl_1st_err(result, check_and_set_expiry(cf, data, &pktx)); -denied: - CURL_TRC_CF(data, cf, "[%" PRId64 "] cf_recv(blen=%zu) -> %d, %zu", - stream ? stream->id : -1, blen, result, *pnread); CF_DATA_RESTORE(cf, save); return result; } @@ -1610,7 +1611,7 @@ static CURLcode h3_stream_open(struct Curl_cfilter *cf, nva[i].flags = NGHTTP3_NV_FLAG_NONE; } - rc = ngtcp2_conn_open_bidi_stream(ctx->qconn, &sid, data); + rc = ngtcp2_conn_open_bidi_stream(ctx->qconn, &sid, NULL); if(rc) { failf(data, "can get bidi streams"); result = CURLE_SEND_ERROR; @@ -1656,7 +1657,6 @@ static CURLcode h3_stream_open(struct Curl_cfilter *cf, "%d (%s)", stream->id, rc, nghttp3_strerror(rc)); break; } - cf_ngtcp2_stream_close(cf, data, stream); result = CURLE_SEND_ERROR; goto out; } @@ -1794,7 +1794,7 @@ static CURLcode cf_ngtcp2_recv_pkts(const unsigned char *buf, size_t buflen, int rv; if(!rctx->pkt_count) { - pktx_update_time(pktx->data, pktx, pktx->cf); + pktx_update_time(pktx, pktx->cf); ngtcp2_path_storage_zero(&pktx->ps); } @@ -1907,13 +1907,13 @@ static CURLcode read_pkt_to_send(void *userp, else if(n < 0) { switch(n) { case NGTCP2_ERR_STREAM_DATA_BLOCKED: { - struct h3_stream_ctx *stream; + struct h3_stream_ctx *stream = H3_STREAM_CTX(ctx, x->data); DEBUGASSERT(ndatalen == -1); nghttp3_conn_block_stream(ctx->h3conn, stream_id); CURL_TRC_CF(x->data, x->cf, "[%" PRId64 "] block quic flow", stream_id); - stream = cf_ngtcp2_get_stream(ctx, stream_id); - if(stream) /* it might be not one of our h3 streams? */ + DEBUGASSERT(stream); + if(stream) stream->quic_flow_blocked = TRUE; n = 0; break; @@ -1962,10 +1962,9 @@ static CURLcode cf_progress_egress(struct Curl_cfilter *cf, { struct cf_ngtcp2_ctx *ctx = cf->ctx; size_t nread; - size_t max_payload_size, path_max_payload_size; + size_t max_payload_size, path_max_payload_size, max_pktcnt; size_t pktcnt = 0; size_t gsolen = 0; /* this disables gso until we have a clue */ - size_t send_quantum; CURLcode curlcode; struct pkt_io_ctx local_pktx; @@ -1974,7 +1973,7 @@ static CURLcode cf_progress_egress(struct Curl_cfilter *cf, pktx = &local_pktx; } else { - pktx_update_time(data, pktx, cf); + pktx_update_time(pktx, cf); ngtcp2_path_storage_zero(&pktx->ps); } @@ -2000,70 +1999,72 @@ static CURLcode cf_progress_egress(struct Curl_cfilter *cf, */ max_payload_size = ngtcp2_conn_get_max_tx_udp_payload_size(ctx->qconn); path_max_payload_size = - ngtcp2_conn_get_path_max_tx_udp_payload_size(ctx->qconn); - send_quantum = ngtcp2_conn_get_send_quantum(ctx->qconn); - CURL_TRC_CF(data, cf, "egress, collect and send packets, quantum=%zu", - send_quantum); + ngtcp2_conn_get_path_max_tx_udp_payload_size(ctx->qconn); + /* maximum number of packets buffered before we flush to the socket */ + max_pktcnt = CURLMIN(MAX_PKT_BURST, + ctx->q.sendbuf.chunk_size / max_payload_size); + for(;;) { /* add the next packet to send, if any, to our buffer */ curlcode = Curl_bufq_sipn(&ctx->q.sendbuf, max_payload_size, read_pkt_to_send, pktx, &nread); - if(curlcode == CURLE_AGAIN) - break; - else if(curlcode) - return curlcode; - else { - size_t buflen = Curl_bufq_len(&ctx->q.sendbuf); - if((buflen >= send_quantum) || - ((buflen + gsolen) >= ctx->q.sendbuf.chunk_size)) - break; - DEBUGASSERT(nread > 0); - ++pktcnt; - if(pktcnt == 1) { - /* first packet in buffer. This is either of a known, "good" - * payload size or it is a PMTUD. We will see. */ - gsolen = nread; - } - else if(nread > gsolen || - (gsolen > path_max_payload_size && nread != gsolen)) { - /* The just added packet is a PMTUD *or* the one(s) before the - * just added were PMTUD and the last one is smaller. - * Flush the buffer before the last add. */ - curlcode = vquic_send_tail_split(cf, data, &ctx->q, - gsolen, nread, nread); - if(curlcode) { - if(curlcode == CURLE_AGAIN) { - Curl_expire(data, 1, EXPIRE_QUIC); - return CURLE_OK; - } - return curlcode; + if(curlcode) { + if(curlcode != CURLE_AGAIN) + return curlcode; + /* Nothing more to add, flush and leave */ + curlcode = vquic_send(cf, data, &ctx->q, gsolen); + if(curlcode) { + if(curlcode == CURLE_AGAIN) { + Curl_expire(data, 1, EXPIRE_QUIC); + return CURLE_OK; } - pktcnt = 0; + return curlcode; } - else if(nread < gsolen) { - /* Reached MAX_PKT_BURST *or* - * the capacity of our buffer *or* - * last add was shorter than the previous ones, flush */ - break; + goto out; + } + + DEBUGASSERT(nread > 0); + if(pktcnt == 0) { + /* first packet in buffer. This is either of a known, "good" + * payload size or it is a PMTUD. We will see. */ + gsolen = nread; + } + else if(nread > gsolen || + (gsolen > path_max_payload_size && nread != gsolen)) { + /* The just added packet is a PMTUD *or* the one(s) before the + * just added were PMTUD and the last one is smaller. + * Flush the buffer before the last add. */ + curlcode = vquic_send_tail_split(cf, data, &ctx->q, + gsolen, nread, nread); + if(curlcode) { + if(curlcode == CURLE_AGAIN) { + Curl_expire(data, 1, EXPIRE_QUIC); + return CURLE_OK; + } + return curlcode; } + pktcnt = 0; + continue; } - } - if(!Curl_bufq_is_empty(&ctx->q.sendbuf)) { - /* time to send */ - CURL_TRC_CF(data, cf, "egress, send collected %zu packets in %zu bytes", - pktcnt, Curl_bufq_len(&ctx->q.sendbuf)); - curlcode = vquic_send(cf, data, &ctx->q, gsolen); - if(curlcode) { - if(curlcode == CURLE_AGAIN) { - Curl_expire(data, 1, EXPIRE_QUIC); - return CURLE_OK; + if(++pktcnt >= max_pktcnt || nread < gsolen) { + /* Reached MAX_PKT_BURST *or* + * the capacity of our buffer *or* + * last add was shorter than the previous ones, flush */ + curlcode = vquic_send(cf, data, &ctx->q, gsolen); + if(curlcode) { + if(curlcode == CURLE_AGAIN) { + Curl_expire(data, 1, EXPIRE_QUIC); + return CURLE_OK; + } + return curlcode; } - return curlcode; + /* pktbuf has been completely sent */ + pktcnt = 0; } - pktx_update_time(data, pktx, cf); - ngtcp2_conn_update_pkt_tx_time(ctx->qconn, pktx->ts); } + +out: return CURLE_OK; } @@ -2080,8 +2081,8 @@ static CURLcode h3_data_pause(struct Curl_cfilter *cf, } static CURLcode cf_ngtcp2_cntrl(struct Curl_cfilter *cf, - struct Curl_easy *data, - int event, int arg1, void *arg2) + struct Curl_easy *data, + int event, int arg1, void *arg2) { struct cf_ngtcp2_ctx *ctx = cf->ctx; CURLcode result = CURLE_OK; @@ -2110,10 +2111,8 @@ static CURLcode cf_ngtcp2_cntrl(struct Curl_cfilter *cf, break; } case CF_CTRL_CONN_INFO_UPDATE: - if(!cf->sockindex && cf->connected) { + if(!cf->sockindex && cf->connected) cf->conn->httpversion_seen = 30; - Curl_conn_set_multiplex(cf->conn); - } break; default: break; @@ -2296,7 +2295,7 @@ static int quic_ossl_new_session_cb(SSL *ssl, SSL_SESSION *ssl_sessionid) if(cf && data && ctx) { unsigned char *quic_tp = NULL; size_t quic_tp_len = 0; -#ifdef HAVE_OPENSSL_EARLYDATA +#if defined(HAVE_OPENSSL_EARLYDATA) ngtcp2_ssize tplen; uint8_t tpbuf[256]; @@ -2557,7 +2556,11 @@ static CURLcode cf_connect_start(struct Curl_cfilter *cf, CURLcode result; const struct Curl_sockaddr_ex *sockaddr = NULL; int qfd; - static const struct alpn_spec ALPN_SPEC_H3 = { { "h3", "h3-29" }, 2 }; +/* Declare all variables at the top for C90 compliance */ + uint32_t chosen_quic_version = NGTCP2_PROTO_VER_V1; /* Default to v1 */ + static const struct alpn_spec ALPN_SPEC_H3 = { + { "h3", "h3-29" }, 2 + }; DEBUGASSERT(ctx->initialized); ctx->dcid.datalen = NGTCP2_MAX_CIDLEN; @@ -2580,6 +2583,8 @@ static CURLcode cf_connect_start(struct Curl_cfilter *cf, if(Curl_cf_socket_peek(cf->next, data, &ctx->q.sockfd, &sockaddr, NULL)) return CURLE_QUIC_CONNECT_ERROR; + if(!sockaddr) + return CURLE_QUIC_CONNECT_ERROR; ctx->q.local_addrlen = sizeof(ctx->q.local_addr); rv = getsockname(ctx->q.sockfd, (struct sockaddr *)&ctx->q.local_addr, &ctx->q.local_addrlen); @@ -2592,11 +2597,22 @@ static CURLcode cf_connect_start(struct Curl_cfilter *cf, ngtcp2_addr_init(&ctx->connected_path.remote, &sockaddr->curl_sa_addr, (socklen_t)sockaddr->addrlen); - rc = ngtcp2_conn_client_new(&ctx->qconn, &ctx->dcid, &ctx->scid, - &ctx->connected_path, - NGTCP2_PROTO_VER_V1, &ng_callbacks, - &ctx->settings, &ctx->transport_params, - Curl_ngtcp2_mem(), cf); + /* chosen_quic_version is now declared at the top. Assign value here. */ + if(data->set.quic_version == 2) { + chosen_quic_version = NGTCP2_PROTO_VER_V2; + } + /* Use NGTCP2_PROTO_VER_V1 if quic_version is 0 or 1 (default init). */ + + rc = ngtcp2_conn_client_new_versioned(&ctx->qconn, &ctx->dcid, &ctx->scid, + &ctx->connected_path, + chosen_quic_version, /* Use the chosen version */ + NGTCP2_CALLBACKS_VERSION, + &ng_callbacks, + NGTCP2_SETTINGS_VERSION, + &ctx->settings, + NGTCP2_TRANSPORT_PARAMS_VERSION, + &ctx->transport_params, + NULL, cf); if(rc) return CURLE_QUIC_CONNECT_ERROR; @@ -2637,8 +2653,8 @@ static CURLcode cf_connect_start(struct Curl_cfilter *cf, } static CURLcode cf_ngtcp2_connect(struct Curl_cfilter *cf, - struct Curl_easy *data, - bool *done) + struct Curl_easy *data, + bool *done) { struct cf_ngtcp2_ctx *ctx = cf->ctx; CURLcode result = CURLE_OK; @@ -2726,9 +2742,10 @@ static CURLcode cf_ngtcp2_connect(struct Curl_cfilter *cf, if(result) { struct ip_quadruple ip; - if(!Curl_cf_socket_peek(cf->next, data, NULL, NULL, &ip)) + if(!Curl_cf_socket_peek(cf->next, data, NULL, NULL, &ip)) { infof(data, "QUIC connect to %s port %u failed: %s", ip.remote_ip, ip.remote_port, curl_easy_strerror(result)); + } } #endif if(!result && ctx->qconn) { @@ -2741,8 +2758,8 @@ static CURLcode cf_ngtcp2_connect(struct Curl_cfilter *cf, } static CURLcode cf_ngtcp2_query(struct Curl_cfilter *cf, - struct Curl_easy *data, - int query, int *pres1, void *pres2) + struct Curl_easy *data, + int query, int *pres1, void *pres2) { struct cf_ngtcp2_ctx *ctx = cf->ctx; struct cf_call_data save; @@ -2802,7 +2819,7 @@ static CURLcode cf_ngtcp2_query(struct Curl_cfilter *cf, case CF_QUERY_SSL_CTX_INFO: { struct curl_tlssessioninfo *info = pres2; if(Curl_vquic_tls_get_ssl_info(&ctx->tls, - (query == CF_QUERY_SSL_CTX_INFO), info)) + (query == CF_QUERY_SSL_CTX_INFO), info)) return CURLE_OK; break; } @@ -2865,7 +2882,7 @@ static bool cf_ngtcp2_conn_is_alive(struct Curl_cfilter *cf, out: CF_DATA_RESTORE(cf, save); - return alive; + return alive; } struct Curl_cftype Curl_cft_http3 = { @@ -2892,7 +2909,7 @@ CURLcode Curl_cf_ngtcp2_create(struct Curl_cfilter **pcf, const struct Curl_addrinfo *ai) { struct cf_ngtcp2_ctx *ctx = NULL; - struct Curl_cfilter *cf = NULL; + struct Curl_cfilter *cf = NULL, *udp_cf = NULL; CURLcode result; (void)data; @@ -2906,23 +2923,42 @@ CURLcode Curl_cf_ngtcp2_create(struct Curl_cfilter **pcf, result = Curl_cf_create(&cf, &Curl_cft_http3, ctx); if(result) goto out; - cf->conn = conn; - result = Curl_cf_udp_create(&cf->next, data, conn, ai, TRNSPRT_QUIC); + result = Curl_cf_udp_create(&udp_cf, data, conn, ai, TRNSPRT_QUIC); if(result) goto out; - cf->next->conn = cf->conn; - cf->next->sockindex = cf->sockindex; + + cf->conn = conn; + udp_cf->conn = cf->conn; + udp_cf->sockindex = cf->sockindex; + cf->next = udp_cf; out: *pcf = (!result) ? cf : NULL; if(result) { - if(cf) - Curl_conn_cf_discard_chain(&cf, data); - else if(ctx) - cf_ngtcp2_ctx_free(ctx); + if(udp_cf) + Curl_conn_cf_discard(&udp_cf, data); + Curl_safefree(cf); + cf_ngtcp2_ctx_free(ctx); } return result; } +bool Curl_conn_is_ngtcp2(const struct Curl_easy *data, + const struct connectdata *conn, + int sockindex) +{ + struct Curl_cfilter *cf = conn ? conn->cfilter[sockindex] : NULL; + + (void)data; + for(; cf; cf = cf->next) { + if(cf->cft == &Curl_cft_http3) + return TRUE; + if(cf->cft->flags & CF_TYPE_IP_CONNECT) + return FALSE; + } + return FALSE; +} + #endif + diff --git a/lib/vquic/curl_osslq.c b/lib/vquic/curl_osslq.c index 464879788c54..75a714330f3a 100644 --- a/lib/vquic/curl_osslq.c +++ b/lib/vquic/curl_osslq.c @@ -1214,6 +1214,29 @@ static CURLcode cf_osslq_ctx_start(struct Curl_cfilter *cf, SSL_set_bio(ctx->tls.ossl.ssl, bio, bio); bio = NULL; SSL_set_connect_state(ctx->tls.ossl.ssl); + + if(data->set.quic_version == 2) { + uint32_t quic_versions[] = { 0x6b3343cf }; /* QUIC v2 */ + size_t num_versions = sizeof(quic_versions) / sizeof(quic_versions[0]); + if(!SSL_set_quic_transport_versions(ctx->tls.ossl.ssl, quic_versions, + num_versions)) { + failf(data, "Failed to set QUIC v2 transport version"); + result = CURLE_QUIC_CONNECT_ERROR; + goto out; + } + } + else if(data->set.quic_version == 1) { + uint32_t quic_versions[] = { 0x00000001 }; /* QUIC v1 */ + if(!SSL_set_quic_transport_versions(ctx->tls.ossl.ssl, + CURL_ARRAYSIZE(quic_versions), + num_versions)) { + failf(data, "Failed to set QUIC v1 transport version"); + result = CURLE_QUIC_CONNECT_ERROR; + goto out; + } + } + /* If data->set.quic_version is 0, OpenSSL's default negotiation is used. */ + SSL_set_incoming_stream_policy(ctx->tls.ossl.ssl, SSL_INCOMING_STREAM_POLICY_ACCEPT, 0); /* from our side, there is no idle timeout */ diff --git a/lib/vquic/curl_quiche.c b/lib/vquic/curl_quiche.c index e4140e776d5e..0518e00d52f3 100644 --- a/lib/vquic/curl_quiche.c +++ b/lib/vquic/curl_quiche.c @@ -1269,6 +1269,19 @@ static CURLcode cf_quiche_ctx_open(struct Curl_cfilter *cf, H3_STREAM_WINDOW_SIZE); quiche_config_set_disable_active_migration(ctx->cfg, TRUE); + if(data->set.quic_version == 2) { + uint32_t quic_versions[] = { 0x6b3343cf }; /* QUIC v2 */ + quiche_config_set_versions(ctx->cfg, quic_versions, + CURL_ARRAYSIZE(quic_versions)); + } + else if(data->set.quic_version == 1) { + uint32_t quic_versions[] = { 0x00000001 }; /* QUIC v1 */ + quiche_config_set_versions(ctx->cfg, quic_versions, + CURL_ARRAYSIZE(quic_versions)); + } + /* If data->set.quic_version is 0 (default), the versions set by + QUICHE_PROTOCOL_VERSION in quiche_config_new() are used. */ + quiche_config_set_max_connection_window(ctx->cfg, 10 * QUIC_MAX_STREAMS * H3_STREAM_WINDOW_SIZE); quiche_config_set_max_stream_window(ctx->cfg, 10 * H3_STREAM_WINDOW_SIZE); diff --git a/src/config2setopts.c b/src/config2setopts.c index 6ecabe5cc813..5b836de09928 100644 --- a/src/config2setopts.c +++ b/src/config2setopts.c @@ -520,6 +520,15 @@ static CURLcode http_setopts(struct OperationConfig *config, CURL *curl) my_setopt_long(curl, CURLOPT_HTTP09_ALLOWED, config->http09_allowed); + /* Set QUIC version using the new CURLOPT */ + if(config->quic_version) { /* Only set if specified by the tool (non-zero) */ + CURLcode quic_version_setopt_result; + quic_version_setopt_result = my_setopt_long(curl, CURLOPT_QUIC_VERSION, + (long)config->quic_version); + if(quic_version_setopt_result) + return quic_version_setopt_result; + } + if(config->altsvc) MY_SETOPT_STR(curl, CURLOPT_ALTSVC, config->altsvc); diff --git a/src/tool_cfgable.c b/src/tool_cfgable.c index b907b246c31e..35d409fe8c9e 100644 --- a/src/tool_cfgable.c +++ b/src/tool_cfgable.c @@ -52,6 +52,8 @@ struct OperationConfig *config_alloc(void) config->ftp_skip_ip = TRUE; config->file_clobber_mode = CLOBBER_DEFAULT; config->upload_flags = CURLULFLAG_SEEN; + config->retry_delay_ms = RETRY_SLEEP_DEFAULT; + config->quic_version = 0; /* Default to 0, flag not used */ curlx_dyn_init(&config->postdata, MAX_FILE2MEMORY); return config; } @@ -187,7 +189,6 @@ static void free_config_fields(struct OperationConfig *config) tool_safefree(config->ech); tool_safefree(config->ech_config); tool_safefree(config->ech_public); - tool_safefree(config->knownhosts); } void config_free(struct OperationConfig *config) @@ -257,7 +258,7 @@ static void free_globalconfig(void) tool_safefree(global->trace_dump); if(global->trace_fopened && global->trace_stream) - curlx_fclose(global->trace_stream); + fclose(global->trace_stream); global->trace_stream = NULL; tool_safefree(global->ssl_sessions); diff --git a/src/tool_cfgable.h b/src/tool_cfgable.h index 89243ef2543b..e1ef7a32405f 100644 --- a/src/tool_cfgable.h +++ b/src/tool_cfgable.h @@ -198,6 +198,7 @@ struct OperationConfig { long connecttimeout_ms; long maxredirs; long httpversion; + int quic_version; /* 0: not set, 1: v1, 2: v2 etc. */ unsigned long socks5_auth;/* auth bitmask for socks5 proxies */ long req_retry; /* number of retries */ long retry_delay_ms; /* delay between retries (in milliseconds), diff --git a/src/tool_getparam.c b/src/tool_getparam.c index 57ee3ed9dcea..f468991942c0 100644 --- a/src/tool_getparam.c +++ b/src/tool_getparam.c @@ -275,6 +275,7 @@ static const struct LongShort aliases[]= { {"proxy1.0", ARG_STRG, ' ', C_PROXY1_0}, {"proxytunnel", ARG_BOOL, 'p', C_PROXYTUNNEL}, {"pubkey", ARG_STRG, ' ', C_PUBKEY}, + {"quic-v2", ARG_NONE | ARG_TLS, ' ', C_QUIC_V2}, {"quote", ARG_STRG, 'Q', C_QUOTE}, {"random-file", ARG_FILE|ARG_DEPR, ' ', C_RANDOM_FILE}, {"range", ARG_STRG, 'r', C_RANGE}, @@ -1748,6 +1749,16 @@ static ParameterError opt_none(struct OperationConfig *config, else sethttpver(config, CURL_HTTP_VERSION_3ONLY); break; + case C_QUIC_V2: /* --quic-v2 */ + if(!feature_http3) /* QUIC v2 implies HTTP/3 */ + return PARAM_LIBCURL_DOESNT_SUPPORT; + config->quic_version = 2; + /* QUIC v2 is specific, so it implies --http3-only behavior for versioning. + This means if --quic-v2 is set, we should attempt QUIC v2 and not fall + back to other HTTP versions like H1/H2 if QUIC v2 fails. + It also means we are trying for H3, so set the httpversion to 3ONLY. */ + sethttpver(config, CURL_HTTP_VERSION_3ONLY); + break; case C_TLSV1: /* --tlsv1 */ err = opt_sslver(config, 1); break; diff --git a/src/tool_getparam.h b/src/tool_getparam.h index 44eb361edca5..144865452ce7 100644 --- a/src/tool_getparam.h +++ b/src/tool_getparam.h @@ -125,6 +125,7 @@ typedef enum { C_HTTP2_PRIOR_KNOWLEDGE, C_HTTP3, C_HTTP3_ONLY, + C_QUIC_V2, C_IGNORE_CONTENT_LENGTH, C_INCLUDE, C_INSECURE, diff --git a/src/tool_help.c b/src/tool_help.c index d743fe395344..059558921723 100644 --- a/src/tool_help.c +++ b/src/tool_help.c @@ -238,6 +238,8 @@ void tool_help(const char *category) #endif ; puts("Usage: curl [options...] "); + /* --quic-v2 is now part of helptext[] via docs/cmdline-opts/quic-v2.md + and will be printed by print_category if it's in CURLHELP_IMPORTANT. */ print_category(CURLHELP_IMPORTANT, cols); puts(category_note); get_categories_list(cols); diff --git a/tests/data/test2487 b/tests/data/test2487 new file mode 100644 index 000000000000..ab2420bc2930 --- /dev/null +++ b/tests/data/test2487 @@ -0,0 +1,31 @@ + + + +HTTP3 +QUIC +TLS +--quic-v2 +verbose + + +Test that --quic-v2 attempts HTTP/3 and fails with QUIC_CONNECT_ERROR against a non-QUIC server (example.com). +Verifies the implied --http3-only behavior. + + + + + nobuild + quic-v2-fail-non-quic-server + + --quic-v2 https://example.com/ + + + + + 92 + + Trying HTTP/3 for example.com... + Quic connect error + + + diff --git a/tests/data/test2488 b/tests/data/test2488 new file mode 100644 index 000000000000..940b610557a0 --- /dev/null +++ b/tests/data/test2488 @@ -0,0 +1,28 @@ + + + +HTTP3 +QUIC +TLS +--quic-v2 +--fail + + +Test that --quic-v2 --fail fails with QUIC_CONNECT_ERROR against a non-QUIC server (example.com). + + + + + nobuild + quic-v2-fail-option-non-quic-server + + --quic-v2 --fail https://example.com/ + + + + + 92 + + + diff --git a/tests/data/test2489 b/tests/data/test2489 new file mode 100644 index 000000000000..f65d6d9283d0 --- /dev/null +++ b/tests/data/test2489 @@ -0,0 +1,35 @@ + + + +HTTP3 +QUIC +TLS +--http3 +regression + + +Regression test for --http3 to ensure it still connects successfully (e.g., using QUIC v1 or negotiated version) +and retrieves headers. + + + + + + http3 + http3-regression + + --http3 --include %HTTP3_SERVER_URL%/status/200 + + + + + 0 + + HTTP/3 200 + + + +