use super::*;
fn ip_from_headers_trusted(
headers: &axum::http::HeaderMap,
peer: std::net::IpAddr,
trusted: &TrustedProxies,
) -> std::net::IpAddr {
trusted
.client_ip(headers, Some(peer))
.get()
.expect("a known peer always resolves to an address")
}
use axum::http::HeaderMap;
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
#[test]
fn trusted_proxies_loopback_only_contains_v4_and_v6_loopback() {
let trusted = TrustedProxies::loopback_only();
assert!(trusted.trusts_hop(IpAddr::V4(Ipv4Addr::LOCALHOST)));
assert!(trusted.trusts_hop(IpAddr::V6(Ipv6Addr::LOCALHOST)));
}
#[test]
fn trusted_proxies_rejects_unrelated_peer() {
let trusted = TrustedProxies::loopback_only();
assert!(!trusted.trusts_hop(IpAddr::V4(Ipv4Addr::new(203, 0, 113, 1))));
}
#[test]
fn trusted_proxies_empty_set_trusts_nothing() {
let trusted = TrustedProxies::new([]);
assert!(!trusted.trusts_hop(IpAddr::V4(Ipv4Addr::LOCALHOST)));
}
#[test]
fn trusted_proxies_matches_inside_a_v4_cidr_and_rejects_outside() {
let trusted = TrustedProxies::from_cidrs(["10.0.0.0/8"]).expect("valid CIDR");
assert!(trusted.trusts_hop("10.0.0.1".parse().unwrap()));
assert!(trusted.trusts_hop("10.255.255.254".parse().unwrap()));
assert!(!trusted.trusts_hop("11.0.0.1".parse().unwrap()));
assert!(!trusted.trusts_hop("9.255.255.255".parse().unwrap()));
}
#[test]
fn trusted_proxies_cidr_respects_the_prefix_boundary() {
let trusted = TrustedProxies::from_cidrs(["192.0.2.2/31"]).expect("valid CIDR");
assert!(trusted.trusts_hop("192.0.2.2".parse().unwrap()));
assert!(trusted.trusts_hop("192.0.2.3".parse().unwrap()));
assert!(!trusted.trusts_hop("192.0.2.1".parse().unwrap()));
assert!(!trusted.trusts_hop("192.0.2.4".parse().unwrap()));
}
#[test]
fn trusted_proxies_cidr_prefix_zero_matches_the_whole_family() {
let v4 = TrustedProxies::from_cidrs(["0.0.0.0/0"]).expect("valid CIDR");
assert!(v4.trusts_hop("203.0.113.9".parse().unwrap()));
assert!(
!v4.trusts_hop("2001:db8::1".parse().unwrap()),
"family must not cross"
);
let v6 = TrustedProxies::from_cidrs(["::/0"]).expect("valid CIDR");
assert!(v6.trusts_hop("2001:db8::1".parse().unwrap()));
assert!(
!v6.trusts_hop("203.0.113.9".parse().unwrap()),
"family must not cross"
);
}
#[test]
fn trusted_proxies_matches_a_v6_cidr() {
let trusted = TrustedProxies::from_cidrs(["2001:db8::/32"]).expect("valid CIDR");
assert!(trusted.trusts_hop("2001:db8::1".parse().unwrap()));
assert!(trusted.trusts_hop("2001:db8:ffff::1".parse().unwrap()));
assert!(!trusted.trusts_hop("2001:db9::1".parse().unwrap()));
}
#[test]
fn trusted_proxies_composes_exact_addresses_with_ranges() {
let trusted = TrustedProxies::new(["203.0.113.9".parse().unwrap()])
.with_cidrs(["10.0.0.0/8"])
.expect("valid CIDR");
assert!(trusted.trusts_hop("203.0.113.9".parse().unwrap()));
assert!(trusted.trusts_hop("10.1.2.3".parse().unwrap()));
assert!(!trusted.trusts_hop("198.51.100.1".parse().unwrap()));
}
#[test]
fn cidr_parse_rejects_malformed_specs() {
assert!(matches!(
TrustedProxies::from_cidrs(["10.0.0.0"]),
Err(CidrParseError::MissingPrefix(_))
));
assert!(matches!(
TrustedProxies::from_cidrs(["not-an-ip/8"]),
Err(CidrParseError::Address(_))
));
assert!(matches!(
TrustedProxies::from_cidrs(["10.0.0.0/x"]),
Err(CidrParseError::Prefix(_))
));
assert!(matches!(
TrustedProxies::from_cidrs(["10.0.0.0/33"]),
Err(CidrParseError::Prefix(_))
));
}
fn xff(value: &str) -> HeaderMap {
let mut headers = HeaderMap::new();
headers.insert("X-Forwarded-For", value.parse().unwrap());
headers
}
fn proxy() -> IpAddr {
"10.0.0.1".parse().unwrap()
}
fn trusted_proxy_set() -> TrustedProxies {
TrustedProxies::from_cidrs(["10.0.0.0/8"]).expect("valid CIDR")
}
#[test]
fn untrusted_peer_ignores_forwarded_headers_entirely() {
let peer: IpAddr = "203.0.113.9".parse().unwrap();
let got = ip_from_headers_trusted(&xff("198.51.100.1"), peer, &trusted_proxy_set());
assert_eq!(
got, peer,
"a peer we do not trust cannot name its own address"
);
}
#[test]
fn client_prepended_forwarded_entry_does_not_win() {
let got = ip_from_headers_trusted(
&xff("192.0.2.5, 203.0.113.9"),
proxy(),
&trusted_proxy_set(),
);
assert_eq!(
got,
"203.0.113.9".parse::<IpAddr>().unwrap(),
"must take the rightmost untrusted entry, not the client-supplied one"
);
}
#[test]
fn walks_back_past_a_chain_of_trusted_hops() {
let got = ip_from_headers_trusted(
&xff("192.0.2.5, 203.0.113.9, 10.0.0.7, 10.0.0.8"),
proxy(),
&trusted_proxy_set(),
);
assert_eq!(got, "203.0.113.9".parse::<IpAddr>().unwrap());
}
#[test]
fn all_entries_trusted_means_the_request_started_inside() {
let got = ip_from_headers_trusted(&xff("10.0.0.7, 10.0.0.8"), proxy(), &trusted_proxy_set());
assert_eq!(got, proxy());
}
#[test]
fn malformed_entry_stops_the_walk_rather_than_trusting_past_it() {
let got = ip_from_headers_trusted(
&xff("203.0.113.9, junk, 10.0.0.8"),
proxy(),
&trusted_proxy_set(),
);
assert_eq!(got, proxy());
}
#[test]
fn forwarded_for_beats_a_forged_real_ip() {
let mut headers = xff("203.0.113.9");
headers.insert("X-Real-IP", "192.0.2.5".parse().unwrap());
let trusted = trusted_proxy_set().proxy_overwrites_real_ip();
let got = ip_from_headers_trusted(&headers, proxy(), &trusted);
assert_eq!(got, "203.0.113.9".parse::<IpAddr>().unwrap());
}
#[test]
fn real_ip_is_not_read_without_the_operator_assertion() {
let mut headers = HeaderMap::new();
headers.insert("X-Real-IP", "203.0.113.9".parse().unwrap());
let got = ip_from_headers_trusted(&headers, proxy(), &trusted_proxy_set());
assert_eq!(
got,
proxy(),
"an unasserted X-Real-IP must lose to the peer"
);
}
#[test]
fn real_ip_is_read_where_the_operator_asserts_the_proxy_overwrites_it() {
let mut headers = HeaderMap::new();
headers.insert("X-Real-IP", "203.0.113.9".parse().unwrap());
let trusted = trusted_proxy_set().proxy_overwrites_real_ip();
let got = ip_from_headers_trusted(&headers, proxy(), &trusted);
assert_eq!(got, "203.0.113.9".parse::<IpAddr>().unwrap());
}
#[test]
fn no_forwarded_headers_yields_the_peer() {
let got = ip_from_headers_trusted(&HeaderMap::new(), proxy(), &trusted_proxy_set());
assert_eq!(got, proxy());
}
#[test]
fn second_forwarded_for_line_cannot_hide_the_real_client() {
let mut headers = axum::http::HeaderMap::new();
headers.append("X-Forwarded-For", "9.9.9.9".parse().unwrap()); headers.append("X-Forwarded-For", "203.0.113.7".parse().unwrap());
let proxy: std::net::IpAddr = "10.0.0.1".parse().unwrap();
let trusted = TrustedProxies::new([proxy]);
assert_eq!(
ip_from_headers_trusted(&headers, proxy, &trusted),
"203.0.113.7".parse::<std::net::IpAddr>().unwrap(),
"the rightmost untrusted entry across ALL field lines is the client"
);
}
#[test]
fn repeated_real_ip_lines_are_not_believed() {
let mut headers = axum::http::HeaderMap::new();
headers.append("X-Real-IP", "9.9.9.9".parse().unwrap());
headers.append("X-Real-IP", "203.0.113.7".parse().unwrap());
let proxy: std::net::IpAddr = "10.0.0.1".parse().unwrap();
let trusted = TrustedProxies::new([proxy]).proxy_overwrites_real_ip();
assert_eq!(
ip_from_headers_trusted(&headers, proxy, &trusted),
proxy,
"an ambiguous X-Real-IP must not be trusted even where the \
operator asserted the proxy overwrites it"
);
}
#[test]
fn the_real_ip_assertion_survives_composition() {
let proxy: std::net::IpAddr = "10.0.0.1".parse().unwrap();
let trusted = TrustedProxies::new([proxy])
.proxy_overwrites_real_ip()
.with_cidrs(["192.0.2.0/24"])
.expect("valid cidr");
let mut headers = axum::http::HeaderMap::new();
headers.insert("X-Real-IP", "203.0.113.7".parse().unwrap());
assert_eq!(
ip_from_headers_trusted(&headers, proxy, &trusted),
"203.0.113.7".parse::<std::net::IpAddr>().unwrap()
);
}
#[test]
fn cidr_with_host_bits_is_rejected_not_widened() {
let err = TrustedProxies::from_cidrs(["10.0.0.5/8"]).unwrap_err();
assert!(
matches!(err, CidrParseError::HostBitsSet(_)),
"expected HostBitsSet, got {err:?}"
);
assert!(TrustedProxies::from_cidrs(["2001:db8::1/32"]).is_err());
assert!(TrustedProxies::from_cidrs(["10.0.0.0/8"]).is_ok());
assert!(TrustedProxies::from_cidrs(["10.0.0.5/32"]).is_ok());
assert!(TrustedProxies::from_cidrs(["2001:db8::/32"]).is_ok());
assert!(TrustedProxies::from_cidrs(["::/0"]).is_ok());
}
mod resolved {
use super::super::*;
use axum::body::Body;
use axum::extract::ConnectInfo;
use axum::http::Request;
use std::net::SocketAddr;
fn parts_with(peer: Option<&str>, headers: &[(&str, &str)]) -> axum::http::request::Parts {
let mut b = Request::builder();
for (k, v) in headers {
b = b.header(*k, *v);
}
let mut req = b.body(Body::empty()).unwrap();
if let Some(p) = peer {
let addr: SocketAddr = format!("{p}:51234").parse().unwrap();
req.extensions_mut().insert(ConnectInfo(addr));
}
req.into_parts().0
}
#[tokio::test]
async fn without_the_layer_there_is_no_address() {
let mut p = parts_with(
Some("203.0.113.9"),
&[("X-Real-IP", "8.8.8.8"), ("X-Forwarded-For", "8.8.4.4")],
);
let ip = ClientIp::from_request_parts(&mut p, &()).await.unwrap();
assert_eq!(ip.get(), None);
}
#[tokio::test]
async fn the_layer_s_answer_reaches_the_handler() {
let mut p = parts_with(None, &[]);
p.extensions
.insert(ClientIp::for_test(Some("198.51.100.7".parse().unwrap())));
let ip = ClientIp::from_request_parts(&mut p, &()).await.unwrap();
assert_eq!(ip.get(), Some("198.51.100.7".parse().unwrap()));
}
#[tokio::test]
async fn rotating_a_forged_header_does_not_move_the_answer() {
let trusted = TrustedProxies::loopback_only();
let peer: std::net::IpAddr = "203.0.113.9".parse().unwrap();
let mut headers = axum::http::HeaderMap::new();
headers.insert("X-Real-IP", "8.8.8.8".parse().unwrap());
let first = trusted.client_ip(&headers, Some(peer)).get().unwrap();
headers.insert("X-Real-IP", "9.9.9.9".parse().unwrap());
let second = trusted.client_ip(&headers, Some(peer)).get().unwrap();
assert_eq!(first, peer);
assert_eq!(first, second);
}
}
mod private_transport {
use super::super::*;
use axum::http::HeaderMap;
fn xff(value: &str) -> HeaderMap {
let mut h = HeaderMap::new();
h.insert("X-Forwarded-For", value.parse().unwrap());
h
}
#[test]
fn rightmost_entry_wins_when_the_peer_cannot_be_named() {
let trusted = TrustedProxies::private_transport();
let peer: std::net::IpAddr = "100.64.3.9".parse().unwrap(); let headers = xff("8.8.8.8, 198.51.100.7");
assert_eq!(
trusted.client_ip(&headers, Some(peer)).get().unwrap(),
"198.51.100.7".parse::<std::net::IpAddr>().unwrap(),
"the ingress appends the client last; a prepended entry must not win"
);
}
#[test]
fn trusting_the_peer_does_not_trust_the_chain() {
let trusted = TrustedProxies::private_transport();
let peer: std::net::IpAddr = "100.64.3.9".parse().unwrap();
assert!(trusted.trusts_peer(peer));
assert!(!trusted.trusts_hop(peer));
assert!(!trusted.trusts_hop("198.51.100.7".parse().unwrap()));
}
#[test]
fn a_named_hop_composes_with_an_unnamed_peer() {
let trusted = TrustedProxies::private_transport()
.with_cidrs(["203.0.113.0/24"])
.expect("valid cidr");
let peer: std::net::IpAddr = "100.64.3.9".parse().unwrap();
let headers = xff("8.8.8.8, 198.51.100.7, 203.0.113.5");
assert_eq!(
trusted.client_ip(&headers, Some(peer)).get().unwrap(),
"198.51.100.7".parse::<std::net::IpAddr>().unwrap(),
);
}
#[test]
fn no_chain_means_the_peer() {
let trusted = TrustedProxies::private_transport();
let peer: std::net::IpAddr = "100.64.3.9".parse().unwrap();
assert_eq!(
trusted
.client_ip(&HeaderMap::new(), Some(peer))
.get()
.unwrap(),
peer
);
}
#[test]
fn enumerated_sets_are_unaffected() {
let trusted = TrustedProxies::loopback_only();
let peer: std::net::IpAddr = "203.0.113.9".parse().unwrap();
assert!(!trusted.trusts_peer(peer));
assert_eq!(
trusted
.client_ip(&xff("8.8.8.8"), Some(peer))
.get()
.unwrap(),
peer
);
}
}
mod no_peer {
use super::super::*;
use axum::http::HeaderMap;
fn xff(value: &str) -> HeaderMap {
let mut h = HeaderMap::new();
h.insert("X-Forwarded-For", value.parse().unwrap());
h
}
#[test]
fn private_transport_resolves_without_a_peer() {
let trusted = TrustedProxies::private_transport();
assert_eq!(
trusted.client_ip(&xff("8.8.8.8, 198.51.100.7"), None).get(),
Some("198.51.100.7".parse().unwrap()),
);
}
#[test]
fn an_address_set_without_a_peer_yields_nothing() {
let trusted = TrustedProxies::loopback_only();
assert_eq!(
trusted.client_ip(&xff("8.8.8.8, 198.51.100.7"), None).get(),
None,
);
}
#[test]
fn no_peer_and_no_chain_yields_nothing() {
let trusted = TrustedProxies::private_transport();
assert_eq!(trusted.client_ip(&HeaderMap::new(), None).get(), None);
}
#[test]
fn with_a_peer_it_agrees_with_the_peer_taking_form() {
let trusted = TrustedProxies::loopback_only();
let peer: std::net::IpAddr = "127.0.0.1".parse().unwrap();
let headers = xff("8.8.8.8, 198.51.100.7");
assert_eq!(
trusted.client_ip(&headers, Some(peer)).get(),
Some("198.51.100.7".parse().unwrap()),
);
}
}
mod resolved_elsewhere {
use super::super::*;
#[test]
fn an_address_resolved_elsewhere_reaches_the_consumers() {
let ip: std::net::IpAddr = "198.51.100.7".parse().unwrap();
assert_eq!(ClientIp::resolved(Some(ip)).get(), Some(ip));
}
#[test]
fn resolving_to_nothing_is_expressible() {
assert_eq!(ClientIp::resolved(None).get(), None);
assert_eq!(ClientIp::default().get(), None);
}
}
mod provenance {
use super::super::*;
use axum::http::HeaderMap;
fn xff(value: &str) -> HeaderMap {
let mut h = HeaderMap::new();
h.insert("X-Forwarded-For", value.parse().unwrap());
h
}
#[test]
fn an_unnamed_proxy_reports_peer_not_forwarded() {
let trusted = TrustedProxies::default();
let proxy: std::net::IpAddr = "10.0.0.1".parse().unwrap();
let got = trusted.client_ip(&xff("198.51.100.7"), Some(proxy));
assert_eq!(got.get(), Some(proxy));
assert_eq!(got.source(), Source::Peer);
}
#[test]
fn a_named_proxy_reports_forwarded() {
let trusted = TrustedProxies::from_cidrs(["10.0.0.0/8"]).unwrap();
let proxy: std::net::IpAddr = "10.0.0.1".parse().unwrap();
let got = trusted.client_ip(&xff("198.51.100.7"), Some(proxy));
assert_eq!(got.get(), Some("198.51.100.7".parse().unwrap()));
assert_eq!(got.source(), Source::Forwarded);
}
#[test]
fn nothing_resolved_reports_unknown() {
assert_eq!(ClientIp::default().source(), Source::Unknown);
assert_eq!(ClientIp::default().get(), None);
}
#[test]
fn a_trusted_peer_with_no_chain_reports_peer() {
let trusted = TrustedProxies::loopback_only();
let peer: std::net::IpAddr = "127.0.0.1".parse().unwrap();
let got = trusted.client_ip(&HeaderMap::new(), Some(peer));
assert_eq!(got.get(), Some(peer));
assert_eq!(got.source(), Source::Peer);
}
#[test]
fn an_application_supplied_address_says_so() {
let ip: std::net::IpAddr = "198.51.100.7".parse().unwrap();
assert_eq!(ClientIp::resolved(Some(ip)).source(), Source::Supplied);
}
}