fix anwser token and database issus

This commit is contained in:
tamakyi
2026-09-15 11:06:09 +08:00
parent e683e798aa
commit adcc42cc8c
33 changed files with 1011 additions and 802 deletions
+20 -46
View File
@@ -9,11 +9,8 @@ import (
"github.com/tamakyi/TamaBox/internal/conf"
)
// NormalizeIP 规范化 IP 字符串:
// - 去除两端空白
// - 剥离端口("1.2.3.4:8080" / "[::1]:8080")
// - 校验合法性,非法返回空串
// - IPv4-mapped IPv6(::ffff:1.2.3.4)归一为 IPv4,便于黑名单/限流精确匹配
// NormalizeIP 规范化 IP:去空白、剥端口、校验合法性(非法返回空串),
// 并把 IPv4-mapped IPv6(::ffff:1.2.3.4)归一为 IPv4,便于黑名单 / 限流精确匹配。
func NormalizeIP(s string) string {
s = strings.TrimSpace(s)
if s == "" {
@@ -33,21 +30,15 @@ func NormalizeIP(s string) string {
}
// LimitKey 把客户端 IP 归一化为「限流 / 自动封禁」用的聚合键。
// IPv4 原样;IPv6 掩码到 /64 —— 住宅 / 隧道分配通常就是一个 /64(2^64 个地址),
// 若按完整地址计数,攻击者每请求换源地址即可让阈值永不触发,且键会无限膨胀内存。
//
// - IPv4:原样返回(每个地址独立计数)。
// - IPv6:掩码到 /64 前缀后返回,形如 "2001:db8:1:2::/64"。
//
// 为什么 IPv6 必须聚合:单个住宅或隧道 IPv6 分配通常就是一个 /64(2^64 个地址)。
// 若以完整地址作键,攻击者每次请求换一个源地址即可让每个键只命中一次,
// 使限流与 autoban 的阈值永远无法触发,同时海量键还会持续膨胀内存。
// /64 是 IPv6 的最小实际分配单位,按此聚合可在不误伤正常用户的前提下堵住绕过。
//
// ⚠️ 仅用于限流 / 封禁计数。IP 黑名单必须继续使用精确匹配(见 ipblacklist.go),
// 不要套用本函数,否则会把整个 /64 段一并拉黑。
// ⚠️ 仅用于限流 / 封禁计数。IP 黑名单必须继续精确匹配(见 ipblacklist.go),
// 否则会把整个 /64 段一并拉黑。
func LimitKey(ip string) string {
parsed := net.ParseIP(ip)
if parsed == nil {
// 无法解析时原样返回,保持调用方行为可预期(空串由调用方自行判断)。
// 无法解析时原样返回,保持调用方可预期(空串由调用方自行判断)。
return ip
}
if v4 := parsed.To4(); v4 != nil {
@@ -59,14 +50,11 @@ func LimitKey(ip string) string {
// trustedProxyNets 缓存解析后的可信代理网段(conf.Init 之后由 InitTrustedProxies 填充)。
var trustedProxyNets []*net.IPNet
// InitTrustedProxies 解析 conf.Security.TrustedProxies(逗号分隔的 IP / CIDR 列表),
// 仅限“你自己的”反向代理/负载均衡基础设施(例如本机 nginx 的 127.0.0.1、私有网络 LB),
// 在 conf.Init 之后调用一次。列表为空时,X-Forwarded-For / X-Real-IP 永不被信任。
// InitTrustedProxies 解析 conf.Security.TrustedProxies(逗号分隔 IP / CIDR),在 conf.Init 之后调用一次。
// 仅限"你自己的"反向代理 / LB(如本机 nginx 127.0.0.1、私有网络 LB);列表为空时 XFF / X-Real-IP 永不被信任。
//
// 设计目标:避免维护 Cloudflare / EdgeOne 等上游 CDN 的庞大 CIDR 清单。
// 站点若架在 CDN 后,应配合 Cloudflare Tunnel / Authenticated Origin Pull(Cloudflare)或
// 防火墙仅放行回源网段(Cloudflare / EdgeOne)锁定源站只接受 CDN 连接,此时
// CF-Connecting-IP / EO-Client-IP 即为权威客户端 IP,无需任何 CIDR 白名单。
// 不为 CDN 维护庞大 CIDR 清单:站点架在 CDN 后时,应配合 Tunnel / Authenticated Origin Pull
// 或防火墙仅放行回源网段锁定源站,此时 CF-Connecting-IP / EO-Client-IP 即权威客户端 IP。
func InitTrustedProxies() error {
trustedProxyNets = nil
for _, raw := range strings.Split(conf.Security.TrustedProxies, ",") {
@@ -114,36 +102,22 @@ func isTrustedProxy(ipStr string) bool {
// ResolveClientIP 统一解析客户端真实 IP,供速率限制、IP 黑名单与审计记录共用。
//
// 解析策略(不依赖上游 CDN 的庞大 CIDR 清单):
// 1. 未开启 TrustProxy:一切以 TCP 对端 RemoteAddr 为准(不可伪造)。
// 2. 开启 TrustProxy:
// a. 优先信任 CDN 权威客户端头:CF-Connecting-IP(Cloudflare)→ EO-Client-IP
// (腾讯 EdgeOne)。这类头只能由对应 CDN 边缘按真实客户端写入,前提是源站已锁定
// 为仅接受这些 CDN 的回源连接(攻击者无法直连源站伪造)。
// b. 仅当 TCP 对端落在 trusted_proxies(你自己的基础设施)内时,才解析
// X-Forwarded-For / X-Real-IP,并从右向左剥离已知可信跳数取原始客户端。
// c. 否则回退到 RemoteAddr,绝不信任客户端自行提供的头。
// 策略:未开 TrustProxy 时一律用不可伪造的 TCP RemoteAddr;开启后 (a) 先信 CDN 权威头
// CF-Connecting-IP → EO-Client-IP,(b) 仅当 TCP 对端落在 trusted_proxies 内才从右向左
// 解析 XFF / X-Real-IP,(c) 否则回退 RemoteAddr,绝不信任客户端自供的头。
//
// ⚠️ 若 trust_proxy = true 但源站仍可被人直连(未用 Tunnel / 未锁源站),攻击者可自行
// 发送 CF-Connecting-IP / EO-Client-IP 头,此时本函数会信任之。因此生产开启 trust_proxy
// 时,务必配合「Cloudflare Tunnel / Authenticated Origin Pull / 防火墙仅放行 CDN 回源
// 网段」等手段锁定源站,使伪造头无法抵达应用。
// ⚠️ trust_proxy=true 但源站可被直连(未用 Tunnel / 未锁源站)时,攻击者可自造
// CF-Connecting-IP / EO-Client-IP 并被本函数信任 —— 生产开启必须锁定源站只接受 CDN 回源。
//
// 双 CDN 部署(境内 EdgeOne + 境外 Cloudflare)的额外约束:
// 若回源请求携带对方 CDN 的权威头(经 EdgeOne 回源的请求带 CF-Connecting-IP、经
// Cloudflare 回源的请求带 EO-Client-IP),攻击者即可借道另一家伪造身份,绕过限流 /
// 自动封禁 / 单 IP 投票限制,或嫁祸他人。因此必须:① 防火墙仅放行两家 CDN 的回源网段;
// ② 在各家 CDN 的规则引擎中删除入站请求里「对方 CDN」的权威头,使每个请求最终只携带
// 自己所在 CDN 的头(操作步骤见 outputs/edgeone-cloudflare-dual-cdn-guide.md)。
// Cloudflare 侧因 CF-Connecting-IP 恒被覆盖,且用 Transform Rule 删掉 EO-Client-IP 即可;
// EdgeOne 侧用规则引擎删掉 CF-Connecting-IP 即可。
// ⚠️ 双 CDN(境内 EdgeOne + 境外 Cloudflare)时必须:① 防火墙仅放行两家回源网段;
// ② 在各家规则引擎中删除入站请求里「对方 CDN」的权威头,否则攻击者可借道另一家伪造身份,
// 绕过限流 / 自动封禁 / 单 IP 投票限制。步骤见 outputs/edgeone-cloudflare-dual-cdn-guide.md。
func ResolveClientIP(r *http.Request) string {
if !conf.Security.TrustProxy {
return NormalizeIP(r.RemoteAddr)
}
// (a) CDN 权威客户端头:CF-Connecting-IP(Cloudflare)优先,其次 EO-Client-IP(腾讯
// EdgeOne,等效于 CF-Connecting-IP 的权威头,回源时由 EdgeOne 边缘写入客户端真实 IP)。
// (a) CDN 权威客户端头(只能由 CDN 边缘写入):Cloudflare 优先,其次腾讯 EdgeOne。
if cfIP := NormalizeIP(r.Header.Get("CF-Connecting-IP")); cfIP != "" {
return cfIP
}