From a73321cb2790a9c5da97b616c67ebb27281e4aea Mon Sep 17 00:00:00 2001 From: Jun Kurihara Date: Fri, 4 Jul 2025 17:10:24 +0900 Subject: [PATCH] initial support forwarded header --- .gitignore | 1 + rpxy-lib/src/backend/upstream_opts.rs | 3 + .../handler_manipulate_messages.rs | 4 +- rpxy-lib/src/message_handler/utils_headers.rs | 140 +++++++++++++++++- 4 files changed, 142 insertions(+), 6 deletions(-) diff --git a/.gitignore b/.gitignore index 20b6fd3..6febdfa 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ .vscode .private +.tmp docker/log docker/cache docker/config diff --git a/rpxy-lib/src/backend/upstream_opts.rs b/rpxy-lib/src/backend/upstream_opts.rs index 68309ca..90ec7de 100644 --- a/rpxy-lib/src/backend/upstream_opts.rs +++ b/rpxy-lib/src/backend/upstream_opts.rs @@ -13,6 +13,8 @@ pub enum UpstreamOption { ForceHttp11Upstream, /// Force HTTP/2 upstream ForceHttp2Upstream, + /// Add RFC 7239 Forwarded header + ForwardedHeader, // TODO: Adds more options for heder override } impl TryFrom<&str> for UpstreamOption { @@ -24,6 +26,7 @@ impl TryFrom<&str> for UpstreamOption { "upgrade_insecure_requests" => Ok(Self::UpgradeInsecureRequests), "force_http11_upstream" => Ok(Self::ForceHttp11Upstream), "force_http2_upstream" => Ok(Self::ForceHttp2Upstream), + "forwarded_header" => Ok(Self::ForwardedHeader), _ => Err(RpxyError::UnsupportedUpstreamOption), } } diff --git a/rpxy-lib/src/message_handler/handler_manipulate_messages.rs b/rpxy-lib/src/message_handler/handler_manipulate_messages.rs index dc58486..1f925e2 100644 --- a/rpxy-lib/src/message_handler/handler_manipulate_messages.rs +++ b/rpxy-lib/src/message_handler/handler_manipulate_messages.rs @@ -87,7 +87,7 @@ where remove_connection_header(headers); // delete hop headers including header.connection remove_hop_header(headers); - // X-Forwarded-For + // X-Forwarded-For (and Forwarded if exists) add_forwarding_header(headers, client_addr, listen_addr, tls_enabled, &original_uri)?; // Add te: trailer if te_trailer @@ -126,7 +126,7 @@ where // apply upstream-specific headers given in upstream_option let headers = req.headers_mut(); - // apply upstream options to header + // apply upstream options to header, after X-Forwarded-For is added apply_upstream_options_to_header(headers, &upstream_chosen.uri, upstream_candidates)?; // update uri in request diff --git a/rpxy-lib/src/message_handler/utils_headers.rs b/rpxy-lib/src/message_handler/utils_headers.rs index fe351d9..f6efac2 100644 --- a/rpxy-lib/src/message_handler/utils_headers.rs +++ b/rpxy-lib/src/message_handler/utils_headers.rs @@ -96,6 +96,7 @@ fn override_host_header(headers: &mut HeaderMap, upstream_base_uri: &Uri) -> Res } /// Apply options to request header, which are specified in the configuration +/// This function is called after almost all other headers has been set and updated. pub(super) fn apply_upstream_options_to_header( headers: &mut HeaderMap, upstream_base_uri: &Uri, @@ -117,6 +118,22 @@ pub(super) fn apply_upstream_options_to_header( .entry(header::UPGRADE_INSECURE_REQUESTS) .or_insert(HeaderValue::from_bytes(b"1").unwrap()); } + UpstreamOption::ForwardedHeader => { + // This is called after X-Forwarded-For is added + // Generate RFC 7239 Forwarded header + let host = headers.get(header::HOST).and_then(|h| h.to_str().ok()).unwrap_or("unknown"); + let tls = upstream_base_uri.scheme_str() == Some("https"); + + match generate_forwarded_header(headers, tls, host) { + Ok(forwarded_value) => { + add_header_entry_overwrite_if_exist(headers, "forwarded", forwarded_value)?; + } + Err(e) => { + // Log warning but don't fail the request if Forwarded generation fails + warn!("Failed to generate Forwarded header: {}", e); + } + } + } _ => (), } } @@ -194,7 +211,9 @@ pub(super) fn make_cookie_single_line(headers: &mut HeaderMap) -> Result<()> { Ok(()) } -/// Add forwarding headers like `x-forwarded-for`. +/// Add or update forwarding headers like `x-forwarded-for`. +/// If only `forwarded` header exists, it will update `x-forwarded-for` with the proxy chain. +/// If both `x-forwarded-for` and `forwarded` headers exist, it will update `x-forwarded-for` first and then add `forwarded` header. pub(super) fn add_forwarding_header( headers: &mut HeaderMap, client_addr: &SocketAddr, @@ -202,10 +221,40 @@ pub(super) fn add_forwarding_header( tls: bool, uri_str: &str, ) -> Result<()> { - // default process - // optional process defined by upstream_option is applied in fn apply_upstream_options let canonical_client_addr = client_addr.to_canonical().ip().to_string(); - append_header_entry_with_comma(headers, "x-forwarded-for", &canonical_client_addr)?; + let has_forwarded = headers.contains_key("forwarded"); + let has_xff = headers.contains_key("x-forwarded-for"); + + // Handle incoming Forwarded header (Case 2: only Forwarded exists) + if has_forwarded && !has_xff { + // Extract proxy chain from Forwarded header and update X-Forwarded-For for consistency + update_xff_from_forwarded(headers, client_addr)?; + } else { + // Case 1: only X-Forwarded-For exists, or Case 3: both exist (conservative: use X-Forwarded-For) + // TODO: In future PR, implement proper RFC 7239 precedence + // where Forwarded header should take priority over X-Forwarded-For + // This requires careful testing to ensure no breaking changes + append_header_entry_with_comma(headers, "x-forwarded-for", &canonical_client_addr)?; + } + + // IMPORTANT: If Forwarded header exists, always update it for consistency + // This ensures headers remain consistent even when forwarded_header upstream option is not specified + if has_forwarded { + let host = headers + .get(header::HOST) + .and_then(|h| h.to_str().ok()) + .unwrap_or("unknown"); + + match generate_forwarded_header(headers, tls, host) { + Ok(forwarded_value) => { + add_header_entry_overwrite_if_exist(headers, "forwarded", forwarded_value)?; + } + Err(e) => { + // Log warning but don't fail the request if Forwarded generation fails + warn!("Failed to update existing Forwarded header for consistency: {}", e); + } + } + } // Single line cookie header // TODO: This should be only for HTTP/1.1. For 2+, this can be multi-lined. @@ -232,6 +281,89 @@ pub(super) fn add_forwarding_header( Ok(()) } +/// Extract proxy chain from existing Forwarded header +fn extract_forwarded_chain(headers: &HeaderMap) -> Vec { + headers + .get("forwarded") + .and_then(|h| h.to_str().ok()) + .map(|forwarded_str| { + // Parse Forwarded header entries (comma-separated) + forwarded_str + .split(',') + .flat_map(|entry| entry.split(';')) + .map(str::trim) + .filter_map(|param| param.strip_prefix("for=")) + .map(|for_value| { + // Remove quotes from IPv6 addresses for consistency with X-Forwarded-For + if let Some(ipv6) = for_value.strip_prefix("\"[").and_then(|s| s.strip_suffix("]\"")) { + ipv6.to_string() + } else { + for_value.to_string() + } + }) + .collect() + }) + .unwrap_or_default() +} + +/// Update X-Forwarded-For with proxy chain from Forwarded header for consistency +fn update_xff_from_forwarded(headers: &mut HeaderMap, client_addr: &SocketAddr) -> Result<()> { + let forwarded_chain = extract_forwarded_chain(headers); + + if !forwarded_chain.is_empty() { + // Replace X-Forwarded-For with the chain from Forwarded header + headers.remove("x-forwarded-for"); + for ip in forwarded_chain { + append_header_entry_with_comma(headers, "x-forwarded-for", &ip)?; + } + } + + // Append current client IP (standard behavior) + let canonical_client_addr = client_addr.to_canonical().ip().to_string(); + append_header_entry_with_comma(headers, "x-forwarded-for", &canonical_client_addr)?; + + Ok(()) +} + +/// Generate RFC 7239 Forwarded header from X-Forwarded-For +/// This function assumes that the X-Forwarded-For header is present and well-formed. +fn generate_forwarded_header(headers: &HeaderMap, tls: bool, host: &str) -> Result { + let for_values = headers + .get("x-forwarded-for") + .and_then(|h| h.to_str().ok()) + .map(|xff_str| { + xff_str + .split(',') + .map(str::trim) + .filter(|ip| !ip.is_empty()) + .map(|ip| { + // Format IP according to RFC 7239 (quote IPv6) + if ip.contains(':') { + format!("\"[{}]\"", ip) + } else { + ip.to_string() + } + }) + .collect::>() + .join(",for=") + }) + .unwrap_or_default(); + + if for_values.is_empty() { + return Err(anyhow!("No X-Forwarded-For header found for Forwarded generation")); + } + + // Build forwarded header value + let forwarded_value = format!( + "for={};proto={};host={}", + for_values, + if tls { "https" } else { "http" }, + host + ); + + Ok(forwarded_value) +} + /// Remove connection header pub(super) fn remove_connection_header(headers: &mut HeaderMap) { if let Some(values) = headers.get(header::CONNECTION) {