relay_server/extractors/
forwarded_for.rs

1use std::convert::Infallible;
2use std::net::SocketAddr;
3
4use axum::extract::{ConnectInfo, FromRequestParts};
5use axum::http::HeaderMap;
6use axum::http::request::Parts;
7
8#[derive(Debug)]
9pub struct ForwardedFor(String);
10
11impl ForwardedFor {
12    /// The defacto standard header for identifying the originating IP address of a client, [`X-Forwarded-For`].
13    ///
14    /// [`X-Forwarded-For`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For)
15    const FORWARDED_HEADER: &'static str = "X-Forwarded-For";
16    /// Sentry's custom forwarded for header.
17    ///
18    /// Clients or proxies can use `X-Sentry-Forwarded-For` when they cannot control the value of
19    /// the [`X-Forwarded-For`](Self::FORWARDED_HEADER) header.
20    ///
21    /// The Sentry SaaS infrastructure sets this header.
22    const SENTRY_FORWARDED_HEADER: &'static str = "X-Sentry-Forwarded-For";
23    /// Vercel forwards the client ip in its own [`X-Vercel-Forwarded-For`] header.
24    ///
25    /// [`X-Vercel-Forwarded-For`](https://vercel.com/docs/concepts/edge-network/headers#x-vercel-forwarded-for)
26    const VERCEL_FORWARDED_HEADER: &'static str = "X-Vercel-Forwarded-For";
27    /// Cloudflare forwards the client ip in its own [`CF-Connecting-IP`] header.
28    ///
29    /// [`CF-Connecting-IP`](https://developers.cloudflare.com/fundamentals/reference/http-request-headers/#cf-connecting-ip)
30    const CLOUDFLARE_FORWARDED_HEADER: &'static str = "CF-Connecting-IP";
31
32    /// Extracts the clients ip from a [`HeaderMap`].
33    ///
34    /// The function prioritizes more specific vendor headers over the more generic/common headers
35    /// to allow clients to override and modify headers even when they do not have control over
36    /// reverse proxies.
37    ///
38    /// First match wins in order:
39    /// - [`Self::CLOUDFLARE_FORWARDED_HEADER`], highest priority since users may use Cloudflare
40    ///   infront of Vercel, it is generally the first layer.
41    /// - [`Self::VERCEL_FORWARDED_HEADER`]
42    /// - [`Self::SENTRY_FORWARDED_HEADER`]
43    /// - [`Self::FORWARDED_HEADER`].
44    fn get_forwarded_for_ip(header_map: &HeaderMap) -> Option<&str> {
45        // List of headers to check from highest to lowest priority.
46        let headers = [
47            Self::CLOUDFLARE_FORWARDED_HEADER,
48            Self::VERCEL_FORWARDED_HEADER,
49            Self::SENTRY_FORWARDED_HEADER,
50            Self::FORWARDED_HEADER,
51        ];
52
53        headers
54            .into_iter()
55            .flat_map(|header| header_map.get(header))
56            .flat_map(|value| value.to_str().ok())
57            .find(|value| !value.is_empty())
58    }
59
60    pub fn into_inner(self) -> String {
61        self.0
62    }
63}
64
65impl AsRef<str> for ForwardedFor {
66    fn as_ref(&self) -> &str {
67        &self.0
68    }
69}
70
71impl From<ForwardedFor> for String {
72    fn from(forwarded: ForwardedFor) -> Self {
73        forwarded.into_inner()
74    }
75}
76
77impl<S> FromRequestParts<S> for ForwardedFor
78where
79    S: Send + Sync,
80{
81    type Rejection = Infallible;
82
83    async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
84        let peer_addr = ConnectInfo::<SocketAddr>::from_request_parts(parts, state)
85            .await
86            .map(|ConnectInfo(peer)| peer.ip().to_string())
87            .ok();
88
89        let forwarded = Self::get_forwarded_for_ip(&parts.headers);
90
91        Ok(ForwardedFor(match (forwarded, peer_addr) {
92            (None, None) => String::new(),
93            (None, Some(peer_addr)) => peer_addr,
94            (Some(forwarded), None) => forwarded.to_owned(),
95            (Some(forwarded), Some(peer_addr)) => format!("{forwarded}, {peer_addr}"),
96        }))
97    }
98}
99
100#[cfg(test)]
101mod tests {
102    use super::*;
103    use axum::http::HeaderValue;
104
105    #[test]
106    fn test_prefer_vercel_forwarded() {
107        let vercel_ip = "192.158.1.38";
108        let other_ip = "111.222.3.44";
109
110        let mut headermap = HeaderMap::default();
111        headermap.insert(
112            ForwardedFor::VERCEL_FORWARDED_HEADER,
113            HeaderValue::from_str(vercel_ip).unwrap(),
114        );
115        headermap.insert(
116            ForwardedFor::FORWARDED_HEADER,
117            HeaderValue::from_str(other_ip).unwrap(),
118        );
119
120        let forwarded = ForwardedFor::get_forwarded_for_ip(&headermap);
121
122        assert_eq!(forwarded, Some(vercel_ip));
123    }
124
125    #[test]
126    fn test_prefer_cf_forwarded() {
127        let cf_ip = "192.158.1.38";
128        let other_ip = "111.222.3.44";
129
130        let mut headermap = HeaderMap::default();
131        headermap.insert(
132            ForwardedFor::CLOUDFLARE_FORWARDED_HEADER,
133            HeaderValue::from_str(cf_ip).unwrap(),
134        );
135        headermap.insert(
136            ForwardedFor::FORWARDED_HEADER,
137            HeaderValue::from_str(other_ip).unwrap(),
138        );
139
140        let forwarded = ForwardedFor::get_forwarded_for_ip(&headermap);
141
142        assert_eq!(forwarded, Some(cf_ip));
143    }
144
145    #[test]
146    fn test_prefer_sentry_forwarded() {
147        let sentry_ip = "192.158.1.38";
148        let other_ip = "111.222.3.44";
149
150        let mut headermap = HeaderMap::default();
151        headermap.insert(
152            ForwardedFor::SENTRY_FORWARDED_HEADER,
153            HeaderValue::from_str(sentry_ip).unwrap(),
154        );
155        headermap.insert(
156            ForwardedFor::FORWARDED_HEADER,
157            HeaderValue::from_str(other_ip).unwrap(),
158        );
159
160        let forwarded = ForwardedFor::get_forwarded_for_ip(&headermap);
161
162        assert_eq!(forwarded, Some(sentry_ip));
163    }
164
165    /// If there's no vercel or sentry header then use the normal `X-Forwarded-For`-header.
166    #[test]
167    fn test_fall_back_on_forwarded_for_header() {
168        let other_ip = "111.222.3.44";
169
170        let mut headermap = HeaderMap::default();
171        headermap.insert(
172            ForwardedFor::FORWARDED_HEADER,
173            HeaderValue::from_str(other_ip).unwrap(),
174        );
175
176        let forwarded = ForwardedFor::get_forwarded_for_ip(&headermap);
177
178        assert_eq!(forwarded, Some(other_ip));
179    }
180
181    #[test]
182    fn test_get_none_if_empty_header() {
183        let mut headermap = HeaderMap::default();
184        headermap.insert(
185            ForwardedFor::FORWARDED_HEADER,
186            HeaderValue::from_str("").unwrap(),
187        );
188
189        let forwarded = ForwardedFor::get_forwarded_for_ip(&headermap);
190        assert!(forwarded.is_none());
191    }
192
193    #[test]
194    fn test_get_none_if_invalid_header() {
195        let other_ip = "111.222.3.44";
196
197        let mut headermap = HeaderMap::default();
198        headermap.insert("X-Invalid-Header", HeaderValue::from_str(other_ip).unwrap());
199
200        let forwarded = ForwardedFor::get_forwarded_for_ip(&headermap);
201        assert!(forwarded.is_none());
202    }
203}