From 00478e4a1a112df25b71799bea8d6841afd3a5aa Mon Sep 17 00:00:00 2001 From: xfy Date: Thu, 18 Jun 2026 11:28:06 +0800 Subject: [PATCH] perf(rate-limit): use lenient bucket when client IP is unknown MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit TRUSTED_PROXY_COUNT=0(默认)时,Dioxus server function 拿不到 TCP 对端, get_client_ip 返回 "unknown",所有匿名请求共用严格桶(1 req/s, burst 5), 正常用户的高频请求被误杀。check_strict_limit 现在对 unknown IP 改走 宽松桶(30 req/s, burst 100,可通过 RATE_LIMIT_UNKNOWN_* 调整)。 配好反向代理后走真实 IP,仍命中严格桶。新增 2 个 serial 测试锁定两路行为。 --- src/api/rate_limit.rs | 71 ++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 67 insertions(+), 4 deletions(-) diff --git a/src/api/rate_limit.rs b/src/api/rate_limit.rs index d9856bf..020c57c 100644 --- a/src/api/rate_limit.rs +++ b/src/api/rate_limit.rs @@ -61,6 +61,20 @@ static COMMENT_LIMITER: LazyLock> = LazyLock::ne ) }); +#[cfg(feature = "server")] +/// 当无法识别真实客户端 IP("unknown")时使用的宽松限流桶。 +/// +/// TRUSTED_PROXY_COUNT=0(默认)时,Dioxus server function 拿不到 TCP 对端地址, +/// get_client_ip 会返回 "unknown",导致所有匿名请求共享同一个严格桶 +/// (1 req/s, burst 5),正常用户的高频请求被误杀。此桶阈值更高, +/// 通过 env RATE_LIMIT_UNKNOWN_PER_SEC / RATE_LIMIT_UNKNOWN_BURST 可调。 +static UNKNOWN_BUCKET_LIMITER: LazyLock> = LazyLock::new(|| { + RateLimiter::keyed( + Quota::per_second(env_or("RATE_LIMIT_UNKNOWN_PER_SEC", 30)) + .allow_burst(env_or("RATE_LIMIT_UNKNOWN_BURST", 100)), + ) +}); + #[cfg(feature = "server")] /// 检查评论请求是否超出限流阈值。 pub fn check_comment_limit(ip: &str) -> Result<(), String> { @@ -184,11 +198,23 @@ pub fn get_client_ip(headers: &http::HeaderMap) -> String { #[cfg(feature = "server")] /// 检查严格限流(注册、登录等敏感接口)。 +/// +/// 当 IP 为 "unknown"(无法识别真实客户端,通常是 TRUSTED_PROXY_COUNT=0 +/// 且调用方为 Dioxus server function 时)改用宽松桶,避免所有匿名请求共享 +/// 严格桶导致正常用户被误杀。生产环境配好 TRUSTED_PROXY_COUNT 后走真实 IP, +/// 始终命中严格桶。 pub fn check_strict_limit(ip: &str) -> Result<(), String> { - STRICT_LIMITER - .check_key(&ip.to_string()) - .map(|_| ()) - .map_err(|_| "请求过于频繁,请稍后再试".to_string()) + if ip == "unknown" { + UNKNOWN_BUCKET_LIMITER + .check_key(&ip.to_string()) + .map(|_| ()) + .map_err(|_| "服务繁忙,请稍后再试".to_string()) + } else { + STRICT_LIMITER + .check_key(&ip.to_string()) + .map(|_| ()) + .map_err(|_| "请求过于频繁,请稍后再试".to_string()) + } } #[cfg(feature = "server")] @@ -370,6 +396,43 @@ mod tests { } } + #[test] + #[serial_test::serial] + fn check_strict_unknown_ip_uses_lenient_bucket() { + // "unknown" 桶 burst 为 100,少量请求应全部放行,不被严格桶误杀。 + // 用 serial 隔离,因为 UNKNOWN_BUCKET_LIMITER 是全局状态。 + for _ in 0..20 { + assert!( + super::check_strict_limit("unknown").is_ok(), + "unknown bucket should allow small bursts, not hit strict 1 req/s limit" + ); + } + } + + #[test] + #[serial_test::serial] + fn check_strict_real_ip_uses_strict_bucket() { + // 真实 IP 命中严格桶(1 req/s, burst 5)。连发超过 burst 应被限流。 + // 用一个唯一的 IP 避免与其他测试状态冲突。 + let unique_ip = "198.51.100.42"; + let mut allowed = 0; + let mut blocked = false; + for _ in 0..50 { + match super::check_strict_limit(unique_ip) { + Ok(()) => allowed += 1, + Err(_) => blocked = true, + } + if blocked { + break; + } + } + assert!(blocked, "strict bucket should eventually block real IP burst"); + assert!( + allowed <= 6, + "strict burst is 5, allowed should be <= 6, got {allowed}" + ); + } + // 测试辅助函数:绕过环境变量读取,直接指定 trusted_proxy_count。 fn get_client_ip_with_trusted_and_peer( headers: &HeaderMap,