Merge pull request #548 from samwafgo/feat_banIP_moreinfo

feat: add ip failure
This commit is contained in:
samwafgo
2025-11-21 17:17:58 +08:00
committed by GitHub
14 changed files with 409 additions and 57 deletions

View File

@@ -12,6 +12,7 @@ type APIGroup struct {
WafAllowUrlApi
WafLdpUrlApi
WafAntiCCApi
WafIPFailureApi
WafBlockIpApi
WafBlockUrlApi
WafAccountApi

170
api/waf_ip_failure.go Normal file
View File

@@ -0,0 +1,170 @@
package api
import (
"SamWaf/enums"
"SamWaf/global"
"SamWaf/model/common/response"
"SamWaf/model/request"
response2 "SamWaf/model/response"
"SamWaf/utils"
"SamWaf/wafipban"
"SamWaf/waftask"
"fmt"
"strings"
"github.com/gin-gonic/gin"
)
type WafIPFailureApi struct {
}
// GetConfigApi 获取IP失败封禁配置
func (w *WafIPFailureApi) GetConfigApi(c *gin.Context) {
var req request.WafIPFailureConfigReq
err := c.ShouldBind(&req)
if err == nil {
configResp := response2.IPFailureConfigResp{
Enabled: global.GCONFIG_IP_FAILURE_BAN_ENABLED,
StatusCodes: global.GCONFIG_IP_FAILURE_STATUS_CODES,
LockTime: global.GCONFIG_IP_FAILURE_BAN_LOCK_TIME,
}
response.OkWithDetailed(configResp, "获取成功", c)
} else {
response.FailWithMessage("解析失败", c)
}
}
// SetConfigApi 设置IP失败封禁配置
func (w *WafIPFailureApi) SetConfigApi(c *gin.Context) {
var req request.WafIPFailureSetConfigReq
err := c.ShouldBindJSON(&req)
if err == nil {
// 更新配置
// 1. 更新启用状态
config := wafSystemConfigService.GetDetailByItemApi(request.WafSystemConfigDetailByItemReq{Item: "ip_failure_ban_enabled"})
wafSystemConfigService.ModifyApi(request.WafSystemConfigEditReq{
Id: config.Id,
Item: config.Item,
ItemClass: config.ItemClass,
Value: fmt.Sprintf("%d", req.Enabled),
Remarks: config.Remarks,
ItemType: config.ItemType,
Options: config.Options,
})
// 2. 更新状态码
config = wafSystemConfigService.GetDetailByItemApi(request.WafSystemConfigDetailByItemReq{Item: "ip_failure_status_codes"})
wafSystemConfigService.ModifyApi(request.WafSystemConfigEditReq{
Id: config.Id,
Item: config.Item,
ItemClass: config.ItemClass,
Value: req.StatusCodes,
Remarks: config.Remarks,
ItemType: config.ItemType,
Options: config.Options,
})
// 3. 更新锁定时间
config = wafSystemConfigService.GetDetailByItemApi(request.WafSystemConfigDetailByItemReq{Item: "ip_failure_ban_lock_time"})
wafSystemConfigService.ModifyApi(request.WafSystemConfigEditReq{
Id: config.Id,
Item: config.Item,
ItemClass: config.ItemClass,
Value: fmt.Sprintf("%d", req.LockTime),
Remarks: config.Remarks,
ItemType: config.ItemType,
Options: config.Options,
})
// 重新加载配置
waftask.TaskLoadSetting(true)
response.OkWithMessage("设置成功", c)
} else {
response.FailWithMessage("解析失败", c)
}
}
// GetBanIpListApi 获取被封禁的IP列表
func (w *WafIPFailureApi) GetBanIpListApi(c *gin.Context) {
// 获取所有IP失败记录
banIpList := global.GCACHE_WAFCACHE.ListAvailableKeysWithPrefix(enums.CACHE_IP_FAILURE_PRE)
beans := make([]response2.IPFailureIpResp, 0, len(banIpList))
manager := wafipban.GetIPFailureManager()
// 遍历 banIpList将每个 IP 信息添加到 beans 中
for banKey, duration := range banIpList {
// 去掉 IP 的前缀
ip := strings.TrimPrefix(banKey, enums.CACHE_IP_FAILURE_PRE)
// 获取IP失败详细信息
record := manager.GetFailureInfo(ip)
if record == nil {
continue
}
// 只显示满足条件的IP即有阈值记录的IP表示是被规则封禁的
if record.TriggerMinutes == 0 || record.TriggerCount == 0 {
continue
}
// 将剩余时间格式化小于1分钟显示秒否则显示时和分
var remainTime string
totalMinutes := int(duration.Minutes())
if totalMinutes < 1 {
// 小于1分钟显示秒
seconds := int(duration.Seconds())
remainTime = fmt.Sprintf("%d秒", seconds)
} else {
// 大于等于1分钟显示时和分
hours := int(duration.Hours())
minutes := totalMinutes % 60
if hours > 0 {
remainTime = fmt.Sprintf("%d时%02d分", hours, minutes)
} else {
remainTime = fmt.Sprintf("%d分", minutes)
}
}
region := utils.GetCountry(ip)
// 将信息添加到 beans 中
beans = append(beans, response2.IPFailureIpResp{
IP: ip,
FailCount: record.Count,
FirstTime: record.FirstTime.Format("2006-01-02 15:04:05"),
LastTime: record.LastTime.Format("2006-01-02 15:04:05"),
RemainTime: remainTime,
Region: fmt.Sprintf("%v", region),
TriggerMinutes: record.TriggerMinutes,
TriggerCount: record.TriggerCount,
})
}
// 计算总条目数
total := len(beans)
// 返回带分页信息的响应
response.OkWithDetailed(response.PageResult{
List: beans,
Total: int64(total),
PageIndex: 1,
PageSize: 999999,
}, "获取成功", c)
}
// RemoveIPFailureBanIPApi 移除被封禁的IP
func (w *WafIPFailureApi) RemoveIPFailureBanIPApi(c *gin.Context) {
var req request.WafIPFailureRemoveBanIpReq
err := c.ShouldBindJSON(&req)
if err == nil {
// 直接清除失败记录窗口累积
manager := wafipban.GetIPFailureManager()
manager.ClearIPFailure(req.Ip)
global.GCACHE_WAFCACHE.Remove(enums.CACHE_CCVISITBAN_PRE + req.Ip)
response.OkWithMessage(req.Ip+" 移除成功", c)
} else {
response.FailWithMessage("解析失败", c)
}
}

View File

@@ -9,6 +9,6 @@ security:
soft_id: SamWafCom
user_code: 8ad2bca0-4fd0-46aa-afe6-833132a05eee
zlog:
db_debug_enable: false
db_debug_enable: true
debug_enable: false
outputformat: console

View File

@@ -55,9 +55,8 @@ var (
GCONFIG_RECORD_GPT_MODEL string = "deepseek-chat" //GPT 模型名称
// IP失败封禁相关配置
GCONFIG_IP_FAILURE_STATUS_CODES string = "401|403|404|444|429|503" //失败状态码配置,支持多个用|分隔,也支持正则表达式
GCONFIG_IP_FAILURE_BAN_ENABLED int64 = 0 //是否启用IP失败封禁 1启用 0禁用
GCONFIG_IP_FAILURE_BAN_TIME_WINDOW int64 = 5 //IP失败封禁时间窗口(分钟)默认5分钟
GCONFIG_IP_FAILURE_BAN_MAX_COUNT int64 = 10 //IP失败封禁最大失败次数 默认10次
GCONFIG_IP_FAILURE_STATUS_CODES string = "401|403|404|444|429|503" //失败状态码配置,支持多个用|分隔,也支持正则表达式
GCONFIG_IP_FAILURE_BAN_ENABLED int64 = 0 //是否启用IP失败封禁 1启用 0禁用
GCONFIG_IP_FAILURE_BAN_LOCK_TIME int64 = 10 //IP失败封禁锁定时间(分钟)默认10分钟
)

View File

@@ -123,6 +123,33 @@ func (w *WebLog) GetIPFailureCount(minutes int64) int64 {
return getIPFailureCount(w.SRC_IP, minutes)
}
// RecordIPFailureThreshold 记录IP失败封禁的阈值信息当规则匹配时调用
// minutes: 触发封禁的时间窗口(分钟)
// count: 触发封禁的失败次数阈值
func (w *WebLog) RecordIPFailureThreshold(minutes int64, count int64) {
if w.SRC_IP == "" {
return
}
// 如果是bot且危险程度是0不记录阈值
if w.IsBot == 1 && w.RISK_LEVEL == 0 {
return
}
// 如果是证书申请路径,不记录阈值
if getSSLChallengePath != nil {
sslPath := getSSLChallengePath()
if sslPath != "" && strings.HasPrefix(w.URL, sslPath) {
return
}
}
// 调用IP失败管理器记录阈值延迟导入避免编译时循环依赖
if recordIPFailureThreshold != nil {
recordIPFailureThreshold(w.SRC_IP, minutes, count)
}
}
// getSSLChallengePath 获取SSL证书验证路径延迟导入避免循环依赖
var getSSLChallengePath func() string
@@ -139,6 +166,14 @@ func SetIPFailureCountGetter(fn func(string, int64) int64) {
getIPFailureCount = fn
}
// recordIPFailureThreshold 记录IP失败封禁阈值通过函数变量实现延迟导入
var recordIPFailureThreshold func(string, int64, int64)
// SetIPFailureThresholdRecorder 设置IP失败封禁阈值记录函数
func SetIPFailureThresholdRecorder(fn func(string, int64, int64)) {
recordIPFailureThreshold = fn
}
type WAFLog struct {
REQ_UUID string `json:"req_uuid"`
ACTION string `json:"action"`

View File

@@ -212,7 +212,7 @@ func (m *wafSystenService) run() {
global.GWAF_OWASP = wafowasp.NewWafOWASP(true, utils.GetCurrentDir())
// 初始化ip ban
wafipban.InitIPBanManager()
wafipban.InitIPBanManager(global.GCACHE_WAFCACHE)
//提前初始化
global.GDATA_CURRENT_LOG_DB_MAP = map[string]*gorm.DB{}

View File

@@ -0,0 +1,17 @@
package request
// WafIPFailureConfigReq 获取IP失败封禁配置请求
type WafIPFailureConfigReq struct {
}
// WafIPFailureSetConfigReq 设置IP失败封禁配置请求
type WafIPFailureSetConfigReq struct {
Enabled int64 `json:"enabled" form:"enabled"` // 是否启用IP失败封禁 1启用 0禁用
StatusCodes string `json:"status_codes" form:"status_codes"` // 失败状态码配置
LockTime int64 `json:"lock_time" form:"lock_time"` // 封禁锁定时间(分钟)
}
// WafIPFailureRemoveBanIpReq 移除封禁IP请求
type WafIPFailureRemoveBanIpReq struct {
Ip string `json:"ip" form:"ip"` //移除封禁IP
}

View File

@@ -0,0 +1,20 @@
package response
// IPFailureConfigResp IP失败封禁配置响应
type IPFailureConfigResp struct {
Enabled int64 `json:"enabled"` // 是否启用IP失败封禁 1启用 0禁用
StatusCodes string `json:"status_codes"` // 失败状态码配置
LockTime int64 `json:"lock_time"` // 封禁锁定时间(分钟)
}
// IPFailureIpResp IP失败封禁IP响应
type IPFailureIpResp struct {
IP string `json:"ip"` // IP地址
FailCount int64 `json:"fail_count"` // 失败次数
FirstTime string `json:"first_time"` // 首次失败时间
LastTime string `json:"last_time"` // 最后失败时间
RemainTime string `json:"remain_time"` // 剩余封禁时间
Region string `json:"region"` // IP归属地
TriggerMinutes int64 `json:"trigger_minutes"` // 触发封禁的时间窗口(分钟)
TriggerCount int64 `json:"trigger_count"` // 触发封禁的失败次数阈值
}

View File

@@ -10,6 +10,7 @@ type ApiGroup struct {
AllowUrlRouter
LdpUrlRouter
AntiCCRouter
IPFailureRouter
BlockIpRouter
BlockUrlRouter
AccountRouter

19
router/waf_ip_failure.go Normal file
View File

@@ -0,0 +1,19 @@
package router
import (
"SamWaf/api"
"github.com/gin-gonic/gin"
)
type IPFailureRouter struct {
}
func (receiver *IPFailureRouter) InitIPFailureRouter(group *gin.RouterGroup) {
api := api.APIGroupAPP.WafIPFailureApi
router := group.Group("")
router.GET("/samwaf/wafhost/ipfailure/config", api.GetConfigApi)
router.POST("/samwaf/wafhost/ipfailure/config", api.SetConfigApi)
router.GET("/samwaf/wafhost/ipfailure/baniplist", api.GetBanIpListApi)
router.POST("/samwaf/wafhost/ipfailure/removebanip", api.RemoveIPFailureBanIPApi)
}

View File

@@ -8,8 +8,27 @@ import (
"SamWaf/model/wafenginmodel"
"net/http"
"net/url"
"regexp"
"strconv"
)
// parseIPFailureThreshold 从规则文本中解析IP失败阈值信息
// 解析类似 "MF.GetIPFailureCount(5) > 10" 的模式
// 返回: minutes, count, 是否找到
func parseIPFailureThreshold(ruleText string) (int64, int64, bool) {
// 匹配 GetIPFailureCount(数字) > 数字 或 GetIPFailureCount(数字) >= 数字 的模式
re := regexp.MustCompile(`GetIPFailureCount\s*\(\s*(\d+)\s*\)\s*[>=]+\s*(\d+)`)
matches := re.FindStringSubmatch(ruleText)
if len(matches) >= 3 {
minutes, err1 := strconv.ParseInt(matches[1], 10, 64)
count, err2 := strconv.ParseInt(matches[2], 10, 64)
if err1 == nil && err2 == nil {
return minutes, count, true
}
}
return 0, 0, false
}
/*
*
检测rule
@@ -30,6 +49,13 @@ func (waf *WafEngine) CheckRule(r *http.Request, weblogbean *innerbean.WebLog, f
rulestr := ""
for _, v := range ruleMatchs {
rulestr = rulestr + v.RuleDescription + ","
minutes, count, found := parseIPFailureThreshold(v.GrlText)
if found {
// 记录阈值信息
weblogbean.RecordIPFailureThreshold(minutes, count)
}
}
weblogbean.RISK_LEVEL = 1
@@ -53,6 +79,18 @@ func (waf *WafEngine) CheckRule(r *http.Request, weblogbean *innerbean.WebLog, f
for _, v := range ruleMatchs {
rulestr = rulestr + v.RuleDescription + ","
}
// 尝试从规则数据中解析阈值信息
// 遍历规则数据,查找包含 GetIPFailureCount 的规则
for _, ruleData := range waf.HostTarget[global.GWAF_GLOBAL_HOST_NAME].RuleData {
if ruleData.RuleContent != "" {
minutes, count, found := parseIPFailureThreshold(ruleData.RuleContent)
if found {
// 记录阈值信息
weblogbean.RecordIPFailureThreshold(minutes, count)
break // 找到第一个匹配的规则即可
}
}
}
weblogbean.RISK_LEVEL = 1
result.IsBlock = true

View File

@@ -13,11 +13,24 @@ import (
"time"
)
func InitIPBanManager() {
func InitIPBanManager(wafCache *cache.WafCache) {
// 初始化IP失败管理器单例使用传入的cache
ipFailureManagerOnce.Do(func() {
ipFailureManagerInstance = &IPFailureManager{
cache: wafCache,
statusMap: make(map[int]bool),
}
ipFailureManagerInstance.initStatusCodes()
})
// 注册到innerbean包供WebLog使用
innerbean.SetIPFailureCountGetter(func(ip string, minutes int64) int64 {
return GetIPFailureManager().GetFailureCount(ip, minutes)
})
// 注册IP失败封禁阈值记录函数
innerbean.SetIPFailureThresholdRecorder(func(ip string, minutes int64, count int64) {
GetIPFailureManager().RecordFailureThreshold(ip, minutes, count)
})
// 注册SSL证书验证路径获取函数
innerbean.SetSSLChallengePathGetter(func() string {
return global.GSSL_HTTP_CHANGLE_PATH
@@ -26,10 +39,13 @@ func InitIPBanManager() {
// IPFailureRecord IP失败记录
type IPFailureRecord struct {
IP string
Count int64
FirstTime time.Time
LastTime time.Time
IP string
Events []time.Time
Count int64
FirstTime time.Time
LastTime time.Time
TriggerMinutes int64 // 触发封禁的时间窗口(分钟)
TriggerCount int64 // 触发封禁的失败次数阈值
}
// IPFailureManager IP失败管理器
@@ -46,14 +62,11 @@ var (
)
// GetIPFailureManager 获取IP失败管理器单例
// 注意:需要先调用 InitIPBanManager 进行初始化
func GetIPFailureManager() *IPFailureManager {
ipFailureManagerOnce.Do(func() {
ipFailureManagerInstance = &IPFailureManager{
cache: cache.InitWafCache(),
statusMap: make(map[int]bool),
}
ipFailureManagerInstance.initStatusCodes()
})
if ipFailureManagerInstance == nil {
zlog.Error("IPFailureManager 未初始化,请先调用 InitIPBanManager")
}
return ipFailureManagerInstance
}
@@ -159,27 +172,31 @@ func (m *IPFailureManager) RecordFailure(webLog *innerbean.WebLog) {
if record == nil {
record = &IPFailureRecord{
IP: ip,
Count: 1,
Events: []time.Time{},
FirstTime: now,
LastTime: now,
}
} else {
// 检查时间窗口
timeWindow := time.Duration(global.GCONFIG_IP_FAILURE_BAN_TIME_WINDOW) * time.Minute
if now.Sub(record.FirstTime) > timeWindow {
// 超出时间窗口,重置计数
record.Count = 1
record.FirstTime = now
record.LastTime = now
} else {
// 在时间窗口内,增加计数
record.Count++
record.LastTime = now
}
// 记录事件
record.Events = append(record.Events, now)
// 清理过期事件(按封锁时间作为保留窗口)
retention := time.Duration(global.GCONFIG_IP_FAILURE_BAN_LOCK_TIME) * time.Minute
windowStart := now.Add(-retention)
var valid []time.Time
for _, t := range record.Events {
if t.After(windowStart) {
valid = append(valid, t)
}
}
record.Events = valid
record.Count = int64(len(record.Events))
if len(record.Events) > 0 {
record.FirstTime = record.Events[0]
}
record.LastTime = now
// 保存到缓存TTL设置为时间窗口的2倍
ttl := time.Duration(global.GCONFIG_IP_FAILURE_BAN_TIME_WINDOW*2) * time.Minute
// 保存到缓存TTL设置为封锁时间
ttl := time.Duration(global.GCONFIG_IP_FAILURE_BAN_LOCK_TIME) * time.Minute
m.cache.SetWithTTlRenewTime(key, record, ttl)
}
@@ -201,25 +218,15 @@ func (m *IPFailureManager) GetFailureCount(ip string, minutes int64) int64 {
return 0
}
// 检查时间窗口
timeWindow := time.Duration(minutes) * time.Minute
now := time.Now()
if now.Sub(record.FirstTime) > timeWindow {
// 超出时间窗口返回0
return 0
windowStart := now.Add(-time.Duration(minutes) * time.Minute)
cnt := int64(0)
for _, t := range record.Events {
if t.After(windowStart) {
cnt++
}
}
return record.Count
}
// IsIPBanned 检查IP是否应该被封禁
func (m *IPFailureManager) IsIPBanned(ip string) bool {
if ip == "" || global.GCONFIG_IP_FAILURE_BAN_ENABLED == 0 {
return false
}
count := m.GetFailureCount(ip, global.GCONFIG_IP_FAILURE_BAN_TIME_WINDOW)
return count >= global.GCONFIG_IP_FAILURE_BAN_MAX_COUNT
return cnt
}
// ClearIPFailure 清除IP的失败记录
@@ -250,3 +257,51 @@ func (m *IPFailureManager) GetFailureInfo(ip string) *IPFailureRecord {
return record
}
// RecordFailureThreshold 记录IP失败封禁的阈值信息当规则匹配时调用
// ip: IP地址
// minutes: 触发封禁的时间窗口(分钟)
// count: 触发封禁的失败次数阈值
func (m *IPFailureManager) RecordFailureThreshold(ip string, minutes int64, count int64) {
if ip == "" || global.GCONFIG_IP_FAILURE_BAN_ENABLED == 0 {
return
}
key := enums.CACHE_IP_FAILURE_PRE + ip
val := m.cache.Get(key)
if val == nil {
// 如果记录不存在,创建一个新记录
record := &IPFailureRecord{
IP: ip,
Events: []time.Time{},
TriggerMinutes: minutes,
TriggerCount: count,
FirstTime: time.Now(),
LastTime: time.Now(),
}
ttl := time.Duration(global.GCONFIG_IP_FAILURE_BAN_LOCK_TIME) * time.Minute
m.cache.SetWithTTlRenewTime(key, record, ttl)
return
}
record, ok := val.(*IPFailureRecord)
if !ok {
return
}
// 更新阈值信息(如果新的阈值更严格,则更新)
if record.TriggerMinutes == 0 || record.TriggerCount == 0 {
record.TriggerMinutes = minutes
record.TriggerCount = count
} else {
// 如果新的阈值更严格(时间窗口更小或次数更少),则更新
if minutes < record.TriggerMinutes || (minutes == record.TriggerMinutes && count < record.TriggerCount) {
record.TriggerMinutes = minutes
record.TriggerCount = count
}
}
// 保存更新后的记录
ttl := time.Duration(global.GCONFIG_IP_FAILURE_BAN_LOCK_TIME) * time.Minute
m.cache.SetWithTTlRenewTime(key, record, ttl)
}

View File

@@ -52,6 +52,7 @@ func (web *WafWebManager) initRouter(r *gin.Engine) {
router.ApiGroupApp.InitAllowUrlRouter(RouterGroup)
router.ApiGroupApp.InitLdpUrlRouter(RouterGroup)
router.ApiGroupApp.InitAntiCCRouter(RouterGroup)
router.ApiGroupApp.InitIPFailureRouter(RouterGroup)
router.ApiGroupApp.InitBlockIpRouter(RouterGroup)
router.ApiGroupApp.InitBlockUrlRouter(RouterGroup)
router.ApiGroupApp.InitAccountRouter(RouterGroup)

View File

@@ -117,11 +117,8 @@ func setConfigIntValue(name string, value int64, change int) {
case "ip_failure_ban_enabled":
global.GCONFIG_IP_FAILURE_BAN_ENABLED = value
break
case "ip_failure_ban_time_window":
global.GCONFIG_IP_FAILURE_BAN_TIME_WINDOW = value
break
case "ip_failure_ban_max_count":
global.GCONFIG_IP_FAILURE_BAN_MAX_COUNT = value
case "ip_failure_ban_lock_time":
global.GCONFIG_IP_FAILURE_BAN_LOCK_TIME = value
break
default:
zlog.Warn("Unknown config item:", name)
@@ -298,6 +295,5 @@ func TaskLoadSetting(initLoad bool) {
// IP失败封禁相关配置
updateConfigStringItem(initLoad, "security", "ip_failure_status_codes", global.GCONFIG_IP_FAILURE_STATUS_CODES, "失败状态码配置,支持多个用|分隔也支持正则表达式例如401|403|404|444|429|503 或 ^4[0-9]{2}$", "string", "", configMap)
updateConfigIntItem(initLoad, "security", "ip_failure_ban_enabled", global.GCONFIG_IP_FAILURE_BAN_ENABLED, "是否启用IP失败封禁1启用 0禁用", "options", "0|禁用,1|启用", configMap)
updateConfigIntItem(initLoad, "security", "ip_failure_ban_time_window", global.GCONFIG_IP_FAILURE_BAN_TIME_WINDOW, "IP失败封禁时间窗口(单位:分钟,默认5分钟)", "int", "", configMap)
updateConfigIntItem(initLoad, "security", "ip_failure_ban_max_count", global.GCONFIG_IP_FAILURE_BAN_MAX_COUNT, "IP失败封禁最大失败次数默认10次", "int", "", configMap)
updateConfigIntItem(initLoad, "security", "ip_failure_ban_lock_time", global.GCONFIG_IP_FAILURE_BAN_LOCK_TIME, "IP失败封禁锁定时间(单位:分钟,默认10分钟)", "int", "", configMap)
}