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::::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::().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::().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); } } }