1use crate::handler::BoxFuture;
37use crate::middleware::{Middleware, Next};
38use crate::request::Request;
39use crate::response::Response;
40use rustlavel_core::Config;
41use std::net::IpAddr;
42
43#[derive(Debug, Clone, Default)]
45pub struct Forwarded {
46 pub ip: Option<String>,
48 pub scheme: Option<String>,
50 pub host: Option<String>,
52 pub port: Option<u16>,
54}
55
56#[derive(Debug, Clone, PartialEq, Eq)]
58enum Trusted {
59 Any,
60 Address(IpAddr),
61 Network { base: IpAddr, prefix: u8 },
62}
63
64impl Trusted {
65 fn parse(entry: &str) -> Option<Trusted> {
66 let entry = entry.trim();
67 if entry == "*" || entry.eq_ignore_ascii_case("any") {
68 return Some(Trusted::Any);
69 }
70 match entry.split_once('/') {
71 None => entry.parse().ok().map(Trusted::Address),
72 Some((base, prefix)) => {
73 let base: IpAddr = base.trim().parse().ok()?;
74 let prefix: u8 = prefix.trim().parse().ok()?;
75 let width = if base.is_ipv4() { 32 } else { 128 };
76 (prefix <= width).then_some(Trusted::Network { base, prefix })
77 }
78 }
79 }
80
81 fn contains(&self, address: IpAddr) -> bool {
82 match self {
83 Trusted::Any => true,
84 Trusted::Address(trusted) => *trusted == address,
85 Trusted::Network { base, prefix } => in_network(*base, *prefix, address),
86 }
87 }
88}
89
90fn in_network(base: IpAddr, prefix: u8, address: IpAddr) -> bool {
99 let (base, address) = match (base, address) {
100 (IpAddr::V4(base), IpAddr::V4(address)) => (base.octets().to_vec(), address.octets().to_vec()),
101 (IpAddr::V6(base), IpAddr::V6(address)) => (base.octets().to_vec(), address.octets().to_vec()),
102 _ => return false,
103 };
104
105 let whole_bytes = (prefix / 8) as usize;
106 if base[..whole_bytes] != address[..whole_bytes] {
107 return false;
108 }
109 let leftover = prefix % 8;
110 if leftover == 0 {
111 return true;
112 }
113 let mask = 0xFFu8 << (8 - leftover);
114 base[whole_bytes] & mask == address[whole_bytes] & mask
115}
116
117#[derive(Debug, Clone, Default)]
118pub struct TrustProxies {
119 proxies: Vec<Trusted>,
120}
121
122impl TrustProxies {
123 pub fn none() -> Self {
126 TrustProxies::default()
127 }
128
129 pub fn at<I, S>(proxies: I) -> Self
135 where
136 I: IntoIterator<Item = S>,
137 S: AsRef<str>,
138 {
139 TrustProxies {
140 proxies: proxies.into_iter().filter_map(|p| Trusted::parse(p.as_ref())).collect(),
141 }
142 }
143
144 pub fn any() -> Self {
153 TrustProxies { proxies: vec![Trusted::Any] }
154 }
155
156 pub fn from_config(config: &Config) -> Self {
161 TrustProxies::at(config.list("trustedproxy.proxies"))
162 }
163
164 fn trusts(&self, address: IpAddr) -> bool {
165 self.proxies.iter().any(|proxy| proxy.contains(address))
166 }
167
168 fn client_from(&self, chain: &str) -> Option<String> {
176 let hops: Vec<&str> = chain.split(',').map(str::trim).filter(|h| !h.is_empty()).collect();
177 for hop in hops.iter().rev() {
178 let address = strip_port(hop).parse::<IpAddr>().ok()?;
179 if !self.trusts(address) {
180 return Some(address.to_string());
181 }
182 }
183 hops.first().map(|hop| strip_port(hop).to_string())
186 }
187}
188
189fn strip_port(hop: &str) -> &str {
191 let hop = hop.trim();
192 if let Some(rest) = hop.strip_prefix('[') {
193 return rest.split(']').next().unwrap_or(hop);
194 }
195 match hop.rsplit_once(':') {
197 Some((address, _)) if !address.contains(':') => address,
198 _ => hop,
199 }
200}
201
202impl Middleware for TrustProxies {
203 fn handle(&self, mut request: Request, next: Next) -> BoxFuture<Response> {
204 let peer = request.peer_addr().map(|addr| addr.ip());
205 if !peer.is_some_and(|peer| self.trusts(peer)) {
208 return next.run(request);
209 }
210
211 let mut forwarded = Forwarded::default();
212 if let Some(chain) = request.header("x-forwarded-for") {
213 forwarded.ip = self.client_from(chain);
214 }
215 if let Some(scheme) = request.header("x-forwarded-proto") {
216 let scheme = scheme.split(',').next().unwrap_or("").trim().to_ascii_lowercase();
217 if scheme == "http" || scheme == "https" {
218 forwarded.scheme = Some(scheme);
219 }
220 }
221 if let Some(host) = request.header("x-forwarded-host")
222 && let Some(host) = host.split(',').next().map(str::trim).filter(|h| !h.is_empty())
223 {
224 forwarded.host = Some(host.to_string());
225 }
226 if let Some(port) = request.header("x-forwarded-port") {
227 forwarded.port = port.split(',').next().and_then(|p| p.trim().parse().ok());
228 }
229
230 request.extend(forwarded);
231 next.run(request)
232 }
233}
234
235#[cfg(test)]
236mod tests {
237 use super::*;
238 use crate::method::Method;
239 use crate::router::Router;
240 use crate::testing::TestClient;
241
242 fn client(trust: TrustProxies) -> TestClient {
243 let mut router = Router::new();
244 router.middleware(trust);
245 router.get("/", |req: Request| async move {
246 Response::text(format!(
247 "{}|{}|{}",
248 req.ip().unwrap_or_else(|| "none".into()),
249 req.scheme(),
250 req.forwarded_host().unwrap_or("none")
251 ))
252 });
253 TestClient::new(router)
254 }
255
256 fn from(peer: &str, chain: &str) -> Request {
258 Request::new(Method::Get, "/")
259 .with_peer(format!("{peer}:44321").parse().expect("an address"))
260 .with_header("x-forwarded-for", chain)
261 }
262
263 #[tokio::test]
264 async fn without_a_trust_list_the_header_is_ignored_entirely() {
265 let response = client(TrustProxies::none()).send(from("203.0.113.9", "1.2.3.4")).await;
266 assert_eq!(response.body(), "203.0.113.9|http|none", "the peer, not what it claimed");
267 }
268
269 #[tokio::test]
270 async fn an_untrusted_peer_cannot_choose_its_own_address() {
271 let trust = TrustProxies::at(["10.0.0.0/8"]);
274 let response = client(trust).send(from("203.0.113.9", "9.9.9.9")).await;
275 assert_eq!(response.body(), "203.0.113.9|http|none");
276 }
277
278 #[tokio::test]
279 async fn a_trusted_proxy_is_believed() {
280 let trust = TrustProxies::at(["10.0.0.0/8"]);
281 let response = client(trust).send(from("10.1.2.3", "203.0.113.9")).await;
282 assert_eq!(response.body(), "203.0.113.9|http|none");
283 }
284
285 #[tokio::test]
286 async fn trusted_hops_are_stripped_from_the_right() {
287 let trust = TrustProxies::at(["10.0.0.0/8"]);
290 let request = from("10.0.0.2", "203.0.113.9, 10.0.0.1, 10.0.0.2");
291 assert_eq!(client(trust).send(request).await.body(), "203.0.113.9|http|none");
292 }
293
294 #[tokio::test]
295 async fn a_spoofed_prefix_before_the_real_client_is_not_believed() {
296 let trust = TrustProxies::at(["10.0.0.0/8"]);
299 let request = from("10.0.0.1", "9.9.9.9, 203.0.113.9");
300 assert_eq!(client(trust).send(request).await.body(), "203.0.113.9|http|none");
301 }
302
303 #[tokio::test]
304 async fn ports_are_stripped_from_forwarded_addresses() {
305 let trust = TrustProxies::at(["10.0.0.0/8"]);
306 let request = from("10.0.0.1", "203.0.113.9:51234");
307 assert_eq!(client(trust).send(request).await.body(), "203.0.113.9|http|none");
308 }
309
310 #[tokio::test]
311 async fn the_scheme_and_host_come_from_a_trusted_proxy_only() {
312 let trust = TrustProxies::at(["10.0.0.0/8"]);
313 let trusted = from("10.0.0.1", "203.0.113.9")
314 .with_header("x-forwarded-proto", "https")
315 .with_header("x-forwarded-host", "app.example.com");
316 assert_eq!(client(trust.clone()).send(trusted).await.body(), "203.0.113.9|https|app.example.com");
317
318 let spoofed = from("198.51.100.7", "1.2.3.4")
319 .with_header("x-forwarded-proto", "https")
320 .with_header("x-forwarded-host", "evil.example");
321 assert_eq!(client(trust).send(spoofed).await.body(), "198.51.100.7|http|none");
322 }
323
324 #[tokio::test]
325 async fn any_trusts_whoever_connected() {
326 let response = client(TrustProxies::any()).send(from("203.0.113.9", "1.2.3.4")).await;
327 assert_eq!(response.body(), "1.2.3.4|http|none");
328 }
329
330 #[test]
331 fn cidr_matching_covers_both_families_and_odd_prefixes() {
332 let ten = Trusted::parse("10.0.0.0/8").unwrap();
333 assert!(ten.contains("10.255.255.255".parse().unwrap()));
334 assert!(!ten.contains("11.0.0.1".parse().unwrap()));
335
336 let odd = Trusted::parse("192.168.4.0/22").unwrap();
338 assert!(odd.contains("192.168.7.255".parse().unwrap()));
339 assert!(!odd.contains("192.168.8.1".parse().unwrap()));
340
341 let v6 = Trusted::parse("2001:db8::/32").unwrap();
342 assert!(v6.contains("2001:db8:1234::1".parse().unwrap()));
343 assert!(!v6.contains("2001:db9::1".parse().unwrap()));
344 assert!(!v6.contains("10.0.0.1".parse().unwrap()), "families never match");
345
346 assert_eq!(Trusted::parse("not an address"), None);
347 assert_eq!(Trusted::parse("10.0.0.0/33"), None);
348 assert_eq!(Trusted::parse("*"), Some(Trusted::Any));
349 }
350
351 #[test]
352 fn a_typo_in_the_list_is_dropped_rather_than_widening_it() {
353 let trust = TrustProxies::at(["10.0.0.0/8", "hello", ""]);
354 assert_eq!(trust.proxies.len(), 1);
355 assert!(!trust.trusts("203.0.113.9".parse().unwrap()));
356 }
357
358 #[test]
359 fn from_config_reads_a_comma_separated_env_value() {
360 let config = Config::new();
361 config.set("trustedproxy.proxies", "10.0.0.0/8, 192.168.1.7");
362 let trust = TrustProxies::from_config(&config);
363 assert!(trust.trusts("10.9.9.9".parse().unwrap()));
364 assert!(trust.trusts("192.168.1.7".parse().unwrap()));
365 assert!(!trust.trusts("192.168.1.8".parse().unwrap()));
366
367 assert!(!TrustProxies::from_config(&Config::new()).trusts("10.0.0.1".parse().unwrap()));
368 }
369
370 #[test]
371 fn ipv6_hops_keep_their_colons() {
372 assert_eq!(strip_port("[2001:db8::1]:443"), "2001:db8::1");
373 assert_eq!(strip_port("2001:db8::1"), "2001:db8::1");
374 assert_eq!(strip_port("1.2.3.4:80"), "1.2.3.4");
375 assert_eq!(strip_port("1.2.3.4"), "1.2.3.4");
376 }
377}