Skip to content

Commit a7d830d

Browse files
authored
real_ip: Remove Heroku router workaround (#10249)
We have upgraded our servers to use the "Heroku Router 2.0" which fixed the `X-Forwarded-For` bug and no longer appends the IP to the first header it sees. This means we can finally remove our workaround and parse the header appropriately.
1 parent 5f8b70c commit a7d830d

File tree

1 file changed

+17
-46
lines changed

1 file changed

+17
-46
lines changed

src/real_ip.rs

Lines changed: 17 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -38,50 +38,21 @@ fn is_cloud_front_ip(ip: &IpAddr) -> bool {
3838
.any(|trusted_proxy| trusted_proxy.contains(*ip))
3939
}
4040

41+
/// Extracts the client IP address from the `X-Forwarded-For` header.
42+
///
43+
/// This function will return the last valid non-CloudFront IP address in the
44+
/// `X-Forwarded-For` header, if any.
4145
pub fn process_xff_headers(headers: &HeaderMap) -> Option<IpAddr> {
42-
let mut xff_iter = headers.get_all(X_FORWARDED_FOR).iter();
43-
let Some(first_header) = xff_iter.next() else {
44-
debug!(target: "real_ip", "No X-Forwarded-For header found");
45-
return None;
46-
};
47-
48-
let has_more_headers = xff_iter.next().is_some();
49-
if has_more_headers {
50-
// This only happens for requests going directly to crates-io.herokuapp.com,
51-
// since AWS CloudFront automatically merges these headers into one.
52-
//
53-
// The Heroku router has a bug where it currently (2023-10-25) appends
54-
// the connecting IP to the **first** header instead of the last.
55-
//
56-
// In this specific scenario we will read the IP from the first header,
57-
// instead of the last, to work around the Heroku bug. We also don't
58-
// have to care about the trusted proxies, since the request was
59-
// apparently sent to Heroku directly.
60-
61-
debug!(target: "real_ip", ?first_header, "Multiple X-Forwarded-For headers found, using the first one due to Heroku bug");
62-
63-
parse_xff_header(first_header)
64-
.into_iter()
65-
.filter_map(|r| r.ok())
66-
.next_back()
67-
} else {
68-
// If the request came in through CloudFront we only get a single,
69-
// merged header.
70-
//
71-
// If the request came in through Heroku and only had a single header
72-
// originally, then we also only get a single header.
73-
//
74-
// In this case return the right-most IP address that is not in the list
75-
// of IPs from trusted proxies (i.e. CloudFront).
76-
77-
debug!(target: "real_ip", ?first_header, "Single X-Forwarded-For header found");
78-
79-
parse_xff_header(first_header)
80-
.into_iter()
81-
.filter_map(|r| r.ok())
82-
.filter(|ip| !is_cloud_front_ip(ip))
83-
.next_back()
84-
}
46+
headers
47+
.get_all(X_FORWARDED_FOR)
48+
.iter()
49+
.flat_map(|header| {
50+
parse_xff_header(header)
51+
.into_iter()
52+
.filter_map(|r| r.ok())
53+
.filter(|ip| !is_cloud_front_ip(ip))
54+
})
55+
.next_back()
8556
}
8657

8758
/// Parses the content of an `X-Forwarded-For` header into a
@@ -142,11 +113,11 @@ mod tests {
142113
test(vec![b"1.1.1.1, 130.176.118.147"], Some("1.1.1.1"));
143114
test(vec![b"1.1.1.1, 2.2.2.2, 130.176.118.147"], Some("2.2.2.2"));
144115

145-
// Heroku workaround
146-
test(vec![b"1.1.1.1, 2.2.2.2", b"3.3.3.3"], Some("2.2.2.2"));
116+
// Multiple headers behavior
117+
test(vec![b"1.1.1.1, 2.2.2.2", b"3.3.3.3"], Some("3.3.3.3"));
147118
test(
148119
vec![b"1.1.1.1, 130.176.118.147", b"3.3.3.3"],
149-
Some("130.176.118.147"),
120+
Some("3.3.3.3"),
150121
);
151122
}
152123

0 commit comments

Comments
 (0)