-
-
Notifications
You must be signed in to change notification settings - Fork 1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
ClientIP middleware proposal, intended to replace RealIP #967
base: master
Are you sure you want to change the base?
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I’ve checked the implementation, and I completely agree with the approach.
It addresses the concerns beautifully, and I’m very grateful for the effort put into this!
c2354ea
to
4b70ef1
Compare
Rebased against master. The CI tests are now passing since we dropped support for Go 1.19 and older and thus can we use @jawnsy @adam-p @convto @Dirbaio @pkieltyka @mfridman @lrstanley @n33pm @c2h5oh I'd appreciate any more reviews 👀 🙏 . |
// Ignore loopback, private, or unspecified addresses. | ||
if ip.IsLoopback() || ip.IsPrivate() || ip.IsUnspecified() { | ||
continue | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If trusted prefixes are set then we only care and/or can trust the first value past those trusted prefixes. No loopback, private or unspecified addresses should exist past/in between trusted prefixes unless headers have been tampered with.
- Move trusted prefixes check block before this
- Loopback/private/unspecified check should exit without setting client IP if any trusted prefixes are set
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think the loopback/private/unspecified checks should be here at all. The user has specified that they trust certain reverse proxies, and I don't think we should have code here that second-guesses that. I think the first non-trusted IP should be returned regardless of what it is.
I would even prefer that there not be a check for IP validity at all (i.e., the netip.ParseAddr) call above, but it's obviously necessary for prefix checking. If the parse fails, I think it should trigger an error condition and not just be skipped.
These kinds of bad/incorrect IP failures indicate a configuration error. Which is pretty bad!
I'm even not sure I like "don't store the IP" as the way to handle error states. Panicking by default is unpalatable (although that's how I prefer to handle developer/configuration errors). Maybe there can be a flag or callback or something? I don't feel super strongly about it, though.
// | ||
// Note: Private IP ranges (e.g., "10.0.0.0/8", "192.168.0.0/16", "172.16.0.0/12") | ||
// are automatically excluded by netip.Addr.IsPrivate() and do not need to be added here. | ||
func ClientIPFromXFFHeader(trustedIPPrefixes ...string) func(http.Handler) http.Handler { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Having numTrustedProxies
as an alternative to trusted prefixes would be a useful option too. If you control exactly n proxies, but not necessarily all possible proxies on a network (e.g. corporate intranet situation).
When set we should look only at nth rightmost value and only check if it's unspecified or loopback to reject - private is fine.
// - "True-Client-IP" - Akamai, Cloudflare Enterprise | ||
// - "X-Azure-ClientIP" - Azure Front Door | ||
// - "Fastly-Client-IP" - Fastly |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Back when I wrote the blog post I found (mostly by reading, not testing) that these are not "trusted headers":
- Azure's
X-Azure-ClientIP
- Akamai's
True-Client-IP
- Fastly's
Fastly-Client-IP
// Ignore loopback, private, or unspecified addresses. | ||
if ip.IsLoopback() || ip.IsPrivate() || ip.IsUnspecified() { | ||
continue | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think the loopback/private/unspecified checks should be here at all. The user has specified that they trust certain reverse proxies, and I don't think we should have code here that second-guesses that. I think the first non-trusted IP should be returned regardless of what it is.
I would even prefer that there not be a check for IP validity at all (i.e., the netip.ParseAddr) call above, but it's obviously necessary for prefix checking. If the parse fails, I think it should trigger an error condition and not just be skipped.
These kinds of bad/incorrect IP failures indicate a configuration error. Which is pretty bad!
I'm even not sure I like "don't store the IP" as the way to handle error states. Panicking by default is unpalatable (although that's how I prefer to handle developer/configuration errors). Maybe there can be a flag or callback or something? I don't feel super strongly about it, though.
|
||
// Parse the IP address from the trusted header. | ||
ip, err := netip.ParseAddr(r.Header.Get(trustedHeader)) | ||
if err != nil || ip.IsLoopback() || ip.IsUnspecified() || ip.IsPrivate() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For the same reasons as I mention in reply to @c2h5oh's comment below, I don't think these checks should be here at all. (In short: The user has said this is trustworthy and we should treat it as such.)
// field of the HTTP request and injects it into the request context if it is | ||
// not already set. The parsed IP address can be retrieved using GetClientIP(). | ||
// | ||
// The middleware ignores invalid or private IPs. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As above: Why? The user has indicated that this is the value they want.
But also... I don't think you're actually doing the loopback/private/unspecified check here, so I don't think this comment is true?
// Returns an empty string if no valid IP is found. | ||
func GetClientIP(ctx context.Context) string { | ||
ip, ok := ctx.Value(clientIPCtxKey).(netip.Addr) | ||
if !ok || !ip.IsValid() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Depending on above discussions, the ip.IsValid check might not be right.
} | ||
|
||
func TestClientIPFromXFFHeader(t *testing.T) { | ||
tt := []struct { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would prefer to see tests that exercise the trusted range functionality.
A proposed solution for the RealIP middleware issues outlined at:
#708
https://adam-p.ca/blog/2022/03/x-forwarded-for/
#711
#453
#908
Summary of differences:
RealIP
,ClientIP
middleware doesn't change the value ofreq.RemoteAddr
.GetClientIP()
getterbelonging to their infrastructure, such as proxies or Load Balances, depending on their setup
Kudos to Adam Pritchard, Jonathan Yu, Yuya Okumura, Liam Stanley, and everyone else involved in the detailed reports and discussion.
I'm seeking early feedback and reviews on the proposed API.
CC @jawnsy @adam-p @convto @Dirbaio @pkieltyka @mfridman @lrstanley @n33pm
P.S.: This PR uses the
"net/netip"
package and assumes that we'll accept proposal to drop support for Go 1.17 and older. Please have a look!Example usage:
Get the client IP address value in HTTP handler: