mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-31 01:09:23 +00:00
Refactor trusted-proxies: modernize utils, improve safety, and fix clippy lints (#1693)
Co-authored-by: majinghe <42570491+majinghe@users.noreply.github.com> Co-authored-by: GatewayJ <835269233@qq.com> Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com> Co-authored-by: houseme <4829346+houseme@users.noreply.github.com> Co-authored-by: heihutu <30542132+heihutu@users.noreply.github.com>
This commit is contained in:
@@ -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!("rustfs_trusted_proxy_cache_hits").increment(1);
|
||||
return is_trusted;
|
||||
}
|
||||
|
||||
// Cache miss: perform validation and update cache.
|
||||
metrics::counter!("rustfs_trusted_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!("rustfs_trusted_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,
|
||||
}
|
||||
@@ -0,0 +1,257 @@
|
||||
// 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::{ProxyError, TrustedProxyConfig, ValidationMode, 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::TrustedProxy::Single(ip) => {
|
||||
trusted_ip_cache.insert(*ip);
|
||||
}
|
||||
crate::TrustedProxy::Cidr(network) => {
|
||||
// For small networks, cache all IPs to speed up lookups.
|
||||
// Only cache IPv4 networks to avoid iterating huge IPv6 ranges.
|
||||
if network.is_ipv4() && 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);
|
||||
|
||||
// Enforce maximum hop limit.
|
||||
if full_chain.len() > self.config.max_hops {
|
||||
return Err(ProxyError::ChainTooLong(full_chain.len(), self.config.max_hops));
|
||||
}
|
||||
|
||||
// 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()
|
||||
&& 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 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"));
|
||||
}
|
||||
|
||||
if !is_valid_ip_address(ip) {
|
||||
return Err(ProxyError::IpParseError(format!("Invalid IP address in chain: {}", ip)));
|
||||
}
|
||||
}
|
||||
|
||||
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 !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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,219 @@
|
||||
// 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::{ProxyError, ValidationMode};
|
||||
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!(
|
||||
"rustfs_trusted_proxy_validation_attempts_total",
|
||||
"Total number of proxy validation attempts"
|
||||
);
|
||||
describe_counter!(
|
||||
"rustfs_trusted_proxy_validation_success_total",
|
||||
"Total number of successful proxy validations"
|
||||
);
|
||||
describe_counter!(
|
||||
"rustfs_trusted_proxy_validation_failure_total",
|
||||
"Total number of failed proxy validations"
|
||||
);
|
||||
describe_counter!(
|
||||
"rustfs_trusted_proxy_validation_failure_by_type_total",
|
||||
"Total number of failed proxy validations categorized by error type"
|
||||
);
|
||||
describe_gauge!("rustfs_trusted_proxy_chain_length", "Current length of proxy chains being validated");
|
||||
describe_histogram!(
|
||||
"rustfs_trusted_proxy_validation_duration_seconds",
|
||||
"Time taken to validate a proxy chain in seconds"
|
||||
);
|
||||
describe_gauge!(
|
||||
"rustfs_trusted_proxy_cache_size",
|
||||
"Current number of entries in the proxy validation cache"
|
||||
);
|
||||
describe_counter!("rustfs_trusted_proxy_cache_hits_total", "Total number of cache hits for proxy validation");
|
||||
describe_counter!(
|
||||
"rustfs_trusted_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!(
|
||||
"rustfs_trusted_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!(
|
||||
"rustfs_trusted_proxy_validation_success_total",
|
||||
"app" => self.app_name.clone(),
|
||||
"trusted" => from_trusted_proxy.to_string()
|
||||
)
|
||||
.increment(1);
|
||||
|
||||
gauge!(
|
||||
"rustfs_trusted_proxy_chain_length",
|
||||
"app" => self.app_name.clone()
|
||||
)
|
||||
.set(proxy_hops as f64);
|
||||
|
||||
histogram!(
|
||||
"rustfs_trusted_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!(
|
||||
"rustfs_trusted_proxy_validation_failure_total",
|
||||
"app" => self.app_name.clone(),
|
||||
"error_type" => error_type
|
||||
)
|
||||
.increment(1);
|
||||
|
||||
counter!(
|
||||
"rustfs_trusted_proxy_validation_failure_by_type_total",
|
||||
"app" => self.app_name.clone(),
|
||||
"error_type" => error_type
|
||||
)
|
||||
.increment(1);
|
||||
|
||||
histogram!(
|
||||
"rustfs_trusted_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!(
|
||||
"rustfs_trusted_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!("rustfs_trusted_proxy_cache_hits_total", "app" => self.app_name.clone()).increment(hits);
|
||||
counter!("rustfs_trusted_proxy_cache_misses_total", "app" => self.app_name.clone()).increment(misses);
|
||||
gauge!("rustfs_trusted_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!(" - rustfs_trusted_proxy_validation_attempts_total");
|
||||
info!(" - rustfs_trusted_proxy_validation_success_total");
|
||||
info!(" - rustfs_trusted_proxy_validation_failure_total");
|
||||
info!(" - rustfs_trusted_proxy_validation_failure_by_type_total");
|
||||
info!(" - rustfs_trusted_proxy_chain_length");
|
||||
info!(" - rustfs_trusted_proxy_validation_duration_seconds");
|
||||
info!(" - rustfs_trusted_proxy_cache_size");
|
||||
info!(" - rustfs_trusted_proxy_cache_hits_total");
|
||||
info!(" - rustfs_trusted_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)
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
//! 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::*;
|
||||
@@ -0,0 +1,337 @@
|
||||
// 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::{ProxyChainAnalyzer, ProxyError, ProxyMetrics, TrustedProxyConfig, ValidationMode};
|
||||
|
||||
/// 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, handling IPv6 addresses in brackets as per RFC 7239.
|
||||
let ip_str = if value.starts_with('[') {
|
||||
if let Some(end) = value.find(']') {
|
||||
&value[1..end]
|
||||
} else {
|
||||
continue; // Invalid format, skip
|
||||
}
|
||||
} else {
|
||||
// For IPv4 or IPv6 without brackets, take the part before the first colon.
|
||||
value.split(':').next().unwrap_or(value)
|
||||
};
|
||||
|
||||
if let Ok(ip) = ip_str.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.
|
||||
pub 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| {
|
||||
// Handle IPv6 addresses in brackets, e.g., [::1]:8080
|
||||
let ip_str = if s.starts_with('[') {
|
||||
if let Some(end) = s.find(']') {
|
||||
&s[1..end]
|
||||
} else {
|
||||
s // Invalid format, try parsing as is
|
||||
}
|
||||
} else {
|
||||
// For IPv4 or IPv6 without brackets, take the part before the first colon.
|
||||
s.split(':').next().unwrap_or(s)
|
||||
};
|
||||
ip_str.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>,
|
||||
}
|
||||
Reference in New Issue
Block a user