Merge 2dc3c023731c6f8e21f658a02e4a99eb22b889de into d3ffc9985281dcf4d3bef604cce4e662b1a327a6

This commit is contained in:
Yvan Wang 2026-04-18 00:14:05 +08:00 committed by GitHub
commit 49145d46f2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 63 additions and 2 deletions

33
gin.go
View File

@ -485,8 +485,8 @@ func (engine *Engine) validateHeader(header string) (clientIP string, valid bool
} }
items := strings.Split(header, ",") items := strings.Split(header, ",")
for i := len(items) - 1; i >= 0; i-- { for i := len(items) - 1; i >= 0; i-- {
ipStr := strings.TrimSpace(items[i]) item := strings.TrimSpace(items[i])
ip := net.ParseIP(ipStr) ipStr, ip := parseForwardedForItem(item)
if ip == nil { if ip == nil {
break break
} }
@ -500,6 +500,35 @@ func (engine *Engine) validateHeader(header string) (clientIP string, valid bool
return "", false return "", false
} }
// parseForwardedForItem normalizes a single X-Forwarded-For entry and parses it.
// It accepts the four common forms emitted by reverse proxies:
//
// - "1.2.3.4"
// - "2001:db8::1"
// - "[2001:db8::1]" (IIS/ARR style)
// - "1.2.3.4:12345" (with port, some LBs)
// - "[2001:db8::1]:12345" (IIS/ARR + port)
//
// The returned string is the IP without brackets or port, so callers see a
// consistent form regardless of which proxy produced the header.
func parseForwardedForItem(item string) (string, net.IP) {
// Try host:port form first (handles "ip:port" and "[ipv6]:port").
if host, _, err := net.SplitHostPort(item); err == nil {
if ip := net.ParseIP(host); ip != nil {
return host, ip
}
}
// Strip optional surrounding brackets for bare "[ipv6]" with no port.
unbracketed := item
if strings.HasPrefix(unbracketed, "[") && strings.HasSuffix(unbracketed, "]") {
unbracketed = unbracketed[1 : len(unbracketed)-1]
}
if ip := net.ParseIP(unbracketed); ip != nil {
return unbracketed, ip
}
return "", nil
}
// updateRouteTree do update to the route tree recursively // updateRouteTree do update to the route tree recursively
func updateRouteTree(n *node) { func updateRouteTree(n *node) {
n.path = strings.ReplaceAll(n.path, escapedColon, colon) n.path = strings.ReplaceAll(n.path, escapedColon, colon)

View File

@ -1156,3 +1156,35 @@ func TestUpdateRouteTreesCalledOnce(t *testing.T) {
assert.Equal(t, "ok", w.Body.String()) assert.Equal(t, "ok", w.Body.String())
} }
} }
func TestValidateHeaderForwardedForForms(t *testing.T) {
engine := New()
// Disable trusted proxies so the rightmost parseable entry is returned.
require.NoError(t, engine.SetTrustedProxies(nil))
tests := []struct {
name string
header string
wantIP string
wantOK bool
}{
{"plain IPv4", "192.168.8.39", "192.168.8.39", true},
{"plain IPv6", "240e:318:2f4a:de56::240", "240e:318:2f4a:de56::240", true},
{"bracketed IPv6 (IIS/ARR)", "[240e:318:2f4a:de56::240]", "240e:318:2f4a:de56::240", true},
{"IPv4 with port", "192.168.8.39:38792", "192.168.8.39", true},
{"bracketed IPv6 with port", "[240e:318:2f4a:de56::240]:38792", "240e:318:2f4a:de56::240", true},
{"IPv6 loopback bracketed", "[::1]", "::1", true},
{"chain with port on last entry", "1.2.3.4, 5.6.7.8:9000", "5.6.7.8", true},
{"empty", "", "", false},
{"garbage", "not-an-ip", "", false},
{"bracketed garbage", "[not-an-ip]", "", false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gotIP, gotOK := engine.validateHeader(tt.header)
assert.Equal(t, tt.wantOK, gotOK)
assert.Equal(t, tt.wantIP, gotIP)
})
}
}