Files
kevin f440bb22b6 增加工具模块与可信代理配置
- internal/utils:ClientIP 按序枚举 CDN/代理头(含 RFC 7239 Forwarded),仅在可信代理来源时采信,否则回退直连 IP;RemoteIP 取直连地址;RandomString 生成安全随机串
- 新增 server.trusted_proxies 配置(IP/CIDR,ConfigVersion 2→3 自动补全),启动时同步应用到 gin 与 utils
- 初始管理员密码生成改用 utils.RandomString,原密码测试迁至 utils
2026-09-21 20:28:38 +08:00

185 lines
4.3 KiB
Go

// Package utils 提供通用工具函数:客户端 IP 解析、随机字符串等。
package utils
import (
"fmt"
"net"
"net/http"
"strings"
"sync"
"github.com/gin-gonic/gin"
)
// clientIPHeaders 按优先级排列的单值客户端 IP 请求头;Forwarded 与 X-Forwarded-For 因需要链式解析单独处理。
var clientIPHeaders = []string{
"CF-Connecting-IP",
"True-Client-IP",
"Ali-CDN-Real-IP",
"X-Real-IP",
"X-Client-IP",
"Fastly-Client-IP",
}
var (
trustedMu sync.RWMutex
trustedCIDRs []*net.IPNet
)
// SetTrustedProxies 设置可信代理/CDN 回源网段,支持 IP 与 CIDR,启动时调用一次;并发安全。
func SetTrustedProxies(cidrs []string) error {
parsed := make([]*net.IPNet, 0, len(cidrs))
for _, raw := range cidrs {
value := strings.TrimSpace(raw)
if value == "" {
continue
}
if ip := net.ParseIP(value); ip != nil {
bits := 128
if ip.To4() != nil {
bits = 32
}
_, cidr, err := net.ParseCIDR(fmt.Sprintf("%s/%d", ip.String(), bits))
if err != nil {
return fmt.Errorf("invalid trusted proxy %q: %w", raw, err)
}
parsed = append(parsed, cidr)
continue
}
if _, cidr, err := net.ParseCIDR(value); err == nil {
parsed = append(parsed, cidr)
continue
}
return fmt.Errorf("invalid trusted proxy %q", raw)
}
trustedMu.Lock()
trustedCIDRs = parsed
trustedMu.Unlock()
return nil
}
// ClientIP 依次枚举 CDN/代理请求头,仅在直连地址可信时采信;否则返回直连 IP。
func ClientIP(c *gin.Context) string {
direct := RemoteIP(c)
if !isTrustedPeer(c, direct) {
return direct
}
for _, header := range clientIPHeaders {
if ip := normalizeIP(c.GetHeader(header)); ip != "" {
return ip
}
}
if ip := pickClientIP(parseForwarded(c.GetHeader("Forwarded"))); ip != "" {
return ip
}
if ip := pickClientIP(parseIPList(c.GetHeader("X-Forwarded-For"))); ip != "" {
return ip
}
return direct
}
// RemoteIP 返回直连地址(已去端口),不读取任何请求头。
func RemoteIP(c *gin.Context) string {
return normalizeIP(c.Request.RemoteAddr)
}
// pickClientIP 从右往左取第一个非可信代理 IP;全部可信时取最左值。
func pickClientIP(chain []string) string {
for i := len(chain) - 1; i >= 0; i-- {
if !isTrustedStringIP(chain[i]) {
return chain[i]
}
}
if len(chain) > 0 {
return chain[0]
}
return ""
}
func isTrustedPeer(c *gin.Context, direct string) bool {
if isUnixSocket(c) {
return true
}
ip := net.ParseIP(direct)
return ip != nil && isTrustedIP(ip)
}
func isTrustedIP(ip net.IP) bool {
trustedMu.RLock()
defer trustedMu.RUnlock()
for _, cidr := range trustedCIDRs {
if cidr.Contains(ip) {
return true
}
}
return false
}
func isTrustedStringIP(value string) bool {
ip := net.ParseIP(value)
return ip != nil && isTrustedIP(ip)
}
func isUnixSocket(c *gin.Context) bool {
addr, ok := c.Request.Context().Value(http.LocalAddrContextKey).(net.Addr)
return ok && strings.HasPrefix(addr.Network(), "unix")
}
// normalizeIP 清理引号与端口后校验为合法 IP,非法返回空串。
func normalizeIP(raw string) string {
value := strings.TrimSpace(raw)
if value == "" {
return ""
}
value = strings.Trim(value, `"`)
if host, _, err := net.SplitHostPort(value); err == nil {
value = host
}
value = strings.Trim(value, "[]")
ip := net.ParseIP(value)
if ip == nil {
return ""
}
return ip.String()
}
func parseIPList(value string) []string {
if strings.TrimSpace(value) == "" {
return nil
}
parts := strings.Split(value, ",")
ips := make([]string, 0, len(parts))
for _, part := range parts {
if ip := normalizeIP(part); ip != "" {
ips = append(ips, ip)
}
}
return ips
}
// parseForwarded 解析 RFC 7239 Forwarded 头中的 for= 参数,按出现顺序返回合法 IP。
func parseForwarded(value string) []string {
if strings.TrimSpace(value) == "" {
return nil
}
var ips []string
for _, element := range strings.Split(value, ",") {
for _, param := range strings.Split(element, ";") {
key, val, ok := strings.Cut(strings.TrimSpace(param), "=")
if !ok || !strings.EqualFold(strings.TrimSpace(key), "for") {
continue
}
candidate := strings.TrimSpace(val)
if candidate == "" || candidate == "_hidden" || strings.EqualFold(candidate, "unknown") {
continue
}
if ip := normalizeIP(candidate); ip != "" {
ips = append(ips, ip)
}
}
}
return ips
}