├── .github └── workflows │ └── go.yml ├── LICENSE ├── README.md ├── go.mod ├── go.sum ├── realip.go └── realip_test.go /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request] 2 | name: test and build 3 | jobs: 4 | lint: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - name: Install Go 8 | uses: actions/setup-go@v1 9 | with: 10 | go-version: 1.12.x 11 | - name: Checkout code 12 | uses: actions/checkout@v1 13 | - name: Install golangci-lint 14 | run: | 15 | go get github.com/golangci/golangci-lint/cmd/golangci-lint 16 | - name: Run linters 17 | run: | 18 | export PATH=$PATH:$(go env GOPATH)/bin 19 | golangci-lint -E bodyclose,misspell,gocyclo,dupl,gofmt,golint,unconvert,goimports,depguard,gocritic,funlen,interfacer run 20 | 21 | test: 22 | runs-on: ubuntu-latest 23 | steps: 24 | - name: Install Go 25 | uses: actions/setup-go@v1 26 | with: 27 | go-version: 1.12.x 28 | - name: Checkout code 29 | uses: actions/checkout@v1 30 | - name: Run tests 31 | run: go test -v -covermode=count 32 | 33 | build: 34 | runs-on: ubuntu-latest 35 | needs: [lint, test] 36 | steps: 37 | - name: Install Go 38 | uses: actions/setup-go@v1 39 | with: 40 | go-version: 1.12.x 41 | - name: Checkout code 42 | uses: actions/checkout@v1 43 | - name: build 44 | run: | 45 | export GO111MODULE=on 46 | GOOS=linux GOARCH=amd64 go build -o bin/ci-test-linux-amd64 47 | - name: upload artifacts 48 | uses: actions/upload-artifact@master 49 | with: 50 | name: binaries 51 | path: bin/ 52 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 SHEN SHENG 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FastHTTP - RealIP 2 | 3 | [![GoDoc](https://godoc.org/github.com/ferluci/fast-realip?status.svg)](https://godoc.org/github.com/ferluci/fast-realip) 4 | 5 | Go package that can be used to get client's real public IP from [Fast HTTP](https://github.com/valyala/fasthttp) request, which usually useful for logging HTTP server. 6 | 7 | This is fork from [realip](https://github.com/tomasen/realip) for [Fast HTTP](https://github.com/valyala/fasthttp) with some imporvements. 8 | ### Feature 9 | 10 | * Follows the rule of X-Real-IP 11 | * Follows the rule of X-Forwarded-For 12 | * Exclude local or private address 13 | 14 | 15 | ## How It Works 16 | 17 | It looks for specific headers in the request and falls back to some defaults if they do not exist. 18 | 19 | The user ip is determined by the following order: 20 | 21 | 1. `X-Client-IP` 22 | 2. `X-Original-Forwarded-For` 23 | 3. `X-Forwarded-For` (Header may return multiple IP addresses in the format: "client IP, proxy 1 IP, proxy 2 IP", so we take the the first one.) 24 | 4. `CF-Connecting-IP` (Cloudflare) 25 | 5. `Fastly-Client-Ip` (Fastly CDN and Firebase hosting header when forwared to a cloud function) 26 | 6. `True-Client-Ip` (Akamai and Cloudflare) 27 | 7. `X-Real-IP` (Nginx proxy/FastCGI) 28 | 8. `X-Forwarded`, `Forwarded-For` and `Forwarded` (Variations of #2) 29 | 9. `ctx.RemoteAddr().String()` 30 | 31 | ## Install 32 | ```go 33 | go get -u github.com/valyala/fasthttp 34 | ``` 35 | ## Example 36 | 37 | ```go 38 | package main 39 | 40 | import ( 41 | "log" 42 | "github.com/valyala/fasthttp" 43 | "github.com/ferluci/fast-realip" 44 | ) 45 | 46 | func main() { 47 | if err := fasthttp.ListenAndServe(":8080", realipHandler); err != nil { 48 | log.Fatalf("Error in ListenAndServe: %s", err) 49 | } 50 | } 51 | 52 | func realipHandler(ctx *fasthttp.RequestCtx) { 53 | clientIP := realip.FromRequest(ctx) 54 | log.Println("GET / from", clientIP) 55 | } 56 | 57 | 58 | ``` 59 | 60 | ## Developing 61 | 62 | Commited code must pass: 63 | 64 | * [golint](https://github.com/golang/lint) 65 | * [go vet](https://godoc.org/golang.org/x/tools/cmd/vet) 66 | * [gofmt](https://golang.org/cmd/gofmt) 67 | * [go test](https://golang.org/cmd/go/#hdr-Test_packages): 68 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/ferluci/fast-realip 2 | 3 | go 1.12 4 | 5 | require github.com/valyala/fasthttp v1.9.0 6 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/klauspost/compress v1.8.2 h1:Bx0qjetmNjdFXASH02NSAREKpiaDwkO1DRZ3dV2KCcs= 2 | github.com/klauspost/compress v1.8.2/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= 3 | github.com/klauspost/cpuid v1.2.1 h1:vJi+O/nMdFt0vqm8NZBI6wzALWdA2X+egi0ogNyrC/w= 4 | github.com/klauspost/cpuid v1.2.1/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= 5 | github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= 6 | github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= 7 | github.com/valyala/fasthttp v1.9.0 h1:hNpmUdy/+ZXYpGy0OBfm7K0UQTzb73W0T0U4iJIVrMw= 8 | github.com/valyala/fasthttp v1.9.0/go.mod h1:FstJa9V+Pj9vQ7OJie2qMHdwemEDaDiSdBnvPM1Su9w= 9 | github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio= 10 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 11 | golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 12 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 13 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 14 | -------------------------------------------------------------------------------- /realip.go: -------------------------------------------------------------------------------- 1 | package realip 2 | 3 | import ( 4 | "errors" 5 | "net" 6 | "net/http" 7 | "strings" 8 | 9 | "github.com/valyala/fasthttp" 10 | ) 11 | 12 | // Should use canonical format of the header key s 13 | // https://golang.org/pkg/net/http/#CanonicalHeaderKey 14 | 15 | // Header may return multiple IP addresses in the format: "client IP, proxy 1 IP, proxy 2 IP", so we take the the first one. 16 | var xOriginalForwardedForHeader = http.CanonicalHeaderKey("X-Original-Forwarded-For") 17 | var xForwardedForHeader = http.CanonicalHeaderKey("X-Forwarded-For") 18 | var xForwardedHeader = http.CanonicalHeaderKey("X-Forwarded") 19 | var forwardedForHeader = http.CanonicalHeaderKey("Forwarded-For") 20 | var forwardedHeader = http.CanonicalHeaderKey("Forwarded") 21 | 22 | // Standard headers used by Amazon EC2, Heroku, and others 23 | var xClientIPHeader = http.CanonicalHeaderKey("X-Client-IP") 24 | 25 | // Nginx proxy/FastCGI 26 | var xRealIPHeader = http.CanonicalHeaderKey("X-Real-IP") 27 | 28 | // Cloudflare. 29 | // @see https://support.cloudflare.com/hc/en-us/articles/200170986-How-does-Cloudflare-handle-HTTP-Request-headers- 30 | // CF-Connecting-IP - applied to every request to the origin. 31 | var cfConnectingIPHeader = http.CanonicalHeaderKey("CF-Connecting-IP") 32 | 33 | // Fastly CDN and Firebase hosting header when forwared to a cloud function 34 | var fastlyClientIPHeader = http.CanonicalHeaderKey("Fastly-Client-Ip") 35 | 36 | // Akamai and Cloudflare 37 | var trueClientIPHeader = http.CanonicalHeaderKey("True-Client-Ip") 38 | 39 | var cidrs []*net.IPNet 40 | 41 | func init() { 42 | maxCidrBlocks := []string{ 43 | "127.0.0.1/8", // localhost 44 | "10.0.0.0/8", // 24-bit block 45 | "172.16.0.0/12", // 20-bit block 46 | "192.168.0.0/16", // 16-bit block 47 | "169.254.0.0/16", // link local address 48 | "::1/128", // localhost IPv6 49 | "fc00::/7", // unique local address IPv6 50 | "fe80::/10", // link local address IPv6 51 | } 52 | 53 | cidrs = make([]*net.IPNet, len(maxCidrBlocks)) 54 | for i, maxCidrBlock := range maxCidrBlocks { 55 | _, cidr, _ := net.ParseCIDR(maxCidrBlock) 56 | cidrs[i] = cidr 57 | } 58 | } 59 | 60 | // isLocalAddress works by checking if the address is under private CIDR blocks. 61 | // List of private CIDR blocks can be seen on : 62 | // 63 | // https://en.wikipedia.org/wiki/Private_network 64 | // 65 | // https://en.wikipedia.org/wiki/Link-local_address 66 | func isPrivateAddress(address string) (bool, error) { 67 | ipAddress := net.ParseIP(address) 68 | if ipAddress == nil { 69 | return false, errors.New("address is not valid") 70 | } 71 | 72 | for i := range cidrs { 73 | if cidrs[i].Contains(ipAddress) { 74 | return true, nil 75 | } 76 | } 77 | 78 | return false, nil 79 | } 80 | 81 | // FromRequest returns client's real public IP address from http request headers. 82 | func FromRequest(ctx *fasthttp.RequestCtx) string { 83 | xClientIP := ctx.Request.Header.Peek(xClientIPHeader) 84 | if xClientIP != nil { 85 | return string(xClientIP) 86 | } 87 | 88 | xOriginalForwardedFor := ctx.Request.Header.Peek(xOriginalForwardedForHeader) 89 | if xOriginalForwardedFor != nil { 90 | requestIP, err := retrieveForwardedIP(string(xOriginalForwardedFor)) 91 | if err == nil { 92 | return requestIP 93 | } 94 | } 95 | 96 | xForwardedFor := ctx.Request.Header.Peek(xForwardedForHeader) 97 | if xForwardedFor != nil { 98 | requestIP, err := retrieveForwardedIP(string(xForwardedFor)) 99 | if err == nil { 100 | return requestIP 101 | } 102 | } 103 | 104 | if ip, err := fromSpecialHeaders(ctx); err == nil { 105 | return ip 106 | } 107 | 108 | if ip, err := fromForwardedHeaders(ctx); err == nil { 109 | return ip 110 | } 111 | 112 | var remoteIP string 113 | remoteAddr := ctx.RemoteAddr().String() 114 | 115 | if strings.ContainsRune(remoteAddr, ':') { 116 | remoteIP, _, _ = net.SplitHostPort(remoteAddr) 117 | } else { 118 | remoteIP = remoteAddr 119 | } 120 | return remoteIP 121 | } 122 | 123 | func fromSpecialHeaders(ctx *fasthttp.RequestCtx) (string, error) { 124 | ipHeaders := [...]string{cfConnectingIPHeader, fastlyClientIPHeader, trueClientIPHeader, xRealIPHeader} 125 | for _, iplHeader := range ipHeaders { 126 | if clientIP := ctx.Request.Header.Peek(iplHeader); clientIP != nil { 127 | return string(clientIP), nil 128 | } 129 | } 130 | return "", errors.New("can't get ip from special headers") 131 | } 132 | 133 | func fromForwardedHeaders(ctx *fasthttp.RequestCtx) (string, error) { 134 | forwardedHeaders := [...]string{xForwardedHeader, forwardedForHeader, forwardedHeader} 135 | for _, forwardedHeader := range forwardedHeaders { 136 | if forwarded := ctx.Request.Header.Peek(forwardedHeader); forwarded != nil { 137 | if clientIP, err := retrieveForwardedIP(string(forwarded)); err == nil { 138 | return clientIP, nil 139 | } 140 | } 141 | } 142 | return "", errors.New("can't get ip from forwarded headers") 143 | } 144 | 145 | func retrieveForwardedIP(forwardedHeader string) (string, error) { 146 | for _, address := range strings.Split(forwardedHeader, ",") { 147 | if len(address) > 0 { 148 | address = strings.TrimSpace(address) 149 | isPrivate, err := isPrivateAddress(address) 150 | switch { 151 | case !isPrivate && err == nil: 152 | return address, nil 153 | case isPrivate && err == nil: 154 | return "", errors.New("forwarded ip is private") 155 | default: 156 | return "", err 157 | } 158 | } 159 | } 160 | return "", errors.New("empty or invalid forwarded header") 161 | } 162 | -------------------------------------------------------------------------------- /realip_test.go: -------------------------------------------------------------------------------- 1 | package realip 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "testing" 7 | 8 | "github.com/valyala/fasthttp" 9 | ) 10 | 11 | func TestIsPrivateAddr(t *testing.T) { 12 | testData := map[string]bool{ 13 | "127.0.0.0": true, 14 | "10.0.0.0": true, 15 | "169.254.0.0": true, 16 | "192.168.0.0": true, 17 | "::1": true, 18 | "fc00::": true, 19 | 20 | "172.15.0.0": false, 21 | "172.16.0.0": true, 22 | "172.31.0.0": true, 23 | "172.32.0.0": false, 24 | 25 | "147.12.56.11": false, 26 | } 27 | 28 | for addr, isLocal := range testData { 29 | isPrivate, err := isPrivateAddress(addr) 30 | if err != nil { 31 | t.Errorf("fail processing %s: %v", addr, err) 32 | } 33 | 34 | if isPrivate != isLocal { 35 | format := "%s should " 36 | if !isLocal { 37 | format += "not " 38 | } 39 | format += "be local address" 40 | 41 | t.Errorf(format, addr) 42 | } 43 | } 44 | } 45 | 46 | type testIP struct { 47 | name string 48 | request *fasthttp.RequestCtx 49 | expected string 50 | } 51 | 52 | func TestRealIP(t *testing.T) { 53 | newRequest := func(remoteAddr string, headers map[string]string) *fasthttp.RequestCtx { 54 | var ctx fasthttp.RequestCtx 55 | addr := &net.TCPAddr{ 56 | IP: net.ParseIP(remoteAddr), 57 | } 58 | ctx.Init(&ctx.Request, addr, nil) 59 | 60 | for header, value := range headers { 61 | ctx.Request.Header.Set(header, value) 62 | } 63 | 64 | return &ctx 65 | } 66 | 67 | testData := []testIP{ 68 | { 69 | name: "No header", 70 | request: newRequest("144.12.54.87", map[string]string{}), 71 | expected: "144.12.54.87", 72 | }, 73 | { 74 | name: "Has X-Forwarded-For", 75 | request: newRequest("", map[string]string{"X-Forwarded-For": "144.12.54.87"}), 76 | expected: "144.12.54.87", 77 | }, 78 | { 79 | name: "Has multiple X-Forwarded-For", 80 | request: newRequest("", map[string]string{ 81 | "X-Forwarded-For": fmt.Sprintf("%s,%s,%s", "119.14.55.11", "144.12.54.87", "127.0.0.0"), 82 | }), 83 | expected: "119.14.55.11", 84 | }, 85 | { 86 | name: "Has X-Real-IP", 87 | request: newRequest("", map[string]string{"X-Real-IP": "144.12.54.87"}), 88 | expected: "144.12.54.87", 89 | }, 90 | { 91 | name: "Has multiple X-Forwarded-For and X-Real-IP", 92 | request: newRequest("", map[string]string{ 93 | "X-Real-IP": "119.14.55.11", 94 | "X-Forwarded-For": fmt.Sprintf("%s,%s", "144.12.54.87", "127.0.0.0"), 95 | }), 96 | expected: "144.12.54.87", 97 | }, 98 | } 99 | 100 | // Run test 101 | for _, v := range testData { 102 | if actual := FromRequest(v.request); v.expected != actual { 103 | t.Errorf("%s: expected %s but get %s", v.name, v.expected, actual) 104 | } 105 | } 106 | } 107 | --------------------------------------------------------------------------------