package route import ( "github.com/flamego/flamego" "github.com/sirupsen/logrus" "github.com/tamakyi/TamaBox/internal/security/autoban" "github.com/tamakyi/TamaBox/internal/security/honeypot" "github.com/tamakyi/TamaBox/internal/shared" ) // autobanMiddleware 自动封禁中间件:挂载在静态黑名单之后、业务路由之前。 // - 已封禁 IP 直接 403,不再进入业务 // - 敏感路径命中 → 计入窗口统计(达到阈值触发临时封禁 + 邮件告警) // - 真正的 404 探测由 f.NotFound 处理器单独计数(见 route.go), // 不再把业务性的"302 → /"(语言切换 / 登出 / 投票不存在 / 黑名单跳转等)误判为扫描。 // - 核心检测逻辑见 internal/security/autoban(可独立测试);告警发送函数由本层注入。 func autobanMiddleware(b *autoban.Banner, hp *honeypot.Honeypot) flamego.Handler { return func(ctx flamego.Context) { // 经 shared.LimitKey 归一化后再计数与判定:IPv4 原样,IPv6 聚合到 /64。 // 否则攻击者可在单个 /64 内轮换地址,使每个键只命中一次而永不触发阈值。 // 记录与判定必须使用同一个键,否则封禁将无法命中。 ip := shared.LimitKey(shared.ResolveClientIP(ctx.Request().Request)) if ip == "" { return } path := ctx.Request().URL.Path if b.IsBanned(ip) { // Debug 级别:被封住的扫描器会持续请求,Info 会让日志彻底淹没。 // 需要现场确认「封禁真的在拦」时把日志级别调到 debug 即可。 logrus.WithFields(logrus.Fields{ "ban_key": ip, "path": path, }).Debug("拒绝已封禁来源") writeBanned(ctx.ResponseWriter(), ctx.Request().Request.Method) return } kind := autoban.KindNone // 蜜罐命中的路径由蜜罐处理器自己计数(它需要同时知道「是否触发了封禁」 // 以便落库)。若这里再记一次,同一次请求会被计两遍,阈值实际减半。 if hp == nil || !hp.Match(path) { if autoban.IsSensitivePath(path) { kind = autoban.KindSensitive } } if kind != autoban.KindNone { defer b.Record(ip, path, kind) } // 让下游业务路由先执行,敏感路径的计数延后到响应返回后记录。 ctx.Next() } }