From 4ec90c5c0d634254b6179a06a7b0db820a2989ec Mon Sep 17 00:00:00 2001 From: mpl Date: Mon, 26 Aug 2019 12:20:06 +0200 Subject: [PATCH] Add rate limiter, rename maxConn into inFlightReq Co-authored-by: Julien Salleyron Co-authored-by: Jean-Baptiste Doumenjou --- .../{maxconnection.png => inflightreq.png} | Bin docs/content/assets/styles/extra.css | 4 + docs/content/middlewares/inflightreq.md | 247 +++++++++ docs/content/middlewares/maxconnection.md | 71 --- docs/content/middlewares/overview.md | 2 +- docs/content/middlewares/ratelimit.md | 372 ++++++++++--- .../dynamic-configuration/docker-labels.yml | 8 +- .../reference/dynamic-configuration/file.toml | 9 +- .../reference/dynamic-configuration/file.yaml | 19 +- .../marathon-labels.json | 13 +- docs/mkdocs.yml | 2 +- go.mod | 2 + integration/fixtures/ratelimit/simple.toml | 11 +- integration/integration_test.go | 3 +- integration/ratelimit_test.go | 40 +- pkg/config/dynamic/fixtures/sample.toml | 9 +- pkg/config/dynamic/middlewares.go | 104 ++-- pkg/config/dynamic/zz_generated.deepcopy.go | 90 +-- pkg/config/file/fixtures/sample.toml | 9 +- pkg/config/label/label_test.go | 516 +++++++++--------- pkg/ip/strategy.go | 9 +- pkg/ip/strategy_test.go | 2 +- pkg/middlewares/extractor.go | 49 ++ pkg/middlewares/inflightreq/inflight_req.go | 57 ++ .../maxconnection/max_connection.go | 48 -- pkg/middlewares/ratelimiter/rate_limiter.go | 134 ++++- .../ratelimiter/rate_limiter_test.go | 160 ++++++ pkg/provider/docker/config_test.go | 36 +- pkg/provider/marathon/config_test.go | 16 +- pkg/server/middleware/middlewares.go | 28 +- 30 files changed, 1419 insertions(+), 651 deletions(-) rename docs/content/assets/img/middleware/{maxconnection.png => inflightreq.png} (100%) create mode 100644 docs/content/middlewares/inflightreq.md delete mode 100644 docs/content/middlewares/maxconnection.md create mode 100644 pkg/middlewares/extractor.go create mode 100644 pkg/middlewares/inflightreq/inflight_req.go delete mode 100644 pkg/middlewares/maxconnection/max_connection.go create mode 100644 pkg/middlewares/ratelimiter/rate_limiter_test.go diff --git a/docs/content/assets/img/middleware/maxconnection.png b/docs/content/assets/img/middleware/inflightreq.png similarity index 100% rename from docs/content/assets/img/middleware/maxconnection.png rename to docs/content/assets/img/middleware/inflightreq.png diff --git a/docs/content/assets/styles/extra.css b/docs/content/assets/styles/extra.css index 4498eacf1..f865034b0 100644 --- a/docs/content/assets/styles/extra.css +++ b/docs/content/assets/styles/extra.css @@ -34,6 +34,10 @@ h3 { font-weight: bold !important; } +.md-typeset h5 { + text-transform: none; +} + figcaption { text-align: center; font-size: 0.8em; diff --git a/docs/content/middlewares/inflightreq.md b/docs/content/middlewares/inflightreq.md new file mode 100644 index 000000000..9b07b7a82 --- /dev/null +++ b/docs/content/middlewares/inflightreq.md @@ -0,0 +1,247 @@ +# InFlightReq + +Limiting the Number of Simultaneous In-Flight Requests +{: .subtitle } + +![InFlightReq](../assets/img/middleware/inflightreq.png) + +To proactively prevent services from being overwhelmed with high load, a limit on the number of simultaneous in-flight requests can be applied. + +## Configuration Examples + +```yaml tab="Docker" +labels: +- "traefik.http.middlewares.test-inflightreq.inflightreq.amount=10" +``` + +```yaml tab="Kubernetes" +apiVersion: traefik.containo.us/v1alpha1 +kind: Middleware +metadata: + name: test-inflightreq +spec: + inFlightReq: + amount: 10 +``` + +```json tab="Marathon" +"labels": { + "traefik.http.middlewares.test-inflightreq.inflightreq.amount": "10" +} +``` + +```yaml tab="Rancher" +# Limiting to 10 simultaneous connections +labels: +- "traefik.http.middlewares.test-inflightreq.inflightreq.amount=10" +``` + +```toml tab="File (TOML)" +# Limiting to 10 simultaneous connections +[http.middlewares] + [http.middlewares.test-inflightreq.inFlightReq] + amount = 10 +``` + +```yaml tab="File (YAML)" +# Limiting to 10 simultaneous connections +http: + middlewares: + test-inflightreq: + inFlightReq: + amount: 10 +``` + +## Configuration Options + +### `amount` + +The `amount` option defines the maximum amount of allowed simultaneous in-flight request. +The middleware will return an `HTTP 429 Too Many Requests` if there are already `amount` requests in progress (based on the same `sourceCriterion` strategy). + +### `sourceCriterion` + +SourceCriterion defines what criterion is used to group requests as originating from a common source. +The precedence order is `ipStrategy`, then `requestHeaderName`, then `requestHost`. +If none are set, the default is to use the `requestHost`. + +#### `sourceCriterion.ipStrategy` + +The `ipStrategy` option defines two parameters that sets how Traefik will determine the client IP: `depth`, and `excludedIPs`. + +##### `ipStrategy.depth` + +The `depth` option tells Traefik to use the `X-Forwarded-For` header and take the IP located at the `depth` position (starting from the right). + +- If `depth` is greater than the total number of IPs in `X-Forwarded-For`, then the client IP will be empty. +- `depth` is ignored if its value is is lesser than or equal to 0. + +!!! note "Example of Depth & X-Forwarded-For" + + If `depth` was equal to 2, and the request `X-Forwarded-For` header was `"10.0.0.1,11.0.0.1,12.0.0.1,13.0.0.1"` then the "real" client IP would be `"10.0.0.1"` (at depth 4) but the IP used as the criterion would be `"12.0.0.1"` (`depth=2`). + + | `X-Forwarded-For` | `depth` | clientIP | + |-----------------------------------------|---------|--------------| + | `"10.0.0.1,11.0.0.1,12.0.0.1,13.0.0.1"` | `1` | `"13.0.0.1"` | + | `"10.0.0.1,11.0.0.1,12.0.0.1,13.0.0.1"` | `3` | `"11.0.0.1"` | + | `"10.0.0.1,11.0.0.1,12.0.0.1,13.0.0.1"` | `5` | `""` | + +##### `ipStrategy.excludedIPs` + +`excludedIPs` tells Traefik to scan the `X-Forwarded-For` header and pick the first IP not in the list. + +!!! important + If `depth` is specified, `excludedIPs` is ignored. + +!!! note "Example of ExcludedIPs & X-Forwarded-For" + + | `X-Forwarded-For` | `excludedIPs` | clientIP | + |-----------------------------------------|-----------------------|--------------| + | `"10.0.0.1,11.0.0.1,12.0.0.1,13.0.0.1"` | `"12.0.0.1,13.0.0.1"` | `"11.0.0.1"` | + | `"10.0.0.1,11.0.0.1,12.0.0.1,13.0.0.1"` | `"15.0.0.1,13.0.0.1"` | `"12.0.0.1"` | + | `"10.0.0.1,11.0.0.1,12.0.0.1,13.0.0.1"` | `"10.0.0.1,13.0.0.1"` | `"12.0.0.1"` | + | `"10.0.0.1,11.0.0.1,12.0.0.1,13.0.0.1"` | `"15.0.0.1,16.0.0.1"` | `"13.0.0.1"` | + | `"10.0.0.1,11.0.0.1"` | `"10.0.0.1,11.0.0.1"` | `""` | + +```yaml tab="Docker" +labels: +- "traefik.http.middlewares.test-inflightreq.inflightreq.sourcecriterion.ipstrategy.excludedips=127.0.0.1/32, 192.168.1.7" +``` + +```yaml tab="Kubernetes" +apiVersion: traefik.containo.us/v1alpha1 +kind: Middleware +metadata: + name: test-inflightreq +spec: + inFlightReq: + sourceCriterion: + ipStrategy: + excludedIPs: + - 127.0.0.1/32 + - 192.168.1.7 +``` + +```yaml tab="Rancher" +labels: +- "traefik.http.middlewares.test-inflightreq.inflightreq.sourcecriterion.ipstrategy.excludedips=127.0.0.1/32, 192.168.1.7" +``` + +```json tab="Marathon" +"labels": { + "traefik.http.middlewares.test-inflightreq.inflightreq.sourcecriterion.ipstrategy.excludedips": "127.0.0.1/32, 192.168.1.7" +} +``` + +```toml tab="File (TOML)" +[http.middlewares] + [http.middlewares.test-inflightreq.inflightreq] + [http.middlewares.test-inflightreq.inFlightReq.sourceCriterion.ipStrategy] + excludedIPs = ["127.0.0.1/32", "192.168.1.7"] +``` + +```yaml tab="File (YAML)" +http: + middlewares: + test-inflightreq: + inFlightReq: + sourceCriterion: + ipStrategy: + excludedIPs: + - "127.0.0.1/32" + - "192.168.1.7" +``` + +#### `sourceCriterion.requestHeaderName` + +Requests having the same value for the given header are grouped as coming from the same source. + +```yaml tab="Docker" +labels: +- "traefik.http.middlewares.test-inflightreq.inflightreq.sourcecriterion.requestheadername=username" +``` + +```yaml tab="Kubernetes" +apiVersion: traefik.containo.us/v1alpha1 +kind: Middleware +metadata: + name: test-inflightreq +spec: + inFlightReq: + sourceCriterion: + requestHeaderName: username +``` + +```yaml tab="Rancher" +labels: +- "traefik.http.middlewares.test-inflightreq.inflightreq.sourcecriterion.requestheadername=username" +``` + +```json tab="Marathon" +"labels": { + "traefik.http.middlewares.test-inflightreq.inflightreq.sourcecriterion.requestheadername": "username" +} +``` + +```toml tab="File (TOML)" +[http.middlewares] + [http.middlewares.test-inflightreq.inflightreq] + [http.middlewares.test-inflightreq.inFlightReq.sourceCriterion] + requestHeaderName = "username" +``` + +```yaml tab="File (YAML)" +http: + middlewares: + test-inflightreq: + inFlightReq: + sourceCriterion: + requestHeaderName: username +``` + +#### `sourceCriterion.requestHost` + +Whether to consider the request host as the source. + +```yaml tab="Docker" +labels: +- "traefik.http.middlewares.test-inflightreq.inflightreq.sourcecriterion.requesthost=true" +``` + +```yaml tab="Kubernetes" +apiVersion: traefik.containo.us/v1alpha1 +kind: Middleware +metadata: + name: test-inflightreq +spec: + inFlightReq: + sourceCriterion: + requestHost: true +``` + +```yaml tab="Rancher" +labels: +- "traefik.http.middlewares.test-inflightreq.inflightreq.sourcecriterion.requesthost=true" +``` + +```json tab="Marathon" +"labels": { + "traefik.http.middlewares.test-inflightreq.inflightreq.sourcecriterion.requesthost": "true" +} +``` + +```toml tab="File (TOML)" +[http.middlewares] + [http.middlewares.test-inflightreq.inflightreq] + [http.middlewares.test-inflightreq.inFlightReq.sourceCriterion] + requestHost = true +``` + +```yaml tab="File (YAML)" +http: + middlewares: + test-inflightreq: + inFlightReq: + sourceCriterion: + requestHost: true +``` diff --git a/docs/content/middlewares/maxconnection.md b/docs/content/middlewares/maxconnection.md deleted file mode 100644 index bfddf30d9..000000000 --- a/docs/content/middlewares/maxconnection.md +++ /dev/null @@ -1,71 +0,0 @@ -# MaxConnection - -Limiting the Number of Simultaneous Clients -{: .subtitle } - -![MaxConnection](../assets/img/middleware/maxconnection.png) - -To proactively prevent services from being overwhelmed with high load, a maximum connection limit can be applied. - -## Configuration Examples - -```yaml tab="Docker" -# Limiting to 10 simultaneous connections -labels: -- "traefik.http.middlewares.test-maxconn.maxconn.amount=10" -``` - -```yaml tab="Kubernetes" -apiVersion: traefik.containo.us/v1alpha1 -kind: Middleware -metadata: - name: test-maxconn -spec: - maxConn: - amount: 10 -``` - -```json tab="Marathon" -"labels": { - "traefik.http.middlewares.test-maxconn.maxconn.amount": "10" -} -``` - -```yaml tab="Rancher" -# Limiting to 10 simultaneous connections -labels: -- "traefik.http.middlewares.test-maxconn.maxconn.amount=10" -``` - -```toml tab="File (TOML)" -# Limiting to 10 simultaneous connections -[http.middlewares] - [http.middlewares.test-maxconn.maxConn] - amount = 10 -``` - -```yaml tab="File (YAML)" -# Limiting to 10 simultaneous connections -http: - middlewares: - test-maxconn: - maxConn: - amount: 10 -``` - -## Configuration Options - -### `amount` - -The `amount` option defines the maximum amount of allowed simultaneous connections. -The middleware will return an `HTTP 429 Too Many Requests` if there are already `amount` requests in progress (based on the same `extractorFunc` strategy). - -### `extractorFunc` - -The `extractorFunc` defines the strategy used to categorize requests. - -The possible values are: - -- `request.host` categorizes requests based on the request host. -- `client.ip` categorizes requests based on the client ip. -- `request.header.ANY_HEADER` categorizes requests based on the provided `ANY_HEADER` value. diff --git a/docs/content/middlewares/overview.md b/docs/content/middlewares/overview.md index 91740e41f..a69ccddca 100644 --- a/docs/content/middlewares/overview.md +++ b/docs/content/middlewares/overview.md @@ -208,7 +208,7 @@ and therefore this specification would be ignored even if present. | [ForwardAuth](forwardauth.md) | Authentication delegation | Security, Authentication | | [Headers](headers.md) | Add / Update headers | Security | | [IPWhiteList](ipwhitelist.md) | Limit the allowed client IPs | Security, Request lifecycle | -| [MaxConnection](maxconnection.md) | Limit the number of simultaneous connections | Security, Request lifecycle | +| [InFlightReq](inflightreq.md) | Limit the number of simultaneous connections | Security, Request lifecycle | | [PassTLSClientCert](passtlsclientcert.md) | Adding Client Certificates in a Header | Security | | [RateLimit](ratelimit.md) | Limit the call frequency | Security, Request lifecycle | | [RedirectScheme](redirectscheme.md) | Redirect easily the client elsewhere | Request lifecycle | diff --git a/docs/content/middlewares/ratelimit.md b/docs/content/middlewares/ratelimit.md index fa3856df5..058c67b27 100644 --- a/docs/content/middlewares/ratelimit.md +++ b/docs/content/middlewares/ratelimit.md @@ -1,9 +1,6 @@ # RateLimit -!!! warning - This middleware is disable for now. - -Protection from Too Many Calls +To Control the Number of Requests Going to a Service {: .subtitle } ![RateLimit](../assets/img/middleware/ratelimit.png) @@ -13,124 +10,337 @@ The RateLimit middleware ensures that services will receive a _fair_ number of r ## Configuration Example ```yaml tab="Docker" -# Here, an average of 5 requests every 3 seconds is allowed and an average of 100 requests every 10 seconds. -# These can "burst" up to 10 and 200 in each period, respectively. +# Here, an average of 100 requests per second is allowed. +# In addition, a burst of 50 requests is allowed. labels: -- "traefik.http.middlewares.test-ratelimit.ratelimit.extractorfunc=client.ip" -- "traefik.http.middlewares.test-ratelimit.ratelimit.rateset.rate0.period=10s" -- "traefik.http.middlewares.test-ratelimit.ratelimit.rateset.rate0.average=100" -- "traefik.http.middlewares.test-ratelimit.ratelimit.rateset.rate0.burst=200" -- "traefik.http.middlewares.test-ratelimit.ratelimit.rateset.rate1.period=3s" -- "traefik.http.middlewares.test-ratelimit.ratelimit.rateset.rate1.average=5" -- "traefik.http.middlewares.test-ratelimit.ratelimit.rateset.rate1.burst=10" - +- "traefik.http.middlewares.test-ratelimit.ratelimit.average=100" +- "traefik.http.middlewares.test-ratelimit.ratelimit.burst=50" ``` ```yaml tab="Kubernetes" -# Here, an average of 5 requests every 3 seconds is allowed and an average of 100 requests every 10 seconds. -# These can "burst" up to 10 and 200 in each period, respectively. +# Here, an average of 100 requests per second is allowed. +# In addition, a burst of 50 requests is allowed. apiVersion: traefik.containo.us/v1alpha1 kind: Middleware metadata: name: test-ratelimit spec: rateLimit: - extractorFunc: client.ip - rateSet: - rate0: - period: 10s - average: 100 - burst: 200 - rate1: - period: 3s - average: 5 - burst: 10 + average: 100 + burst: 50 ``` ```json tab="Marathon" "labels": { - "traefik.http.middlewares.test-ratelimit.ratelimit.extractorfunc": "client.ip", - "traefik.http.middlewares.test-ratelimit.ratelimit.rateset.rate0.period": "10s", - "traefik.http.middlewares.test-ratelimit.ratelimit.rateset.rate0.average": "100", - "traefik.http.middlewares.test-ratelimit.ratelimit.rateset.rate0.burst": "200", - "traefik.http.middlewares.test-ratelimit.ratelimit.rateset.rate1.period": "3s", - "traefik.http.middlewares.test-ratelimit.ratelimit.rateset.rate1.average": "5", - "traefik.http.middlewares.test-ratelimit.ratelimit.rateset.rate1.burst": "10" + "traefik.http.middlewares.test-ratelimit.ratelimit.average": "100", + "traefik.http.middlewares.test-ratelimit.ratelimit.burst": "50" } ``` ```yaml tab="Rancher" -# Here, an average of 5 requests every 3 seconds is allowed and an average of 100 requests every 10 seconds. -# These can "burst" up to 10 and 200 in each period, respectively. +# Here, an average of 100 requests per second is allowed. +# In addition, a burst of 50 requests is allowed. labels: -- "traefik.http.middlewares.test-ratelimit.ratelimit.extractorfunc=client.ip" -- "traefik.http.middlewares.test-ratelimit.ratelimit.rateset.rate0.period=10s" -- "traefik.http.middlewares.test-ratelimit.ratelimit.rateset.rate0.average=100" -- "traefik.http.middlewares.test-ratelimit.ratelimit.rateset.rate0.burst=200" -- "traefik.http.middlewares.test-ratelimit.ratelimit.rateset.rate1.period=3s" -- "traefik.http.middlewares.test-ratelimit.ratelimit.rateset.rate1.average=5" -- "traefik.http.middlewares.test-ratelimit.ratelimit.rateset.rate1.burst=10" - +- "traefik.http.middlewares.test-ratelimit.ratelimit.average=100" +- "traefik.http.middlewares.test-ratelimit.ratelimit.burst=50" ``` ```toml tab="File (TOML)" -# Here, an average of 5 requests every 3 seconds is allowed and an average of 100 requests every 10 seconds. -# These can "burst" up to 10 and 200 in each period, respectively. +# Here, an average of 100 requests per second is allowed. +# In addition, a burst of 50 requests is allowed. [http.middlewares] [http.middlewares.test-ratelimit.rateLimit] - extractorFunc = "client.ip" - - [http.middlewares.test-ratelimit.rateLimit.rateSet.rate0] - period = "10s" - average = 100 - burst = 200 - - [http.middlewares.test-ratelimit.rateLimit.rateSet.rate1] - period = "3s" - average = 5 - burst = 10 + average = 100 + burst = 50 ``` ```yaml tab="File (YAML)" -# Here, an average of 5 requests every 3 seconds is allowed and an average of 100 requests every 10 seconds. -# These can "burst" up to 10 and 200 in each period, respectively. +# Here, an average of 100 requests per second is allowed. +# In addition, a burst of 50 requests is allowed. http: middlewares: test-ratelimit: rateLimit: - extractorFunc: "client.ip" - rateSet: - rate0: - period: "10s" - average: 100 - burst: 200 - rate1: - period: "3s" - average: 5 - burst: 10 + average: 100 + burst: 50 ``` ## Configuration Options -### `extractorFunc` +### `average` + +Average is the maximum rate, in requests/s, allowed for the given source. +It defaults to 0, which means no rate limiting. + +```yaml tab="Docker" +labels: +- "traefik.http.middlewares.test-ratelimit.ratelimit.average=100" +``` + +```yaml tab="Kubernetes" +apiVersion: traefik.containo.us/v1alpha1 +kind: Middleware +metadata: + name: test-ratelimit +spec: + rateLimit: + average: 100 +``` + +```json tab="Marathon" +"labels": { + "traefik.http.middlewares.test-ratelimit.ratelimit.average": "100", +} +``` + +```yaml tab="Rancher" +labels: +- "traefik.http.middlewares.test-ratelimit.ratelimit.average=100" +``` + +```toml tab="File (TOML)" +[http.middlewares] + [http.middlewares.test-ratelimit.rateLimit] + average = 100 +``` + +```yaml tab="File (YAML)" +http: + middlewares: + test-ratelimit: + rateLimit: + average: 100 +``` + +### `burst` + +Burst is the maximum number of requests allowed to go through in the same arbitrarily small period of time. +It defaults to 1. + +```yaml tab="Docker" +labels: +- "traefik.http.middlewares.test-ratelimit.ratelimit.burst=100" +``` + +```yaml tab="Kubernetes" +apiVersion: traefik.containo.us/v1alpha1 +kind: Middleware +metadata: + name: test-ratelimit +spec: + rateLimit: + burst: 100 +``` + +```json tab="Marathon" +"labels": { + "traefik.http.middlewares.test-ratelimit.ratelimit.burst": "100", +} +``` + +```yaml tab="Rancher" +labels: +- "traefik.http.middlewares.test-ratelimit.ratelimit.burst=100" + +``` + +```toml tab="File (TOML)" +[http.middlewares] + [http.middlewares.test-ratelimit.rateLimit] + burst = 100 +``` + +```yaml tab="File (YAML)" +http: + middlewares: + test-ratelimit: + rateLimit: + burst: 100 +``` + +### `sourceCriterion` -The `extractorFunc` option defines the strategy used to categorize requests. +SourceCriterion defines what criterion is used to group requests as originating from a common source. +The precedence order is `ipStrategy`, then `requestHeaderName`, then `requestHost`. +If none are set, the default is to use the request's remote address field (as an `ipStrategy`). -The possible values are: +#### `sourceCriterion.ipStrategy` -- `request.host` categorizes requests based on the request host. -- `client.ip` categorizes requests based on the client ip. -- `request.header.ANY_HEADER` categorizes requests based on the provided `ANY_HEADER` value. +The `ipStrategy` option defines two parameters that sets how Traefik will determine the client IP: `depth`, and `excludedIPs`. -### `rateSet` +##### `ipStrategy.depth` -You can combine multiple rate limits. -The rate limit will trigger with the first reached limit. +The `depth` option tells Traefik to use the `X-Forwarded-For` header and take the IP located at the `depth` position (starting from the right). -Each rate limit has 3 options, `period`, `average`, and `burst`. +- If `depth` is greater than the total number of IPs in `X-Forwarded-For`, then the client IP will be empty. +- `depth` is ignored if its value is is lesser than or equal to 0. -The rate limit will allow an average of `average` requests every `period`, with a maximum of `burst` request on that period. +!!! note "Example of Depth & X-Forwarded-For" -!!! note "Period Format" + If `depth` was equal to 2, and the request `X-Forwarded-For` header was `"10.0.0.1,11.0.0.1,12.0.0.1,13.0.0.1"` then the "real" client IP would be `"10.0.0.1"` (at depth 4) but the IP used as the criterion would be `"12.0.0.1"` (`depth=2`). - Period is to be given in a format understood by [time.ParseDuration](https://golang.org/pkg/time/#ParseDuration). + | `X-Forwarded-For` | `depth` | clientIP | + |-----------------------------------------|---------|--------------| + | `"10.0.0.1,11.0.0.1,12.0.0.1,13.0.0.1"` | `1` | `"13.0.0.1"` | + | `"10.0.0.1,11.0.0.1,12.0.0.1,13.0.0.1"` | `3` | `"11.0.0.1"` | + | `"10.0.0.1,11.0.0.1,12.0.0.1,13.0.0.1"` | `5` | `""` | + +##### `ipStrategy.excludedIPs` + +`excludedIPs` tells Traefik to scan the `X-Forwarded-For` header and pick the first IP not in the list. + +!!! important + If `depth` is specified, `excludedIPs` is ignored. + +!!! note "Example of ExcludedIPs & X-Forwarded-For" + + | `X-Forwarded-For` | `excludedIPs` | clientIP | + |-----------------------------------------|-----------------------|--------------| + | `"10.0.0.1,11.0.0.1,12.0.0.1,13.0.0.1"` | `"12.0.0.1,13.0.0.1"` | `"11.0.0.1"` | + | `"10.0.0.1,11.0.0.1,12.0.0.1,13.0.0.1"` | `"15.0.0.1,13.0.0.1"` | `"12.0.0.1"` | + | `"10.0.0.1,11.0.0.1,12.0.0.1,13.0.0.1"` | `"10.0.0.1,13.0.0.1"` | `"12.0.0.1"` | + | `"10.0.0.1,11.0.0.1,12.0.0.1,13.0.0.1"` | `"15.0.0.1,16.0.0.1"` | `"13.0.0.1"` | + | `"10.0.0.1,11.0.0.1"` | `"10.0.0.1,11.0.0.1"` | `""` | + +```yaml tab="Docker" +labels: +- "traefik.http.middlewares.test-ratelimit.ratelimit.sourcecriterion.ipstrategy.excludedips=127.0.0.1/32, 192.168.1.7" +``` + +```yaml tab="Kubernetes" +apiVersion: traefik.containo.us/v1alpha1 +kind: Middleware +metadata: + name: test-ratelimit +spec: + rateLimit: + sourceCriterion: + ipStrategy: + excludedIPs: + - 127.0.0.1/32 + - 192.168.1.7 +``` + +```yaml tab="Rancher" +labels: +- "traefik.http.middlewares.test-ratelimit.ratelimit.sourcecriterion.ipstrategy.excludedips=127.0.0.1/32, 192.168.1.7" +``` + +```json tab="Marathon" +"labels": { + "traefik.http.middlewares.test-ratelimit.ratelimit.sourcecriterion.ipstrategy.excludedips": "127.0.0.1/32, 192.168.1.7" +} +``` + +```toml tab="File (TOML)" +[http.middlewares] + [http.middlewares.test-ratelimit.rateLimit] + [http.middlewares.test-ratelimit.rateLimit.sourceCriterion.ipStrategy] + excludedIPs = ["127.0.0.1/32", "192.168.1.7"] +``` + +```yaml tab="File (YAML)" +http: + middlewares: + test-ratelimit: + rateLimit: + sourceCriterion: + ipStrategy: + excludedIPs: + - "127.0.0.1/32" + - "192.168.1.7" +``` + +#### `sourceCriterion.requestHeaderName` + +Requests having the same value for the given header are grouped as coming from the same source. + +```yaml tab="Docker" +labels: +- "traefik.http.middlewares.test-ratelimit.ratelimit.sourcecriterion.requestheadername=username" +``` + +```yaml tab="Kubernetes" +apiVersion: traefik.containo.us/v1alpha1 +kind: Middleware +metadata: + name: test-ratelimit +spec: + rateLimit: + sourceCriterion: + requestHeaderName: username +``` + +```yaml tab="Rancher" +labels: +- "traefik.http.middlewares.test-ratelimit.ratelimit.sourcecriterion.requestheadername=username" +``` + +```json tab="Marathon" +"labels": { + "traefik.http.middlewares.test-ratelimit.ratelimit.sourcecriterion.requestheadername": "username" +} +``` + +```toml tab="File (TOML)" +[http.middlewares] + [http.middlewares.test-ratelimit.rateLimit] + [http.middlewares.test-ratelimit.rateLimit.sourceCriterion] + requestHeaderName = "username" +``` + +```yaml tab="File (YAML)" +http: + middlewares: + test-ratelimit: + rateLimit: + sourceCriterion: + requestHeaderName: username +``` + +#### `sourceCriterion.requestHost` + +Whether to consider the request host as the source. + +```yaml tab="Docker" +labels: +- "traefik.http.middlewares.test-ratelimit.ratelimit.sourcecriterion.requesthost=true" +``` + +```yaml tab="Kubernetes" +apiVersion: traefik.containo.us/v1alpha1 +kind: Middleware +metadata: + name: test-ratelimit +spec: + rateLimit: + sourceCriterion: + requestHost: true +``` + +```yaml tab="Rancher" +labels: +- "traefik.http.middlewares.test-ratelimit.ratelimit.sourcecriterion.requesthost=true" +``` + +```json tab="Marathon" +"labels": { + "traefik.http.middlewares.test-ratelimit.ratelimit.sourcecriterion.requesthost": "true" +} +``` + +```toml tab="File (TOML)" +[http.middlewares] + [http.middlewares.test-ratelimit.rateLimit] + [http.middlewares.test-ratelimit.rateLimit.sourceCriterion] + requestHost = true +``` + +```yaml tab="File (YAML)" +http: + middlewares: + test-ratelimit: + rateLimit: + sourceCriterion: + requestHost: true +``` \ No newline at end of file diff --git a/docs/content/reference/dynamic-configuration/docker-labels.yml b/docs/content/reference/dynamic-configuration/docker-labels.yml index 69c40b9f5..c01640149 100644 --- a/docs/content/reference/dynamic-configuration/docker-labels.yml +++ b/docs/content/reference/dynamic-configuration/docker-labels.yml @@ -64,8 +64,12 @@ - "traefik.http.middlewares.middleware10.ipwhitelist.ipstrategy.depth=42" - "traefik.http.middlewares.middleware10.ipwhitelist.ipstrategy.excludedips=foobar, foobar" - "traefik.http.middlewares.middleware10.ipwhitelist.sourcerange=foobar, foobar" -- "traefik.http.middlewares.middleware11.maxconn.amount=42" -- "traefik.http.middlewares.middleware11.maxconn.extractorfunc=foobar" +- "traefik.http.middlewares.middleware11.inflightreq.amount=42" +- "traefik.http.middlewares.middleware11.inflightreq.sourcecriterion.requestheadername=foobar" +- "traefik.http.middlewares.middleware11.inflightreq.sourcecriterion.requesthost=true" +- "traefik.http.middlewares.middleware11.inflightreq.sourcecriterion.ipstrategy.depth=42" +- "traefik.http.middlewares.middleware11.inflightreq.sourcecriterion.ipstrategy.excludedips=foobar, foobar" +- "traefik.http.middlewares.middleware11.inflightreq.sourcecriterion.requesthost=true" - "traefik.http.middlewares.middleware12.passtlsclientcert.info.issuer.commonname=true" - "traefik.http.middlewares.middleware12.passtlsclientcert.info.issuer.country=true" - "traefik.http.middlewares.middleware12.passtlsclientcert.info.issuer.domaincomponent=true" diff --git a/docs/content/reference/dynamic-configuration/file.toml b/docs/content/reference/dynamic-configuration/file.toml index 1e0a6bfb5..71ad8d891 100644 --- a/docs/content/reference/dynamic-configuration/file.toml +++ b/docs/content/reference/dynamic-configuration/file.toml @@ -170,9 +170,14 @@ depth = 42 excludedIPs = ["foobar", "foobar"] [http.middlewares.Middleware11] - [http.middlewares.Middleware11.maxConn] + [http.middlewares.Middleware11.inFlightReq] amount = 42 - extractorFunc = "foobar" + [http.middlewares.Middleware11.inFlightReq.sourceCriterion] + requestHeaderName = "foobar" + requestHost = true + [http.middlewares.Middleware11.inFlightReq.sourceCriterion.ipStrategy] + depth = 42 + excludedIPs = ["foobar", "foobar"] [http.middlewares.Middleware12] [http.middlewares.Middleware12.passTLSClientCert] pem = true diff --git a/docs/content/reference/dynamic-configuration/file.yaml b/docs/content/reference/dynamic-configuration/file.yaml index bea253fdc..89753c2a5 100644 --- a/docs/content/reference/dynamic-configuration/file.yaml +++ b/docs/content/reference/dynamic-configuration/file.yaml @@ -198,9 +198,14 @@ http: - foobar - foobar Middleware11: - maxConn: + inFlightReq: amount: 42 - extractorFunc: foobar + sourceCriterion: + ipStrategy: + depth: 42 + excludedIPs: [ foobar, foobar ] + requestHeaderName: foobar + requestHost: true Middleware12: passTLSClientCert: pem: true @@ -254,6 +259,16 @@ http: regex: - foobar - foobar + Middleware20: + rateLimit: + average: 42 + burst: 42 + sourceCriterion: + ipStrategy: + depth: 42 + excludedIPs: [ foobar, foobar ] + requestHeaderName: foobar + requestHost: true tcp: routers: TCPRouter0: diff --git a/docs/content/reference/dynamic-configuration/marathon-labels.json b/docs/content/reference/dynamic-configuration/marathon-labels.json index 6c2432fdb..163924b4a 100644 --- a/docs/content/reference/dynamic-configuration/marathon-labels.json +++ b/docs/content/reference/dynamic-configuration/marathon-labels.json @@ -64,8 +64,11 @@ "traefik.http.middlewares.middleware10.ipwhitelist.ipstrategy.depth": "42", "traefik.http.middlewares.middleware10.ipwhitelist.ipstrategy.excludedips": "foobar, foobar", "traefik.http.middlewares.middleware10.ipwhitelist.sourcerange": "foobar, foobar", -"traefik.http.middlewares.middleware11.maxconn.amount": "42", -"traefik.http.middlewares.middleware11.maxconn.extractorfunc": "foobar", +"traefik.http.middlewares.Middleware11.inflightreq.amount": "42", +"traefik.http.middlewares.Middleware11.inflightreq.sourcecriterion.ipstrategy.depth": "42", +"traefik.http.middlewares.Middleware11.inflightreq.sourcecriterion.ipstrategy.excludedips": "foobar, fiibar", +"traefik.http.middlewares.Middleware11.inflightreq.sourcecriterion.requestheadername": "foobar", +"traefik.http.middlewares.Middleware11.inflightreq.sourcecriterion.requesthost": "true", "traefik.http.middlewares.middleware12.passtlsclientcert.info.issuer.commonname": "true", "traefik.http.middlewares.middleware12.passtlsclientcert.info.issuer.country": "true", "traefik.http.middlewares.middleware12.passtlsclientcert.info.issuer.domaincomponent": "true", @@ -96,6 +99,12 @@ "traefik.http.middlewares.middleware17.retry.attempts": "42", "traefik.http.middlewares.middleware18.stripprefix.prefixes": "foobar, foobar", "traefik.http.middlewares.middleware19.stripprefixregex.regex": "foobar, foobar", +"traefik.http.middlewares.Middleware20.ratelimit.average": "42", +"traefik.http.middlewares.Middleware20.ratelimit.burst": "42", +"traefik.http.middlewares.Middleware20.ratelimit.sourcecriterion.requestheadername": "foobar", +"traefik.http.middlewares.Middleware20.ratelimit.sourcecriterion.requesthost": "true", +"traefik.http.middlewares.Middleware20.ratelimit.sourcecriterion.ipstrategy.depth": "42", +"traefik.http.middlewares.Middleware20.ratelimit.sourcecriterion.ipstrategy.excludedips": "foobar, foobar", "traefik.http.routers.router0.entrypoints": "foobar, foobar", "traefik.http.routers.router0.middlewares": "foobar, foobar", "traefik.http.routers.router0.priority": "42", diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index 76f03293b..46cba835d 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -102,7 +102,7 @@ nav: - 'ForwardAuth': 'middlewares/forwardauth.md' - 'Headers': 'middlewares/headers.md' - 'IpWhitelist': 'middlewares/ipwhitelist.md' - - 'Maxconn': 'middlewares/maxconnection.md' + - 'InFlightReq': 'middlewares/inflightreq.md' - 'PassTLSClientCert': 'middlewares/passtlsclientcert.md' - 'RateLimit': 'middlewares/ratelimit.md' - 'RedirectRegex': 'middlewares/redirectregex.md' diff --git a/go.mod b/go.mod index df006a4ad..9facb5675 100644 --- a/go.mod +++ b/go.mod @@ -57,6 +57,7 @@ require ( github.com/libkermit/docker-check v0.0.0-20171122104347-1113af38e591 github.com/looplab/fsm v0.1.0 // indirect github.com/magiconair/properties v1.8.1 // indirect + github.com/mailgun/ttlmap v0.0.0-20170619185759-c1c17f74874f github.com/miekg/dns v1.1.15 github.com/mitchellh/copystructure v1.0.0 github.com/mitchellh/hashstructure v1.0.0 @@ -90,6 +91,7 @@ require ( github.com/vulcand/predicate v1.1.0 golang.org/x/net v0.0.0-20190724013045-ca1201d0de80 golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a // indirect + golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 // indirect google.golang.org/grpc v1.22.1 gopkg.in/DataDog/dd-trace-go.v1 v1.16.1 diff --git a/integration/fixtures/ratelimit/simple.toml b/integration/fixtures/ratelimit/simple.toml index 49373088a..7bdb5a367 100644 --- a/integration/fixtures/ratelimit/simple.toml +++ b/integration/fixtures/ratelimit/simple.toml @@ -24,15 +24,8 @@ [http.middlewares] [http.middlewares.ratelimit.rateLimit] - extractorfunc = "client.ip" - [http.middlewares.ratelimit.rateLimit.rateSet.rateset1] - period = "60s" - average = 4 - burst = 5 - [http.middlewares.ratelimit.rateLimit.rateSet.rateset2] - period = "3s" - average = 1 - burst = 2 + average = 100 + burst = 1 [http.services] [http.services.service1] diff --git a/integration/integration_test.go b/integration/integration_test.go index 2ec9a448e..934da96d8 100644 --- a/integration/integration_test.go +++ b/integration/integration_test.go @@ -54,8 +54,7 @@ func init() { check.Suite(&LogRotationSuite{}) check.Suite(&MarathonSuite{}) check.Suite(&MarathonSuite15{}) - // TODO: disable temporarily - // check.Suite(&RateLimitSuite{}) + check.Suite(&RateLimitSuite{}) check.Suite(&RestSuite{}) check.Suite(&RetrySuite{}) check.Suite(&SimpleSuite{}) diff --git a/integration/ratelimit_test.go b/integration/ratelimit_test.go index bd43117a9..32a9c12c3 100644 --- a/integration/ratelimit_test.go +++ b/integration/ratelimit_test.go @@ -37,29 +37,19 @@ func (s *RateLimitSuite) TestSimpleConfiguration(c *check.C) { err = try.GetRequest("http://127.0.0.1:8080/api/rawdata", 1*time.Second, try.BodyContains("ratelimit")) c.Assert(err, checker.IsNil) - err = try.GetRequest("http://127.0.0.1:8081/", 500*time.Millisecond, try.StatusCodeIs(http.StatusOK)) - c.Assert(err, checker.IsNil) - err = try.GetRequest("http://127.0.0.1:8081/", 500*time.Millisecond, try.StatusCodeIs(http.StatusOK)) - c.Assert(err, checker.IsNil) - err = try.GetRequest("http://127.0.0.1:8081/", 500*time.Millisecond, try.StatusCodeIs(http.StatusTooManyRequests)) - c.Assert(err, checker.IsNil) - - // sleep for 4 seconds to be certain the configured time period has elapsed - // then test another request and verify a 200 status code - time.Sleep(4 * time.Second) - err = try.GetRequest("http://127.0.0.1:8081/", 500*time.Millisecond, try.StatusCodeIs(http.StatusOK)) - c.Assert(err, checker.IsNil) - - // continue requests at 3 second intervals to test the other rate limit time period - time.Sleep(3 * time.Second) - err = try.GetRequest("http://127.0.0.1:8081/", 500*time.Millisecond, try.StatusCodeIs(http.StatusOK)) - c.Assert(err, checker.IsNil) - - time.Sleep(3 * time.Second) - err = try.GetRequest("http://127.0.0.1:8081/", 500*time.Millisecond, try.StatusCodeIs(http.StatusOK)) - c.Assert(err, checker.IsNil) - - time.Sleep(3 * time.Second) - err = try.GetRequest("http://127.0.0.1:8081/", 500*time.Millisecond, try.StatusCodeIs(http.StatusTooManyRequests)) - c.Assert(err, checker.IsNil) + start := time.Now() + count := 0 + for { + err = try.GetRequest("http://127.0.0.1:8081/", 500*time.Millisecond, try.StatusCodeIs(http.StatusOK)) + c.Assert(err, checker.IsNil) + count++ + if count > 100 { + break + } + } + stop := time.Now() + elapsed := stop.Sub(start) + if elapsed < time.Second*99/100 { + c.Fatalf("requests throughput was too fast wrt to rate limiting: 100 requests in %v", elapsed) + } } diff --git a/pkg/config/dynamic/fixtures/sample.toml b/pkg/config/dynamic/fixtures/sample.toml index 748cf6497..a90cee3f6 100644 --- a/pkg/config/dynamic/fixtures/sample.toml +++ b/pkg/config/dynamic/fixtures/sample.toml @@ -295,9 +295,14 @@ key = "foobar" insecureSkipVerify = true [http.middlewares.Middleware16] - [http.middlewares.Middleware16.maxConn] + [http.middlewares.Middleware16.inFlightReq] amount = 42 - extractorFunc = "foobar" + [http.middlewares.Middleware16.inFlightReq.sourceCriterion] + requestHeaderName = "foobar" + requestHost = true + [http.middlewares.Middleware16.inFlightReq.sourceCriterion.ipStrategy] + depth = 42 + excludedIPs = ["foobar", "foobar"] [http.middlewares.Middleware17] [http.middlewares.Middleware17.buffering] maxRequestBodyBytes = 42 diff --git a/pkg/config/dynamic/middlewares.go b/pkg/config/dynamic/middlewares.go index 578ed2e28..b8fb8df27 100644 --- a/pkg/config/dynamic/middlewares.go +++ b/pkg/config/dynamic/middlewares.go @@ -8,30 +8,28 @@ import ( "os" "github.com/containous/traefik/v2/pkg/ip" - "github.com/containous/traefik/v2/pkg/types" ) // +k8s:deepcopy-gen=true // Middleware holds the Middleware configuration. type Middleware struct { - AddPrefix *AddPrefix `json:"addPrefix,omitempty" toml:"addPrefix,omitempty" yaml:"addPrefix,omitempty"` - StripPrefix *StripPrefix `json:"stripPrefix,omitempty" toml:"stripPrefix,omitempty" yaml:"stripPrefix,omitempty"` - StripPrefixRegex *StripPrefixRegex `json:"stripPrefixRegex,omitempty" toml:"stripPrefixRegex,omitempty" yaml:"stripPrefixRegex,omitempty"` - ReplacePath *ReplacePath `json:"replacePath,omitempty" toml:"replacePath,omitempty" yaml:"replacePath,omitempty"` - ReplacePathRegex *ReplacePathRegex `json:"replacePathRegex,omitempty" toml:"replacePathRegex,omitempty" yaml:"replacePathRegex,omitempty"` - Chain *Chain `json:"chain,omitempty" toml:"chain,omitempty" yaml:"chain,omitempty"` - IPWhiteList *IPWhiteList `json:"ipWhiteList,omitempty" toml:"ipWhiteList,omitempty" yaml:"ipWhiteList,omitempty"` - Headers *Headers `json:"headers,omitempty" toml:"headers,omitempty" yaml:"headers,omitempty"` - Errors *ErrorPage `json:"errors,omitempty" toml:"errors,omitempty" yaml:"errors,omitempty"` - // TODO: disable temporarily - // RateLimit *RateLimit `json:"rateLimit,omitempty" toml:"rateLimit,omitempty" yaml:"rateLimit,omitempty"` + AddPrefix *AddPrefix `json:"addPrefix,omitempty" toml:"addPrefix,omitempty" yaml:"addPrefix,omitempty"` + StripPrefix *StripPrefix `json:"stripPrefix,omitempty" toml:"stripPrefix,omitempty" yaml:"stripPrefix,omitempty"` + StripPrefixRegex *StripPrefixRegex `json:"stripPrefixRegex,omitempty" toml:"stripPrefixRegex,omitempty" yaml:"stripPrefixRegex,omitempty"` + ReplacePath *ReplacePath `json:"replacePath,omitempty" toml:"replacePath,omitempty" yaml:"replacePath,omitempty"` + ReplacePathRegex *ReplacePathRegex `json:"replacePathRegex,omitempty" toml:"replacePathRegex,omitempty" yaml:"replacePathRegex,omitempty"` + Chain *Chain `json:"chain,omitempty" toml:"chain,omitempty" yaml:"chain,omitempty"` + IPWhiteList *IPWhiteList `json:"ipWhiteList,omitempty" toml:"ipWhiteList,omitempty" yaml:"ipWhiteList,omitempty"` + Headers *Headers `json:"headers,omitempty" toml:"headers,omitempty" yaml:"headers,omitempty"` + Errors *ErrorPage `json:"errors,omitempty" toml:"errors,omitempty" yaml:"errors,omitempty"` + RateLimit *RateLimit `json:"rateLimit,omitempty" toml:"rateLimit,omitempty" yaml:"rateLimit,omitempty"` RedirectRegex *RedirectRegex `json:"redirectRegex,omitempty" toml:"redirectRegex,omitempty" yaml:"redirectRegex,omitempty"` RedirectScheme *RedirectScheme `json:"redirectScheme,omitempty" toml:"redirectScheme,omitempty" yaml:"redirectScheme,omitempty"` BasicAuth *BasicAuth `json:"basicAuth,omitempty" toml:"basicAuth,omitempty" yaml:"basicAuth,omitempty"` DigestAuth *DigestAuth `json:"digestAuth,omitempty" toml:"digestAuth,omitempty" yaml:"digestAuth,omitempty"` ForwardAuth *ForwardAuth `json:"forwardAuth,omitempty" toml:"forwardAuth,omitempty" yaml:"forwardAuth,omitempty"` - MaxConn *MaxConn `json:"maxConn,omitempty" toml:"maxConn,omitempty" yaml:"maxConn,omitempty"` + InFlightReq *InFlightReq `json:"inFlightReq,omitempty" toml:"inFlightReq,omitempty" yaml:"inFlightReq,omitempty"` Buffering *Buffering `json:"buffering,omitempty" toml:"buffering,omitempty" yaml:"buffering,omitempty"` CircuitBreaker *CircuitBreaker `json:"circuitBreaker,omitempty" toml:"circuitBreaker,omitempty" yaml:"circuitBreaker,omitempty"` Compress *Compress `json:"compress,omitempty" toml:"compress,omitempty" yaml:"compress,omitempty" label:"allowEmpty"` @@ -219,10 +217,11 @@ func (h *Headers) HasSecureHeadersDefined() bool { type IPStrategy struct { Depth int `json:"depth,omitempty" toml:"depth,omitempty" yaml:"depth,omitempty" export:"true"` ExcludedIPs []string `json:"excludedIPs,omitempty" toml:"excludedIPs,omitempty" yaml:"excludedIPs,omitempty"` + // TODO(mpl): I think we should make RemoteAddr an explicit field. For one thing, it would yield better documentation. } -// Get an IP selection strategy -// if nil return the RemoteAddr strategy +// Get an IP selection strategy. +// If nil return the RemoteAddr strategy // else return a strategy base on the configuration using the X-Forwarded-For Header. // Depth override the ExcludedIPs func (s *IPStrategy) Get() (ip.Strategy, error) { @@ -259,15 +258,17 @@ type IPWhiteList struct { // +k8s:deepcopy-gen=true -// MaxConn holds maximum connection configuration. -type MaxConn struct { - Amount int64 `json:"amount,omitempty" toml:"amount,omitempty" yaml:"amount,omitempty"` - ExtractorFunc string `json:"extractorFunc,omitempty" toml:"extractorFunc,omitempty" yaml:"extractorFunc,omitempty"` +// InFlightReq limits the number of requests being processed and served concurrently. +type InFlightReq struct { + Amount int64 `json:"amount,omitempty" toml:"amount,omitempty" yaml:"amount,omitempty"` + SourceCriterion *SourceCriterion `json:"sourceCriterion,omitempty" toml:"sourceCriterion,omitempty" yaml:"sourceCriterion,omitempty"` } -// SetDefaults Default values for a MaxConn. -func (m *MaxConn) SetDefaults() { - m.ExtractorFunc = "request.host" +// SetDefaults Default values for a InFlightReq. +func (i *InFlightReq) SetDefaults() { + i.SourceCriterion = &SourceCriterion{ + RequestHost: true, + } } // +k8s:deepcopy-gen=true @@ -280,25 +281,34 @@ type PassTLSClientCert struct { // +k8s:deepcopy-gen=true -// Rate holds the rate limiting configuration for a specific time period. -type Rate struct { - Period types.Duration `json:"period,omitempty" toml:"period,omitempty" yaml:"period,omitempty"` - Average int64 `json:"average,omitempty" toml:"average,omitempty" yaml:"average,omitempty"` - Burst int64 `json:"burst,omitempty" toml:"burst,omitempty" yaml:"burst,omitempty"` +// SourceCriterion defines what criterion is used to group requests as originating from a common source. +// The precedence order is IPStrategy, then RequestHeaderName. +// If none are set, the default is to use the request's remote address field. +type SourceCriterion struct { + IPStrategy *IPStrategy `json:"ipStrategy" toml:"ipStrategy, omitempty"` + RequestHeaderName string `json:"requestHeaderName,omitempty" toml:"requestHeaderName,omitempty" yaml:"requestHeaderName,omitempty"` + RequestHost bool `json:"requestHost,omitempty" toml:"requestHost,omitempty" yaml:"requestHost,omitempty"` } // +k8s:deepcopy-gen=true -// RateLimit holds the rate limiting configuration for a given frontend. +// RateLimit holds the rate limiting configuration for a given router. type RateLimit struct { - RateSet map[string]*Rate `json:"rateSet,omitempty" toml:"rateSet,omitempty" yaml:"rateSet,omitempty"` - // FIXME replace by ipStrategy see oxy and replace - ExtractorFunc string `json:"extractorFunc,omitempty" toml:"extractorFunc,omitempty" yaml:"extractorFunc,omitempty"` + // Average is the maximum rate, in requests/s, allowed for the given source. + // It defaults to 0, which means no rate limiting. + Average int64 `json:"average,omitempty" toml:"average,omitempty" yaml:"average,omitempty"` + // Burst is the maximum number of requests allowed to arrive in the same arbitrarily small period of time. + // It defaults to 1. + Burst int64 `json:"burst,omitempty" toml:"burst,omitempty" yaml:"burst,omitempty"` + SourceCriterion *SourceCriterion `json:"sourceCriterion,omitempty" toml:"sourceCriterion,omitempty" yaml:"sourceCriterion,omitempty"` } -// SetDefaults Default values for a MaxConn. +// SetDefaults sets the default values on a RateLimit. func (r *RateLimit) SetDefaults() { - r.ExtractorFunc = "request.host" + r.Burst = 1 + r.SourceCriterion = &SourceCriterion{ + IPStrategy: &IPStrategy{}, + } } // +k8s:deepcopy-gen=true @@ -398,30 +408,30 @@ type ClientTLS struct { } // CreateTLSConfig creates a TLS config from ClientTLS structures. -func (clientTLS *ClientTLS) CreateTLSConfig() (*tls.Config, error) { - if clientTLS == nil { +func (c *ClientTLS) CreateTLSConfig() (*tls.Config, error) { + if c == nil { return nil, nil } var err error caPool := x509.NewCertPool() clientAuth := tls.NoClientCert - if clientTLS.CA != "" { + if c.CA != "" { var ca []byte - if _, errCA := os.Stat(clientTLS.CA); errCA == nil { - ca, err = ioutil.ReadFile(clientTLS.CA) + if _, errCA := os.Stat(c.CA); errCA == nil { + ca, err = ioutil.ReadFile(c.CA) if err != nil { return nil, fmt.Errorf("failed to read CA. %s", err) } } else { - ca = []byte(clientTLS.CA) + ca = []byte(c.CA) } if !caPool.AppendCertsFromPEM(ca) { return nil, fmt.Errorf("failed to parse CA") } - if clientTLS.CAOptional { + if c.CAOptional { clientAuth = tls.VerifyClientCertIfGiven } else { clientAuth = tls.RequireAndVerifyClientCert @@ -429,16 +439,16 @@ func (clientTLS *ClientTLS) CreateTLSConfig() (*tls.Config, error) { } cert := tls.Certificate{} - _, errKeyIsFile := os.Stat(clientTLS.Key) + _, errKeyIsFile := os.Stat(c.Key) - if !clientTLS.InsecureSkipVerify && (len(clientTLS.Cert) == 0 || len(clientTLS.Key) == 0) { + if !c.InsecureSkipVerify && (len(c.Cert) == 0 || len(c.Key) == 0) { return nil, fmt.Errorf("TLS Certificate or Key file must be set when TLS configuration is created") } - if len(clientTLS.Cert) > 0 && len(clientTLS.Key) > 0 { - if _, errCertIsFile := os.Stat(clientTLS.Cert); errCertIsFile == nil { + if len(c.Cert) > 0 && len(c.Key) > 0 { + if _, errCertIsFile := os.Stat(c.Cert); errCertIsFile == nil { if errKeyIsFile == nil { - cert, err = tls.LoadX509KeyPair(clientTLS.Cert, clientTLS.Key) + cert, err = tls.LoadX509KeyPair(c.Cert, c.Key) if err != nil { return nil, fmt.Errorf("failed to load TLS keypair: %v", err) } @@ -447,7 +457,7 @@ func (clientTLS *ClientTLS) CreateTLSConfig() (*tls.Config, error) { } } else { if errKeyIsFile != nil { - cert, err = tls.X509KeyPair([]byte(clientTLS.Cert), []byte(clientTLS.Key)) + cert, err = tls.X509KeyPair([]byte(c.Cert), []byte(c.Key)) if err != nil { return nil, fmt.Errorf("failed to load TLS keypair: %v", err) @@ -461,7 +471,7 @@ func (clientTLS *ClientTLS) CreateTLSConfig() (*tls.Config, error) { return &tls.Config{ Certificates: []tls.Certificate{cert}, RootCAs: caPool, - InsecureSkipVerify: clientTLS.InsecureSkipVerify, + InsecureSkipVerify: c.InsecureSkipVerify, ClientAuth: clientAuth, }, nil } diff --git a/pkg/config/dynamic/zz_generated.deepcopy.go b/pkg/config/dynamic/zz_generated.deepcopy.go index 454015d9c..84aefd044 100644 --- a/pkg/config/dynamic/zz_generated.deepcopy.go +++ b/pkg/config/dynamic/zz_generated.deepcopy.go @@ -525,17 +525,22 @@ func (in *IPWhiteList) DeepCopy() *IPWhiteList { } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *MaxConn) DeepCopyInto(out *MaxConn) { +func (in *InFlightReq) DeepCopyInto(out *InFlightReq) { *out = *in + if in.SourceCriterion != nil { + in, out := &in.SourceCriterion, &out.SourceCriterion + *out = new(SourceCriterion) + (*in).DeepCopyInto(*out) + } return } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MaxConn. -func (in *MaxConn) DeepCopy() *MaxConn { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new InFlightReq. +func (in *InFlightReq) DeepCopy() *InFlightReq { if in == nil { return nil } - out := new(MaxConn) + out := new(InFlightReq) in.DeepCopyInto(out) return out } @@ -609,6 +614,11 @@ func (in *Middleware) DeepCopyInto(out *Middleware) { *out = new(ErrorPage) (*in).DeepCopyInto(*out) } + if in.RateLimit != nil { + in, out := &in.RateLimit, &out.RateLimit + *out = new(RateLimit) + (*in).DeepCopyInto(*out) + } if in.RedirectRegex != nil { in, out := &in.RedirectRegex, &out.RedirectRegex *out = new(RedirectRegex) @@ -634,10 +644,10 @@ func (in *Middleware) DeepCopyInto(out *Middleware) { *out = new(ForwardAuth) (*in).DeepCopyInto(*out) } - if in.MaxConn != nil { - in, out := &in.MaxConn, &out.MaxConn - *out = new(MaxConn) - **out = **in + if in.InFlightReq != nil { + in, out := &in.InFlightReq, &out.InFlightReq + *out = new(InFlightReq) + (*in).DeepCopyInto(*out) } if in.Buffering != nil { in, out := &in.Buffering, &out.Buffering @@ -698,39 +708,13 @@ func (in *PassTLSClientCert) DeepCopy() *PassTLSClientCert { return out } -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *Rate) DeepCopyInto(out *Rate) { - *out = *in - return -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Rate. -func (in *Rate) DeepCopy() *Rate { - if in == nil { - return nil - } - out := new(Rate) - in.DeepCopyInto(out) - return out -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *RateLimit) DeepCopyInto(out *RateLimit) { *out = *in - if in.RateSet != nil { - in, out := &in.RateSet, &out.RateSet - *out = make(map[string]*Rate, len(*in)) - for key, val := range *in { - var outVal *Rate - if val == nil { - (*out)[key] = nil - } else { - in, out := &val, &outVal - *out = new(Rate) - **out = **in - } - (*out)[key] = outVal - } + if in.SourceCriterion != nil { + in, out := &in.SourceCriterion, &out.SourceCriterion + *out = new(SourceCriterion) + (*in).DeepCopyInto(*out) } return } @@ -996,6 +980,27 @@ func (in *Service) DeepCopy() *Service { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SourceCriterion) DeepCopyInto(out *SourceCriterion) { + *out = *in + if in.IPStrategy != nil { + in, out := &in.IPStrategy, &out.IPStrategy + *out = new(IPStrategy) + (*in).DeepCopyInto(*out) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SourceCriterion. +func (in *SourceCriterion) DeepCopy() *SourceCriterion { + if in == nil { + return nil + } + out := new(SourceCriterion) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Sticky) DeepCopyInto(out *Sticky) { *out = *in @@ -1295,6 +1300,11 @@ func (in Users) DeepCopy() Users { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *WRRService) DeepCopyInto(out *WRRService) { *out = *in + if in.Weight != nil { + in, out := &in.Weight, &out.Weight + *out = new(int) + **out = **in + } return } @@ -1314,7 +1324,9 @@ func (in *WeightedRoundRobin) DeepCopyInto(out *WeightedRoundRobin) { if in.Services != nil { in, out := &in.Services, &out.Services *out = make([]WRRService, len(*in)) - copy(*out, *in) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } } if in.Sticky != nil { in, out := &in.Sticky, &out.Sticky diff --git a/pkg/config/file/fixtures/sample.toml b/pkg/config/file/fixtures/sample.toml index 94dd9e694..920ee573b 100644 --- a/pkg/config/file/fixtures/sample.toml +++ b/pkg/config/file/fixtures/sample.toml @@ -287,9 +287,14 @@ key = "foobar" insecureSkipVerify = true [http.middlewares.Middleware16] - [http.middlewares.Middleware16.maxConn] + [http.middlewares.Middleware16.inFlightReq] amount = 42 - extractorFunc = "foobar" + [http.middlewares.Middleware16.inFlightReq.sourceCriterion] + requestHeaderName = "foobar" + requestHost = true + [http.middlewares.Middleware16.inFlightReq.sourceCriterion.ipStrategy] + depth = 42 + excludedIPs = ["foobar", "foobar"] [http.middlewares.Middleware17] [http.middlewares.Middleware17.buffering] maxRequestBodyBytes = 42 diff --git a/pkg/config/label/label_test.go b/pkg/config/label/label_test.go index 0edc8bc31..1fedf3046 100644 --- a/pkg/config/label/label_test.go +++ b/pkg/config/label/label_test.go @@ -11,112 +11,113 @@ import ( func TestDecodeConfiguration(t *testing.T) { labels := map[string]string{ - "traefik.http.middlewares.Middleware0.addprefix.prefix": "foobar", - "traefik.http.middlewares.Middleware1.basicauth.headerfield": "foobar", - "traefik.http.middlewares.Middleware1.basicauth.realm": "foobar", - "traefik.http.middlewares.Middleware1.basicauth.removeheader": "true", - "traefik.http.middlewares.Middleware1.basicauth.users": "foobar, fiibar", - "traefik.http.middlewares.Middleware1.basicauth.usersfile": "foobar", - "traefik.http.middlewares.Middleware2.buffering.maxrequestbodybytes": "42", - "traefik.http.middlewares.Middleware2.buffering.maxresponsebodybytes": "42", - "traefik.http.middlewares.Middleware2.buffering.memrequestbodybytes": "42", - "traefik.http.middlewares.Middleware2.buffering.memresponsebodybytes": "42", - "traefik.http.middlewares.Middleware2.buffering.retryexpression": "foobar", - "traefik.http.middlewares.Middleware3.chain.middlewares": "foobar, fiibar", - "traefik.http.middlewares.Middleware4.circuitbreaker.expression": "foobar", - "traefik.http.middlewares.Middleware5.digestauth.headerfield": "foobar", - "traefik.http.middlewares.Middleware5.digestauth.realm": "foobar", - "traefik.http.middlewares.Middleware5.digestauth.removeheader": "true", - "traefik.http.middlewares.Middleware5.digestauth.users": "foobar, fiibar", - "traefik.http.middlewares.Middleware5.digestauth.usersfile": "foobar", - "traefik.http.middlewares.Middleware6.errors.query": "foobar", - "traefik.http.middlewares.Middleware6.errors.service": "foobar", - "traefik.http.middlewares.Middleware6.errors.status": "foobar, fiibar", - "traefik.http.middlewares.Middleware7.forwardauth.address": "foobar", - "traefik.http.middlewares.Middleware7.forwardauth.authresponseheaders": "foobar, fiibar", - "traefik.http.middlewares.Middleware7.forwardauth.tls.ca": "foobar", - "traefik.http.middlewares.Middleware7.forwardauth.tls.caoptional": "true", - "traefik.http.middlewares.Middleware7.forwardauth.tls.cert": "foobar", - "traefik.http.middlewares.Middleware7.forwardauth.tls.insecureskipverify": "true", - "traefik.http.middlewares.Middleware7.forwardauth.tls.key": "foobar", - "traefik.http.middlewares.Middleware7.forwardauth.trustforwardheader": "true", - "traefik.http.middlewares.Middleware8.headers.accesscontrolallowcredentials": "true", - "traefik.http.middlewares.Middleware8.headers.allowedhosts": "foobar, fiibar", - "traefik.http.middlewares.Middleware8.headers.accesscontrolallowheaders": "X-foobar, X-fiibar", - "traefik.http.middlewares.Middleware8.headers.accesscontrolallowmethods": "GET, PUT", - "traefik.http.middlewares.Middleware8.headers.accesscontrolalloworigin": "foobar", - "traefik.http.middlewares.Middleware8.headers.accesscontrolexposeheaders": "X-foobar, X-fiibar", - "traefik.http.middlewares.Middleware8.headers.accesscontrolmaxage": "200", - "traefik.http.middlewares.Middleware8.headers.addvaryheader": "true", - "traefik.http.middlewares.Middleware8.headers.browserxssfilter": "true", - "traefik.http.middlewares.Middleware8.headers.contentsecuritypolicy": "foobar", - "traefik.http.middlewares.Middleware8.headers.contenttypenosniff": "true", - "traefik.http.middlewares.Middleware8.headers.custombrowserxssvalue": "foobar", - "traefik.http.middlewares.Middleware8.headers.customframeoptionsvalue": "foobar", - "traefik.http.middlewares.Middleware8.headers.customrequestheaders.name0": "foobar", - "traefik.http.middlewares.Middleware8.headers.customrequestheaders.name1": "foobar", - "traefik.http.middlewares.Middleware8.headers.customresponseheaders.name0": "foobar", - "traefik.http.middlewares.Middleware8.headers.customresponseheaders.name1": "foobar", - "traefik.http.middlewares.Middleware8.headers.forcestsheader": "true", - "traefik.http.middlewares.Middleware8.headers.framedeny": "true", - "traefik.http.middlewares.Middleware8.headers.hostsproxyheaders": "foobar, fiibar", - "traefik.http.middlewares.Middleware8.headers.isdevelopment": "true", - "traefik.http.middlewares.Middleware8.headers.publickey": "foobar", - "traefik.http.middlewares.Middleware8.headers.referrerpolicy": "foobar", - "traefik.http.middlewares.Middleware8.headers.featurepolicy": "foobar", - "traefik.http.middlewares.Middleware8.headers.sslforcehost": "true", - "traefik.http.middlewares.Middleware8.headers.sslhost": "foobar", - "traefik.http.middlewares.Middleware8.headers.sslproxyheaders.name0": "foobar", - "traefik.http.middlewares.Middleware8.headers.sslproxyheaders.name1": "foobar", - "traefik.http.middlewares.Middleware8.headers.sslredirect": "true", - "traefik.http.middlewares.Middleware8.headers.ssltemporaryredirect": "true", - "traefik.http.middlewares.Middleware8.headers.stsincludesubdomains": "true", - "traefik.http.middlewares.Middleware8.headers.stspreload": "true", - "traefik.http.middlewares.Middleware8.headers.stsseconds": "42", - "traefik.http.middlewares.Middleware9.ipwhitelist.ipstrategy.depth": "42", - "traefik.http.middlewares.Middleware9.ipwhitelist.ipstrategy.excludedips": "foobar, fiibar", - "traefik.http.middlewares.Middleware9.ipwhitelist.sourcerange": "foobar, fiibar", - "traefik.http.middlewares.Middleware10.maxconn.amount": "42", - "traefik.http.middlewares.Middleware10.maxconn.extractorfunc": "foobar", - "traefik.http.middlewares.Middleware11.passtlsclientcert.info.notafter": "true", - "traefik.http.middlewares.Middleware11.passtlsclientcert.info.notbefore": "true", - "traefik.http.middlewares.Middleware11.passtlsclientcert.info.sans": "true", - "traefik.http.middlewares.Middleware11.passtlsclientcert.info.subject.commonname": "true", - "traefik.http.middlewares.Middleware11.passtlsclientcert.info.subject.country": "true", - "traefik.http.middlewares.Middleware11.passtlsclientcert.info.subject.domaincomponent": "true", - "traefik.http.middlewares.Middleware11.passtlsclientcert.info.subject.locality": "true", - "traefik.http.middlewares.Middleware11.passtlsclientcert.info.subject.organization": "true", - "traefik.http.middlewares.Middleware11.passtlsclientcert.info.subject.province": "true", - "traefik.http.middlewares.Middleware11.passtlsclientcert.info.subject.serialnumber": "true", - "traefik.http.middlewares.Middleware11.passtlsclientcert.info.issuer.commonname": "true", - "traefik.http.middlewares.Middleware11.passtlsclientcert.info.issuer.country": "true", - "traefik.http.middlewares.Middleware11.passtlsclientcert.info.issuer.domaincomponent": "true", - "traefik.http.middlewares.Middleware11.passtlsclientcert.info.issuer.locality": "true", - "traefik.http.middlewares.Middleware11.passtlsclientcert.info.issuer.organization": "true", - "traefik.http.middlewares.Middleware11.passtlsclientcert.info.issuer.province": "true", - "traefik.http.middlewares.Middleware11.passtlsclientcert.info.issuer.serialnumber": "true", - "traefik.http.middlewares.Middleware11.passtlsclientcert.pem": "true", - // TODO: disable temporarily (rateLimit) - // "traefik.http.middlewares.Middleware12.ratelimit.extractorfunc": "foobar", - // "traefik.http.middlewares.Middleware12.ratelimit.rateset.Rate0.average": "42", - // "traefik.http.middlewares.Middleware12.ratelimit.rateset.Rate0.burst": "42", - // "traefik.http.middlewares.Middleware12.ratelimit.rateset.Rate0.period": "42", - // "traefik.http.middlewares.Middleware12.ratelimit.rateset.Rate1.average": "42", - // "traefik.http.middlewares.Middleware12.ratelimit.rateset.Rate1.burst": "42", - // "traefik.http.middlewares.Middleware12.ratelimit.rateset.Rate1.period": "42", - "traefik.http.middlewares.Middleware13.redirectregex.permanent": "true", - "traefik.http.middlewares.Middleware13.redirectregex.regex": "foobar", - "traefik.http.middlewares.Middleware13.redirectregex.replacement": "foobar", - "traefik.http.middlewares.Middleware13b.redirectscheme.scheme": "https", - "traefik.http.middlewares.Middleware13b.redirectscheme.port": "80", - "traefik.http.middlewares.Middleware13b.redirectscheme.permanent": "true", - "traefik.http.middlewares.Middleware14.replacepath.path": "foobar", - "traefik.http.middlewares.Middleware15.replacepathregex.regex": "foobar", - "traefik.http.middlewares.Middleware15.replacepathregex.replacement": "foobar", - "traefik.http.middlewares.Middleware16.retry.attempts": "42", - "traefik.http.middlewares.Middleware17.stripprefix.prefixes": "foobar, fiibar", - "traefik.http.middlewares.Middleware18.stripprefixregex.regex": "foobar, fiibar", - "traefik.http.middlewares.Middleware19.compress": "true", + "traefik.http.middlewares.Middleware0.addprefix.prefix": "foobar", + "traefik.http.middlewares.Middleware1.basicauth.headerfield": "foobar", + "traefik.http.middlewares.Middleware1.basicauth.realm": "foobar", + "traefik.http.middlewares.Middleware1.basicauth.removeheader": "true", + "traefik.http.middlewares.Middleware1.basicauth.users": "foobar, fiibar", + "traefik.http.middlewares.Middleware1.basicauth.usersfile": "foobar", + "traefik.http.middlewares.Middleware2.buffering.maxrequestbodybytes": "42", + "traefik.http.middlewares.Middleware2.buffering.maxresponsebodybytes": "42", + "traefik.http.middlewares.Middleware2.buffering.memrequestbodybytes": "42", + "traefik.http.middlewares.Middleware2.buffering.memresponsebodybytes": "42", + "traefik.http.middlewares.Middleware2.buffering.retryexpression": "foobar", + "traefik.http.middlewares.Middleware3.chain.middlewares": "foobar, fiibar", + "traefik.http.middlewares.Middleware4.circuitbreaker.expression": "foobar", + "traefik.http.middlewares.Middleware5.digestauth.headerfield": "foobar", + "traefik.http.middlewares.Middleware5.digestauth.realm": "foobar", + "traefik.http.middlewares.Middleware5.digestauth.removeheader": "true", + "traefik.http.middlewares.Middleware5.digestauth.users": "foobar, fiibar", + "traefik.http.middlewares.Middleware5.digestauth.usersfile": "foobar", + "traefik.http.middlewares.Middleware6.errors.query": "foobar", + "traefik.http.middlewares.Middleware6.errors.service": "foobar", + "traefik.http.middlewares.Middleware6.errors.status": "foobar, fiibar", + "traefik.http.middlewares.Middleware7.forwardauth.address": "foobar", + "traefik.http.middlewares.Middleware7.forwardauth.authresponseheaders": "foobar, fiibar", + "traefik.http.middlewares.Middleware7.forwardauth.tls.ca": "foobar", + "traefik.http.middlewares.Middleware7.forwardauth.tls.caoptional": "true", + "traefik.http.middlewares.Middleware7.forwardauth.tls.cert": "foobar", + "traefik.http.middlewares.Middleware7.forwardauth.tls.insecureskipverify": "true", + "traefik.http.middlewares.Middleware7.forwardauth.tls.key": "foobar", + "traefik.http.middlewares.Middleware7.forwardauth.trustforwardheader": "true", + "traefik.http.middlewares.Middleware8.headers.accesscontrolallowcredentials": "true", + "traefik.http.middlewares.Middleware8.headers.allowedhosts": "foobar, fiibar", + "traefik.http.middlewares.Middleware8.headers.accesscontrolallowheaders": "X-foobar, X-fiibar", + "traefik.http.middlewares.Middleware8.headers.accesscontrolallowmethods": "GET, PUT", + "traefik.http.middlewares.Middleware8.headers.accesscontrolalloworigin": "foobar", + "traefik.http.middlewares.Middleware8.headers.accesscontrolexposeheaders": "X-foobar, X-fiibar", + "traefik.http.middlewares.Middleware8.headers.accesscontrolmaxage": "200", + "traefik.http.middlewares.Middleware8.headers.addvaryheader": "true", + "traefik.http.middlewares.Middleware8.headers.browserxssfilter": "true", + "traefik.http.middlewares.Middleware8.headers.contentsecuritypolicy": "foobar", + "traefik.http.middlewares.Middleware8.headers.contenttypenosniff": "true", + "traefik.http.middlewares.Middleware8.headers.custombrowserxssvalue": "foobar", + "traefik.http.middlewares.Middleware8.headers.customframeoptionsvalue": "foobar", + "traefik.http.middlewares.Middleware8.headers.customrequestheaders.name0": "foobar", + "traefik.http.middlewares.Middleware8.headers.customrequestheaders.name1": "foobar", + "traefik.http.middlewares.Middleware8.headers.customresponseheaders.name0": "foobar", + "traefik.http.middlewares.Middleware8.headers.customresponseheaders.name1": "foobar", + "traefik.http.middlewares.Middleware8.headers.forcestsheader": "true", + "traefik.http.middlewares.Middleware8.headers.framedeny": "true", + "traefik.http.middlewares.Middleware8.headers.hostsproxyheaders": "foobar, fiibar", + "traefik.http.middlewares.Middleware8.headers.isdevelopment": "true", + "traefik.http.middlewares.Middleware8.headers.publickey": "foobar", + "traefik.http.middlewares.Middleware8.headers.referrerpolicy": "foobar", + "traefik.http.middlewares.Middleware8.headers.featurepolicy": "foobar", + "traefik.http.middlewares.Middleware8.headers.sslforcehost": "true", + "traefik.http.middlewares.Middleware8.headers.sslhost": "foobar", + "traefik.http.middlewares.Middleware8.headers.sslproxyheaders.name0": "foobar", + "traefik.http.middlewares.Middleware8.headers.sslproxyheaders.name1": "foobar", + "traefik.http.middlewares.Middleware8.headers.sslredirect": "true", + "traefik.http.middlewares.Middleware8.headers.ssltemporaryredirect": "true", + "traefik.http.middlewares.Middleware8.headers.stsincludesubdomains": "true", + "traefik.http.middlewares.Middleware8.headers.stspreload": "true", + "traefik.http.middlewares.Middleware8.headers.stsseconds": "42", + "traefik.http.middlewares.Middleware9.ipwhitelist.ipstrategy.depth": "42", + "traefik.http.middlewares.Middleware9.ipwhitelist.ipstrategy.excludedips": "foobar, fiibar", + "traefik.http.middlewares.Middleware9.ipwhitelist.sourcerange": "foobar, fiibar", + "traefik.http.middlewares.Middleware10.inflightreq.amount": "42", + "traefik.http.middlewares.Middleware10.inflightreq.sourcecriterion.ipstrategy.depth": "42", + "traefik.http.middlewares.Middleware10.inflightreq.sourcecriterion.ipstrategy.excludedips": "foobar, fiibar", + "traefik.http.middlewares.Middleware10.inflightreq.sourcecriterion.requestheadername": "foobar", + "traefik.http.middlewares.Middleware10.inflightreq.sourcecriterion.requesthost": "true", + "traefik.http.middlewares.Middleware11.passtlsclientcert.info.notafter": "true", + "traefik.http.middlewares.Middleware11.passtlsclientcert.info.notbefore": "true", + "traefik.http.middlewares.Middleware11.passtlsclientcert.info.sans": "true", + "traefik.http.middlewares.Middleware11.passtlsclientcert.info.subject.commonname": "true", + "traefik.http.middlewares.Middleware11.passtlsclientcert.info.subject.country": "true", + "traefik.http.middlewares.Middleware11.passtlsclientcert.info.subject.domaincomponent": "true", + "traefik.http.middlewares.Middleware11.passtlsclientcert.info.subject.locality": "true", + "traefik.http.middlewares.Middleware11.passtlsclientcert.info.subject.organization": "true", + "traefik.http.middlewares.Middleware11.passtlsclientcert.info.subject.province": "true", + "traefik.http.middlewares.Middleware11.passtlsclientcert.info.subject.serialnumber": "true", + "traefik.http.middlewares.Middleware11.passtlsclientcert.info.issuer.commonname": "true", + "traefik.http.middlewares.Middleware11.passtlsclientcert.info.issuer.country": "true", + "traefik.http.middlewares.Middleware11.passtlsclientcert.info.issuer.domaincomponent": "true", + "traefik.http.middlewares.Middleware11.passtlsclientcert.info.issuer.locality": "true", + "traefik.http.middlewares.Middleware11.passtlsclientcert.info.issuer.organization": "true", + "traefik.http.middlewares.Middleware11.passtlsclientcert.info.issuer.province": "true", + "traefik.http.middlewares.Middleware11.passtlsclientcert.info.issuer.serialnumber": "true", + "traefik.http.middlewares.Middleware11.passtlsclientcert.pem": "true", + "traefik.http.middlewares.Middleware12.ratelimit.average": "42", + "traefik.http.middlewares.Middleware12.ratelimit.burst": "42", + "traefik.http.middlewares.Middleware12.ratelimit.sourcecriterion.requestheadername": "foobar", + "traefik.http.middlewares.Middleware12.ratelimit.sourcecriterion.requesthost": "true", + "traefik.http.middlewares.Middleware12.ratelimit.sourcecriterion.ipstrategy.depth": "42", + "traefik.http.middlewares.Middleware12.ratelimit.sourcecriterion.ipstrategy.excludedips": "foobar, foobar", + "traefik.http.middlewares.Middleware13.redirectregex.permanent": "true", + "traefik.http.middlewares.Middleware13.redirectregex.regex": "foobar", + "traefik.http.middlewares.Middleware13.redirectregex.replacement": "foobar", + "traefik.http.middlewares.Middleware13b.redirectscheme.scheme": "https", + "traefik.http.middlewares.Middleware13b.redirectscheme.port": "80", + "traefik.http.middlewares.Middleware13b.redirectscheme.permanent": "true", + "traefik.http.middlewares.Middleware14.replacepath.path": "foobar", + "traefik.http.middlewares.Middleware15.replacepathregex.regex": "foobar", + "traefik.http.middlewares.Middleware15.replacepathregex.replacement": "foobar", + "traefik.http.middlewares.Middleware16.retry.attempts": "42", + "traefik.http.middlewares.Middleware17.stripprefix.prefixes": "foobar, fiibar", + "traefik.http.middlewares.Middleware18.stripprefixregex.regex": "foobar, fiibar", + "traefik.http.middlewares.Middleware19.compress": "true", "traefik.http.routers.Router0.entrypoints": "foobar, fiibar", "traefik.http.routers.Router0.middlewares": "foobar, fiibar", @@ -273,9 +274,16 @@ func TestDecodeConfiguration(t *testing.T) { }, }, "Middleware10": { - MaxConn: &dynamic.MaxConn{ - Amount: 42, - ExtractorFunc: "foobar", + InFlightReq: &dynamic.InFlightReq{ + Amount: 42, + SourceCriterion: &dynamic.SourceCriterion{ + IPStrategy: &dynamic.IPStrategy{ + Depth: 42, + ExcludedIPs: []string{"foobar", "fiibar"}, + }, + RequestHeaderName: "foobar", + RequestHost: true, + }, }, }, "Middleware11": { @@ -306,24 +314,20 @@ func TestDecodeConfiguration(t *testing.T) { }, }, }, - // TODO: disable temporarily (rateLimit) - // "Middleware12": { - // RateLimit: &dynamic.RateLimit{ - // RateSet: map[string]*dynamic.Rate{ - // "Rate0": { - // Period: types.Duration(42 * time.Second), - // Average: 42, - // Burst: 42, - // }, - // "Rate1": { - // Period: types.Duration(42 * time.Second), - // Average: 42, - // Burst: 42, - // }, - // }, - // ExtractorFunc: "foobar", - // }, - // }, + "Middleware12": { + RateLimit: &dynamic.RateLimit{ + Average: 42, + Burst: 42, + SourceCriterion: &dynamic.SourceCriterion{ + IPStrategy: &dynamic.IPStrategy{ + Depth: 42, + ExcludedIPs: []string{"foobar", "foobar"}, + }, + RequestHeaderName: "foobar", + RequestHost: true, + }, + }, + }, "Middleware13": { RedirectRegex: &dynamic.RedirectRegex{ Regex: "foobar", @@ -674,9 +678,16 @@ func TestEncodeConfiguration(t *testing.T) { }, }, "Middleware10": { - MaxConn: &dynamic.MaxConn{ - Amount: 42, - ExtractorFunc: "foobar", + InFlightReq: &dynamic.InFlightReq{ + Amount: 42, + SourceCriterion: &dynamic.SourceCriterion{ + IPStrategy: &dynamic.IPStrategy{ + Depth: 42, + ExcludedIPs: []string{"foobar", "fiibar"}, + }, + RequestHeaderName: "foobar", + RequestHost: true, + }, }, }, "Middleware11": { @@ -706,24 +717,20 @@ func TestEncodeConfiguration(t *testing.T) { }, }, }, - // TODO: disable temporarily (rateLimit) - // "Middleware12": { - // RateLimit: &dynamic.RateLimit{ - // RateSet: map[string]*dynamic.Rate{ - // "Rate0": { - // Period: types.Duration(42 * time.Nanosecond), - // Average: 42, - // Burst: 42, - // }, - // "Rate1": { - // Period: types.Duration(42 * time.Nanosecond), - // Average: 42, - // Burst: 42, - // }, - // }, - // ExtractorFunc: "foobar", - // }, - // }, + "Middleware12": { + RateLimit: &dynamic.RateLimit{ + Average: 42, + Burst: 42, + SourceCriterion: &dynamic.SourceCriterion{ + IPStrategy: &dynamic.IPStrategy{ + Depth: 42, + ExcludedIPs: []string{"foobar", "foobar"}, + }, + RequestHeaderName: "foobar", + RequestHost: true, + }, + }, + }, "Middleware13": { RedirectRegex: &dynamic.RedirectRegex{ Regex: "foobar", @@ -975,112 +982,113 @@ func TestEncodeConfiguration(t *testing.T) { require.NoError(t, err) expected := map[string]string{ - "traefik.HTTP.Middlewares.Middleware0.AddPrefix.Prefix": "foobar", - "traefik.HTTP.Middlewares.Middleware1.BasicAuth.HeaderField": "foobar", - "traefik.HTTP.Middlewares.Middleware1.BasicAuth.Realm": "foobar", - "traefik.HTTP.Middlewares.Middleware1.BasicAuth.RemoveHeader": "true", - "traefik.HTTP.Middlewares.Middleware1.BasicAuth.Users": "foobar, fiibar", - "traefik.HTTP.Middlewares.Middleware1.BasicAuth.UsersFile": "foobar", - "traefik.HTTP.Middlewares.Middleware2.Buffering.MaxRequestBodyBytes": "42", - "traefik.HTTP.Middlewares.Middleware2.Buffering.MaxResponseBodyBytes": "42", - "traefik.HTTP.Middlewares.Middleware2.Buffering.MemRequestBodyBytes": "42", - "traefik.HTTP.Middlewares.Middleware2.Buffering.MemResponseBodyBytes": "42", - "traefik.HTTP.Middlewares.Middleware2.Buffering.RetryExpression": "foobar", - "traefik.HTTP.Middlewares.Middleware3.Chain.Middlewares": "foobar, fiibar", - "traefik.HTTP.Middlewares.Middleware4.CircuitBreaker.Expression": "foobar", - "traefik.HTTP.Middlewares.Middleware5.DigestAuth.HeaderField": "foobar", - "traefik.HTTP.Middlewares.Middleware5.DigestAuth.Realm": "foobar", - "traefik.HTTP.Middlewares.Middleware5.DigestAuth.RemoveHeader": "true", - "traefik.HTTP.Middlewares.Middleware5.DigestAuth.Users": "foobar, fiibar", - "traefik.HTTP.Middlewares.Middleware5.DigestAuth.UsersFile": "foobar", - "traefik.HTTP.Middlewares.Middleware6.Errors.Query": "foobar", - "traefik.HTTP.Middlewares.Middleware6.Errors.Service": "foobar", - "traefik.HTTP.Middlewares.Middleware6.Errors.Status": "foobar, fiibar", - "traefik.HTTP.Middlewares.Middleware7.ForwardAuth.Address": "foobar", - "traefik.HTTP.Middlewares.Middleware7.ForwardAuth.AuthResponseHeaders": "foobar, fiibar", - "traefik.HTTP.Middlewares.Middleware7.ForwardAuth.TLS.CA": "foobar", - "traefik.HTTP.Middlewares.Middleware7.ForwardAuth.TLS.CAOptional": "true", - "traefik.HTTP.Middlewares.Middleware7.ForwardAuth.TLS.Cert": "foobar", - "traefik.HTTP.Middlewares.Middleware7.ForwardAuth.TLS.InsecureSkipVerify": "true", - "traefik.HTTP.Middlewares.Middleware7.ForwardAuth.TLS.Key": "foobar", - "traefik.HTTP.Middlewares.Middleware7.ForwardAuth.TrustForwardHeader": "true", - "traefik.HTTP.Middlewares.Middleware8.Headers.AccessControlAllowCredentials": "true", - "traefik.HTTP.Middlewares.Middleware8.Headers.AccessControlAllowHeaders": "X-foobar, X-fiibar", - "traefik.HTTP.Middlewares.Middleware8.Headers.AccessControlAllowMethods": "GET, PUT", - "traefik.HTTP.Middlewares.Middleware8.Headers.AccessControlAllowOrigin": "foobar", - "traefik.HTTP.Middlewares.Middleware8.Headers.AccessControlExposeHeaders": "X-foobar, X-fiibar", - "traefik.HTTP.Middlewares.Middleware8.Headers.AccessControlMaxAge": "200", - "traefik.HTTP.Middlewares.Middleware8.Headers.AddVaryHeader": "true", - "traefik.HTTP.Middlewares.Middleware8.Headers.AllowedHosts": "foobar, fiibar", - "traefik.HTTP.Middlewares.Middleware8.Headers.BrowserXSSFilter": "true", - "traefik.HTTP.Middlewares.Middleware8.Headers.ContentSecurityPolicy": "foobar", - "traefik.HTTP.Middlewares.Middleware8.Headers.ContentTypeNosniff": "true", - "traefik.HTTP.Middlewares.Middleware8.Headers.CustomBrowserXSSValue": "foobar", - "traefik.HTTP.Middlewares.Middleware8.Headers.CustomFrameOptionsValue": "foobar", - "traefik.HTTP.Middlewares.Middleware8.Headers.CustomRequestHeaders.name0": "foobar", - "traefik.HTTP.Middlewares.Middleware8.Headers.CustomRequestHeaders.name1": "foobar", - "traefik.HTTP.Middlewares.Middleware8.Headers.CustomResponseHeaders.name0": "foobar", - "traefik.HTTP.Middlewares.Middleware8.Headers.CustomResponseHeaders.name1": "foobar", - "traefik.HTTP.Middlewares.Middleware8.Headers.ForceSTSHeader": "true", - "traefik.HTTP.Middlewares.Middleware8.Headers.FrameDeny": "true", - "traefik.HTTP.Middlewares.Middleware8.Headers.HostsProxyHeaders": "foobar, fiibar", - "traefik.HTTP.Middlewares.Middleware8.Headers.IsDevelopment": "true", - "traefik.HTTP.Middlewares.Middleware8.Headers.PublicKey": "foobar", - "traefik.HTTP.Middlewares.Middleware8.Headers.ReferrerPolicy": "foobar", - "traefik.HTTP.Middlewares.Middleware8.Headers.FeaturePolicy": "foobar", - "traefik.HTTP.Middlewares.Middleware8.Headers.SSLForceHost": "true", - "traefik.HTTP.Middlewares.Middleware8.Headers.SSLHost": "foobar", - "traefik.HTTP.Middlewares.Middleware8.Headers.SSLProxyHeaders.name0": "foobar", - "traefik.HTTP.Middlewares.Middleware8.Headers.SSLProxyHeaders.name1": "foobar", - "traefik.HTTP.Middlewares.Middleware8.Headers.SSLRedirect": "true", - "traefik.HTTP.Middlewares.Middleware8.Headers.SSLTemporaryRedirect": "true", - "traefik.HTTP.Middlewares.Middleware8.Headers.STSIncludeSubdomains": "true", - "traefik.HTTP.Middlewares.Middleware8.Headers.STSPreload": "true", - "traefik.HTTP.Middlewares.Middleware8.Headers.STSSeconds": "42", - "traefik.HTTP.Middlewares.Middleware9.IPWhiteList.IPStrategy.Depth": "42", - "traefik.HTTP.Middlewares.Middleware9.IPWhiteList.IPStrategy.ExcludedIPs": "foobar, fiibar", - "traefik.HTTP.Middlewares.Middleware9.IPWhiteList.SourceRange": "foobar, fiibar", - "traefik.HTTP.Middlewares.Middleware10.MaxConn.Amount": "42", - "traefik.HTTP.Middlewares.Middleware10.MaxConn.ExtractorFunc": "foobar", - "traefik.HTTP.Middlewares.Middleware11.PassTLSClientCert.Info.NotAfter": "true", - "traefik.HTTP.Middlewares.Middleware11.PassTLSClientCert.Info.NotBefore": "true", - "traefik.HTTP.Middlewares.Middleware11.PassTLSClientCert.Info.Sans": "true", - "traefik.HTTP.Middlewares.Middleware11.PassTLSClientCert.Info.Subject.Country": "true", - "traefik.HTTP.Middlewares.Middleware11.PassTLSClientCert.Info.Subject.Province": "true", - "traefik.HTTP.Middlewares.Middleware11.PassTLSClientCert.Info.Subject.Locality": "true", - "traefik.HTTP.Middlewares.Middleware11.PassTLSClientCert.Info.Subject.Organization": "true", - "traefik.HTTP.Middlewares.Middleware11.PassTLSClientCert.Info.Subject.CommonName": "true", - "traefik.HTTP.Middlewares.Middleware11.PassTLSClientCert.Info.Subject.SerialNumber": "true", - "traefik.HTTP.Middlewares.Middleware11.PassTLSClientCert.Info.Subject.DomainComponent": "true", - "traefik.HTTP.Middlewares.Middleware11.PassTLSClientCert.Info.Issuer.Country": "true", - "traefik.HTTP.Middlewares.Middleware11.PassTLSClientCert.Info.Issuer.Province": "true", - "traefik.HTTP.Middlewares.Middleware11.PassTLSClientCert.Info.Issuer.Locality": "true", - "traefik.HTTP.Middlewares.Middleware11.PassTLSClientCert.Info.Issuer.Organization": "true", - "traefik.HTTP.Middlewares.Middleware11.PassTLSClientCert.Info.Issuer.CommonName": "true", - "traefik.HTTP.Middlewares.Middleware11.PassTLSClientCert.Info.Issuer.SerialNumber": "true", - "traefik.HTTP.Middlewares.Middleware11.PassTLSClientCert.Info.Issuer.DomainComponent": "true", - "traefik.HTTP.Middlewares.Middleware11.PassTLSClientCert.PEM": "true", - // TODO: disable temporarily (rateLimit) - // "traefik.HTTP.Middlewares.Middleware12.RateLimit.ExtractorFunc": "foobar", - // "traefik.HTTP.Middlewares.Middleware12.RateLimit.RateSet.Rate0.Average": "42", - // "traefik.HTTP.Middlewares.Middleware12.RateLimit.RateSet.Rate0.Burst": "42", - // "traefik.HTTP.Middlewares.Middleware12.RateLimit.RateSet.Rate0.Period": "42", - // "traefik.HTTP.Middlewares.Middleware12.RateLimit.RateSet.Rate1.Average": "42", - // "traefik.HTTP.Middlewares.Middleware12.RateLimit.RateSet.Rate1.Burst": "42", - // "traefik.HTTP.Middlewares.Middleware12.RateLimit.RateSet.Rate1.Period": "42", - "traefik.HTTP.Middlewares.Middleware13.RedirectRegex.Regex": "foobar", - "traefik.HTTP.Middlewares.Middleware13.RedirectRegex.Replacement": "foobar", - "traefik.HTTP.Middlewares.Middleware13.RedirectRegex.Permanent": "true", - "traefik.HTTP.Middlewares.Middleware13b.RedirectScheme.Scheme": "https", - "traefik.HTTP.Middlewares.Middleware13b.RedirectScheme.Port": "80", - "traefik.HTTP.Middlewares.Middleware13b.RedirectScheme.Permanent": "true", - "traefik.HTTP.Middlewares.Middleware14.ReplacePath.Path": "foobar", - "traefik.HTTP.Middlewares.Middleware15.ReplacePathRegex.Regex": "foobar", - "traefik.HTTP.Middlewares.Middleware15.ReplacePathRegex.Replacement": "foobar", - "traefik.HTTP.Middlewares.Middleware16.Retry.Attempts": "42", - "traefik.HTTP.Middlewares.Middleware17.StripPrefix.Prefixes": "foobar, fiibar", - "traefik.HTTP.Middlewares.Middleware18.StripPrefixRegex.Regex": "foobar, fiibar", - "traefik.HTTP.Middlewares.Middleware19.Compress": "true", + "traefik.HTTP.Middlewares.Middleware0.AddPrefix.Prefix": "foobar", + "traefik.HTTP.Middlewares.Middleware1.BasicAuth.HeaderField": "foobar", + "traefik.HTTP.Middlewares.Middleware1.BasicAuth.Realm": "foobar", + "traefik.HTTP.Middlewares.Middleware1.BasicAuth.RemoveHeader": "true", + "traefik.HTTP.Middlewares.Middleware1.BasicAuth.Users": "foobar, fiibar", + "traefik.HTTP.Middlewares.Middleware1.BasicAuth.UsersFile": "foobar", + "traefik.HTTP.Middlewares.Middleware2.Buffering.MaxRequestBodyBytes": "42", + "traefik.HTTP.Middlewares.Middleware2.Buffering.MaxResponseBodyBytes": "42", + "traefik.HTTP.Middlewares.Middleware2.Buffering.MemRequestBodyBytes": "42", + "traefik.HTTP.Middlewares.Middleware2.Buffering.MemResponseBodyBytes": "42", + "traefik.HTTP.Middlewares.Middleware2.Buffering.RetryExpression": "foobar", + "traefik.HTTP.Middlewares.Middleware3.Chain.Middlewares": "foobar, fiibar", + "traefik.HTTP.Middlewares.Middleware4.CircuitBreaker.Expression": "foobar", + "traefik.HTTP.Middlewares.Middleware5.DigestAuth.HeaderField": "foobar", + "traefik.HTTP.Middlewares.Middleware5.DigestAuth.Realm": "foobar", + "traefik.HTTP.Middlewares.Middleware5.DigestAuth.RemoveHeader": "true", + "traefik.HTTP.Middlewares.Middleware5.DigestAuth.Users": "foobar, fiibar", + "traefik.HTTP.Middlewares.Middleware5.DigestAuth.UsersFile": "foobar", + "traefik.HTTP.Middlewares.Middleware6.Errors.Query": "foobar", + "traefik.HTTP.Middlewares.Middleware6.Errors.Service": "foobar", + "traefik.HTTP.Middlewares.Middleware6.Errors.Status": "foobar, fiibar", + "traefik.HTTP.Middlewares.Middleware7.ForwardAuth.Address": "foobar", + "traefik.HTTP.Middlewares.Middleware7.ForwardAuth.AuthResponseHeaders": "foobar, fiibar", + "traefik.HTTP.Middlewares.Middleware7.ForwardAuth.TLS.CA": "foobar", + "traefik.HTTP.Middlewares.Middleware7.ForwardAuth.TLS.CAOptional": "true", + "traefik.HTTP.Middlewares.Middleware7.ForwardAuth.TLS.Cert": "foobar", + "traefik.HTTP.Middlewares.Middleware7.ForwardAuth.TLS.InsecureSkipVerify": "true", + "traefik.HTTP.Middlewares.Middleware7.ForwardAuth.TLS.Key": "foobar", + "traefik.HTTP.Middlewares.Middleware7.ForwardAuth.TrustForwardHeader": "true", + "traefik.HTTP.Middlewares.Middleware8.Headers.AccessControlAllowCredentials": "true", + "traefik.HTTP.Middlewares.Middleware8.Headers.AccessControlAllowHeaders": "X-foobar, X-fiibar", + "traefik.HTTP.Middlewares.Middleware8.Headers.AccessControlAllowMethods": "GET, PUT", + "traefik.HTTP.Middlewares.Middleware8.Headers.AccessControlAllowOrigin": "foobar", + "traefik.HTTP.Middlewares.Middleware8.Headers.AccessControlExposeHeaders": "X-foobar, X-fiibar", + "traefik.HTTP.Middlewares.Middleware8.Headers.AccessControlMaxAge": "200", + "traefik.HTTP.Middlewares.Middleware8.Headers.AddVaryHeader": "true", + "traefik.HTTP.Middlewares.Middleware8.Headers.AllowedHosts": "foobar, fiibar", + "traefik.HTTP.Middlewares.Middleware8.Headers.BrowserXSSFilter": "true", + "traefik.HTTP.Middlewares.Middleware8.Headers.ContentSecurityPolicy": "foobar", + "traefik.HTTP.Middlewares.Middleware8.Headers.ContentTypeNosniff": "true", + "traefik.HTTP.Middlewares.Middleware8.Headers.CustomBrowserXSSValue": "foobar", + "traefik.HTTP.Middlewares.Middleware8.Headers.CustomFrameOptionsValue": "foobar", + "traefik.HTTP.Middlewares.Middleware8.Headers.CustomRequestHeaders.name0": "foobar", + "traefik.HTTP.Middlewares.Middleware8.Headers.CustomRequestHeaders.name1": "foobar", + "traefik.HTTP.Middlewares.Middleware8.Headers.CustomResponseHeaders.name0": "foobar", + "traefik.HTTP.Middlewares.Middleware8.Headers.CustomResponseHeaders.name1": "foobar", + "traefik.HTTP.Middlewares.Middleware8.Headers.ForceSTSHeader": "true", + "traefik.HTTP.Middlewares.Middleware8.Headers.FrameDeny": "true", + "traefik.HTTP.Middlewares.Middleware8.Headers.HostsProxyHeaders": "foobar, fiibar", + "traefik.HTTP.Middlewares.Middleware8.Headers.IsDevelopment": "true", + "traefik.HTTP.Middlewares.Middleware8.Headers.PublicKey": "foobar", + "traefik.HTTP.Middlewares.Middleware8.Headers.ReferrerPolicy": "foobar", + "traefik.HTTP.Middlewares.Middleware8.Headers.FeaturePolicy": "foobar", + "traefik.HTTP.Middlewares.Middleware8.Headers.SSLForceHost": "true", + "traefik.HTTP.Middlewares.Middleware8.Headers.SSLHost": "foobar", + "traefik.HTTP.Middlewares.Middleware8.Headers.SSLProxyHeaders.name0": "foobar", + "traefik.HTTP.Middlewares.Middleware8.Headers.SSLProxyHeaders.name1": "foobar", + "traefik.HTTP.Middlewares.Middleware8.Headers.SSLRedirect": "true", + "traefik.HTTP.Middlewares.Middleware8.Headers.SSLTemporaryRedirect": "true", + "traefik.HTTP.Middlewares.Middleware8.Headers.STSIncludeSubdomains": "true", + "traefik.HTTP.Middlewares.Middleware8.Headers.STSPreload": "true", + "traefik.HTTP.Middlewares.Middleware8.Headers.STSSeconds": "42", + "traefik.HTTP.Middlewares.Middleware9.IPWhiteList.IPStrategy.Depth": "42", + "traefik.HTTP.Middlewares.Middleware9.IPWhiteList.IPStrategy.ExcludedIPs": "foobar, fiibar", + "traefik.HTTP.Middlewares.Middleware9.IPWhiteList.SourceRange": "foobar, fiibar", + "traefik.HTTP.Middlewares.Middleware10.InFlightReq.Amount": "42", + "traefik.HTTP.Middlewares.Middleware10.InFlightReq.SourceCriterion.IPStrategy.Depth": "42", + "traefik.HTTP.Middlewares.Middleware10.InFlightReq.SourceCriterion.IPStrategy.ExcludedIPs": "foobar, fiibar", + "traefik.HTTP.Middlewares.Middleware10.InFlightReq.SourceCriterion.RequestHeaderName": "foobar", + "traefik.HTTP.Middlewares.Middleware10.InFlightReq.SourceCriterion.RequestHost": "true", + "traefik.HTTP.Middlewares.Middleware11.PassTLSClientCert.Info.NotAfter": "true", + "traefik.HTTP.Middlewares.Middleware11.PassTLSClientCert.Info.NotBefore": "true", + "traefik.HTTP.Middlewares.Middleware11.PassTLSClientCert.Info.Sans": "true", + "traefik.HTTP.Middlewares.Middleware11.PassTLSClientCert.Info.Subject.Country": "true", + "traefik.HTTP.Middlewares.Middleware11.PassTLSClientCert.Info.Subject.Province": "true", + "traefik.HTTP.Middlewares.Middleware11.PassTLSClientCert.Info.Subject.Locality": "true", + "traefik.HTTP.Middlewares.Middleware11.PassTLSClientCert.Info.Subject.Organization": "true", + "traefik.HTTP.Middlewares.Middleware11.PassTLSClientCert.Info.Subject.CommonName": "true", + "traefik.HTTP.Middlewares.Middleware11.PassTLSClientCert.Info.Subject.SerialNumber": "true", + "traefik.HTTP.Middlewares.Middleware11.PassTLSClientCert.Info.Subject.DomainComponent": "true", + "traefik.HTTP.Middlewares.Middleware11.PassTLSClientCert.Info.Issuer.Country": "true", + "traefik.HTTP.Middlewares.Middleware11.PassTLSClientCert.Info.Issuer.Province": "true", + "traefik.HTTP.Middlewares.Middleware11.PassTLSClientCert.Info.Issuer.Locality": "true", + "traefik.HTTP.Middlewares.Middleware11.PassTLSClientCert.Info.Issuer.Organization": "true", + "traefik.HTTP.Middlewares.Middleware11.PassTLSClientCert.Info.Issuer.CommonName": "true", + "traefik.HTTP.Middlewares.Middleware11.PassTLSClientCert.Info.Issuer.SerialNumber": "true", + "traefik.HTTP.Middlewares.Middleware11.PassTLSClientCert.Info.Issuer.DomainComponent": "true", + "traefik.HTTP.Middlewares.Middleware11.PassTLSClientCert.PEM": "true", + "traefik.HTTP.Middlewares.Middleware12.RateLimit.Average": "42", + "traefik.HTTP.Middlewares.Middleware12.RateLimit.Burst": "42", + "traefik.HTTP.Middlewares.Middleware12.RateLimit.SourceCriterion.RequestHeaderName": "foobar", + "traefik.HTTP.Middlewares.Middleware12.RateLimit.SourceCriterion.RequestHost": "true", + "traefik.HTTP.Middlewares.Middleware12.RateLimit.SourceCriterion.IPStrategy.Depth": "42", + "traefik.HTTP.Middlewares.Middleware12.RateLimit.SourceCriterion.IPStrategy.ExcludedIPs": "foobar, foobar", + "traefik.HTTP.Middlewares.Middleware13.RedirectRegex.Regex": "foobar", + "traefik.HTTP.Middlewares.Middleware13.RedirectRegex.Replacement": "foobar", + "traefik.HTTP.Middlewares.Middleware13.RedirectRegex.Permanent": "true", + "traefik.HTTP.Middlewares.Middleware13b.RedirectScheme.Scheme": "https", + "traefik.HTTP.Middlewares.Middleware13b.RedirectScheme.Port": "80", + "traefik.HTTP.Middlewares.Middleware13b.RedirectScheme.Permanent": "true", + "traefik.HTTP.Middlewares.Middleware14.ReplacePath.Path": "foobar", + "traefik.HTTP.Middlewares.Middleware15.ReplacePathRegex.Regex": "foobar", + "traefik.HTTP.Middlewares.Middleware15.ReplacePathRegex.Replacement": "foobar", + "traefik.HTTP.Middlewares.Middleware16.Retry.Attempts": "42", + "traefik.HTTP.Middlewares.Middleware17.StripPrefix.Prefixes": "foobar, fiibar", + "traefik.HTTP.Middlewares.Middleware18.StripPrefixRegex.Regex": "foobar, fiibar", + "traefik.HTTP.Middlewares.Middleware19.Compress": "true", "traefik.HTTP.Routers.Router0.EntryPoints": "foobar, fiibar", "traefik.HTTP.Routers.Router0.Middlewares": "foobar, fiibar", diff --git a/pkg/ip/strategy.go b/pkg/ip/strategy.go index dcb9f8b91..260c5021b 100644 --- a/pkg/ip/strategy.go +++ b/pkg/ip/strategy.go @@ -1,6 +1,7 @@ package ip import ( + "net" "net/http" "strings" ) @@ -17,9 +18,13 @@ type Strategy interface { // RemoteAddrStrategy a strategy that always return the remote address type RemoteAddrStrategy struct{} -// GetIP return the selected IP +// GetIP returns the selected IP func (s *RemoteAddrStrategy) GetIP(req *http.Request) string { - return req.RemoteAddr + ip, _, err := net.SplitHostPort(req.RemoteAddr) + if err != nil { + return req.RemoteAddr + } + return ip } // DepthStrategy a strategy based on the depth inside the X-Forwarded-For from right to left diff --git a/pkg/ip/strategy_test.go b/pkg/ip/strategy_test.go index 54fe64837..8a05acd63 100644 --- a/pkg/ip/strategy_test.go +++ b/pkg/ip/strategy_test.go @@ -16,7 +16,7 @@ func TestRemoteAddrStrategy_GetIP(t *testing.T) { }{ { desc: "Use RemoteAddr", - expected: "192.0.2.1:1234", + expected: "192.0.2.1", }, } diff --git a/pkg/middlewares/extractor.go b/pkg/middlewares/extractor.go new file mode 100644 index 000000000..75b700523 --- /dev/null +++ b/pkg/middlewares/extractor.go @@ -0,0 +1,49 @@ +package middlewares + +import ( + "context" + "errors" + "fmt" + "net/http" + + "github.com/containous/traefik/v2/pkg/config/dynamic" + "github.com/containous/traefik/v2/pkg/log" + "github.com/vulcand/oxy/utils" +) + +// GetSourceExtractor returns the SourceExtractor function corresponding to the given sourceMatcher. +// It defaults to a RemoteAddrStrategy IPStrategy if need be. +func GetSourceExtractor(ctx context.Context, sourceMatcher *dynamic.SourceCriterion) (utils.SourceExtractor, error) { + if sourceMatcher == nil || + sourceMatcher.IPStrategy == nil && + sourceMatcher.RequestHeaderName == "" && !sourceMatcher.RequestHost { + sourceMatcher = &dynamic.SourceCriterion{ + IPStrategy: &dynamic.IPStrategy{}, + } + } + + logger := log.FromContext(ctx) + if sourceMatcher.IPStrategy != nil { + strategy, err := sourceMatcher.IPStrategy.Get() + if err != nil { + return nil, err + } + + logger.Debug("Using IPStrategy") + return utils.ExtractorFunc(func(req *http.Request) (string, int64, error) { + return strategy.GetIP(req), 1, nil + }), nil + } + + if sourceMatcher.RequestHeaderName != "" { + logger.Debug("Using RequestHeaderName") + return utils.NewExtractor(fmt.Sprintf("request.header.%s", sourceMatcher.RequestHeaderName)) + } + + if sourceMatcher.RequestHost { + logger.Debug("Using RequestHost") + return utils.NewExtractor("request.host") + } + + return nil, errors.New("no SourceCriterion criterion defined") +} diff --git a/pkg/middlewares/inflightreq/inflight_req.go b/pkg/middlewares/inflightreq/inflight_req.go new file mode 100644 index 000000000..4be488bd8 --- /dev/null +++ b/pkg/middlewares/inflightreq/inflight_req.go @@ -0,0 +1,57 @@ +package inflightreq + +import ( + "context" + "fmt" + "net/http" + + "github.com/containous/traefik/v2/pkg/config/dynamic" + "github.com/containous/traefik/v2/pkg/log" + "github.com/containous/traefik/v2/pkg/middlewares" + "github.com/containous/traefik/v2/pkg/tracing" + "github.com/opentracing/opentracing-go/ext" + "github.com/vulcand/oxy/connlimit" +) + +const ( + typeName = "InFlightReq" +) + +type inFlightReq struct { + handler http.Handler + name string +} + +// New creates a max request middleware. +func New(ctx context.Context, next http.Handler, config dynamic.InFlightReq, name string) (http.Handler, error) { + ctxLog := log.With(ctx, log.Str(log.MiddlewareName, name), log.Str(log.MiddlewareType, typeName)) + log.FromContext(ctxLog).Debug("Creating middleware") + + if config.SourceCriterion == nil || + config.SourceCriterion.IPStrategy == nil && + config.SourceCriterion.RequestHeaderName == "" && !config.SourceCriterion.RequestHost { + config.SourceCriterion = &dynamic.SourceCriterion{ + RequestHost: true, + } + } + + sourceMatcher, err := middlewares.GetSourceExtractor(ctxLog, config.SourceCriterion) + if err != nil { + return nil, fmt.Errorf("error creating requests limiter: %v", err) + } + + handler, err := connlimit.New(next, sourceMatcher, config.Amount) + if err != nil { + return nil, fmt.Errorf("error creating connection limit: %v", err) + } + + return &inFlightReq{handler: handler, name: name}, nil +} + +func (i *inFlightReq) GetTracingInformation() (string, ext.SpanKindEnum) { + return i.name, tracing.SpanKindNoneEnum +} + +func (i *inFlightReq) ServeHTTP(rw http.ResponseWriter, req *http.Request) { + i.handler.ServeHTTP(rw, req) +} diff --git a/pkg/middlewares/maxconnection/max_connection.go b/pkg/middlewares/maxconnection/max_connection.go deleted file mode 100644 index 9e3660fa0..000000000 --- a/pkg/middlewares/maxconnection/max_connection.go +++ /dev/null @@ -1,48 +0,0 @@ -package maxconnection - -import ( - "context" - "fmt" - "net/http" - - "github.com/containous/traefik/v2/pkg/config/dynamic" - "github.com/containous/traefik/v2/pkg/middlewares" - "github.com/containous/traefik/v2/pkg/tracing" - "github.com/opentracing/opentracing-go/ext" - "github.com/vulcand/oxy/connlimit" - "github.com/vulcand/oxy/utils" -) - -const ( - typeName = "MaxConnection" -) - -type maxConnection struct { - handler http.Handler - name string -} - -// New creates a max connection middleware. -func New(ctx context.Context, next http.Handler, maxConns dynamic.MaxConn, name string) (http.Handler, error) { - middlewares.GetLogger(ctx, name, typeName).Debug("Creating middleware") - - extractFunc, err := utils.NewExtractor(maxConns.ExtractorFunc) - if err != nil { - return nil, fmt.Errorf("error creating connection limit: %v", err) - } - - handler, err := connlimit.New(next, extractFunc, maxConns.Amount) - if err != nil { - return nil, fmt.Errorf("error creating connection limit: %v", err) - } - - return &maxConnection{handler: handler, name: name}, nil -} - -func (mc *maxConnection) GetTracingInformation() (string, ext.SpanKindEnum) { - return mc.name, tracing.SpanKindNoneEnum -} - -func (mc *maxConnection) ServeHTTP(rw http.ResponseWriter, req *http.Request) { - mc.handler.ServeHTTP(rw, req) -} diff --git a/pkg/middlewares/ratelimiter/rate_limiter.go b/pkg/middlewares/ratelimiter/rate_limiter.go index 157d7c8c5..73682c200 100644 --- a/pkg/middlewares/ratelimiter/rate_limiter.go +++ b/pkg/middlewares/ratelimiter/rate_limiter.go @@ -1,54 +1,146 @@ +// Package ratelimiter implements a rate limiting and traffic shaping middleware with a set of token buckets. package ratelimiter import ( "context" + "fmt" "net/http" + "sync" "time" "github.com/containous/traefik/v2/pkg/config/dynamic" + "github.com/containous/traefik/v2/pkg/log" "github.com/containous/traefik/v2/pkg/middlewares" "github.com/containous/traefik/v2/pkg/tracing" + "github.com/mailgun/ttlmap" "github.com/opentracing/opentracing-go/ext" - "github.com/vulcand/oxy/ratelimit" "github.com/vulcand/oxy/utils" + "golang.org/x/time/rate" ) const ( - typeName = "RateLimiterType" + typeName = "RateLimiterType" + maxSources = 65536 ) +// rateLimiter implements rate limiting and traffic shaping with a set of token buckets; +// one for each traffic source. The same parameters are applied to all the buckets. type rateLimiter struct { - handler http.Handler - name string + name string + rate rate.Limit // reqs/s + burst int64 + // maxDelay is the maximum duration we're willing to wait for a bucket reservation to become effective, in nanoseconds. + // For now it is somewhat arbitrarily set to 1/rate. + maxDelay time.Duration + sourceMatcher utils.SourceExtractor + next http.Handler + + bucketsMu sync.Mutex + buckets *ttlmap.TtlMap // actual buckets, keyed by source. } -// New creates rate limiter middleware. +// New returns a rate limiter middleware. func New(ctx context.Context, next http.Handler, config dynamic.RateLimit, name string) (http.Handler, error) { - middlewares.GetLogger(ctx, name, typeName).Debug("Creating middleware") + ctxLog := log.With(ctx, log.Str(log.MiddlewareName, name), log.Str(log.MiddlewareType, typeName)) + log.FromContext(ctxLog).Debug("Creating middleware") - extractFunc, err := utils.NewExtractor(config.ExtractorFunc) - if err != nil { - return nil, err - } - - rateSet := ratelimit.NewRateSet() - for _, rate := range config.RateSet { - if err = rateSet.Add(time.Duration(rate.Period), rate.Average, rate.Burst); err != nil { - return nil, err + if config.SourceCriterion == nil || + config.SourceCriterion.IPStrategy == nil && + config.SourceCriterion.RequestHeaderName == "" && !config.SourceCriterion.RequestHost { + config.SourceCriterion = &dynamic.SourceCriterion{ + IPStrategy: &dynamic.IPStrategy{}, } } - rl, err := ratelimit.New(next, extractFunc, rateSet) + sourceMatcher, err := middlewares.GetSourceExtractor(ctxLog, config.SourceCriterion) if err != nil { return nil, err } - return &rateLimiter{handler: rl, name: name}, nil + + buckets, err := ttlmap.NewMap(maxSources) + if err != nil { + return nil, err + } + + burst := config.Burst + if burst <= 0 { + burst = 1 + } + + // Logically, we should set maxDelay to ~infinity when config.Average == 0 (because it means to rate limiting), + // but since the reservation will give us a delay = 0 anyway in this case, we're good even with any maxDelay >= 0. + var maxDelay time.Duration + if config.Average != 0 { + maxDelay = time.Second / time.Duration(config.Average*2) + } + + return &rateLimiter{ + name: name, + rate: rate.Limit(config.Average), + burst: burst, + maxDelay: maxDelay, + next: next, + sourceMatcher: sourceMatcher, + buckets: buckets, + }, nil } -func (r *rateLimiter) GetTracingInformation() (string, ext.SpanKindEnum) { - return r.name, tracing.SpanKindNoneEnum +func (rl *rateLimiter) GetTracingInformation() (string, ext.SpanKindEnum) { + return rl.name, tracing.SpanKindNoneEnum } -func (r *rateLimiter) ServeHTTP(rw http.ResponseWriter, req *http.Request) { - r.handler.ServeHTTP(rw, req) +func (rl *rateLimiter) ServeHTTP(w http.ResponseWriter, r *http.Request) { + logger := middlewares.GetLogger(r.Context(), rl.name, typeName) + + source, amount, err := rl.sourceMatcher.Extract(r) + if err != nil { + logger.Errorf("could not extract source of request: %v", err) + http.Error(w, "could not extract source of request", http.StatusInternalServerError) + return + } + + if amount != 1 { + logger.Infof("ignoring token bucket amount > 1: %d", amount) + } + + rl.bucketsMu.Lock() + defer rl.bucketsMu.Unlock() + + var bucket *rate.Limiter + if rlSource, exists := rl.buckets.Get(source); exists { + bucket = rlSource.(*rate.Limiter) + } else { + bucket = rate.NewLimiter(rl.rate, int(rl.burst)) + if err := rl.buckets.Set(source, bucket, int(rl.maxDelay)*10+1); err != nil { + logger.Errorf("could not insert bucket: %v", err) + http.Error(w, "could not insert bucket", http.StatusInternalServerError) + return + } + } + + res := bucket.Reserve() + if !res.OK() { + http.Error(w, "No bursty traffic allowed", http.StatusTooManyRequests) + return + } + + delay := res.Delay() + if delay > rl.maxDelay { + res.Cancel() + rl.serveDelayError(w, r, delay) + return + } + + time.Sleep(delay) + rl.next.ServeHTTP(w, r) +} + +func (rl *rateLimiter) serveDelayError(w http.ResponseWriter, r *http.Request, delay time.Duration) { + w.Header().Set("Retry-After", fmt.Sprintf("%.0f", delay.Seconds())) + w.Header().Set("X-Retry-In", delay.String()) + w.WriteHeader(http.StatusTooManyRequests) + + if _, err := w.Write([]byte(http.StatusText(http.StatusTooManyRequests))); err != nil { + middlewares.GetLogger(r.Context(), rl.name, typeName).Errorf("could not serve 429: %v", err) + } } diff --git a/pkg/middlewares/ratelimiter/rate_limiter_test.go b/pkg/middlewares/ratelimiter/rate_limiter_test.go new file mode 100644 index 000000000..94fcedba8 --- /dev/null +++ b/pkg/middlewares/ratelimiter/rate_limiter_test.go @@ -0,0 +1,160 @@ +package ratelimiter + +import ( + "context" + "fmt" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/containous/traefik/v2/pkg/config/dynamic" + "github.com/containous/traefik/v2/pkg/testhelpers" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/vulcand/oxy/utils" +) + +func TestNewRateLimiter(t *testing.T) { + testCases := []struct { + desc string + config dynamic.RateLimit + expectedMaxDelay time.Duration + expectedSourceIP string + }{ + { + desc: "maxDelay computation", + config: dynamic.RateLimit{ + Average: 200, + Burst: 10, + }, + expectedMaxDelay: 2500 * time.Microsecond, + }, + { + desc: "default SourceMatcher is remote address ip strategy", + config: dynamic.RateLimit{ + Average: 200, + Burst: 10, + }, + expectedSourceIP: "127.0.0.1", + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}) + + h, err := New(context.Background(), next, test.config, "rate-limiter") + require.NoError(t, err) + + rtl, _ := h.(*rateLimiter) + if test.expectedMaxDelay != 0 { + assert.Equal(t, test.expectedMaxDelay, rtl.maxDelay) + } + + if test.expectedSourceIP != "" { + extractor, ok := rtl.sourceMatcher.(utils.ExtractorFunc) + require.True(t, ok, "Not an ExtractorFunc") + + req := http.Request{ + RemoteAddr: fmt.Sprintf("%s:1234", test.expectedSourceIP), + } + + ip, _, err := extractor(&req) + assert.NoError(t, err) + assert.Equal(t, test.expectedSourceIP, ip) + } + }) + } +} + +func TestRateLimit(t *testing.T) { + testCases := []struct { + desc string + config dynamic.RateLimit + reqCount int + }{ + { + desc: "Average is respected", + config: dynamic.RateLimit{ + Average: 100, + Burst: 1, + }, + reqCount: 200, + }, + { + desc: "Burst is taken into account", + config: dynamic.RateLimit{ + Average: 100, + Burst: 200, + }, + reqCount: 300, + }, + { + desc: "Zero average ==> no rate limiting", + config: dynamic.RateLimit{ + Average: 0, + Burst: 1, + }, + reqCount: 100, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + reqCount := 0 + next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + reqCount++ + }) + + h, err := New(context.Background(), next, test.config, "rate-limiter") + require.NoError(t, err) + + start := time.Now() + for { + if reqCount >= test.reqCount { + break + } + + req := testhelpers.MustNewRequest(http.MethodGet, "http://localhost", nil) + req.RemoteAddr = "127.0.0.1:1234" + w := httptest.NewRecorder() + + h.ServeHTTP(w, req) + // TODO(mpl): predict and count the 200 VS the 429? + } + + stop := time.Now() + elapsed := stop.Sub(start) + if test.config.Average == 0 { + if elapsed > time.Millisecond { + t.Fatalf("rate should not have been limited, but: %d requests in %v", reqCount, elapsed) + } + return + } + + // Assume allowed burst is initially consumed in an infinitesimal period of time + var expectedDuration time.Duration + if test.config.Average != 0 { + expectedDuration = time.Duration((int64(test.reqCount)-test.config.Burst+1)/test.config.Average) * time.Second + } + + // Allow for a 2% leeway + minDuration := expectedDuration * 98 / 100 + maxDuration := expectedDuration * 102 / 100 + + if elapsed < minDuration { + t.Fatalf("rate was faster than expected: %d requests in %v", reqCount, elapsed) + } + if elapsed > maxDuration { + t.Fatalf("rate was slower than expected: %d requests in %v", reqCount, elapsed) + } + }) + } +} diff --git a/pkg/provider/docker/config_test.go b/pkg/provider/docker/config_test.go index 61636b474..c7c9bbdb5 100644 --- a/pkg/provider/docker/config_test.go +++ b/pkg/provider/docker/config_test.go @@ -1007,13 +1007,13 @@ func Test_buildConfiguration(t *testing.T) { }, }, { - desc: "one container with MaxConn in label (default value)", + desc: "one container with InFlightReq in label (default value)", containers: []dockerData{ { ServiceName: "Test", Name: "Test", Labels: map[string]string{ - "traefik.http.middlewares.Middleware1.maxconn.amount": "42", + "traefik.http.middlewares.Middleware1.inflightreq.amount": "42", }, NetworkSettings: networkSettings{ Ports: nat.PortMap{ @@ -1054,9 +1054,11 @@ func Test_buildConfiguration(t *testing.T) { }, Middlewares: map[string]*dynamic.Middleware{ "Middleware1": { - MaxConn: &dynamic.MaxConn{ - Amount: 42, - ExtractorFunc: "request.host", + InFlightReq: &dynamic.InFlightReq{ + Amount: 42, + SourceCriterion: &dynamic.SourceCriterion{ + RequestHost: true, + }, }, }, }, @@ -1071,7 +1073,7 @@ func Test_buildConfiguration(t *testing.T) { ServiceName: "Test", Name: "Test", Labels: map[string]string{ - "traefik.http.middlewares.Middleware1.maxconn.amount": "42", + "traefik.http.middlewares.Middleware1.inflightreq.amount": "42", }, NetworkSettings: networkSettings{ Ports: nat.PortMap{ @@ -1090,7 +1092,7 @@ func Test_buildConfiguration(t *testing.T) { ServiceName: "Test", Name: "Test", Labels: map[string]string{ - "traefik.http.middlewares.Middleware1.maxconn.amount": "42", + "traefik.http.middlewares.Middleware1.inflightreq.amount": "42", }, NetworkSettings: networkSettings{ Ports: nat.PortMap{ @@ -1119,9 +1121,11 @@ func Test_buildConfiguration(t *testing.T) { }, Middlewares: map[string]*dynamic.Middleware{ "Middleware1": { - MaxConn: &dynamic.MaxConn{ - Amount: 42, - ExtractorFunc: "request.host", + InFlightReq: &dynamic.InFlightReq{ + Amount: 42, + SourceCriterion: &dynamic.SourceCriterion{ + RequestHost: true, + }, }, }, }, @@ -1151,7 +1155,7 @@ func Test_buildConfiguration(t *testing.T) { ServiceName: "Test", Name: "Test", Labels: map[string]string{ - "traefik.http.middlewares.Middleware1.maxconn.amount": "42", + "traefik.http.middlewares.Middleware1.inflightreq.amount": "42", }, NetworkSettings: networkSettings{ Ports: nat.PortMap{ @@ -1170,7 +1174,7 @@ func Test_buildConfiguration(t *testing.T) { ServiceName: "Test", Name: "Test", Labels: map[string]string{ - "traefik.http.middlewares.Middleware1.maxconn.amount": "41", + "traefik.http.middlewares.Middleware1.inflightreq.amount": "41", }, NetworkSettings: networkSettings{ Ports: nat.PortMap{ @@ -1224,7 +1228,7 @@ func Test_buildConfiguration(t *testing.T) { ServiceName: "Test", Name: "Test", Labels: map[string]string{ - "traefik.http.middlewares.Middleware1.maxconn.amount": "42", + "traefik.http.middlewares.Middleware1.inflightreq.amount": "42", }, NetworkSettings: networkSettings{ Ports: nat.PortMap{ @@ -1243,7 +1247,7 @@ func Test_buildConfiguration(t *testing.T) { ServiceName: "Test", Name: "Test", Labels: map[string]string{ - "traefik.http.middlewares.Middleware1.maxconn.amount": "41", + "traefik.http.middlewares.Middleware1.inflightreq.amount": "41", }, NetworkSettings: networkSettings{ Ports: nat.PortMap{ @@ -1262,7 +1266,7 @@ func Test_buildConfiguration(t *testing.T) { ServiceName: "Test", Name: "Test", Labels: map[string]string{ - "traefik.http.middlewares.Middleware1.maxconn.amount": "40", + "traefik.http.middlewares.Middleware1.inflightreq.amount": "40", }, NetworkSettings: networkSettings{ Ports: nat.PortMap{ @@ -1809,7 +1813,7 @@ func Test_buildConfiguration(t *testing.T) { ServiceName: "Test", Name: "Test", Labels: map[string]string{ - "traefik.http.middlewares.Middleware1.maxconn.amount": "42", + "traefik.http.middlewares.Middleware1.inflightreq.amount": "42", }, NetworkSettings: networkSettings{ Ports: nat.PortMap{}, diff --git a/pkg/provider/marathon/config_test.go b/pkg/provider/marathon/config_test.go index 8bc35e958..296490f08 100644 --- a/pkg/provider/marathon/config_test.go +++ b/pkg/provider/marathon/config_test.go @@ -595,13 +595,13 @@ func TestBuildConfiguration(t *testing.T) { appID("/app"), appPorts(80, 81), withTasks(localhostTask(taskPorts(80, 81))), - withLabel("traefik.http.middlewares.Middleware1.maxconn.amount", "42"), + withLabel("traefik.http.middlewares.Middleware1.inflightreq.amount", "42"), ), application( appID("/app2"), appPorts(80, 81), withTasks(localhostTask(taskPorts(80, 81))), - withLabel("traefik.http.middlewares.Middleware1.maxconn.amount", "42"), + withLabel("traefik.http.middlewares.Middleware1.inflightreq.amount", "42"), )), expected: &dynamic.Configuration{ TCP: &dynamic.TCPConfiguration{ @@ -621,9 +621,11 @@ func TestBuildConfiguration(t *testing.T) { }, Middlewares: map[string]*dynamic.Middleware{ "Middleware1": { - MaxConn: &dynamic.MaxConn{ - Amount: 42, - ExtractorFunc: "request.host", + InFlightReq: &dynamic.InFlightReq{ + Amount: 42, + SourceCriterion: &dynamic.SourceCriterion{ + RequestHost: true, + }, }, }, }, @@ -659,13 +661,13 @@ func TestBuildConfiguration(t *testing.T) { appID("/app"), appPorts(80, 81), withTasks(localhostTask(taskPorts(80, 81))), - withLabel("traefik.http.middlewares.Middleware1.maxconn.amount", "42"), + withLabel("traefik.http.middlewares.Middleware1.inflightreq.amount", "42"), ), application( appID("/app2"), appPorts(80, 81), withTasks(localhostTask(taskPorts(80, 81))), - withLabel("traefik.http.middlewares.Middleware1.maxconn.amount", "41"), + withLabel("traefik.http.middlewares.Middleware1.inflightreq.amount", "41"), )), expected: &dynamic.Configuration{ TCP: &dynamic.TCPConfiguration{ diff --git a/pkg/server/middleware/middlewares.go b/pkg/server/middleware/middlewares.go index ea8254f32..4fd747780 100644 --- a/pkg/server/middleware/middlewares.go +++ b/pkg/server/middleware/middlewares.go @@ -17,9 +17,10 @@ import ( "github.com/containous/traefik/v2/pkg/middlewares/compress" "github.com/containous/traefik/v2/pkg/middlewares/customerrors" "github.com/containous/traefik/v2/pkg/middlewares/headers" + "github.com/containous/traefik/v2/pkg/middlewares/inflightreq" "github.com/containous/traefik/v2/pkg/middlewares/ipwhitelist" - "github.com/containous/traefik/v2/pkg/middlewares/maxconnection" "github.com/containous/traefik/v2/pkg/middlewares/passtlsclientcert" + "github.com/containous/traefik/v2/pkg/middlewares/ratelimiter" "github.com/containous/traefik/v2/pkg/middlewares/redirect" "github.com/containous/traefik/v2/pkg/middlewares/replacepath" "github.com/containous/traefik/v2/pkg/middlewares/replacepathregex" @@ -122,7 +123,7 @@ func (b *Builder) buildConstructor(ctx context.Context, middlewareName string) ( } // Buffering - if config.Buffering != nil && config.MaxConn.Amount != 0 { + if config.Buffering != nil && config.InFlightReq.Amount != 0 { if middleware != nil { return nil, badConf } @@ -211,13 +212,13 @@ func (b *Builder) buildConstructor(ctx context.Context, middlewareName string) ( } } - // MaxConn - if config.MaxConn != nil && config.MaxConn.Amount != 0 { + // InFlightReq + if config.InFlightReq != nil && config.InFlightReq.Amount != 0 { if middleware != nil { return nil, badConf } middleware = func(next http.Handler) (http.Handler, error) { - return maxconnection.New(ctx, next, *config.MaxConn, middlewareName) + return inflightreq.New(ctx, next, *config.InFlightReq, middlewareName) } } @@ -231,16 +232,15 @@ func (b *Builder) buildConstructor(ctx context.Context, middlewareName string) ( } } - // TODO: disable temporarily (rateLimit) // RateLimit - // if config.RateLimit != nil { - // if middleware != nil { - // return nil, badConf - // } - // middleware = func(next http.Handler) (http.Handler, error) { - // return ratelimiter.New(ctx, next, *config.RateLimit, middlewareName) - // } - // } + if config.RateLimit != nil { + if middleware != nil { + return nil, badConf + } + middleware = func(next http.Handler) (http.Handler, error) { + return ratelimiter.New(ctx, next, *config.RateLimit, middlewareName) + } + } // RedirectRegex if config.RedirectRegex != nil {