- internal/utils:ClientIP 按序枚举 CDN/代理头(含 RFC 7239 Forwarded),仅在可信代理来源时采信,否则回退直连 IP;RemoteIP 取直连地址;RandomString 生成安全随机串 - 新增 server.trusted_proxies 配置(IP/CIDR,ConfigVersion 2→3 自动补全),启动时同步应用到 gin 与 utils - 初始管理员密码生成改用 utils.RandomString,原密码测试迁至 utils
185 lines
4.3 KiB
Go
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
|
|
}
|