435 lines
19 KiB
Rust
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);
|
|
}
|
|
}
|
|
|
|
}
|