This repository has been archived by the owner on Dec 26, 2022. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 27
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit aef835a
Showing
5 changed files
with
183 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
displayName: Traefik Real IP | ||
type: middleware | ||
import: github.com/soulbalz/traefik-real-ip | ||
|
||
summary: When traefik is deployed behind a load balancer, it should get the real IP from the X-Forwarded-For or Cf-Connecting-Ip (if from Cloudflare) header. | ||
|
||
testData: | ||
excludednets: | ||
- "1.1.1.1/24" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
# Traefik Real IP | ||
|
||
If Traefik is behind a load balancer, it won't be able to get the Real IP from the external client by checking the remote IP address. | ||
|
||
This plugin solves this issue by overwriting the X-Real-Ip with an IP from the X-Forwarded-For or Cf-Connecting-Ip (if from Cloudflare) header. The real IP will be the first one that is not included in any of the CIDRs passed as the ExcludedNets parameter. The evaluation of the X-Forwarded-For or Cf-Connecting-Ip (if from Cloudflare) IPs will go from the last to the first one. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
module github.com/soulbalz/traefik-real-ip | ||
|
||
go 1.13 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,91 @@ | ||
package traefik_real_ip | ||
|
||
import ( | ||
"context" | ||
"net" | ||
"net/http" | ||
"strings" | ||
) | ||
|
||
const ( | ||
xRealIP = "X-Real-Ip" | ||
xForwardedFor = "X-Forwarded-For" | ||
cfConnectingIp = "Cf-Connecting-Ip" | ||
) | ||
|
||
// Config the plugin configuration. | ||
type Config struct { | ||
ExcludedNets []string `json:"excludednets,omitempty" toml:"excludednets,omitempty" yaml:"excludednets,omitempty"` | ||
} | ||
|
||
// CreateConfig creates the default plugin configuration. | ||
func CreateConfig() *Config { | ||
return &Config{ | ||
ExcludedNets: []string{}, | ||
} | ||
} | ||
|
||
// RealIPOverWriter is a plugin that blocks incoming requests depending on their source IP. | ||
type RealIPOverWriter struct { | ||
next http.Handler | ||
name string | ||
ExcludedNets []*net.IPNet | ||
} | ||
|
||
// New created a new Demo plugin. | ||
func New(ctx context.Context, next http.Handler, config *Config, name string) (http.Handler, error) { | ||
ipOverWriter := &RealIPOverWriter{ | ||
next: next, | ||
name: name, | ||
} | ||
|
||
for _, v := range config.ExcludedNets { | ||
_, excludedNet, err := net.ParseCIDR(v) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
ipOverWriter.ExcludedNets = append(ipOverWriter.ExcludedNets, excludedNet) | ||
} | ||
|
||
return ipOverWriter, nil | ||
} | ||
|
||
func (r *RealIPOverWriter) ServeHTTP(rw http.ResponseWriter, req *http.Request) { | ||
forwardedIPs := strings.Split(req.Header.Get(xForwardedFor), ",") | ||
|
||
// TODO - Implement a max for the iterations | ||
var realIP string | ||
for i := len(forwardedIPs) - 1; i >= 0; i-- { | ||
// TODO - Check if TrimSpace is necessary | ||
trimmedIP := strings.TrimSpace(forwardedIPs[i]) | ||
if !r.excludedIP(trimmedIP) { | ||
realIP = trimmedIP | ||
break | ||
} | ||
} | ||
|
||
if realIP == "" { | ||
realIP = req.Header.Get(cfConnectingIp) | ||
} | ||
|
||
req.Header.Set(xRealIP, realIP) | ||
|
||
r.next.ServeHTTP(rw, req) | ||
} | ||
|
||
func (r *RealIPOverWriter) excludedIP(s string) bool { | ||
ip := net.ParseIP(s) | ||
if ip == nil { | ||
// log the error and fallback to the default value (check if true is ok) | ||
return true | ||
} | ||
|
||
for _, network := range r.ExcludedNets { | ||
if network.Contains(ip) { | ||
return true | ||
} | ||
} | ||
|
||
return false | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
package traefik_real_ip_test | ||
|
||
import ( | ||
"context" | ||
"net/http" | ||
"net/http/httptest" | ||
"testing" | ||
|
||
plugin "github.com/soulbalz/traefik-real-ip" | ||
) | ||
|
||
func TestNew(t *testing.T) { | ||
cfg := plugin.CreateConfig() | ||
cfg.ExcludedNets = []string{"127.0.0.1/24"} | ||
|
||
ctx := context.Background() | ||
next := http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {}) | ||
|
||
handler, err := plugin.New(ctx, next, cfg, "traefik-real-ip") | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
|
||
testCases := []struct { | ||
header string | ||
desc string | ||
xForwardedFor string | ||
expected string | ||
}{ | ||
{ | ||
header: "X-Forwarded-For", | ||
desc: "don't forward", | ||
xForwardedFor: "127.0.0.2", | ||
expected: "", | ||
}, | ||
{ | ||
header: "X-Forwarded-For", | ||
desc: "forward", | ||
xForwardedFor: "10.0.0.1", | ||
expected: "10.0.0.1", | ||
}, | ||
{ | ||
header: "Cf-Connecting-Ip", | ||
desc: "forward", | ||
xForwardedFor: "10.0.0.1", | ||
expected: "10.0.0.1", | ||
}, | ||
} | ||
|
||
for _, test := range testCases { | ||
test := test | ||
t.Run(test.desc, func(t *testing.T) { | ||
recorder := httptest.NewRecorder() | ||
|
||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "http://localhost", nil) | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
|
||
req.Header.Set(test.header, test.xForwardedFor) | ||
|
||
handler.ServeHTTP(recorder, req) | ||
|
||
assertHeader(t, req, "X-Real-Ip", test.expected) | ||
}) | ||
} | ||
} | ||
|
||
func assertHeader(t *testing.T, req *http.Request, key, expected string) { | ||
t.Helper() | ||
|
||
if req.Header.Get(key) != expected { | ||
t.Errorf("invalid header value: %s", req.Header.Get(key)) | ||
} | ||
} |