fix(ratelimit): 强制清理按最旧活跃时间排序,避免随机删活跃用户

原版用 'for k := range rl.requests' 直接删除超限条目,Go map 遍历顺序
随机,可能命中 timestamp 仍在窗口内的活跃用户,导致其下次请求拿到新配额
(限流语义被破坏)。

同时存在 abuse vector: 攻击者用大量伪造 IP 撑爆 map 触发清理,反而
'清洗'掉自己留的活跃条目,绕过限流。

修复:
- 收集 (key, lastTs) 到 slice,按 lastTs 升序排(最旧在前)
- lastTs 取 timestamps 末尾(Allow 总 append 到末尾,即最近请求时间)
- 从前往后逐个 delete,直到 len(rl.requests) <= MaxRateLimiterEntries/2
- 阈值/触发条件/warning 日志保持不变

复杂度 O(n log n),仅在超 MaxRateLimiterEntries(10w)时触发,代价可接受。
cleanup 仍在 Allow 入口的 rl.mutex.Lock() 区域内,无需额外锁。
This commit is contained in:
sun
2026-09-21 11:14:48 +08:00
parent 19d83f7a3b
commit 6ed25e8e4b
+24 -2
View File
@@ -5,6 +5,7 @@ import (
"net"
"net/http"
"os"
"sort"
"strconv"
"strings"
"sync"
@@ -128,11 +129,32 @@ func (rl *RateLimiter) cleanup() {
if len(rl.requests) > common.MaxRateLimiterEntries {
log.Printf("警告: 限流器条目数 %d 超过上限 %d,触发强制清理", len(rl.requests), common.MaxRateLimiterEntries)
for k := range rl.requests {
// 【修复】原版用 `for k := range rl.requests` 删,Go map 遍历顺序随机,
// 会随机删掉活跃用户(其条目 timestamp 仍在窗口内),导致该用户下次请求
// 拿到新配额 — 攻击者可用大量伪造 IP 撑爆 map 触发清理,反而"清洗"
// 自己留的活跃条目,绕过限流。
// 修复:按"最近一次请求时间(lastTs)"升序排序,删最旧的(最可能已离开/低频),
// 保留最活跃用户,语义符合"限流器只淘汰冷条目"的预期。
// 排序复杂度 O(n log n),但只在超 10w 条目时触发,代价可接受。
type entry struct {
key string
lastTs time.Time
}
entries := make([]entry, 0, len(rl.requests))
for k, v := range rl.requests {
// 走到这里 v 一定非空(cleanup 第一阶段会把空 timestamps 删掉),
// 取最后一个 timestamp 作为"最近活跃时间"。
lastTs := v[len(v)-1]
entries = append(entries, entry{key: k, lastTs: lastTs})
}
sort.Slice(entries, func(i, j int) bool {
return entries[i].lastTs.Before(entries[j].lastTs)
})
for _, e := range entries {
if len(rl.requests) <= common.MaxRateLimiterEntries/2 {
break
}
delete(rl.requests, k)
delete(rl.requests, e.key)
}
}
}