initial support forwarded header
This commit is contained in:
parent
6e7f432aec
commit
a73321cb27
4 changed files with 142 additions and 6 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -1,5 +1,6 @@
|
||||||
.vscode
|
.vscode
|
||||||
.private
|
.private
|
||||||
|
.tmp
|
||||||
docker/log
|
docker/log
|
||||||
docker/cache
|
docker/cache
|
||||||
docker/config
|
docker/config
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,8 @@ pub enum UpstreamOption {
|
||||||
ForceHttp11Upstream,
|
ForceHttp11Upstream,
|
||||||
/// Force HTTP/2 upstream
|
/// Force HTTP/2 upstream
|
||||||
ForceHttp2Upstream,
|
ForceHttp2Upstream,
|
||||||
|
/// Add RFC 7239 Forwarded header
|
||||||
|
ForwardedHeader,
|
||||||
// TODO: Adds more options for heder override
|
// TODO: Adds more options for heder override
|
||||||
}
|
}
|
||||||
impl TryFrom<&str> for UpstreamOption {
|
impl TryFrom<&str> for UpstreamOption {
|
||||||
|
|
@ -24,6 +26,7 @@ impl TryFrom<&str> for UpstreamOption {
|
||||||
"upgrade_insecure_requests" => Ok(Self::UpgradeInsecureRequests),
|
"upgrade_insecure_requests" => Ok(Self::UpgradeInsecureRequests),
|
||||||
"force_http11_upstream" => Ok(Self::ForceHttp11Upstream),
|
"force_http11_upstream" => Ok(Self::ForceHttp11Upstream),
|
||||||
"force_http2_upstream" => Ok(Self::ForceHttp2Upstream),
|
"force_http2_upstream" => Ok(Self::ForceHttp2Upstream),
|
||||||
|
"forwarded_header" => Ok(Self::ForwardedHeader),
|
||||||
_ => Err(RpxyError::UnsupportedUpstreamOption),
|
_ => Err(RpxyError::UnsupportedUpstreamOption),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -87,7 +87,7 @@ where
|
||||||
remove_connection_header(headers);
|
remove_connection_header(headers);
|
||||||
// delete hop headers including header.connection
|
// delete hop headers including header.connection
|
||||||
remove_hop_header(headers);
|
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_forwarding_header(headers, client_addr, listen_addr, tls_enabled, &original_uri)?;
|
||||||
|
|
||||||
// Add te: trailer if te_trailer
|
// Add te: trailer if te_trailer
|
||||||
|
|
@ -126,7 +126,7 @@ where
|
||||||
|
|
||||||
// apply upstream-specific headers given in upstream_option
|
// apply upstream-specific headers given in upstream_option
|
||||||
let headers = req.headers_mut();
|
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)?;
|
apply_upstream_options_to_header(headers, &upstream_chosen.uri, upstream_candidates)?;
|
||||||
|
|
||||||
// update uri in request
|
// update uri in request
|
||||||
|
|
|
||||||
|
|
@ -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
|
/// 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(
|
pub(super) fn apply_upstream_options_to_header(
|
||||||
headers: &mut HeaderMap,
|
headers: &mut HeaderMap,
|
||||||
upstream_base_uri: &Uri,
|
upstream_base_uri: &Uri,
|
||||||
|
|
@ -117,6 +118,22 @@ pub(super) fn apply_upstream_options_to_header(
|
||||||
.entry(header::UPGRADE_INSECURE_REQUESTS)
|
.entry(header::UPGRADE_INSECURE_REQUESTS)
|
||||||
.or_insert(HeaderValue::from_bytes(b"1").unwrap());
|
.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(())
|
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(
|
pub(super) fn add_forwarding_header(
|
||||||
headers: &mut HeaderMap,
|
headers: &mut HeaderMap,
|
||||||
client_addr: &SocketAddr,
|
client_addr: &SocketAddr,
|
||||||
|
|
@ -202,10 +221,40 @@ pub(super) fn add_forwarding_header(
|
||||||
tls: bool,
|
tls: bool,
|
||||||
uri_str: &str,
|
uri_str: &str,
|
||||||
) -> Result<()> {
|
) -> 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();
|
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
|
// Single line cookie header
|
||||||
// TODO: This should be only for HTTP/1.1. For 2+, this can be multi-lined.
|
// 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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Extract proxy chain from existing Forwarded header
|
||||||
|
fn extract_forwarded_chain(headers: &HeaderMap) -> Vec<String> {
|
||||||
|
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<String> {
|
||||||
|
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::<Vec<_>>()
|
||||||
|
.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
|
/// Remove connection header
|
||||||
pub(super) fn remove_connection_header(headers: &mut HeaderMap) {
|
pub(super) fn remove_connection_header(headers: &mut HeaderMap) {
|
||||||
if let Some(values) = headers.get(header::CONNECTION) {
|
if let Some(values) = headers.get(header::CONNECTION) {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue