├── go.sum ├── .gitignore ├── go.mod ├── _examples ├── README.md └── tollbooth │ ├── go.mod │ ├── main.go │ └── go.sum ├── .github └── workflows │ └── test.yml ├── LICENSE ├── ranges ├── cloudflare.go └── cloudfront.go ├── example_middleware_test.go ├── example_playground_test.go ├── README.md ├── realclientip.go └── realclientip_test.go /go.sum: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/settings.json 2 | 3 | *.out 4 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/realclientip/realclientip-go 2 | 3 | go 1.13 4 | -------------------------------------------------------------------------------- /_examples/README.md: -------------------------------------------------------------------------------- 1 | These are examples of usage that require dependencies that we don't want to become dependencies of our main project. 2 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request] 2 | name: Test 3 | jobs: 4 | test: 5 | strategy: 6 | matrix: 7 | go-version: [1.13.x, 1.14.x, 1.15.x, 1.16.x, 1.17.x, 1.18.x] 8 | os: [ubuntu-latest] 9 | runs-on: ${{ matrix.os }} 10 | steps: 11 | - uses: actions/setup-go@v3 12 | with: 13 | go-version: ${{ matrix.go-version }} 14 | - uses: actions/checkout@v3 15 | - run: go test ./... 16 | -------------------------------------------------------------------------------- /_examples/tollbooth/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/realclientip/realclientip-go/_examples/tollbooth 2 | 3 | go 1.18 4 | 5 | replace github.com/realclientip/realclientip-go => ../.. 6 | 7 | require ( 8 | github.com/didip/tollbooth/v6 v6.1.2 9 | github.com/realclientip/realclientip-go v0.0.0-20220324120256-a2b8bb8de17c 10 | ) 11 | 12 | require ( 13 | github.com/go-pkgz/expirable-cache v0.0.3 // indirect 14 | github.com/pkg/errors v0.9.1 // indirect 15 | golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1 // indirect 16 | ) 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD Zero Clause License 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any 4 | purpose with or without fee is hereby granted. 5 | 6 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 7 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY 8 | AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 9 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 10 | LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR 11 | OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 12 | PERFORMANCE OF THIS SOFTWARE. 13 | -------------------------------------------------------------------------------- /ranges/cloudflare.go: -------------------------------------------------------------------------------- 1 | package ranges 2 | 3 | // Cloudflare's internet IP ranges. 4 | // It is taken from: https://www.cloudflare.com/ips/. 5 | // As an alternative, and to ensure up-to-date results, use the Cloudflare API to retrieve 6 | // these ranges at runtime: https://api.cloudflare.com/#cloudflare-ips-properties 7 | var Cloudflare = []string{ 8 | "173.245.48.0/20", 9 | "103.21.244.0/22", 10 | "103.22.200.0/22", 11 | "103.31.4.0/22", 12 | "141.101.64.0/18", 13 | "108.162.192.0/18", 14 | "190.93.240.0/20", 15 | "188.114.96.0/20", 16 | "197.234.240.0/22", 17 | "198.41.128.0/17", 18 | "162.158.0.0/15", 19 | "104.16.0.0/13", 20 | "104.24.0.0/14", 21 | "172.64.0.0/13", 22 | "131.0.72.0/22", 23 | "2400:cb00::/32", 24 | "2606:4700::/32", 25 | "2803:f800::/32", 26 | "2405:b500::/32", 27 | "2405:8100::/32", 28 | "2a06:98c0::/29", 29 | "2c0f:f248::/32", 30 | } 31 | -------------------------------------------------------------------------------- /_examples/tollbooth/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net/http" 7 | 8 | "github.com/didip/tollbooth/v6" 9 | "github.com/realclientip/realclientip-go" 10 | ) 11 | 12 | func main() { 13 | // Choose the right strategy for our network configuration 14 | strat, err := realclientip.NewRightmostNonPrivateStrategy("X-Forwarded-For") 15 | if err != nil { 16 | log.Fatal("realclientip.NewRightmostNonPrivateStrategy returned error (bad input)") 17 | } 18 | 19 | lmt := tollbooth.NewLimiter(1, nil) 20 | 21 | // We'll make a fake request 22 | req, _ := http.NewRequest("GET", "https://example.com", nil) 23 | req.Header.Add("X-Forwarded-For", "1.1.1.1, 2.2.2.2, 3.3.3.3, 192.168.1.1") 24 | req.RemoteAddr = "192.168.1.2:8888" 25 | 26 | clientIP := strat.ClientIP(req.Header, req.RemoteAddr) 27 | if clientIP == "" { 28 | // This should probably result in the request being denied 29 | log.Fatal("strat.ClientIP found no IP") 30 | } 31 | 32 | // We don't want to include the zone in our limiter key 33 | clientIP, _ = realclientip.SplitHostZone(clientIP) 34 | 35 | if httpErr := tollbooth.LimitByKeys(lmt, []string{clientIP}); httpErr != nil { 36 | fmt.Println("We got limited!?!", httpErr) 37 | } else { 38 | fmt.Println("Request allowed") 39 | } 40 | 41 | // Output: Request allowed 42 | } 43 | -------------------------------------------------------------------------------- /_examples/tollbooth/go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/didip/tollbooth/v6 v6.1.2 h1:Kdqxmqw9YTv0uKajBUiWQg+GURL/k4vy9gmLCL01PjQ= 4 | github.com/didip/tollbooth/v6 v6.1.2/go.mod h1:xjcse6CTHCLuOkzsWrEgdy9WPJFv+p/x6v+MyfP+O9s= 5 | github.com/go-pkgz/expirable-cache v0.0.3 h1:rTh6qNPp78z0bQE6HDhXBHUwqnV9i09Vm6dksJLXQDc= 6 | github.com/go-pkgz/expirable-cache v0.0.3/go.mod h1:+IauqN00R2FqNRLCLA+X5YljQJrwB179PfiAoMPlTlQ= 7 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 8 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 9 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 10 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 11 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 12 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 13 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 14 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 15 | github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= 16 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 17 | golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1 h1:NusfzzA6yGQ+ua51ck7E3omNUX/JuqbFSaRGqU8CcLI= 18 | golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 19 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 20 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 21 | gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= 22 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 23 | -------------------------------------------------------------------------------- /example_middleware_test.go: -------------------------------------------------------------------------------- 1 | package realclientip_test 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io/ioutil" 7 | "log" 8 | "net/http" 9 | "net/http/httptest" 10 | 11 | "github.com/realclientip/realclientip-go" 12 | ) 13 | 14 | func Example_middleware() { 15 | // Choose the right strategy for our network configuration 16 | strat, err := realclientip.NewRightmostNonPrivateStrategy("X-Forwarded-For") 17 | if err != nil { 18 | log.Fatal("realclientip.NewRightmostNonPrivateStrategy returned error (bad input)") 19 | } 20 | 21 | // Place our middleware before the handler 22 | handlerWithMiddleware := clientIPMiddleware(strat, http.HandlerFunc(handler)) 23 | httpServer := httptest.NewServer(handlerWithMiddleware) 24 | defer httpServer.Close() 25 | 26 | req, _ := http.NewRequest("GET", httpServer.URL, nil) 27 | req.Header.Add("X-Forwarded-For", "1.1.1.1, 2.2.2.2, 3.3.3.3, 192.168.1.1") 28 | 29 | client := &http.Client{} 30 | resp, err := client.Do(req) 31 | if err != nil { 32 | log.Fatal(err) 33 | } 34 | 35 | b, err := ioutil.ReadAll(resp.Body) 36 | if err != nil { 37 | log.Fatal(err) 38 | } 39 | 40 | fmt.Printf("%s", b) 41 | // Output: 42 | // your IP: 3.3.3.3 43 | } 44 | 45 | type clientIPCtxKey struct{} 46 | 47 | // Adds the "real" client IP to the request context under the clientIPCtxKey{} key. 48 | // If the client IP couldn't be obtained, the value will be an empty string. 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 { 52 | fn := func(w http.ResponseWriter, r *http.Request) { 53 | clientIP := strat.ClientIP(r.Header, r.RemoteAddr) 54 | if clientIP == "" { 55 | // Write error log. Consider aborting the request depending on use. 56 | log.Fatal("Failed to find client IP") 57 | } 58 | 59 | r = r.WithContext(context.WithValue(r.Context(), clientIPCtxKey{}, clientIP)) 60 | next.ServeHTTP(w, r) 61 | } 62 | 63 | return http.HandlerFunc(fn) 64 | } 65 | 66 | func handler(w http.ResponseWriter, r *http.Request) { 67 | clientIP := r.Context().Value(clientIPCtxKey{}) 68 | fmt.Fprintln(w, "your IP:", clientIP) 69 | } 70 | -------------------------------------------------------------------------------- /example_playground_test.go: -------------------------------------------------------------------------------- 1 | package realclientip_test 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/realclientip/realclientip-go" 8 | ) 9 | 10 | func Example_playground() { 11 | // We'll make a fake request 12 | req, _ := http.NewRequest("GET", "https://example.com", nil) 13 | req.Header.Add("X-Forwarded-For", "1.1.1.1, 2001:db8:cafe::99%eth0, 3.3.3.3, 192.168.1.1") 14 | req.Header.Add("Forwarded", `For=fe80::abcd;By=fe80::1234, Proto=https;For=::ffff:188.0.2.128, For="[2001:db8:cafe::17]:4848", For=fc00::1`) 15 | req.Header.Add("X-Real-IP", "4.4.4.4") 16 | req.RemoteAddr = "192.168.1.2:8888" 17 | 18 | var strat realclientip.Strategy 19 | 20 | strat = realclientip.RemoteAddrStrategy{} 21 | fmt.Printf("\n%T: %+v\n", strat, strat) 22 | fmt.Println(strat.ClientIP(req.Header, req.RemoteAddr)) // 192.168.1.2 23 | 24 | strat, _ = realclientip.NewSingleIPHeaderStrategy("X-Real-IP") 25 | fmt.Printf("\n%T: %+v\n", strat, strat) 26 | fmt.Println(strat.ClientIP(req.Header, req.RemoteAddr)) // 4.4.4.4 27 | 28 | strat, _ = realclientip.NewLeftmostNonPrivateStrategy("Forwarded") 29 | fmt.Printf("\n%T: %+v\n", strat, strat) 30 | fmt.Println(strat.ClientIP(req.Header, req.RemoteAddr)) // 188.0.2.128 31 | 32 | strat, _ = realclientip.NewRightmostNonPrivateStrategy("X-Forwarded-For") 33 | fmt.Printf("\n%T: %+v\n", strat, strat) 34 | fmt.Println(strat.ClientIP(req.Header, req.RemoteAddr)) // 3.3.3.3 35 | 36 | strat, _ = realclientip.NewRightmostTrustedCountStrategy("Forwarded", 2) 37 | fmt.Printf("\n%T: %+v\n", strat, strat) 38 | fmt.Println(strat.ClientIP(req.Header, req.RemoteAddr)) // 2001:db8:cafe::17 39 | 40 | trustedRanges, _ := realclientip.AddressesAndRangesToIPNets([]string{"192.168.0.0/16", "3.3.3.3"}...) 41 | strat, _ = realclientip.NewRightmostTrustedRangeStrategy("X-Forwarded-For", trustedRanges) 42 | fmt.Printf("\n%T: %+v\n", strat, strat) 43 | fmt.Println(strat.ClientIP(req.Header, req.RemoteAddr)) // 2001:db8:cafe::99%eth0 44 | ipAddr, _ := realclientip.ParseIPAddr(strat.ClientIP(req.Header, req.RemoteAddr)) 45 | fmt.Println(ipAddr.IP) // 2001:db8:cafe::99 46 | 47 | strat = realclientip.NewChainStrategy( 48 | realclientip.Must(realclientip.NewSingleIPHeaderStrategy("Cf-Connecting-IP")), 49 | realclientip.RemoteAddrStrategy{}, 50 | ) 51 | fmt.Printf("\n%T: %+v\n", strat, strat) 52 | fmt.Println(strat.ClientIP(req.Header, req.RemoteAddr)) // 192.168.1.2 53 | 54 | // Output: 55 | // realclientip.RemoteAddrStrategy: {} 56 | // 192.168.1.2 57 | // 58 | // realclientip.SingleIPHeaderStrategy: {headerName:X-Real-Ip} 59 | // 4.4.4.4 60 | // 61 | // realclientip.LeftmostNonPrivateStrategy: {headerName:Forwarded} 62 | // 188.0.2.128 63 | // 64 | // realclientip.RightmostNonPrivateStrategy: {headerName:X-Forwarded-For} 65 | // 3.3.3.3 66 | // 67 | // realclientip.RightmostTrustedCountStrategy: {headerName:Forwarded trustedCount:2} 68 | // 2001:db8:cafe::17 69 | // 70 | // realclientip.RightmostTrustedRangeStrategy: {headerName:X-Forwarded-For trustedRanges:[192.168.0.0/16 3.3.3.3/32] 71 | // 2001:db8:cafe::99%eth0 72 | // 2001:db8:cafe::99 73 | // 74 | // realclientip.ChainStrategy: {strategies:[realclientip.SingleIPHeaderStrategy{headerName:Cf-Connecting-Ip} realclientip.RemoteAddrStrategy{}]} 75 | // 192.168.1.2 76 | } 77 | -------------------------------------------------------------------------------- /ranges/cloudfront.go: -------------------------------------------------------------------------------- 1 | package ranges 2 | 3 | // AWS CloudFront's internet IP ranges. 4 | // Taken from https://d7uri8nf7uskq.cloudfront.net/tools/list-cloudfront-ips 5 | // For more information, see: https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/LocationsOfEdgeServers.html 6 | // For a guaranteed up-to-date list, consider using the "managed prefix list". 7 | var CloudFront = []string{ 8 | // CLOUDFRONT_GLOBAL_IP_LIST 9 | "120.52.22.96/27", 10 | "205.251.249.0/24", 11 | "180.163.57.128/26", 12 | "204.246.168.0/22", 13 | "18.160.0.0/15", 14 | "205.251.252.0/23", 15 | "54.192.0.0/16", 16 | "204.246.173.0/24", 17 | "54.230.200.0/21", 18 | "120.253.240.192/26", 19 | "116.129.226.128/26", 20 | "130.176.0.0/17", 21 | "108.156.0.0/14", 22 | "99.86.0.0/16", 23 | "205.251.200.0/21", 24 | "223.71.71.128/25", 25 | "13.32.0.0/15", 26 | "120.253.245.128/26", 27 | "13.224.0.0/14", 28 | "70.132.0.0/18", 29 | "15.158.0.0/16", 30 | "13.249.0.0/16", 31 | "18.238.0.0/15", 32 | "18.244.0.0/15", 33 | "205.251.208.0/20", 34 | "65.9.128.0/18", 35 | "130.176.128.0/18", 36 | "58.254.138.0/25", 37 | "54.230.208.0/20", 38 | "116.129.226.0/25", 39 | "52.222.128.0/17", 40 | "18.164.0.0/15", 41 | "64.252.128.0/18", 42 | "205.251.254.0/24", 43 | "54.230.224.0/19", 44 | "71.152.0.0/17", 45 | "216.137.32.0/19", 46 | "204.246.172.0/24", 47 | "18.172.0.0/15", 48 | "120.52.39.128/27", 49 | "118.193.97.64/26", 50 | "223.71.71.96/27", 51 | "18.154.0.0/15", 52 | "54.240.128.0/18", 53 | "205.251.250.0/23", 54 | "180.163.57.0/25", 55 | "52.46.0.0/18", 56 | "223.71.11.0/27", 57 | "52.82.128.0/19", 58 | "54.230.0.0/17", 59 | "54.230.128.0/18", 60 | "54.239.128.0/18", 61 | "130.176.224.0/20", 62 | "36.103.232.128/26", 63 | "52.84.0.0/15", 64 | "143.204.0.0/16", 65 | "144.220.0.0/16", 66 | "120.52.153.192/26", 67 | "119.147.182.0/25", 68 | "120.232.236.0/25", 69 | "54.182.0.0/16", 70 | "58.254.138.128/26", 71 | "120.253.245.192/27", 72 | "54.239.192.0/19", 73 | "18.64.0.0/14", 74 | "120.52.12.64/26", 75 | "99.84.0.0/16", 76 | "130.176.192.0/19", 77 | "52.124.128.0/17", 78 | "204.246.164.0/22", 79 | "13.35.0.0/16", 80 | "204.246.174.0/23", 81 | "36.103.232.0/25", 82 | "119.147.182.128/26", 83 | "118.193.97.128/25", 84 | "120.232.236.128/26", 85 | "204.246.176.0/20", 86 | "65.8.0.0/16", 87 | "65.9.0.0/17", 88 | "108.138.0.0/15", 89 | "120.253.241.160/27", 90 | "64.252.64.0/18", 91 | 92 | // CLOUDFRONT_REGIONAL_EDGE_IP_LIST 93 | "13.113.196.64/26", 94 | "13.113.203.0/24", 95 | "52.199.127.192/26", 96 | "13.124.199.0/24", 97 | "3.35.130.128/25", 98 | "52.78.247.128/26", 99 | "13.233.177.192/26", 100 | "15.207.13.128/25", 101 | "15.207.213.128/25", 102 | "52.66.194.128/26", 103 | "13.228.69.0/24", 104 | "52.220.191.0/26", 105 | "13.210.67.128/26", 106 | "13.54.63.128/26", 107 | "99.79.169.0/24", 108 | "18.192.142.0/23", 109 | "35.158.136.0/24", 110 | "52.57.254.0/24", 111 | "13.48.32.0/24", 112 | "18.200.212.0/23", 113 | "52.212.248.0/26", 114 | "3.10.17.128/25", 115 | "3.11.53.0/24", 116 | "52.56.127.0/25", 117 | "15.188.184.0/24", 118 | "52.47.139.0/24", 119 | "18.229.220.192/26", 120 | "54.233.255.128/26", 121 | "3.231.2.0/25", 122 | "3.234.232.224/27", 123 | "3.236.169.192/26", 124 | "3.236.48.0/23", 125 | "34.195.252.0/24", 126 | "34.226.14.0/24", 127 | "13.59.250.0/26", 128 | "18.216.170.128/25", 129 | "3.128.93.0/24", 130 | "3.134.215.0/24", 131 | "52.15.127.128/26", 132 | "3.101.158.0/23", 133 | "52.52.191.128/26", 134 | "34.216.51.0/25", 135 | "34.223.12.224/27", 136 | "34.223.80.192/26", 137 | "35.162.63.192/26", 138 | "35.167.191.128/26", 139 | "44.227.178.0/24", 140 | "44.234.108.128/25", 141 | "44.234.90.252/30", 142 | } 143 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![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)][playground] 3 | [![Test](https://github.com/realclientip/realclientip-go/actions/workflows/test.yml/badge.svg)](https://github.com/realclientip/realclientip-go/actions/workflows/test.yml) 4 | ![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)](https://choosealicense.com/licenses/0bsd/) 6 | 7 | # realclientip-go 8 | 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. 10 | 11 | [xff-post]: https://adam-p.ca/blog/2022/03/x-forwarded-for/ 12 | 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. 14 | 15 | 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. 16 | 17 | ## Usage 18 | 19 | This library provides strategies for extracting the desired "real" client IP from various headers or from `http.Request.RemoteAddr` (the client socket IP). 20 | 21 | ```golang 22 | strategy, err := realclientip.NewRightmostTrustedCountStrategy("X-Forwarded-For", 2) 23 | ... 24 | clientIP := strategy.ClientIP(req.Header, req.RemoteAddr) 25 | ``` 26 | 27 | Try it out [in the playground][playground]. 28 | 29 | [playground]: https://go.dev/play/p/egZnBWJfedk 30 | 31 | 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. 32 | 33 | `ClientIP` is threadsafe for all strategies. The same strategy instance can be used for handling all HTTP requests, for example. 34 | 35 | [documentation]: (https://pkg.go.dev/github.com/realclientip/realclientip-go) 36 | 37 | There are examples of use in the [documentation] and [`_examples` directory](/_examples/). 38 | 39 | ### Strategy failures 40 | 41 | 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. 42 | 43 | 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. 44 | 45 | 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. 46 | 47 | ### Headers 48 | 49 | Leftmost-ish and rightmost-ish strategies support the `X-Forwarded-For` and `Forwarded` headers. 50 | 51 | `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]. 52 | 53 | 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. 54 | 55 | 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. 56 | 57 | [single-ip-wiki]: https://github.com/realclientip/realclientip-go/wiki/Single-IP-Headers 58 | 59 | #### `Forwarded` header support 60 | 61 | 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. 62 | 63 | [`Forwarded` header]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Forwarded 64 | [RFC 7239]: https://datatracker.ietf.org/doc/html/rfc7239 65 | [`Test_forwardedHeaderRFCDeviations`]: https://github.com/realclientip/realclientip-go/blob/65719ac74acb471001b3049b4270a3cc38920a30/realclientip_test.go#L1895 66 | 67 | ### IPv6 zones 68 | 69 | 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). 70 | 71 | To split the zone off and discard it, you may use `realclientip.SplitHostZone`. 72 | 73 | [strip-zone-post]: https://adam-p.ca/blog/2022/03/strip-ipv6-zone/ 74 | 75 | ### Known IP ranges 76 | 77 | 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. 78 | 79 | (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.) 80 | 81 | ## Implementation decisions and notes 82 | 83 | ### `net` vs `netip` 84 | 85 | At the time of writing this library, Go 1.18 was only just released. It made sense to use the older `net` package rather than the newer `netip`, so that the required Go version wouldn't be so high as to exclude some users of the library. 86 | 87 | In the future we may wish to switch to using `netip`, but it will require API changes to `AddressesAndRangesToIPNets`, `RightmostTrustedRangeStrategy`, and `ParseIPAddr`. 88 | 89 | ### Disallowed valid IPs 90 | 91 | The values `0.0.0.0` (zero) and `::` (unspecified) are valid IPs, strictly speaking. However, this library treats them as invalid as they don't make sense to its intended uses. If you have a valid use case for them, please open an issue. 92 | 93 | ### Normalizing IPs 94 | 95 | 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.). 96 | 97 | ### Input format strictness 98 | 99 | Some input is allowed that isn't strictly correct. Some examples: 100 | 101 | * IPv4 with brackets: `[2.2.2.2]:1234` 102 | * IPv4 with zone: `2.2.2.2%eth0` 103 | * Non-numeric port values: `2.2.2.2:nope` 104 | * Other `Forwarded` header [deviations][`Test_forwardedHeaderRFCDeviations`] 105 | 106 | It could be argued that it would be better to be absolutely strict in what is accepted. 107 | 108 | ### Code comments 109 | 110 | As this library aspires to be a "reference implementation", the code is heavily commented. Perhaps more than is strictly necessary. 111 | 112 | ### Pre-creating Strategies 113 | 114 | 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: 115 | 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. 116 | 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.) 117 | 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. 118 | 119 | An alternative approach could be using functions like: 120 | 121 | ``` 122 | func RightmostTrustedCountStrategy(headerName string, trustedRanges []*net.IPNet, headers http.Header, remoteAddr string) (Strategy, ip, error) { 123 | ... 124 | strat, _, err := RightmostTrustedRangeStrategy("Forward", 2, "", "") // pre-create 125 | _, ip, err := RightmostTrustedRangeStrategy("Forward", 2, r.Header, r.RemoteAddr) // use direct 126 | ``` 127 | 128 | But perhaps that's no less awkward. 129 | 130 | ### Interfaces vs Functions 131 | 132 | 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: 133 | * It seems slightly more Go-idiomatic. 134 | * It allows for adding new methods in the future without breaking the API. (Such as `String()`.) 135 | * It allows for configuration information to appear in a printf of a strategy struct. This can be useful for logging. 136 | * The function approach is still easy to use, with the bound `ClientIP` method: 137 | ```golang 138 | getClientIP := NewRightmostTrustedCountStrategy("Forwarded", 2).ClientIP 139 | ``` 140 | 141 | [constructed functions]: https://github.com/realclientip/realclientip-go/commit/3254ce300803eff09a0a82d0e5557e77b98f1ef6#diff-c16a5957939fea196ee5371da58bfec10c4a10a7c360b575566759e5101a293bR19-L22 142 | 143 | ## Other language implementations 144 | 145 | 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. 146 | -------------------------------------------------------------------------------- /realclientip.go: -------------------------------------------------------------------------------- 1 | // SPDX: 0BSD 2 | 3 | // Package realclientip provides strategies for obtaining the "real" client IP from HTTP requests. 4 | package realclientip 5 | 6 | import ( 7 | "fmt" 8 | "net" 9 | "net/http" 10 | "strings" 11 | ) 12 | 13 | // Strategy is satisfied by all of the specific strategies in this package. It can be used 14 | // instead of the concrete types if the strategy is to be determined at runtime, 15 | // depending on configuration, for example. 16 | type Strategy interface { 17 | // ClientIP returns empty string if there is no derivable IP. In many cases this 18 | // should be treated as a misconfiguration error, unless the strategy is attempting to 19 | // get an untrustworthy or optional value. 20 | // All implementations of this method must be threadsafe. 21 | ClientIP(headers http.Header, remoteAddr string) string 22 | } 23 | 24 | const ( 25 | // Pre-canonicalized constants to avoid typos later on 26 | xForwardedForHdr = "X-Forwarded-For" 27 | forwardedHdr = "Forwarded" 28 | ) 29 | 30 | // Must panics if err is not nil. This can be used to make sure the strategy-making 31 | // functions do not return an error. It can also facilitate calling NewChainStrategy(). 32 | // It can be called like Must(NewSingleIPHeaderStrategy("X-Real-IP")). 33 | func Must(strat Strategy, err error) Strategy { 34 | if err != nil { 35 | panic(fmt.Sprintf("err is not nil: %v", err)) 36 | } 37 | return strat 38 | } 39 | 40 | // ChainStrategy attempts to use the given strategies in order. If the first one returns 41 | // an empty string, the second one is tried, and so on, until a good IP is found or the 42 | // strategies are exhausted. 43 | // A common use for this is if a server is both directly connected to the internet and 44 | // expecting a header to check. It might be called like: 45 | // NewChainStrategy(Must(LeftmostNonPrivateStrategy("X-Forwarded-For")), RemoteAddrStrategy) 46 | type ChainStrategy struct { 47 | strategies []Strategy 48 | } 49 | 50 | // NewChainStrategy creates a ChainStrategy that attempts to use the given strategies to 51 | // derive the client IP, stopping when the first one succeeds. 52 | func NewChainStrategy(strategies ...Strategy) ChainStrategy { 53 | return ChainStrategy{strategies: strategies} 54 | } 55 | 56 | // ClientIP derives the client IP using this strategy. 57 | // headers is expected to be like http.Request.Header. 58 | // remoteAddr is expected to be like http.Request.RemoteAddr. 59 | // The returned IP may contain a zone identifier. 60 | // If all chained strategies fail to derive a valid IP, an empty string is returned. 61 | func (strat ChainStrategy) ClientIP(headers http.Header, remoteAddr string) string { 62 | for _, subStrat := range strat.strategies { 63 | result := subStrat.ClientIP(headers, remoteAddr) 64 | if result != "" { 65 | return result 66 | } 67 | } 68 | return "" 69 | } 70 | 71 | func (strat ChainStrategy) String() string { 72 | var b strings.Builder 73 | b.WriteString("{strategies:[") 74 | for i, s := range strat.strategies { 75 | if i > 0 { 76 | b.WriteString(" ") 77 | } 78 | b.WriteString(fmt.Sprintf("%T%+v", s, s)) 79 | } 80 | b.WriteString("]}") 81 | return b.String() 82 | } 83 | 84 | // RemoteAddrStrategy returns the client socket IP, stripped of port. 85 | // This strategy should be used if the server accept direct connections, rather than 86 | // through a reverse proxy. 87 | type RemoteAddrStrategy struct{} 88 | 89 | // ClientIP derives the client IP using this strategy. 90 | // remoteAddr is expected to be like http.Request.RemoteAddr. 91 | // The returned IP may contain a zone identifier. 92 | // If no valid IP can be derived, empty string will be returned. This should only happen 93 | // if remoteAddr has been modified to something illegal, or if the server is accepting 94 | // connections on a Unix domain socket (in which case RemoteAddr is "@"). 95 | func (strat RemoteAddrStrategy) ClientIP(_ http.Header, remoteAddr string) string { 96 | ipAddr := goodIPAddr(remoteAddr) 97 | if ipAddr == nil { 98 | return "" 99 | } 100 | 101 | return ipAddr.String() 102 | } 103 | 104 | // SingleIPHeaderStrategy derives an IP address from a single-IP header. 105 | // A non-exhaustive list of such single-IP headers is: 106 | // X-Real-IP, CF-Connecting-IP, True-Client-IP, Fastly-Client-IP, X-Azure-ClientIP, X-Azure-SocketIP. 107 | // This strategy should be used when the given header is added by a trusted reverse proxy. 108 | // You must ensure that this header is not spoofable (as is possible with Akamai's use of 109 | // True-Client-IP, Fastly's default use of Fastly-Client-IP, and Azure's X-Azure-ClientIP). 110 | // See the single-IP wiki page for more info: https://github.com/realclientip/realclientip-go/wiki/Single-IP-Headers 111 | type SingleIPHeaderStrategy struct { 112 | headerName string 113 | } 114 | 115 | // NewSingleIPHeaderStrategy creates a SingleIPHeaderStrategy that uses the headerName 116 | // request header to get the client IP. 117 | func NewSingleIPHeaderStrategy(headerName string) (SingleIPHeaderStrategy, error) { 118 | if headerName == "" { 119 | return SingleIPHeaderStrategy{}, fmt.Errorf("SingleIPHeaderStrategy header must not be empty") 120 | } 121 | 122 | // We will be using the headerName for lookups in the http.Header map, which is keyed 123 | // by canonicalized header name. We'll canonicalize here so we only have to do it once. 124 | headerName = http.CanonicalHeaderKey(headerName) 125 | 126 | if headerName == xForwardedForHdr || headerName == forwardedHdr { 127 | return SingleIPHeaderStrategy{}, fmt.Errorf("SingleIPHeaderStrategy header must not be %s or %s", xForwardedForHdr, forwardedHdr) 128 | } 129 | 130 | return SingleIPHeaderStrategy{headerName: headerName}, nil 131 | } 132 | 133 | // ClientIP derives the client IP using this strategy. 134 | // headers is expected to be like http.Request.Header. 135 | // The returned IP may contain a zone identifier. 136 | // If no valid IP can be derived, empty string will be returned. 137 | func (strat SingleIPHeaderStrategy) ClientIP(headers http.Header, _ string) string { 138 | // RFC 2616 does not allow multiple instances of single-IP headers (or any non-list header). 139 | // It is debatable whether it is better to treat multiple such headers as an error 140 | // (more correct) or simply pick one of them (more flexible). As we've already 141 | // told the user tom make sure the header is not spoofable, we're going to use the 142 | // last header instance if there are multiple. (Using the last is arbitrary, but 143 | // in theory it should be the newest value.) 144 | ipStr := lastHeader(headers, strat.headerName) 145 | if ipStr == "" { 146 | // There is no header 147 | return "" 148 | } 149 | 150 | ipAddr := goodIPAddr(ipStr) 151 | if ipAddr == nil { 152 | // The header value is invalid 153 | return "" 154 | } 155 | 156 | return ipAddr.String() 157 | } 158 | 159 | // LeftmostNonPrivateStrategy derives the client IP from the leftmost valid and 160 | // non-private IP address in the X-Fowarded-For for Forwarded header. This 161 | // strategy should be used when a valid, non-private IP closest to the client is desired. 162 | // Note that this MUST NOT BE USED FOR SECURITY PURPOSES. This IP can be TRIVIALLY 163 | // SPOOFED. 164 | type LeftmostNonPrivateStrategy struct { 165 | headerName string 166 | } 167 | 168 | // NewLeftmostNonPrivateStrategy creates a LeftmostNonPrivateStrategy. headerName must be 169 | // "X-Forwarded-For" or "Forwarded". 170 | func NewLeftmostNonPrivateStrategy(headerName string) (LeftmostNonPrivateStrategy, error) { 171 | if headerName == "" { 172 | return LeftmostNonPrivateStrategy{}, fmt.Errorf("LeftmostNonPrivateStrategy header must not be empty") 173 | } 174 | 175 | // We will be using the headerName for lookups in the http.Header map, which is keyed 176 | // by canonicalized header name. We'll do that here so we only have to do it once. 177 | headerName = http.CanonicalHeaderKey(headerName) 178 | 179 | if headerName != xForwardedForHdr && headerName != forwardedHdr { 180 | return LeftmostNonPrivateStrategy{}, fmt.Errorf("LeftmostNonPrivateStrategy header must be %s or %s", xForwardedForHdr, forwardedHdr) 181 | } 182 | 183 | return LeftmostNonPrivateStrategy{headerName: headerName}, nil 184 | } 185 | 186 | // ClientIP derives the client IP using this strategy. 187 | // headers is expected to be like http.Request.Header. 188 | // The returned IP may contain a zone identifier. 189 | // If no valid IP can be derived, empty string will be returned. 190 | func (strat LeftmostNonPrivateStrategy) ClientIP(headers http.Header, _ string) string { 191 | ipAddrs := getIPAddrList(headers, strat.headerName) 192 | for _, ip := range ipAddrs { 193 | if ip != nil && !isPrivateOrLocal(ip.IP) { 194 | // This is the leftmost valid, non-private IP 195 | return ip.String() 196 | } 197 | } 198 | 199 | // We failed to find any valid, non-private IP 200 | return "" 201 | } 202 | 203 | // RightmostNonPrivateStrategy derives the client IP from the rightmost valid, 204 | // non-private/non-internal IP address in the X-Fowarded-For for Forwarded header. This 205 | // strategy should be used when all reverse proxies between the internet and the 206 | // server have private-space IP addresses. 207 | type RightmostNonPrivateStrategy struct { 208 | headerName string 209 | } 210 | 211 | // NewRightmostNonPrivateStrategy creates a RightmostNonPrivateStrategy. headerName must 212 | // be "X-Forwarded-For" or "Forwarded". 213 | func NewRightmostNonPrivateStrategy(headerName string) (RightmostNonPrivateStrategy, error) { 214 | if headerName == "" { 215 | return RightmostNonPrivateStrategy{}, fmt.Errorf("RightmostNonPrivateStrategy header must not be empty") 216 | } 217 | 218 | // We will be using the headerName for lookups in the http.Header map, which is keyed 219 | // by canonicalized header name. We'll do that here so we only have to do it once. 220 | headerName = http.CanonicalHeaderKey(headerName) 221 | 222 | if headerName != xForwardedForHdr && headerName != forwardedHdr { 223 | return RightmostNonPrivateStrategy{}, fmt.Errorf("RightmostNonPrivateStrategy header must be %s or %s", xForwardedForHdr, forwardedHdr) 224 | } 225 | 226 | return RightmostNonPrivateStrategy{headerName: headerName}, nil 227 | } 228 | 229 | // ClientIP derives the client IP using this strategy. 230 | // headers is expected to be like http.Request.Header. 231 | // The returned IP may contain a zone identifier. 232 | // If no valid IP can be derived, empty string will be returned. 233 | func (strat RightmostNonPrivateStrategy) ClientIP(headers http.Header, _ string) string { 234 | ipAddrs := getIPAddrList(headers, strat.headerName) 235 | // Look backwards through the list of IP addresses 236 | for i := len(ipAddrs) - 1; i >= 0; i-- { 237 | if ipAddrs[i] != nil && !isPrivateOrLocal(ipAddrs[i].IP) { 238 | // This is the rightmost non-private IP 239 | return ipAddrs[i].String() 240 | } 241 | } 242 | 243 | // We failed to find any valid, non-private IP 244 | return "" 245 | } 246 | 247 | // RightmostTrustedCountStrategy derives the client IP from the valid IP address added by 248 | // the first trusted reverse proxy to the X-Forwarded-For or Forwarded header. This 249 | // Strategy should be used when there is a fixed number of trusted reverse proxies that 250 | // are appending IP addresses to the header. 251 | type RightmostTrustedCountStrategy struct { 252 | headerName string 253 | trustedCount int 254 | } 255 | 256 | // NewRightmostTrustedCountStrategy creates a RightmostTrustedCountStrategy. headerName 257 | // must be "X-Forwarded-For" or "Forwarded". trustedCount is the number of trusted 258 | // reverse proxies. The IP returned will be the (trustedCount-1)th from the right. For 259 | // example, if there's only one trusted proxy, this strategy will return the last 260 | // (rightmost) IP address. 261 | func NewRightmostTrustedCountStrategy(headerName string, trustedCount int) (RightmostTrustedCountStrategy, error) { 262 | if headerName == "" { 263 | return RightmostTrustedCountStrategy{}, fmt.Errorf("RightmostTrustedCountStrategy header must not be empty") 264 | } 265 | 266 | if trustedCount <= 0 { 267 | return RightmostTrustedCountStrategy{}, fmt.Errorf("RightmostTrustedCountStrategy count must be greater than zero") 268 | } 269 | 270 | // We will be using the headerName for lookups in the http.Header map, which is keyed 271 | // by canonicalized header name. We'll do that here so we only have to do it once. 272 | headerName = http.CanonicalHeaderKey(headerName) 273 | 274 | if headerName != xForwardedForHdr && headerName != forwardedHdr { 275 | return RightmostTrustedCountStrategy{}, fmt.Errorf("RightmostNonPrivateStrategy header must be %s or %s", xForwardedForHdr, forwardedHdr) 276 | } 277 | 278 | return RightmostTrustedCountStrategy{headerName: headerName, trustedCount: trustedCount}, nil 279 | } 280 | 281 | // ClientIP derives the client IP using this strategy. 282 | // headers is expected to be like http.Request.Header. 283 | // The returned IP may contain a zone identifier. 284 | // If no valid IP can be derived, empty string will be returned. 285 | func (strat RightmostTrustedCountStrategy) ClientIP(headers http.Header, _ string) string { 286 | ipAddrs := getIPAddrList(headers, strat.headerName) 287 | 288 | // We want the (N-1)th from the rightmost. For example, if there's only one 289 | // trusted proxy, we want the last. 290 | rightmostIndex := len(ipAddrs) - 1 291 | targetIndex := rightmostIndex - (strat.trustedCount - 1) 292 | 293 | if targetIndex < 0 { 294 | // This is a misconfiguration error. There were fewer IPs than we expected. 295 | return "" 296 | } 297 | 298 | resultIP := ipAddrs[targetIndex] 299 | 300 | if resultIP == nil { 301 | // This is a misconfiguration error. Our first trusted proxy didn't add a 302 | // valid IP address to the header. 303 | return "" 304 | } 305 | 306 | return resultIP.String() 307 | } 308 | 309 | // AddressesAndRangesToIPNets converts a slice of strings with IPv4 and IPv6 addresses and 310 | // CIDR ranges (prefixes) to net.IPNet instances. 311 | // If net.ParseCIDR or net.ParseIP fail, an error will be returned. 312 | // Zones in addresses or ranges are not allowed and will result in an error. This is because: 313 | // a) net.ParseCIDR will fail to parse a range with a zone, and 314 | // b) netip.ParsePrefix will succeed but silently throw away the zone; then 315 | // netip.Prefix.Contains will return false for any IP with a zone, causing confusion and bugs. 316 | func AddressesAndRangesToIPNets(ranges ...string) ([]net.IPNet, error) { 317 | var result []net.IPNet 318 | for _, r := range ranges { 319 | if strings.Contains(r, "%") { 320 | return nil, fmt.Errorf("zones are not allowed: %q", r) 321 | } 322 | 323 | if strings.Contains(r, "/") { 324 | // This is a CIDR/prefix 325 | _, ipNet, err := net.ParseCIDR(r) 326 | if err != nil { 327 | return nil, fmt.Errorf("net.ParseCIDR failed for %q: %w", r, err) 328 | } 329 | result = append(result, *ipNet) 330 | } else { 331 | // This is a single IP; convert it to a range including only itself 332 | ip := net.ParseIP(r) 333 | if ip == nil { 334 | return nil, fmt.Errorf("net.ParseIP failed for %q", r) 335 | } 336 | 337 | // To use the right size IP and mask, we need to know if the address is IPv4 or v6. 338 | // Attempt to convert it to IPv4 to find out. 339 | if ipv4 := ip.To4(); ipv4 != nil { 340 | ip = ipv4 341 | } 342 | 343 | // Mask all the bits 344 | mask := len(ip) * 8 345 | result = append(result, net.IPNet{ 346 | IP: ip, 347 | Mask: net.CIDRMask(mask, mask), 348 | }) 349 | } 350 | } 351 | 352 | return result, nil 353 | } 354 | 355 | // RightmostTrustedRangeStrategy derives the client IP from the rightmost valid IP address 356 | // in the X-Forwarded-For or Forwarded header which is not in a set of trusted IP ranges. 357 | // This strategy should be used when the IP ranges of the reverse proxies between the 358 | // internet and the server are known. 359 | // If a third-party WAF, CDN, etc., is used, you SHOULD use a method of verifying its 360 | // access to your origin that is stronger than checking its IP address (e.g., using 361 | // authenticated pulls). Failure to do so can result in scenarios like: 362 | // You use AWS CloudFront in front of a server you host elsewhere. An attacker creates a 363 | // CF distribution that points at your origin server. The attacker uses Lambda@Edge to 364 | // spoof the Host and X-Forwarded-For headers. Now your "trusted" reverse proxy is no 365 | // longer trustworthy. 366 | type RightmostTrustedRangeStrategy struct { 367 | headerName string 368 | trustedRanges []net.IPNet 369 | } 370 | 371 | // NewRightmostTrustedRangeStrategy creates a RightmostTrustedRangeStrategy. headerName 372 | // must be "X-Forwarded-For" or "Forwarded". trustedRanges must contain all trusted 373 | // reverse proxies on the path to this server. trustedRanges can be private/internal or 374 | // external (for example, if a third-party reverse proxy is used). 375 | func NewRightmostTrustedRangeStrategy(headerName string, trustedRanges []net.IPNet) (RightmostTrustedRangeStrategy, error) { 376 | if headerName == "" { 377 | return RightmostTrustedRangeStrategy{}, fmt.Errorf("RightmostTrustedRangeStrategy header must not be empty") 378 | } 379 | 380 | // We will be using the headerName for lookups in the http.Header map, which is keyed 381 | // by canonicalized header name. We'll do that here so we only have to do it once. 382 | headerName = http.CanonicalHeaderKey(headerName) 383 | 384 | if headerName != xForwardedForHdr && headerName != forwardedHdr { 385 | return RightmostTrustedRangeStrategy{}, fmt.Errorf("RightmostTrustedRangeStrategy header must be %s or %s", xForwardedForHdr, forwardedHdr) 386 | } 387 | 388 | return RightmostTrustedRangeStrategy{headerName: headerName, trustedRanges: trustedRanges}, nil 389 | } 390 | 391 | // ClientIP derives the client IP using this strategy. 392 | // headers is expected to be like http.Request.Header. 393 | // The returned IP may contain a zone identifier. 394 | // If no valid IP can be derived, empty string will be returned. 395 | func (strat RightmostTrustedRangeStrategy) ClientIP(headers http.Header, _ string) string { 396 | ipAddrs := getIPAddrList(headers, strat.headerName) 397 | // Look backwards through the list of IP addresses 398 | for i := len(ipAddrs) - 1; i >= 0; i-- { 399 | if ipAddrs[i] != nil && isIPContainedInRanges(ipAddrs[i].IP, strat.trustedRanges) { 400 | // This IP is trusted 401 | continue 402 | } 403 | 404 | // At this point we have found the first-from-the-rightmost untrusted IP 405 | 406 | if ipAddrs[i] == nil { 407 | return "" 408 | } 409 | 410 | return ipAddrs[i].String() 411 | } 412 | 413 | // Either there are no addresses or they are all in our trusted ranges 414 | return "" 415 | } 416 | 417 | func (strat RightmostTrustedRangeStrategy) String() string { 418 | var b strings.Builder 419 | b.WriteString(fmt.Sprintf("{headerName:%v trustedRanges:[", strat.headerName)) 420 | for i, r := range strat.trustedRanges { 421 | if i > 0 { 422 | b.WriteString(" ") 423 | } 424 | b.WriteString(r.String()) 425 | } 426 | b.WriteString("]") 427 | return b.String() 428 | } 429 | 430 | // lastHeader returns the last header with the given name. It returns empty string if the 431 | // header is not found or if the header has an empty value. No validation is done on the 432 | // IP string. headerName must already be canonicalized. 433 | // This should be used with single-IP headers, like X-Real-IP. Per RFC 2616, they should 434 | // not have multiple headers, but if they do we can hope we're getting the newest/best by 435 | // taking the last instance. 436 | // This MUST NOT be used with list headers, like X-Forwarded-For and Forwarded. 437 | func lastHeader(headers http.Header, headerName string) string { 438 | // Note that Go's Header map uses canonicalized keys 439 | matches, ok := headers[headerName] 440 | if !ok || len(matches) == 0 { 441 | // For our uses of this function, returning an empty string in this case is fine 442 | return "" 443 | } 444 | 445 | return matches[len(matches)-1] 446 | } 447 | 448 | // getIPAddrList creates a single list of all of the X-Forwarded-For or Forwarded header 449 | // values, in order. Any invalid IPs will result in nil elements. headerName must already 450 | // be canonicalized. 451 | func getIPAddrList(headers http.Header, headerName string) []*net.IPAddr { 452 | var result []*net.IPAddr 453 | 454 | // There may be multiple XFF headers present. We need to iterate through them all, 455 | // in order, and collect all of the IPs. 456 | // Note that we're not joining all of the headers into a single string and then 457 | // splitting. Doing it that way would use more memory. 458 | // Note that Go's Header map uses canonicalized keys. 459 | for _, h := range headers[headerName] { 460 | // We now have a string with comma-separated list items 461 | for _, rawListItem := range strings.Split(h, ",") { 462 | // The IPs are often comma-space separated, so we'll need to trim the string 463 | rawListItem = strings.TrimSpace(rawListItem) 464 | 465 | var ipAddr *net.IPAddr 466 | // If this is the XFF header, rawListItem is just an IP; 467 | // if it's the Forwarded header, then there's more parsing to do. 468 | if headerName == forwardedHdr { 469 | ipAddr = parseForwardedListItem(rawListItem) 470 | } else { // == XFF 471 | ipAddr = goodIPAddr(rawListItem) 472 | } 473 | 474 | // ipAddr is nil if not valid 475 | result = append(result, ipAddr) 476 | } 477 | } 478 | 479 | // Possible performance improvements: 480 | // Here we are parsing _all_ of the IPs in the XFF headers, but we don't need all of 481 | // them. Instead, we could start from the left or the right (depending on strategy), 482 | // parse as we go, and stop when we've come to the one we want. But that would make 483 | // the various strategies somewhat more complex. 484 | 485 | return result 486 | } 487 | 488 | // parseForwardedListItem parses a Forwarded header list item, and returns the "for" IP 489 | // address. Nil is returned if the "for" IP is absent or invalid. 490 | func parseForwardedListItem(fwd string) *net.IPAddr { 491 | // The header list item can look like these kinds of thing: 492 | // For="[2001:db8:cafe::17%zone]:4711" 493 | // For="[2001:db8:cafe::17%zone]" 494 | // for=192.0.2.60;proto=http; by=203.0.113.43 495 | // for=192.0.2.43 496 | 497 | // First split up "for=", "by=", "host=", etc. 498 | fwdParts := strings.Split(fwd, ";") 499 | 500 | // Find the "for=" part, since that has the IP we want (maybe) 501 | var forPart string 502 | for _, fp := range fwdParts { 503 | // Whitespace is allowed around the semicolons 504 | fp = strings.TrimSpace(fp) 505 | 506 | fpSplit := strings.Split(fp, "=") 507 | if len(fpSplit) != 2 { 508 | // There are too many or too few equal signs in this part 509 | continue 510 | } 511 | 512 | if strings.EqualFold(fpSplit[0], "for") { 513 | // We found the "for=" part 514 | forPart = fpSplit[1] 515 | break 516 | } 517 | } 518 | 519 | // There shouldn't (per RFC 7239) be spaces around the semicolon or equal sign. It might 520 | // be more correct to consider spaces an error, but we'll tolerate and trim them. 521 | forPart = strings.TrimSpace(forPart) 522 | 523 | // Get rid of any quotes, such as surrounding IPv6 addresses. 524 | // Note that doing this without checking if the quotes are present means that we are 525 | // effectively accepting IPv6 addresses that don't strictly conform to RFC 7239, which 526 | // requires quotes. https://www.rfc-editor.org/rfc/rfc7239#section-4 527 | // This behaviour is debatable. 528 | // It also means that we will accept IPv4 addresses with quotes, which is correct. 529 | forPart = trimMatchedEnds(forPart, `"`) 530 | 531 | if forPart == "" { 532 | // We failed to find a "for=" part 533 | return nil 534 | } 535 | 536 | ipAddr := goodIPAddr(forPart) 537 | if ipAddr == nil { 538 | // The IP extracted from the "for=" part isn't valid 539 | return nil 540 | } 541 | 542 | return ipAddr 543 | } 544 | 545 | // ParseIPAddr parses the given string into a net.IPAddr, which is a useful type for 546 | // dealing with IPs have zones. The Go stdlib net package is lacking such a function. 547 | // This will also discard any port number from the input. 548 | func ParseIPAddr(ipStr string) (net.IPAddr, error) { 549 | host, _, err := net.SplitHostPort(ipStr) 550 | if err == nil { 551 | ipStr = host 552 | } 553 | // We continue even if net.SplitHostPort returned an error. This is because it may 554 | // complain that there are "too many colons" in an IPv6 address that has no brackets 555 | // and no port. net.ParseIP will be the final arbiter of validity. 556 | 557 | // Square brackets around IPv6 addresses may be used in the Forwarded header. 558 | // net.ParseIP doesn't like them, so we'll trim them off. 559 | ipStr = trimMatchedEnds(ipStr, "[]") 560 | 561 | ipStr, zone := SplitHostZone(ipStr) 562 | 563 | res := net.IPAddr{ 564 | IP: net.ParseIP(ipStr), 565 | Zone: zone, 566 | } 567 | 568 | if res.IP == nil { 569 | return net.IPAddr{}, fmt.Errorf("net.ParseIP failed") 570 | } 571 | 572 | return res, nil 573 | } 574 | 575 | // MustParseIPAddr panics if ParseIPAddr fails. 576 | func MustParseIPAddr(ipStr string) net.IPAddr { 577 | ipAddr, err := ParseIPAddr(ipStr) 578 | if err != nil { 579 | panic(fmt.Sprintf("ParseIPAddr failed: %v", err)) 580 | } 581 | return ipAddr 582 | } 583 | 584 | // goodIPAddr wraps ParseIPAddr and adds a check for unspecified (like "::") and zero-value 585 | // addresses (like "0.0.0.0"). These are nominally valid IPs (net.ParseIP will accept them), 586 | // but they are undesirable for the purposes of this library. 587 | // Note that this function should be the only use of ParseIPAddr in this library. 588 | func goodIPAddr(ipStr string) *net.IPAddr { 589 | ipAddr, err := ParseIPAddr(ipStr) 590 | if err != nil { 591 | return nil 592 | } 593 | 594 | if ipAddr.IP.IsUnspecified() { 595 | return nil 596 | } 597 | 598 | return &ipAddr 599 | } 600 | 601 | // SplitHostZone splits a "host%zone" string into its components. If there is no zone, 602 | // host is the original input and zone is empty. 603 | func SplitHostZone(s string) (host, zone string) { 604 | // This is copied from an unexported function in the Go stdlib: 605 | // https://github.com/golang/go/blob/5c9b6e8e63e012513b1cb1a4a08ff23dec4137a1/src/net/ipsock.go#L219-L228 606 | 607 | // The IPv6 scoped addressing zone identifier starts after the last percent sign. 608 | if i := strings.LastIndexByte(s, '%'); i > 0 { 609 | host, zone = s[:i], s[i+1:] 610 | } else { 611 | host = s 612 | } 613 | return 614 | } 615 | 616 | // mustParseCIDR panics if net.ParseCIDR fails 617 | func mustParseCIDR(s string) net.IPNet { 618 | _, ipNet, err := net.ParseCIDR(s) 619 | if err != nil { 620 | panic(err) 621 | } 622 | return *ipNet 623 | } 624 | 625 | // privateAndLocalRanges net.IPNets that are loopback, private, link local, default unicast. 626 | // Based on https://github.com/wader/filtertransport/blob/bdd9e61eee7804e94ceb927c896b59920345c6e4/filter.go#L36-L64 627 | // which is based on https://github.com/letsencrypt/boulder/blob/master/bdns/dns.go 628 | var privateAndLocalRanges = []net.IPNet{ 629 | mustParseCIDR("10.0.0.0/8"), // RFC1918 630 | mustParseCIDR("172.16.0.0/12"), // private 631 | mustParseCIDR("192.168.0.0/16"), // private 632 | mustParseCIDR("127.0.0.0/8"), // RFC5735 633 | mustParseCIDR("0.0.0.0/8"), // RFC1122 Section 3.2.1.3 634 | mustParseCIDR("169.254.0.0/16"), // RFC3927 635 | mustParseCIDR("192.0.0.0/24"), // RFC 5736 636 | mustParseCIDR("192.0.2.0/24"), // RFC 5737 637 | mustParseCIDR("198.51.100.0/24"), // Assigned as TEST-NET-2 638 | mustParseCIDR("203.0.113.0/24"), // Assigned as TEST-NET-3 639 | mustParseCIDR("192.88.99.0/24"), // RFC 3068 640 | mustParseCIDR("192.18.0.0/15"), // RFC 2544 641 | mustParseCIDR("224.0.0.0/4"), // RFC 3171 642 | mustParseCIDR("240.0.0.0/4"), // RFC 1112 643 | mustParseCIDR("255.255.255.255/32"), // RFC 919 Section 7 644 | mustParseCIDR("100.64.0.0/10"), // RFC 6598 645 | mustParseCIDR("::/128"), // RFC 4291: Unspecified Address 646 | mustParseCIDR("::1/128"), // RFC 4291: Loopback Address 647 | mustParseCIDR("100::/64"), // RFC 6666: Discard Address Block 648 | mustParseCIDR("2001::/23"), // RFC 2928: IETF Protocol Assignments 649 | mustParseCIDR("2001:2::/48"), // RFC 5180: Benchmarking 650 | mustParseCIDR("2001:db8::/32"), // RFC 3849: Documentation 651 | mustParseCIDR("2001::/32"), // RFC 4380: TEREDO 652 | mustParseCIDR("fc00::/7"), // RFC 4193: Unique-Local 653 | mustParseCIDR("fe80::/10"), // RFC 4291: Section 2.5.6 Link-Scoped Unicast 654 | mustParseCIDR("ff00::/8"), // RFC 4291: Section 2.7 655 | mustParseCIDR("2002::/16"), // RFC 7526: 6to4 anycast prefix deprecated 656 | } 657 | 658 | // isIPContainedInRanges returns true if the given IP is contained in at least one of the given ranges 659 | func isIPContainedInRanges(ip net.IP, ranges []net.IPNet) bool { 660 | for _, r := range ranges { 661 | if r.Contains(ip) { 662 | return true 663 | } 664 | } 665 | return false 666 | } 667 | 668 | // isPrivateOrLocal return true if the given IP address is private, local, or otherwise 669 | // not suitable for an external client IP. 670 | func isPrivateOrLocal(ip net.IP) bool { 671 | return isIPContainedInRanges(ip, privateAndLocalRanges) 672 | } 673 | 674 | // trimMatchedEnds trims s if and only if the first and last bytes in s are in chars. 675 | // If chars is a single character (like `"`), then the first and last bytes must match 676 | // that single character. If chars is two characters (like `[]`), the first byte in s 677 | // must match the first byte in chars, and the last bytes in s must match the last byte 678 | // in chars. 679 | // This helps us ensure that we only trim _matched_ quotes and brackets, 680 | // which strings.Trim doesn't provide. 681 | func trimMatchedEnds(s string, chars string) string { 682 | if len(chars) != 1 && len(chars) != 2 { 683 | panic("trimMatchedEnds chars must be length 1 or 2") 684 | } 685 | 686 | first, last := chars[0], chars[0] 687 | if len(chars) > 1 { 688 | last = chars[1] 689 | } 690 | 691 | if len(s) < 2 { 692 | return s 693 | } 694 | 695 | if s[0] != first { 696 | return s 697 | } 698 | 699 | if s[len(s)-1] != last { 700 | return s 701 | } 702 | 703 | return s[1 : len(s)-1] 704 | } 705 | -------------------------------------------------------------------------------- /realclientip_test.go: -------------------------------------------------------------------------------- 1 | // SPDX: 0BSD 2 | 3 | package realclientip 4 | 5 | import ( 6 | "fmt" 7 | "net" 8 | "net/http" 9 | "reflect" 10 | "testing" 11 | 12 | "github.com/realclientip/realclientip-go/ranges" 13 | ) 14 | 15 | /* 16 | IP types and formats to test: 17 | 18 | IPv4 with no port 19 | 192.0.2.60 20 | 21 | IPv4 with port 22 | 192.0.2.60:4833 23 | 24 | IPv6 with no port 25 | 2607:f8b0:4004:83f::200e 26 | 27 | IPv6 with port 28 | [2607:f8b0:4004:83f::200e]:4711 29 | 30 | IPv6 with zone and no port 31 | fe80::abcd%zone 32 | 33 | IPv6 with port and zone 34 | [fe80::abcd%zone]:4711 35 | 36 | IPv4-mapped IPv6 37 | ::ffff:188.0.2.128 38 | 39 | IPv4-mapped IPv6 with port 40 | [::ffff:188.0.2.128]:48483 41 | 42 | IPv4-mapped IPv6 in IPv6 (hex) form 43 | ::ffff:bc15:0006 44 | (this is 188.0.2.128; for an internal address use ::ffff:ac15:0006) 45 | 46 | NAT64 IPv4-mapped IPv6 47 | 64:ff9b::188.0.2.128 48 | (net.ParseIP converts to 64:ff9b::bc00:280, but netip.ParseAddr doesn't) 49 | 50 | IPv4 loopback 51 | 127.0.0.1 52 | 53 | IPv6 loopback 54 | ::1 55 | 56 | Forwarded header tests also require testing with quotes around full address. 57 | */ 58 | 59 | func ipAddrsEqual(a, b net.IPAddr) bool { 60 | return a.IP.Equal(b.IP) && a.Zone == b.Zone 61 | } 62 | 63 | func TestRemoteAddrStrategy(t *testing.T) { 64 | // Ensure the strategy interface is implemented 65 | var _ Strategy = RemoteAddrStrategy{} 66 | 67 | type args struct { 68 | headers http.Header 69 | remoteAddr string 70 | } 71 | tests := []struct { 72 | name string 73 | args args 74 | want string 75 | }{ 76 | { 77 | name: "IPv4 with port", 78 | args: args{ 79 | remoteAddr: "2.2.2.2:1234", 80 | }, 81 | want: "2.2.2.2", 82 | }, 83 | { 84 | name: "IPv4 with no port", 85 | args: args{ 86 | remoteAddr: "2.2.2.2", 87 | }, 88 | want: "2.2.2.2", 89 | }, 90 | { 91 | name: "IPv6 with port", 92 | args: args{ 93 | remoteAddr: "[2607:f8b0:4004:83f::18]:3838", 94 | }, 95 | want: "2607:f8b0:4004:83f::18", 96 | }, 97 | { 98 | name: "IPv6 with no port", 99 | args: args{ 100 | remoteAddr: "2607:f8b0:4004:83f::18", 101 | }, 102 | want: "2607:f8b0:4004:83f::18", 103 | }, 104 | { 105 | name: "IPv6 with zone and no port", 106 | args: args{ 107 | remoteAddr: `fe80::1111%eth0`, 108 | }, 109 | want: `fe80::1111%eth0`, 110 | }, 111 | { 112 | name: "IPv6 with zone and port", 113 | args: args{ 114 | remoteAddr: `[fe80::2222%eth0]:4848`, 115 | }, 116 | want: `fe80::2222%eth0`, 117 | }, 118 | { 119 | name: "IPv4-mapped IPv6", 120 | args: args{ 121 | remoteAddr: "[::ffff:172.21.0.6]:4747", 122 | }, 123 | // It is okay that this converts to the IPv4 format, since it's most important 124 | // that the respresentation be consistent. It might also be good that it does, 125 | // so that it will match the same plain IPv4 address. 126 | // net/netip.ParseAddr gives a different form: "::ffff:172.21.0.6" 127 | want: "172.21.0.6", 128 | }, 129 | { 130 | name: "IPv4-mapped IPv6 in IPv6 form", 131 | args: args{ 132 | remoteAddr: "0:0:0:0:0:ffff:bc15:0006", 133 | }, 134 | // net/netip.ParseAddr gives a different form: "::ffff:188.21.0.6" 135 | want: "188.21.0.6", 136 | }, 137 | { 138 | name: "NAT64 IPv4-mapped IPv6", 139 | args: args{ 140 | remoteAddr: "[64:ff9b::188.0.2.128]:4747", 141 | }, 142 | // net.ParseIP and net/netip.ParseAddr convert to this. This is fine, as it is 143 | // done consistently. 144 | want: "64:ff9b::bc00:280", 145 | }, 146 | { 147 | name: "6to4 IPv4-mapped IPv6", 148 | args: args{ 149 | remoteAddr: "[2002:c000:204::]:4747", 150 | }, 151 | want: "2002:c000:204::", 152 | }, 153 | { 154 | name: "IPv4 loopback", 155 | args: args{ 156 | remoteAddr: "127.0.0.1", 157 | }, 158 | want: "127.0.0.1", 159 | }, 160 | { 161 | name: "IPv6 loopback", 162 | args: args{ 163 | remoteAddr: "::1", 164 | }, 165 | want: "::1", 166 | }, 167 | { 168 | name: "Garbage header (unused)", 169 | args: args{ 170 | headers: http.Header{"X-Forwarded-For": []string{"!!!"}}, 171 | remoteAddr: "2.2.2.2:1234", 172 | }, 173 | want: "2.2.2.2", 174 | }, 175 | { 176 | name: "Fail: empty RemoteAddr", 177 | args: args{ 178 | remoteAddr: "", 179 | }, 180 | want: "", 181 | }, 182 | { 183 | name: "Fail: garbage RemoteAddr", 184 | args: args{ 185 | remoteAddr: "ohno", 186 | }, 187 | want: "", 188 | }, 189 | { 190 | name: "Fail: zero RemoteAddr IP", 191 | args: args{ 192 | remoteAddr: "0.0.0.0", 193 | }, 194 | want: "", 195 | }, 196 | { 197 | name: "Fail: unspecified RemoteAddr IP", 198 | args: args{ 199 | remoteAddr: "::", 200 | }, 201 | want: "", 202 | }, 203 | { 204 | name: "Fail: Unix domain socket", 205 | args: args{ 206 | remoteAddr: "@", 207 | }, 208 | want: "", 209 | }, 210 | } 211 | for _, tt := range tests { 212 | t.Run(tt.name, func(t *testing.T) { 213 | strat := RemoteAddrStrategy{} 214 | if got := strat.ClientIP(tt.args.headers, tt.args.remoteAddr); got != tt.want { 215 | t.Fatalf("ClientIP = %q, want %q", got, tt.want) 216 | } 217 | }) 218 | } 219 | } 220 | 221 | func TestSingleIPHeaderStrategy(t *testing.T) { 222 | // Ensure the strategy interface is implemented 223 | var _ Strategy = SingleIPHeaderStrategy{} 224 | 225 | type args struct { 226 | headerName string 227 | headers http.Header 228 | remoteAddr string 229 | } 230 | tests := []struct { 231 | name string 232 | args args 233 | want string 234 | wantErr bool 235 | }{ 236 | { 237 | name: "IPv4 with port", 238 | args: args{ 239 | headerName: "True-Client-IP", 240 | headers: http.Header{ 241 | "X-Real-Ip": []string{"1.1.1.1"}, 242 | "True-Client-Ip": []string{"2.2.2.2:49489"}, 243 | "X-Forwarded-For": []string{"3.3.3.3"}}, 244 | }, 245 | want: "2.2.2.2", 246 | }, 247 | { 248 | name: "IPv4 with no port", 249 | args: args{ 250 | headerName: "X-Real-IP", 251 | headers: http.Header{ 252 | "X-Real-Ip": []string{"1.1.1.1"}, 253 | "True-Client-Ip": []string{"2.2.2.2:49489"}, 254 | "X-Forwarded-For": []string{"3.3.3.3"}}, 255 | }, 256 | want: "1.1.1.1", 257 | }, 258 | { 259 | name: "IPv6 with port", 260 | args: args{ 261 | headerName: "X-Real-IP", 262 | headers: http.Header{ 263 | "X-Real-Ip": []string{"[2607:f8b0:4004:83f::18]:3838"}, 264 | "True-Client-Ip": []string{"2.2.2.2:49489"}, 265 | "X-Forwarded-For": []string{"3.3.3.3"}}, 266 | }, 267 | want: "2607:f8b0:4004:83f::18", 268 | }, 269 | { 270 | name: "IPv6 with no port", 271 | args: args{ 272 | headerName: "X-Real-IP", 273 | headers: http.Header{ 274 | "X-Real-Ip": []string{"2607:f8b0:4004:83f::19"}, 275 | "True-Client-Ip": []string{"2.2.2.2:49489"}, 276 | "X-Forwarded-For": []string{"3.3.3.3"}}, 277 | }, 278 | want: "2607:f8b0:4004:83f::19", 279 | }, 280 | { 281 | name: "IPv6 with zone and no port", 282 | args: args{ 283 | headerName: "a-b-c-d", 284 | headers: http.Header{ 285 | "X-Real-Ip": []string{"2607:f8b0:4004:83f::19"}, 286 | "A-B-C-D": []string{"fe80::1111%zone"}, 287 | "X-Forwarded-For": []string{"3.3.3.3"}}, 288 | }, 289 | want: "fe80::1111%zone", 290 | }, 291 | { 292 | name: "IPv6 with zone and port", 293 | args: args{ 294 | headerName: "a-b-c-d", 295 | headers: http.Header{ 296 | "X-Real-Ip": []string{"2607:f8b0:4004:83f::19"}, 297 | "A-B-C-D": []string{"[fe80::1111%zone]:4848"}, 298 | "X-Forwarded-For": []string{"3.3.3.3"}}, 299 | }, 300 | want: "fe80::1111%zone", 301 | }, 302 | { 303 | name: "IPv6 with brackets but no port", 304 | args: args{ 305 | headerName: "x-real-ip", 306 | headers: http.Header{ 307 | "X-Real-Ip": []string{"2607:f8b0:4004:83f::19"}, 308 | "A-B-C-D": []string{"[fe80::1111%zone]:4848"}, 309 | "X-Forwarded-For": []string{"3.3.3.3"}}, 310 | }, 311 | want: "2607:f8b0:4004:83f::19", 312 | }, 313 | { 314 | name: "IP-mapped IPv6", 315 | args: args{ 316 | headerName: "x-real-ip", 317 | headers: http.Header{ 318 | "X-Real-Ip": []string{"::ffff:172.21.0.6"}, 319 | "A-B-C-D": []string{"[fe80::1111%zone]:4848"}, 320 | "X-Forwarded-For": []string{"3.3.3.3"}}, 321 | }, 322 | want: "172.21.0.6", 323 | }, 324 | { 325 | name: "IPv4-mapped IPv6 in IPv6 form", 326 | args: args{ 327 | headerName: "x-real-ip", 328 | headers: http.Header{ 329 | "X-Real-Ip": []string{"[64:ff9b::188.0.2.128]:4747"}, 330 | "A-B-C-D": []string{"[fe80::1111%zone]:4848"}, 331 | "X-Forwarded-For": []string{"3.3.3.3"}}, 332 | }, 333 | want: "64:ff9b::bc00:280", 334 | }, 335 | { 336 | name: "6to4 IPv4-mapped IPv6", 337 | args: args{ 338 | headerName: "x-real-ip", 339 | headers: http.Header{ 340 | "X-Real-Ip": []string{"2002:c000:204::"}, 341 | "A-B-C-D": []string{"[fe80::1111%zone]:4848"}, 342 | "X-Forwarded-For": []string{"3.3.3.3"}}, 343 | }, 344 | want: "2002:c000:204::", 345 | }, 346 | { 347 | name: "IPv4 loopback", 348 | args: args{ 349 | headerName: "x-real-ip", 350 | headers: http.Header{ 351 | "X-Real-Ip": []string{"127.0.0.1"}, 352 | "A-B-C-D": []string{"[fe80::1111%zone]:4848"}, 353 | "X-Forwarded-For": []string{"3.3.3.3"}}, 354 | }, 355 | want: "127.0.0.1", 356 | }, 357 | { 358 | name: "Fail: missing header", 359 | args: args{ 360 | headerName: "x-real-ip", 361 | headers: http.Header{ 362 | "A-B-C-D": []string{"[fe80::1111%zone]:4848"}, 363 | "X-Forwarded-For": []string{"3.3.3.3"}}, 364 | }, 365 | want: "", 366 | }, 367 | { 368 | name: "Fail: garbage IP", 369 | args: args{ 370 | headerName: "True-Client-Ip", 371 | headers: http.Header{ 372 | "X-Real-Ip": []string{"::1"}, 373 | "True-Client-Ip": []string{"nope"}, 374 | "X-Forwarded-For": []string{"3.3.3.3"}}, 375 | }, 376 | want: "", 377 | }, 378 | { 379 | name: "Fail: zero IP", 380 | args: args{ 381 | headerName: "True-Client-Ip", 382 | headers: http.Header{ 383 | "X-Real-Ip": []string{"::1"}, 384 | "True-Client-Ip": []string{"0.0.0.0"}, 385 | "X-Forwarded-For": []string{"3.3.3.3"}}, 386 | }, 387 | want: "", 388 | }, 389 | { 390 | name: "Error: empty header name", 391 | args: args{ 392 | headerName: "", 393 | headers: http.Header{ 394 | "X-Real-Ip": []string{"::1"}, 395 | "True-Client-Ip": []string{"2.2.2.2"}, 396 | "X-Forwarded-For": []string{"3.3.3.3"}}, 397 | }, 398 | wantErr: true, 399 | }, 400 | { 401 | name: "Error: X-Forwarded-For header", 402 | args: args{ 403 | headerName: "X-Forwarded-For", 404 | headers: http.Header{ 405 | "X-Real-Ip": []string{"::1"}, 406 | "True-Client-Ip": []string{"2.2.2.2"}, 407 | "X-Forwarded-For": []string{"3.3.3.3"}}, 408 | }, 409 | wantErr: true, 410 | }, 411 | } 412 | for _, tt := range tests { 413 | t.Run(tt.name, func(t *testing.T) { 414 | strat, err := NewSingleIPHeaderStrategy(tt.args.headerName) 415 | if (err != nil) != tt.wantErr { 416 | t.Fatalf("NewSingleIPHeaderStrategy error = %v, wantErr %v", err, tt.wantErr) 417 | return 418 | } 419 | 420 | if err != nil { 421 | // We can't continue 422 | return 423 | } 424 | 425 | got := strat.ClientIP(tt.args.headers, tt.args.remoteAddr) 426 | if !reflect.DeepEqual(got, tt.want) { 427 | t.Fatalf("ClientIP = %q, want %q", got, tt.want) 428 | } 429 | }) 430 | } 431 | } 432 | 433 | func TestLeftmostNonPrivateStrategy(t *testing.T) { 434 | // Ensure the strategy interface is implemented 435 | var _ Strategy = LeftmostNonPrivateStrategy{} 436 | 437 | type args struct { 438 | headerName string 439 | headers http.Header 440 | remoteAddr string 441 | } 442 | tests := []struct { 443 | name string 444 | args args 445 | want string 446 | wantErr bool 447 | }{ 448 | { 449 | name: "IPv4 with port", 450 | args: args{ 451 | headerName: "X-Forwarded-For", 452 | headers: http.Header{ 453 | "X-Real-Ip": []string{`1.1.1.1`}, 454 | "X-Forwarded-For": []string{`2.2.2.2:3384, 3.3.3.3`, `4.4.4.4`}, 455 | }, 456 | }, 457 | want: "2.2.2.2", 458 | }, 459 | { 460 | name: "IPv4 with no port", 461 | args: args{ 462 | headerName: "Forwarded", 463 | headers: http.Header{ 464 | "X-Real-Ip": []string{`1.1.1.1`}, 465 | "X-Forwarded-For": []string{`2.2.2.2:3384, 3.3.3.3`, `4.4.4.4`}, 466 | "Forwarded": []string{`For=5.5.5.5`, `For=6.6.6.6`}, 467 | }, 468 | }, 469 | want: "5.5.5.5", 470 | }, 471 | { 472 | name: "IPv6 with port", 473 | args: args{ 474 | headerName: "X-Forwarded-For", 475 | headers: http.Header{ 476 | "X-Real-Ip": []string{`1.1.1.1`}, 477 | "X-Forwarded-For": []string{`[2607:f8b0:4004:83f::18]:3838, 3.3.3.3`, `4.4.4.4`}, 478 | }, 479 | }, 480 | want: "2607:f8b0:4004:83f::18", 481 | }, 482 | { 483 | name: "IPv6 with no port", 484 | args: args{ 485 | headerName: "Forwarded", 486 | headers: http.Header{ 487 | "X-Real-Ip": []string{`1.1.1.1`}, 488 | "X-Forwarded-For": []string{`2.2.2.2:3384, 3.3.3.3`, `4.4.4.4`}, 489 | "Forwarded": []string{`Host=blah;For="2607:f8b0:4004:83f::18";Proto=https`}, 490 | }, 491 | }, 492 | want: "2607:f8b0:4004:83f::18", 493 | }, 494 | { 495 | name: "IPv6 with port and zone", 496 | args: args{ 497 | headerName: "Forwarded", 498 | headers: http.Header{ 499 | "X-Real-Ip": []string{`1.1.1.1`}, 500 | "X-Forwarded-For": []string{`2.2.2.2:3384, 3.3.3.3`, `4.4.4.4`}, 501 | "Forwarded": []string{`For=[fe80::1111%zone], Host=blah;For="[2607:f8b0:4004:83f::18%zone]:9943";Proto=https`, `host=what;for=6.6.6.6;proto=https`}, 502 | }, 503 | }, 504 | want: "2607:f8b0:4004:83f::18%zone", 505 | }, 506 | { 507 | name: "IPv6 with port and zone, no quotes", 508 | args: args{ 509 | headerName: "Forwarded", 510 | headers: http.Header{ 511 | "X-Real-Ip": []string{`1.1.1.1`}, 512 | "X-Forwarded-For": []string{`2.2.2.2:3384, 3.3.3.3`, `4.4.4.4`}, 513 | "Forwarded": []string{`For=[fe80::1111%zone], Host=blah;For=[2607:f8b0:4004:83f::18%zone]:9943;Proto=https`, `host=what;for=6.6.6.6;proto=https`}, 514 | }, 515 | }, 516 | want: "2607:f8b0:4004:83f::18%zone", 517 | }, 518 | { 519 | name: "IPv4-mapped IPv6", 520 | args: args{ 521 | headerName: "x-forwarded-for", 522 | headers: http.Header{ 523 | "X-Real-Ip": []string{`1.1.1.1`}, 524 | "X-Forwarded-For": []string{`::ffff:188.0.2.128, 3.3.3.3`, `4.4.4.4`}, 525 | "Forwarded": []string{`Host=blah;For="7.7.7.7";Proto=https`, `host=what;for=6.6.6.6;proto=https`}, 526 | }, 527 | }, 528 | want: "188.0.2.128", 529 | }, 530 | { 531 | name: "IPv4-mapped IPv6 with port", 532 | args: args{ 533 | headerName: "x-forwarded-for", 534 | headers: http.Header{ 535 | "X-Real-Ip": []string{`1.1.1.1`}, 536 | "X-Forwarded-For": []string{`[::ffff:188.0.2.128]:48483, 3.3.3.3`, `4.4.4.4`}, 537 | "Forwarded": []string{`Host=blah;For="7.7.7.7";Proto=https`, `host=what;for=6.6.6.6;proto=https`}, 538 | }, 539 | }, 540 | want: "188.0.2.128", 541 | }, 542 | { 543 | name: "IPv4-mapped IPv6 in IPv6 (hex) form", 544 | args: args{ 545 | headerName: "forwarded", 546 | headers: http.Header{ 547 | "X-Real-Ip": []string{`1.1.1.1`}, 548 | "X-Forwarded-For": []string{`[::ffff:188.0.2.128]:48483, 3.3.3.3`, `4.4.4.4`}, 549 | "Forwarded": []string{`For="::ffff:bc15:0006"`, `host=what;for=6.6.6.6;proto=https`}, 550 | }, 551 | }, 552 | want: "188.21.0.6", 553 | }, 554 | { 555 | name: "NAT64 IPv4-mapped IPv6", 556 | args: args{ 557 | headerName: "x-forwarded-for", 558 | headers: http.Header{ 559 | "X-Real-Ip": []string{`1.1.1.1`}, 560 | "X-Forwarded-For": []string{`64:ff9b::188.0.2.128, 3.3.3.3`, `4.4.4.4`}, 561 | "Forwarded": []string{`For="::ffff:bc15:0006"`, `host=what;for=6.6.6.6;proto=https`}, 562 | }, 563 | }, 564 | want: "64:ff9b::bc00:280", 565 | }, 566 | { 567 | name: "XFF: leftmost not desirable", 568 | args: args{ 569 | headerName: "x-forwarded-for", 570 | headers: http.Header{ 571 | "X-Real-Ip": []string{`1.1.1.1`}, 572 | "X-Forwarded-For": []string{`::1, nope`, `4.4.4.4, 5.5.5.5`}, 573 | "Forwarded": []string{`For="::ffff:bc15:0006"`, `host=what;for=6.6.6.6;proto=https`}, 574 | }, 575 | }, 576 | want: "4.4.4.4", 577 | }, 578 | { 579 | name: "Forwarded: leftmost not desirable", 580 | args: args{ 581 | headerName: "Forwarded", 582 | headers: http.Header{ 583 | "X-Real-Ip": []string{`1.1.1.1`}, 584 | "X-Forwarded-For": []string{`::1, nope`, `4.4.4.4, 5.5.5.5`}, 585 | "Forwarded": []string{`For="", For="::ffff:192.168.1.1"`, `host=what;for=:48485;proto=https,For="2607:f8b0:4004:83f::18"`}, 586 | }, 587 | }, 588 | want: "2607:f8b0:4004:83f::18", 589 | }, 590 | { 591 | name: "Fail: XFF: none acceptable", 592 | args: args{ 593 | headerName: "X-Forwarded-For", 594 | headers: http.Header{ 595 | "X-Real-Ip": []string{`1.1.1.1`}, 596 | "X-Forwarded-For": []string{`::1, nope, ::, 0.0.0.0`, `192.168.1.1, !?!`}, 597 | "Forwarded": []string{`For="", For="::ffff:192.168.1.1"`, `host=what;for=:48485;proto=https,For="fe80::abcd%zone"`}, 598 | }, 599 | }, 600 | want: "", 601 | }, 602 | { 603 | name: "Fail: Forwarded: none acceptable", 604 | args: args{ 605 | headerName: "Forwarded", 606 | headers: http.Header{ 607 | "X-Real-Ip": []string{`1.1.1.1`}, 608 | "X-Forwarded-For": []string{`::1, nope`, `192.168.1.1, 2.2.2.2`}, 609 | "Forwarded": []string{`For="", For="::ffff:192.168.1.1"`, `host=what;for=:48485;proto=https,For="::ffff:ac15:0006%zone",For="::",For=0.0.0.0`}, 610 | }, 611 | }, 612 | want: "", 613 | }, 614 | { 615 | name: "Fail: XFF: no header", 616 | args: args{ 617 | headerName: "Forwarded", 618 | headers: http.Header{ 619 | "X-Real-Ip": []string{`1.1.1.1`}, 620 | "Forwarded": []string{`For="", For="::ffff:192.168.1.1"`, `host=what;for=:48485;proto=https,For="::ffff:ac15:0006%zone"`}, 621 | }, 622 | }, 623 | want: "", 624 | }, 625 | { 626 | name: "Fail: Forwarded: no header", 627 | args: args{ 628 | headerName: "forwarded", 629 | headers: http.Header{ 630 | "X-Real-Ip": []string{`1.1.1.1`}, 631 | "X-Forwarded-For": []string{`64:ff9b::188.0.2.128, 3.3.3.3`, `4.4.4.4`}, 632 | }, 633 | }, 634 | want: "", 635 | }, 636 | { 637 | name: "Error: empty header name", 638 | args: args{ 639 | headerName: "", 640 | headers: http.Header{ 641 | "X-Real-Ip": []string{"::1"}, 642 | "True-Client-Ip": []string{"2.2.2.2"}, 643 | "X-Forwarded-For": []string{"3.3.3.3"}}, 644 | }, 645 | wantErr: true, 646 | }, 647 | { 648 | name: "Error: invalid header", 649 | args: args{ 650 | headerName: "X-Real-IP", 651 | headers: http.Header{ 652 | "X-Real-Ip": []string{"::1"}, 653 | "True-Client-Ip": []string{"2.2.2.2"}, 654 | "X-Forwarded-For": []string{"3.3.3.3"}}, 655 | }, 656 | wantErr: true, 657 | }, 658 | } 659 | for _, tt := range tests { 660 | t.Run(tt.name, func(t *testing.T) { 661 | strat, err := NewLeftmostNonPrivateStrategy(tt.args.headerName) 662 | if (err != nil) != tt.wantErr { 663 | t.Fatalf("NewLeftmostNonPrivateStrategy error = %v, wantErr %v", err, tt.wantErr) 664 | return 665 | } 666 | 667 | if err != nil { 668 | // We can't continue 669 | return 670 | } 671 | 672 | got := strat.ClientIP(tt.args.headers, tt.args.remoteAddr) 673 | if !reflect.DeepEqual(got, tt.want) { 674 | t.Fatalf("ClientIP = %q, want %q", got, tt.want) 675 | } 676 | }) 677 | } 678 | } 679 | 680 | func TestRightmostNonPrivateStrategy(t *testing.T) { 681 | // Ensure the strategy interface is implemented 682 | var _ Strategy = RightmostNonPrivateStrategy{} 683 | 684 | type args struct { 685 | headerName string 686 | headers http.Header 687 | remoteAddr string 688 | } 689 | tests := []struct { 690 | name string 691 | args args 692 | want string 693 | wantErr bool 694 | }{ 695 | { 696 | name: "IPv4 with port", 697 | args: args{ 698 | headerName: "X-Forwarded-For", 699 | headers: http.Header{ 700 | "X-Real-Ip": []string{`1.1.1.1`}, 701 | "X-Forwarded-For": []string{`2.2.2.2:3384, 3.3.3.3`, `4.4.4.4:39333`}, 702 | }, 703 | }, 704 | want: "4.4.4.4", 705 | }, 706 | { 707 | name: "IPv4 with no port", 708 | args: args{ 709 | headerName: "Forwarded", 710 | headers: http.Header{ 711 | "X-Real-Ip": []string{`1.1.1.1`}, 712 | "X-Forwarded-For": []string{`2.2.2.2:3384, 3.3.3.3`, `4.4.4.4`}, 713 | "Forwarded": []string{`For=5.5.5.5`, `For=6.6.6.6`}, 714 | }, 715 | }, 716 | want: "6.6.6.6", 717 | }, 718 | { 719 | name: "IPv6 with port", 720 | args: args{ 721 | headerName: "X-Forwarded-For", 722 | headers: http.Header{ 723 | "X-Real-Ip": []string{`1.1.1.1`}, 724 | "X-Forwarded-For": []string{`[2607:f8b0:4004:83f::18]:3838`}, 725 | }, 726 | }, 727 | want: "2607:f8b0:4004:83f::18", 728 | }, 729 | { 730 | name: "IPv6 with no port", 731 | args: args{ 732 | headerName: "Forwarded", 733 | headers: http.Header{ 734 | "X-Real-Ip": []string{`1.1.1.1`}, 735 | "X-Forwarded-For": []string{`2.2.2.2:3384, 3.3.3.3`, `4.4.4.4`}, 736 | "Forwarded": []string{`host=what;for=6.6.6.6;proto=https`, `Host=blah;For="2607:f8b0:4004:83f::18";Proto=https`}, 737 | }, 738 | }, 739 | want: "2607:f8b0:4004:83f::18", 740 | }, 741 | { 742 | name: "IPv6 with port and zone", 743 | args: args{ 744 | headerName: "Forwarded", 745 | headers: http.Header{ 746 | "X-Real-Ip": []string{`1.1.1.1`}, 747 | "X-Forwarded-For": []string{`2.2.2.2:3384, 3.3.3.3`, `4.4.4.4`}, 748 | "Forwarded": []string{`host=what;for=6.6.6.6;proto=https`, `For="[2607:f8b0:4004:83f::18%eth0]:3393";Proto=https`, `Host=blah;For="[fe80::1111%zone]:9943";Proto=https`}, 749 | }, 750 | }, 751 | want: "2607:f8b0:4004:83f::18%eth0", 752 | }, 753 | { 754 | name: "IPv6 with port and zone, no quotes", 755 | args: args{ 756 | headerName: "Forwarded", 757 | headers: http.Header{ 758 | "X-Real-Ip": []string{`1.1.1.1`}, 759 | "X-Forwarded-For": []string{`2.2.2.2:3384, 3.3.3.3`, `4.4.4.4`}, 760 | "Forwarded": []string{`host=what;for=6.6.6.6;proto=https`, `For="[2607:f8b0:4004:83f::18%eth0]:3393";Proto=https`, `Host=blah;For=[fe80::1111%zone]:9943;Proto=https`}, 761 | }, 762 | }, 763 | want: "2607:f8b0:4004:83f::18%eth0", 764 | }, 765 | { 766 | name: "IPv4-mapped IPv6", 767 | args: args{ 768 | headerName: "x-forwarded-for", 769 | headers: http.Header{ 770 | "X-Real-Ip": []string{`1.1.1.1`}, 771 | "X-Forwarded-For": []string{`3.3.3.3`, `4.4.4.4, ::ffff:188.0.2.128`}, 772 | "Forwarded": []string{`Host=blah;For="7.7.7.7";Proto=https`, `host=what;for=6.6.6.6;proto=https`}, 773 | }, 774 | }, 775 | want: "188.0.2.128", 776 | }, 777 | { 778 | name: "IPv4-mapped IPv6 with port", 779 | args: args{ 780 | headerName: "x-forwarded-for", 781 | headers: http.Header{ 782 | "X-Real-Ip": []string{`1.1.1.1`}, 783 | "X-Forwarded-For": []string{`3.3.3.3`, `4.4.4.4,[::ffff:188.0.2.128]:48483`}, 784 | "Forwarded": []string{`Host=blah;For="7.7.7.7";Proto=https`, `host=what;for=6.6.6.6;proto=https`}, 785 | }, 786 | }, 787 | want: "188.0.2.128", 788 | }, 789 | { 790 | name: "IPv4-mapped IPv6 in IPv6 (hex) form", 791 | args: args{ 792 | headerName: "forwarded", 793 | headers: http.Header{ 794 | "X-Real-Ip": []string{`1.1.1.1`}, 795 | "X-Forwarded-For": []string{`[::ffff:188.0.2.128]:48483, 3.3.3.3`, `4.4.4.4`}, 796 | "Forwarded": []string{`host=what;for=6.6.6.6;proto=https`, `For="::ffff:bc15:0006"`}, 797 | }, 798 | }, 799 | want: "188.21.0.6", 800 | }, 801 | { 802 | name: "NAT64 IPv4-mapped IPv6", 803 | args: args{ 804 | headerName: "x-forwarded-for", 805 | headers: http.Header{ 806 | "X-Real-Ip": []string{`1.1.1.1`}, 807 | "X-Forwarded-For": []string{`3.3.3.3`, `4.4.4.4, 64:ff9b::188.0.2.128`}, 808 | "Forwarded": []string{`For="::ffff:bc15:0006"`, `host=what;for=6.6.6.6;proto=https`}, 809 | }, 810 | }, 811 | want: "64:ff9b::bc00:280", 812 | }, 813 | { 814 | name: "XFF: rightmost not desirable", 815 | args: args{ 816 | headerName: "x-forwarded-for", 817 | headers: http.Header{ 818 | "X-Real-Ip": []string{`1.1.1.1`}, 819 | "X-Forwarded-For": []string{`4.4.4.4, 5.5.5.5`, `::1, nope`}, 820 | "Forwarded": []string{`For="::ffff:bc15:0006"`, `host=what;for=6.6.6.6;proto=https`}, 821 | }, 822 | }, 823 | want: "5.5.5.5", 824 | }, 825 | { 826 | name: "Forwarded: rightmost not desirable", 827 | args: args{ 828 | headerName: "Forwarded", 829 | headers: http.Header{ 830 | "X-Real-Ip": []string{`1.1.1.1`}, 831 | "X-Forwarded-For": []string{`::1, nope`, `4.4.4.4, 5.5.5.5`}, 832 | "Forwarded": []string{`host=what;for=:48485;proto=https,For=2.2.2.2`, `For="", For="::ffff:192.168.1.1"`}, 833 | }, 834 | }, 835 | want: "2.2.2.2", 836 | }, 837 | { 838 | name: "Fail: XFF: none acceptable", 839 | args: args{ 840 | headerName: "X-Forwarded-For", 841 | headers: http.Header{ 842 | "X-Real-Ip": []string{`1.1.1.1`}, 843 | "X-Forwarded-For": []string{`::1, nope`, `192.168.1.1, !?!, ::, 0.0.0.0`}, 844 | "Forwarded": []string{`For="", For="::ffff:192.168.1.1"`, `host=what;for=:48485;proto=https,For="fe80::abcd%zone"`}, 845 | }, 846 | }, 847 | want: "", 848 | }, 849 | { 850 | name: "Fail: Forwarded: none acceptable", 851 | args: args{ 852 | headerName: "Forwarded", 853 | headers: http.Header{ 854 | "X-Real-Ip": []string{`1.1.1.1`}, 855 | "X-Forwarded-For": []string{`::1, nope`, `192.168.1.1, 2.2.2.2`}, 856 | "Forwarded": []string{`For="", For="::ffff:192.168.1.1"`, `host=what;for=:48485;proto=https,For="::ffff:ac15:0006%zone", For="::", For=0.0.0.0`}, 857 | }, 858 | }, 859 | want: "", 860 | }, 861 | { 862 | name: "Fail: XFF: no header", 863 | args: args{ 864 | headerName: "Forwarded", 865 | headers: http.Header{ 866 | "X-Real-Ip": []string{`1.1.1.1`}, 867 | "Forwarded": []string{`For="", For="::ffff:192.168.1.1"`, `host=what;for=:48485;proto=https,For="::ffff:ac15:0006%zone"`}, 868 | }, 869 | remoteAddr: "9.9.9.9", 870 | }, 871 | want: "", 872 | }, 873 | { 874 | name: "Fail: Forwarded: no header", 875 | args: args{ 876 | headerName: "forwarded", 877 | headers: http.Header{ 878 | "X-Real-Ip": []string{`1.1.1.1`}, 879 | "X-Forwarded-For": []string{`64:ff9b::188.0.2.128, 3.3.3.3`, `4.4.4.4`}, 880 | }, 881 | }, 882 | want: "", 883 | }, 884 | { 885 | name: "Error: empty header name", 886 | args: args{ 887 | headerName: "", 888 | headers: http.Header{ 889 | "X-Real-Ip": []string{"::1"}, 890 | "True-Client-Ip": []string{"2.2.2.2"}, 891 | "X-Forwarded-For": []string{"3.3.3.3"}}, 892 | }, 893 | wantErr: true, 894 | }, 895 | { 896 | name: "Error: invalid header", 897 | args: args{ 898 | headerName: "X-Real-IP", 899 | headers: http.Header{ 900 | "X-Real-Ip": []string{"::1"}, 901 | "True-Client-Ip": []string{"2.2.2.2"}, 902 | "X-Forwarded-For": []string{"3.3.3.3"}}, 903 | }, 904 | wantErr: true, 905 | }, 906 | } 907 | for _, tt := range tests { 908 | t.Run(tt.name, func(t *testing.T) { 909 | strat, err := NewRightmostNonPrivateStrategy(tt.args.headerName) 910 | if (err != nil) != tt.wantErr { 911 | t.Fatalf("NewRightmostNonPrivateStrategy error = %v, wantErr %v", err, tt.wantErr) 912 | return 913 | } 914 | 915 | if err != nil { 916 | // We can't continue 917 | return 918 | } 919 | 920 | got := strat.ClientIP(tt.args.headers, tt.args.remoteAddr) 921 | if !reflect.DeepEqual(got, tt.want) { 922 | t.Fatalf("ClientIP = %q, want %q", got, tt.want) 923 | } 924 | }) 925 | } 926 | } 927 | 928 | func TestRightmostTrustedCountStrategy(t *testing.T) { 929 | // Ensure the strategy interface is implemented 930 | var _ Strategy = RightmostTrustedCountStrategy{} 931 | 932 | type args struct { 933 | headerName string 934 | trustedCount int 935 | headers http.Header 936 | remoteAddr string 937 | } 938 | tests := []struct { 939 | name string 940 | args args 941 | want string 942 | wantErr bool 943 | }{ 944 | // TODO: Is it okay not to test every IP type, since the logic is sufficiently similar to RightmostNonPrivateStrategy? 945 | 946 | { 947 | name: "Count one", 948 | args: args{ 949 | headerName: "Forwarded", 950 | trustedCount: 1, 951 | headers: http.Header{ 952 | "X-Real-Ip": []string{`1.1.1.1`}, 953 | "X-Forwarded-For": []string{`4.4.4.4, 5.5.5.5`, `::1, fe80::382b:141b:fa4a:2a16%28`}, 954 | "Forwarded": []string{`For="::ffff:bc15:0006"`, `host=what;for=6.6.6.6;proto=https`}, 955 | }, 956 | }, 957 | want: "6.6.6.6", 958 | }, 959 | { 960 | name: "Count five", 961 | args: args{ 962 | headerName: "X-Forwarded-For", 963 | trustedCount: 5, 964 | headers: http.Header{ 965 | "X-Real-Ip": []string{`1.1.1.1`}, 966 | "X-Forwarded-For": []string{`4.4.4.4, 5.5.5.5`, `::1, fe80::382b:141b:fa4a:2a16%28`, `7.7.7.7.7, 8.8.8.8, 9.9.9.9, 10.10.10.10,11.11.11.11, 12.12.12.12`}, 967 | "Forwarded": []string{`For="::ffff:bc15:0006"`, `host=what;for=6.6.6.6;proto=https`}, 968 | }, 969 | }, 970 | want: "8.8.8.8", 971 | }, 972 | { 973 | name: "Fail: header too short/count too large", 974 | args: args{ 975 | headerName: "X-Forwarded-For", 976 | trustedCount: 50, 977 | headers: http.Header{ 978 | "X-Real-Ip": []string{`1.1.1.1`}, 979 | "X-Forwarded-For": []string{`4.4.4.4, 5.5.5.5`, `::1, fe80::382b:141b:fa4a:2a16%28`, `7.7.7.7.7, 8.8.8.8, 9.9.9.9, 10.10.10.10,11.11.11.11, 12.12.12.12`}, 980 | "Forwarded": []string{`For="::ffff:bc15:0006"`, `host=what;for=6.6.6.6;proto=https`}, 981 | }, 982 | }, 983 | want: "", 984 | }, 985 | { 986 | name: "Fail: bad value at count index", 987 | args: args{ 988 | headerName: "Forwarded", 989 | trustedCount: 2, 990 | headers: http.Header{ 991 | "X-Real-Ip": []string{`1.1.1.1`}, 992 | "X-Forwarded-For": []string{`4.4.4.4, 5.5.5.5`, `::1, fe80::382b:141b:fa4a:2a16%28`, `7.7.7.7.7, 8.8.8.8, 9.9.9.9, 10.10.10.10,11.11.11.11, 12.12.12.12`}, 993 | "Forwarded": []string{`For="::ffff:bc15:0006"`, `For=nope`, `host=what;for=6.6.6.6;proto=https`}, 994 | }, 995 | }, 996 | want: "", 997 | }, 998 | { 999 | name: "Fail: zero value at count index", 1000 | args: args{ 1001 | headerName: "Forwarded", 1002 | trustedCount: 2, 1003 | headers: http.Header{ 1004 | "X-Real-Ip": []string{`1.1.1.1`}, 1005 | "X-Forwarded-For": []string{`4.4.4.4, 5.5.5.5`, `::1, fe80::382b:141b:fa4a:2a16%28`, `7.7.7.7.7, 8.8.8.8, 9.9.9.9, 10.10.10.10,11.11.11.11, 12.12.12.12`}, 1006 | "Forwarded": []string{`For="::ffff:bc15:0006"`, `For=0.0.0.0`, `host=what;for=6.6.6.6;proto=https`}, 1007 | }, 1008 | }, 1009 | want: "", 1010 | }, 1011 | { 1012 | name: "Fail: header missing", 1013 | args: args{ 1014 | headerName: "Forwarded", 1015 | trustedCount: 1, 1016 | headers: http.Header{ 1017 | "X-Real-Ip": []string{`1.1.1.1`}, 1018 | "X-Forwarded-For": []string{`4.4.4.4, 5.5.5.5`, `::1, fe80::382b:141b:fa4a:2a16%28`, `7.7.7.7.7, 8.8.8.8, 9.9.9.9, 10.10.10.10,11.11.11.11, 12.12.12.12`}, 1019 | }, 1020 | }, 1021 | want: "", 1022 | }, 1023 | { 1024 | name: "Error: empty header name", 1025 | args: args{ 1026 | headerName: "", 1027 | trustedCount: 1, 1028 | headers: http.Header{ 1029 | "X-Real-Ip": []string{"::1"}, 1030 | "True-Client-Ip": []string{"2.2.2.2"}, 1031 | "X-Forwarded-For": []string{"3.3.3.3"}}, 1032 | }, 1033 | wantErr: true, 1034 | }, 1035 | { 1036 | name: "Error: invalid header", 1037 | args: args{ 1038 | headerName: "X-Real-IP", 1039 | trustedCount: 1, 1040 | headers: http.Header{ 1041 | "X-Real-Ip": []string{"::1"}, 1042 | "True-Client-Ip": []string{"2.2.2.2"}, 1043 | "X-Forwarded-For": []string{"3.3.3.3"}}, 1044 | }, 1045 | wantErr: true, 1046 | }, 1047 | { 1048 | name: "Error: zero trustedCount", 1049 | args: args{ 1050 | headerName: "x-forwarded-for", 1051 | trustedCount: 0, 1052 | headers: http.Header{ 1053 | "X-Real-Ip": []string{`1.1.1.1`}, 1054 | "X-Forwarded-For": []string{`4.4.4.4, 5.5.5.5`, `::1, nope`, `fe80::382b:141b:fa4a:2a16%28`}, 1055 | "Forwarded": []string{`For="::ffff:bc15:0006"`, `host=what;for=6.6.6.6;proto=https`}, 1056 | }, 1057 | }, 1058 | wantErr: true, 1059 | }, 1060 | { 1061 | name: "Error: negative trustedCount", 1062 | args: args{ 1063 | headerName: "X-Forwarded-For", 1064 | trustedCount: -999, 1065 | headers: http.Header{ 1066 | "X-Real-Ip": []string{`1.1.1.1`}, 1067 | "X-Forwarded-For": []string{`2.2.2.2:3384, 3.3.3.3`, `4.4.4.4:39333`}, 1068 | }, 1069 | }, 1070 | wantErr: true, 1071 | }, 1072 | } 1073 | for _, tt := range tests { 1074 | t.Run(tt.name, func(t *testing.T) { 1075 | strat, err := NewRightmostTrustedCountStrategy(tt.args.headerName, tt.args.trustedCount) 1076 | if (err != nil) != tt.wantErr { 1077 | t.Fatalf("NewRightmostTrustedCountStrategy error = %v, wantErr %v", err, tt.wantErr) 1078 | return 1079 | } 1080 | 1081 | if err != nil { 1082 | // We can't continue 1083 | return 1084 | } 1085 | 1086 | got := strat.ClientIP(tt.args.headers, tt.args.remoteAddr) 1087 | if !reflect.DeepEqual(got, tt.want) { 1088 | t.Fatalf("ClientIP = %q, want %q", got, tt.want) 1089 | } 1090 | }) 1091 | } 1092 | } 1093 | 1094 | func TestAddressesAndRangesToIPNets(t *testing.T) { 1095 | tests := []struct { 1096 | name string 1097 | ranges []string 1098 | want []string 1099 | wantErr bool 1100 | }{ 1101 | { 1102 | name: "Empty input", 1103 | ranges: []string{}, 1104 | want: nil, 1105 | }, 1106 | { 1107 | name: "Single IPv4 address", 1108 | ranges: []string{"1.1.1.1"}, 1109 | want: []string{"1.1.1.1/32"}, 1110 | }, 1111 | { 1112 | name: "Single IPv6 address", 1113 | ranges: []string{"2607:f8b0:4004:83f::200e"}, 1114 | want: []string{"2607:f8b0:4004:83f::200e/128"}, 1115 | }, 1116 | { 1117 | name: "Single IPv4 range", 1118 | ranges: []string{"1.1.1.1/16"}, 1119 | want: []string{"1.1.0.0/16"}, 1120 | }, 1121 | { 1122 | name: "Single IPv6 range", 1123 | ranges: []string{"2607:f8b0:4004:83f::200e/48"}, 1124 | want: []string{"2607:f8b0:4004::/48"}, 1125 | }, 1126 | { 1127 | name: "Mixed input", 1128 | ranges: []string{ 1129 | "1.1.1.1", "2607:f8b0:4004:83f::200e", 1130 | "1.1.1.1/32", "2607:f8b0:4004:83f::200e/128", 1131 | "1.1.1.1/16", "2607:f8b0:4004:83f::200e/56", 1132 | "::ffff:188.0.2.128/112", "::ffff:bc15:0006/104", 1133 | "64:ff9b::188.0.2.128/112", 1134 | }, 1135 | want: []string{ 1136 | "1.1.1.1/32", "2607:f8b0:4004:83f::200e/128", 1137 | "1.1.1.1/32", "2607:f8b0:4004:83f::200e/128", 1138 | "1.1.0.0/16", "2607:f8b0:4004:800::/56", 1139 | "188.0.0.0/16", "188.0.0.0/8", 1140 | "64:ff9b::bc00:0/112", 1141 | }, 1142 | }, 1143 | { 1144 | name: "No input", 1145 | ranges: nil, 1146 | want: nil, 1147 | }, 1148 | { 1149 | name: "Error: garbage CIDR", 1150 | ranges: []string{"2607:f8b0:4004:83f::200e/nope"}, 1151 | wantErr: true, 1152 | }, 1153 | { 1154 | name: "Error: CIDR with zone", 1155 | ranges: []string{"fe80::abcd%nope/64"}, 1156 | wantErr: true, 1157 | }, 1158 | { 1159 | name: "Error: garbage IP", 1160 | ranges: []string{"1.1.1.nope"}, 1161 | wantErr: true, 1162 | }, 1163 | { 1164 | name: "Error: empty value", 1165 | ranges: []string{""}, 1166 | wantErr: true, 1167 | }, 1168 | } 1169 | for _, tt := range tests { 1170 | t.Run(tt.name, func(t *testing.T) { 1171 | got, err := AddressesAndRangesToIPNets(tt.ranges...) 1172 | if (err != nil) != tt.wantErr { 1173 | t.Fatalf("AddressesAndRangesToIPNets() error = %v, wantErr %v", err, tt.wantErr) 1174 | return 1175 | } 1176 | 1177 | if err != nil { 1178 | // We can't continue 1179 | return 1180 | } 1181 | 1182 | if len(got) != len(tt.want) { 1183 | t.Fatalf("len mismatch: %d != %d", len(got), len(tt.want)) 1184 | } 1185 | 1186 | for i := 0; i < len(got); i++ { 1187 | if got[i].String() != tt.want[i] { 1188 | t.Fatalf("got does not equal want; %d: %q != %q", i, got[i].String(), tt.want[i]) 1189 | } 1190 | } 1191 | }) 1192 | } 1193 | } 1194 | 1195 | func TestRightmostTrustedRangeStrategy(t *testing.T) { 1196 | // Ensure the strategy interface is implemented 1197 | var _ Strategy = RightmostTrustedRangeStrategy{} 1198 | 1199 | type args struct { 1200 | headerName string 1201 | headers http.Header 1202 | remoteAddr string 1203 | trustedRanges []string 1204 | } 1205 | tests := []struct { 1206 | name string 1207 | args args 1208 | want string 1209 | wantErr bool 1210 | }{ 1211 | { 1212 | name: "No ranges", 1213 | args: args{ 1214 | headerName: "X-Forwarded-For", 1215 | headers: http.Header{ 1216 | "X-Real-Ip": []string{`1.1.1.1`}, 1217 | "X-Forwarded-For": []string{`2.2.2.2:3384, 3.3.3.3`, `4.4.4.4`}, 1218 | }, 1219 | trustedRanges: nil, 1220 | }, 1221 | want: "4.4.4.4", 1222 | }, 1223 | { 1224 | name: "One range", 1225 | args: args{ 1226 | headerName: "X-Forwarded-For", 1227 | headers: http.Header{ 1228 | "X-Real-Ip": []string{`1.1.1.1`}, 1229 | "X-Forwarded-For": []string{`2.2.2.2:3384, 3.3.3.3`, `4.4.4.4`}, 1230 | }, 1231 | trustedRanges: []string{`4.4.4.0/24`}, 1232 | }, 1233 | want: "3.3.3.3", 1234 | }, 1235 | { 1236 | name: "One IP", 1237 | args: args{ 1238 | headerName: "X-Forwarded-For", 1239 | headers: http.Header{ 1240 | "X-Real-Ip": []string{`1.1.1.1`}, 1241 | "X-Forwarded-For": []string{`2.2.2.2:3384, 3.3.3.3`, `4.4.4.4`}, 1242 | }, 1243 | trustedRanges: []string{`4.4.4.4`}, 1244 | }, 1245 | want: "3.3.3.3", 1246 | }, 1247 | { 1248 | name: "Many kinds of ranges", 1249 | args: args{ 1250 | headerName: "Forwarded", 1251 | headers: http.Header{ 1252 | "X-Real-Ip": []string{`1.1.1.1`}, 1253 | "X-Forwarded-For": []string{`2.2.2.2:3384, 3.3.3.3`, `4.4.4.4`}, 1254 | "Forwarded": []string{ 1255 | `For=99.99.99.99, For=4.4.4.8, For="[2607:f8b0:4004:83f::200e]:4747"`, 1256 | `For=2.2.2.2:8883, For=64:ff9b::188.0.2.200, For=3.3.5.5, For=2001:db7::abcd`, 1257 | }, 1258 | }, 1259 | trustedRanges: []string{ 1260 | `2.2.2.2/32`, `2607:f8b0:4004:83f::200e/128`, 1261 | `3.3.0.0/16`, `2001:db7::/64`, 1262 | `::ffff:4.4.4.4/124`, `64:ff9b::188.0.2.128/112`, 1263 | }, 1264 | }, 1265 | want: "99.99.99.99", 1266 | }, 1267 | { 1268 | name: "Cloudflare ranges", 1269 | args: args{ 1270 | headerName: "X-Forwarded-For", 1271 | headers: http.Header{ 1272 | "X-Real-Ip": []string{`1.1.1.1`}, 1273 | "X-Forwarded-For": []string{`2.2.2.2:3384, 3.3.3.3`, `4.4.4.4`, `2400:cb00::1`}, 1274 | }, 1275 | trustedRanges: ranges.Cloudflare, 1276 | }, 1277 | want: "4.4.4.4", 1278 | }, 1279 | { 1280 | name: "Fail: no non-trusted IP", 1281 | args: args{ 1282 | headerName: "X-Forwarded-For", 1283 | headers: http.Header{ 1284 | "X-Real-Ip": []string{`1.1.1.1`}, 1285 | "X-Forwarded-For": []string{`2.2.2.2:3384, 2.2.2.3`, `2.2.2.4`}, 1286 | }, 1287 | trustedRanges: []string{`2.2.2.0/24`}, 1288 | }, 1289 | want: "", 1290 | }, 1291 | { 1292 | name: "Fail: rightmost non-trusted IP invalid", 1293 | args: args{ 1294 | headerName: "X-Forwarded-For", 1295 | headers: http.Header{ 1296 | "X-Real-Ip": []string{`1.1.1.1`}, 1297 | "X-Forwarded-For": []string{`nope, 2.2.2.2:3384, 2.2.2.3`, `2.2.2.4`}, 1298 | }, 1299 | trustedRanges: []string{`2.2.2.0/24`}, 1300 | }, 1301 | want: "", 1302 | }, 1303 | { 1304 | name: "Fail: rightmost non-trusted IP unspecified", 1305 | args: args{ 1306 | headerName: "X-Forwarded-For", 1307 | headers: http.Header{ 1308 | "X-Real-Ip": []string{`1.1.1.1`}, 1309 | "X-Forwarded-For": []string{`::, 2.2.2.2:3384, 2.2.2.3`, `2.2.2.4`}, 1310 | }, 1311 | trustedRanges: []string{`2.2.2.0/24`}, 1312 | }, 1313 | want: "", 1314 | }, 1315 | { 1316 | name: "Fail: no values in header", 1317 | args: args{ 1318 | headerName: "X-Forwarded-For", 1319 | headers: http.Header{ 1320 | "X-Real-Ip": []string{`1.1.1.1`}, 1321 | "X-Forwarded-For": []string{}}, 1322 | trustedRanges: []string{`2.2.2.0/24`}, 1323 | }, 1324 | want: "", 1325 | }, 1326 | { 1327 | name: "Error: empty header nanme", 1328 | args: args{ 1329 | headerName: "", 1330 | headers: http.Header{ 1331 | "X-Real-Ip": []string{`1.1.1.1`}, 1332 | "X-Forwarded-For": []string{`2.2.2.2:3384, 3.3.3.3`, `4.4.4.4`}, 1333 | }, 1334 | trustedRanges: nil, 1335 | }, 1336 | wantErr: true, 1337 | }, 1338 | { 1339 | name: "Error: bad header nanme", 1340 | args: args{ 1341 | headerName: "Not-XFF-Or-Forwarded", 1342 | headers: http.Header{ 1343 | "X-Real-Ip": []string{`1.1.1.1`}, 1344 | "X-Forwarded-For": []string{`2.2.2.2:3384, 3.3.3.3`, `4.4.4.4`}, 1345 | }, 1346 | trustedRanges: nil, 1347 | }, 1348 | wantErr: true, 1349 | }, 1350 | } 1351 | for _, tt := range tests { 1352 | t.Run(tt.name, func(t *testing.T) { 1353 | ranges, err := AddressesAndRangesToIPNets(tt.args.trustedRanges...) 1354 | if err != nil { 1355 | // We're not testing AddressesAndRangesToIPNets here 1356 | t.Fatalf("AddressesAndRangesToIPNets failed") 1357 | } 1358 | 1359 | strat, err := NewRightmostTrustedRangeStrategy(tt.args.headerName, ranges) 1360 | if (err != nil) != tt.wantErr { 1361 | t.Fatalf("NewRightmostTrustedRangeStrategy error = %v, wantErr %v", err, tt.wantErr) 1362 | return 1363 | } 1364 | 1365 | if err != nil { 1366 | // We can't continue 1367 | return 1368 | } 1369 | 1370 | got := strat.ClientIP(tt.args.headers, tt.args.remoteAddr) 1371 | if !reflect.DeepEqual(got, tt.want) { 1372 | t.Fatalf("ClientIP = %q, want %q", got, tt.want) 1373 | } 1374 | }) 1375 | } 1376 | } 1377 | 1378 | func TestChainStrategy(t *testing.T) { 1379 | type args struct { 1380 | strategies []Strategy 1381 | headers http.Header 1382 | remoteAddr string 1383 | } 1384 | tests := []struct { 1385 | name string 1386 | args args 1387 | want string 1388 | }{ 1389 | { 1390 | name: "Single strategy", 1391 | args: args{ 1392 | strategies: []Strategy{RemoteAddrStrategy{}}, 1393 | headers: http.Header{ 1394 | "X-Real-Ip": []string{`1.1.1.1`}, 1395 | "X-Forwarded-For": []string{`2.2.2.2:3384, 3.3.3.3`, `4.4.4.4`}, 1396 | }, 1397 | remoteAddr: `5.5.5.5`, 1398 | }, 1399 | want: "5.5.5.5", 1400 | }, 1401 | { 1402 | name: "Multiple strategies", 1403 | args: args{ 1404 | strategies: []Strategy{ 1405 | Must(NewRightmostNonPrivateStrategy("Forwarded")), 1406 | Must(NewSingleIPHeaderStrategy("true-client-ip")), 1407 | Must(NewSingleIPHeaderStrategy("x-real-ip")), 1408 | RemoteAddrStrategy{}, 1409 | }, 1410 | headers: http.Header{ 1411 | "X-Real-Ip": []string{`1.1.1.1`}, 1412 | "X-Forwarded-For": []string{`2.2.2.2:3384, 3.3.3.3`, `4.4.4.4`}, 1413 | }, 1414 | remoteAddr: `5.5.5.5`, 1415 | }, 1416 | want: "1.1.1.1", 1417 | }, 1418 | { 1419 | name: "Fail: No strategies", 1420 | args: args{ 1421 | strategies: nil, 1422 | headers: http.Header{ 1423 | "X-Real-Ip": []string{`1.1.1.1`}, 1424 | "X-Forwarded-For": []string{`2.2.2.2:3384, 3.3.3.3`, `4.4.4.4`}, 1425 | }, 1426 | remoteAddr: `5.5.5.5`, 1427 | }, 1428 | want: "", 1429 | }, 1430 | { 1431 | name: "Fail: Multiple strategies, all fail", 1432 | args: args{ 1433 | strategies: []Strategy{ 1434 | Must(NewRightmostNonPrivateStrategy("Forwarded")), 1435 | Must(NewSingleIPHeaderStrategy("true-client-ip")), 1436 | Must(NewSingleIPHeaderStrategy("x-real-ip")), 1437 | RemoteAddrStrategy{}, 1438 | }, 1439 | headers: http.Header{ 1440 | "X-Forwarded-For": []string{`2.2.2.2:3384, 3.3.3.3`, `4.4.4.4`}, 1441 | }, 1442 | remoteAddr: "", 1443 | }, 1444 | want: "", 1445 | }, 1446 | } 1447 | for _, tt := range tests { 1448 | t.Run(tt.name, func(t *testing.T) { 1449 | strat := NewChainStrategy(tt.args.strategies...) 1450 | 1451 | got := strat.ClientIP(tt.args.headers, tt.args.remoteAddr) 1452 | if !reflect.DeepEqual(got, tt.want) { 1453 | t.Fatalf("ClientIP = %q, want %q", got, tt.want) 1454 | } 1455 | }) 1456 | } 1457 | } 1458 | 1459 | func TestMust(t *testing.T) { 1460 | // We test the non-panic path elsewhere, but we need to specifically check the panic case 1461 | defer func() { 1462 | if r := recover(); r == nil { 1463 | t.Fatalf("Must() did not panic") 1464 | } 1465 | }() 1466 | 1467 | Must(RemoteAddrStrategy{}, fmt.Errorf("oh no")) 1468 | } 1469 | 1470 | func TestMustParseIPAddr(t *testing.T) { 1471 | // We test the non-panic path elsewhere, but we need to specifically check the panic case 1472 | defer func() { 1473 | if r := recover(); r == nil { 1474 | t.Fatalf("MustParseIPAddr() did not panic") 1475 | } 1476 | }() 1477 | 1478 | MustParseIPAddr("nope") 1479 | } 1480 | 1481 | func TestParseIPAddr(t *testing.T) { 1482 | tests := []struct { 1483 | name string 1484 | ipStr string 1485 | want net.IPAddr 1486 | wantErr bool 1487 | }{ 1488 | { 1489 | name: "Empty zone", 1490 | ipStr: "1.1.1.1%", 1491 | want: net.IPAddr{IP: net.ParseIP("1.1.1.1"), Zone: ""}, 1492 | }, 1493 | { 1494 | name: "No zone", 1495 | ipStr: "1.1.1.1", 1496 | want: net.IPAddr{IP: net.ParseIP("1.1.1.1"), Zone: ""}, 1497 | }, 1498 | { 1499 | name: "With zone", 1500 | ipStr: "fe80::abcd%zone", 1501 | want: net.IPAddr{IP: net.ParseIP("fe80::abcd"), Zone: "zone"}, 1502 | }, 1503 | { 1504 | name: "With zone and port", 1505 | ipStr: "[2607:f8b0:4004:83f::200e%zone]:4484", 1506 | want: net.IPAddr{IP: net.ParseIP("2607:f8b0:4004:83f::200e"), Zone: "zone"}, 1507 | }, 1508 | { 1509 | name: "With port", 1510 | ipStr: "1.1.1.1:48944", 1511 | want: net.IPAddr{IP: net.ParseIP("1.1.1.1"), Zone: ""}, 1512 | }, 1513 | { 1514 | name: "Bad port (is discarded)", 1515 | ipStr: "[fe80::abcd%eth0]:xyz", 1516 | want: net.IPAddr{IP: net.ParseIP("fe80::abcd"), Zone: "eth0"}, 1517 | }, 1518 | { 1519 | name: "Zero address", 1520 | ipStr: "0.0.0.0", 1521 | want: net.IPAddr{IP: net.ParseIP("0.0.0.0"), Zone: ""}, 1522 | }, 1523 | { 1524 | name: "Unspecified address", 1525 | ipStr: "::", 1526 | want: net.IPAddr{IP: net.ParseIP("::"), Zone: ""}, 1527 | }, 1528 | { 1529 | name: "Error: bad IP with zone", 1530 | ipStr: "nope%zone", 1531 | wantErr: true, 1532 | }, 1533 | { 1534 | name: "Error: bad IP", 1535 | ipStr: "nope!!", 1536 | wantErr: true, 1537 | }, 1538 | } 1539 | for _, tt := range tests { 1540 | t.Run(tt.name, func(t *testing.T) { 1541 | got, err := ParseIPAddr(tt.ipStr) 1542 | if (err != nil) != tt.wantErr { 1543 | t.Fatalf("ParseIPAddr() error = %v, wantErr %v, got = %v", err, tt.wantErr, got) 1544 | return 1545 | } 1546 | 1547 | if !ipAddrsEqual(got, tt.want) { 1548 | t.Fatalf("ParseIPAddr() = %v, want %v", got, tt.want) 1549 | } 1550 | }) 1551 | } 1552 | } 1553 | 1554 | func Test_goodIPAddr(t *testing.T) { 1555 | // This is mostly a copy of TestParseIPAddr, except that zero and unspecified addresses are disallowed 1556 | tests := []struct { 1557 | name string 1558 | ipStr string 1559 | want *net.IPAddr 1560 | }{ 1561 | { 1562 | name: "Empty zone", 1563 | ipStr: "1.1.1.1%", 1564 | want: &net.IPAddr{IP: net.ParseIP("1.1.1.1"), Zone: ""}, 1565 | }, 1566 | { 1567 | name: "No zone", 1568 | ipStr: "1.1.1.1", 1569 | want: &net.IPAddr{IP: net.ParseIP("1.1.1.1"), Zone: ""}, 1570 | }, 1571 | { 1572 | name: "With zone", 1573 | ipStr: "fe80::abcd%zone", 1574 | want: &net.IPAddr{IP: net.ParseIP("fe80::abcd"), Zone: "zone"}, 1575 | }, 1576 | { 1577 | name: "With zone and port", 1578 | ipStr: "[2607:f8b0:4004:83f::200e%zone]:4484", 1579 | want: &net.IPAddr{IP: net.ParseIP("2607:f8b0:4004:83f::200e"), Zone: "zone"}, 1580 | }, 1581 | { 1582 | name: "With port", 1583 | ipStr: "1.1.1.1:48944", 1584 | want: &net.IPAddr{IP: net.ParseIP("1.1.1.1"), Zone: ""}, 1585 | }, 1586 | { 1587 | name: "Bad port (is discarded)", 1588 | ipStr: "[fe80::abcd%eth0]:xyz", 1589 | want: &net.IPAddr{IP: net.ParseIP("fe80::abcd"), Zone: "eth0"}, 1590 | }, 1591 | { 1592 | name: "Error: Zero address", 1593 | ipStr: "0.0.0.0", 1594 | want: nil, 1595 | }, 1596 | { 1597 | name: "Error: Unspecified address", 1598 | ipStr: "::", 1599 | want: nil, 1600 | }, 1601 | { 1602 | name: "Error: bad IP with zone", 1603 | ipStr: "nope%zone", 1604 | want: nil, 1605 | }, 1606 | { 1607 | name: "Error: bad IP", 1608 | ipStr: "nope!!", 1609 | want: nil, 1610 | }, 1611 | } 1612 | for _, tt := range tests { 1613 | t.Run(tt.name, func(t *testing.T) { 1614 | got := goodIPAddr(tt.ipStr) 1615 | 1616 | if got == nil || tt.want == nil { 1617 | if got != tt.want { 1618 | t.Fatalf("ParseIPAddr() = %v, want %v", got, tt.want) 1619 | } 1620 | return 1621 | } 1622 | 1623 | if !ipAddrsEqual(*got, *tt.want) { 1624 | t.Fatalf("ParseIPAddr() = %v, want %v", *got, *tt.want) 1625 | } 1626 | }) 1627 | } 1628 | } 1629 | 1630 | func Test_isPrivateOrLocal(t *testing.T) { 1631 | tests := []struct { 1632 | name string 1633 | ip string 1634 | want bool 1635 | }{ 1636 | { 1637 | name: "IPv4 loopback", 1638 | ip: `127.0.0.2`, 1639 | want: true, 1640 | }, 1641 | { 1642 | name: "IPv6 loopback", 1643 | ip: `::1`, 1644 | want: true, 1645 | }, 1646 | { 1647 | name: "IPv4 10.*", 1648 | ip: `10.0.0.1`, 1649 | want: true, 1650 | }, 1651 | { 1652 | name: "IPv4 192.168.*", 1653 | ip: `192.168.1.1`, 1654 | want: true, 1655 | }, 1656 | { 1657 | name: "IPv6 unique local address", 1658 | ip: `fd12:3456:789a:1::1`, 1659 | want: true, 1660 | }, 1661 | { 1662 | name: "IPv4 link-local", 1663 | ip: `169.254.1.1`, 1664 | want: true, 1665 | }, 1666 | { 1667 | name: "IPv6 link-local", 1668 | ip: `fe80::abcd`, 1669 | want: true, 1670 | }, 1671 | { 1672 | name: "Non-local IPv4", 1673 | ip: `1.1.1.1`, 1674 | want: false, 1675 | }, 1676 | { 1677 | name: "Non-local IPv4-mapped IPv6", 1678 | ip: `::ffff:188.0.2.128`, 1679 | want: false, 1680 | }, 1681 | } 1682 | for _, tt := range tests { 1683 | t.Run(tt.name, func(t *testing.T) { 1684 | ip := net.ParseIP(tt.ip) 1685 | if ip == nil { 1686 | t.Fatalf("net.ParseIP failed; bad test input") 1687 | } 1688 | if got := isPrivateOrLocal(ip); got != tt.want { 1689 | t.Fatalf("isPrivateOrLocal() = %v, want %v", got, tt.want) 1690 | } 1691 | }) 1692 | } 1693 | } 1694 | 1695 | func Test_mustParseCIDR(t *testing.T) { 1696 | // We test the non-panic path elsewhere, but we need to specifically check the panic case 1697 | defer func() { 1698 | if r := recover(); r == nil { 1699 | t.Fatalf("mustParseCIDR() did not panic") 1700 | } 1701 | }() 1702 | 1703 | mustParseCIDR("nope") 1704 | } 1705 | 1706 | func Test_trimMatchedEnds(t *testing.T) { 1707 | // We test the non-panic paths elsewhere, but we need to specifically check the panic case 1708 | defer func() { 1709 | if r := recover(); r == nil { 1710 | t.Fatalf("trimMatchedEnds() did not panic") 1711 | } 1712 | }() 1713 | 1714 | trimMatchedEnds("nope", "abcd") 1715 | } 1716 | 1717 | func Test_parseForwardedListItem(t *testing.T) { 1718 | mustParseIPAddrPtr := func(ipStr string) *net.IPAddr { 1719 | res := MustParseIPAddr(ipStr) 1720 | return &res 1721 | } 1722 | 1723 | tests := []struct { 1724 | name string 1725 | fwd string 1726 | want *net.IPAddr 1727 | }{ 1728 | { 1729 | // This is the correct form for IPv6 wit port 1730 | name: "IPv6 with port and quotes", 1731 | fwd: `For="[2607:f8b0:4004:83f::200e]:4711"`, 1732 | want: mustParseIPAddrPtr("2607:f8b0:4004:83f::200e"), 1733 | }, 1734 | { 1735 | // This is the correct form for IP with no port 1736 | name: "IPv6 with quotes, brackets and no port", 1737 | fwd: `fOR="[2607:f8b0:4004:83f::200e]"`, 1738 | want: mustParseIPAddrPtr("2607:f8b0:4004:83f::200e"), 1739 | }, 1740 | { 1741 | // RFC deviation: missing brackets 1742 | name: "IPv6 with quotes, no brackets, and no port", 1743 | fwd: `for="2607:f8b0:4004:83f::200e"`, 1744 | want: mustParseIPAddrPtr("2607:f8b0:4004:83f::200e"), 1745 | }, 1746 | { 1747 | // RFC deviation: missing quotes 1748 | name: "IPv6 with brackets, no quotes, and no port", 1749 | fwd: `FOR=[2607:f8b0:4004:83f::200e]`, 1750 | want: mustParseIPAddrPtr("2607:f8b0:4004:83f::200e"), 1751 | }, 1752 | { 1753 | // RFC deviation: missing quotes 1754 | name: "IPv6 with port and no quotes", 1755 | fwd: `For=[2607:f8b0:4004:83f::200e]:4711`, 1756 | want: mustParseIPAddrPtr("2607:f8b0:4004:83f::200e"), 1757 | }, 1758 | { 1759 | name: "IPv6 with port, quotes, and zone", 1760 | fwd: `For="[fe80::abcd%zone]:4711"`, 1761 | want: mustParseIPAddrPtr("fe80::abcd%zone"), 1762 | }, 1763 | { 1764 | // RFC deviation: missing brackets 1765 | name: "IPv6 with zone, no quotes, no port", 1766 | fwd: `For="fe80::abcd%zone"`, 1767 | want: mustParseIPAddrPtr("fe80::abcd%zone"), 1768 | }, 1769 | { 1770 | // RFC deviation: missing quotes 1771 | name: "IPv4 with port", 1772 | fwd: `FoR=192.0.2.60:4711`, 1773 | want: mustParseIPAddrPtr("192.0.2.60"), 1774 | }, 1775 | { 1776 | name: "IPv4 with no port", 1777 | fwd: `for=192.0.2.60`, 1778 | want: mustParseIPAddrPtr("192.0.2.60"), 1779 | }, 1780 | { 1781 | name: "IPv4 with quotes", 1782 | fwd: `for="192.0.2.60"`, 1783 | want: mustParseIPAddrPtr("192.0.2.60"), 1784 | }, 1785 | { 1786 | name: "IPv4 with port and quotes", 1787 | fwd: `for="192.0.2.60:4823"`, 1788 | want: mustParseIPAddrPtr("192.0.2.60"), 1789 | }, 1790 | { 1791 | name: "Error: invalid IPv4", 1792 | fwd: `for=192.0.2.999`, 1793 | want: nil, 1794 | }, 1795 | { 1796 | name: "Error: invalid IPv6", 1797 | fwd: `for="2607:f8b0:4004:83f::999999"`, 1798 | want: nil, 1799 | }, 1800 | { 1801 | name: "Error: non-IP identifier", 1802 | fwd: `for="_test"`, 1803 | want: nil, 1804 | }, 1805 | { 1806 | name: "Error: empty IP value", 1807 | fwd: `for=`, 1808 | want: nil, 1809 | }, 1810 | { 1811 | name: "Multiple IPv4 directives", 1812 | fwd: `by=1.1.1.1; for=2.2.2.2;host=myhost; proto=https`, 1813 | want: mustParseIPAddrPtr("2.2.2.2"), 1814 | }, 1815 | { 1816 | // RFC deviation: missing quotes around IPv6 1817 | name: "Multiple IPv6 directives", 1818 | fwd: `by=1::1;host=myhost;for=2::2;proto=https`, 1819 | want: mustParseIPAddrPtr("2::2"), 1820 | }, 1821 | { 1822 | // RFC deviation: missing quotes around IPv6 1823 | name: "Multiple mixed directives", 1824 | fwd: `by=1::1;host=myhost;proto=https;for=2.2.2.2`, 1825 | want: mustParseIPAddrPtr("2.2.2.2"), 1826 | }, 1827 | { 1828 | name: "IPv4-mapped IPv6", 1829 | fwd: `for="[::ffff:188.0.2.128]"`, 1830 | want: mustParseIPAddrPtr("188.0.2.128"), 1831 | }, 1832 | { 1833 | name: "IPv4-mapped IPv6 with port and quotes", 1834 | fwd: `for="[::ffff:188.0.2.128]:49428"`, 1835 | want: mustParseIPAddrPtr("188.0.2.128"), 1836 | }, 1837 | { 1838 | name: "IPv4-mapped IPv6 in IPv6 form", 1839 | fwd: `for="[0:0:0:0:0:ffff:bc15:0006]"`, 1840 | want: mustParseIPAddrPtr("188.21.0.6"), 1841 | }, 1842 | { 1843 | name: "NAT64 IPv4-mapped IPv6", 1844 | fwd: `for="[64:ff9b::188.0.2.128]"`, 1845 | want: mustParseIPAddrPtr("64:ff9b::188.0.2.128"), 1846 | }, 1847 | { 1848 | name: "IPv4 loopback", 1849 | fwd: `for=127.0.0.1`, 1850 | want: mustParseIPAddrPtr("127.0.0.1"), 1851 | }, 1852 | { 1853 | name: "IPv6 loopback", 1854 | fwd: `for="[::1]"`, 1855 | want: mustParseIPAddrPtr("::1"), 1856 | }, 1857 | { 1858 | // RFC deviation: quotes must be matched 1859 | name: "Error: Unmatched quote", 1860 | fwd: `for="1.1.1.1`, 1861 | want: nil, 1862 | }, 1863 | { 1864 | // RFC deviation: brackets must be matched 1865 | name: "Error: IPv6 loopback", 1866 | fwd: `for="::1]"`, 1867 | want: nil, 1868 | }, 1869 | { 1870 | name: "Error: misplaced quote", 1871 | fwd: `for="[0:0:0:0:0:ffff:bc15:0006"]`, 1872 | want: nil, 1873 | }, 1874 | { 1875 | name: "Error: garbage", 1876 | fwd: "ads\x00jkl&#*(383fdljk", 1877 | want: nil, 1878 | }, 1879 | { 1880 | // Per RFC 7230 section 3.2.6, this should not be an error, but we don't have 1881 | // full syntax support yet. 1882 | name: "RFC deviation: quoted pair", 1883 | fwd: `for=1.1.1.\1`, 1884 | want: nil, 1885 | }, 1886 | { 1887 | // Per RFC 7239, this extraneous whitespace should be an error, but we don't 1888 | // have full syntax support yet. 1889 | name: "RFC deviation: Incorrect whitespace", 1890 | fwd: `for= 1.1.1.1`, 1891 | want: mustParseIPAddrPtr("1.1.1.1"), 1892 | }, 1893 | } 1894 | for _, tt := range tests { 1895 | t.Run(tt.name, func(t *testing.T) { 1896 | got := parseForwardedListItem(tt.fwd) 1897 | 1898 | if got == nil || tt.want == nil { 1899 | if got != tt.want { 1900 | t.Fatalf("parseForwardedListItem() = %v, want %v", got, tt.want) 1901 | } 1902 | return 1903 | } 1904 | 1905 | if !ipAddrsEqual(*got, *tt.want) { 1906 | t.Fatalf("parseForwardedListItem() = %v, want %v", got, tt.want) 1907 | } 1908 | }) 1909 | } 1910 | } 1911 | 1912 | // Demonstrate parsing deviations from Forwarded header syntax RFCs, particularly 1913 | // RFC 7239 (Forwarded header) and RFC 7230 (HTTP/1.1 syntax) section 3.2.6. 1914 | func Test_forwardedHeaderRFCDeviations(t *testing.T) { 1915 | mustParseIPAddrPtr := func(s string) *net.IPAddr { 1916 | res := MustParseIPAddr(s) 1917 | return &res 1918 | } 1919 | 1920 | type args struct { 1921 | headers http.Header 1922 | headerName string 1923 | } 1924 | tests := []struct { 1925 | name string 1926 | args args 1927 | want []*net.IPAddr 1928 | }{ 1929 | { 1930 | // The value in quotes should be a single value but we split by comma, so it's not. 1931 | // The first and third "For=" bits have one double-quote in them, so they are 1932 | // considered invalid by our parser. The second is still in the quoted-string, 1933 | // but doesn't have any quotes in it, so it parses okay. 1934 | name: "Comma in quotes", 1935 | args: args{ 1936 | headers: http.Header{"Forwarded": []string{`For="1.1.1.1, For=2.2.2.2, For=3.3.3.3", For="4.4.4.4"`}}, 1937 | headerName: "Forwarded", 1938 | }, 1939 | // There are really only two values, so we actually want: {nil, "4.4.4.4"} 1940 | want: []*net.IPAddr{nil, mustParseIPAddrPtr("2.2.2.2"), nil, mustParseIPAddrPtr("4.4.4.4")}, 1941 | }, 1942 | { 1943 | // Per 7239, the opening unmatched quote makes the whole rest of the header invalid. 1944 | // But that would mean that an attacker can invalidate the whole header with a 1945 | // quote character early on, even the trusted IPs added by our reverse proxies. 1946 | // Our actual behaviour is probably the best approach. 1947 | name: "Unmatched quote", 1948 | args: args{ 1949 | headers: http.Header{"Forwarded": []string{`For="1.1.1.1, For=2.2.2.2`}}, 1950 | headerName: "Forwarded", 1951 | }, 1952 | // There are really only two values, so the RFC would require: {nil} (or empty slice?) 1953 | want: []*net.IPAddr{nil, mustParseIPAddrPtr("2.2.2.2")}, 1954 | }, 1955 | { 1956 | // The invalid non-For parameter should invalidate the whole item, but we're 1957 | // not checking anything but the "For=" part. 1958 | name: "Invalid characters", 1959 | args: args{ 1960 | headers: http.Header{"Forwarded": []string{`For=1.1.1.1;@!=😀, For=2.2.2.2`}}, 1961 | headerName: "Forwarded", 1962 | }, 1963 | // Only the last value is valid, so it should be: {nil, "2.2.2.2"} 1964 | want: []*net.IPAddr{mustParseIPAddrPtr("1.1.1.1"), mustParseIPAddrPtr("2.2.2.2")}, 1965 | }, 1966 | { 1967 | // The duplicate "For=" parameter should invalidate the whole item but we don't check for it 1968 | name: "Duplicate token", 1969 | args: args{ 1970 | headers: http.Header{"Forwarded": []string{`For=1.1.1.1;For=2.2.2.2, For=3.3.3.3`}}, 1971 | headerName: "Forwarded", 1972 | }, 1973 | // Only the last value is valid, so it should be: {nil, "3.3.3.3"} 1974 | want: []*net.IPAddr{mustParseIPAddrPtr("1.1.1.1"), mustParseIPAddrPtr("3.3.3.3")}, 1975 | }, 1976 | { 1977 | // An escaped character in quotes should be unescaped, but we're not doing it. 1978 | // (And if we do end up doing it, make sure that `\\` becomes `\` after escaping. 1979 | // And escaping is only allowed in quoted strings.) 1980 | // There is no good reason for any part of an IP address to be escaped anyway. 1981 | name: "Escaped character", 1982 | args: args{ 1983 | headers: http.Header{"Forwarded": []string{`For="3.3.3.\3"`}}, 1984 | headerName: "Forwarded", 1985 | }, 1986 | // The value is valid, so it should be: {nil, "3.3.3.3"} 1987 | want: []*net.IPAddr{nil}, 1988 | }, 1989 | { 1990 | // Spaces are not allowed around the equal signs, but due to the way we parse 1991 | // a space after the equal will pass but one before won't. 1992 | name: "Equal sign spaces", 1993 | args: args{ 1994 | headers: http.Header{"Forwarded": []string{`For =1.1.1.1, For= 3.3.3.3`}}, 1995 | headerName: "Forwarded", 1996 | }, 1997 | // Neither value is valid, so it should be: {nil, nil} 1998 | want: []*net.IPAddr{nil, mustParseIPAddrPtr("3.3.3.3")}, 1999 | }, 2000 | { 2001 | // Disallowed characters are only allowed in quoted strings. This means 2002 | // that IPv6 addresses must be quoted. 2003 | name: "Disallowed characters in unquoted value (like colons and square brackets", 2004 | args: args{ 2005 | headers: http.Header{"Forwarded": []string{`For=[2607:f8b0:4004:83f::200e]`}}, 2006 | headerName: "Forwarded", 2007 | }, 2008 | // Value is invalid without quotes, so should be {nil} 2009 | want: []*net.IPAddr{mustParseIPAddrPtr("2607:f8b0:4004:83f::200e")}, 2010 | }, 2011 | { 2012 | // IPv6 addresses are required to be contained in square brackets. We don't 2013 | // require this simply to be more flexible in what is accepted. 2014 | name: "IPv6 brackets", 2015 | args: args{ 2016 | headers: http.Header{"Forwarded": []string{`For="2607:f8b0:4004:83f::200e"`}}, 2017 | headerName: "Forwarded", 2018 | }, 2019 | // IPv6 is invalid without brackets, so should be {nil} 2020 | want: []*net.IPAddr{mustParseIPAddrPtr("2607:f8b0:4004:83f::200e")}, 2021 | }, 2022 | { 2023 | // IPv4 addresses are _not_ supposed to be in square brackets, but we trim 2024 | // them unconditionally. 2025 | name: "IPv4 brackets", 2026 | args: args{ 2027 | headers: http.Header{"Forwarded": []string{`For="[1.1.1.1]"`}}, 2028 | headerName: "Forwarded", 2029 | }, 2030 | // IPv4 is invalid with brackets, so should be {nil} 2031 | want: []*net.IPAddr{mustParseIPAddrPtr("1.1.1.1")}, 2032 | }, 2033 | } 2034 | 2035 | for _, tt := range tests { 2036 | t.Run(tt.name, func(t *testing.T) { 2037 | if got := getIPAddrList(tt.args.headers, tt.args.headerName); !reflect.DeepEqual(got, tt.want) { 2038 | t.Errorf("getIPAddrList() = %v, want %v", got, tt.want) 2039 | } 2040 | }) 2041 | } 2042 | } 2043 | --------------------------------------------------------------------------------