From c5a76ba43623b260dc675847eaf3f089857ddccf Mon Sep 17 00:00:00 2001 From: wsczx Date: Sun, 29 Sep 2024 20:04:20 +0800 Subject: [PATCH 01/48] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E9=AA=8C=E8=AF=81=E9=98=B2=E7=88=86=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 | 5 ++ server/base/config.go | 5 ++ server/conf/server.toml | 10 ++- server/handler/antiBruteForce.go | 130 +++++++++++++++++++++++++++++++ server/handler/link_auth.go | 3 + server/handler/server.go | 2 +- 6 files changed, 152 insertions(+), 3 deletions(-) create mode 100644 server/handler/antiBruteForce.go diff --git a/server/base/cfg.go b/server/base/cfg.go index 53d9a85..679d15e 100644 --- a/server/base/cfg.go +++ b/server/base/cfg.go @@ -84,6 +84,11 @@ 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"` } func initServerCfg() { diff --git a/server/base/config.go b/server/base/config.go index 6642659..93c0cb0 100644 --- a/server/base/config.go +++ b/server/base/config.go @@ -71,6 +71,11 @@ var configs = []config{ {Typ: cfgBool, Name: "display_error", Usage: "客户端显示详细错误信息(线上环境慎开启)", ValBool: false}, {Typ: cfgBool, Name: "exclude_export_ip", Usage: "排除出口ip路由(出口ip不加密传输)", ValBool: true}, + + {Typ: cfgInt, Name: "max_ban_score", Usage: "单位时间内最大尝试次数", ValInt: 5}, + {Typ: cfgInt, Name: "ban_reset_time", Usage: "设置单位时间(秒),超过则重置计数", ValInt: 1}, + {Typ: cfgInt, Name: "lock_time", Usage: "超过最大尝试次数后的锁定时长(秒)", ValInt: 300}, + {Typ: cfgInt, Name: "user_state_expiration", Usage: "用户状态的保存周期(秒),超过则清空计数", ValInt: 900}, } var envs = map[string]string{} diff --git a/server/conf/server.toml b/server/conf/server.toml index 88ff491..d34871f 100644 --- a/server/conf/server.toml +++ b/server/conf/server.toml @@ -53,8 +53,14 @@ ipv4_end = "192.168.90.200" #是否自动添加nat iptables_nat = true +#单位时间内最大尝试次数 +max_ban_score = 5 +#设置单位时间(秒),超过则重置计数 +ban_reset_time = 10 +#超过最大尝试次数后的锁定时长(秒) +lock_time = 300 +#用户状态的保存周期(秒),超过则清空计数 +user_state_expiration = 900 #客户端显示详细错误信息(线上环境慎开启) display_error = true - - diff --git a/server/handler/antiBruteForce.go b/server/handler/antiBruteForce.go new file mode 100644 index 0000000..48f1bbc --- /dev/null +++ b/server/handler/antiBruteForce.go @@ -0,0 +1,130 @@ +package handler + +import ( + "encoding/xml" + "io" + "net/http" + "strings" + "sync" + "time" + + "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() + } +} + +// 防爆破中间件 +func antiBruteForce(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // 如果最大验证失败次数为0,则不启用防爆破功能 + if base.Cfg.MaxBanCount == 0 { + return + } + body, err := io.ReadAll(r.Body) + if err != nil { + http.Error(w, "Failed to read request body", http.StatusBadRequest) + return + } + r.Body.Close() + + cr := ClientRequest{} + err = xml.Unmarshal(body, &cr) + if err != nil { + http.Error(w, "Failed to parse XML", http.StatusBadRequest) + return + } + + 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 + } + } + + // 如果超过时间窗口,重置失败计数 + if time.Since(state.LastAttempt) > time.Duration(base.Cfg.BanResetTime)*time.Second { + state.FailureCount = 0 + } + + state.LastAttempt = time.Now() + mu.Unlock() + + // 重新设置请求体以便后续处理器可以访问 + r.Body = io.NopCloser(strings.NewReader(string(body))) + + // 调用下一个处理器 + next.ServeHTTP(w, r) + + // 从 context 中获取登录状态 + loginStatus, ok := r.Context().Value(loginStatusKey).(bool) + if !ok { + // 如果没有找到登录状态,默认为登录失败 + loginStatus = false + } + + // 更新用户登录状态 + 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 // 成功登录后重置 + } + }) +} diff --git a/server/handler/link_auth.go b/server/handler/link_auth.go index f4c9950..8924290 100644 --- a/server/handler/link_auth.go +++ b/server/handler/link_auth.go @@ -2,6 +2,7 @@ package handler import ( "bytes" + "context" "crypto/md5" "encoding/xml" "fmt" @@ -88,6 +89,7 @@ func LinkAuth(w http.ResponseWriter, r *http.Request) { // TODO 用户密码校验 err = dbdata.CheckUser(cr.Auth.Username, cr.Auth.Password, cr.GroupSelect) if err != nil { + r = r.WithContext(context.WithValue(r.Context(), loginStatusKey, false)) // 传递登录失败状态 base.Warn(err, r.RemoteAddr) ua.Info = err.Error() ua.Status = dbdata.UserAuthFail @@ -101,6 +103,7 @@ func LinkAuth(w http.ResponseWriter, r *http.Request) { tplRequest(tpl_request, w, data) return } + r = r.WithContext(context.WithValue(r.Context(), loginStatusKey, true)) // 传递登录成功状态 dbdata.UserActLogIns.Add(ua, userAgent) // if !ok { // w.WriteHeader(http.StatusOK) diff --git a/server/handler/server.go b/server/handler/server.go index eadf549..2fc5baf 100644 --- a/server/handler/server.go +++ b/server/handler/server.go @@ -111,7 +111,7 @@ func initRoute() http.Handler { }) r.HandleFunc("/", LinkHome).Methods(http.MethodGet) - r.HandleFunc("/", LinkAuth).Methods(http.MethodPost) + r.Handle("/", antiBruteForce(http.HandlerFunc(LinkAuth))).Methods(http.MethodPost) r.HandleFunc("/CSCOSSLC/tunnel", LinkTunnel).Methods(http.MethodConnect) r.HandleFunc("/otp_qr", LinkOtpQr).Methods(http.MethodGet) r.HandleFunc(fmt.Sprintf("/profile_%s.xml", base.Cfg.ProfileName), func(w http.ResponseWriter, r *http.Request) { From 9e700830beb02cf5c0b5a0b8c05f674ac80e3a9a Mon Sep 17 00:00:00 2001 From: wsczx Date: Sun, 29 Sep 2024 21:09:15 +0800 Subject: [PATCH 02/48] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=9C=AA=E5=90=AF?= =?UTF-8?q?=E5=8A=A8=E9=98=B2=E7=88=86=E5=8A=9F=E8=83=BD=E5=AF=BC=E8=87=B4?= =?UTF-8?q?=E6=97=A0=E6=B3=95=E9=AA=8C=E8=AF=81=E7=9A=84Bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/base/config.go | 2 +- server/conf/server.toml | 2 +- server/handler/antiBruteForce.go | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/server/base/config.go b/server/base/config.go index 93c0cb0..f676c19 100644 --- a/server/base/config.go +++ b/server/base/config.go @@ -72,7 +72,7 @@ var configs = []config{ {Typ: cfgBool, Name: "display_error", Usage: "客户端显示详细错误信息(线上环境慎开启)", ValBool: false}, {Typ: cfgBool, Name: "exclude_export_ip", Usage: "排除出口ip路由(出口ip不加密传输)", ValBool: true}, - {Typ: cfgInt, Name: "max_ban_score", Usage: "单位时间内最大尝试次数", ValInt: 5}, + {Typ: cfgInt, Name: "max_ban_score", Usage: "单位时间内最大尝试次数,0为关闭防爆功能", ValInt: 5}, {Typ: cfgInt, Name: "ban_reset_time", Usage: "设置单位时间(秒),超过则重置计数", ValInt: 1}, {Typ: cfgInt, Name: "lock_time", Usage: "超过最大尝试次数后的锁定时长(秒)", ValInt: 300}, {Typ: cfgInt, Name: "user_state_expiration", Usage: "用户状态的保存周期(秒),超过则清空计数", ValInt: 900}, diff --git a/server/conf/server.toml b/server/conf/server.toml index d34871f..2d60201 100644 --- a/server/conf/server.toml +++ b/server/conf/server.toml @@ -53,7 +53,7 @@ ipv4_end = "192.168.90.200" #是否自动添加nat iptables_nat = true -#单位时间内最大尝试次数 +#单位时间内最大尝试次数,0为关闭防爆功能 max_ban_score = 5 #设置单位时间(秒),超过则重置计数 ban_reset_time = 10 diff --git a/server/handler/antiBruteForce.go b/server/handler/antiBruteForce.go index 48f1bbc..c84902a 100644 --- a/server/handler/antiBruteForce.go +++ b/server/handler/antiBruteForce.go @@ -54,6 +54,7 @@ func antiBruteForce(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // 如果最大验证失败次数为0,则不启用防爆破功能 if base.Cfg.MaxBanCount == 0 { + next.ServeHTTP(w, r) return } body, err := io.ReadAll(r.Body) From 2fedb281e8e189828c98482ab68bc2baf2e4ac7a Mon Sep 17 00:00:00 2001 From: wsczx Date: Thu, 3 Oct 2024 23:29:41 +0800 Subject: [PATCH 03/48] =?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 04/48] =?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 05/48] =?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 06/48] =?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 07/48] =?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{} From 11bd9861e5e9c44007d38eb56fb4c4894313c245 Mon Sep 17 00:00:00 2001 From: wsczx Date: Mon, 7 Oct 2024 17:11:56 +0800 Subject: [PATCH 08/48] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E5=BC=B9=E7=AA=97?= =?UTF-8?q?=E8=BE=93=E5=85=A5OTP=E5=8A=A8=E6=80=81=E7=A0=81=E7=9A=84?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/dbdata/user.go | 16 +-- server/handler/antiBruteForce.go | 7 +- server/handler/link_auth.go | 106 +++++++-------- server/handler/link_auth_otp.go | 222 +++++++++++++++++++++++++++++++ server/handler/link_base.go | 3 + server/handler/server.go | 1 + 6 files changed, 288 insertions(+), 67 deletions(-) create mode 100644 server/handler/link_auth_otp.go diff --git a/server/dbdata/user.go b/server/dbdata/user.go index a84ddaf..18db099 100644 --- a/server/dbdata/user.go +++ b/server/dbdata/user.go @@ -117,13 +117,13 @@ func checkLocalUser(name, pwd, group string) error { } // 判断otp信息 pinCode := pwd - if !v.DisableOtp { - pinCode = pwd[:pl-6] - otp := pwd[pl-6:] - if !checkOtp(name, otp, v.OtpSecret) { - return fmt.Errorf("%s %s", name, "动态码错误") - } - } + // if !v.DisableOtp { + // pinCode = pwd[:pl-6] + // otp := pwd[pl-6:] + // if !CheckOtp(name, otp, v.OtpSecret) { + // return fmt.Errorf("%s %s", name, "动态码错误") + // } + // } // 判断用户密码 if pinCode != v.PinCode { @@ -171,7 +171,7 @@ func init() { } // 判断令牌信息 -func checkOtp(name, otp, secret string) bool { +func CheckOtp(name, otp, secret string) bool { key := fmt.Sprintf("%s:%s", name, otp) userOtpMux.Lock() diff --git a/server/handler/antiBruteForce.go b/server/handler/antiBruteForce.go index 947a8a9..248f8d2 100644 --- a/server/handler/antiBruteForce.go +++ b/server/handler/antiBruteForce.go @@ -3,7 +3,6 @@ package handler import ( "encoding/xml" "io" - "log" "net" "net/http" "strings" @@ -85,21 +84,21 @@ func antiBruteForce(next http.Handler) http.Handler { // 检查全局 IP 锁定 if base.Cfg.MaxGlobalIPBanCount > 0 && lockManager.checkGlobalIPLock(ip, now) { - log.Printf("IP %s is globally locked. Try again later.", ip) + base.Warn("IP", ip, "is globally locked. Try again later.") http.Error(w, "Account globally locked due to too many failed attempts. Try again later.", http.StatusTooManyRequests) return } // 检查全局用户锁定 if base.Cfg.MaxGlobalUserBanCount > 0 && lockManager.checkGlobalUserLock(username, now) { - log.Printf("User %s is globally locked. Try again later.", username) + base.Warn("User", username, "is globally locked. Try again later.") 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) + base.Warn("IP", ip, "is locked for user", username, "Try again later.") http.Error(w, "Account locked due to too many failed attempts. Try again later.", http.StatusTooManyRequests) return } diff --git a/server/handler/link_auth.go b/server/handler/link_auth.go index 8924290..fe0be6d 100644 --- a/server/handler/link_auth.go +++ b/server/handler/link_auth.go @@ -3,11 +3,9 @@ package handler import ( "bytes" "context" - "crypto/md5" "encoding/xml" "fmt" "io" - "net" "net/http" "net/http/httputil" "strings" @@ -47,7 +45,10 @@ func LinkAuth(w http.ResponseWriter, r *http.Request) { } defer r.Body.Close() - cr := ClientRequest{} + cr := &ClientRequest{ + RemoteAddr: r.RemoteAddr, + UserAgent: userAgent, + } err = xml.Unmarshal(body, &cr) if err != nil { base.Error(err) @@ -78,7 +79,7 @@ func LinkAuth(w http.ResponseWriter, r *http.Request) { return } // 用户活动日志 - ua := dbdata.UserActLog{ + ua := &dbdata.UserActLog{ Username: cr.Auth.Username, GroupName: cr.GroupSelect, RemoteAddr: r.RemoteAddr, @@ -86,6 +87,11 @@ func LinkAuth(w http.ResponseWriter, r *http.Request) { DeviceType: cr.DeviceId.DeviceType, PlatformVersion: cr.DeviceId.PlatformVersion, } + + sessionData := &AuthSession{ + ClientRequest: cr, + UserActLog: ua, + } // TODO 用户密码校验 err = dbdata.CheckUser(cr.Auth.Username, cr.Auth.Password, cr.GroupSelect) if err != nil { @@ -93,7 +99,7 @@ func LinkAuth(w http.ResponseWriter, r *http.Request) { base.Warn(err, r.RemoteAddr) ua.Info = err.Error() ua.Status = dbdata.UserAuthFail - dbdata.UserActLogIns.Add(ua, userAgent) + dbdata.UserActLogIns.Add(*ua, userAgent) w.WriteHeader(http.StatusOK) data := RequestData{Group: cr.GroupSelect, Groups: dbdata.GetGroupNamesNormal(), Error: "用户名或密码错误"} @@ -104,72 +110,62 @@ func LinkAuth(w http.ResponseWriter, r *http.Request) { return } r = r.WithContext(context.WithValue(r.Context(), loginStatusKey, true)) // 传递登录成功状态 - dbdata.UserActLogIns.Add(ua, userAgent) - // if !ok { - // w.WriteHeader(http.StatusOK) - // data := RequestData{Group: cr.GroupSelect, Groups: base.Cfg.UserGroups, Error: "请先激活用户"} - // tplRequest(tpl_request, w, data) - // return - // } + dbdata.UserActLogIns.Add(*ua, userAgent) - // 创建新的session信息 - sess := sessdata.NewSession("") - sess.Username = cr.Auth.Username - sess.Group = cr.GroupSelect - oriMac := cr.MacAddressList.MacAddress - sess.UniqueIdGlobal = cr.DeviceId.UniqueIdGlobal - sess.UserAgent = userAgent - sess.DeviceType = ua.DeviceType - sess.PlatformVersion = ua.PlatformVersion - sess.RemoteAddr = r.RemoteAddr - // 获取客户端mac地址 - sess.UniqueMac = true - macHw, err := net.ParseMAC(oriMac) + v := &dbdata.User{} + err = dbdata.One("Username", cr.Auth.Username, v) if err != nil { - var sum [16]byte - if sess.UniqueIdGlobal != "" { - sum = md5.Sum([]byte(sess.UniqueIdGlobal)) - } else { - sum = md5.Sum([]byte(sess.Token)) - sess.UniqueMac = false - } - macHw = sum[0:5] // 5个byte - macHw = append([]byte{0x02}, macHw...) - sess.MacAddr = macHw.String() + base.Error("Failed to get TOTP secret for user:", cr.Auth.Username, err) + http.Error(w, "Failed to get TOTP secret", http.StatusInternalServerError) + return } - sess.MacHw = macHw - // 统一macAddr的格式 - sess.MacAddr = macHw.String() + // 用户otp验证 + if !v.DisableOtp { + sessionID, err := GenerateSessionID() + if err != nil { + base.Error("Failed to generate session ID: ", err) + http.Error(w, "Failed to generate session ID", http.StatusInternalServerError) + return + } - other := &dbdata.SettingOther{} - _ = dbdata.SettingGet(other) - rd := RequestData{SessionId: sess.Sid, SessionToken: sess.Sid + "@" + sess.Token, - Banner: other.Banner, ProfileName: base.Cfg.ProfileName, ProfileHash: profileHash, CertHash: certHash} - w.WriteHeader(http.StatusOK) - tplRequest(tpl_complete, w, rd) - base.Info("login", cr.Auth.Username, userAgent) + sessionData.ClientRequest.Auth.OtpSecret = v.OtpSecret + SessStore.SaveAuthSession(sessionID, sessionData) + + SetCookie(w, "auth-session-id", sessionID, 0) + + data := RequestData{} + w.WriteHeader(http.StatusOK) + tplRequest(tpl_otp, w, data) + return + } + + CreateSession(w, r, sessionData) } const ( tpl_request = iota tpl_complete + tpl_otp ) func tplRequest(typ int, w io.Writer, data RequestData) { - if typ == tpl_request { + switch typ { + case tpl_request: t, _ := template.New("auth_request").Parse(auth_request) _ = t.Execute(w, data) - return - } + case tpl_complete: + if data.Banner != "" { + buf := new(bytes.Buffer) + _ = xml.EscapeText(buf, []byte(data.Banner)) + data.Banner = buf.String() + } - if data.Banner != "" { - buf := new(bytes.Buffer) - _ = xml.EscapeText(buf, []byte(data.Banner)) - data.Banner = buf.String() + t, _ := template.New("auth_complete").Parse(auth_complete) + _ = t.Execute(w, data) + case tpl_otp: + t, _ := template.New("auth_otp").Parse(auth_otp) + _ = t.Execute(w, data) } - - t, _ := template.New("auth_complete").Parse(auth_complete) - _ = t.Execute(w, data) } // 设置输出信息 diff --git a/server/handler/link_auth_otp.go b/server/handler/link_auth_otp.go new file mode 100644 index 0000000..313aa63 --- /dev/null +++ b/server/handler/link_auth_otp.go @@ -0,0 +1,222 @@ +package handler + +import ( + "crypto/md5" + "crypto/rand" + "crypto/sha256" + "encoding/base64" + "encoding/xml" + "fmt" + "io" + "net" + "net/http" + "sync" + + "github.com/bjdgyc/anylink/base" + "github.com/bjdgyc/anylink/dbdata" + "github.com/bjdgyc/anylink/sessdata" +) + +var SessStore = NewSessionStore() + +type AuthSession struct { + ClientRequest *ClientRequest + UserActLog *dbdata.UserActLog +} + +// 存储临时会话信息 +type SessionStore struct { + session map[string]*AuthSession + mu sync.Mutex +} + +func NewSessionStore() *SessionStore { + return &SessionStore{ + session: make(map[string]*AuthSession), + } +} + +func (s *SessionStore) SaveAuthSession(sessionID string, session *AuthSession) { + s.mu.Lock() + defer s.mu.Unlock() + s.session[sessionID] = session +} + +func (s *SessionStore) GetAuthSession(sessionID string) (*AuthSession, error) { + s.mu.Lock() + defer s.mu.Unlock() + + session, exists := s.session[sessionID] + if !exists { + return nil, fmt.Errorf("auth session not found") + } + + return session, nil +} + +func (s *SessionStore) DeleteAuthSession(sessionID string) { + s.mu.Lock() + defer s.mu.Unlock() + delete(s.session, sessionID) +} + +func GenerateSessionID() (string, error) { + b := make([]byte, 32) + _, err := rand.Read(b) + if err != nil { + return "", fmt.Errorf("failed to generate session ID: %w", err) + } + + hash := sha256.Sum256(b) + sessionID := base64.URLEncoding.EncodeToString(hash[:]) + return sessionID, nil +} + +func SetCookie(w http.ResponseWriter, name, value string, maxAge int) { + cookie := &http.Cookie{ + Name: name, + Value: value, + MaxAge: maxAge, + Path: "/", + HttpOnly: true, + Secure: true, + SameSite: http.SameSiteLaxMode, + } + http.SetCookie(w, cookie) +} + +func GetCookie(r *http.Request, name string) (string, error) { + cookie, err := r.Cookie(name) + if err != nil { + return "", fmt.Errorf("failed to get cookie: %v", err) + } + return cookie.Value, nil +} + +func DeleteCookie(w http.ResponseWriter, name string) { + cookie := &http.Cookie{ + Name: name, + Value: "", + MaxAge: -1, + Path: "/", + HttpOnly: true, + Secure: true, + SameSite: http.SameSiteStrictMode, + } + http.SetCookie(w, cookie) +} +func CreateSession(w http.ResponseWriter, r *http.Request, authSession *AuthSession) { + cr := authSession.ClientRequest + ua := authSession.UserActLog + + sess := sessdata.NewSession("") + sess.Username = cr.Auth.Username + sess.Group = cr.GroupSelect + oriMac := cr.MacAddressList.MacAddress + sess.UniqueIdGlobal = cr.DeviceId.UniqueIdGlobal + sess.UserAgent = cr.UserAgent + sess.DeviceType = ua.DeviceType + sess.PlatformVersion = ua.PlatformVersion + sess.RemoteAddr = cr.RemoteAddr + // 获取客户端mac地址 + sess.UniqueMac = true + macHw, err := net.ParseMAC(oriMac) + if err != nil { + var sum [16]byte + if sess.UniqueIdGlobal != "" { + sum = md5.Sum([]byte(sess.UniqueIdGlobal)) + } else { + sum = md5.Sum([]byte(sess.Token)) + sess.UniqueMac = false + } + macHw = sum[0:5] // 5个byte + macHw = append([]byte{0x02}, macHw...) + sess.MacAddr = macHw.String() + } + sess.MacHw = macHw + // 统一macAddr的格式 + sess.MacAddr = macHw.String() + + other := &dbdata.SettingOther{} + dbdata.SettingGet(other) + rd := RequestData{ + SessionId: sess.Sid, + SessionToken: sess.Sid + "@" + sess.Token, + Banner: other.Banner, + ProfileName: base.Cfg.ProfileName, + ProfileHash: profileHash, + CertHash: certHash, + } + + w.WriteHeader(http.StatusOK) + tplRequest(tpl_complete, w, rd) + base.Info("login", cr.Auth.Username, cr.UserAgent) +} + +func LinkAuth_otp(w http.ResponseWriter, r *http.Request) { + sessionID, err := GetCookie(r, "auth-session-id") + if err != nil { + http.Error(w, "Invalid session, please login again", http.StatusUnauthorized) + return + } + + sessionData, err := SessStore.GetAuthSession(sessionID) + if err != nil { + http.Error(w, "Invalid session, please login again", http.StatusUnauthorized) + return + } + + body, err := io.ReadAll(r.Body) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + return + } + defer r.Body.Close() + + cr := ClientRequest{} + err = xml.Unmarshal(body, &cr) + if err != nil { + base.Error(err) + w.WriteHeader(http.StatusBadRequest) + return + } + + ua := sessionData.UserActLog + username := sessionData.ClientRequest.Auth.Username + otpSecret := sessionData.ClientRequest.Auth.OtpSecret + otp := cr.Auth.SecondaryPassword + + if !dbdata.CheckOtp(username, otp, otpSecret) { + base.Warn("OTP 动态码错误", r.RemoteAddr) + ua.Info = "OTP 动态码错误" + ua.Status = dbdata.UserAuthFail + dbdata.UserActLogIns.Add(*ua, sessionData.ClientRequest.UserAgent) + + w.WriteHeader(http.StatusOK) + data := RequestData{Error: "OTP 动态码错误"} + if base.Cfg.DisplayError { + data.Error = "OTP 动态码错误" + } + tplRequest(tpl_otp, w, data) + return + } + CreateSession(w, r, sessionData) + + // 删除临时会话信息 + SessStore.DeleteAuthSession(sessionID) + // DeleteCookie(w, "auth-session-id") +} + +var auth_otp = ` + + + OTP 动态码验证 + 请输入您的 OTP 动态码 + {{if .Error}} + 验证失败: %s + {{end}} +
+ +
+
+
` diff --git a/server/handler/link_base.go b/server/handler/link_base.go index c46da15..95c3e5d 100644 --- a/server/handler/link_base.go +++ b/server/handler/link_base.go @@ -17,6 +17,8 @@ type ClientRequest struct { Version string `xml:"version"` // 客户端版本号 GroupAccess string `xml:"group-access"` // 请求的地址 GroupSelect string `xml:"group-select"` // 选择的组名 + RemoteAddr string `xml:"remote_addr"` + UserAgent string `xml:"user_agent"` SessionId string `xml:"session-id"` SessionToken string `xml:"session-token"` Auth auth `xml:"auth"` @@ -27,6 +29,7 @@ type ClientRequest struct { type auth struct { Username string `xml:"username"` Password string `xml:"password"` + OtpSecret string `xml:"otp_secret"` SecondaryPassword string `xml:"secondary_password"` } diff --git a/server/handler/server.go b/server/handler/server.go index 2fc5baf..bf0a140 100644 --- a/server/handler/server.go +++ b/server/handler/server.go @@ -114,6 +114,7 @@ func initRoute() http.Handler { r.Handle("/", antiBruteForce(http.HandlerFunc(LinkAuth))).Methods(http.MethodPost) r.HandleFunc("/CSCOSSLC/tunnel", LinkTunnel).Methods(http.MethodConnect) r.HandleFunc("/otp_qr", LinkOtpQr).Methods(http.MethodGet) + r.HandleFunc("/otp-verification", LinkAuth_otp) r.HandleFunc(fmt.Sprintf("/profile_%s.xml", base.Cfg.ProfileName), func(w http.ResponseWriter, r *http.Request) { b, _ := os.ReadFile(base.Cfg.Profile) w.Write(b) From fd383b92f5701a1a43e141b7e60bb67c92b56368 Mon Sep 17 00:00:00 2001 From: wsczx Date: Mon, 7 Oct 2024 17:31:21 +0800 Subject: [PATCH 09/48] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E4=BD=BF=E7=94=A8?= =?UTF-8?q?=E7=AC=AC=E4=B8=89=E6=96=B9=E9=AA=8C=E8=AF=81=E6=96=B9=E5=BC=8F?= =?UTF-8?q?=E6=97=A0=E6=B3=95=E5=BB=BA=E7=AB=8B=E8=BF=9E=E6=8E=A5=E7=9A=84?= =?UTF-8?q?Bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/handler/link_auth.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/handler/link_auth.go b/server/handler/link_auth.go index fe0be6d..cabe728 100644 --- a/server/handler/link_auth.go +++ b/server/handler/link_auth.go @@ -115,8 +115,8 @@ func LinkAuth(w http.ResponseWriter, r *http.Request) { v := &dbdata.User{} err = dbdata.One("Username", cr.Auth.Username, v) if err != nil { - base.Error("Failed to get TOTP secret for user:", cr.Auth.Username, err) - http.Error(w, "Failed to get TOTP secret", http.StatusInternalServerError) + base.Info("正在使用第三方认证方式登录") + CreateSession(w, r, sessionData) return } // 用户otp验证 From 4c219a3127324a2ade6d42249bd1558288a49534 Mon Sep 17 00:00:00 2001 From: wsczx Date: Tue, 8 Oct 2024 00:15:59 +0800 Subject: [PATCH 10/48] =?UTF-8?q?=E5=88=A0=E9=99=A4CheckUser=E6=B5=8B?= =?UTF-8?q?=E8=AF=95=E5=8D=95=E5=85=83=E7=9A=84otp=E9=AA=8C=E8=AF=81?= =?UTF-8?q?=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/dbdata/user_test.go | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/server/dbdata/user_test.go b/server/dbdata/user_test.go index 2238837..ea54918 100644 --- a/server/dbdata/user_test.go +++ b/server/dbdata/user_test.go @@ -4,7 +4,6 @@ import ( "testing" "github.com/stretchr/testify/assert" - "github.com/xlzd/gotp" ) func TestCheckUser(t *testing.T) { @@ -30,10 +29,10 @@ func TestCheckUser(t *testing.T) { ast.Nil(err) // 验证 PinCode + OtpSecret - totp := gotp.NewDefaultTOTP(u.OtpSecret) - secret := totp.Now() - err = CheckUser("aaa", u.PinCode+secret, group) - ast.Nil(err) + // totp := gotp.NewDefaultTOTP(u.OtpSecret) + // secret := totp.Now() + // err = CheckUser("aaa", u.PinCode+secret, group) + // ast.Nil(err) // 单独验证密码 u.DisableOtp = true From 1c5b269aa31a916dcfdab8e85093e8d74dcf6ed7 Mon Sep 17 00:00:00 2001 From: bjdgyc Date: Tue, 22 Oct 2024 13:22:45 +0800 Subject: [PATCH 11/48] =?UTF-8?q?=E4=BF=AE=E6=94=B9=20postgres=20=E9=93=BE?= =?UTF-8?q?=E6=8E=A5=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 0fce86e..cdaa584 100644 --- a/README.md +++ b/README.md @@ -59,7 +59,6 @@ AnyLink 服务端仅在 CentOS 7、CentOS 8、Ubuntu 18.04、Ubuntu 20.04 测试 > > - ### 使用问题 > 对于测试环境,可以直接进行测试,需要客户端取消勾选【阻止不受信任的服务器(Block connections to untrusted servers)】 @@ -161,11 +160,11 @@ sudo ./anylink > > 数据库表结构自动生成,无需手动导入(请赋予 DDL 权限) -| db_type | db_source | -|----------|--------------------------------------------------------| -| sqlite3 | ./conf/anylink.db | -| mysql | user:password@tcp(127.0.0.1:3306)/anylink?charset=utf8 | -| postgres | user:password@localhost/anylink?sslmode=verify-full | +| db_type | db_source | +|----------|----------------------------------------------------------------| +| sqlite3 | ./conf/anylink.db | +| mysql | user:password@tcp(127.0.0.1:3306)/anylink?charset=utf8 | +| postgres | postgres://user:password@localhost/anylink?sslmode=verify-full | > 示例配置文件 > From c0c15815f9e8f24eb35671a2a037edfaf46ee943 Mon Sep 17 00:00:00 2001 From: bjdgyc Date: Tue, 22 Oct 2024 14:14:10 +0800 Subject: [PATCH 12/48] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/base/cfg.go | 7 ++++ server/sessdata/online.go | 67 +++++++++++++++++++---------------- version | 2 +- web/src/pages/user/Online.vue | 5 ++- 4 files changed, 49 insertions(+), 32 deletions(-) diff --git a/server/base/cfg.go b/server/base/cfg.go index 6c31310..fba15f0 100644 --- a/server/base/cfg.go +++ b/server/base/cfg.go @@ -2,9 +2,11 @@ package base import ( "fmt" + "github.com/bjdgyc/anylink/pkg/utils" "os" "path/filepath" "reflect" + "strings" ) const ( @@ -123,6 +125,11 @@ func initServerCfg() { if Cfg.JwtSecret == defaultJwt { fmt.Fprintln(os.Stderr, "=== 使用默认的jwt_secret有安全风险,请设置新的jwt_secret ===") + + // 安全问题,自动生成新的密钥 + jwtSecret, _ := utils.RandSecret(40, 60) + jwtSecret = strings.Trim(jwtSecret, "=") + Cfg.JwtSecret = jwtSecret } fmt.Printf("ServerCfg: %+v \n", Cfg) diff --git a/server/sessdata/online.go b/server/sessdata/online.go index 8785c19..89c39d7 100644 --- a/server/sessdata/online.go +++ b/server/sessdata/online.go @@ -11,21 +11,22 @@ import ( ) type Online struct { - Token string `json:"token"` - Username string `json:"username"` - Group string `json:"group"` - MacAddr string `json:"mac_addr"` - UniqueMac bool `json:"unique_mac"` - Ip net.IP `json:"ip"` - RemoteAddr string `json:"remote_addr"` - TunName string `json:"tun_name"` - Mtu int `json:"mtu"` - Client string `json:"client"` - BandwidthUp string `json:"bandwidth_up"` - BandwidthDown string `json:"bandwidth_down"` - BandwidthUpAll string `json:"bandwidth_up_all"` - BandwidthDownAll string `json:"bandwidth_down_all"` - LastLogin time.Time `json:"last_login"` + Token string `json:"token"` + Username string `json:"username"` + Group string `json:"group"` + MacAddr string `json:"mac_addr"` + UniqueMac bool `json:"unique_mac"` + Ip net.IP `json:"ip"` + RemoteAddr string `json:"remote_addr"` + TransportProtocol string `json:"transport_protocol"` + TunName string `json:"tun_name"` + Mtu int `json:"mtu"` + Client string `json:"client"` + BandwidthUp string `json:"bandwidth_up"` + BandwidthDown string `json:"bandwidth_down"` + BandwidthUpAll string `json:"bandwidth_up_all"` + BandwidthDownAll string `json:"bandwidth_down_all"` + LastLogin time.Time `json:"last_login"` } type Onlines []Online @@ -90,22 +91,28 @@ func GetOnlineSess(search_cate string, search_text string, show_sleeper bool) [] } if show_sleeper || v.IsActive { + transportProtocol := "TCP" + dSess := cSess.GetDtlsSession() + if dSess != nil { + transportProtocol = "UDP" + } val := Online{ - Token: v.Token, - Ip: cSess.IpAddr, - Username: v.Username, - Group: v.Group, - MacAddr: v.MacAddr, - UniqueMac: v.UniqueMac, - RemoteAddr: cSess.RemoteAddr, - TunName: cSess.IfName, - Mtu: cSess.Mtu, - Client: cSess.Client, - BandwidthUp: utils.HumanByte(cSess.BandwidthUpPeriod.Load()) + "/s", - BandwidthDown: utils.HumanByte(cSess.BandwidthDownPeriod.Load()) + "/s", - BandwidthUpAll: utils.HumanByte(cSess.BandwidthUpAll.Load()), - BandwidthDownAll: utils.HumanByte(cSess.BandwidthDownAll.Load()), - LastLogin: v.LastLogin, + Token: v.Token, + Ip: cSess.IpAddr, + Username: v.Username, + Group: v.Group, + MacAddr: v.MacAddr, + UniqueMac: v.UniqueMac, + RemoteAddr: cSess.RemoteAddr, + TransportProtocol: transportProtocol, + TunName: cSess.IfName, + Mtu: cSess.Mtu, + Client: cSess.Client, + BandwidthUp: utils.HumanByte(cSess.BandwidthUpPeriod.Load()) + "/s", + BandwidthDown: utils.HumanByte(cSess.BandwidthDownPeriod.Load()) + "/s", + BandwidthUpAll: utils.HumanByte(cSess.BandwidthUpAll.Load()), + BandwidthDownAll: utils.HumanByte(cSess.BandwidthDownAll.Load()), + LastLogin: v.LastLogin, } datas = append(datas, val) } diff --git a/version b/version index e96a871..d61567c 100644 --- a/version +++ b/version @@ -1 +1 @@ -0.12.2 \ No newline at end of file +0.12.3 \ No newline at end of file diff --git a/web/src/pages/user/Online.vue b/web/src/pages/user/Online.vue index 2ba25ad..d3a2b49 100644 --- a/web/src/pages/user/Online.vue +++ b/web/src/pages/user/Online.vue @@ -99,7 +99,10 @@ prop="remote_addr" label="远端地址"> - + + From a0569b09f2b88e5cd04876b3d121f342f87ed9d7 Mon Sep 17 00:00:00 2001 From: bjdgyc Date: Wed, 23 Oct 2024 17:34:03 +0800 Subject: [PATCH 13/48] =?UTF-8?q?=E4=BC=98=E5=8C=96ip=E5=88=86=E9=85=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/sessdata/ip_pool.go | 145 +++++++++++++++++++++++------------ web/src/pages/user/IpMap.vue | 1 + 2 files changed, 96 insertions(+), 50 deletions(-) diff --git a/server/sessdata/ip_pool.go b/server/sessdata/ip_pool.go index e1971a5..8d49496 100644 --- a/server/sessdata/ip_pool.go +++ b/server/sessdata/ip_pool.go @@ -13,11 +13,9 @@ import ( var ( IpPool = &ipPoolConfig{} ipActive = map[string]bool{} - // ipKeep and ipLease ipAddr => type - // ipLease = map[string]bool{} + // ipKeep and ipLease ipAddr => macAddr + // ipKeep = map[string]string{} ipPoolMux sync.Mutex - // 记录循环点 - loopCurIp uint32 ) type ipPoolConfig struct { @@ -73,17 +71,16 @@ func initIpPool() { // func getIpLease() { // xdb := dbdata.GetXdb() // keepIpMaps := []dbdata.IpMap{} -// sNow := time.Now().Add(-1 * time.Duration(base.Cfg.IpLease) * time.Second) -// err := xdb.Cols("ip_addr").Where("keep=?", true). -// Or("unique_mac=? and last_login>?", true, sNow).Find(&keepIpMaps) +// // sNow := time.Now().Add(-1 * time.Duration(base.Cfg.IpLease) * time.Second) +// err := xdb.Cols("ip_addr", "mac_addr").Where("keep=?", true).Find(&keepIpMaps) // if err != nil { // base.Error(err) // } -// // fmt.Println(keepIpMaps) +// log.Println(keepIpMaps) // ipPoolMux.Lock() -// ipLease = map[string]bool{} +// ipKeep = map[string]string{} // for _, v := range keepIpMaps { -// ipLease[v.IpAddr] = true +// ipKeep[v.IpAddr] = v.MacAddr // } // ipPoolMux.Unlock() // } @@ -95,6 +92,7 @@ func AcquireIp(username, macAddr string, uniqueMac bool) (newIp net.IP) { defer func() { ipPoolMux.Unlock() base.Trace("AcquireIp end:", username, macAddr, uniqueMac, newIp) + base.Info("AcquireIp ip:", username, macAddr, uniqueMac, newIp) }() var ( @@ -102,6 +100,7 @@ func AcquireIp(username, macAddr string, uniqueMac bool) (newIp net.IP) { tNow = time.Now() ) + // 获取到客户端 macAddr 的情况 if uniqueMac { // 判断是否已经分配过 mi := &dbdata.IpMap{} @@ -124,8 +123,9 @@ func AcquireIp(username, macAddr string, uniqueMac bool) (newIp net.IP) { _, ok := ipActive[ipStr] // 检测原有ip是否在新的ip池内 // IpPool.Ipv4IPNet.Contains(ip) && - if !ok && - utils.Ip2long(ip) >= IpPool.IpLongMin && + // ip符合规范 + // 检测原有ip是否在新的ip池内 + if !ok && utils.Ip2long(ip) >= IpPool.IpLongMin && utils.Ip2long(ip) <= IpPool.IpLongMax { mi.Username = username mi.LastLogin = tNow @@ -135,61 +135,84 @@ func AcquireIp(username, macAddr string, uniqueMac bool) (newIp net.IP) { ipActive[ipStr] = true return ip } - // 删除当前macAddr - mi = &dbdata.IpMap{MacAddr: macAddr} - _ = dbdata.Del(mi) - } else { - // 没有获取到mac的情况 - ipMaps := []dbdata.IpMap{} - err = dbdata.FindWhere(&ipMaps, 50, 1, "username=? and unique_mac=?", username, false) - if err != nil { - // 没有查询到数据 - if dbdata.CheckErrNotFound(err) { - return loopIp(username, macAddr, uniqueMac) - } - // 查询报错 - base.Error(err) + // ip保留 + if mi.Keep { + base.Error(username, macAddr, ipStr, "保留ip不匹配CIDR") return nil } - // 遍历mac记录 - for _, mi := range ipMaps { - ipStr := mi.IpAddr - ip := net.ParseIP(ipStr) + // 删除当前macAddr + mi = &dbdata.IpMap{MacAddr: macAddr} + _ = dbdata.Del(mi) + return loopIp(username, macAddr, uniqueMac) + } - // 跳过活跃连接 - if _, ok := ipActive[ipStr]; ok { - continue - } - // 跳过保留ip - if mi.Keep { - continue - } - // 没有mac的 不需要验证租期 - // mi.LastLogin.Before(leaseTime) && - if utils.Ip2long(ip) >= IpPool.IpLongMin && - utils.Ip2long(ip) <= IpPool.IpLongMax { - mi.LastLogin = tNow - mi.MacAddr = macAddr - mi.UniqueMac = uniqueMac - // 回写db数据 - _ = dbdata.Set(mi) - ipActive[ipStr] = true - return ip - } + // 没有获取到mac的情况 + ipMaps := []dbdata.IpMap{} + err = dbdata.FindWhere(&ipMaps, 50, 1, "username=?", username) + if err != nil { + // 没有查询到数据 + if dbdata.CheckErrNotFound(err) { + return loopIp(username, macAddr, uniqueMac) + } + // 查询报错 + base.Error(err) + return nil + } + + // 遍历mac记录 + for _, mi := range ipMaps { + ipStr := mi.IpAddr + ip := net.ParseIP(ipStr) + + // 跳过活跃连接 + if _, ok := ipActive[ipStr]; ok { + continue + } + // 跳过保留ip + if mi.Keep { + continue + } + if mi.UniqueMac { + continue + } + + // 没有mac的 不需要验证租期 + // mi.LastLogin.Before(leaseTime) && + if utils.Ip2long(ip) >= IpPool.IpLongMin && + utils.Ip2long(ip) <= IpPool.IpLongMax { + mi.Username = username + mi.LastLogin = tNow + mi.MacAddr = macAddr + mi.UniqueMac = uniqueMac + // 回写db数据 + _ = dbdata.Set(mi) + ipActive[ipStr] = true + return ip } } return loopIp(username, macAddr, uniqueMac) } +var ( + // 记录循环点 + loopCurIp uint32 + loopFarIp = &dbdata.IpMap{LastLogin: time.Now()} + loopFarI = uint32(0) +) + func loopIp(username, macAddr string, uniqueMac bool) net.IP { var ( i uint32 ip net.IP ) + // 重新赋值 + loopFarIp = &dbdata.IpMap{LastLogin: time.Now()} + loopFarI = uint32(0) + i, ip = loopLong(loopCurIp, IpPool.IpLongMax, username, macAddr, uniqueMac) if ip != nil { loopCurIp = i @@ -202,6 +225,22 @@ func loopIp(username, macAddr string, uniqueMac bool) net.IP { return ip } + // ip分配完,从头开始 + loopCurIp = IpPool.IpLongMin + + if loopFarIp.Id > 0 { + // 使用最早登陆的 ip + ipStr := loopFarIp.IpAddr + ip = net.ParseIP(ipStr) + mi := &dbdata.IpMap{IpAddr: ipStr, MacAddr: macAddr, UniqueMac: uniqueMac, Username: username, LastLogin: time.Now()} + // 回写db数据 + _ = dbdata.Set(mi) + ipActive[ipStr] = true + + return ip + } + + // 全都在线,没有数据可用 base.Warn("no ip available, please see ip_map table row", username, macAddr) return nil } @@ -247,6 +286,7 @@ func loopLong(start, end uint32, username, macAddr string, uniqueMac bool) (uint // 判断租期 if mi.LastLogin.Before(leaseTime) { // 存在记录,说明已经超过租期,可以直接使用 + mi.Username = username mi.LastLogin = tNow mi.MacAddr = macAddr mi.UniqueMac = uniqueMac @@ -255,6 +295,11 @@ func loopLong(start, end uint32, username, macAddr string, uniqueMac bool) (uint ipActive[ipStr] = true return i, ip } + // 其他情况判断最早登陆 + if mi.LastLogin.Before(loopFarIp.LastLogin) { + loopFarIp = mi + loopFarI = i + } } return 0, nil diff --git a/web/src/pages/user/IpMap.vue b/web/src/pages/user/IpMap.vue index 5b4755a..78d49e6 100644 --- a/web/src/pages/user/IpMap.vue +++ b/web/src/pages/user/IpMap.vue @@ -48,6 +48,7 @@ label="唯一MAC"> From cb9d023a9621147511c16c2776496e33f86eb700 Mon Sep 17 00:00:00 2001 From: bjdgyc Date: Wed, 23 Oct 2024 18:13:06 +0800 Subject: [PATCH 14/48] =?UTF-8?q?=E6=81=A2=E5=A4=8D=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/dbdata/user.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/server/dbdata/user.go b/server/dbdata/user.go index 18db099..b4d7817 100644 --- a/server/dbdata/user.go +++ b/server/dbdata/user.go @@ -117,13 +117,13 @@ func checkLocalUser(name, pwd, group string) error { } // 判断otp信息 pinCode := pwd - // if !v.DisableOtp { - // pinCode = pwd[:pl-6] - // otp := pwd[pl-6:] - // if !CheckOtp(name, otp, v.OtpSecret) { - // return fmt.Errorf("%s %s", name, "动态码错误") - // } - // } + if !v.DisableOtp { + pinCode = pwd[:pl-6] + otp := pwd[pl-6:] + if !CheckOtp(name, otp, v.OtpSecret) { + return fmt.Errorf("%s %s", name, "动态码错误") + } + } // 判断用户密码 if pinCode != v.PinCode { From 49b40b5ee4c3b89d35ff39b257948ba1b8c72767 Mon Sep 17 00:00:00 2001 From: bjdgyc Date: Wed, 23 Oct 2024 18:18:37 +0800 Subject: [PATCH 15/48] =?UTF-8?q?=E6=81=A2=E5=A4=8D=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/dbdata/user.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/server/dbdata/user.go b/server/dbdata/user.go index b4d7817..18db099 100644 --- a/server/dbdata/user.go +++ b/server/dbdata/user.go @@ -117,13 +117,13 @@ func checkLocalUser(name, pwd, group string) error { } // 判断otp信息 pinCode := pwd - if !v.DisableOtp { - pinCode = pwd[:pl-6] - otp := pwd[pl-6:] - if !CheckOtp(name, otp, v.OtpSecret) { - return fmt.Errorf("%s %s", name, "动态码错误") - } - } + // if !v.DisableOtp { + // pinCode = pwd[:pl-6] + // otp := pwd[pl-6:] + // if !CheckOtp(name, otp, v.OtpSecret) { + // return fmt.Errorf("%s %s", name, "动态码错误") + // } + // } // 判断用户密码 if pinCode != v.PinCode { From 772b1118ebde09c4a3b1ab3b617d597f9765e80a Mon Sep 17 00:00:00 2001 From: bjdgyc Date: Thu, 24 Oct 2024 11:20:45 +0800 Subject: [PATCH 16/48] =?UTF-8?q?=E4=BF=AE=E6=94=B9docker=E4=BB=A3?= =?UTF-8?q?=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 17 ++++++++++++++--- docker/Dockerfile | 4 +++- docker/docker_entrypoint.sh | 10 ++++++++-- docker/init_release.sh | 7 ++++--- server/base/mod.go | 2 +- web/src/pages/user/Online.vue | 1 + 6 files changed, 31 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index cdaa584..7c05590 100644 --- a/README.md +++ b/README.md @@ -390,7 +390,17 @@ ipv4_end = "10.1.2.200" docker run -it --rm bjdgyc/anylink tool -d ``` -6. 启动容器 +6. iptables兼容设置 + ```bash + # 默认 iptables 使用 nf_tables 设置转发规则,如果内核低于 4.19 版本,需要特殊配置 + docker run -itd --name anylink --privileged \ + -e IPTABLES_LEGACY=on \ + -p 443:443 -p 8800:8800 -p 443:443/udp \ + --restart=always \ + bjdgyc/anylink + ``` + +7. 启动容器 ```bash # 默认启动 docker run -itd --name anylink --privileged \ @@ -410,7 +420,7 @@ ipv4_end = "10.1.2.200" docker restart anylink ``` -6. 使用自定义参数启动容器 +8. 使用自定义参数启动容器 ```bash # 参数可以参考 ./anylink tool -d # 可以使用命令行参数 或者 环境变量 配置 @@ -459,7 +469,8 @@ ipv4_end = "10.1.2.200" - [AnyConnect Secure Client](https://www.cisco.com/) (可通过群文件下载: Windows/macOS/Linux/Android/iOS) - [OpenConnect](https://gitlab.com/openconnect/openconnect) (Windows/macOS/Linux) - [三方 AnyLink Secure Client](https://github.com/tlslink/anylink-client) (Windows/macOS/Linux) -- 【推荐】三方客户端下载地址(Windows/macOS/Linux/Android/iOS) [国内地址](https://ocserv.yydy.link:2023) [国外地址](https://cisco.yydy.link/#/) +- 【推荐】三方客户端下载地址( + Windows/macOS/Linux/Android/iOS) [国内地址](https://ocserv.yydy.link:2023) [国外地址](https://cisco.yydy.link/#/) ## Contribution diff --git a/docker/Dockerfile b/docker/Dockerfile index 909e352..a7b5cd3 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -38,7 +38,9 @@ LABEL maintainer="github.com/bjdgyc" ARG CN="no" ENV TZ=Asia/Shanghai -ENV ANYLINK_IN_CONTAINER=true +#开关变量 on off +ENV ANYLINK_IN_CONTAINER="on" +ENV IPTABLES_LEGACY="off" WORKDIR /app COPY docker/init_release.sh /tmp/ diff --git a/docker/docker_entrypoint.sh b/docker/docker_entrypoint.sh index e2e00a1..e48921e 100644 --- a/docker/docker_entrypoint.sh +++ b/docker/docker_entrypoint.sh @@ -1,4 +1,4 @@ -#!/bin/sh +#!/bin/bash var1=$1 #set -x @@ -19,13 +19,19 @@ case $var1 in #iptables -nL -t nat # 启动服务 先判断配置文件是否存在 - if [ ! -f /app/conf/profile.xml ]; then + if [[ ! -f /app/conf/profile.xml ]]; then /bin/cp -r /home/conf-bak/* /app/conf/ echo "After the configuration file is initialized, the container will be forcibly exited. Restart the container." echo "配置文件初始化完成后,容器会强制退出,请重新启动容器。" exit 1 fi + # 兼容老版本 iptables + if [[ $IPTABLES_LEGACY == "on" ]]; then + rm /sbin/iptables + ln -s /sbin/iptables-legacy /sbin/iptables + fi + exec /app/anylink "$@" ;; esac diff --git a/docker/init_release.sh b/docker/init_release.sh index 1d2df88..e83071f 100644 --- a/docker/init_release.sh +++ b/docker/init_release.sh @@ -13,10 +13,11 @@ fi # docker 启动使用 4.19 以上内核 apk add --no-cache ca-certificates bash iproute2 tzdata iptables -# alpine:3.19 兼容老版 iptables +# alpine:3.19 兼容老版本 iptables apk add --no-cache iptables-legacy -rm /sbin/iptables -ln -s /sbin/iptables-legacy /sbin/iptables + +#rm /sbin/iptables +#ln -s /sbin/iptables-legacy /sbin/iptables chmod +x /app/docker_entrypoint.sh diff --git a/server/base/mod.go b/server/base/mod.go index 481e576..28fde7e 100644 --- a/server/base/mod.go +++ b/server/base/mod.go @@ -22,7 +22,7 @@ var ( func initMod() { container := os.Getenv(inContainerKey) - if container == "true" { + if container == "on" { InContainer = true } log.Println("InContainer", InContainer) diff --git a/web/src/pages/user/Online.vue b/web/src/pages/user/Online.vue index d3a2b49..1e506a2 100644 --- a/web/src/pages/user/Online.vue +++ b/web/src/pages/user/Online.vue @@ -86,6 +86,7 @@ label="唯一MAC"> From bd6ee0b1405416e84bc80dd75cb0abe190986d1c Mon Sep 17 00:00:00 2001 From: bjdgyc Date: Thu, 24 Oct 2024 18:10:29 +0800 Subject: [PATCH 17/48] =?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/link_auth.go | 1 - server/handler/link_auth_otp.go | 34 +++++++++++++++++++++------------ server/pkg/utils/util.go | 16 ++++++++++++++-- server/sessdata/session.go | 22 +++------------------ 4 files changed, 39 insertions(+), 34 deletions(-) diff --git a/server/handler/link_auth.go b/server/handler/link_auth.go index cabe728..fa42c96 100644 --- a/server/handler/link_auth.go +++ b/server/handler/link_auth.go @@ -159,7 +159,6 @@ func tplRequest(typ int, w io.Writer, data RequestData) { _ = xml.EscapeText(buf, []byte(data.Banner)) data.Banner = buf.String() } - t, _ := template.New("auth_complete").Parse(auth_complete) _ = t.Execute(w, data) case tpl_otp: diff --git a/server/handler/link_auth_otp.go b/server/handler/link_auth_otp.go index 313aa63..683c577 100644 --- a/server/handler/link_auth_otp.go +++ b/server/handler/link_auth_otp.go @@ -2,26 +2,28 @@ package handler import ( "crypto/md5" - "crypto/rand" - "crypto/sha256" - "encoding/base64" "encoding/xml" "fmt" "io" "net" "net/http" "sync" + "sync/atomic" "github.com/bjdgyc/anylink/base" "github.com/bjdgyc/anylink/dbdata" + "github.com/bjdgyc/anylink/pkg/utils" "github.com/bjdgyc/anylink/sessdata" ) var SessStore = NewSessionStore() +const maxOtpErrCount = 3 + type AuthSession struct { ClientRequest *ClientRequest UserActLog *dbdata.UserActLog + OtpErrCount atomic.Uint32 // otp错误次数 } // 存储临时会话信息 @@ -60,15 +62,17 @@ func (s *SessionStore) DeleteAuthSession(sessionID string) { delete(s.session, sessionID) } +func (a *AuthSession) AddOtpErrCount(i int) int { + newI := a.OtpErrCount.Add(uint32(i)) + return int(newI) +} + func GenerateSessionID() (string, error) { - b := make([]byte, 32) - _, err := rand.Read(b) - if err != nil { - return "", fmt.Errorf("failed to generate session ID: %w", err) + sessionID := utils.RandomRunes(32) + if sessionID == "" { + return "", fmt.Errorf("failed to generate session ID") } - hash := sha256.Sum256(b) - sessionID := base64.URLEncoding.EncodeToString(hash[:]) return sessionID, nil } @@ -186,14 +190,20 @@ func LinkAuth_otp(w http.ResponseWriter, r *http.Request) { otpSecret := sessionData.ClientRequest.Auth.OtpSecret otp := cr.Auth.SecondaryPassword + // 动态码错误 if !dbdata.CheckOtp(username, otp, otpSecret) { - base.Warn("OTP 动态码错误", r.RemoteAddr) + if sessionData.AddOtpErrCount(1) > maxOtpErrCount { + http.Error(w, "TooManyError, please login again", http.StatusBadRequest) + return + } + + base.Warn("OTP 动态码错误", username, r.RemoteAddr) ua.Info = "OTP 动态码错误" ua.Status = dbdata.UserAuthFail dbdata.UserActLogIns.Add(*ua, sessionData.ClientRequest.UserAgent) w.WriteHeader(http.StatusOK) - data := RequestData{Error: "OTP 动态码错误"} + data := RequestData{Error: "请求错误"} if base.Cfg.DisplayError { data.Error = "OTP 动态码错误" } @@ -216,7 +226,7 @@ var auth_otp = ` 验证失败: %s {{end}}
- +
` diff --git a/server/pkg/utils/util.go b/server/pkg/utils/util.go index 2b17f26..54f7df1 100644 --- a/server/pkg/utils/util.go +++ b/server/pkg/utils/util.go @@ -1,7 +1,10 @@ package utils import ( + crand "crypto/rand" + "encoding/hex" "fmt" + "log" "math/rand" "strings" "sync/atomic" @@ -83,9 +86,7 @@ func HumanByte(bf interface{}) string { func RandomRunes(length int) string { letterRunes := []rune("abcdefghijklmnpqrstuvwxy1234567890") - bytes := make([]rune, length) - for i := range bytes { bytes[i] = letterRunes[rand.Intn(len(letterRunes))] } @@ -93,6 +94,17 @@ func RandomRunes(length int) string { return string(bytes) } +func RandomHex(length int) string { + b := make([]byte, length) + _, err := crand.Read(b) + if err != nil { + log.Println(err) + return "" + } + + return hex.EncodeToString(b) +} + func ParseName(name string) string { name = strings.ReplaceAll(name, " ", "-") name = strings.ReplaceAll(name, "'", "-") diff --git a/server/sessdata/session.go b/server/sessdata/session.go index 60fae1d..9885419 100644 --- a/server/sessdata/session.go +++ b/server/sessdata/session.go @@ -2,7 +2,6 @@ package sessdata import ( "fmt" - "math/rand" "net" "strconv" "strings" @@ -12,6 +11,7 @@ import ( "github.com/bjdgyc/anylink/base" "github.com/bjdgyc/anylink/dbdata" + "github.com/bjdgyc/anylink/pkg/utils" mapset "github.com/deckarep/golang-set" ) @@ -91,10 +91,6 @@ type Session struct { CSess *ConnSession } -func init() { - rand.Seed(time.Now().UnixNano()) -} - func checkSession() { // 检测过期的session go func() { @@ -144,28 +140,16 @@ func CloseUserLimittimeSession() { } } -func GenToken() string { - // 生成32位的 token - bToken := make([]byte, 32) - rand.Read(bToken) - return fmt.Sprintf("%x", bToken) -} - func NewSession(token string) *Session { if token == "" { - btoken := make([]byte, 32) - rand.Read(btoken) - token = fmt.Sprintf("%x", btoken) + token = utils.RandomHex(32) } // 生成 dtlsn session_id - dtlsid := make([]byte, 32) - rand.Read(dtlsid) - sess := &Session{ Sid: fmt.Sprintf("%d", time.Now().Unix()), Token: token, - DtlsSid: fmt.Sprintf("%x", dtlsid), + DtlsSid: utils.RandomHex(32), LastLogin: time.Now(), } From 96fd114c25ff8a762690173d671f70048b46d7db Mon Sep 17 00:00:00 2001 From: bjdgyc Date: Fri, 25 Oct 2024 10:41:48 +0800 Subject: [PATCH 18/48] =?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/link_auth_otp.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/server/handler/link_auth_otp.go b/server/handler/link_auth_otp.go index 683c577..91ef5b3 100644 --- a/server/handler/link_auth_otp.go +++ b/server/handler/link_auth_otp.go @@ -172,6 +172,8 @@ func LinkAuth_otp(w http.ResponseWriter, r *http.Request) { body, err := io.ReadAll(r.Body) if err != nil { + base.Error(err) + SessStore.DeleteAuthSession(sessionID) w.WriteHeader(http.StatusBadRequest) return } @@ -181,6 +183,7 @@ func LinkAuth_otp(w http.ResponseWriter, r *http.Request) { err = xml.Unmarshal(body, &cr) if err != nil { base.Error(err) + SessStore.DeleteAuthSession(sessionID) w.WriteHeader(http.StatusBadRequest) return } @@ -193,6 +196,7 @@ func LinkAuth_otp(w http.ResponseWriter, r *http.Request) { // 动态码错误 if !dbdata.CheckOtp(username, otp, otpSecret) { if sessionData.AddOtpErrCount(1) > maxOtpErrCount { + SessStore.DeleteAuthSession(sessionID) http.Error(w, "TooManyError, please login again", http.StatusBadRequest) return } From fdc755bd98440c5e07c0d974777d61ccecd18a2d Mon Sep 17 00:00:00 2001 From: bjdgyc Date: Fri, 25 Oct 2024 12:00:45 +0800 Subject: [PATCH 19/48] =?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 --- deploy/docker-compose.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/deploy/docker-compose.yaml b/deploy/docker-compose.yaml index be08dcc..fe1f28c 100644 --- a/deploy/docker-compose.yaml +++ b/deploy/docker-compose.yaml @@ -12,6 +12,7 @@ services: - 443:443/udp environment: LINK_LOG_LEVEL: info + #IPTABLES_LEGACY: "on" command: - --conf=/app/conf/server.toml #volumes: From f8685490dc7b1f7870f99c03d7d83a288580641c Mon Sep 17 00:00:00 2001 From: wsczx Date: Sat, 26 Oct 2024 09:13:02 +0800 Subject: [PATCH 20/48] =?UTF-8?q?1.=E4=BF=AE=E5=A4=8D=E9=98=B2=E7=88=86?= =?UTF-8?q?=E7=AD=96=E7=95=A5=E7=94=A8=E6=88=B7=E7=99=BB=E5=BD=95=E6=88=90?= =?UTF-8?q?=E5=8A=9F=E5=90=8E=E6=B2=A1=E6=9C=89=E9=87=8D=E7=BD=AE=E8=AE=A1?= =?UTF-8?q?=E6=95=B0=E7=9A=84Bug=202.=E5=A2=9E=E5=8A=A0otp=E9=98=B2?= =?UTF-8?q?=E7=88=86=203.=E6=B7=BB=E5=8A=A0otp=E4=BD=BF=E7=94=A8=E8=AF=B4?= =?UTF-8?q?=E6=98=8E=204.=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/dbdata/db.go | 3 +- server/handler/antiBruteForce.go | 72 +++++++------- server/handler/link_auth.go | 5 +- server/handler/link_auth_otp.go | 3 + server/handler/server.go | 2 +- web/src/pages/user/List.vue | 166 ++++++++----------------------- 6 files changed, 86 insertions(+), 165 deletions(-) diff --git a/server/dbdata/db.go b/server/dbdata/db.go index f33ed37..388476b 100644 --- a/server/dbdata/db.go +++ b/server/dbdata/db.go @@ -203,7 +203,8 @@ const accountMail = `

