From 834efa7dfd3a6a747c394789120fae9fe8eae938 Mon Sep 17 00:00:00 2001 From: Cupertino Date: Sat, 15 Aug 2026 17:22:54 -0300 Subject: [PATCH 01/10] feat: implement http3 feature with curl's ngtpc2 back-end --- Cargo.toml | 1 + curl-sys/Cargo.toml | 4 ++++ curl-sys/build.rs | 54 +++++++++++++++++++++++++++++++++++++-------- curl-sys/lib.rs | 6 +++++ 4 files changed, 56 insertions(+), 9 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 1dda47c5a..10df7b291 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -44,6 +44,7 @@ default = ["ssl"] ssl = ["openssl-sys", "openssl-probe", "curl-sys/ssl"] # OpenSSL/system TLS backend mesalink = ["curl-sys/mesalink"] # MesaLink TLS backend http2 = ["curl-sys/http2"] +http3 = ["curl-sys/http3"] spnego = ["curl-sys/spnego"] rustls = ["curl-sys/rustls"] static-curl = ["curl-sys/static-curl"] diff --git a/curl-sys/Cargo.toml b/curl-sys/Cargo.toml index 1dc2e4f80..d5a59e655 100644 --- a/curl-sys/Cargo.toml +++ b/curl-sys/Cargo.toml @@ -20,6 +20,9 @@ path = "lib.rs" libz-sys = { version = "1.0.18", default-features = false, features = ["libc"] } libc = "0.2.2" libnghttp2-sys = { optional = true, version = "0.1.3" } +nghttp3-sys = { optional = true, version = "0.2" } +ngtcp2-sys = { optional = true, version = "0.2" } +aws-lc-sys = { optional = true, version = "0.43", features = ["ssl"] } [dependencies.rustls-ffi] version = "0.15" @@ -43,6 +46,7 @@ pkg-config = "0.3.3" default = ["ssl"] ssl = ["openssl-sys"] http2 = ["libnghttp2-sys"] +http3 = ["nghttp3-sys", "ngtcp2-sys", "aws-lc-sys"] mesalink = [] rustls = ["rustls-ffi"] static-curl = [] diff --git a/curl-sys/build.rs b/curl-sys/build.rs index 5ae0ed8e7..0298ceb96 100644 --- a/curl-sys/build.rs +++ b/curl-sys/build.rs @@ -8,7 +8,10 @@ fn main() { println!( "cargo:rustc-check-cfg=cfg(\ libcurl_vendored,\ + link_aws_lc,\ link_libnghttp2,\ + link_libnghttp3,\ + link_libngtcp2,\ link_libz,\ link_openssl,\ )" @@ -31,7 +34,10 @@ fn main() { if !cfg!(feature = "static-curl") { // OSX ships libcurl by default, so we just use that version // so long as it has the right features enabled. - if target.contains("apple") && (!cfg!(feature = "http2") || curl_config_reports_http2()) { + if target.contains("apple") + && (!cfg!(feature = "http2") || curl_config_reports("HTTP2", "http-2")) + && (!cfg!(feature = "http3") || curl_config_reports("HTTP3", "http-3")) + { return println!("cargo:rustc-flags=-l curl"); } @@ -300,6 +306,22 @@ fn main() { } } + if cfg!(feature = "http3") { + cfg.define("USE_NGTCP2", None) + .define("USE_NGHTTP3", None) + .define("NGTCP2_STATICLIB", None) + .define("NGHTTP3_STATICLIB", None) + .file("curl/lib/vquic/cf-ngtcp2.c") + .file("curl/lib/vquic/cf-ngtcp2-cmn.c"); + + println!("cargo:rustc-cfg=link_aws_lc"); + println!("cargo:rustc-cfg=link_libnghttp3"); + println!("cargo:rustc-cfg=link_libngtcp2"); + cfg.include(env::var_os("DEP_NGTCP2_INCLUDE").unwrap()) + .include(env::var_os("DEP_NGHTTP3_INCLUDE").unwrap()) + .include(aws_lc_include().unwrap()); + } + println!("cargo:rustc-cfg=link_libz"); if let Some(path) = env::var_os("DEP_Z_INCLUDE") { cfg.include(path); @@ -313,7 +335,11 @@ fn main() { // Configure TLS backend. Since Cargo does not support mutually exclusive // features, make sure we only compile one vtls. - if cfg!(feature = "rustls") { + if cfg!(feature = "http3") { + cfg.define("USE_OPENSSL", None) + .define("OPENSSL_IS_AWSLC", None) + .file("curl/lib/vtls/openssl.c"); + } else if cfg!(feature = "rustls") { cfg.define("USE_RUSTLS", None) .file("curl/lib/vtls/cipher_suite.c") .file("curl/lib/vtls/rustls.c") @@ -558,9 +584,11 @@ fn try_pkg_config() -> bool { } }; - // Not all system builds of libcurl have http2 features enabled, so if we've - // got a http2-requested build then we may fall back to a build from source. - if cfg!(feature = "http2") && !curl_config_reports_http2() { + // Not all system builds of libcurl have optional HTTP features enabled, so + // fall back to a source build when a requested feature is missing. + if (cfg!(feature = "http2") && !curl_config_reports("HTTP2", "http-2")) + || (cfg!(feature = "http3") && !curl_config_reports("HTTP3", "http-3")) + { return false; } @@ -573,7 +601,7 @@ fn try_pkg_config() -> bool { true } -fn curl_config_reports_http2() -> bool { +fn curl_config_reports(feature: &str, description: &str) -> bool { let output = Command::new("curl-config").arg("--features").output(); let output = match output { Ok(out) => out, @@ -587,13 +615,21 @@ fn curl_config_reports_http2() -> bool { return false; } let stdout = String::from_utf8_lossy(&output.stdout); - if !stdout.contains("HTTP2") { + if !stdout.split_whitespace().any(|item| item == feature) { println!( - "failed to find http-2 feature enabled in pkg-config-found \ - libcurl, building from source" + "failed to find {} feature enabled in pkg-config-found \ + libcurl, building from source", + description ); return false; } true } + +fn aws_lc_include() -> Option { + env::vars_os().find_map(|(key, value)| { + let key = key.to_string_lossy(); + (key.starts_with("DEP_AWS_LC_") && key.ends_with("_INCLUDE")).then_some(value) + }) +} diff --git a/curl-sys/lib.rs b/curl-sys/lib.rs index f6d4457d4..8a3131a9b 100644 --- a/curl-sys/lib.rs +++ b/curl-sys/lib.rs @@ -2,10 +2,16 @@ #![doc(html_root_url = "https://docs.rs/curl-sys/0.4")] // These `extern crate` are required for conditional linkages of curl. +#[cfg(link_aws_lc)] +extern crate aws_lc_sys; #[cfg(link_libnghttp2)] extern crate libnghttp2_sys; #[cfg(link_libz)] extern crate libz_sys; +#[cfg(link_libnghttp3)] +extern crate nghttp3_sys; +#[cfg(link_libngtcp2)] +extern crate ngtcp2_sys; #[cfg(link_openssl)] extern crate openssl_sys; #[cfg(feature = "rustls")] From 5a3a2306095591df2385fc0421613c64bf0e357f Mon Sep 17 00:00:00 2001 From: Cupertino Date: Sat, 15 Aug 2026 17:23:21 -0300 Subject: [PATCH 02/10] docs: list http3 feature in README --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 6debe8979..f4ad5a0bb 100644 --- a/README.md +++ b/README.md @@ -127,6 +127,7 @@ with various Cargo features: Note that Rustls support is experimental within Curl itself and may have significant bugs, so we don't offer any sort of stability guarantee with this feature. - `http2`: Enable HTTP/2 support via libnghttp2. Disabled by default. +- `http3`: Enable HTTP/3 support via ngtcp2 and nghttp3 using AWS-LC as the TLS backend. This TLS backend takes precedence over the `ssl` and `rustls` features. Disabled by default. - `static-curl`: Use a bundled libcurl version and statically link to it. Disabled by default. - `static-ssl`: Use a bundled OpenSSL version and statically link to it. Only applies on platforms that use OpenSSL. Disabled by default. - `spnego`: Enable SPNEGO support. Disabled by default. From 2ee0b2e91ed16fe41ed0ed8357f91c7ec16189a8 Mon Sep 17 00:00:00 2001 From: Cupertino Date: Sat, 15 Aug 2026 17:26:18 -0300 Subject: [PATCH 03/10] test: add protocol tests for http3 feature --- tests/protocols.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/protocols.rs b/tests/protocols.rs index 7215fe972..d3273cb69 100644 --- a/tests/protocols.rs +++ b/tests/protocols.rs @@ -17,3 +17,11 @@ fn static_with_ftp_enabled() { .next() .is_some()); } + +#[cfg(feature = "http3")] +#[test] +fn with_http3_enabled() { + let version = curl::Version::get(); + assert!(version.feature_http3()); + assert!(version.quic_version().is_some()); +} From 6e87704bce76dfe6eeb5e49d812c70e89f565085 Mon Sep 17 00:00:00 2001 From: Tam Nguyen Date: Wed, 7 Oct 2026 10:12:12 +1100 Subject: [PATCH 04/10] fix: preserve Apple SecTrust with HTTP/3 --- curl-sys/build.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/curl-sys/build.rs b/curl-sys/build.rs index 5a4d8d0d9..187e9cb5b 100644 --- a/curl-sys/build.rs +++ b/curl-sys/build.rs @@ -376,6 +376,11 @@ fn main() { cfg.define("USE_OPENSSL", None) .define("OPENSSL_IS_AWSLC", None) .file("curl/lib/vtls/openssl.c"); + + if target.contains("-apple-") && cfg!(feature = "apple-sectrust") { + cfg.define("USE_APPLE_SECTRUST", None) + .file("curl/lib/vtls/apple.c"); + } } else if cfg!(feature = "rustls") { cfg.define("USE_RUSTLS", None) .file("curl/lib/vtls/cipher_suite.c") From c613736d9849312218828fef3a27d22f0708ecef Mon Sep 17 00:00:00 2001 From: Tam Nguyen Date: Wed, 7 Oct 2026 10:12:24 +1100 Subject: [PATCH 05/10] test: exercise HTTP/3 feature in CI --- ci/run.sh | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/ci/run.sh b/ci/run.sh index 0abfb5fd6..b308832c0 100755 --- a/ci/run.sh +++ b/ci/run.sh @@ -32,6 +32,12 @@ if [ "$TARGET" != "x86_64-pc-windows-gnu" ] && [ "$TARGET" != "i686-pc-windows-m then cargo test --target $TARGET --no-run --features rustls,static-curl $features fi +# HTTP/3 also uses AWS-LC. Skip the same targets that cannot currently build +# that dependency in this CI environment. +if [ "$TARGET" != "x86_64-pc-windows-gnu" ] && [ "$TARGET" != "i686-pc-windows-msvc" ] +then + cargo test --target $TARGET --no-run --features http3,static-curl $features +fi # Then with all extra protocols enabled. cargo test --target $TARGET --no-run --features static-curl,protocol-ftp,ntlm $features if [ -z "$NO_RUN" ]; then @@ -39,9 +45,15 @@ if [ -z "$NO_RUN" ]; then cargo test --target $TARGET --features static-curl $features cargo test --target $TARGET --features static-curl,protocol-ftp $features cargo test --target $TARGET --features static-curl,http2 $features + if [ "$TARGET" != "i686-pc-windows-msvc" ]; then + cargo test --target $TARGET --features static-curl,http3 $features + fi case "$TARGET" in - *-apple-*) cargo test --target $TARGET --features apple-sectrust $features ;; + *-apple-*) + cargo test --target $TARGET --features apple-sectrust $features + cargo test --target $TARGET --features apple-sectrust,http3 $features + ;; esac # Note that `-Clink-dead-code` is passed here to suppress `--gc-sections` to From 69392561b6e6fc9d4bf7823416d3d31165528e5d Mon Sep 17 00:00:00 2001 From: Tam Nguyen Date: Wed, 7 Oct 2026 10:12:34 +1100 Subject: [PATCH 06/10] fix: use native Windows CA store for HTTP/3 --- curl-sys/build.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/curl-sys/build.rs b/curl-sys/build.rs index 187e9cb5b..5d9e6beed 100644 --- a/curl-sys/build.rs +++ b/curl-sys/build.rs @@ -381,6 +381,11 @@ fn main() { cfg.define("USE_APPLE_SECTRUST", None) .file("curl/lib/vtls/apple.c"); } + + if windows { + cfg.define("CURL_CA_NATIVE", None) + .define("CURL_DISABLE_CA_SEARCH", None); + } } else if cfg!(feature = "rustls") { cfg.define("USE_RUSTLS", None) .file("curl/lib/vtls/cipher_suite.c") From 087c6540652d95b55b0769fe5a28c1037edd7bc6 Mon Sep 17 00:00:00 2001 From: Tam Nguyen Date: Wed, 7 Oct 2026 10:12:50 +1100 Subject: [PATCH 07/10] fix: guarantee HTTP/3 when probing vcpkg --- curl-sys/build.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/curl-sys/build.rs b/curl-sys/build.rs index 5d9e6beed..b10522a4e 100644 --- a/curl-sys/build.rs +++ b/curl-sys/build.rs @@ -73,7 +73,10 @@ fn main() { // Next, fall back and try to use pkg-config if its available. if windows { - if try_vcpkg() { + // vcpkg does not expose enough information to verify whether the + // discovered libcurl was built with HTTP/3 support. Build the + // vendored HTTP/3 stack instead so the feature is guaranteed. + if !cfg!(feature = "http3") && try_vcpkg() { return; } } else if try_pkg_config() { From b1342967e9e30d91f8eb8a64486cc91a75e0be60 Mon Sep 17 00:00:00 2001 From: Tam Nguyen Date: Wed, 7 Oct 2026 10:21:21 +1100 Subject: [PATCH 08/10] fix: include HTTP/3 capsule sources --- curl-sys/build.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/curl-sys/build.rs b/curl-sys/build.rs index b10522a4e..2a6ed010d 100644 --- a/curl-sys/build.rs +++ b/curl-sys/build.rs @@ -351,6 +351,8 @@ fn main() { .define("USE_NGHTTP3", None) .define("NGTCP2_STATICLIB", None) .define("NGHTTP3_STATICLIB", None) + .file("curl/lib/vquic/capsule.c") + .file("curl/lib/vquic/cf-capsule.c") .file("curl/lib/vquic/cf-ngtcp2.c") .file("curl/lib/vquic/cf-ngtcp2-cmn.c"); From 10f1d202978d1f60e31e38e83404ba061ec95d05 Mon Sep 17 00:00:00 2001 From: Tam Nguyen Date: Wed, 7 Oct 2026 11:15:29 +1100 Subject: [PATCH 09/10] fix: target HTTP/3 CI coverage to supported jobs --- .github/workflows/main.yml | 6 +++++- ci/Dockerfile-linux64 | 2 +- ci/run.sh | 13 +++++++------ 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 680d47194..157119919 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -19,6 +19,7 @@ jobs: rust: stable docker: linux64 target: x86_64-unknown-linux-gnu + http3: '1' - build: ubuntu-lts os: ubuntu-24.04 rust: '1.85' @@ -59,6 +60,7 @@ jobs: os: macos-latest rust: stable target: aarch64-apple-darwin + http3: '1' - build: win32 os: windows-latest rust: stable @@ -67,6 +69,7 @@ jobs: os: windows-latest rust: stable target: x86_64-pc-windows-msvc + http3: '1' steps: - uses: actions/checkout@v1 with: @@ -75,7 +78,7 @@ jobs: run: rustup update ${{ matrix.rust }} --no-self-update && rustup default ${{ matrix.rust }} shell: bash - run: rustup target add ${{ matrix.target }} - - run: TARGET=${{ matrix.target }} ./ci/run.sh + - run: HTTP3_TESTS=${{ matrix.http3 }} TARGET=${{ matrix.target }} ./ci/run.sh if: matrix.os != 'ubuntu-latest' name: Run non-docker tests shell: bash @@ -91,6 +94,7 @@ jobs: -v `pwd`/ci/.cargo:/src/.cargo:ro \ -v `rustc --print sysroot`:/usr/local:ro \ -e TARGET=${{ matrix.target }} \ + -e HTTP3_TESTS=${{ matrix.http3 }} \ -e NO_RUN=${{ matrix.no_run }} \ -e CARGO_TARGET_DIR=/src/target \ rust \ diff --git a/ci/Dockerfile-linux64 b/ci/Dockerfile-linux64 index 29c6d53af..1930cc67a 100644 --- a/ci/Dockerfile-linux64 +++ b/ci/Dockerfile-linux64 @@ -2,6 +2,6 @@ FROM ubuntu:24.04 RUN apt-get update RUN apt-get install -y --no-install-recommends \ - gcc ca-certificates make libc6-dev \ + gcc g++ cmake ca-certificates make libc6-dev \ libssl-dev \ pkg-config diff --git a/ci/run.sh b/ci/run.sh index b308832c0..4c972389e 100755 --- a/ci/run.sh +++ b/ci/run.sh @@ -32,10 +32,9 @@ if [ "$TARGET" != "x86_64-pc-windows-gnu" ] && [ "$TARGET" != "i686-pc-windows-m then cargo test --target $TARGET --no-run --features rustls,static-curl $features fi -# HTTP/3 also uses AWS-LC. Skip the same targets that cannot currently build -# that dependency in this CI environment. -if [ "$TARGET" != "x86_64-pc-windows-gnu" ] && [ "$TARGET" != "i686-pc-windows-msvc" ] -then +# HTTP/3 has additional native build dependencies and a higher dependency MSRV. +# Exercise it only in CI jobs that explicitly opt in to HTTP/3 coverage. +if [ "$HTTP3_TESTS" = "1" ]; then cargo test --target $TARGET --no-run --features http3,static-curl $features fi # Then with all extra protocols enabled. @@ -45,14 +44,16 @@ if [ -z "$NO_RUN" ]; then cargo test --target $TARGET --features static-curl $features cargo test --target $TARGET --features static-curl,protocol-ftp $features cargo test --target $TARGET --features static-curl,http2 $features - if [ "$TARGET" != "i686-pc-windows-msvc" ]; then + if [ "$HTTP3_TESTS" = "1" ]; then cargo test --target $TARGET --features static-curl,http3 $features fi case "$TARGET" in *-apple-*) cargo test --target $TARGET --features apple-sectrust $features - cargo test --target $TARGET --features apple-sectrust,http3 $features + if [ "$HTTP3_TESTS" = "1" ]; then + cargo test --target $TARGET --features apple-sectrust,http3 $features + fi ;; esac From 93366731d3e48eacc8f819a36ec70b688626359c Mon Sep 17 00:00:00 2001 From: Tam Nguyen Date: Wed, 7 Oct 2026 11:34:46 +1100 Subject: [PATCH 10/10] fix: install libclang for HTTP/3 Linux CI --- ci/Dockerfile-linux64 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ci/Dockerfile-linux64 b/ci/Dockerfile-linux64 index 1930cc67a..333d20a5e 100644 --- a/ci/Dockerfile-linux64 +++ b/ci/Dockerfile-linux64 @@ -2,6 +2,6 @@ FROM ubuntu:24.04 RUN apt-get update RUN apt-get install -y --no-install-recommends \ - gcc g++ cmake ca-certificates make libc6-dev \ + gcc g++ cmake libclang-dev ca-certificates make libc6-dev \ libssl-dev \ pkg-config