Skip to main content

yggdrasil/api/
rate_limit.rs

1//! 基于 governor 的多级限流。
2//!
3//! 提供 strict、upload、image、comment、comment_upload(双层)、code_exec(双层)
4//! 等限流器,支持从 `X-Forwarded-For` / `X-Real-IP` 中提取客户端 IP,
5//! 并可通过 `TRUSTED_PROXY_COUNT` 配置 XFF 中真实客户端 IP 右侧的可信段数。
6//!
7//! 当未配置可信代理时,Axum handler 可回退到 TCP 连接的对端地址;
8//! Leptos server function 无法获取对端地址,会退回到 `"unknown"` key,
9//! 此时所有请求共享一个限流桶。生产环境应在反向代理后部署并正确配置
10//! `TRUSTED_PROXY_COUNT`。
11//!
12//! 仅在 `feature = "server"` 时生效。
13
14#[cfg(feature = "server")]
15use axum::http::StatusCode;
16#[cfg(feature = "server")]
17use axum::response::{IntoResponse, Response};
18#[cfg(feature = "server")]
19use governor::{clock::Clock, DefaultKeyedRateLimiter, Quota, RateLimiter};
20#[cfg(feature = "server")]
21use std::num::NonZeroU32;
22#[cfg(feature = "server")]
23use std::sync::LazyLock;
24#[cfg(feature = "server")]
25use std::sync::Once;
26#[cfg(feature = "server")]
27use std::time::Duration;
28
29#[cfg(feature = "server")]
30pub(crate) fn nz(val: u32) -> NonZeroU32 {
31    // val.max(1) 保证 ≥ 1,NonZeroU32::new 必然 Some;expect 说明该不变量。
32    NonZeroU32::new(val.max(1)).expect("val.max(1) 保证非零,NonZeroU32::new 不可能失败")
33}
34
35#[cfg(feature = "server")]
36static STRICT_LIMITER: LazyLock<DefaultKeyedRateLimiter<String>> = LazyLock::new(|| {
37    let c = crate::config::rate_limit();
38    RateLimiter::keyed(Quota::per_second(nz(c.strict_per_sec)).allow_burst(nz(c.strict_burst)))
39});
40
41#[cfg(feature = "server")]
42static UPLOAD_LIMITER: LazyLock<DefaultKeyedRateLimiter<String>> = LazyLock::new(|| {
43    let c = crate::config::rate_limit();
44    RateLimiter::keyed(Quota::per_second(nz(c.upload_per_sec)).allow_burst(nz(c.upload_burst)))
45});
46
47#[cfg(feature = "server")]
48static IMAGE_LIMITER: LazyLock<DefaultKeyedRateLimiter<String>> = LazyLock::new(|| {
49    let c = crate::config::rate_limit();
50    RateLimiter::keyed(Quota::per_second(nz(c.image_per_sec)).allow_burst(nz(c.image_burst)))
51});
52
53#[cfg(feature = "server")]
54static COMMENT_LIMITER: LazyLock<DefaultKeyedRateLimiter<String>> = LazyLock::new(|| {
55    let c = crate::config::rate_limit();
56    RateLimiter::keyed(Quota::per_second(nz(c.comment_per_sec)).allow_burst(nz(c.comment_burst)))
57});
58
59#[cfg(feature = "server")]
60/// 评论图片上传单 IP 每秒限流(默认 1 req/s,突发 5)。
61/// 评论区允许匿名上传图片,与 admin 的 UPLOAD_LIMITER 隔离,避免互占额度。
62static COMMENT_UPLOAD_LIMITER: LazyLock<DefaultKeyedRateLimiter<String>> = LazyLock::new(|| {
63    let c = crate::config::rate_limit();
64    RateLimiter::keyed(
65        Quota::per_second(nz(c.comment_upload_per_sec)).allow_burst(nz(c.comment_upload_burst)),
66    )
67});
68
69#[cfg(feature = "server")]
70/// 评论图片上传单 IP 每日限额(默认 30 次/天)。
71/// 匿名可传图意味着任何人都能消耗磁盘,需要硬性日上限(模拟方式同
72/// CODE_EXEC_DAILY_LIMITER:24h 补充 1 token、突发上限即日额度)。
73static COMMENT_UPLOAD_DAILY_LIMITER: LazyLock<DefaultKeyedRateLimiter<String>> =
74    LazyLock::new(|| {
75        let c = crate::config::rate_limit();
76        RateLimiter::keyed(
77            Quota::with_period(Duration::from_secs(86_400))
78                .expect("with_period 仅在 Duration 为 0 时返回 None;86_400s 必然 Some")
79                .allow_burst(nz(c.comment_upload_daily)),
80        )
81    });
82
83#[cfg(feature = "server")]
84/// 代码执行单 IP 每秒限流(默认 1 req/s,突发 3)。
85/// 防止单个客户端高频提交容器任务,与下方日限额共同构成双层速率限制。
86static CODE_EXEC_LIMITER: LazyLock<DefaultKeyedRateLimiter<String>> = LazyLock::new(|| {
87    let c = crate::config::rate_limit();
88    RateLimiter::keyed(
89        Quota::per_second(nz(c.code_exec_per_sec)).allow_burst(nz(c.code_exec_burst)),
90    )
91});
92
93#[cfg(feature = "server")]
94/// 代码执行单 IP 每日限额(默认 50 次/天)。
95/// 容器执行成本高(CPU/内存/启动延迟),需要硬性日上限防止资源耗尽。
96///
97/// governor 0.8 无 `Quota::per_day`,用 `with_period(24h)` + `allow_burst(daily)`
98/// 模拟:每 24h 补充 1 token、突发上限即日额度,等效于「每日最多 daily 次」。
99static CODE_EXEC_DAILY_LIMITER: LazyLock<DefaultKeyedRateLimiter<String>> = LazyLock::new(|| {
100    let c = crate::config::rate_limit();
101    RateLimiter::keyed(
102        Quota::with_period(Duration::from_secs(86_400))
103            .expect("with_period 仅在 Duration 为 0 时返回 None;86_400s 必然 Some")
104            .allow_burst(nz(c.code_exec_daily)),
105    )
106});
107
108#[cfg(feature = "server")]
109/// 搜索建议(⌘K 面板逐字查询)每 IP 每秒 10 次、突发 30 次:比 strict 宽松,
110/// 正常打字不会被拦,批量抓取仍有上限。额度固定,不随设置表变化。
111static SUGGEST_LIMITER: LazyLock<DefaultKeyedRateLimiter<String>> =
112    LazyLock::new(|| RateLimiter::keyed(Quota::per_second(nz(10)).allow_burst(nz(30))));
113
114#[cfg(feature = "server")]
115/// MCP bearer 解析前的每 IP 限流:解析要查库,伪造令牌洪泛先在这里拦下。每 IP 每秒 30 次、
116/// 突发 100 次,高于单令牌配额(10/30),同一出口 IP 的多个 agent 不受影响。额度固定。
117static MCP_PREAUTH_LIMITER: LazyLock<DefaultKeyedRateLimiter<String>> =
118    LazyLock::new(|| RateLimiter::keyed(Quota::per_second(nz(30)).allow_burst(nz(100))));
119
120#[cfg(feature = "server")]
121/// 当无法识别真实客户端 IP("unknown")时使用的宽松限流桶。
122///
123/// TRUSTED_PROXY_COUNT=0(默认)时,Leptos server function 拿不到 TCP 对端地址,
124/// get_client_ip 会返回 "unknown",导致所有匿名请求共享同一个严格桶
125/// (1 req/s, burst 5),正常用户的高频请求被误杀。此桶阈值更高,
126/// 可通过 settings 表的 ratelimit_unknown_* 配置项调整(重启生效)。
127static UNKNOWN_BUCKET_LIMITER: LazyLock<DefaultKeyedRateLimiter<String>> = LazyLock::new(|| {
128    let c = crate::config::rate_limit();
129    RateLimiter::keyed(Quota::per_second(nz(c.unknown_per_sec)).allow_burst(nz(c.unknown_burst)))
130});
131
132/// 限流桶 GC 间隔(秒):周期性调用 governor 的 `retain_recent`,回收已恢复为
133/// 「初始态」的 IP 键,防止 IP 轮换攻击下键空间无限膨胀。
134///
135/// 默认 300 秒。governor 的 `retain_recent` 只丢弃与「新桶」不可区分的键(即限流
136/// 窗口早已冷却、保留与否都不影响后续请求),因此即便间隔较长,内存占用也只反映
137/// 「最近活跃过且仍在限流窗口内」的 IP 集合,而非历史全集。由 settings 表的
138/// ratelimit_gc_interval_secs 配置项控制(重启生效),值越小越激进,回收越勤。
139#[cfg(feature = "server")]
140fn limiter_gc_interval() -> Duration {
141    let secs = crate::config::rate_limit().gc_interval_secs;
142    Duration::from_secs(u64::from(secs.max(1)))
143}
144
145/// 启动后台限流桶 GC 任务(全进程仅生效一次)。
146///
147/// 十一个 IP 键控限流器均为独立的 `DefaultKeyedRateLimiter`,没有集中状态表,故采用
148/// 惰性启动:首个请求到达任一 `check_*` 时,经 `Once` 派生一个常驻 tokio 任务,按
149/// [`limiter_gc_interval`] 周期对全部限流器调用 `retain_recent`,回收长时间未命中的
150/// 键。这直接缓解 IP 轮换攻击下的内存膨胀——攻击者不断换 IP 制造新键,GC 周期性剔除
151/// 已冷却的旧键,使键集合大小收敛到「限流窗口内的活跃 IP」。
152#[cfg(feature = "server")]
153fn ensure_limiter_gc() {
154    static SPAWNED: std::sync::Once = std::sync::Once::new();
155    SPAWNED.call_once(|| {
156        // 仅在 tokio 运行时上下文中派生 GC 任务。测试(非 #[tokio::test])无运行时,
157        // tokio::spawn 会 panic 并毒化 Once,使后续调用全部 panic。try_current 在无
158        // 运行时时返回 Err,静默跳过——测试不依赖 GC,生产必有运行时。
159        if tokio::runtime::Handle::try_current().is_err() {
160            return;
161        }
162        let interval = limiter_gc_interval();
163        crate::tasks::spawn_supervised("rate_limit_gc", async move {
164            loop {
165                tokio::time::sleep(interval).await;
166                STRICT_LIMITER.retain_recent();
167                UPLOAD_LIMITER.retain_recent();
168                IMAGE_LIMITER.retain_recent();
169                COMMENT_LIMITER.retain_recent();
170                COMMENT_UPLOAD_LIMITER.retain_recent();
171                COMMENT_UPLOAD_DAILY_LIMITER.retain_recent();
172                CODE_EXEC_LIMITER.retain_recent();
173                CODE_EXEC_DAILY_LIMITER.retain_recent();
174                UNKNOWN_BUCKET_LIMITER.retain_recent();
175                SUGGEST_LIMITER.retain_recent();
176                MCP_PREAUTH_LIMITER.retain_recent();
177            }
178        });
179    });
180}
181
182#[cfg(feature = "server")]
183/// 检查评论请求是否超出限流阈值。
184pub fn check_comment_limit(ip: &str) -> Result<(), String> {
185    ensure_limiter_gc();
186    COMMENT_LIMITER
187        .check_key(&ip.to_string())
188        .map(|_| ())
189        .map_err(|_| "评论过于频繁,请稍后再试".to_string())
190}
191
192#[cfg(feature = "server")]
193/// 检查评论图片上传的双层速率限制(每秒突发 + 每日总额)。
194///
195/// 两层任一被限即拒绝。评论区允许匿名上传,日限额是防磁盘耗尽的关键防线。
196pub fn check_comment_upload_limit(ip: &str) -> Result<(), String> {
197    ensure_limiter_gc();
198    COMMENT_UPLOAD_LIMITER
199        .check_key(&ip.to_string())
200        .map_err(|_| "上传过于频繁,请稍后再试".to_string())?;
201    COMMENT_UPLOAD_DAILY_LIMITER
202        .check_key(&ip.to_string())
203        .map_err(|_| "今日上传次数已达上限,请明天再试".to_string())?;
204    Ok(())
205}
206
207#[cfg(feature = "server")]
208/// 检查图片访问请求是否超出限流阈值。
209///
210/// 超限时返回带 `Retry-After` 头的 429 响应:等待秒数取自 governor 的
211/// `NotUntil`(到下一个可用令牌的精确时长,向上取整、至少 1s),
212/// 客户端可据此安排重试而非盲等。
213pub fn check_image_limit(ip: &str) -> Result<(), Box<Response>> {
214    ensure_limiter_gc();
215    IMAGE_LIMITER
216        .check_key(&ip.to_string())
217        .map(|_| ())
218        .map_err(|not_until| {
219            let wait = not_until.wait_time_from(governor::clock::DefaultClock::default().now());
220            let secs = (wait.as_millis() as u64).div_ceil(1000).max(1);
221            Box::new(
222                (
223                    StatusCode::TOO_MANY_REQUESTS,
224                    [(axum::http::header::RETRY_AFTER, secs.to_string())],
225                )
226                    .into_response(),
227            )
228        })
229}
230
231#[cfg(feature = "server")]
232async fn trusted_proxy_count() -> usize {
233    crate::api::settings::runtime_security_settings()
234        .await
235        .trusted_proxy_count as usize
236}
237
238#[cfg(feature = "server")]
239fn is_valid_ip(ip: &str) -> bool {
240    ip.parse::<std::net::IpAddr>().is_ok()
241}
242
243/// 从 `X-Forwarded-For` 头按可信段数提取真实客户端 IP。
244///
245/// # 伪造风险(务必正确配置 `TRUSTED_PROXY_COUNT`)
246///
247/// XFF 头由客户端**可写**,本函数以 `parts[len-1-trusted_proxy_count]` 选取
248/// 从右数第 `trusted_proxy_count + 1` 段。每层代理追加的是自己的对端地址,最外层
249/// 代理追加的就是客户端本身,所以 `TRUSTED_PROXY_COUNT` 应等于 XFF 中**真实客户端
250/// IP 右侧的段数**,而不是代理层数(CDN + 源站反代两层都追加时取 1)。一旦不符即可被滥用:
251/// - **配得偏大**:会选中客户端伪造的地址——攻击者在请求里塞入多个伪 XFF 段,
252///   使选中的「客户端 IP」落为其伪造值,从而绕过按 IP 的限流(每次伪造一个新 IP),
253///   或令多个真实用户被错误归并到同一代理 IP 的桶里。
254/// - **配得偏小**:会选中某一跳中间代理的 IP,导致所有用户共享同一个限流桶。
255///
256/// 段数不足 `trusted_proxy_count + 1` 时返回 `None`,由调用方回退 `X-Real-IP`。
257/// 因此只有单层反代时,该代理须用 `$remote_addr` 覆盖 XFF(而非追加)并设置
258/// `X-Real-IP`,再配 1;追加模式下 1 会选中客户端伪造的段。源站还必须只接受上游
259/// 代理回源,否则直连源站同样可伪造。
260#[cfg(feature = "server")]
261fn ip_from_x_forwarded_for(value: &str, trusted_proxy_count: usize) -> Option<String> {
262    // X-Forwarded-For 格式:client, proxy1, proxy2, ..., proxyN
263    // 越靠右的地址离服务端越近。
264    let parts: Vec<&str> = value
265        .split(',')
266        .map(str::trim)
267        .filter(|s| !s.is_empty())
268        .collect();
269
270    if trusted_proxy_count == 0 || parts.len() <= trusted_proxy_count {
271        return None;
272    }
273
274    // 真实客户端 IP 位于右侧第 trusted_proxy_count + 1 个。
275    let idx = parts.len() - 1 - trusted_proxy_count;
276    let ip = parts[idx].to_string();
277    if is_valid_ip(&ip) {
278        Some(ip)
279    } else {
280        None
281    }
282}
283
284#[cfg(feature = "server")]
285fn ip_from_x_real_ip(value: &str) -> Option<String> {
286    let ip = value.trim().to_string();
287    if is_valid_ip(&ip) {
288        Some(ip)
289    } else {
290        None
291    }
292}
293
294#[cfg(feature = "server")]
295fn get_client_ip_internal(
296    headers: &http::HeaderMap,
297    trusted: usize,
298    peer: Option<std::net::SocketAddr>,
299) -> String {
300    if trusted > 0 {
301        if let Some(value) = headers.get("x-forwarded-for").and_then(|v| v.to_str().ok()) {
302            if let Some(ip) = ip_from_x_forwarded_for(value, trusted) {
303                return ip;
304            }
305        }
306
307        if let Some(ip) = headers
308            .get("x-real-ip")
309            .and_then(|v| v.to_str().ok())
310            .and_then(ip_from_x_real_ip)
311        {
312            return ip;
313        }
314    }
315
316    if let Some(addr) = peer {
317        return addr.ip().to_string();
318    }
319
320    // 落到 unknown 是预期的常见路径(TRUSTED_PROXY_COUNT=0 + server function 无 TCP
321    // 对端地址),每个请求都打 WARN 会淹没日志,故进程级仅警告一次。
322    static UNKNOWN_IP_WARNED: Once = Once::new();
323    UNKNOWN_IP_WARNED.call_once(|| {
324        tracing::warn!(
325            "无法获取客户端真实 IP(TRUSTED_PROXY_COUNT=0 不解析代理头,\
326             且当前上下文无 TCP 对端地址,通常为 server function),限流将按 'unknown' 键聚合"
327        );
328    });
329    "unknown".to_string()
330}
331
332#[cfg(feature = "server")]
333/// 根据可信代理段数从请求头中提取客户端 IP,并校验 IP 合法性。
334///
335/// 当未配置可信代理时,不会信任任何 `X-Forwarded-For` / `X-Real-IP` 头,
336/// 而是直接返回 `peer` 中的 TCP 对端地址(如果提供)。
337pub async fn get_client_ip_with_peer(
338    headers: &http::HeaderMap,
339    peer: Option<std::net::SocketAddr>,
340) -> String {
341    get_client_ip_internal(headers, trusted_proxy_count().await, peer)
342}
343#[cfg(feature = "server")]
344/// 使用「站点配置 → 安全」面板的可信代理段数提取客户端 IP。
345///
346/// 适用于 Leptos server function 等无法获取 `ConnectInfo` 的场景。
347/// 生产环境建议配合反向代理与设置面板的 TRUSTED_PROXY_COUNT 使用。
348pub async fn get_client_ip(headers: &http::HeaderMap) -> String {
349    get_client_ip_internal(headers, trusted_proxy_count().await, None)
350}
351
352#[cfg(feature = "server")]
353/// 检查严格限流(注册、登录等敏感接口)。
354///
355/// 当 IP 为 "unknown"(无法识别真实客户端,通常是 TRUSTED_PROXY_COUNT=0
356/// 且调用方为 Leptos server function 时)改用宽松桶,避免所有匿名请求共享
357/// 严格桶导致正常用户被误杀。生产环境配好 TRUSTED_PROXY_COUNT 后走真实 IP,
358/// 始终命中严格桶。
359pub fn check_strict_limit(ip: &str) -> Result<(), String> {
360    ensure_limiter_gc();
361    if ip == "unknown" {
362        UNKNOWN_BUCKET_LIMITER
363            .check_key(&ip.to_string())
364            .map(|_| ())
365            .map_err(|_| "服务繁忙,请稍后再试".to_string())
366    } else {
367        STRICT_LIMITER
368            .check_key(&ip.to_string())
369            .map(|_| ())
370            .map_err(|_| "请求过于频繁,请稍后再试".to_string())
371    }
372}
373
374#[cfg(feature = "server")]
375/// 检查搜索建议请求是否超出限流阈值;无法识别 IP 时与 strict 一样落到 unknown 宽松桶。
376pub fn check_suggest_limit(ip: &str) -> Result<(), String> {
377    ensure_limiter_gc();
378    let limiter = if ip == "unknown" {
379        &*UNKNOWN_BUCKET_LIMITER
380    } else {
381        &*SUGGEST_LIMITER
382    };
383    limiter
384        .check_key(&ip.to_string())
385        .map(|_| ())
386        .map_err(|_| "搜索过于频繁,请稍后再试".to_string())
387}
388
389#[cfg(feature = "server")]
390/// MCP 令牌解析前的每 IP 限流;"unknown" 作为普通键,不与 strict/unknown 桶共享额度。
391pub fn check_mcp_preauth_limit(ip: &str) -> Result<(), &'static str> {
392    ensure_limiter_gc();
393    MCP_PREAUTH_LIMITER
394        .check_key(&ip.to_string())
395        .map(|_| ())
396        .map_err(|_| "请求过于频繁,请稍后再试")
397}
398
399#[cfg(feature = "server")]
400/// 检查上传请求是否超出限流阈值。
401pub fn check_upload_limit(ip: &str) -> Result<(), String> {
402    ensure_limiter_gc();
403    UPLOAD_LIMITER
404        .check_key(&ip.to_string())
405        .map(|_| ())
406        .map_err(|_| "上传过于频繁,请稍后再试".to_string())
407}
408
409#[cfg(feature = "server")]
410/// 检查代码执行请求的双层速率限制(每秒突发 + 每日总额)。
411///
412/// 两层任一被限即拒绝。返回中文错误消息,供 server function 直接透传给前端。
413pub fn check_code_exec_limit(ip: &str) -> Result<(), String> {
414    ensure_limiter_gc();
415    CODE_EXEC_LIMITER
416        .check_key(&ip.to_string())
417        .map_err(|_| "请求过于频繁,请稍后再试".to_string())?;
418    CODE_EXEC_DAILY_LIMITER
419        .check_key(&ip.to_string())
420        .map_err(|_| "今日运行次数已达上限,请明天再试".to_string())?;
421    Ok(())
422}
423
424#[cfg(all(test, feature = "server"))]
425mod tests {
426    use super::*;
427    use http::HeaderMap;
428    use std::net::{IpAddr, Ipv4Addr, SocketAddr};
429
430    #[test]
431    fn get_client_ip_from_x_forwarded_for_with_one_trusted_proxy() {
432        let mut headers = HeaderMap::new();
433        headers.insert("x-forwarded-for", "1.2.3.4, 5.6.7.8".parse().unwrap());
434        assert_eq!(
435            get_client_ip_with_trusted_and_peer(&headers, 1, None),
436            "1.2.3.4"
437        );
438    }
439
440    #[test]
441    fn get_client_ip_from_x_forwarded_for_with_two_trusted_proxies() {
442        let mut headers = HeaderMap::new();
443        headers.insert(
444            "x-forwarded-for",
445            "1.2.3.4, 5.6.7.8, 9.10.11.12".parse().unwrap(),
446        );
447        assert_eq!(
448            get_client_ip_with_trusted_and_peer(&headers, 2, None),
449            "1.2.3.4"
450        );
451    }
452
453    #[test]
454    fn get_client_ip_ignores_x_forwarded_for_when_no_trusted_proxies() {
455        let mut headers = HeaderMap::new();
456        headers.insert("x-forwarded-for", "1.2.3.4, 5.6.7.8".parse().unwrap());
457        assert_eq!(
458            get_client_ip_with_trusted_and_peer(&headers, 0, None),
459            "unknown"
460        );
461    }
462
463    #[test]
464    fn get_client_ip_falls_back_to_peer_when_no_trusted_proxies() {
465        let mut headers = HeaderMap::new();
466        headers.insert("x-forwarded-for", "1.2.3.4, 5.6.7.8".parse().unwrap());
467        let peer = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 12345);
468        assert_eq!(
469            get_client_ip_with_trusted_and_peer(&headers, 0, Some(peer)),
470            "127.0.0.1"
471        );
472    }
473
474    #[test]
475    fn get_client_ip_from_x_real_ip_when_trusted() {
476        let mut headers = HeaderMap::new();
477        headers.insert("x-real-ip", "9.8.7.6".parse().unwrap());
478        assert_eq!(
479            get_client_ip_with_trusted_and_peer(&headers, 1, None),
480            "9.8.7.6"
481        );
482    }
483
484    #[test]
485    fn get_client_ip_x_real_ip_ignored_when_not_trusted() {
486        let mut headers = HeaderMap::new();
487        headers.insert("x-real-ip", "9.8.7.6".parse().unwrap());
488        let peer = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1)), 12345);
489        assert_eq!(
490            get_client_ip_with_trusted_and_peer(&headers, 0, Some(peer)),
491            "192.168.1.1"
492        );
493    }
494
495    #[test]
496    fn get_client_ip_x_forwarded_for_takes_priority_over_x_real_ip() {
497        let mut headers = HeaderMap::new();
498        headers.insert("x-forwarded-for", "1.1.1.1, 2.2.2.2".parse().unwrap());
499        headers.insert("x-real-ip", "3.3.3.3".parse().unwrap());
500        assert_eq!(
501            get_client_ip_with_trusted_and_peer(&headers, 1, None),
502            "1.1.1.1"
503        );
504    }
505
506    #[test]
507    fn get_client_ip_no_headers_returns_unknown() {
508        let headers = HeaderMap::new();
509        assert_eq!(
510            get_client_ip_with_trusted_and_peer(&headers, 1, None),
511            "unknown"
512        );
513    }
514
515    #[test]
516    fn get_client_ip_ignores_short_x_forwarded_for_list() {
517        let mut headers = HeaderMap::new();
518        headers.insert("x-forwarded-for", "1.2.3.4".parse().unwrap());
519        assert_eq!(
520            get_client_ip_with_trusted_and_peer(&headers, 2, None),
521            "unknown"
522        );
523    }
524
525    #[test]
526    fn get_client_ip_ignores_x_forwarded_for_equal_to_proxy_count() {
527        let mut headers = HeaderMap::new();
528        headers.insert("x-forwarded-for", "1.2.3.4, 5.6.7.8".parse().unwrap());
529        assert_eq!(
530            get_client_ip_with_trusted_and_peer(&headers, 2, None),
531            "unknown"
532        );
533    }
534
535    #[test]
536    fn get_client_ip_ignores_empty_x_forwarded_for_entries() {
537        let mut headers = HeaderMap::new();
538        headers.insert(
539            "x-forwarded-for",
540            " , 1.2.3.4 , 5.6.7.8 , ".parse().unwrap(),
541        );
542        assert_eq!(
543            get_client_ip_with_trusted_and_peer(&headers, 1, None),
544            "1.2.3.4"
545        );
546    }
547
548    #[test]
549    fn get_client_ip_rejects_invalid_x_forwarded_for_value() {
550        let mut headers = HeaderMap::new();
551        headers.insert("x-forwarded-for", "not-an-ip, 5.6.7.8".parse().unwrap());
552        assert_eq!(
553            get_client_ip_with_trusted_and_peer(&headers, 1, None),
554            "unknown"
555        );
556    }
557
558    #[test]
559    fn get_client_ip_rejects_invalid_x_real_ip_value() {
560        let mut headers = HeaderMap::new();
561        headers.insert("x-real-ip", "not-an-ip".parse().unwrap());
562        assert_eq!(
563            get_client_ip_with_trusted_and_peer(&headers, 1, None),
564            "unknown"
565        );
566    }
567
568    #[test]
569    fn get_client_ip_prefers_xff_over_peer() {
570        let mut headers = HeaderMap::new();
571        headers.insert("x-forwarded-for", "1.2.3.4, 5.6.7.8".parse().unwrap());
572        let peer = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 12345);
573        assert_eq!(
574            get_client_ip_with_trusted_and_peer(&headers, 1, Some(peer)),
575            "1.2.3.4"
576        );
577    }
578
579    #[tokio::test]
580    async fn get_client_ip_defaults_to_unknown_without_db() {
581        // 无 DB 连接的单元测试环境,trusted_proxy_count 回退默认 0,
582        // 不信任任何 XFF 头,返回 "unknown"。
583        let mut headers = HeaderMap::new();
584        headers.insert("x-forwarded-for", "1.2.3.4, 5.6.7.8".parse().unwrap());
585        assert_eq!(get_client_ip(&headers).await, "unknown");
586    }
587
588    #[test]
589    #[serial_test::serial]
590    fn check_strict_unknown_ip_uses_lenient_bucket() {
591        // "unknown" 桶 burst 为 100,少量请求应全部放行,不被严格桶误杀。
592        // 用 serial 隔离,因为 UNKNOWN_BUCKET_LIMITER 是全局状态。
593        for _ in 0..20 {
594            assert!(
595                super::check_strict_limit("unknown").is_ok(),
596                "unknown bucket should allow small bursts, not hit strict 1 req/s limit"
597            );
598        }
599    }
600
601    #[test]
602    #[serial_test::serial]
603    fn check_strict_real_ip_uses_strict_bucket() {
604        // 真实 IP 命中严格桶(1 req/s, burst 5)。连发超过 burst 应被限流。
605        // 用一个唯一的 IP 避免与其他测试状态冲突。
606        let unique_ip = "198.51.100.42";
607        let mut allowed = 0;
608        let mut blocked = false;
609        for _ in 0..50 {
610            match super::check_strict_limit(unique_ip) {
611                Ok(()) => allowed += 1,
612                Err(_) => blocked = true,
613            }
614            if blocked {
615                break;
616            }
617        }
618        assert!(
619            blocked,
620            "strict bucket should eventually block real IP burst"
621        );
622        assert!(
623            allowed <= 6,
624            "strict burst is 5, allowed should be <= 6, got {allowed}"
625        );
626    }
627
628    #[test]
629    #[serial_test::serial]
630    fn check_mcp_preauth_blocks_after_burst() {
631        // MCP 预鉴权桶(30 rps, burst 100):唯一 IP 连发 200 次,最终必被拒,放行数不超过突发额度。
632        let unique_ip = "198.51.100.77";
633        let mut allowed = 0;
634        let mut blocked = false;
635        for _ in 0..200 {
636            match super::check_mcp_preauth_limit(unique_ip) {
637                Ok(()) => allowed += 1,
638                Err(_) => blocked = true,
639            }
640        }
641        assert!(blocked, "MCP 预鉴权桶应在突发后限流");
642        assert!(
643            allowed <= 101,
644            "突发额度为 100,放行数应 <= 101,实际 {allowed}"
645        );
646    }
647
648    // 测试辅助函数:绕过环境变量读取,直接指定 trusted_proxy_count。
649    fn get_client_ip_with_trusted_and_peer(
650        headers: &HeaderMap,
651        trusted: usize,
652        peer: Option<SocketAddr>,
653    ) -> String {
654        get_client_ip_internal(headers, trusted, peer)
655    }
656}