use std::net::{IpAddr, SocketAddr};
use axum::extract::ConnectInfo;
use axum::http::HeaderMap;
use ipnet::IpNet;
#[must_use]
pub fn parse_trusted_proxies(raw: &[String]) -> Vec<IpNet> {
raw.iter()
.filter_map(|s| {
let trimmed = s.trim();
if trimmed.is_empty() {
return None;
}
if let Ok(net) = trimmed.parse::<IpNet>() {
return Some(net);
}
if let Ok(addr) = trimmed.parse::<IpAddr>() {
let prefix = match addr {
IpAddr::V4(_) => 32,
IpAddr::V6(_) => 128,
};
if let Ok(net) = IpNet::new(addr, prefix) {
return Some(net);
}
}
tracing::warn!(entry = %trimmed, "ignoring invalid trusted_proxies entry");
None
})
.collect()
}
fn is_trusted(addr: IpAddr, trusted: &[IpNet]) -> bool {
trusted.iter().any(|net| net.contains(&addr))
}
#[must_use]
pub fn resolve_client_ip(
headers: &HeaderMap,
connect_info: Option<&ConnectInfo<SocketAddr>>,
trusted: &[IpNet],
) -> Option<IpAddr> {
let peer_ip = connect_info.map(|c| c.0.ip())?;
if !is_trusted(peer_ip, trusted) {
return Some(peer_ip);
}
if let Some(xff) = headers.get("x-forwarded-for").and_then(|v| v.to_str().ok()) {
let hops: Vec<&str> = xff
.split(',')
.map(str::trim)
.filter(|s| !s.is_empty())
.collect();
for hop in hops.iter().rev() {
if let Ok(addr) = hop.parse::<IpAddr>()
&& !is_trusted(addr, trusted)
{
return Some(addr);
}
}
}
for header in ["x-real-ip", "cf-connecting-ip"] {
if let Some(raw) = headers.get(header).and_then(|v| v.to_str().ok())
&& let Ok(addr) = raw.trim().parse::<IpAddr>()
&& !is_trusted(addr, trusted)
{
return Some(addr);
}
}
Some(peer_ip)
}