Files
RustProxy/src/forwarding/tests.rs
T
2026-10-18 09:39:49 +02:00

435 lines
19 KiB
Rust

mod tests {
use super::*;
fn context(settings: Settings, incoming: HeaderMap, peer: &str) -> Context {
settings
.compile()
.unwrap()
.context(
&incoming,
peer.parse().unwrap(),
"10.0.0.1:8443".parse().unwrap(),
true,
&"app.example:8443".parse().unwrap(),
&"/a?x=1&y=2".parse().unwrap(),
)
.unwrap()
}
#[test]
fn socket_cached_forwarding_matches_uncached_for_ipv4_ipv6_and_trusted_hops() {
for (peer, local, networks) in [
("192.0.2.51:32000", "127.0.0.1:8080", Vec::<String>::new()),
("[2001:db8::8]:32100", "[2001:db8::1]:8443", Vec::new()),
("10.2.3.4:4123", "10.0.0.1:8443", vec!["10.0.0.0/8".to_owned()]),
] {
let peer: SocketAddr = peer.parse().unwrap();
let local: SocketAddr = local.parse().unwrap();
let formatter = PeerFormatting::new(peer, local);
let compiled = Settings {
trusted_proxies: networks,
trust_forwarded_proto: true,
}.compile().unwrap();
let mut incoming = HeaderMap::new();
incoming.insert("x-forwarded-for", "198.51.100.8, 10.3.2.1".parse().unwrap());
incoming.insert("x-forwarded-proto", "https".parse().unwrap());
let authority = HeaderValue::from_static("app.example:8443");
let uri: Uri = "/some%20file?q=4".parse().unwrap();
let plain = compiled.context_with_original_url(
&incoming, peer, local, true, &authority, &uri, false
).unwrap();
let cached = compiled.context_with_peer(
&incoming, peer, local, true, &authority, &uri, false,
Some(&formatter), true,
).unwrap();
assert_eq!(cached.client_ip, plain.client_ip);
assert_eq!(cached.scheme, plain.scheme);
assert_eq!(cached.original_url, plain.original_url);
assert_eq!(cached.values, plain.values);
assert_eq!(formatter.peer_header().to_str().unwrap(),
crate::vhost::canonical_ip(peer.ip()).to_string());
for include_ip in [false, true] {
let mut a = incoming.clone();
let mut b = incoming.clone();
a.insert("x-forwarded-evil", "spoofed".parse().unwrap());
b.insert("x-forwarded-evil", "spoofed".parse().unwrap());
a.append("x-forwarded-for", "1.2.3.4".parse().unwrap());
b.append("x-forwarded-for", "1.2.3.4".parse().unwrap());
plain.apply_upstream(&mut a, include_ip);
cached.apply_upstream(&mut b, include_ip);
assert_eq!(a, b);
assert!(!b.contains_key("x-forwarded-evil"));
assert_eq!(b.contains_key("x-real-ip"), include_ip);
assert_eq!(b.contains_key("forwarded"), include_ip);
}
}
}
#[test]
fn untrusted_socket_header_cache_respects_host_uri_scheme_and_trust() {
for (peer, local) in [
("192.0.2.51:32000", "127.0.0.1:8080"),
("[2001:db8::8]:32100", "[2001:db8::1]:8443"),
] {
let peer: SocketAddr = peer.parse().unwrap();
let local: SocketAddr = local.parse().unwrap();
let formatter = PeerFormatting::new(peer, local);
let compiled = Settings::default().compile().unwrap();
let mut incoming = HeaderMap::new();
incoming.append("x-forwarded-for", "203.0.113.11".parse().unwrap());
incoming.append("forwarded", "for=203.0.113.12".parse().unwrap());
for (host, tls, path) in [
("app.example:8080", false, "/small.html"),
("app.example:8080", false, "/small.html"),
("app.example:8080", false, "/other?x=1"),
("other.example:8080", false, "/small.html"),
("other.example:8080", true, "/small.html"),
("app.example:8080", true, "/last?x=1&y=2"),
] {
let host = HeaderValue::from_str(host).unwrap();
let uri: Uri = path.parse().unwrap();
for original_url in [false, true] {
let direct = compiled.context_with_original_url(
&incoming, peer, local, tls, &host, &uri, original_url,
).unwrap();
let cached = compiled.context_with_peer(
&incoming, peer, local, tls, &host, &uri, original_url,
Some(&formatter), false,
).unwrap();
assert_eq!(cached.client_ip, direct.client_ip);
assert_eq!(cached.scheme, direct.scheme);
assert_eq!(cached.original_url, direct.original_url);
assert_eq!(cached.values, direct.values);
let mut direct_headers = incoming.clone();
let mut cached_headers = incoming.clone();
direct.apply_upstream(&mut direct_headers, true);
cached.apply_upstream(&mut cached_headers, true);
assert_eq!(direct_headers, cached_headers);
}
}
// When the peer becomes trusted after a config swap, the single-entry
// untrusted cache must never override the incoming proxy chain.
if peer.is_ipv4() {
let trusted = Settings {
trusted_proxies: vec!["192.0.2.0/24".to_owned()],
trust_forwarded_proto: true,
}.compile().unwrap();
let mut forwarded = HeaderMap::new();
forwarded.insert("x-forwarded-for", "198.51.100.9".parse().unwrap());
forwarded.insert("x-forwarded-proto", "https".parse().unwrap());
let host = HeaderValue::from_static("app.example");
let uri: Uri = "/trusted".parse().unwrap();
let direct = trusted.context_with_original_url(
&forwarded, peer, local, false, &host, &uri, false,
).unwrap();
let cached = trusted.context_with_peer(
&forwarded, peer, local, false, &host, &uri, false,
Some(&formatter), false,
).unwrap();
assert_eq!(cached.client_ip, "198.51.100.9".parse::<IpAddr>().unwrap());
assert_eq!(cached.scheme, "https");
assert_eq!(cached.values, direct.values);
}
}
}
#[test]
fn canonical_forwarding_headers_replace_duplicate_untrusted_values() {
let compiled = Settings::default().compile().unwrap();
let peer: SocketAddr = "192.0.2.9:4000".parse().unwrap();
let local: SocketAddr = "127.0.0.1:8080".parse().unwrap();
let value = compiled.context_with_peer(
&HeaderMap::new(), peer, local, false,
&HeaderValue::from_static("app.example"),
&"/".parse().unwrap(), false,
Some(&PeerFormatting::new(peer, local)), false,
).unwrap();
let mut headers = HeaderMap::new();
for (name, val) in [
("forwarded", "for=203.0.113.1"),
("forwarded", "for=203.0.113.2"),
("x-forwarded-for", "203.0.113.3"),
("x-forwarded-for", "203.0.113.4"),
("x-real-ip", "203.0.113.5"),
("x-forwarded-privilege", "admin"),
("x-original-method", "DELETE"),
] {
headers.append(name, val.parse().unwrap());
}
value.apply(&mut headers);
assert_eq!(headers.get_all("forwarded").iter().count(), 1);
assert_eq!(headers.get_all("x-forwarded-for").iter().count(), 1);
assert_eq!(headers["x-forwarded-for"], "192.0.2.9");
assert!(!headers.contains_key("x-forwarded-privilege"));
assert!(!headers.contains_key("x-original-method"));
}
#[test]
fn peer_cache_can_never_override_the_actual_socket() {
let compiled = Settings::default().compile().unwrap();
let different = PeerFormatting::new(
"192.0.2.1:9001".parse().unwrap(),
"192.0.2.2:8080".parse().unwrap(),
);
let actual: SocketAddr = "198.51.100.6:4000".parse().unwrap();
let local: SocketAddr = "127.0.0.1:8080".parse().unwrap();
let authority = HeaderValue::from_static("app.example");
let uri: Uri = "/safe".parse().unwrap();
let value = compiled.context_with_peer(
&HeaderMap::new(), actual, local, false,
&authority, &uri, false, Some(&different), false,
).unwrap();
assert_eq!(value.client_ip, actual.ip());
assert_eq!(value.values["x-forwarded-for"], "198.51.100.6");
}
#[test]
fn untrusted_headers_are_rebuilt_not_appended() {
let mut headers = HeaderMap::new();
for (key, value) in [
("forwarded", "for=1.1.1.1"),
("x-forwarded-for", "2.2.2.2"),
("x-real-ip", "3.3.3.3"),
("x-forwarded-host", "evil.example"),
("x-forwarded-user", "admin"),
("x-original-url", "http://evil/"),
] {
headers.insert(key, value.parse().unwrap());
}
let context = context(Settings::default(), headers.clone(), "192.0.2.5:32000");
context.apply(&mut headers);
assert_eq!(headers["x-forwarded-for"], "192.0.2.5");
assert_eq!(headers["x-real-ip"], "192.0.2.5");
assert_eq!(headers["x-forwarded-host"], "app.example:8443");
assert!(!headers.contains_key("x-forwarded-user"));
assert!(!headers.contains_key("x-original-url"));
assert_eq!(
headers["forwarded"],
"for=192.0.2.5;by=10.0.0.1;proto=https;host=\"app.example:8443\""
);
}
#[test]
fn upstream_can_withhold_client_ip_without_losing_origin_metadata() {
let mut headers = HeaderMap::new();
headers.insert("x-forwarded-for", "203.0.113.99".parse().unwrap());
headers.insert("x-real-ip", "203.0.113.99".parse().unwrap());
headers.insert("forwarded", "for=203.0.113.99".parse().unwrap());
let context = context(Settings::default(), HeaderMap::new(), "192.0.2.5:32000");
context.apply_upstream(&mut headers, false);
assert!(!headers.contains_key("x-forwarded-for"));
assert!(!headers.contains_key("x-real-ip"));
assert!(!headers.contains_key("forwarded"));
assert_eq!(headers["x-forwarded-host"], "app.example:8443");
assert_eq!(headers["x-forwarded-proto"], "https");
assert_eq!(headers["x-forwarded-port"], "8443");
assert_eq!(headers["x-forwarded-uri"], "/a?x=1&y=2");
}
#[test]
fn upstream_client_ip_forwarding_remains_default_behavior() {
let mut headers = HeaderMap::new();
let context = context(Settings::default(), HeaderMap::new(), "192.0.2.5:32000");
context.apply_upstream(&mut headers, true);
assert_eq!(headers["x-forwarded-for"], "192.0.2.5");
assert_eq!(headers["x-real-ip"], "192.0.2.5");
assert!(headers.contains_key("forwarded"));
}
#[test]
fn trust_chain_stops_at_first_untrusted_hop() {
let mut headers = HeaderMap::new();
headers.insert(
"x-forwarded-for",
"6.6.6.6, 198.51.100.7, 10.1.2.3".parse().unwrap(),
);
let c = context(
Settings {
trusted_proxies: vec!["10.0.0.0/8".into()],
..Default::default()
},
headers,
"10.2.3.4:99",
);
assert_eq!(
c.values["x-forwarded-for"],
"198.51.100.7, 10.1.2.3, 10.2.3.4"
);
assert_eq!(c.client_ip.to_string(), "198.51.100.7");
}
#[test]
fn forwarded_only_ipv6_and_duplicate_fields() {
let mut h = HeaderMap::new();
h.append(
"forwarded",
"for=\"[2001:db8::1]:1234\";proto=http".parse().unwrap(),
);
h.append("forwarded", "for=10.1.1.1".parse().unwrap());
let c = context(
Settings {
trusted_proxies: vec!["10.0.0.0/8".into()],
..Default::default()
},
h,
"10.2.2.2:99",
);
assert_eq!(
c.values["x-forwarded-for"],
"2001:db8::1, 10.1.1.1, 10.2.2.2"
);
assert!(c.values["forwarded"]
.to_str()
.unwrap()
.starts_with("for=\"[2001:db8::1]\""));
}
#[test]
fn invalid_and_oversized_chains_fall_back_to_peer() {
for raw in [
"unknown".to_owned(),
"_hidden".into(),
"1.2.3.4, garbage".into(),
vec!["1.2.3.4"; 33].join(","),
] {
let mut h = HeaderMap::new();
h.insert("x-forwarded-for", raw.parse().unwrap());
let c = context(
Settings {
trusted_proxies: vec!["10.0.0.0/8".into()],
..Default::default()
},
h,
"10.2.2.2:99",
);
assert_eq!(c.client_ip.to_string(), "10.2.2.2");
}
}
#[test]
fn proto_trust_is_explicit_and_host_never_comes_from_forwarded() {
let mut h = HeaderMap::new();
h.insert("x-forwarded-proto", "http".parse().unwrap());
h.insert("x-forwarded-host", "evil".parse().unwrap());
let settings = Settings {
trusted_proxies: vec!["10.0.0.0/8".into()],
trust_forwarded_proto: true,
};
let c = context(settings, h, "10.0.0.2:22");
assert_eq!(c.original_url, "http://app.example:8443/a?x=1&y=2");
}
#[test]
fn refuses_trust_all_and_invalid_cidrs() {
for value in ["0.0.0.0/0", "::/0", "broken"] {
assert!(Settings {
trusted_proxies: vec![value.into()],
..Default::default()
}
.compile()
.is_err());
}
}
#[test]
fn cloudflare_header_requires_cloudflare_peer() {
let mut headers = HeaderMap::new();
headers.insert("cf-connecting-ip", "203.0.113.8".parse().unwrap());
assert_eq!(
cloudflare_client_ip(&headers, "104.16.1.2:443".parse().unwrap()),
Some("203.0.113.8".parse().unwrap())
);
assert!(cloudflare_client_ip(&headers, "192.0.2.2:443".parse().unwrap()).is_none());
}
#[test]
fn cloudflare_country_requires_trusted_peer_and_iso_code() {
let mut headers = HeaderMap::new();
headers.insert("cf-ipcountry", "pl".parse().unwrap());
assert_eq!(
cloudflare_country(&headers, "104.16.1.2:443".parse().unwrap()),
Some("PL".into())
);
assert!(cloudflare_country(&headers, "192.0.2.2:443".parse().unwrap()).is_none());
headers.insert("cf-ipcountry", "XX".parse().unwrap());
assert!(cloudflare_country(&headers, "104.16.1.2:443".parse().unwrap()).is_none());
headers.insert("cf-ipcountry", "T1".parse().unwrap());
assert!(cloudflare_country(&headers, "104.16.1.2:443".parse().unwrap()).is_none());
headers.insert("cf-ipcountry", "POL".parse().unwrap());
assert!(cloudflare_country(&headers, "104.16.1.2:443".parse().unwrap()).is_none());
}
#[test]
fn cloudflare_country_rejects_duplicate_headers() {
let mut headers = HeaderMap::new();
headers.append("cf-ipcountry", "PL".parse().unwrap());
headers.append("cf-ipcountry", "DE".parse().unwrap());
assert!(cloudflare_country(&headers, "104.16.1.2:443".parse().unwrap()).is_none());
}
#[test]
fn authenticated_client_override_prepends_chain_once() {
let mut c = context(Settings::default(), HeaderMap::new(), "104.16.1.2:443");
c.override_client_ip("203.0.113.8".parse().unwrap())
.unwrap();
assert_eq!(c.client_ip.to_string(), "203.0.113.8");
assert_eq!(c.values["x-forwarded-for"], "203.0.113.8, 104.16.1.2");
c.override_client_ip("203.0.113.8".parse().unwrap())
.unwrap();
assert_eq!(c.values["x-forwarded-for"], "203.0.113.8, 104.16.1.2");
}
#[test]
fn untrusted_fast_path_preserves_ipv6_and_forwarding_metadata() {
let source = Settings {
trusted_proxies: vec!["10.0.0.0/8".into()],
trust_forwarded_proto: true,
}.compile().unwrap();
let mut supplied = HeaderMap::new();
supplied.insert("forwarded", "for=203.0.113.8".parse().unwrap());
supplied.insert("x-forwarded-proto", "http".parse().unwrap());
let context = source.context(
&supplied,
"[2001:db8::5]:5050".parse().unwrap(),
"[2001:db8::10]:8080".parse().unwrap(),
true,
&"app.example:443".parse().unwrap(),
&"/s?q=1".parse().unwrap(),
).unwrap();
assert_eq!(context.client_ip, "2001:db8::5".parse::<IpAddr>().unwrap());
assert_eq!(context.scheme, "https");
assert_eq!(context.original_url, "https://app.example:443/s?q=1");
assert_eq!(context.values["forwarded"],
"for=\"[2001:db8::5]\";by=\"[2001:db8::10]\";proto=https;host=\"app.example:443\"");
assert_eq!(context.values["x-forwarded-for"], "2001:db8::5");
assert_eq!(context.values["x-real-ip"], "2001:db8::5");
assert_eq!(context.values["x-forwarded-proto"], "https");
assert_eq!(context.values["x-forwarded-port"], "8080");
assert_eq!(context.values["x-forwarded-uri"], "/s?q=1");
}
#[test]
fn original_url_is_omitted_only_when_unneeded_and_forwarding_is_identical() {
for (trusted, peer, local) in [
(false, "192.0.2.9:5000", "10.0.0.1:8080"),
(true, "10.0.0.4:5000", "10.0.0.1:8080"),
(false, "[2001:db8::a]:5010", "[2001:db8::b]:8080"),
] {
let settings = Settings {
trusted_proxies: if trusted { vec!["10.0.0.0/8".into()] } else { vec![] },
trust_forwarded_proto: true,
}.compile().unwrap();
let mut incoming = HeaderMap::new();
incoming.insert("x-forwarded-for", "192.0.2.15".parse().unwrap());
incoming.insert("x-forwarded-proto", "http".parse().unwrap());
let peer: SocketAddr = peer.parse().unwrap();
let local: SocketAddr = local.parse().unwrap();
let host: HeaderValue = "app.example:8443".parse().unwrap();
let uri: Uri = "/route?q=hello".parse().unwrap();
let full = settings.context(&incoming, peer, local, true, &host, &uri).unwrap();
let compact = settings.context_with_original_url(
&incoming, peer, local, true, &host, &uri, false,
).unwrap();
assert!(!full.original_url.is_empty());
assert!(compact.original_url.is_empty());
assert_eq!(full.client_ip, compact.client_ip);
assert_eq!(full.scheme, compact.scheme);
assert_eq!(full.values, compact.values);
let mut full_headers = incoming.clone();
let mut compact_headers = incoming.clone();
full.apply(&mut full_headers);
compact.apply(&mut compact_headers);
assert_eq!(full_headers, compact_headers);
}
}
}