Skip to main content

pacenotes_server/reports/
client_address.rs

1/// The client a request comes from, for rate limiting.
2///
3/// Behind Caddy every peer is Caddy, so `X-Forwarded-For` names the client, but
4/// only a trusted proxy's is believed: its right-most entry that is not itself a
5/// trusted proxy. An unparsable entry there falls back to the peer rather than
6/// reading further left, where the client writes.
7pub fn client_ip(
8    peer: std::net::IpAddr,
9    headers: &axum::http::HeaderMap,
10    trusted: &[super::Cidr],
11) -> std::net::IpAddr {
12    let peer = peer.to_canonical();
13    let is_trusted = |ip: std::net::IpAddr| trusted.iter().any(|network| network.contains(ip));
14    if !is_trusted(peer) {
15        return peer;
16    }
17    let hops: Vec<&str> = headers
18        .get_all("x-forwarded-for")
19        .iter()
20        .filter_map(|value| value.to_str().ok())
21        .flat_map(|value| value.split(','))
22        .map(str::trim)
23        .collect();
24    for hop in hops.into_iter().rev() {
25        match hop.parse::<std::net::IpAddr>() {
26            Ok(ip) if is_trusted(ip.to_canonical()) => continue,
27            Ok(ip) => return ip.to_canonical(),
28            Err(_) => return peer,
29        }
30    }
31    peer
32}
33
34#[cfg(test)]
35mod tests {
36    fn forwarded_for(value: &str) -> axum::http::HeaderMap {
37        let mut headers = axum::http::HeaderMap::new();
38        headers.insert(
39            "x-forwarded-for",
40            axum::http::HeaderValue::from_str(value).unwrap(),
41        );
42        headers
43    }
44
45    fn ip(text: &str) -> std::net::IpAddr {
46        text.parse().unwrap()
47    }
48
49    fn trusted() -> Vec<super::super::Cidr> {
50        vec![super::super::Cidr::parse("10.0.0.0/24").unwrap()]
51    }
52
53    const CADDY: &str = "10.0.0.90";
54
55    #[test]
56    fn through_a_trusted_proxy_the_client_is_the_entry_it_appended() {
57        let client = super::client_ip(ip(CADDY), &forwarded_for("198.51.100.1"), &trusted());
58        assert_eq!(client, ip("198.51.100.1"));
59    }
60
61    #[test]
62    fn a_forged_entry_left_of_the_proxys_is_ignored() {
63        let headers = forwarded_for("1.1.1.1, 198.51.100.1");
64        assert_eq!(
65            super::client_ip(ip(CADDY), &headers, &trusted()),
66            ip("198.51.100.1")
67        );
68    }
69
70    #[test]
71    fn the_header_from_anyone_else_is_ignored() {
72        let stranger = ip("203.0.113.7");
73        let headers = forwarded_for("1.1.1.1");
74        assert_eq!(super::client_ip(stranger, &headers, &trusted()), stranger);
75        assert_eq!(
76            super::client_ip(ip(CADDY), &forwarded_for("198.51.100.1"), &[]),
77            ip(CADDY)
78        );
79    }
80
81    #[test]
82    fn garbage_where_the_client_belongs_falls_back_to_the_peer() {
83        let headers = forwarded_for("1.1.1.1, unknown");
84        assert_eq!(super::client_ip(ip(CADDY), &headers, &trusted()), ip(CADDY));
85    }
86}