package middleware import ( "fmt" "testing" "time" ) // jiu 反馈波安全 review item 2:攻击者可用无限伪造 IP(哪怕已按 SetTrustedProxies(nil) // 锁死了 XFF 伪造这条路,真实的分布式来源/僵尸网络仍能刷出海量不同 socket 源 IP)撑爆 // buckets map。这条白盒测试直接灌入超过 maxBuckets 的不同 IP,断言 map 有硬上限、 // 不会无限增长,且超限后的新 IP 被安全拒绝(而不是继续建桶或 panic)。 func TestIPRateLimiterBucketMapBounded(t *testing.T) { rl := NewIPRateLimiter(1) for i := 0; i < maxBuckets; i++ { rl.allow(fmt.Sprintf("spoofed-ip-%d", i)) } rl.mu.Lock() sizeAtCap := len(rl.buckets) rl.mu.Unlock() if sizeAtCap != maxBuckets { t.Fatalf("灌满硬上限后 map size 应等于 maxBuckets(%d), got %d", maxBuckets, sizeAtCap) } // 硬上限已满、且都是刚创建的活跃桶(sweep 回收不了任何一个):新 IP 应被拒绝 // (安全侧,宁可误限也不无界建桶),不应放行/建桶。 for i := 0; i < 100; i++ { if rl.allow(fmt.Sprintf("overflow-ip-%d", i)) { t.Fatalf("硬上限已满时,超限的新 IP(overflow-ip-%d)应被拒绝而非放行", i) } } rl.mu.Lock() sizeAfterOverflow := len(rl.buckets) rl.mu.Unlock() if sizeAfterOverflow > maxBuckets { t.Fatalf("超限请求后 buckets map 不应继续增长: size=%d max=%d", sizeAfterOverflow, maxBuckets) } } // TestIPRateLimiterSweepLocked_EvictsStaleKeepsFresh 白盒直测 sweepLocked 本身的 // 回收语义(补 TestIPRateLimiterBucketMapBounded 没覆盖到的部分——那条只验证 // map 有硬上限,没验证"过期 bucket 真的会被回收"这件事):>10min 无请求的桶应被 // 回收,近期活跃的桶应保留。updated 字段用白盒测试直接拨表,不必真等 10 分钟。 func TestIPRateLimiterSweepLocked_EvictsStaleKeepsFresh(t *testing.T) { rl := NewIPRateLimiter(1) now := time.Now() rl.allow("stale-ip") rl.allow("fresh-ip") rl.mu.Lock() stale := rl.buckets["stale-ip"] fresh := rl.buckets["fresh-ip"] rl.mu.Unlock() stale.mu.Lock() stale.updated = now.Add(-11 * time.Minute) // 超过 10min 阈值 stale.mu.Unlock() fresh.mu.Lock() fresh.updated = now.Add(-1 * time.Minute) // 未超阈值 fresh.mu.Unlock() rl.mu.Lock() rl.sweepLocked(now) _, staleStillThere := rl.buckets["stale-ip"] _, freshStillThere := rl.buckets["fresh-ip"] rl.mu.Unlock() if staleStillThere { t.Fatalf("超过 10min 不活跃的桶应被 sweepLocked 回收,仍在 map 中") } if !freshStillThere { t.Fatalf("近期活跃(<10min)的桶不应被误回收") } } // TestIPRateLimiterOverflowRecoversViaStaleSweep 端到端验证:硬上限已满时,一个 // 早已沉寂(>10min 不活跃)的桶会在下一次 allow() 触发的 sweep 里被回收腾出位置, // 新 IP 能正常建桶并放行——而不是被永久拒绝。这是 maxBuckets backstop 之外真实 // 存在的自愈路径(allow() 里 len(buckets)>=maxBuckets 分支先 sweepLocked 再判定, // 见 ratelimit.go)。 func TestIPRateLimiterOverflowRecoversViaStaleSweep(t *testing.T) { rl := NewIPRateLimiter(1) for i := 0; i < maxBuckets; i++ { rl.allow(fmt.Sprintf("filler-ip-%d", i)) } // 把其中一个桶拨成 11 分钟前不活跃,模拟"曾经的来源已沉寂"。 rl.mu.Lock() b := rl.buckets["filler-ip-0"] rl.mu.Unlock() b.mu.Lock() b.updated = time.Now().Add(-11 * time.Minute) b.mu.Unlock() // map 已满,新 IP 进来触发 sweep:应回收 filler-ip-0 的位置,新 IP 首次请求 // (满 burst)应放行,而不是被当成"硬上限已满"直接拒绝。 if !rl.allow("new-ip-after-sweep") { t.Fatalf("stale 桶应被 sweep 回收腾出位置,新 IP 首次请求应放行,却被拒绝") } rl.mu.Lock() _, staleStillThere := rl.buckets["filler-ip-0"] _, newThere := rl.buckets["new-ip-after-sweep"] size := len(rl.buckets) rl.mu.Unlock() if staleStillThere { t.Fatalf("stale 桶 filler-ip-0 应已被回收") } if !newThere { t.Fatalf("新 IP 应已建桶") } if size > maxBuckets { t.Fatalf("回收后总数不应超过硬上限: size=%d max=%d", size, maxBuckets) } }