Skip to content

Commit

Permalink
feature: ClientIP predicate checks only request.RemoteAddr (#1617)
Browse files Browse the repository at this point in the history
Signed-off-by: Sandor Szücs <[email protected]>
  • Loading branch information
szuecs authored Nov 24, 2020
1 parent 6008afe commit 807227a
Show file tree
Hide file tree
Showing 4 changed files with 153 additions and 20 deletions.
22 changes: 22 additions & 0 deletions docs/reference/predicates.md
Original file line number Diff line number Diff line change
Expand Up @@ -452,6 +452,28 @@ Examples:
SourceFromLast("1.2.3.4", "2.2.2.0/24")
```

## ClientIP

ClientIP implements a custom predicate to match routes based on
the client IP of a request.

Parameters:

* ClientIP (string, ..) varargs with IPs or CIDR

Examples:

```
// only match requests from 1.2.3.4
ClientIP("1.2.3.4")
// only match requests from 1.2.3.0 - 1.2.3.255
ClientIP("1.2.3.0/24")
// only match requests from 1.2.3.4 and the 2.2.2.0/24 network
ClientIP("1.2.3.4", "2.2.2.0/24")
```

## Tee

The Tee predicate matches a route when a request is spawn from the
Expand Down
40 changes: 29 additions & 11 deletions predicates/source/source.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,37 +50,51 @@ import (
)

const (
Name = "Source"
NameLast = "SourceFromLast"
Name = "Source"
NameLast = "SourceFromLast"
NameClientIP = "ClientIP"
)

var InvalidArgsError = errors.New("invalid arguments")

type sourcePred int

const (
source sourcePred = iota
sourceFromLast
clientIP
)

type spec struct {
fromLast bool
typ sourcePred
}

type predicate struct {
fromLast bool
typ sourcePred
acceptedSourceNets []net.IPNet
}

func New() routing.PredicateSpec { return &spec{} }
func NewFromLast() routing.PredicateSpec { return &spec{fromLast: true} }
func New() routing.PredicateSpec { return &spec{typ: source} }
func NewFromLast() routing.PredicateSpec { return &spec{typ: sourceFromLast} }
func NewClientIP() routing.PredicateSpec { return &spec{typ: clientIP} }

func (s *spec) Name() string {
if s.fromLast {
switch s.typ {
case sourceFromLast:
return NameLast
case clientIP:
return NameClientIP
default:
return Name
}
return Name
}

func (s *spec) Create(args []interface{}) (routing.Predicate, error) {
if len(args) == 0 {
return nil, InvalidArgsError
}

p := &predicate{fromLast: s.fromLast}
p := &predicate{typ: s.typ}

for i := range args {
if s, ok := args[i].(string); ok {
Expand All @@ -105,9 +119,13 @@ func (s *spec) Create(args []interface{}) (routing.Predicate, error) {

func (p *predicate) Match(r *http.Request) bool {
var src net.IP
if p.fromLast {
switch p.typ {
case sourceFromLast:
src = snet.RemoteHostFromLast(r)
} else {
case clientIP:
h, _, _ := net.SplitHostPort(r.RemoteAddr)
src = net.ParseIP(h)
default:
src = snet.RemoteHost(r)
}

Expand Down
110 changes: 101 additions & 9 deletions predicates/source/source_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,18 @@ import (
"testing"
)

func TestName(t *testing.T) {
if s := New().Name(); s != Name {
t.Fatalf("Failed to get Name %s, got %s", Name, s)
}
if s := NewFromLast().Name(); s != NameLast {
t.Fatalf("Failed to get Name %s, got %s", NameLast, s)
}
if s := NewClientIP().Name(); s != NameClientIP {
t.Fatalf("Failed to get Name %s, got %s", NameClientIP, s)
}
}

func TestCreate(t *testing.T) {
for _, ti := range []struct {
msg string
Expand Down Expand Up @@ -48,11 +60,15 @@ func TestCreate(t *testing.T) {
false,
}} {
t.Run(ti.msg, func(t *testing.T) {
_, err := (&spec{}).Create(ti.args)
_, err := (New()).Create(ti.args)
if err == nil && ti.err || err != nil && !ti.err {
t.Error(ti.msg, "failure case", err, ti.err)
}
_, err = (&spec{fromLast: true}).Create(ti.args)
_, err = (NewFromLast()).Create(ti.args)
if err == nil && ti.err || err != nil && !ti.err {
t.Error(ti.msg, "failure case", err, ti.err)
}
_, err = (NewClientIP()).Create(ti.args)
if err == nil && ti.err || err != nil && !ti.err {
t.Error(ti.msg, "failure case", err, ti.err)
}
Expand Down Expand Up @@ -109,17 +125,17 @@ func TestMatching(t *testing.T) {
}, {
"should work for IPv6",
[]interface{}{"C0:FF::EE"},
&http.Request{RemoteAddr: "C0:FF::EE"},
&http.Request{RemoteAddr: "[C0:FF::EE]:5123"},
true,
}, {
"should work for IPv6 with mask - pass",
[]interface{}{"C0:FF::EE/127"},
&http.Request{RemoteAddr: "C0:FF::EF"},
&http.Request{RemoteAddr: "[C0:FF::EF]:5123"},
true,
}, {
"should work for IPv6 with mask - reject",
[]interface{}{"C0:FF::EE/127"},
&http.Request{RemoteAddr: "C0:FF::EC"},
&http.Request{RemoteAddr: "[C0:FF::EC]:5123"},
false,
}} {
t.Run(ti.msg, func(t *testing.T) {
Expand Down Expand Up @@ -185,21 +201,21 @@ func TestMatchingFromLast(t *testing.T) {
}, {
"should work for IPv6",
[]interface{}{"C0:FF::EE"},
&http.Request{RemoteAddr: "C0:FF::EE"},
&http.Request{RemoteAddr: "[C0:FF::EE]:4123"},
true,
}, {
"should work for IPv6 with mask - pass",
[]interface{}{"C0:FF::EE/127"},
&http.Request{RemoteAddr: "C0:FF::EF"},
&http.Request{RemoteAddr: "[C0:FF::EF]:4123"},
true,
}, {
"should work for IPv6 with mask - reject",
[]interface{}{"C0:FF::EE/127"},
&http.Request{RemoteAddr: "C0:FF::EC"},
&http.Request{RemoteAddr: "[C0:FF::EC]:4123"},
false,
}} {
t.Run(ti.msg, func(t *testing.T) {
pred, err := (&spec{fromLast: true}).Create(ti.args)
pred, err := (&spec{typ: sourceFromLast}).Create(ti.args)
if err != nil {
t.Error("failed to create predicate", err)
} else {
Expand All @@ -211,3 +227,79 @@ func TestMatchingFromLast(t *testing.T) {
})
}
}

func TestMatchingClientIP(t *testing.T) {
for _, ti := range []struct {
msg string
args []interface{}
req *http.Request
matches bool
}{{
"happy case",
[]interface{}{"127.0.0.1"},
&http.Request{RemoteAddr: "127.0.0.1:1234"},
true,
}, {
"sad case",
[]interface{}{"127.0.0.1"},
&http.Request{RemoteAddr: "127.0.0.2:51234"},
false,
}, {
"should match on netmask",
[]interface{}{"127.0.0.1/30"},
&http.Request{RemoteAddr: "127.0.0.2:1234"},
true,
}, {
"should correctly handle netmask",
[]interface{}{"127.0.0.0/31"},
&http.Request{RemoteAddr: "127.0.0.2:1234"},
false,
}, {
"should correctly handle netmask",
[]interface{}{"127.0.0.0/30"},
&http.Request{RemoteAddr: "127.0.0.2:1234"},
true,
}, {
"should consider multiple masks",
[]interface{}{"127.0.0.1", "8.8.8.8/24"},
&http.Request{RemoteAddr: "8.8.8.127:1234"},
true,
}, {
"if available, should not use X-Forwarded-For for matching,match",
[]interface{}{"127.0.0.1"},
&http.Request{RemoteAddr: "127.0.0.1:1234", Header: http.Header{"X-Forwarded-For": []string{"8.8.8.8"}}},
true,
}, {
"if available, should not use X-Forwarded-For for matching, no match",
[]interface{}{"8.8.8.8"},
&http.Request{RemoteAddr: "127.0.0.1:1234", Header: http.Header{"X-Forwarded-For": []string{"8.8.8.8"}}},
false,
}, {
"should work for IPv6",
[]interface{}{"C0:FF::EE"},
&http.Request{RemoteAddr: "[C0:FF::EE]:1234"},
true,
}, {
"should work for IPv6 with mask - pass",
[]interface{}{"C0:FF::EE/127"},
&http.Request{RemoteAddr: "[C0:FF::EF]:1234"},
true,
}, {
"should work for IPv6 with mask - reject",
[]interface{}{"C0:FF::EE/127"},
&http.Request{RemoteAddr: "[C0:FF::EC]:1234"},
false,
}} {
t.Run(ti.msg, func(t *testing.T) {
pred, err := (&spec{typ: clientIP}).Create(ti.args)
if err != nil {
t.Error("failed to create predicate", err)
} else {
matches := pred.Match(ti.req)
if matches != ti.matches {
t.Error(ti.msg, "failed to match as expected")
}
}
})
}
}
1 change: 1 addition & 0 deletions skipper.go
Original file line number Diff line number Diff line change
Expand Up @@ -1229,6 +1229,7 @@ func run(o Options, sig chan os.Signal, idleConnsCH chan struct{}) error {
o.CustomPredicates = append(o.CustomPredicates,
source.New(),
source.NewFromLast(),
source.NewClientIP(),
interval.NewBetween(),
interval.NewBefore(),
interval.NewAfter(),
Expand Down

0 comments on commit 807227a

Please sign in to comment.