Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ futures-util = "0.3"
flate2 = "1.1"
http = "1.4"
http-body-util = "0.1"
httpdate = "1.0"
h3 = "0.0.8"
h3-quinn = "0.0.10"
hickory-resolver = "0.24.4"
Expand All @@ -56,4 +57,5 @@ tokio-rustls = "0.26"
tower-service = "0.3"
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt"] }
uuid = { version = "1.18", features = ["v7", "std"] }
webpki = { package = "rustls-webpki", version = "0.103.11", default-features = false, features = ["std", "aws-lc-rs"] }
3 changes: 1 addition & 2 deletions RELEASE_NOTES_v0.1.3-rc.13.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,5 +62,4 @@ Release-oriented validation passed for `v0.1.3-rc.13` with:
short smoke path behind manual dispatch
- fuzz smoke is currently a prerelease gate, not a stable-release-only
differentiator
- the nginx comparison harness remains external to the normal test matrix and is
still optional for prerelease preparation
- the HTTP/3 release gate does not include an in-repo nginx comparison harness
16 changes: 12 additions & 4 deletions crates/rginx-app/tests/phase1.rs
Original file line number Diff line number Diff line change
Expand Up @@ -267,11 +267,19 @@ fn parse_http_response(bytes: &[u8]) -> Result<ParsedResponse, String> {

fn assert_generated_request_id(value: Option<&str>) {
let value = value.expect("response should include x-request-id");
assert_eq!(value.len(), "rginx-0000000000000000".len());
assert!(value.starts_with("rginx-"), "generated request id should use the rginx- prefix");
assert_eq!(value.len(), 36, "generated request id should be a UUIDv7");
assert_eq!(value.as_bytes()[8], b'-');
assert_eq!(value.as_bytes()[13], b'-');
assert_eq!(value.as_bytes()[14], b'7', "generated request id should be UUIDv7");
assert_eq!(value.as_bytes()[18], b'-');
assert!(
value["rginx-".len()..].chars().all(|ch| ch.is_ascii_hexdigit()),
"generated request id should end with lowercase hex digits, got {value:?}"
matches!(value.as_bytes()[19].to_ascii_lowercase(), b'8' | b'9' | b'a' | b'b'),
"generated request id should use the RFC 4122 variant, got {value:?}"
);
assert_eq!(value.as_bytes()[23], b'-');
assert!(
value.chars().filter(|ch| *ch != '-').all(|ch| ch.is_ascii_hexdigit()),
"generated request id should use UUID hex digits, got {value:?}"
);
}

Expand Down
42 changes: 41 additions & 1 deletion crates/rginx-app/tests/upstream_http2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ const TEST_SERVER_KEY_PEM: &str = "-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkq
struct ObservedRequest {
version: Version,
path: String,
authority: Option<String>,
alpn_protocol: Option<String>,
}

Expand Down Expand Up @@ -63,6 +64,37 @@ async fn proxies_to_https_upstreams_over_http2_when_alpn_negotiates_h2() {
fs::remove_dir_all(upstream_temp_dir).expect("upstream temp dir should be removed");
}

#[tokio::test(flavor = "multi_thread")]
async fn preserves_hostname_authority_when_h2_upstream_dials_resolved_ip() {
let (upstream_addr, observed_rx, upstream_task, upstream_temp_dir) = spawn_h2_upstream().await;

let listen_addr = reserve_loopback_addr();
let upstream_url = format!("https://localhost:{}", upstream_addr.port());
let mut server = ServerHarness::spawn("rginx-upstream-h2-authority", |_| {
proxy_config_for_upstream_url(listen_addr, &upstream_url)
});
server.wait_for_http_ready(listen_addr, Duration::from_secs(5));

let (status, body) = fetch_text_response(listen_addr, "/")
.expect("rginx should return a successful upstream response");
assert_eq!(status, 200);
assert_eq!(body, "upstream h2 ok\n");

let observed = tokio::time::timeout(Duration::from_secs(5), observed_rx)
.await
.expect("upstream request should be observed before timeout")
.expect("upstream observation channel should complete");
assert_eq!(observed.version, Version::HTTP_2);
assert_eq!(observed.path, "/");
assert_eq!(observed.alpn_protocol.as_deref(), Some("h2"));
let expected_authority = upstream_url.strip_prefix("https://").unwrap();
assert_eq!(observed.authority.as_deref(), Some(expected_authority));

server.shutdown_and_wait(Duration::from_secs(5));
upstream_task.await.expect("upstream h2 server task should finish");
fs::remove_dir_all(upstream_temp_dir).expect("upstream temp dir should be removed");
}

async fn spawn_h2_upstream()
-> (SocketAddr, oneshot::Receiver<ObservedRequest>, JoinHandle<()>, PathBuf) {
let temp_dir = temp_dir("rginx-upstream-h2");
Expand Down Expand Up @@ -109,6 +141,7 @@ async fn spawn_h2_upstream()
let _ = sender.send(ObservedRequest {
version: request.version(),
path: request.uri().path().to_string(),
authority: request.uri().authority().map(|value| value.to_string()),
alpn_protocol,
});
}
Expand All @@ -133,10 +166,17 @@ async fn spawn_h2_upstream()
}

fn proxy_config(listen_addr: SocketAddr, upstream_addr: SocketAddr) -> String {
proxy_config_for_upstream_url(
listen_addr,
&format!("https://127.0.0.1:{}", upstream_addr.port()),
)
}

