#[derive(Debug, Clone, PartialEq)]
#[doc(hidden)]
pub struct IpRange {
pub network: u128,
pub mask: u128,
}
#[derive(Debug, Clone, PartialEq)]
pub enum TrustedProxies {
All,
Ranges(Vec<IpRangeEntry>),
}
#[derive(Debug, Clone, PartialEq)]
pub struct IpRangeEntry(IpRange);
pub const DEFAULT_TRUSTED_PROXIES: &str = "127.0.0.0/8, ::1, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 100.64.0.0/10, fc00::/7";
impl TrustedProxies {
pub fn parse(list: &str) -> Result<TrustedProxies, String> {
let list = if list.trim().is_empty() { DEFAULT_TRUSTED_PROXIES } else { list.trim() };
if list == "*" {
return Ok(TrustedProxies::All);
}
if list.eq_ignore_ascii_case("none") {
return Ok(TrustedProxies::Ranges(Vec::new()));
}
list.split(',')
.map(str::trim)
.filter(|e| !e.is_empty())
.map(|e| IpRange::parse(e).map(IpRangeEntry).ok_or_else(|| format!("trusted_proxies: {e:?} is not an address or CIDR range")))
.collect::<Result<Vec<_>, _>>()
.map(TrustedProxies::Ranges)
}
pub fn trusts(&self, ip: &str) -> bool {
match self {
TrustedProxies::All => true,
TrustedProxies::Ranges(ranges) => parse_ip(ip).is_some_and(|a| ranges.iter().any(|r| (a & r.0.mask) == r.0.network)),
}
}
}
pub fn is_valid_entry(entry: &str) -> bool {
IpRange::parse(entry).is_some()
}
impl IpRange {
#[doc(hidden)]
pub fn parse(entry: &str) -> Option<Self> {
let entry = entry.trim();
let (addr, prefix_len) = match entry.split_once('/') {
Some((addr, len)) => {
let len: u32 = len.parse().ok()?;
let is_v4 = addr.parse::<std::net::Ipv4Addr>().is_ok();
let max = if is_v4 { 32 } else { 128 };
if len > max {
return None;
}
(addr, if is_v4 { len + 96 } else { len })
}
None => (entry, 128),
};
let network = parse_ip(addr)?;
let mask = if prefix_len == 0 { 0 } else { !0u128 << (128 - prefix_len) };
Some(IpRange { network: network & mask, mask })
}
}
#[doc(hidden)]
pub fn parse_ip(s: &str) -> Option<u128> {
let s = s.trim().trim_start_matches('[').trim_end_matches(']');
let s = s.split('%').next()?;
match s.parse::<std::net::IpAddr>().ok()? {
std::net::IpAddr::V4(v4) => Some(u128::from(v4.to_ipv6_mapped())),
std::net::IpAddr::V6(v6) => Some(u128::from(v6)),
}
}
pub fn client_ip<'a>(request: &crate::request::Request, peer: &'a str) -> std::borrow::Cow<'a, str> {
let config = crate::server_config::ServerConfig::current();
client_ip_with(request, peer, &config.trusted_proxies, config.client_ip_forwarded)
}
#[doc(hidden)]
pub fn client_ip_with<'a>(request: &crate::request::Request, peer: &'a str, trusted: &TrustedProxies, forwarded: bool) -> std::borrow::Cow<'a, str> {
use std::borrow::Cow;
if !forwarded || !trusted.trusts(peer) {
return Cow::Borrowed(peer);
}
let xff: Vec<&str> = request
.headers
.iter()
.filter(|h| h.name.eq_ignore_ascii_case("x-forwarded-for"))
.flat_map(|h| h.value.split(','))
.collect();
let hops: Vec<&str> = if xff.is_empty() {
request
.headers
.iter()
.filter(|h| h.name.eq_ignore_ascii_case("forwarded"))
.flat_map(|h| h.value.split(','))
.filter_map(|element| {
element.split(';').find_map(|pair| {
let (k, v) = pair.split_once('=')?;
k.trim().eq_ignore_ascii_case("for").then_some(v)
})
})
.collect()
} else {
xff
};
let mut chosen: Option<String> = None;
for hop in hops.iter().rev() {
let Some(ip) = hop_address(hop) else { break };
let trusted_hop = trusted.trusts(&ip);
chosen = Some(ip);
if !trusted_hop {
break;
}
}
chosen.map_or(Cow::Borrowed(peer), Cow::Owned)
}
fn hop_address(hop: &str) -> Option<String> {
let hop = hop.trim().trim_matches('"');
let bare = if let Some(rest) = hop.strip_prefix('[') {
rest.split(']').next()?
} else if hop.matches(':').count() == 1 {
hop.split(':').next()?
} else {
hop
};
bare.parse::<std::net::IpAddr>().ok().map(|ip| ip.to_string())
}
#[cfg(test)]
mod client_ip_tests {
use super::*;
use crate::header::Header;
use crate::request::Request;
fn with(headers: &[(&str, &str)]) -> Request {
Request {
method: "GET".into(),
request_uri: "/".into(),
http_version: "HTTP/1.1".into(),
headers: headers.iter().map(|(n, v)| Header::new(n.to_string(), v.to_string())).collect(),
body: Default::default(),
}
}
fn private() -> TrustedProxies {
TrustedProxies::parse("").unwrap()
}
#[test]
fn a_trusted_peer_gives_the_client_in_x_forwarded_for() {
let r = with(&[("X-Forwarded-For", "88.99.218.47, 172.30.0.3")]);
assert_eq!(client_ip_with(&r, "172.30.0.3", &private(), true), "88.99.218.47");
}
#[test]
fn a_client_cant_choose_its_address() {
let r = with(&[("X-Forwarded-For", "6.6.6.6,88.99.218.47, 172.30.0.3")]);
assert_eq!(client_ip_with(&r, "172.30.0.3", &private(), true), "88.99.218.47");
let r = with(&[("X-Forwarded-For", "10.9.9.9, 88.99.218.47")]);
assert_eq!(client_ip_with(&r, "10.0.0.1", &private(), true), "88.99.218.47");
}
#[test]
fn an_untrusted_peer_or_peer_mode_keeps_the_peer() {
let r = with(&[("X-Forwarded-For", "1.2.3.4")]);
assert_eq!(client_ip_with(&r, "88.99.218.47", &private(), true), "88.99.218.47");
assert_eq!(client_ip_with(&r, "10.0.0.1", &private(), false), "10.0.0.1");
assert_eq!(client_ip_with(&r, "10.0.0.1", &TrustedProxies::parse("none").unwrap(), true), "10.0.0.1");
}
#[test]
fn several_lines_ports_and_ipv6() {
let r = with(&[("X-Forwarded-For", "203.0.113.7:4711"), ("X-Forwarded-For", "10.0.0.2")]);
assert_eq!(client_ip_with(&r, "10.0.0.1", &private(), true), "203.0.113.7");
let r = with(&[("X-Forwarded-For", "[2001:db8::1]:443, fd00::2")]);
assert_eq!(client_ip_with(&r, "::1", &private(), true), "2001:db8::1");
}
#[test]
fn forwarded_when_there_is_no_x_forwarded_for() {
let r = with(&[("Forwarded", "for=\"[2001:db8::7]\";proto=https, for=10.0.0.9")]);
assert_eq!(client_ip_with(&r, "10.0.0.1", &private(), true), "2001:db8::7");
}
#[test]
fn garbage_or_only_trusted_hops() {
let r = with(&[("X-Forwarded-For", "203.0.113.7, unknown, 10.0.0.5")]);
assert_eq!(client_ip_with(&r, "10.0.0.1", &private(), true), "10.0.0.5");
let r = with(&[("X-Forwarded-For", "nonsense")]);
assert_eq!(client_ip_with(&r, "10.0.0.1", &private(), true), "10.0.0.1");
let r = with(&[]);
assert_eq!(client_ip_with(&r, "10.0.0.1", &private(), true), "10.0.0.1");
let r = with(&[("X-Forwarded-For", "10.1.1.1, 10.2.2.2")]);
assert_eq!(client_ip_with(&r, "10.0.0.1", &private(), true), "10.1.1.1");
}
}