use axum::extract::ConnectInfo;
use axum::http::Request;
use std::net::{IpAddr, SocketAddr};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TrustedProxy {
pub network: IpAddr,
pub prefix_len: u8,
}
impl TrustedProxy {
#[must_use]
pub fn parse(value: &str) -> Option<Self> {
let trimmed = value.trim();
if trimmed.is_empty() {
return None;
}
let (addr, prefix_len) = if let Some((addr, prefix)) = trimmed.split_once('/') {
let addr = addr.trim().parse::<IpAddr>().ok()?;
let prefix_len = prefix.trim().parse::<u8>().ok()?;
(addr, prefix_len)
} else {
let addr = trimmed.parse::<IpAddr>().ok()?;
let prefix_len = match addr {
IpAddr::V4(_) => 32,
IpAddr::V6(_) => 128,
};
(addr, prefix_len)
};
let max_prefix = match addr {
IpAddr::V4(_) => 32,
IpAddr::V6(_) => 128,
};
(prefix_len <= max_prefix).then_some(Self {
network: addr,
prefix_len,
})
}
#[must_use]
pub fn contains(&self, ip: IpAddr) -> bool {
if self.prefix_len == 0 {
return matches!(
(self.network, ip),
(IpAddr::V4(_), IpAddr::V4(_)) | (IpAddr::V6(_), IpAddr::V6(_))
);
}
match (self.network, ip) {
(IpAddr::V4(network), IpAddr::V4(candidate)) => {
let shift = 32_u8.saturating_sub(self.prefix_len);
(u32::from(network) >> shift) == (u32::from(candidate) >> shift)
}
(IpAddr::V6(network), IpAddr::V6(candidate)) => {
let shift = 128_u8.saturating_sub(self.prefix_len);
(u128::from(network) >> shift) == (u128::from(candidate) >> shift)
}
(IpAddr::V4(_), IpAddr::V6(_)) | (IpAddr::V6(_), IpAddr::V4(_)) => false,
}
}
}
pub fn extract_client_ip<B>(
req: &Request<B>,
trust_forwarded_headers: bool,
trusted_proxies: &[TrustedProxy],
trusted_proxies_configured: bool,
) -> Option<IpAddr> {
let peer_ip = req
.extensions()
.get::<ConnectInfo<SocketAddr>>()
.map(|ConnectInfo(addr)| addr.ip())
.or_else(|| req.extensions().get::<SocketAddr>().map(SocketAddr::ip));
if trust_forwarded_headers {
let allowed = if trusted_proxies_configured {
peer_ip.is_some_and(|ip| is_trusted_proxy(ip, trusted_proxies))
} else {
true
};
if allowed {
let all_xff: Vec<&str> = req
.headers()
.get_all("x-forwarded-for")
.iter()
.filter_map(|v| v.to_str().ok())
.collect();
if !all_xff.is_empty() {
let joined = all_xff.join(", ");
if let Some(xff_ip) = client_ip_from_x_forwarded_for(
&joined,
peer_ip,
trusted_proxies,
trusted_proxies_configured,
) {
return Some(xff_ip);
}
}
if let Some(real_ip) = req
.headers()
.get("x-real-ip")
.and_then(|v| v.to_str().ok())
.map(str::trim)
.filter(|s| !s.is_empty())
.and_then(|s| s.parse::<IpAddr>().ok())
{
return Some(real_ip);
}
}
}
peer_ip
}
fn client_ip_from_x_forwarded_for(
header: &str,
peer_ip: Option<IpAddr>,
trusted_proxies: &[TrustedProxy],
trusted_proxies_configured: bool,
) -> Option<IpAddr> {
if !trusted_proxies_configured {
let mut entries = header.rsplit(',').map(str::trim).filter(|s| !s.is_empty());
let last = entries.next()?;
if let Ok(last_ip) = last.parse::<IpAddr>() {
if peer_ip.is_some_and(|peer_ip| last_ip == peer_ip)
&& let Some(prev) = entries.next()
&& let Ok(prev_ip) = prev.parse::<IpAddr>()
{
return Some(prev_ip);
}
return Some(last_ip);
}
return None;
}
for entry in header.rsplit(',').map(str::trim).filter(|s| !s.is_empty()) {
if let Ok(ip) = entry.parse::<IpAddr>()
&& !is_trusted_proxy(ip, trusted_proxies)
{
return Some(ip);
}
}
None
}
fn is_trusted_proxy(ip: IpAddr, trusted_proxies: &[TrustedProxy]) -> bool {
trusted_proxies
.iter()
.any(|trusted_proxy| trusted_proxy.contains(ip))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_trusted_proxy_valid_ipv4_no_prefix() {
let proxy = TrustedProxy::parse("192.168.1.1").unwrap();
assert_eq!(proxy.network, "192.168.1.1".parse::<IpAddr>().unwrap());
assert_eq!(proxy.prefix_len, 32);
}
#[test]
fn parse_trusted_proxy_valid_ipv6_no_prefix() {
let proxy = TrustedProxy::parse("2001:db8::1").unwrap();
assert_eq!(proxy.network, "2001:db8::1".parse::<IpAddr>().unwrap());
assert_eq!(proxy.prefix_len, 128);
}
#[test]
fn parse_trusted_proxy_valid_ipv4_with_prefix() {
let proxy = TrustedProxy::parse("10.0.0.0/8").unwrap();
assert_eq!(proxy.network, "10.0.0.0".parse::<IpAddr>().unwrap());
assert_eq!(proxy.prefix_len, 8);
}
#[test]
fn parse_trusted_proxy_valid_ipv6_with_prefix() {
let proxy = TrustedProxy::parse("2001:db8::/32").unwrap();
assert_eq!(proxy.network, "2001:db8::".parse::<IpAddr>().unwrap());
assert_eq!(proxy.prefix_len, 32);
}
#[test]
fn parse_trusted_proxy_invalid_ip() {
assert!(TrustedProxy::parse("not.an.ip").is_none());
assert!(TrustedProxy::parse("256.256.256.256").is_none());
}
#[test]
fn parse_trusted_proxy_invalid_prefix() {
assert!(TrustedProxy::parse("192.168.1.1/33").is_none());
assert!(TrustedProxy::parse("2001:db8::/129").is_none());
assert!(TrustedProxy::parse("192.168.1.1/abc").is_none());
}
#[test]
fn parse_trusted_proxy_empty_or_whitespace() {
assert!(TrustedProxy::parse("").is_none());
assert!(TrustedProxy::parse(" ").is_none());
}
#[test]
fn contains_ipv4_exact_match() {
let proxy = TrustedProxy::parse("192.168.1.1").unwrap();
assert!(proxy.contains("192.168.1.1".parse().unwrap()));
assert!(!proxy.contains("192.168.1.2".parse().unwrap()));
}
#[test]
fn contains_ipv6_exact_match() {
let proxy = TrustedProxy::parse("2001:db8::1").unwrap();
assert!(proxy.contains("2001:db8::1".parse().unwrap()));
assert!(!proxy.contains("2001:db8::2".parse().unwrap()));
}
#[test]
fn contains_ipv4_cidr() {
let proxy = TrustedProxy::parse("10.0.0.0/8").unwrap();
assert!(proxy.contains("10.1.2.3".parse().unwrap()));
assert!(proxy.contains("10.255.255.255".parse().unwrap()));
assert!(!proxy.contains("11.0.0.0".parse().unwrap()));
}
#[test]
fn contains_ipv6_cidr() {
let proxy = TrustedProxy::parse("2001:db8::/32").unwrap();
assert!(proxy.contains("2001:db8:1234::1".parse().unwrap()));
assert!(!proxy.contains("2001:db9::1".parse().unwrap()));
}
#[test]
fn contains_zero_prefix() {
let proxy4 = TrustedProxy::parse("0.0.0.0/0").unwrap();
assert!(proxy4.contains("1.2.3.4".parse().unwrap()));
assert!(proxy4.contains("255.255.255.255".parse().unwrap()));
assert!(!proxy4.contains("2001:db8::1".parse().unwrap()));
let proxy6 = TrustedProxy::parse("::/0").unwrap();
assert!(proxy6.contains("2001:db8::1".parse().unwrap()));
assert!(proxy6.contains("::1".parse().unwrap()));
assert!(!proxy6.contains("1.2.3.4".parse().unwrap())); }
#[test]
fn contains_mismatched_family() {
let proxy4 = TrustedProxy::parse("192.168.1.0/24").unwrap();
assert!(!proxy4.contains("2001:db8::1".parse().unwrap()));
let proxy6 = TrustedProxy::parse("2001:db8::/32").unwrap();
assert!(!proxy6.contains("192.168.1.1".parse().unwrap()));
}
fn make_req_with_peer(ip: &str) -> Request<()> {
let mut req = Request::new(());
let addr = format!("{ip}:12345").parse::<SocketAddr>().unwrap();
req.extensions_mut().insert(ConnectInfo(addr));
req
}
#[test]
fn extract_ip_no_forwarded_headers_trust_disabled() {
let req = make_req_with_peer("192.168.1.1");
let ip = extract_client_ip(&req, false, &[], false);
assert_eq!(ip, Some("192.168.1.1".parse().unwrap()));
}
#[test]
fn extract_ip_no_forwarded_headers_trust_enabled() {
let req = make_req_with_peer("192.168.1.1");
let ip = extract_client_ip(&req, true, &[], false);
assert_eq!(ip, Some("192.168.1.1".parse().unwrap()));
}
#[test]
fn extract_ip_xff_untrusted_proxy() {
let mut req = make_req_with_peer("192.168.1.1");
req.headers_mut()
.insert("x-forwarded-for", "1.2.3.4".parse().unwrap());
let trusted = vec![TrustedProxy::parse("10.0.0.0/8").unwrap()];
let ip = extract_client_ip(&req, true, &trusted, true);
assert_eq!(ip, Some("192.168.1.1".parse().unwrap()));
}
#[test]
fn extract_ip_xff_trusted_proxy() {
let mut req = make_req_with_peer("10.0.0.1");
req.headers_mut()
.insert("x-forwarded-for", "1.2.3.4".parse().unwrap());
let trusted = vec![TrustedProxy::parse("10.0.0.0/8").unwrap()];
let ip = extract_client_ip(&req, true, &trusted, true);
assert_eq!(ip, Some("1.2.3.4".parse().unwrap()));
}
#[test]
fn extract_ip_xff_chain_trust_configured() {
let mut req = make_req_with_peer("10.0.0.1");
req.headers_mut()
.insert("x-forwarded-for", "1.2.3.4, 10.0.0.2".parse().unwrap());
let trusted = vec![TrustedProxy::parse("10.0.0.0/8").unwrap()];
let ip = extract_client_ip(&req, true, &trusted, true);
assert_eq!(ip, Some("1.2.3.4".parse().unwrap()));
}
#[test]
fn extract_ip_xff_chain_trust_not_configured() {
let mut req = make_req_with_peer("10.0.0.1");
req.headers_mut()
.insert("x-forwarded-for", "1.2.3.4, 10.0.0.1".parse().unwrap());
let ip = extract_client_ip(&req, true, &[], false);
assert_eq!(ip, Some("1.2.3.4".parse().unwrap()));
}
#[test]
fn extract_ip_x_real_ip_trusted() {
let mut req = make_req_with_peer("10.0.0.1");
req.headers_mut()
.insert("x-real-ip", "1.2.3.4".parse().unwrap());
let trusted = vec![TrustedProxy::parse("10.0.0.0/8").unwrap()];
let ip = extract_client_ip(&req, true, &trusted, true);
assert_eq!(ip, Some("1.2.3.4".parse().unwrap()));
}
}