From 9d3f23be06a6dc61739809deea60d3d53e79deb7 Mon Sep 17 00:00:00 2001 From: CarloTUM Date: Sat, 10 Oct 2026 23:56:49 +0200 Subject: [PATCH 1/3] curl-sys: http3 feature, ngtcp2 and nghttp3 from bundled sources HTTP/3 through libcurl's ngtcp2 backend on OpenSSL's QUIC API, which needs OpenSSL 3.5 or newer. ngtcp2 (v1.25.0) and nghttp3 (v1.18.0) come in as submodules and are compiled with cc like libcurl itself: their version.h is generated from the cmake template, the handful of HAVE_* defines their cmake configure would find is set per platform. No cmake, no bindgen, no new crates. The feature implies ssl and static-curl. With rustls or on Windows it only warns and does nothing, so --all-features keeps building. An OpenSSL older than 3.5 stops the build with a message that points at static-ssl. sendmmsg stays off: libcurl's mmsg path needs _GNU_SOURCE for struct mmsghdr and IOV_MAX, which this build does not define. Also adds CURL_HTTP_VERSION_3ONLY. --- .gitmodules | 6 +++ curl-sys/Cargo.toml | 15 +++++- curl-sys/build.rs | 124 ++++++++++++++++++++++++++++++++++++++++++++ curl-sys/lib.rs | 1 + curl-sys/nghttp3 | 1 + curl-sys/ngtcp2 | 1 + 6 files changed, 147 insertions(+), 1 deletion(-) create mode 160000 curl-sys/nghttp3 create mode 160000 curl-sys/ngtcp2 diff --git a/.gitmodules b/.gitmodules index fba1ad48d4..d9ce04edb4 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,9 @@ [submodule "curl-sys/curl"] path = curl-sys/curl url = https://github.com/curl/curl.git +[submodule "curl-sys/nghttp3"] + path = curl-sys/nghttp3 + url = https://github.com/ngtcp2/nghttp3 +[submodule "curl-sys/ngtcp2"] + path = curl-sys/ngtcp2 + url = https://github.com/ngtcp2/ngtcp2 diff --git a/curl-sys/Cargo.toml b/curl-sys/Cargo.toml index b6663832cb..ff4cf9427f 100644 --- a/curl-sys/Cargo.toml +++ b/curl-sys/Cargo.toml @@ -11,7 +11,19 @@ documentation = "https://docs.rs/curl-sys" readme = "../README.md" categories = ["external-ffi-bindings"] edition = "2018" -exclude = ["curl/docs/", "curl/tests/"] +exclude = [ + "curl/docs/", + "curl/tests/", + "nghttp3/doc/", + "nghttp3/examples/", + "nghttp3/fuzz/", + "nghttp3/tests/", + "ngtcp2/doc/", + "ngtcp2/examples/", + "ngtcp2/fuzz/", + "ngtcp2/tests/", + "ngtcp2/third-party/", +] [badges.maintenance] status = "passively-maintained" @@ -47,6 +59,7 @@ pkg-config = "0.3.3" default = ["ssl"] ssl = ["openssl-sys"] http2 = ["libnghttp2-sys"] +http3 = ["ssl", "static-curl"] mesalink = [] rustls = ["rustls-ffi"] static-curl = [] diff --git a/curl-sys/build.rs b/curl-sys/build.rs index 7d028d4103..f2c1f06ce8 100644 --- a/curl-sys/build.rs +++ b/curl-sys/build.rs @@ -35,6 +35,8 @@ fn write_windows_config(dst: &Path) { fn main() { println!("cargo:rerun-if-changed=curl"); + println!("cargo:rerun-if-changed=nghttp3"); + println!("cargo:rerun-if-changed=ngtcp2"); println!( "cargo:rustc-check-cfg=cfg(\ libcurl_vendored,\ @@ -350,6 +352,10 @@ fn main() { // Configure TLS backend. Since Cargo does not support mutually exclusive // features, make sure we only compile one vtls. + if cfg!(feature = "http3") && (cfg!(feature = "rustls") || windows) { + // not a hard error, so that --all-features still builds + println!("cargo:warning=the http3 feature needs libcurl's OpenSSL backend on a unix target; ignored"); + } if cfg!(feature = "rustls") { cfg.define("USE_RUSTLS", None) .file("curl/lib/vtls/cipher_suite.c") @@ -404,6 +410,9 @@ fn main() { if let Some(path) = env::var_os("DEP_OPENSSL_INCLUDE") { cfg.include(path); } + if cfg!(feature = "http3") { + http3(&mut cfg, &dst, &target); + } } } @@ -641,3 +650,118 @@ fn curl_config_reports_http2() -> bool { true } + +/// HTTP/3 through libcurl's ngtcp2 backend on OpenSSL's QUIC API (3.5+). +/// ngtcp2 and nghttp3 are built from the bundled sources like libcurl is, +/// with `cc` and a generated `version.h` instead of their cmake setups. +fn http3(curl: &mut cc::Build, dst: &Path, target: &str) { + let version = env::var("DEP_OPENSSL_VERSION_NUMBER") + .ok() + .and_then(|v| u64::from_str_radix(&v, 16).ok()) + .unwrap_or(0); + if version < 0x3_05_00_00_0 { + panic!( + "the http3 feature needs OpenSSL 3.5 or newer for its QUIC API \ + (found 0x{version:x}); the static-ssl feature builds one from source" + ); + } + let openssl_include = env::var_os("DEP_OPENSSL_INCLUDE").map(PathBuf::from); + + // nghttp3/version.h and ngtcp2/version.h from their cmake templates + let generated = dst.join("quic-include"); + for lib in ["nghttp3", "ngtcp2"] { + let cmake = fs::read_to_string(format!("{lib}/CMakeLists.txt")).unwrap(); + let version = cmake + .lines() + .find_map(|l| l.trim().strip_prefix(&format!("project({lib} VERSION "))) + .and_then(|l| l.split_whitespace().next()) + .expect("project version in CMakeLists.txt"); + let num = version + .split('.') + .map(|p| p.parse::().unwrap()) + .fold(0, |acc, p| (acc << 8) | p); + let template = + fs::read_to_string(format!("{lib}/lib/includes/{lib}/version.h.in")).unwrap(); + let out = generated.join(lib); + fs::create_dir_all(&out).unwrap(); + fs::write( + out.join("version.h"), + template + .replace("@PACKAGE_VERSION@", version) + .replace("@PACKAGE_VERSION_NUM@", &format!("0x{num:06x}")), + ) + .unwrap(); + } + + let c_files = |dir: &str| { + let mut files: Vec = fs::read_dir(dir) + .unwrap() + .map(|e| e.unwrap().path()) + .filter(|p| p.extension().is_some_and(|e| e == "c")) + .collect(); + files.sort(); + files + }; + // what the cmake configure would have found on these platforms + let platform = |build: &mut cc::Build| { + build + .define("HAVE_ARPA_INET_H", None) + .define("HAVE_NETINET_IN_H", None) + .define("HAVE_UNISTD_H", None) + .warnings(false); + if target.contains("linux") || target.contains("android") { + build + .define("HAVE_ENDIAN_H", None) + .define("HAVE_BYTESWAP_H", None) + .define("HAVE_DECL_BE64TOH", "1") + .define("HAVE_DECL_BSWAP_64", "1"); + } else if target.contains("bsd") { + build + .define("HAVE_SYS_ENDIAN_H", None) + .define("HAVE_DECL_BE64TOH", "1"); + } + }; + + let mut nghttp3 = cc::Build::new(); + platform(&mut nghttp3); + nghttp3 + .include("nghttp3/lib/includes") + .include("nghttp3/lib") + .include(&generated) + .define("NGHTTP3_STATICLIB", None) + .files(c_files("nghttp3/lib")) + .file("nghttp3/lib/sfparse/sfparse.c") + .compile("nghttp3"); + + let mut ngtcp2 = cc::Build::new(); + platform(&mut ngtcp2); + ngtcp2 + .include("ngtcp2/lib/includes") + .include("ngtcp2/lib") + .include("ngtcp2/crypto/includes") + .include("ngtcp2/crypto") + .include(&generated) + .define("NGTCP2_STATICLIB", None) + .files(c_files("ngtcp2/lib")) + .file("ngtcp2/crypto/shared.c") + .file("ngtcp2/crypto/ossl/ossl.c"); + if let Some(path) = &openssl_include { + ngtcp2.include(path); + } + ngtcp2.compile("ngtcp2"); + + curl.define("USE_NGTCP2", None) + .define("USE_NGHTTP3", None) + .define("OPENSSL_QUIC_API2", None) + .define("NGTCP2_STATICLIB", None) + .define("NGHTTP3_STATICLIB", None) + .define("HAVE_SENDMSG", None) + .define("HAVE_NETINET_UDP_H", None) + .include("nghttp3/lib/includes") + .include("ngtcp2/lib/includes") + .include("ngtcp2/crypto/includes") + .include(&generated) + .files(c_files("curl/lib/vquic")); + // no HAVE_SENDMMSG: its struct mmsghdr and IOV_MAX need _GNU_SOURCE, + // which this build does not set; sendmsg is the portable path +} diff --git a/curl-sys/lib.rs b/curl-sys/lib.rs index 5e5b10c285..5733650162 100644 --- a/curl-sys/lib.rs +++ b/curl-sys/lib.rs @@ -677,6 +677,7 @@ pub const CURL_HTTP_VERSION_2_PRIOR_KNOWLEDGE: c_int = 5; /// Use HTTP/3, fallback to HTTP/2 or HTTP/1 if needed. /// (Added in CURL 7.66.0) pub const CURL_HTTP_VERSION_3: c_int = 30; +pub const CURL_HTTP_VERSION_3ONLY: c_int = 31; // Note that the type here is wrong, it's just intended to just be an enum. pub const CURL_SSLVERSION_DEFAULT: __enum_ty = 0; diff --git a/curl-sys/nghttp3 b/curl-sys/nghttp3 new file mode 160000 index 0000000000..dbfc242861 --- /dev/null +++ b/curl-sys/nghttp3 @@ -0,0 +1 @@ +Subproject commit dbfc24286138cb0b6490160e7ca87fe1ce6722a0 diff --git a/curl-sys/ngtcp2 b/curl-sys/ngtcp2 new file mode 160000 index 0000000000..f9e9ff01ad --- /dev/null +++ b/curl-sys/ngtcp2 @@ -0,0 +1 @@ +Subproject commit f9e9ff01ad2c8116bc09de4f644b0028a61486a6 From 6b25f3f7b7838dbccb4434bd0db9100c9eb0b54e Mon Sep 17 00:00:00 2001 From: CarloTUM Date: Sat, 10 Oct 2026 23:56:49 +0200 Subject: [PATCH 2/3] http3 feature, HttpVersion::V3Only, example and CI job HttpVersion::V3Only (CURL_HTTP_VERSION_3ONLY) fails instead of falling back to HTTP/2 or 1.1, which is what makes an HTTP/3 test meaningful. examples/http3.rs fetches cloudflare-quic.com that way; a CI job builds the feature with static-ssl and runs the example. --- .github/workflows/main.yml | 12 ++++++++++++ Cargo.toml | 1 + README.md | 1 + examples/http3.rs | 30 ++++++++++++++++++++++++++++++ src/easy/handler.rs | 4 ++++ 5 files changed, 48 insertions(+) create mode 100644 examples/http3.rs diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 680d471949..ae23380f11 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -98,6 +98,18 @@ jobs: if: matrix.os == 'ubuntu-latest' name: Run docker tests + http3: + name: HTTP/3 (ngtcp2 on OpenSSL) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + - name: Install Rust + run: rustup update stable && rustup default stable + - run: cargo test --no-run --features http3,static-ssl + - run: cargo run --example http3 --features http3,static-ssl + rustfmt: name: Rustfmt runs-on: ubuntu-latest diff --git a/Cargo.toml b/Cargo.toml index fc46a6e66e..2d6fd72b85 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -48,6 +48,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", "ssl", "static-curl"] spnego = ["curl-sys/spnego"] rustls = ["curl-sys/rustls"] static-curl = ["curl-sys/static-curl"] diff --git a/README.md b/README.md index d0911bea9a..5a55e53370 100644 --- a/README.md +++ b/README.md @@ -129,6 +129,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 via libcurl's ngtcp2 backend, built from bundled ngtcp2 and nghttp3 sources on OpenSSL's QUIC API. Implies `ssl` and `static-curl`; needs OpenSSL 3.5 or newer, which `static-ssl` provides where the system's is older. Unix only. Disabled by default. - `static-curl`: Use a bundled libcurl version and statically link to it. Disabled by default. - `apple-sectrust`: Use Apple SecTrust for certificate verification with bundled libcurl and OpenSSL on Apple platforms. Implies `ssl` and `static-curl`. 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. diff --git a/examples/http3.rs b/examples/http3.rs new file mode 100644 index 0000000000..26e494fab4 --- /dev/null +++ b/examples/http3.rs @@ -0,0 +1,30 @@ +//! Fetches a page over HTTP/3 and nothing else: `V3Only` makes libcurl +//! fail instead of falling back to HTTP/2 or HTTP/1.1. +//! +//! Needs the `http3` feature (and `static-ssl` where the system OpenSSL is +//! older than 3.5): +//! +//! cargo run --example http3 --features http3,static-ssl + +use curl::easy::{Easy, HttpVersion}; +use curl::Version; +use std::io::{stdout, Write}; + +fn main() -> Result<(), curl::Error> { + let version = Version::get(); + assert!( + version.feature_http3(), + "this libcurl has no HTTP/3: {}", + version.version() + ); + let mut easy = Easy::new(); + easy.url("https://cloudflare-quic.com/")?; + easy.http_version(HttpVersion::V3Only)?; + easy.write_function(|data| { + stdout().write_all(data).unwrap(); + Ok(data.len()) + })?; + easy.perform()?; + eprintln!("\n{} over HTTP/3", easy.response_code()?); + Ok(()) +} diff --git a/src/easy/handler.rs b/src/easy/handler.rs index d9aa7b6ff5..af7436dfcb 100644 --- a/src/easy/handler.rs +++ b/src/easy/handler.rs @@ -462,6 +462,10 @@ pub enum HttpVersion { /// /// (Added in CURL 7.66.0) V3 = curl_sys::CURL_HTTP_VERSION_3 as isize, + + /// Use HTTP/3 and fail if that is not possible, no fallback to earlier + /// versions. (Added in CURL 7.88.0) + V3Only = curl_sys::CURL_HTTP_VERSION_3ONLY as isize, } /// Possible values to pass to the `ssl_version` and `ssl_min_max_version` method. From 76655cf68e78d5aa390e921ed7caebe69d9da81a Mon Sep 17 00:00:00 2001 From: CarloTUM Date: Sun, 11 Oct 2026 00:00:01 +0200 Subject: [PATCH 3/3] Update workflow covers ngtcp2 and nghttp3 too The weekly job that bumps the curl submodule to the latest release now runs as a matrix over curl, ngtcp2 and nghttp3, one pull request each, so the http3 feature does not add a library that nobody updates. --- .github/workflows/update-curl.yml | 41 ++++++++++++++++++++++--------- 1 file changed, 30 insertions(+), 11 deletions(-) diff --git a/.github/workflows/update-curl.yml b/.github/workflows/update-curl.yml index 0a113a8f94..4eb706f13b 100644 --- a/.github/workflows/update-curl.yml +++ b/.github/workflows/update-curl.yml @@ -1,7 +1,8 @@ -# This workflow automates some of the process of updating curl-sys to the latest -# version of curl upstream. +# This workflow automates some of the process of updating the bundled +# libraries (curl, and ngtcp2 and nghttp3 for the http3 feature) to their +# latest upstream releases. One pull request per library. -name: Update curl +name: Update bundled libraries on: schedule: @@ -14,6 +15,22 @@ jobs: permissions: contents: write pull-requests: write + strategy: + fail-fast: false + matrix: + include: + - name: curl + repo: curl/curl + path: curl-sys/curl + changelog: https://curl.se/ch/TAG.html + - name: ngtcp2 + repo: ngtcp2/ngtcp2 + path: curl-sys/ngtcp2 + changelog: https://github.com/ngtcp2/ngtcp2/releases/tag/TAG + - name: nghttp3 + repo: ngtcp2/nghttp3 + path: curl-sys/nghttp3 + changelog: https://github.com/ngtcp2/nghttp3/releases/tag/TAG steps: - uses: actions/checkout@v7 with: @@ -24,25 +41,27 @@ jobs: env: GH_TOKEN: ${{ secrets.GH_PAT }} run: | - LATEST_TAG=$(gh release view --repo curl/curl --json tagName --jq .tagName) - cd curl-sys/curl + LATEST_TAG=$(gh release view --repo ${{ matrix.repo }} --json tagName --jq .tagName) + cd ${{ matrix.path }} git fetch --tags git checkout $LATEST_TAG echo "tag=$LATEST_TAG" >> $GITHUB_OUTPUT + echo "changelog=${{ matrix.changelog }}" | sed "s/TAG/$LATEST_TAG/" >> $GITHUB_OUTPUT - name: Create pull request uses: peter-evans/create-pull-request@v7 with: token: ${{ secrets.GH_PAT }} - branch: update-curl-to-${{ steps.update.outputs.tag }} - commit-message: "chore: update curl to ${{ steps.update.outputs.tag }}" - title: "Update curl to ${{ steps.update.outputs.tag }}" + branch: update-${{ matrix.name }}-to-${{ steps.update.outputs.tag }} + commit-message: "chore: update ${{ matrix.name }} to ${{ steps.update.outputs.tag }}" + title: "Update ${{ matrix.name }} to ${{ steps.update.outputs.tag }}" body: | - Update the bundled curl submodule to version ${{ steps.update.outputs.tag }}. + Update the bundled ${{ matrix.name }} submodule to version ${{ steps.update.outputs.tag }}. - Changelog: + Changelog: <${{ steps.update.outputs.changelog }}> This PR was created in an automated fashion. Additional changes may - be required to get the new version of curl to work with our + be required to get the new version of ${{ matrix.name }} to work with our `build.rs` script. + # one label for all bundled libraries; it already exists in the repo labels: curl-update