1
0
Fork 0

IP Whitelists for Frontend (with Docker- & Kubernetes-Provider Support)

This commit is contained in:
MaZderMind 2017-04-30 11:22:07 +02:00 committed by Ludovic Fernandez
parent 55f610422a
commit 5f0b215e90
16 changed files with 731 additions and 14 deletions

View file

@ -272,6 +272,7 @@ func (p *Provider) loadDockerConfig(containersInspected []dockerData) *types.Con
"getServicePassHostHeader": p.getServicePassHostHeader,
"getServicePriority": p.getServicePriority,
"getServiceBackend": p.getServiceBackend,
"getWhitelistSourceRange": p.getWhitelistSourceRange,
}
// filter containers
filteredContainers := fun.Filter(func(container dockerData) bool {
@ -663,6 +664,15 @@ func (p *Provider) getPassHostHeader(container dockerData) string {
return "true"
}
func (p *Provider) getWhitelistSourceRange(container dockerData) []string {
var whitelistSourceRange []string
if whitelistSourceRangeLabel, err := getLabel(container, "traefik.frontend.whitelistSourceRange"); err == nil {
whitelistSourceRange = provider.SplitAndTrimString(whitelistSourceRangeLabel)
}
return whitelistSourceRange
}
func (p *Provider) getPriority(container dockerData) string {
if priority, err := getLabel(container, "traefik.frontend.priority"); err == nil {
return priority

View file

@ -400,6 +400,68 @@ func TestDockerGetPassHostHeader(t *testing.T) {
}
}
func TestDockerGetWhitelistSourceRange(t *testing.T) {
containers := []struct {
desc string
container docker.ContainerJSON
expected []string
}{
{
desc: "no whitelist-label",
container: containerJSON(),
expected: nil,
},
{
desc: "whitelist-label with empty string",
container: containerJSON(labels(map[string]string{
"traefik.frontend.whitelistSourceRange": "",
})),
expected: nil,
},
{
desc: "whitelist-label with IPv4 mask",
container: containerJSON(labels(map[string]string{
"traefik.frontend.whitelistSourceRange": "1.2.3.4/16",
})),
expected: []string{
"1.2.3.4/16",
},
},
{
desc: "whitelist-label with IPv6 mask",
container: containerJSON(labels(map[string]string{
"traefik.frontend.whitelistSourceRange": "fe80::/16",
})),
expected: []string{
"fe80::/16",
},
},
{
desc: "whitelist-label with multiple masks",
container: containerJSON(labels(map[string]string{
"traefik.frontend.whitelistSourceRange": "1.1.1.1/24, 1234:abcd::42/32",
})),
expected: []string{
"1.1.1.1/24",
"1234:abcd::42/32",
},
},
}
for _, e := range containers {
e := e
t.Run(e.desc, func(t *testing.T) {
t.Parallel()
dockerData := parseContainer(e.container)
provider := &Provider{}
actual := provider.getWhitelistSourceRange(dockerData)
if !reflect.DeepEqual(actual, e.expected) {
t.Errorf("expected %q, got %q", e.expected, actual)
}
})
}
}
func TestDockerGetLabel(t *testing.T) {
containers := []struct {
container docker.ContainerJSON

View file

@ -31,6 +31,8 @@ const (
ruleTypePathStrip = "PathStrip"
ruleTypePath = "Path"
ruleTypePathPrefix = "PathPrefix"
annotationKubernetesWhitelistSourceRange = "ingress.kubernetes.io/whitelist-source-range"
)
const traefikDefaultRealm = "traefik"
@ -171,17 +173,21 @@ func (p *Provider) loadIngresses(k8sClient Client) (*types.Configuration, error)
return nil, errors.New("no realm customization supported")
}
witelistSourceRangeAnnotation := i.Annotations[annotationKubernetesWhitelistSourceRange]
whitelistSourceRange := provider.SplitAndTrimString(witelistSourceRangeAnnotation)
if _, exists := templateObjects.Frontends[r.Host+pa.Path]; !exists {
basicAuthCreds, err := handleBasicAuthConfig(i, k8sClient)
if err != nil {
return nil, err
}
templateObjects.Frontends[r.Host+pa.Path] = &types.Frontend{
Backend: r.Host + pa.Path,
PassHostHeader: PassHostHeader,
Routes: make(map[string]types.Route),
Priority: len(pa.Path),
BasicAuth: basicAuthCreds,
Backend: r.Host + pa.Path,
PassHostHeader: PassHostHeader,
Routes: make(map[string]types.Route),
Priority: len(pa.Path),
BasicAuth: basicAuthCreds,
WhitelistSourceRange: whitelistSourceRange,
}
}
if len(r.Host) > 0 {

View file

@ -1523,6 +1523,35 @@ func TestIngressAnnotations(t *testing.T) {
},
},
},
{
ObjectMeta: v1.ObjectMeta{
Namespace: "testing",
Annotations: map[string]string{
"kubernetes.io/ingress.class": "traefik",
"ingress.kubernetes.io/whitelist-source-range": "1.1.1.1/24, 1234:abcd::42/32",
},
},
Spec: v1beta1.IngressSpec{
Rules: []v1beta1.IngressRule{
{
Host: "test",
IngressRuleValue: v1beta1.IngressRuleValue{
HTTP: &v1beta1.HTTPIngressRuleValue{
Paths: []v1beta1.HTTPIngressPath{
{
Path: "/whitelist-source-range",
Backend: v1beta1.IngressBackend{
ServiceName: "service1",
ServicePort: intstr.FromInt(80),
},
},
},
},
},
},
},
},
},
}
services := []*v1.Service{
{
@ -1613,6 +1642,19 @@ func TestIngressAnnotations(t *testing.T) {
Method: "wrr",
},
},
"test/whitelist-source-range": {
Servers: map[string]types.Server{
"http://example.com": {
URL: "http://example.com",
Weight: 1,
},
},
CircuitBreaker: nil,
LoadBalancer: &types.LoadBalancer{
Sticky: false,
Method: "wrr",
},
},
},
Frontends: map[string]*types.Frontend{
"foo/bar": {
@ -1655,6 +1697,23 @@ func TestIngressAnnotations(t *testing.T) {
},
BasicAuth: []string{"myUser:myEncodedPW"},
},
"test/whitelist-source-range": {
Backend: "test/whitelist-source-range",
PassHostHeader: true,
WhitelistSourceRange: []string{
"1.1.1.1/24",
"1234:abcd::42/32",
},
Priority: len("/whitelist-source-range"),
Routes: map[string]types.Route{
"/whitelist-source-range": {
Rule: "PathPrefix:/whitelist-source-range",
},
"test": {
Rule: "Host:test",
},
},
},
},
}

19
provider/string_util.go Normal file
View file

@ -0,0 +1,19 @@
package provider
import "strings"
// SplitAndTrimString splits separatedString at the comma character and trims each
// piece, filtering out empty pieces. Returns the list of pieces or nil if the input
// did not contain a non-empty piece.
func SplitAndTrimString(separatedString string) []string {
listOfStrings := strings.Split(separatedString, ",")
var trimmedListOfStrings []string
for _, s := range listOfStrings {
s = strings.TrimSpace(s)
if len(s) > 0 {
trimmedListOfStrings = append(trimmedListOfStrings, s)
}
}
return trimmedListOfStrings
}

View file

@ -0,0 +1,61 @@
package provider
import (
"github.com/stretchr/testify/assert"
"testing"
)
func TestSplitAndTrimString(t *testing.T) {
cases := []struct {
desc string
input string
expected []string
}{
{
desc: "empty string",
input: "",
expected: nil,
}, {
desc: "one piece",
input: "foo",
expected: []string{"foo"},
}, {
desc: "two pieces",
input: "foo,bar",
expected: []string{"foo", "bar"},
}, {
desc: "three pieces",
input: "foo,bar,zoo",
expected: []string{"foo", "bar", "zoo"},
}, {
desc: "two pieces with whitespace",
input: " foo , bar ",
expected: []string{"foo", "bar"},
}, {
desc: "consecutive commas",
input: " foo ,, bar ",
expected: []string{"foo", "bar"},
}, {
desc: "consecutive commas with witespace",
input: " foo , , bar ",
expected: []string{"foo", "bar"},
}, {
desc: "leading and trailing commas",
input: ",, foo , , bar,, , ",
expected: []string{"foo", "bar"},
}, {
desc: "no valid pieces",
input: ", , , ,, ,",
expected: nil,
},
}
for _, test := range cases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
actual := SplitAndTrimString(test.input)
assert.Equal(t, test.expected, actual)
})
}
}