mirror of
https://github.com/rustfs/rustfs.git
synced 2026-05-07 06:37:42 +08:00
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>
This commit is contained in:
@@ -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".
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<Arc<AppConfig>> = OnceLock::new();
|
||||
static METRICS: OnceLock<Option<ProxyMetrics>> = OnceLock::new();
|
||||
|
||||
/// Global instance of the trusted proxy layer.
|
||||
static PROXY_LAYER: OnceLock<TrustedProxyLayer> = OnceLock::new();
|
||||
static PROXY_LAYER: OnceLock<LegacyTrustedProxyLayer> = OnceLock::new();
|
||||
|
||||
/// Global flag indicating if the trusted proxy middleware is enabled.
|
||||
static ENABLED: OnceLock<bool> = OnceLock::new();
|
||||
@@ -39,33 +39,32 @@ static ENABLED: OnceLock<bool> = 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))
|
||||
}
|
||||
|
||||
@@ -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::*;
|
||||
|
||||
@@ -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<S> Layer<S> for TrustedProxyLayer {
|
||||
type Service = TrustedProxyMiddleware<S>;
|
||||
type Service = LegacyTrustedProxyMiddleware<S>;
|
||||
|
||||
fn layer(&self, inner: S) -> Self::Service {
|
||||
TrustedProxyMiddleware {
|
||||
LegacyTrustedProxyMiddleware {
|
||||
inner,
|
||||
validator: self.validator.clone(),
|
||||
enabled: self.enabled,
|
||||
|
||||
@@ -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<S> TrustedProxyMiddleware<S> {
|
||||
}
|
||||
|
||||
/// 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)
|
||||
}
|
||||
}
|
||||
|
||||
463
crates/trusted-proxies/src/simple.rs
Normal file
463
crates/trusted-proxies/src/simple.rs
Normal file
@@ -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<bool> = OnceLock::new();
|
||||
static IMPLEMENTATION: OnceLock<TrustedProxyImplementation> = OnceLock::new();
|
||||
static LAYER: OnceLock<TrustedProxyLayer> = 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<S> Layer<S> for TrustedProxyLayer {
|
||||
type Service = TrustedProxyMiddleware<S>;
|
||||
|
||||
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<S> {
|
||||
Simple(SimpleTrustedProxyMiddleware<S>),
|
||||
Legacy(LegacyTrustedProxyMiddleware<S>),
|
||||
}
|
||||
|
||||
impl<S, ReqBody> Service<Request<ReqBody>> for TrustedProxyMiddleware<S>
|
||||
where
|
||||
S: Service<Request<ReqBody>> + 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<Result<(), Self::Error>> {
|
||||
match self {
|
||||
Self::Simple(service) => service.poll_ready(cx),
|
||||
Self::Legacy(service) => service.poll_ready(cx),
|
||||
}
|
||||
}
|
||||
|
||||
fn call(&mut self, req: Request<ReqBody>) -> 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<S> Layer<S> for SimpleTrustedProxyLayer {
|
||||
type Service = SimpleTrustedProxyMiddleware<S>;
|
||||
|
||||
fn layer(&self, inner: S) -> Self::Service {
|
||||
SimpleTrustedProxyMiddleware {
|
||||
inner,
|
||||
enabled: self.enabled,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Minimal middleware used by RustFS by default.
|
||||
#[derive(Clone)]
|
||||
pub struct SimpleTrustedProxyMiddleware<S> {
|
||||
inner: S,
|
||||
enabled: bool,
|
||||
}
|
||||
|
||||
impl<S, ReqBody> Service<Request<ReqBody>> for SimpleTrustedProxyMiddleware<S>
|
||||
where
|
||||
S: Service<Request<ReqBody>> + 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<Result<(), Self::Error>> {
|
||||
self.inner.poll_ready(cx)
|
||||
}
|
||||
|
||||
fn call(&mut self, mut req: Request<ReqBody>) -> Self::Future {
|
||||
if self.enabled {
|
||||
let peer_addr = req.extensions().get::<SocketAddr>().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<SocketAddr>, 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<IpAddr> {
|
||||
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<IpAddr> {
|
||||
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<IpAddr> {
|
||||
let value = headers.get(name)?.to_str().ok()?;
|
||||
parse_ip_token(value)
|
||||
}
|
||||
|
||||
fn parse_forwarded_header(headers: &HeaderMap) -> Option<IpAddr> {
|
||||
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<IpAddr> {
|
||||
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);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
|
||||
66
crates/trusted-proxies/tests/proxy_layer.rs
Normal file
66
crates/trusted-proxies/tests/proxy_layer.rs
Normal file
@@ -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<Body>| async move {
|
||||
let client_info = request.extensions().get::<ClientInfo>().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<Body>| async move {
|
||||
let client_info = request.extensions().get::<ClientInfo>().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");
|
||||
}
|
||||
Reference in New Issue
Block a user