fn proxy_config_for_upstream_url(listen_addr: SocketAddr, upstream_url: &str) -> String {
format!(
"Config(\n runtime: RuntimeConfig(\n shutdown_timeout_secs: 2,\n ),\n server: ServerConfig(\n listen: {:?},\n ),\n upstreams: [\n UpstreamConfig(\n name: \"backend\",\n peers: [\n UpstreamPeerConfig(\n url: {:?},\n ),\n ],\n tls: Some(Insecure),\n protocol: Auto,\n server_name_override: Some(\"localhost\"),\n ),\n ],\n locations: [\n{ready_route} LocationConfig(\n matcher: Exact(\"/\"),\n handler: Proxy(\n upstream: \"backend\",\n ),\n ),\n ],\n)\n",
listen_addr.to_string(),
format!("https://127.0.0.1:{}", upstream_addr.port()),
upstream_url,
ready_route = READY_ROUTE_CONFIG,
)
}
Expand Down
3 changes: 2 additions & 1 deletion crates/rginx-config/src/compile/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,9 @@ pub fn compile_with_base(raw: Config, base_dir: impl AsRef<Path>) -> Result<Conf
let compiled_server = server::compile_legacy_server(server, base_dir, any_vhost_tls)?;
(vec![compiled_server.listener.clone()], compiled_server.server_names)
} else {
let default_server_header = server.server_header;
let default_server_names = server.server_names;
let listeners = server::compile_listeners(raw_listeners, base_dir)?;
let listeners = server::compile_listeners(raw_listeners, default_server_header, base_dir)?;
(listeners, default_server_names)
};
let upstreams = upstream::compile_upstreams(raw_upstreams, base_dir)?;
Expand Down
25 changes: 21 additions & 4 deletions crates/rginx-config/src/compile/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@ use std::net::IpAddr;
use std::path::Path;
use std::time::Duration;

use http::HeaderValue;
use ipnet::IpNet;
use rginx_core::{
AccessLogFormat, Error, Listener, ListenerHttp3, OcspConfig, OcspNonceMode,
OcspResponderPolicy, Result, Server, ServerCertificateBundle, ServerClientAuthMode,
ServerClientAuthPolicy, ServerTls, TlsCipherSuite, TlsKeyExchangeGroup, TlsVersion,
VirtualHostTls,
AccessLogFormat, DEFAULT_SERVER_HEADER, Error, Listener, ListenerHttp3, OcspConfig,
OcspNonceMode, OcspResponderPolicy, Result, Server, ServerCertificateBundle,
ServerClientAuthMode, ServerClientAuthPolicy, ServerTls, TlsCipherSuite, TlsKeyExchangeGroup,
TlsVersion, VirtualHostTls,
};

use crate::model::{
Expand Down Expand Up @@ -36,6 +37,7 @@ pub(super) fn compile_legacy_server(
) -> Result<CompiledServer> {
let ServerConfig {
listen,
server_header,
proxy_protocol,
default_certificate,
server_names,
Expand All @@ -56,6 +58,7 @@ pub(super) fn compile_legacy_server(
let compiled = compile_server_fields(
ServerFieldConfig {
listen,
server_header,
default_certificate,
trusted_proxies,
keep_alive,
Expand Down Expand Up @@ -93,6 +96,7 @@ pub(super) fn compile_legacy_server(
/// Compiles explicit listener blocks into runtime listener definitions.
pub(super) fn compile_listeners(
listeners: Vec<ListenerConfig>,
default_server_header: Option<String>,
base_dir: &Path,
) -> Result<Vec<Listener>> {
listeners
Expand All @@ -101,6 +105,7 @@ pub(super) fn compile_listeners(
let ListenerConfig {
name,
listen,
server_header,
proxy_protocol,
default_certificate,
trusted_proxies,
Expand All @@ -119,6 +124,7 @@ pub(super) fn compile_listeners(
let compiled = compile_server_fields(
ServerFieldConfig {
listen,
server_header: server_header.or_else(|| default_server_header.clone()),
default_certificate,
trusted_proxies,
keep_alive,
Expand Down Expand Up @@ -289,6 +295,7 @@ struct CompiledServerFields {

struct ServerFieldConfig {
listen: String,
server_header: Option<String>,
default_certificate: Option<String>,
trusted_proxies: Vec<String>,
keep_alive: Option<bool>,
Expand All @@ -309,6 +316,7 @@ fn compile_server_fields(
) -> Result<CompiledServerFields> {
let ServerFieldConfig {
listen,
server_header,
default_certificate,
trusted_proxies,
keep_alive,
Expand All @@ -326,6 +334,7 @@ fn compile_server_fields(
Ok(CompiledServerFields {
server: Server {
listen_addr: listen.parse()?,
server_header: compile_server_header(server_header)?,
default_certificate: compile_default_certificate(default_certificate),
trusted_proxies: compile_trusted_proxies(trusted_proxies)?,
keep_alive: keep_alive.unwrap_or(true),
Expand All @@ -342,6 +351,14 @@ fn compile_server_fields(
})
}

/// Parses the configured `Server` response header value, defaulting to `rginx`.
fn compile_server_header(server_header: Option<String>) -> Result<HeaderValue> {
let value = server_header.unwrap_or_else(|| DEFAULT_SERVER_HEADER.to_string());
let value = value.trim();
HeaderValue::from_str(value)
.map_err(|error| Error::Config(format!("server_header `{value}` is invalid: {error}")))
}

/// Normalizes the optional default certificate server name.
fn compile_default_certificate(default_certificate: Option<String>) -> Option<String> {
default_certificate.map(|name| name.trim().to_lowercase())
Expand Down
Loading