From 2fedb281e8e189828c98482ab68bc2baf2e4ac7a Mon Sep 17 00:00:00 2001 From: wsczx Date: Thu, 3 Oct 2024 23:29:41 +0800 Subject: [PATCH 1/5] =?UTF-8?q?1.=E9=87=8D=E6=9E=84=E9=98=B2=E7=88=86?= =?UTF-8?q?=E9=80=BB=E8=BE=91=EF=BC=8C=E5=9F=BA=E4=BA=8EIP+UserName?= =?UTF-8?q?=E9=94=81=E5=AE=9A=EF=BC=88=E5=8D=95=E4=BD=8D=E6=97=B6=E9=97=B4?= =?UTF-8?q?=E5=86=85=EF=BC=8C=E9=94=81=E5=AE=9A=E7=9B=B8=E5=90=8CIP?= =?UTF-8?q?=E4=B8=8B=E7=9A=84=E7=9B=B8=E5=90=8C=E7=94=A8=E6=88=B7=EF=BC=8C?= =?UTF-8?q?=E5=85=B6=E5=AE=83IP=E5=92=8C=E7=94=A8=E6=88=B7=E4=B8=8D?= =?UTF-8?q?=E5=BD=B1=E5=93=8D=EF=BC=89=202.=E5=A2=9E=E5=8A=A0=E5=9F=BA?= =?UTF-8?q?=E4=BA=8E=E7=94=A8=E6=88=B7=E7=9A=84=E5=85=A8=E5=B1=80=E9=94=81?= =?UTF-8?q?=E5=AE=9A=203.=E5=A2=9E=E5=8A=A0=E5=9F=BA=E4=BA=8EIP=E7=9A=84?= =?UTF-8?q?=E5=85=A8=E5=B1=80=E9=94=81=E5=AE=9A=204.=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E5=8D=95=E4=BD=8D=E6=97=B6=E9=97=B4=E5=86=85=E7=9A=84=E8=AF=B7?= =?UTF-8?q?=E6=B1=82=E9=A2=91=E7=8E=87=E9=99=90=E5=88=B6=EF=BC=88=E6=9A=82?= =?UTF-8?q?=E6=9C=AA=E5=BC=80=E6=94=BE=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/base/cfg.go | 17 +- server/base/config.go | 13 +- server/conf/server.toml | 21 +- server/handler/antiBruteForce.go | 368 +++++++++++++++++++++++++------ 4 files changed, 337 insertions(+), 82 deletions(-) diff --git a/server/base/cfg.go b/server/base/cfg.go index 679d15e..96dca98 100644 --- a/server/base/cfg.go +++ b/server/base/cfg.go @@ -85,10 +85,19 @@ type ServerConfig struct { DisplayError bool `json:"display_error"` ExcludeExportIp bool `json:"exclude_export_ip"` - MaxBanCount int `json:"max_ban_score"` - BanResetTime int `json:"ban_reset_time"` - LockTime int `json:"lock_time"` - UserStateExpiration int `json:"user_state_expiration"` + AntiBruteForce bool `json:"anti_brute_force"` + + MaxBanCount int `json:"max_ban_score"` + BanResetTime int `json:"ban_reset_time"` + LockTime int `json:"lock_time"` + + MaxGlobalUserBanCount int `json:"max_global_user_ban_count"` + GlobalUserBanResetTime int `json:"global_user_ban_reset_time"` + GlobalUserLockTime int `json:"global_user_lock_time"` + + MaxGlobalIPBanCount int `json:"max_global_ip_ban_count"` + GlobalIPBanResetTime int `json:"global_ip_ban_reset_time"` + GlobalIPLockTime int `json:"global_ip_lock_time"` } func initServerCfg() { diff --git a/server/base/config.go b/server/base/config.go index f676c19..3fc434f 100644 --- a/server/base/config.go +++ b/server/base/config.go @@ -72,10 +72,19 @@ var configs = []config{ {Typ: cfgBool, Name: "display_error", Usage: "客户端显示详细错误信息(线上环境慎开启)", ValBool: false}, {Typ: cfgBool, Name: "exclude_export_ip", Usage: "排除出口ip路由(出口ip不加密传输)", ValBool: true}, + {Typ: cfgBool, Name: "anti_brute_force", Usage: "是否开启防爆功能", ValBool: true}, + {Typ: cfgInt, Name: "max_ban_score", Usage: "单位时间内最大尝试次数,0为关闭防爆功能", ValInt: 5}, - {Typ: cfgInt, Name: "ban_reset_time", Usage: "设置单位时间(秒),超过则重置计数", ValInt: 1}, + {Typ: cfgInt, Name: "ban_reset_time", Usage: "设置单位时间(秒),超过则重置计数", ValInt: 10}, {Typ: cfgInt, Name: "lock_time", Usage: "超过最大尝试次数后的锁定时长(秒)", ValInt: 300}, - {Typ: cfgInt, Name: "user_state_expiration", Usage: "用户状态的保存周期(秒),超过则清空计数", ValInt: 900}, + + {Typ: cfgInt, Name: "max_global_user_ban_count", Usage: "全局用户单位时间内最大尝试次数", ValInt: 20}, + {Typ: cfgInt, Name: "global_user_ban_reset_time", Usage: "全局用户设置单位时间(秒)", ValInt: 600}, + {Typ: cfgInt, Name: "global_user_lock_time", Usage: "全局用户锁定时间(秒)", ValInt: 300}, + + {Typ: cfgInt, Name: "max_global_ip_ban_count", Usage: "全局IP单位时间内最大尝试次数", ValInt: 40}, + {Typ: cfgInt, Name: "global_ip_ban_reset_time", Usage: "全局IP设置单位时间(秒)", ValInt: 1200}, + {Typ: cfgInt, Name: "global_ip_lock_time", Usage: "全局IP锁定时间(秒)", ValInt: 300}, } var envs = map[string]string{} diff --git a/server/conf/server.toml b/server/conf/server.toml index 2d60201..418a799 100644 --- a/server/conf/server.toml +++ b/server/conf/server.toml @@ -53,14 +53,29 @@ ipv4_end = "192.168.90.200" #是否自动添加nat iptables_nat = true -#单位时间内最大尝试次数,0为关闭防爆功能 +#防爆全局开关 +anti_brute_force = true + +#单位时间内最大尝试次数,0为全局关闭防爆功能 max_ban_score = 5 #设置单位时间(秒),超过则重置计数 ban_reset_time = 10 #超过最大尝试次数后的锁定时长(秒) lock_time = 300 -#用户状态的保存周期(秒),超过则清空计数 -user_state_expiration = 900 + +#全局用户单位时间内最大尝试次数,0为关闭该功能 +max_global_user_ban_count = 20 +#全局用户设置单位时间(秒) +global_user_ban_reset_time = 600 +#全局用户锁定时间(秒) +global_user_lock_time = 300 + +#全局IP单位时间内最大尝试次数,0为关闭该功能 +max_global_ip_ban_count = 40 +#全局IP设置单位时间(秒) +global_ip_ban_reset_time = 1200 +#全局IP锁定时间(秒) +global_ip_lock_time = 300 #客户端显示详细错误信息(线上环境慎开启) display_error = true diff --git a/server/handler/antiBruteForce.go b/server/handler/antiBruteForce.go index c84902a..684a669 100644 --- a/server/handler/antiBruteForce.go +++ b/server/handler/antiBruteForce.go @@ -3,6 +3,8 @@ package handler import ( "encoding/xml" "io" + "log" + "net" "net/http" "strings" "sync" @@ -11,58 +13,31 @@ import ( "github.com/bjdgyc/anylink/base" ) -// UserState 用于存储用户的登录状态 -type UserState struct { - FailureCount int - LastAttempt time.Time - LockTime time.Time -} - // 自定义 contextKey 类型,避免键冲突 type contextKey string // 定义常量作为上下文的键 const loginStatusKey contextKey = "login_status" -// 用户状态映射 -var userStates = make(map[string]*UserState) -var mu sync.Mutex - func init() { - go cleanupUserStates() -} - -// 清理过期的登录状态 -func cleanupUserStates() { - ticker := time.NewTicker(1 * time.Minute) - defer ticker.Stop() - - for range ticker.C { - mu.Lock() - now := time.Now() - for username, state := range userStates { - if now.Sub(state.LastAttempt) > time.Duration(base.Cfg.UserStateExpiration)*time.Second { - delete(userStates, username) - } - } - mu.Unlock() - } + lockManager.startCleanupTicker() } // 防爆破中间件 func antiBruteForce(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // 如果最大验证失败次数为0,则不启用防爆破功能 - if base.Cfg.MaxBanCount == 0 { + // 防爆破功能全局开关 + if !base.Cfg.AntiBruteForce { next.ServeHTTP(w, r) return } + body, err := io.ReadAll(r.Body) if err != nil { http.Error(w, "Failed to read request body", http.StatusBadRequest) return } - r.Body.Close() + defer r.Body.Close() cr := ClientRequest{} err = xml.Unmarshal(body, &cr) @@ -72,35 +47,49 @@ func antiBruteForce(next http.Handler) http.Handler { } username := cr.Auth.Username - - // 更新用户登录状态 - mu.Lock() - state, exists := userStates[username] - if !exists { - state = &UserState{} - userStates[username] = state - } - // 检查是否已超过锁定时间 - if !state.LockTime.IsZero() { - if time.Now().After(state.LockTime) { - // 如果已经超过了锁定时间,重置失败计数和锁定时间 - state.FailureCount = 0 - state.LockTime = time.Time{} - } else { - // 如果还在锁定时间内,返回错误信息 - http.Error(w, "Account locked due to too many failed attempts. Try again later.", http.StatusTooManyRequests) - mu.Unlock() - return - } + ip, _, err := net.SplitHostPort(r.RemoteAddr) // 提取纯 IP 地址,去掉端口号 + if err != nil { + http.Error(w, "Unable to parse IP address", http.StatusInternalServerError) + return } - // 如果超过时间窗口,重置失败计数 - if time.Since(state.LastAttempt) > time.Duration(base.Cfg.BanResetTime)*time.Second { - state.FailureCount = 0 + now := time.Now() + + // // 速率限制 + // lockManager.mu.RLock() + // limiter, exists := lockManager.rateLimiter[ip] + // if !exists { + // limiter = rate.NewLimiter(rate.Limit(base.Cfg.RateLimit), base.Cfg.Burst) + // lockManager.rateLimiter[ip] = limiter + // } + // lockManager.mu.RUnlock() + + // if !limiter.Allow() { + // log.Printf("Rate limit exceeded for IP %s. Try again later.", ip) + // http.Error(w, "Rate limit exceeded. Try again later.", http.StatusTooManyRequests) + // return + // } + + // 检查全局 IP 锁定 + if base.Cfg.MaxGlobalIPBanCount > 0 && lockManager.checkGlobalIPLock(ip, now) { + log.Printf("IP %s is globally locked. Try again later.", ip) + http.Error(w, "Account globally locked due to too many failed attempts. Try again later.", http.StatusTooManyRequests) + return } - state.LastAttempt = time.Now() - mu.Unlock() + // 检查全局用户锁定 + if base.Cfg.MaxGlobalUserBanCount > 0 && lockManager.checkGlobalUserLock(username, now) { + log.Printf("User %s is globally locked. Try again later.", username) + http.Error(w, "Account globally locked due to too many failed attempts. Try again later.", http.StatusTooManyRequests) + return + } + + // 检查单个用户的 IP 锁定 + if base.Cfg.MaxBanCount > 0 && lockManager.checkUserIPLock(username, ip, now) { + log.Printf("IP %s is locked for user %s. Try again later.", ip, username) + http.Error(w, "Account locked due to too many failed attempts. Try again later.", http.StatusTooManyRequests) + return + } // 重新设置请求体以便后续处理器可以访问 r.Body = io.NopCloser(strings.NewReader(string(body))) @@ -109,23 +98,256 @@ func antiBruteForce(next http.Handler) http.Handler { next.ServeHTTP(w, r) // 从 context 中获取登录状态 - loginStatus, ok := r.Context().Value(loginStatusKey).(bool) - if !ok { - // 如果没有找到登录状态,默认为登录失败 - loginStatus = false - } + loginStatus, _ := r.Context().Value(loginStatusKey).(bool) // 更新用户登录状态 - mu.Lock() - defer mu.Unlock() - - if !loginStatus { - state.FailureCount++ - if state.FailureCount >= base.Cfg.MaxBanCount { - state.LockTime = time.Now().Add(time.Duration(base.Cfg.LockTime) * time.Second) - } - } else { - state.FailureCount = 0 // 成功登录后重置 - } + lockManager.updateGlobalIPLock(ip, now, loginStatus) + lockManager.updateGlobalUserLock(username, now, loginStatus) + lockManager.updateUserIPLock(username, ip, now, loginStatus) }) } + +type LockState struct { + FailureCount int + LockTime time.Time + LastAttempt time.Time +} + +type LockManager struct { + mu sync.RWMutex + ipLocks map[string]*LockState // 全局IP锁定状态 + userLocks map[string]*LockState // 全局用户锁定状态 + ipUserLocks map[string]map[string]*LockState // 单用户IP锁定状态 + // rateLimiter map[string]*rate.Limiter // 速率限制器 + cleanupTicker *time.Ticker +} + +var lockManager = &LockManager{ + ipLocks: make(map[string]*LockState), + userLocks: make(map[string]*LockState), + ipUserLocks: make(map[string]map[string]*LockState), + // rateLimiter: make(map[string]*rate.Limiter), +} + +func (lm *LockManager) startCleanupTicker() { + lm.cleanupTicker = time.NewTicker(1 * time.Minute) + go func() { + for range lm.cleanupTicker.C { + lm.cleanupExpiredLocks() + } + }() +} + +// 定期清理过期的锁定 +func (lm *LockManager) cleanupExpiredLocks() { + go func() { + for range time.Tick(5 * time.Minute) { + now := time.Now() + + var ipKeys, userKeys []string + var IPuserKeys []struct{ user, ip string } + + lm.mu.Lock() + for ip, state := range lm.ipLocks { + if now.Sub(state.LastAttempt) > time.Duration(base.Cfg.GlobalIPBanResetTime)*time.Second { + ipKeys = append(ipKeys, ip) + } + } + + for user, state := range lm.userLocks { + if now.Sub(state.LastAttempt) > time.Duration(base.Cfg.GlobalUserBanResetTime)*time.Second { + userKeys = append(userKeys, user) + } + } + + for user, ipMap := range lm.ipUserLocks { + for ip, state := range ipMap { + if now.Sub(state.LastAttempt) > time.Duration(base.Cfg.BanResetTime)*time.Second { + IPuserKeys = append(IPuserKeys, struct{ user, ip string }{user, ip}) + } + } + } + lm.mu.Unlock() + + lm.mu.Lock() + for _, ip := range ipKeys { + delete(lm.ipLocks, ip) + } + for _, user := range userKeys { + delete(lm.userLocks, user) + } + for _, key := range IPuserKeys { + delete(lm.ipUserLocks[key.user], key.ip) + if len(lm.ipUserLocks[key.user]) == 0 { + delete(lm.ipUserLocks, key.user) + } + } + lm.mu.Unlock() + } + }() +} + +// 检查全局 IP 锁定 +func (lm *LockManager) checkGlobalIPLock(ip string, now time.Time) bool { + lm.mu.RLock() + defer lm.mu.RUnlock() + + state, exists := lm.ipLocks[ip] + if !exists { + return false + } + + if !state.LockTime.IsZero() && now.Before(state.LockTime) { + return true + } + + // 如果超过时间窗口,重置失败计数 + if now.Sub(state.LastAttempt) > time.Duration(base.Cfg.GlobalIPBanResetTime)*time.Second { + state.FailureCount = 0 + state.LockTime = time.Time{} + } + + return false +} + +// 检查全局用户锁定 +func (lm *LockManager) checkGlobalUserLock(username string, now time.Time) bool { + // 我也不知道为什么cisco anyconnect每次连接会先传一个空用户请求···· + if username == "" { + return false + } + lm.mu.RLock() + defer lm.mu.RUnlock() + + state, exists := lm.userLocks[username] + if !exists { + return false + } + + if !state.LockTime.IsZero() && now.Before(state.LockTime) { + return true + } + + // 如果超过时间窗口,重置失败计数 + if now.Sub(state.LastAttempt) > time.Duration(base.Cfg.GlobalUserBanResetTime)*time.Second { + state.FailureCount = 0 + state.LockTime = time.Time{} + } + + return false +} + +// 检查单个用户的 IP 锁定 +func (lm *LockManager) checkUserIPLock(username, ip string, now time.Time) bool { + // 我也不知道为什么cisco anyconnect每次连接会先传一个空用户请求···· + if username == "" { + return false + } + lm.mu.RLock() + defer lm.mu.RUnlock() + + userIPMap, userExists := lm.ipUserLocks[username] + if !userExists { + return false + } + + state, ipExists := userIPMap[ip] + if !ipExists { + return false + } + + if !state.LockTime.IsZero() && now.Before(state.LockTime) { + return true + } + + // 如果超过时间窗口,重置失败计数 + if now.Sub(state.LastAttempt) > time.Duration(base.Cfg.BanResetTime)*time.Second { + state.FailureCount = 0 + state.LockTime = time.Time{} + } + + return false +} + +// 更新全局 IP 锁定状态 +func (lm *LockManager) updateGlobalIPLock(ip string, now time.Time, success bool) { + lm.mu.Lock() + defer lm.mu.Unlock() + + state, exists := lm.ipLocks[ip] + if !exists { + state = &LockState{} + lm.ipLocks[ip] = state + } + + if success { + state.FailureCount = 0 + state.LockTime = time.Time{} + } else { + state.FailureCount++ + if state.FailureCount >= base.Cfg.MaxGlobalIPBanCount { + state.LockTime = now.Add(time.Duration(base.Cfg.GlobalIPLockTime) * time.Second) + } + } + state.LastAttempt = now +} + +// 更新全局用户锁定状态 +func (lm *LockManager) updateGlobalUserLock(username string, now time.Time, success bool) { + // 我也不知道为什么cisco anyconnect每次连接会先传一个空用户请求···· + if username == "" { + return + } + lm.mu.Lock() + defer lm.mu.Unlock() + + state, exists := lm.userLocks[username] + if !exists { + state = &LockState{} + lm.userLocks[username] = state + } + + if success { + state.FailureCount = 0 + state.LockTime = time.Time{} + } else { + state.FailureCount++ + if state.FailureCount >= base.Cfg.MaxGlobalUserBanCount { + state.LockTime = now.Add(time.Duration(base.Cfg.GlobalUserLockTime) * time.Second) + } + } + state.LastAttempt = now +} + +// 更新单个用户的 IP 锁定状态 +func (lm *LockManager) updateUserIPLock(username, ip string, now time.Time, success bool) { + // 我也不知道为什么cisco anyconnect每次连接会先传一个空用户请求···· + if username == "" { + return + } + lm.mu.Lock() + defer lm.mu.Unlock() + + userIPMap, userExists := lm.ipUserLocks[username] + if !userExists { + userIPMap = make(map[string]*LockState) + lm.ipUserLocks[username] = userIPMap + } + + state, ipExists := userIPMap[ip] + if !ipExists { + state = &LockState{} + userIPMap[ip] = state + } + + if success { + state.FailureCount = 0 + state.LockTime = time.Time{} + } else { + state.FailureCount++ + if state.FailureCount >= base.Cfg.MaxBanCount { + state.LockTime = now.Add(time.Duration(base.Cfg.LockTime) * time.Second) + } + } + state.LastAttempt = now +} From f195ae2d3023c696d97230735e83d8992f98b5af Mon Sep 17 00:00:00 2001 From: wsczx Date: Fri, 4 Oct 2024 00:17:56 +0800 Subject: [PATCH 2/5] =?UTF-8?q?=E4=BC=98=E5=8C=96=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/handler/antiBruteForce.go | 70 ++++++++++++++------------------ 1 file changed, 30 insertions(+), 40 deletions(-) diff --git a/server/handler/antiBruteForce.go b/server/handler/antiBruteForce.go index 684a669..507856c 100644 --- a/server/handler/antiBruteForce.go +++ b/server/handler/antiBruteForce.go @@ -197,16 +197,13 @@ func (lm *LockManager) checkGlobalIPLock(ip string, now time.Time) bool { return false } + // 如果超过时间窗口,重置失败计数 + lm.resetLockStateIfExpired(state, now, base.Cfg.GlobalIPBanResetTime) + if !state.LockTime.IsZero() && now.Before(state.LockTime) { return true } - // 如果超过时间窗口,重置失败计数 - if now.Sub(state.LastAttempt) > time.Duration(base.Cfg.GlobalIPBanResetTime)*time.Second { - state.FailureCount = 0 - state.LockTime = time.Time{} - } - return false } @@ -224,16 +221,13 @@ func (lm *LockManager) checkGlobalUserLock(username string, now time.Time) bool return false } + // 如果超过时间窗口,重置失败计数 + lm.resetLockStateIfExpired(state, now, base.Cfg.GlobalUserBanResetTime) + if !state.LockTime.IsZero() && now.Before(state.LockTime) { return true } - // 如果超过时间窗口,重置失败计数 - if now.Sub(state.LastAttempt) > time.Duration(base.Cfg.GlobalUserBanResetTime)*time.Second { - state.FailureCount = 0 - state.LockTime = time.Time{} - } - return false } @@ -256,16 +250,13 @@ func (lm *LockManager) checkUserIPLock(username, ip string, now time.Time) bool return false } + // 如果超过时间窗口,重置失败计数 + lm.resetLockStateIfExpired(state, now, base.Cfg.BanResetTime) + if !state.LockTime.IsZero() && now.Before(state.LockTime) { return true } - // 如果超过时间窗口,重置失败计数 - if now.Sub(state.LastAttempt) > time.Duration(base.Cfg.BanResetTime)*time.Second { - state.FailureCount = 0 - state.LockTime = time.Time{} - } - return false } @@ -280,16 +271,7 @@ func (lm *LockManager) updateGlobalIPLock(ip string, now time.Time, success bool lm.ipLocks[ip] = state } - if success { - state.FailureCount = 0 - state.LockTime = time.Time{} - } else { - state.FailureCount++ - if state.FailureCount >= base.Cfg.MaxGlobalIPBanCount { - state.LockTime = now.Add(time.Duration(base.Cfg.GlobalIPLockTime) * time.Second) - } - } - state.LastAttempt = now + lm.updateLockState(state, now, success, base.Cfg.MaxGlobalIPBanCount, base.Cfg.GlobalIPLockTime) } // 更新全局用户锁定状态 @@ -307,16 +289,7 @@ func (lm *LockManager) updateGlobalUserLock(username string, now time.Time, succ lm.userLocks[username] = state } - if success { - state.FailureCount = 0 - state.LockTime = time.Time{} - } else { - state.FailureCount++ - if state.FailureCount >= base.Cfg.MaxGlobalUserBanCount { - state.LockTime = now.Add(time.Duration(base.Cfg.GlobalUserLockTime) * time.Second) - } - } - state.LastAttempt = now + lm.updateLockState(state, now, success, base.Cfg.MaxGlobalUserBanCount, base.Cfg.GlobalUserLockTime) } // 更新单个用户的 IP 锁定状态 @@ -340,14 +313,31 @@ func (lm *LockManager) updateUserIPLock(username, ip string, now time.Time, succ userIPMap[ip] = state } + lm.updateLockState(state, now, success, base.Cfg.MaxBanCount, base.Cfg.LockTime) +} + +// 更新锁定状态 +func (lm *LockManager) updateLockState(state *LockState, now time.Time, success bool, maxBanCount, lockTime int) { if success { state.FailureCount = 0 state.LockTime = time.Time{} } else { state.FailureCount++ - if state.FailureCount >= base.Cfg.MaxBanCount { - state.LockTime = now.Add(time.Duration(base.Cfg.LockTime) * time.Second) + if state.FailureCount >= maxBanCount { + state.LockTime = now.Add(time.Duration(lockTime) * time.Second) } } state.LastAttempt = now } + +// 超过时间窗口时重置锁定状态 +func (lm *LockManager) resetLockStateIfExpired(state *LockState, now time.Time, resetTime int) { + if state == nil || state.LastAttempt.IsZero() { + return + } + + if now.Sub(state.LastAttempt) > time.Duration(resetTime)*time.Second { + state.FailureCount = 0 + state.LockTime = time.Time{} + } +} From 59748fe39506b16c35ed01f8d1d3564d485c3836 Mon Sep 17 00:00:00 2001 From: wsczx Date: Fri, 4 Oct 2024 11:55:46 +0800 Subject: [PATCH 3/5] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E9=94=81=E5=AE=9A?= =?UTF-8?q?=E7=8A=B6=E6=80=81=E8=AE=B0=E5=BD=95=E7=94=9F=E5=91=BD=E5=91=A8?= =?UTF-8?q?=E6=9C=9F=E9=85=8D=E7=BD=AE=E9=A1=B9=EF=BC=8C=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E6=B8=85=E7=90=86=E5=86=85=E5=AD=98=E7=9A=84=E5=AE=9A=E6=97=B6?= =?UTF-8?q?=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/base/cfg.go | 2 + server/base/config.go | 8 +-- server/conf/server.toml | 7 ++- server/handler/antiBruteForce.go | 86 ++++++++++++++++---------------- 4 files changed, 54 insertions(+), 49 deletions(-) diff --git a/server/base/cfg.go b/server/base/cfg.go index 96dca98..d0afc8f 100644 --- a/server/base/cfg.go +++ b/server/base/cfg.go @@ -98,6 +98,8 @@ type ServerConfig struct { MaxGlobalIPBanCount int `json:"max_global_ip_ban_count"` GlobalIPBanResetTime int `json:"global_ip_ban_reset_time"` GlobalIPLockTime int `json:"global_ip_lock_time"` + + GlobalLockStateExpirationTime int `json:"global_lock_state_expiration_time"` } func initServerCfg() { diff --git a/server/base/config.go b/server/base/config.go index 3fc434f..cd10674 100644 --- a/server/base/config.go +++ b/server/base/config.go @@ -74,17 +74,19 @@ var configs = []config{ {Typ: cfgBool, Name: "anti_brute_force", Usage: "是否开启防爆功能", ValBool: true}, - {Typ: cfgInt, Name: "max_ban_score", Usage: "单位时间内最大尝试次数,0为关闭防爆功能", ValInt: 5}, + {Typ: cfgInt, Name: "max_ban_score", Usage: "单位时间内最大尝试次数,0为关闭该功能", ValInt: 5}, {Typ: cfgInt, Name: "ban_reset_time", Usage: "设置单位时间(秒),超过则重置计数", ValInt: 10}, {Typ: cfgInt, Name: "lock_time", Usage: "超过最大尝试次数后的锁定时长(秒)", ValInt: 300}, - {Typ: cfgInt, Name: "max_global_user_ban_count", Usage: "全局用户单位时间内最大尝试次数", ValInt: 20}, + {Typ: cfgInt, Name: "max_global_user_ban_count", Usage: "全局用户单位时间内最大尝试次数,0为关闭该功能", ValInt: 20}, {Typ: cfgInt, Name: "global_user_ban_reset_time", Usage: "全局用户设置单位时间(秒)", ValInt: 600}, {Typ: cfgInt, Name: "global_user_lock_time", Usage: "全局用户锁定时间(秒)", ValInt: 300}, - {Typ: cfgInt, Name: "max_global_ip_ban_count", Usage: "全局IP单位时间内最大尝试次数", ValInt: 40}, + {Typ: cfgInt, Name: "max_global_ip_ban_count", Usage: "全局IP单位时间内最大尝试次数,0为关闭该功能", ValInt: 40}, {Typ: cfgInt, Name: "global_ip_ban_reset_time", Usage: "全局IP设置单位时间(秒)", ValInt: 1200}, {Typ: cfgInt, Name: "global_ip_lock_time", Usage: "全局IP锁定时间(秒)", ValInt: 300}, + + {Typ: cfgInt, Name: "global_lock_state_expiration_time", Usage: "全局锁定状态的保存生命周期(秒),超过则删除记录", ValInt: 3600}, } var envs = map[string]string{} diff --git a/server/conf/server.toml b/server/conf/server.toml index 418a799..eb8706b 100644 --- a/server/conf/server.toml +++ b/server/conf/server.toml @@ -53,10 +53,10 @@ ipv4_end = "192.168.90.200" #是否自动添加nat iptables_nat = true -#防爆全局开关 +#防爆破全局开关 anti_brute_force = true -#单位时间内最大尝试次数,0为全局关闭防爆功能 +#单位时间内最大尝试次数,0为关闭该功能 max_ban_score = 5 #设置单位时间(秒),超过则重置计数 ban_reset_time = 10 @@ -77,5 +77,8 @@ global_ip_ban_reset_time = 1200 #全局IP锁定时间(秒) global_ip_lock_time = 300 +#全局锁定状态的保存生命周期(秒),超过则删除记录 +global_lock_state_expiration_time = 3600 + #客户端显示详细错误信息(线上环境慎开启) display_error = true diff --git a/server/handler/antiBruteForce.go b/server/handler/antiBruteForce.go index 507856c..8a8a888 100644 --- a/server/handler/antiBruteForce.go +++ b/server/handler/antiBruteForce.go @@ -20,7 +20,9 @@ type contextKey string const loginStatusKey contextKey = "login_status" func init() { - lockManager.startCleanupTicker() + if base.Cfg.AntiBruteForce { + lockManager.startCleanupTicker() + } } // 防爆破中间件 @@ -130,7 +132,7 @@ var lockManager = &LockManager{ } func (lm *LockManager) startCleanupTicker() { - lm.cleanupTicker = time.NewTicker(1 * time.Minute) + lm.cleanupTicker = time.NewTicker(5 * time.Minute) go func() { for range lm.cleanupTicker.C { lm.cleanupExpiredLocks() @@ -140,51 +142,47 @@ func (lm *LockManager) startCleanupTicker() { // 定期清理过期的锁定 func (lm *LockManager) cleanupExpiredLocks() { - go func() { - for range time.Tick(5 * time.Minute) { - now := time.Now() + now := time.Now() - var ipKeys, userKeys []string - var IPuserKeys []struct{ user, ip string } + var ipKeys, userKeys []string + var IPuserKeys []struct{ user, ip string } - lm.mu.Lock() - for ip, state := range lm.ipLocks { - if now.Sub(state.LastAttempt) > time.Duration(base.Cfg.GlobalIPBanResetTime)*time.Second { - ipKeys = append(ipKeys, ip) - } - } - - for user, state := range lm.userLocks { - if now.Sub(state.LastAttempt) > time.Duration(base.Cfg.GlobalUserBanResetTime)*time.Second { - userKeys = append(userKeys, user) - } - } - - for user, ipMap := range lm.ipUserLocks { - for ip, state := range ipMap { - if now.Sub(state.LastAttempt) > time.Duration(base.Cfg.BanResetTime)*time.Second { - IPuserKeys = append(IPuserKeys, struct{ user, ip string }{user, ip}) - } - } - } - lm.mu.Unlock() - - lm.mu.Lock() - for _, ip := range ipKeys { - delete(lm.ipLocks, ip) - } - for _, user := range userKeys { - delete(lm.userLocks, user) - } - for _, key := range IPuserKeys { - delete(lm.ipUserLocks[key.user], key.ip) - if len(lm.ipUserLocks[key.user]) == 0 { - delete(lm.ipUserLocks, key.user) - } - } - lm.mu.Unlock() + lm.mu.Lock() + for ip, state := range lm.ipLocks { + if now.Sub(state.LastAttempt) > time.Duration(base.Cfg.GlobalLockStateExpirationTime)*time.Second { + ipKeys = append(ipKeys, ip) } - }() + } + + for user, state := range lm.userLocks { + if now.Sub(state.LastAttempt) > time.Duration(base.Cfg.GlobalLockStateExpirationTime)*time.Second { + userKeys = append(userKeys, user) + } + } + + for user, ipMap := range lm.ipUserLocks { + for ip, state := range ipMap { + if now.Sub(state.LastAttempt) > time.Duration(base.Cfg.GlobalLockStateExpirationTime)*time.Second { + IPuserKeys = append(IPuserKeys, struct{ user, ip string }{user, ip}) + } + } + } + lm.mu.Unlock() + + lm.mu.Lock() + for _, ip := range ipKeys { + delete(lm.ipLocks, ip) + } + for _, user := range userKeys { + delete(lm.userLocks, user) + } + for _, key := range IPuserKeys { + delete(lm.ipUserLocks[key.user], key.ip) + if len(lm.ipUserLocks[key.user]) == 0 { + delete(lm.ipUserLocks, key.user) + } + } + lm.mu.Unlock() } // 检查全局 IP 锁定 From c8cb9c163ab426dbc9f3f90d2f2fdb017069cfab Mon Sep 17 00:00:00 2001 From: wsczx Date: Fri, 4 Oct 2024 16:02:24 +0800 Subject: [PATCH 4/5] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E5=85=A8=E5=B1=80IP?= =?UTF-8?q?=E7=99=BD=E5=90=8D=E5=8D=95=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/base/cfg.go | 3 +- server/base/config.go | 1 + server/conf/server.toml | 1 + server/handler/antiBruteForce.go | 86 ++++++++++++++++++++++++++------ server/handler/start.go | 2 + 5 files changed, 77 insertions(+), 16 deletions(-) diff --git a/server/base/cfg.go b/server/base/cfg.go index d0afc8f..6c31310 100644 --- a/server/base/cfg.go +++ b/server/base/cfg.go @@ -85,7 +85,8 @@ type ServerConfig struct { DisplayError bool `json:"display_error"` ExcludeExportIp bool `json:"exclude_export_ip"` - AntiBruteForce bool `json:"anti_brute_force"` + AntiBruteForce bool `json:"anti_brute_force"` + IPWhitelist string `json:"ip_whitelist"` MaxBanCount int `json:"max_ban_score"` BanResetTime int `json:"ban_reset_time"` diff --git a/server/base/config.go b/server/base/config.go index cd10674..65084d4 100644 --- a/server/base/config.go +++ b/server/base/config.go @@ -73,6 +73,7 @@ var configs = []config{ {Typ: cfgBool, Name: "exclude_export_ip", Usage: "排除出口ip路由(出口ip不加密传输)", ValBool: true}, {Typ: cfgBool, Name: "anti_brute_force", Usage: "是否开启防爆功能", ValBool: true}, + {Typ: cfgStr, Name: "ip_whitelist", Usage: "全局IP白名单,多个用逗号分隔,支持单IP和CIDR范围", ValStr: "192.168.90.1,172.16.0.0/24"}, {Typ: cfgInt, Name: "max_ban_score", Usage: "单位时间内最大尝试次数,0为关闭该功能", ValInt: 5}, {Typ: cfgInt, Name: "ban_reset_time", Usage: "设置单位时间(秒),超过则重置计数", ValInt: 10}, diff --git a/server/conf/server.toml b/server/conf/server.toml index eb8706b..d54702e 100644 --- a/server/conf/server.toml +++ b/server/conf/server.toml @@ -55,6 +55,7 @@ iptables_nat = true #防爆破全局开关 anti_brute_force = true +ip_whitelist = "192.168.90.1,172.16.0.0/24" #单位时间内最大尝试次数,0为关闭该功能 max_ban_score = 5 diff --git a/server/handler/antiBruteForce.go b/server/handler/antiBruteForce.go index 8a8a888..77be83a 100644 --- a/server/handler/antiBruteForce.go +++ b/server/handler/antiBruteForce.go @@ -18,10 +18,15 @@ type contextKey string // 定义常量作为上下文的键 const loginStatusKey contextKey = "login_status" +const defaultGlobalLockStateExpirationTime = 3600 -func init() { +func initAntiBruteForce() { if base.Cfg.AntiBruteForce { + if base.Cfg.GlobalLockStateExpirationTime <= 0 { + base.Cfg.GlobalLockStateExpirationTime = defaultGlobalLockStateExpirationTime + } lockManager.startCleanupTicker() + lockManager.initIPWhitelist() } } @@ -56,6 +61,12 @@ func antiBruteForce(next http.Handler) http.Handler { } now := time.Now() + // 检查IP是否在白名单中 + if lockManager.isWhitelisted(ip) { + r.Body = io.NopCloser(strings.NewReader(string(body))) + next.ServeHTTP(w, r) + return + } // // 速率限制 // lockManager.mu.RLock() @@ -114,23 +125,69 @@ type LockState struct { LockTime time.Time LastAttempt time.Time } +type IPWhitelists struct { + IP net.IP + CIDR *net.IPNet +} type LockManager struct { - mu sync.RWMutex - ipLocks map[string]*LockState // 全局IP锁定状态 - userLocks map[string]*LockState // 全局用户锁定状态 - ipUserLocks map[string]map[string]*LockState // 单用户IP锁定状态 + mu sync.Mutex + ipLocks map[string]*LockState // 全局IP锁定状态 + userLocks map[string]*LockState // 全局用户锁定状态 + ipUserLocks map[string]map[string]*LockState // 单用户IP锁定状态 + ipWhitelists []IPWhitelists // 全局IP白名单,包含IP地址和CIDR范围 // rateLimiter map[string]*rate.Limiter // 速率限制器 cleanupTicker *time.Ticker } var lockManager = &LockManager{ - ipLocks: make(map[string]*LockState), - userLocks: make(map[string]*LockState), - ipUserLocks: make(map[string]map[string]*LockState), + ipLocks: make(map[string]*LockState), + userLocks: make(map[string]*LockState), + ipUserLocks: make(map[string]map[string]*LockState), + ipWhitelists: make([]IPWhitelists, 0), // rateLimiter: make(map[string]*rate.Limiter), } +// 初始化IP白名单 +func (lm *LockManager) initIPWhitelist() { + ipWhitelist := strings.Split(base.Cfg.IPWhitelist, ",") + for _, ipWhitelist := range ipWhitelist { + ipWhitelist = strings.TrimSpace(ipWhitelist) + if ipWhitelist == "" { + continue + } + + _, ipNet, err := net.ParseCIDR(ipWhitelist) + if err == nil { + lm.ipWhitelists = append(lm.ipWhitelists, IPWhitelists{CIDR: ipNet}) + continue + } + + ip := net.ParseIP(ipWhitelist) + if ip != nil { + lm.ipWhitelists = append(lm.ipWhitelists, IPWhitelists{IP: ip}) + continue + } + } +} + +// 检查 IP 是否在白名单中 +func (lm *LockManager) isWhitelisted(ip string) bool { + clientIP := net.ParseIP(ip) + if clientIP == nil { + return false + } + for _, ipWhitelist := range lm.ipWhitelists { + if ipWhitelist.CIDR != nil && ipWhitelist.CIDR.Contains(clientIP) { + return true + } + if ipWhitelist.IP != nil && ipWhitelist.IP.Equal(clientIP) { + return true + } + } + return false +} + func (lm *LockManager) startCleanupTicker() { lm.cleanupTicker = time.NewTicker(5 * time.Minute) go func() { @@ -187,8 +244,8 @@ func (lm *LockManager) cleanupExpiredLocks() { // 检查全局 IP 锁定 func (lm *LockManager) checkGlobalIPLock(ip string, now time.Time) bool { - lm.mu.RLock() - defer lm.mu.RUnlock() + lm.mu.Lock() + defer lm.mu.Unlock() state, exists := lm.ipLocks[ip] if !exists { @@ -211,14 +268,13 @@ func (lm *LockManager) checkGlobalUserLock(username string, now time.Time) bool if username == "" { return false } - lm.mu.RLock() - defer lm.mu.RUnlock() + lm.mu.Lock() + defer lm.mu.Unlock() state, exists := lm.userLocks[username] if !exists { return false } - // 如果超过时间窗口,重置失败计数 lm.resetLockStateIfExpired(state, now, base.Cfg.GlobalUserBanResetTime) @@ -235,8 +291,8 @@ func (lm *LockManager) checkUserIPLock(username, ip string, now time.Time) bool if username == "" { return false } - lm.mu.RLock() - defer lm.mu.RUnlock() + lm.mu.Lock() + defer lm.mu.Unlock() userIPMap, userExists := lm.ipUserLocks[username] if !userExists { diff --git a/server/handler/start.go b/server/handler/start.go index 5f4637d..e582739 100644 --- a/server/handler/start.go +++ b/server/handler/start.go @@ -17,6 +17,8 @@ func Start() { sessdata.Start() cron.Start() + initAntiBruteForce() //初始化防爆破定时器和IP白名单 + // 开启服务器转发 err := execCmd([]string{"sysctl -w net.ipv4.ip_forward=1"}) if err != nil { From 1c6fc446c94f709deb1a1661bfa86caf51abe27d Mon Sep 17 00:00:00 2001 From: wsczx Date: Fri, 4 Oct 2024 19:22:23 +0800 Subject: [PATCH 5/5] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=97=A0=E6=B3=95?= =?UTF-8?q?=E8=87=AA=E5=8A=A8=E8=A7=A3=E9=94=81=E7=9A=84Bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/conf/server.toml | 4 +++- server/handler/antiBruteForce.go | 10 +++++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/server/conf/server.toml b/server/conf/server.toml index d54702e..5dbf53d 100644 --- a/server/conf/server.toml +++ b/server/conf/server.toml @@ -55,12 +55,14 @@ iptables_nat = true #防爆破全局开关 anti_brute_force = true +#全局IP白名单,多个用逗号分隔,支持单IP和CIDR范围 ip_whitelist = "192.168.90.1,172.16.0.0/24" +#锁定时间最好不要超过单位时间 #单位时间内最大尝试次数,0为关闭该功能 max_ban_score = 5 #设置单位时间(秒),超过则重置计数 -ban_reset_time = 10 +ban_reset_time = 600 #超过最大尝试次数后的锁定时长(秒) lock_time = 300 diff --git a/server/handler/antiBruteForce.go b/server/handler/antiBruteForce.go index 77be83a..947a8a9 100644 --- a/server/handler/antiBruteForce.go +++ b/server/handler/antiBruteForce.go @@ -384,12 +384,20 @@ func (lm *LockManager) updateLockState(state *LockState, now time.Time, success state.LastAttempt = now } -// 超过时间窗口时重置锁定状态 +// 超过窗口时间和锁定时间时重置锁定状态 func (lm *LockManager) resetLockStateIfExpired(state *LockState, now time.Time, resetTime int) { if state == nil || state.LastAttempt.IsZero() { return } + // 如果超过锁定时间,重置锁定状态 + if !state.LockTime.IsZero() && now.After(state.LockTime) { + state.FailureCount = 0 + state.LockTime = time.Time{} + return + } + + // 如果超过窗口时间,重置失败计数 if now.Sub(state.LastAttempt) > time.Duration(resetTime)*time.Second { state.FailureCount = 0 state.LockTime = time.Time{}