featherbit/plugins/native/
real_ip.rs1use async_trait::async_trait;
12use std::collections::HashMap;
13use std::net::IpAddr;
14
15use ipnet::IpNet;
16
17use crate::context::Context;
18use crate::plugins::{Plugin, PluginOutput, PluginResult};
19
20pub struct RealIpPlugin {
28 source: String,
30 trusted_addresses: Option<Vec<IpNet>>,
34 recursive: bool,
37}
38
39fn parse_ip_port(addr: &str) -> Option<(IpAddr, Option<u16>)> {
43 let addr = addr.trim();
44
45 if let Some(rest) = addr.strip_prefix('[') {
47 let (ip, tail) = rest.split_once(']')?;
48 let ip: IpAddr = ip.parse().ok()?;
49 return match tail.strip_prefix(':') {
50 Some(p) => {
51 let port = p.parse::<u16>().ok().filter(|p| *p > 0)?;
52 Some((ip, Some(port)))
53 }
54 None if tail.is_empty() => Some((ip, None)),
55 None => None,
56 };
57 }
58
59 if let Ok(ip) = addr.parse::<IpAddr>() {
61 return Some((ip, None));
62 }
63
64 let (ip, port) = addr.rsplit_once(':')?;
66 if ip.contains(':') {
67 return None;
68 }
69 let ip: IpAddr = ip.parse().ok()?;
70 let port = port.parse::<u16>().ok().filter(|p| *p > 0)?;
71 Some((ip, Some(port)))
72}
73
74impl RealIpPlugin {
75 pub fn from_config(config: &HashMap<String, serde_json::Value>) -> Result<Self, String> {
100 let source = config
101 .get("source")
102 .and_then(|v| v.as_str())
103 .filter(|s| !s.is_empty())
104 .map(String::from)
105 .ok_or("real-ip plugin requires 'source' (a variable name, e.g. http_x_real_ip)")?;
106
107 let trusted_addresses = match config.get("trusted_addresses") {
108 None => None,
109 Some(raw) => {
110 let items = raw
111 .as_array()
112 .ok_or("trusted_addresses must be an array of IPs/CIDRs")?;
113 if items.is_empty() {
114 return Err("trusted_addresses must contain at least one IP/CIDR".to_string());
115 }
116 let nets = items
117 .iter()
118 .map(|item| {
119 let s = item
120 .as_str()
121 .ok_or("trusted_addresses items must be strings")?;
122 if let Ok(net) = s.parse::<IpNet>() {
123 Ok(net)
124 } else if let Ok(ip) = s.parse::<IpAddr>() {
125 Ok(IpNet::from(ip))
126 } else {
127 Err(format!("invalid ip address: {}", s))
128 }
129 })
130 .collect::<Result<Vec<_>, String>>()?;
131 Some(nets)
132 }
133 };
134
135 let recursive = config
136 .get("recursive")
137 .and_then(|v| v.as_bool())
138 .unwrap_or(false);
139
140 Ok(Self {
141 source,
142 trusted_addresses,
143 recursive,
144 })
145 }
146
147 fn is_trusted(&self, ip: IpAddr) -> bool {
150 self.trusted_addresses
151 .as_ref()
152 .is_some_and(|nets| nets.iter().any(|net| net.contains(&ip)))
153 }
154
155 fn get_addr(&self, ctx: &Context) -> Option<String> {
158 if self.source == "http_x_forwarded_for" {
159 let value = ctx.request.headers.get("x-forwarded-for")?.last()?;
162 let parts: Vec<&str> = value.split(',').map(str::trim).collect();
163
164 if parts.len() == 1 {
165 return Some(parts[0].to_string());
166 }
167
168 if self.recursive && self.trusted_addresses.is_some() {
169 for part in parts[1..].iter().rev() {
173 let trusted = part.parse::<IpAddr>().is_ok_and(|ip| self.is_trusted(ip));
174 if !trusted {
175 return Some(part.to_string());
176 }
177 }
178 return Some(parts[0].to_string());
179 }
180
181 return parts.last().map(|s| s.to_string());
183 }
184
185 crate::vars::resolve(ctx, &self.source).map(|v| v.into_owned())
186 }
187}
188
189#[async_trait]
190impl Plugin for RealIpPlugin {
191 fn plugin_type(&self) -> &str {
192 "real-ip"
193 }
194
195 async fn execute(&self, mut ctx: Context) -> PluginResult {
196 let passthrough = |ctx: Context| Ok(PluginOutput::success(ctx));
197
198 let direct = parse_ip_port(&ctx.request.remote_addr);
200 if self.trusted_addresses.is_some() {
201 match direct {
202 Some((ip, _)) if self.is_trusted(ip) => {}
203 _ => return passthrough(ctx),
204 }
205 }
206
207 let Some(addr) = self.get_addr(&ctx) else {
208 return passthrough(ctx);
209 };
210
211 let Some((ip, port)) = parse_ip_port(&addr) else {
212 return passthrough(ctx);
214 };
215
216 let port = port.or_else(|| direct.and_then(|(_, p)| p));
218 ctx.request.remote_addr = match (ip, port) {
219 (IpAddr::V6(v6), Some(p)) => format!("[{}]:{}", v6, p),
220 (ip, Some(p)) => format!("{}:{}", ip, p),
221 (ip, None) => ip.to_string(),
222 };
223
224 passthrough(ctx)
225 }
226}
227
228#[cfg(test)]
229mod tests {
230 use super::*;
231 use crate::context::{GatewayRequest, GatewayResponse, Protocol};
232 use bytes::Bytes;
233
234 fn test_context(remote_addr: &str) -> Context {
235 Context {
236 request: GatewayRequest {
237 method: "GET".to_string(),
238 path: "/".to_string(),
239 host: "localhost".to_string(),
240 scheme: "http".to_string(),
241 headers: HashMap::new(),
242 query_params: HashMap::new(),
243 body: Bytes::new(),
244 remote_addr: remote_addr.to_string(),
245 protocol: Protocol::Http1,
246 },
247 response: GatewayResponse {
248 status_code: 0,
249 headers: HashMap::new(),
250 body: Bytes::new(),
251 stream: None,
252 },
253 message: HashMap::new(),
254 errors: Vec::new(),
255 }
256 }
257
258 fn config(json: serde_json::Value) -> HashMap<String, serde_json::Value> {
259 serde_json::from_value(json).unwrap()
260 }
261
262 #[test]
263 fn test_real_ip_config_validation() {
264 assert!(RealIpPlugin::from_config(&HashMap::new()).is_err());
266
267 assert!(RealIpPlugin::from_config(&config(serde_json::json!({
269 "source": "http_x_real_ip",
270 "trusted_addresses": ["not-an-ip"]
271 })))
272 .is_err());
273
274 assert!(RealIpPlugin::from_config(&config(serde_json::json!({
276 "source": "http_x_real_ip",
277 "trusted_addresses": []
278 })))
279 .is_err());
280
281 assert!(RealIpPlugin::from_config(&config(serde_json::json!({
283 "source": "http_x_forwarded_for",
284 "trusted_addresses": ["10.0.0.0/8", "127.0.0.1"],
285 "recursive": true
286 })))
287 .is_ok());
288 }
289
290 #[tokio::test]
291 async fn test_real_ip_rewrites_from_x_real_ip() {
292 let plugin = RealIpPlugin::from_config(&config(serde_json::json!({
293 "source": "http_x_real_ip",
294 "trusted_addresses": ["127.0.0.0/24"]
295 })))
296 .unwrap();
297
298 let mut ctx = test_context("127.0.0.1:5000");
299 ctx.request
300 .headers
301 .insert("x-real-ip".to_string(), vec!["203.0.113.7".to_string()]);
302
303 let result = plugin.execute(ctx).await.unwrap();
304 assert_eq!(result.context.request.remote_addr, "203.0.113.7:5000");
306 }
307
308 #[tokio::test]
309 async fn test_real_ip_untrusted_peer_is_passthrough() {
310 let plugin = RealIpPlugin::from_config(&config(serde_json::json!({
311 "source": "http_x_real_ip",
312 "trusted_addresses": ["127.0.0.0/24"]
313 })))
314 .unwrap();
315
316 let mut ctx = test_context("198.51.100.9:5000");
317 ctx.request
318 .headers
319 .insert("x-real-ip".to_string(), vec!["203.0.113.7".to_string()]);
320
321 let result = plugin.execute(ctx).await.unwrap();
322 assert_eq!(result.context.request.remote_addr, "198.51.100.9:5000");
323 }
324
325 #[tokio::test]
326 async fn test_real_ip_missing_or_bad_source_is_passthrough() {
327 let plugin = RealIpPlugin::from_config(&config(serde_json::json!({
328 "source": "http_x_real_ip",
329 "trusted_addresses": ["127.0.0.0/24"]
330 })))
331 .unwrap();
332
333 let ctx = test_context("127.0.0.1:5000");
335 let result = plugin.execute(ctx).await.unwrap();
336 assert_eq!(result.context.request.remote_addr, "127.0.0.1:5000");
337
338 let mut ctx = test_context("127.0.0.1:5000");
340 ctx.request
341 .headers
342 .insert("x-real-ip".to_string(), vec!["unknown".to_string()]);
343 let result = plugin.execute(ctx).await.unwrap();
344 assert_eq!(result.context.request.remote_addr, "127.0.0.1:5000");
345 }
346
347 #[tokio::test]
348 async fn test_real_ip_xff_non_recursive_takes_last() {
349 let plugin = RealIpPlugin::from_config(&config(serde_json::json!({
350 "source": "http_x_forwarded_for",
351 "trusted_addresses": ["127.0.0.0/24"]
352 })))
353 .unwrap();
354
355 let mut ctx = test_context("127.0.0.1:5000");
356 ctx.request.headers.insert(
357 "x-forwarded-for".to_string(),
358 vec!["203.0.113.7, 10.1.1.1, 10.2.2.2".to_string()],
359 );
360
361 let result = plugin.execute(ctx).await.unwrap();
362 assert_eq!(result.context.request.remote_addr, "10.2.2.2:5000");
363 }
364
365 #[tokio::test]
366 async fn test_real_ip_xff_recursive_skips_trusted_hops() {
367 let plugin = RealIpPlugin::from_config(&config(serde_json::json!({
368 "source": "http_x_forwarded_for",
369 "trusted_addresses": ["127.0.0.0/24", "10.0.0.0/8"],
370 "recursive": true
371 })))
372 .unwrap();
373
374 let mut ctx = test_context("127.0.0.1:5000");
377 ctx.request.headers.insert(
378 "x-forwarded-for".to_string(),
379 vec!["203.0.113.7, 10.1.1.1, 10.2.2.2".to_string()],
380 );
381 let result = plugin.execute(ctx).await.unwrap();
382 assert_eq!(result.context.request.remote_addr, "203.0.113.7:5000");
383
384 let mut ctx = test_context("127.0.0.1:5000");
386 ctx.request.headers.insert(
387 "x-forwarded-for".to_string(),
388 vec!["10.9.9.9, 10.1.1.1".to_string()],
389 );
390 let result = plugin.execute(ctx).await.unwrap();
391 assert_eq!(result.context.request.remote_addr, "10.9.9.9:5000");
392 }
393
394 #[tokio::test]
395 async fn test_real_ip_source_port_wins_over_peer_port() {
396 let plugin = RealIpPlugin::from_config(&config(serde_json::json!({
397 "source": "http_x_real_ip"
398 })))
399 .unwrap();
400
401 let mut ctx = test_context("127.0.0.1:5000");
403 ctx.request.headers.insert(
404 "x-real-ip".to_string(),
405 vec!["203.0.113.7:8443".to_string()],
406 );
407 let result = plugin.execute(ctx).await.unwrap();
408 assert_eq!(result.context.request.remote_addr, "203.0.113.7:8443");
409 }
410
411 #[tokio::test]
412 async fn test_real_ip_ipv6_source() {
413 let plugin = RealIpPlugin::from_config(&config(serde_json::json!({
414 "source": "http_x_real_ip",
415 "trusted_addresses": ["127.0.0.0/24"]
416 })))
417 .unwrap();
418
419 let mut ctx = test_context("127.0.0.1:5000");
420 ctx.request.headers.insert(
421 "x-real-ip".to_string(),
422 vec!["[2001:db8::1]:9000".to_string()],
423 );
424 let result = plugin.execute(ctx).await.unwrap();
425 assert_eq!(result.context.request.remote_addr, "[2001:db8::1]:9000");
426
427 let mut ctx = test_context("127.0.0.1:5000");
428 ctx.request
429 .headers
430 .insert("x-real-ip".to_string(), vec!["2001:db8::1".to_string()]);
431 let result = plugin.execute(ctx).await.unwrap();
432 assert_eq!(result.context.request.remote_addr, "[2001:db8::1]:5000");
434 }
435
436 #[test]
437 fn test_real_ip_parse_ip_port_forms() {
438 assert_eq!(
439 parse_ip_port("1.2.3.4:80"),
440 Some(("1.2.3.4".parse().unwrap(), Some(80)))
441 );
442 assert_eq!(
443 parse_ip_port("1.2.3.4"),
444 Some(("1.2.3.4".parse().unwrap(), None))
445 );
446 assert_eq!(
447 parse_ip_port("[::1]:8080"),
448 Some(("::1".parse().unwrap(), Some(8080)))
449 );
450 assert_eq!(parse_ip_port("::1"), Some(("::1".parse().unwrap(), None)));
451 assert_eq!(parse_ip_port("1.2.3.4:0"), None); assert_eq!(parse_ip_port("1.2.3.4:99999"), None);
453 assert_eq!(parse_ip_port("nonsense"), None);
454 }
455}