use std::{
borrow::Cow,
net::{IpAddr, Ipv4Addr, Ipv6Addr},
};
use http::{HeaderMap, HeaderName, header};
use ipnet::{IpNet, Ipv4Net, Ipv6Net};
use crate::{RemoteAddr, request::Request};
#[derive(Debug, Clone, Copy)]
pub(crate) struct ClientIp(pub(crate) Option<IpAddr>);
#[derive(Debug, Clone)]
pub struct TrustedProxies {
networks: Vec<IpNet>,
nearest: usize,
header: ForwardedHeader,
}
impl TrustedProxies {
#[must_use]
pub fn new() -> Self {
Self {
networks: Vec::new(),
nearest: 0,
header: ForwardedHeader::XForwardedFor,
}
}
#[must_use]
#[track_caller]
pub fn networks<I>(mut self, networks: I) -> Self
where
I: IntoIterator,
I::Item: IntoIpNet,
{
const MAPPED_PREFIX: u8 = 96;
for network in networks {
let network = match network.into_ip_net() {
IpNet::V6(v6) if v6.prefix_len() >= MAPPED_PREFIX => match v6.addr().to_canonical()
{
IpAddr::V4(addr) => Ipv4Net::new(addr, v6.prefix_len() - MAPPED_PREFIX)
.map_or(IpNet::V6(v6), IpNet::V4),
IpAddr::V6(_) => IpNet::V6(v6),
},
network => network,
};
self.networks.push(network);
}
self
}
#[must_use]
pub fn nearest(mut self, count: usize) -> Self {
self.nearest = count;
self
}
#[must_use]
pub fn header(mut self, header: ForwardedHeader) -> Self {
self.header = header;
self
}
pub(crate) fn resolve(&self, request: &Request) -> Option<IpAddr> {
let remote = request
.extensions()
.get::<RemoteAddr>()
.map(|remote| remote.0.ip());
self.client_ip(remote, request.headers())
}
pub(crate) fn client_ip(&self, remote: Option<IpAddr>, headers: &HeaderMap) -> Option<IpAddr> {
let remote = remote.map(|ip| ip.to_canonical());
if !self.is_trusted(remote, 0) {
return remote;
}
let values = headers.get_all(self.header.name()).iter();
match self.header {
ForwardedHeader::Single(_) => {
let mut values = values;
let value = values.next()?;
if values.next().is_some() {
return None;
}
parse_node(value.to_str().ok()?.trim())
}
ForwardedHeader::Forwarded => {
let fields: Vec<&[u8]> = values
.map(|value| {
let field = value.as_bytes();
is_well_quoted(field).then_some(field)
})
.collect::<Option<_>>()?;
let addresses: Vec<Option<IpAddr>> = fields
.into_iter()
.flat_map(|field| {
split_quoted(field, b',')
.filter(|element| !element.trim_ascii().is_empty())
.map(|element| {
forwarded_for(element).and_then(|node| parse_node(&node))
})
})
.collect();
self.walk(remote, addresses.into_iter().rev())
}
ForwardedHeader::XForwardedFor => {
let entries = values.rev().flat_map(|value| {
value
.as_bytes()
.split(|&byte| byte == b',')
.map(<[u8]>::trim_ascii)
.filter(|entry| !entry.is_empty())
.rev()
});
self.walk(
remote,
entries.map(|entry| str::from_utf8(entry).ok().and_then(parse_node)),
)
}
}
}
fn walk(
&self,
remote: Option<IpAddr>,
addresses: impl Iterator<Item = Option<IpAddr>>,
) -> Option<IpAddr> {
let mut farthest = remote;
for (hop, address) in addresses.enumerate() {
let hop = hop + 1;
if hop >= self.nearest {
let ip = address?;
if !self.is_trusted(Some(ip), hop) {
return Some(ip);
}
}
farthest = address;
}
farthest
}
fn is_trusted(&self, addr: Option<IpAddr>, hop: usize) -> bool {
hop < self.nearest
|| addr.is_some_and(|ip| self.networks.iter().any(|network| network.contains(&ip)))
}
}
impl Default for TrustedProxies {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ForwardedHeader {
XForwardedFor,
Forwarded,
Single(HeaderName),
}
impl ForwardedHeader {
fn name(&self) -> &HeaderName {
static X_FORWARDED_FOR: HeaderName = HeaderName::from_static("x-forwarded-for");
match self {
Self::XForwardedFor => &X_FORWARDED_FOR,
Self::Forwarded => &header::FORWARDED,
Self::Single(name) => name,
}
}
}
pub trait IntoIpNet {
#[track_caller]
fn into_ip_net(self) -> IpNet;
}
impl IntoIpNet for &str {
#[track_caller]
fn into_ip_net(self) -> IpNet {
self.parse::<IpNet>()
.or_else(|_| self.parse::<IpAddr>().map(IpNet::from))
.unwrap_or_else(|_| panic!("invalid trusted proxy network `{self}`"))
}
}
impl IntoIpNet for String {
#[track_caller]
fn into_ip_net(self) -> IpNet {
self.as_str().into_ip_net()
}
}
impl IntoIpNet for IpNet {
fn into_ip_net(self) -> IpNet {
self
}
}
impl IntoIpNet for Ipv4Net {
fn into_ip_net(self) -> IpNet {
IpNet::V4(self)
}
}
impl IntoIpNet for Ipv6Net {
fn into_ip_net(self) -> IpNet {
IpNet::V6(self)
}
}
impl IntoIpNet for IpAddr {
fn into_ip_net(self) -> IpNet {
IpNet::from(self)
}
}
impl<T> IntoIpNet for &T
where
T: IntoIpNet + Clone,
{
#[track_caller]
fn into_ip_net(self) -> IpNet {
self.clone().into_ip_net()
}
}
fn parse_node(value: &str) -> Option<IpAddr> {
if let Ok(ip) = value.parse::<IpAddr>() {
return Some(ip.to_canonical());
}
let (ip, port) = if let Some(rest) = value.strip_prefix('[') {
let (ip, rest) = rest.split_once(']')?;
let ip = IpAddr::V6(ip.parse::<Ipv6Addr>().ok()?);
if rest.is_empty() {
return Some(ip.to_canonical());
}
(ip, rest.strip_prefix(':')?)
} else {
let (ip, port) = value.rsplit_once(':')?;
(IpAddr::V4(ip.parse::<Ipv4Addr>().ok()?), port)
};
is_node_port(port).then(|| ip.to_canonical())
}
fn is_node_port(value: &str) -> bool {
match value.strip_prefix('_') {
Some(obfuscated) => {
!obfuscated.is_empty()
&& obfuscated
.chars()
.all(|c| c.is_ascii_alphanumeric() || matches!(c, '.' | '_' | '-'))
}
None => (1..=5).contains(&value.len()) && value.bytes().all(|b| b.is_ascii_digit()),
}
}
fn forwarded_for(element: &[u8]) -> Option<Cow<'_, str>> {
let mut found = None;
for pair in split_quoted(element, b';') {
let pair = str::from_utf8(pair).ok()?;
if pair.trim().is_empty() {
continue;
}
let (name, value) = pair.split_once('=')?;
if name.trim().eq_ignore_ascii_case("for") {
if found.is_some() {
return None;
}
found = Some(unquote(value.trim())?);
}
}
found
}
#[derive(Default)]
struct QuoteState {
quoted: bool,
escaped: bool,
}
impl QuoteState {
fn feed(&mut self, byte: u8) -> bool {
if self.escaped {
self.escaped = false;
} else if self.quoted {
match byte {
b'\\' => self.escaped = true,
b'"' => self.quoted = false,
_ => {}
}
} else if byte == b'"' {
self.quoted = true;
} else {
return true;
}
false
}
fn is_closed(&self) -> bool {
!self.quoted && !self.escaped
}
}
fn is_well_quoted(value: &[u8]) -> bool {
let mut state = QuoteState::default();
for &byte in value {
state.feed(byte);
}
state.is_closed()
}
fn split_quoted(value: &[u8], delimiter: u8) -> impl Iterator<Item = &[u8]> {
let mut state = QuoteState::default();
value.split(move |&byte| state.feed(byte) && byte == delimiter)
}
fn unquote(value: &str) -> Option<Cow<'_, str>> {
let Some(rest) = value.strip_prefix('"') else {
return Some(Cow::Borrowed(value));
};
let inner = rest.strip_suffix('"')?;
if !inner.contains('\\') {
return Some(Cow::Borrowed(inner));
}
let mut unescaped = String::with_capacity(inner.len());
let mut chars = inner.chars();
while let Some(c) = chars.next() {
match c {
'\\' => unescaped.push(chars.next()?),
c => unescaped.push(c),
}
}
Some(Cow::Owned(unescaped))
}
#[cfg(test)]
mod tests {
use http::HeaderValue;
use super::*;
fn ip(value: &str) -> IpAddr {
value.parse().unwrap()
}
fn headers(values: &[(&str, &str)]) -> HeaderMap {
let mut headers = HeaderMap::new();
for (name, value) in values {
headers.append(
HeaderName::from_bytes(name.as_bytes()).unwrap(),
HeaderValue::from_str(value).unwrap(),
);
}
headers
}
fn resolve(proxies: &TrustedProxies, remote: Option<&str>, xff: &str) -> Option<IpAddr> {
proxies.client_ip(remote.map(ip), &headers(&[("x-forwarded-for", xff)]))
}
#[test]
fn without_trusted_proxies_the_peer_is_the_client() {
let proxies = TrustedProxies::new();
assert_eq!(
resolve(&proxies, Some("203.0.113.9"), "198.51.100.1"),
Some(ip("203.0.113.9"))
);
}
#[test]
fn without_a_peer_or_trusted_proxies_the_client_is_unknown() {
let proxies = TrustedProxies::new();
assert_eq!(resolve(&proxies, None, "198.51.100.1"), None);
}
#[test]
fn a_trusted_peer_reports_the_client() {
let proxies = TrustedProxies::new().networks(["10.0.0.0/8"]);
assert_eq!(
resolve(&proxies, Some("10.1.2.3"), "198.51.100.1"),
Some(ip("198.51.100.1"))
);
}
#[test]
fn an_untrusted_peer_is_the_client_whatever_the_header_says() {
let proxies = TrustedProxies::new().networks(["10.0.0.0/8"]);
assert_eq!(
resolve(&proxies, Some("203.0.113.9"), "198.51.100.1"),
Some(ip("203.0.113.9"))
);
}
#[test]
fn the_walk_skips_every_trusted_hop() {
let proxies = TrustedProxies::new().networks(["10.0.0.0/8", "192.0.2.0/24"]);
assert_eq!(
resolve(&proxies, Some("10.0.0.1"), "198.51.100.1, 192.0.2.7"),
Some(ip("198.51.100.1"))
);
}
#[test]
fn the_walk_stops_at_the_first_untrusted_address() {
let proxies = TrustedProxies::new().networks(["10.0.0.0/8"]);
assert_eq!(
resolve(&proxies, Some("10.0.0.1"), "1.1.1.1, 198.51.100.1"),
Some(ip("198.51.100.1"))
);
}
#[test]
fn a_chain_of_trusted_addresses_resolves_to_the_farthest() {
let proxies = TrustedProxies::new().networks(["10.0.0.0/8"]);
assert_eq!(
resolve(&proxies, Some("10.0.0.1"), "10.0.0.5, 10.0.0.3"),
Some(ip("10.0.0.5"))
);
}
#[test]
fn a_trusted_peer_without_a_header_is_the_client() {
let proxies = TrustedProxies::new().networks(["10.0.0.0/8"]);
assert_eq!(
proxies.client_ip(Some(ip("10.0.0.1")), &HeaderMap::new()),
Some(ip("10.0.0.1"))
);
}
#[test]
fn a_single_address_network_matches_that_address_only() {
let proxies = TrustedProxies::new().networks(["10.0.0.1"]);
assert_eq!(
resolve(&proxies, Some("10.0.0.1"), "198.51.100.1"),
Some(ip("198.51.100.1"))
);
assert_eq!(
resolve(&proxies, Some("10.0.0.2"), "198.51.100.1"),
Some(ip("10.0.0.2"))
);
}
#[test]
fn networks_accept_parsed_values() {
let network: IpNet = "10.0.0.0/8".parse().unwrap();
let proxies = TrustedProxies::new()
.networks([network])
.networks([ip("192.0.2.7")]);
for peer in ["10.1.2.3", "192.0.2.7"] {
assert_eq!(
resolve(&proxies, Some(peer), "198.51.100.1"),
Some(ip("198.51.100.1"))
);
}
assert_eq!(
resolve(&proxies, Some("192.0.2.8"), "198.51.100.1"),
Some(ip("192.0.2.8"))
);
}
#[test]
fn networks_accept_references() {
let strings = vec![String::from("10.0.0.0/8")];
let addresses = [ip("192.0.2.7")];
let proxies = TrustedProxies::new()
.networks(&strings)
.networks(addresses.iter());
for peer in ["10.1.2.3", "192.0.2.7"] {
assert_eq!(
resolve(&proxies, Some(peer), "198.51.100.1"),
Some(ip("198.51.100.1"))
);
}
assert_eq!(
resolve(&proxies, Some("192.0.2.8"), "198.51.100.1"),
Some(ip("192.0.2.8"))
);
}
#[test]
fn ipv6_networks_match() {
let proxies = TrustedProxies::new().networks(["fd00::/8"]);
assert_eq!(
resolve(&proxies, Some("fd12::1"), "2001:db8::1"),
Some(ip("2001:db8::1"))
);
assert_eq!(
resolve(&proxies, Some("2001:db8::2"), "2001:db8::1"),
Some(ip("2001:db8::2"))
);
}
#[test]
fn a_mapped_ipv4_peer_matches_an_ipv4_network() {
let proxies = TrustedProxies::new().networks(["10.0.0.0/8"]);
assert_eq!(
resolve(&proxies, Some("::ffff:10.0.0.1"), "::ffff:198.51.100.1"),
Some(ip("198.51.100.1"))
);
}
#[test]
fn a_mapped_ipv4_network_is_read_as_ipv4() {
for network in ["::ffff:10.0.0.0/104", "::ffff:10.0.0.1"] {
let proxies = TrustedProxies::new().networks([network]);
for peer in ["10.0.0.1", "::ffff:10.0.0.1"] {
assert_eq!(
resolve(&proxies, Some(peer), "198.51.100.1"),
Some(ip("198.51.100.1")),
"{network} should contain {peer}"
);
}
assert_eq!(
resolve(&proxies, Some("192.0.2.1"), "198.51.100.1"),
Some(ip("192.0.2.1")),
"{network} should not contain 192.0.2.1"
);
}
}
#[test]
fn an_ipv6_network_never_contains_an_ipv4_address() {
let proxies = TrustedProxies::new().networks(["::/0"]);
for peer in ["10.0.0.1", "::ffff:10.0.0.1"] {
assert_eq!(
resolve(&proxies, Some(peer), "198.51.100.1"),
Some(ip("10.0.0.1"))
);
}
}
#[test]
fn a_zero_prefix_matches_everything() {
let proxies = TrustedProxies::new().networks(["0.0.0.0/0", "::/0"]);
assert_eq!(
resolve(&proxies, Some("203.0.113.9"), "198.51.100.1"),
Some(ip("198.51.100.1"))
);
assert_eq!(
resolve(&proxies, Some("2001:db8::2"), "2001:db8::1"),
Some(ip("2001:db8::1"))
);
}
#[test]
#[should_panic(expected = "invalid trusted proxy network `10.0.0.0/33`")]
fn a_prefix_beyond_the_address_length_panics() {
let _ = TrustedProxies::new().networks(["10.0.0.0/33"]);
}
#[test]
#[should_panic(expected = "invalid trusted proxy network `proxy.internal`")]
fn a_host_name_panics() {
let _ = TrustedProxies::new().networks(["proxy.internal"]);
}
#[test]
fn the_nearest_proxy_is_trusted_whatever_its_address() {
let proxies = TrustedProxies::new().nearest(1);
assert_eq!(
resolve(&proxies, Some("203.0.113.9"), "198.51.100.1"),
Some(ip("198.51.100.1"))
);
}
#[test]
fn a_peer_without_an_address_counts_as_the_nearest_proxy() {
let proxies = TrustedProxies::new().nearest(1);
assert_eq!(
resolve(&proxies, None, "198.51.100.1"),
Some(ip("198.51.100.1"))
);
}
#[test]
fn nearest_counts_hops_out_from_the_peer() {
let proxies = TrustedProxies::new().nearest(2);
assert_eq!(
resolve(
&proxies,
Some("203.0.113.9"),
"1.1.1.1, 198.51.100.1, 192.0.2.7"
),
Some(ip("198.51.100.1"))
);
}
#[test]
fn a_hop_trusted_by_position_needs_no_readable_address() {
let proxies = TrustedProxies::new().nearest(2);
for value in ["198.51.100.1, unknown", "198.51.100.1, _lb1"] {
assert_eq!(
resolve(&proxies, Some("203.0.113.9"), value),
Some(ip("198.51.100.1")),
"{value}"
);
}
assert_eq!(resolve(&proxies, Some("203.0.113.9"), "unknown"), None);
assert_eq!(
resolve(&proxies, Some("203.0.113.9"), "unknown, 10.0.0.3"),
None
);
}
#[test]
fn nearest_and_networks_combine() {
let proxies = TrustedProxies::new().nearest(1).networks(["192.0.2.0/24"]);
assert_eq!(
resolve(&proxies, Some("203.0.113.9"), "198.51.100.1, 192.0.2.7"),
Some(ip("198.51.100.1"))
);
}
#[test]
fn entries_span_several_header_lines() {
let proxies = TrustedProxies::new().networks(["10.0.0.0/8"]);
let headers = headers(&[
("x-forwarded-for", "198.51.100.1"),
("x-forwarded-for", "10.0.0.5, 10.0.0.3"),
]);
assert_eq!(
proxies.client_ip(Some(ip("10.0.0.1")), &headers),
Some(ip("198.51.100.1"))
);
}
#[test]
fn ports_and_brackets_are_stripped() {
let proxies = TrustedProxies::new().nearest(1);
assert_eq!(
resolve(&proxies, Some("10.0.0.1"), "198.51.100.1:4242"),
Some(ip("198.51.100.1"))
);
assert_eq!(
resolve(&proxies, Some("10.0.0.1"), "[2001:db8::1]:4242"),
Some(ip("2001:db8::1"))
);
assert_eq!(
resolve(&proxies, Some("10.0.0.1"), "[2001:db8::1]"),
Some(ip("2001:db8::1"))
);
assert_eq!(
resolve(&proxies, Some("10.0.0.1"), "[2001:db8::1]:_port1"),
Some(ip("2001:db8::1"))
);
}
#[test]
fn a_malformed_node_leaves_the_client_unknown() {
let proxies = TrustedProxies::new().nearest(1);
for value in [
"\"198.51.100.1\"",
"198.51.100.1:",
"198.51.100.1:123456",
"198.51.100.1:http",
"[2001:db8::1",
"[2001:db8::1]:",
"[2001:db8::1]:4242x",
"[2001:db8::1]garbage",
"[198.51.100.1]",
] {
assert_eq!(resolve(&proxies, Some("10.0.0.1"), value), None, "{value}");
}
}
#[test]
fn an_unreadable_entry_leaves_the_client_unknown() {
let proxies = TrustedProxies::new().nearest(1);
assert_eq!(resolve(&proxies, Some("10.0.0.1"), "unknown"), None);
assert_eq!(resolve(&proxies, Some("10.0.0.1"), "_hidden"), None);
assert_eq!(resolve(&proxies, Some("10.0.0.1"), "<script>"), None);
}
#[test]
fn an_unreadable_entry_beyond_the_client_is_never_read() {
let proxies = TrustedProxies::new().nearest(1);
assert_eq!(
resolve(&proxies, Some("10.0.0.1"), "garbage, 198.51.100.1"),
Some(ip("198.51.100.1"))
);
}
#[test]
fn empty_entries_are_skipped() {
let proxies = TrustedProxies::new().nearest(1);
assert_eq!(
resolve(&proxies, Some("10.0.0.1"), "198.51.100.1, ,"),
Some(ip("198.51.100.1"))
);
}
#[test]
fn a_byte_outside_ascii_spoils_its_entry_only() {
let proxies = TrustedProxies::new().nearest(1);
let resolve = |value: &[u8]| {
let mut map = HeaderMap::new();
map.append("x-forwarded-for", HeaderValue::from_bytes(value).unwrap());
proxies.client_ip(Some(ip("10.0.0.1")), &map)
};
assert_eq!(resolve(b"\xff\xfe, 198.51.100.1"), Some(ip("198.51.100.1")));
assert_eq!(resolve(b"198.51.100.1\xff"), None);
assert_eq!(resolve(b"\xff"), None);
}
#[test]
fn the_forwarded_header_reads_the_for_parameter() {
let proxies = TrustedProxies::new()
.networks(["10.0.0.0/8"])
.header(ForwardedHeader::Forwarded);
let map = headers(&[(
"forwarded",
"For=198.51.100.1;proto=https, for=\"[2001:db8::1]:443\";by=10.0.0.1",
)]);
assert_eq!(
proxies.client_ip(Some(ip("10.0.0.1")), &map),
Some(ip("2001:db8::1"))
);
let map = headers(&[("forwarded", "for=198.51.100.1;proto=https, for=10.0.0.3")]);
assert_eq!(
proxies.client_ip(Some(ip("10.0.0.1")), &map),
Some(ip("198.51.100.1"))
);
}
#[test]
fn forwarded_quoted_strings_may_contain_the_delimiters() {
let proxies = TrustedProxies::new()
.nearest(1)
.header(ForwardedHeader::Forwarded);
let resolve = |value: &str| {
let map = headers(&[("forwarded", value)]);
proxies.client_ip(Some(ip("10.0.0.1")), &map)
};
assert_eq!(
resolve("for=198.51.100.1;ext=\",for=203.0.113.9\""),
Some(ip("198.51.100.1"))
);
assert_eq!(
resolve("for=\"[2001:db8::1]\";ext=\"a;for=203.0.113.9\""),
Some(ip("2001:db8::1"))
);
assert_eq!(
resolve("for=\"[2001:db8::1]:80\";ext=\"x\\\"y,for=203.0.113.9\""),
Some(ip("2001:db8::1"))
);
assert_eq!(resolve("for=\"198.51.\\100.1\""), Some(ip("198.51.100.1")));
}
#[test]
fn a_forwarded_value_with_an_unclosed_quote_leaves_the_client_unknown() {
let proxies = TrustedProxies::new()
.nearest(1)
.header(ForwardedHeader::Forwarded);
for value in [
"for=\"198.51.100.1",
"for=\"198.51.100.1\\\"",
"for=203.0.113.9;ext=\", for=198.51.100.1",
] {
let map = headers(&[("forwarded", value)]);
assert_eq!(
proxies.client_ip(Some(ip("10.0.0.1")), &map),
None,
"{value}"
);
}
let map = headers(&[
("forwarded", "for=203.0.113.9;ext=\""),
("forwarded", "for=198.51.100.1"),
]);
assert_eq!(proxies.client_ip(Some(ip("10.0.0.1")), &map), None);
}
#[test]
fn a_forwarded_byte_outside_ascii_spoils_its_element_only() {
let proxies = TrustedProxies::new()
.nearest(1)
.header(ForwardedHeader::Forwarded);
let resolve = |value: &[u8]| {
let mut map = HeaderMap::new();
map.append("forwarded", HeaderValue::from_bytes(value).unwrap());
proxies.client_ip(Some(ip("10.0.0.1")), &map)
};
assert_eq!(
resolve(b"for=\xff\xfe, for=198.51.100.1"),
Some(ip("198.51.100.1"))
);
assert_eq!(
resolve(b"for=203.0.113.9;ext=\"\xff,for=1.1.1.1\", for=198.51.100.1"),
Some(ip("198.51.100.1"))
);
assert_eq!(resolve(b"for=198.51.100.1;ext=\xff"), None);
}
#[test]
fn a_forwarded_element_with_two_for_parameters_is_ambiguous() {
let proxies = TrustedProxies::new()
.nearest(1)
.header(ForwardedHeader::Forwarded);
let map = headers(&[("forwarded", "for=203.0.113.9;for=198.51.100.1")]);
assert_eq!(proxies.client_ip(Some(ip("10.0.0.1")), &map), None);
}
#[test]
fn a_forwarded_element_without_an_address_leaves_the_client_unknown() {
let proxies = TrustedProxies::new()
.nearest(1)
.header(ForwardedHeader::Forwarded);
for value in ["proto=https", "for=unknown", "for=_hidden"] {
let headers = headers(&[("forwarded", value)]);
assert_eq!(proxies.client_ip(Some(ip("10.0.0.1")), &headers), None);
}
}
#[test]
fn the_forwarded_header_ignores_x_forwarded_for() {
let proxies = TrustedProxies::new()
.nearest(1)
.header(ForwardedHeader::Forwarded);
assert_eq!(
resolve(&proxies, Some("10.0.0.1"), "198.51.100.1"),
Some(ip("10.0.0.1"))
);
}
#[test]
fn a_single_header_is_the_client_behind_a_trusted_peer() {
let proxies =
TrustedProxies::new()
.networks(["10.0.0.0/8"])
.header(ForwardedHeader::Single(HeaderName::from_static(
"cf-connecting-ip",
)));
let headers = headers(&[
("cf-connecting-ip", "198.51.100.1"),
("x-forwarded-for", "1.1.1.1, 198.51.100.1, 192.0.2.7"),
]);
assert_eq!(
proxies.client_ip(Some(ip("10.0.0.1")), &headers),
Some(ip("198.51.100.1"))
);
assert_eq!(
proxies.client_ip(Some(ip("203.0.113.9")), &headers),
Some(ip("203.0.113.9"))
);
assert_eq!(
proxies.client_ip(Some(ip("10.0.0.1")), &HeaderMap::new()),
None
);
}
#[test]
fn a_single_header_with_more_than_one_address_is_ambiguous() {
let proxies = TrustedProxies::new()
.nearest(1)
.header(ForwardedHeader::Single(HeaderName::from_static(
"x-real-ip",
)));
let map = headers(&[("x-real-ip", "1.1.1.1"), ("x-real-ip", "198.51.100.1")]);
assert_eq!(proxies.client_ip(Some(ip("10.0.0.1")), &map), None);
let map = headers(&[("x-real-ip", "1.1.1.1, 198.51.100.1")]);
assert_eq!(proxies.client_ip(Some(ip("10.0.0.1")), &map), None);
}
#[test]
fn a_single_header_with_a_byte_outside_ascii_is_unreadable() {
let proxies = TrustedProxies::new()
.nearest(1)
.header(ForwardedHeader::Single(HeaderName::from_static(
"x-real-ip",
)));
let mut map = HeaderMap::new();
map.append(
"x-real-ip",
HeaderValue::from_bytes(b"198.51.100.1\xff").unwrap(),
);
assert_eq!(proxies.client_ip(Some(ip("10.0.0.1")), &map), None);
}
}