diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 3e44060..e39c88b 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -19,6 +19,16 @@ updates: schedule: interval: "daily" + - package-ecosystem: "cargo" + directory: "/rpxy-certs" + schedule: + interval: "daily" + + - package-ecosystem: "cargo" + directory: "/rpxy-acme" + schedule: + interval: "daily" + # Enable version updates for Docker - package-ecosystem: "docker" directory: "/docker" diff --git a/.github/workflows/release_docker.yml b/.github/workflows/release_docker.yml index 6accef5..9ebc053 100644 --- a/.github/workflows/release_docker.yml +++ b/.github/workflows/release_docker.yml @@ -16,7 +16,7 @@ env: jobs: build_and_push: - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 if: ${{ github.event_name == 'push' }} || ${{ github.event_name == 'pull_request' && github.event.pull_request.merged == true }} strategy: fail-fast: false @@ -170,6 +170,14 @@ jobs: platforms: ${{ matrix.platforms }} labels: ${{ steps.meta.outputs.labels }} + - name: check pull_request title + if: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.ref == 'develop' && github.event.pull_request.base.ref == 'main' && github.event.pull_request.merged == true }} + uses: kaisugi/action-regex-match@v1.0.1 + id: regex-match + with: + text: ${{ github.event.pull_request.title }} + regex: "^(\\d+\\.\\d+\\.\\d+)$" + - name: Release build and push from main branch if: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.ref == 'develop' && github.event.pull_request.base.ref == 'main' && github.event.pull_request.merged == true }} uses: docker/build-push-action@v6 @@ -181,6 +189,8 @@ jobs: ${{ env.GHCR }}/${{ env.GHCR_IMAGE_NAME }}:latest${{ matrix.tags-suffix }} ${{ env.DH_REGISTRY_NAME }}:latest${{ matrix.tags-suffix }} ${{ matrix.aliases }} + ${{ env.GHCR }}/${{ env.GHCR_IMAGE_NAME }}:${{ github.event.pull_request.title }}${{ matrix.tags-suffix }} + ${{ env.DH_REGISTRY_NAME }}:${{ github.event.pull_request.title }}${{ matrix.tags-suffix }} build-contexts: ${{ matrix.build-contexts }} file: ${{ matrix.dockerfile }} cache-from: type=gha,scope=rpxy-latest-${{ matrix.target }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e6b294..18deef2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,22 @@ # CHANGELOG -## 0.9.7 or 0.10.0 (Unreleased) +## 0.9.8 or 0.10.0 (Unreleased) + +## 0.9.7 + +### Improvement + +- Feat: add version tag for docker images via github actions +- Feat: support gRPC: This makes rpxy to serve gRPC requests on the same port as HTTP and HTTPS, i.e., listen_port and listen_port_tls. This means that by using the different subdomain for HTTP(S) and gRPC, we can multiplex them on same ports without opening another port dedicated to gRPC. To this end, this update made the forwarder to force HTTP/2 for gRPC requests towards backend (gRPC) app. +- Deps and refactor + +### Bugfix + +- Fixed bug for the upstream option "force_http2_upstream" + +### Other + +- Tentative downgrade of github actions `runs-on` from ubuntu-latest to ubuntu-22.04. ## 0.9.6 diff --git a/Cargo.toml b/Cargo.toml index ee0db6c..2cb948f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace.package] -version = "0.9.6" +version = "0.9.7" authors = ["Jun Kurihara"] homepage = "https://github.com/junkurihara/rust-rpxy" repository = "https://github.com/junkurihara/rust-rpxy" diff --git a/LICENSE b/LICENSE index 096f5e1..ddb8c03 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2024 Jun Kurihara +Copyright (c) 2025 Jun Kurihara Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index ebe3de3..912ed62 100644 --- a/README.md +++ b/README.md @@ -14,15 +14,28 @@ [^pure_rust]: Doubtfully can be claimed to be written in pure Rust since current `rpxy` is based on `aws-lc-rs` for cryptographic operations. -By default, `rpxy` provides the *TLS connection sanitization* by correctly binding a certificate used to establish a secure channel with the backend application. Specifically, it always keeps the consistency between the given SNI (server name indication) in `ClientHello` of the underlying TLS and the domain name given by the overlaid HTTP HOST header (or URL in Request line) [^1]. Additionally, as a somewhat unstable feature, our `rpxy` can handle the brand-new HTTP/3 connection thanks to [`quinn`](https://github.com/quinn-rs/quinn), [`s2n-quic`](https://github.com/aws/s2n-quic) and [`hyperium/h3`](https://github.com/hyperium/h3).[^h3lib] Furthermore, `rpxy` supports the automatic issuance and renewal of certificates via [TLS-ALPN-01 (RFC8737)](https://www.rfc-editor.org/rfc/rfc8737) of [ACME protocol (RFC8555)](https://www.rfc-editor.org/rfc/rfc8555) thanks to [`rustls-acme`](https://github.com/FlorianUekermann/rustls-acme), and the hybridized post-quantum key exchange [`X25519MLKEM768`](https://www.ietf.org/archive/id/draft-kwiatkowski-tls-ecdhe-mlkem-02.html)[^kyber] for TLS/QUIC incoming and outgoing initiation thanks to [`rustls-post-quantum`](https://docs.rs/rustls-post-quantum/latest/rustls_post_quantum/). +Supported features are summarized as follows: - [^h3lib]: HTTP/3 libraries are mutually exclusive. You need to explicitly specify `s2n-quic` with `--no-default-features` flag. Also note that if you build `rpxy` with `s2n-quic`, then it requires `openssl` just for building the package. +- Supported HTTP(S) protocols: HTTP/1.1, HTTP/2 and brand-new HTTP/3 [^h3lib] +- gRPC is also supported +- Serving multiple domain names with TLS termination +- Mutual TLS authentication with client certificates +- Automated certificate issuance and renewal via TLS-ALPN-01 ACME protocol [^acme] +- Post-quantum key exchange for TLS/QUIC [^kyber] +- TLS connection sanitization to avoid the domain fronting [^sanitization] +- Load balancing with round-robin, random, and sticky session +- and more... - [^kyber]: This is already a default feature. Also note that `X25519MLKEM768` is still a draft version yet this is widely used on the Internet. +[^h3lib]: HTTP/3 is enabled thanks to [`quinn`](https://github.com/quinn-rs/quinn), [`s2n-quic`](https://github.com/aws/s2n-quic) and [`hyperium/h3`](https://github.com/hyperium/h3). HTTP/3 libraries are mutually exclusive. You need to explicitly specify `s2n-quic` with `--no-default-features` flag. Also note that if you build `rpxy` with `s2n-quic`, then it requires `openssl` just for building the package. + +[^acme]: `rpxy` supports the automatic issuance and renewal of certificates via [TLS-ALPN-01 (RFC8737)](https://www.rfc-editor.org/rfc/rfc8737) of [ACME protocol (RFC8555)](https://www.rfc-editor.org/rfc/rfc8555) thanks to [`rustls-acme`](https://github.com/FlorianUekermann/rustls-acme). + +[^kyber]: `rpxy` supports the hybridized post-quantum key exchange [`X25519MLKEM768`](https://www.ietf.org/archive/id/draft-kwiatkowski-tls-ecdhe-mlkem-02.html)[^kyber] for TLS/QUIC incoming and outgoing initiation thanks to [`rustls-post-quantum`](https://docs.rs/rustls-post-quantum/latest/rustls_post_quantum/). This is already a default feature. Also note that `X25519MLKEM768` is still a draft version yet this is widely used on the Internet. + +[^sanitization]: By default, `rpxy` provides the *TLS connection sanitization* by correctly binding a certificate used to establish a secure channel with the backend application. Specifically, it always keeps the consistency between the given SNI (server name indication) in `ClientHello` of the underlying TLS and the domain name given by the overlaid HTTP HOST header (or URL in Request line). We should note that NGINX doesn't guarantee such a consistency by default. To this end, you have to add `if` statement in the configuration file in NGINX. This project is still *work-in-progress*. But it is already working in some production environments and serves a number of domain names. Furthermore it *significantly outperforms* NGINX and Caddy, e.g., *1.5x faster than NGINX*, in the setting of a very simple HTTP reverse-proxy scenario (See [`bench`](./bench/) directory). - [^1]: We should note that NGINX doesn't guarantee such a consistency by default. To this end, you have to add `if` statement in the configuration file in NGINX. ## Installing/Building an Executable Binary of `rpxy` @@ -422,6 +435,18 @@ Check a third party project [`Gamerboy59/rpxy-webui`](https://github.com/Gamerbo todo! +## Credits + +`rpxy` cannot be built without the following projects and inspirations: + +- [`hyper`](https://github.com/hyperium/hyper) and [`hyperium/h3`](https://github.com/hyperium/h3) +- [`rustls`](https://github.com/rustls/rustls) +- [`tokio`](https://github.com/tokio-rs/tokio) +- [`quinn`](https://github.com/quinn-rs/quinn) +- [`s2n-quic`](https://github.com/aws/s2n-quic) +- [`rustls-acme`](https://github.com/FlorianUekermann/rustls-acme) + + ## License `rpxy` is free, open-source software licensed under MIT License. diff --git a/docker/Dockerfile b/docker/Dockerfile index 707c342..c213bc1 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -40,6 +40,7 @@ RUN apt-get update && apt-get install -qy --no-install-recommends $BUILD_DEPS ${ echo "Install toolchain" && \ rustup target add $(cat /arch)-${TARGET_SUFFIX} && \ echo "Building rpxy from source" && \ + cargo update &&\ cargo build --release --target=$(cat /arch)-${TARGET_SUFFIX} ${CARGO_FEATURES} && \ strip --strip-all /tmp/target/$(cat /arch)-${TARGET_SUFFIX}/release/rpxy &&\ cp /tmp/target/$(cat /arch)-${TARGET_SUFFIX}/release/rpxy /tmp/target/release/rpxy diff --git a/docker/Dockerfile-slim b/docker/Dockerfile-slim index 0aa69cc..b58a408 100644 --- a/docker/Dockerfile-slim +++ b/docker/Dockerfile-slim @@ -25,6 +25,7 @@ COPY . /tmp/ ENV RUSTFLAGS "-C link-arg=-s" RUN echo "Building rpxy from source" && \ + cargo update && \ cargo build --release --target $(cat /arch)-unknown-linux-musl ${CARGO_FEATURES} && \ musl-strip --strip-all /tmp/target/$(cat /arch)-unknown-linux-musl/release/rpxy && \ cp /tmp/target/$(cat /arch)-unknown-linux-musl/release/rpxy /tmp/target/release/rpxy diff --git a/rpxy-acme/Cargo.toml b/rpxy-acme/Cargo.toml index bd8114e..113f01e 100644 --- a/rpxy-acme/Cargo.toml +++ b/rpxy-acme/Cargo.toml @@ -16,15 +16,15 @@ post-quantum = ["rustls-post-quantum"] [dependencies] url = { version = "2.5.4" } ahash = "0.8.11" -thiserror = "2.0.9" +thiserror = "2.0.11" tracing = "0.1.41" -async-trait = "0.1.84" +async-trait = "0.1.85" base64 = "0.22.1" -aws-lc-rs = { version = "1.12.0", default-features = false, features = [ +aws-lc-rs = { version = "1.12.2", default-features = false, features = [ "aws-lc-sys", ] } blocking = "1.6.1" -rustls = { version = "0.23.20", default-features = false, features = [ +rustls = { version = "0.23.22", default-features = false, features = [ "std", "aws_lc_rs", ] } @@ -32,7 +32,7 @@ rustls-platform-verifier = { version = "0.5.0" } rustls-acme = { path = "../submodules/rustls-acme/", default-features = false, features = [ "aws-lc-rs", ] } -rustls-post-quantum = { version = "0.2.1", optional = true } -tokio = { version = "1.42.0", default-features = false } +rustls-post-quantum = { version = "0.2.2", optional = true } +tokio = { version = "1.43.0", default-features = false } tokio-util = { version = "0.7.13", default-features = false } tokio-stream = { version = "0.1.17", default-features = false } diff --git a/rpxy-bin/Cargo.toml b/rpxy-bin/Cargo.toml index 16bc0d3..393688e 100644 --- a/rpxy-bin/Cargo.toml +++ b/rpxy-bin/Cargo.toml @@ -33,7 +33,7 @@ mimalloc = { version = "*", default-features = false } anyhow = "1.0.95" ahash = "0.8.11" serde = { version = "1.0.217", default-features = false, features = ["derive"] } -tokio = { version = "1.42.0", default-features = false, features = [ +tokio = { version = "1.43.0", default-features = false, features = [ "net", "rt-multi-thread", "time", @@ -41,11 +41,11 @@ tokio = { version = "1.42.0", default-features = false, features = [ "macros", ] } tokio-util = { version = "0.7.13", default-features = false } -async-trait = "0.1.84" +async-trait = "0.1.85" futures-util = { version = "0.3.31", default-features = false } # config -clap = { version = "4.5.23", features = ["std", "cargo", "wrap_help"] } +clap = { version = "4.5.27", features = ["std", "cargo", "wrap_help"] } toml = { version = "0.8.19", default-features = false, features = ["parse"] } hot_reload = "0.1.8" serde_ignored = "0.1.10" diff --git a/rpxy-certs/Cargo.toml b/rpxy-certs/Cargo.toml index f060b1a..8ed4fc4 100644 --- a/rpxy-certs/Cargo.toml +++ b/rpxy-certs/Cargo.toml @@ -19,10 +19,10 @@ http3 = [] ahash = { version = "0.8.11" } tracing = { version = "0.1.41" } derive_builder = { version = "0.20.2" } -thiserror = { version = "2.0.9" } +thiserror = { version = "2.0.11" } hot_reload = { version = "0.1.8" } -async-trait = { version = "0.1.84" } -rustls = { version = "0.23.20", default-features = false, features = [ +async-trait = { version = "0.1.85" } +rustls = { version = "0.23.22", default-features = false, features = [ "std", "aws_lc_rs", ] } @@ -31,11 +31,11 @@ rustls-webpki = { version = "0.102.8", default-features = false, features = [ "std", "aws_lc_rs", ] } -rustls-post-quantum = { version = "0.2.1", optional = true } -x509-parser = { version = "0.16.0" } +rustls-post-quantum = { version = "0.2.2", optional = true } +x509-parser = { version = "0.17.0" } [dev-dependencies] -tokio = { version = "1.42.0", default-features = false, features = [ +tokio = { version = "1.43.0", default-features = false, features = [ "rt-multi-thread", "macros", ] } diff --git a/rpxy-lib/Cargo.toml b/rpxy-lib/Cargo.toml index 511e177..f14d821 100644 --- a/rpxy-lib/Cargo.toml +++ b/rpxy-lib/Cargo.toml @@ -36,12 +36,12 @@ post-quantum = [ ] [dependencies] -rand = "0.8.5" +rand = "0.9.0" ahash = "0.8.11" bytes = "1.9.0" derive_builder = "0.20.2" futures = { version = "0.3.31", features = ["alloc", "async-await"] } -tokio = { version = "1.42.0", default-features = false, features = [ +tokio = { version = "1.43.0", default-features = false, features = [ "net", "rt-multi-thread", "time", @@ -50,17 +50,17 @@ tokio = { version = "1.42.0", default-features = false, features = [ "fs", ] } tokio-util = { version = "0.7.13", default-features = false } -pin-project-lite = "0.2.15" -async-trait = "0.1.84" +pin-project-lite = "0.2.16" +async-trait = "0.1.85" # Error handling anyhow = "1.0.95" -thiserror = "2.0.9" +thiserror = "2.0.11" # http for both server and client http = "1.2.0" http-body-util = "0.1.2" -hyper = { version = "1.5.2", default-features = false } +hyper = { version = "1.6.0", default-features = false } hyper-util = { version = "0.1.10", features = ["full"] } futures-util = { version = "0.3.31", default-features = false } futures-channel = { version = "0.3.31", default-features = false } @@ -80,8 +80,8 @@ hyper-rustls = { version = "0.27.5", default-features = false, features = [ # tls and cert management for server rpxy-certs = { path = "../rpxy-certs/", default-features = false } hot_reload = "0.1.8" -rustls = { version = "0.23.20", default-features = false } -rustls-post-quantum = { version = "0.2.1", optional = true } +rustls = { version = "0.23.22", default-features = false } +rustls-post-quantum = { version = "0.2.2", optional = true } tokio-rustls = { version = "0.26.1", features = ["early-data"] } # acme @@ -94,11 +94,11 @@ tracing = { version = "0.1.41" } quinn = { version = "0.11.6", optional = true } h3 = { version = "0.0.6", features = ["tracing"], optional = true } h3-quinn = { version = "0.0.7", optional = true } -s2n-quic = { version = "1.51.0", path = "../submodules/s2n-quic/quic/s2n-quic/", default-features = false, features = [ +s2n-quic = { version = "1.52.1", path = "../submodules/s2n-quic/quic/s2n-quic/", default-features = false, features = [ "provider-tls-rustls", ], optional = true } -s2n-quic-core = { version = "0.51.0", path = "../submodules/s2n-quic/quic/s2n-quic-core", default-features = false, optional = true } -s2n-quic-rustls = { version = "0.51.0", path = "../submodules/s2n-quic/quic/s2n-quic-rustls", optional = true } +s2n-quic-core = { version = "0.52.1", path = "../submodules/s2n-quic/quic/s2n-quic-core", default-features = false, optional = true } +s2n-quic-rustls = { version = "0.52.1", path = "../submodules/s2n-quic/quic/s2n-quic-rustls", optional = true } s2n-quic-h3 = { path = "../submodules/s2n-quic/quic/s2n-quic-h3/", features = [ "tracing", ], optional = true } @@ -108,7 +108,7 @@ socket2 = { version = "0.5.8", features = ["all"], optional = true } # cache http-cache-semantics = { path = "../submodules/rusty-http-cache-semantics", default-features = false, optional = true } -lru = { version = "0.12.5", optional = true } +lru = { version = "0.13.0", optional = true } sha2 = { version = "0.10.8", default-features = false, optional = true } # cookie handling for sticky cookie diff --git a/rpxy-lib/src/backend/load_balance/load_balance_main.rs b/rpxy-lib/src/backend/load_balance/load_balance_main.rs index 0b3eff8..0db21dd 100644 --- a/rpxy-lib/src/backend/load_balance/load_balance_main.rs +++ b/rpxy-lib/src/backend/load_balance/load_balance_main.rs @@ -80,8 +80,8 @@ impl LoadBalanceRandomBuilder { impl LoadBalanceWithPointer for LoadBalanceRandom { /// Returns the random index within the range fn get_ptr(&self, _info: Option<&LoadBalanceContext>) -> PointerToUpstream { - let mut rng = rand::thread_rng(); - let ptr = rng.gen_range(0..self.num_upstreams); + let mut rng = rand::rng(); + let ptr = rng.random_range(0..self.num_upstreams); PointerToUpstream { ptr, context: None } } } diff --git a/rpxy-lib/src/forwarder/client.rs b/rpxy-lib/src/forwarder/client.rs index bc99b41..292be06 100644 --- a/rpxy-lib/src/forwarder/client.rs +++ b/rpxy-lib/src/forwarder/client.rs @@ -226,7 +226,9 @@ where let connector = builder.https_or_http().enable_all_versions().wrap_connector(http.clone()); let connector_h2 = builder_h2.https_or_http().enable_http2().wrap_connector(http); let inner = Client::builder(LocalExecutor::new(_globals.runtime_handle.clone())).build::<_, B1>(connector); - let inner_h2 = Client::builder(LocalExecutor::new(_globals.runtime_handle.clone())).build::<_, B1>(connector_h2); + let inner_h2 = Client::builder(LocalExecutor::new(_globals.runtime_handle.clone())) + .http2_only(true) + .build::<_, B1>(connector_h2); Ok(Self { inner, diff --git a/rpxy-lib/src/message_handler/handler_main.rs b/rpxy-lib/src/message_handler/handler_main.rs index 4b324df..23133a2 100644 --- a/rpxy-lib/src/message_handler/handler_main.rs +++ b/rpxy-lib/src/message_handler/handler_main.rs @@ -200,7 +200,7 @@ where // Handle StatusCode::SWITCHING_PROTOCOLS in response let upgrade_in_response = extract_upgrade(res_backend.headers()); let should_upgrade = match (upgrade_in_request.as_ref(), upgrade_in_response.as_ref()) { - (Some(u_req), Some(u_res)) => u_req.to_ascii_lowercase() == u_res.to_ascii_lowercase(), + (Some(u_req), Some(u_res)) => u_req.eq_ignore_ascii_case(u_res), _ => false, }; diff --git a/rpxy-lib/src/message_handler/utils_headers.rs b/rpxy-lib/src/message_handler/utils_headers.rs index d058f88..1fa3c99 100644 --- a/rpxy-lib/src/message_handler/utils_headers.rs +++ b/rpxy-lib/src/message_handler/utils_headers.rs @@ -272,7 +272,7 @@ pub(super) fn extract_upgrade(headers: &HeaderMap) -> Option { .to_str() .unwrap_or("") .split(',') - .any(|w| w.trim().to_ascii_lowercase() == header::UPGRADE.as_str().to_ascii_lowercase()) + .any(|w| w.trim().eq_ignore_ascii_case(header::UPGRADE.as_str())) { if let Some(u) = headers.get(header::UPGRADE) { if let Ok(m) = u.to_str() { diff --git a/rpxy-lib/src/message_handler/utils_request.rs b/rpxy-lib/src/message_handler/utils_request.rs index b60835f..0a0a77c 100644 --- a/rpxy-lib/src/message_handler/utils_request.rs +++ b/rpxy-lib/src/message_handler/utils_request.rs @@ -59,6 +59,18 @@ pub(super) fn update_request_line( upstream_chosen: &Upstream, upstream_candidates: &UpstreamCandidates, ) -> anyhow::Result<()> { + // If request is grpc, HTTP/2 is required + if req + .headers() + .get(header::CONTENT_TYPE) + .map(|v| v.as_bytes().starts_with(b"application/grpc")) + == Some(true) + { + debug!("Must be http/2 for gRPC request."); + *req.version_mut() = Version::HTTP_2; + return Ok(()); + } + // If not specified (force_httpXX_upstream) and https, version is preserved except for http/3 if upstream_chosen.uri.scheme() == Some(&Scheme::HTTP) { // Change version to http/1.1 when destination scheme is http diff --git a/submodules/rustls-acme b/submodules/rustls-acme index af2d016..a65d7e7 160000 --- a/submodules/rustls-acme +++ b/submodules/rustls-acme @@ -1 +1 @@ -Subproject commit af2d016b6aa4e09586253a0459efc4af6635c79b +Subproject commit a65d7e7000b49e6e1e14daf32baee094f4d8dacd diff --git a/submodules/rusty-http-cache-semantics b/submodules/rusty-http-cache-semantics index d5b5efd..2500716 160000 --- a/submodules/rusty-http-cache-semantics +++ b/submodules/rusty-http-cache-semantics @@ -1 +1 @@ -Subproject commit d5b5efd9de4dab3c958c50be5380652d801cc65f +Subproject commit 2500716b70bd6e548cdf690188ded7afe6726330 diff --git a/submodules/s2n-quic b/submodules/s2n-quic index b49cb51..7852417 160000 --- a/submodules/s2n-quic +++ b/submodules/s2n-quic @@ -1 +1 @@ -Subproject commit b49cb517d0256864a9382f04fedd0e9f71531d85 +Subproject commit 78524172f54af5e3d5a0404b230d265c82eaf446