From adef7200f6a67526e7531a4bba19494f7651e339 Mon Sep 17 00:00:00 2001 From: vermishelle Date: Tue, 3 Oct 2017 11:22:03 +0300 Subject: [PATCH 01/14] Fix grammar --- docs/configuration/acme.md | 2 +- docs/user-guide/docker-and-lets-encrypt.md | 2 +- docs/user-guide/swarm-mode.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/configuration/acme.md b/docs/configuration/acme.md index 1e259c460..ce6a1055f 100644 --- a/docs/configuration/acme.md +++ b/docs/configuration/acme.md @@ -177,7 +177,7 @@ Enable on demand certificate. This will request a certificate from Let's Encrypt during the first TLS handshake for a hostname that does not yet have a certificate. !!! warning - TLS handshakes will be slow when requesting a hostname certificate for the first time, this can leads to DoS attacks. + TLS handshakes will be slow when requesting a hostname certificate for the first time, this can lead to DoS attacks. !!! warning Take note that Let's Encrypt have [rate limiting](https://letsencrypt.org/docs/rate-limits) diff --git a/docs/user-guide/docker-and-lets-encrypt.md b/docs/user-guide/docker-and-lets-encrypt.md index 1f803cb4b..1ee4f0e33 100644 --- a/docs/user-guide/docker-and-lets-encrypt.md +++ b/docs/user-guide/docker-and-lets-encrypt.md @@ -74,7 +74,7 @@ Also, we're making sure the container is automatically restarted by the Docker e We're publishing the default HTTP ports `80` and `443` on the host, and making sure the container is placed within the `web` network we've created earlier on. Finally, we're giving this container a static name called `traefik`. -Let's take a look at a simply `traefik.toml` configuration as well before we'll create the Traefik container: +Let's take a look at a simple `traefik.toml` configuration as well before we'll create the Traefik container: ```toml debug = false diff --git a/docs/user-guide/swarm-mode.md b/docs/user-guide/swarm-mode.md index dc5a84ccb..c57e7331b 100644 --- a/docs/user-guide/swarm-mode.md +++ b/docs/user-guide/swarm-mode.md @@ -307,7 +307,7 @@ cat ./cookies.txt whoami1.traefik FALSE / FALSE 0 _TRAEFIK_BACKEND http://10.0.0.15:80 ``` -If you load the cookies file (`-b cookies.txt`) for the next request, you will see that stickyness is maintained: +If you load the cookies file (`-b cookies.txt`) for the next request, you will see that stickiness is maintained: ```shell curl -b cookies.txt -H Host:whoami1.traefik http://$(docker-machine ip manager) From c94e5f358974cc3d5299339d58bff34dc3ed0a66 Mon Sep 17 00:00:00 2001 From: Emile Vauge Date: Thu, 5 Oct 2017 08:42:02 +0200 Subject: [PATCH 02/14] Delay first version check --- cmd/traefik/traefik.go | 1 + 1 file changed, 1 insertion(+) diff --git a/cmd/traefik/traefik.go b/cmd/traefik/traefik.go index 0c6fea52d..d9c703e8d 100644 --- a/cmd/traefik/traefik.go +++ b/cmd/traefik/traefik.go @@ -361,6 +361,7 @@ func CreateKvSource(traefikConfiguration *TraefikConfiguration) (*staert.KvSourc func checkNewVersion() { ticker := time.NewTicker(24 * time.Hour) safe.Go(func() { + time.Sleep(10 * time.Minute) version.CheckNewVersion() for { select { From 8a6743438077a6d089260c12adf3da5eca59fb3a Mon Sep 17 00:00:00 2001 From: Timo Reimann Date: Thu, 5 Oct 2017 12:14:03 +0200 Subject: [PATCH 03/14] Sanitize cookie names. --- server/server.go | 28 +++++++++++++++++++++++++++- server/server_test.go | 7 +++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/server/server.go b/server/server.go index e13dff8fd..370c5c79d 100644 --- a/server/server.go +++ b/server/server.go @@ -15,6 +15,7 @@ import ( "reflect" "regexp" "sort" + "strings" "sync" "time" @@ -826,7 +827,7 @@ func (server *Server) loadConfig(configurations types.Configurations, globalConf } stickySession := config.Backends[frontend.Backend].LoadBalancer.Sticky - cookieName := "_TRAEFIK_BACKEND_" + frontend.Backend + cookieName := getCookieName(frontend.Backend) var sticky *roundrobin.StickySession if stickySession { @@ -1208,3 +1209,28 @@ func (server *Server) buildRetryMiddleware(handler http.Handler, globalConfig co return middlewares.NewRetry(retryAttempts, handler, retryListeners) } + +// getCookieName returns a cookie name from the given backend, sanitizing +// characters that do not satisfy the requirements of RFC 2616. +func getCookieName(backend string) string { + const cookiePrefix = "_TRAEFIK_BACKEND_" + sanitizer := func(r rune) rune { + switch r { + case '!', '#', '$', '%', '&', '\'', '*', '+', '-', '.', '^', '`', '|', '~': + return r + } + + switch { + case r >= 'a' && r <= 'z': + fallthrough + case r >= 'A' && r <= 'Z': + fallthrough + case r >= '0' && r <= '9': + return r + default: + return '_' + } + } + + return cookiePrefix + strings.Map(sanitizer, backend) +} diff --git a/server/server_test.go b/server/server_test.go index a1575fddb..bbac7b5d2 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -659,6 +659,13 @@ func TestServerResponseEmptyBackend(t *testing.T) { } } +func TestGetCookieName(t *testing.T) { + want := "_TRAEFIK_BACKEND__my_BACKEND-v1.0~rc1" + if got := getCookieName("/my/BACKEND-v1.0~rc1"); got != want { + t.Errorf("got sticky cookie name %q, want %q", got, want) + } +} + func buildDynamicConfig(dynamicConfigBuilders ...func(*types.Configuration)) *types.Configuration { config := &types.Configuration{ Frontends: make(map[string]*types.Frontend), From 9db8773055ab3974edfd7f9064691fdc47c59cc9 Mon Sep 17 00:00:00 2001 From: Marco Jantke Date: Fri, 6 Oct 2017 09:20:13 +0200 Subject: [PATCH 04/14] fix flakiness in log rotation test --- integration/log_rotation_test.go | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/integration/log_rotation_test.go b/integration/log_rotation_test.go index 631757809..a0a97d8fd 100644 --- a/integration/log_rotation_test.go +++ b/integration/log_rotation_test.go @@ -57,10 +57,19 @@ func (s *LogRotationSuite) TestAccessLogRotation(c *check.C) { c.Assert(err, checker.IsNil) // Verify access.log.rotated output as expected + logAccessLogFile(c, traefikTestAccessLogFile+".rotated") lineCount := verifyLogLines(c, traefikTestAccessLogFile+".rotated", 0, true) c.Assert(lineCount, checker.GreaterOrEqualThan, 1) + // make sure that the access log file is at least created before we do assertions on it + err = try.Do(1*time.Second, func() error { + _, err := os.Stat(traefikTestAccessLogFile) + return err + }) + c.Assert(err, checker.IsNil, check.Commentf("access log file was not created in time")) + // Verify access.log output as expected + logAccessLogFile(c, traefikTestAccessLogFile) lineCount = verifyLogLines(c, traefikTestAccessLogFile, lineCount, true) c.Assert(lineCount, checker.Equals, 3) @@ -111,6 +120,12 @@ func (s *LogRotationSuite) TestTraefikLogRotation(c *check.C) { c.Assert(lineCount, checker.GreaterOrEqualThan, 7) } +func logAccessLogFile(c *check.C, fileName string) { + output, err := ioutil.ReadFile(fileName) + c.Assert(err, checker.IsNil) + c.Logf("Contents of file %s\n%s", fileName, string(output)) +} + func verifyEmptyErrorLog(c *check.C, name string) { err := try.Do(5*time.Second, func() error { traefikLog, e2 := ioutil.ReadFile(name) @@ -130,7 +145,6 @@ func verifyLogLines(c *check.C, fileName string, countInit int, accessLog bool) count := countInit for rotatedLog.Scan() { line := rotatedLog.Text() - c.Log(line) if accessLog { if len(line) > 0 { CheckAccessLogFormat(c, line, count) From 5a578c53753db739662b8a63a55549f4af721d69 Mon Sep 17 00:00:00 2001 From: Shane Smith-Sahnow Date: Fri, 6 Oct 2017 01:44:03 -0700 Subject: [PATCH 05/14] Updating make run-dev --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 0c04bc283..2dc206ce7 100644 --- a/Makefile +++ b/Makefile @@ -97,7 +97,7 @@ dist: run-dev: go generate - go build + go build ./cmd/traefik ./traefik generate-webui: build-webui From fd70e6edb1b202014fed4b973d57a7414eab6905 Mon Sep 17 00:00:00 2001 From: Marco Jantke Date: Fri, 6 Oct 2017 11:34:03 +0200 Subject: [PATCH 06/14] enable prefix matching within slash boundaries --- middlewares/stripPrefix.go | 10 +--------- middlewares/stripPrefix_test.go | 8 ++++++++ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/middlewares/stripPrefix.go b/middlewares/stripPrefix.go index 2f2529796..8c1499d63 100644 --- a/middlewares/stripPrefix.go +++ b/middlewares/stripPrefix.go @@ -16,17 +16,9 @@ type StripPrefix struct { func (s *StripPrefix) ServeHTTP(w http.ResponseWriter, r *http.Request) { for _, prefix := range s.Prefixes { - origPrefix := strings.TrimSpace(prefix) - if origPrefix == r.URL.Path { - r.URL.Path = "/" - s.serveRequest(w, r, origPrefix) - return - } - - prefix = strings.TrimSuffix(origPrefix, "/") + "/" if p := strings.TrimPrefix(r.URL.Path, prefix); len(p) < len(r.URL.Path) { r.URL.Path = "/" + strings.TrimPrefix(p, "/") - s.serveRequest(w, r, origPrefix) + s.serveRequest(w, r, strings.TrimSpace(prefix)) return } } diff --git a/middlewares/stripPrefix_test.go b/middlewares/stripPrefix_test.go index 2103102c1..2e8778d72 100644 --- a/middlewares/stripPrefix_test.go +++ b/middlewares/stripPrefix_test.go @@ -86,6 +86,14 @@ func TestStripPrefix(t *testing.T) { expectedPath: "/", expectedHeader: "/stat", }, + { + desc: "prefix matching within slash boundaries", + prefixes: []string{"/stat"}, + path: "/status", + expectedStatusCode: http.StatusOK, + expectedPath: "/us", + expectedHeader: "/stat", + }, } for _, test := range tests { From 72f3b1ed39aa66e4c155188902647955879c6376 Mon Sep 17 00:00:00 2001 From: Timo Reimann Date: Mon, 9 Oct 2017 12:12:03 +0200 Subject: [PATCH 07/14] Remove pod from RBAC rules. --- docs/user-guide/kubernetes.md | 1 - examples/k8s/traefik-rbac.yaml | 1 - 2 files changed, 2 deletions(-) diff --git a/docs/user-guide/kubernetes.md b/docs/user-guide/kubernetes.md index 274135af2..c0e5a90b5 100644 --- a/docs/user-guide/kubernetes.md +++ b/docs/user-guide/kubernetes.md @@ -32,7 +32,6 @@ rules: - apiGroups: - "" resources: - - pods - services - endpoints - secrets diff --git a/examples/k8s/traefik-rbac.yaml b/examples/k8s/traefik-rbac.yaml index 3f0ab476d..db5fb8c3d 100644 --- a/examples/k8s/traefik-rbac.yaml +++ b/examples/k8s/traefik-rbac.yaml @@ -7,7 +7,6 @@ rules: - apiGroups: - "" resources: - - pods - services - endpoints - secrets From 18d8537d291b5c62d55284d76bad293794253a27 Mon Sep 17 00:00:00 2001 From: Timo Reimann Date: Mon, 9 Oct 2017 12:36:03 +0200 Subject: [PATCH 08/14] Document ways to partition Ingresses in the k8s guide. --- docs/user-guide/kubernetes.md | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/docs/user-guide/kubernetes.md b/docs/user-guide/kubernetes.md index c0e5a90b5..530fc9845 100644 --- a/docs/user-guide/kubernetes.md +++ b/docs/user-guide/kubernetes.md @@ -686,13 +686,23 @@ If you were to visit `example.com/static` the request would then be passed onto So you could set `disablePassHostHeaders` to `true` in your toml file and then enable passing the host header per ingress if you wanted. -## Excluding an ingress from Træfik +## Partitioning the Ingress object space -You can control which ingress Træfik cares about by using the `kubernetes.io/ingress.class` annotation. +By default, Træfik processes every Ingress objects it observes. At times, however, it may be desirable to ignore certain objects. The following sub-sections describe common use cases and how they can be handled with Træfik. -By default if the annotation is not set at all Træfik will include the ingress. -If the annotation is set to anything other than traefik or a blank string Træfik will ignore it. +### Between Træfik and other Ingress controller implementations +Sometimes Træfik runs along other Ingress controller implementations. One such example is when both Træfik and a cloud provider Ingress controller are active. + +The `kubernetes.io/ingress.class` annotation can be attached to any Ingress object in order to control whether Træfik should handle it. + +If the annotation is missing, contains an empty value, or the value `traefik`, then the Træfik controller will take responsibility and process the associated Ingress object. If the annotation contains any other value (usually the name of a different Ingress controller), Træfik will ignore the object. + +### Between multiple Træfik Deployments + +Sometimes multiple Træfik Deployments are supposed to run concurrently. For instance, it is conceivable to have one Deployment deal with internal and another one with external traffic. + +For such cases, it is advisable to classify Ingress objects through a label and configure the `labelSelector` option per each Træfik Deployment accordingly. To stick with the internal/external example above, all Ingress objects meant for internal traffic could receive a `traffic-type: internal` label while objects designated for external traffic receive a `traffic-type: external` label. The label selectors on the Træfik Deployments would then be `traffic-type=internal` and `traffic-type=external`, respectively. ## Production advice From ed2eb7b5a6030b15a858c6cc205140ac391cb7e8 Mon Sep 17 00:00:00 2001 From: Timo Reimann Date: Mon, 9 Oct 2017 14:16:03 +0200 Subject: [PATCH 09/14] Quote priority values in annotation examples. --- docs/configuration/backends/kubernetes.md | 2 +- docs/user-guide/kubernetes.md | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/configuration/backends/kubernetes.md b/docs/configuration/backends/kubernetes.md index 9b549403d..2d63466d4 100644 --- a/docs/configuration/backends/kubernetes.md +++ b/docs/configuration/backends/kubernetes.md @@ -86,7 +86,7 @@ Annotations can be used on containers to override default behaviour for the whol - `traefik.frontend.rule.type: PathPrefixStrip` Override the default frontend rule type. Default: `PathPrefix`. -- `traefik.frontend.priority: 3` +- `traefik.frontend.priority: "3"` Override the default frontend rule priority. Annotations can be used on the Kubernetes service to override default behaviour: diff --git a/docs/user-guide/kubernetes.md b/docs/user-guide/kubernetes.md index 530fc9845..fa72b20ac 100644 --- a/docs/user-guide/kubernetes.md +++ b/docs/user-guide/kubernetes.md @@ -590,7 +590,7 @@ kind: Ingress metadata: name: wildcard-cheeses annotations: - traefik.frontend.priority: 1 + traefik.frontend.priority: "1" spec: rules: - host: *.minikube @@ -605,7 +605,7 @@ kind: Ingress metadata: name: specific-cheeses annotations: - traefik.frontend.priority: 2 + traefik.frontend.priority: "2" spec: rules: - host: specific.minikube @@ -617,6 +617,7 @@ spec: servicePort: http ``` +Note that priority values must be quoted to avoid them being interpreted as numbers (which are illegal for annotations). ## Forwarding to ExternalNames From a9d4b09bdb330356ce26ab48bdba85ffe34fd363 Mon Sep 17 00:00:00 2001 From: Ludovic Fernandez Date: Tue, 10 Oct 2017 11:10:02 +0200 Subject: [PATCH 10/14] Stickiness cookie name --- provider/consul/consul_catalog.go | 47 ++++++++++----- provider/docker/docker.go | 26 +++++--- provider/dynamodb/dynamodb.go | 6 +- provider/ecs/ecs.go | 42 ++++++++----- provider/kubernetes/kubernetes.go | 12 +++- provider/kubernetes/kubernetes_test.go | 27 +-------- provider/kv/kv.go | 26 ++++++-- provider/marathon/marathon.go | 22 +++++-- provider/marathon/marathon_test.go | 27 +++++++-- provider/provider.go | 2 +- provider/rancher/rancher.go | 22 +++++-- server/cookie/cookie.go | 57 ++++++++++++++++++ server/cookie/cookie_test.go | 83 ++++++++++++++++++++++++++ server/server.go | 67 +++++++++------------ server/server_test.go | 52 +++++++--------- templates/consul_catalog.tmpl | 6 +- templates/docker.tmpl | 5 +- templates/ecs.tmpl | 5 +- templates/kubernetes.tmpl | 5 +- templates/kv.tmpl | 18 +++--- templates/marathon.tmpl | 5 +- templates/rancher.tmpl | 5 +- types/common_label.go | 81 +++++++++---------------- types/types.go | 10 +++- 24 files changed, 434 insertions(+), 224 deletions(-) create mode 100644 server/cookie/cookie.go create mode 100644 server/cookie/cookie_test.go diff --git a/provider/consul/consul_catalog.go b/provider/consul/consul_catalog.go index 0d7876358..dd2c75156 100644 --- a/provider/consul/consul_catalog.go +++ b/provider/consul/consul_catalog.go @@ -385,10 +385,6 @@ func (p *CatalogProvider) getBackendName(node *api.ServiceEntry, index int) stri return serviceName } -func (p *CatalogProvider) getAttribute(name string, tags []string, defaultValue string) string { - return p.getTag(p.getPrefixedName(name), tags, defaultValue) -} - func (p *CatalogProvider) getBasicAuth(tags []string) []string { list := p.getAttribute("frontend.auth.basic", tags, "") if list != "" { @@ -397,6 +393,27 @@ func (p *CatalogProvider) getBasicAuth(tags []string) []string { return []string{} } +func (p *CatalogProvider) hasStickinessLabel(tags []string) bool { + stickinessTag := p.getTag(types.LabelBackendLoadbalancerStickiness, tags, "") + + stickyTag := p.getTag(types.LabelBackendLoadbalancerSticky, tags, "") + if len(stickyTag) > 0 { + log.Warn("Deprecated configuration found: %s. Please use %s.", types.LabelBackendLoadbalancerSticky, types.LabelBackendLoadbalancerStickiness) + } + + stickiness := len(stickinessTag) > 0 && strings.EqualFold(strings.TrimSpace(stickinessTag), "true") + sticky := len(stickyTag) > 0 && strings.EqualFold(strings.TrimSpace(stickyTag), "true") + return stickiness || sticky +} + +func (p *CatalogProvider) getStickinessCookieName(tags []string) string { + return p.getTag(types.LabelBackendLoadbalancerStickinessCookieName, tags, "") +} + +func (p *CatalogProvider) getAttribute(name string, tags []string, defaultValue string) string { + return p.getTag(p.getPrefixedName(name), tags, defaultValue) +} + func (p *CatalogProvider) hasTag(name string, tags []string) bool { // Very-very unlikely that a Consul tag would ever start with '=!=' tag := p.getTag(name, tags, "=!=") @@ -439,16 +456,18 @@ func (p *CatalogProvider) getConstraintTags(tags []string) []string { func (p *CatalogProvider) buildConfig(catalog []catalogUpdate) *types.Configuration { var FuncMap = template.FuncMap{ - "getBackend": p.getBackend, - "getFrontendRule": p.getFrontendRule, - "getBackendName": p.getBackendName, - "getBackendAddress": p.getBackendAddress, - "getAttribute": p.getAttribute, - "getBasicAuth": p.getBasicAuth, - "getTag": p.getTag, - "hasTag": p.hasTag, - "getEntryPoints": p.getEntryPoints, - "hasMaxconnAttributes": p.hasMaxconnAttributes, + "getBackend": p.getBackend, + "getFrontendRule": p.getFrontendRule, + "getBackendName": p.getBackendName, + "getBackendAddress": p.getBackendAddress, + "getBasicAuth": p.getBasicAuth, + "hasStickinessLabel": p.hasStickinessLabel, + "getStickinessCookieName": p.getStickinessCookieName, + "getAttribute": p.getAttribute, + "getTag": p.getTag, + "hasTag": p.hasTag, + "getEntryPoints": p.getEntryPoints, + "hasMaxconnAttributes": p.hasMaxconnAttributes, } allNodes := []*api.ServiceEntry{} diff --git a/provider/docker/docker.go b/provider/docker/docker.go index 1dec90b4b..ec0b7f707 100644 --- a/provider/docker/docker.go +++ b/provider/docker/docker.go @@ -275,7 +275,8 @@ func (p *Provider) loadDockerConfig(containersInspected []dockerData) *types.Con "hasMaxConnLabels": p.hasMaxConnLabels, "getMaxConnAmount": p.getMaxConnAmount, "getMaxConnExtractorFunc": p.getMaxConnExtractorFunc, - "getSticky": p.getSticky, + "getStickinessCookieName": p.getStickinessCookieName, + "hasStickinessLabel": p.hasStickinessLabel, "getIsBackendLBSwarm": p.getIsBackendLBSwarm, "hasServices": p.hasServices, "getServiceNames": p.getServiceNames, @@ -328,10 +329,8 @@ func (p *Provider) loadDockerConfig(containersInspected []dockerData) *types.Con } func (p *Provider) hasCircuitBreakerLabel(container dockerData) bool { - if _, err := getLabel(container, types.LabelBackendCircuitbreakerExpression); err != nil { - return false - } - return true + _, err := getLabel(container, types.LabelBackendCircuitbreakerExpression) + return err == nil } // Regexp used to extract the name of the service and the name of the property for this service @@ -645,11 +644,22 @@ func (p *Provider) getWeight(container dockerData) string { return "0" } -func (p *Provider) getSticky(container dockerData) string { - if label, err := getLabel(container, types.LabelBackendLoadbalancerSticky); err == nil { +func (p *Provider) hasStickinessLabel(container dockerData) bool { + _, errStickiness := getLabel(container, types.LabelBackendLoadbalancerStickiness) + + label, errSticky := getLabel(container, types.LabelBackendLoadbalancerSticky) + if len(label) > 0 { + log.Warn("Deprecated configuration found: %s. Please use %s.", types.LabelBackendLoadbalancerSticky, types.LabelBackendLoadbalancerStickiness) + } + + return errStickiness == nil || (errSticky == nil && strings.EqualFold(strings.TrimSpace(label), "true")) +} + +func (p *Provider) getStickinessCookieName(container dockerData) string { + if label, err := getLabel(container, types.LabelBackendLoadbalancerStickinessCookieName); err == nil { return label } - return "false" + return "" } func (p *Provider) getIsBackendLBSwarm(container dockerData) string { diff --git a/provider/dynamodb/dynamodb.go b/provider/dynamodb/dynamodb.go index 62070aa81..c021b598b 100644 --- a/provider/dynamodb/dynamodb.go +++ b/provider/dynamodb/dynamodb.go @@ -162,12 +162,12 @@ func (p *Provider) Provide(configurationChan chan<- types.ConfigMessage, pool *s }) operation := func() error { - aws, err := p.createClient() + awsClient, err := p.createClient() if err != nil { return handleCanceled(ctx, err) } - configuration, err := p.loadDynamoConfig(aws) + configuration, err := p.loadDynamoConfig(awsClient) if err != nil { return handleCanceled(ctx, err) } @@ -184,7 +184,7 @@ func (p *Provider) Provide(configurationChan chan<- types.ConfigMessage, pool *s log.Debug("Watching Provider...") select { case <-reload.C: - configuration, err := p.loadDynamoConfig(aws) + configuration, err := p.loadDynamoConfig(awsClient) if err != nil { return handleCanceled(ctx, err) } diff --git a/provider/ecs/ecs.go b/provider/ecs/ecs.go index 3c543d9b9..a8bb03365 100644 --- a/provider/ecs/ecs.go +++ b/provider/ecs/ecs.go @@ -122,12 +122,12 @@ func (p *Provider) Provide(configurationChan chan<- types.ConfigMessage, pool *s }) operation := func() error { - aws, err := p.createClient() + awsClient, err := p.createClient() if err != nil { return err } - configuration, err := p.loadECSConfig(ctx, aws) + configuration, err := p.loadECSConfig(ctx, awsClient) if err != nil { return handleCanceled(ctx, err) } @@ -143,7 +143,7 @@ func (p *Provider) Provide(configurationChan chan<- types.ConfigMessage, pool *s for { select { case <-reload.C: - configuration, err := p.loadECSConfig(ctx, aws) + configuration, err := p.loadECSConfig(ctx, awsClient) if err != nil { return handleCanceled(ctx, err) } @@ -180,11 +180,12 @@ func wrapAws(ctx context.Context, req *request.Request) error { func (p *Provider) loadECSConfig(ctx context.Context, client *awsClient) (*types.Configuration, error) { var ecsFuncMap = template.FuncMap{ - "filterFrontends": p.filterFrontends, - "getFrontendRule": p.getFrontendRule, - "getBasicAuth": p.getBasicAuth, - "getLoadBalancerSticky": p.getLoadBalancerSticky, - "getLoadBalancerMethod": p.getLoadBalancerMethod, + "filterFrontends": p.filterFrontends, + "getFrontendRule": p.getFrontendRule, + "getBasicAuth": p.getBasicAuth, + "getLoadBalancerMethod": p.getLoadBalancerMethod, + "hasStickinessLabel": p.hasStickinessLabel, + "getStickinessCookieName": p.getStickinessCookieName, } instances, err := p.listInstances(ctx, client) @@ -477,14 +478,27 @@ func (p *Provider) getBasicAuth(i ecsInstance) []string { return []string{} } -func (p *Provider) getLoadBalancerSticky(instances []ecsInstance) string { +func getFirstInstanceLabel(instances []ecsInstance, labelName string) string { if len(instances) > 0 { - label := instances[0].label(types.LabelBackendLoadbalancerSticky) - if label != "" { - return label - } + return instances[0].label(labelName) } - return "false" + return "" +} + +func (p *Provider) hasStickinessLabel(instances []ecsInstance) bool { + stickinessLabel := getFirstInstanceLabel(instances, types.LabelBackendLoadbalancerStickiness) + + stickyLabel := getFirstInstanceLabel(instances, types.LabelBackendLoadbalancerSticky) + if len(stickyLabel) > 0 { + log.Warn("Deprecated configuration found: %s. Please use %s.", types.LabelBackendLoadbalancerSticky, types.LabelBackendLoadbalancerStickiness) + } + stickiness := len(stickinessLabel) > 0 && strings.EqualFold(strings.TrimSpace(stickinessLabel), "true") + sticky := len(stickyLabel) > 0 && strings.EqualFold(strings.TrimSpace(stickyLabel), "true") + return stickiness || sticky +} + +func (p *Provider) getStickinessCookieName(instances []ecsInstance) string { + return getFirstInstanceLabel(instances, types.LabelBackendLoadbalancerStickinessCookieName) } func (p *Provider) getLoadBalancerMethod(instances []ecsInstance) string { diff --git a/provider/kubernetes/kubernetes.go b/provider/kubernetes/kubernetes.go index d6cdd1782..e5f5a65b5 100644 --- a/provider/kubernetes/kubernetes.go +++ b/provider/kubernetes/kubernetes.go @@ -18,6 +18,7 @@ import ( "github.com/containous/traefik/log" "github.com/containous/traefik/provider" "github.com/containous/traefik/safe" + "github.com/containous/traefik/server/cookie" "github.com/containous/traefik/types" "k8s.io/client-go/pkg/api/v1" "k8s.io/client-go/pkg/apis/extensions/v1beta1" @@ -160,7 +161,6 @@ func (p *Provider) loadIngresses(k8sClient Client) (*types.Configuration, error) templateObjects.Backends[r.Host+pa.Path] = &types.Backend{ Servers: make(map[string]types.Server), LoadBalancer: &types.LoadBalancer{ - Sticky: false, Method: "wrr", }, } @@ -247,8 +247,14 @@ func (p *Provider) loadIngresses(k8sClient Client) (*types.Configuration, error) templateObjects.Backends[r.Host+pa.Path].LoadBalancer.Method = "drr" } - if service.Annotations[types.LabelBackendLoadbalancerSticky] == "true" { - templateObjects.Backends[r.Host+pa.Path].LoadBalancer.Sticky = true + if len(service.Annotations[types.LabelBackendLoadbalancerSticky]) > 0 { + log.Warn("Deprecated configuration found: %s. Please use %s.", types.LabelBackendLoadbalancerSticky, types.LabelBackendLoadbalancerStickiness) + } + + if service.Annotations[types.LabelBackendLoadbalancerSticky] == "true" || service.Annotations[types.LabelBackendLoadbalancerStickiness] == "true" { + templateObjects.Backends[r.Host+pa.Path].LoadBalancer.Stickiness = &types.Stickiness{ + CookieName: cookie.GenerateName(r.Host + pa.Path), + } } protocol := "http" diff --git a/provider/kubernetes/kubernetes_test.go b/provider/kubernetes/kubernetes_test.go index 823f6df47..e8df03ea1 100644 --- a/provider/kubernetes/kubernetes_test.go +++ b/provider/kubernetes/kubernetes_test.go @@ -243,7 +243,6 @@ func TestLoadIngresses(t *testing.T) { }, CircuitBreaker: nil, LoadBalancer: &types.LoadBalancer{ - Sticky: false, Method: "wrr", }, }, @@ -256,7 +255,6 @@ func TestLoadIngresses(t *testing.T) { }, CircuitBreaker: nil, LoadBalancer: &types.LoadBalancer{ - Sticky: false, Method: "wrr", }, }, @@ -273,7 +271,6 @@ func TestLoadIngresses(t *testing.T) { }, CircuitBreaker: nil, LoadBalancer: &types.LoadBalancer{ - Sticky: false, Method: "wrr", }, }, @@ -489,7 +486,6 @@ func TestGetPassHostHeader(t *testing.T) { Servers: map[string]types.Server{}, CircuitBreaker: nil, LoadBalancer: &types.LoadBalancer{ - Sticky: false, Method: "wrr", }, }, @@ -591,7 +587,6 @@ func TestOnlyReferencesServicesFromOwnNamespace(t *testing.T) { Servers: map[string]types.Server{}, CircuitBreaker: nil, LoadBalancer: &types.LoadBalancer{ - Sticky: false, Method: "wrr", }, }, @@ -771,7 +766,6 @@ func TestLoadNamespacedIngresses(t *testing.T) { Servers: map[string]types.Server{}, CircuitBreaker: nil, LoadBalancer: &types.LoadBalancer{ - Sticky: false, Method: "wrr", }, }, @@ -779,7 +773,6 @@ func TestLoadNamespacedIngresses(t *testing.T) { Servers: map[string]types.Server{}, CircuitBreaker: nil, LoadBalancer: &types.LoadBalancer{ - Sticky: false, Method: "wrr", }, }, @@ -996,7 +989,6 @@ func TestLoadMultipleNamespacedIngresses(t *testing.T) { Servers: map[string]types.Server{}, CircuitBreaker: nil, LoadBalancer: &types.LoadBalancer{ - Sticky: false, Method: "wrr", }, }, @@ -1004,7 +996,6 @@ func TestLoadMultipleNamespacedIngresses(t *testing.T) { Servers: map[string]types.Server{}, CircuitBreaker: nil, LoadBalancer: &types.LoadBalancer{ - Sticky: false, Method: "wrr", }, }, @@ -1012,7 +1003,6 @@ func TestLoadMultipleNamespacedIngresses(t *testing.T) { Servers: map[string]types.Server{}, CircuitBreaker: nil, LoadBalancer: &types.LoadBalancer{ - Sticky: false, Method: "wrr", }, }, @@ -1118,7 +1108,6 @@ func TestHostlessIngress(t *testing.T) { Servers: map[string]types.Server{}, CircuitBreaker: nil, LoadBalancer: &types.LoadBalancer{ - Sticky: false, Method: "wrr", }, }, @@ -1320,7 +1309,6 @@ func TestServiceAnnotations(t *testing.T) { }, LoadBalancer: &types.LoadBalancer{ Method: "drr", - Sticky: false, }, }, "bar": { @@ -1337,7 +1325,9 @@ func TestServiceAnnotations(t *testing.T) { CircuitBreaker: nil, LoadBalancer: &types.LoadBalancer{ Method: "wrr", - Sticky: true, + Stickiness: &types.Stickiness{ + CookieName: "_4155f", + }, }, }, }, @@ -1601,7 +1591,6 @@ func TestIngressAnnotations(t *testing.T) { }, CircuitBreaker: nil, LoadBalancer: &types.LoadBalancer{ - Sticky: false, Method: "wrr", }, }, @@ -1614,7 +1603,6 @@ func TestIngressAnnotations(t *testing.T) { }, CircuitBreaker: nil, LoadBalancer: &types.LoadBalancer{ - Sticky: false, Method: "wrr", }, }, @@ -1627,7 +1615,6 @@ func TestIngressAnnotations(t *testing.T) { }, CircuitBreaker: nil, LoadBalancer: &types.LoadBalancer{ - Sticky: false, Method: "wrr", }, }, @@ -1640,7 +1627,6 @@ func TestIngressAnnotations(t *testing.T) { }, CircuitBreaker: nil, LoadBalancer: &types.LoadBalancer{ - Sticky: false, Method: "wrr", }, }, @@ -1653,7 +1639,6 @@ func TestIngressAnnotations(t *testing.T) { }, CircuitBreaker: nil, LoadBalancer: &types.LoadBalancer{ - Sticky: false, Method: "wrr", }, }, @@ -1807,7 +1792,6 @@ func TestPriorityHeaderValue(t *testing.T) { }, CircuitBreaker: nil, LoadBalancer: &types.LoadBalancer{ - Sticky: false, Method: "wrr", }, }, @@ -1909,7 +1893,6 @@ func TestInvalidPassHostHeaderValue(t *testing.T) { }, CircuitBreaker: nil, LoadBalancer: &types.LoadBalancer{ - Sticky: false, Method: "wrr", }, }, @@ -2192,14 +2175,12 @@ func TestMissingResources(t *testing.T) { CircuitBreaker: nil, LoadBalancer: &types.LoadBalancer{ Method: "wrr", - Sticky: false, }, }, "missing_service": { Servers: map[string]types.Server{}, LoadBalancer: &types.LoadBalancer{ Method: "wrr", - Sticky: false, }, }, "missing_endpoints": { @@ -2207,7 +2188,6 @@ func TestMissingResources(t *testing.T) { CircuitBreaker: nil, LoadBalancer: &types.LoadBalancer{ Method: "wrr", - Sticky: false, }, }, "missing_endpoint_subsets": { @@ -2215,7 +2195,6 @@ func TestMissingResources(t *testing.T) { CircuitBreaker: nil, LoadBalancer: &types.LoadBalancer{ Method: "wrr", - Sticky: false, }, }, }, diff --git a/provider/kv/kv.go b/provider/kv/kv.go index e61ea5be9..36beb8650 100644 --- a/provider/kv/kv.go +++ b/provider/kv/kv.go @@ -139,11 +139,13 @@ func (p *Provider) loadConfig() *types.Configuration { } var KvFuncMap = template.FuncMap{ - "List": p.list, - "ListServers": p.listServers, - "Get": p.get, - "SplitGet": p.splitGet, - "Last": p.last, + "List": p.list, + "ListServers": p.listServers, + "Get": p.get, + "SplitGet": p.splitGet, + "Last": p.last, + "hasStickinessLabel": p.hasStickinessLabel, + "getStickinessCookieName": p.getStickinessCookieName, } configuration, err := p.GetConfiguration("templates/kv.tmpl", KvFuncMap, templateObjects) @@ -239,3 +241,17 @@ func (p *Provider) checkConstraints(keys ...string) bool { } return true } + +func (p *Provider) hasStickinessLabel(rootPath string) bool { + stickiness, err := p.kvclient.Exists(rootPath + "/loadbalancer/stickiness") + if err != nil { + log.Debugf("Error occurs when check stickiness: %v", err) + } + sticky := p.get("false", rootPath, "/loadbalancer", "/sticky") + + return stickiness || (len(sticky) != 0 && strings.EqualFold(strings.TrimSpace(sticky), "true")) +} + +func (p *Provider) getStickinessCookieName(rootPath string) string { + return p.get("", rootPath, "/loadbalancer", "/stickiness", "/cookiename") +} diff --git a/provider/marathon/marathon.go b/provider/marathon/marathon.go index 0ea7627be..d8639b13c 100644 --- a/provider/marathon/marathon.go +++ b/provider/marathon/marathon.go @@ -188,7 +188,8 @@ func (p *Provider) loadMarathonConfig() *types.Configuration { "getMaxConnAmount": p.getMaxConnAmount, "getLoadBalancerMethod": p.getLoadBalancerMethod, "getCircuitBreakerExpression": p.getCircuitBreakerExpression, - "getSticky": p.getSticky, + "getStickinessCookieName": p.getStickinessCookieName, + "hasStickinessLabel": p.hasStickinessLabel, "hasHealthCheckLabels": p.hasHealthCheckLabels, "getHealthCheckPath": p.getHealthCheckPath, "getHealthCheckInterval": p.getHealthCheckInterval, @@ -428,11 +429,22 @@ func (p *Provider) getProtocol(application marathon.Application, serviceName str return "http" } -func (p *Provider) getSticky(application marathon.Application) string { - if sticky, ok := p.getAppLabel(application, types.LabelBackendLoadbalancerSticky); ok { - return sticky +func (p *Provider) hasStickinessLabel(application marathon.Application) bool { + _, okStickiness := p.getAppLabel(application, types.LabelBackendLoadbalancerStickiness) + + label, okSticky := p.getAppLabel(application, types.LabelBackendLoadbalancerSticky) + if len(label) > 0 { + log.Warn("Deprecated configuration found: %s. Please use %s.", types.LabelBackendLoadbalancerSticky, types.LabelBackendLoadbalancerStickiness) } - return "false" + + return okStickiness || (okSticky && strings.EqualFold(strings.TrimSpace(label), "true")) +} + +func (p *Provider) getStickinessCookieName(application marathon.Application) string { + if label, ok := p.getAppLabel(application, types.LabelBackendLoadbalancerStickinessCookieName); ok { + return label + } + return "" } func (p *Provider) getPassHostHeader(application marathon.Application, serviceName string) string { diff --git a/provider/marathon/marathon_test.go b/provider/marathon/marathon_test.go index e5cc01e93..e78fd3f0b 100644 --- a/provider/marathon/marathon_test.go +++ b/provider/marathon/marathon_test.go @@ -855,21 +855,36 @@ func TestMarathonGetProtocol(t *testing.T) { } } -func TestMarathonGetSticky(t *testing.T) { +func TestMarathonHasStickinessLabel(t *testing.T) { cases := []struct { desc string application marathon.Application - expected string + expected bool }{ { desc: "label missing", application: application(), - expected: "false", + expected: false, }, { - desc: "label existing", + desc: "label existing and value equals true (deprecated)", application: application(label(types.LabelBackendLoadbalancerSticky, "true")), - expected: "true", + expected: true, + }, + { + desc: "label existing and value equals false (deprecated)", + application: application(label(types.LabelBackendLoadbalancerSticky, "false")), + expected: false, + }, + { + desc: "label existing and value equals true", + application: application(label(types.LabelBackendLoadbalancerStickiness, "true")), + expected: true, + }, + { + desc: "label existing and value equals false ", + application: application(label(types.LabelBackendLoadbalancerStickiness, "true")), + expected: true, }, } @@ -878,7 +893,7 @@ func TestMarathonGetSticky(t *testing.T) { t.Run(c.desc, func(t *testing.T) { t.Parallel() provider := &Provider{} - actual := provider.getSticky(c.application) + actual := provider.hasStickinessLabel(c.application) if actual != c.expected { t.Errorf("actual %q, expected %q", actual, c.expected) } diff --git a/provider/provider.go b/provider/provider.go index cc1e12edf..be727db80 100644 --- a/provider/provider.go +++ b/provider/provider.go @@ -34,7 +34,7 @@ type BaseProvider struct { // MatchConstraints must match with EVERY single contraint // returns first constraint that do not match or nil func (p *BaseProvider) MatchConstraints(tags []string) (bool, *types.Constraint) { - // if there is no tags and no contraints, filtering is disabled + // if there is no tags and no constraints, filtering is disabled if len(tags) == 0 && len(p.Constraints) == 0 { return true, nil } diff --git a/provider/rancher/rancher.go b/provider/rancher/rancher.go index 6fcb8fc1c..6327f460b 100644 --- a/provider/rancher/rancher.go +++ b/provider/rancher/rancher.go @@ -112,11 +112,22 @@ func (p *Provider) getCircuitBreakerExpression(service rancherData) string { return "NetworkErrorRatio() > 1" } -func (p *Provider) getSticky(service rancherData) string { - if _, err := getServiceLabel(service, types.LabelBackendLoadbalancerSticky); err == nil { - return "true" +func (p *Provider) hasStickinessLabel(service rancherData) bool { + _, errStickiness := getServiceLabel(service, types.LabelBackendLoadbalancerStickiness) + + label, errSticky := getServiceLabel(service, types.LabelBackendLoadbalancerSticky) + if len(label) > 0 { + log.Warn("Deprecated configuration found: %s. Please use %s.", types.LabelBackendLoadbalancerSticky, types.LabelBackendLoadbalancerStickiness) } - return "false" + + return errStickiness == nil || (errSticky == nil && strings.EqualFold(strings.TrimSpace(label), "true")) +} + +func (p *Provider) getStickinessCookieName(service rancherData, backendName string) string { + if label, err := getServiceLabel(service, types.LabelBackendLoadbalancerStickinessCookieName); err == nil { + return label + } + return "" } func (p *Provider) getBackend(service rancherData) string { @@ -222,7 +233,8 @@ func (p *Provider) loadRancherConfig(services []rancherData) *types.Configuratio "hasMaxConnLabels": p.hasMaxConnLabels, "getMaxConnAmount": p.getMaxConnAmount, "getMaxConnExtractorFunc": p.getMaxConnExtractorFunc, - "getSticky": p.getSticky, + "hasStickinessLabel": p.hasStickinessLabel, + "getStickinessCookieName": p.getStickinessCookieName, } // filter services diff --git a/server/cookie/cookie.go b/server/cookie/cookie.go new file mode 100644 index 000000000..780661963 --- /dev/null +++ b/server/cookie/cookie.go @@ -0,0 +1,57 @@ +package cookie + +import ( + "crypto/sha1" + "fmt" + "strings" + + "github.com/containous/traefik/log" +) + +const cookieNameLength = 6 + +// GetName of a cookie +func GetName(cookieName string, backendName string) string { + if len(cookieName) != 0 { + return sanitizeName(cookieName) + } + + return GenerateName(backendName) +} + +// GenerateName Generate a hashed name +func GenerateName(backendName string) string { + data := []byte("_TRAEFIK_BACKEND_" + backendName) + + hash := sha1.New() + _, err := hash.Write(data) + if err != nil { + // Impossible case + log.Errorf("Fail to create cookie name: %v", err) + } + + return fmt.Sprintf("_%x", hash.Sum(nil))[:cookieNameLength] +} + +// sanitizeName According to [RFC 2616](https://www.ietf.org/rfc/rfc2616.txt) section 2.2 +func sanitizeName(backend string) string { + sanitizer := func(r rune) rune { + switch r { + case '!', '#', '$', '%', '&', '\'', '*', '+', '-', '.', '^', '`', '|', '~': + return r + } + + switch { + case 'a' <= r && r <= 'z': + fallthrough + case 'A' <= r && r <= 'Z': + fallthrough + case '0' <= r && r <= '9': + return r + default: + return '_' + } + } + + return strings.Map(sanitizer, backend) +} diff --git a/server/cookie/cookie_test.go b/server/cookie/cookie_test.go new file mode 100644 index 000000000..799b906f3 --- /dev/null +++ b/server/cookie/cookie_test.go @@ -0,0 +1,83 @@ +package cookie + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestGetName(t *testing.T) { + testCases := []struct { + desc string + cookieName string + backendName string + expectedCookieName string + }{ + { + desc: "with backend name, without cookie name", + cookieName: "", + backendName: "/my/BACKEND-v1.0~rc1", + expectedCookieName: "_5f7bc", + }, + { + desc: "without backend name, with cookie name", + cookieName: "/my/BACKEND-v1.0~rc1", + backendName: "", + expectedCookieName: "_my_BACKEND-v1.0~rc1", + }, + { + desc: "with backend name, with cookie name", + cookieName: "containous", + backendName: "treafik", + expectedCookieName: "containous", + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + cookieName := GetName(test.cookieName, test.backendName) + + assert.Equal(t, test.expectedCookieName, cookieName) + }) + } +} + +func Test_sanitizeName(t *testing.T) { + testCases := []struct { + desc string + srcName string + expectedName string + }{ + { + desc: "with /", + srcName: "/my/BACKEND-v1.0~rc1", + expectedName: "_my_BACKEND-v1.0~rc1", + }, + { + desc: "some chars", + srcName: "!#$%&'()*+-./:<=>?@[]^_`{|}~", + expectedName: "!#$%&'__*+-._________^_`_|_~", + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + cookieName := sanitizeName(test.srcName) + + assert.Equal(t, test.expectedName, cookieName, "Cookie name") + }) + } +} + +func TestGenerateName(t *testing.T) { + cookieName := GenerateName("containous") + + assert.Len(t, "_8a7bc", 6) + assert.Equal(t, "_8a7bc", cookieName) +} diff --git a/server/server.go b/server/server.go index 370c5c79d..a25283e33 100644 --- a/server/server.go +++ b/server/server.go @@ -15,7 +15,6 @@ import ( "reflect" "regexp" "sort" - "strings" "sync" "time" @@ -31,6 +30,7 @@ import ( mauth "github.com/containous/traefik/middlewares/auth" "github.com/containous/traefik/provider" "github.com/containous/traefik/safe" + "github.com/containous/traefik/server/cookie" "github.com/containous/traefik/types" "github.com/streamrail/concurrent-map" thoas_stats "github.com/thoas/stats" @@ -335,11 +335,11 @@ func (server *Server) listenProviders(stop chan bool) { lastReceivedConfigurationValue := lastReceivedConfiguration.Get().(time.Time) providersThrottleDuration := time.Duration(server.globalConfiguration.ProvidersThrottleDuration) if time.Now().After(lastReceivedConfigurationValue.Add(providersThrottleDuration)) { - log.Debugf("Last %s config received more than %s, OK", configMsg.ProviderName, server.globalConfiguration.ProvidersThrottleDuration.String()) + log.Debugf("Last %s config received more than %s, OK", configMsg.ProviderName, server.globalConfiguration.ProvidersThrottleDuration) // last config received more than n s ago server.configurationValidatedChan <- configMsg } else { - log.Debugf("Last %s config received less than %s, waiting...", configMsg.ProviderName, server.globalConfiguration.ProvidersThrottleDuration.String()) + log.Debugf("Last %s config received less than %s, waiting...", configMsg.ProviderName, server.globalConfiguration.ProvidersThrottleDuration) safe.Go(func() { <-time.After(providersThrottleDuration) lastReceivedConfigurationValue := lastReceivedConfiguration.Get().(time.Time) @@ -826,11 +826,10 @@ func (server *Server) loadConfig(configurations types.Configurations, globalConf continue frontend } - stickySession := config.Backends[frontend.Backend].LoadBalancer.Sticky - cookieName := getCookieName(frontend.Backend) var sticky *roundrobin.StickySession - - if stickySession { + var cookieName string + if stickiness := config.Backends[frontend.Backend].LoadBalancer.Stickiness; stickiness != nil { + cookieName = cookie.GetName(stickiness.CookieName, frontend.Backend) sticky = roundrobin.NewStickySession(cookieName) } @@ -839,7 +838,7 @@ func (server *Server) loadConfig(configurations types.Configurations, globalConf case types.Drr: log.Debugf("Creating load-balancer drr") rebalancer, _ := roundrobin.NewRebalancer(rr, roundrobin.RebalancerLogger(oxyLogger)) - if stickySession { + if sticky != nil { log.Debugf("Sticky session with cookie %v", cookieName) rebalancer, _ = roundrobin.NewRebalancer(rr, roundrobin.RebalancerLogger(oxyLogger), roundrobin.RebalancerStickySession(sticky)) } @@ -856,7 +855,7 @@ func (server *Server) loadConfig(configurations types.Configurations, globalConf lb = middlewares.NewEmptyBackendHandler(rebalancer, lb) case types.Wrr: log.Debugf("Creating load-balancer wrr") - if stickySession { + if sticky != nil { log.Debugf("Sticky session with cookie %v", cookieName) if server.accessLoggerMiddleware != nil { rr, _ = roundrobin.New(saveFrontend, roundrobin.EnableStickySession(sticky)) @@ -1150,16 +1149,31 @@ func (server *Server) configureFrontends(frontends map[string]*types.Frontend) { func (*Server) configureBackends(backends map[string]*types.Backend) { for backendName, backend := range backends { + if backend.LoadBalancer != nil && backend.LoadBalancer.Sticky { + log.Warn("Deprecated configuration found: %s. Please use %s.", "backend.LoadBalancer.Sticky", "backend.LoadBalancer.Stickiness") + } + _, err := types.NewLoadBalancerMethod(backend.LoadBalancer) - if err != nil { + if err == nil { + if backend.LoadBalancer != nil && backend.LoadBalancer.Stickiness == nil && backend.LoadBalancer.Sticky { + backend.LoadBalancer.Stickiness = &types.Stickiness{} + } + } else { log.Debugf("Validation of load balancer method for backend %s failed: %s. Using default method wrr.", backendName, err) - var sticky bool + + var stickiness *types.Stickiness if backend.LoadBalancer != nil { - sticky = backend.LoadBalancer.Sticky + if backend.LoadBalancer.Stickiness != nil { + stickiness = backend.LoadBalancer.Stickiness + } else if backend.LoadBalancer.Sticky { + if backend.LoadBalancer.Stickiness == nil { + stickiness = &types.Stickiness{} + } + } } backend.LoadBalancer = &types.LoadBalancer{ - Method: "wrr", - Sticky: sticky, + Method: "wrr", + Stickiness: stickiness, } } } @@ -1209,28 +1223,3 @@ func (server *Server) buildRetryMiddleware(handler http.Handler, globalConfig co return middlewares.NewRetry(retryAttempts, handler, retryListeners) } - -// getCookieName returns a cookie name from the given backend, sanitizing -// characters that do not satisfy the requirements of RFC 2616. -func getCookieName(backend string) string { - const cookiePrefix = "_TRAEFIK_BACKEND_" - sanitizer := func(r rune) rune { - switch r { - case '!', '#', '$', '%', '&', '\'', '*', '+', '-', '.', '^', '`', '|', '~': - return r - } - - switch { - case r >= 'a' && r <= 'z': - fallthrough - case r >= 'A' && r <= 'Z': - fallthrough - case r >= '0' && r <= '9': - return r - default: - return '_' - } - } - - return cookiePrefix + strings.Map(sanitizer, backend) -} diff --git a/server/server_test.go b/server/server_test.go index bbac7b5d2..9fc83bae2 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -422,52 +422,49 @@ func TestConfigureBackends(t *testing.T) { defaultMethod := "wrr" tests := []struct { - desc string - lb *types.LoadBalancer - wantMethod string - wantSticky bool + desc string + lb *types.LoadBalancer + wantMethod string + wantStickiness *types.Stickiness }{ { desc: "valid load balancer method with sticky enabled", lb: &types.LoadBalancer{ - Method: validMethod, - Sticky: true, + Method: validMethod, + Stickiness: &types.Stickiness{}, }, - wantMethod: validMethod, - wantSticky: true, + wantMethod: validMethod, + wantStickiness: &types.Stickiness{}, }, { desc: "valid load balancer method with sticky disabled", lb: &types.LoadBalancer{ - Method: validMethod, - Sticky: false, + Method: validMethod, + Stickiness: nil, }, wantMethod: validMethod, - wantSticky: false, }, { desc: "invalid load balancer method with sticky enabled", lb: &types.LoadBalancer{ - Method: "Invalid", - Sticky: true, + Method: "Invalid", + Stickiness: &types.Stickiness{}, }, - wantMethod: defaultMethod, - wantSticky: true, + wantMethod: defaultMethod, + wantStickiness: &types.Stickiness{}, }, { desc: "invalid load balancer method with sticky disabled", lb: &types.LoadBalancer{ - Method: "Invalid", - Sticky: false, + Method: "Invalid", + Stickiness: nil, }, wantMethod: defaultMethod, - wantSticky: false, }, { desc: "missing load balancer", lb: nil, wantMethod: defaultMethod, - wantSticky: false, }, } @@ -485,8 +482,8 @@ func TestConfigureBackends(t *testing.T) { }) wantLB := types.LoadBalancer{ - Method: test.wantMethod, - Sticky: test.wantSticky, + Method: test.wantMethod, + Stickiness: test.wantStickiness, } if !reflect.DeepEqual(*backend.LoadBalancer, wantLB) { t.Errorf("got backend load-balancer\n%v\nwant\n%v\n", spew.Sdump(backend.LoadBalancer), spew.Sdump(wantLB)) @@ -659,13 +656,6 @@ func TestServerResponseEmptyBackend(t *testing.T) { } } -func TestGetCookieName(t *testing.T) { - want := "_TRAEFIK_BACKEND__my_BACKEND-v1.0~rc1" - if got := getCookieName("/my/BACKEND-v1.0~rc1"); got != want { - t.Errorf("got sticky cookie name %q, want %q", got, want) - } -} - func buildDynamicConfig(dynamicConfigBuilders ...func(*types.Configuration)) *types.Configuration { config := &types.Configuration{ Frontends: make(map[string]*types.Frontend), @@ -726,6 +716,10 @@ func withServer(name, url string) func(backend *types.Backend) { func withLoadBalancer(method string, sticky bool) func(*types.Backend) { return func(be *types.Backend) { - be.LoadBalancer = &types.LoadBalancer{Method: method, Sticky: sticky} + if sticky { + be.LoadBalancer = &types.LoadBalancer{Method: method, Stickiness: &types.Stickiness{CookieName: "test"}} + } else { + be.LoadBalancer = &types.LoadBalancer{Method: method} + } } } diff --git a/templates/consul_catalog.tmpl b/templates/consul_catalog.tmpl index edcf9a7a1..b055a4aa4 100644 --- a/templates/consul_catalog.tmpl +++ b/templates/consul_catalog.tmpl @@ -17,8 +17,12 @@ {{end}} [backends."backend-{{$service}}".loadbalancer] - sticky = {{getAttribute "backend.loadbalancer.sticky" .Attributes "false"}} method = "{{getAttribute "backend.loadbalancer" .Attributes "wrr"}}" + sticky = {{getAttribute "backend.loadbalancer.sticky" .Attributes "false"}} + {{if hasStickinessLabel .Attributes}} + [Backends."backend-{{$service}}".LoadBalancer.Stickiness] + cookieName = {{getStickinessCookieName .Attributes}} + {{end}} {{if hasMaxconnAttributes .Attributes}} [backends."backend-{{$service}}".maxconn] diff --git a/templates/docker.tmpl b/templates/docker.tmpl index 50dafc423..6e18a9904 100644 --- a/templates/docker.tmpl +++ b/templates/docker.tmpl @@ -8,7 +8,10 @@ {{if hasLoadBalancerLabel $backend}} [backends.backend-{{$backendName}}.loadbalancer] method = "{{getLoadBalancerMethod $backend}}" - sticky = {{getSticky $backend}} + {{if hasStickinessLabel $backend}} + [Backends."{{$backendName}}".LoadBalancer.Stickiness] + cookieName = {{getStickinessCookieName $backend}} + {{end}} {{end}} {{if hasMaxConnLabels $backend}} diff --git a/templates/ecs.tmpl b/templates/ecs.tmpl index 36e9e29b4..a325f7539 100644 --- a/templates/ecs.tmpl +++ b/templates/ecs.tmpl @@ -1,7 +1,10 @@ [backends]{{range $serviceName, $instances := .Services}} [backends.backend-{{ $serviceName }}.loadbalancer] - sticky = {{ getLoadBalancerSticky $instances}} method = "{{ getLoadBalancerMethod $instances}}" + {{if hasStickinessLabel $instances}} + [Backends.backend-{{ $serviceName }}.LoadBalancer.Stickiness] + cookieName = {{getStickinessCookieName $instances}} + {{end}} {{range $index, $i := $instances}} [backends.backend-{{ $i.Name }}.servers.server-{{ $i.Name }}{{ $i.ID }}] diff --git a/templates/kubernetes.tmpl b/templates/kubernetes.tmpl index be70e9a80..47fec081c 100644 --- a/templates/kubernetes.tmpl +++ b/templates/kubernetes.tmpl @@ -6,8 +6,9 @@ {{end}} [backends."{{$backendName}}".loadbalancer] method = "{{$backend.LoadBalancer.Method}}" - {{if $backend.LoadBalancer.Sticky}} - sticky = true + {{if $backend.LoadBalancer.Stickiness}} + [Backends."{{$backendName}}".LoadBalancer.Stickiness] + cookieName = {{$backend.LoadBalancer.Stickiness.CookieName}} {{end}} {{range $serverName, $server := $backend.Servers}} [backends."{{$backendName}}".servers."{{$serverName}}"] diff --git a/templates/kv.tmpl b/templates/kv.tmpl index 88a17f114..ebbbb39c2 100644 --- a/templates/kv.tmpl +++ b/templates/kv.tmpl @@ -3,25 +3,29 @@ [backends]{{range $backends}} {{$backend := .}} +{{$backendName := Last $backend}} {{$servers := ListServers $backend }} {{$circuitBreaker := Get "" . "/circuitbreaker/" "expression"}} {{with $circuitBreaker}} -[backends."{{Last $backend}}".circuitBreaker] +[backends."{{$backendName}}".circuitBreaker] expression = "{{$circuitBreaker}}" {{end}} {{$loadBalancer := Get "" . "/loadbalancer/" "method"}} -{{$sticky := Get "false" . "/loadbalancer/" "sticky"}} {{with $loadBalancer}} -[backends."{{Last $backend}}".loadBalancer] +[backends."{{$backendName}}".loadBalancer] method = "{{$loadBalancer}}" - sticky = {{$sticky}} + sticky = {{ Get "false" . "/loadbalancer/" "sticky" }} + {{if hasStickinessLabel $backend}} + [Backends."{{$backendName}}".LoadBalancer.Stickiness] + cookieName = {{getStickinessCookieName $backend}} + {{end}} {{end}} {{$healthCheck := Get "" . "/healthcheck/" "path"}} {{with $healthCheck}} -[backends."{{Last $backend}}".healthCheck] +[backends."{{$backendName}}".healthCheck] path = "{{$healthCheck}}" interval = "{{ Get "30s" $backend "/healthcheck/" "interval" }}" {{end}} @@ -30,14 +34,14 @@ {{$maxConnExtractorFunc := Get "" . "/maxconn/" "extractorfunc"}} {{with $maxConnAmt}} {{with $maxConnExtractorFunc}} -[backends."{{Last $backend}}".maxConn] +[backends."{{$backendName}}".maxConn] amount = {{$maxConnAmt}} extractorFunc = "{{$maxConnExtractorFunc}}" {{end}} {{end}} {{range $servers}} -[backends."{{Last $backend}}".servers."{{Last .}}"] +[backends."{{$backendName}}".servers."{{Last .}}"] url = "{{Get "" . "/url"}}" weight = {{Get "0" . "/weight"}} {{end}} diff --git a/templates/marathon.tmpl b/templates/marathon.tmpl index 5292ebb37..e5c802a63 100644 --- a/templates/marathon.tmpl +++ b/templates/marathon.tmpl @@ -20,7 +20,10 @@ {{ if hasLoadBalancerLabels $app }} [backends."backend{{getBackend $app $serviceName }}".loadbalancer] method = "{{getLoadBalancerMethod $app }}" - sticky = {{getSticky $app}} + {{if hasStickinessLabel $app}} + [Backends."backend{{getBackend $app $serviceName }}".LoadBalancer.Stickiness] + cookieName = {{getStickinessCookieName $app}} + {{end}} {{end}} {{ if hasCircuitBreakerLabels $app }} [backends."backend{{getBackend $app $serviceName }}".circuitbreaker] diff --git a/templates/rancher.tmpl b/templates/rancher.tmpl index f98bc3289..90a10ac56 100644 --- a/templates/rancher.tmpl +++ b/templates/rancher.tmpl @@ -8,7 +8,10 @@ {{if hasLoadBalancerLabel $backend}} [backends.backend-{{$backendName}}.loadbalancer] method = "{{getLoadBalancerMethod $backend}}" - sticky = {{getSticky $backend}} + {{if hasStickinessLabel $backend}} + [Backends."{{$backendName}}".LoadBalancer.Stickiness] + cookieName = {{getStickinessCookieName $backend}} + {{end}} {{end}} {{if hasMaxConnLabels $backend}} diff --git a/types/common_label.go b/types/common_label.go index e22f53c62..1f974615a 100644 --- a/types/common_label.go +++ b/types/common_label.go @@ -2,59 +2,36 @@ package types import "strings" +// Traefik labels const ( - // LabelPrefix Traefik label - LabelPrefix = "traefik." - // LabelDomain Traefik label - LabelDomain = LabelPrefix + "domain" - // LabelEnable Traefik label - LabelEnable = LabelPrefix + "enable" - // LabelPort Traefik label - LabelPort = LabelPrefix + "port" - // LabelPortIndex Traefik label - LabelPortIndex = LabelPrefix + "portIndex" - // LabelProtocol Traefik label - LabelProtocol = LabelPrefix + "protocol" - // LabelTags Traefik label - LabelTags = LabelPrefix + "tags" - // LabelWeight Traefik label - LabelWeight = LabelPrefix + "weight" - // LabelFrontendAuthBasic Traefik label - LabelFrontendAuthBasic = LabelPrefix + "frontend.auth.basic" - // LabelFrontendEntryPoints Traefik label - LabelFrontendEntryPoints = LabelPrefix + "frontend.entryPoints" - // LabelFrontendPassHostHeader Traefik label - LabelFrontendPassHostHeader = LabelPrefix + "frontend.passHostHeader" - // LabelFrontendPriority Traefik label - LabelFrontendPriority = LabelPrefix + "frontend.priority" - // LabelFrontendRule Traefik label - LabelFrontendRule = LabelPrefix + "frontend.rule" - // LabelFrontendRuleType Traefik label - LabelFrontendRuleType = LabelPrefix + "frontend.rule.type" - // LabelTraefikFrontendValue Traefik label - LabelTraefikFrontendValue = LabelPrefix + "frontend.value" - // LabelTraefikFrontendWhitelistSourceRange Traefik label - LabelTraefikFrontendWhitelistSourceRange = LabelPrefix + "frontend.whitelistSourceRange" - // LabelBackend Traefik label - LabelBackend = LabelPrefix + "backend" - // LabelBackendID Traefik label - LabelBackendID = LabelPrefix + "backend.id" - // LabelTraefikBackendCircuitbreaker Traefik label - LabelTraefikBackendCircuitbreaker = LabelPrefix + "backend.circuitbreaker" - // LabelBackendCircuitbreakerExpression Traefik label - LabelBackendCircuitbreakerExpression = LabelPrefix + "backend.circuitbreaker.expression" - // LabelBackendHealthcheckPath Traefik label - LabelBackendHealthcheckPath = LabelPrefix + "backend.healthcheck.path" - // LabelBackendHealthcheckInterval Traefik label - LabelBackendHealthcheckInterval = LabelPrefix + "backend.healthcheck.interval" - // LabelBackendLoadbalancerMethod Traefik label - LabelBackendLoadbalancerMethod = LabelPrefix + "backend.loadbalancer.method" - // LabelBackendLoadbalancerSticky Traefik label - LabelBackendLoadbalancerSticky = LabelPrefix + "backend.loadbalancer.sticky" - // LabelBackendMaxconnAmount Traefik label - LabelBackendMaxconnAmount = LabelPrefix + "backend.maxconn.amount" - // LabelBackendMaxconnExtractorfunc Traefik label - LabelBackendMaxconnExtractorfunc = LabelPrefix + "backend.maxconn.extractorfunc" + LabelPrefix = "traefik." + LabelDomain = LabelPrefix + "domain" + LabelEnable = LabelPrefix + "enable" + LabelPort = LabelPrefix + "port" + LabelPortIndex = LabelPrefix + "portIndex" + LabelProtocol = LabelPrefix + "protocol" + LabelTags = LabelPrefix + "tags" + LabelWeight = LabelPrefix + "weight" + LabelFrontendAuthBasic = LabelPrefix + "frontend.auth.basic" + LabelFrontendEntryPoints = LabelPrefix + "frontend.entryPoints" + LabelFrontendPassHostHeader = LabelPrefix + "frontend.passHostHeader" + LabelFrontendPriority = LabelPrefix + "frontend.priority" + LabelFrontendRule = LabelPrefix + "frontend.rule" + LabelFrontendRuleType = LabelPrefix + "frontend.rule.type" + LabelTraefikFrontendValue = LabelPrefix + "frontend.value" + LabelTraefikFrontendWhitelistSourceRange = LabelPrefix + "frontend.whitelistSourceRange" + LabelBackend = LabelPrefix + "backend" + LabelBackendID = LabelPrefix + "backend.id" + LabelTraefikBackendCircuitbreaker = LabelPrefix + "backend.circuitbreaker" + LabelBackendCircuitbreakerExpression = LabelPrefix + "backend.circuitbreaker.expression" + LabelBackendHealthcheckPath = LabelPrefix + "backend.healthcheck.path" + LabelBackendHealthcheckInterval = LabelPrefix + "backend.healthcheck.interval" + LabelBackendLoadbalancerMethod = LabelPrefix + "backend.loadbalancer.method" + LabelBackendLoadbalancerSticky = LabelPrefix + "backend.loadbalancer.sticky" + LabelBackendLoadbalancerStickiness = LabelPrefix + "backend.loadbalancer.stickiness" + LabelBackendLoadbalancerStickinessCookieName = LabelPrefix + "backend.loadbalancer.stickiness.cookieName" + LabelBackendMaxconnAmount = LabelPrefix + "backend.maxconn.amount" + LabelBackendMaxconnExtractorfunc = LabelPrefix + "backend.maxconn.extractorfunc" ) //ServiceLabel converts a key value of Label*, given a serviceName, into a pattern .. diff --git a/types/types.go b/types/types.go index 2bf2b9cf7..fb67af6ad 100644 --- a/types/types.go +++ b/types/types.go @@ -33,8 +33,14 @@ type MaxConn struct { // LoadBalancer holds load balancing configuration. type LoadBalancer struct { - Method string `json:"method,omitempty"` - Sticky bool `json:"sticky,omitempty"` + Method string `json:"method,omitempty"` + Sticky bool `json:"sticky,omitempty"` // Deprecated: use Stickiness instead + Stickiness *Stickiness `json:"stickiness,omitempty"` +} + +// Stickiness holds sticky session configuration. +type Stickiness struct { + CookieName string `json:"cookieName,omitempty"` } // CircuitBreaker holds circuit breaker configuration. From 93a1db77c5430f1e0ea8e68e74ac687f03c7a74c Mon Sep 17 00:00:00 2001 From: SALLEYRON Julien Date: Tue, 10 Oct 2017 12:14:03 +0200 Subject: [PATCH 11/14] Move http2 configure transport --- docs/user-guide/grpc.md | 7 +++- .../fixtures/grpc/config_insecure.toml | 29 +++++++++++++ integration/grpc_test.go | 42 +++++++++++++++++++ server/server.go | 2 +- 4 files changed, 77 insertions(+), 3 deletions(-) create mode 100644 integration/fixtures/grpc/config_insecure.toml diff --git a/docs/user-guide/grpc.md b/docs/user-guide/grpc.md index 53126c3ec..f4f609d92 100644 --- a/docs/user-guide/grpc.md +++ b/docs/user-guide/grpc.md @@ -3,7 +3,7 @@ This section explains how to use Traefik as reverse proxy for gRPC application with self-signed certificates. !!! warning - As gRPC needs HTTP2, we need valid HTTPS certificates on both gRPC Server and Træfik. + As gRPC needs HTTP2, we need HTTPS certificates on both gRPC Server and Træfik.

gRPC architecture @@ -76,9 +76,12 @@ RootCAs = [ "./backend.cert" ] rule = "Host:frontend.local" ``` +!!! warning + With some backends, the server URLs use the IP, so you may need to configure `InsecureSkipVerify` instead of the `RootCAS` to activate HTTPS without hostname verification. + ## Conclusion -We don't need specific configuration to use gRPC in Træfik, we just need to be careful that all the exchanges (between client and Træfik, and between Træfik and backend) are valid HTTPS communications (without `InsecureSkipVerify` enabled) because gRPC use HTTP2. +We don't need specific configuration to use gRPC in Træfik, we just need to be careful that all the exchanges (between client and Træfik, and between Træfik and backend) are HTTPS communications because gRPC uses HTTP2. ## A gRPC example in go diff --git a/integration/fixtures/grpc/config_insecure.toml b/integration/fixtures/grpc/config_insecure.toml new file mode 100644 index 000000000..66285ce06 --- /dev/null +++ b/integration/fixtures/grpc/config_insecure.toml @@ -0,0 +1,29 @@ +defaultEntryPoints = ["https"] + +InsecureSkipVerify = true + +[entryPoints] + [entryPoints.https] + address = ":4443" + [entryPoints.https.tls] + [[entryPoints.https.tls.certificates]] + CertFile = """{{ .CertContent }}""" + KeyFile = """{{ .KeyContent }}""" + + +[web] + address = ":8080" + +[file] + +[backends] + [backends.backend1] + [backends.backend1.servers.server1] + url = "https://127.0.0.1:{{ .GRPCServerPort }}" + + +[frontends] + [frontends.frontend1] + backend = "backend1" + [frontends.frontend1.routes.test_1] + rule = "Host:127.0.0.1" diff --git a/integration/grpc_test.go b/integration/grpc_test.go index f9ec89b7b..d597b289c 100644 --- a/integration/grpc_test.go +++ b/integration/grpc_test.go @@ -113,3 +113,45 @@ func (s *GRPCSuite) TestGRPC(c *check.C) { c.Assert(err, check.IsNil) c.Assert(response, check.Equals, "Hello World") } + +func (s *GRPCSuite) TestGRPCInsecure(c *check.C) { + lis, err := net.Listen("tcp", ":0") + _, port, err := net.SplitHostPort(lis.Addr().String()) + c.Assert(err, check.IsNil) + + go func() { + err := startGRPCServer(lis) + c.Log(err) + c.Assert(err, check.IsNil) + }() + + file := s.adaptFile(c, "fixtures/grpc/config_insecure.toml", struct { + CertContent string + KeyContent string + GRPCServerPort string + }{ + CertContent: string(LocalhostCert), + KeyContent: string(LocalhostKey), + GRPCServerPort: port, + }) + + defer os.Remove(file) + cmd, display := s.traefikCmd(withConfigFile(file)) + defer display(c) + + err = cmd.Start() + c.Assert(err, check.IsNil) + defer cmd.Process.Kill() + + // wait for Traefik + err = try.GetRequest("http://127.0.0.1:8080/api/providers", 1*time.Second, try.BodyContains("Host:127.0.0.1")) + c.Assert(err, check.IsNil) + var response string + err = try.Do(1*time.Second, func() error { + response, err = callHelloClientGRPC() + return err + }) + + c.Assert(err, check.IsNil) + c.Assert(response, check.Equals, "Hello World") +} diff --git a/server/server.go b/server/server.go index a25283e33..25b990a52 100644 --- a/server/server.go +++ b/server/server.go @@ -154,8 +154,8 @@ func createHTTPTransport(globalConfiguration configuration.GlobalConfiguration) transport.TLSClientConfig = &tls.Config{ RootCAs: createRootCACertPool(globalConfiguration.RootCAs), } - http2.ConfigureTransport(transport) } + http2.ConfigureTransport(transport) return transport } From 6287a3dd539866e29e47b1f626a2f24caa642f18 Mon Sep 17 00:00:00 2001 From: Emile Vauge Date: Tue, 10 Oct 2017 14:50:03 +0200 Subject: [PATCH 12/14] Add trusted whitelist proxy protocol --- .../anonymize/anonymize_config_test.go | 8 +- configuration/configuration.go | 20 ++- configuration/configuration_test.go | 20 +-- docs/configuration/entrypoints.md | 8 +- middlewares/ip_whitelister.go | 76 ++++----- server/server.go | 20 ++- server/server_test.go | 4 +- whitelist/ip.go | 75 ++++++++ .../ip_test.go | 161 ++++++++++-------- 9 files changed, 249 insertions(+), 143 deletions(-) create mode 100644 whitelist/ip.go rename middlewares/ip_whitelister_test.go => whitelist/ip_test.go (65%) diff --git a/cmd/traefik/anonymize/anonymize_config_test.go b/cmd/traefik/anonymize/anonymize_config_test.go index 43b0844d2..3ec49cddd 100644 --- a/cmd/traefik/anonymize/anonymize_config_test.go +++ b/cmd/traefik/anonymize/anonymize_config_test.go @@ -84,7 +84,9 @@ func TestDo_globalConfiguration(t *testing.T) { }, WhitelistSourceRange: []string{"foo WhitelistSourceRange 1", "foo WhitelistSourceRange 2", "foo WhitelistSourceRange 3"}, Compress: true, - ProxyProtocol: true, + ProxyProtocol: &configuration.ProxyProtocol{ + TrustedIPs: []string{"127.0.0.1/32", "192.168.0.1"}, + }, }, "fii": { Network: "fii Network", @@ -125,7 +127,9 @@ func TestDo_globalConfiguration(t *testing.T) { }, WhitelistSourceRange: []string{"fii WhitelistSourceRange 1", "fii WhitelistSourceRange 2", "fii WhitelistSourceRange 3"}, Compress: true, - ProxyProtocol: true, + ProxyProtocol: &configuration.ProxyProtocol{ + TrustedIPs: []string{"127.0.0.1/32", "192.168.0.1"}, + }, }, } config.Cluster = &types.Cluster{ diff --git a/configuration/configuration.go b/configuration/configuration.go index f3e91fbd0..b112af7e6 100644 --- a/configuration/configuration.go +++ b/configuration/configuration.go @@ -231,7 +231,14 @@ func (ep *EntryPoints) Set(value string) error { } compress := toBool(result, "Compress") - proxyProtocol := toBool(result, "ProxyProtocol") + + var proxyProtocol *ProxyProtocol + if len(result["ProxyProtocol"]) > 0 { + trustedIPs := strings.Split(result["ProxyProtocol"], ",") + proxyProtocol = &ProxyProtocol{ + TrustedIPs: trustedIPs, + } + } (*ep)[result["Name"]] = &EntryPoint{ Address: result["Address"], @@ -246,7 +253,7 @@ func (ep *EntryPoints) Set(value string) error { } func parseEntryPointsConfiguration(value string) (map[string]string, error) { - regex := regexp.MustCompile(`(?:Name:(?P\S*))\s*(?:Address:(?P

\S*))?\s*(?:TLS:(?P\S*))?\s*(?PTLS)?\s*(?:CA:(?P\S*))?\s*(?:Redirect\.EntryPoint:(?P\S*))?\s*(?:Redirect\.Regex:(?P\S*))?\s*(?:Redirect\.Replacement:(?P\S*))?\s*(?:Compress:(?P\S*))?\s*(?:WhiteListSourceRange:(?P\S*))?\s*(?:ProxyProtocol:(?P\S*))?`) + regex := regexp.MustCompile(`(?:Name:(?P\S*))\s*(?:Address:(?P
\S*))?\s*(?:TLS:(?P\S*))?\s*(?PTLS)?\s*(?:CA:(?P\S*))?\s*(?:Redirect\.EntryPoint:(?P\S*))?\s*(?:Redirect\.Regex:(?P\S*))?\s*(?:Redirect\.Replacement:(?P\S*))?\s*(?:Compress:(?P\S*))?\s*(?:WhiteListSourceRange:(?P\S*))?\s*(?:ProxyProtocol\.TrustedIPs:(?P\S*))?`) match := regex.FindAllStringSubmatch(value, -1) if match == nil { return nil, fmt.Errorf("bad EntryPoints format: %s", value) @@ -293,8 +300,8 @@ type EntryPoint struct { Redirect *Redirect `export:"true"` Auth *types.Auth `export:"true"` WhitelistSourceRange []string - Compress bool `export:"true"` - ProxyProtocol bool `export:"true"` + Compress bool `export:"true"` + ProxyProtocol *ProxyProtocol `export:"true"` } // Redirect configures a redirection of an entry point to another, or to an URL @@ -443,3 +450,8 @@ type ForwardingTimeouts struct { DialTimeout flaeg.Duration `description:"The amount of time to wait until a connection to a backend server can be established. Defaults to 30 seconds. If zero, no timeout exists" export:"true"` ResponseHeaderTimeout flaeg.Duration `description:"The amount of time to wait for a server's response headers after fully writing the request (including its body, if any). If zero, no timeout exists" export:"true"` } + +// ProxyProtocol contains Proxy-Protocol configuration +type ProxyProtocol struct { + TrustedIPs []string +} diff --git a/configuration/configuration_test.go b/configuration/configuration_test.go index 5d476e4df..591e32d91 100644 --- a/configuration/configuration_test.go +++ b/configuration/configuration_test.go @@ -16,7 +16,7 @@ func Test_parseEntryPointsConfiguration(t *testing.T) { }{ { name: "all parameters", - value: "Name:foo Address:bar TLS:goo TLS CA:car Redirect.EntryPoint:RedirectEntryPoint Redirect.Regex:RedirectRegex Redirect.Replacement:RedirectReplacement Compress:true WhiteListSourceRange:WhiteListSourceRange ProxyProtocol:true", + value: "Name:foo Address:bar TLS:goo TLS CA:car Redirect.EntryPoint:RedirectEntryPoint Redirect.Regex:RedirectRegex Redirect.Replacement:RedirectReplacement Compress:true WhiteListSourceRange:WhiteListSourceRange ProxyProtocol.TrustedIPs:192.168.0.1", expectedResult: map[string]string{ "Name": "foo", "Address": "bar", @@ -27,18 +27,10 @@ func Test_parseEntryPointsConfiguration(t *testing.T) { "RedirectRegex": "RedirectRegex", "RedirectReplacement": "RedirectReplacement", "WhiteListSourceRange": "WhiteListSourceRange", - "ProxyProtocol": "true", + "ProxyProtocol": "192.168.0.1", "Compress": "true", }, }, - { - name: "proxy protocol on", - value: "Name:foo ProxyProtocol:on", - expectedResult: map[string]string{ - "Name": "foo", - "ProxyProtocol": "on", - }, - }, { name: "compress on", value: "Name:foo Compress:on", @@ -142,7 +134,7 @@ func TestEntryPoints_Set(t *testing.T) { }{ { name: "all parameters", - expression: "Name:foo Address:bar TLS:goo,gii TLS CA:car Redirect.EntryPoint:RedirectEntryPoint Redirect.Regex:RedirectRegex Redirect.Replacement:RedirectReplacement Compress:true WhiteListSourceRange:Range ProxyProtocol:true", + expression: "Name:foo Address:bar TLS:goo,gii TLS CA:car Redirect.EntryPoint:RedirectEntryPoint Redirect.Regex:RedirectRegex Redirect.Replacement:RedirectReplacement Compress:true WhiteListSourceRange:Range ProxyProtocol.TrustedIPs:192.168.0.1", expectedEntryPointName: "foo", expectedEntryPoint: &EntryPoint{ Address: "bar", @@ -151,8 +143,10 @@ func TestEntryPoints_Set(t *testing.T) { Regex: "RedirectRegex", Replacement: "RedirectReplacement", }, - Compress: true, - ProxyProtocol: true, + Compress: true, + ProxyProtocol: &ProxyProtocol{ + TrustedIPs: []string{"192.168.0.1"}, + }, WhitelistSourceRange: []string{"Range"}, TLS: &TLS{ ClientCAFiles: []string{"car"}, diff --git a/docs/configuration/entrypoints.md b/docs/configuration/entrypoints.md index 9fe9799ff..3defd7fd5 100644 --- a/docs/configuration/entrypoints.md +++ b/docs/configuration/entrypoints.md @@ -185,16 +185,20 @@ To enable IP whitelisting at the entrypoint level. [entryPoints] [entryPoints.http] address = ":80" - whiteListSourceRange = ["127.0.0.1/32"] + whiteListSourceRange = ["127.0.0.1/32", "192.168.1.7"] ``` ## ProxyProtocol Support To enable [ProxyProtocol](https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt) support. +Only IPs in `trustedIPs` will lead to remote client address replacement: you should declare your load-balancer IP or CIDR range here. + ```toml [entryPoints] [entryPoints.http] address = ":80" - proxyprotocol = true + [entryPoints.http.proxyProtocol] + trustedIPs = ["127.0.0.1/32", "192.168.1.7"] ``` +² \ No newline at end of file diff --git a/middlewares/ip_whitelister.go b/middlewares/ip_whitelister.go index 847762236..490986c29 100644 --- a/middlewares/ip_whitelister.go +++ b/middlewares/ip_whitelister.go @@ -6,80 +6,70 @@ import ( "net/http" "github.com/containous/traefik/log" + "github.com/containous/traefik/whitelist" "github.com/pkg/errors" "github.com/urfave/negroni" ) -// IPWhitelister is a middleware that provides Checks of the Requesting IP against a set of Whitelists -type IPWhitelister struct { - handler negroni.Handler - whitelists []*net.IPNet +// IPWhiteLister is a middleware that provides Checks of the Requesting IP against a set of Whitelists +type IPWhiteLister struct { + handler negroni.Handler + whiteLister *whitelist.IP } -// NewIPWhitelister builds a new IPWhitelister given a list of CIDR-Strings to whitelist -func NewIPWhitelister(whitelistStrings []string) (*IPWhitelister, error) { +// NewIPWhitelister builds a new IPWhiteLister given a list of CIDR-Strings to whitelist +func NewIPWhitelister(whitelistStrings []string) (*IPWhiteLister, error) { if len(whitelistStrings) == 0 { return nil, errors.New("no whitelists provided") } - whitelister := IPWhitelister{} + whiteLister := IPWhiteLister{} - for _, whitelistString := range whitelistStrings { - _, whitelist, err := net.ParseCIDR(whitelistString) - if err != nil { - return nil, fmt.Errorf("parsing CIDR whitelist %s: %v", whitelist, err) - } - whitelister.whitelists = append(whitelister.whitelists, whitelist) + ip, err := whitelist.NewIP(whitelistStrings) + if err != nil { + return nil, fmt.Errorf("parsing CIDR whitelist %s: %v", whitelistStrings, err) } + whiteLister.whiteLister = ip - whitelister.handler = negroni.HandlerFunc(whitelister.handle) - log.Debugf("configured %u IP whitelists: %s", len(whitelister.whitelists), whitelister.whitelists) + whiteLister.handler = negroni.HandlerFunc(whiteLister.handle) + log.Debugf("configured %u IP whitelists: %s", len(whitelistStrings), whitelistStrings) - return &whitelister, nil + return &whiteLister, nil } -func (whitelister *IPWhitelister) handle(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) { - remoteIP, err := ipFromRemoteAddr(r.RemoteAddr) +func (wl *IPWhiteLister) handle(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) { + ipAddress, _, err := net.SplitHostPort(r.RemoteAddr) if err != nil { log.Warnf("unable to parse remote-address from header: %s - rejecting", r.RemoteAddr) reject(w) return } - for _, whitelist := range whitelister.whitelists { - if whitelist.Contains(*remoteIP) { - log.Debugf("source-IP %s matched whitelist %s - passing", remoteIP, whitelist) - next.ServeHTTP(w, r) - return - } + allowed, ip, err := wl.whiteLister.Contains(ipAddress) + if err != nil { + log.Debugf("source-IP %s matched none of the whitelists - rejecting", ipAddress) + reject(w) + return } - log.Debugf("source-IP %s matched none of the whitelists - rejecting", remoteIP) + if allowed { + log.Debugf("source-IP %s matched whitelist %s - passing", ipAddress, wl.whiteLister) + next.ServeHTTP(w, r) + return + } + + log.Debugf("source-IP %s matched none of the whitelists - rejecting", ip) reject(w) } +func (wl *IPWhiteLister) ServeHTTP(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc) { + wl.handler.ServeHTTP(rw, r, next) +} + func reject(w http.ResponseWriter) { statusCode := http.StatusForbidden w.WriteHeader(statusCode) w.Write([]byte(http.StatusText(statusCode))) } - -func ipFromRemoteAddr(addr string) (*net.IP, error) { - ip, _, err := net.SplitHostPort(addr) - if err != nil { - return nil, fmt.Errorf("can't extract IP/Port from address %s: %s", addr, err) - } - - userIP := net.ParseIP(ip) - if userIP == nil { - return nil, fmt.Errorf("can't parse IP from address %s", ip) - } - - return &userIP, nil -} - -func (whitelister *IPWhitelister) ServeHTTP(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc) { - whitelister.handler.ServeHTTP(rw, r, next) -} diff --git a/server/server.go b/server/server.go index 25b990a52..24360d967 100644 --- a/server/server.go +++ b/server/server.go @@ -6,6 +6,7 @@ import ( "crypto/x509" "encoding/json" "errors" + "fmt" "io/ioutil" "net" "net/http" @@ -32,6 +33,7 @@ import ( "github.com/containous/traefik/safe" "github.com/containous/traefik/server/cookie" "github.com/containous/traefik/types" + "github.com/containous/traefik/whitelist" "github.com/streamrail/concurrent-map" thoas_stats "github.com/thoas/stats" "github.com/urfave/negroni" @@ -652,8 +654,22 @@ func (server *Server) prepareServer(entryPointName string, entryPoint *configura return nil, nil, err } - if entryPoint.ProxyProtocol { - listener = &proxyproto.Listener{Listener: listener} + if entryPoint.ProxyProtocol != nil { + IPs, err := whitelist.NewIP(entryPoint.ProxyProtocol.TrustedIPs) + if err != nil { + return nil, nil, fmt.Errorf("Error creating whitelist: %s", err) + } + log.Infof("Enabling ProxyProtocol for trusted IPs %v", entryPoint.ProxyProtocol.TrustedIPs) + listener = &proxyproto.Listener{ + Listener: listener, + SourceCheck: func(addr net.Addr) (bool, error) { + ip, ok := addr.(*net.TCPAddr) + if !ok { + return false, fmt.Errorf("Type error %v", addr) + } + return IPs.ContainsIP(ip.IP) + }, + } } return &http.Server{ diff --git a/server/server_test.go b/server/server_test.go index 9fc83bae2..a5ec9b091 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -355,7 +355,7 @@ func TestNewServerWithWhitelistSourceRange(t *testing.T) { "foo", }, middlewareConfigured: false, - errMessage: "parsing CIDR whitelist : invalid CIDR address: foo", + errMessage: "parsing CIDR whitelist [foo]: parsing CIDR whitelist : invalid CIDR address: foo", }, } @@ -536,7 +536,7 @@ func TestServerEntrypointWhitelistConfig(t *testing.T) { handler := srvEntryPoint.httpServer.Handler.(*negroni.Negroni) found := false for _, handler := range handler.Handlers() { - if reflect.TypeOf(handler) == reflect.TypeOf((*middlewares.IPWhitelister)(nil)) { + if reflect.TypeOf(handler) == reflect.TypeOf((*middlewares.IPWhiteLister)(nil)) { found = true } } diff --git a/whitelist/ip.go b/whitelist/ip.go new file mode 100644 index 000000000..af3b691e7 --- /dev/null +++ b/whitelist/ip.go @@ -0,0 +1,75 @@ +package whitelist + +import ( + "fmt" + "net" + + "github.com/pkg/errors" +) + +// IP allows to check that addresses are in a white list +type IP struct { + whiteListsIPs []*net.IP + whiteListsNet []*net.IPNet +} + +// NewIP builds a new IP given a list of CIDR-Strings to whitelist +func NewIP(whitelistStrings []string) (*IP, error) { + if len(whitelistStrings) == 0 { + return nil, errors.New("no whiteListsNet provided") + } + + ip := IP{} + + for _, whitelistString := range whitelistStrings { + ipAddr := net.ParseIP(whitelistString) + if ipAddr != nil { + ip.whiteListsIPs = append(ip.whiteListsIPs, &ipAddr) + } else { + _, whitelist, err := net.ParseCIDR(whitelistString) + if err != nil { + return nil, fmt.Errorf("parsing CIDR whitelist %s: %v", whitelist, err) + } + ip.whiteListsNet = append(ip.whiteListsNet, whitelist) + } + } + + return &ip, nil +} + +// Contains checks if provided address is in the white list +func (ip *IP) Contains(addr string) (bool, net.IP, error) { + ipAddr, err := ipFromRemoteAddr(addr) + if err != nil { + return false, nil, fmt.Errorf("unable to parse address: %s: %s", addr, err) + } + + contains, err := ip.ContainsIP(ipAddr) + return contains, ipAddr, err +} + +// ContainsIP checks if provided address is in the white list +func (ip *IP) ContainsIP(addr net.IP) (bool, error) { + for _, whiteListIP := range ip.whiteListsIPs { + if whiteListIP.Equal(addr) { + return true, nil + } + } + + for _, whiteListNet := range ip.whiteListsNet { + if whiteListNet.Contains(addr) { + return true, nil + } + } + + return false, nil +} + +func ipFromRemoteAddr(addr string) (net.IP, error) { + userIP := net.ParseIP(addr) + if userIP == nil { + return nil, fmt.Errorf("can't parse IP from address %s", addr) + } + + return userIP, nil +} diff --git a/middlewares/ip_whitelister_test.go b/whitelist/ip_test.go similarity index 65% rename from middlewares/ip_whitelister_test.go rename to whitelist/ip_test.go index 939af88bd..46ab7142f 100644 --- a/middlewares/ip_whitelister_test.go +++ b/whitelist/ip_test.go @@ -1,19 +1,14 @@ -package middlewares +package whitelist import ( - "fmt" "net" - "net/http" - "net/http/httptest" "testing" - "github.com/containous/traefik/testhelpers" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/urfave/negroni" ) -func TestNewIPWhitelister(t *testing.T) { +func TestNew(t *testing.T) { cases := []struct { desc string whitelistStrings []string @@ -24,12 +19,12 @@ func TestNewIPWhitelister(t *testing.T) { desc: "nil whitelist", whitelistStrings: nil, expectedWhitelists: nil, - errMessage: "no whitelists provided", + errMessage: "no whiteListsNet provided", }, { desc: "empty whitelist", whitelistStrings: []string{}, expectedWhitelists: nil, - errMessage: "no whitelists provided", + errMessage: "no whiteListsNet provided", }, { desc: "whitelist containing empty string", whitelistStrings: []string{ @@ -80,12 +75,12 @@ func TestNewIPWhitelister(t *testing.T) { test := test t.Run(test.desc, func(t *testing.T) { t.Parallel() - whitelister, err := NewIPWhitelister(test.whitelistStrings) + whitelister, err := NewIP(test.whitelistStrings) if test.errMessage != "" { require.EqualError(t, err, test.errMessage) } else { require.NoError(t, err) - for index, actual := range whitelister.whitelists { + for index, actual := range whitelister.whiteListsNet { expected := test.expectedWhitelists[index] assert.Equal(t, expected.IP, actual.IP) assert.Equal(t, expected.Mask.String(), actual.Mask.String()) @@ -95,7 +90,7 @@ func TestNewIPWhitelister(t *testing.T) { } } -func TestIPWhitelisterHandle(t *testing.T) { +func TestIsAllowed(t *testing.T) { cases := []struct { desc string whitelistStrings []string @@ -122,6 +117,23 @@ func TestIPWhitelisterHandle(t *testing.T) { }, { desc: "IPv4 single IP", + whitelistStrings: []string{ + "8.8.8.8", + }, + passIPs: []string{ + "8.8.8.8", + }, + rejectIPs: []string{ + "8.8.8.7", + "8.8.8.9", + "8.8.8.0", + "8.8.8.255", + "4.4.4.4", + "127.0.0.1", + }, + }, + { + desc: "IPv4 Net single IP", whitelistStrings: []string{ "8.8.8.8/32", }, @@ -167,16 +179,16 @@ func TestIPWhitelisterHandle(t *testing.T) { "2a03:4000:6:d080::/64", }, passIPs: []string{ - "[2a03:4000:6:d080::]", - "[2a03:4000:6:d080::1]", - "[2a03:4000:6:d080:dead:beef:ffff:ffff]", - "[2a03:4000:6:d080::42]", + "2a03:4000:6:d080::", + "2a03:4000:6:d080::1", + "2a03:4000:6:d080:dead:beef:ffff:ffff", + "2a03:4000:6:d080::42", }, rejectIPs: []string{ - "[2a03:4000:7:d080::]", - "[2a03:4000:7:d080::1]", - "[fe80::]", - "[4242::1]", + "2a03:4000:7:d080::", + "2a03:4000:7:d080::1", + "fe80::", + "4242::1", }, }, { @@ -185,12 +197,12 @@ func TestIPWhitelisterHandle(t *testing.T) { "2a03:4000:6:d080::42/128", }, passIPs: []string{ - "[2a03:4000:6:d080::42]", + "2a03:4000:6:d080::42", }, rejectIPs: []string{ - "[2a03:4000:6:d080::1]", - "[2a03:4000:6:d080:dead:beef:ffff:ffff]", - "[2a03:4000:6:d080::43]", + "2a03:4000:6:d080::1", + "2a03:4000:6:d080:dead:beef:ffff:ffff", + "2a03:4000:6:d080::43", }, }, { @@ -200,18 +212,18 @@ func TestIPWhitelisterHandle(t *testing.T) { "fe80::/16", }, passIPs: []string{ - "[2a03:4000:6:d080::]", - "[2a03:4000:6:d080::1]", - "[2a03:4000:6:d080:dead:beef:ffff:ffff]", - "[2a03:4000:6:d080::42]", - "[fe80::1]", - "[fe80:aa00:00bb:4232:ff00:eeee:00ff:1111]", - "[fe80::fe80]", + "2a03:4000:6:d080::", + "2a03:4000:6:d080::1", + "2a03:4000:6:d080:dead:beef:ffff:ffff", + "2a03:4000:6:d080::42", + "fe80::1", + "fe80:aa00:00bb:4232:ff00:eeee:00ff:1111", + "fe80::fe80", }, rejectIPs: []string{ - "[2a03:4000:7:d080::]", - "[2a03:4000:7:d080::1]", - "[4242::1]", + "2a03:4000:7:d080::", + "2a03:4000:7:d080::1", + "4242::1", }, }, { @@ -223,13 +235,13 @@ func TestIPWhitelisterHandle(t *testing.T) { "8.8.8.8/8", }, passIPs: []string{ - "[2a03:4000:6:d080::]", - "[2a03:4000:6:d080::1]", - "[2a03:4000:6:d080:dead:beef:ffff:ffff]", - "[2a03:4000:6:d080::42]", - "[fe80::1]", - "[fe80:aa00:00bb:4232:ff00:eeee:00ff:1111]", - "[fe80::fe80]", + "2a03:4000:6:d080::", + "2a03:4000:6:d080::1", + "2a03:4000:6:d080:dead:beef:ffff:ffff", + "2a03:4000:6:d080::42", + "fe80::1", + "fe80:aa00:00bb:4232:ff00:eeee:00ff:1111", + "fe80::fe80", "1.2.3.1", "1.2.3.32", "1.2.3.156", @@ -240,9 +252,9 @@ func TestIPWhitelisterHandle(t *testing.T) { "8.255.255.255", }, rejectIPs: []string{ - "[2a03:4000:7:d080::]", - "[2a03:4000:7:d080::1]", - "[4242::1]", + "2a03:4000:7:d080::", + "2a03:4000:7:d080::1", + "4242::1", "1.2.16.1", "1.2.32.1", "127.0.0.1", @@ -256,13 +268,6 @@ func TestIPWhitelisterHandle(t *testing.T) { "127.0.0.1/32", }, passIPs: nil, - rejectIPs: []string{ - "foo", - "10.0.0.350", - "fe:::80", - "", - "\\&$§&/(", - }, }, } @@ -270,38 +275,44 @@ func TestIPWhitelisterHandle(t *testing.T) { test := test t.Run(test.desc, func(t *testing.T) { t.Parallel() - whitelister, err := NewIPWhitelister(test.whitelistStrings) + whiteLister, err := NewIP(test.whitelistStrings) require.NoError(t, err) - require.NotNil(t, whitelister) - - handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - fmt.Fprintln(w, "traefik") - }) - n := negroni.New(whitelister) - n.UseHandler(handler) + require.NotNil(t, whiteLister) for _, testIP := range test.passIPs { - req := testhelpers.MustNewRequest(http.MethodGet, "/", nil) - - req.RemoteAddr = testIP + ":2342" - recorder := httptest.NewRecorder() - n.ServeHTTP(recorder, req) - - assert.Equal(t, http.StatusOK, recorder.Code, testIP+" should have passed "+test.desc) - assert.Contains(t, recorder.Body.String(), "traefik") + allowed, ip, err := whiteLister.Contains(testIP) + require.NoError(t, err) + require.NotNil(t, ip, err) + assert.True(t, allowed, testIP+" should have passed "+test.desc) } for _, testIP := range test.rejectIPs { - req := testhelpers.MustNewRequest(http.MethodGet, "/", nil) - - req.RemoteAddr = testIP + ":2342" - recorder := httptest.NewRecorder() - n.ServeHTTP(recorder, req) - - assert.Equal(t, http.StatusForbidden, recorder.Code, testIP+" should not have passed "+test.desc) - assert.NotContains(t, recorder.Body.String(), "traefik") + allowed, ip, err := whiteLister.Contains(testIP) + require.NoError(t, err) + require.NotNil(t, ip, err) + assert.False(t, allowed, testIP+" should not have passed "+test.desc) } }) } } + +func TestBrokenIPs(t *testing.T) { + brokenIPs := []string{ + "foo", + "10.0.0.350", + "fe:::80", + "", + "\\&$§&/(", + } + + whiteLister, err := NewIP([]string{"1.2.3.4/24"}) + require.NoError(t, err) + + for _, testIP := range brokenIPs { + _, ip, err := whiteLister.Contains(testIP) + assert.Error(t, err) + require.Nil(t, ip, err) + } + +} From 14cec7e6107e4a12aa98a6d36f6f399b66bbed4d Mon Sep 17 00:00:00 2001 From: Ludovic Fernandez Date: Tue, 10 Oct 2017 15:24:03 +0200 Subject: [PATCH 13/14] Stickiness documentation --- docs/basics.md | 27 +++++++++++++-- docs/configuration/backends/consul.md | 31 +++++++++-------- docs/configuration/backends/docker.md | 42 ++++++++++++----------- docs/configuration/backends/ecs.md | 26 +++++++------- docs/configuration/backends/kubernetes.md | 6 +++- docs/configuration/backends/marathon.md | 4 ++- docs/configuration/backends/rancher.md | 25 ++++++++------ docs/user-guide/swarm-mode.md | 2 +- 8 files changed, 102 insertions(+), 61 deletions(-) diff --git a/docs/basics.md b/docs/basics.md index 5793a54e8..79ecf9398 100644 --- a/docs/basics.md +++ b/docs/basics.md @@ -346,12 +346,33 @@ For example: - Another possible value for `extractorfunc` is `client.ip` which will categorize requests based on client source ip. - Lastly `extractorfunc` can take the value of `request.header.ANY_HEADER` which will categorize requests based on `ANY_HEADER` that you provide. +### Sticky sessions + Sticky sessions are supported with both load balancers. -When sticky sessions are enabled, a cookie called `_TRAEFIK_BACKEND` is set on the initial request. +When sticky sessions are enabled, a cookie is set on the initial request. +The default cookie name is an abbreviation of a sha1 (ex: `_1d52e`). On subsequent requests, the client will be directed to the backend stored in the cookie if it is still healthy. If not, a new backend will be assigned. -For example: +To activate sticky session: + +```toml +[backends] + [backends.backend1] + [backends.backend1.loadbalancer.stickiness] +``` + +To customize the cookie name: + +```toml +[backends] + [backends.backend1] + [backends.backend1.loadbalancer.stickiness] + cookieName = "my_cookie" +``` + +The deprecated way: + ```toml [backends] [backends.backend1] @@ -359,6 +380,8 @@ For example: sticky = true ``` +### Health Check + A health check can be configured in order to remove a backend from LB rotation as long as it keeps returning HTTP status codes other than `200 OK` to HTTP GET requests periodically carried out by Traefik. The check is defined by a pathappended to the backend URL and an interval (given in a format understood by [time.ParseDuration](https://golang.org/pkg/time/#ParseDuration)) specifying how often the health check should be executed (the default being 30 seconds). Each backend must respond to the health check within 5 seconds. diff --git a/docs/configuration/backends/consul.md b/docs/configuration/backends/consul.md index d4d762918..ba1fb1866 100644 --- a/docs/configuration/backends/consul.md +++ b/docs/configuration/backends/consul.md @@ -117,17 +117,20 @@ To enable constraints see [backend-specific constraints section](/configuration/ Additional settings can be defined using Consul Catalog tags. -| Tag | Description | -|---------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `traefik.enable=false` | Disable this container in Træfik | -| `traefik.protocol=https` | Override the default `http` protocol | -| `traefik.backend.weight=10` | Assign this weight to the container | -| `traefik.backend.circuitbreaker=EXPR` | Create a [circuit breaker](/basics/#backends) to be used against the backend, ex: `NetworkErrorRatio() > 0.` | -| `traefik.backend.loadbalancer=drr` | Override the default load balancing mode | -| `traefik.backend.maxconn.amount=10` | Set a maximum number of connections to the backend. Must be used in conjunction with the below label to take effect. | -| `traefik.backend.maxconn.extractorfunc=client.ip` | Set the function to be used against the request to determine what to limit maximum connections to the backend by. Must be used in conjunction with the above label to take effect. | -| `traefik.frontend.rule=Host:test.traefik.io` | Override the default frontend rule (Default: `Host:{{.ServiceName}}.{{.Domain}}`). | -| `traefik.frontend.passHostHeader=true` | Forward client `Host` header to the backend. | -| `traefik.frontend.priority=10` | Override default frontend priority | -| `traefik.frontend.entryPoints=http,https` | Assign this frontend to entry points `http` and `https`. Overrides `defaultEntryPoints`. | -| `traefik.frontend.auth.basic=EXPR` | Sets basic authentication for that frontend in CSV format: `User:Hash,User:Hash` | +| Tag | Description | +|-----------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `traefik.enable=false` | Disable this container in Træfik | +| `traefik.protocol=https` | Override the default `http` protocol | +| `traefik.backend.weight=10` | Assign this weight to the container | +| `traefik.backend.circuitbreaker=EXPR` | Create a [circuit breaker](/basics/#backends) to be used against the backend, ex: `NetworkErrorRatio() > 0.` | +| `traefik.backend.maxconn.amount=10` | Set a maximum number of connections to the backend. Must be used in conjunction with the below label to take effect. | +| `traefik.backend.maxconn.extractorfunc=client.ip` | Set the function to be used against the request to determine what to limit maximum connections to the backend by. Must be used in conjunction with the above label to take effect. | +| `traefik.frontend.rule=Host:test.traefik.io` | Override the default frontend rule (Default: `Host:{{.ServiceName}}.{{.Domain}}`). | +| `traefik.frontend.passHostHeader=true` | Forward client `Host` header to the backend. | +| `traefik.frontend.priority=10` | Override default frontend priority | +| `traefik.frontend.entryPoints=http,https` | Assign this frontend to entry points `http` and `https`. Overrides `defaultEntryPoints`. | +| `traefik.frontend.auth.basic=EXPR` | Sets basic authentication for that frontend in CSV format: `User:Hash,User:Hash` | +| `traefik.backend.loadbalancer=drr` | override the default `wrr` load balancer algorithm | +| `traefik.backend.loadbalancer.stickiness=true` | enable backend sticky sessions | +| `traefik.backend.loadbalancer.stickiness.cookieName=NAME` | Manually set the cookie name for sticky sessions | +| `traefik.backend.loadbalancer.sticky=true` | enable backend sticky sessions (DEPRECATED) | diff --git a/docs/configuration/backends/docker.md b/docs/configuration/backends/docker.md index f10db04a3..616dac299 100644 --- a/docs/configuration/backends/docker.md +++ b/docs/configuration/backends/docker.md @@ -148,26 +148,28 @@ To enable constraints see [backend-specific constraints section](/configuration/ Labels can be used on containers to override default behaviour. -| Label | Description | -|---------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `traefik.backend=foo` | Give the name `foo` to the generated backend for this container. | -| `traefik.backend.maxconn.amount=10` | Set a maximum number of connections to the backend. Must be used in conjunction with the below label to take effect. | -| `traefik.backend.maxconn.extractorfunc=client.ip` | Set the function to be used against the request to determine what to limit maximum connections to the backend by. Must be used in conjunction with the above label to take effect. | -| `traefik.backend.loadbalancer.method=drr` | Override the default `wrr` load balancer algorithm | -| `traefik.backend.loadbalancer.sticky=true` | Enable backend sticky sessions | -| `traefik.backend.loadbalancer.swarm=true` | Use Swarm's inbuilt load balancer (only relevant under Swarm Mode). | -| `traefik.backend.circuitbreaker.expression=EXPR` | Create a [circuit breaker](/basics/#backends) to be used against the backend | -| `traefik.port=80` | Register this port. Useful when the container exposes multiples ports. | -| `traefik.protocol=https` | Override the default `http` protocol | -| `traefik.weight=10` | Assign this weight to the container | -| `traefik.enable=false` | Disable this container in Træfik | -| `traefik.frontend.rule=EXPR` | Override the default frontend rule. Default: `Host:{containerName}.{domain}` or `Host:{service}.{project_name}.{domain}` if you are using `docker-compose`. | -| `traefik.frontend.passHostHeader=true` | Forward client `Host` header to the backend. | -| `traefik.frontend.priority=10` | Override default frontend priority | -| `traefik.frontend.entryPoints=http,https` | Assign this frontend to entry points `http` and `https`. Overrides `defaultEntryPoints` | -| `traefik.frontend.auth.basic=EXPR` | Sets basic authentication for that frontend in CSV format: `User:Hash,User:Hash` | -| `traefik.frontend.whitelistSourceRange:RANGE` | List of IP-Ranges which are allowed to access. An unset or empty list allows all Source-IPs to access. If one of the Net-Specifications are invalid, the whole list is invalid and allows all Source-IPs to access. | -| `traefik.docker.network` | Set the docker network to use for connections to this container. If a container is linked to several networks, be sure to set the proper network name (you can check with `docker inspect `) otherwise it will randomly pick one (depending on how docker is returning them). For instance when deploying docker `stack` from compose files, the compose defined networks will be prefixed with the `stack` name. | +| Label | Description | +|-----------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `traefik.backend=foo` | Give the name `foo` to the generated backend for this container. | +| `traefik.backend.maxconn.amount=10` | Set a maximum number of connections to the backend. Must be used in conjunction with the below label to take effect. | +| `traefik.backend.maxconn.extractorfunc=client.ip` | Set the function to be used against the request to determine what to limit maximum connections to the backend by. Must be used in conjunction with the above label to take effect. | +| `traefik.backend.loadbalancer.method=drr` | Override the default `wrr` load balancer algorithm | +| `traefik.backend.loadbalancer.stickiness=true` | Enable backend sticky sessions | +| `traefik.backend.loadbalancer.stickiness.cookieName=NAME` | Manually set the cookie name for sticky sessions | +| `traefik.backend.loadbalancer.sticky=true` | Enable backend sticky sessions (DEPRECATED) | +| `traefik.backend.loadbalancer.swarm=true` | Use Swarm's inbuilt load balancer (only relevant under Swarm Mode). | +| `traefik.backend.circuitbreaker.expression=EXPR` | Create a [circuit breaker](/basics/#backends) to be used against the backend | +| `traefik.port=80` | Register this port. Useful when the container exposes multiples ports. | +| `traefik.protocol=https` | Override the default `http` protocol | +| `traefik.weight=10` | Assign this weight to the container | +| `traefik.enable=false` | Disable this container in Træfik | +| `traefik.frontend.rule=EXPR` | Override the default frontend rule. Default: `Host:{containerName}.{domain}` or `Host:{service}.{project_name}.{domain}` if you are using `docker-compose`. | +| `traefik.frontend.passHostHeader=true` | Forward client `Host` header to the backend. | +| `traefik.frontend.priority=10` | Override default frontend priority | +| `traefik.frontend.entryPoints=http,https` | Assign this frontend to entry points `http` and `https`. Overrides `defaultEntryPoints` | +| `traefik.frontend.auth.basic=EXPR` | Sets basic authentication for that frontend in CSV format: `User:Hash,User:Hash` | +| `traefik.frontend.whitelistSourceRange:RANGE` | List of IP-Ranges which are allowed to access. An unset or empty list allows all Source-IPs to access. If one of the Net-Specifications are invalid, the whole list is invalid and allows all Source-IPs to access. | +| `traefik.docker.network` | Set the docker network to use for connections to this container. If a container is linked to several networks, be sure to set the proper network name (you can check with `docker inspect `) otherwise it will randomly pick one (depending on how docker is returning them). For instance when deploying docker `stack` from compose files, the compose defined networks will be prefixed with the `stack` name. | ### On Service diff --git a/docs/configuration/backends/ecs.md b/docs/configuration/backends/ecs.md index 6b7a148cb..b19373009 100644 --- a/docs/configuration/backends/ecs.md +++ b/docs/configuration/backends/ecs.md @@ -124,15 +124,17 @@ Træfik needs the following policy to read ECS information: Labels can be used on task containers to override default behaviour: -| Label | Description | -|---------------------------------------------------|------------------------------------------------------------------------------------------| -| `traefik.protocol=https` | override the default `http` protocol | -| `traefik.weight=10` | assign this weight to the container | -| `traefik.enable=false` | disable this container in Træfik | -| `traefik.backend.loadbalancer.method=drr` | override the default `wrr` load balancer algorithm | -| `traefik.backend.loadbalancer.sticky=true` | enable backend sticky sessions | -| `traefik.frontend.rule=Host:test.traefik.io` | override the default frontend rule (Default: `Host:{containerName}.{domain}`). | -| `traefik.frontend.passHostHeader=true` | forward client `Host` header to the backend. | -| `traefik.frontend.priority=10` | override default frontend priority | -| `traefik.frontend.entryPoints=http,https` | assign this frontend to entry points `http` and `https`. Overrides `defaultEntryPoints`. | -| `traefik.frontend.auth.basic=EXPR` | Sets basic authentication for that frontend in CSV format: `User:Hash,User:Hash` | +| Label | Description | +|-----------------------------------------------------------|------------------------------------------------------------------------------------------| +| `traefik.protocol=https` | override the default `http` protocol | +| `traefik.weight=10` | assign this weight to the container | +| `traefik.enable=false` | disable this container in Træfik | +| `traefik.backend.loadbalancer.method=drr` | override the default `wrr` load balancer algorithm | +| `traefik.backend.loadbalancer.stickiness=true` | enable backend sticky sessions | +| `traefik.backend.loadbalancer.stickiness.cookieName=NAME` | Manually set the cookie name for sticky sessions | +| `traefik.backend.loadbalancer.sticky=true` | enable backend sticky sessions (DEPRECATED) | +| `traefik.frontend.rule=Host:test.traefik.io` | override the default frontend rule (Default: `Host:{containerName}.{domain}`). | +| `traefik.frontend.passHostHeader=true` | forward client `Host` header to the backend. | +| `traefik.frontend.priority=10` | override default frontend priority | +| `traefik.frontend.entryPoints=http,https` | assign this frontend to entry points `http` and `https`. Overrides `defaultEntryPoints`. | +| `traefik.frontend.auth.basic=EXPR` | Sets basic authentication for that frontend in CSV format: `User:Hash,User:Hash` | \ No newline at end of file diff --git a/docs/configuration/backends/kubernetes.md b/docs/configuration/backends/kubernetes.md index 2d63466d4..84da91e95 100644 --- a/docs/configuration/backends/kubernetes.md +++ b/docs/configuration/backends/kubernetes.md @@ -93,8 +93,12 @@ Annotations can be used on the Kubernetes service to override default behaviour: - `traefik.backend.loadbalancer.method=drr` Override the default `wrr` load balancer algorithm -- `traefik.backend.loadbalancer.sticky=true` +- `traefik.backend.loadbalancer.stickiness=true` Enable backend sticky sessions +- `traefik.backend.loadbalancer.stickiness.cookieName=NAME` + Manually set the cookie name for sticky sessions +- `traefik.backend.loadbalancer.sticky=true` + Enable backend sticky sessions (DEPRECATED) You can find here an example [ingress](https://raw.githubusercontent.com/containous/traefik/master/examples/k8s/cheese-ingress.yaml) and [replication controller](https://raw.githubusercontent.com/containous/traefik/master/examples/k8s/traefik.yaml). diff --git a/docs/configuration/backends/marathon.md b/docs/configuration/backends/marathon.md index d67513c7a..cc4f763cc 100644 --- a/docs/configuration/backends/marathon.md +++ b/docs/configuration/backends/marathon.md @@ -153,7 +153,9 @@ Labels can be used on containers to override default behaviour: | `traefik.backend.maxconn.amount=10` | set a maximum number of connections to the backend. Must be used in conjunction with the below label to take effect. | | `traefik.backend.maxconn.extractorfunc=client.ip` | set the function to be used against the request to determine what to limit maximum connections to the backend by. Must be used in conjunction with the above label to take effect. | | `traefik.backend.loadbalancer.method=drr` | override the default `wrr` load balancer algorithm | -| `traefik.backend.loadbalancer.sticky=true` | enable backend sticky sessions | +| `traefik.backend.loadbalancer.sticky=true` | enable backend sticky sessions (DEPRECATED) | +| `traefik.backend.loadbalancer.stickiness=true` | enable backend sticky sessions | +| `traefik.backend.loadbalancer.stickiness.cookieName=NAME` | Manually set the cookie name for sticky sessions | | `traefik.backend.circuitbreaker.expression=NetworkErrorRatio() > 0.5` | create a [circuit breaker](/basics/#backends) to be used against the backend | | `traefik.backend.healthcheck.path=/health` | set the Traefik health check path [default: no health checks] | | `traefik.backend.healthcheck.interval=5s` | sets a custom health check interval in Go-parseable (`time.ParseDuration`) format [default: 30s] | diff --git a/docs/configuration/backends/rancher.md b/docs/configuration/backends/rancher.md index ffea34d83..a3d31c046 100644 --- a/docs/configuration/backends/rancher.md +++ b/docs/configuration/backends/rancher.md @@ -114,13 +114,18 @@ secretKey = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" Labels can be used on task containers to override default behaviour: -| Label | Description | -|----------------------------------------------|------------------------------------------------------------------------------------------| -| `traefik.protocol=https` | override the default `http` protocol | -| `traefik.weight=10` | assign this weight to the container | -| `traefik.enable=false` | disable this container in Træfik | -| `traefik.frontend.rule=Host:test.traefik.io` | override the default frontend rule (Default: `Host:{containerName}.{domain}`). | -| `traefik.frontend.passHostHeader=true` | forward client `Host` header to the backend. | -| `traefik.frontend.priority=10` | override default frontend priority | -| `traefik.frontend.entryPoints=http,https` | assign this frontend to entry points `http` and `https`. Overrides `defaultEntryPoints`. | -| `traefik.frontend.auth.basic=EXPR` | Sets basic authentication for that frontend in CSV format: `User:Hash,User:Hash`. | +| Label | Description | +|-----------------------------------------------------------------------|------------------------------------------------------------------------------------------| +| `traefik.protocol=https` | Override the default `http` protocol | +| `traefik.weight=10` | Assign this weight to the container | +| `traefik.enable=false` | Disable this container in Træfik | +| `traefik.frontend.rule=Host:test.traefik.io` | Override the default frontend rule (Default: `Host:{containerName}.{domain}`). | +| `traefik.frontend.passHostHeader=true` | Forward client `Host` header to the backend. | +| `traefik.frontend.priority=10` | Override default frontend priority | +| `traefik.frontend.entryPoints=http,https` | Assign this frontend to entry points `http` and `https`. Overrides `defaultEntryPoints`. | +| `traefik.frontend.auth.basic=EXPR` | Sets basic authentication for that frontend in CSV format: `User:Hash,User:Hash`. | +| `traefik.backend.circuitbreaker.expression=NetworkErrorRatio() > 0.5` | Create a [circuit breaker](/basics/#backends) to be used against the backend | +| `traefik.backend.loadbalancer.method=drr` | Override the default `wrr` load balancer algorithm | +| `traefik.backend.loadbalancer.stickiness=true` | Enable backend sticky sessions | +| `traefik.backend.loadbalancer.stickiness.cookieName=NAME` | Manually set the cookie name for sticky sessions | +| `traefik.backend.loadbalancer.sticky=true` | Enable backend sticky sessions (DEPRECATED) | diff --git a/docs/user-guide/swarm-mode.md b/docs/user-guide/swarm-mode.md index c57e7331b..f9a4cc1f3 100644 --- a/docs/user-guide/swarm-mode.md +++ b/docs/user-guide/swarm-mode.md @@ -126,7 +126,7 @@ docker-machine ssh manager "docker service create \ ``` !!! note - We set `whoami1` to use sticky sessions (`--label traefik.backend.loadbalancer.sticky=true`). + We set `whoami1` to use sticky sessions (`--label traefik.backend.loadbalancer.stickiness=true`). We'll demonstrate that later. !!! note From 7a2ce5956356f74abacbabf62ac801004f379468 Mon Sep 17 00:00:00 2001 From: Ludovic Fernandez Date: Tue, 10 Oct 2017 15:50:03 +0200 Subject: [PATCH 14/14] Prepare release v1.4.0-rc5 --- CHANGELOG.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c399a57e9..acc1bc899 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,26 @@ # Change Log +## [v1.4.0-rc5](https://github.com/containous/traefik/tree/v1.4.0-rc5) (2017-10-10) +[All Commits](https://github.com/containous/traefik/compare/v1.4.0-rc4...v1.4.0-rc5) + +**Enhancements:** +- **[middleware]** Add trusted whitelist proxy protocol ([#2234](https://github.com/containous/traefik/pull/2234) by [emilevauge](https://github.com/emilevauge)) + +**Bug fixes:** +- **[consul,docker,ecs,k8s,marathon,rancher,sticky-session]** Stickiness cookie name ([#2232](https://github.com/containous/traefik/pull/2232) by [ldez](https://github.com/ldez)) +- **[logs]** Fix flakiness in log rotation test ([#2213](https://github.com/containous/traefik/pull/2213) by [marco-jantke](https://github.com/marco-jantke)) +- **[middleware]** Enable prefix matching within slash boundaries ([#2214](https://github.com/containous/traefik/pull/2214) by [marco-jantke](https://github.com/marco-jantke)) +- **[sticky-session]** Sanitize cookie names. ([#2216](https://github.com/containous/traefik/pull/2216) by [timoreimann](https://github.com/timoreimann)) +- Move http2 configure transport ([#2231](https://github.com/containous/traefik/pull/2231) by [Juliens](https://github.com/Juliens)) +- Delay first version check ([#2215](https://github.com/containous/traefik/pull/2215) by [emilevauge](https://github.com/emilevauge)) + +**Documentation:** +- **[acme]** Fix grammar ([#2208](https://github.com/containous/traefik/pull/2208) by [mvasin](https://github.com/mvasin)) +- **[docker,ecs,k8s,marathon,rancher]** Stickiness documentation ([#2238](https://github.com/containous/traefik/pull/2238) by [ldez](https://github.com/ldez)) +- **[k8s]** Quote priority values in annotation examples. ([#2230](https://github.com/containous/traefik/pull/2230) by [timoreimann](https://github.com/timoreimann)) +- **[k8s]** Remove pod from RBAC rules. ([#2229](https://github.com/containous/traefik/pull/2229) by [timoreimann](https://github.com/timoreimann)) +- **[k8s]** Document ways to partition Ingresses in the k8s guide. ([#2223](https://github.com/containous/traefik/pull/2223) by [timoreimann](https://github.com/timoreimann)) + ## [v1.4.0-rc4](https://github.com/containous/traefik/tree/v1.4.0-rc4) (2017-10-02) [All Commits](https://github.com/containous/traefik/compare/v1.4.0-rc3...v1.4.0-rc4)