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:
Jlypx
2026-07-19 21:39:49 +08:00
co-authored by Sisyphus
parent 8a147fcc51
commit 6e2ba0e4cd
2 changed files with 99 additions and 29 deletions
+33 -3
View File
@@ -8,6 +8,24 @@ import (
"github.com/gin-gonic/gin"
)
const legacyForwardedIPTrustKey = "sub2api.legacy_forwarded_ip_trust"
// 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)
}
}
func requestUsesLegacyForwardedIPTrust(c *gin.Context) bool {
if c == nil {
return true
}
enabled, ok := c.Get(legacyForwardedIPTrustKey)
return !ok || enabled == true
}
// 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-
@@ -16,6 +34,9 @@ func GetClientIP(c *gin.Context) string {
if c == nil {
return ""
}
if !requestUsesLegacyForwardedIPTrust(c) {
return GetTrustedClientIP(c)
}
// Preserve the historical precedence used by existing reverse-proxy
// deployments, while skipping an internal proxy address when a public XFF
@@ -66,9 +87,18 @@ func GetTrustedClientIP(c *gin.Context) string {
}
// GetSecurityClientIP returns the address used by security-sensitive paths.
// The legacy toggle remains in the signature for configuration/API
// compatibility, but cannot make raw forwarding headers trustworthy by itself.
func GetSecurityClientIP(c *gin.Context, _ bool) string {
// 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 c != nil {
if requestTrust, ok := c.Get(legacyForwardedIPTrustKey); ok {
trustForwarded = requestTrust == true
}
}
if trustForwarded {
return GetClientIP(c)
}
return GetTrustedClientIP(c)
}
+66 -26
View File
@@ -73,41 +73,30 @@ func TestCheckIPRestrictionWithCompiledRules_InvalidWhitelistStillDenies(t *test
require.Equal(t, "access denied", reason)
}
func TestGetSecurityClientIPNeverTrustsHeadersFromUntrustedPeer(t *testing.T) {
func TestGetSecurityClientIPSwitchEnabledUsesLegacyHeaders(t *testing.T) {
gin.SetMode(gin.TestMode)
for _, tc := range []struct {
name string
trustForwarded bool
want string
}{
{name: "legacy toggle disabled", trustForwarded: false, want: "9.9.9.9"},
{name: "legacy toggle enabled", trustForwarded: true, want: "9.9.9.9"},
} {
t.Run(tc.name, func(t *testing.T) {
r := gin.New()
require.NoError(t, r.SetTrustedProxies(nil))
r.GET("/t", func(c *gin.Context) {
c.String(200, GetSecurityClientIP(c, tc.trustForwarded))
})
r := gin.New()
require.NoError(t, r.SetTrustedProxies(nil))
r.GET("/t", func(c *gin.Context) {
c.String(200, GetSecurityClientIP(c, true))
})
w := httptest.NewRecorder()
req := httptest.NewRequest("GET", "/t", nil)
req.RemoteAddr = "9.9.9.9:12345"
req.Header.Set("X-Real-IP", "1.2.3.4")
r.ServeHTTP(w, req)
w := httptest.NewRecorder()
req := httptest.NewRequest("GET", "/t", nil)
req.RemoteAddr = "9.9.9.9:12345"
req.Header.Set("X-Real-IP", "1.2.3.4")
r.ServeHTTP(w, req)
require.Equal(t, 200, w.Code)
require.Equal(t, tc.want, w.Body.String())
})
}
require.Equal(t, 200, w.Code)
require.Equal(t, "1.2.3.4", w.Body.String())
}
func TestGetSecurityClientIPUsesConfiguredTrustedProxy(t *testing.T) {
func TestGetSecurityClientIPSwitchDisabledUsesConfiguredTrustedProxy(t *testing.T) {
gin.SetMode(gin.TestMode)
r := gin.New()
require.NoError(t, r.SetTrustedProxies([]string{"9.9.9.9"}))
r.GET("/t", func(c *gin.Context) { c.String(200, GetSecurityClientIP(c, true)) })
r.GET("/t", func(c *gin.Context) { c.String(200, GetSecurityClientIP(c, false)) })
w := httptest.NewRecorder()
req := httptest.NewRequest("GET", "/t", nil)
@@ -117,3 +106,54 @@ func TestGetSecurityClientIPUsesConfiguredTrustedProxy(t *testing.T) {
require.Equal(t, "1.2.3.4", w.Body.String())
}
func TestGetClientIPSwitchDisabledUsesTrustedProxyChain(t *testing.T) {
gin.SetMode(gin.TestMode)
r := gin.New()
require.NoError(t, r.SetTrustedProxies(nil))
r.GET("/t", func(c *gin.Context) {
SetLegacyForwardedIPTrust(c, false)
c.String(200, GetClientIP(c))
})
w := httptest.NewRecorder()
req := httptest.NewRequest("GET", "/t", nil)
req.RemoteAddr = "9.9.9.9:12345"
req.Header.Set("X-Real-IP", "1.2.3.4")
r.ServeHTTP(w, req)
require.Equal(t, "9.9.9.9", w.Body.String())
}
func TestGetSecurityClientIPRequestSnapshotOverridesLiveFallback(t *testing.T) {
gin.SetMode(gin.TestMode)
tests := []struct {
name string
requestTrust bool
fallbackTrust bool
want string
}{
{name: "captured secure mode wins", requestTrust: false, fallbackTrust: true, want: "9.9.9.9"},
{name: "captured compatibility mode wins", requestTrust: true, fallbackTrust: false, want: "1.2.3.4"},
}
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) {
SetLegacyForwardedIPTrust(c, test.requestTrust)
c.String(200, GetSecurityClientIP(c, test.fallbackTrust))
})
w := httptest.NewRecorder()
req := httptest.NewRequest("GET", "/t", nil)
req.RemoteAddr = "9.9.9.9:12345"
req.Header.Set("X-Real-IP", "1.2.3.4")
r.ServeHTTP(w, req)
require.Equal(t, test.want, w.Body.String())
})
}
}