use std::{net::IpAddr, sync::OnceLock};
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: String,
pub proto: String,
pub host: Option<String>,
pub real_ip: String,
}
impl TrustedProxies {
pub fn forwarded(
&self,
peer: IpAddr,
incoming: Incoming<'_>,
proto: &str,
host: Option<&str>,
) -> Forwarded {
let trusted = self.trusts(&peer);
let kept = |value: Option<&str>| {
value
.map(str::trim)
.filter(|value| trusted && !value.is_empty())
.map(ToString::to_string)
};
Forwarded {
forwarded_for: match kept(incoming.forwarded_for) {
Some(chain) => format!("{chain}, {peer}"),
None => peer.to_string(),
},
proto: kept(incoming.proto).unwrap_or_else(|| proto.to_string()),
host: kept(incoming.host).or_else(|| host.map(ToString::to_string)),
real_ip: self.client(peer, incoming.forwarded_for).to_string(),
}
}
}
pub fn forwarded(
peer: IpAddr,
incoming: Incoming<'_>,
proto: &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 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"));
}
#[test]
fn test_forwarded_headers_from_clients_are_replaced() {
let proxies = trusted(&["10.0.0.0/8"]);
let incoming = super::Incoming {
forwarded_for: Some("1.2.3.4"),
proto: Some("https"),
host: Some("forged.test"),
};
assert_eq!(
proxies.forwarded(ip("203.0.113.9"), incoming, "http", Some("a.test")),
super::Forwarded {
forwarded_for: "203.0.113.9".to_string(),
proto: "http".to_string(),
host: Some("a.test".to_string()),
real_ip: "203.0.113.9".to_string(),
}
);
}
#[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!(
proxies.forwarded(ip("10.0.0.1"), incoming, "http", Some("a.test")),
super::Forwarded {
forwarded_for: "203.0.113.9, 10.0.0.2, 10.0.0.1".to_string(),
proto: "https".to_string(),
host: Some("www.a.test".to_string()),
real_ip: "203.0.113.9".to_string(),
}
);
let forwarded =
proxies.forwarded(ip("10.0.0.1"), super::Incoming::default(), "https", None);
assert_eq!(forwarded.forwarded_for, "10.0.0.1");
assert_eq!(forwarded.proto, "https");
assert_eq!(forwarded.host, None);
}
}