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) 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 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/cmd/traefik/traefik.go b/cmd/traefik/traefik.go index 1987a1eff..1ce892af5 100644 --- a/cmd/traefik/traefik.go +++ b/cmd/traefik/traefik.go @@ -349,6 +349,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 { diff --git a/configuration/configuration.go b/configuration/configuration.go index 4e8755f08..108a195d1 100644 --- a/configuration/configuration.go +++ b/configuration/configuration.go @@ -284,7 +284,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"], @@ -299,7 +306,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) @@ -346,8 +353,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 @@ -497,6 +504,11 @@ type ForwardingTimeouts struct { 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 +} + // LifeCycle contains configurations relevant to the lifecycle (such as the // shutdown phase) of Traefik. type LifeCycle struct { diff --git a/configuration/configuration_test.go b/configuration/configuration_test.go index ed20c6b02..b6fc259ab 100644 --- a/configuration/configuration_test.go +++ b/configuration/configuration_test.go @@ -18,7 +18,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", @@ -29,18 +29,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", @@ -144,7 +136,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", @@ -153,8 +145,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/basics.md b/docs/basics.md index 490e67b8e..c3ef994b3 100644 --- a/docs/basics.md +++ b/docs/basics.md @@ -375,12 +375,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] @@ -388,6 +409,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/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/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 9b549403d..84da91e95 100644 --- a/docs/configuration/backends/kubernetes.md +++ b/docs/configuration/backends/kubernetes.md @@ -86,15 +86,19 @@ 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: - `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/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/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/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/docs/user-guide/kubernetes.md b/docs/user-guide/kubernetes.md index 56195e74c..357f7d6cd 100644 --- a/docs/user-guide/kubernetes.md +++ b/docs/user-guide/kubernetes.md @@ -33,7 +33,6 @@ rules: - apiGroups: - "" resources: - - pods - services - endpoints - secrets @@ -594,7 +593,7 @@ kind: Ingress metadata: name: wildcard-cheeses annotations: - traefik.frontend.priority: 1 + traefik.frontend.priority: "1" spec: rules: - host: *.minikube @@ -609,7 +608,7 @@ kind: Ingress metadata: name: specific-cheeses annotations: - traefik.frontend.priority: 2 + traefik.frontend.priority: "2" spec: rules: - host: specific.minikube @@ -621,6 +620,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 @@ -690,13 +690,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 diff --git a/docs/user-guide/swarm-mode.md b/docs/user-guide/swarm-mode.md index dc5a84ccb..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 @@ -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) 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 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/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) 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/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 { 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 6f8ba53c7..6f825420a 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 (p *Provider) getFirstInstanceLabel(instances []ecsInstance, labelName string) string { if len(instances) > 0 { - label := p.label(instances[0], types.LabelBackendLoadbalancerSticky) - if label != "" { - return label - } + return p.label(instances[0], labelName) } - return "false" + return "" +} + +func (p *Provider) hasStickinessLabel(instances []ecsInstance) bool { + stickinessLabel := p.getFirstInstanceLabel(instances, types.LabelBackendLoadbalancerStickiness) + + stickyLabel := p.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 p.getFirstInstanceLabel(instances, types.LabelBackendLoadbalancerStickinessCookieName) } func (p *Provider) getLoadBalancerMethod(instances []ecsInstance) string { diff --git a/provider/kubernetes/kubernetes.go b/provider/kubernetes/kubernetes.go index 54ea9b364..9b8b27e96 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 b91085b6a..5d33f73de 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", }, }, @@ -673,7 +668,6 @@ func TestHostlessIngress(t *testing.T) { Servers: map[string]types.Server{}, CircuitBreaker: nil, LoadBalancer: &types.LoadBalancer{ - Sticky: false, Method: "wrr", }, }, @@ -875,7 +869,6 @@ func TestServiceAnnotations(t *testing.T) { }, LoadBalancer: &types.LoadBalancer{ Method: "drr", - Sticky: false, }, }, "bar": { @@ -892,7 +885,9 @@ func TestServiceAnnotations(t *testing.T) { CircuitBreaker: nil, LoadBalancer: &types.LoadBalancer{ Method: "wrr", - Sticky: true, + Stickiness: &types.Stickiness{ + CookieName: "_4155f", + }, }, }, }, @@ -1156,7 +1151,6 @@ func TestIngressAnnotations(t *testing.T) { }, CircuitBreaker: nil, LoadBalancer: &types.LoadBalancer{ - Sticky: false, Method: "wrr", }, }, @@ -1169,7 +1163,6 @@ func TestIngressAnnotations(t *testing.T) { }, CircuitBreaker: nil, LoadBalancer: &types.LoadBalancer{ - Sticky: false, Method: "wrr", }, }, @@ -1182,7 +1175,6 @@ func TestIngressAnnotations(t *testing.T) { }, CircuitBreaker: nil, LoadBalancer: &types.LoadBalancer{ - Sticky: false, Method: "wrr", }, }, @@ -1195,7 +1187,6 @@ func TestIngressAnnotations(t *testing.T) { }, CircuitBreaker: nil, LoadBalancer: &types.LoadBalancer{ - Sticky: false, Method: "wrr", }, }, @@ -1208,7 +1199,6 @@ func TestIngressAnnotations(t *testing.T) { }, CircuitBreaker: nil, LoadBalancer: &types.LoadBalancer{ - Sticky: false, Method: "wrr", }, }, @@ -1362,7 +1352,6 @@ func TestPriorityHeaderValue(t *testing.T) { }, CircuitBreaker: nil, LoadBalancer: &types.LoadBalancer{ - Sticky: false, Method: "wrr", }, }, @@ -1464,7 +1453,6 @@ func TestInvalidPassHostHeaderValue(t *testing.T) { }, CircuitBreaker: nil, LoadBalancer: &types.LoadBalancer{ - Sticky: false, Method: "wrr", }, }, @@ -1747,14 +1735,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": { @@ -1762,7 +1748,6 @@ func TestMissingResources(t *testing.T) { CircuitBreaker: nil, LoadBalancer: &types.LoadBalancer{ Method: "wrr", - Sticky: false, }, }, "missing_endpoint_subsets": { @@ -1770,7 +1755,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 bf9581e00..b4f6ec187 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" @@ -30,7 +31,9 @@ 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/containous/traefik/whitelist" "github.com/streamrail/concurrent-map" thoas_stats "github.com/thoas/stats" "github.com/urfave/negroni" @@ -154,8 +157,8 @@ func createHTTPTransport(globalConfiguration configuration.GlobalConfiguration) transport.TLSClientConfig = &tls.Config{ RootCAs: createRootCACertPool(globalConfiguration.RootCAs), } - http2.ConfigureTransport(transport) } + http2.ConfigureTransport(transport) return transport } @@ -335,11 +338,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) @@ -652,8 +655,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{ @@ -826,11 +843,10 @@ func (server *Server) loadConfig(configurations types.Configurations, globalConf continue frontend } - stickySession := config.Backends[frontend.Backend].LoadBalancer.Sticky - cookieName := "_TRAEFIK_BACKEND_" + 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 +855,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 +872,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)) @@ -1159,16 +1175,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, } } } diff --git a/server/server_test.go b/server/server_test.go index a1575fddb..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", }, } @@ -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)) @@ -539,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 } } @@ -719,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 b5bd1638d..8581988f3 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 010a995b8..283eb1d19 100644 --- a/types/types.go +++ b/types/types.go @@ -34,8 +34,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. 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) + } + +}