diff --git a/CHANGELOG.md b/CHANGELOG.md index dc1bc3aa..8812e9e8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,17 @@ ## 0.10.0 (Unreleased) +## 0.9.1 + +### Important Changes + +- Feat: Support `https_redirection_port` option to redirect http requests to https with custom port. + +### Improvement + +- Refactor: lots of minor improvements +- Deps + ## 0.9.0 ### Important Changes diff --git a/Cargo.toml b/Cargo.toml index 01c02639..bc6b0437 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace.package] -version = "0.9.0" +version = "0.9.1" authors = ["Jun Kurihara"] homepage = "https://github.com/junkurihara/rust-rpxy" repository = "https://github.com/junkurihara/rust-rpxy" diff --git a/README.md b/README.md index 221d6be0..274a0969 100644 --- a/README.md +++ b/README.md @@ -104,7 +104,7 @@ Otherwise, say, a request to `other.example.com` is simply rejected with the sta If you want to host multiple and distinct domain names in a single IP address/port, simply create multiple `app.""` entries in config file like ```toml -default_application = "app1" +default_app = "app1" [apps.app1] server_name = "app1.example.com" @@ -115,7 +115,7 @@ server_name = "app2.example.org" #... ``` -Here we note that by specifying `default_application` entry, *HTTP* requests will be served by the specified application if HOST header or URL in Request line doesn't match any `server_name`s in `reverse_proxy` entries. For HTTPS requests, it will be rejected since the secure connection cannot be established for the unknown server name. +Here we note that by specifying `default_app` entry, *HTTP* requests will be served by the specified application if HOST header or URL in Request line doesn't match any `server_name`s in `reverse_proxy` entries. For HTTPS requests, it will be rejected since the secure connection cannot be established for the unknown server name. #### HTTPS to Backend Application @@ -315,6 +315,16 @@ The above configuration is common to all ACME enabled domains. Note that the htt ## TIPS +### Set custom port for HTTPS redirection + +Consider a case where `rpxy` is running on a container. Then when the container manager maps port A (e.g., 80/443) of the host to port B (e.g., 8080/8443) of the container for http and https, `rpxy` must be configured with port B for `listen_port` and `listen_port_tls`. However, when you want to set `http_redirection=true` for some backend apps, `rpxy` issues the redirection response 301 with the port B by default, which is not accessible from the outside of the container. To avoid this, you can set a custom port for the redirection response by specifying `https_redirection_port` in `config.toml`. In this case, port A should be set for `https_redirection_port`, then the redirection response 301 will be issued with the port A. + +```toml +listen_port = 8080 +listen_port_tls = 8443 +https_redirection_port = 443 +``` + ### Using Private Key Issued by Let's Encrypt If you obtain certificates and private keys from [Let's Encrypt](https://letsencrypt.org/), you have PKCS1-formatted private keys. So you need to convert such retrieved private keys into PKCS8 format to use in `rpxy`. diff --git a/config-example.toml b/config-example.toml index d279e50c..b0a19458 100644 --- a/config-example.toml +++ b/config-example.toml @@ -10,6 +10,11 @@ listen_port = 8080 listen_port_tls = 8443 +# Optional. If you listen on a custom port like 8443 but redirect with firewall to 443 +# When you specify this, the server sends a redirection response 301 with specified port to the client for plaintext http request. +# Otherwise, the server sends 301 with the same port as `listen_port_tls`. +# https_redirection_port = 443 + # Optional for h2 and http1.1 tcp_listen_backlog = 1024 diff --git a/rpxy-acme/Cargo.toml b/rpxy-acme/Cargo.toml index 679c94d8..17e1257a 100644 --- a/rpxy-acme/Cargo.toml +++ b/rpxy-acme/Cargo.toml @@ -15,7 +15,7 @@ url = { version = "2.5.2" } rustc-hash = "2.0.0" thiserror = "1.0.63" tracing = "0.1.40" -async-trait = "0.1.81" +async-trait = "0.1.82" base64 = "0.22.1" aws-lc-rs = { version = "1.8.1", default-features = false, features = [ "aws-lc-sys", @@ -25,10 +25,10 @@ rustls = { version = "0.23.12", default-features = false, features = [ "std", "aws_lc_rs", ] } -rustls-platform-verifier = { version = "0.3.3" } +rustls-platform-verifier = { version = "0.3.4" } rustls-acme = { path = "../submodules/rustls-acme/", default-features = false, features = [ "aws-lc-rs", ] } -tokio = { version = "1.39.2", default-features = false } -tokio-util = { version = "0.7.11", default-features = false } -tokio-stream = { version = "0.1.15", default-features = false } +tokio = { version = "1.40.0", default-features = false } +tokio-util = { version = "0.7.12", default-features = false } +tokio-stream = { version = "0.1.16", default-features = false } diff --git a/rpxy-bin/Cargo.toml b/rpxy-bin/Cargo.toml index 91fe4d68..c689483e 100644 --- a/rpxy-bin/Cargo.toml +++ b/rpxy-bin/Cargo.toml @@ -31,21 +31,21 @@ rpxy-lib = { path = "../rpxy-lib/", default-features = false, features = [ mimalloc = { version = "*", default-features = false } anyhow = "1.0.86" rustc-hash = "2.0.0" -serde = { version = "1.0.204", default-features = false, features = ["derive"] } -tokio = { version = "1.39.2", default-features = false, features = [ +serde = { version = "1.0.209", default-features = false, features = ["derive"] } +tokio = { version = "1.40.0", default-features = false, features = [ "net", "rt-multi-thread", "time", "sync", "macros", ] } -tokio-util = { version = "0.7.11", default-features = false } -async-trait = "0.1.81" +tokio-util = { version = "0.7.12", default-features = false } +async-trait = "0.1.82" futures-util = { version = "0.3.30", default-features = false } # config -clap = { version = "4.5.11", features = ["std", "cargo", "wrap_help"] } -toml = { version = "0.8.17", default-features = false, features = ["parse"] } +clap = { version = "4.5.17", features = ["std", "cargo", "wrap_help"] } +toml = { version = "0.8.19", default-features = false, features = ["parse"] } hot_reload = "0.1.6" # logging diff --git a/rpxy-bin/src/config/parse.rs b/rpxy-bin/src/config/parse.rs index a591c409..7292b583 100644 --- a/rpxy-bin/src/config/parse.rs +++ b/rpxy-bin/src/config/parse.rs @@ -59,6 +59,13 @@ pub fn build_settings(config: &ConfigToml) -> std::result::Result<(ProxyConfig, "Some apps serves only plaintext HTTP" ); } + // https redirection port must be configured only when both http_port and https_port are configured. + if proxy_config.https_redirection_port.is_some() { + ensure!( + proxy_config.https_port.is_some() && proxy_config.http_port.is_some(), + "https_redirection_port can be specified only when both http_port and https_port are specified" + ); + } // https redirection can be configured if both ports are active if !(proxy_config.https_port.is_some() && proxy_config.http_port.is_some()) { ensure!( diff --git a/rpxy-bin/src/config/toml.rs b/rpxy-bin/src/config/toml.rs index b2a70bb5..9befc196 100644 --- a/rpxy-bin/src/config/toml.rs +++ b/rpxy-bin/src/config/toml.rs @@ -13,6 +13,7 @@ pub struct ConfigToml { pub listen_port: Option, pub listen_port_tls: Option, pub listen_ipv6: Option, + pub https_redirection_port: Option, pub tcp_listen_backlog: Option, pub max_concurrent_streams: Option, pub max_clients: Option, @@ -107,6 +108,11 @@ impl TryInto for &ConfigToml { // listen port and socket http_port: self.listen_port, https_port: self.listen_port_tls, + https_redirection_port: if self.https_redirection_port.is_some() { + self.https_redirection_port + } else { + self.listen_port_tls + }, ..Default::default() }; ensure!( diff --git a/rpxy-certs/Cargo.toml b/rpxy-certs/Cargo.toml index 072450ef..233a4eff 100644 --- a/rpxy-certs/Cargo.toml +++ b/rpxy-certs/Cargo.toml @@ -17,23 +17,23 @@ http3 = [] [dependencies] rustc-hash = { version = "2.0.0" } tracing = { version = "0.1.40" } -derive_builder = { version = "0.20.0" } +derive_builder = { version = "0.20.1" } thiserror = { version = "1.0.63" } hot_reload = { version = "0.1.6" } -async-trait = { version = "0.1.81" } +async-trait = { version = "0.1.82" } rustls = { version = "0.23.12", default-features = false, features = [ "std", "aws_lc_rs", ] } -rustls-pemfile = { version = "2.1.2" } -rustls-webpki = { version = "0.102.6", default-features = false, features = [ +rustls-pemfile = { version = "2.1.3" } +rustls-webpki = { version = "0.102.7", default-features = false, features = [ "std", "aws_lc_rs", ] } x509-parser = { version = "0.16.0" } [dev-dependencies] -tokio = { version = "1.39.2", default-features = false, features = [ +tokio = { version = "1.40.0", default-features = false, features = [ "rt-multi-thread", "macros", ] } diff --git a/rpxy-lib/Cargo.toml b/rpxy-lib/Cargo.toml index e8fef1ae..89c4868c 100644 --- a/rpxy-lib/Cargo.toml +++ b/rpxy-lib/Cargo.toml @@ -32,10 +32,10 @@ acme = ["dep:rpxy-acme"] [dependencies] rand = "0.8.5" rustc-hash = "2.0.0" -bytes = "1.7.0" -derive_builder = "0.20.0" +bytes = "1.7.1" +derive_builder = "0.20.1" futures = { version = "0.3.30", features = ["alloc", "async-await"] } -tokio = { version = "1.39.2", default-features = false, features = [ +tokio = { version = "1.40.0", default-features = false, features = [ "net", "rt-multi-thread", "time", @@ -43,9 +43,9 @@ tokio = { version = "1.39.2", default-features = false, features = [ "macros", "fs", ] } -tokio-util = { version = "0.7.11", default-features = false } +tokio-util = { version = "0.7.12", default-features = false } pin-project-lite = "0.2.14" -async-trait = "0.1.81" +async-trait = "0.1.82" # Error handling anyhow = "1.0.86" @@ -55,7 +55,7 @@ thiserror = "1.0.63" http = "1.1.0" http-body-util = "0.1.2" hyper = { version = "1.4.1", default-features = false } -hyper-util = { version = "0.1.6", features = ["full"] } +hyper-util = { version = "0.1.7", features = ["full"] } futures-util = { version = "0.3.30", default-features = false } futures-channel = { version = "0.3.30", default-features = false } @@ -64,9 +64,7 @@ hyper-tls = { version = "0.6.0", features = [ "alpn", "vendored", ], optional = true } -# TODO: Work around to enable rustls-platform-verifier feature: https://github.com/rustls/hyper-rustls/pull/276 -# hyper-rustls = { version = "0.27.2", default-features = false, features = [ -hyper-rustls = { git = "https://github.com/junkurihara/hyper-rustls", branch = "main", features = [ +hyper-rustls = { version = "0.27.3", default-features = false, features = [ "aws-lc-rs", "http1", "http2", @@ -86,17 +84,17 @@ rpxy-acme = { path = "../rpxy-acme/", default-features = false, optional = true tracing = { version = "0.1.40" } # http/3 -quinn = { version = "0.11.2", optional = true } +quinn = { version = "0.11.5", optional = true } h3 = { version = "0.0.6", features = ["tracing"], optional = true } h3-quinn = { version = "0.0.7", optional = true } s2n-quic-h3 = { path = "../submodules/s2n-quic-h3/", features = [ "tracing", ], optional = true } -s2n-quic = { version = "1.43.0", default-features = false, features = [ +s2n-quic = { version = "1.45.0", default-features = false, features = [ "provider-tls-rustls", ], optional = true } -s2n-quic-core = { version = "0.43.0", default-features = false, optional = true } -s2n-quic-rustls = { version = "0.43.0", optional = true } +s2n-quic-core = { version = "0.45.0", default-features = false, optional = true } +s2n-quic-rustls = { version = "0.45.0", optional = true } ########## # for UDP socket wit SO_REUSEADDR when h3 with quinn socket2 = { version = "0.5.7", features = ["all"], optional = true } diff --git a/rpxy-lib/src/globals.rs b/rpxy-lib/src/globals.rs index 97aadefc..82d66c0d 100644 --- a/rpxy-lib/src/globals.rs +++ b/rpxy-lib/src/globals.rs @@ -30,8 +30,12 @@ pub struct ProxyConfig { pub listen_sockets: Vec, /// http port pub http_port: Option, - /// https port + /// https port listening for TLS by default pub https_port: Option, + /// https redirection port that notifies the client the port to connect to. + /// Tis is used when the reverse proxy is behind a middlebox mapping the https port A to the reverse proxy's https port B. + /// Typically, it is the container environment. (e.g. the host exposes 443 and the container exposes 8443 for https, then the redirection port is 443) + pub https_redirection_port: Option, /// tcp listen backlog pub tcp_listen_backlog: u32, @@ -85,6 +89,7 @@ impl Default for ProxyConfig { listen_sockets: Vec::new(), http_port: None, https_port: None, + https_redirection_port: None, tcp_listen_backlog: TCP_LISTEN_BACKLOG, // TODO: Reconsider each timeout values diff --git a/rpxy-lib/src/message_handler/handler_main.rs b/rpxy-lib/src/message_handler/handler_main.rs index 9ce63f8f..4b324dfc 100644 --- a/rpxy-lib/src/message_handler/handler_main.rs +++ b/rpxy-lib/src/message_handler/handler_main.rs @@ -121,7 +121,11 @@ where "Redirect to secure connection: {}", <&ServerName as TryInto>::try_into(&backend_app.server_name).unwrap_or_default() ); - return secure_redirection_response(&backend_app.server_name, self.globals.proxy_config.https_port, &req); + return secure_redirection_response( + &backend_app.server_name, + self.globals.proxy_config.https_redirection_port, + &req, + ); } // Find reverse proxy for given path and choose one of upstream host diff --git a/rpxy-lib/src/message_handler/utils_headers.rs b/rpxy-lib/src/message_handler/utils_headers.rs index 9be45e50..d058f880 100644 --- a/rpxy-lib/src/message_handler/utils_headers.rs +++ b/rpxy-lib/src/message_handler/utils_headers.rs @@ -30,16 +30,12 @@ pub(super) fn takeout_sticky_cookie_lb_context( let cookies_iter = entry .iter() .flat_map(|v| v.to_str().unwrap_or("").split(';').map(|v| v.trim())); - let (sticky_cookies, without_sticky_cookies): (Vec<_>, Vec<_>) = cookies_iter - .into_iter() - .partition(|v| v.starts_with(expected_cookie_name)); + let (sticky_cookies, without_sticky_cookies): (Vec<_>, Vec<_>) = + cookies_iter.into_iter().partition(|v| v.starts_with(expected_cookie_name)); if sticky_cookies.is_empty() { return Ok(None); } - ensure!( - sticky_cookies.len() == 1, - "Invalid cookie: Multiple sticky cookie values" - ); + ensure!(sticky_cookies.len() == 1, "Invalid cookie: Multiple sticky cookie values"); let cookies_passed_to_upstream = without_sticky_cookies.join("; "); let cookie_passed_to_lb = sticky_cookies.first().unwrap(); @@ -59,10 +55,7 @@ pub(super) fn takeout_sticky_cookie_lb_context( /// Set-Cookie if LB Sticky is enabled and if cookie is newly created/updated. /// Set-Cookie response header could be in multiple lines. /// https://developer.mozilla.org/ja/docs/Web/HTTP/Headers/Set-Cookie -pub(super) fn set_sticky_cookie_lb_context( - headers: &mut HeaderMap, - context_from_lb: &LoadBalanceContext, -) -> Result<()> { +pub(super) fn set_sticky_cookie_lb_context(headers: &mut HeaderMap, context_from_lb: &LoadBalanceContext) -> Result<()> { let sticky_cookie_string: String = context_from_lb.sticky_cookie.clone().try_into()?; let new_header_val: HeaderValue = sticky_cookie_string.parse()?; let expected_cookie_name = &context_from_lb.sticky_cookie.value.name; @@ -122,7 +115,7 @@ pub(super) fn apply_upstream_options_to_header( // add upgrade-insecure-requests in request header if not exist headers .entry(header::UPGRADE_INSECURE_REQUESTS) - .or_insert(HeaderValue::from_bytes(&[b'1']).unwrap()); + .or_insert(HeaderValue::from_bytes(b"1").unwrap()); } _ => (), } @@ -141,7 +134,7 @@ pub(super) fn append_header_entry_with_comma(headers: &mut HeaderMap, key: &str, // entry.append(value.parse::()?); let mut new_value = Vec::::with_capacity(entry.get().as_bytes().len() + 2 + value.len()); new_value.put_slice(entry.get().as_bytes()); - new_value.put_slice(&[b',', b' ']); + new_value.put_slice(b", "); new_value.put_slice(value.as_bytes()); entry.insert(HeaderValue::from_bytes(&new_value)?); } diff --git a/rpxy-lib/src/message_handler/utils_request.rs b/rpxy-lib/src/message_handler/utils_request.rs index 8939433b..b60835fb 100644 --- a/rpxy-lib/src/message_handler/utils_request.rs +++ b/rpxy-lib/src/message_handler/utils_request.rs @@ -16,7 +16,7 @@ impl InspectParseHost for Request { /// Inspect and extract hostname from either the request HOST header or request line fn inspect_parse_host(&self) -> Result> { let drop_port = |v: &[u8]| { - if v.starts_with(&[b'[']) { + if v.starts_with(b"[") { // v6 address with bracket case. if port is specified, always it is in this case. let mut iter = v.split(|ptr| ptr == &b'[' || ptr == &b']'); iter.next().ok_or(anyhow!("Invalid Host header"))?; // first item is always blank diff --git a/submodules/rustls-acme b/submodules/rustls-acme index 43719fb0..af2d016b 160000 --- a/submodules/rustls-acme +++ b/submodules/rustls-acme @@ -1 +1 @@ -Subproject commit 43719fb04cc522c039c9e7420567a38416f9fec7 +Subproject commit af2d016b6aa4e09586253a0459efc4af6635c79b diff --git a/submodules/s2n-quic-h3/Cargo.toml b/submodules/s2n-quic-h3/Cargo.toml index b4459244..595fdcc9 100644 --- a/submodules/s2n-quic-h3/Cargo.toml +++ b/submodules/s2n-quic-h3/Cargo.toml @@ -15,8 +15,8 @@ futures = { version = "0.3", default-features = false } h3 = { version = "0.0.6", features = ["tracing"] } # s2n-quic = { path = "../s2n-quic" } # s2n-quic-core = { path = "../s2n-quic-core" } -s2n-quic = { version = "1.43.0" } -s2n-quic-core = { version = "0.43.0" } +s2n-quic = { version = "1.45.0" } +s2n-quic-core = { version = "0.45.0" } tracing = { version = "0.1.40", optional = true } [features]