| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378 |
- // Package ip 提供客户端 IP 地址提取工具。
- package ip
- import (
- "net"
- "strings"
- "github.com/gin-gonic/gin"
- )
- const forwardedIPSettingsKey = "sub2api.forwarded_ip_settings"
- type forwardedIPSettings struct {
- trustForwarded bool
- headers []string
- }
- // SetForwardedIPSettings snapshots the forwarded-IP mode and custom header list
- // for this request.
- func SetForwardedIPSettings(c *gin.Context, enabled bool, headers []string) {
- if c == nil {
- return
- }
- c.Set(forwardedIPSettingsKey, forwardedIPSettings{
- trustForwarded: enabled,
- headers: append([]string(nil), headers...),
- })
- }
- // SetLegacyForwardedIPTrust records whether raw forwarding headers override
- // Gin's server.trusted_proxies chain for this request.
- func SetLegacyForwardedIPTrust(c *gin.Context, enabled bool) {
- SetForwardedIPSettings(c, enabled, nil)
- }
- func requestForwardedIPSettings(c *gin.Context) (forwardedIPSettings, bool) {
- if c == nil {
- return forwardedIPSettings{}, false
- }
- value, ok := c.Get(forwardedIPSettingsKey)
- if !ok {
- return forwardedIPSettings{}, false
- }
- settings, ok := value.(forwardedIPSettings)
- return settings, ok
- }
- func requestUsesLegacyForwardedIPTrust(c *gin.Context) bool {
- settings, ok := requestForwardedIPSettings(c)
- return !ok || settings.trustForwarded
- }
- // GetClientIP resolves the client address using the legacy forwarding-header
- // precedence used before the trusted-proxy hardening. It remains the
- // compatibility path for request metadata and usage/error logs; security-
- // sensitive callers must use GetTrustedClientIP or GetSecurityClientIP.
- func GetClientIP(c *gin.Context) string {
- if c == nil {
- return ""
- }
- if !requestUsesLegacyForwardedIPTrust(c) {
- return GetTrustedClientIP(c)
- }
- settings, _ := requestForwardedIPSettings(c)
- customIP, customFallback := resolveCustomForwardedClientIP(c, settings.headers)
- if customIP != "" {
- return customIP
- }
- // Preserve the historical precedence used by existing reverse-proxy
- // deployments, while skipping an internal proxy address when a public XFF
- // value is available. This covers Docker/Nginx setups that accidentally
- // write the bridge address into X-Real-IP.
- legacyIP, legacyFallback := resolveLegacyForwardedHeaderIP(c)
- if legacyIP != "" {
- return legacyIP
- }
- if customFallback != "" {
- return customFallback
- }
- if legacyFallback != "" {
- return legacyFallback
- }
- return normalizeIP(c.ClientIP())
- }
- func resolveCustomForwardedClientIP(c *gin.Context, headers []string) (string, string) {
- if c == nil {
- return "", ""
- }
- var fallback string
- for _, header := range headers {
- for _, value := range c.Request.Header.Values(header) {
- for _, candidate := range strings.Split(value, ",") {
- parsed := net.ParseIP(strings.TrimSpace(candidate))
- if parsed == nil {
- continue
- }
- normalized := parsed.String()
- if isPrivateIP(normalized) {
- if fallback == "" {
- fallback = normalized
- }
- continue
- }
- return normalized, fallback
- }
- }
- }
- return "", fallback
- }
- func resolveLegacyForwardedHeaderIP(c *gin.Context) (string, string) {
- var fallback string
- if forwarded := normalizeValidIP(c.GetHeader("CF-Connecting-IP")); forwarded != "" {
- fallback = forwarded
- if !isPrivateIP(forwarded) {
- return forwarded, fallback
- }
- }
- if realIP := normalizeValidIP(c.GetHeader("X-Real-IP")); realIP != "" {
- if fallback == "" {
- fallback = realIP
- }
- if !isPrivateIP(realIP) {
- return realIP, fallback
- }
- }
- if xff := c.GetHeader("X-Forwarded-For"); xff != "" {
- ips := strings.Split(xff, ",")
- for _, candidate := range ips {
- candidate = normalizeValidIP(candidate)
- if candidate != "" && !isPrivateIP(candidate) {
- return candidate, fallback
- }
- }
- if fallback == "" {
- for _, candidate := range ips {
- if candidate = normalizeValidIP(candidate); candidate != "" {
- fallback = candidate
- break
- }
- }
- }
- }
- return "", fallback
- }
- // GetTrustedClientIP 从 Gin 的可信代理解析链提取客户端 IP。
- // 该方法依赖 gin.Engine.SetTrustedProxies 配置,不会优先直接信任原始转发头值。
- // 适用于 ACL / 风控等安全敏感场景。
- func GetTrustedClientIP(c *gin.Context) string {
- if c == nil {
- return ""
- }
- return normalizeIP(c.ClientIP())
- }
- // GetSecurityClientIP returns the address used by security-sensitive paths.
- // When legacy forwarded-IP trust is enabled, raw forwarding headers take over
- // client-IP resolution. When disabled, Gin's server.trusted_proxies chain is
- // authoritative.
- func GetSecurityClientIP(c *gin.Context, trustForwarded bool) string {
- if requestSettings, ok := requestForwardedIPSettings(c); ok {
- trustForwarded = requestSettings.trustForwarded
- }
- if trustForwarded {
- return GetClientIP(c)
- }
- return GetTrustedClientIP(c)
- }
- // normalizeIP 规范化 IP 地址,去除端口号和空格。
- func normalizeIP(ip string) string {
- ip = strings.TrimSpace(ip)
- // 移除端口号(如 "192.168.1.1:8080" -> "192.168.1.1")
- if host, _, err := net.SplitHostPort(ip); err == nil {
- return host
- }
- return ip
- }
- // normalizeValidIP 规范化并验证代理头中的候选值,避免把 unknown、主机名等非法值传给安全服务。
- func normalizeValidIP(value string) string {
- normalized := normalizeIP(value)
- parsed := net.ParseIP(normalized)
- if parsed == nil {
- return ""
- }
- return parsed.String()
- }
- // privateNets contains the private/loopback ranges skipped while selecting a
- // public address from a legacy X-Forwarded-For chain.
- var privateNets []*net.IPNet
- func init() {
- for _, cidr := range []string{
- "10.0.0.0/8",
- "172.16.0.0/12",
- "192.168.0.0/16",
- "127.0.0.0/8",
- "::1/128",
- "fc00::/7",
- } {
- _, block, err := net.ParseCIDR(cidr)
- if err != nil {
- panic("invalid CIDR: " + cidr)
- }
- privateNets = append(privateNets, block)
- }
- }
- // CompiledIPRules 表示预编译的 IP 匹配规则。
- // PatternCount 记录原始规则数量,用于保留“规则存在但全无效”时的行为语义。
- type CompiledIPRules struct {
- CIDRs []*net.IPNet
- IPs []net.IP
- PatternCount int
- }
- // CompileIPRules 将 IP/CIDR 字符串规则预编译为可复用结构。
- // 非法规则会被忽略,但 PatternCount 会保留原始规则条数。
- func CompileIPRules(patterns []string) *CompiledIPRules {
- compiled := &CompiledIPRules{
- CIDRs: make([]*net.IPNet, 0, len(patterns)),
- IPs: make([]net.IP, 0, len(patterns)),
- PatternCount: len(patterns),
- }
- for _, pattern := range patterns {
- normalized := strings.TrimSpace(pattern)
- if normalized == "" {
- continue
- }
- if strings.Contains(normalized, "/") {
- _, cidr, err := net.ParseCIDR(normalized)
- if err != nil || cidr == nil {
- continue
- }
- compiled.CIDRs = append(compiled.CIDRs, cidr)
- continue
- }
- parsedIP := net.ParseIP(normalized)
- if parsedIP == nil {
- continue
- }
- compiled.IPs = append(compiled.IPs, parsedIP)
- }
- return compiled
- }
- func matchesCompiledRules(parsedIP net.IP, rules *CompiledIPRules) bool {
- if parsedIP == nil || rules == nil {
- return false
- }
- for _, cidr := range rules.CIDRs {
- if cidr.Contains(parsedIP) {
- return true
- }
- }
- for _, ruleIP := range rules.IPs {
- if parsedIP.Equal(ruleIP) {
- return true
- }
- }
- return false
- }
- func isPrivateIP(ipStr string) bool {
- ip := net.ParseIP(ipStr)
- if ip == nil {
- return false
- }
- for _, block := range privateNets {
- if block.Contains(ip) {
- return true
- }
- }
- return false
- }
- // MatchesPattern 检查 IP 是否匹配指定的模式(支持单个 IP 或 CIDR)。
- // pattern 可以是:
- // - 单个 IP: "192.168.1.100"
- // - CIDR 范围: "192.168.1.0/24"
- func MatchesPattern(clientIP, pattern string) bool {
- ip := net.ParseIP(clientIP)
- if ip == nil {
- return false
- }
- // 尝试解析为 CIDR
- if strings.Contains(pattern, "/") {
- _, cidr, err := net.ParseCIDR(pattern)
- if err != nil {
- return false
- }
- return cidr.Contains(ip)
- }
- // 作为单个 IP 处理
- patternIP := net.ParseIP(pattern)
- if patternIP == nil {
- return false
- }
- return ip.Equal(patternIP)
- }
- // MatchesAnyPattern 检查 IP 是否匹配任意一个模式。
- func MatchesAnyPattern(clientIP string, patterns []string) bool {
- for _, pattern := range patterns {
- if MatchesPattern(clientIP, pattern) {
- return true
- }
- }
- return false
- }
- // CheckIPRestriction 检查 IP 是否被 API Key 的 IP 限制允许。
- // 返回值:(是否允许, 拒绝原因)
- // 逻辑:
- // 1. 先检查黑名单,如果在黑名单中则直接拒绝
- // 2. 如果白名单不为空,IP 必须在白名单中
- // 3. 如果白名单为空,允许访问(除非被黑名单拒绝)
- func CheckIPRestriction(clientIP string, whitelist, blacklist []string) (bool, string) {
- return CheckIPRestrictionWithCompiledRules(
- clientIP,
- CompileIPRules(whitelist),
- CompileIPRules(blacklist),
- )
- }
- // CheckIPRestrictionWithCompiledRules 使用预编译规则检查 IP 是否允许访问。
- func CheckIPRestrictionWithCompiledRules(clientIP string, whitelist, blacklist *CompiledIPRules) (bool, string) {
- // 规范化 IP
- clientIP = normalizeIP(clientIP)
- if clientIP == "" {
- return false, "access denied"
- }
- parsedIP := net.ParseIP(clientIP)
- if parsedIP == nil {
- return false, "access denied"
- }
- // 1. 检查黑名单
- if blacklist != nil && blacklist.PatternCount > 0 && matchesCompiledRules(parsedIP, blacklist) {
- return false, "access denied"
- }
- // 2. 检查白名单(如果设置了白名单,IP 必须在其中)
- if whitelist != nil && whitelist.PatternCount > 0 && !matchesCompiledRules(parsedIP, whitelist) {
- return false, "access denied"
- }
- return true, ""
- }
- // ValidateIPPattern 验证 IP 或 CIDR 格式是否有效。
- func ValidateIPPattern(pattern string) bool {
- if strings.Contains(pattern, "/") {
- _, _, err := net.ParseCIDR(pattern)
- return err == nil
- }
- return net.ParseIP(pattern) != nil
- }
- // ValidateIPPatterns 验证多个 IP 或 CIDR 格式。
- // 返回无效的模式列表。
- func ValidateIPPatterns(patterns []string) []string {
- var invalid []string
- for _, p := range patterns {
- if !ValidateIPPattern(p) {
- invalid = append(invalid, p)
- }
- }
- return invalid
- }
|