diff --git a/.github/workflows/validate.yaml b/.github/workflows/validate.yaml index 708b88e24..b7195defa 100644 --- a/.github/workflows/validate.yaml +++ b/.github/workflows/validate.yaml @@ -8,7 +8,7 @@ on: env: GO_VERSION: '1.23' GOLANGCI_LINT_VERSION: v1.60.3 - MISSSPELL_VERSION: v0.6.0 + MISSPELL_VERSION: v0.6.0 jobs: @@ -29,8 +29,8 @@ jobs: - name: Install golangci-lint ${{ env.GOLANGCI_LINT_VERSION }} run: curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin ${GOLANGCI_LINT_VERSION} - - name: Install missspell ${{ env.MISSSPELL_VERSION }} - run: curl -sfL https://raw.githubusercontent.com/golangci/misspell/master/install-misspell.sh | sh -s -- -b $(go env GOPATH)/bin ${MISSSPELL_VERSION} + - name: Install misspell ${{ env.MISSPELL_VERSION }} + run: curl -sfL https://raw.githubusercontent.com/golangci/misspell/master/install-misspell.sh | sh -s -- -b $(go env GOPATH)/bin ${MISSPELL_VERSION} - name: Avoid generating webui run: touch webui/static/index.html diff --git a/CHANGELOG.md b/CHANGELOG.md index c49d27439..24b1ceff8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,23 @@ +## [v2.11.9](https://github.com/traefik/traefik/tree/v2.11.9) (2024-09-16) +[All Commits](https://github.com/traefik/traefik/compare/v2.11.8...v2.11.9) + +**Bug fixes:** +- **[acme]** Update go-acme/lego to v4.18.0 ([#11060](https://github.com/traefik/traefik/pull/11060) by [ldez](https://github.com/ldez)) +- **[acme]** Allow handling ACME challenges with custom routers ([#10981](https://github.com/traefik/traefik/pull/10981) by [rtribotte](https://github.com/rtribotte)) +- **[logs,middleware]** Make the keys of the accessLog.fields.names map case-insensitive ([#11040](https://github.com/traefik/traefik/pull/11040) by [SpecLad](https://github.com/SpecLad)) +- **[logs,middleware]** Ensure proper logs for aborted streaming responses ([#10819](https://github.com/traefik/traefik/pull/10819) by [hood](https://github.com/hood)) +- **[middleware,server]** Cleanup Connection headers before passing the middleware chain ([#11077](https://github.com/traefik/traefik/pull/11077) by [kevinpollet](https://github.com/kevinpollet)) +- **[plugins]** Upgrade paerser to v0.2.1 ([#11048](https://github.com/traefik/traefik/pull/11048) by [mmatur](https://github.com/mmatur)) +- **[server,tcp]** Prevent error logging when TCP WRR pool is empty ([#10989](https://github.com/traefik/traefik/pull/10989) by [kevinpollet](https://github.com/kevinpollet)) +- **[webui]** Upgrade webui dependencies ([#11031](https://github.com/traefik/traefik/pull/11031) by [mloiseleur](https://github.com/mloiseleur)) + +**Documentation:** +- **[acme]** Fix typo in multiple DNS challenge provider warning ([#11001](https://github.com/traefik/traefik/pull/11001) by [tired-engineer](https://github.com/tired-engineer)) +- **[k8s]** Update k8s quickstart permissions ([#11049](https://github.com/traefik/traefik/pull/11049) by [mmatur](https://github.com/mmatur)) +- **[metrics]** Remove documentation for unimplemented service retries metric ([#10983](https://github.com/traefik/traefik/pull/10983) by [rtribotte](https://github.com/rtribotte)) +- **[middleware]** Unify tab titles ([#11072](https://github.com/traefik/traefik/pull/11072) by [jsoref](https://github.com/jsoref)) +- Give valid examples for exposing dashboard with default Helm values ([#11015](https://github.com/traefik/traefik/pull/11015) by [holysoles](https://github.com/holysoles)) + ## [v3.1.2](https://github.com/traefik/traefik/tree/v3.1.2) (2024-08-06) [All Commits](https://github.com/traefik/traefik/compare/v3.1.1...v3.1.2) diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index e917aa340..588d2aa61 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -47,7 +47,7 @@ Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. -When an inapropriate behavior is reported, maintainers will discuss on the Maintainer's Discord before marking the message as "abuse". +When an inappropriate behavior is reported, maintainers will discuss on the Maintainer's Discord before marking the message as "abuse". This conversation beforehand avoids one-sided decisions. The first message will be edited and marked as abuse. diff --git a/cmd/traefik/traefik.go b/cmd/traefik/traefik.go index 267514b16..6b41ef2e2 100644 --- a/cmd/traefik/traefik.go +++ b/cmd/traefik/traefik.go @@ -364,7 +364,7 @@ func setupServer(staticConfiguration *static.Configuration) (*server.Server, err if _, ok := resolverNames[rt.TLS.CertResolver]; !ok { log.Error().Err(err).Str(logs.RouterName, rtName).Str("certificateResolver", rt.TLS.CertResolver). - Msg("Router uses a non-existent certificate resolver") + Msg("Router uses a nonexistent certificate resolver") } } }) diff --git a/docs/content/https/acme.md b/docs/content/https/acme.md index 3355bdbb2..07f65b5c3 100644 --- a/docs/content/https/acme.md +++ b/docs/content/https/acme.md @@ -11,7 +11,7 @@ Automatic HTTPS You can configure Traefik to use an ACME provider (like Let's Encrypt) for automatic certificate generation. !!! warning "Let's Encrypt and Rate Limiting" - Note that Let's Encrypt API has [rate limiting](https://letsencrypt.org/docs/rate-limits). These last up to **one week**, and can not be overridden. + Note that Let's Encrypt API has [rate limiting](https://letsencrypt.org/docs/rate-limits). These last up to **one week**, and cannot be overridden. When running Traefik in a container this file should be persisted across restarts. If Traefik requests new certificates each time it starts up, a crash-looping container can quickly reach Let's Encrypt's ratelimits. @@ -298,7 +298,7 @@ Use the `DNS-01` challenge to generate and renew ACME certificates by provisioni Multiple DNS challenge provider are not supported with Traefik, but you can use `CNAME` to handle that. For example, if you have `example.org` (account foo) and `example.com` (account bar) you can create a CNAME on `example.org` called `_acme-challenge.example.org` pointing to `challenge.example.com`. - This way, you can obtain certificates for `example.com` with the `foo` account. + This way, you can obtain certificates for `example.org` with the `bar` account. !!! important A `provider` is mandatory. diff --git a/docs/content/middlewares/http/inflightreq.md b/docs/content/middlewares/http/inflightreq.md index e7b7ef695..16809ac80 100644 --- a/docs/content/middlewares/http/inflightreq.md +++ b/docs/content/middlewares/http/inflightreq.md @@ -278,7 +278,7 @@ spec: requestHost: true ``` -```yaml tab="Cosul Catalog" +```yaml tab="Consul Catalog" - "traefik.http.middlewares.test-inflightreq.inflightreq.sourcecriterion.requesthost=true" ``` diff --git a/docs/content/middlewares/http/overview.md b/docs/content/middlewares/http/overview.md index 96ba29e74..b6a1eb1b8 100644 --- a/docs/content/middlewares/http/overview.md +++ b/docs/content/middlewares/http/overview.md @@ -24,7 +24,7 @@ whoami: - "traefik.http.routers.router1.middlewares=foo-add-prefix@docker" ``` -```yaml tab="Kubernetes IngressRoute" +```yaml tab="IngressRoute" # As a Kubernetes Traefik IngressRoute --- apiVersion: traefik.io/v1alpha1 diff --git a/docs/content/middlewares/overview.md b/docs/content/middlewares/overview.md index ca9ae3e6b..a6d7f46be 100644 --- a/docs/content/middlewares/overview.md +++ b/docs/content/middlewares/overview.md @@ -35,7 +35,7 @@ whoami: - "traefik.http.routers.router1.middlewares=foo-add-prefix@docker" ``` -```yaml tab="Kubernetes IngressRoute" +```yaml tab="IngressRoute" --- apiVersion: traefik.io/v1alpha1 kind: Middleware diff --git a/docs/content/middlewares/tcp/overview.md b/docs/content/middlewares/tcp/overview.md index 4b461d3ba..288f8d827 100644 --- a/docs/content/middlewares/tcp/overview.md +++ b/docs/content/middlewares/tcp/overview.md @@ -24,7 +24,7 @@ whoami: - "traefik.tcp.routers.router1.middlewares=foo-ip-allowlist@docker" ``` -```yaml tab="Kubernetes IngressRoute" +```yaml tab="IngressRoute" # As a Kubernetes Traefik IngressRoute --- apiVersion: traefik.io/v1alpha1 diff --git a/docs/content/migration/v1-to-v2.md b/docs/content/migration/v1-to-v2.md index 709c53be5..47d89b250 100644 --- a/docs/content/migration/v1-to-v2.md +++ b/docs/content/migration/v1-to-v2.md @@ -44,7 +44,7 @@ Then any router can refer to an instance of the wanted middleware. - "traefik.frontend.auth.basic.users=test:$$apr1$$H6uskkkW$$IgXLP6ewTrSuBkTrqE8wj/,test2:$$apr1$$d9hr9HBB$$4HxwgUir3HP4EsggP/QNo0" ``` - ```yaml tab="K8s Ingress" + ```yaml tab="Ingress" apiVersion: networking.k8s.io/v1beta1 kind: Ingress metadata: @@ -107,7 +107,7 @@ Then any router can refer to an instance of the wanted middleware. - "traefik.http.middlewares.auth.basicauth.users=test:$$apr1$$H6uskkkW$$IgXLP6ewTrSuBkTrqE8wj/,test2:$$apr1$$d9hr9HBB$$4HxwgUir3HP4EsggP/QNo0" ``` - ```yaml tab="K8s IngressRoute" + ```yaml tab="IngressRoute" # The definitions below require the definitions for the Middleware and IngressRoute kinds. # https://doc.traefik.io/traefik/reference/dynamic-configuration/kubernetes-crd/#definitions apiVersion: traefik.io/v1alpha1 @@ -278,7 +278,7 @@ Then, a [router's TLS field](../routing/routers/index.md#tls) can refer to one o ] ``` - ```yaml tab="K8s IngressRoute" + ```yaml tab="IngressRoute" # The definitions below require the definitions for the TLSOption and IngressRoute kinds. # https://doc.traefik.io/traefik/reference/dynamic-configuration/kubernetes-crd/#definitions apiVersion: traefik.io/v1alpha1 @@ -442,7 +442,7 @@ To apply a redirection: traefik.http.middlewares.https_redirect.redirectscheme.permanent: true ``` - ```yaml tab="K8s IngressRoute" + ```yaml tab="IngressRoute" apiVersion: traefik.io/v1alpha1 kind: IngressRoute metadata: @@ -561,7 +561,7 @@ with the path `/admin` stripped, e.g. to `http://:/`. In this case, yo - "traefik.frontend.rule=Host:example.org;PathPrefixStrip:/admin" ``` - ```yaml tab="Kubernetes Ingress" + ```yaml tab="Ingress" apiVersion: networking.k8s.io/v1beta1 kind: Ingress metadata: @@ -595,7 +595,7 @@ with the path `/admin` stripped, e.g. to `http://:/`. In this case, yo - "traefik.http.middlewares.admin-stripprefix.stripprefix.prefixes=/admin" ``` - ```yaml tab="Kubernetes IngressRoute" + ```yaml tab="IngressRoute" --- apiVersion: traefik.io/v1alpha1 kind: IngressRoute diff --git a/docs/content/migration/v2.md b/docs/content/migration/v2.md index 0e56a8fee..3876a1668 100644 --- a/docs/content/migration/v2.md +++ b/docs/content/migration/v2.md @@ -432,7 +432,7 @@ For more advanced use cases, you can use either the [RedirectScheme middleware]( Following up on the deprecation started [previously](#x509-commonname-deprecation), as the `x509ignoreCN=0` value for the `GODEBUG` is [deprecated in Go 1.17](https://tip.golang.org/doc/go1.17#crypto/x509), -the legacy behavior related to the CommonName field can not be enabled at all anymore. +the legacy behavior related to the CommonName field cannot be enabled at all anymore. ## v2.5.3 to v2.5.4 diff --git a/docs/content/operations/cli.md b/docs/content/operations/cli.md index cf4257ae5..a7c9a7fbf 100644 --- a/docs/content/operations/cli.md +++ b/docs/content/operations/cli.md @@ -33,7 +33,7 @@ traefik [--flag[=true|false| ]] [-f [true|false| ]] All flags are documented in the [(static configuration) CLI reference](../reference/static-configuration/cli.md). -!!! info "Flags are case insensitive." +!!! info "Flags are case-insensitive." ### `healthcheck` diff --git a/docs/content/providers/overview.md b/docs/content/providers/overview.md index 1aba0f932..7ee8ad75c 100644 --- a/docs/content/providers/overview.md +++ b/docs/content/providers/overview.md @@ -81,7 +81,7 @@ For the list of the providers names, see the [supported providers](#supported-pr - "traefik.http.routers.my-container.middlewares=add-foo-prefix@file" ``` - ```yaml tab="Kubernetes Ingress Route" + ```yaml tab="IngressRoute" apiVersion: traefik.io/v1alpha1 kind: IngressRoute metadata: @@ -103,7 +103,7 @@ For the list of the providers names, see the [supported providers](#supported-pr # when the cross-provider syntax is used. ``` - ```yaml tab="Kubernetes Ingress" + ```yaml tab="Ingress" apiVersion: traefik.io/v1alpha1 kind: Middleware metadata: diff --git a/docs/content/reference/dynamic-configuration/consul-catalog.md b/docs/content/reference/dynamic-configuration/consul-catalog.md index 534fd953a..2669b8153 100644 --- a/docs/content/reference/dynamic-configuration/consul-catalog.md +++ b/docs/content/reference/dynamic-configuration/consul-catalog.md @@ -8,7 +8,7 @@ description: "View the reference for performing dynamic configurations with Trae Dynamic configuration with Consul Catalog {: .subtitle } -The labels are case insensitive. +The labels are case-insensitive. ```yaml --8<-- "content/reference/dynamic-configuration/consul-catalog.yml" diff --git a/docs/content/reference/dynamic-configuration/ecs.md b/docs/content/reference/dynamic-configuration/ecs.md index e32038f5a..cf721befd 100644 --- a/docs/content/reference/dynamic-configuration/ecs.md +++ b/docs/content/reference/dynamic-configuration/ecs.md @@ -8,7 +8,7 @@ description: "Learn how to do dynamic configuration in Traefik Proxy with AWS EC Dynamic configuration with ECS provider {: .subtitle } -The labels are case insensitive. +The labels are case-insensitive. ```yaml --8<-- "content/reference/dynamic-configuration/ecs.yml" diff --git a/docs/content/reference/dynamic-configuration/nomad.md b/docs/content/reference/dynamic-configuration/nomad.md index 0a8b334ad..680e621b4 100644 --- a/docs/content/reference/dynamic-configuration/nomad.md +++ b/docs/content/reference/dynamic-configuration/nomad.md @@ -8,7 +8,7 @@ description: "View the reference for performing dynamic configurations with Trae Dynamic configuration with Nomad Service Discovery {: .subtitle } -The labels are case insensitive. +The labels are case-insensitive. ```yaml --8<-- "content/reference/dynamic-configuration/nomad.yml" diff --git a/docs/content/reference/dynamic-configuration/rancher.md b/docs/content/reference/dynamic-configuration/rancher.md new file mode 100644 index 000000000..e69de29bb diff --git a/docs/content/reference/static-configuration/cli-ref.md b/docs/content/reference/static-configuration/cli-ref.md index cd245706d..31cba972a 100644 --- a/docs/content/reference/static-configuration/cli-ref.md +++ b/docs/content/reference/static-configuration/cli-ref.md @@ -117,9 +117,15 @@ Entry points definition. (Default: ```false```) `--entrypoints..address`: Entry point address. +`--entrypoints..allowacmebypass`: +Enables handling of ACME TLS and HTTP challenges with custom routers. (Default: ```false```) + `--entrypoints..asdefault`: Adds this EntryPoint to the list of default EntryPoints to be used on routers that don't have any Entrypoint defined. (Default: ```false```) +`--entrypoints..forwardedheaders.connection`: +List of Connection headers that are allowed to pass through the middleware chain before being removed. + `--entrypoints..forwardedheaders.insecure`: Trust all forwarded headers. (Default: ```false```) diff --git a/docs/content/reference/static-configuration/env-ref.md b/docs/content/reference/static-configuration/env-ref.md index 02873474b..4482c12c6 100644 --- a/docs/content/reference/static-configuration/env-ref.md +++ b/docs/content/reference/static-configuration/env-ref.md @@ -117,9 +117,15 @@ Entry points definition. (Default: ```false```) `TRAEFIK_ENTRYPOINTS__ADDRESS`: Entry point address. +`TRAEFIK_ENTRYPOINTS__ALLOWACMEBYPASS`: +Enables handling of ACME TLS and HTTP challenges with custom routers. (Default: ```false```) + `TRAEFIK_ENTRYPOINTS__ASDEFAULT`: Adds this EntryPoint to the list of default EntryPoints to be used on routers that don't have any Entrypoint defined. (Default: ```false```) +`TRAEFIK_ENTRYPOINTS__FORWARDEDHEADERS_CONNECTION`: +List of Connection headers that are allowed to pass through the middleware chain before being removed. + `TRAEFIK_ENTRYPOINTS__FORWARDEDHEADERS_INSECURE`: Trust all forwarded headers. (Default: ```false```) diff --git a/docs/content/reference/static-configuration/file.toml b/docs/content/reference/static-configuration/file.toml index 8a033d2f2..ee8b9a01b 100644 --- a/docs/content/reference/static-configuration/file.toml +++ b/docs/content/reference/static-configuration/file.toml @@ -30,6 +30,7 @@ [entryPoints] [entryPoints.EntryPoint0] address = "foobar" + allowACMEByPass = true reusePort = true asDefault = true [entryPoints.EntryPoint0.transport] @@ -48,6 +49,7 @@ [entryPoints.EntryPoint0.forwardedHeaders] insecure = true trustedIPs = ["foobar", "foobar"] + connection = ["foobar", "foobar"] [entryPoints.EntryPoint0.http] middlewares = ["foobar", "foobar"] encodeQuerySemicolons = true diff --git a/docs/content/reference/static-configuration/file.yaml b/docs/content/reference/static-configuration/file.yaml index d8a9b1362..9d5df1cc6 100644 --- a/docs/content/reference/static-configuration/file.yaml +++ b/docs/content/reference/static-configuration/file.yaml @@ -35,6 +35,7 @@ tcpServersTransport: entryPoints: EntryPoint0: address: foobar + allowACMEByPass: true reusePort: true asDefault: true transport: @@ -57,6 +58,9 @@ entryPoints: trustedIPs: - foobar - foobar + connection: + - foobar + - foobar http: redirections: entryPoint: diff --git a/docs/content/routing/entrypoints.md b/docs/content/routing/entrypoints.md index 04c730924..f01d06785 100644 --- a/docs/content/routing/entrypoints.md +++ b/docs/content/routing/entrypoints.md @@ -233,6 +233,35 @@ If both TCP and UDP are wanted for the same port, two entryPoints definitions ar Full details for how to specify `address` can be found in [net.Listen](https://golang.org/pkg/net/#Listen) (and [net.Dial](https://golang.org/pkg/net/#Dial)) of the doc for go. +### AllowACMEByPass + +_Optional, Default=false_ + +`allowACMEByPass` determines whether a user defined router can handle ACME TLS or HTTP challenges instead of the Traefik dedicated one. +This option can be used when a Traefik instance has one or more certificate resolvers configured, +but is also used to route challenges connections/requests to services that could also initiate their own ACME challenges. + +??? info "No Certificate Resolvers configured" + + It is not necessary to use the `allowACMEByPass' option certificate option if no certificate resolver is defined. + In fact, Traefik will automatically allow ACME TLS or HTTP requests to be handled by custom routers in this case, since there can be no concurrency with its own challenge handlers. + +```yaml tab="File (YAML)" +entryPoints: + foo: + allowACMEByPass: true +``` + +```toml tab="File (TOML)" +[entryPoints.foo] + [entryPoints.foo.allowACMEByPass] + allowACMEByPass = true +``` + +```bash tab="CLI" +--entryPoints.name.allowACMEByPass=true +``` + ### ReusePort _Optional, Default=false_ @@ -500,6 +529,40 @@ You can configure Traefik to trust the forwarded headers information (`X-Forward --entryPoints.web.forwardedHeaders.insecure ``` +??? info "`forwardedHeaders.connection`" + + As per RFC7230, Traefik respects the Connection options from the client request. + By doing so, it removes any header field(s) listed in the request Connection header and the Connection header field itself when empty. + The removal happens as soon as the request is handled by Traefik, + thus the removed headers are not available when the request passes through the middleware chain. + The `connection` option lists the Connection headers allowed to passthrough the middleware chain before their removal. + + ```yaml tab="File (YAML)" + ## Static configuration + entryPoints: + web: + address: ":80" + forwardedHeaders: + connection: + - foobar + ``` + + ```toml tab="File (TOML)" + ## Static configuration + [entryPoints] + [entryPoints.web] + address = ":80" + + [entryPoints.web.forwardedHeaders] + connection = ["foobar"] + ``` + + ```bash tab="CLI" + ## Static configuration + --entryPoints.web.address=:80 + --entryPoints.web.forwardedHeaders.connection=foobar + ``` + ### Transport #### `respondingTimeouts` diff --git a/docs/content/routing/providers/consul-catalog.md b/docs/content/routing/providers/consul-catalog.md index 4f5b6435f..82bf1542d 100644 --- a/docs/content/routing/providers/consul-catalog.md +++ b/docs/content/routing/providers/consul-catalog.md @@ -24,7 +24,7 @@ With Consul Catalog, Traefik can leverage tags attached to a service to generate !!! info "tags" - - tags are case insensitive. + - tags are case-insensitive. - The complete list of tags can be found [the reference page](../../reference/dynamic-configuration/consul-catalog.md) ### General diff --git a/docs/content/routing/providers/docker.md b/docs/content/routing/providers/docker.md index 41452de4d..7b1f81408 100644 --- a/docs/content/routing/providers/docker.md +++ b/docs/content/routing/providers/docker.md @@ -95,7 +95,7 @@ With Docker, Traefik can leverage labels attached to a container to generate rou !!! info "Labels" - - Labels are case insensitive. + - Labels are case-insensitive. - The complete list of labels can be found in [the reference page](../../reference/dynamic-configuration/docker.md). ### General diff --git a/docs/content/routing/providers/ecs.md b/docs/content/routing/providers/ecs.md index e2167b4e8..fa29eb8a1 100644 --- a/docs/content/routing/providers/ecs.md +++ b/docs/content/routing/providers/ecs.md @@ -22,7 +22,7 @@ With ECS, Traefik can leverage labels attached to a container to generate routin !!! info "labels" - - labels are case insensitive. + - labels are case-insensitive. - The complete list of labels can be found in [the reference page](../../reference/dynamic-configuration/ecs.md). ### General diff --git a/docs/content/routing/providers/kv.md b/docs/content/routing/providers/kv.md index 3a6cc7744..fad639d19 100644 --- a/docs/content/routing/providers/kv.md +++ b/docs/content/routing/providers/kv.md @@ -12,7 +12,7 @@ A Story of key & values !!! info "Keys" - - Keys are case insensitive. + - Keys are case-insensitive. - The complete list of keys can be found in [the reference page](../../reference/dynamic-configuration/kv.md). ### Routers diff --git a/docs/content/routing/providers/marathon.md b/docs/content/routing/providers/marathon.md new file mode 100644 index 000000000..e69de29bb diff --git a/docs/content/routing/providers/nomad.md b/docs/content/routing/providers/nomad.md index 9f585dbc6..c1dc31f30 100644 --- a/docs/content/routing/providers/nomad.md +++ b/docs/content/routing/providers/nomad.md @@ -24,7 +24,7 @@ With Nomad, Traefik can leverage tags attached to a service to generate routing !!! info "tags" - - tags are case insensitive. + - tags are case-insensitive. - The complete list of tags can be found [the reference page](../../reference/dynamic-configuration/nomad.md) ### General diff --git a/docs/content/routing/providers/swarm.md b/docs/content/routing/providers/swarm.md index cdf406f75..f6dcfb1ef 100644 --- a/docs/content/routing/providers/swarm.md +++ b/docs/content/routing/providers/swarm.md @@ -118,7 +118,7 @@ With Docker Swarm, Traefik can leverage labels attached to a service to generate !!! info "Labels" - - Labels are case insensitive. + - Labels are case-insensitive. - The complete list of labels can be found in [the reference page](../../reference/dynamic-configuration/docker.md). ### General diff --git a/docs/content/routing/routers/index.md b/docs/content/routing/routers/index.md index 99c06852b..7e93c79c3 100644 --- a/docs/content/routing/routers/index.md +++ b/docs/content/routing/routers/index.md @@ -1197,7 +1197,7 @@ A value of `0` for the priority is ignored: `priority = 0` means that the defaul | Router-2 | ```ClientIP(`192.168.0.0/24`)``` | 26 | Which means that requests from `192.168.0.12` would go to Router-2 even though Router-1 is intended to specifically handle them. - To achieve this intention, a priority (higher than 26) should be set on Router-1. + To achieve this intention, a priority (greater than 26) should be set on Router-1. ??? example "Setting priorities -- using the [File Provider](../../providers/file.md)" diff --git a/docs/content/user-guides/docker-compose/acme-dns/index.md b/docs/content/user-guides/docker-compose/acme-dns/index.md index 5be0287ba..5affbe009 100644 --- a/docs/content/user-guides/docker-compose/acme-dns/index.md +++ b/docs/content/user-guides/docker-compose/acme-dns/index.md @@ -1,6 +1,6 @@ --- title: "Traefik Docker DNS Challenge Documentation" -description: "Learn how to create a certificate with the Let's Encrypt DNS challenge to use HTTPS on a Service exposed with Traefik Proxy. Read the tehnical documentation." +description: "Learn how to create a certificate with the Let's Encrypt DNS challenge to use HTTPS on a Service exposed with Traefik Proxy. Read the technical documentation." --- # Docker-compose with Let's Encrypt: DNS Challenge diff --git a/integration/fixtures/headers/connection_hop_by_hop_headers.toml b/integration/fixtures/headers/connection_hop_by_hop_headers.toml new file mode 100644 index 000000000..091e1995c --- /dev/null +++ b/integration/fixtures/headers/connection_hop_by_hop_headers.toml @@ -0,0 +1,37 @@ +[global] + checkNewVersion = false + sendAnonymousUsage = false + +[log] + level = "DEBUG" + +# Limiting the Logs to Specific Fields +[accessLog] + format = "json" + filePath = "access.log" + + [accessLog.fields.headers.names] + "Foo" = "keep" + "Bar" = "keep" + +[entryPoints] + [entryPoints.web] + address = ":8000" + [entryPoints.web.forwardedHeaders] + insecure = true + connection = ["Foo"] + +[providers.file] + filename = "{{ .SelfFilename }}" + +## dynamic configuration ## + +[http.routers] + [http.routers.router1] + rule = "Host(`test.localhost`)" + service = "service1" + +[http.services] + [http.services.service1.loadBalancer] + [[http.services.service1.loadBalancer.servers]] + url = "http://127.0.0.1:9000" diff --git a/integration/fixtures/https/clientca/README.md b/integration/fixtures/https/clientca/README.md index 33a5f5260..f870c622d 100644 --- a/integration/fixtures/https/clientca/README.md +++ b/integration/fixtures/https/clientca/README.md @@ -48,7 +48,7 @@ openssl genrsa -out client3.key 2048 # Locality Name (eg, city) []:. # Organization Name (eg, company) [Internet Widgits Pty Ltd]:. # Organizational Unit Name (eg, section) []:. -# Common Name (e.g. server FQDN or YOUR name) []:clien1.example.com +# Common Name (e.g. server FQDN or YOUR name) []:client1.example.com # Email Address []:. # # Please enter the following 'extra' attributes @@ -58,7 +58,7 @@ openssl genrsa -out client3.key 2048 # Issuer # CN = ca1.example.com # Subject -# CN = clien1.example.com +# CN = client1.example.com openssl req -key client1.key -new -out client1.csr # Country Name (2 letter code) [AU]:. diff --git a/integration/headers_test.go b/integration/headers_test.go index e47fe1d73..c3c1377ca 100644 --- a/integration/headers_test.go +++ b/integration/headers_test.go @@ -4,6 +4,7 @@ import ( "net" "net/http" "net/http/httptest" + "os" "testing" "time" @@ -20,6 +21,11 @@ func TestHeadersSuite(t *testing.T) { suite.Run(t, new(HeadersSuite)) } +func (s *HeadersSuite) TearDownTest() { + s.displayTraefikLogFile(traefikTestLogFile) + _ = os.Remove(traefikTestAccessLogFile) +} + func (s *HeadersSuite) TestSimpleConfiguration() { s.traefikCmd(withConfigFile("fixtures/headers/basic.toml")) @@ -62,6 +68,53 @@ func (s *HeadersSuite) TestReverseProxyHeaderRemoved() { require.NoError(s.T(), err) } +func (s *HeadersSuite) TestConnectionHopByHop() { + file := s.adaptFile("fixtures/headers/connection_hop_by_hop_headers.toml", struct{}{}) + s.traefikCmd(withConfigFile(file)) + + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _, found := r.Header["X-Forwarded-For"] + assert.True(s.T(), found) + xHost, found := r.Header["X-Forwarded-Host"] + assert.True(s.T(), found) + assert.Equal(s.T(), "localhost", xHost[0]) + + _, found = r.Header["Foo"] + assert.False(s.T(), found) + _, found = r.Header["Bar"] + assert.False(s.T(), found) + }) + + listener, err := net.Listen("tcp", "127.0.0.1:9000") + require.NoError(s.T(), err) + + ts := &httptest.Server{ + Listener: listener, + Config: &http.Server{Handler: handler}, + } + ts.Start() + defer ts.Close() + + req, err := http.NewRequest(http.MethodGet, "http://127.0.0.1:8000/", nil) + require.NoError(s.T(), err) + req.Host = "test.localhost" + req.Header = http.Header{ + "Connection": {"Foo,Bar,X-Forwarded-For,X-Forwarded-Host"}, + "Foo": {"bar"}, + "Bar": {"foo"}, + "X-Forwarded-Host": {"localhost"}, + } + + err = try.Request(req, time.Second, try.StatusCodeIs(http.StatusOK)) + require.NoError(s.T(), err) + + accessLog, err := os.ReadFile(traefikTestAccessLogFile) + require.NoError(s.T(), err) + + assert.Contains(s.T(), string(accessLog), "\"request_Foo\":\"bar\"") + assert.NotContains(s.T(), string(accessLog), "\"request_Bar\":\"\"") +} + func (s *HeadersSuite) TestCorsResponses() { file := s.adaptFile("fixtures/headers/cors.toml", struct{}{}) s.traefikCmd(withConfigFile(file)) diff --git a/integration/log_rotation_test.go b/integration/log_rotation_test.go index 269d17d69..c6ee5574f 100644 --- a/integration/log_rotation_test.go +++ b/integration/log_rotation_test.go @@ -24,7 +24,7 @@ const traefikTestAccessLogFileRotated = traefikTestAccessLogFile + ".rotated" // Log rotation integration test suite. type LogRotationSuite struct{ BaseSuite } -func TestLogRorationSuite(t *testing.T) { +func TestLogRotationSuite(t *testing.T) { suite.Run(t, new(LogRotationSuite)) } diff --git a/integration/simple_test.go b/integration/simple_test.go index 018a4b67a..42f907462 100644 --- a/integration/simple_test.go +++ b/integration/simple_test.go @@ -1382,7 +1382,7 @@ func (s *SimpleSuite) TestDebugLog() { req, err := http.NewRequest(http.MethodGet, "http://localhost:8000/whoami", http.NoBody) require.NoError(s.T(), err) - req.Header.Set("Autorization", "Bearer ThisIsABearerToken") + req.Header.Set("Authorization", "Bearer ThisIsABearerToken") response, err := http.DefaultClient.Do(req) require.NoError(s.T(), err) diff --git a/integration/try/try.go b/integration/try/try.go index 886459e1e..df3db0c9e 100644 --- a/integration/try/try.go +++ b/integration/try/try.go @@ -19,7 +19,7 @@ const ( type timedAction func(timeout time.Duration, operation DoCondition) error // Sleep pauses the current goroutine for at least the duration d. -// Deprecated: Use only when use an other Try[...] functions is not possible. +// Deprecated: Use only when use another Try[...] functions is not possible. func Sleep(d time.Duration) { d = applyCIMultiplier(d) time.Sleep(d) diff --git a/pkg/config/kv/kv_node_test.go b/pkg/config/kv/kv_node_test.go index 86b7d6ea2..7712b839e 100644 --- a/pkg/config/kv/kv_node_test.go +++ b/pkg/config/kv/kv_node_test.go @@ -70,8 +70,8 @@ func TestDecodeToNode(t *testing.T) { { desc: "several entries, level 0", in: map[string]string{ - "traefik": "bar", - "traefic": "bur", + "traefik": "bar", + "traefik_": "bur", }, expected: expected{error: true}, }, @@ -120,7 +120,7 @@ func TestDecodeToNode(t *testing.T) { }}, }, { - desc: "several entries, level 2, case insensitive", + desc: "several entries, level 2, case-insensitive", in: map[string]string{ "traefik/foo/aaa": "bar", "traefik/Foo/bbb": "bur", diff --git a/pkg/config/static/entrypoints.go b/pkg/config/static/entrypoints.go index 9b48ddce4..79a0acf5b 100644 --- a/pkg/config/static/entrypoints.go +++ b/pkg/config/static/entrypoints.go @@ -12,6 +12,7 @@ import ( // EntryPoint holds the entry point configuration. type EntryPoint struct { Address string `description:"Entry point address." json:"address,omitempty" toml:"address,omitempty" yaml:"address,omitempty"` + AllowACMEByPass bool `description:"Enables handling of ACME TLS and HTTP challenges with custom routers." json:"allowACMEByPass,omitempty" toml:"allowACMEByPass,omitempty" yaml:"allowACMEByPass,omitempty"` ReusePort bool `description:"Enables EntryPoints from the same or different processes listening on the same TCP/UDP port." json:"reusePort,omitempty" toml:"reusePort,omitempty" yaml:"reusePort,omitempty"` AsDefault bool `description:"Adds this EntryPoint to the list of default EntryPoints to be used on routers that don't have any Entrypoint defined." json:"asDefault,omitempty" toml:"asDefault,omitempty" yaml:"asDefault,omitempty"` Transport *EntryPointsTransport `description:"Configures communication between clients and Traefik." json:"transport,omitempty" toml:"transport,omitempty" yaml:"transport,omitempty" export:"true"` @@ -111,6 +112,7 @@ type TLSConfig struct { type ForwardedHeaders struct { Insecure bool `description:"Trust all forwarded headers." json:"insecure,omitempty" toml:"insecure,omitempty" yaml:"insecure,omitempty" export:"true"` TrustedIPs []string `description:"Trust only forwarded headers from selected IPs." json:"trustedIPs,omitempty" toml:"trustedIPs,omitempty" yaml:"trustedIPs,omitempty"` + Connection []string `description:"List of Connection headers that are allowed to pass through the middleware chain before being removed." json:"connection,omitempty" toml:"connection,omitempty" yaml:"connection,omitempty"` } // ProxyProtocol contains Proxy-Protocol configuration. diff --git a/pkg/ip/strategy_test.go b/pkg/ip/strategy_test.go index 8bc5d285e..b836223eb 100644 --- a/pkg/ip/strategy_test.go +++ b/pkg/ip/strategy_test.go @@ -46,7 +46,7 @@ func TestDepthStrategy_GetIP(t *testing.T) { expected: "10.0.0.3", }, { - desc: "Use non existing depth in XForwardedFor", + desc: "Use nonexistent depth in XForwardedFor", depth: 2, xForwardedFor: "", expected: "", diff --git a/pkg/middlewares/accesslog/logger.go b/pkg/middlewares/accesslog/logger.go index 20ee4659f..d6055795e 100644 --- a/pkg/middlewares/accesslog/logger.go +++ b/pkg/middlewares/accesslog/logger.go @@ -106,15 +106,28 @@ func NewHandler(config *types.AccessLog) (*Handler, error) { Level: logrus.InfoLevel, } - // Transform headers names in config to a canonical form, to be used as is without further transformations. - if config.Fields != nil && config.Fields.Headers != nil && len(config.Fields.Headers.Names) > 0 { - fields := map[string]string{} + // Transform header names to a canonical form, to be used as is without further transformations, + // and transform field names to lower case, to enable case-insensitive lookup. + if config.Fields != nil { + if len(config.Fields.Names) > 0 { + fields := map[string]string{} - for h, v := range config.Fields.Headers.Names { - fields[textproto.CanonicalMIMEHeaderKey(h)] = v + for h, v := range config.Fields.Names { + fields[strings.ToLower(h)] = v + } + + config.Fields.Names = fields } - config.Fields.Headers.Names = fields + if config.Fields.Headers != nil && len(config.Fields.Headers.Names) > 0 { + fields := map[string]string{} + + for h, v := range config.Fields.Headers.Names { + fields[textproto.CanonicalMIMEHeaderKey(h)] = v + } + + config.Fields.Headers.Names = fields + } } logHandler := &Handler{ @@ -184,16 +197,6 @@ func (h *Handler) ServeHTTP(rw http.ResponseWriter, req *http.Request, next http }, } - defer func() { - if h.config.BufferingSize > 0 { - h.logHandlerChan <- handlerParams{ - logDataTable: logDataTable, - } - return - } - h.logTheRoundTrip(logDataTable) - }() - reqWithDataTable := req.WithContext(context.WithValue(req.Context(), DataTableKey, logDataTable)) core[RequestCount] = nextRequestCount() @@ -238,19 +241,30 @@ func (h *Handler) ServeHTTP(rw http.ResponseWriter, req *http.Request, next http return } + defer func() { + logDataTable.DownstreamResponse = downstreamResponse{ + headers: rw.Header().Clone(), + } + + logDataTable.DownstreamResponse.status = capt.StatusCode() + logDataTable.DownstreamResponse.size = capt.ResponseSize() + logDataTable.Request.size = capt.RequestSize() + + if _, ok := core[ClientUsername]; !ok { + core[ClientUsername] = usernameIfPresent(reqWithDataTable.URL) + } + + if h.config.BufferingSize > 0 { + h.logHandlerChan <- handlerParams{ + logDataTable: logDataTable, + } + return + } + + h.logTheRoundTrip(logDataTable) + }() + next.ServeHTTP(rw, reqWithDataTable) - - if _, ok := core[ClientUsername]; !ok { - core[ClientUsername] = usernameIfPresent(reqWithDataTable.URL) - } - - logDataTable.DownstreamResponse = downstreamResponse{ - headers: rw.Header().Clone(), - } - - logDataTable.DownstreamResponse.status = capt.StatusCode() - logDataTable.DownstreamResponse.size = capt.ResponseSize() - logDataTable.Request.size = capt.RequestSize() } // Close closes the Logger (i.e. the file, drain logHandlerChan, etc). @@ -334,7 +348,7 @@ func (h *Handler) logTheRoundTrip(logDataTable *LogData) { fields := logrus.Fields{} for k, v := range logDataTable.Core { - if h.config.Fields.Keep(k) { + if h.config.Fields.Keep(strings.ToLower(k)) { fields[k] = v } } diff --git a/pkg/middlewares/accesslog/logger_test.go b/pkg/middlewares/accesslog/logger_test.go index ae72cd365..ba89e344c 100644 --- a/pkg/middlewares/accesslog/logger_test.go +++ b/pkg/middlewares/accesslog/logger_test.go @@ -2,6 +2,7 @@ package accesslog import ( "bytes" + "context" "crypto/tls" "crypto/x509" "crypto/x509/pkix" @@ -24,6 +25,7 @@ import ( "github.com/stretchr/testify/require" ptypes "github.com/traefik/paerser/types" "github.com/traefik/traefik/v3/pkg/middlewares/capture" + "github.com/traefik/traefik/v3/pkg/middlewares/recovery" "github.com/traefik/traefik/v3/pkg/types" ) @@ -164,7 +166,7 @@ func TestLoggerHeaderFields(t *testing.T) { }, }, { - desc: "with case insensitive match on header name", + desc: "with case-insensitive match on header name", header: "User-Agent", expected: types.AccessLogKeep, accessLogFields: types.AccessLogFields{ @@ -465,6 +467,32 @@ func TestLoggerJSON(t *testing.T) { RequestRefererHeader: assertString(testReferer), }, }, + { + desc: "fields and headers with unconventional letter case", + config: &types.AccessLog{ + FilePath: "", + Format: JSONFormat, + Fields: &types.AccessLogFields{ + DefaultMode: "drop", + Names: map[string]string{ + "rEqUeStHoSt": "keep", + }, + Headers: &types.FieldHeaders{ + DefaultMode: "drop", + Names: map[string]string{ + "ReFeReR": "keep", + }, + }, + }, + }, + expected: map[string]func(t *testing.T, value interface{}){ + RequestHost: assertString(testHostname), + "level": assertString("info"), + "msg": assertString(""), + "time": assertNotEmpty(), + RequestRefererHeader: assertString(testReferer), + }, + }, } for _, test := range testCases { @@ -496,6 +524,64 @@ func TestLoggerJSON(t *testing.T) { } } +func TestLogger_AbortedRequest(t *testing.T) { + expected := map[string]func(t *testing.T, value interface{}){ + RequestContentSize: assertFloat64(0), + RequestHost: assertString(testHostname), + RequestAddr: assertString(testHostname), + RequestMethod: assertString(testMethod), + RequestPath: assertString(""), + RequestProtocol: assertString(testProto), + RequestScheme: assertString(testScheme), + RequestPort: assertString("-"), + DownstreamStatus: assertFloat64(float64(200)), + DownstreamContentSize: assertFloat64(float64(40)), + RequestRefererHeader: assertString(testReferer), + RequestUserAgentHeader: assertString(testUserAgent), + ServiceURL: assertString("http://stream"), + ServiceAddr: assertString("127.0.0.1"), + ServiceName: assertString("stream"), + ClientUsername: assertString(testUsername), + ClientHost: assertString(testHostname), + ClientPort: assertString(strconv.Itoa(testPort)), + ClientAddr: assertString(fmt.Sprintf("%s:%d", testHostname, testPort)), + "level": assertString("info"), + "msg": assertString(""), + RequestCount: assertFloat64NotZero(), + Duration: assertFloat64NotZero(), + Overhead: assertFloat64NotZero(), + RetryAttempts: assertFloat64(float64(0)), + "time": assertNotEmpty(), + StartLocal: assertNotEmpty(), + StartUTC: assertNotEmpty(), + "downstream_Content-Type": assertString("text/plain"), + "downstream_Transfer-Encoding": assertString("chunked"), + "downstream_Cache-Control": assertString("no-cache"), + } + + config := &types.AccessLog{ + FilePath: filepath.Join(t.TempDir(), logFileNameSuffix), + Format: JSONFormat, + } + doLoggingWithAbortedStream(t, config) + + logData, err := os.ReadFile(config.FilePath) + require.NoError(t, err) + + jsonData := make(map[string]interface{}) + err = json.Unmarshal(logData, &jsonData) + require.NoError(t, err) + + assert.Equal(t, len(expected), len(jsonData)) + + for field, assertion := range expected { + assertion(t, jsonData[field]) + if t.Failed() { + return + } + } +} + func TestNewLogHandlerOutputStdout(t *testing.T) { testCases := []struct { desc string @@ -832,3 +918,89 @@ func logWriterTestHandlerFunc(rw http.ResponseWriter, r *http.Request) { rw.WriteHeader(testStatus) } + +func doLoggingWithAbortedStream(t *testing.T, config *types.AccessLog) { + t.Helper() + + logger, err := NewHandler(config) + require.NoError(t, err) + t.Cleanup(func() { + err := logger.Close() + require.NoError(t, err) + }) + + if config.FilePath != "" { + _, err = os.Stat(config.FilePath) + require.NoError(t, err, "logger should create "+config.FilePath) + } + + reqContext, cancelRequest := context.WithCancel(context.Background()) + + req := &http.Request{ + Header: map[string][]string{ + "User-Agent": {testUserAgent}, + "Referer": {testReferer}, + }, + Proto: testProto, + Host: testHostname, + Method: testMethod, + RemoteAddr: fmt.Sprintf("%s:%d", testHostname, testPort), + URL: &url.URL{ + User: url.UserPassword(testUsername, ""), + }, + Body: nil, + } + + req = req.WithContext(reqContext) + + chain := alice.New() + chain = chain.Append(func(next http.Handler) (http.Handler, error) { + return recovery.New(context.Background(), next) + }) + chain = chain.Append(capture.Wrap) + chain = chain.Append(WrapHandler(logger)) + + service := NewFieldHandler(http.HandlerFunc(streamBackend), ServiceURL, "http://stream", nil) + service = NewFieldHandler(service, ServiceAddr, "127.0.0.1", nil) + service = NewFieldHandler(service, ServiceName, "stream", AddServiceFields) + + handler, err := chain.Then(service) + require.NoError(t, err) + + go func() { + time.Sleep(499 * time.Millisecond) + cancelRequest() + }() + + handler.ServeHTTP(httptest.NewRecorder(), req) +} + +func streamBackend(rw http.ResponseWriter, r *http.Request) { + // Get the Flusher to flush the response to the client + flusher, ok := rw.(http.Flusher) + if !ok { + http.Error(rw, "Streaming unsupported!", http.StatusInternalServerError) + return + } + + // Set the headers for streaming + rw.Header().Set("Content-Type", "text/plain") + rw.Header().Set("Transfer-Encoding", "chunked") + rw.Header().Set("Cache-Control", "no-cache") + + for { + time.Sleep(100 * time.Millisecond) + + select { + case <-r.Context().Done(): + panic(http.ErrAbortHandler) + + default: + if _, err := fmt.Fprint(rw, "FOOBAR!!!!"); err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + flusher.Flush() + } + } +} diff --git a/pkg/middlewares/connectionheader/connectionheader.go b/pkg/middlewares/auth/connectionheader.go similarity index 97% rename from pkg/middlewares/connectionheader/connectionheader.go rename to pkg/middlewares/auth/connectionheader.go index 12a994f17..30d3ab87c 100644 --- a/pkg/middlewares/connectionheader/connectionheader.go +++ b/pkg/middlewares/auth/connectionheader.go @@ -1,4 +1,4 @@ -package connectionheader +package auth import ( "net/http" diff --git a/pkg/middlewares/connectionheader/connectionheader_test.go b/pkg/middlewares/auth/connectionheader_test.go similarity index 98% rename from pkg/middlewares/connectionheader/connectionheader_test.go rename to pkg/middlewares/auth/connectionheader_test.go index bd41d58d8..00d719ef0 100644 --- a/pkg/middlewares/connectionheader/connectionheader_test.go +++ b/pkg/middlewares/auth/connectionheader_test.go @@ -1,4 +1,4 @@ -package connectionheader +package auth import ( "net/http" diff --git a/pkg/middlewares/auth/forward.go b/pkg/middlewares/auth/forward.go index ee9e01a6e..943e04bbe 100644 --- a/pkg/middlewares/auth/forward.go +++ b/pkg/middlewares/auth/forward.go @@ -13,7 +13,6 @@ import ( "github.com/traefik/traefik/v3/pkg/config/dynamic" "github.com/traefik/traefik/v3/pkg/middlewares" - "github.com/traefik/traefik/v3/pkg/middlewares/connectionheader" "github.com/traefik/traefik/v3/pkg/middlewares/observability" "github.com/traefik/traefik/v3/pkg/tracing" "github.com/traefik/traefik/v3/pkg/types" @@ -121,7 +120,7 @@ func (fa *forwardAuth) GetTracingInformation() (string, string, trace.SpanKind) func (fa *forwardAuth) ServeHTTP(rw http.ResponseWriter, req *http.Request) { logger := middlewares.GetLogger(req.Context(), fa.name, typeNameForward) - req = connectionheader.Remove(req) + req = Remove(req) forwardReq, err := http.NewRequestWithContext(req.Context(), http.MethodGet, fa.address, nil) if err != nil { diff --git a/pkg/middlewares/customerrors/custom_errors.go b/pkg/middlewares/customerrors/custom_errors.go index b985b6326..c5a1c6f22 100644 --- a/pkg/middlewares/customerrors/custom_errors.go +++ b/pkg/middlewares/customerrors/custom_errors.go @@ -235,7 +235,7 @@ func (cc *codeCatcher) Flush() { // since we want to serve the ones from the error page, // so we just don't flush. // (e.g., To prevent superfluous WriteHeader on request with a - // `Transfert-Encoding: chunked` header). + // `Transfer-Encoding: chunked` header). if cc.caughtFilteredCode { return } diff --git a/pkg/middlewares/forwardedheaders/forwarded_header.go b/pkg/middlewares/forwardedheaders/forwarded_header.go index 3f4e32301..f03bea9ab 100644 --- a/pkg/middlewares/forwardedheaders/forwarded_header.go +++ b/pkg/middlewares/forwardedheaders/forwarded_header.go @@ -3,10 +3,13 @@ package forwardedheaders import ( "net" "net/http" + "net/textproto" "os" + "slices" "strings" "github.com/traefik/traefik/v3/pkg/ip" + "golang.org/x/net/http/httpguts" ) const ( @@ -42,19 +45,20 @@ var xHeaders = []string{ // Unless insecure is set, // it first removes all the existing values for those headers if the remote address is not one of the trusted ones. type XForwarded struct { - insecure bool - trustedIps []string - ipChecker *ip.Checker - next http.Handler - hostname string + insecure bool + trustedIPs []string + connectionHeaders []string + ipChecker *ip.Checker + next http.Handler + hostname string } // NewXForwarded creates a new XForwarded. -func NewXForwarded(insecure bool, trustedIps []string, next http.Handler) (*XForwarded, error) { +func NewXForwarded(insecure bool, trustedIPs []string, connectionHeaders []string, next http.Handler) (*XForwarded, error) { var ipChecker *ip.Checker - if len(trustedIps) > 0 { + if len(trustedIPs) > 0 { var err error - ipChecker, err = ip.NewChecker(trustedIps) + ipChecker, err = ip.NewChecker(trustedIPs) if err != nil { return nil, err } @@ -66,11 +70,12 @@ func NewXForwarded(insecure bool, trustedIps []string, next http.Handler) (*XFor } return &XForwarded{ - insecure: insecure, - trustedIps: trustedIps, - ipChecker: ipChecker, - next: next, - hostname: hostname, + insecure: insecure, + trustedIPs: trustedIPs, + connectionHeaders: connectionHeaders, + ipChecker: ipChecker, + next: next, + hostname: hostname, }, nil } @@ -189,9 +194,53 @@ func (x *XForwarded) ServeHTTP(w http.ResponseWriter, r *http.Request) { x.rewrite(r) + x.removeConnectionHeaders(r) + x.next.ServeHTTP(w, r) } +func (x *XForwarded) removeConnectionHeaders(req *http.Request) { + var reqUpType string + if httpguts.HeaderValuesContainsToken(req.Header[connection], upgrade) { + reqUpType = unsafeHeader(req.Header).Get(upgrade) + } + + var connectionHopByHopHeaders []string + for _, f := range req.Header[connection] { + for _, sf := range strings.Split(f, ",") { + if sf = textproto.TrimString(sf); sf != "" { + // Connection header cannot dictate to remove X- headers managed by Traefik, + // as per rfc7230 https://datatracker.ietf.org/doc/html/rfc7230#section-6.1, + // A proxy or gateway MUST ... and then remove the Connection header field itself + // (or replace it with the intermediary's own connection options for the forwarded message). + if slices.Contains(xHeaders, sf) { + continue + } + + // Keep headers allowed through the middleware chain. + if slices.Contains(x.connectionHeaders, sf) { + connectionHopByHopHeaders = append(connectionHopByHopHeaders, sf) + continue + } + + // Apply Connection header option. + req.Header.Del(sf) + } + } + } + + if reqUpType != "" { + connectionHopByHopHeaders = append(connectionHopByHopHeaders, upgrade) + unsafeHeader(req.Header).Set(upgrade, reqUpType) + } + if len(connectionHopByHopHeaders) > 0 { + unsafeHeader(req.Header).Set(connection, strings.Join(connectionHopByHopHeaders, ",")) + return + } + + unsafeHeader(req.Header).Del(connection) +} + // unsafeHeader allows to manage Header values. // Must be used only when the header name is already a canonical key. type unsafeHeader map[string][]string diff --git a/pkg/middlewares/forwardedheaders/forwarded_header_test.go b/pkg/middlewares/forwardedheaders/forwarded_header_test.go index 8e1d10925..414fd5007 100644 --- a/pkg/middlewares/forwardedheaders/forwarded_header_test.go +++ b/pkg/middlewares/forwardedheaders/forwarded_header_test.go @@ -12,15 +12,16 @@ import ( func TestServeHTTP(t *testing.T) { testCases := []struct { - desc string - insecure bool - trustedIps []string - incomingHeaders map[string][]string - remoteAddr string - expectedHeaders map[string]string - tls bool - websocket bool - host string + desc string + insecure bool + trustedIps []string + connectionHeaders []string + incomingHeaders map[string][]string + remoteAddr string + expectedHeaders map[string]string + tls bool + websocket bool + host string }{ { desc: "all Empty", @@ -269,6 +270,196 @@ func TestServeHTTP(t *testing.T) { xForwardedServer: "foo.com:8080", }, }, + { + desc: "Untrusted: Connection header has no effect on X- forwarded headers", + insecure: false, + incomingHeaders: map[string][]string{ + connection: { + xForwardedProto, + xForwardedFor, + xForwardedURI, + xForwardedMethod, + xForwardedHost, + xForwardedPort, + xForwardedTLSClientCert, + xForwardedTLSClientCertInfo, + xRealIP, + }, + xForwardedProto: {"foo"}, + xForwardedFor: {"foo"}, + xForwardedURI: {"foo"}, + xForwardedMethod: {"foo"}, + xForwardedHost: {"foo"}, + xForwardedPort: {"foo"}, + xForwardedTLSClientCert: {"foo"}, + xForwardedTLSClientCertInfo: {"foo"}, + xRealIP: {"foo"}, + }, + expectedHeaders: map[string]string{ + xForwardedProto: "http", + xForwardedFor: "", + xForwardedURI: "", + xForwardedMethod: "", + xForwardedHost: "", + xForwardedPort: "80", + xForwardedTLSClientCert: "", + xForwardedTLSClientCertInfo: "", + xRealIP: "", + connection: "", + }, + }, + { + desc: "Trusted (insecure): Connection header has no effect on X- forwarded headers", + insecure: true, + incomingHeaders: map[string][]string{ + connection: { + xForwardedProto, + xForwardedFor, + xForwardedURI, + xForwardedMethod, + xForwardedHost, + xForwardedPort, + xForwardedTLSClientCert, + xForwardedTLSClientCertInfo, + xRealIP, + }, + xForwardedProto: {"foo"}, + xForwardedFor: {"foo"}, + xForwardedURI: {"foo"}, + xForwardedMethod: {"foo"}, + xForwardedHost: {"foo"}, + xForwardedPort: {"foo"}, + xForwardedTLSClientCert: {"foo"}, + xForwardedTLSClientCertInfo: {"foo"}, + xRealIP: {"foo"}, + }, + expectedHeaders: map[string]string{ + xForwardedProto: "foo", + xForwardedFor: "foo", + xForwardedURI: "foo", + xForwardedMethod: "foo", + xForwardedHost: "foo", + xForwardedPort: "foo", + xForwardedTLSClientCert: "foo", + xForwardedTLSClientCertInfo: "foo", + xRealIP: "foo", + connection: "", + }, + }, + { + desc: "Untrusted and Connection: Connection header has no effect on X- forwarded headers", + insecure: false, + connectionHeaders: []string{ + xForwardedProto, + xForwardedFor, + xForwardedURI, + xForwardedMethod, + xForwardedHost, + xForwardedPort, + xForwardedTLSClientCert, + xForwardedTLSClientCertInfo, + xRealIP, + }, + incomingHeaders: map[string][]string{ + connection: { + xForwardedProto, + xForwardedFor, + xForwardedURI, + xForwardedMethod, + xForwardedHost, + xForwardedPort, + xForwardedTLSClientCert, + xForwardedTLSClientCertInfo, + xRealIP, + }, + xForwardedProto: {"foo"}, + xForwardedFor: {"foo"}, + xForwardedURI: {"foo"}, + xForwardedMethod: {"foo"}, + xForwardedHost: {"foo"}, + xForwardedPort: {"foo"}, + xForwardedTLSClientCert: {"foo"}, + xForwardedTLSClientCertInfo: {"foo"}, + xRealIP: {"foo"}, + }, + expectedHeaders: map[string]string{ + xForwardedProto: "http", + xForwardedFor: "", + xForwardedURI: "", + xForwardedMethod: "", + xForwardedHost: "", + xForwardedPort: "80", + xForwardedTLSClientCert: "", + xForwardedTLSClientCertInfo: "", + xRealIP: "", + connection: "", + }, + }, + { + desc: "Trusted (insecure) and Connection: Connection header has no effect on X- forwarded headers", + insecure: true, + connectionHeaders: []string{ + xForwardedProto, + xForwardedFor, + xForwardedURI, + xForwardedMethod, + xForwardedHost, + xForwardedPort, + xForwardedTLSClientCert, + xForwardedTLSClientCertInfo, + xRealIP, + }, + incomingHeaders: map[string][]string{ + connection: { + xForwardedProto, + xForwardedFor, + xForwardedURI, + xForwardedMethod, + xForwardedHost, + xForwardedPort, + xForwardedTLSClientCert, + xForwardedTLSClientCertInfo, + xRealIP, + }, + xForwardedProto: {"foo"}, + xForwardedFor: {"foo"}, + xForwardedURI: {"foo"}, + xForwardedMethod: {"foo"}, + xForwardedHost: {"foo"}, + xForwardedPort: {"foo"}, + xForwardedTLSClientCert: {"foo"}, + xForwardedTLSClientCertInfo: {"foo"}, + xRealIP: {"foo"}, + }, + expectedHeaders: map[string]string{ + xForwardedProto: "foo", + xForwardedFor: "foo", + xForwardedURI: "foo", + xForwardedMethod: "foo", + xForwardedHost: "foo", + xForwardedPort: "foo", + xForwardedTLSClientCert: "foo", + xForwardedTLSClientCertInfo: "foo", + xRealIP: "foo", + connection: "", + }, + }, + { + desc: "Connection: one remove, and one passthrough header", + connectionHeaders: []string{ + "foo", + }, + incomingHeaders: map[string][]string{ + connection: { + "foo", + }, + "Foo": {"bar"}, + "Bar": {"foo"}, + }, + expectedHeaders: map[string]string{ + "Bar": "foo", + }, + }, } for _, test := range testCases { @@ -299,7 +490,7 @@ func TestServeHTTP(t *testing.T) { } } - m, err := NewXForwarded(test.insecure, test.trustedIps, + m, err := NewXForwarded(test.insecure, test.trustedIps, test.connectionHeaders, http.HandlerFunc(func(_ http.ResponseWriter, _ *http.Request) {})) require.NoError(t, err) @@ -382,3 +573,74 @@ func Test_isWebsocketRequest(t *testing.T) { }) } } + +func TestConnection(t *testing.T) { + testCases := []struct { + desc string + reqHeaders map[string]string + connectionHeaders []string + expected http.Header + }{ + { + desc: "simple remove", + reqHeaders: map[string]string{ + "Foo": "bar", + connection: "foo", + }, + expected: http.Header{}, + }, + { + desc: "remove and upgrade", + reqHeaders: map[string]string{ + upgrade: "test", + "Foo": "bar", + connection: "upgrade,foo", + }, + expected: http.Header{ + upgrade: []string{"test"}, + connection: []string{"Upgrade"}, + }, + }, + { + desc: "no remove", + reqHeaders: map[string]string{ + "Foo": "bar", + connection: "fii", + }, + expected: http.Header{ + "Foo": []string{"bar"}, + }, + }, + { + desc: "no remove because connection header pass through", + reqHeaders: map[string]string{ + "Foo": "bar", + connection: "Foo", + }, + connectionHeaders: []string{"Foo"}, + expected: http.Header{ + "Foo": []string{"bar"}, + connection: []string{"Foo"}, + }, + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + forwarded, err := NewXForwarded(true, nil, test.connectionHeaders, nil) + require.NoError(t, err) + + req := httptest.NewRequest(http.MethodGet, "https://localhost", nil) + + for k, v := range test.reqHeaders { + req.Header.Set(k, v) + } + + forwarded.removeConnectionHeaders(req) + + assert.Equal(t, test.expected, req.Header) + }) + } +} diff --git a/pkg/middlewares/headers/headers.go b/pkg/middlewares/headers/headers.go index e393aa1a6..861d1066d 100644 --- a/pkg/middlewares/headers/headers.go +++ b/pkg/middlewares/headers/headers.go @@ -8,7 +8,6 @@ import ( "github.com/traefik/traefik/v3/pkg/config/dynamic" "github.com/traefik/traefik/v3/pkg/middlewares" - "github.com/traefik/traefik/v3/pkg/middlewares/connectionheader" "go.opentelemetry.io/otel/trace" ) @@ -46,12 +45,11 @@ func New(ctx context.Context, next http.Handler, cfg dynamic.Headers, name strin if hasCustomHeaders || hasCorsHeaders { logger.Debug().Msgf("Setting up customHeaders/Cors from %v", cfg) - h, err := NewHeader(nextHandler, cfg) + var err error + handler, err = NewHeader(nextHandler, cfg) if err != nil { return nil, err } - - handler = connectionheader.Remover(h) } return &headers{ diff --git a/pkg/middlewares/ratelimiter/rate_limiter.go b/pkg/middlewares/ratelimiter/rate_limiter.go index 0b1a6aaa1..d22b99a42 100644 --- a/pkg/middlewares/ratelimiter/rate_limiter.go +++ b/pkg/middlewares/ratelimiter/rate_limiter.go @@ -149,7 +149,7 @@ func (rl *rateLimiter) ServeHTTP(rw http.ResponseWriter, req *http.Request) { } // We Set even in the case where the source already exists, - // because we want to update the expiryTime everytime we get the source, + // because we want to update the expiryTime every time we get the source, // as the expiryTime is supposed to reflect the activity (or lack thereof) on that source. if err := rl.buckets.Set(source, bucket, rl.ttl); err != nil { logger.Error().Err(err).Msg("Could not insert/update bucket") diff --git a/pkg/provider/acme/provider.go b/pkg/provider/acme/provider.go index 34f5e12d7..037123e14 100644 --- a/pkg/provider/acme/provider.go +++ b/pkg/provider/acme/provider.go @@ -730,7 +730,7 @@ func deleteUnnecessaryDomains(ctx context.Context, domains []types.Domain) []typ } // Check if CN or SANS to check already exists - // or can not be checked by a wildcard + // or cannot be checked by a wildcard var newDomainsToCheck []string for _, domainProcessed := range domainToCheck.ToStrArray() { if idxDomain < idxDomainToCheck && isDomainAlreadyChecked(domainProcessed, domain.ToStrArray()) { diff --git a/pkg/provider/docker/builder_test.go b/pkg/provider/docker/builder_test.go index f8723c74e..1213b7c01 100644 --- a/pkg/provider/docker/builder_test.go +++ b/pkg/provider/docker/builder_test.go @@ -205,7 +205,7 @@ func withEndpointSpec(ops ...func(*swarm.EndpointSpec)) func(*swarm.Service) { } } -func modeDNSSR(spec *swarm.EndpointSpec) { +func modeDNSRR(spec *swarm.EndpointSpec) { spec.Mode = swarm.ResolutionModeDNSRR } diff --git a/pkg/provider/docker/config_test.go b/pkg/provider/docker/config_test.go index 114a00076..2d4d151c0 100644 --- a/pkg/provider/docker/config_test.go +++ b/pkg/provider/docker/config_test.go @@ -3976,7 +3976,7 @@ func TestDynConfBuilder_getIPAddress_swarm(t *testing.T) { networks map[string]*network.Summary }{ { - service: swarmService(withEndpointSpec(modeDNSSR)), + service: swarmService(withEndpointSpec(modeDNSRR)), expected: "", networks: map[string]*network.Summary{}, }, diff --git a/pkg/provider/docker/pswarm_test.go b/pkg/provider/docker/pswarm_test.go index f1cb4b27e..c9fae5f12 100644 --- a/pkg/provider/docker/pswarm_test.go +++ b/pkg/provider/docker/pswarm_test.go @@ -114,7 +114,7 @@ func TestSwarmProvider_listServices(t *testing.T) { "traefik.docker.network": "barnet", "traefik.docker.LBSwarm": "true", }), - withEndpointSpec(modeDNSSR)), + withEndpointSpec(modeDNSRR)), }, dockerVersion: "1.30", networks: []network.Summary{}, @@ -140,7 +140,7 @@ func TestSwarmProvider_listServices(t *testing.T) { "traefik.docker.network": "barnet", "traefik.docker.LBSwarm": "true", }), - withEndpointSpec(modeDNSSR)), + withEndpointSpec(modeDNSRR)), }, dockerVersion: "1.30", networks: []network.Summary{ @@ -185,7 +185,7 @@ func TestSwarmProvider_listServices(t *testing.T) { serviceLabels(map[string]string{ "traefik.docker.network": "barnet", }), - withEndpointSpec(modeDNSSR)), + withEndpointSpec(modeDNSRR)), }, tasks: []swarm.Task{ swarmTask("id1", diff --git a/pkg/provider/docker/shared_test.go b/pkg/provider/docker/shared_test.go index 630a7fee9..0f542d00a 100644 --- a/pkg/provider/docker/shared_test.go +++ b/pkg/provider/docker/shared_test.go @@ -86,7 +86,7 @@ func Test_getPort_swarm(t *testing.T) { }{ { service: swarmService( - withEndpointSpec(modeDNSSR), + withEndpointSpec(modeDNSRR), ), networks: map[string]*docker.NetworkResource{}, serverPort: "8080", diff --git a/pkg/provider/traefik/internal.go b/pkg/provider/traefik/internal.go index 1f1b09a52..ca0add5be 100644 --- a/pkg/provider/traefik/internal.go +++ b/pkg/provider/traefik/internal.go @@ -91,15 +91,27 @@ func (i *Provider) createConfiguration(ctx context.Context) *dynamic.Configurati } func (i *Provider) acme(cfg *dynamic.Configuration) { - var eps []string + allowACMEByPass := map[string]bool{} + for name, ep := range i.staticCfg.EntryPoints { + allowACMEByPass[name] = ep.AllowACMEByPass + } + var eps []string + var epsByPass []string uniq := map[string]struct{}{} for _, resolver := range i.staticCfg.CertificatesResolvers { if resolver.ACME != nil && resolver.ACME.HTTPChallenge != nil && resolver.ACME.HTTPChallenge.EntryPoint != "" { - if _, ok := uniq[resolver.ACME.HTTPChallenge.EntryPoint]; !ok { - eps = append(eps, resolver.ACME.HTTPChallenge.EntryPoint) - uniq[resolver.ACME.HTTPChallenge.EntryPoint] = struct{}{} + if _, ok := uniq[resolver.ACME.HTTPChallenge.EntryPoint]; ok { + continue } + uniq[resolver.ACME.HTTPChallenge.EntryPoint] = struct{}{} + + if allowByPass, ok := allowACMEByPass[resolver.ACME.HTTPChallenge.EntryPoint]; ok && allowByPass { + epsByPass = append(epsByPass, resolver.ACME.HTTPChallenge.EntryPoint) + continue + } + + eps = append(eps, resolver.ACME.HTTPChallenge.EntryPoint) } } @@ -115,6 +127,17 @@ func (i *Provider) acme(cfg *dynamic.Configuration) { cfg.HTTP.Routers["acme-http"] = rt cfg.HTTP.Services["acme-http"] = &dynamic.Service{} } + + if len(epsByPass) > 0 { + rt := &dynamic.Router{ + Rule: "PathPrefix(`/.well-known/acme-challenge/`)", + EntryPoints: epsByPass, + Service: "acme-http@internal", + } + + cfg.HTTP.Routers["acme-http-bypass"] = rt + cfg.HTTP.Services["acme-http"] = &dynamic.Service{} + } } func (i *Provider) redirection(ctx context.Context, cfg *dynamic.Configuration) { diff --git a/pkg/redactor/redactor_config_test.go b/pkg/redactor/redactor_config_test.go index 592d91cfd..15c996193 100644 --- a/pkg/redactor/redactor_config_test.go +++ b/pkg/redactor/redactor_config_test.go @@ -777,7 +777,7 @@ func TestDo_staticConfiguration(t *testing.T) { } config.Providers.HTTP = &http.Provider{ - Endpoint: "Myenpoint", + Endpoint: "Myendpoint", PollInterval: 42, PollTimeout: 42, TLS: &types.ClientTLS{ diff --git a/pkg/server/router/router_test.go b/pkg/server/router/router_test.go index c1ae3436e..9b1cab388 100644 --- a/pkg/server/router/router_test.go +++ b/pkg/server/router/router_test.go @@ -858,7 +858,7 @@ func BenchmarkService(b *testing.B) { LoadBalancer: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{ { - URL: "tchouck", + URL: "tchouk", }, }, }, diff --git a/pkg/server/router/tcp/router.go b/pkg/server/router/tcp/router.go index 89096c7ca..06ea5c223 100644 --- a/pkg/server/router/tcp/router.go +++ b/pkg/server/router/tcp/router.go @@ -21,6 +21,8 @@ const defaultBufSize = 4096 // Router is a TCP router. type Router struct { + acmeTLSPassthrough bool + // Contains TCP routes. muxerTCP tcpmuxer.Muxer // Contains TCP TLS routes. @@ -164,7 +166,7 @@ func (r *Router) ServeTCP(conn tcp.WriteCloser) { } // Handling ACME-TLS/1 challenges. - if slices.Contains(hello.protos, tlsalpn01.ACMETLS1Protocol) { + if !r.acmeTLSPassthrough && slices.Contains(hello.protos, tlsalpn01.ACMETLS1Protocol) { r.acmeTLSALPNHandler().ServeTCP(r.GetConn(conn, hello.peeked)) return } @@ -317,6 +319,10 @@ func (r *Router) SetHTTPSHandler(handler http.Handler, config *tls.Config) { r.httpsTLSConfig = config } +func (r *Router) EnableACMETLSPassthrough() { + r.acmeTLSPassthrough = true +} + // Conn is a connection proxy that handles Peeked bytes. type Conn struct { // Peeked are the bytes that have been read from Conn for the purposes of route matching, diff --git a/pkg/server/router/tcp/router_test.go b/pkg/server/router/tcp/router_test.go index b95f0d867..16f633443 100644 --- a/pkg/server/router/tcp/router_test.go +++ b/pkg/server/router/tcp/router_test.go @@ -212,9 +212,10 @@ func Test_Routing(t *testing.T) { } testCases := []struct { - desc string - routers []applyRouter - checks []checkCase + desc string + routers []applyRouter + checks []checkCase + allowACMETLSPassthrough bool }{ { desc: "No routers", @@ -271,6 +272,18 @@ func Test_Routing(t *testing.T) { }, }, }, + { + desc: "TCP TLS passthrough catches ACME TLS", + allowACMETLSPassthrough: true, + routers: []applyRouter{routerTCPTLSCatchAllPassthrough}, + checks: []checkCase{ + { + desc: "ACME TLS Challenge", + checkRouter: checkACMETLS, + expectedError: "tls: first record does not look like a TLS handshake", + }, + }, + }, { desc: "Single TCP CatchAll router", routers: []applyRouter{routerTCPCatchAll}, @@ -596,6 +609,10 @@ func Test_Routing(t *testing.T) { router, err := manager.buildEntryPointHandler(context.Background(), dynConf.TCPRouters, dynConf.Routers, nil, nil) require.NoError(t, err) + if test.allowACMETLSPassthrough { + router.EnableACMETLSPassthrough() + } + epListener, err := net.Listen("tcp", "127.0.0.1:0") require.NoError(t, err) @@ -717,7 +734,7 @@ func routerTCPTLSCatchAll(conf *runtime.Configuration) { } } -// routerTCPTLSCatchAllPassthrough a TCP TLS CatchAll Passthrough - HostSNI(`*`) router with TLS 1.0 config. +// routerTCPTLSCatchAllPassthrough a TCP TLS CatchAll Passthrough - HostSNI(`*`) router with TLS 1.2 config. func routerTCPTLSCatchAllPassthrough(conf *runtime.Configuration) { conf.TCPRouters["tcp-tls-catchall-passthrough"] = &runtime.TCPRouterInfo{ TCPRouter: &dynamic.TCPRouter{ diff --git a/pkg/server/routerfactory.go b/pkg/server/routerfactory.go index 1ece7096a..7526bf3a3 100644 --- a/pkg/server/routerfactory.go +++ b/pkg/server/routerfactory.go @@ -21,8 +21,9 @@ import ( // RouterFactory the factory of TCP/UDP routers. type RouterFactory struct { - entryPointsTCP []string - entryPointsUDP []string + entryPointsTCP []string + entryPointsUDP []string + allowACMEByPass map[string]bool managerFactory *service.ManagerFactory @@ -40,9 +41,20 @@ type RouterFactory struct { func NewRouterFactory(staticConfiguration static.Configuration, managerFactory *service.ManagerFactory, tlsManager *tls.Manager, observabilityMgr *middleware.ObservabilityMgr, pluginBuilder middleware.PluginsBuilder, dialerManager *tcp.DialerManager, ) *RouterFactory { + handlesTLSChallenge := false + for _, resolver := range staticConfiguration.CertificatesResolvers { + if resolver.ACME.TLSChallenge != nil { + handlesTLSChallenge = true + break + } + } + + allowACMEByPass := map[string]bool{} var entryPointsTCP, entryPointsUDP []string - for name, cfg := range staticConfiguration.EntryPoints { - protocol, err := cfg.GetProtocol() + for name, ep := range staticConfiguration.EntryPoints { + allowACMEByPass[name] = ep.AllowACMEByPass || !handlesTLSChallenge + + protocol, err := ep.GetProtocol() if err != nil { // Should never happen because Traefik should not start if protocol is invalid. log.Error().Err(err).Msg("Invalid protocol") @@ -63,6 +75,7 @@ func NewRouterFactory(staticConfiguration static.Configuration, managerFactory * tlsManager: tlsManager, pluginBuilder: pluginBuilder, dialerManager: dialerManager, + allowACMEByPass: allowACMEByPass, } } @@ -95,6 +108,12 @@ func (f *RouterFactory) CreateRouters(rtConf *runtime.Configuration) (map[string rtTCPManager := tcprouter.NewManager(rtConf, svcTCPManager, middlewaresTCPBuilder, handlersNonTLS, handlersTLS, f.tlsManager) routersTCP := rtTCPManager.BuildHandlers(ctx, f.entryPointsTCP) + for ep, r := range routersTCP { + if allowACMEByPass, ok := f.allowACMEByPass[ep]; ok && allowACMEByPass { + r.EnableACMETLSPassthrough() + } + } + // UDP svcUDPManager := udpsvc.NewManager(rtConf) rtUDPManager := udprouter.NewManager(rtConf, svcUDPManager) diff --git a/pkg/server/server_entrypoint_tcp.go b/pkg/server/server_entrypoint_tcp.go index ca901c90f..34285d0b1 100644 --- a/pkg/server/server_entrypoint_tcp.go +++ b/pkg/server/server_entrypoint_tcp.go @@ -185,7 +185,10 @@ func NewTCPEntryPoint(ctx context.Context, name string, config *static.EntryPoin return nil, fmt.Errorf("error preparing server: %w", err) } - rt := &tcprouter.Router{} + rt, err := tcprouter.NewRouter() + if err != nil { + return nil, fmt.Errorf("error preparing tcp router: %w", err) + } reqDecorator := requestdecorator.New(hostResolverConfig) @@ -607,6 +610,7 @@ func createHTTPServer(ctx context.Context, ln net.Listener, configuration *stati handler, err = forwardedheaders.NewXForwarded( configuration.ForwardedHeaders.Insecure, configuration.ForwardedHeaders.TrustedIPs, + configuration.ForwardedHeaders.Connection, next) if err != nil { return nil, err diff --git a/pkg/server/server_entrypoint_tcp_test.go b/pkg/server/server_entrypoint_tcp_test.go index a8f5719d7..c300c45e8 100644 --- a/pkg/server/server_entrypoint_tcp_test.go +++ b/pkg/server/server_entrypoint_tcp_test.go @@ -20,7 +20,9 @@ import ( ) func TestShutdownHijacked(t *testing.T) { - router := &tcprouter.Router{} + router, err := tcprouter.NewRouter() + require.NoError(t, err) + router.SetHTTPHandler(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { conn, _, err := rw.(http.Hijacker).Hijack() require.NoError(t, err) @@ -34,7 +36,9 @@ func TestShutdownHijacked(t *testing.T) { } func TestShutdownHTTP(t *testing.T) { - router := &tcprouter.Router{} + router, err := tcprouter.NewRouter() + require.NoError(t, err) + router.SetHTTPHandler(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { rw.WriteHeader(http.StatusOK) time.Sleep(time.Second) @@ -167,7 +171,9 @@ func TestReadTimeoutWithoutFirstByte(t *testing.T) { }, nil, nil) require.NoError(t, err) - router := &tcprouter.Router{} + router, err := tcprouter.NewRouter() + require.NoError(t, err) + router.SetHTTPHandler(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { rw.WriteHeader(http.StatusOK) })) @@ -204,7 +210,9 @@ func TestReadTimeoutWithFirstByte(t *testing.T) { }, nil, nil) require.NoError(t, err) - router := &tcprouter.Router{} + router, err := tcprouter.NewRouter() + require.NoError(t, err) + router.SetHTTPHandler(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { rw.WriteHeader(http.StatusOK) })) @@ -244,7 +252,9 @@ func TestKeepAliveMaxRequests(t *testing.T) { }, nil, nil) require.NoError(t, err) - router := &tcprouter.Router{} + router, err := tcprouter.NewRouter() + require.NoError(t, err) + router.SetHTTPHandler(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { rw.WriteHeader(http.StatusOK) })) @@ -290,7 +300,9 @@ func TestKeepAliveMaxTime(t *testing.T) { }, nil, nil) require.NoError(t, err) - router := &tcprouter.Router{} + router, err := tcprouter.NewRouter() + require.NoError(t, err) + router.SetHTTPHandler(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { rw.WriteHeader(http.StatusOK) })) diff --git a/pkg/tls/certificate_store_test.go b/pkg/tls/certificate_store_test.go index 21b0730ed..cbc668bd6 100644 --- a/pkg/tls/certificate_store_test.go +++ b/pkg/tls/certificate_store_test.go @@ -47,7 +47,7 @@ func TestGetBestCertificate(t *testing.T) { expectedCert: "*.snitest.com", }, { - desc: "Best Match with dynamic wildcard only, case insensitive", + desc: "Best Match with dynamic wildcard only, case-insensitive", domainToCheck: "bar.www.snitest.com", dynamicCert: "*.www.snitest.com", expectedCert: "*.www.snitest.com", diff --git a/pkg/types/domain_test.go b/pkg/types/domain_test.go index f9f7d42cb..490e1732a 100644 --- a/pkg/types/domain_test.go +++ b/pkg/types/domain_test.go @@ -137,7 +137,7 @@ func TestMatchDomain(t *testing.T) { expected: true, }, { - desc: "dot replaced by a cahr", + desc: "dot replaced by a char", certDomain: "sub.sub.traefik.wtf", domain: "sub.sub.traefikiwtf", expected: false, diff --git a/webui/src/components/_commons/NavBar.vue b/webui/src/components/_commons/NavBar.vue index d57d68dd5..8a618b9d4 100644 --- a/webui/src/components/_commons/NavBar.vue +++ b/webui/src/components/_commons/NavBar.vue @@ -129,7 +129,7 @@ align="left" icon="eva-github-outline" no-caps - label="Github repository" + label="GitHub repository" class="btn-submenu full-width" />