├── .assets └── icon.png ├── .gitignore ├── .traefik.yml ├── README.md ├── get_real_ip.go ├── get_real_ip_test.go └── go.mod /.assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Paxxs/traefik-get-real-ip/e88f9671d837f319f4dcd133a4e8f0af4b5bc5cd/.assets/icon.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /data -------------------------------------------------------------------------------- /.traefik.yml: -------------------------------------------------------------------------------- 1 | displayName: Traefik Get Real IP 2 | type: middleware 3 | import: github.com/Paxxs/traefik-get-real-ip 4 | iconPath: .assets/icon.png 5 | 6 | summary: By retrieving the correct real IP from single or multiple different load balancers(eg.Cloudflare), this plugin effectively prevents IP spoofing. 7 | 8 | testData: 9 | Proxy: 10 | - proxyHeadername: X-From-Cdn 11 | proxyHeadervalue: cdn1 12 | realIP: X-Forwarded-For 13 | - proxyHeadername: X-From-Cdn 14 | proxyHeadervalue: cdn2 15 | realIP: Client-Ip 16 | - proxyHeadername: X-From-Cdn 17 | proxyHeadervalue: cdn3 18 | realIP: Cf-Connecting-Ip 19 | overwriteXFF: true 20 | - proxyHeadername: "*" 21 | realIP: RemoteAddr -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Traefik Get Real IP address 2 | 3 | 4 | 5 | When traefik is deployed behind multiple load balancers, this plugin can be used to detect different load balancers and extract the real IP from different header fields, then output the value to the `x-real-ip` header. 6 | 7 | This plugin can prevent IP spoofing by checking if the values form the received header information of the load balancer match before extracting the IP address. 8 | 9 | For example, in the configuration of `CloudFlare` load balancer shown below, we configure it to only accept the header `x-from-cdn` with a value equal to `cf-foo`, and extract the IP address from the `Cf-Connecting-Ip` header. Since users never know about the existence of the `x-from-cdn` header or its required value `cf-foo`, it remains secure 🛡️. To increase complexity and avoid being guessed, you can use a random string :) 10 | 11 | ``` 12 | CloudFlare 13 | ┌─────────┐ 14 | │ ├────────────────────────────────► ┌───────┬────────┐ 15 | └─────────┘ x-from-cdn:cf-foo │ │ │ 16 | Cf-Connecting-Ip: realip │ │ │ 17 | CDN2 │ │ │ 18 | ┌─────────┐ │ │ paxxs's│ 19 | │ ├────────────────────────────────► │traefik│ │ x-real-ip:realip 20 | └─────────┘ x-from-cdn:mf-bar │ │Get-rea ├─────────────► 21 | Client-iP: realip │ │ l-ip │ 22 | CDN3 │ │Plugin │ 23 | ┌─────────┐ │ │ │ 24 | │ ├───────────────────────────────► │ │ │ 25 | └─────────┘ x-from-cdn:mf-fun └───────┴────────┘ 26 | x-forwarded-for: realip,x.x.x.x 27 | (truthedIP) ▲ ▲ 28 | │ │ 29 | ┌────────┐ │ │ 30 | └────────┘ ────────────────────────────────────┘ │ 31 | "*" │ 32 | ┌────────┐ RemoteAddr/etc.. │ 33 | └────────┘ ───────────────────────────────────────┘ 34 | ``` 35 | 36 | ## CDN Configuration 37 | 38 | E.g. Cloudflare: 39 | 40 | Rules > Transform Rules > HTTP Request Header Modification > Add 41 | - Set static Header: `X-From-Cdn` 42 | - Value: `cf-foo` 43 | 44 | ![image](https://user-images.githubusercontent.com/10364775/164590908-43edab8a-cdc8-4d4c-abd6-542b6c798f3b.png) 45 | 46 | ![image](https://user-images.githubusercontent.com/10364775/164591134-4dd2fc97-cd0e-4deb-8fe3-bcd4555ebbde.png) 47 | 48 | ## Traefik Configuration 49 | ### Static 50 | 51 | Plugin Info: 52 | - moduleName: `github.com/Paxxs/traefik-get-real-ip` 53 | - version: `v1.0.2` 54 | 55 | Traefik Configuration: 56 | - yml 57 | - toml 58 | - docker-labels 59 | 60 | ```yml 61 | pilot: 62 | token: [REDACTED] 63 | 64 | experimental: 65 | plugins: 66 | real-ip: 67 | moduleName: github.com/Paxxs/traefik-get-real-ip 68 | version: [Please fill the latest version !] 69 | ``` 70 | 71 | ### Dynamic 72 | 73 | - yml 74 | - toml 75 | - docker labels 76 | - Kubernetes 77 | 78 | ```yml 79 | http: 80 | middlewares: 81 | real-ip-foo: 82 | plugin: 83 | real-ip: 84 | Proxy: 85 | - proxyHeadername: X-From-Cdn 86 | proxyHeadervalue: mf-fun 87 | realIP: X-Forwarded-For 88 | - proxyHeadername: X-From-Cdn 89 | proxyHeadervalue: mf-bar 90 | realIP: Client-Ip 91 | OverwriteXFF: true # default: false, v1.0.2 or above 92 | - proxyHeadername: X-From-Cdn 93 | proxyHeadervalue: cf-foo 94 | realIP: Cf-Connecting-Ip 95 | OverwriteXFF: true # default: false, v1.0.2 or above 96 | - proxyHeadername: "*" 97 | realIP: RemoteAddr 98 | 99 | routers: 100 | my-router: 101 | rule: Host(`localhost`) 102 | middlewares: 103 | - real-ip-foo 104 | service: my-service 105 | 106 | services: 107 | my-service: 108 | loadBalancer: 109 | servers: 110 | - url: 'http://127.0.0.1' 111 | ``` 112 | -------------------------------------------------------------------------------- /get_real_ip.go: -------------------------------------------------------------------------------- 1 | package traefik_get_real_ip 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net" 7 | "net/http" 8 | "os" 9 | "strings" 10 | ) 11 | 12 | const ( 13 | xRealIP = "X-Real-Ip" 14 | xForwardedFor = "X-Forwarded-For" 15 | ) 16 | 17 | // Proxy 配置文件中的数组结构 18 | type Proxy struct { 19 | ProxyHeadername string `yaml:"proxyHeadername"` 20 | ProxyHeadervalue string `yaml:"proxyHeadervalue"` 21 | RealIP string `yaml:"realIP"` 22 | OverwriteXFF bool `yaml:"overwriteXFF"` // override X-Forwarded-For 23 | } 24 | 25 | // Config the plugin configuration. 26 | type Config struct { 27 | Proxy []Proxy `yaml:"proxy"` 28 | } 29 | 30 | // CreateConfig creates the default plugin configuration. 31 | func CreateConfig() *Config { 32 | return &Config{} 33 | } 34 | 35 | // GetRealIP Define plugin 36 | type GetRealIP struct { 37 | next http.Handler 38 | name string 39 | proxy []Proxy 40 | } 41 | 42 | // New creates and returns a new realip plugin instance. 43 | func New(ctx context.Context, next http.Handler, config *Config, name string) (http.Handler, error) { 44 | log("☃️ Config loaded.(%d) %v", len(config.Proxy), config) 45 | 46 | return &GetRealIP{ 47 | next: next, 48 | name: name, 49 | proxy: config.Proxy, 50 | }, nil 51 | } 52 | 53 | // 真正干事情了 54 | func (g *GetRealIP) ServeHTTP(rw http.ResponseWriter, req *http.Request) { 55 | // fmt.Println("☃️当前配置:", g.proxy, "remoteaddr", req.RemoteAddr) 56 | var realIPStr string 57 | for _, proxy := range g.proxy { 58 | if proxy.ProxyHeadername == "*" || req.Header.Get(proxy.ProxyHeadername) == proxy.ProxyHeadervalue { 59 | log("🐸 Current Proxy:%s(%s)", proxy.ProxyHeadervalue, proxy.ProxyHeadername) 60 | 61 | // CDN来源确定 62 | nIP := req.Header.Get(proxy.RealIP) 63 | if proxy.RealIP == "RemoteAddr" { 64 | nIP, _, _ = net.SplitHostPort(req.RemoteAddr) 65 | } 66 | forwardedIPs := strings.Split(nIP, ",") // 从头部获取到IP并分割(主要担心xff有多个IP) 67 | 68 | // 只有单个IP也只会返回单个IP slice 69 | log("👀 IPs:'%v' %d", forwardedIPs, len(forwardedIPs)) 70 | // 如果有多个,得到第一个 IP 71 | for i := 0; i <= len(forwardedIPs)-1; i++ { 72 | trimmedIP := strings.TrimSpace(forwardedIPs[i]) 73 | finalIP := g.getIP(trimmedIP) 74 | log("currentIP:%s, index:%d, result:%s", trimmedIP, i, finalIP) 75 | if finalIP != nil { 76 | realIPStr = finalIP.String() 77 | break 78 | } 79 | } 80 | } 81 | // 获取到后直接设定 realIP 82 | if realIPStr != "" { 83 | if proxy.OverwriteXFF { 84 | log("🐸 Modify XFF to:%s", realIPStr) 85 | req.Header.Set(xForwardedFor, realIPStr) 86 | } 87 | req.Header.Set(xRealIP, realIPStr) 88 | break 89 | } 90 | } 91 | g.next.ServeHTTP(rw, req) 92 | } 93 | 94 | // getIP 是用来获取有效IP的,传入参数 s 为 ip文本,格式为 x.x.x.x 或 x.x.x.x:1234 95 | // 96 | // getIP is used to obtain valid IP addresses. The parameter s is the input IP text, 97 | // which should be in the format of x.x.x.x or x.x.x.x:1234. 98 | func (g *GetRealIP) getIP(s string) net.IP { 99 | pureIP, _, err := net.SplitHostPort(s) // 如果有端口号则分离得到ip 100 | if err != nil { 101 | pureIP = s 102 | } 103 | ip := net.ParseIP(pureIP) // 解析是否为合法 ip 104 | return ip 105 | } 106 | 107 | // log 是用于输出日志,使用方法类似 Sprintf,但末尾已经包含换行 108 | // 109 | // log is used for logging output, with a usage similar to Sprintf, 110 | // but it already includes a newline character at the end. 111 | func log(format string, a ...interface{}) { 112 | os.Stdout.WriteString("[get-realip] " + fmt.Sprintf(format, a...) + "\n") 113 | } 114 | 115 | // err是用于输出错误日志,使用方法类似 Sprintf,但末尾已经包含换行 116 | // 117 | // err is used for output err logs, and it usage is simillar to Sprintf, 118 | // but with a newline character already included at the end. 119 | // func err(format string, a ...interface{}) { 120 | // os.Stderr.WriteString(fmt.Sprintf(format, a...) + "\n") 121 | // } 122 | -------------------------------------------------------------------------------- /get_real_ip_test.go: -------------------------------------------------------------------------------- 1 | package traefik_get_real_ip_test 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | "net/http/httptest" 8 | "testing" 9 | 10 | plugin "github.com/Paxxs/traefik-get-real-ip" 11 | ) 12 | 13 | func TestNew(t *testing.T) { 14 | cfg := plugin.CreateConfig() 15 | cfg.Proxy = []plugin.Proxy{ 16 | { 17 | ProxyHeadername: "X-From-Cdn", 18 | ProxyHeadervalue: "1", 19 | RealIP: "X-Forwarded-For", 20 | }, 21 | { 22 | ProxyHeadername: "X-From-Cdn", 23 | ProxyHeadervalue: "2", 24 | RealIP: "Client-Ip", 25 | }, 26 | { 27 | ProxyHeadername: "X-From-Cdn", 28 | ProxyHeadervalue: "3", 29 | RealIP: "Cf-Connecting-Ip", 30 | }, 31 | { 32 | ProxyHeadername: "X-From-Cdn", 33 | ProxyHeadervalue: "4", 34 | RealIP: "X-Forwarded-For", 35 | }, 36 | { 37 | ProxyHeadername: "X-From-Cdn", 38 | ProxyHeadervalue: "5", 39 | RealIP: "Client-Ip", 40 | }, 41 | { 42 | ProxyHeadername: "*", 43 | ProxyHeadervalue: "6", 44 | RealIP: "RemoteAddr", 45 | }, 46 | } 47 | ctx := context.Background() 48 | next := http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {}) 49 | 50 | handler, err := plugin.New(ctx, next, cfg, "traefik-get-real-ip") 51 | if err != nil { 52 | t.Fatal(err) 53 | } 54 | 55 | testCases := []struct { 56 | xff string // X-Forwarded-For 57 | xFromProxy string // cdn标识 58 | realIPHeader string // CDN传递IP字段 59 | realIP string // CDN传递IP字段值 60 | desc string 61 | expected string 62 | remoteAddr string 63 | }{ 64 | { 65 | xff: "奇怪的,东西🤣,10.0.0.1, 2.2.2.2,3.3.3.3", 66 | xFromProxy: "1", 67 | realIPHeader: "Client-Ip", 68 | realIP: "10.0.0.2", 69 | desc: "Proxy 1 通过 xff 传递IP", 70 | remoteAddr: "172.18.0.1:1000", 71 | expected: "10.0.0.1", 72 | }, 73 | { 74 | xff: "10.0.1.2", 75 | xFromProxy: "2", 76 | realIPHeader: "Client-Ip", 77 | realIP: "10.0.1.1", 78 | desc: "Proxy 2 通过 Client-Ip 传递IP", 79 | remoteAddr: "172.18.0.2:2000", 80 | expected: "10.0.1.1", 81 | }, 82 | { 83 | xff: "10.0.0.2", 84 | xFromProxy: "3", 85 | realIPHeader: "Cf-Connecting-Ip", 86 | realIP: "10.0.2.1", 87 | desc: "Proxy 3 通过 Cf-Connecting-Ip 传递IP", 88 | remoteAddr: "172.18.0.3:3000", 89 | expected: "10.0.2.1", 90 | }, 91 | { 92 | xff: "奇怪的,东西🤣,10.0.3.1:2345, 2.2.2.2,3.3.3.3", 93 | xFromProxy: "4", 94 | realIPHeader: "Client-Ip", 95 | realIP: "10.0.3.2", 96 | desc: "Proxy 4 通过 xff 传递IP带端口号", 97 | remoteAddr: "172.18.0.4:4000", 98 | expected: "10.0.3.1", 99 | }, 100 | { 101 | xff: "10.0.5.1", 102 | xFromProxy: "5", 103 | realIP: "RemoteAddr", 104 | desc: "Proxy 5 取远程地址", 105 | remoteAddr: "172.18.0.5:55122", 106 | expected: "172.18.0.5", 107 | }, 108 | { 109 | xff: "sss", 110 | xFromProxy: "6", 111 | realIPHeader: "Client-Ip", 112 | realIP: "6", 113 | desc: "Proxy 6 未正确传递", 114 | remoteAddr: "172.18.0.6:6000", 115 | expected: "172.18.0.6", 116 | }, 117 | } 118 | 119 | for _, test := range testCases { 120 | t.Run(test.desc, func(t *testing.T) { 121 | reorder := httptest.NewRecorder() 122 | 123 | req, err := http.NewRequestWithContext(ctx, http.MethodGet, "http://localhost", nil) 124 | if err != nil { 125 | t.Fatal(err) 126 | } 127 | 128 | fmt.Println("\n😊 测试:", test.desc) 129 | fmt.Println(test) 130 | 131 | req.RemoteAddr = test.remoteAddr 132 | req.Header.Set(test.realIPHeader, test.realIP) 133 | req.Header.Set("X-From-Cdn", test.xFromProxy) 134 | req.Header.Set("X-Forwarded-For", test.xff) 135 | 136 | handler.ServeHTTP(reorder, req) 137 | 138 | assertHeader(t, req, "X-Real-Ip", test.expected) 139 | 140 | }) 141 | } 142 | } 143 | 144 | func assertHeader(t *testing.T, req *http.Request, key, expected string) { 145 | t.Helper() 146 | if req.Header.Get(key) != expected { 147 | t.Errorf("invalid header value: got %s, want %s", req.Header.Get(key), expected) 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/Paxxs/traefik-get-real-ip 2 | 3 | go 1.13 4 | --------------------------------------------------------------------------------