middleware: take the client address from the right of X-Forwarded-For (closes #181)
check / check (push) Failing after 39s
check / check (push) Failing after 39s
realIP took the first X-Forwarded-For entry, which the client itself can write, so behind a proxy that appends to the header a client chose the address dnswatcher logs and the /metrics rate limit counts. It now walks the entries from the right past trusted proxies, using the existing trusted-proxy check, and takes the first that is not one; the leftmost when all are. All X-Forwarded-For header lines are read as one list, since a proxy may add its own line instead of appending to the client's. An empty entry where the client address belongs falls back to the peer address, as an empty first entry did before. X-Real-IP is unchanged. Model: opus-5-5
This commit is contained in:
@@ -482,3 +482,99 @@ func TestMetricsRateLimitKeysOnClientAddress(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestRealIP checks which address realIP takes as the client's. Each
|
||||
// element of forwardedFor is sent as an X-Forwarded-For header line of
|
||||
// its own, and 198.51.100.9 is always an entry the client wrote itself.
|
||||
func TestRealIP(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
remoteAddr string
|
||||
xRealIP string
|
||||
forwardedFor []string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
"untrusted peer, both headers ignored",
|
||||
"198.51.100.1:4000",
|
||||
"203.0.113.1",
|
||||
[]string{"203.0.113.2"},
|
||||
"198.51.100.1",
|
||||
},
|
||||
{
|
||||
"X-Real-IP from a trusted proxy wins",
|
||||
"10.0.0.1:4000",
|
||||
"203.0.113.1",
|
||||
[]string{"203.0.113.2"},
|
||||
"203.0.113.1",
|
||||
},
|
||||
{
|
||||
"client's own entry, then the one the proxy added",
|
||||
"10.0.0.1:4000",
|
||||
"",
|
||||
[]string{"198.51.100.9, 203.0.113.1"},
|
||||
"203.0.113.1",
|
||||
},
|
||||
{
|
||||
"several trusted proxies",
|
||||
"10.0.0.1:4000",
|
||||
"",
|
||||
[]string{"198.51.100.9, 203.0.113.1, 10.0.0.3, 10.0.0.2"},
|
||||
"203.0.113.1",
|
||||
},
|
||||
{
|
||||
"proxy adds a header line of its own",
|
||||
"10.0.0.1:4000",
|
||||
"",
|
||||
[]string{"198.51.100.9", "203.0.113.1"},
|
||||
"203.0.113.1",
|
||||
},
|
||||
{
|
||||
"every entry a trusted proxy",
|
||||
"10.0.0.1:4000",
|
||||
"",
|
||||
[]string{"10.0.0.3, 10.0.0.2"},
|
||||
"10.0.0.3",
|
||||
},
|
||||
{
|
||||
"empty where the client address belongs",
|
||||
"10.0.0.1:4000",
|
||||
"",
|
||||
[]string{"203.0.113.1, , 10.0.0.2"},
|
||||
"10.0.0.1",
|
||||
},
|
||||
{
|
||||
"no headers from a trusted proxy",
|
||||
"10.0.0.1:4000",
|
||||
"",
|
||||
nil,
|
||||
"10.0.0.1",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
req := httptest.NewRequestWithContext(
|
||||
t.Context(), http.MethodGet, "/", nil,
|
||||
)
|
||||
req.RemoteAddr = tt.remoteAddr
|
||||
|
||||
if tt.xRealIP != "" {
|
||||
req.Header.Set("X-Real-IP", tt.xRealIP)
|
||||
}
|
||||
|
||||
for _, line := range tt.forwardedFor {
|
||||
req.Header.Add("X-Forwarded-For", line)
|
||||
}
|
||||
|
||||
got := middleware.RealIP(req)
|
||||
if got != tt.want {
|
||||
t.Errorf("realIP = %q, want %q", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user