From 7215761784aa647782121a6b22b48e1528bdca4a Mon Sep 17 00:00:00 2001 From: houseme Date: Sat, 25 Apr 2026 09:25:32 +0800 Subject: [PATCH] feat(trusted-proxies): add switchable simple and legacy modes (#2674) Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: houseme <4829346+houseme@users.noreply.github.com> --- crates/config/src/constants/proxy.rs | 5 + crates/trusted-proxies/README.md | 31 ++ crates/trusted-proxies/src/config/env.rs | 5 +- crates/trusted-proxies/src/global.rs | 45 +- crates/trusted-proxies/src/lib.rs | 11 +- .../trusted-proxies/src/middleware/layer.rs | 6 +- .../trusted-proxies/src/middleware/service.rs | 4 +- crates/trusted-proxies/src/simple.rs | 463 ++++++++++++++++++ .../tests/integration/proxy_tests.rs | 4 +- crates/trusted-proxies/tests/proxy_layer.rs | 66 +++ 10 files changed, 606 insertions(+), 34 deletions(-) create mode 100644 crates/trusted-proxies/src/simple.rs create mode 100644 crates/trusted-proxies/tests/proxy_layer.rs diff --git a/crates/config/src/constants/proxy.rs b/crates/config/src/constants/proxy.rs index 09a367205..647f19482 100644 --- a/crates/config/src/constants/proxy.rs +++ b/crates/config/src/constants/proxy.rs @@ -20,6 +20,11 @@ pub const ENV_TRUSTED_PROXY_ENABLED: &str = "RUSTFS_TRUSTED_PROXY_ENABLED"; /// Trusted proxy middleware is enabled by default. pub const DEFAULT_TRUSTED_PROXY_ENABLED: bool = true; +/// Environment variable to select the trusted proxy implementation. +pub const ENV_TRUSTED_PROXY_IMPLEMENTATION: &str = "RUSTFS_TRUSTED_PROXY_IMPLEMENTATION"; +/// The simplified implementation is used by default. +pub const DEFAULT_TRUSTED_PROXY_IMPLEMENTATION: &str = "simple"; + /// Environment variable for the proxy validation mode. pub const ENV_TRUSTED_PROXY_VALIDATION_MODE: &str = "RUSTFS_TRUSTED_PROXY_VALIDATION_MODE"; /// Default validation mode is "hop_by_hop". diff --git a/crates/trusted-proxies/README.md b/crates/trusted-proxies/README.md index 942ed5743..e3a3f39d4 100644 --- a/crates/trusted-proxies/README.md +++ b/crates/trusted-proxies/README.md @@ -4,6 +4,13 @@ The `rustfs-trusted-proxies` module provides secure and efficient management of ecosystem. It is designed to handle multi-layer proxy architectures, ensuring accurate client IP identification while maintaining a zero-trust security model. +## Modes + +- **Simple default**: only trusts forwarding headers when the direct peer IP is + internal. +- **Legacy full mode**: keeps the original proxy-chain validation, available + via `legacy_*` helpers. + ## Features - **Multi-Layer Proxy Validation**: Supports `Strict`, `Lenient`, and `HopByHop` validation modes to accurately identify @@ -23,6 +30,7 @@ The module is configured primarily through environment variables: | Variable | Default | Description | |-----------------------------------------------|---------------------|---------------------------------------------------------| | `RUSTFS_TRUSTED_PROXY_ENABLED` | `true` | Enable the trusted proxy middleware | +| `RUSTFS_TRUSTED_PROXY_IMPLEMENTATION` | `simple` | Select `simple` or `legacy` implementation | | `RUSTFS_TRUSTED_PROXY_VALIDATION_MODE` | `hop_by_hop` | Validation strategy (`strict`, `lenient`, `hop_by_hop`) | | `RUSTFS_TRUSTED_PROXY_NETWORKS` | `127.0.0.1,::1,...` | Comma-separated list of trusted CIDR ranges | | `RUSTFS_TRUSTED_PROXY_MAX_HOPS` | `10` | Maximum allowed proxy hops | @@ -58,6 +66,29 @@ let app = Router::new() }); ``` +### Simple default mode + +The default mode only trusts forwarding headers from internal IPs. + +```bash +RUSTFS_TRUSTED_PROXY_IMPLEMENTATION=simple +``` + +### Legacy mode + +The original implementation is still available: + +```rust +rustfs_trusted_proxies::legacy_init(); +let layer = rustfs_trusted_proxies::LegacyTrustedProxyLayer::enabled(config, None); +``` + +Or switch the global default path: + +```bash +RUSTFS_TRUSTED_PROXY_IMPLEMENTATION=legacy +``` + ### Accessing Client Info Retrieve the verified client information in your handlers or other middleware: diff --git a/crates/trusted-proxies/src/config/env.rs b/crates/trusted-proxies/src/config/env.rs index 53b887ff8..a982ae0d8 100644 --- a/crates/trusted-proxies/src/config/env.rs +++ b/crates/trusted-proxies/src/config/env.rs @@ -19,8 +19,8 @@ use ipnetwork::IpNetwork; use rustfs_config::{ ENV_TRUSTED_PROXY_CHAIN_CONTINUITY_CHECK, ENV_TRUSTED_PROXY_CLOUD_METADATA_ENABLED, ENV_TRUSTED_PROXY_CLOUD_METADATA_TIMEOUT, ENV_TRUSTED_PROXY_CLOUDFLARE_IPS_ENABLED, ENV_TRUSTED_PROXY_ENABLE_RFC7239, ENV_TRUSTED_PROXY_ENABLED, - ENV_TRUSTED_PROXY_EXTRA_PROXIES, ENV_TRUSTED_PROXY_IPS, ENV_TRUSTED_PROXY_MAX_HOPS, ENV_TRUSTED_PROXY_PROXIES, - ENV_TRUSTED_PROXY_VALIDATION_MODE, + ENV_TRUSTED_PROXY_EXTRA_PROXIES, ENV_TRUSTED_PROXY_IMPLEMENTATION, ENV_TRUSTED_PROXY_IPS, ENV_TRUSTED_PROXY_MAX_HOPS, + ENV_TRUSTED_PROXY_PROXIES, ENV_TRUSTED_PROXY_VALIDATION_MODE, }; use std::str::FromStr; // ==================== Helper Functions ==================== @@ -72,6 +72,7 @@ pub fn is_env_set(key: &str) -> bool { pub fn get_all_proxy_env_vars() -> Vec<(String, String)> { let vars = [ ENV_TRUSTED_PROXY_ENABLED, + ENV_TRUSTED_PROXY_IMPLEMENTATION, ENV_TRUSTED_PROXY_VALIDATION_MODE, ENV_TRUSTED_PROXY_ENABLE_RFC7239, ENV_TRUSTED_PROXY_MAX_HOPS, diff --git a/crates/trusted-proxies/src/global.rs b/crates/trusted-proxies/src/global.rs index 691e55bd0..162d1cb2e 100644 --- a/crates/trusted-proxies/src/global.rs +++ b/crates/trusted-proxies/src/global.rs @@ -17,7 +17,7 @@ //! This module provides a unified interface for initializing and using the //! trusted proxy functionality within the RustFS server. -use crate::{AppConfig, ConfigLoader, ProxyMetrics, TrustedProxyLayer, default_proxy_metrics}; +use crate::{AppConfig, ConfigLoader, LegacyTrustedProxyLayer, ProxyMetrics, default_proxy_metrics}; use rustfs_config::{DEFAULT_TRUSTED_PROXY_ENABLED, ENV_TRUSTED_PROXY_ENABLED}; use std::sync::Arc; use std::sync::OnceLock; @@ -29,7 +29,7 @@ static CONFIG: OnceLock> = OnceLock::new(); static METRICS: OnceLock> = OnceLock::new(); /// Global instance of the trusted proxy layer. -static PROXY_LAYER: OnceLock = OnceLock::new(); +static PROXY_LAYER: OnceLock = OnceLock::new(); /// Global flag indicating if the trusted proxy middleware is enabled. static ENABLED: OnceLock = OnceLock::new(); @@ -39,33 +39,32 @@ static ENABLED: OnceLock = OnceLock::new(); /// This function should be called once at the start of the application. /// It loads the configuration, initializes metrics, and sets up the proxy layer. pub fn init() { - // Check if the trusted proxy system is enabled via environment variable. - let enabled = rustfs_utils::get_env_bool(ENV_TRUSTED_PROXY_ENABLED, DEFAULT_TRUSTED_PROXY_ENABLED); - ENABLED.set(enabled).expect("Trusted proxy enabled flag already initialized"); + let enabled = is_enabled(); + ENABLED.get_or_init(|| enabled); if !enabled { tracing::info!("Trusted Proxies module is disabled via configuration"); return; } - // Load configuration from environment variables. - let config = Arc::new(ConfigLoader::from_env_or_default()); - CONFIG.set(config.clone()).expect("Trusted proxy config already initialized"); + let config = CONFIG.get_or_init(|| Arc::new(ConfigLoader::from_env_or_default())).clone(); - // Initialize metrics if enabled. - let metrics = if config.monitoring.metrics_enabled { - let m = default_proxy_metrics(enabled); - Some(m) - } else { - None - }; - METRICS - .set(metrics.clone()) - .expect("Trusted proxy metrics already initialized"); + METRICS.get_or_init(|| { + if config.monitoring.metrics_enabled { + Some(default_proxy_metrics(enabled)) + } else { + None + } + }); - // Initialize the trusted proxy layer. - let layer = TrustedProxyLayer::with_cache_config(config.proxy.clone(), config.cache.clone(), metrics, enabled); - PROXY_LAYER.set(layer).expect("Trusted proxy layer already initialized"); + PROXY_LAYER.get_or_init(|| { + LegacyTrustedProxyLayer::with_cache_config( + config.proxy.clone(), + config.cache.clone(), + METRICS.get().and_then(|m| m.clone()), + enabled, + ) + }); tracing::info!("Trusted Proxies module initialized"); ConfigLoader::print_summary(&config); @@ -78,7 +77,7 @@ pub fn init() { /// # Panics /// /// Panics if `init()` has not been called. -pub fn layer() -> &'static TrustedProxyLayer { +pub fn layer() -> &'static LegacyTrustedProxyLayer { PROXY_LAYER .get() .expect("Trusted proxy system not initialized. Call init() first.") @@ -102,5 +101,5 @@ pub fn metrics() -> Option<&'static ProxyMetrics> { /// Returns true if the trusted proxy system is enabled. pub fn is_enabled() -> bool { - *ENABLED.get().unwrap_or(&false) + *ENABLED.get_or_init(|| rustfs_utils::get_env_bool(ENV_TRUSTED_PROXY_ENABLED, DEFAULT_TRUSTED_PROXY_ENABLED)) } diff --git a/crates/trusted-proxies/src/lib.rs b/crates/trusted-proxies/src/lib.rs index 6252160a3..aebd1f658 100644 --- a/crates/trusted-proxies/src/lib.rs +++ b/crates/trusted-proxies/src/lib.rs @@ -18,12 +18,19 @@ mod error; mod global; mod middleware; mod proxy; +mod simple; mod utils; pub use cloud::*; pub use config::*; pub use error::*; -pub use global::{config as global_config, init, is_enabled, layer, metrics}; -pub use middleware::{TrustedProxyLayer, TrustedProxyMiddleware}; +pub use global::{ + config as legacy_global_config, init as legacy_init, is_enabled as legacy_is_enabled, layer as legacy_layer, + metrics as legacy_metrics, +}; +pub use middleware::{TrustedProxyLayer as LegacyTrustedProxyLayer, TrustedProxyMiddleware as LegacyTrustedProxyMiddleware}; pub use proxy::*; +pub use simple::{ + TrustedProxyImplementation, TrustedProxyLayer, TrustedProxyMiddleware, implementation, init, is_enabled, layer, +}; pub use utils::*; diff --git a/crates/trusted-proxies/src/middleware/layer.rs b/crates/trusted-proxies/src/middleware/layer.rs index 452a20ed0..62434efc9 100644 --- a/crates/trusted-proxies/src/middleware/layer.rs +++ b/crates/trusted-proxies/src/middleware/layer.rs @@ -17,9 +17,9 @@ use std::sync::Arc; use tower::Layer; +use crate::LegacyTrustedProxyMiddleware; use crate::ProxyValidator; use crate::TrustedProxyConfig; -use crate::TrustedProxyMiddleware; use crate::{CacheConfig, ProxyMetrics}; /// Tower Layer for the trusted proxy middleware. @@ -73,10 +73,10 @@ impl TrustedProxyLayer { } impl Layer for TrustedProxyLayer { - type Service = TrustedProxyMiddleware; + type Service = LegacyTrustedProxyMiddleware; fn layer(&self, inner: S) -> Self::Service { - TrustedProxyMiddleware { + LegacyTrustedProxyMiddleware { inner, validator: self.validator.clone(), enabled: self.enabled, diff --git a/crates/trusted-proxies/src/middleware/service.rs b/crates/trusted-proxies/src/middleware/service.rs index d1ce52845..0e0c69b9a 100644 --- a/crates/trusted-proxies/src/middleware/service.rs +++ b/crates/trusted-proxies/src/middleware/service.rs @@ -14,7 +14,7 @@ //! Tower service implementation for the trusted proxy middleware. -use crate::{ClientInfo, ProxyValidator, TrustedProxyLayer}; +use crate::{ClientInfo, ProxyValidator}; use http::Request; use std::sync::Arc; use std::task::{Context, Poll}; @@ -43,7 +43,7 @@ impl TrustedProxyMiddleware { } /// Creates a new `TrustedProxyMiddleware` from a `TrustedProxyLayer`. - pub fn from_layer(inner: S, layer: &TrustedProxyLayer) -> Self { + pub fn from_layer(inner: S, layer: &super::layer::TrustedProxyLayer) -> Self { Self::new(inner, layer.validator.clone(), layer.enabled) } } diff --git a/crates/trusted-proxies/src/simple.rs b/crates/trusted-proxies/src/simple.rs new file mode 100644 index 000000000..39e07fd0f --- /dev/null +++ b/crates/trusted-proxies/src/simple.rs @@ -0,0 +1,463 @@ +// Copyright 2024 RustFS Team +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Simplified trusted proxy mode. +//! +//! The crate keeps both the simplified and legacy implementations. The default +//! runtime path uses the simplified rule set, and an environment variable can +//! switch the global entrypoints to the legacy chain validator. + +use crate::{ClientInfo, LegacyTrustedProxyLayer, LegacyTrustedProxyMiddleware, ValidationMode, global}; +use axum::http::{HeaderMap, Request}; +use rustfs_config::{ + DEFAULT_TRUSTED_PROXY_ENABLED, DEFAULT_TRUSTED_PROXY_IMPLEMENTATION, ENV_TRUSTED_PROXY_ENABLED, + ENV_TRUSTED_PROXY_IMPLEMENTATION, +}; +use std::fmt; +use std::net::{IpAddr, SocketAddr}; +use std::str::FromStr; +use std::sync::OnceLock; +use std::task::{Context, Poll}; +use tower::{Layer, Service}; +use tracing::debug; + +/// Constant switch for the crate's default integration path. +pub const SIMPLE_INTERNAL_ONLY_DEFAULT: bool = true; + +const HEADER_FORWARDED: &str = "forwarded"; +const HEADER_X_FORWARDED_FOR: &str = "x-forwarded-for"; +const HEADER_X_REAL_IP: &str = "x-real-ip"; + +static ENABLED: OnceLock = OnceLock::new(); +static IMPLEMENTATION: OnceLock = OnceLock::new(); +static LAYER: OnceLock = OnceLock::new(); + +/// Selects which implementation is used by the global entrypoints. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)] +pub enum TrustedProxyImplementation { + #[default] + Simple, + Legacy, +} + +impl TrustedProxyImplementation { + fn from_env() -> Self { + parse_implementation(std::env::var(ENV_TRUSTED_PROXY_IMPLEMENTATION).ok().as_deref()) + } +} + +impl fmt::Display for TrustedProxyImplementation { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(match self { + Self::Simple => "simple", + Self::Legacy => "legacy", + }) + } +} + +/// Initializes the default trusted proxy implementation. +pub fn init() { + let enabled = is_enabled(); + let implementation = implementation(); + let _ = layer(); + + tracing::info!( + enabled, + implementation = %implementation, + simple_internal_only = SIMPLE_INTERNAL_ONLY_DEFAULT, + "Trusted proxy middleware initialized" + ); +} + +/// Returns whether the default trusted proxy implementation is enabled. +pub fn is_enabled() -> bool { + *ENABLED.get_or_init(|| rustfs_utils::get_env_bool(ENV_TRUSTED_PROXY_ENABLED, DEFAULT_TRUSTED_PROXY_ENABLED)) +} + +/// Returns the selected implementation. +pub fn implementation() -> TrustedProxyImplementation { + *IMPLEMENTATION.get_or_init(TrustedProxyImplementation::from_env) +} + +/// Returns the default trusted proxy layer. +pub fn layer() -> &'static TrustedProxyLayer { + LAYER.get_or_init(build_layer) +} + +fn build_layer() -> TrustedProxyLayer { + if !is_enabled() { + return TrustedProxyLayer::disabled(); + } + + match implementation() { + TrustedProxyImplementation::Simple => TrustedProxyLayer::enabled(), + TrustedProxyImplementation::Legacy => { + global::init(); + TrustedProxyLayer::legacy(global::layer().clone()) + } + } +} + +/// Public layer wrapper for both implementations. +#[derive(Clone, Debug)] +pub enum TrustedProxyLayer { + Simple(SimpleTrustedProxyLayer), + Legacy(LegacyTrustedProxyLayer), +} + +impl TrustedProxyLayer { + pub fn enabled() -> Self { + Self::Simple(SimpleTrustedProxyLayer::enabled()) + } + + pub fn disabled() -> Self { + Self::Simple(SimpleTrustedProxyLayer::disabled()) + } + + pub fn legacy(layer: LegacyTrustedProxyLayer) -> Self { + Self::Legacy(layer) + } + + pub fn is_enabled(&self) -> bool { + match self { + Self::Simple(layer) => layer.is_enabled(), + Self::Legacy(layer) => layer.is_enabled(), + } + } + + pub fn is_legacy(&self) -> bool { + matches!(self, Self::Legacy(_)) + } +} + +impl Layer for TrustedProxyLayer { + type Service = TrustedProxyMiddleware; + + fn layer(&self, inner: S) -> Self::Service { + match self { + Self::Simple(layer) => TrustedProxyMiddleware::Simple(layer.layer(inner)), + Self::Legacy(layer) => TrustedProxyMiddleware::Legacy(layer.layer(inner)), + } + } +} + +/// Public middleware wrapper for both implementations. +#[derive(Clone)] +pub enum TrustedProxyMiddleware { + Simple(SimpleTrustedProxyMiddleware), + Legacy(LegacyTrustedProxyMiddleware), +} + +impl Service> for TrustedProxyMiddleware +where + S: Service> + Clone + Send + 'static, + S::Future: Send, +{ + type Response = S::Response; + type Error = S::Error; + type Future = S::Future; + + fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { + match self { + Self::Simple(service) => service.poll_ready(cx), + Self::Legacy(service) => service.poll_ready(cx), + } + } + + fn call(&mut self, req: Request) -> Self::Future { + match self { + Self::Simple(service) => service.call(req), + Self::Legacy(service) => service.call(req), + } + } +} + +/// Minimal layer used by RustFS by default. +#[derive(Clone, Debug, Default)] +pub struct SimpleTrustedProxyLayer { + enabled: bool, +} + +impl SimpleTrustedProxyLayer { + pub fn enabled() -> Self { + Self { enabled: true } + } + + pub fn disabled() -> Self { + Self { enabled: false } + } + + pub fn is_enabled(&self) -> bool { + self.enabled + } +} + +impl Layer for SimpleTrustedProxyLayer { + type Service = SimpleTrustedProxyMiddleware; + + fn layer(&self, inner: S) -> Self::Service { + SimpleTrustedProxyMiddleware { + inner, + enabled: self.enabled, + } + } +} + +/// Minimal middleware used by RustFS by default. +#[derive(Clone)] +pub struct SimpleTrustedProxyMiddleware { + inner: S, + enabled: bool, +} + +impl Service> for SimpleTrustedProxyMiddleware +where + S: Service> + Clone + Send + 'static, + S::Future: Send, +{ + type Response = S::Response; + type Error = S::Error; + type Future = S::Future; + + fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { + self.inner.poll_ready(cx) + } + + fn call(&mut self, mut req: Request) -> Self::Future { + if self.enabled { + let peer_addr = req.extensions().get::().copied(); + let client_info = resolve_client_info(peer_addr, req.headers()); + req.extensions_mut().insert(client_info); + } else { + debug!("Simple trusted proxy middleware is disabled"); + } + + self.inner.call(req) + } +} + +fn resolve_client_info(peer_addr: Option, headers: &HeaderMap) -> ClientInfo { + let Some(peer_addr) = peer_addr else { + return ClientInfo::direct(SocketAddr::new(IpAddr::from([0, 0, 0, 0]), 0)); + }; + + if !is_internal_ip(peer_addr.ip()) { + return ClientInfo::direct(peer_addr); + } + + match forwarded_client_ip(headers) { + Some(real_ip) if is_usable_ip(real_ip) && real_ip != peer_addr.ip() => { + ClientInfo::from_trusted_proxy(real_ip, None, None, peer_addr.ip(), 1, ValidationMode::Lenient, Vec::new()) + } + _ => ClientInfo::direct(peer_addr), + } +} + +fn forwarded_client_ip(headers: &HeaderMap) -> Option { + parse_x_forwarded_for(headers) + .or_else(|| parse_single_ip_header(headers, HEADER_X_REAL_IP)) + .or_else(|| parse_forwarded_header(headers)) +} + +fn parse_x_forwarded_for(headers: &HeaderMap) -> Option { + let value = headers.get(HEADER_X_FORWARDED_FOR)?.to_str().ok()?; + let first = value.split(',').next()?.trim(); + parse_ip_token(first) +} + +fn parse_single_ip_header(headers: &HeaderMap, name: &str) -> Option { + let value = headers.get(name)?.to_str().ok()?; + parse_ip_token(value) +} + +fn parse_forwarded_header(headers: &HeaderMap) -> Option { + let value = headers.get(HEADER_FORWARDED)?.to_str().ok()?; + let first_entry = value.split(',').next()?.trim(); + + for part in first_entry.split(';') { + let Some((key, raw_value)) = part.split_once('=') else { + continue; + }; + if key.trim().eq_ignore_ascii_case("for") { + return parse_ip_token(raw_value.trim()); + } + } + + None +} + +fn parse_ip_token(value: &str) -> Option { + let value = value.trim().trim_matches('"'); + if value.is_empty() || value.eq_ignore_ascii_case("unknown") || value.starts_with('_') { + return None; + } + + if let Some(bracketed) = value.strip_prefix('[') + && let Some(end) = bracketed.find(']') + { + return IpAddr::from_str(&bracketed[..end]).ok(); + } + + if let Ok(ip) = IpAddr::from_str(value) { + return Some(ip); + } + + if let Ok(socket_addr) = SocketAddr::from_str(value) { + return Some(socket_addr.ip()); + } + + None +} + +fn is_internal_ip(ip: IpAddr) -> bool { + match ip { + IpAddr::V4(ip) => ip.is_private() || ip.is_loopback() || ip.is_link_local(), + IpAddr::V6(ip) => ip.is_loopback() || ip.is_unique_local() || ip.is_unicast_link_local(), + } +} + +fn is_usable_ip(ip: IpAddr) -> bool { + !ip.is_unspecified() && !ip.is_multicast() +} + +fn parse_implementation(value: Option<&str>) -> TrustedProxyImplementation { + match value.map(|v| v.trim().to_ascii_lowercase()) { + Some(mode) if mode == "legacy" || mode == "full" || mode == "full_legacy" => TrustedProxyImplementation::Legacy, + Some(mode) if mode == "simple" || mode == "internal_only" || mode == "internal-only" => { + TrustedProxyImplementation::Simple + } + Some(mode) if mode == DEFAULT_TRUSTED_PROXY_IMPLEMENTATION => TrustedProxyImplementation::Simple, + _ => TrustedProxyImplementation::Simple, + } +} + +#[cfg(test)] +mod tests { + use super::{ + ENV_TRUSTED_PROXY_IMPLEMENTATION, HEADER_FORWARDED, HEADER_X_FORWARDED_FOR, HEADER_X_REAL_IP, TrustedProxyImplementation, + TrustedProxyLayer, forwarded_client_ip, is_internal_ip, parse_implementation, parse_ip_token, resolve_client_info, + }; + use crate::ClientInfo; + use axum::http::{HeaderMap, HeaderValue}; + use serial_test::serial; + use std::net::{IpAddr, SocketAddr}; + + #[test] + fn test_simple_mode_is_default() { + assert!(TrustedProxyLayer::enabled().is_enabled()); + assert!(!TrustedProxyLayer::disabled().is_enabled()); + } + + #[test] + fn test_parse_implementation() { + assert_eq!(parse_implementation(Some("simple")), TrustedProxyImplementation::Simple); + assert_eq!(parse_implementation(Some("legacy")), TrustedProxyImplementation::Legacy); + assert_eq!(parse_implementation(Some("full")), TrustedProxyImplementation::Legacy); + assert_eq!(parse_implementation(Some("internal-only")), TrustedProxyImplementation::Simple); + assert_eq!(parse_implementation(Some("unknown")), TrustedProxyImplementation::Simple); + } + + #[test] + fn test_parse_ip_token() { + assert_eq!(parse_ip_token("203.0.113.10"), Some(IpAddr::from([203, 0, 113, 10]))); + assert_eq!(parse_ip_token("203.0.113.10:9000"), Some(IpAddr::from([203, 0, 113, 10]))); + assert_eq!(parse_ip_token("[2001:db8::10]:9000"), Some("2001:db8::10".parse().unwrap())); + assert_eq!(parse_ip_token("unknown"), None); + } + + #[test] + fn test_forwarded_header_priority() { + let mut headers = HeaderMap::new(); + headers.insert(HEADER_X_FORWARDED_FOR, HeaderValue::from_static("203.0.113.10, 10.0.0.5")); + headers.insert(HEADER_X_REAL_IP, HeaderValue::from_static("198.51.100.10")); + headers.insert(HEADER_FORWARDED, HeaderValue::from_static("for=192.0.2.60;proto=https")); + assert_eq!(forwarded_client_ip(&headers), Some(IpAddr::from([203, 0, 113, 10]))); + } + + #[test] + fn test_forwarded_header_fallback() { + let mut headers = HeaderMap::new(); + headers.insert(HEADER_FORWARDED, HeaderValue::from_static("for=203.0.113.10;proto=https")); + assert_eq!(forwarded_client_ip(&headers), Some(IpAddr::from([203, 0, 113, 10]))); + } + + #[test] + fn test_internal_peer_can_override_real_ip() { + let mut headers = HeaderMap::new(); + headers.insert(HEADER_X_FORWARDED_FOR, HeaderValue::from_static("203.0.113.10")); + + let client_info = resolve_client_info(Some(SocketAddr::from(([10, 0, 0, 5], 9000))), &headers); + assert_eq!(client_info.real_ip, IpAddr::from([203, 0, 113, 10])); + assert!(client_info.is_from_trusted_proxy); + assert_eq!(client_info.proxy_ip, Some(IpAddr::from([10, 0, 0, 5]))); + } + + #[test] + fn test_public_peer_keeps_direct_ip() { + let mut headers = HeaderMap::new(); + headers.insert(HEADER_X_FORWARDED_FOR, HeaderValue::from_static("203.0.113.10")); + + let peer_addr = SocketAddr::from(([8, 8, 8, 8], 9000)); + let client_info = resolve_client_info(Some(peer_addr), &headers); + assert_eq!(client_info.real_ip, peer_addr.ip()); + assert!(!client_info.is_from_trusted_proxy); + } + + #[test] + fn test_missing_headers_keep_direct_ip() { + let peer_addr = SocketAddr::from(([192, 168, 1, 20], 9000)); + let client_info = resolve_client_info(Some(peer_addr), &HeaderMap::new()); + assert_eq!(client_info.real_ip, peer_addr.ip()); + assert!(!client_info.is_from_trusted_proxy); + } + + #[test] + fn test_missing_peer_addr_uses_direct_placeholder() { + let client_info = resolve_client_info(None, &HeaderMap::new()); + assert_eq!( + client_info.real_ip, + ClientInfo::direct(SocketAddr::new(IpAddr::from([0, 0, 0, 0]), 0)).real_ip + ); + } + + #[test] + fn test_forwarded_header_segment_without_equals() { + // A segment without '=' before 'for=' must not abort parsing. + let mut headers = HeaderMap::new(); + headers.insert(HEADER_FORWARDED, HeaderValue::from_static("proto;for=203.0.113.10")); + assert_eq!(forwarded_client_ip(&headers), Some(IpAddr::from([203, 0, 113, 10]))); + } + + #[test] + fn test_parse_ip_token_invalid_port_rejected() { + // A bare "ip:notaport" token must not be accepted as a valid IP. + assert_eq!(parse_ip_token("203.0.113.10:notaport"), None); + } + + #[test] + fn test_internal_ip_detection() { + assert!(is_internal_ip(IpAddr::from([10, 0, 0, 1]))); + assert!(is_internal_ip(IpAddr::from([127, 0, 0, 1]))); + assert!(is_internal_ip("fd00::1".parse().unwrap())); + assert!(!is_internal_ip(IpAddr::from([203, 0, 113, 10]))); + } + + #[test] + #[serial] + fn test_implementation_from_env() { + temp_env::with_vars(vec![(ENV_TRUSTED_PROXY_IMPLEMENTATION, Some("legacy"))], || { + assert_eq!(TrustedProxyImplementation::from_env(), TrustedProxyImplementation::Legacy); + }); + } +} diff --git a/crates/trusted-proxies/tests/integration/proxy_tests.rs b/crates/trusted-proxies/tests/integration/proxy_tests.rs index 480152497..db0b02260 100644 --- a/crates/trusted-proxies/tests/integration/proxy_tests.rs +++ b/crates/trusted-proxies/tests/integration/proxy_tests.rs @@ -14,14 +14,14 @@ use axum::body::Body; use axum::{Router, routing::get}; -use rustfs_trusted_proxies::{TrustedProxy, TrustedProxyConfig, TrustedProxyLayer, ValidationMode}; +use rustfs_trusted_proxies::{LegacyTrustedProxyLayer, TrustedProxy, TrustedProxyConfig, ValidationMode}; use tower::ServiceExt; #[tokio::test] async fn test_proxy_validation_flow() { let proxies = vec![TrustedProxy::Single("127.0.0.1".parse().unwrap())]; let config = TrustedProxyConfig::new(proxies, ValidationMode::HopByHop, true, 10, true, vec![]); - let proxy_layer = TrustedProxyLayer::enabled(config, None); + let proxy_layer = LegacyTrustedProxyLayer::enabled(config, None); let app = Router::new().route("/test", get(|| async { "OK" })).layer(proxy_layer); diff --git a/crates/trusted-proxies/tests/proxy_layer.rs b/crates/trusted-proxies/tests/proxy_layer.rs new file mode 100644 index 000000000..afe1f331a --- /dev/null +++ b/crates/trusted-proxies/tests/proxy_layer.rs @@ -0,0 +1,66 @@ +// Copyright 2024 RustFS Team +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use axum::{ + body::{self, Body}, + http::{Request, Response}, +}; +use rustfs_trusted_proxies::{ClientInfo, TrustedProxyLayer}; +use std::convert::Infallible; +use std::net::SocketAddr; +use tower::{Layer, ServiceExt, service_fn}; + +#[tokio::test] +async fn test_layer_inserts_client_info_for_internal_proxy() { + let peer_addr = SocketAddr::from(([10, 0, 0, 5], 9000)); + let service = service_fn(|request: Request| async move { + let client_info = request.extensions().get::().cloned().unwrap(); + Ok::<_, Infallible>(Response::new(Body::from(client_info.real_ip.to_string()))) + }); + let service = TrustedProxyLayer::enabled().layer(service); + + let mut request = Request::builder() + .uri("/") + .header("x-forwarded-for", "203.0.113.10") + .body(Body::empty()) + .unwrap(); + request.extensions_mut().insert(peer_addr); + + let response = service.oneshot(request).await.unwrap(); + + let body = body::to_bytes(response.into_body(), usize::MAX).await.unwrap(); + assert_eq!(std::str::from_utf8(&body).unwrap(), "203.0.113.10"); +} + +#[tokio::test] +async fn test_layer_ignores_forwarded_header_for_public_peer() { + let peer_addr = SocketAddr::from(([8, 8, 8, 8], 9000)); + let service = service_fn(|request: Request| async move { + let client_info = request.extensions().get::().cloned().unwrap(); + Ok::<_, Infallible>(Response::new(Body::from(client_info.real_ip.to_string()))) + }); + let service = TrustedProxyLayer::enabled().layer(service); + + let mut request = Request::builder() + .uri("/") + .header("x-forwarded-for", "203.0.113.10") + .body(Body::empty()) + .unwrap(); + request.extensions_mut().insert(peer_addr); + + let response = service.oneshot(request).await.unwrap(); + + let body = body::to_bytes(response.into_body(), usize::MAX).await.unwrap(); + assert_eq!(std::str::from_utf8(&body).unwrap(), "8.8.8.8"); +}