您好:

  • 请使用OTP软件扫描动态码二维码
  • 然后使用anyconnect客户端进行登陆
  • -
  • 登陆密码为 【PIN码+动态码】(中间没有+号)
  • +
  • 登陆密码为 PIN 码
  • +
  • OTP密码为扫码后生成的动态码

diff --git a/server/handler/antiBruteForce.go b/server/handler/antiBruteForce.go index 248f8d2..da799f5 100644 --- a/server/handler/antiBruteForce.go +++ b/server/handler/antiBruteForce.go @@ -12,11 +12,7 @@ import ( "github.com/bjdgyc/anylink/base" ) -// 自定义 contextKey 类型,避免键冲突 -type contextKey string - -// 定义常量作为上下文的键 -const loginStatusKey contextKey = "login_status" +const loginStatusKey = "login_status" const defaultGlobalLockStateExpirationTime = 3600 func initAntiBruteForce() { @@ -53,6 +49,20 @@ func antiBruteForce(next http.Handler) http.Handler { } username := cr.Auth.Username + if r.URL.Path == "/otp-verification" { + sessionID, err := GetCookie(r, "auth-session-id") + if err != nil { + http.Error(w, "Invalid session, please login again", http.StatusUnauthorized) + return + } + + sessionData, err := SessStore.GetAuthSession(sessionID) + if err != nil { + http.Error(w, "Invalid session, please login again", http.StatusUnauthorized) + return + } + username = sessionData.ClientRequest.Auth.Username + } ip, _, err := net.SplitHostPort(r.RemoteAddr) // 提取纯 IP 地址,去掉端口号 if err != nil { http.Error(w, "Unable to parse IP address", http.StatusInternalServerError) @@ -109,13 +119,17 @@ func antiBruteForce(next http.Handler) http.Handler { // 调用下一个处理器 next.ServeHTTP(w, r) - // 从 context 中获取登录状态 - loginStatus, _ := r.Context().Value(loginStatusKey).(bool) + // 检查登录状态 + Status, _ := lockManager.loginStatus.Load(loginStatusKey) + loginStatus, _ := Status.(bool) // 更新用户登录状态 lockManager.updateGlobalIPLock(ip, now, loginStatus) lockManager.updateGlobalUserLock(username, now, loginStatus) lockManager.updateUserIPLock(username, ip, now, loginStatus) + + // 清除登录状态 + lockManager.loginStatus.Delete(loginStatusKey) }) } @@ -131,6 +145,7 @@ type IPWhitelists struct { type LockManager struct { mu sync.Mutex + loginStatus sync.Map // 登录状态 ipLocks map[string]*LockState // 全局IP锁定状态 userLocks map[string]*LockState // 全局用户锁定状态 ipUserLocks map[string]map[string]*LockState // 单用户IP锁定状态 @@ -140,6 +155,7 @@ type LockManager struct { } var lockManager = &LockManager{ + loginStatus: sync.Map{}, ipLocks: make(map[string]*LockState), userLocks: make(map[string]*LockState), ipUserLocks: make(map[string]map[string]*LockState), @@ -251,14 +267,7 @@ 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 - } - - return false + return lm.checkLockState(state, now, base.Cfg.GlobalIPBanResetTime) } // 检查全局用户锁定 @@ -274,14 +283,7 @@ func (lm *LockManager) checkGlobalUserLock(username string, now time.Time) bool if !exists { return false } - // 如果超过时间窗口,重置失败计数 - lm.resetLockStateIfExpired(state, now, base.Cfg.GlobalUserBanResetTime) - - if !state.LockTime.IsZero() && now.Before(state.LockTime) { - return true - } - - return false + return lm.checkLockState(state, now, base.Cfg.GlobalUserBanResetTime) } // 检查单个用户的 IP 锁定 @@ -303,14 +305,7 @@ 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 - } - - return false + return lm.checkLockState(state, now, base.Cfg.BanResetTime) } // 更新全局 IP 锁定状态 @@ -383,22 +378,27 @@ func (lm *LockManager) updateLockState(state *LockState, now time.Time, success state.LastAttempt = now } -// 超过窗口时间和锁定时间时重置锁定状态 -func (lm *LockManager) resetLockStateIfExpired(state *LockState, now time.Time, resetTime int) { +// 检查锁定状态 +func (lm *LockManager) checkLockState(state *LockState, now time.Time, resetTime int) bool { if state == nil || state.LastAttempt.IsZero() { - return + return false } // 如果超过锁定时间,重置锁定状态 if !state.LockTime.IsZero() && now.After(state.LockTime) { state.FailureCount = 0 state.LockTime = time.Time{} - return + return false } - // 如果超过窗口时间,重置失败计数 if now.Sub(state.LastAttempt) > time.Duration(resetTime)*time.Second { state.FailureCount = 0 state.LockTime = time.Time{} + return false } + // 如果锁定时间还在有效期内,继续锁定 + if !state.LockTime.IsZero() && now.Before(state.LockTime) { + return true + } + return false } diff --git a/server/handler/link_auth.go b/server/handler/link_auth.go index fa42c96..a5a9f3d 100644 --- a/server/handler/link_auth.go +++ b/server/handler/link_auth.go @@ -2,7 +2,6 @@ package handler import ( "bytes" - "context" "encoding/xml" "fmt" "io" @@ -95,7 +94,7 @@ func LinkAuth(w http.ResponseWriter, r *http.Request) { // TODO 用户密码校验 err = dbdata.CheckUser(cr.Auth.Username, cr.Auth.Password, cr.GroupSelect) if err != nil { - r = r.WithContext(context.WithValue(r.Context(), loginStatusKey, false)) // 传递登录失败状态 + lockManager.loginStatus.Store(loginStatusKey, false) // 记录登录失败状态 base.Warn(err, r.RemoteAddr) ua.Info = err.Error() ua.Status = dbdata.UserAuthFail @@ -109,7 +108,6 @@ func LinkAuth(w http.ResponseWriter, r *http.Request) { tplRequest(tpl_request, w, data) return } - r = r.WithContext(context.WithValue(r.Context(), loginStatusKey, true)) // 传递登录成功状态 dbdata.UserActLogIns.Add(*ua, userAgent) v := &dbdata.User{} @@ -121,6 +119,7 @@ func LinkAuth(w http.ResponseWriter, r *http.Request) { } // 用户otp验证 if !v.DisableOtp { + lockManager.loginStatus.Store(loginStatusKey, true) // 重置OTP验证计数 sessionID, err := GenerateSessionID() if err != nil { base.Error("Failed to generate session ID: ", err) diff --git a/server/handler/link_auth_otp.go b/server/handler/link_auth_otp.go index 91ef5b3..64b6362 100644 --- a/server/handler/link_auth_otp.go +++ b/server/handler/link_auth_otp.go @@ -110,6 +110,8 @@ func DeleteCookie(w http.ResponseWriter, name string) { http.SetCookie(w, cookie) } func CreateSession(w http.ResponseWriter, r *http.Request, authSession *AuthSession) { + lockManager.loginStatus.Store(loginStatusKey, true) // 更新登录成功状态 + cr := authSession.ClientRequest ua := authSession.UserActLog @@ -200,6 +202,7 @@ func LinkAuth_otp(w http.ResponseWriter, r *http.Request) { http.Error(w, "TooManyError, please login again", http.StatusBadRequest) return } + lockManager.loginStatus.Store(loginStatusKey, false) // 记录登录失败状态 base.Warn("OTP 动态码错误", username, r.RemoteAddr) ua.Info = "OTP 动态码错误" diff --git a/server/handler/server.go b/server/handler/server.go index bf0a140..c257aad 100644 --- a/server/handler/server.go +++ b/server/handler/server.go @@ -114,7 +114,7 @@ func initRoute() http.Handler { r.Handle("/", antiBruteForce(http.HandlerFunc(LinkAuth))).Methods(http.MethodPost) r.HandleFunc("/CSCOSSLC/tunnel", LinkTunnel).Methods(http.MethodConnect) r.HandleFunc("/otp_qr", LinkOtpQr).Methods(http.MethodGet) - r.HandleFunc("/otp-verification", LinkAuth_otp) + r.Handle("/otp-verification", antiBruteForce(http.HandlerFunc(LinkAuth_otp))).Methods(http.MethodPost) r.HandleFunc(fmt.Sprintf("/profile_%s.xml", base.Cfg.ProfileName), func(w http.ResponseWriter, r *http.Request) { b, _ := os.ReadFile(base.Cfg.Profile) w.Write(b) diff --git a/web/src/pages/user/List.vue b/web/src/pages/user/List.vue index 4e4476d..cf898ba 100644 --- a/web/src/pages/user/List.vue +++ b/web/src/pages/user/List.vue @@ -3,22 +3,13 @@ - 添加 + 添加 - + 批量添加 @@ -32,79 +23,45 @@ + @keydown.enter.native="searchEnterFun"> - 搜索 + 搜索 - 重置搜索 + 重置搜索 - + - + - + - + - + - + - + - + - + - +