v0.34.0_10

This commit is contained in:
Mateusz Gruszczyński committed 2026-10-18 09:24:03 +02:00
1 parent 8c4cbc583f
commit dd2502dd11
13 files changed
+389 -113

No files matched your search

+108 -45
View File
@@ -28,6 +28,8 @@ pub struct Context {
pub original_url: String,
values: HeaderMap,
}
include!("forwarding/peer_formatting.rs");
impl Settings {
pub fn compile(&self) -> Result<Compiled> {
ensure!(
@@ -231,24 +233,58 @@ impl Compiled {
uri: &Uri,
include_original_url: bool,
) -> Result<Context> {
let peer_ip = canonical(peer.ip());
self.context_with_peer(
incoming, peer, local, tls, authority, uri, include_original_url,
None, false,
)
}
/// Only the host/path validation and formatting data from the same request
/// and accepted connection are reused. Dynamic trust and XFF are evaluated
/// on every request, including after an in-place configuration reload.
#[allow(clippy::too_many_arguments)]
pub(crate) fn context_with_peer(
&self,
incoming: &HeaderMap,
peer: SocketAddr,
local: SocketAddr,
tls: bool,
authority: &HeaderValue,
uri: &Uri,
include_original_url: bool,
cached: Option<&PeerFormatting>,
validated_host: bool,
) -> Result<Context> {
let cached = cached.filter(|cached| cached.peer == peer && cached.local == local);
let peer_ip = cached.map_or_else(|| canonical(peer.ip()), |cached| cached.peer_ip);
let trusted_peer = self.trusted(peer_ip);
if !trusted_peer {
let scheme = if tls { "https" } else { "http" };
let authority_text = authority.to_str()?;
crate::config::request_host(authority_text)?;
if !validated_host { crate::config::request_host(authority_text)?; }
let target = uri.path_and_query().map(|v| v.as_str()).unwrap_or("/");
let original_url = if include_original_url {
format!("{scheme}://{authority_text}{target}")
} else {
String::new()
};
let peer_text = peer_ip.to_string();
let peer_header = HeaderValue::from_str(&peer_text)?;
let peer_node = if peer_ip.is_ipv4() { None } else { Some(node(peer_ip)) };
let peer_text = cached.is_none().then(|| peer_ip.to_string());
let ipv6_node = (cached.is_none() && peer_ip.is_ipv6()).then(|| node(peer_ip));
let peer_header = if let Some(cached) = cached {
cached.peer_header.clone()
} else {
HeaderValue::from_str(peer_text.as_deref().unwrap_or(""))?
};
let by_text = cached.is_none().then(|| node(canonical(local.ip())));
let peer_node = cached.map_or_else(
|| if peer_ip.is_ipv4() { peer_text.as_deref().unwrap_or("") }
else { ipv6_node.as_deref().unwrap_or("") },
|cached| cached.peer_node.as_str(),
);
let by_node = cached.map_or_else(|| by_text.as_deref().unwrap_or(""),
|cached| cached.by_node.as_str());
let forwarded = format!(
"for={};by={};proto={scheme};host=\"{authority_text}\"",
peer_node.as_deref().unwrap_or(&peer_text), node(canonical(local.ip()))
"for={peer_node};by={by_node};proto={scheme};host=\"{authority_text}\""
);
let mut values = HeaderMap::with_capacity(7);
values.insert("forwarded", forwarded.parse()?);
@@ -256,7 +292,11 @@ impl Compiled {
values.insert("x-real-ip", peer_header);
values.insert("x-forwarded-host", authority.clone());
values.insert("x-forwarded-proto", HeaderValue::from_static(scheme));
values.insert("x-forwarded-port", local.port().to_string().parse()?);
values.insert("x-forwarded-port", if let Some(cached) = cached {
cached.port_header.clone()
} else {
local.port().to_string().parse()?
});
values.insert("x-forwarded-uri", target.parse()?);
return Ok(Context {
client_ip: peer_ip,
@@ -287,29 +327,39 @@ impl Compiled {
}
let authority_text = authority.to_str()?;
// Validate before inserting authority into Forwarded's quoted host field.
crate::config::request_host(authority_text)?;
if !validated_host { crate::config::request_host(authority_text)?; }
let original_target = uri.path_and_query().map(|v| v.as_str()).unwrap_or("/");
let original_url = if include_original_url {
format!("{scheme}://{authority_text}{original_target}")
} else {
String::new()
};
let mut forwarded: Vec<_> = ips.iter().map(|ip| format!("for={}", node(*ip))).collect();
let last = forwarded.last_mut().expect("actual peer is always present");
last.push_str(&format!(
";by={};proto={scheme};host=\"{authority_text}\"",
node(canonical(local.ip()))
));
let mut values = HeaderMap::new();
values.insert("forwarded", forwarded.join(", ").parse()?);
values.insert(
"x-forwarded-for",
ips.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
.join(", ")
.parse()?,
);
use std::fmt::Write as _;
let mut forwarded = String::with_capacity(ips.len() * 52 + 80 + authority_text.len());
let mut xff = String::with_capacity(ips.len() * 44);
for (index, ip) in ips.iter().enumerate() {
if index > 0 { forwarded.push_str(", "); xff.push_str(", "); }
forwarded.push_str("for=");
if index + 1 == ips.len() {
if let Some(cached) = cached { forwarded.push_str(&cached.peer_node); }
else { forwarded.push_str(&node(*ip)); }
} else {
forwarded.push_str(&node(*ip));
}
if index + 1 == ips.len() {
if let Some(cached) = cached { xff.push_str(cached.peer_header.to_str()?); }
else { write!(&mut xff, "{ip}")?; }
} else {
write!(&mut xff, "{ip}")?;
}
}
forwarded.push_str(";by=");
if let Some(cached) = cached { forwarded.push_str(&cached.by_node); }
else { forwarded.push_str(&node(canonical(local.ip()))); }
write!(&mut forwarded, ";proto={scheme};host=\"{authority_text}\"")?;
let mut values = HeaderMap::with_capacity(7);
values.insert("forwarded", forwarded.parse()?);
values.insert("x-forwarded-for", xff.parse()?);
values.insert("x-real-ip", client_ip.to_string().parse()?);
values.insert("x-forwarded-host", authority.clone());
values.insert("x-forwarded-proto", HeaderValue::from_static(scheme));
@@ -321,7 +371,11 @@ impl Compiled {
} else {
local.port()
};
values.insert("x-forwarded-port", port.to_string().parse()?);
values.insert("x-forwarded-port", if cached.is_some() && !(trusted_peer && self.trust_proto) {
cached.expect("tested above").port_header.clone()
} else {
port.to_string().parse()?
});
values.insert("x-forwarded-uri", original_target.parse()?);
Ok(Context {
client_ip,
@@ -371,21 +425,35 @@ impl Context {
Ok(())
}
pub fn apply(&self, headers: &mut HeaderMap) {
let remove: Vec<_> = headers
.keys()
.filter(|name| {
name.as_str().starts_with("x-forwarded-")
|| matches!(
name.as_str(),
"forwarded" | "x-real-ip" | "x-original-url" | "x-original-method"
)
})
.cloned()
.collect();
for name in remove {
headers.remove(name);
self.apply_clean(headers, true);
}
fn apply_clean(&self, headers: &mut HeaderMap, include_client_ip: bool) {
// HeaderMap::insert replaces all values for a known canonical name.
// Remove unknown/spoofed forwarding names while retaining the valid ones.
// The normal request has no extra spoofed header names. Allocate
// a removal list only for requests which actually need sanitization.
let mut remove = None;
for name in headers.keys() {
let text = name.as_str();
if matches!(text, "x-original-url" | "x-original-method")
|| (text.starts_with("x-forwarded-")
&& !self.values.contains_key(text))
{
remove.get_or_insert_with(Vec::new).push(name.clone());
}
}
if let Some(remove) = remove {
for name in remove { headers.remove(name); }
}
if !include_client_ip {
for name in ["forwarded", "x-forwarded-for", "x-real-ip"] {
headers.remove(name);
}
}
for (name, value) in &self.values {
if !include_client_ip && matches!(name.as_str(), "forwarded" | "x-forwarded-for" | "x-real-ip") {
continue;
}
headers.insert(name.clone(), value.clone());
}
}
@@ -395,12 +463,7 @@ impl Context {
/// the visitor address so frameworks such as ProxyFix see the RustProxy socket peer.
/// Authentication verifiers use `apply` directly and still receive the real client IP.
pub fn apply_upstream(&self, headers: &mut HeaderMap, forward_client_ip: bool) {
self.apply(headers);
if !forward_client_ip {
headers.remove("forwarded");
headers.remove("x-forwarded-for");
headers.remove("x-real-ip");
}
self.apply_clean(headers, forward_client_ip);
}
}
#[cfg(test)]