gcli2api / internal /server /wafip.go
a3216's picture
chore: 同步到上游 1.12.0-panel + 凭证同步/独立启动器/保活
6d60378 verified
Raw History Blame Contribute Delete
3.97 kB
// wafip.go WAF IP 级拦截 fail-fast 状态机(任务书 waf-ip-failfast)。
//
// 背景(fork-scan-absorb T-1 / BulidH 实测):WAF 403 拦的是网关出口 IP 而非
// 账号——3 个账号 1 秒内全 403。既有 ErrWafBlock 账号级软冷却(wafCooldownBase
// 起的有界退避)在 IP 级拦截时不够:轮转会把一次客户端请求放大 MaxRotate 倍,
// 同一出口 IP 继续打上游只会加重风控。
//
// 判定:ErrWafBlock 基础上的短窗多号计数——wafIPWindow(60s 滑动窗)内
// ≥ wafIPThreshold 个**不同** UID 接连命中 WAF 403 → 判定 IP 级拦截,激活至
// now+wafIPWindow。单号反复 403(账号级偶发)永不触发:只数不同号。
// 激活期内新命中不续期(保守:不做主动探测,窗口自然解除)。
//
// 归属层评估:放 server(Handler 局部)而非 pool——IP 级状态唯一消费者是
// chatCompletions 轮转循环(是否继续轮转),pool 是账号级记账层,跨 UID 语义
// 不属于任何账号;server 已有进程级状态先例 degradeGate(mu+until 同风格)。
// 账号级软冷却照常记账(applyErrorPolicy 不变),IP 级状态只改变「是否继续
// 轮转」——协同不叠加。进程内状态、重启清零(窗口 60s,重建成本极低)。
package server
import (
"log"
"sync"
"time"
)
// wafIPWindow IP 级判定滑动窗 + 激活时长:窗内不同账号命中 WAF 403 达阈值即
// 判 IP 级拦截,激活同样长(到期自然解除)。var 仅供测试注入短窗(生产恒 60s,
// 任务书「如 60s 滑动窗」口径)。
var wafIPWindow = 60 * time.Second
// wafIPThreshold 判定阈值:窗内不同 UID 数达到该值激活。取 2——「多号」的最小
// 定义:单号反复 403 永不触发(账号级偶发归软冷却管),两个不同号在 60s 内接连
// 被拦(同一出口 IP)已是 IP 级证据(BulidH 实测 3 号 1s 全拦,阈值 2 更早止损,
// 少放大一次轮转)。
const wafIPThreshold = 2
// wafIPGate WAF IP 级拦截状态机(Handler 内嵌,零值可用)。
type wafIPGate struct {
mu sync.Mutex
hits map[string]time.Time // uid → 最近一次 WAF 403 时刻(判定窗内,惰性剪枝)
until time.Time // IP 级拦截激活截止;零值 = 未激活
}
// noteWaf 记一次某账号的 WAF 403,返回记账后 IP 级拦截是否激活(调用方据此
// fail-fast 终止轮转,优先于 rotateBackoff 退避)。
// - 已激活(now < until):不续期、不记账(窗口期内不重置——保守自然解除)→ true;
// - 未激活:记 hits[uid]=now(同号重复命中覆盖不累计,判定口径是「不同号数」),
// 剪掉窗外的过期命中;不同 UID 数达 wafIPThreshold → 激活到 now+wafIPWindow
// (打一条 WARN 供观测),清空判定窗(解除后需全新命中重新判定,不叠旧账)。
func (g *wafIPGate) noteWaf(uid string) bool {
now := time.Now()
g.mu.Lock()
defer g.mu.Unlock()
if now.Before(g.until) {
return true // 激活期内新命中:不续期(自然解除语义,任务书第 3 条)
}
if g.hits == nil {
g.hits = map[string]time.Time{}
}
g.hits[uid] = now
for u, t := range g.hits {
if now.Sub(t) > wafIPWindow {
delete(g.hits, u)
}
}
if len(g.hits) >= wafIPThreshold {
g.until = now.Add(wafIPWindow)
log.Printf("WARN: [server] waf ip-level block: %d accounts hit waf 403 within %s, rotate fail-fast until %s", len(g.hits), wafIPWindow, g.until.Format(time.RFC3339))
g.hits = map[string]time.Time{}
return true
}
return false
}
// active 报告 IP 级拦截是否激活(末端错误文案区分 IP 级/账号级措辞用)。
func (g *wafIPGate) active() bool {
g.mu.Lock()
defer g.mu.Unlock()
return time.Now().Before(g.until)
}