This commit is contained in:
unknown
2026-02-04 20:27:13 +08:00
commit 3b042d1dad
9410 changed files with 1488147 additions and 0 deletions

View File

@@ -0,0 +1,355 @@
// Copyright 2023 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
//go:build plus
package nodes
import (
"fmt"
iplib "github.com/TeaOSLab/EdgeCommon/pkg/iplibrary"
"github.com/TeaOSLab/EdgeCommon/pkg/nodeconfigs"
"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs"
"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs/firewallconfigs"
"github.com/TeaOSLab/EdgeNode/internal/conns"
"github.com/TeaOSLab/EdgeNode/internal/iplibrary"
"github.com/TeaOSLab/EdgeNode/internal/utils"
"github.com/TeaOSLab/EdgeNode/internal/utils/agents"
"github.com/TeaOSLab/EdgeNode/internal/utils/counters"
"github.com/TeaOSLab/EdgeNode/internal/utils/fasttime"
maputils "github.com/TeaOSLab/EdgeNode/internal/utils/maps"
"github.com/TeaOSLab/EdgeNode/internal/utils/ttlcache"
"github.com/TeaOSLab/EdgeNode/internal/waf"
"github.com/iwind/TeaGo/Tea"
"github.com/iwind/TeaGo/types"
stringutil "github.com/iwind/TeaGo/utils/string"
"net/http"
"net/url"
"path/filepath"
"strings"
)
type ccBlockedCounter struct {
count int32
updatedAt int64
}
var ccBlockedMap = maputils.NewFixedMap[string, *ccBlockedCounter](65535)
func (this *HTTPRequest) doCC() (block bool) {
if this.nodeConfig == nil || this.ReqServer == nil {
return
}
var validatePath = "/GE/CC/VALIDATOR"
var maxConnections = 30
// 策略
var httpCCPolicy = this.nodeConfig.FindHTTPCCPolicyWithClusterId(this.ReqServer.ClusterId)
var scope = firewallconfigs.FirewallScopeGlobal
if httpCCPolicy != nil {
if !httpCCPolicy.IsOn {
return
}
if len(httpCCPolicy.RedirectsChecking.ValidatePath) > 0 {
validatePath = httpCCPolicy.RedirectsChecking.ValidatePath
}
if httpCCPolicy.MaxConnectionsPerIP > 0 {
maxConnections = httpCCPolicy.MaxConnectionsPerIP
}
scope = httpCCPolicy.FirewallScope()
}
var ccConfig = this.web.CC
if ccConfig == nil || !ccConfig.IsOn || (this.RawReq.URL.Path != validatePath && !ccConfig.MatchURL(this.requestScheme()+"://"+this.ReqHost+this.Path())) {
return
}
// 忽略常用文件
if ccConfig.IgnoreCommonFiles {
if len(this.RawReq.Referer()) > 0 {
var ext = filepath.Ext(this.RawReq.URL.Path)
if len(ext) > 0 && utils.IsCommonFileExtension(ext) {
return
}
}
}
// 检查白名单
var remoteAddr = this.requestRemoteAddr(true)
// 检查是否为白名单直连
if !Tea.IsTesting() && this.nodeConfig.IPIsAutoAllowed(remoteAddr) {
return
}
// 是否在全局名单中
canGoNext, isInAllowedList, _ := iplibrary.AllowIP(remoteAddr, this.ReqServer.Id)
if !canGoNext {
this.disableLog = true
this.Close()
return true
}
if isInAllowedList {
return false
}
// WAF黑名单
if waf.SharedIPBlackList.Contains(waf.IPTypeAll, scope, this.ReqServer.Id, remoteAddr) {
this.disableLog = true
this.Close()
return true
}
// 检查是否启用QPS
if ccConfig.MinQPSPerIP > 0 && this.RawReq.URL.Path != validatePath {
var avgQPS = counters.SharedCounter.IncreaseKey("QPS:"+remoteAddr, 60) / 60
if avgQPS <= 0 {
avgQPS = 1
}
if avgQPS < types.Uint32(ccConfig.MinQPSPerIP) {
return false
}
}
// 检查连接数
if conns.SharedMap.CountIPConns(remoteAddr) >= maxConnections {
this.ccForbid(5)
var forbiddenTimes = this.increaseCCCounter(remoteAddr)
waf.SharedIPBlackList.RecordIP(waf.IPTypeAll, scope, this.ReqServer.Id, remoteAddr, fasttime.Now().Unix()+int64(forbiddenTimes*1800), 0, scope == firewallconfigs.FirewallScopeGlobal, 0, 0, "CC防护拦截并发连接数超出"+types.String(maxConnections)+"个")
return true
}
// GET302验证
if ccConfig.EnableGET302 &&
this.RawReq.Method == http.MethodGet &&
!agents.SharedManager.ContainsIP(remoteAddr) /** 搜索引擎亲和性 **/ &&
!strings.HasPrefix(this.RawReq.URL.Path, "/baidu_verify_") /** 百度验证 **/ &&
!strings.HasPrefix(this.RawReq.URL.Path, "/google") /** Google验证 **/ {
// 忽略搜索引擎
var canSkip302 = false
var ipResult = iplib.LookupIP(remoteAddr)
if ipResult != nil && ipResult.IsOk() {
var providerName = ipResult.ProviderName()
canSkip302 = strings.Contains(providerName, "百度") || strings.Contains(providerName, "谷歌") || strings.Contains(providerName, "baidu") || strings.Contains(providerName, "google")
}
if !canSkip302 {
// 检查参数
var ccWhiteListKey = "HTTP-CC-GET302-" + remoteAddr
var currentTime = fasttime.Now().Unix()
if this.RawReq.URL.Path == validatePath {
this.DisableAccessLog()
this.DisableStat()
// TODO 根据浏览器信息决定是否校验referer
if !this.checkCCRedirects(httpCCPolicy, remoteAddr) {
return true
}
var key = this.RawReq.URL.Query().Get("key")
var pieces = strings.Split(key, ".") // key1.key2.timestamp
if len(pieces) != 3 {
this.ccForbid(1)
return true
}
var urlKey = pieces[0]
var timestampKey = pieces[1]
var timestamp = pieces[2]
var targetURL = this.RawReq.URL.Query().Get("url")
var realURLKey = stringutil.Md5(sharedNodeConfig.Secret + "@" + targetURL + "@" + remoteAddr)
if urlKey != realURLKey {
this.ccForbid(2)
return true
}
// 校验时间
if timestampKey != stringutil.Md5(sharedNodeConfig.Secret+"@"+timestamp) {
this.ccForbid(3)
return true
}
var elapsedSeconds = currentTime - types.Int64(timestamp)
if elapsedSeconds > 10 /** 10秒钟 **/ { // 如果校验时间过长,则可能阻止当前访问
if elapsedSeconds > 300 /** 5分钟 **/ { // 如果超时时间过长就跳回原URL
httpRedirect(this.writer, this.RawReq, targetURL, http.StatusFound)
} else {
this.ccForbid(4)
}
return true
}
// 加入到临时白名单
ttlcache.SharedInt64Cache.Write(ccWhiteListKey, 1, currentTime+600 /** 10分钟 **/)
// 跳转回原来URL
httpRedirect(this.writer, this.RawReq, targetURL, http.StatusFound)
return true
} else {
// 检查临时白名单
if ttlcache.SharedInt64Cache.Read(ccWhiteListKey) == nil {
if !this.checkCCRedirects(httpCCPolicy, remoteAddr) {
return true
}
var urlKey = stringutil.Md5(sharedNodeConfig.Secret + "@" + this.URL() + "@" + remoteAddr)
var timestampKey = stringutil.Md5(sharedNodeConfig.Secret + "@" + types.String(currentTime))
// 跳转到验证URL
this.DisableStat()
httpRedirect(this.writer, this.RawReq, validatePath+"?key="+urlKey+"."+timestampKey+"."+types.String(currentTime)+"&url="+url.QueryEscape(this.URL()), http.StatusFound)
return true
}
}
}
} else if this.RawReq.URL.Path == validatePath {
// 直接跳回
var targetURL = this.RawReq.URL.Query().Get("url")
httpRedirect(this.writer, this.RawReq, targetURL, http.StatusFound)
return true
}
// Key
var ccKeys = []string{}
if ccConfig.WithRequestPath {
ccKeys = append(ccKeys, "HTTP-CC-"+remoteAddr+"-"+this.Path()) // 这里可以忽略域名,因为一个正常用户同时访问多个域名的可能性不大
} else {
ccKeys = append(ccKeys, "HTTP-CC-"+remoteAddr)
}
// 指纹
if this.IsHTTPS && ccConfig.EnableFingerprint {
var requestConn = this.RawReq.Context().Value(HTTPConnContextKey)
if requestConn != nil {
clientConn, ok := requestConn.(ClientConnInterface)
if ok {
var fingerprint = clientConn.Fingerprint()
if len(fingerprint) > 0 {
var fingerprintString = fmt.Sprintf("%x", fingerprint)
if ccConfig.WithRequestPath {
ccKeys = append(ccKeys, "HTTP-CC-"+fingerprintString+"-"+this.Path()) // 这里可以忽略域名,因为一个正常用户同时访问多个域名的可能性不大
} else {
ccKeys = append(ccKeys, "HTTP-CC-"+fingerprintString)
}
}
}
}
}
// 检查阈值
var thresholds = ccConfig.Thresholds
if len(thresholds) == 0 || ccConfig.UseDefaultThresholds {
if httpCCPolicy != nil && len(httpCCPolicy.Thresholds) > 0 {
thresholds = httpCCPolicy.Thresholds
} else {
thresholds = serverconfigs.DefaultHTTPCCThresholds
}
}
if len(thresholds) == 0 {
return
}
var currentTime = fasttime.Now().Unix()
for _, threshold := range thresholds {
if threshold.PeriodSeconds <= 0 || threshold.MaxRequests <= 0 {
continue
}
for _, ccKey := range ccKeys {
var count = counters.SharedCounter.IncreaseKey(ccKey+"-T"+types.String(threshold.PeriodSeconds), int(threshold.PeriodSeconds))
if count >= types.Uint32(threshold.MaxRequests) {
this.writeCode(http.StatusTooManyRequests, "Too many requests, please wait for a few minutes.", "访问过于频繁,请稍等片刻后再访问。")
// 记录到黑名单
if threshold.BlockSeconds > 0 {
// 如果被重复拦截,则加大惩罚力度
var forbiddenTimes = this.increaseCCCounter(remoteAddr)
waf.SharedIPBlackList.RecordIP(waf.IPTypeAll, scope, this.ReqServer.Id, remoteAddr, currentTime+int64(threshold.BlockSeconds*forbiddenTimes), 0, scope == firewallconfigs.FirewallScopeGlobal, 0, 0, "CC防护拦截在"+types.String(threshold.PeriodSeconds)+"秒内请求超过"+types.String(threshold.MaxRequests)+"次")
}
this.tags = append(this.tags, "CCProtection")
this.isAttack = true
// 关闭连接
this.writer.Flush()
this.Close()
// 关闭同一个IP其他连接
conns.SharedMap.CloseIPConns(remoteAddr)
return true
}
}
}
return
}
// TODO 对forbidden比较多的IP进行惩罚
func (this *HTTPRequest) ccForbid(code int) {
this.writeCode(http.StatusForbidden, "The request has been blocked by cc policy.", "当前请求已被CC策略拦截。")
}
// 检查跳转次数
func (this *HTTPRequest) checkCCRedirects(httpCCPolicy *nodeconfigs.HTTPCCPolicy, remoteAddr string) bool {
// 如果无效跳转次数太多,则拦截
var ccRedirectKey = "HTTP-CC-GET302-" + remoteAddr + "-REDIRECTS"
var maxRedirectDurationSeconds = 120
var maxRedirects uint32 = 30
var blockSeconds int64 = 3600
if httpCCPolicy != nil && httpCCPolicy.IsOn {
if httpCCPolicy.RedirectsChecking.DurationSeconds > 0 {
maxRedirectDurationSeconds = httpCCPolicy.RedirectsChecking.DurationSeconds
}
if httpCCPolicy.RedirectsChecking.MaxRedirects > 0 {
maxRedirects = types.Uint32(httpCCPolicy.RedirectsChecking.MaxRedirects)
}
if httpCCPolicy.RedirectsChecking.BlockSeconds > 0 {
blockSeconds = int64(httpCCPolicy.RedirectsChecking.BlockSeconds)
}
}
var countRedirects = counters.SharedCounter.IncreaseKey(ccRedirectKey, maxRedirectDurationSeconds)
if countRedirects >= maxRedirects {
// 加入黑名单
var scope = firewallconfigs.FirewallScopeGlobal
if httpCCPolicy != nil {
scope = httpCCPolicy.FirewallScope()
}
waf.SharedIPBlackList.RecordIP(waf.IPTypeAll, scope, this.ReqServer.Id, remoteAddr, fasttime.Now().Unix()+blockSeconds, 0, scope == firewallconfigs.FirewallScopeGlobal, 0, 0, "CC防护拦截在"+types.String(maxRedirectDurationSeconds)+"秒内无效跳转"+types.String(maxRedirects)+"次")
this.Close()
return false
}
return true
}
// 对CC禁用次数进行计数
func (this *HTTPRequest) increaseCCCounter(remoteAddr string) int32 {
counter, ok := ccBlockedMap.Get(remoteAddr)
if !ok {
counter = &ccBlockedCounter{
count: 1,
updatedAt: fasttime.Now().Unix(),
}
ccBlockedMap.Put(remoteAddr, counter)
} else {
if counter.updatedAt < fasttime.Now().Unix()-86400 /** 有效期间不要超过1天防止无限期封禁 **/ {
counter.count = 0
}
counter.updatedAt = fasttime.Now().Unix()
if counter.count < 32 {
counter.count++
}
}
return counter.count
}