Files
seaweedfs/weed/s3api/s3err/audit_fluent_test.go
T
Chris Lu 6572b472c3 fix(s3): honor X-Forwarded-For in audit log remote_ip (#9295)
* fix(s3): honor X-Forwarded-For in audit log remote_ip

When SeaweedFS S3 sits behind a reverse proxy (e.g., Caddy), the audit
log's `remote_ip` was reporting the proxy's address because only
`X-Real-IP` and `r.RemoteAddr` were consulted. Caddy and most other
proxies set `X-Forwarded-For` by default but not `X-Real-IP`, so the
real client IP was lost.

Check `X-Forwarded-For` first (using the left-most non-empty entry as
the originating client), then fall back to `X-Real-IP`, then
`r.RemoteAddr`.

Fixes #9293

* fix(s3): strip port from RemoteAddr fallback in audit log

Address PR review: the X-Forwarded-For and X-Real-IP paths return
host-only values, while the RemoteAddr fallback was returning
"host:port", making the remote_ip field inconsistent with both the
other code paths and the "192.0.2.3" example in the AccessLog struct.
Use net.SplitHostPort to strip the port, falling back to the raw
RemoteAddr for non-IP markers (e.g., "@" for unix sockets).
2026-04-30 15:19:04 -07:00

88 lines
2.3 KiB
Go

package s3err
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/seaweedfs/seaweedfs/weed/util/request_id"
"github.com/stretchr/testify/assert"
)
func TestGetAccessLogUsesAmzRequestID(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/bucket/object", nil)
req = req.WithContext(request_id.Set(req.Context(), "req-123"))
log := GetAccessLog(req, http.StatusOK, ErrNone)
assert.Equal(t, "req-123", log.RequestID)
}
func TestGetAccessLogRemoteIP(t *testing.T) {
tests := []struct {
name string
remoteAddr string
xRealIP string
xForwardedFor string
expectedRemote string
}{
{
name: "falls back to RemoteAddr (port stripped) when no headers set",
remoteAddr: "10.89.0.1:35832",
expectedRemote: "10.89.0.1",
},
{
name: "preserves IPv6 host from RemoteAddr",
remoteAddr: "[2001:db8::1]:35832",
expectedRemote: "2001:db8::1",
},
{
name: "returns RemoteAddr unchanged when no port present",
remoteAddr: "@",
expectedRemote: "@",
},
{
name: "uses X-Real-IP when X-Forwarded-For is absent",
remoteAddr: "10.89.0.1:35832",
xRealIP: "203.0.113.7",
expectedRemote: "203.0.113.7",
},
{
name: "prefers X-Forwarded-For over X-Real-IP",
remoteAddr: "10.89.0.1:35832",
xRealIP: "203.0.113.7",
xForwardedFor: "198.51.100.42",
expectedRemote: "198.51.100.42",
},
{
name: "uses first hop in X-Forwarded-For chain",
remoteAddr: "10.89.0.1:35832",
xForwardedFor: "198.51.100.42, 10.0.0.5, 10.89.0.1",
expectedRemote: "198.51.100.42",
},
{
name: "skips empty leading entries in X-Forwarded-For",
remoteAddr: "10.89.0.1:35832",
xForwardedFor: ", 198.51.100.42",
expectedRemote: "198.51.100.42",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/bucket/object", nil)
req.RemoteAddr = tc.remoteAddr
if tc.xRealIP != "" {
req.Header.Set("X-Real-IP", tc.xRealIP)
}
if tc.xForwardedFor != "" {
req.Header.Set("X-Forwarded-For", tc.xForwardedFor)
}
log := GetAccessLog(req, http.StatusOK, ErrNone)
assert.Equal(t, tc.expectedRemote, log.RemoteIP)
})
}
}