Allow specifying header source for client IP - #224
Merged
Conversation
There was a problem hiding this comment.
Pull request overview
This PR adds an opt-in way to treat an alternative “original client IP” header (e.g. Cloudflare’s True-Client-IP) as the source-of-truth by rewriting X-Forwarded-For early in request handling, so logging (remote_addr) and upstream forwarding behave as if X-Forwarded-For had been set by the downstream proxy.
Changes:
- Add
ServiceOptions.ClientIPHeaderand rewriteX-Forwarded-Forfrom that trusted header when configured. - Add
--client-ip-headerdeploy flag to configure the trusted header name. - Add a focused service test validating
X-Forwarded-Forrewriting and forwarding behavior.
Tip
If you aren't ready for review, convert to a draft PR.
Click "Convert to draft" or run gh pr ready --undo.
Click "Ready for review" or run gh pr ready to reengage.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
| internal/server/service.go | Introduces ClientIPHeader option and rewrites X-Forwarded-For from the configured trusted header before proxying/logging. |
| internal/server/service_test.go | Adds a test that verifies the rewritten X-Forwarded-For value observed by the target and preserves the original trusted header. |
| internal/cmd/deploy.go | Exposes the new behavior via --client-ip-header deploy flag. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
kevinmcconnell
force-pushed
the
true-client-ip-support
branch
from
July 16, 2026 12:02
bad4579 to
73559ec
Compare
kevinmcconnell
force-pushed
the
true-client-ip-support
branch
from
July 16, 2026 12:16
73559ec to
4400c42
Compare
Typically a downstreeam proxy will pass the original client IP via `X-Forwarded-For`, which we already handle. However some proxies use a different header. For example, Cloudflare typically sets it in `True-Client-IP`. To support this, add a new `--client-ip-header` deploy flag which specifies the name of the header to use. When this is set, we copy the content of that header into `X-Forwarded-For` before logging and proxying, as if `X-Forwarded-For` had been set that way in the request.
kevinmcconnell
force-pushed
the
true-client-ip-support
branch
from
July 16, 2026 12:26
4400c42 to
b44a731
Compare
Comment on lines
+460
to
+462
| if options.ClientIPHeader != "" { | ||
| handler = WithClientIPMiddleware(options.ClientIPHeader, handler) | ||
| } |
| deployCommand.cmd.Flags().StringSliceVar(&deployCommand.args.TargetOptions.LogResponseHeaders, "log-response-header", nil, "Additional response header to log (may be specified multiple times)") | ||
| deployCommand.cmd.Flags().StringSliceVar(&deployCommand.args.ServiceOptions.ExcludeMetricsPaths, "exclude-metrics-path", nil, "Request path(s) to exclude from Prometheus metrics (may be specified multiple times)") | ||
| deployCommand.cmd.Flags().BoolVar(&deployCommand.args.TargetOptions.ForwardHeaders, "forward-headers", false, "Forward X-Forwarded headers to target (default false if TLS enabled; otherwise true)") | ||
| deployCommand.cmd.Flags().StringVar(&deployCommand.args.ServiceOptions.ClientIPHeader, "client-ip-header", "", "Request header containing the original client IP; used to populate X-Forwarded-For when present") |
Comment on lines
+29
to
+70
| func TestService_ClientIPHeaderRewritesXForwardedFor(t *testing.T) { | ||
| var xForwardedFor, trueClientIP string | ||
|
|
||
| serviceOptions := defaultServiceOptions | ||
| serviceOptions.ClientIPHeader = "True-Client-IP" | ||
|
|
||
| targetOptions := defaultTargetOptions | ||
| targetOptions.ForwardHeaders = true | ||
|
|
||
| service := testCreateServiceWithHandler(t, serviceOptions, targetOptions, | ||
| http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||
| if r.URL.Path != defaultHealthCheckConfig.Path { | ||
| xForwardedFor = r.Header.Get("X-Forwarded-For") | ||
| trueClientIP = r.Header.Get("True-Client-IP") | ||
| } | ||
| })) | ||
|
|
||
| req := httptest.NewRequest(http.MethodGet, "http://example.com/", nil) | ||
| req.Header.Set("True-Client-IP", "203.0.113.9") | ||
| req.Header.Set("X-Forwarded-For", "6.6.6.6") | ||
|
|
||
| clientIP, _, err := net.SplitHostPort(req.RemoteAddr) | ||
| require.NoError(t, err) | ||
|
|
||
| w := httptest.NewRecorder() | ||
| service.ServeHTTP(w, req) | ||
|
|
||
| require.Equal(t, http.StatusOK, w.Result().StatusCode) | ||
| require.Equal(t, "203.0.113.9, "+clientIP, xForwardedFor) | ||
| require.Equal(t, "203.0.113.9", trueClientIP) | ||
|
|
||
| // Without the trusted header, the client-supplied X-Forwarded-For is | ||
| // forwarded unmodified, as usual. | ||
| req = httptest.NewRequest(http.MethodGet, "http://example.com/", nil) | ||
| req.Header.Set("X-Forwarded-For", "6.6.6.6") | ||
|
|
||
| w = httptest.NewRecorder() | ||
| service.ServeHTTP(w, req) | ||
|
|
||
| require.Equal(t, http.StatusOK, w.Result().StatusCode) | ||
| require.Equal(t, "6.6.6.6, "+clientIP, xForwardedFor) | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Typically a downstreeam proxy will pass the original client IP via
X-Forwarded-For, which we already handle. However some proxies use a different header. For example, Cloudflare typically sets it inTrue-Client-IP.To support this, add a new
--client-ip-headerdeploy flag which specifies the name of the header to use. When this is set, we copy the content of that header intoX-Forwarded-Forbefore logging and proxying, as ifX-Forwarded-Forhad been set that way in the request.