Skip to content

Commit 3254ce3

Browse files
committed
switch to Strategy as an interface
1 parent e181488 commit 3254ce3

File tree

8 files changed

+570
-449
lines changed

8 files changed

+570
-449
lines changed

README.md

Lines changed: 51 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
11
[![GoDoc](https://godoc.org/github.com/realclientip/realclientip-go?status.svg)](http://godoc.org/github.com/realclientip/realclientip-go)
2-
[![Go Playground](https://img.shields.io/badge/Go-playground-%23007d9c?style=flat)](https://go.dev/play/p/Z0jNsEcOCnL)
2+
[![Go Playground](https://img.shields.io/badge/Go-playground-%23007d9c?style=flat)](https://go.dev/play/p/NUvc6U8KQKI)
33
[![Test](https://github.com/realclientip/realclientip-go/actions/workflows/test.yml/badge.svg)](https://github.com/realclientip/realclientip-go/actions/workflows/test.yml)
44
![coverage](https://img.shields.io/badge/coverage-100%25-success?style=flat)
5-
![license](https://img.shields.io/badge/license-0BSD-important.svg?style=flat)
5+
[![license](https://img.shields.io/badge/license-0BSD-important.svg?style=flat)](https://choosealicense.com/licenses/0bsd/)
66

77
# realclientip-go
88

9-
`X-Forwarded-For` and other "real" client IP headers are [often used incorrectly](https://adam-p.ca/blog/2022/03/x-forwarded-for/), resulting in bugs and security vulnerabilities. This library is an attempt to create a reference implementation of the correct ways to use such headers.
9+
`X-Forwarded-For` and other "real" client IP headers are [often used incorrectly][xff-post], resulting in bugs and security vulnerabilities. This library is an attempt to create a reference implementation of the correct ways to use such headers.
1010

11-
This library is written in Go, but the hope is that it will be reimplemented in other languages. Please open an issue if you would like to create such an implementation.
11+
[xff-post]: https://adam-p.ca/blog/2022/03/x-forwarded-for/
1212

13+
This library is written in Go, but the hope is that it will be reimplemented in other languages. Please open an issue if you would like to create such an implementation.
1314

1415
This library is freely licensed. You may use it as a dependency or copy it or modify it or anything else you want. It has no dependencies, is written in pure Go, and supports Go versions as far back as 1.13.
1516

@@ -18,46 +19,62 @@ This library is freely licensed. You may use it as a dependency or copy it or mo
1819
This library provides strategies for extracting the desired "real" client IP from various headers or from `http.Request.RemoteAddr` (the client socket IP).
1920

2021
```golang
21-
clientIPStrategy, err := realclientip.RightmostTrustedCountStrategy("X-Forwarded-For", 1)
22+
strategy, err := realclientip.NewRightmostTrustedCountStrategy("X-Forwarded-For", 2)
2223
...
23-
clientIP := clientIPStrategy(req.Header, req.RemoteAddr)
24+
clientIP := strategy.ClientIP(req.Header, req.RemoteAddr)
2425
```
2526

26-
Try it out [in the playground](https://go.dev/play/p/Z0jNsEcOCnL).
27+
Try it out [in the playground](https://go.dev/play/p/NUvc6U8KQKI).
28+
29+
There are a number of different strategies available -- the right one will depend on your network configuration. See the [documentation] to find out what's available and which you should use.
2730

28-
There are a number of different strategies available -- the right one will depend on your network configuration. See the [documentation](https://pkg.go.dev/github.com/realclientip/realclientip-go) to find out what's available and which you should use.
31+
`ClientIP` is threadsafe for all strategies. The same strategy instance can be used for handling all HTTP requests, for example.
2932

30-
There are some more extensive examples of use in the [`_examples` directory](/_examples/).
33+
[documentation]: (https://pkg.go.dev/github.com/realclientip/realclientip-go)
34+
35+
There are examples of use in the [documentation] and [`_examples` directory](/_examples/).
3136

3237
### Strategy failures
3338

34-
The Strategy used must be chosen and tuned for your network configuration. This _should_ result in the Strategy _never_ returning an empty string -- i.e., never failing to find a candidate for the "real" IP. Consequently, getting an empty-string result should be treated as an application error, perhaps even worthy of panicking.
39+
The strategy used must be chosen and tuned for your network configuration. This _should_ result in the strategy _never_ returning an empty string -- i.e., never failing to find a candidate for the "real" IP. Consequently, getting an empty-string result should be treated as an application error, perhaps even worthy of panicking.
3540

36-
For example, if you have 2 levels of trusted reverse proxies, you would probably use `RightmostTrustedCountStrategy` and it should work every time. If you're directly connected to the internet, you would probably use `RemoteAddrStrategy` or something like `ChainStrategies(LeftmostNonPrivateStrategy(...), RemoteAddrStrategy)` and you will be sure to get a value every time. If you're behind Cloudflare, you would probably use `SingleIPHeaderStrategy("Cf-Connecting-IP")` and it should work every time.
41+
For example, if you have 2 levels of trusted reverse proxies, you would probably use `RightmostTrustedCountStrategy` and it should work every time. If you're directly connected to the internet, you would probably use `RemoteAddrStrategy` or something like `ChainStrategy(LeftmostNonPrivateStrategy(...), RemoteAddrStrategy)` and you will be sure to get a value every time. If you're behind Cloudflare, you would probably use `SingleIPHeaderStrategy("Cf-Connecting-IP")` and it should work every time.
3742

38-
So if an empty string is returned, it is either because the Strategy choice or configuration is incorrect, or your network configuration has changed. In either case, immediate remediation is required.
43+
So if an empty string is returned, it is either because the strategy choice or configuration is incorrect or your network configuration has changed. In either case, immediate remediation is required.
3944

4045
### Headers
4146

4247
Leftmost-ish and rightmost-ish strategies support the `X-Forwarded-For` and `Forwarded` headers.
4348

44-
`SingleIPHeaderStrategy` supports any header containing a single IP address or IP:port. For a list of some common headers, see the [Single-IP Headers wiki page](https://github.com/realclientip/realclientip-go/wiki/Single-IP-Headers).
49+
`SingleIPHeaderStrategy` supports any header containing a single IP address or IP:port. For a list of some common headers, see the [Single-IP Headers wiki page][single-ip-wiki].
50+
51+
You must choose exactly the correct header for your configuration. Choosing the wrong header can result in failing to get the client IP or falling victim to IP spoofing.
52+
53+
Do not abuse `ChainStrategy` to check multiple headers. There is likely only one header you should be checking, and checking more can leave you vulnerable to IP spoofing.
54+
55+
[single-ip-wiki]: https://github.com/realclientip/realclientip-go/wiki/Single-IP-Headers
4556

4657
#### `Forwarded` header support
4758

48-
Support for the `Forwarded` header should be sufficient for the vast majority of rightmost-ish uses, but it is not complete and doesn't completely adhere to [RFC 7239](https://datatracker.ietf.org/doc/html/rfc7239). See the [`Test_forwardedHeaderRFCDeviations`](https://github.com/realclientip/realclientip-go/blob/65719ac74acb471001b3049b4270a3cc38920a30/realclientip_test.go#L1895) test for details on deviations.
59+
Support for the [`Forwarded` header] should be sufficient for the vast majority of rightmost-ish uses, but it is not complete and doesn't completely adhere to [RFC 7239]. See the [`Test_forwardedHeaderRFCDeviations`] test for details on deviations.
60+
61+
[`Forwarded` header]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Forwarded
62+
[RFC 7239]: https://datatracker.ietf.org/doc/html/rfc7239
63+
[`Test_forwardedHeaderRFCDeviations`]: https://github.com/realclientip/realclientip-go/blob/65719ac74acb471001b3049b4270a3cc38920a30/realclientip_test.go#L1895
4964

5065
### IPv6 zones
5166

52-
IPv6 zone identifiers are retained in the IP address returned by the strategies. [Whether you should keep the zone](https://adam-p.ca/blog/2022/03/strip-ipv6-zone/) depends on your specific use case. As a general rule, if you are not immediately using the IP address (for example, if you are appending it to the `X-Forwarded-For` header and passing it on), then you _should_ include the zone. This allows downstream consumers the option to use it. If your code is the final consumer of the IP address, then keeping the zone will depend on your specific case (for example: if you're logging the IP, then you probably want the zone; if you are rate limiting by IP, then you probably want to discard it).
67+
IPv6 zone identifiers are retained in the IP address returned by the strategies. [Whether you should keep the zone][strip-zone-post] depends on your specific use case. As a general rule, if you are not immediately using the IP address (for example, if you are appending it to the `X-Forwarded-For` header and passing it on), then you _should_ include the zone. This allows downstream consumers the option to use it. If your code is the final consumer of the IP address, then keeping the zone will depend on your specific case (for example: if you're logging the IP, then you probably want the zone; if you are rate limiting by IP, then you probably want to discard it).
5368

5469
To split the zone off and discard it, you may use `realclientip.SplitHostZone`.
5570

71+
[strip-zone-post]: https://adam-p.ca/blog/2022/03/strip-ipv6-zone/
72+
5673
### Known IP ranges
5774

5875
There is a copy of [Cloudflare's IP ranges](https://www.cloudflare.com/ips/) under `ranges.Cloudflare`. This can be used with `realclientip.RightmostTrustedRangeStrategy`. We may add more known cloud provider ranges in the future. Contributions are welcome to add new providers or update existing ones.
5976

60-
(It might be preferable to use [provider APIs](https://api.cloudflare.com/#cloudflare-ips-properties) to retrieve the ranges, as they are guaranteed to be up to date.)
77+
(It might be preferable to use [provider APIs](https://api.cloudflare.com/#cloudflare-ips-properties) to retrieve the ranges, as they are guaranteed to be up-to-date.)
6178

6279
## Implementation decisions and notes
6380

@@ -73,7 +90,7 @@ The values `0.0.0.0` (zero) and `::` (unspecified) are valid IPs, strictly speak
7390

7491
### Normalizing IPs
7592

76-
All IPs output by the library are first convert to a structure (like `net.IP`) and then stringified. This helps normalize the cases where there are multiple ways of encoding the same IP -- like `192.0.2.1` and `::ffff:192.0.2.1`, and the various zero-collapsed states of IPv6 (`fe80::1` vs `fe80::0:0:0:1`, etc.).
93+
All IPs output by the library are first converted to a structure (like `net.IP`) and then stringified. This helps normalize the cases where there are multiple ways of encoding the same IP -- like `192.0.2.1` and `::ffff:192.0.2.1`, and the various zero-collapsed states of IPv6 (`fe80::1` vs `fe80::0:0:0:1`, etc.).
7794

7895
### Input format strictness
7996

@@ -82,9 +99,7 @@ Some input is allowed that isn't strictly correct. Some examples:
8299
* IPv4 with brackets: `[2.2.2.2]:1234`
83100
* IPv4 with zone: `2.2.2.2%eth0`
84101
* Non-numeric port values: `2.2.2.2:nope`
85-
* `Forwarded` header with too much space: `For= 2.2.2.2`
86-
* `Forwarded` header no quotes around IPv6 values: `For=[2001:db8:cafe::18]:1234`
87-
* `Forwarded` header components that are invalid (as long as `For=` is also present): `For="2001:db8:cafe::18";Nope=what`
102+
* Other `Forwarded` header [deviations][`Test_forwardedHeaderRFCDeviations`]
88103

89104
It could be argued that it would be better to be absolutely strict in what is accepted.
90105

@@ -94,10 +109,10 @@ As this library aspires to be a "reference implementation", the code is heavily
94109

95110
### Pre-creating Strategies
96111

97-
Most of the strategies are created by calling a function, like `RightmostTrustedCountStrategy("Forwarded", 2)`. That can make it awkward to create-and-call at the same time, like `RightmostTrustedCountStrategy("Forwarded", 2)(r.Header, r.RemoteAddr)`. We could have instead implemented non-pre-created functions, like `RightmostTrustedCountStrategy("Forwarded", 2, r.Header, r.RemoteAddr)`. The reasons for the way we did it include:
98-
1. A consistent call signature -- i.e., the `Strategy` type. This enables `ChainStrategies`.
112+
Strategies are created by calling a constructor, like `NewRightmostTrustedCountStrategy("Forwarded", 2)`. That can make it awkward to create-and-call at the same time, like `NewRightmostTrustedCountStrategy("Forwarded", 2).ClientIP(r.Header, r.RemoteAddr)`. We could have instead implemented non-pre-created functions, like `RightmostTrustedCountStrategy("Forwarded", 2, r.Header, r.RemoteAddr)`. The reasons for the way we did it include:
113+
1. A consistent interface. This enables `ChainStrategy`. It also enables library users to have code paths that aren't strategy-dependent, in case they want the strategy to be configurable.
99114
2. Pre-creation allows us to put as much of the invariant processing as possible into the creation step. (Although, in practice, so far, this is only the header name canonicalization.)
100-
3. No error return is required from the strategies. (Although they can -- but should not -- return empty string.) All error-prone processing is done in the pre-creation.
115+
3. No error return is required from the strategy `ClientIP` calls. (Although they can -- but should not -- return empty string.) All error-prone processing is done in the pre-creation.
101116

102117
An alternative approach could be using functions like:
103118

@@ -110,6 +125,19 @@ _, ip, err := RightmostTrustedRangeStrategy("Forward", 2, r.Header, r.RemoteAddr
110125

111126
But perhaps that's no less awkward.
112127

128+
### Interfaces vs Functions
129+
130+
A pre-release implementation of this library [constructed functions] rather than structs that implement an interface. The switch to the latter was made for a few reasons:
131+
* It seems slightly more Go-idiomatic.
132+
* It allows for adding new methods in the future without breaking the API. (Such as `String()`.)
133+
* It allows for configuration information to appear in a printf of a strategy struct. This can be useful for logging.
134+
* The function approach is still easy to use, with the bound `ClientIP` method:
135+
```golang
136+
getClientIP := NewRightmostTrustedCountStrategy("Forwarded", 2).ClientIP
137+
```
138+
139+
[constructed functions]: https://github.com/realclientip/realclientip-go/blob/f72b2beafd60ac4a2a07a943f47795760c105688/realclientip.go#L22
140+
113141
## Other language implementations
114142

115143
If you want to reproduce this implementation in another language, please create an issue and we'll make a repo under this organization for you to use.

_examples/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
These are examples of usage that require dependencies that we don't want to become dependencies of our main project.

_examples/middleware/go.mod

Lines changed: 0 additions & 7 deletions
This file was deleted.

_examples/tollbooth/main.go

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@ import (
1111

1212
func main() {
1313
// Choose the right strategy for our network configuration
14-
clientIPStrategy, err := realclientip.RightmostNonPrivateStrategy("X-Forwarded-For")
14+
strat, err := realclientip.NewRightmostNonPrivateStrategy("X-Forwarded-For")
1515
if err != nil {
16-
log.Fatal("realclientip.RightmostNonPrivateStrategy returned error (bad input)")
16+
log.Fatal("realclientip.NewRightmostNonPrivateStrategy returned error (bad input)")
1717
}
1818

1919
lmt := tollbooth.NewLimiter(1, nil)
@@ -23,10 +23,10 @@ func main() {
2323
req.Header.Add("X-Forwarded-For", "1.1.1.1, 2.2.2.2, 3.3.3.3, 192.168.1.1")
2424
req.RemoteAddr = "192.168.1.2:8888"
2525

26-
clientIP := clientIPStrategy(req.Header, req.RemoteAddr)
26+
clientIP := strat.ClientIP(req.Header, req.RemoteAddr)
2727
if clientIP == "" {
2828
// This should probably result in the request being denied
29-
log.Fatal("clientIPStrategy found no IP")
29+
log.Fatal("strat.ClientIP found no IP")
3030
}
3131

3232
// We don't want to include the zone in our limiter key
@@ -37,4 +37,6 @@ func main() {
3737
} else {
3838
fmt.Println("Request allowed")
3939
}
40+
41+
// Output: Request allowed
4042
}

_examples/middleware/main.go renamed to example_middleware_test.go

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package main
1+
package realclientip_test
22

33
import (
44
"context"
@@ -11,15 +11,16 @@ import (
1111
"github.com/realclientip/realclientip-go"
1212
)
1313

14-
func main() {
14+
func Example_middleware() {
1515
// Choose the right strategy for our network configuration
16-
clientIPStrategy, err := realclientip.RightmostNonPrivateStrategy("X-Forwarded-For")
16+
strat, err := realclientip.NewRightmostNonPrivateStrategy("X-Forwarded-For")
1717
if err != nil {
18-
log.Fatal("realclientip.RightmostNonPrivateStrategy returned error (bad input)")
18+
log.Fatal("realclientip.NewRightmostNonPrivateStrategy returned error (bad input)")
1919
}
2020

2121
// Place our middleware before the handler
22-
httpServer := httptest.NewServer(clientIPMiddleware(clientIPStrategy, http.HandlerFunc(handler)))
22+
handlerWithMiddleware := clientIPMiddleware(strat, http.HandlerFunc(handler))
23+
httpServer := httptest.NewServer(handlerWithMiddleware)
2324
defer httpServer.Close()
2425

2526
req, _ := http.NewRequest("GET", httpServer.URL, nil)
@@ -37,15 +38,19 @@ func main() {
3738
}
3839

3940
fmt.Printf("%s", b)
41+
// Output:
42+
// your IP: 3.3.3.3
4043
}
4144

4245
type clientIPCtxKey struct{}
4346

4447
// Adds the "real" client IP to the request context under the clientIPCtxKey{} key.
4548
// If the client IP couldn't be obtained, the value will be an empty string.
46-
func clientIPMiddleware(clientIPStrategy realclientip.Strategy, next http.Handler) http.Handler {
49+
// We could use the RightmostNonPrivateStrategy concrete type, but instead we'll pass
50+
// around the Strategy interface, in case we decide to change our strategy in the future.
51+
func clientIPMiddleware(strat realclientip.Strategy, next http.Handler) http.Handler {
4752
fn := func(w http.ResponseWriter, r *http.Request) {
48-
clientIP := clientIPStrategy(r.Header, r.RemoteAddr)
53+
clientIP := strat.ClientIP(r.Header, r.RemoteAddr)
4954
if clientIP == "" {
5055
// Write error log. Consider aborting the request depending on use.
5156
log.Fatal("Failed to find client IP")
@@ -59,5 +64,6 @@ func clientIPMiddleware(clientIPStrategy realclientip.Strategy, next http.Handle
5964
}
6065

6166
func handler(w http.ResponseWriter, r *http.Request) {
62-
fmt.Fprintln(w, "your IP:", r.Context().Value(clientIPCtxKey{}))
67+
clientIP := r.Context().Value(clientIPCtxKey{})
68+
fmt.Fprintln(w, "your IP:", clientIP)
6369
}
File renamed without changes.

0 commit comments

Comments
 (0)