//! Single trust boundary for TCP, TLS and QUIC. Incoming proxy headers are never //! authority for routing, listener selection or identity without a trusted peer. use anyhow::{ensure, Context as _, Result}; use http::{HeaderMap, HeaderValue, Uri}; use ipnet::IpNet; use serde::{Deserialize, Serialize}; use std::{ net::{IpAddr, SocketAddr}, sync::LazyLock, }; #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(default, deny_unknown_fields)] pub struct Settings { pub trusted_proxies: Vec, /// Enable only when the trusted last hop sanitizes X-Forwarded-Proto. pub trust_forwarded_proto: bool, } #[derive(Debug)] pub struct Compiled { networks: Vec, trust_proto: bool, } #[derive(Debug, Clone)] pub struct Context { pub client_ip: IpAddr, pub scheme: &'static str, pub original_url: String, values: HeaderMap, } impl Settings { pub fn compile(&self) -> Result { ensure!( self.trusted_proxies.len() <= 32, "at most 32 trusted proxy CIDRs" ); let networks = self .trusted_proxies .iter() .map(|value| { ensure!(value.len() <= 64, "trusted proxy CIDR is too long"); let net: IpNet = value.parse().context("invalid trusted proxy CIDR")?; ensure!( net.prefix_len() != 0, "trusting every address is not permitted; list your actual proxy networks" ); Ok(net) }) .collect::>>()?; Ok(Compiled { networks, trust_proto: self.trust_forwarded_proto, }) } } fn canonical(ip: IpAddr) -> IpAddr { crate::vhost::canonical_ip(ip) } const CLOUDFLARE_CIDRS: &[&str] = &[ "103.21.244.0/22", "103.22.200.0/22", "103.31.4.0/22", "104.16.0.0/13", "104.24.0.0/14", "108.162.192.0/18", "131.0.72.0/22", "141.101.64.0/18", "162.158.0.0/15", "172.64.0.0/13", "173.245.48.0/20", "188.114.96.0/20", "190.93.240.0/20", "197.234.240.0/22", "198.41.128.0/17", "2400:cb00::/32", "2606:4700::/32", "2803:f800::/32", "2405:b500::/32", "2405:8100::/32", "2a06:98c0::/29", "2c0f:f248::/32", ]; static CLOUDFLARE_NETWORKS: LazyLock> = LazyLock::new(|| { CLOUDFLARE_CIDRS .iter() .map(|value| value.parse().expect("static Cloudflare CIDR")) .collect() }); fn cloudflare_peer(peer: SocketAddr) -> bool { let peer = canonical(peer.ip()); CLOUDFLARE_NETWORKS .iter() .any(|network| network.contains(&peer)) } /// Return CF-Connecting-IP only when the TCP peer itself is in Cloudflare's /// published edge ranges. The header is otherwise untrusted user input. pub fn cloudflare_client_ip(headers: &HeaderMap, peer: SocketAddr) -> Option { if !cloudflare_peer(peer) { return None; } let mut values = headers.get_all("cf-connecting-ip").iter(); let value = values.next()?; if values.next().is_some() { return None; } value .to_str() .ok()? .trim() .parse::() .ok() .map(canonical) } /// Return a trusted ISO country code supplied by Cloudflare. `CF-IPCountry` is /// accepted only from a Cloudflare edge and only as one two-letter code. pub fn cloudflare_country(headers: &HeaderMap, peer: SocketAddr) -> Option { if !cloudflare_peer(peer) { return None; } let mut values = headers.get_all("cf-ipcountry").iter(); let value = values.next()?; if values.next().is_some() { return None; } let value = value.to_str().ok()?.trim(); if value.len() != 2 || !value.bytes().all(|byte| byte.is_ascii_alphabetic()) { return None; } let country = value.to_ascii_uppercase(); (country != "XX").then_some(country) } fn parse_node(value: &str) -> Option { let value = value.trim(); let value = if value.starts_with('"') { value.strip_prefix('"')?.strip_suffix('"')? } else { value }; if value.contains('\\') || value.contains('%') { return None; } value .parse::() .ok() .or_else(|| value.parse::().ok().map(|address| address.ip())) .or_else(|| { value .strip_prefix('[')? .strip_suffix(']')? .parse::() .ok() }) .map(canonical) } fn chain(headers: &HeaderMap) -> Option> { let xff = headers.contains_key("x-forwarded-for"); let name = if xff { "x-forwarded-for" } else { "forwarded" }; let mut result = Vec::new(); let mut bytes = 0; for value in headers.get_all(name) { bytes += value.as_bytes().len(); if bytes > 4096 { return None; } for item in value.to_str().ok()?.split(',') { if result.len() >= 32 { return None; } let ip = if xff { parse_node(item)? } else { let mut found = None; for parameter in item.split(';') { let (key, value) = parameter.split_once('=')?; if key.trim().eq_ignore_ascii_case("for") { if found.is_some() { return None; } found = Some(parse_node(value)?); } } found? }; result.push(ip); } } Some(result) } fn node(ip: IpAddr) -> String { match ip { IpAddr::V4(_) => ip.to_string(), IpAddr::V6(_) => format!("\"[{ip}]\""), } } impl Compiled { pub fn trusted(&self, ip: IpAddr) -> bool { self.networks.iter().any(|net| net.contains(&canonical(ip))) } /// Whether this peer may define the externally visible request scheme. /// Keeping this separate from `trusted` preserves the explicit /// `trust_forwarded_proto` opt-in used by the public proxy path. pub fn trusted_forwarded_proto(&self, ip: IpAddr) -> bool { self.trust_proto && self.trusted(ip) } pub fn context( &self, incoming: &HeaderMap, peer: SocketAddr, local: SocketAddr, tls: bool, authority: &HeaderValue, uri: &Uri, ) -> Result { let peer_ip = canonical(peer.ip()); let trusted_peer = self.trusted(peer_ip); let mut ips = if trusted_peer { chain(incoming).unwrap_or_default() } else { Vec::new() }; ips.push(peer_ip); // Discard everything left of the first untrusted hop walking from us. let first = ips.iter().rposition(|ip| !self.trusted(*ip)).unwrap_or(0); ips.drain(..first); let client_ip = ips[0]; let mut scheme = if tls { "https" } else { "http" }; if trusted_peer && self.trust_proto && incoming.get_all("x-forwarded-proto").iter().count() == 1 { scheme = match incoming .get("x-forwarded-proto") .and_then(|v| v.to_str().ok()) { Some("https") => "https", Some("http") => "http", _ => scheme, }; } let authority_text = authority.to_str()?; // Validate before inserting authority into Forwarded's quoted host field. crate::config::request_host(authority_text)?; let original_target = uri.path_and_query().map(|v| v.as_str()).unwrap_or("/"); let original_url = format!("{scheme}://{authority_text}{original_target}"); 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::>() .join(", ") .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)); let port = if trusted_peer && self.trust_proto { authority_text .parse::()? .port_u16() .unwrap_or(if scheme == "https" { 443 } else { 80 }) } else { local.port() }; values.insert("x-forwarded-port", port.to_string().parse()?); values.insert("x-forwarded-uri", original_target.parse()?); Ok(Context { client_ip, scheme, original_url, values, }) } } impl Context { /// Replace the effective client with a previously authenticated edge header. /// Existing proxy-chain metadata is retained behind the visitor address. pub fn override_client_ip(&mut self, client: IpAddr) -> Result<()> { let client = canonical(client); let client_text = client.to_string(); let existing_xff = self .values .get("x-forwarded-for") .and_then(|value| value.to_str().ok()) .unwrap_or(""); let already_first = existing_xff .split(',') .next() .and_then(parse_node) .is_some_and(|value| value == client); if !already_first { let xff = if existing_xff.is_empty() { client_text.clone() } else { format!("{client_text}, {existing_xff}") }; self.values.insert("x-forwarded-for", xff.parse()?); let existing = self .values .get("forwarded") .and_then(|value| value.to_str().ok()) .unwrap_or(""); let forwarded = if existing.is_empty() { format!("for={}", node(client)) } else { format!("for={}, {existing}", node(client)) }; self.values.insert("forwarded", forwarded.parse()?); } self.values.insert("x-real-ip", client_text.parse()?); self.client_ip = client; 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); } for (name, value) in &self.values { headers.insert(name.clone(), value.clone()); } } /// Apply trusted forwarding metadata to an application request. When client-IP /// forwarding is disabled, preserve scheme/host/port/URI metadata but withhold /// 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"); } } } #[cfg(test)] include!("forwarding/tests.rs");