mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-07 14:58:23 +08:00
fix: 按自定义请求头解析客户端 IP
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
@@ -8,22 +8,46 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
const legacyForwardedIPTrustKey = "sub2api.legacy_forwarded_ip_trust"
|
||||
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) {
|
||||
if c != nil {
|
||||
c.Set(legacyForwardedIPTrustKey, enabled)
|
||||
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 {
|
||||
if c == nil {
|
||||
return true
|
||||
}
|
||||
enabled, ok := c.Get(legacyForwardedIPTrustKey)
|
||||
return !ok || enabled == true
|
||||
settings, ok := requestForwardedIPSettings(c)
|
||||
return !ok || settings.trustForwarded
|
||||
}
|
||||
|
||||
// GetClientIP resolves the client address using the legacy forwarding-header
|
||||
@@ -38,15 +62,61 @@ func GetClientIP(c *gin.Context) string {
|
||||
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 := normalizeIP(c.GetHeader("CF-Connecting-IP")); forwarded != "" {
|
||||
fallback = forwarded
|
||||
if !isPrivateIP(forwarded) {
|
||||
return forwarded
|
||||
return forwarded, fallback
|
||||
}
|
||||
}
|
||||
if realIP := normalizeIP(c.GetHeader("X-Real-IP")); realIP != "" {
|
||||
@@ -54,7 +124,7 @@ func GetClientIP(c *gin.Context) string {
|
||||
fallback = realIP
|
||||
}
|
||||
if !isPrivateIP(realIP) {
|
||||
return realIP
|
||||
return realIP, fallback
|
||||
}
|
||||
}
|
||||
if xff := c.GetHeader("X-Forwarded-For"); xff != "" {
|
||||
@@ -62,18 +132,14 @@ func GetClientIP(c *gin.Context) string {
|
||||
for _, candidate := range ips {
|
||||
candidate = strings.TrimSpace(candidate)
|
||||
if candidate != "" && !isPrivateIP(candidate) {
|
||||
return normalizeIP(candidate)
|
||||
return normalizeIP(candidate), fallback
|
||||
}
|
||||
}
|
||||
if fallback == "" && len(ips) > 0 {
|
||||
fallback = normalizeIP(strings.TrimSpace(ips[0]))
|
||||
}
|
||||
}
|
||||
if fallback != "" {
|
||||
return fallback
|
||||
}
|
||||
|
||||
return normalizeIP(c.ClientIP())
|
||||
return "", fallback
|
||||
}
|
||||
|
||||
// GetTrustedClientIP 从 Gin 的可信代理解析链提取客户端 IP。
|
||||
@@ -91,10 +157,8 @@ func GetTrustedClientIP(c *gin.Context) string {
|
||||
// client-IP resolution. When disabled, Gin's server.trusted_proxies chain is
|
||||
// authoritative.
|
||||
func GetSecurityClientIP(c *gin.Context, trustForwarded bool) string {
|
||||
if c != nil {
|
||||
if requestTrust, ok := c.Get(legacyForwardedIPTrustKey); ok {
|
||||
trustForwarded = requestTrust == true
|
||||
}
|
||||
if requestSettings, ok := requestForwardedIPSettings(c); ok {
|
||||
trustForwarded = requestSettings.trustForwarded
|
||||
}
|
||||
if trustForwarded {
|
||||
return GetClientIP(c)
|
||||
|
||||
@@ -92,6 +92,101 @@ func TestGetSecurityClientIPSwitchEnabledUsesLegacyHeaders(t *testing.T) {
|
||||
require.Equal(t, "1.2.3.4", w.Body.String())
|
||||
}
|
||||
|
||||
func TestGetSecurityClientIPCustomHeaderPrecedenceAndFallback(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
trustForward bool
|
||||
headers []string
|
||||
requestHeaders map[string]string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "configured order precedes built-ins",
|
||||
trustForward: true,
|
||||
headers: []string{"X-CDN-First", "X-CDN-Second"},
|
||||
requestHeaders: map[string]string{
|
||||
"X-CDN-First": "198.51.100.10",
|
||||
"X-CDN-Second": "203.0.113.20",
|
||||
"CF-Connecting-IP": "8.8.8.8",
|
||||
},
|
||||
want: "198.51.100.10",
|
||||
},
|
||||
{
|
||||
name: "comma candidates skip invalid and private values",
|
||||
trustForward: true,
|
||||
headers: []string{"X-CDN-First", "X-CDN-Second"},
|
||||
requestHeaders: map[string]string{
|
||||
"X-CDN-First": "not-an-ip, 10.0.0.8",
|
||||
"X-CDN-Second": "also-bad, 203.0.113.9",
|
||||
},
|
||||
want: "203.0.113.9",
|
||||
},
|
||||
{
|
||||
name: "legacy public header wins over custom private fallback",
|
||||
trustForward: true,
|
||||
headers: []string{"X-CDN-IP"},
|
||||
requestHeaders: map[string]string{
|
||||
"X-CDN-IP": "10.0.0.8",
|
||||
"X-Real-IP": "1.2.3.4",
|
||||
},
|
||||
want: "1.2.3.4",
|
||||
},
|
||||
{
|
||||
name: "custom private fallback retains configured precedence",
|
||||
trustForward: true,
|
||||
headers: []string{"X-CDN-IP"},
|
||||
requestHeaders: map[string]string{
|
||||
"X-CDN-IP": "10.0.0.8",
|
||||
"X-Real-IP": "192.168.1.4",
|
||||
},
|
||||
want: "10.0.0.8",
|
||||
},
|
||||
{
|
||||
name: "invalid custom value continues to built-ins",
|
||||
trustForward: true,
|
||||
headers: []string{"X-CDN-IP"},
|
||||
requestHeaders: map[string]string{
|
||||
"X-CDN-IP": "1.2.3.4:443",
|
||||
"CF-Connecting-IP": "4.4.4.4",
|
||||
},
|
||||
want: "4.4.4.4",
|
||||
},
|
||||
{
|
||||
name: "disabled mode ignores custom and legacy headers",
|
||||
trustForward: false,
|
||||
headers: []string{"X-CDN-IP"},
|
||||
requestHeaders: map[string]string{
|
||||
"X-CDN-IP": "1.2.3.4",
|
||||
"X-Real-IP": "4.4.4.4",
|
||||
},
|
||||
want: "9.9.9.9",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
r := gin.New()
|
||||
require.NoError(t, r.SetTrustedProxies(nil))
|
||||
r.GET("/t", func(c *gin.Context) {
|
||||
SetForwardedIPSettings(c, test.trustForward, test.headers)
|
||||
c.String(200, GetSecurityClientIP(c, !test.trustForward))
|
||||
})
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req := httptest.NewRequest("GET", "/t", nil)
|
||||
req.RemoteAddr = "9.9.9.9:12345"
|
||||
for name, value := range test.requestHeaders {
|
||||
req.Header.Set(name, value)
|
||||
}
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
require.Equal(t, test.want, w.Body.String())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetSecurityClientIPSwitchDisabledUsesConfiguredTrustedProxy(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
r := gin.New()
|
||||
@@ -125,6 +220,28 @@ func TestGetClientIPSwitchDisabledUsesTrustedProxyChain(t *testing.T) {
|
||||
require.Equal(t, "9.9.9.9", w.Body.String())
|
||||
}
|
||||
|
||||
func TestGetSecurityClientIPRequestSnapshotCopiesCustomHeaders(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
r := gin.New()
|
||||
require.NoError(t, r.SetTrustedProxies(nil))
|
||||
r.GET("/t", func(c *gin.Context) {
|
||||
headers := []string{"X-Original-IP"}
|
||||
SetForwardedIPSettings(c, true, headers)
|
||||
headers[0] = "X-Mutated-IP"
|
||||
c.String(200, GetSecurityClientIP(c, false))
|
||||
})
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req := httptest.NewRequest("GET", "/t", nil)
|
||||
req.RemoteAddr = "9.9.9.9:12345"
|
||||
req.Header.Set("X-Original-IP", "1.2.3.4")
|
||||
req.Header.Set("X-Mutated-IP", "4.4.4.4")
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
require.Equal(t, "1.2.3.4", w.Body.String())
|
||||
}
|
||||
|
||||
func TestGetSecurityClientIPRequestSnapshotOverridesLiveFallback(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user