Skip to content
This repository has been archived by the owner on Dec 26, 2022. It is now read-only.

Commit

Permalink
first commit
Browse files Browse the repository at this point in the history
  • Loading branch information
soulbalz committed Jan 22, 2021
0 parents commit aef835a
Show file tree
Hide file tree
Showing 5 changed files with 183 additions and 0 deletions.
9 changes: 9 additions & 0 deletions .traefik.yml
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"
5 changes: 5 additions & 0 deletions README.md
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.
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module github.com/soulbalz/traefik-real-ip

go 1.13
91 changes: 91 additions & 0 deletions real_ip.go
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
}
75 changes: 75 additions & 0 deletions real_ip_test.go
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))
}
}

0 comments on commit aef835a

Please sign in to comment.