feat(audit): show trace with HTTP's client_ip and user if logged in + add support of trusted proxy via CIDR

This commit is contained in:
Edouard Vanbelle
2026-04-26 02:27:45 +02:00
parent e4ea2211ea
commit 8e1a738056
8 changed files with 308 additions and 56 deletions
+7 -50
View File
@@ -4,29 +4,21 @@
//! counts per client IP. Each protected endpoint group gets its own
//! [`RateLimiter`] instance with independently tuneable limits.
//!
//! The middleware extracts the client IP from (in order):
//! 1. `X-Forwarded-For` header (first entry — set by reverse proxies)
//! 2. `X-Real-Ip` header
//! 3. The TCP peer address from the connection info
//! Client IP resolution is delegated to [`super::trusted_proxy::client_ip`],
//! which honours `OXICLOUD_TRUST_PROXY_CIDR` for proxy-header forwarding.
//!
//! When the limit is exceeded a `429 Too Many Requests` response is returned
//! with a `Retry-After` header indicating how many seconds to wait.
use axum::{
extract::ConnectInfo,
http::{HeaderValue, Request, StatusCode},
middleware::Next,
response::{IntoResponse, Response},
};
use moka::sync::Cache;
use std::net::SocketAddr;
use std::sync::{Arc, OnceLock};
use std::sync::Arc;
use std::time::Duration;
/// Cached value of `OXICLOUD_TRUST_PROXY_HEADERS` env var.
/// Read once on first access, never again — avoids a syscall per request.
static TRUST_PROXY: OnceLock<bool> = OnceLock::new();
/// A simple sliding-window counter keyed by IP address.
///
/// Each key lives for `window` seconds; every request increments the counter.
@@ -94,46 +86,11 @@ impl RateLimiter {
/// Extract the most-likely real client IP from headers / connection info.
///
/// Proxy headers (`X-Forwarded-For`, `X-Real-Ip`) are only trusted when
/// `OXICLOUD_TRUST_PROXY_HEADERS=true` is set. Without a trusted reverse
/// proxy in front of the app, an attacker can spoof these headers to bypass
/// rate limiting.
/// Proxy headers (`X-Forwarded-For`, `X-Real-Ip`) are only trusted when the
/// TCP peer address falls within `OXICLOUD_TRUST_PROXY_CIDR`. Without a
/// configured CIDR list an attacker could spoof headers to bypass rate limiting.
pub fn extract_client_ip<B>(req: &Request<B>) -> String {
let trust_proxy = *TRUST_PROXY.get_or_init(|| {
std::env::var("OXICLOUD_TRUST_PROXY_HEADERS")
.map(|v| v == "true" || v == "1")
.unwrap_or(false)
});
let headers = req.headers();
if trust_proxy {
// 1. X-Forwarded-For (first entry — closest to the client)
if let Some(xff) = headers.get("x-forwarded-for").and_then(|v| v.to_str().ok())
&& let Some(first) = xff.split(',').next()
{
let ip = first.trim();
if !ip.is_empty() {
return ip.to_string();
}
}
// 2. X-Real-Ip
if let Some(xri) = headers.get("x-real-ip").and_then(|v| v.to_str().ok()) {
let ip = xri.trim();
if !ip.is_empty() {
return ip.to_string();
}
}
}
// 3. TCP peer (ConnectInfo extension set by axum::serve)
if let Some(addr) = req.extensions().get::<ConnectInfo<SocketAddr>>() {
return addr.0.ip().to_string();
}
// Fallback — should never happen behind axum::serve
"unknown".to_string()
super::trusted_proxy::client_ip(req, false)
}
/// Build a rate-limit response with the standard `Retry-After` header.