fraiseql_auth/proxy.rs
1//! Proxy and IP address extraction with security validation
2
3use std::net::IpAddr;
4
5/// Validate that a string is a valid IP address format
6///
7/// # SECURITY
8/// Prevents injection attacks where malformed IPs could bypass validation.
9/// Returns None for any invalid IP format.
10fn validate_ip_format(ip_str: &str) -> Option<IpAddr> {
11 ip_str.parse::<IpAddr>().ok()
12}
13
14/// Proxy configuration for X-Forwarded-For header validation
15#[derive(Debug, Clone)]
16pub struct ProxyConfig {
17 /// List of trusted proxy IPs (e.g., load balancer, Nginx, HAProxy IPs)
18 /// Only X-Forwarded-For headers from these IPs are trusted
19 pub trusted_proxies: Vec<IpAddr>,
20 /// If true, require request to come from a trusted proxy to use X-Forwarded-For
21 pub require_trusted_proxy: bool,
22}
23
24impl ProxyConfig {
25 /// Create a new proxy configuration
26 #[must_use]
27 pub const fn new(trusted_proxies: Vec<IpAddr>, require_trusted_proxy: bool) -> Self {
28 Self {
29 trusted_proxies,
30 require_trusted_proxy,
31 }
32 }
33
34 /// Create a proxy config that trusts all local proxies (127.0.0.1 only).
35 ///
36 /// # Panics
37 ///
38 /// Cannot panic — the IP literal `"127.0.0.1"` is always valid.
39 #[must_use]
40 pub fn localhost_only() -> Self {
41 Self {
42 trusted_proxies: vec!["127.0.0.1".parse().expect("valid IP")], /* Reason: "127.0.0.1" is a compile-time literal and always parses successfully */
43 require_trusted_proxy: true,
44 }
45 }
46
47 /// Create a proxy config with no trusted proxies
48 #[must_use]
49 pub const fn none() -> Self {
50 Self {
51 trusted_proxies: Vec::new(),
52 require_trusted_proxy: false,
53 }
54 }
55
56 /// Check if an IP address is a trusted proxy
57 ///
58 /// # SECURITY
59 /// Validates IP format before checking against trusted list.
60 /// Returns false for any invalid IP format, preventing bypass attempts.
61 #[must_use]
62 pub fn is_trusted_proxy(&self, ip: &str) -> bool {
63 if self.trusted_proxies.is_empty() {
64 return false;
65 }
66
67 // Validate IP format and check against trusted list
68 match validate_ip_format(ip) {
69 Some(addr) => self.trusted_proxies.contains(&addr),
70 None => false, // Invalid IP format is not trusted
71 }
72 }
73
74 /// Extract client IP from headers with security validation
75 ///
76 /// # SECURITY
77 /// Only trusts X-Forwarded-For if the request comes from a trusted proxy.
78 /// Falls back to direct connection IP if X-Forwarded-For cannot be validated.
79 /// Validates all extracted IPs to ensure proper format.
80 ///
81 /// This prevents IP spoofing attacks where an attacker sends a malicious
82 /// X-Forwarded-For header to bypass rate limiting or access controls.
83 #[must_use]
84 pub fn extract_client_ip(
85 &self,
86 headers: &axum::http::HeaderMap,
87 socket_addr: Option<std::net::SocketAddr>,
88 ) -> Option<String> {
89 let direct_ip = socket_addr.map(|addr| addr.ip().to_string());
90
91 // If no direct IP available, return early
92 let direct_ip_str = direct_ip.as_deref().unwrap_or("");
93
94 // Check X-Forwarded-For if proxy is trusted
95 if let Some(forwarded_for) = headers.get("x-forwarded-for").and_then(|v| v.to_str().ok()) {
96 if self.is_trusted_proxy(direct_ip_str) {
97 // Extract first IP from X-Forwarded-For (client IP in chain)
98 if let Some(ip_str) = forwarded_for.split(',').next().map(|ip| ip.trim()) {
99 // SECURITY: Validate IP format before returning
100 if validate_ip_format(ip_str).is_some() {
101 return Some(ip_str.to_string());
102 }
103 // Invalid IP format - fall through to use direct IP
104 }
105 }
106 // X-Forwarded-For present but from untrusted proxy - ignore it and use direct IP
107 if let Some(ip) = direct_ip {
108 return Some(ip);
109 }
110 }
111
112 // Check X-Real-IP if proxy is trusted
113 if let Some(real_ip) = headers.get("x-real-ip").and_then(|v| v.to_str().ok()) {
114 if self.is_trusted_proxy(direct_ip_str) {
115 // SECURITY: Validate IP format before returning
116 if validate_ip_format(real_ip).is_some() {
117 return Some(real_ip.to_string());
118 }
119 // Invalid IP format - fall through to use direct IP
120 }
121 // X-Real-IP present but from untrusted proxy - ignore it and use direct IP
122 if let Some(ip) = direct_ip {
123 return Some(ip);
124 }
125 }
126
127 // Fall back to direct connection IP (already validated by Axum)
128 direct_ip
129 }
130}