use std::{net::IpAddr, sync::OnceLock};
use http::HeaderValue;
use ipnet::IpNet;
#[derive(Debug, Default, Clone, PartialEq)]
pub struct TrustedProxies(Vec<IpNet>);
static TRUSTED: OnceLock<TrustedProxies> = OnceLock::new();
impl TrustedProxies {
pub fn parse(proxies: &[String]) -> Result<Self, String> {
proxies
.iter()
.map(|proxy| {
proxy
.parse::<IpNet>()
.or_else(|_| proxy.parse::<IpAddr>().map(IpNet::from))
.map_err(|_| {
format!(
"trusted_proxies: {proxy:?} is not an address or a network (10.0.0.0/8)"
)
})
})
.collect::<Result<Vec<_>, _>>()
.map(Self)
}
fn trusts(&self, ip: &IpAddr) -> bool {
self.0.iter().any(|network| network.contains(ip))
}
pub fn client(&self, peer: IpAddr, forwarded_for: Option<&str>) -> IpAddr {
if !self.trusts(&peer) {
return peer;
}
let mut client = peer;
for hop in forwarded_for.unwrap_or_default().rsplit(',') {
let Some(ip) = parse_hop(hop.trim()) else {
break;
};
client = ip;
if !self.trusts(&ip) {
break;
}
}
client
}
}
fn parse_hop(hop: &str) -> Option<IpAddr> {
hop.parse::<IpAddr>()
.ok()
.or_else(|| hop.parse::<std::net::SocketAddr>().ok().map(|addr| addr.ip()))
}
#[derive(Debug, Default, Clone, Copy)]
pub struct Incoming<'a> {
pub forwarded_for: Option<&'a str>,
pub proto: Option<&'a str>,
pub host: Option<&'a str>,
}
#[derive(Debug, PartialEq)]
#[allow(clippy::struct_field_names)] pub struct Forwarded {
pub forwarded_for: HeaderValue,
pub proto: HeaderValue,
pub host: Option<HeaderValue>,
pub real_ip: HeaderValue,
}
impl TrustedProxies {
pub fn forwarded<'a>(
&self,
peer: IpAddr,
incoming: impl FnOnce() -> Incoming<'a>,
proto: &'static str,
host: Option<&str>,
) -> Forwarded {
let host = host.and_then(|host| HeaderValue::from_str(host).ok());
if !self.trusts(&peer) {
let peer = ip_value(peer);
return Forwarded {
forwarded_for: peer.clone(),
proto: HeaderValue::from_static(proto),
host,
real_ip: peer,
};
}
let incoming = incoming();
let kept = |value: Option<&str>| {
value
.map(str::trim)
.filter(|value| !value.is_empty())
.and_then(|value| HeaderValue::from_str(value).ok())
};
Forwarded {
forwarded_for: incoming
.forwarded_for
.map(str::trim)
.filter(|chain| !chain.is_empty())
.and_then(|chain| HeaderValue::from_str(&format!("{chain}, {peer}")).ok())
.unwrap_or_else(|| ip_value(peer)),
proto: kept(incoming.proto).unwrap_or_else(|| HeaderValue::from_static(proto)),
host: kept(incoming.host).or(host),
real_ip: ip_value(self.client(peer, incoming.forwarded_for)),
}
}
}
fn ip_value(ip: IpAddr) -> HeaderValue {
use std::io::Write;
let mut buffer = [0u8; 64];
let mut cursor = std::io::Cursor::new(&mut buffer[..]);
let _ = write!(cursor, "{ip}");
let length = usize::try_from(cursor.position()).unwrap_or_default();
buffer
.get(..length)
.and_then(|text| HeaderValue::from_bytes(text).ok())
.unwrap_or_else(|| HeaderValue::from_static(""))
}
pub fn forwarded<'a>(
peer: IpAddr,
incoming: impl FnOnce() -> Incoming<'a>,
proto: &'static str,
host: Option<&str>,
) -> Forwarded {
TRUSTED
.get()
.unwrap_or(&TrustedProxies(vec![]))
.forwarded(peer, incoming, proto, host)
}
pub fn init(proxies: TrustedProxies) {
let _ = TRUSTED.set(proxies);
}
pub fn of_session(session: &pingora::proxy::Session) -> Option<IpAddr> {
let peer = session.client_addr()?.as_inet()?.ip();
let forwarded_for = session
.req_header()
.headers
.get("x-forwarded-for")
.and_then(|value| value.to_str().ok());
Some(client_ip(peer, forwarded_for))
}
pub fn client_ip(peer: IpAddr, forwarded_for: Option<&str>) -> IpAddr {
match TRUSTED.get() {
Some(trusted) => trusted.client(peer, forwarded_for),
None => peer,
}
}
#[cfg(test)]
mod tests {
use std::net::IpAddr;
use super::TrustedProxies;
fn ip(value: &str) -> IpAddr {
value.parse().unwrap()
}
fn trusted(proxies: &[&str]) -> TrustedProxies {
TrustedProxies::parse(&proxies.iter().map(ToString::to_string).collect::<Vec<_>>())
.unwrap()
}
#[test]
fn test_parse() {
assert!(TrustedProxies::parse(&["10.0.0.0/8".into(), "127.0.0.1".into(), "::1".into()]).is_ok());
assert_eq!(
TrustedProxies::parse(&["10.0.0".into()]),
Err("trusted_proxies: \"10.0.0\" is not an address or a network (10.0.0.0/8)".to_string())
);
}
#[test]
fn test_untrusted_connections_are_the_client() {
let proxies = trusted(&["10.0.0.0/8"]);
assert_eq!(proxies.client(ip("203.0.113.9"), Some("1.2.3.4")), ip("203.0.113.9"));
assert_eq!(TrustedProxies::default().client(ip("10.0.0.1"), Some("1.2.3.4")), ip("10.0.0.1"));
}
#[test]
fn test_behind_trusted_proxies() {
let proxies = trusted(&["10.0.0.0/8", "192.168.1.1"]);
assert_eq!(proxies.client(ip("10.0.0.1"), Some("203.0.113.9")), ip("203.0.113.9"));
assert_eq!(
proxies.client(ip("10.0.0.1"), Some("203.0.113.9, 192.168.1.1, 10.1.1.1")),
ip("203.0.113.9")
);
assert_eq!(
proxies.client(ip("10.0.0.1"), Some("1.2.3.4, 203.0.113.9")),
ip("203.0.113.9")
);
assert_eq!(proxies.client(ip("10.0.0.1"), Some(" 2001:db8::1 ")), ip("2001:db8::1"));
assert_eq!(proxies.client(ip("10.0.0.1"), Some("203.0.113.9:4711")), ip("203.0.113.9"));
assert_eq!(proxies.client(ip("10.0.0.1"), Some("[2001:db8::1]:4711")), ip("2001:db8::1"));
}
#[test]
fn test_without_a_usable_header() {
let proxies = trusted(&["10.0.0.0/8"]);
assert_eq!(proxies.client(ip("10.0.0.1"), None), ip("10.0.0.1"));
assert_eq!(proxies.client(ip("10.0.0.1"), Some("")), ip("10.0.0.1"));
assert_eq!(proxies.client(ip("10.0.0.1"), Some("10.0.0.2, 10.0.0.3")), ip("10.0.0.2"));
assert_eq!(proxies.client(ip("10.0.0.1"), Some("unknown, 10.0.0.2")), ip("10.0.0.2"));
}
fn text(value: &http::HeaderValue) -> &str {
value.to_str().unwrap()
}
fn headers(forwarded: &super::Forwarded) -> (&str, &str, Option<&str>, &str) {
(
text(&forwarded.forwarded_for),
text(&forwarded.proto),
forwarded.host.as_ref().map(text),
text(&forwarded.real_ip),
)
}
#[test]
fn test_forwarded_headers_from_clients_are_replaced() {
let proxies = trusted(&["10.0.0.0/8"]);
let forwarded = proxies.forwarded(
ip("203.0.113.9"),
|| unreachable!("headers read for a client that isn't a trusted proxy"),
"http",
Some("a.test"),
);
assert_eq!(
headers(&forwarded),
("203.0.113.9", "http", Some("a.test"), "203.0.113.9")
);
let forwarded =
proxies.forwarded(ip("2001:db8::1"), super::Incoming::default, "https", None);
assert_eq!(headers(&forwarded), ("2001:db8::1", "https", None, "2001:db8::1"));
}
#[test]
fn test_forwarded_headers_from_trusted_proxies_are_kept() {
let proxies = trusted(&["10.0.0.0/8"]);
let incoming = || super::Incoming {
forwarded_for: Some("203.0.113.9, 10.0.0.2"),
proto: Some("https"),
host: Some("www.a.test"),
};
assert_eq!(
headers(&proxies.forwarded(ip("10.0.0.1"), incoming, "http", Some("a.test"))),
(
"203.0.113.9, 10.0.0.2, 10.0.0.1",
"https",
Some("www.a.test"),
"203.0.113.9"
)
);
let forwarded =
proxies.forwarded(ip("10.0.0.1"), super::Incoming::default, "https", None);
assert_eq!(headers(&forwarded), ("10.0.0.1", "https", None, "10.0.0.1"));
}
}