use std::net::IpAddr;
use std::str::FromStr;
use crate::NetworkConfig;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum IpSource {
DirectPeer,
TrustedHeader,
FallbackMissingHeader,
FallbackMalformed,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ResolvedClientIp {
pub ip: IpAddr,
pub source: IpSource,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Cidr {
V4(u32, u8),
V6(u128, u8),
}
impl Cidr {
fn parse(s: &str) -> Option<Cidr> {
let (addr_part, prefix_part) = match s.trim().split_once('/') {
Some((a, p)) => (a.trim(), Some(p.trim())),
None => (s.trim(), None),
};
match IpAddr::from_str(addr_part).ok()? {
IpAddr::V4(a) => {
let prefix = match prefix_part {
Some(p) => p.parse().ok().filter(|&p| p <= 32)?,
None => 32,
};
Some(Cidr::V4(u32::from(a), prefix))
}
IpAddr::V6(a) => {
let prefix = match prefix_part {
Some(p) => p.parse().ok().filter(|&p| p <= 128)?,
None => 128,
};
Some(Cidr::V6(u128::from(a), prefix))
}
}
}
fn contains(&self, ip: IpAddr) -> bool {
match (self, ip) {
(Cidr::V4(net, prefix), IpAddr::V4(addr)) => mask_eq_u32(*net, u32::from(addr), *prefix),
(Cidr::V6(net, prefix), IpAddr::V6(addr)) => {
mask_eq_u128(*net, u128::from(addr), *prefix)
}
_ => false,
}
}
}
fn mask_eq_u32(a: u32, b: u32, prefix: u8) -> bool {
let mask = match prefix {
0 => 0,
32 => u32::MAX,
p => u32::MAX << (32 - p),
};
(a & mask) == (b & mask)
}
fn mask_eq_u128(a: u128, b: u128, prefix: u8) -> bool {
let mask = match prefix {
0 => 0,
128 => u128::MAX,
p => u128::MAX << (128 - p),
};
(a & mask) == (b & mask)
}
pub fn is_valid_cidr(s: &str) -> bool {
Cidr::parse(s).is_some()
}
pub struct ClientIpResolver {
trusted: Vec<Cidr>,
header: String, hops: usize,
}
impl ClientIpResolver {
pub fn from_config(cfg: &NetworkConfig) -> Self {
let trusted = cfg.trusted_proxies.iter().filter_map(|s| Cidr::parse(s)).collect();
Self {
trusted,
header: cfg.client_ip_header.to_ascii_lowercase(),
hops: cfg.trusted_hops,
}
}
pub fn trusted_count(&self) -> usize {
self.trusted.len()
}
pub fn resolve(&self, peer: IpAddr, headers: &[(String, String)]) -> ResolvedClientIp {
if !self.trusted.iter().any(|c| c.contains(peer)) {
return ResolvedClientIp { ip: peer, source: IpSource::DirectPeer };
}
let Some(value) = self.header_value(headers) else {
return ResolvedClientIp { ip: peer, source: IpSource::FallbackMissingHeader };
};
let chain: Vec<&str> =
value.split(',').map(str::trim).filter(|s| !s.is_empty()).collect();
if self.hops == 0 || self.hops > chain.len() {
return ResolvedClientIp { ip: peer, source: IpSource::FallbackMalformed };
}
let idx = chain.len() - self.hops;
match IpAddr::from_str(chain[idx]) {
Ok(ip) => ResolvedClientIp { ip, source: IpSource::TrustedHeader },
Err(_) => ResolvedClientIp { ip: peer, source: IpSource::FallbackMalformed },
}
}
fn header_value<'a>(&self, headers: &'a [(String, String)]) -> Option<&'a str> {
headers
.iter()
.find(|(k, _)| k.eq_ignore_ascii_case(&self.header))
.map(|(_, v)| v.as_str())
}
}
#[cfg(test)]
mod tests {
use super::*;
fn resolver(trusted: &[&str], hops: usize) -> ClientIpResolver {
ClientIpResolver::from_config(&NetworkConfig {
trusted_proxies: trusted.iter().map(|s| s.to_string()).collect(),
client_ip_header: "x-forwarded-for".to_string(),
trusted_hops: hops,
})
}
fn ip(s: &str) -> IpAddr {
s.parse().unwrap()
}
fn xff(v: &str) -> Vec<(String, String)> {
vec![("x-forwarded-for".to_string(), v.to_string())]
}
#[test]
fn cidr_v4_prefixes() {
assert!(Cidr::parse("10.0.0.0/8").unwrap().contains(ip("10.255.1.2")));
assert!(!Cidr::parse("10.0.0.0/8").unwrap().contains(ip("11.0.0.1")));
assert!(Cidr::parse("127.0.0.1").unwrap().contains(ip("127.0.0.1")));
assert!(!Cidr::parse("127.0.0.1").unwrap().contains(ip("127.0.0.2")));
assert!(Cidr::parse("0.0.0.0/0").unwrap().contains(ip("203.0.113.7")));
}
#[test]
fn cidr_v6_prefixes() {
assert!(Cidr::parse("::1").unwrap().contains(ip("::1")));
assert!(!Cidr::parse("::1").unwrap().contains(ip("::2")));
assert!(Cidr::parse("fd00::/8").unwrap().contains(ip("fd12:3456::1")));
assert!(!Cidr::parse("fd00::/8").unwrap().contains(ip("fe80::1")));
assert!(Cidr::parse("::/0").unwrap().contains(ip("2001:db8::1")));
}
#[test]
fn cidr_family_mismatch_never_matches() {
assert!(!Cidr::parse("0.0.0.0/0").unwrap().contains(ip("::1")));
assert!(!Cidr::parse("::/0").unwrap().contains(ip("1.2.3.4")));
}
#[test]
fn invalid_cidr_is_skipped() {
let r = resolver(&["not-an-ip", "10.0.0.0/8", "10.0.0.0/99"], 1);
assert_eq!(r.trusted_count(), 1);
}
#[test]
fn untrusted_peer_uses_peer_ignoring_xff() {
let r = resolver(&["10.0.0.0/8"], 1);
let got = r.resolve(ip("203.0.113.9"), &xff("1.2.3.4"));
assert_eq!(got.ip, ip("203.0.113.9"));
assert_eq!(got.source, IpSource::DirectPeer);
}
#[test]
fn trusted_peer_takes_hop_from_right_not_leftmost() {
let r = resolver(&["10.0.0.0/8"], 1);
let got = r.resolve(ip("10.0.0.5"), &xff("attacker, 198.51.100.7"));
assert_eq!(got.ip, ip("198.51.100.7"));
assert_eq!(got.source, IpSource::TrustedHeader);
}
#[test]
fn empty_trusted_proxies_always_uses_peer() {
let r = resolver(&[], 1);
let got = r.resolve(ip("10.0.0.5"), &xff("1.2.3.4"));
assert_eq!(got.ip, ip("10.0.0.5"));
assert_eq!(got.source, IpSource::DirectPeer);
}
#[test]
fn malformed_xff_behind_trusted_falls_back_to_peer() {
let r = resolver(&["10.0.0.0/8"], 1);
let got = r.resolve(ip("10.0.0.5"), &xff("not-an-ip"));
assert_eq!(got.ip, ip("10.0.0.5"));
assert_eq!(got.source, IpSource::FallbackMalformed);
}
#[test]
fn missing_header_behind_trusted_falls_back_to_peer() {
let r = resolver(&["10.0.0.0/8"], 1);
let got = r.resolve(ip("10.0.0.5"), &[]);
assert_eq!(got.ip, ip("10.0.0.5"));
assert_eq!(got.source, IpSource::FallbackMissingHeader);
}
#[test]
fn chain_shorter_than_hops_never_picks_leftmost() {
let r = resolver(&["10.0.0.0/8"], 2);
let got = r.resolve(ip("10.0.0.5"), &xff("attacker"));
assert_eq!(got.ip, ip("10.0.0.5"));
assert_eq!(got.source, IpSource::FallbackMalformed);
}
#[test]
fn ipv6_trusted_peer_reads_xff() {
let r = resolver(&["::1"], 1);
let got = r.resolve(ip("::1"), &xff("198.51.100.7"));
assert_eq!(got.ip, ip("198.51.100.7"));
assert_eq!(got.source, IpSource::TrustedHeader);
}
#[test]
fn two_hops_selects_correct_position() {
let r = resolver(&["10.0.0.0/8"], 2);
let got = r.resolve(ip("10.0.0.5"), &xff("9.9.9.9, 198.51.100.7, 10.0.0.9"));
assert_eq!(got.ip, ip("198.51.100.7"));
assert_eq!(got.source, IpSource::TrustedHeader);
}
}