Compare commits

...

28 Commits

Author SHA1 Message Date
houseme
0b3dfaa587 add feature for hyper-util 2026-01-16 17:19:52 +08:00
houseme
2fd57696de Merge branch 'main' of github.com:rustfs/rustfs into fix/axum-trusted-proxies 2026-01-16 11:46:30 +08:00
houseme
bcc2831384 fmt and remove unused crates 2026-01-15 19:05:51 +08:00
houseme
e3ff5c073c upgrade s3s from 0.13.0-alpha.1 to 0.13.0-alpha.2 (#1518) 2026-01-15 18:23:36 +08:00
GatewayJ
bc0f5292f3 fix: standart policy format (#1508) 2026-01-15 18:23:36 +08:00
majinghe
5d617a0998 fix: change health check statement to fix unhealthy issue for docker … (#1515) 2026-01-15 18:23:36 +08:00
houseme
b8a578c6cf Merge branch 'main' of github.com:rustfs/rustfs into fix/axum-trusted-proxies
# Conflicts:
#	Cargo.lock
2026-01-15 12:07:55 +08:00
houseme
5c4ade2a20 fix 2026-01-15 09:08:06 +08:00
houseme
27293622eb Merge branch 'main' of github.com:rustfs/rustfs into fix/axum-trusted-proxies
# Conflicts:
#	Cargo.lock
2026-01-15 09:07:56 +08:00
houseme
6f83f00bed fix 2026-01-14 23:41:58 +08:00
houseme
f6ffde0ec0 Merge branch 'main' of github.com:rustfs/rustfs into fix/axum-trusted-proxies 2026-01-14 23:35:12 +08:00
houseme
b10cecb724 fmt 2026-01-14 23:34:52 +08:00
houseme
e6a91fab05 feat(trusted-proxies): optimize core architecture and localize documentation
- **Zero-Trust Security**: Implemented multi-mode proxy validation (Strict, Lenient, Hop-by-Hop) to ensure client IP integrity.
- **High Performance**: Integrated `moka` for asynchronous, thread-safe caching of IP validation results.
- **Cloud Native**: Enhanced automatic metadata discovery and IP range fetching for AWS, Azure, and GCP.
- **Observability**: Added Prometheus metrics and structured JSON logging for production-grade monitoring.
- **Refactoring**: Standardized environment variable loading using `rustfs_utils::envs`.
- **Localization**: Translated all source code comments and documentation from Chinese to English.
- **Test Suite**: Fixed test dependencies and updated integration tests for Axum/Tower compatibility.
- **Documentation**: Completed `README.md` with comprehensive configuration and usage guides.
2026-01-14 23:24:58 +08:00
houseme
60d3374804 Merge branch 'main' of github.com:rustfs/rustfs into fix/axum-trusted-proxies 2026-01-14 20:42:16 +08:00
houseme
8c00838398 Merge branch 'main' of github.com:rustfs/rustfs into fix/axum-trusted-proxies
# Conflicts:
#	crates/config/src/constants/mod.rs
#	crates/config/src/lib.rs
2026-01-14 18:10:35 +08:00
houseme
91c613f2d7 init 2026-01-12 01:23:12 +08:00
houseme
01af6f2837 Merge branch 'main' of github.com:rustfs/rustfs into fix/axum-trusted-proxies
# Conflicts:
#	Cargo.lock
2026-01-11 23:54:05 +08:00
houseme
66c6d4707e add copyright 2026-01-10 22:23:46 +08:00
houseme
7ac850d4ed Merge branch 'main' of github.com:rustfs/rustfs into fix/axum-trusted-proxies 2026-01-08 16:48:57 +08:00
houseme
9f060eae5e upgrade cargo.lock 2026-01-08 16:48:40 +08:00
houseme
6d80190a10 Merge branch 'main' of github.com:rustfs/rustfs into fix/axum-trusted-proxies 2026-01-07 15:46:11 +08:00
houseme
805a567dfc Merge branch 'main' of github.com:rustfs/rustfs into fix/axum-trusted-proxies 2026-01-05 20:34:48 +08:00
houseme
d72fef5ce6 init 2026-01-05 20:34:30 +08:00
houseme
b78f0d0bf1 Merge branch 'main' of github.com:rustfs/rustfs into fix/axum-trusted-proxies
# Conflicts:
#	Cargo.lock
#	Cargo.toml
2026-01-05 16:36:36 +08:00
houseme
720919843d fmt, fix and cargo shear --fix 2026-01-03 23:03:35 +08:00
houseme
bc676dd57c Merge branch 'main' of github.com:rustfs/rustfs into fix/axum-trusted-proxies
* 'main' of github.com:rustfs/rustfs:
  Add workflow to mark stale issues automatically
  fix: remove nginx-ingress default body size limit (#1335)
  feat:Permission verification for deleting versions (#1341)
  chore: upgrade GitHub Actions artifact actions (#1339)
  chore: replace native-tls with pure rustls for FTPS/SFTP e2e tests (#1334)
  chore: upgrade dependencies and migrate to aws-lc-rs (#1333)
  fix: s3 list object versions next marker (#1328)
  fix(tagging): fix e2e test_object_tagging failure (#1327)
  Feat/ftps&sftp (#1308)

# Conflicts:
#	Cargo.lock
#	Cargo.toml
#	crates/config/src/constants/mod.rs
#	crates/config/src/lib.rs
#	rustfs/Cargo.toml
2026-01-03 21:56:02 +08:00
houseme
36db23b620 add rustfs-trusted-proxies package 2026-01-03 21:49:59 +08:00
houseme
86f93abb13 improve code 2025-12-31 00:16:54 +08:00
59 changed files with 5665 additions and 40 deletions

View File

@@ -52,6 +52,10 @@ runs:
sudo apt-get install -y \
musl-tools \
build-essential \
cmake \
libclang-dev \
golang \
perl \
pkg-config \
libssl-dev

34
Cargo.lock generated
View File

@@ -3174,6 +3174,12 @@ version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "780955b8b195a21ab8e4ac6b60dd1dbdcec1dc6c51c0617964b08c81785e12c9"
[[package]]
name = "dotenvy"
version = "0.15.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b"
[[package]]
name = "doxygen-rs"
version = "0.4.2"
@@ -7640,6 +7646,7 @@ dependencies = [
"rustfs-s3select-api",
"rustfs-s3select-query",
"rustfs-targets",
"rustfs-trusted-proxies",
"rustfs-utils",
"rustfs-zip",
"rustls",
@@ -8217,6 +8224,33 @@ dependencies = [
"uuid",
]
[[package]]
name = "rustfs-trusted-proxies"
version = "0.0.5"
dependencies = [
"async-trait",
"axum",
"chrono",
"dotenvy",
"http 1.4.0",
"ipnetwork",
"lazy_static",
"metrics",
"moka",
"regex",
"reqwest",
"rustfs-utils",
"serde",
"serde_json",
"thiserror 2.0.17",
"tokio",
"tower",
"tower-http",
"tracing",
"tracing-subscriber",
"uuid",
]
[[package]]
name = "rustfs-utils"
version = "0.0.5"

View File

@@ -15,8 +15,10 @@
[workspace]
members = [
"rustfs", # Core file system implementation
"crates/ahm", # Asynchronous Hash Map for concurrent data structures
"crates/appauth", # Application authentication and authorization
"crates/audit", # Audit target management system with multi-target fan-out
"crates/checksums", # client checksums
"crates/common", # Shared utilities and data structures
"crates/config", # Configuration management
"crates/credentials", # Credential management system
@@ -25,8 +27,10 @@ members = [
"crates/e2e_test", # End-to-end test suite
"crates/filemeta", # File metadata management
"crates/iam", # Identity and Access Management
"crates/kms", # Key Management Service
"crates/lock", # Distributed locking implementation
"crates/madmin", # Management dashboard and admin API interface
"crates/mcp", # MCP server for S3 operations
"crates/notify", # Notification system for events
"crates/obs", # Observability utilities
"crates/policy", # Policy management
@@ -36,13 +40,10 @@ members = [
"crates/s3select-api", # S3 Select API interface
"crates/s3select-query", # S3 Select query engine
"crates/signer", # client signer
"crates/checksums", # client checksums
"crates/trusted-proxies", # Trusted proxies management
"crates/utils", # Utility functions and helpers
"crates/workers", # Worker thread pools and task scheduling
"crates/zip", # ZIP file handling and compression
"crates/ahm", # Asynchronous Hash Map for concurrent data structures
"crates/mcp", # MCP server for S3 operations
"crates/kms", # Key Management Service
]
resolver = "2"
@@ -89,6 +90,7 @@ rustfs-rio = { path = "crates/rio", version = "0.0.5" }
rustfs-s3select-api = { path = "crates/s3select-api", version = "0.0.5" }
rustfs-s3select-query = { path = "crates/s3select-query", version = "0.0.5" }
rustfs-signer = { path = "crates/signer", version = "0.0.5" }
rustfs-trusted-proxies = { path = "crates/trusted-proxies", version = "0.0.5" }
rustfs-targets = { path = "crates/targets", version = "0.0.5" }
rustfs-utils = { path = "crates/utils", version = "0.0.5" }
rustfs-workers = { path = "crates/workers", version = "0.0.5" }
@@ -107,10 +109,11 @@ futures-util = "0.3.31"
pollster = "0.4.0"
hyper = { version = "1.8.1", features = ["http2", "http1", "server"] }
hyper-rustls = { version = "0.27.7", default-features = false, features = ["native-tokio", "http1", "tls12", "logging", "http2", "aws-lc-rs", "webpki-roots"] }
hyper-util = { version = "0.1.19", features = ["tokio", "server-auto", "server-graceful"] }
hyper-util = { version = "0.1.19", features = ["tokio", "server-auto", "server-graceful", "tracing"] }
http = "1.4.0"
http-body = "1.0.1"
http-body-util = "0.1.3"
#reqwest = { version = "0.13.1", default-features = false, features = ["rustls", "charset", "http2", "system-proxy", "stream", "json", "blocking", "query", "form"] }
reqwest = { version = "0.12.28", default-features = false, features = ["rustls-tls-no-provider", "charset", "http2", "system-proxy", "stream", "json", "blocking"] }
socket2 = "0.6.1"
tokio = { version = "1.49.0", features = ["fs", "rt-multi-thread"] }

View File

@@ -35,6 +35,10 @@ RUN set -eux; \
ca-certificates \
curl \
git \
cmake \
libclang-dev \
golang \
perl \
pkg-config \
libssl-dev \
lld \

View File

@@ -175,7 +175,7 @@ make help-docker # 显示所有 Docker 相关命令
### 访问 RustFS
5. **访问控制台**: 打开浏览器并访问 `http://localhost:9000` 进入 RustFS 控制台。
* 默认账号/密码: `rustfsadmin` / `rustfsadmin`
* 默认账号/密码`rustfsadmin` / `rustfsadmin`
6. **创建存储桶**: 使用控制台为您​​的对象创建一个新的存储桶 (Bucket)。
7. **上传对象**: 您可以直接通过控制台上传文件,或使用 S3 兼容的 API/客户端与您的 RustFS 实例进行交互。

View File

@@ -21,6 +21,7 @@ pub(crate) mod heal;
pub(crate) mod object;
pub(crate) mod profiler;
pub(crate) mod protocols;
pub(crate) mod proxy;
pub(crate) mod quota;
pub(crate) mod runtime;
pub(crate) mod targets;

View File

@@ -0,0 +1,28 @@
// 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.
/// RUSTFS_HTTP_TRUSTED_PROXIES
/// Environment variable name for trusted proxies configuration
/// Example: RUSTFS_HTTP_TRUSTED_PROXIES="127.0.0.1,::1,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,fc00::/7"
/// If not set, defaults to local loopback and common private networks
/// Used in proxy configuration loading
/// Refer to `TrustedProxiesConfig` for details
pub const ENV_TRUSTED_PROXIES: &str = "RUSTFS_HTTP_TRUSTED_PROXIES";
/// Default trusted proxies: Local loopback and common private networks
/// Used when the environment variable is not set
/// Format: Comma-separated list of IPs and CIDR blocks
/// Example: RUSTFS_HTTP_TRUSTED_PROXIES="127.0.0.1,::1,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,fc00::/7"
/// Refer to `TrustedProxiesConfig` for details
pub const DEFAULT_TRUSTED_PROXIES: &str = "127.0.0.1,::1,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,fc00::/7";

View File

@@ -33,6 +33,8 @@ pub use constants::profiler::*;
#[cfg(feature = "constants")]
pub use constants::protocols::*;
#[cfg(feature = "constants")]
pub use constants::proxy::*;
#[cfg(feature = "constants")]
pub use constants::quota::*;
#[cfg(feature = "constants")]
pub use constants::runtime::*;

View File

@@ -38,13 +38,28 @@ impl TryFrom<u8> for ID {
impl ID {
pub(crate) fn get_key(&self, password: &[u8], salt: &[u8]) -> Result<[u8; 32], crate::Error> {
// Validate inputs for security
// if password.is_empty() {
// return Err(crate::Error::ErrInvalidInput("Password cannot be empty".to_string()));
// }
// if salt.len() < 16 {
// return Err(crate::Error::ErrInvalidInput("Salt must be at least 16 bytes".to_string()));
// }
let mut key = [0u8; 32];
match self {
ID::Pbkdf2AESGCM => pbkdf2_hmac::<Sha256>(password, salt, 8192, &mut key),
_ => {
let params = Params::new(64 * 1024, 1, 4, Some(32))?;
let argon_2id = Argon2::new(Algorithm::Argon2id, Version::V0x13, params);
argon_2id.hash_password_into(password, salt, &mut key)?;
ID::Pbkdf2AESGCM => {
pbkdf2_hmac::<Sha256>(password, salt, 8192, &mut key);
}
ID::Argon2idAESGCM | ID::Argon2idChaCHa20Poly1305 => {
const ARGON2_MEMORY: u32 = 64 * 1024;
const ARGON2_ITERATIONS: u32 = 1;
const ARGON2_PARALLELISM: u32 = 4;
const ARGON2_OUTPUT_LEN: usize = 32;
let params = Params::new(ARGON2_MEMORY, ARGON2_ITERATIONS, ARGON2_PARALLELISM, Some(ARGON2_OUTPUT_LEN))?;
let argon2 = Argon2::new(Algorithm::Argon2id, Version::V0x13, params);
argon2.hash_password_into(password, salt, &mut key)?;
}
}

View File

@@ -106,7 +106,7 @@ fn test_encrypt_decrypt_binary_data() -> Result<(), crate::Error> {
#[test]
fn test_encrypt_decrypt_unicode_data() -> Result<(), crate::Error> {
let unicode_strings = [
"Hello, 世界! 🌍",
"Hello, 世界🌍",
"Тест на русском языке",
"العربية اختبار",
"🚀🔐💻🌟⭐",

View File

@@ -20,6 +20,12 @@ pub enum Error {
#[error("invalid encryption algorithm ID: {0}")]
ErrInvalidAlgID(u8),
#[error("invalid input: {0}")]
ErrInvalidInput(String),
#[error("invalid key length")]
ErrInvalidKeyLength,
#[cfg(any(test, feature = "crypto"))]
#[error("{0}")]
ErrInvalidLength(#[from] sha2::digest::InvalidLength),
@@ -38,4 +44,13 @@ pub enum Error {
#[error("jwt err: {0}")]
ErrJwt(#[from] jsonwebtoken::errors::Error),
#[error("io error: {0}")]
ErrIo(#[from] std::io::Error),
#[error("invalid signature")]
ErrInvalidSignature,
#[error("invalid token")]
ErrInvalidToken,
}

View File

@@ -0,0 +1,33 @@
# Server Configuration
SERVER_HOST=0.0.0.0
SERVER_PORT=3000
# Trusted Proxy Configuration
TRUSTED_PROXY_NETWORKS=127.0.0.1,::1,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,fd00::/8
TRUSTED_PROXY_EXTRA_NETWORKS=
TRUSTED_PROXY_VALIDATION_MODE=hop_by_hop
TRUSTED_PROXY_ENABLE_RFC7239=true
TRUSTED_PROXY_MAX_HOPS=10
TRUSTED_PROXY_CHAIN_CONTINUITY_CHECK=true
TRUSTED_PROXY_LOG_FAILED_VALIDATIONS=true
# Cache Configuration
TRUSTED_PROXY_CACHE_CAPACITY=10000
TRUSTED_PROXY_CACHE_TTL_SECONDS=300
TRUSTED_PROXY_CACHE_CLEANUP_INTERVAL=60
# Monitoring Configuration
TRUSTED_PROXY_METRICS_ENABLED=true
TRUSTED_PROXY_LOG_LEVEL=info
TRUSTED_PROXY_STRUCTURED_LOGGING=false
TRUSTED_PROXY_TRACING_ENABLED=true
# Cloud Integration
TRUSTED_PROXY_CLOUD_METADATA_ENABLED=false
TRUSTED_PROXY_CLOUD_METADATA_TIMEOUT=5
TRUSTED_PROXY_CLOUDFLARE_IPS_ENABLED=false
TRUSTED_PROXY_CLOUD_PROVIDER_FORCE=
# Application
RUST_LOG=info
RUST_BACKTRACE=1

View File

@@ -0,0 +1,63 @@
# 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.
[package]
name = "rustfs-trusted-proxies"
edition.workspace = true
license.workspace = true
repository.workspace = true
rust-version.workspace = true
version.workspace = true
homepage.workspace = true
description = " RustFS Trusted Proxies module provides secure and efficient management of trusted proxy servers within the RustFS ecosystem, enhancing network security and performance."
keywords = ["trusted-proxies", "network-security", "rustfs", "proxy-management"]
categories = ["network-programming", "security", "web-programming"]
[dependencies]
async-trait = { workspace = true }
axum = { workspace = true }
chrono = { workspace = true }
http = { workspace = true }
tower-http = { workspace = true }
ipnetwork = { workspace = true }
metrics = { workspace = true }
moka = { workspace = true, features = ["future"] }
reqwest = { workspace = true }
rustfs-utils = { workspace = true }
serde.workspace = true
serde_json.workspace = true
thiserror = { workspace = true }
tokio = { workspace = true, features = ["macros", "rt-multi-thread", "sync", "time", "test-util"] }
tower = { workspace = true }
tracing = { workspace = true }
tracing-subscriber = { workspace = true }
uuid = { workspace = true, features = ["v4"] }
regex = { workspace = true }
lazy_static = { workspace = true }
dotenvy = "0.15.7"
[dev-dependencies]
tokio = { workspace = true, features = ["full", "test-util"] }
tower = { workspace = true, features = ["util"] }
[lints]
workspace = true
[[test]]
name = "unit_tests"
path = "tests/unit/mod.rs"
[[test]]
name = "integration_tests"
path = "tests/integration/mod.rs"

View File

@@ -0,0 +1,71 @@
# RustFS Trusted Proxies
The `rustfs-trusted-proxies` module provides secure and efficient management of trusted proxy servers within the RustFS ecosystem. It is designed to handle multi-layer proxy architectures, ensuring accurate client IP identification while maintaining a zero-trust security model.
## Features
- **Multi-Layer Proxy Validation**: Supports `Strict`, `Lenient`, and `HopByHop` validation modes to accurately identify the real client IP address.
- **Zero-Trust Security**: Verifies every hop in the proxy chain against a configurable list of trusted networks.
- **Cloud Integration**: Automatic discovery of trusted IP ranges for major cloud providers including AWS, Azure, and GCP.
- **High Performance**: Utilizes the `moka` cache for fast lookup of validation results and `axum` for a high-performance web interface.
- **Observability**: Built-in support for Prometheus metrics and structured JSON logging via `tracing`.
- **RFC 7239 Support**: Full support for the modern `Forwarded` header alongside legacy `X-Forwarded-For` headers.
## Configuration
The module is configured primarily through environment variables:
| Variable | Default | Description |
|----------|---------|-------------|
| `TRUSTED_PROXY_VALIDATION_MODE` | `hop_by_hop` | Validation strategy (`strict`, `lenient`, `hop_by_hop`) |
| `TRUSTED_PROXY_NETWORKS` | `127.0.0.1,::1,...` | Comma-separated list of trusted CIDR ranges |
| `TRUSTED_PROXY_MAX_HOPS` | `10` | Maximum allowed proxy hops |
| `TRUSTED_PROXY_CACHE_CAPACITY` | `10000` | Max entries in the validation cache |
| `TRUSTED_PROXY_METRICS_ENABLED` | `true` | Enable Prometheus metrics collection |
| `TRUSTED_PROXY_CLOUD_METADATA_ENABLED` | `false` | Enable auto-discovery of cloud IP ranges |
## Usage
### As a Middleware
Integrate the trusted proxy validation into your Axum application:
```rust
use rustfs_trusted_proxies::{TrustedProxyLayer, TrustedProxyConfig};
let config = TrustedProxyConfig::default();
let layer = TrustedProxyLayer::enabled(config, None);
let app = Router::new()
.route("/", get(handler))
.layer(layer);
```
### Accessing Client Info
Retrieve the verified client information in your handlers:
```rust
use rustfs_trusted_proxies::ClientInfo;
async fn handler(Extension(client_info): Extension<ClientInfo>) -> impl IntoResponse {
println!("Real Client IP: {}", client_info.real_ip);
}
```
## Development
### Pre-Commit Checklist
Before committing, ensure all checks pass:
```bash
make pre-commit
```
### Testing
Run the test suite:
```bash
cargo test --workspace --exclude e2e_test
```
## License
Licensed under the Apache License, Version 2.0.

View File

@@ -0,0 +1,150 @@
// 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.
//! API request handlers for the trusted proxy service.
use crate::AppState;
use crate::error::AppError;
use crate::middleware::ClientInfo;
use axum::{
extract::{Request, State},
http::StatusCode,
response::{IntoResponse, Json},
};
use serde_json::{Value, json};
/// Health check endpoint to verify service availability.
#[allow(dead_code)]
pub async fn health_check() -> impl IntoResponse {
Json(json!({
"status": "healthy",
"timestamp": chrono::Utc::now().to_rfc3339(),
"service": "trusted-proxy",
"version": env!("CARGO_PKG_VERSION"),
}))
}
/// Returns the current application configuration.
#[allow(dead_code)]
pub async fn show_config(State(state): State<AppState>) -> Result<Json<Value>, AppError> {
let config = &state.config;
let response = json!({
"server": {
"addr": config.server_addr.to_string(),
},
"proxy": {
"trusted_networks_count": config.proxy.proxies.len(),
"validation_mode": config.proxy.validation_mode.as_str(),
"max_hops": config.proxy.max_hops,
"enable_rfc7239": config.proxy.enable_rfc7239,
},
"cache": {
"capacity": config.cache.capacity,
"ttl_seconds": config.cache.ttl_seconds,
},
"monitoring": {
"metrics_enabled": config.monitoring.metrics_enabled,
"log_level": config.monitoring.log_level,
},
"cloud": {
"metadata_enabled": config.cloud.metadata_enabled,
"cloudflare_enabled": config.cloud.cloudflare_ips_enabled,
},
});
Ok(Json(response))
}
/// Returns information about the client as identified by the trusted proxy middleware.
#[allow(dead_code)]
pub async fn client_info(State(_state): State<AppState>, req: Request) -> impl IntoResponse {
// Retrieve the verified client information from the request extensions.
let client_info = req.extensions().get::<ClientInfo>();
match client_info {
Some(info) => {
let response = json!({
"client": {
"real_ip": info.real_ip.to_string(),
"is_from_trusted_proxy": info.is_from_trusted_proxy,
"proxy_hops": info.proxy_hops,
"validation_mode": info.validation_mode.as_str(),
},
"headers": {
"forwarded_host": info.forwarded_host,
"forwarded_proto": info.forwarded_proto,
},
"warnings": info.warnings,
"timestamp": chrono::Utc::now().to_rfc3339(),
});
Json(response).into_response()
}
None => {
let response = json!({
"error": "Client information not available",
"message": "The trusted proxy middleware may not be enabled or configured correctly.",
});
(StatusCode::INTERNAL_SERVER_ERROR, Json(response)).into_response()
}
}
}
/// Debugging endpoint that returns all proxy-related headers received in the request.
#[allow(dead_code)]
pub async fn proxy_test(req: Request) -> Json<Value> {
// Collect all headers related to proxying.
let headers: Vec<(String, String)> = req
.headers()
.iter()
.filter(|(name, _)| {
let name_str = name.as_str().to_lowercase();
name_str.contains("forwarded") || name_str.contains("x-forwarded") || name_str.contains("x-real")
})
.map(|(name, value)| (name.to_string(), value.to_str().unwrap_or("[INVALID]").to_string()))
.collect();
// Get the direct peer address.
let peer_addr = req
.extensions()
.get::<std::net::SocketAddr>()
.map(|addr| addr.to_string())
.unwrap_or_else(|| "unknown".to_string());
Json(json!({
"peer_addr": peer_addr,
"method": req.method().to_string(),
"uri": req.uri().to_string(),
"proxy_headers": headers,
"timestamp": chrono::Utc::now().to_rfc3339(),
}))
}
/// Endpoint for retrieving Prometheus metrics.
#[allow(dead_code)]
pub async fn metrics(State(state): State<AppState>) -> impl IntoResponse {
if !state.config.monitoring.metrics_enabled {
return (StatusCode::NOT_FOUND, "Metrics are not enabled").into_response();
}
// In a production environment, this would return the actual Prometheus-formatted metrics.
let metrics_summary = json!({
"status": "metrics_enabled",
"note": "Prometheus metrics are being collected. Use a compatible exporter to view them.",
});
Json(metrics_summary).into_response()
}

View File

@@ -0,0 +1,15 @@
// 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.
mod handlers;

View File

@@ -0,0 +1,253 @@
// 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.
//! Cloud provider detection and metadata fetching.
use async_trait::async_trait;
use std::time::Duration;
use tracing::{debug, info, warn};
use crate::error::AppError;
/// Supported cloud providers.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum CloudProvider {
/// Amazon Web Services
Aws,
/// Microsoft Azure
Azure,
/// Google Cloud Platform
Gcp,
/// DigitalOcean
DigitalOcean,
/// Cloudflare
Cloudflare,
/// Unknown or custom provider.
Unknown(String),
}
impl CloudProvider {
/// Detects the cloud provider based on environment variables.
pub fn detect_from_env() -> Option<Self> {
// Check for AWS environment variables.
if std::env::var("AWS_EXECUTION_ENV").is_ok()
|| std::env::var("AWS_REGION").is_ok()
|| std::env::var("EC2_INSTANCE_ID").is_ok()
{
return Some(Self::Aws);
}
// Check for Azure environment variables.
if std::env::var("WEBSITE_SITE_NAME").is_ok()
|| std::env::var("WEBSITE_INSTANCE_ID").is_ok()
|| std::env::var("APPSETTING_WEBSITE_SITE_NAME").is_ok()
{
return Some(Self::Azure);
}
// Check for GCP environment variables.
if std::env::var("GCP_PROJECT").is_ok()
|| std::env::var("GOOGLE_CLOUD_PROJECT").is_ok()
|| std::env::var("GAE_INSTANCE").is_ok()
{
return Some(Self::Gcp);
}
// Check for DigitalOcean environment variables.
if std::env::var("DIGITALOCEAN_REGION").is_ok() {
return Some(Self::DigitalOcean);
}
// Check for Cloudflare environment variables.
if std::env::var("CF_PAGES").is_ok() || std::env::var("CF_WORKERS").is_ok() {
return Some(Self::Cloudflare);
}
None
}
/// Returns the canonical name of the cloud provider.
pub fn name(&self) -> &str {
match self {
Self::Aws => "aws",
Self::Azure => "azure",
Self::Gcp => "gcp",
Self::DigitalOcean => "digitalocean",
Self::Cloudflare => "cloudflare",
Self::Unknown(name) => name,
}
}
/// Parses a cloud provider from a string.
pub fn from_str(s: &str) -> Self {
match s.to_lowercase().as_str() {
"aws" | "amazon" => Self::Aws,
"azure" | "microsoft" => Self::Azure,
"gcp" | "google" => Self::Gcp,
"digitalocean" | "do" => Self::DigitalOcean,
"cloudflare" | "cf" => Self::Cloudflare,
_ => Self::Unknown(s.to_string()),
}
}
}
/// Trait for fetching metadata from a specific cloud provider.
#[async_trait]
pub trait CloudMetadataFetcher: Send + Sync {
/// Returns the name of the provider.
fn provider_name(&self) -> &str;
/// Fetches the network CIDR ranges for the current instance.
async fn fetch_network_cidrs(&self) -> Result<Vec<ipnetwork::IpNetwork>, AppError>;
/// Fetches the public IP ranges for the cloud provider.
async fn fetch_public_ip_ranges(&self) -> Result<Vec<ipnetwork::IpNetwork>, AppError>;
/// Fetches all IP ranges that should be considered trusted proxies.
async fn fetch_trusted_proxy_ranges(&self) -> Result<Vec<ipnetwork::IpNetwork>, AppError> {
let mut ranges = Vec::new();
match self.fetch_network_cidrs().await {
Ok(cidrs) => ranges.extend(cidrs),
Err(e) => warn!("Failed to fetch network CIDRs from {}: {}", self.provider_name(), e),
}
match self.fetch_public_ip_ranges().await {
Ok(public_ranges) => ranges.extend(public_ranges),
Err(e) => warn!("Failed to fetch public IP ranges from {}: {}", self.provider_name(), e),
}
Ok(ranges)
}
}
/// Detector for identifying the current cloud environment and fetching relevant metadata.
#[derive(Debug, Clone)]
pub struct CloudDetector {
/// Whether cloud detection is enabled.
enabled: bool,
/// Timeout for metadata requests.
timeout: Duration,
/// Optionally force a specific provider.
forced_provider: Option<CloudProvider>,
}
impl CloudDetector {
/// Creates a new `CloudDetector`.
pub fn new(enabled: bool, timeout: Duration, forced_provider: Option<String>) -> Self {
let forced_provider = forced_provider.map(|s| CloudProvider::from_str(&s));
Self {
enabled,
timeout,
forced_provider,
}
}
/// Identifies the current cloud provider.
pub fn detect_provider(&self) -> Option<CloudProvider> {
if !self.enabled {
return None;
}
if let Some(provider) = self.forced_provider.as_ref() {
return Some(provider.clone());
}
CloudProvider::detect_from_env()
}
/// Fetches trusted IP ranges for the detected cloud provider.
pub async fn fetch_trusted_ranges(&self) -> Result<Vec<ipnetwork::IpNetwork>, AppError> {
if !self.enabled {
debug!("Cloud metadata fetching is disabled");
return Ok(Vec::new());
}
let provider = self.detect_provider();
match provider {
Some(CloudProvider::Aws) => {
info!("Detected AWS environment, fetching metadata");
let fetcher = crate::cloud::metadata::AwsMetadataFetcher::new(self.timeout);
fetcher.fetch_trusted_proxy_ranges().await
}
Some(CloudProvider::Azure) => {
info!("Detected Azure environment, fetching metadata");
let fetcher = crate::cloud::metadata::AzureMetadataFetcher::new(self.timeout);
fetcher.fetch_trusted_proxy_ranges().await
}
Some(CloudProvider::Gcp) => {
info!("Detected GCP environment, fetching metadata");
let fetcher = crate::cloud::metadata::GcpMetadataFetcher::new(self.timeout);
fetcher.fetch_trusted_proxy_ranges().await
}
Some(CloudProvider::Cloudflare) => {
info!("Detected Cloudflare environment");
let ranges = crate::cloud::ranges::CloudflareIpRanges::fetch().await?;
Ok(ranges)
}
Some(CloudProvider::DigitalOcean) => {
info!("Detected DigitalOcean environment");
let ranges = crate::cloud::ranges::DigitalOceanIpRanges::fetch().await?;
Ok(ranges)
}
Some(CloudProvider::Unknown(name)) => {
warn!("Unknown cloud provider detected: {}", name);
Ok(Vec::new())
}
None => {
debug!("No cloud provider detected");
Ok(Vec::new())
}
}
}
/// Attempts to fetch metadata from all supported providers sequentially.
pub async fn try_all_providers(&self) -> Result<Vec<ipnetwork::IpNetwork>, AppError> {
if !self.enabled {
return Ok(Vec::new());
}
let providers: Vec<Box<dyn CloudMetadataFetcher>> = vec![
Box::new(crate::cloud::metadata::AwsMetadataFetcher::new(self.timeout)),
Box::new(crate::cloud::metadata::AzureMetadataFetcher::new(self.timeout)),
Box::new(crate::cloud::metadata::GcpMetadataFetcher::new(self.timeout)),
];
for provider in providers {
let provider_name = provider.provider_name();
debug!("Trying to fetch metadata from {}", provider_name);
match provider.fetch_trusted_proxy_ranges().await {
Ok(ranges) => {
if !ranges.is_empty() {
info!("Fetched {} IP ranges from {}", ranges.len(), provider_name);
return Ok(ranges);
}
}
Err(e) => {
debug!("Failed to fetch metadata from {}: {}", provider_name, e);
}
}
}
Ok(Vec::new())
}
}
/// Returns a default `CloudDetector` with detection disabled.
pub fn default_cloud_detector() -> CloudDetector {
CloudDetector::new(false, Duration::from_secs(5), None)
}

View File

@@ -0,0 +1,156 @@
// 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.
//! AWS metadata fetching implementation for identifying trusted proxy ranges.
use async_trait::async_trait;
use reqwest::Client;
use std::str::FromStr;
use std::time::Duration;
use tracing::{debug, info};
use crate::cloud::detector::CloudMetadataFetcher;
use crate::error::AppError;
/// Fetcher for AWS-specific metadata.
#[derive(Debug, Clone)]
pub struct AwsMetadataFetcher {
client: Client,
metadata_endpoint: String,
}
impl AwsMetadataFetcher {
/// Creates a new `AwsMetadataFetcher`.
///
/// # Arguments
///
/// * `timeout` - Duration to use for HTTP request timeouts.
///
/// Returns a new instance of `AwsMetadataFetcher`.
pub fn new(timeout: Duration) -> Self {
let client = Client::builder().timeout(timeout).build().unwrap_or_else(|_| Client::new());
Self {
client,
metadata_endpoint: "http://169.254.169.254".to_string(),
}
}
/// Retrieves an IMDSv2 token for secure metadata access.
#[allow(dead_code)]
async fn get_metadata_token(&self) -> Result<String, AppError> {
let url = format!("{}/latest/api/token", self.metadata_endpoint);
match self
.client
.put(&url)
.header("X-aws-ec2-metadata-token-ttl-seconds", "21600")
.send()
.await
{
Ok(response) => {
if response.status().is_success() {
let token = response
.text()
.await
.map_err(|e| AppError::cloud(format!("Failed to read IMDSv2 token: {}", e)))?;
Ok(token)
} else {
debug!("IMDSv2 token request failed with status: {}", response.status());
Err(AppError::cloud("Failed to obtain IMDSv2 token"))
}
}
Err(e) => {
debug!("IMDSv2 token request failed: {}", e);
Err(AppError::cloud(format!("IMDSv2 request failed: {}", e)))
}
}
}
}
#[async_trait]
impl CloudMetadataFetcher for AwsMetadataFetcher {
fn provider_name(&self) -> &str {
"aws"
}
async fn fetch_network_cidrs(&self) -> Result<Vec<ipnetwork::IpNetwork>, AppError> {
// Simplified implementation: returns standard AWS VPC private ranges.
let default_ranges = vec![
"10.0.0.0/8", // Large VPCs
"172.16.0.0/12", // Medium VPCs
"192.168.0.0/16", // Small VPCs
];
let networks: Result<Vec<_>, _> = default_ranges
.into_iter()
.map(|s| ipnetwork::IpNetwork::from_str(s))
.collect();
match networks {
Ok(networks) => {
debug!("Using default AWS VPC network ranges");
Ok(networks)
}
Err(e) => Err(AppError::cloud(format!("Failed to parse default AWS ranges: {}", e))),
}
}
async fn fetch_public_ip_ranges(&self) -> Result<Vec<ipnetwork::IpNetwork>, AppError> {
let url = "https://ip-ranges.amazonaws.com/ip-ranges.json";
#[derive(Debug, serde::Deserialize)]
struct AwsIpRanges {
prefixes: Vec<AwsPrefix>,
}
#[derive(Debug, serde::Deserialize)]
struct AwsPrefix {
ip_prefix: String,
service: String,
}
match self.client.get(url).timeout(Duration::from_secs(5)).send().await {
Ok(response) => {
if response.status().is_success() {
let ip_ranges: AwsIpRanges = response
.json()
.await
.map_err(|e| AppError::cloud(format!("Failed to parse AWS IP ranges JSON: {}", e)))?;
let mut networks = Vec::new();
for prefix in ip_ranges.prefixes {
// Include EC2 and CloudFront ranges as potential trusted proxies.
if prefix.service == "EC2" || prefix.service == "CLOUDFRONT" {
if let Ok(network) = ipnetwork::IpNetwork::from_str(&prefix.ip_prefix) {
networks.push(network);
}
}
}
info!("Successfully fetched {} AWS public IP ranges", networks.len());
Ok(networks)
} else {
debug!("Failed to fetch AWS IP ranges: HTTP {}", response.status());
Ok(Vec::new())
}
}
Err(e) => {
debug!("Failed to fetch AWS IP ranges: {}", e);
Ok(Vec::new())
}
}
}
}

View File

@@ -0,0 +1,307 @@
// 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.
//! Azure Cloud metadata fetching implementation for identifying trusted proxy ranges.
use async_trait::async_trait;
use reqwest::Client;
use serde::Deserialize;
use std::str::FromStr;
use std::time::Duration;
use tracing::{debug, info, warn};
use crate::cloud::detector::CloudMetadataFetcher;
use crate::error::AppError;
/// Fetcher for Azure-specific metadata.
#[derive(Debug, Clone)]
pub struct AzureMetadataFetcher {
client: Client,
metadata_endpoint: String,
}
impl AzureMetadataFetcher {
/// Creates a new `AzureMetadataFetcher`.
pub fn new(timeout: Duration) -> Self {
let client = Client::builder().timeout(timeout).build().unwrap_or_else(|_| Client::new());
Self {
client,
metadata_endpoint: "http://169.254.169.254".to_string(),
}
}
/// Retrieves metadata from the Azure Instance Metadata Service (IMDS).
async fn get_metadata(&self, path: &str) -> Result<String, AppError> {
let url = format!("{}/metadata/{}?api-version=2021-05-01", self.metadata_endpoint, path);
debug!("Fetching Azure metadata from: {}", url);
match self.client.get(&url).header("Metadata", "true").send().await {
Ok(response) => {
if response.status().is_success() {
let text = response
.text()
.await
.map_err(|e| AppError::cloud(format!("Failed to read Azure metadata response: {}", e)))?;
Ok(text)
} else {
debug!("Azure metadata request failed with status: {}", response.status());
Err(AppError::cloud(format!("Azure metadata API returned status: {}", response.status())))
}
}
Err(e) => {
debug!("Azure metadata request failed: {}", e);
Err(AppError::cloud(format!("Azure metadata request failed: {}", e)))
}
}
}
/// Fetches Azure public IP ranges from the official Microsoft download source.
async fn fetch_azure_ip_ranges(&self) -> Result<Vec<ipnetwork::IpNetwork>, AppError> {
// Official Azure IP ranges download URL (periodically updated).
let url =
"https://download.microsoft.com/download/7/1/D/71D86715-5596-4529-9B13-DA13A5DE5B63/ServiceTags_Public_20231211.json";
#[derive(Debug, Deserialize)]
struct AzureServiceTags {
values: Vec<AzureServiceTag>,
}
#[derive(Debug, Deserialize)]
struct AzureServiceTag {
name: String,
properties: AzureServiceTagProperties,
}
#[derive(Debug, Deserialize)]
struct AzureServiceTagProperties {
address_prefixes: Vec<String>,
}
debug!("Fetching Azure IP ranges from: {}", url);
match self.client.get(url).timeout(Duration::from_secs(10)).send().await {
Ok(response) => {
if response.status().is_success() {
let service_tags: AzureServiceTags = response
.json()
.await
.map_err(|e| AppError::cloud(format!("Failed to parse Azure IP ranges JSON: {}", e)))?;
let mut networks = Vec::new();
for tag in service_tags.values {
// Include general Azure datacenter ranges, excluding specific internal services.
if tag.name.contains("Azure") && !tag.name.contains("ActiveDirectory") {
for prefix in tag.properties.address_prefixes {
if let Ok(network) = ipnetwork::IpNetwork::from_str(&prefix) {
networks.push(network);
}
}
}
}
info!("Successfully fetched {} Azure public IP ranges", networks.len());
Ok(networks)
} else {
debug!("Failed to fetch Azure IP ranges: HTTP {}", response.status());
Ok(Vec::new())
}
}
Err(e) => {
debug!("Failed to fetch Azure IP ranges: {}", e);
// Fallback to hardcoded ranges if the download fails.
Self::default_azure_ranges()
}
}
}
/// Returns a set of default Azure IP ranges as a fallback.
fn default_azure_ranges() -> Result<Vec<ipnetwork::IpNetwork>, AppError> {
let ranges = vec![
"13.64.0.0/11",
"13.96.0.0/13",
"13.104.0.0/14",
"20.33.0.0/16",
"20.34.0.0/15",
"20.36.0.0/14",
"20.40.0.0/13",
"20.48.0.0/12",
"20.64.0.0/10",
"20.128.0.0/16",
"20.135.0.0/16",
"20.136.0.0/13",
"20.150.0.0/15",
"20.157.0.0/16",
"20.184.0.0/13",
"20.190.0.0/16",
"20.192.0.0/10",
"40.64.0.0/10",
"40.80.0.0/12",
"40.96.0.0/13",
"40.112.0.0/13",
"40.120.0.0/14",
"40.124.0.0/16",
"40.125.0.0/17",
"51.12.0.0/15",
"51.104.0.0/15",
"51.120.0.0/16",
"51.124.0.0/16",
"51.132.0.0/16",
"51.136.0.0/15",
"51.138.0.0/16",
"51.140.0.0/14",
"51.144.0.0/15",
"52.96.0.0/12",
"52.112.0.0/14",
"52.120.0.0/14",
"52.124.0.0/16",
"52.125.0.0/16",
"52.126.0.0/15",
"52.130.0.0/15",
"52.136.0.0/13",
"52.144.0.0/15",
"52.146.0.0/15",
"52.148.0.0/14",
"52.152.0.0/13",
"52.160.0.0/12",
"52.176.0.0/13",
"52.184.0.0/14",
"52.188.0.0/14",
"52.224.0.0/11",
"65.52.0.0/14",
"104.40.0.0/13",
"104.208.0.0/13",
"104.215.0.0/16",
"137.116.0.0/15",
"137.135.0.0/16",
"138.91.0.0/16",
"157.56.0.0/16",
"168.61.0.0/16",
"168.62.0.0/15",
"191.233.0.0/18",
"193.149.0.0/19",
"2603:1000::/40",
"2603:1010::/40",
"2603:1020::/40",
"2603:1030::/40",
"2603:1040::/40",
"2603:1050::/40",
"2603:1060::/40",
"2603:1070::/40",
"2603:1080::/40",
"2603:1090::/40",
"2603:10a0::/40",
"2603:10b0::/40",
"2603:10c0::/40",
"2603:10d0::/40",
"2603:10e0::/40",
"2603:10f0::/40",
"2603:1100::/40",
];
let networks: Result<Vec<_>, _> = ranges.into_iter().map(|s| ipnetwork::IpNetwork::from_str(s)).collect();
match networks {
Ok(networks) => {
debug!("Using default Azure public IP ranges");
Ok(networks)
}
Err(e) => Err(AppError::cloud(format!("Failed to parse default Azure ranges: {}", e))),
}
}
}
#[async_trait]
impl CloudMetadataFetcher for AzureMetadataFetcher {
fn provider_name(&self) -> &str {
"azure"
}
async fn fetch_network_cidrs(&self) -> Result<Vec<ipnetwork::IpNetwork>, AppError> {
// Attempt to fetch network interface information from Azure IMDS.
match self.get_metadata("instance/network/interface").await {
Ok(metadata) => {
#[derive(Debug, Deserialize)]
struct AzureNetworkInterface {
ipv4: AzureIpv4Info,
}
#[derive(Debug, Deserialize)]
struct AzureIpv4Info {
subnet: Vec<AzureSubnet>,
}
#[derive(Debug, Deserialize)]
struct AzureSubnet {
address: String,
prefix: String,
}
let interfaces: Vec<AzureNetworkInterface> = serde_json::from_str(&metadata)
.map_err(|e| AppError::cloud(format!("Failed to parse Azure network metadata JSON: {}", e)))?;
let mut cidrs = Vec::new();
for interface in interfaces {
for subnet in interface.ipv4.subnet {
let cidr = format!("{}/{}", subnet.address, subnet.prefix);
if let Ok(network) = ipnetwork::IpNetwork::from_str(&cidr) {
cidrs.push(network);
}
}
}
if !cidrs.is_empty() {
info!("Successfully fetched {} network CIDRs from Azure metadata", cidrs.len());
Ok(cidrs)
} else {
debug!("No network CIDRs found in Azure metadata, falling back to defaults");
Self::default_azure_network_ranges()
}
}
Err(e) => {
warn!("Failed to fetch Azure network metadata: {}", e);
Self::default_azure_network_ranges()
}
}
}
async fn fetch_public_ip_ranges(&self) -> Result<Vec<ipnetwork::IpNetwork>, AppError> {
self.fetch_azure_ip_ranges().await
}
}
impl AzureMetadataFetcher {
/// Returns a set of default Azure VNet ranges as a fallback.
fn default_azure_network_ranges() -> Result<Vec<ipnetwork::IpNetwork>, AppError> {
let ranges = vec![
"10.0.0.0/8", // Large VNets
"172.16.0.0/12", // Medium VNets
"192.168.0.0/16", // Small VNets
"100.64.0.0/10", // Azure reserved range
"192.0.0.0/24", // Azure reserved
];
let networks: Result<Vec<_>, _> = ranges.into_iter().map(|s| ipnetwork::IpNetwork::from_str(s)).collect();
match networks {
Ok(networks) => {
debug!("Using default Azure VNet network ranges");
Ok(networks)
}
Err(e) => Err(AppError::cloud(format!("Failed to parse default Azure network ranges: {}", e))),
}
}
}

View File

@@ -0,0 +1,309 @@
// 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.
//! Google Cloud Platform (GCP) metadata fetching implementation for identifying trusted proxy ranges.
use async_trait::async_trait;
use reqwest::Client;
use serde::Deserialize;
use std::str::FromStr;
use std::time::Duration;
use tracing::{debug, info, warn};
use crate::cloud::detector::CloudMetadataFetcher;
use crate::error::AppError;
/// Fetcher for GCP-specific metadata.
#[derive(Debug, Clone)]
pub struct GcpMetadataFetcher {
client: Client,
metadata_endpoint: String,
}
impl GcpMetadataFetcher {
/// Creates a new `GcpMetadataFetcher`.
pub fn new(timeout: Duration) -> Self {
let client = Client::builder().timeout(timeout).build().unwrap_or_else(|_| Client::new());
Self {
client,
metadata_endpoint: "http://metadata.google.internal".to_string(),
}
}
/// Retrieves metadata from the GCP Compute Engine metadata server.
async fn get_metadata(&self, path: &str) -> Result<String, AppError> {
let url = format!("{}/computeMetadata/v1/{}", self.metadata_endpoint, path);
debug!("Fetching GCP metadata from: {}", url);
match self.client.get(&url).header("Metadata-Flavor", "Google").send().await {
Ok(response) => {
if response.status().is_success() {
let text = response
.text()
.await
.map_err(|e| AppError::cloud(format!("Failed to read GCP metadata response: {}", e)))?;
Ok(text)
} else {
debug!("GCP metadata request failed with status: {}", response.status());
Err(AppError::cloud(format!("GCP metadata API returned status: {}", response.status())))
}
}
Err(e) => {
debug!("GCP metadata request failed: {}", e);
Err(AppError::cloud(format!("GCP metadata request failed: {}", e)))
}
}
}
/// Converts a dotted-decimal subnet mask to a CIDR prefix length.
fn subnet_mask_to_prefix_length(mask: &str) -> Result<u8, AppError> {
let parts: Vec<&str> = mask.split('.').collect();
if parts.len() != 4 {
return Err(AppError::cloud(format!("Invalid subnet mask format: {}", mask)));
}
let mut prefix_length = 0;
for part in parts {
let octet: u8 = part
.parse()
.map_err(|_| AppError::cloud(format!("Invalid octet in subnet mask: {}", part)))?;
let mut remaining = octet;
while remaining > 0 {
if remaining & 0x80 == 0x80 {
prefix_length += 1;
remaining <<= 1;
} else {
break;
}
}
if remaining != 0 {
return Err(AppError::cloud("Non-contiguous subnet mask detected"));
}
}
Ok(prefix_length)
}
}
#[async_trait]
impl CloudMetadataFetcher for GcpMetadataFetcher {
fn provider_name(&self) -> &str {
"gcp"
}
async fn fetch_network_cidrs(&self) -> Result<Vec<ipnetwork::IpNetwork>, AppError> {
// Attempt to list network interfaces from GCP metadata.
match self.get_metadata("instance/network-interfaces/").await {
Ok(interfaces_metadata) => {
let interface_indices: Vec<usize> = interfaces_metadata
.lines()
.filter_map(|line| {
let line = line.trim().trim_end_matches('/');
if line.chars().all(|c| c.is_ascii_digit()) {
line.parse().ok()
} else {
None
}
})
.collect();
if interface_indices.is_empty() {
warn!("No network interfaces found in GCP metadata");
return Self::default_gcp_network_ranges();
}
let mut cidrs = Vec::new();
for index in interface_indices {
// Try to get IP and subnet mask for each interface.
let ip_path = format!("instance/network-interfaces/{}/ip", index);
let mask_path = format!("instance/network-interfaces/{}/subnetmask", index);
match tokio::try_join!(self.get_metadata(&ip_path), self.get_metadata(&mask_path)) {
Ok((ip, mask)) => {
let ip = ip.trim();
let mask = mask.trim();
if let (Ok(ip_addr), Ok(prefix_len)) =
(std::net::Ipv4Addr::from_str(ip), Self::subnet_mask_to_prefix_length(mask))
{
let cidr_str = format!("{}/{}", ip_addr, prefix_len);
if let Ok(network) = ipnetwork::IpNetwork::from_str(&cidr_str) {
cidrs.push(network);
}
}
}
Err(e) => {
debug!("Failed to get IP/mask for GCP interface {}: {}", index, e);
}
}
}
if cidrs.is_empty() {
warn!("Could not determine network CIDRs from GCP metadata, falling back to defaults");
Self::default_gcp_network_ranges()
} else {
info!("Successfully fetched {} network CIDRs from GCP metadata", cidrs.len());
Ok(cidrs)
}
}
Err(e) => {
warn!("Failed to fetch GCP network metadata: {}", e);
Self::default_gcp_network_ranges()
}
}
}
async fn fetch_public_ip_ranges(&self) -> Result<Vec<ipnetwork::IpNetwork>, AppError> {
self.fetch_gcp_ip_ranges().await
}
}
impl GcpMetadataFetcher {
/// Fetches GCP public IP ranges from the official Google source.
async fn fetch_gcp_ip_ranges(&self) -> Result<Vec<ipnetwork::IpNetwork>, AppError> {
let url = "https://www.gstatic.com/ipranges/cloud.json";
#[derive(Debug, Deserialize)]
struct GcpIpRanges {
prefixes: Vec<GcpPrefix>,
}
#[derive(Debug, Deserialize)]
struct GcpPrefix {
ipv4_prefix: Option<String>,
}
debug!("Fetching GCP IP ranges from: {}", url);
match self.client.get(url).timeout(Duration::from_secs(10)).send().await {
Ok(response) => {
if response.status().is_success() {
let ip_ranges: GcpIpRanges = response
.json()
.await
.map_err(|e| AppError::cloud(format!("Failed to parse GCP IP ranges JSON: {}", e)))?;
let mut networks = Vec::new();
for prefix in ip_ranges.prefixes {
if let Some(ipv4_prefix) = prefix.ipv4_prefix {
if let Ok(network) = ipnetwork::IpNetwork::from_str(&ipv4_prefix) {
networks.push(network);
}
}
}
info!("Successfully fetched {} GCP public IP ranges", networks.len());
Ok(networks)
} else {
debug!("Failed to fetch GCP IP ranges: HTTP {}", response.status());
Self::default_gcp_ip_ranges()
}
}
Err(e) => {
debug!("Failed to fetch GCP IP ranges: {}", e);
Self::default_gcp_ip_ranges()
}
}
}
/// Returns a set of default GCP public IP ranges as a fallback.
fn default_gcp_ip_ranges() -> Result<Vec<ipnetwork::IpNetwork>, AppError> {
let ranges = vec![
"8.34.208.0/20",
"8.35.192.0/20",
"8.35.208.0/20",
"23.236.48.0/20",
"23.251.128.0/19",
"34.0.0.0/15",
"34.2.0.0/16",
"34.3.0.0/23",
"34.4.0.0/14",
"34.8.0.0/13",
"34.16.0.0/12",
"34.32.0.0/11",
"34.64.0.0/10",
"34.128.0.0/10",
"35.184.0.0/13",
"35.192.0.0/14",
"35.196.0.0/15",
"35.198.0.0/16",
"35.200.0.0/13",
"35.208.0.0/12",
"35.224.0.0/12",
"35.240.0.0/13",
"104.154.0.0/15",
"104.196.0.0/14",
"107.167.160.0/19",
"107.178.192.0/18",
"108.59.80.0/20",
"108.170.192.0/18",
"108.177.0.0/17",
"130.211.0.0/16",
"136.112.0.0/12",
"142.250.0.0/15",
"146.148.0.0/17",
"172.217.0.0/16",
"172.253.0.0/16",
"173.194.0.0/16",
"192.178.0.0/15",
"209.85.128.0/17",
"216.58.192.0/19",
"216.239.32.0/19",
"2001:4860::/32",
"2404:6800::/32",
"2600:1900::/28",
"2607:f8b0::/32",
"2620:15c::/36",
"2800:3f0::/32",
"2a00:1450::/32",
"2c0f:fb50::/32",
];
let networks: Result<Vec<_>, _> = ranges.into_iter().map(|s| ipnetwork::IpNetwork::from_str(s)).collect();
match networks {
Ok(networks) => {
debug!("Using default GCP public IP ranges");
Ok(networks)
}
Err(e) => Err(AppError::cloud(format!("Failed to parse default GCP ranges: {}", e))),
}
}
/// Returns a set of default GCP VPC ranges as a fallback.
fn default_gcp_network_ranges() -> Result<Vec<ipnetwork::IpNetwork>, AppError> {
let ranges = vec![
"10.0.0.0/8", // Large VPCs
"172.16.0.0/12", // Medium VPCs
"192.168.0.0/16", // Small VPCs
"100.64.0.0/10", // GCP reserved range
];
let networks: Result<Vec<_>, _> = ranges.into_iter().map(|s| ipnetwork::IpNetwork::from_str(s)).collect();
match networks {
Ok(networks) => {
debug!("Using default GCP VPC network ranges");
Ok(networks)
}
Err(e) => Err(AppError::cloud(format!("Failed to parse default GCP network ranges: {}", e))),
}
}
}

View File

@@ -0,0 +1,26 @@
// 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.
//! Cloud provider metadata fetching
//!
//! This module contains implementations for fetching metadata
//! from various cloud providers.
mod aws;
mod azure;
mod gcp;
pub use aws::*;
pub use azure::*;
pub use gcp::*;

View File

@@ -0,0 +1,28 @@
// 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.
//! Cloud service integration module
//!
//! This module provides integration with various cloud providers
//! for automatic IP range detection and metadata fetching.
mod detector;
pub mod metadata;
mod ranges;
pub use detector::*;
pub use ranges::*;
// Re-export metadata module types
pub use metadata::*;

View File

@@ -0,0 +1,216 @@
// 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.
//! Static and dynamic IP range definitions for various cloud providers.
use std::str::FromStr;
use std::time::Duration;
use ipnetwork::IpNetwork;
use reqwest::Client;
use tracing::{debug, info};
use crate::error::AppError;
/// Utility for fetching Cloudflare IP ranges.
pub struct CloudflareIpRanges;
impl CloudflareIpRanges {
/// Returns a static list of Cloudflare IP ranges.
pub async fn fetch() -> Result<Vec<IpNetwork>, AppError> {
let ranges = vec![
// IPv4 ranges
"103.21.244.0/22",
"103.22.200.0/22",
"103.31.4.0/22",
"104.16.0.0/13",
"104.24.0.0/14",
"108.162.192.0/18",
"131.0.72.0/22",
"141.101.64.0/18",
"162.158.0.0/15",
"172.64.0.0/13",
"173.245.48.0/20",
"188.114.96.0/20",
"190.93.240.0/20",
"197.234.240.0/22",
"198.41.128.0/17",
// IPv6 ranges
"2400:cb00::/32",
"2606:4700::/32",
"2803:f800::/32",
"2405:b500::/32",
"2405:8100::/32",
"2a06:98c0::/29",
"2c0f:f248::/32",
];
let networks: Result<Vec<_>, _> = ranges.into_iter().map(|s| IpNetwork::from_str(s)).collect();
match networks {
Ok(networks) => {
info!("Loaded {} static Cloudflare IP ranges", networks.len());
Ok(networks)
}
Err(e) => Err(AppError::cloud(format!("Failed to parse static Cloudflare IP ranges: {}", e))),
}
}
/// Fetches the latest Cloudflare IP ranges from their official API.
pub async fn fetch_from_api() -> Result<Vec<IpNetwork>, AppError> {
let client = Client::builder()
.timeout(Duration::from_secs(10))
.build()
.map_err(|e| AppError::cloud(format!("Failed to create HTTP client: {}", e)))?;
let urls = ["https://www.cloudflare.com/ips-v4", "https://www.cloudflare.com/ips-v6"];
let mut all_ranges = Vec::new();
for url in urls {
match client.get(url).send().await {
Ok(response) => {
if response.status().is_success() {
let text = response
.text()
.await
.map_err(|e| AppError::cloud(format!("Failed to read response from {}: {}", url, e)))?;
let ranges: Result<Vec<_>, _> = text
.lines()
.map(|line| line.trim())
.filter(|line| !line.is_empty())
.map(IpNetwork::from_str)
.collect();
match ranges {
Ok(mut networks) => {
debug!("Fetched {} IP ranges from {}", networks.len(), url);
all_ranges.append(&mut networks);
}
Err(e) => {
debug!("Failed to parse IP ranges from {}: {}", url, e);
}
}
} else {
debug!("Failed to fetch IP ranges from {}: HTTP {}", url, response.status());
}
}
Err(e) => {
debug!("Failed to fetch from {}: {}", url, e);
}
}
}
if all_ranges.is_empty() {
// Fallback to static list if API requests fail.
Self::fetch().await
} else {
info!("Successfully fetched {} Cloudflare IP ranges from API", all_ranges.len());
Ok(all_ranges)
}
}
}
/// Utility for fetching DigitalOcean IP ranges.
pub struct DigitalOceanIpRanges;
impl DigitalOceanIpRanges {
/// Returns a static list of DigitalOcean IP ranges.
pub async fn fetch() -> Result<Vec<IpNetwork>, AppError> {
let ranges = vec![
// Datacenter IP ranges
"64.227.0.0/16",
"138.197.0.0/16",
"139.59.0.0/16",
"157.230.0.0/16",
"159.65.0.0/16",
"167.99.0.0/16",
"178.128.0.0/16",
"206.189.0.0/16",
"207.154.0.0/16",
"209.97.0.0/16",
// Load Balancer IP ranges
"144.126.0.0/16",
"143.198.0.0/16",
"161.35.0.0/16",
];
let networks: Result<Vec<_>, _> = ranges.into_iter().map(|s| IpNetwork::from_str(s)).collect();
match networks {
Ok(networks) => {
info!("Loaded {} static DigitalOcean IP ranges", networks.len());
Ok(networks)
}
Err(e) => Err(AppError::cloud(format!("Failed to parse static DigitalOcean IP ranges: {}", e))),
}
}
}
/// Utility for fetching Google Cloud IP ranges.
pub struct GoogleCloudIpRanges;
impl GoogleCloudIpRanges {
/// Fetches the latest Google Cloud IP ranges from their official source.
pub async fn fetch() -> Result<Vec<IpNetwork>, AppError> {
let client = Client::builder()
.timeout(Duration::from_secs(10))
.build()
.map_err(|e| AppError::cloud(format!("Failed to create HTTP client: {}", e)))?;
let url = "https://www.gstatic.com/ipranges/cloud.json";
#[derive(Debug, serde::Deserialize)]
struct GoogleIpRanges {
prefixes: Vec<GooglePrefix>,
}
#[derive(Debug, serde::Deserialize)]
struct GooglePrefix {
ipv4_prefix: Option<String>,
}
match client.get(url).send().await {
Ok(response) => {
if response.status().is_success() {
let ip_ranges: GoogleIpRanges = response
.json()
.await
.map_err(|e| AppError::cloud(format!("Failed to parse Google IP ranges JSON: {}", e)))?;
let mut networks = Vec::new();
for prefix in ip_ranges.prefixes {
if let Some(ipv4_prefix) = prefix.ipv4_prefix {
if let Ok(network) = IpNetwork::from_str(&ipv4_prefix) {
networks.push(network);
}
}
}
info!("Successfully fetched {} Google Cloud IP ranges from API", networks.len());
Ok(networks)
} else {
debug!("Failed to fetch Google IP ranges: HTTP {}", response.status());
Ok(Vec::new())
}
}
Err(e) => {
debug!("Failed to fetch Google IP ranges: {}", e);
Ok(Vec::new())
}
}
}
}

View File

@@ -0,0 +1,208 @@
// 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.
//! Environment variable configuration constants and helpers for the trusted proxy system.
use crate::error::ConfigError;
use ipnetwork::IpNetwork;
use std::str::FromStr;
// ==================== Base Proxy Configuration ====================
/// Environment variable for the proxy validation mode.
pub const ENV_PROXY_VALIDATION_MODE: &str = "TRUSTED_PROXY_VALIDATION_MODE";
/// Default validation mode is "hop_by_hop".
pub const DEFAULT_PROXY_VALIDATION_MODE: &str = "hop_by_hop";
/// Environment variable to enable RFC 7239 "Forwarded" header support.
pub const ENV_PROXY_ENABLE_RFC7239: &str = "TRUSTED_PROXY_ENABLE_RFC7239";
/// RFC 7239 support is enabled by default.
pub const DEFAULT_PROXY_ENABLE_RFC7239: bool = true;
/// Environment variable for the maximum allowed proxy hops.
pub const ENV_PROXY_MAX_HOPS: &str = "TRUSTED_PROXY_MAX_HOPS";
/// Default maximum hops is 10.
pub const DEFAULT_PROXY_MAX_HOPS: usize = 10;
/// Environment variable to enable proxy chain continuity checks.
pub const ENV_PROXY_CHAIN_CONTINUITY_CHECK: &str = "TRUSTED_PROXY_CHAIN_CONTINUITY_CHECK";
/// Continuity checks are enabled by default.
pub const DEFAULT_PROXY_CHAIN_CONTINUITY_CHECK: bool = true;
/// Environment variable to enable logging of failed proxy validations.
pub const ENV_PROXY_LOG_FAILED_VALIDATIONS: &str = "TRUSTED_PROXY_LOG_FAILED_VALIDATIONS";
/// Logging of failed validations is enabled by default.
pub const DEFAULT_PROXY_LOG_FAILED_VALIDATIONS: bool = true;
// ==================== Trusted Proxy Networks ====================
/// Environment variable for the list of trusted proxy networks (comma-separated IP/CIDR).
pub const ENV_TRUSTED_PROXIES: &str = "TRUSTED_PROXY_NETWORKS";
/// Default trusted networks include localhost and common private ranges.
pub const DEFAULT_TRUSTED_PROXIES: &str = "127.0.0.1,::1,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,fd00::/8";
/// Environment variable for additional trusted proxy networks (production specific).
pub const ENV_EXTRA_TRUSTED_PROXIES: &str = "TRUSTED_PROXY_EXTRA_NETWORKS";
/// No extra trusted networks by default.
pub const DEFAULT_EXTRA_TRUSTED_PROXIES: &str = "";
/// Environment variable for private network ranges used in internal validation.
pub const ENV_PRIVATE_NETWORKS: &str = "TRUSTED_PROXY_PRIVATE_NETWORKS";
/// Default private networks include common RFC 1918 and RFC 4193 ranges.
pub const DEFAULT_PRIVATE_NETWORKS: &str = "10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,fd00::/8";
// ==================== Cache Configuration ====================
/// Environment variable for the proxy validation cache capacity.
pub const ENV_CACHE_CAPACITY: &str = "TRUSTED_PROXY_CACHE_CAPACITY";
/// Default cache capacity is 10,000 entries.
pub const DEFAULT_CACHE_CAPACITY: usize = 10_000;
/// Environment variable for the cache entry time-to-live (TTL) in seconds.
pub const ENV_CACHE_TTL_SECONDS: &str = "TRUSTED_PROXY_CACHE_TTL_SECONDS";
/// Default cache TTL is 300 seconds (5 minutes).
pub const DEFAULT_CACHE_TTL_SECONDS: u64 = 300;
/// Environment variable for the cache cleanup interval in seconds.
pub const ENV_CACHE_CLEANUP_INTERVAL: &str = "TRUSTED_PROXY_CACHE_CLEANUP_INTERVAL";
/// Default cleanup interval is 60 seconds.
pub const DEFAULT_CACHE_CLEANUP_INTERVAL: u64 = 60;
// ==================== Monitoring Configuration ====================
/// Environment variable to enable Prometheus metrics.
pub const ENV_METRICS_ENABLED: &str = "TRUSTED_PROXY_METRICS_ENABLED";
/// Metrics are enabled by default.
pub const DEFAULT_METRICS_ENABLED: bool = true;
/// Environment variable for the application log level.
pub const ENV_LOG_LEVEL: &str = "TRUSTED_PROXY_LOG_LEVEL";
/// Default log level is "info".
pub const DEFAULT_LOG_LEVEL: &str = "info";
/// Environment variable to enable structured JSON logging.
pub const ENV_STRUCTURED_LOGGING: &str = "TRUSTED_PROXY_STRUCTURED_LOGGING";
/// Structured logging is disabled by default.
pub const DEFAULT_STRUCTURED_LOGGING: bool = false;
/// Environment variable to enable distributed tracing.
pub const ENV_TRACING_ENABLED: &str = "TRUSTED_PROXY_TRACING_ENABLED";
/// Tracing is enabled by default.
pub const DEFAULT_TRACING_ENABLED: bool = true;
// ==================== Cloud Integration ====================
/// Environment variable to enable automatic cloud metadata discovery.
pub const ENV_CLOUD_METADATA_ENABLED: &str = "TRUSTED_PROXY_CLOUD_METADATA_ENABLED";
/// Cloud metadata discovery is disabled by default.
pub const DEFAULT_CLOUD_METADATA_ENABLED: bool = false;
/// Environment variable for the cloud metadata request timeout in seconds.
pub const ENV_CLOUD_METADATA_TIMEOUT: &str = "TRUSTED_PROXY_CLOUD_METADATA_TIMEOUT";
/// Default cloud metadata timeout is 5 seconds.
pub const DEFAULT_CLOUD_METADATA_TIMEOUT: u64 = 5;
/// Environment variable to enable Cloudflare IP range integration.
pub const ENV_CLOUDFLARE_IPS_ENABLED: &str = "TRUSTED_PROXY_CLOUDFLARE_IPS_ENABLED";
/// Cloudflare integration is disabled by default.
pub const DEFAULT_CLOUDFLARE_IPS_ENABLED: bool = false;
/// Environment variable to force a specific cloud provider (overrides auto-detection).
pub const ENV_CLOUD_PROVIDER_FORCE: &str = "TRUSTED_PROXY_CLOUD_PROVIDER_FORCE";
/// No forced provider by default.
pub const DEFAULT_CLOUD_PROVIDER_FORCE: &str = "";
// ==================== Helper Functions ====================
/// Parses a comma-separated list of IP/CIDR strings from an environment variable.
pub fn parse_ip_list_from_env(key: &str, default: &str) -> Result<Vec<IpNetwork>, ConfigError> {
let value = std::env::var(key).unwrap_or_else(|_| default.to_string());
if value.trim().is_empty() {
return Ok(Vec::new());
}
let mut networks = Vec::new();
for item in value.split(',') {
let item = item.trim();
if item.is_empty() {
continue;
}
match IpNetwork::from_str(item) {
Ok(network) => networks.push(network),
Err(e) => {
tracing::warn!("Failed to parse network '{}' from environment variable {}: {}", item, key, e);
}
}
}
Ok(networks)
}
/// Parses a comma-separated list of strings from an environment variable.
pub fn parse_string_list_from_env(key: &str, default: &str) -> Vec<String> {
let value = std::env::var(key).unwrap_or_else(|_| default.to_string());
value
.split(',')
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.collect()
}
/// Retrieves a boolean value from an environment variable.
pub fn get_bool_from_env(key: &str, default: bool) -> bool {
std::env::var(key)
.map(|v| match v.to_lowercase().as_str() {
"true" | "1" | "yes" | "on" => true,
"false" | "0" | "no" | "off" => false,
_ => default,
})
.unwrap_or(default)
}
/// Retrieves a `usize` value from an environment variable.
pub fn get_usize_from_env(key: &str, default: usize) -> usize {
std::env::var(key).ok().and_then(|v| v.parse().ok()).unwrap_or(default)
}
/// Retrieves a `u64` value from an environment variable.
pub fn get_u64_from_env(key: &str, default: u64) -> u64 {
std::env::var(key).ok().and_then(|v| v.parse().ok()).unwrap_or(default)
}
/// Retrieves a string value from an environment variable.
pub fn get_string_from_env(key: &str, default: &str) -> String {
std::env::var(key).unwrap_or_else(|_| default.to_string())
}
/// Checks if an environment variable is set.
pub fn is_env_set(key: &str) -> bool {
std::env::var(key).is_ok()
}
/// Returns a list of all proxy-related environment variables and their current values.
pub fn get_all_proxy_env_vars() -> Vec<(String, String)> {
let vars = [
ENV_PROXY_VALIDATION_MODE,
ENV_PROXY_ENABLE_RFC7239,
ENV_PROXY_MAX_HOPS,
ENV_PROXY_CHAIN_CONTINUITY_CHECK,
ENV_TRUSTED_PROXIES,
ENV_EXTRA_TRUSTED_PROXIES,
ENV_CLOUD_METADATA_ENABLED,
ENV_CLOUD_METADATA_TIMEOUT,
ENV_CLOUDFLARE_IPS_ENABLED,
];
vars.iter()
.filter_map(|&key| std::env::var(key).ok().map(|value| (key.to_string(), value)))
.collect()
}

View File

@@ -0,0 +1,201 @@
// 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.
//! Configuration loader for environment variables and files.
use std::net::{IpAddr, SocketAddr};
use crate::config::env::*;
use crate::config::{AppConfig, CacheConfig, CloudConfig, MonitoringConfig, TrustedProxy, TrustedProxyConfig, ValidationMode};
use crate::error::ConfigError;
use rustfs_utils::*;
/// Loader for application configuration.
#[derive(Debug, Clone)]
pub struct ConfigLoader;
impl ConfigLoader {
/// Loads the complete application configuration from environment variables.
pub fn from_env() -> Result<AppConfig, ConfigError> {
// Load proxy-specific configuration.
let proxy_config = Self::load_proxy_config()?;
// Load cache configuration.
let cache_config = Self::load_cache_config();
// Load monitoring and observability configuration.
let monitoring_config = Self::load_monitoring_config();
// Load cloud provider integration configuration.
let cloud_config = Self::load_cloud_config();
// Load server binding address.
let server_addr = Self::load_server_addr();
Ok(AppConfig::new(proxy_config, cache_config, monitoring_config, cloud_config, server_addr))
}
/// Loads trusted proxy configuration from environment variables.
fn load_proxy_config() -> Result<TrustedProxyConfig, ConfigError> {
let mut proxies = Vec::new();
// Parse base trusted proxies from environment.
let base_networks = parse_ip_list_from_env(ENV_TRUSTED_PROXIES, DEFAULT_TRUSTED_PROXIES)?;
for network in base_networks {
proxies.push(TrustedProxy::Cidr(network));
}
// Parse extra trusted proxies from environment.
let extra_networks = parse_ip_list_from_env(ENV_EXTRA_TRUSTED_PROXIES, DEFAULT_EXTRA_TRUSTED_PROXIES)?;
for network in extra_networks {
proxies.push(TrustedProxy::Cidr(network));
}
// Parse individual trusted proxy IPs.
let ip_strings = parse_string_list_from_env("TRUSTED_PROXY_IPS", "");
for ip_str in ip_strings {
if let Ok(ip) = ip_str.parse::<IpAddr>() {
proxies.push(TrustedProxy::Single(ip));
}
}
// Determine validation mode.
let validation_mode_str = get_env_str(ENV_PROXY_VALIDATION_MODE, DEFAULT_PROXY_VALIDATION_MODE);
let validation_mode = ValidationMode::from_str(&validation_mode_str)?;
// Load other proxy settings.
let enable_rfc7239 = get_env_bool(ENV_PROXY_ENABLE_RFC7239, DEFAULT_PROXY_ENABLE_RFC7239);
let max_hops = get_env_usize(ENV_PROXY_MAX_HOPS, DEFAULT_PROXY_MAX_HOPS);
let enable_chain_check = get_env_bool(ENV_PROXY_CHAIN_CONTINUITY_CHECK, DEFAULT_PROXY_CHAIN_CONTINUITY_CHECK);
// Load private network ranges.
let private_networks = parse_ip_list_from_env(ENV_PRIVATE_NETWORKS, DEFAULT_PRIVATE_NETWORKS)?;
Ok(TrustedProxyConfig::new(
proxies,
validation_mode,
enable_rfc7239,
max_hops,
enable_chain_check,
private_networks,
))
}
/// Loads cache configuration from environment variables.
fn load_cache_config() -> CacheConfig {
CacheConfig {
capacity: get_env_usize(ENV_CACHE_CAPACITY, DEFAULT_CACHE_CAPACITY),
ttl_seconds: get_env_u64(ENV_CACHE_TTL_SECONDS, DEFAULT_CACHE_TTL_SECONDS),
cleanup_interval_seconds: get_env_u64(ENV_CACHE_CLEANUP_INTERVAL, DEFAULT_CACHE_CLEANUP_INTERVAL),
}
}
/// Loads monitoring configuration from environment variables.
fn load_monitoring_config() -> MonitoringConfig {
MonitoringConfig {
metrics_enabled: get_env_bool(ENV_METRICS_ENABLED, DEFAULT_METRICS_ENABLED),
log_level: get_env_str(ENV_LOG_LEVEL, DEFAULT_LOG_LEVEL),
structured_logging: get_env_bool(ENV_STRUCTURED_LOGGING, DEFAULT_STRUCTURED_LOGGING),
tracing_enabled: get_env_bool(ENV_TRACING_ENABLED, DEFAULT_TRACING_ENABLED),
log_failed_validations: get_env_bool(ENV_PROXY_LOG_FAILED_VALIDATIONS, DEFAULT_PROXY_LOG_FAILED_VALIDATIONS),
}
}
/// Loads cloud configuration from environment variables.
fn load_cloud_config() -> CloudConfig {
let forced_provider_str = get_env_str(ENV_CLOUD_PROVIDER_FORCE, DEFAULT_CLOUD_PROVIDER_FORCE);
let forced_provider = if forced_provider_str.is_empty() {
None
} else {
Some(forced_provider_str)
};
CloudConfig {
metadata_enabled: get_env_bool(ENV_CLOUD_METADATA_ENABLED, DEFAULT_CLOUD_METADATA_ENABLED),
metadata_timeout_seconds: get_env_u64(ENV_CLOUD_METADATA_TIMEOUT, DEFAULT_CLOUD_METADATA_TIMEOUT),
cloudflare_ips_enabled: get_env_bool(ENV_CLOUDFLARE_IPS_ENABLED, DEFAULT_CLOUDFLARE_IPS_ENABLED),
forced_provider,
}
}
/// Loads the server binding address from environment variables.
fn load_server_addr() -> SocketAddr {
let host = get_env_str("SERVER_HOST", "0.0.0.0");
let port = get_env_usize("SERVER_PORT", 3000) as u16;
format!("{}:{}", host, port)
.parse()
.unwrap_or_else(|_| SocketAddr::from(([0, 0, 0, 0], 3000)))
}
/// Loads configuration from environment, falling back to defaults on failure.
pub fn from_env_or_default() -> AppConfig {
match Self::from_env() {
Ok(config) => {
tracing::info!("Configuration loaded successfully from environment variables");
config
}
Err(e) => {
tracing::warn!("Failed to load configuration from environment: {}. Using defaults", e);
Self::default_config()
}
}
}
/// Returns a default configuration.
pub fn default_config() -> AppConfig {
let proxy_config = TrustedProxyConfig::new(
vec![
TrustedProxy::Single("127.0.0.1".parse().unwrap()),
TrustedProxy::Single("::1".parse().unwrap()),
],
ValidationMode::HopByHop,
true,
10,
true,
vec![
"10.0.0.0/8".parse().unwrap(),
"172.16.0.0/12".parse().unwrap(),
"192.168.0.0/16".parse().unwrap(),
],
);
AppConfig::new(
proxy_config,
CacheConfig::default(),
MonitoringConfig::default(),
CloudConfig::default(),
"0.0.0.0:3000".parse().unwrap(),
)
}
/// Prints a summary of the configuration to the log.
pub fn print_summary(config: &AppConfig) {
tracing::info!("=== Application Configuration ===");
tracing::info!("Server: {}", config.server_addr);
tracing::info!("Trusted Proxies: {}", config.proxy.proxies.len());
tracing::info!("Validation Mode: {:?}", config.proxy.validation_mode);
tracing::info!("Cache Capacity: {}", config.cache.capacity);
tracing::info!("Metrics Enabled: {}", config.monitoring.metrics_enabled);
tracing::info!("Cloud Metadata: {}", config.cloud.metadata_enabled);
if config.monitoring.log_failed_validations {
tracing::info!("Failed validations will be logged");
}
if !config.proxy.proxies.is_empty() {
tracing::debug!("Trusted networks: {:?}", config.proxy.get_network_strings());
}
}
}

View File

@@ -0,0 +1,24 @@
// 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.
mod env;
mod loader;
mod types;
pub use env::*;
// Re-export commonly used types
pub use ipnetwork::IpNetwork;
pub use loader::*;
pub use std::net::{IpAddr, SocketAddr};
pub use types::*;

View File

@@ -0,0 +1,297 @@
// 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.
//! Configuration type definitions for the trusted proxy system.
use ipnetwork::IpNetwork;
use serde::{Deserialize, Serialize};
use std::net::{IpAddr, SocketAddr};
use std::time::Duration;
use crate::error::ConfigError;
/// Proxy validation mode defining how the proxy chain is verified.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ValidationMode {
/// Lenient mode: Accepts the entire chain as long as the last proxy is trusted.
Lenient,
/// Strict mode: Requires all proxies in the chain to be trusted.
Strict,
/// Hop-by-hop mode: Finds the first untrusted proxy from right to left.
/// This is the recommended mode for most production environments.
HopByHop,
}
impl ValidationMode {
/// Parses the validation mode from a string.
pub fn from_str(s: &str) -> Result<Self, ConfigError> {
match s.to_lowercase().as_str() {
"lenient" => Ok(Self::Lenient),
"strict" => Ok(Self::Strict),
"hop_by_hop" | "hopbyhop" => Ok(Self::HopByHop),
_ => Err(ConfigError::InvalidConfig(format!(
"Invalid validation mode: '{}'. Must be one of: lenient, strict, hop_by_hop",
s
))),
}
}
/// Returns the string representation of the validation mode.
pub fn as_str(&self) -> &'static str {
match self {
Self::Lenient => "lenient",
Self::Strict => "strict",
Self::HopByHop => "hop_by_hop",
}
}
}
impl Default for ValidationMode {
fn default() -> Self {
Self::HopByHop
}
}
/// Represents a trusted proxy entry, which can be a single IP or a CIDR range.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TrustedProxy {
/// A single IP address.
Single(IpAddr),
/// An IP network range (CIDR notation).
Cidr(IpNetwork),
}
impl TrustedProxy {
/// Checks if the given IP address matches this proxy configuration.
pub fn contains(&self, ip: &IpAddr) -> bool {
match self {
Self::Single(proxy_ip) => ip == proxy_ip,
Self::Cidr(network) => network.contains(*ip),
}
}
/// Returns the string representation of the proxy entry.
pub fn to_string(&self) -> String {
match self {
Self::Single(ip) => ip.to_string(),
Self::Cidr(network) => network.to_string(),
}
}
}
/// Configuration for trusted proxies and validation logic.
#[derive(Debug, Clone)]
pub struct TrustedProxyConfig {
/// List of trusted proxy entries.
pub proxies: Vec<TrustedProxy>,
/// The validation mode to use for verifying proxy chains.
pub validation_mode: ValidationMode,
/// Whether to enable RFC 7239 "Forwarded" header support.
pub enable_rfc7239: bool,
/// Maximum allowed proxy hops in the chain.
pub max_hops: usize,
/// Whether to enable continuity checks for the proxy chain.
pub enable_chain_continuity_check: bool,
/// Private network ranges that should be treated with caution.
pub private_networks: Vec<IpNetwork>,
}
impl TrustedProxyConfig {
/// Creates a new trusted proxy configuration.
pub fn new(
proxies: Vec<TrustedProxy>,
validation_mode: ValidationMode,
enable_rfc7239: bool,
max_hops: usize,
enable_chain_continuity_check: bool,
private_networks: Vec<IpNetwork>,
) -> Self {
Self {
proxies,
validation_mode,
enable_rfc7239,
max_hops,
enable_chain_continuity_check,
private_networks,
}
}
/// Checks if a SocketAddr originates from a trusted proxy.
pub fn is_trusted(&self, addr: &SocketAddr) -> bool {
let ip = addr.ip();
self.proxies.iter().any(|proxy| proxy.contains(&ip))
}
/// Checks if an IP address belongs to a private network range.
pub fn is_private_network(&self, ip: &IpAddr) -> bool {
self.private_networks.iter().any(|network| network.contains(*ip))
}
/// Returns a list of all network strings for debugging purposes.
pub fn get_network_strings(&self) -> Vec<String> {
self.proxies.iter().map(|p| p.to_string()).collect()
}
/// Returns a summary of the configuration.
pub fn summary(&self) -> String {
format!(
"TrustedProxyConfig {{ proxies: {}, mode: {}, max_hops: {} }}",
self.proxies.len(),
self.validation_mode.as_str(),
self.max_hops
)
}
}
/// Configuration for the internal caching mechanism.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CacheConfig {
/// Maximum number of entries in the cache.
pub capacity: usize,
/// Time-to-live for cache entries in seconds.
pub ttl_seconds: u64,
/// Interval for cache cleanup in seconds.
pub cleanup_interval_seconds: u64,
}
impl Default for CacheConfig {
fn default() -> Self {
Self {
capacity: 10_000,
ttl_seconds: 300,
cleanup_interval_seconds: 60,
}
}
}
impl CacheConfig {
/// Returns the TTL as a Duration.
pub fn ttl_duration(&self) -> Duration {
Duration::from_secs(self.ttl_seconds)
}
/// Returns the cleanup interval as a Duration.
pub fn cleanup_interval(&self) -> Duration {
Duration::from_secs(self.cleanup_interval_seconds)
}
}
/// Configuration for monitoring and observability.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MonitoringConfig {
/// Whether to enable Prometheus metrics.
pub metrics_enabled: bool,
/// The logging level (e.g., "info", "debug").
pub log_level: String,
/// Whether to use structured JSON logging.
pub structured_logging: bool,
/// Whether to enable distributed tracing.
pub tracing_enabled: bool,
/// Whether to log detailed information about failed validations.
pub log_failed_validations: bool,
}
impl Default for MonitoringConfig {
fn default() -> Self {
Self {
metrics_enabled: true,
log_level: "info".to_string(),
structured_logging: false,
tracing_enabled: true,
log_failed_validations: true,
}
}
}
/// Configuration for cloud provider integration.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CloudConfig {
/// Whether to enable automatic cloud metadata discovery.
pub metadata_enabled: bool,
/// Timeout for cloud metadata requests in seconds.
pub metadata_timeout_seconds: u64,
/// Whether to automatically include Cloudflare IP ranges.
pub cloudflare_ips_enabled: bool,
/// Optionally force a specific cloud provider.
pub forced_provider: Option<String>,
}
impl Default for CloudConfig {
fn default() -> Self {
Self {
metadata_enabled: false,
metadata_timeout_seconds: 5,
cloudflare_ips_enabled: false,
forced_provider: None,
}
}
}
impl CloudConfig {
/// Returns the metadata timeout as a Duration.
pub fn metadata_timeout(&self) -> Duration {
Duration::from_secs(self.metadata_timeout_seconds)
}
}
/// Complete application configuration.
#[derive(Debug, Clone)]
pub struct AppConfig {
/// Trusted proxy settings.
pub proxy: TrustedProxyConfig,
/// Cache settings.
pub cache: CacheConfig,
/// Monitoring and observability settings.
pub monitoring: MonitoringConfig,
/// Cloud integration settings.
pub cloud: CloudConfig,
/// The address the server should bind to.
pub server_addr: SocketAddr,
}
impl AppConfig {
/// Creates a new application configuration.
pub fn new(
proxy: TrustedProxyConfig,
cache: CacheConfig,
monitoring: MonitoringConfig,
cloud: CloudConfig,
server_addr: SocketAddr,
) -> Self {
Self {
proxy,
cache,
monitoring,
cloud,
server_addr,
}
}
/// Returns a summary of the application configuration.
pub fn summary(&self) -> String {
format!(
"AppConfig {{\n\
\x20\x20proxy: {},\n\
\x20\x20cache_capacity: {},\n\
\x20\x20metrics: {},\n\
\x20\x20cloud_metadata: {}\n\
}}",
self.proxy.summary(),
self.cache.capacity,
self.monitoring.metrics_enabled,
self.cloud.metadata_enabled
)
}
}

View File

@@ -0,0 +1,82 @@
// 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.
//! Configuration error types for the trusted proxy system.
use std::net::AddrParseError;
/// Errors related to application configuration.
#[derive(Debug, thiserror::Error)]
pub enum ConfigError {
/// Required environment variable is missing.
#[error("Missing environment variable: {0}")]
MissingEnvVar(String),
/// Environment variable exists but could not be parsed.
#[error("Failed to parse environment variable {0}: {1}")]
EnvParseError(String, String),
/// A configuration value is logically invalid.
#[error("Invalid configuration value for {0}: {1}")]
InvalidValue(String, String),
/// An IP address or CIDR range is malformed.
#[error("Invalid IP address or network: {0}")]
InvalidIp(String),
/// Configuration failed overall validation.
#[error("Configuration validation failed: {0}")]
ValidationFailed(String),
/// Two or more configuration settings are in conflict.
#[error("Configuration conflict: {0}")]
Conflict(String),
/// Error reading or parsing a configuration file.
#[error("Config file error: {0}")]
FileError(String),
/// General invalid configuration error.
#[error("Invalid config: {0}")]
InvalidConfig(String),
}
impl From<AddrParseError> for ConfigError {
fn from(err: AddrParseError) -> Self {
Self::InvalidIp(err.to_string())
}
}
impl From<ipnetwork::IpNetworkError> for ConfigError {
fn from(err: ipnetwork::IpNetworkError) -> Self {
Self::InvalidIp(err.to_string())
}
}
impl ConfigError {
/// Creates a `MissingEnvVar` error.
pub fn missing_env_var(key: &str) -> Self {
Self::MissingEnvVar(key.to_string())
}
/// Creates an `EnvParseError`.
pub fn env_parse(key: &str, value: &str) -> Self {
Self::EnvParseError(key.to_string(), value.to_string())
}
/// Creates an `InvalidValue` error.
pub fn invalid_value(field: &str, value: &str) -> Self {
Self::InvalidValue(field.to_string(), value.to_string())
}
}

View File

@@ -0,0 +1,94 @@
// 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.
//! Error types for the trusted proxy system.
mod config;
mod proxy;
pub use config::*;
pub use proxy::*;
/// Unified error type for the application.
#[derive(Debug, thiserror::Error)]
pub enum AppError {
/// Errors related to configuration.
#[error("Configuration error: {0}")]
Config(#[from] ConfigError),
/// Errors related to proxy validation.
#[error("Proxy validation error: {0}")]
Proxy(#[from] ProxyError),
/// Errors related to cloud service integration.
#[error("Cloud service error: {0}")]
Cloud(String),
/// General internal errors.
#[error("Internal error: {0}")]
Internal(String),
/// Standard I/O errors.
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
/// Errors related to HTTP requests or responses.
#[error("HTTP error: {0}")]
Http(String),
}
impl AppError {
/// Creates a new `Cloud` error.
pub fn cloud(msg: impl Into<String>) -> Self {
Self::Cloud(msg.into())
}
/// Creates a new `Internal` error.
pub fn internal(msg: impl Into<String>) -> Self {
Self::Internal(msg.into())
}
/// Creates a new `Http` error.
pub fn http(msg: impl Into<String>) -> Self {
Self::Http(msg.into())
}
/// Returns true if the error is considered recoverable.
pub fn is_recoverable(&self) -> bool {
match self {
Self::Config(_) => true,
Self::Proxy(e) => e.is_recoverable(),
Self::Cloud(_) => true,
Self::Internal(_) => false,
Self::Io(_) => true,
Self::Http(_) => true,
}
}
}
/// Type alias for API error responses (Status Code, Error Message).
pub type ApiError = (axum::http::StatusCode, String);
impl From<AppError> for ApiError {
fn from(err: AppError) -> Self {
match err {
AppError::Config(_) => (axum::http::StatusCode::BAD_REQUEST, err.to_string()),
AppError::Proxy(_) => (axum::http::StatusCode::BAD_REQUEST, err.to_string()),
AppError::Cloud(_) => (axum::http::StatusCode::SERVICE_UNAVAILABLE, err.to_string()),
AppError::Internal(_) => (axum::http::StatusCode::INTERNAL_SERVER_ERROR, err.to_string()),
AppError::Io(_) => (axum::http::StatusCode::INTERNAL_SERVER_ERROR, err.to_string()),
AppError::Http(_) => (axum::http::StatusCode::BAD_GATEWAY, err.to_string()),
}
}
}

View File

@@ -0,0 +1,114 @@
// 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.
//! Proxy validation error types for the trusted proxy system.
use std::net::AddrParseError;
/// Errors that can occur during proxy chain validation.
#[derive(Debug, thiserror::Error)]
pub enum ProxyError {
/// The X-Forwarded-For header is malformed or contains invalid data.
#[error("Invalid X-Forwarded-For header: {0}")]
InvalidXForwardedFor(String),
/// The RFC 7239 Forwarded header is malformed.
#[error("Invalid Forwarded header (RFC 7239): {0}")]
InvalidForwardedHeader(String),
/// General failure during proxy chain validation.
#[error("Proxy chain validation failed: {0}")]
ChainValidationFailed(String),
/// The number of proxy hops exceeds the configured limit.
#[error("Proxy chain too long: {0} hops (max: {1})")]
ChainTooLong(usize, usize),
/// The request originated from a proxy that is not in the trusted list.
#[error("Request from untrusted proxy: {0}")]
UntrustedProxy(String),
/// The proxy chain is not continuous (e.g., an untrusted IP is between trusted ones).
#[error("Proxy chain is not continuous")]
ChainNotContinuous,
/// An IP address in the chain could not be parsed.
#[error("Failed to parse IP address: {0}")]
IpParseError(String),
/// A header value could not be parsed as a string.
#[error("Failed to parse header: {0}")]
HeaderParseError(String),
/// Validation took too long and timed out.
#[error("Validation timeout")]
Timeout,
/// An unexpected internal error occurred during validation.
#[error("Internal validation error: {0}")]
Internal(String),
}
impl From<AddrParseError> for ProxyError {
fn from(err: AddrParseError) -> Self {
Self::IpParseError(err.to_string())
}
}
impl ProxyError {
/// Creates an `InvalidXForwardedFor` error.
pub fn invalid_xff(msg: impl Into<String>) -> Self {
Self::InvalidXForwardedFor(msg.into())
}
/// Creates an `InvalidForwardedHeader` error.
pub fn invalid_forwarded(msg: impl Into<String>) -> Self {
Self::InvalidForwardedHeader(msg.into())
}
/// Creates a `ChainValidationFailed` error.
pub fn chain_failed(msg: impl Into<String>) -> Self {
Self::ChainValidationFailed(msg.into())
}
/// Creates an `UntrustedProxy` error.
pub fn untrusted(proxy: impl Into<String>) -> Self {
Self::UntrustedProxy(proxy.into())
}
/// Creates an `Internal` validation error.
pub fn internal(msg: impl Into<String>) -> Self {
Self::Internal(msg.into())
}
/// Determines if the error is recoverable, meaning the request can still be processed
/// (perhaps by falling back to the direct peer IP).
pub fn is_recoverable(&self) -> bool {
match self {
// These errors typically mean we should use the direct peer IP as a fallback.
Self::UntrustedProxy(_) => true,
Self::ChainTooLong(_, _) => true,
Self::ChainNotContinuous => true,
// These errors suggest malformed requests or severe configuration issues.
Self::InvalidXForwardedFor(_) => false,
Self::InvalidForwardedHeader(_) => false,
Self::ChainValidationFailed(_) => false,
Self::IpParseError(_) => false,
Self::HeaderParseError(_) => false,
Self::Timeout => true,
Self::Internal(_) => false,
}
}
}

View File

@@ -0,0 +1,30 @@
// 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.
pub mod api;
pub mod cloud;
pub mod config;
pub mod error;
pub mod logging;
pub mod middleware;
pub mod proxy;
pub mod state;
pub mod utils;
// Re-export core types for convenience
pub use cloud::*;
pub use config::*;
pub use middleware::{ClientInfo, TrustedProxyLayer, TrustedProxyMiddleware};
pub use proxy::*;
pub use state::AppState;

View File

@@ -0,0 +1,187 @@
// 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.
//! Logging middleware for the Axum web framework.
use crate::logging::Logger;
use std::task::{Context, Poll};
use std::time::Instant;
use tower::Service;
use uuid::Uuid;
/// Tower Layer for request logging middleware.
#[derive(Clone)]
pub struct RequestLoggingLayer {
logger: Logger,
}
impl RequestLoggingLayer {
/// Creates a new `RequestLoggingLayer`.
pub fn new(logger: Logger) -> Self {
Self { logger }
}
}
impl<S> tower::Layer<S> for RequestLoggingLayer {
type Service = RequestLoggingMiddleware<S>;
fn layer(&self, inner: S) -> Self::Service {
RequestLoggingMiddleware {
inner,
logger: self.logger.clone(),
}
}
}
/// Tower Service for request logging middleware.
#[derive(Clone)]
pub struct RequestLoggingMiddleware<S> {
inner: S,
logger: Logger,
}
impl<S> Service<axum::extract::Request> for RequestLoggingMiddleware<S>
where
S: Service<axum::extract::Request, Response = axum::response::Response> + Clone + Send + 'static,
S::Future: Send,
S::Error: std::error::Error + Send + Sync + 'static,
{
type Response = S::Response;
type Error = S::Error;
type Future = std::pin::Pin<Box<dyn std::future::Future<Output = Result<Self::Response, Self::Error>> + Send>>;
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
self.inner.poll_ready(cx)
}
fn call(&mut self, req: axum::extract::Request) -> Self::Future {
let logger = self.logger.clone();
let mut inner = self.inner.clone();
Box::pin(async move {
// Generate a unique request ID for correlation.
let request_id = Uuid::new_v4().to_string();
let start_time = Instant::now();
logger.log_request(&req, &request_id);
// Inject the request ID into the request extensions.
let mut req = req;
req.extensions_mut().insert(RequestId(request_id.clone()));
// Process the request.
let result = inner.call(req).await;
let duration = start_time.elapsed();
// Log the response or error.
match &result {
Ok(response) => {
logger.log_response(response, &request_id, duration);
}
Err(error) => {
logger.log_error(error, Some(&request_id));
}
}
result
})
}
}
/// Wrapper for a unique request ID.
#[derive(Debug, Clone)]
pub struct RequestId(String);
impl RequestId {
/// Returns the request ID as a string slice.
pub fn as_str(&self) -> &str {
&self.0
}
}
impl std::fmt::Display for RequestId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
/// Middleware specifically for logging proxy-related information.
#[derive(Clone)]
pub struct ProxyLoggingMiddleware<S> {
inner: S,
logger: Logger,
}
impl<S> ProxyLoggingMiddleware<S> {
/// Creates a new `ProxyLoggingMiddleware`.
pub fn new(inner: S, logger: Logger) -> Self {
Self { inner, logger }
}
}
impl<S> Service<axum::extract::Request> for ProxyLoggingMiddleware<S>
where
S: Service<axum::extract::Request, Response = axum::response::Response> + 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, req: axum::extract::Request) -> Self::Future {
// Log proxy-specific details if available.
let peer_addr = req.extensions().get::<std::net::SocketAddr>().copied();
let client_info = req.extensions().get::<crate::middleware::ClientInfo>();
if let (Some(addr), Some(info)) = (peer_addr, client_info) {
self.logger
.log_info(&format!("Proxy request from {}: {}", addr, info.to_log_string()), None);
// Log any warnings generated during proxy validation.
if !info.warnings.is_empty() {
for warning in &info.warnings {
self.logger.log_warning(warning, Some("proxy_validation"));
}
}
}
self.inner.call(req)
}
}
/// Tower Layer for proxy logging middleware.
#[derive(Clone)]
pub struct ProxyLoggingLayer {
logger: Logger,
}
impl ProxyLoggingLayer {
/// Creates a new `ProxyLoggingLayer`.
pub fn new(logger: Logger) -> Self {
Self { logger }
}
}
impl<S> tower::Layer<S> for ProxyLoggingLayer {
type Service = ProxyLoggingMiddleware<S>;
fn layer(&self, inner: S) -> Self::Service {
ProxyLoggingMiddleware::new(inner, self.logger.clone())
}
}

View File

@@ -0,0 +1,207 @@
// 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.
//! Logging module for structured logging and observability.
mod middleware;
pub use middleware::*;
/// Configuration for the logging system.
#[derive(Debug, Clone)]
pub struct LoggingConfig {
/// Whether to use structured JSON logging.
pub structured: bool,
/// The logging level (e.g., "info", "debug").
pub level: String,
/// Whether to include a unique request ID in logs.
pub enable_request_id: bool,
/// Whether to log the contents of request bodies.
pub log_request_body: bool,
/// Whether to log the contents of response bodies.
pub log_response_body: bool,
/// List of header names that should be redacted in logs.
pub sensitive_fields: Vec<String>,
}
impl Default for LoggingConfig {
fn default() -> Self {
Self {
structured: false,
level: "info".to_string(),
enable_request_id: true,
log_request_body: false,
log_response_body: false,
sensitive_fields: vec![
"password".to_string(),
"token".to_string(),
"secret".to_string(),
"authorization".to_string(),
"cookie".to_string(),
"set-cookie".to_string(),
],
}
}
}
/// Initializes the global tracing subscriber.
pub fn init_logging(config: &LoggingConfig) -> Result<(), Box<dyn std::error::Error>> {
let filter = tracing_subscriber::EnvFilter::builder()
.with_default_directive(config.level.parse().unwrap_or(tracing::Level::INFO.into()))
.from_env_lossy();
let subscriber = tracing_subscriber::fmt()
.with_env_filter(filter)
.with_target(true)
.with_thread_ids(true)
.with_thread_names(true)
.with_file(true)
.with_line_number(true);
if config.structured {
subscriber.json().init();
} else {
subscriber.init();
}
tracing::info!("Logging initialized with level: {}", config.level);
Ok(())
}
/// Helper for logging application events with consistent metadata.
#[derive(Debug, Clone)]
pub struct Logger {
config: LoggingConfig,
}
impl Logger {
/// Creates a new `Logger`.
pub fn new(config: LoggingConfig) -> Self {
Self { config }
}
/// Logs an incoming HTTP request.
pub fn log_request(&self, req: &http::Request<axum::body::Body>, request_id: &str) {
let method = req.method();
let uri = req.uri();
let version = req.version();
tracing::info!(
request.method = %method,
request.uri = %uri,
request.version = ?version,
request_id = %request_id,
"HTTP request received"
);
if self.config.log_request_body {
self.log_headers(req.headers(), "request");
}
}
/// Logs an outgoing HTTP response.
pub fn log_response(&self, res: &http::Response<axum::body::Body>, request_id: &str, duration: std::time::Duration) {
let status = res.status();
let version = res.version();
tracing::info!(
response.status = %status,
response.version = ?version,
request_id = %request_id,
duration_ms = duration.as_millis(),
"HTTP response sent"
);
if self.config.log_response_body {
self.log_headers(res.headers(), "response");
}
}
/// Logs HTTP headers, redacting sensitive information.
fn log_headers(&self, headers: &http::HeaderMap, header_type: &str) {
let mut header_fields = std::collections::HashMap::new();
for (name, value) in headers {
let name_str = name.to_string();
let value_str = match value.to_str() {
Ok(s) => s.to_string(),
Err(_) => "[BINARY]".to_string(),
};
let is_sensitive = self
.config
.sensitive_fields
.iter()
.any(|field| name_str.to_lowercase().contains(&field.to_lowercase()));
if is_sensitive {
header_fields.insert(name_str, "[REDACTED]".to_string());
} else {
header_fields.insert(name_str, value_str);
}
}
tracing::debug!(
headers = ?header_fields,
header_type = header_type,
"HTTP headers"
);
}
/// Logs an error with optional request context.
pub fn log_error(&self, error: &impl std::error::Error, request_id: Option<&str>) {
if let Some(id) = request_id {
tracing::error!(
error = %error,
error.type = std::any::type_name_of_val(error),
request_id = %id,
"Request error"
);
} else {
tracing::error!(
error = %error,
error.type = std::any::type_name_of_val(error),
"Application error"
);
}
}
/// Logs a warning message.
pub fn log_warning(&self, message: &str, context: Option<&str>) {
if let Some(ctx) = context {
tracing::warn!(message = %message, context = %ctx, "Warning");
} else {
tracing::warn!(message = %message, "Warning");
}
}
/// Logs an informational message.
pub fn log_info(&self, message: &str, context: Option<&str>) {
if let Some(ctx) = context {
tracing::info!(message = %message, context = %ctx, "Info");
} else {
tracing::info!(message = %message, "Info");
}
}
/// Logs a debug message.
pub fn log_debug(&self, message: &str, context: Option<&str>) {
if let Some(ctx) = context {
tracing::debug!(message = %message, context = %ctx, "Debug");
} else {
tracing::debug!(message = %message, "Debug");
}
}
}

View File

@@ -0,0 +1,119 @@
// 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.
//! Main application entry point for the RustFS Trusted Proxies service.
use std::sync::Arc;
use axum::{Router, routing::get};
use tokio::net::TcpListener;
use tracing::{Level, info};
use tracing_subscriber::EnvFilter;
mod api;
mod cloud;
mod config;
mod error;
mod middleware;
mod proxy;
mod state;
mod utils;
use api::handlers;
use config::{AppConfig, ConfigLoader, MonitoringConfig};
use error::AppError;
use middleware::TrustedProxyLayer;
use proxy::metrics::default_proxy_metrics;
use state::AppState;
#[tokio::main]
async fn main() -> Result<(), AppError> {
// Load environment variables from .env file if present.
dotenvy::dotenv().ok();
// Load application configuration from environment variables.
let config = ConfigLoader::from_env_or_default();
// Initialize the logging system.
init_logging(&config.monitoring)?;
// Print a summary of the loaded configuration.
ConfigLoader::print_summary(&config);
// Initialize metrics collector if enabled.
let metrics = if config.monitoring.metrics_enabled {
let m = default_proxy_metrics(true);
m.print_summary();
Some(m)
} else {
None
};
// Create shared application state.
let state = AppState {
config: Arc::new(config.clone()),
metrics: metrics.clone(),
};
// Initialize the trusted proxy middleware layer.
let proxy_layer = TrustedProxyLayer::enabled(config.proxy.clone(), metrics);
// Build the Axum application router.
let app = Router::new()
.route("/health", get(handlers::health_check))
.route("/config", get(handlers::show_config))
.route("/client-info", get(handlers::client_info))
.route("/proxy-test", get(handlers::proxy_test))
.route("/metrics", get(handlers::metrics))
.with_state(state)
.layer(proxy_layer)
.layer(tower_http::trace::TraceLayer::new_for_http())
.layer(tower_http::cors::CorsLayer::permissive())
.layer(tower_http::compression::CompressionLayer::new());
// Bind the TCP listener and start the server.
let addr = config.server_addr;
let listener = TcpListener::bind(addr).await?;
info!("Server listening on http://{}", addr);
info!("Available endpoints:");
info!(" GET /health - Service health check");
info!(" GET /config - Current configuration summary");
info!(" GET /client-info - Extracted client information");
info!(" GET /proxy-test - Debugging endpoint for proxy headers");
info!(" GET /metrics - Prometheus metrics (if enabled)");
axum::serve(listener, app).await?;
Ok(())
}
/// Initializes the tracing subscriber for logging.
fn init_logging(monitoring_config: &MonitoringConfig) -> Result<(), AppError> {
let filter = EnvFilter::builder()
.with_default_directive(monitoring_config.log_level.parse().unwrap_or(Level::INFO.into()))
.from_env_lossy();
let subscriber = tracing_subscriber::fmt().with_env_filter(filter);
if monitoring_config.structured_logging {
subscriber.json().init();
} else {
subscriber.init();
}
info!("Logging initialized with level: {}", monitoring_config.log_level);
Ok(())
}

View File

@@ -0,0 +1,75 @@
// 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.
//! Tower layer implementation for the trusted proxy middleware.
use std::sync::Arc;
use tower::Layer;
use crate::config::TrustedProxyConfig;
use crate::middleware::TrustedProxyMiddleware;
use crate::proxy::ProxyMetrics;
use crate::proxy::ProxyValidator;
/// Tower Layer for the trusted proxy middleware.
#[derive(Clone)]
pub struct TrustedProxyLayer {
/// The validator used to verify proxy chains.
pub(crate) validator: Arc<ProxyValidator>,
/// Whether the middleware is enabled.
pub(crate) enabled: bool,
}
impl TrustedProxyLayer {
/// Creates a new `TrustedProxyLayer`.
pub fn new(config: TrustedProxyConfig, metrics: Option<ProxyMetrics>, enabled: bool) -> Self {
let validator = ProxyValidator::new(config, metrics);
Self {
validator: Arc::new(validator),
enabled,
}
}
/// Creates a new `TrustedProxyLayer` that is enabled by default.
pub fn enabled(config: TrustedProxyConfig, metrics: Option<ProxyMetrics>) -> Self {
Self::new(config, metrics, true)
}
/// Creates a new `TrustedProxyLayer` that is disabled.
pub fn disabled() -> Self {
Self::new(
TrustedProxyConfig::new(Vec::new(), crate::config::ValidationMode::Lenient, true, 10, true, Vec::new()),
None,
false,
)
}
/// Returns true if the middleware is enabled.
pub fn is_enabled(&self) -> bool {
self.enabled
}
}
impl<S> Layer<S> for TrustedProxyLayer {
type Service = TrustedProxyMiddleware<S>;
fn layer(&self, inner: S) -> Self::Service {
TrustedProxyMiddleware {
inner,
validator: self.validator.clone(),
enabled: self.enabled,
}
}
}

View File

@@ -0,0 +1,24 @@
// 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.
//! Middleware module for Axum web framework
mod layer;
mod service;
pub use layer::*;
pub use service::*;
// Re-export commonly used types
pub use crate::proxy::ClientInfo;

View File

@@ -0,0 +1,128 @@
// 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.
//! Tower service implementation for the trusted proxy middleware.
use std::sync::Arc;
use std::task::{Context, Poll};
use axum::extract::Request;
use axum::response::Response;
use tower::Service;
use tracing::{Span, debug, instrument};
use crate::middleware::layer::TrustedProxyLayer;
use crate::proxy::{ClientInfo, ProxyValidator};
/// Tower Service for the trusted proxy middleware.
#[derive(Clone)]
pub struct TrustedProxyMiddleware<S> {
/// The inner service being wrapped.
pub(crate) inner: S,
/// The validator used to verify proxy chains.
pub(crate) validator: Arc<ProxyValidator>,
/// Whether the middleware is enabled.
pub(crate) enabled: bool,
}
impl<S> TrustedProxyMiddleware<S> {
/// Creates a new `TrustedProxyMiddleware`.
pub fn new(inner: S, validator: Arc<ProxyValidator>, enabled: bool) -> Self {
Self {
inner,
validator,
enabled,
}
}
/// Creates a new `TrustedProxyMiddleware` from a `TrustedProxyLayer`.
pub fn from_layer(inner: S, layer: &TrustedProxyLayer) -> Self {
Self::new(inner, layer.validator.clone(), layer.enabled)
}
}
impl<S> Service<Request> for TrustedProxyMiddleware<S>
where
S: Service<Request, Response = Response> + 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)
}
#[instrument(
name = "trusted_proxy_middleware",
skip_all,
fields(
http.method = %req.method(),
http.uri = %req.uri(),
http.version = ?req.version(),
enabled = self.enabled,
)
)]
fn call(&mut self, mut req: Request) -> Self::Future {
let span = Span::current();
// If the middleware is disabled, pass the request through immediately.
if !self.enabled {
debug!("Trusted proxy middleware is disabled");
return self.inner.call(req);
}
let start_time = std::time::Instant::now();
// Extract the direct peer address from the request extensions.
let peer_addr = req.extensions().get::<std::net::SocketAddr>().copied();
if let Some(addr) = peer_addr {
span.record("peer.addr", addr.to_string());
}
// Validate the request and extract client information.
match self.validator.validate_request(peer_addr, req.headers()) {
Ok(client_info) => {
span.record("client.ip", client_info.real_ip.to_string());
span.record("client.trusted", client_info.is_from_trusted_proxy);
span.record("client.hops", client_info.proxy_hops as i64);
// Insert the verified client info into the request extensions.
req.extensions_mut().insert(client_info);
let duration = start_time.elapsed();
debug!("Proxy validation successful in {:?}", duration);
}
Err(err) => {
span.record("error", true);
span.record("error.message", err.to_string());
// If the error is recoverable, fallback to a direct connection info.
if err.is_recoverable() {
let client_info = ClientInfo::direct(
peer_addr.unwrap_or_else(|| std::net::SocketAddr::new(std::net::IpAddr::from([0, 0, 0, 0]), 0)),
);
req.extensions_mut().insert(client_info);
} else {
debug!("Unrecoverable proxy validation error: {}", err);
}
}
}
// Call the inner service.
self.inner.call(req)
}
}

View File

@@ -0,0 +1,84 @@
// 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.
//! High-performance cache implementation for proxy validation results using Moka.
use moka::future::Cache;
use std::net::IpAddr;
use std::time::Duration;
/// Cache for storing IP validation results.
#[derive(Debug, Clone)]
pub struct IpValidationCache {
/// The underlying Moka cache.
cache: Cache<IpAddr, bool>,
/// Whether the cache is enabled.
enabled: bool,
}
impl IpValidationCache {
/// Creates a new `IpValidationCache` using Moka.
pub fn new(capacity: usize, ttl: Duration, enabled: bool) -> Self {
let cache = Cache::builder().max_capacity(capacity as u64).time_to_live(ttl).build();
Self { cache, enabled }
}
/// Checks if an IP is trusted, using the cache if available.
pub async fn is_trusted(&self, ip: &IpAddr, validator: impl FnOnce(&IpAddr) -> bool) -> bool {
if !self.enabled {
return validator(ip);
}
// Attempt to get the result from cache.
if let Some(is_trusted) = self.cache.get(ip).await {
metrics::counter!("proxy.cache.hits").increment(1);
return is_trusted;
}
// Cache miss: perform validation and update cache.
metrics::counter!("proxy.cache.misses").increment(1);
let is_trusted = validator(ip);
self.cache.insert(*ip, is_trusted).await;
is_trusted
}
/// Clears all entries from the cache.
pub async fn clear(&self) {
self.cache.invalidate_all();
metrics::gauge!("proxy.cache.size").set(0.0);
}
/// Returns statistics about the current state of the cache.
pub fn stats(&self) -> CacheStats {
let entry_count = self.cache.entry_count();
CacheStats {
size: entry_count as usize,
// Moka doesn't expose max_capacity directly in a simple way after build,
// but we can track it if needed.
capacity: 0,
}
}
}
/// Statistics about the IP validation cache.
#[derive(Debug, Clone)]
pub struct CacheStats {
/// Current number of entries in the cache.
pub size: usize,
/// Maximum capacity of the cache.
pub capacity: usize,
}

View File

@@ -0,0 +1,261 @@
// 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.
//! Proxy chain analysis and validation logic.
use crate::config::{TrustedProxyConfig, ValidationMode};
use crate::error::ProxyError;
use crate::utils::is_valid_ip_address;
use axum::http::HeaderMap;
use std::collections::HashSet;
use std::net::IpAddr;
use tracing::trace;
/// Result of analyzing a proxy chain.
#[derive(Debug, Clone)]
pub struct ChainAnalysis {
/// The identified real client IP address.
pub client_ip: IpAddr,
/// The number of validated proxy hops.
pub hops: usize,
/// Whether the proxy chain is continuous and trusted.
pub is_continuous: bool,
/// List of warnings generated during analysis.
pub warnings: Vec<String>,
/// The validation mode used for analysis.
pub validation_mode: ValidationMode,
/// The portion of the chain that consists of trusted proxies.
pub trusted_chain: Vec<IpAddr>,
}
/// Analyzer for verifying the integrity of proxy chains.
#[derive(Debug, Clone)]
pub struct ProxyChainAnalyzer {
/// Configuration for trusted proxies.
config: TrustedProxyConfig,
/// Cache of trusted IP addresses for fast lookup.
trusted_ip_cache: HashSet<IpAddr>,
}
impl ProxyChainAnalyzer {
/// Creates a new `ProxyChainAnalyzer`.
pub fn new(config: TrustedProxyConfig) -> Self {
let mut trusted_ip_cache = HashSet::new();
for proxy in &config.proxies {
match proxy {
crate::config::TrustedProxy::Single(ip) => {
trusted_ip_cache.insert(*ip);
}
crate::config::TrustedProxy::Cidr(network) => {
// For small networks, cache all IPs to speed up lookups.
if network.prefix() >= 24 {
for ip in network.iter() {
trusted_ip_cache.insert(ip);
}
}
}
}
}
Self {
config,
trusted_ip_cache,
}
}
/// Analyzes a proxy chain to identify the real client IP and verify trust.
pub fn analyze_chain(
&self,
proxy_chain: &[IpAddr],
current_proxy_ip: IpAddr,
headers: &HeaderMap,
) -> Result<ChainAnalysis, ProxyError> {
trace!("Analyzing proxy chain: {:?} with current proxy: {}", proxy_chain, current_proxy_ip);
// Validate all IP addresses in the chain.
self.validate_ip_addresses(proxy_chain)?;
// Construct the full chain including the direct peer.
let mut full_chain = proxy_chain.to_vec();
full_chain.push(current_proxy_ip);
// Analyze the chain based on the configured validation mode.
let (client_ip, trusted_chain, hops) = match self.config.validation_mode {
ValidationMode::Lenient => self.analyze_lenient(&full_chain),
ValidationMode::Strict => self.analyze_strict(&full_chain)?,
ValidationMode::HopByHop => self.analyze_hop_by_hop(&full_chain),
};
// Check for chain continuity if enabled.
let is_continuous = if self.config.enable_chain_continuity_check {
self.check_chain_continuity(&full_chain, &trusted_chain)
} else {
true
};
// Collect any warnings.
let warnings = self.collect_warnings(&full_chain, &trusted_chain, headers);
// Final validation of the identified client IP.
if !is_valid_ip_address(&client_ip) {
return Err(ProxyError::internal(format!("Invalid client IP identified: {}", client_ip)));
}
Ok(ChainAnalysis {
client_ip,
hops,
is_continuous,
warnings,
validation_mode: self.config.validation_mode,
trusted_chain,
})
}
/// Lenient mode: Accepts the entire chain if the last proxy is trusted.
fn analyze_lenient(&self, chain: &[IpAddr]) -> (IpAddr, Vec<IpAddr>, usize) {
if chain.is_empty() {
return (IpAddr::from([0, 0, 0, 0]), Vec::new(), 0);
}
if let Some(last_proxy) = chain.last() {
if self.is_ip_trusted(last_proxy) {
let client_ip = chain.first().copied().unwrap_or(*last_proxy);
return (client_ip, chain.to_vec(), chain.len());
}
}
let client_ip = chain.first().copied().unwrap_or(IpAddr::from([0, 0, 0, 0]));
(client_ip, Vec::new(), 0)
}
/// Strict mode: Requires every IP in the chain to be trusted.
fn analyze_strict(&self, chain: &[IpAddr]) -> Result<(IpAddr, Vec<IpAddr>, usize), ProxyError> {
if chain.is_empty() {
return Ok((IpAddr::from([0, 0, 0, 0]), Vec::new(), 0));
}
for (i, ip) in chain.iter().enumerate() {
if !self.is_ip_trusted(ip) {
return Err(ProxyError::chain_failed(format!("Proxy at position {} ({}) is not trusted", i, ip)));
}
}
let client_ip = chain.first().copied().unwrap_or(IpAddr::from([0, 0, 0, 0]));
Ok((client_ip, chain.to_vec(), chain.len()))
}
/// Hop-by-hop mode: Traverses the chain from right to left to find the first untrusted IP.
fn analyze_hop_by_hop(&self, chain: &[IpAddr]) -> (IpAddr, Vec<IpAddr>, usize) {
if chain.is_empty() {
return (IpAddr::from([0, 0, 0, 0]), Vec::new(), 0);
}
let mut trusted_chain = Vec::new();
let mut validated_hops = 0;
// Traverse from the most recent proxy back towards the client.
for ip in chain.iter().rev() {
if self.is_ip_trusted(ip) {
trusted_chain.insert(0, *ip);
validated_hops += 1;
} else {
break;
}
}
if trusted_chain.is_empty() {
let client_ip = *chain.last().unwrap();
(client_ip, vec![client_ip], 0)
} else {
let client_ip_index = chain.len().saturating_sub(trusted_chain.len());
let client_ip = if client_ip_index > 0 {
chain[client_ip_index - 1]
} else {
chain[0]
};
(client_ip, trusted_chain, validated_hops)
}
}
/// Verifies that the trusted portion of the chain is a continuous suffix of the full chain.
fn check_chain_continuity(&self, full_chain: &[IpAddr], trusted_chain: &[IpAddr]) -> bool {
if full_chain.len() <= 1 || trusted_chain.is_empty() {
return true;
}
if trusted_chain.len() > full_chain.len() {
return false;
}
let expected_tail = &full_chain[full_chain.len() - trusted_chain.len()..];
expected_tail == trusted_chain
}
/// Validates that IP addresses are not unspecified, multicast, or otherwise invalid.
fn validate_ip_addresses(&self, chain: &[IpAddr]) -> Result<(), ProxyError> {
for ip in chain {
if !is_valid_ip_address(ip) {
return Err(ProxyError::IpParseError(format!("Invalid IP address in chain: {}", ip)));
}
if ip.is_unspecified() {
return Err(ProxyError::invalid_xff("IP address cannot be unspecified (0.0.0.0 or ::)"));
}
if ip.is_multicast() {
return Err(ProxyError::invalid_xff("IP address cannot be multicast"));
}
}
Ok(())
}
/// Checks if an IP address is trusted based on the configuration.
fn is_ip_trusted(&self, ip: &IpAddr) -> bool {
if self.trusted_ip_cache.contains(ip) {
return true;
}
self.config.proxies.iter().any(|proxy| proxy.contains(ip))
}
/// Collects warnings about potential issues in the proxy chain.
fn collect_warnings(&self, full_chain: &[IpAddr], trusted_chain: &[IpAddr], headers: &HeaderMap) -> Vec<String> {
let mut warnings = Vec::new();
if full_chain.len() > self.config.max_hops {
warnings.push(format!(
"Proxy chain length ({}) exceeds configured maximum ({})",
full_chain.len(),
self.config.max_hops
));
}
if !trusted_chain.is_empty() && !headers.contains_key("x-forwarded-for") && !headers.contains_key("forwarded") {
warnings.push("No proxy headers found for request from trusted proxy".to_string());
}
let mut seen_ips = HashSet::new();
for ip in full_chain {
if !seen_ips.insert(ip) {
warnings.push(format!("Duplicate IP address detected in proxy chain: {}", ip));
break;
}
}
warnings
}
}

View File

@@ -0,0 +1,202 @@
// 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.
//! Metrics and monitoring for proxy validation performance and results.
use crate::config::ValidationMode;
use crate::error::ProxyError;
use metrics::{counter, describe_counter, describe_gauge, describe_histogram, gauge, histogram};
use std::time::Duration;
use tracing::info;
/// Collector for proxy validation metrics.
#[derive(Debug, Clone)]
pub struct ProxyMetrics {
/// Whether metrics collection is enabled.
enabled: bool,
/// Application name used as a label for metrics.
app_name: String,
}
impl ProxyMetrics {
/// Creates a new `ProxyMetrics` collector.
pub fn new(app_name: &str, enabled: bool) -> Self {
let metrics = Self {
enabled,
app_name: app_name.to_string(),
};
// Register metric descriptions for Prometheus.
metrics.register_descriptions();
metrics
}
/// Registers descriptions for all metrics.
fn register_descriptions(&self) {
if !self.enabled {
return;
}
describe_counter!("proxy_validation_attempts_total", "Total number of proxy validation attempts");
describe_counter!("proxy_validation_success_total", "Total number of successful proxy validations");
describe_counter!("proxy_validation_failure_total", "Total number of failed proxy validations");
describe_counter!(
"proxy_validation_failure_by_type_total",
"Total number of failed proxy validations categorized by error type"
);
describe_gauge!("proxy_chain_length", "Current length of proxy chains being validated");
describe_histogram!("proxy_validation_duration_seconds", "Time taken to validate a proxy chain in seconds");
describe_gauge!("proxy_cache_size", "Current number of entries in the proxy validation cache");
describe_counter!("proxy_cache_hits_total", "Total number of cache hits for proxy validation");
describe_counter!("proxy_cache_misses_total", "Total number of cache misses for proxy validation");
}
/// Increments the total number of validation attempts.
pub fn increment_validation_attempts(&self) {
if !self.enabled {
return;
}
counter!(
"proxy_validation_attempts_total",
"app" => self.app_name.clone()
)
.increment(1);
}
/// Records a successful validation.
pub fn record_validation_success(&self, from_trusted_proxy: bool, proxy_hops: usize, duration: Duration) {
if !self.enabled {
return;
}
counter!(
"proxy_validation_success_total",
"app" => self.app_name.clone(),
"trusted" => from_trusted_proxy.to_string()
)
.increment(1);
gauge!(
"proxy_chain_length",
"app" => self.app_name.clone()
)
.set(proxy_hops as f64);
histogram!(
"proxy_validation_duration_seconds",
"app" => self.app_name.clone()
)
.record(duration.as_secs_f64());
}
/// Records a failed validation with the specific error type.
pub fn record_validation_failure(&self, error: &ProxyError, duration: Duration) {
if !self.enabled {
return;
}
let error_type = match error {
ProxyError::InvalidXForwardedFor(_) => "invalid_x_forwarded_for",
ProxyError::InvalidForwardedHeader(_) => "invalid_forwarded_header",
ProxyError::ChainValidationFailed(_) => "chain_validation_failed",
ProxyError::ChainTooLong(_, _) => "chain_too_long",
ProxyError::UntrustedProxy(_) => "untrusted_proxy",
ProxyError::ChainNotContinuous => "chain_not_continuous",
ProxyError::IpParseError(_) => "ip_parse_error",
ProxyError::HeaderParseError(_) => "header_parse_error",
ProxyError::Timeout => "timeout",
ProxyError::Internal(_) => "internal",
};
counter!(
"proxy_validation_failure_total",
"app" => self.app_name.clone(),
"error_type" => error_type
)
.increment(1);
counter!(
"proxy_validation_failure_by_type_total",
"app" => self.app_name.clone(),
"error_type" => error_type
)
.increment(1);
histogram!(
"proxy_validation_duration_seconds",
"app" => self.app_name.clone(),
"error_type" => error_type
)
.record(duration.as_secs_f64());
}
/// Records the validation mode currently in use.
pub fn record_validation_mode(&self, mode: ValidationMode) {
if !self.enabled {
return;
}
gauge!(
"proxy_validation_mode",
"app" => self.app_name.clone(),
"mode" => mode.as_str()
)
.set(match mode {
ValidationMode::Lenient => 0.0,
ValidationMode::Strict => 1.0,
ValidationMode::HopByHop => 2.0,
});
}
/// Records cache performance metrics.
pub fn record_cache_metrics(&self, hits: u64, misses: u64, size: usize) {
if !self.enabled {
return;
}
counter!("proxy_cache_hits_total", "app" => self.app_name.clone()).increment(hits);
counter!("proxy_cache_misses_total", "app" => self.app_name.clone()).increment(misses);
gauge!("proxy_cache_size", "app" => self.app_name.clone()).set(size as f64);
}
/// Prints a summary of enabled metrics to the log.
pub fn print_summary(&self) {
if !self.enabled {
info!("Metrics collection is disabled");
return;
}
info!("Proxy metrics enabled for application: {}", self.app_name);
info!("Available metrics:");
info!(" - proxy_validation_attempts_total");
info!(" - proxy_validation_success_total");
info!(" - proxy_validation_failure_total");
info!(" - proxy_validation_failure_by_type_total");
info!(" - proxy_chain_length");
info!(" - proxy_validation_duration_seconds");
info!(" - proxy_cache_size");
info!(" - proxy_cache_hits_total");
info!(" - proxy_cache_misses_total");
}
}
/// Default application name for metrics.
const DEFAULT_APP_NAME: &str = "trusted-proxy";
/// Creates a default `ProxyMetrics` collector.
pub fn default_proxy_metrics(enabled: bool) -> ProxyMetrics {
ProxyMetrics::new(DEFAULT_APP_NAME, enabled)
}

View File

@@ -0,0 +1,32 @@
// 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.
//! Core proxy handling module
//!
//! This module contains the main logic for validating and processing
//! requests through trusted proxies.
mod cache;
mod chain;
mod metrics;
mod validator;
pub use cache::*;
pub use chain::*;
pub use metrics::*;
pub use validator::*;
// Re-export commonly used types
pub use crate::config::{TrustedProxyConfig, ValidationMode};
pub use crate::error::ProxyError;

View File

@@ -0,0 +1,322 @@
// 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.
//! Proxy validator for verifying proxy chains and extracting client information.
use axum::http::HeaderMap;
use std::net::{IpAddr, SocketAddr};
use std::time::Instant;
use tracing::{debug, warn};
use crate::config::{TrustedProxyConfig, ValidationMode};
use crate::error::ProxyError;
use crate::proxy::chain::ProxyChainAnalyzer;
use crate::proxy::metrics::ProxyMetrics;
/// Information about the client extracted from the request and proxy headers.
#[derive(Debug, Clone)]
pub struct ClientInfo {
/// The verified real IP address of the client.
pub real_ip: IpAddr,
/// The original host requested by the client (if provided by a trusted proxy).
pub forwarded_host: Option<String>,
/// The original protocol (http/https) used by the client (if provided by a trusted proxy).
pub forwarded_proto: Option<String>,
/// Whether the request was received from a trusted proxy.
pub is_from_trusted_proxy: bool,
/// The IP address of the proxy that directly connected to this server.
pub proxy_ip: Option<IpAddr>,
/// The number of proxy hops identified in the chain.
pub proxy_hops: usize,
/// The validation mode used for this request.
pub validation_mode: ValidationMode,
/// Any warnings generated during the validation process.
pub warnings: Vec<String>,
}
impl ClientInfo {
/// Creates a `ClientInfo` for a direct connection without any proxies.
pub fn direct(addr: SocketAddr) -> Self {
Self {
real_ip: addr.ip(),
forwarded_host: None,
forwarded_proto: None,
is_from_trusted_proxy: false,
proxy_ip: None,
proxy_hops: 0,
validation_mode: ValidationMode::Lenient,
warnings: Vec::new(),
}
}
/// Creates a `ClientInfo` for a request received through a trusted proxy.
pub fn from_trusted_proxy(
real_ip: IpAddr,
forwarded_host: Option<String>,
forwarded_proto: Option<String>,
proxy_ip: IpAddr,
proxy_hops: usize,
validation_mode: ValidationMode,
warnings: Vec<String>,
) -> Self {
Self {
real_ip,
forwarded_host,
forwarded_proto,
is_from_trusted_proxy: true,
proxy_ip: Some(proxy_ip),
proxy_hops,
validation_mode,
warnings,
}
}
/// Returns a string representation of the client info for logging.
pub fn to_log_string(&self) -> String {
format!(
"client_ip={}, proxy={:?}, hops={}, trusted={}, mode={:?}",
self.real_ip, self.proxy_ip, self.proxy_hops, self.is_from_trusted_proxy, self.validation_mode
)
}
}
/// Core validator that processes incoming requests to verify proxy chains.
#[derive(Debug, Clone)]
pub struct ProxyValidator {
/// Configuration for trusted proxies.
config: TrustedProxyConfig,
/// Analyzer for verifying the integrity of the proxy chain.
chain_analyzer: ProxyChainAnalyzer,
/// Metrics collector for observability.
metrics: Option<ProxyMetrics>,
}
impl ProxyValidator {
/// Creates a new `ProxyValidator` with the given configuration and metrics.
pub fn new(config: TrustedProxyConfig, metrics: Option<ProxyMetrics>) -> Self {
let chain_analyzer = ProxyChainAnalyzer::new(config.clone());
Self {
config,
chain_analyzer,
metrics,
}
}
/// Validates an incoming request and extracts client information.
pub fn validate_request(&self, peer_addr: Option<SocketAddr>, headers: &HeaderMap) -> Result<ClientInfo, ProxyError> {
let start_time = Instant::now();
// Record the start of the validation attempt.
self.record_metric_start();
// Perform the internal validation logic.
let result = self.validate_request_internal(peer_addr, headers);
// Record the result and duration.
let duration = start_time.elapsed();
self.record_metric_result(&result, duration);
result
}
/// Internal logic for request validation.
fn validate_request_internal(&self, peer_addr: Option<SocketAddr>, headers: &HeaderMap) -> Result<ClientInfo, ProxyError> {
// Fallback to unspecified address if peer address is missing.
let peer_addr = peer_addr.unwrap_or_else(|| SocketAddr::new(IpAddr::from([0, 0, 0, 0]), 0));
// Check if the direct peer is a trusted proxy.
if self.config.is_trusted(&peer_addr) {
debug!("Request received from trusted proxy: {}", peer_addr.ip());
// Parse and validate headers from the trusted proxy.
self.validate_trusted_proxy_request(&peer_addr, headers)
} else {
// Log a warning if the request is from a private network but not trusted.
if self.config.is_private_network(&peer_addr.ip()) {
warn!(
"Request from private network but not trusted: {}. This might indicate a configuration issue.",
peer_addr.ip()
);
}
// Treat as a direct connection if the peer is not trusted.
Ok(ClientInfo::direct(peer_addr))
}
}
/// Validates a request that originated from a trusted proxy.
fn validate_trusted_proxy_request(&self, proxy_addr: &SocketAddr, headers: &HeaderMap) -> Result<ClientInfo, ProxyError> {
let proxy_ip = proxy_addr.ip();
// Prefer RFC 7239 "Forwarded" header if enabled, otherwise fallback to legacy headers.
let client_info = if self.config.enable_rfc7239 {
self.try_parse_rfc7239_headers(headers, proxy_ip)
.unwrap_or_else(|| self.parse_legacy_headers(headers))
} else {
self.parse_legacy_headers(headers)
};
// Analyze the integrity and continuity of the proxy chain.
let chain_analysis = self
.chain_analyzer
.analyze_chain(&client_info.proxy_chain, proxy_ip, headers)?;
// Enforce maximum hop limit.
if chain_analysis.hops > self.config.max_hops {
return Err(ProxyError::ChainTooLong(chain_analysis.hops, self.config.max_hops));
}
// Enforce chain continuity if enabled.
if self.config.enable_chain_continuity_check && !chain_analysis.is_continuous {
return Err(ProxyError::ChainNotContinuous);
}
Ok(ClientInfo::from_trusted_proxy(
chain_analysis.client_ip,
client_info.forwarded_host,
client_info.forwarded_proto,
proxy_ip,
chain_analysis.hops,
self.config.validation_mode,
chain_analysis.warnings,
))
}
/// Attempts to parse the RFC 7239 "Forwarded" header.
fn try_parse_rfc7239_headers(&self, headers: &HeaderMap, proxy_ip: IpAddr) -> Option<ParsedHeaders> {
headers
.get("forwarded")
.and_then(|h| h.to_str().ok())
.and_then(|s| Self::parse_forwarded_header(s, proxy_ip))
}
/// Parses legacy proxy headers (X-Forwarded-For, X-Forwarded-Host, X-Forwarded-Proto).
fn parse_legacy_headers(&self, headers: &HeaderMap) -> ParsedHeaders {
let forwarded_host = headers
.get("x-forwarded-host")
.and_then(|h| h.to_str().ok())
.map(String::from);
let forwarded_proto = headers
.get("x-forwarded-proto")
.and_then(|h| h.to_str().ok())
.map(String::from);
let proxy_chain = headers
.get("x-forwarded-for")
.and_then(|h| h.to_str().ok())
.map(Self::parse_x_forwarded_for)
.unwrap_or_default();
ParsedHeaders {
proxy_chain,
forwarded_host,
forwarded_proto,
}
}
/// Parses the RFC 7239 "Forwarded" header value.
fn parse_forwarded_header(header_value: &str, proxy_ip: IpAddr) -> Option<ParsedHeaders> {
// Simplified implementation: processes only the first entry in the header.
let first_part = header_value.split(',').next()?.trim();
let mut proxy_chain = Vec::new();
let mut forwarded_host = None;
let mut forwarded_proto = None;
for part in first_part.split(';') {
let part = part.trim();
if let Some((key, value)) = part.split_once('=') {
let key = key.trim().to_lowercase();
let value = value.trim().trim_matches('"');
match key.as_str() {
"for" => {
// Extract IP address, ignoring port if present.
if let Some(ip_part) = value.split(':').next() {
if let Ok(ip) = ip_part.parse::<IpAddr>() {
proxy_chain.push(ip);
}
}
}
"host" => {
forwarded_host = Some(value.to_string());
}
"proto" => {
forwarded_proto = Some(value.to_string());
}
_ => {}
}
}
}
// Fallback to the proxy IP if no client IP was found in the header.
if proxy_chain.is_empty() {
proxy_chain.push(proxy_ip);
}
Some(ParsedHeaders {
proxy_chain,
forwarded_host,
forwarded_proto,
})
}
/// Parses the X-Forwarded-For header into a list of IP addresses.
fn parse_x_forwarded_for(header_value: &str) -> Vec<IpAddr> {
header_value
.split(',')
.map(|s| s.trim())
.filter(|s| !s.is_empty())
.filter_map(|s| {
// Strip port if present.
let ip_part = s.split(':').next().unwrap_or(s);
ip_part.parse::<IpAddr>().ok()
})
.collect()
}
/// Records the start of a validation attempt in metrics.
fn record_metric_start(&self) {
if let Some(metrics) = &self.metrics {
metrics.increment_validation_attempts();
}
}
/// Records the result of a validation attempt in metrics.
fn record_metric_result(&self, result: &Result<ClientInfo, ProxyError>, duration: std::time::Duration) {
if let Some(metrics) = &self.metrics {
match result {
Ok(client_info) => {
metrics.record_validation_success(client_info.is_from_trusted_proxy, client_info.proxy_hops, duration);
}
Err(err) => {
metrics.record_validation_failure(err, duration);
}
}
}
}
}
/// Internal structure for holding parsed header information.
#[derive(Debug, Clone)]
struct ParsedHeaders {
/// The chain of proxy IPs (client IP is typically the first).
proxy_chain: Vec<IpAddr>,
/// The original host requested.
forwarded_host: Option<String>,
/// The original protocol used.
forwarded_proto: Option<String>,
}

View File

@@ -0,0 +1,27 @@
// 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.
//! Shared application state.
use crate::{AppConfig, ProxyMetrics};
use std::sync::Arc;
/// Global application state shared across handlers and middleware.
#[derive(Clone)]
pub struct AppState {
/// Immutable application configuration.
pub config: Arc<AppConfig>,
/// Optional metrics collector for observability.
pub metrics: Option<ProxyMetrics>,
}

View File

@@ -0,0 +1,271 @@
// 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.
//! IP address utility functions for validation and classification.
use ipnetwork::IpNetwork;
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
use std::str::FromStr;
/// Collection of IP-related utility functions.
pub struct IpUtils;
impl IpUtils {
/// Checks if an IP address is valid for general use (not unspecified, multicast, or reserved).
pub fn is_valid_ip_address(ip: &IpAddr) -> bool {
!ip.is_unspecified() && !ip.is_multicast() && !Self::is_reserved_ip(ip)
}
/// Checks if an IP address belongs to a reserved range.
pub fn is_reserved_ip(ip: &IpAddr) -> bool {
match ip {
IpAddr::V4(ipv4) => Self::is_reserved_ipv4(ipv4),
IpAddr::V6(ipv6) => Self::is_reserved_ipv6(ipv6),
}
}
/// Checks if an IPv4 address belongs to a reserved range.
pub fn is_reserved_ipv4(ip: &Ipv4Addr) -> bool {
let octets = ip.octets();
// Check common reserved IPv4 ranges
matches!(
octets,
[0, _, _, _] | // 0.0.0.0/8
[10, _, _, _] | // 10.0.0.0/8
[100, 64, _, _] | // 100.64.0.0/10
[127, _, _, _] | // 127.0.0.0/8
[169, 254, _, _] | // 169.254.0.0/16
[172, 16..=31, _, _] | // 172.16.0.0/12
[192, 0, 0, _] | // 192.0.0.0/24
[192, 0, 2, _] | // 192.0.2.0/24
[192, 88, 99, _] | // 192.88.99.0/24
[192, 168, _, _] | // 192.168.0.0/16
[198, 18..=19, _, _] | // 198.18.0.0/15
[198, 51, 100, _] | // 198.51.100.0/24
[203, 0, 113, _] | // 203.0.113.0/24
[224..=239, _, _, _] | // 224.0.0.0/4
[240..=255, _, _, _] // 240.0.0.0/4
)
}
/// Checks if an IPv6 address belongs to a reserved range.
pub fn is_reserved_ipv6(ip: &Ipv6Addr) -> bool {
let segments = ip.segments();
// Check common reserved IPv6 ranges
matches!(
segments,
[0, 0, 0, 0, 0, 0, 0, 0] | // ::/128
[0, 0, 0, 0, 0, 0, 0, 1] | // ::1/128
[0x2001, 0xdb8, _, _, _, _, _, _] | // 2001:db8::/32
[0xfc00..=0xfdff, _, _, _, _, _, _, _] | // fc00::/7
[0xfe80..=0xfebf, _, _, _, _, _, _, _] | // fe80::/10
[0xff00..=0xffff, _, _, _, _, _, _, _] // ff00::/8
)
}
/// Checks if an IP address is a private address.
pub fn is_private_ip(ip: &IpAddr) -> bool {
match ip {
IpAddr::V4(ipv4) => Self::is_private_ipv4(ipv4),
IpAddr::V6(ipv6) => Self::is_private_ipv6(ipv6),
}
}
/// Checks if an IPv4 address is a private address.
pub fn is_private_ipv4(ip: &Ipv4Addr) -> bool {
let octets = ip.octets();
matches!(
octets,
[10, _, _, _] | // 10.0.0.0/8
[172, 16..=31, _, _] | // 172.16.0.0/12
[192, 168, _, _] // 192.168.0.0/16
)
}
/// Checks if an IPv6 address is a private address.
pub fn is_private_ipv6(ip: &Ipv6Addr) -> bool {
let segments = ip.segments();
matches!(
segments,
[0xfc00..=0xfdff, _, _, _, _, _, _, _] // fc00::/7
)
}
/// Checks if an IP address is a loopback address.
pub fn is_loopback_ip(ip: &IpAddr) -> bool {
match ip {
IpAddr::V4(ipv4) => ipv4.is_loopback(),
IpAddr::V6(ipv6) => ipv6.is_loopback(),
}
}
/// Checks if an IP address is a link-local address.
pub fn is_link_local_ip(ip: &IpAddr) -> bool {
match ip {
IpAddr::V4(ipv4) => ipv4.is_link_local(),
IpAddr::V6(ipv6) => ipv6.is_unicast_link_local(),
}
}
/// Checks if an IP address is a documentation address (TEST-NET).
pub fn is_documentation_ip(ip: &IpAddr) -> bool {
match ip {
IpAddr::V4(ipv4) => {
let octets = ipv4.octets();
matches!(
octets,
[192, 0, 2, _] | // 192.0.2.0/24
[198, 51, 100, _] | // 198.51.100.0/24
[203, 0, 113, _] // 203.0.113.0/24
)
}
IpAddr::V6(ipv6) => {
let segments = ipv6.segments();
matches!(segments, [0x2001, 0xdb8, _, _, _, _, _, _]) // 2001:db8::/32
}
}
}
/// Parses an IP address or CIDR range from a string.
pub fn parse_ip_or_cidr(s: &str) -> Result<IpNetwork, String> {
IpNetwork::from_str(s).map_err(|e| format!("Failed to parse IP/CIDR '{}': {}", s, e))
}
/// Parses a comma-separated list of IP addresses.
pub fn parse_ip_list(s: &str) -> Result<Vec<IpAddr>, String> {
let mut ips = Vec::new();
for part in s.split(',') {
let part = part.trim();
if part.is_empty() {
continue;
}
match IpAddr::from_str(part) {
Ok(ip) => ips.push(ip),
Err(e) => return Err(format!("Failed to parse IP '{}': {}", part, e)),
}
}
Ok(ips)
}
/// Parses a comma-separated list of IP networks (CIDR).
pub fn parse_network_list(s: &str) -> Result<Vec<IpNetwork>, String> {
let mut networks = Vec::new();
for part in s.split(',') {
let part = part.trim();
if part.is_empty() {
continue;
}
match Self::parse_ip_or_cidr(part) {
Ok(network) => networks.push(network),
Err(e) => return Err(e),
}
}
Ok(networks)
}
/// Checks if an IP address is contained within any of the given networks.
pub fn ip_in_networks(ip: &IpAddr, networks: &[IpNetwork]) -> bool {
networks.iter().any(|network| network.contains(*ip))
}
/// Returns a string description of the IP address type.
pub fn get_ip_type(ip: &IpAddr) -> &'static str {
if Self::is_private_ip(ip) {
"private"
} else if Self::is_loopback_ip(ip) {
"loopback"
} else if Self::is_link_local_ip(ip) {
"link_local"
} else if Self::is_documentation_ip(ip) {
"documentation"
} else if Self::is_reserved_ip(ip) {
"reserved"
} else {
"public"
}
}
/// Returns the canonical string representation of an IP address.
pub fn canonical_ip(ip: &IpAddr) -> String {
match ip {
IpAddr::V4(ipv4) => ipv4.to_string(),
IpAddr::V6(ipv6) => {
// Compress IPv6 address
let mut result = String::new();
let segments = ipv6.segments();
// Find the longest sequence of zero segments
let mut longest_start = 0;
let mut longest_len = 0;
let mut current_start = 0;
let mut current_len = 0;
for (i, &segment) in segments.iter().enumerate() {
if segment == 0 {
if current_len == 0 {
current_start = i;
}
current_len += 1;
} else {
if current_len > longest_len {
longest_start = current_start;
longest_len = current_len;
}
current_len = 0;
}
}
if current_len > longest_len {
longest_start = current_start;
longest_len = current_len;
}
// Format as string
let mut i = 0;
while i < 8 {
if i == longest_start && longest_len > 1 {
result.push_str("::");
i += longest_len;
if i == 8 {
break;
}
} else {
if i > 0 && (i != longest_start + longest_len || longest_len <= 1) {
result.push(':');
}
result.push_str(&format!("{:x}", segments[i]));
i += 1;
}
}
result
}
}
}
}
/// Helper function to check if an IP address is valid.
pub fn is_valid_ip_address(ip: &IpAddr) -> bool {
IpUtils::is_valid_ip_address(ip)
}

View File

@@ -0,0 +1,89 @@
// 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.
//! Utility functions and helpers for the trusted proxy system.
mod ip;
mod validation;
pub use ip::*;
pub use validation::*;
/// Collection of general utility functions.
#[derive(Debug, Clone)]
pub struct Utils;
impl Utils {
/// Generates a unique trace ID.
pub fn generate_trace_id() -> String {
format!("trace-{}", uuid::Uuid::new_v4())
}
/// Generates a unique span ID.
pub fn generate_span_id() -> String {
format!("span-{}", uuid::Uuid::new_v4())
}
/// Safely parses a string into a `usize`, returning a default value on failure.
pub fn safe_parse_usize(s: &str, default: usize) -> usize {
s.parse().unwrap_or(default)
}
/// Safely parses a string into a `u64`, returning a default value on failure.
pub fn safe_parse_u64(s: &str, default: u64) -> u64 {
s.parse().unwrap_or(default)
}
/// Safely parses a string into a boolean, returning a default value on failure.
pub fn safe_parse_bool(s: &str, default: bool) -> bool {
match s.to_lowercase().as_str() {
"true" | "1" | "yes" | "on" => true,
"false" | "0" | "no" | "off" => false,
_ => default,
}
}
/// Formats a `Duration` into a human-readable string.
pub fn format_duration(duration: std::time::Duration) -> String {
if duration.as_secs() > 0 {
format!("{:.2}s", duration.as_secs_f64())
} else if duration.as_millis() > 0 {
format!("{}ms", duration.as_millis())
} else if duration.as_micros() > 0 {
format!("{}µs", duration.as_micros())
} else {
format!("{}ns", duration.as_nanos())
}
}
/// Returns the current UTC timestamp in RFC 3339 format.
pub fn current_timestamp() -> String {
chrono::Utc::now().to_rfc3339()
}
/// Safely retrieves an environment variable.
pub fn get_env_var(key: &str) -> Option<String> {
std::env::var(key).ok()
}
/// Retrieves an environment variable or returns a default value if not set.
pub fn get_env_var_or(key: &str, default: &str) -> String {
std::env::var(key).unwrap_or_else(|_| default.to_string())
}
/// Checks if an environment variable is set.
pub fn has_env_var(key: &str) -> bool {
std::env::var(key).is_ok()
}
}

View File

@@ -0,0 +1,201 @@
// 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.
//! Validation utility functions for various data types.
use http::HeaderMap;
use lazy_static::lazy_static;
use regex::Regex;
use std::net::IpAddr;
use std::str::FromStr;
/// Collection of validation utility functions.
pub struct ValidationUtils;
impl ValidationUtils {
/// Validates an email address format.
pub fn is_valid_email(email: &str) -> bool {
lazy_static! {
static ref EMAIL_REGEX: Regex =
Regex::new(r"^([a-z0-9_+]([a-z0-9_+.]*[a-z0-9_+])?)@([a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,6})").unwrap();
}
EMAIL_REGEX.is_match(email)
}
/// Validates a URL format.
pub fn is_valid_url(url: &str) -> bool {
lazy_static! {
static ref URL_REGEX: Regex =
Regex::new(r"^(https?://)?([a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,6}(/.*)?$").unwrap();
}
URL_REGEX.is_match(url)
}
/// Validates the format of an X-Forwarded-For header value.
pub fn validate_x_forwarded_for(header_value: &str) -> bool {
if header_value.is_empty() {
return false;
}
let ips: Vec<&str> = header_value.split(',').map(|s| s.trim()).collect();
for ip_str in ips {
if ip_str.is_empty() {
return false;
}
let ip_part = ip_str.split(':').next().unwrap_or(ip_str);
if IpAddr::from_str(ip_part).is_err() {
return false;
}
}
true
}
/// Validates the format of an RFC 7239 Forwarded header value.
pub fn validate_forwarded_header(header_value: &str) -> bool {
if header_value.is_empty() {
return false;
}
let parts: Vec<&str> = header_value.split(';').collect();
if parts.is_empty() {
return false;
}
for part in parts {
let part = part.trim();
if !part.contains('=') {
return false;
}
}
true
}
/// Checks if an IP address is within any of the specified CIDR ranges.
pub fn validate_ip_in_range(ip: &IpAddr, cidr_ranges: &[String]) -> bool {
for cidr in cidr_ranges {
if let Ok(network) = ipnetwork::IpNetwork::from_str(cidr) {
if network.contains(*ip) {
return true;
}
}
}
false
}
/// Validates a header value for security (length and control characters).
pub fn validate_header_value(value: &str) -> bool {
for c in value.chars() {
if c.is_control() && c != '\t' && c != '\n' && c != '\r' {
return false;
}
}
if value.len() > 8192 {
return false;
}
true
}
/// Validates an entire HeaderMap for security.
pub fn validate_headers(headers: &HeaderMap) -> bool {
for (name, value) in headers {
let name_str = name.as_str();
if name_str.len() > 256 {
return false;
}
if let Ok(value_str) = value.to_str() {
if !Self::validate_header_value(value_str) {
return false;
}
} else if value.len() > 8192 {
return false;
}
}
true
}
/// Validates a port number.
pub fn validate_port(port: u16) -> bool {
port > 0
}
/// Validates a CIDR notation string.
pub fn validate_cidr(cidr: &str) -> bool {
ipnetwork::IpNetwork::from_str(cidr).is_ok()
}
/// Validates the length of a proxy chain.
pub fn validate_proxy_chain_length(chain: &[IpAddr], max_length: usize) -> bool {
chain.len() <= max_length
}
/// Validates that a proxy chain does not contain duplicate adjacent IPs.
pub fn validate_proxy_chain_continuity(chain: &[IpAddr]) -> bool {
if chain.len() < 2 {
return true;
}
for i in 1..chain.len() {
if chain[i] == chain[i - 1] {
return false;
}
}
true
}
/// Checks if a string contains only safe characters for use in URLs or headers.
pub fn is_safe_string(s: &str) -> bool {
lazy_static! {
static ref SAFE_REGEX: Regex = Regex::new(r"^[a-zA-Z0-9\-._~:/?#\[\]@!$&'()*+,;=]+$").unwrap();
}
SAFE_REGEX.is_match(s)
}
/// Validates rate limiting parameters.
pub fn validate_rate_limit_params(requests: u32, period_seconds: u64) -> bool {
requests > 0 && requests <= 10000 && period_seconds > 0 && period_seconds <= 86400
}
/// Validates cache configuration parameters.
pub fn validate_cache_params(capacity: usize, ttl_seconds: u64) -> bool {
capacity > 0 && capacity <= 1000000 && ttl_seconds > 0 && ttl_seconds <= 86400
}
/// Redacts sensitive information from a string based on provided patterns.
pub fn mask_sensitive_data(data: &str, sensitive_patterns: &[&str]) -> String {
let mut result = data.to_string();
for pattern in sensitive_patterns {
let regex = Regex::new(&format!(r#"(?i){}[:=]\s*([^&\s]+)"#, pattern)).unwrap();
result = regex
.replace_all(&result, |caps: &regex::Captures| format!("{}:[REDACTED]", &caps[1]))
.to_string();
}
result
}
}

View File

@@ -0,0 +1,49 @@
// 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::Body;
use axum::{Router, routing::get};
use rustfs_trusted_proxies::config::{AppConfig, TrustedProxy, TrustedProxyConfig, ValidationMode};
use rustfs_trusted_proxies::state::AppState;
use serde_json::json;
use std::sync::Arc;
use tower::ServiceExt;
fn create_test_app_state() -> AppState {
let proxies = vec![TrustedProxy::Single("127.0.0.1".parse().unwrap())];
let proxy_config = TrustedProxyConfig::new(proxies, ValidationMode::HopByHop, true, 10, true, vec![]);
let config = AppConfig::new(
proxy_config,
rustfs_trusted_proxies::config::CacheConfig::default(),
rustfs_trusted_proxies::config::MonitoringConfig::default(),
rustfs_trusted_proxies::config::CloudConfig::default(),
"127.0.0.1:3000".parse().unwrap(),
);
AppState {
config: Arc::new(config),
metrics: None,
}
}
#[tokio::test]
async fn test_health_check_endpoint() {
let state = create_test_app_state();
let app = Router::new()
.route("/health", get(|| async { axum::response::Json(json!({"status": "healthy"})) }))
.with_state(state);
let request = axum::http::Request::builder().uri("/health").body(Body::empty()).unwrap();
let response = app.oneshot(request).await.unwrap();
assert_eq!(response.status(), 200);
}

View File

@@ -0,0 +1,30 @@
// 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 rustfs_trusted_proxies::cloud::detector::CloudDetector;
use rustfs_trusted_proxies::cloud::metadata::AwsMetadataFetcher;
use std::time::Duration;
#[tokio::test]
async fn test_cloud_detector_disabled() {
let detector = CloudDetector::new(false, Duration::from_secs(1), None);
let provider = detector.detect_provider();
assert!(provider.is_none());
}
#[tokio::test]
async fn test_aws_metadata_fetcher() {
let fetcher = AwsMetadataFetcher::new();
assert_eq!(fetcher.provider_name(), "aws");
}

View File

@@ -0,0 +1,22 @@
// 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.
//! Integration tests for the trusted proxy system.
#[cfg(test)]
mod api_tests;
#[cfg(test)]
mod cloud_tests;
#[cfg(test)]
mod proxy_tests;

View File

@@ -0,0 +1,37 @@
// 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::Body;
use axum::{Router, routing::get};
use rustfs_trusted_proxies::config::{TrustedProxy, TrustedProxyConfig, ValidationMode};
use rustfs_trusted_proxies::middleware::TrustedProxyLayer;
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 app = Router::new().route("/test", get(|| async { "OK" })).layer(proxy_layer);
let request = axum::http::Request::builder()
.uri("/test")
.header("X-Forwarded-For", "203.0.113.195")
.body(Body::empty())
.unwrap();
let response = app.oneshot(request).await.unwrap();
assert_eq!(response.status(), 200);
}

View File

@@ -0,0 +1,117 @@
// 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 rustfs_trusted_proxies::config::env::{DEFAULT_TRUSTED_PROXIES, ENV_TRUSTED_PROXIES};
use rustfs_trusted_proxies::config::{ConfigLoader, TrustedProxy, TrustedProxyConfig, ValidationMode};
use std::net::IpAddr;
#[test]
fn test_config_loader_default() {
std::env::remove_var(ENV_TRUSTED_PROXIES);
let config = ConfigLoader::from_env_or_default();
assert_eq!(config.server_addr.port(), 3000);
assert!(!config.proxy.proxies.is_empty());
assert_eq!(config.proxy.validation_mode, ValidationMode::HopByHop);
assert!(config.proxy.enable_rfc7239);
assert_eq!(config.proxy.max_hops, 10);
}
#[test]
fn test_config_loader_env_vars() {
std::env::set_var(ENV_TRUSTED_PROXIES, "192.168.1.0/24,10.0.0.0/8");
std::env::set_var("TRUSTED_PROXY_VALIDATION_MODE", "strict");
std::env::set_var("TRUSTED_PROXY_MAX_HOPS", "5");
std::env::set_var("SERVER_PORT", "8080");
let config = ConfigLoader::from_env();
if let Ok(config) = config {
assert_eq!(config.server_addr.port(), 8080);
assert_eq!(config.proxy.validation_mode, ValidationMode::Strict);
assert_eq!(config.proxy.max_hops, 5);
std::env::remove_var(ENV_TRUSTED_PROXIES);
std::env::remove_var("TRUSTED_PROXY_VALIDATION_MODE");
std::env::remove_var("TRUSTED_PROXY_MAX_HOPS");
std::env::remove_var("SERVER_PORT");
} else {
panic!("Failed to load configuration from environment variables");
}
}
#[test]
fn test_trusted_proxy_config() {
let proxies = vec![
TrustedProxy::Single("192.168.1.1".parse().unwrap()),
TrustedProxy::Cidr("10.0.0.0/8".parse().unwrap()),
];
let config = TrustedProxyConfig::new(proxies.clone(), ValidationMode::Strict, true, 10, true, vec![]);
assert_eq!(config.proxies.len(), 2);
assert_eq!(config.validation_mode, ValidationMode::Strict);
assert!(config.enable_rfc7239);
assert_eq!(config.max_hops, 10);
assert!(config.enable_chain_continuity_check);
let test_ip: IpAddr = "192.168.1.1".parse().unwrap();
let test_socket_addr = std::net::SocketAddr::new(test_ip, 8080);
assert!(config.is_trusted(&test_socket_addr));
let test_ip2: IpAddr = "10.0.1.1".parse().unwrap();
let test_socket_addr2 = std::net::SocketAddr::new(test_ip2, 8080);
assert!(config.is_trusted(&test_socket_addr2));
}
#[test]
fn test_trusted_proxy_contains() {
let single_proxy = TrustedProxy::Single("192.168.1.1".parse().unwrap());
let test_ip: IpAddr = "192.168.1.1".parse().unwrap();
let test_ip2: IpAddr = "192.168.1.2".parse().unwrap();
assert!(single_proxy.contains(&test_ip));
assert!(!single_proxy.contains(&test_ip2));
let cidr_proxy = TrustedProxy::Cidr("192.168.1.0/24".parse().unwrap());
assert!(cidr_proxy.contains(&test_ip));
assert!(cidr_proxy.contains(&test_ip2));
let test_ip3: IpAddr = "192.168.2.1".parse().unwrap();
assert!(!cidr_proxy.contains(&test_ip3));
}
#[test]
fn test_private_network_check() {
let config = TrustedProxyConfig::new(
Vec::new(),
ValidationMode::Lenient,
true,
10,
true,
vec!["10.0.0.0/8".parse().unwrap(), "192.168.0.0/16".parse().unwrap()],
);
let private_ip: IpAddr = "10.0.1.1".parse().unwrap();
let private_ip2: IpAddr = "192.168.1.1".parse().unwrap();
let public_ip: IpAddr = "8.8.8.8".parse().unwrap();
assert!(config.is_private_network(&private_ip));
assert!(config.is_private_network(&private_ip2));
assert!(!config.is_private_network(&public_ip));
}
#[test]
fn test_default_values() {
assert_eq!(DEFAULT_TRUSTED_PROXIES, "127.0.0.1,::1,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,fd00::/8");
}

View File

@@ -0,0 +1,194 @@
// 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 rustfs_trusted_proxies::utils::IpUtils;
use std::net::IpAddr;
use std::str::FromStr;
#[test]
fn test_is_valid_ip_address() {
let valid_ip: IpAddr = "192.168.1.1".parse().unwrap();
assert!(IpUtils::is_valid_ip_address(&valid_ip));
let unspecified_ip: IpAddr = "0.0.0.0".parse().unwrap();
assert!(!IpUtils::is_valid_ip_address(&unspecified_ip));
let multicast_ip: IpAddr = "224.0.0.1".parse().unwrap();
assert!(!IpUtils::is_valid_ip_address(&multicast_ip));
let valid_ipv6: IpAddr = "2001:db8::1".parse().unwrap();
assert!(IpUtils::is_valid_ip_address(&valid_ipv6));
let unspecified_ipv6: IpAddr = "::".parse().unwrap();
assert!(!IpUtils::is_valid_ip_address(&unspecified_ipv6));
}
#[test]
fn test_is_reserved_ip() {
let private_ip: IpAddr = "10.0.0.1".parse().unwrap();
assert!(IpUtils::is_reserved_ip(&private_ip));
let loopback_ip: IpAddr = "127.0.0.1".parse().unwrap();
assert!(IpUtils::is_reserved_ip(&loopback_ip));
let link_local_ip: IpAddr = "169.254.0.1".parse().unwrap();
assert!(IpUtils::is_reserved_ip(&link_local_ip));
let documentation_ip: IpAddr = "192.0.2.1".parse().unwrap();
assert!(IpUtils::is_reserved_ip(&documentation_ip));
let public_ip: IpAddr = "8.8.8.8".parse().unwrap();
assert!(!IpUtils::is_reserved_ip(&public_ip));
}
#[test]
fn test_is_private_ip() {
assert!(IpUtils::is_private_ip(&"10.0.0.1".parse().unwrap()));
assert!(IpUtils::is_private_ip(&"10.255.255.254".parse().unwrap()));
assert!(IpUtils::is_private_ip(&"172.16.0.1".parse().unwrap()));
assert!(IpUtils::is_private_ip(&"172.31.255.254".parse().unwrap()));
assert!(!IpUtils::is_private_ip(&"172.15.0.1".parse().unwrap()));
assert!(!IpUtils::is_private_ip(&"172.32.0.1".parse().unwrap()));
assert!(IpUtils::is_private_ip(&"192.168.0.1".parse().unwrap()));
assert!(IpUtils::is_private_ip(&"192.168.255.254".parse().unwrap()));
assert!(!IpUtils::is_private_ip(&"8.8.8.8".parse().unwrap()));
assert!(!IpUtils::is_private_ip(&"203.0.113.1".parse().unwrap()));
}
#[test]
fn test_is_loopback_ip() {
assert!(IpUtils::is_loopback_ip(&"127.0.0.1".parse().unwrap()));
assert!(IpUtils::is_loopback_ip(&"127.255.255.254".parse().unwrap()));
assert!(IpUtils::is_loopback_ip(&"::1".parse().unwrap()));
assert!(!IpUtils::is_loopback_ip(&"192.168.1.1".parse().unwrap()));
assert!(!IpUtils::is_loopback_ip(&"2001:db8::1".parse().unwrap()));
}
#[test]
fn test_is_link_local_ip() {
assert!(IpUtils::is_link_local_ip(&"169.254.0.1".parse().unwrap()));
assert!(IpUtils::is_link_local_ip(&"169.254.255.254".parse().unwrap()));
assert!(IpUtils::is_link_local_ip(&"fe80::1".parse().unwrap()));
assert!(IpUtils::is_link_local_ip(&"fe80::abcd:1234:5678:9abc".parse().unwrap()));
assert!(!IpUtils::is_link_local_ip(&"192.168.1.1".parse().unwrap()));
assert!(!IpUtils::is_link_local_ip(&"2001:db8::1".parse().unwrap()));
}
#[test]
fn test_is_documentation_ip() {
assert!(IpUtils::is_documentation_ip(&"192.0.2.1".parse().unwrap()));
assert!(IpUtils::is_documentation_ip(&"198.51.100.1".parse().unwrap()));
assert!(IpUtils::is_documentation_ip(&"203.0.113.1".parse().unwrap()));
assert!(IpUtils::is_documentation_ip(&"2001:db8::1".parse().unwrap()));
assert!(!IpUtils::is_documentation_ip(&"8.8.8.8".parse().unwrap()));
assert!(!IpUtils::is_documentation_ip(&"2001:4860::1".parse().unwrap()));
}
#[test]
fn test_parse_ip_or_cidr() {
let result = IpUtils::parse_ip_or_cidr("192.168.1.1");
assert!(result.is_ok());
let result = IpUtils::parse_ip_or_cidr("192.168.1.0/24");
assert!(result.is_ok());
let result = IpUtils::parse_ip_or_cidr("2001:db8::1");
assert!(result.is_ok());
let result = IpUtils::parse_ip_or_cidr("2001:db8::/32");
assert!(result.is_ok());
let result = IpUtils::parse_ip_or_cidr("invalid");
assert!(result.is_err());
}
#[test]
fn test_parse_ip_list() {
let result = IpUtils::parse_ip_list("192.168.1.1, 10.0.0.1, 8.8.8.8");
assert!(result.is_ok());
let ips = result.unwrap();
assert_eq!(ips.len(), 3);
assert_eq!(ips[0], IpAddr::from_str("192.168.1.1").unwrap());
assert_eq!(ips[1], IpAddr::from_str("10.0.0.1").unwrap());
assert_eq!(ips[2], IpAddr::from_str("8.8.8.8").unwrap());
let result = IpUtils::parse_ip_list("192.168.1.1,10.0.0.1");
assert!(result.is_ok());
assert_eq!(result.unwrap().len(), 2);
let result = IpUtils::parse_ip_list("");
assert!(result.is_ok());
assert!(result.unwrap().is_empty());
let result = IpUtils::parse_ip_list("192.168.1.1, invalid");
assert!(result.is_err());
}
#[test]
fn test_parse_network_list() {
let result = IpUtils::parse_network_list("192.168.1.0/24, 10.0.0.0/8");
assert!(result.is_ok());
let networks = result.unwrap();
assert_eq!(networks.len(), 2);
let result = IpUtils::parse_network_list("192.168.1.1");
assert!(result.is_ok());
assert_eq!(result.unwrap().len(), 1);
let result = IpUtils::parse_network_list("192.168.1.0/24, invalid");
assert!(result.is_err());
}
#[test]
fn test_ip_in_networks() {
let networks = vec!["10.0.0.0/8".parse().unwrap(), "192.168.1.0/24".parse().unwrap()];
let ip_in_network: IpAddr = "10.0.1.1".parse().unwrap();
let ip_in_network2: IpAddr = "192.168.1.100".parse().unwrap();
let ip_not_in_network: IpAddr = "8.8.8.8".parse().unwrap();
assert!(IpUtils::ip_in_networks(&ip_in_network, &networks));
assert!(IpUtils::ip_in_networks(&ip_in_network2, &networks));
assert!(!IpUtils::ip_in_networks(&ip_not_in_network, &networks));
}
#[test]
fn test_get_ip_type() {
assert_eq!(IpUtils::get_ip_type(&"10.0.0.1".parse().unwrap()), "private");
assert_eq!(IpUtils::get_ip_type(&"127.0.0.1".parse().unwrap()), "loopback");
assert_eq!(IpUtils::get_ip_type(&"169.254.0.1".parse().unwrap()), "link_local");
assert_eq!(IpUtils::get_ip_type(&"192.0.2.1".parse().unwrap()), "documentation");
assert_eq!(IpUtils::get_ip_type(&"224.0.0.1".parse().unwrap()), "reserved");
assert_eq!(IpUtils::get_ip_type(&"8.8.8.8".parse().unwrap()), "public");
}
#[test]
fn test_canonical_ip() {
let ipv4: IpAddr = "192.168.001.001".parse().unwrap();
assert_eq!(IpUtils::canonical_ip(&ipv4), "192.168.1.1");
let ipv6_full: IpAddr = "2001:0db8:0000:0000:0000:0000:0000:0001".parse().unwrap();
let ipv6_compressed: IpAddr = "2001:db8::1".parse().unwrap();
assert_eq!(IpUtils::canonical_ip(&ipv6_full), "2001:db8::1");
assert_eq!(IpUtils::canonical_ip(&ipv6_compressed), "2001:db8::1");
let ipv6_multi_zero: IpAddr = "2001:0db8:0000:0000:abcd:0000:0000:1234".parse().unwrap();
assert_eq!(IpUtils::canonical_ip(&ipv6_multi_zero), "2001:db8::abcd:0:0:1234");
}

View File

@@ -0,0 +1,24 @@
// 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.
//! Unit tests for the trusted proxy system components.
#[cfg(test)]
mod config_tests;
#[cfg(test)]
mod ip_tests;
#[cfg(test)]
mod validation_tests;
#[cfg(test)]
mod validator_tests;

View File

@@ -0,0 +1,65 @@
// 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 rustfs_trusted_proxies::utils::ValidationUtils;
use std::net::IpAddr;
#[test]
fn test_email_validation() {
assert!(ValidationUtils::is_valid_email("user@example.com"));
assert!(!ValidationUtils::is_valid_email("invalid-email"));
}
#[test]
fn test_url_validation() {
assert!(ValidationUtils::is_valid_url("https://example.com"));
assert!(!ValidationUtils::is_valid_url("invalid"));
}
#[test]
fn test_x_forwarded_for_validation() {
assert!(ValidationUtils::validate_x_forwarded_for("203.0.113.195"));
assert!(!ValidationUtils::validate_x_forwarded_for("invalid"));
}
#[test]
fn test_forwarded_header_validation() {
assert!(ValidationUtils::validate_forwarded_header("for=192.0.2.60"));
assert!(!ValidationUtils::validate_forwarded_header("invalid"));
}
#[test]
fn test_ip_in_range_validation() {
let cidr_ranges = vec!["10.0.0.0/8".to_string(), "192.168.0.0/16".to_string()];
let ip: IpAddr = "10.0.1.1".parse().unwrap();
assert!(ValidationUtils::validate_ip_in_range(&ip, &cidr_ranges));
}
#[test]
fn test_header_value_validation() {
assert!(ValidationUtils::validate_header_value("text/plain"));
assert!(!ValidationUtils::validate_header_value(&"a".repeat(8193)));
}
#[test]
fn test_port_validation() {
assert!(ValidationUtils::validate_port(80));
assert!(!ValidationUtils::validate_port(0));
}
#[test]
fn test_cidr_validation() {
assert!(ValidationUtils::validate_cidr("192.168.1.0/24"));
assert!(!ValidationUtils::validate_cidr("invalid"));
}

View File

@@ -0,0 +1,56 @@
// 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::http::HeaderMap;
use rustfs_trusted_proxies::config::{TrustedProxy, TrustedProxyConfig, ValidationMode};
use rustfs_trusted_proxies::proxy::chain::ProxyChainAnalyzer;
use rustfs_trusted_proxies::proxy::validator::{ClientInfo, ProxyValidator};
use std::net::{IpAddr, SocketAddr};
use std::str::FromStr;
fn create_test_config() -> TrustedProxyConfig {
let proxies = vec![
TrustedProxy::Single("192.168.1.100".parse().unwrap()),
TrustedProxy::Cidr("10.0.0.0/8".parse().unwrap()),
];
TrustedProxyConfig::new(proxies, ValidationMode::HopByHop, true, 5, true, vec![])
}
#[test]
fn test_client_info_direct() {
let addr = SocketAddr::new(IpAddr::from([192, 168, 1, 1]), 8080);
let client_info = ClientInfo::direct(addr);
assert_eq!(client_info.real_ip, IpAddr::from([192, 168, 1, 1]));
}
#[test]
fn test_parse_x_forwarded_for() {
let header_value = "203.0.113.195, 198.51.100.1";
let result = ProxyValidator::parse_x_forwarded_for(header_value);
assert_eq!(result.len(), 2);
}
#[test]
fn test_proxy_chain_analyzer_hop_by_hop() {
let config = create_test_config();
let analyzer = ProxyChainAnalyzer::new(config);
let chain = vec![
IpAddr::from_str("203.0.113.195").unwrap(),
IpAddr::from_str("10.0.1.100").unwrap(),
];
let current_proxy = IpAddr::from_str("192.168.1.100").unwrap();
let headers = HeaderMap::new();
let result = analyzer.analyze_chain(&chain, current_proxy, &headers);
assert!(result.is_ok());
}

View File

@@ -1,6 +1,6 @@
# 对象路径中的特殊字符 - 完整文档
本目录包含关于在 RustFS 中处理 S3 对象路径中特殊字符(空格、加号、百分号等)的完整文档。
本目录包含关于在 RustFS 中处理 S3 对象路径中特殊字符 (空格、加号、百分号等) 的完整文档。
## 快速链接
@@ -12,18 +12,18 @@
### 问题现象
用户报告了两个问题:
1. **问题 A**: UI 可以导航到包含特殊字符的文件夹,但无法列出其中的内容
用户报告了两个问题
1. **问题 A**: UI 可以导航到包含特殊字符的文件夹但无法列出其中的内容
2. **问题 B**: 上传路径中包含 `+` 号的文件时出现 400 错误
### 根本原因
经过深入调查,包括检查 s3s 库的源代码,我们发现:
经过深入调查包括检查 s3s 库的源代码我们发现
**后端 (RustFS) 工作正常**
- s3s 库正确地对 HTTP 请求中的对象键进行 URL 解码
- RustFS 正确存储和检索包含特殊字符的对象
- 命令行工具(mc, aws-cli)完美工作 → 证明后端正确处理特殊字符
- 命令行工具 (mc, aws-cli) 完美工作 → 证明后端正确处理特殊字符
**问题出在 UI/客户端层**
- 某些客户端未正确进行 URL 编码
@@ -33,8 +33,8 @@
### 解决方案
1. **用户**: 使用正规的 S3 SDK/客户端(它们会自动处理编码)
2. **开发者**: 后端无需修复,但添加了防御性验证和日志
3. **UI**: UI 需要正确对所有请求进行 URL 编码(如适用)
2. **开发者**: 后端无需修复但添加了防御性验证和日志
3. **UI**: UI 需要正确对所有请求进行 URL 编码 (如适用)
## URL 编码快速参考
@@ -44,11 +44,11 @@
| 加号 | `+` | `%2B` | `%2B` |
| 百分号 | `%` | `%25` | `%25` |
**重要**: 在 URL **路径**中,`+` = 字面加号(不是空格)。只有 `%20` = 空格!
**重要**: 在 URL **路径**中`+` = 字面加号 (不是空格)。只有 `%20` = 空格
## 快速示例
### ✅ 正确使用(使用 mc)
### ✅ 正确使用 (使用 mc)
```bash
# 上传
@@ -60,7 +60,7 @@ mc ls "myrustfs/bucket/路径 包含 空格/"
# 结果: ✅ 成功 - mc 正确编码了请求
```
### ❌ 可能失败(原始 HTTP 未编码)
### ❌ 可能失败 (原始 HTTP 未编码)
```bash
# 错误: 未编码
@@ -82,7 +82,7 @@ curl "http://localhost:9000/bucket/%E8%B7%AF%E5%BE%84%20%E5%8C%85%E5%90%AB%20%E7
### ✅ 已完成
1. **后端验证**: 添加了控制字符验证(拒绝空字节、换行符)
1. **后端验证**: 添加了控制字符验证 (拒绝空字节、换行符)
2. **调试日志**: 为包含特殊字符的键添加了日志记录
3. **测试**: 创建了综合 e2e 测试套件
4. **文档**:
@@ -103,7 +103,7 @@ curl "http://localhost:9000/bucket/%E8%B7%AF%E5%BE%84%20%E5%8C%85%E5%90%AB%20%E7
3. **用户沟通**:
- 更新用户文档
- 在 FAQ 中添加故障排除
- 传达已知的 UI 限制(如有)
- 传达已知的 UI 限制 (如有)
## 测试
@@ -134,15 +134,15 @@ aws --endpoint-url=http://localhost:9000 s3 ls "s3://bucket/测试 包含 空格
## 支持
如果遇到特殊字符问题:
如果遇到特殊字符问题
1. **首先**: 查看[客户端指南](./client-special-characters-guide.md)
2. **尝试**: 使用 mc 或 AWS CLI 隔离问题
3. **启用**: 调试日志: `RUST_LOG=rustfs=debug`
4. **报告**: 创建问题,包含:
3. **启用**: 调试日志`RUST_LOG=rustfs=debug`
4. **报告**: 创建问题包含
- 使用的客户端/SDK
- 导致问题的确切对象名称
- mc 是否工作(以隔离后端与客户端)
- mc 是否工作 (以隔离后端与客户端)
- 调试日志
## 相关文档
@@ -154,26 +154,26 @@ aws --endpoint-url=http://localhost:9000 s3 ls "s3://bucket/测试 包含 空格
## 常见问题
**问: 可以在对象名称中使用空格吗?**
: 可以,但请使用能自动处理编码的 S3 SDK。
**问可以在对象名称中使用空格吗**
可以但请使用能自动处理编码的 S3 SDK。
**问: 为什么 `+` 不能用作空格?**
: 在 URL 路径中,`+` 表示字面加号。只有在查询参数中 `+` 才表示空格。在路径中使用 `%20` 表示空格。
**问为什么 `+` 不能用作空格**
在 URL 路径中`+` 表示字面加号。只有在查询参数中 `+` 才表示空格。在路径中使用 `%20` 表示空格。
**问: RustFS 支持对象名称中的 Unicode 吗?**
: 支持,对象名称是 UTF-8 字符串。它们支持任何有效的 UTF-8 字符。
**问RustFS 支持对象名称中的 Unicode 吗**
支持对象名称是 UTF-8 字符串。它们支持任何有效的 UTF-8 字符。
**问: 哪些字符是禁止的?**
: 控制字符(空字节、换行符、回车符)被拒绝。所有可打印字符都是允许的。
**问哪些字符是禁止的**
控制字符 (空字节、换行符、回车符) 被拒绝。所有可打印字符都是允许的。
**问: 如何修复"UI 无法列出文件夹"的问题?**
: 使用 CLI(mc 或 aws-cli)代替。这是 UI 错误,不是后端问题。
**问如何修复"UI 无法列出文件夹"的问题**
使用 CLI(mc 或 aws-cli) 代替。这是 UI 错误不是后端问题。
## 版本历史
- **v1.0** (2025-12-09): 初始文档
- 完成综合分析
- 确定根本原因(UI/客户端问题)
- 确定根本原因 (UI/客户端问题)
- 添加后端验证和日志
- 创建客户端指南
- 添加 E2E 测试

View File

@@ -59,6 +59,7 @@ rustfs-rio.workspace = true
rustfs-s3select-api = { workspace = true }
rustfs-s3select-query = { workspace = true }
rustfs-targets = { workspace = true }
rustfs-trusted-proxies = { workspace = true }
rustfs-utils = { workspace = true, features = ["full"] }
rustfs-zip = { workspace = true }

View File

@@ -112,6 +112,17 @@ async fn async_main() -> Result<()> {
// Initialize performance profiling if enabled
profiling::init_from_env().await;
// rustfs-trusted-proxies
match rustfs_trusted_proxies::initialize().await {
Ok(_) => {
info!(target: "rustfs::main", "Trusted proxies configuration initialized successfully.");
}
Err(e) => {
error!("Failed to initialize trusted proxies configuration: {:?}", e);
return Err(Error::other(e));
}
}
// Initialize TLS if a certificate path is provided
if let Some(tls_path) = &opt.tls_path {
match init_cert(tls_path).await {

View File

@@ -37,6 +37,7 @@ use rustfs_common::GlobalReadiness;
use rustfs_config::{RUSTFS_TLS_CERT, RUSTFS_TLS_KEY};
use rustfs_ecstore::rpc::{TONIC_RPC_PREFIX, verify_rpc_signature};
use rustfs_protos::proto_gen::node_service::node_service_server::NodeServiceServer;
use rustfs_trusted_proxies::{ClientInfo, TrustedProxiesLayer};
use rustfs_utils::net::parse_and_resolve_address;
use rustls::ServerConfig;
use s3s::{host::MultiDomain, service::S3Service, service::S3ServiceBuilder};
@@ -502,8 +503,9 @@ fn process_connection(
None
}
};
let trusted_proxies = rustfs_trusted_proxies::get_trusted_proxies_config().await;
let hybrid_service = ServiceBuilder::new()
.layer(TrustedProxiesLayer::new(trusted_proxies.clone()))
.layer(SetRequestIdLayer::x_request_id(MakeRequestUuid))
.layer(CatchPanicLayer::new())
.layer(AddExtensionLayer::new(remote_addr))
@@ -518,10 +520,15 @@ fn process_connection(
.get(http::header::HeaderName::from_static("x-request-id"))
.and_then(|v| v.to_str().ok())
.unwrap_or("unknown");
let client_info = request.extensions().get::<ClientInfo>();
let real_ip = client_info
.map(|info| info.real_ip.to_string())
.unwrap_or_else(|| "unknown".to_string());
let span = tracing::info_span!("http-request",
trace_id = %trace_id,
status_code = tracing::field::Empty,
method = %request.method(),
real_ip = %real_ip,
uri = %request.uri(),
version = ?request.version(),
);