diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1262ea2917..3e8ee3aaa3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,41 @@
+## [v3.6.18](https://github.com/traefik/traefik/tree/v3.6.18) (2026-06-03)
+[All Commits](https://github.com/traefik/traefik/compare/v3.6.17...v3.6.18)
+
+**Bug fixes:**
+- **[accesslogs]** Escape double quotes in quoted log fields ([#13180](https://github.com/traefik/traefik/pull/13180) @KaanSimsek)
+- **[k8s/gatewayapi]** Escape exact gRPC method matches ([#13201](https://github.com/traefik/traefik/pull/13201) @nickmnt)
+- **[logs, middleware]** Allow query parameters to be dropped from RequestPath in access log ([#13091](https://github.com/traefik/traefik/pull/13091) @calinelson)
+- **[k8s/gatewayapi]** Bump github.com/moby/spdystream to v0.5.1 ([#13252](https://github.com/traefik/traefik/pull/13252) @kevinpollet)
+- **[file]** Improve file provider behavior regarding dangling symlinks ([#12449](https://github.com/traefik/traefik/pull/12449) @fh-yuxiao-zeng)
+- **[server]** Bump github.com/bytedance/sonic to v1.15.1 ([#13254](https://github.com/traefik/traefik/pull/13254) @kevinpollet)
+- **[middleware, authentication]** Add error on basic auth build if users is empty ([#13195](https://github.com/traefik/traefik/pull/13195) @rtribotte)
+- **[k8s/ingress]** Avoid ingress path matcher injection and backport 11d251415 ([#13227](https://github.com/traefik/traefik/pull/13227) @rtribotte)
+- **[server]** Move snicheck to ctx instead of simulated routing ([#13214](https://github.com/traefik/traefik/pull/13214) @juliens)
+- **[middleware]** Reject requests with different paths after StripPrefix and StripPrefixRegex normalisation ([#13215](https://github.com/traefik/traefik/pull/13215) @rtribotte)
+- **[server]** Bump golang.org/x/net to v0.55.0 ([#13251](https://github.com/traefik/traefik/pull/13251) @kevinpollet)
+- **[k8s/gatewayapi]** Change default values and expose configuration for Kubernetes client QPS and Burst ([#13277](https://github.com/traefik/traefik/pull/13277) @kevinpollet)
+- **[server]** Bump golang.org/x/crypto to v0.52.0 ([#13276](https://github.com/traefik/traefik/pull/13276) @rtribotte)
+
+**Documentation:**
+- **[file]** Replace generated File routing reference page ([#13170](https://github.com/traefik/traefik/pull/13170) @sheddy-traefik)
+- **[middleware]** Remove whitespace in HTML tag ([#13160](https://github.com/traefik/traefik/pull/13160) @marbon87)
+- **[k8s/crd]** Fix typo in accesslogs field name ([#13177](https://github.com/traefik/traefik/pull/13177) @PlayMTL)
+- **[k8s/ingress-nginx]** Surface the Ingress status race condition during NGINX coexistence ([#13205](https://github.com/traefik/traefik/pull/13205) @emilevauge)
+- **[k8s/ingress-nginx]** Capitalize NGINX in kubernetesIngressNGINX ([#13236](https://github.com/traefik/traefik/pull/13236) @smellems)
+- Polish grammar in migration guides ([#13174](https://github.com/traefik/traefik/pull/13174) @quyentonndbs)
+- Add @LBF38 as a current maintainer ([#13225](https://github.com/traefik/traefik/pull/13225) @emilevauge)
+
+## [v2.11.47](https://github.com/traefik/traefik/tree/v2.11.47) (2026-06-03)
+[All Commits](https://github.com/traefik/traefik/compare/v2.11.46...v2.11.47)
+
+**Bug fixes:**
+- **[middleware, authentication]** Add error on basic auth build if users is empty ([#13195](https://github.com/traefik/traefik/pull/13195) @rtribotte)
+- **[k8s/ingress]** Avoid ingress path matcher injection and backport 11d251415 ([#13227](https://github.com/traefik/traefik/pull/13227) @rtribotte)
+- **[server]** Move snicheck to ctx instead of simulated routing ([#13214](https://github.com/traefik/traefik/pull/13214) @juliens)
+- **[middleware]** Reject requests with different paths after StripPrefix and StripPrefixRegex normalisation ([#13215](https://github.com/traefik/traefik/pull/13215) @rtribotte)
+- **[server]** Bump golang.org/x/net to v0.55.0 ([#13251](https://github.com/traefik/traefik/pull/13251) @kevinpollet)
+- **[server]** Bump golang.org/x/crypto to v0.52.0 ([#13276](https://github.com/traefik/traefik/pull/13276) @rtribotte)
+
## [v3.7.1](https://github.com/traefik/traefik/tree/v3.7.1) (2026-05-11)
[All Commits](https://github.com/traefik/traefik/compare/v3.7.0...v3.7.1)
diff --git a/docs/content/migrate/nginx-to-traefik.md b/docs/content/migrate/nginx-to-traefik.md
index 3e838296cd..79499038ba 100644
--- a/docs/content/migrate/nginx-to-traefik.md
+++ b/docs/content/migrate/nginx-to-traefik.md
@@ -246,7 +246,7 @@ helm repo update
```bash
helm upgrade --install traefik traefik/traefik \
--namespace traefik --create-namespace \
- --set providers.kubernetesIngressNginx.enabled=true
+ --set providers.kubernetesIngressNGINX.enabled=true
```
Or using a [values file](https://github.com/traefik/traefik-helm-chart/blob/master/traefik/VALUES.md) for more configuration:
@@ -254,7 +254,7 @@ Or using a [values file](https://github.com/traefik/traefik-helm-chart/blob/mast
```yaml tab="traefik-values.yaml"
...
providers:
- kubernetesIngressNginx:
+ kubernetesIngressNGINX:
enabled: true
...
```
diff --git a/docs/content/migrate/v3.md b/docs/content/migrate/v3.md
index e8d8894705..71c7ab9015 100644
--- a/docs/content/migrate/v3.md
+++ b/docs/content/migrate/v3.md
@@ -9,6 +9,43 @@ This guide provides detailed migration steps for upgrading between different Tra
---
+## v3.7.2
+
+### Kubernetes Gateway API Provider
+
+Starting with `v3.7.2`, the QPS and Burst values of the Kubernetes client used by the Kubernetes Gateway API provider have been increased to `50` and `100` respectively (10x the default values of the Kubernetes client).
+
+The Kubernetes Gateway API provider writes status updates intensively to comply with the Kubernetes Gateway API specification.
+This change helps avoid performance issues related to Kubernetes API rate limiting, which can increase the setup time when a new routing configuration is built.
+
+These values are configurable through the [`kubernetesGateway.qps`](../reference/install-configuration/providers/kubernetes/kubernetes-gateway.md#opt-providers-kubernetesgateway-qps)
+and [`kubernetesGateway.burst`](../reference/install-configuration/providers/kubernetes/kubernetes-gateway.md#opt-providers-kubernetesgateway-burst) provider options.
+
+### BasicAuth Middleware
+
+From version `v3.7.2` onwards, the BasicAuth middleware requires a non-empty users configuration in order to be built successfully.
+Previously, the middleware would be built successfully but always return a 401 status code for any request.
+Now, an error occurs and any routers using it will be unmounted. For the same request, a 404 status code is served instead of a 401 status code.
+
+### StripPrefix and StripPrefixRegex Middleware
+
+From version `v3.7.2` onwards, the StripPrefix middleware and the StripPrefixRegex middleware reject requests (`400 Bad Request`)
+when stripping the configured prefix produces a path that differs from its normalised form
+(i.e. a path containing `.` or `..` segments that would be collapsed by normalisation).
+
+This prevents the stripped path from being interpreted as a different resource by the upstream service.
+
+Examples with a configured prefix of `/api`:
+
+| Request path | Path after strip | Normalised path | Result |
+|--------------|------------------|-----------------|--------------|
+| `/api/foo` | `/foo` | `/foo` | `200` (sent) |
+| `/api/` | `/` | `/` | `200` (sent) |
+| `/api./foo` | `/./foo` | `/foo` | `400` |
+| `/api../foo` | `/../foo` | `/foo` | `400` |
+
+---
+
## v3.7.1
### Kubernetes providers: `crossProviderNamespaces`
@@ -107,6 +144,43 @@ Note: TLSOptions for `HostRegexp` matchers remains unsupported. Use wildcard `Ho
---
+## v3.6.18
+
+### Kubernetes Gateway API Provider
+
+Starting with `v3.6.18`, the QPS and Burst values of the Kubernetes client used by the Kubernetes Gateway API provider have been increased to `50` and `100` respectively (10x the default values of the Kubernetes client).
+
+The Kubernetes Gateway API provider writes status updates intensively to comply with the Kubernetes Gateway API specification.
+This change helps avoid performance issues related to Kubernetes API rate limiting, which can increase the setup time when a new routing configuration is built.
+
+These values are configurable through the [`kubernetesGateway.qps`](../reference/install-configuration/providers/kubernetes/kubernetes-gateway.md#opt-providers-kubernetesgateway-qps)
+and [`kubernetesGateway.burst`](../reference/install-configuration/providers/kubernetes/kubernetes-gateway.md#opt-providers-kubernetesgateway-burst) provider options.
+
+### BasicAuth Middleware
+
+From version `v3.6.18` onwards, the BasicAuth middleware requires a non-empty users configuration in order to be built successfully.
+Previously, the middleware would be built successfully but always return a 401 status code for any request.
+Now, an error occurs and any routers using it will be unmounted. For the same request, a 404 status code is served instead of a 401 status code.
+
+### StripPrefix and StripPrefixRegex Middleware
+
+From version `v3.6.18` onwards, the StripPrefix middleware and the StripPrefixRegex middleware reject requests (`400 Bad Request`)
+when stripping the configured prefix produces a path that differs from its normalised form
+(i.e. a path containing `.` or `..` segments that would be collapsed by normalisation).
+
+This prevents the stripped path from being interpreted as a different resource by the upstream service.
+
+Examples with a configured prefix of `/api`:
+
+| Request path | Path after strip | Normalised path | Result |
+|--------------|------------------|-----------------|--------------|
+| `/api/foo` | `/foo` | `/foo` | `200` (sent) |
+| `/api/` | `/` | `/` | `200` (sent) |
+| `/api./foo` | `/./foo` | `/foo` | `400` |
+| `/api../foo` | `/../foo` | `/foo` | `400` |
+
+---
+
## v3.6.17
### Kubernetes providers: `crossProviderNamespaces`
diff --git a/docs/content/reference/install-configuration/configuration-options.md b/docs/content/reference/install-configuration/configuration-options.md
index c4278ccd5a..1076f643fe 100644
--- a/docs/content/reference/install-configuration/configuration-options.md
+++ b/docs/content/reference/install-configuration/configuration-options.md
@@ -364,6 +364,7 @@ THIS FILE MUST NOT BE EDITED BY HAND
| providers.kubernetescrd.throttleduration | Ingress refresh throttle duration | 0 |
| providers.kubernetescrd.token | Kubernetes bearer token (not needed for in-cluster client). It accepts either a token value or a file path to the token. | |
| providers.kubernetesgateway | Enables Kubernetes Gateway API provider. | false |
+| providers.kubernetesgateway.burst | Defines the maximum burst of requests to the Kubernetes API server. | 100 |
| providers.kubernetesgateway.certauthfilepath | Kubernetes certificate authority file path (not needed for in-cluster client). | |
| providers.kubernetesgateway.crossprovidernamespaces | List of namespaces from which Gateway API routes are allowed to declare TraefikService backendRef references. | |
| providers.kubernetesgateway.endpoint | Kubernetes server endpoint (required for external cluster client). | |
@@ -371,6 +372,7 @@ THIS FILE MUST NOT BE EDITED BY HAND
| providers.kubernetesgateway.labelselector | Kubernetes label selector to select specific GatewayClasses. | |
| providers.kubernetesgateway.namespaces | Kubernetes namespaces. | |
| providers.kubernetesgateway.nativelbbydefault | Defines whether to use Native Kubernetes load-balancing by default. | false |
+| providers.kubernetesgateway.qps | Defines the maximum QPS to the Kubernetes API server. Setting this to a negative value will disable client-side ratelimiting. | 50 |
| providers.kubernetesgateway.statusaddress.hostname | Hostname used for Kubernetes Gateway status address. | |
| providers.kubernetesgateway.statusaddress.ip | IP used to set Kubernetes Gateway status address. | |
| providers.kubernetesgateway.statusaddress.service | Published Kubernetes Service to copy status addresses from. | |
diff --git a/docs/content/reference/install-configuration/providers/kubernetes/kubernetes-gateway.md b/docs/content/reference/install-configuration/providers/kubernetes/kubernetes-gateway.md
index 4899475dd0..e0b9398b3d 100644
--- a/docs/content/reference/install-configuration/providers/kubernetes/kubernetes-gateway.md
+++ b/docs/content/reference/install-configuration/providers/kubernetes/kubernetes-gateway.md
@@ -66,22 +66,24 @@ providers:
-| Field | Description | Default | Required |
-|:----------------------------------------------------------------------|:------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:--------|:---------|
-| `providers.providersThrottleDuration` | Minimum amount of time to wait for, after a configuration reload, before taking into account any new configuration refresh event.
If multiple events occur within this time, only the most recent one is taken into account, and all others are discarded.
**This option cannot be set per provider, but the throttling algorithm applies to each of them independently.** | 2s | No |
-| `providers.kubernetesGateway.endpoint` | Server endpoint URL.
More information [here](#endpoint). | "" | No |
-| `providers.kubernetesGateway.experimentalChannel` | Toggles support for the Experimental Channel resources ([Gateway API release channels documentation](https://gateway-api.sigs.k8s.io/concepts/versioning/#release-channels)).
(ex: `TCPRoute`) | false | No |
-| `providers.kubernetesGateway.token` | Bearer token used for the Kubernetes client configuration. | "" | No |
-| `providers.kubernetesGateway.certAuthFilePath` | Path to the certificate authority file.
Used for the Kubernetes client configuration. | "" | No |
-| `providers.kubernetesGateway.namespaces` | Array of namespaces to watch.
If left empty, watch all namespaces. | [] | No |
-| `providers.kubernetesGateway.labelselector` | Allow filtering on `GatewayClass` only. If left empty, Traefik processes all GatewayClass objects in the configured namespaces.
See [label-selectors](https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#label-selectors) for details. | "" | No |
-| `providers.kubernetesGateway.throttleDuration` | Minimum amount of time to wait between two Kubernetes events before producing a new configuration.
This prevents a Kubernetes cluster that updates many times per second from continuously changing your Traefik configuration.
If empty, every event is caught. | 0s | No |
-| `providers.kubernetesGateway.nativeLBByDefault` | Defines whether to use Native Kubernetes load-balancing mode by default. For more information, please check out the `traefik.io/service.nativelb` service annotation documentation. | false | No |
-| `providers.kubernetesGateway.`
`statusAddress.hostname` | Hostname copied to the Gateway `status.addresses`. | "" | No |
-| `providers.kubernetesGateway.`
`statusAddress.ip` | IP address copied to the Gateway `status.addresses`, and currently only supports one IP value (IPv4 or IPv6). | "" | No |
-| `providers.kubernetesGateway.`
`statusAddress.service.namespace` | The namespace of the Kubernetes service to copy status addresses from.
When using third parties tools like External-DNS, this option can be used to copy the service `loadbalancer.status` (containing the service's endpoints IPs) to the Gateway `status.addresses`. | "" | No |
-| `providers.kubernetesGateway.`
`statusAddress.service.name` | The name of the Kubernetes service to copy status addresses from.
When using third parties tools like External-DNS, this option can be used to copy the service `loadbalancer.status` (containing the service's endpoints IPs) to the Gateway `status.addresses`. | "" | No |
-| `providers.kubernetesGateway.crossProviderNamespaces` | List of namespaces from which Gateway API routes (`HTTPRoute`, `TCPRoute`, `TLSRoute`) are allowed to declare a `backendRef` of kind `TraefikService`.
When unset, all namespaces are allowed. When set to `[]`, every such backendRef is rejected and the route is dropped. | [] | No |
+| Field | Description | Default | Required |
+|:----------------------------------------------------------------------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:--------|:---------|
+| `providers.providersThrottleDuration` | Minimum amount of time to wait for, after a configuration reload, before taking into account any new configuration refresh event.
If multiple events occur within this time, only the most recent one is taken into account, and all others are discarded.
**This option cannot be set per provider, but the throttling algorithm applies to each of them independently.** | 2s | No |
+| `providers.kubernetesGateway.endpoint` | Server endpoint URL.
More information [here](#endpoint). | "" | No |
+| `providers.kubernetesGateway.experimentalChannel` | Toggles support for the Experimental Channel resources ([Gateway API release channels documentation](https://gateway-api.sigs.k8s.io/concepts/versioning/#release-channels)).
(ex: `TCPRoute`) | false | No |
+| `providers.kubernetesGateway.token` | Bearer token used for the Kubernetes client configuration. | "" | No |
+| `providers.kubernetesGateway.certAuthFilePath` | Path to the certificate authority file.
Used for the Kubernetes client configuration. | "" | No |
+| `providers.kubernetesGateway.namespaces` | Array of namespaces to watch.
If left empty, watch all namespaces. | [] | No |
+| `providers.kubernetesGateway.labelselector` | Allow filtering on `GatewayClass` only. If left empty, Traefik processes all GatewayClass objects in the configured namespaces.
See [label-selectors](https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#label-selectors) for details. | "" | No |
+| `providers.kubernetesGateway.throttleDuration` | Minimum amount of time to wait between two Kubernetes events before producing a new configuration.
This prevents a Kubernetes cluster that updates many times per second from continuously changing your Traefik configuration.
If empty, every event is caught. | 0s | No |
+| `providers.kubernetesGateway.nativeLBByDefault` | Defines whether to use Native Kubernetes load-balancing mode by default. For more information, please check out the `traefik.io/service.nativelb` service annotation documentation. | false | No |
+| `providers.kubernetesGateway.`
`statusAddress.hostname` | Hostname copied to the Gateway `status.addresses`. | "" | No |
+| `providers.kubernetesGateway.`
`statusAddress.ip` | IP address copied to the Gateway `status.addresses`, and currently only supports one IP value (IPv4 or IPv6). | "" | No |
+| `providers.kubernetesGateway.`
`statusAddress.service.namespace` | The namespace of the Kubernetes service to copy status addresses from.
When using third parties tools like External-DNS, this option can be used to copy the service `loadbalancer.status` (containing the service's endpoints IPs) to the Gateway `status.addresses`. | "" | No |
+| `providers.kubernetesGateway.`
`statusAddress.service.name` | The name of the Kubernetes service to copy status addresses from.
When using third parties tools like External-DNS, this option can be used to copy the service `loadbalancer.status` (containing the service's endpoints IPs) to the Gateway `status.addresses`. | "" | No |
+| `providers.kubernetesGateway.crossProviderNamespaces` | List of namespaces from which Gateway API routes (`HTTPRoute`, `TCPRoute`, `TLSRoute`) are allowed to declare a `backendRef` of kind `TraefikService`.
When unset, all namespaces are allowed. When set to `[]`, every such backendRef is rejected and the route is dropped. | [] | No |
+| providers.kubernetesgateway.qps | Defines the maximum QPS to the Kubernetes API server. Setting this to a negative value will disable client-side ratelimiting. | 50 | No |
+| providers.kubernetesgateway.burst | Defines the maximum burst of requests to the Kubernetes API server. | 100 | No |
diff --git a/go.mod b/go.mod
index bbd1cda5c2..f8e2d281d9 100644
--- a/go.mod
+++ b/go.mod
@@ -99,12 +99,12 @@ require (
go.opentelemetry.io/otel/sdk/log v0.19.0
go.opentelemetry.io/otel/sdk/metric v1.43.0
go.opentelemetry.io/otel/trace v1.43.0
- golang.org/x/crypto v0.50.0
+ golang.org/x/crypto v0.52.0
golang.org/x/mod v0.35.0
- golang.org/x/net v0.53.0
+ golang.org/x/net v0.55.0
golang.org/x/sync v0.20.0
- golang.org/x/sys v0.43.0
- golang.org/x/text v0.36.0
+ golang.org/x/sys v0.45.0
+ golang.org/x/text v0.37.0
golang.org/x/time v0.15.0
golang.org/x/tools v0.44.0
google.golang.org/grpc v1.80.0
@@ -182,7 +182,7 @@ require (
github.com/blendle/zapdriver v1.3.1 // indirect
github.com/bodgit/tsig v1.2.2 // indirect
github.com/boombuler/barcode v1.0.1 // indirect
- github.com/bytedance/sonic v1.12.0 // indirect
+ github.com/bytedance/sonic v1.15.1 // indirect
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/clbanning/mxj/v2 v2.7.0 // indirect
@@ -274,7 +274,6 @@ require (
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12 // indirect
github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213 // indirect
- github.com/klauspost/cpuid/v2 v2.2.5 // indirect
github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b // indirect
github.com/kylelemons/godebug v1.1.0 // indirect
github.com/labbsr0x/bindman-dns-webhook v1.0.2 // indirect
@@ -298,7 +297,7 @@ require (
github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/moby/go-archive v0.2.0 // indirect
github.com/moby/patternmatcher v0.6.1 // indirect
- github.com/moby/spdystream v0.5.0 // indirect
+ github.com/moby/spdystream v0.5.1 // indirect
github.com/moby/sys/sequential v0.6.0 // indirect
github.com/moby/sys/user v0.4.0 // indirect
github.com/moby/sys/userns v0.1.0 // indirect
@@ -401,7 +400,7 @@ require (
golang.org/x/arch v0.4.0 // indirect
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f // indirect
golang.org/x/oauth2 v0.36.0 // indirect
- golang.org/x/term v0.42.0 // indirect
+ golang.org/x/term v0.43.0 // indirect
gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect
google.golang.org/api v0.276.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 // indirect
diff --git a/go.sum b/go.sum
index 781b73a849..b97e7dc353 100644
--- a/go.sum
+++ b/go.sum
@@ -835,10 +835,12 @@ github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
-github.com/bytedance/sonic v1.12.0 h1:YGPgxF9xzaCNvd/ZKdQ28yRovhfMFZQjuk6fKBzZ3ls=
-github.com/bytedance/sonic v1.12.0/go.mod h1:B8Gt/XvtZ3Fqj+iSKMypzymZxw/FVwgIGKzMzT9r/rk=
-github.com/bytedance/sonic/loader v0.2.0 h1:zNprn+lsIP06C/IqCHs3gPQIvnvpKbbxyXQP1iU4kWM=
-github.com/bytedance/sonic/loader v0.2.0/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
+github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
+github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
+github.com/bytedance/sonic v1.15.1 h1:nJD5PmM0vY7J8CT6MxoqbVAAMhkSmV2HgRAUrrpLoOw=
+github.com/bytedance/sonic v1.15.1/go.mod h1:mT2NbXunuaEbnZ+mRIX/vYqKISmgEuHFDI4UzmKx2SA=
+github.com/bytedance/sonic/loader v0.5.1 h1:Ygpfa9zwRCCKSlrp5bBP/b/Xzc3VxsAW+5NIYXrOOpI=
+github.com/bytedance/sonic/loader v0.5.1/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
github.com/c-bata/go-prompt v0.2.5/go.mod h1:vFnjEGDIIA/Lib7giyE4E9c50Lvl8j0S+7FVlAwDAVw=
github.com/c-bata/go-prompt v0.2.6/go.mod h1:/LMAke8wD2FsNu9EXNdHxNLbd9MedkPnCdfpU9wwHfY=
github.com/casbin/casbin/v2 v2.37.0/go.mod h1:vByNa/Fchek0KZUgG5wEsl7iFsiviAYKRtgrQfcJqHg=
@@ -866,10 +868,8 @@ github.com/clbanning/mxj v1.8.4/go.mod h1:BVjHeAH+rl9rs6f+QIpeRl0tfu10SXn1pUSa5P
github.com/clbanning/mxj/v2 v2.7.0 h1:WA/La7UGCanFe5NpHF0Q3DNtnCsVoxbPKuyBNHWRyME=
github.com/clbanning/mxj/v2 v2.7.0/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
-github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
-github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
-github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
-github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
+github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
+github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
@@ -1467,8 +1467,8 @@ github.com/klauspost/compress v1.15.9/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHU
github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE=
github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
-github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg=
-github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
+github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY=
+github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8=
github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b h1:udzkj9S/zlT5X367kqJis0QP7YMxobob6zhzq6Yre00=
github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b/go.mod h1:pcaDhQK0/NJZEvtCO0qQPPropqV0sJOJ6YW7X+9kRwM=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
@@ -1629,8 +1629,8 @@ github.com/moby/moby/client v0.4.0 h1:S+2XegzHQrrvTCvF6s5HFzcrywWQmuVnhOXe2kiWjI
github.com/moby/moby/client v0.4.0/go.mod h1:QWPbvWchQbxBNdaLSpoKpCdf5E+WxFAgNHogCWDoa7g=
github.com/moby/patternmatcher v0.6.1 h1:qlhtafmr6kgMIJjKJMDmMWq7WLkKIo23hsrpR3x084U=
github.com/moby/patternmatcher v0.6.1/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
-github.com/moby/spdystream v0.5.0 h1:7r0J1Si3QO/kjRitvSLVVFUjxMEb/YLj6S9FF62JBCU=
-github.com/moby/spdystream v0.5.0/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI=
+github.com/moby/spdystream v0.5.1 h1:9sNYeYZUcci9R6/w7KDaFWEWeV4LStVG78Mpyq/Zm/Y=
+github.com/moby/spdystream v0.5.1/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI=
github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=
github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=
github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs=
@@ -2239,8 +2239,8 @@ golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDf
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
-golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
-golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=
+golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988=
+golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc=
golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
@@ -2391,8 +2391,8 @@ golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
-golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=
-golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
+golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
+golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -2572,8 +2572,8 @@ golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
-golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
-golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
+golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
+golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
@@ -2595,8 +2595,8 @@ golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0=
-golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY=
-golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY=
+golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4=
+golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -2618,8 +2618,8 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
-golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
-golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
+golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
+golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
diff --git a/integration/fixtures/https/https_domain_fronting.toml b/integration/fixtures/https/https_domain_fronting.toml
index d60883b658..8df317ace3 100644
--- a/integration/fixtures/https/https_domain_fronting.toml
+++ b/integration/fixtures/https/https_domain_fronting.toml
@@ -8,6 +8,7 @@
[entryPoints.websecure]
address = ":4443"
+ [entryPoints.websecure.http3]
[api]
insecure = true
@@ -33,6 +34,35 @@
[http.routers.router3.tls]
options = "mytls"
+[http.routers.router4]
+ rule = "Host(`site4.www.snitest.com`)"
+ service = "service4"
+ [http.routers.router4.tls]
+
+[http.routers.router4path]
+ rule = "Host(`site4.www.snitest.com`) && PathPrefix(`/foo`)"
+ service = "service4"
+ [http.routers.router4path.tls]
+ options = "mytls"
+
+[http.routers.router5]
+ rule = "Host(`site5.www.snitest.com`)"
+ service = "service5"
+ [http.routers.router5.tls]
+ options = "mytls"
+
+[http.routers.router5path]
+ rule = "Host(`site5.www.snitest.com`) && PathPrefix(`/bar`)"
+ service = "service5"
+ [http.routers.router5path.tls]
+ options = "mytls"
+
+[http.routers.router6]
+ rule = "Host(`site6.www.snitest.com.`)"
+ service = "service6"
+ [http.routers.router6.tls]
+ options = "mytls"
+
[http.services.service1]
[[http.services.service1.loadBalancer.servers]]
url = "http://127.0.0.1:9010"
@@ -45,10 +75,22 @@
[[http.services.service3.loadBalancer.servers]]
url = "http://127.0.0.1:9030"
+[http.services.service4]
+ [[http.services.service4.loadBalancer.servers]]
+ url = "http://127.0.0.1:9040"
+
+[http.services.service5]
+ [[http.services.service5.loadBalancer.servers]]
+ url = "http://127.0.0.1:9050"
+
+[http.services.service6]
+ [[http.services.service6.loadBalancer.servers]]
+ url = "http://127.0.0.1:9060"
+
[[tls.certificates]]
certFile = "fixtures/https/wildcard.www.snitest.com.cert"
keyFile = "fixtures/https/wildcard.www.snitest.com.key"
[tls.options]
[tls.options.mytls]
- maxVersion = "VersionTLS12"
+ maxVersion = "VersionTLS13"
diff --git a/integration/https_test.go b/integration/https_test.go
index b9b64e8d67..ab3e190615 100644
--- a/integration/https_test.go
+++ b/integration/https_test.go
@@ -13,6 +13,7 @@ import (
"time"
"github.com/BurntSushi/toml"
+ "github.com/quic-go/quic-go/http3"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
@@ -352,7 +353,7 @@ func (s *HTTPSSuite) TestWithConflictingTLSOptions() {
assert.ErrorContains(s.T(), err, "tls: no supported versions satisfy MinVersion and MaxVersion")
// with unknown tls option
- err = try.GetRequest("http://127.0.0.1:8080/api/rawdata", 1*time.Second, try.BodyContains(fmt.Sprintf("found different TLS options for routers on the same host %v, so using the default TLS options instead", tr4.TLSClientConfig.ServerName)))
+ err = try.GetRequest("http://127.0.0.1:8080/api/rawdata", 1*time.Second, try.BodyContains("found different TLS options for routers on the same host, so using the default TLS options instead"))
require.NoError(s.T(), err)
}
@@ -1083,19 +1084,20 @@ func (s *HTTPSSuite) TestWithDomainFronting() {
defer backend2.Close()
backend3 := startTestServer("9030", http.StatusOK, "server3")
defer backend3.Close()
+ backend5 := startTestServer("9050", http.StatusOK, "server5")
+ defer backend5.Close()
file := s.adaptFile("fixtures/https/https_domain_fronting.toml", struct{}{})
s.traefikCmd(withConfigFile(file))
// wait for Traefik
- err := try.GetRequest("http://127.0.0.1:8080/api/rawdata", 500*time.Millisecond, try.BodyContains("Host(`site1.www.snitest.com`)"))
+ err := try.GetRequest("http://127.0.0.1:8080/api/rawdata", 1000*time.Millisecond, try.BodyContains("Host(`site1.www.snitest.com`)"))
require.NoError(s.T(), err)
testCases := []struct {
desc string
hostHeader string
serverName string
- expectedError bool
expectedContent string
expectedStatusCode int
}{
@@ -1113,14 +1115,6 @@ func (s *HTTPSSuite) TestWithDomainFronting() {
expectedContent: "server3",
expectedStatusCode: http.StatusOK,
},
- {
- desc: "Spaces after the host header",
- hostHeader: "site3.www.snitest.com ",
- serverName: "site3.www.snitest.com",
- expectedError: true,
- expectedContent: "server3",
- expectedStatusCode: http.StatusOK,
- },
{
desc: "Spaces after the servername",
hostHeader: "site3.www.snitest.com",
@@ -1128,14 +1122,6 @@ func (s *HTTPSSuite) TestWithDomainFronting() {
expectedContent: "server3",
expectedStatusCode: http.StatusOK,
},
- {
- desc: "Spaces after the servername and host header",
- hostHeader: "site3.www.snitest.com ",
- serverName: "site3.www.snitest.com ",
- expectedError: true,
- expectedContent: "server3",
- expectedStatusCode: http.StatusOK,
- },
{
desc: "Domain Fronting with same tlsOptions should follow header",
hostHeader: "site1.www.snitest.com",
@@ -1171,6 +1157,34 @@ func (s *HTTPSSuite) TestWithDomainFronting() {
expectedContent: "server1",
expectedStatusCode: http.StatusOK,
},
+ {
+ desc: "Domain Fronting with ambiguous TLS options should produce a 421",
+ hostHeader: "site4.www.snitest.com",
+ serverName: "site3.www.snitest.com",
+ expectedContent: "",
+ expectedStatusCode: http.StatusMisdirectedRequest,
+ },
+ {
+ desc: "Domain Fronting with same non-default TLS options should not produce a 421",
+ hostHeader: "site5.www.snitest.com",
+ serverName: "site3.www.snitest.com",
+ expectedContent: "server5",
+ expectedStatusCode: http.StatusOK,
+ },
+ {
+ desc: "FQDN host header with empty SNI to non-default TLS options route should produce a 421",
+ hostHeader: "site3.www.snitest.com.",
+ serverName: "",
+ expectedContent: "",
+ expectedStatusCode: http.StatusMisdirectedRequest,
+ },
+ {
+ desc: "Non-FQDN host header with empty SNI matching FQDN route rule should produce a 421",
+ hostHeader: "site6.www.snitest.com",
+ serverName: "",
+ expectedContent: "",
+ expectedStatusCode: http.StatusMisdirectedRequest,
+ },
}
for _, test := range testCases {
@@ -1179,11 +1193,10 @@ func (s *HTTPSSuite) TestWithDomainFronting() {
req.Host = test.hostHeader
err = try.RequestWithTransport(req, 500*time.Millisecond, &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true, ServerName: test.serverName}}, try.StatusCodeIs(test.expectedStatusCode), try.BodyContains(test.expectedContent))
- if test.expectedError {
- assert.Error(s.T(), err)
- } else {
- require.NoError(s.T(), err)
- }
+ assert.NoError(s.T(), err, "test %s failed with: %v", test.desc, err)
+
+ err = try.RequestWithTransport(req, 500*time.Millisecond, &http3.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true, ServerName: test.serverName}}, try.StatusCodeIs(test.expectedStatusCode), try.BodyContains(test.expectedContent))
+ assert.NoError(s.T(), err, "test %s failed with: %v", test.desc, err)
}
}
diff --git a/integration/simple_test.go b/integration/simple_test.go
index 159e08175b..be7bd0c55a 100644
--- a/integration/simple_test.go
+++ b/integration/simple_test.go
@@ -949,7 +949,7 @@ func (s *SimpleSuite) TestRouterConfigErrors() {
s.traefikCmd(withConfigFile(file))
// All errors
- err := try.GetRequest("http://127.0.0.1:8080/api/http/routers", 1000*time.Millisecond, try.BodyContains(`["middleware \"unknown@file\" does not exist","found different TLS options for routers on the same host snitest.net, so using the default TLS options instead"]`))
+ err := try.GetRequest("http://127.0.0.1:8080/api/http/routers", 1000*time.Millisecond, try.BodyContains(`["middleware \"unknown@file\" does not exist","found different TLS options for routers on the same host, so using the default TLS options instead"]`))
require.NoError(s.T(), err)
// router3 has an error because it uses an unknown entrypoint
diff --git a/integration/try/try.go b/integration/try/try.go
index 5a432a13d3..32d1f03581 100644
--- a/integration/try/try.go
+++ b/integration/try/try.go
@@ -76,7 +76,7 @@ func Request(req *http.Request, timeout time.Duration, conditions ...ResponseCon
// the condition on the response.
// ResponseCondition may be nil, in which case only the request against the URL must
// succeed.
-func RequestWithTransport(req *http.Request, timeout time.Duration, transport *http.Transport, conditions ...ResponseCondition) error {
+func RequestWithTransport(req *http.Request, timeout time.Duration, transport http.RoundTripper, conditions ...ResponseCondition) error {
resp, err := doTryRequest(req, timeout, transport, conditions...)
if resp != nil && resp.Body != nil {
@@ -140,12 +140,12 @@ func doTryRequest(request *http.Request, timeout time.Duration, transport http.R
func doRequest(action timedAction, timeout time.Duration, request *http.Request, transport http.RoundTripper, conditions ...ResponseCondition) (*http.Response, error) {
var resp *http.Response
return resp, action(timeout, func() error {
- var err error
- client := http.DefaultClient
+ var client http.Client
if transport != nil {
client.Transport = transport
}
+ var err error
resp, err = client.Do(request)
if err != nil {
return err
diff --git a/pkg/config/dynamic/http_config.go b/pkg/config/dynamic/http_config.go
index ecca917482..17682ef269 100644
--- a/pkg/config/dynamic/http_config.go
+++ b/pkg/config/dynamic/http_config.go
@@ -149,9 +149,10 @@ func (r *RouterDeniedEncodedPathCharacters) Map() map[string]struct{} {
// RouterTLSConfig holds the TLS configuration for a router.
type RouterTLSConfig struct {
- Options string `json:"options,omitempty" toml:"options,omitempty" yaml:"options,omitempty" export:"true"`
- CertResolver string `json:"certResolver,omitempty" toml:"certResolver,omitempty" yaml:"certResolver,omitempty" export:"true"`
- Domains []types.Domain `json:"domains,omitempty" toml:"domains,omitempty" yaml:"domains,omitempty" export:"true"`
+ Options string `json:"options,omitempty" toml:"options,omitempty" yaml:"options,omitempty" export:"true"`
+ ResolvedOptions string `json:"-" toml:"-" yaml:"-" label:"-" file:"-" kv:"-" export:"false"`
+ CertResolver string `json:"certResolver,omitempty" toml:"certResolver,omitempty" yaml:"certResolver,omitempty" export:"true"`
+ Domains []types.Domain `json:"domains,omitempty" toml:"domains,omitempty" yaml:"domains,omitempty" export:"true"`
}
// +k8s:deepcopy-gen=true
diff --git a/pkg/middlewares/auth/basic_auth.go b/pkg/middlewares/auth/basic_auth.go
index 5c7ef27171..42a86b8e9d 100644
--- a/pkg/middlewares/auth/basic_auth.go
+++ b/pkg/middlewares/auth/basic_auth.go
@@ -43,6 +43,10 @@ func NewBasic(ctx context.Context, next http.Handler, authConfig dynamic.BasicAu
return nil, err
}
+ if len(users) == 0 {
+ return nil, fmt.Errorf("no users found in %s", authConfig.UsersFile)
+ }
+
// To prevent timing attacks, we need to compute a hash even if the user is not found.
// We assume it to be safe only when the users hashes are all from the same algorithm,
// so we can pick the first one as a random hash to compute.
diff --git a/pkg/middlewares/auth/basic_auth_test.go b/pkg/middlewares/auth/basic_auth_test.go
index 8133461679..777617bede 100644
--- a/pkg/middlewares/auth/basic_auth_test.go
+++ b/pkg/middlewares/auth/basic_auth_test.go
@@ -16,6 +16,15 @@ import (
"github.com/traefik/traefik/v3/pkg/testhelpers"
)
+func TestNewBasicEmpty(t *testing.T) {
+ auth := dynamic.BasicAuth{
+ Users: []string{},
+ }
+
+ _, err := NewBasic(t.Context(), nil, auth, "authName")
+ require.Error(t, err)
+}
+
func TestNewBasicNotFoundSecretIsSet(t *testing.T) {
auth := dynamic.BasicAuth{
Users: []string{"test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/"},
diff --git a/pkg/middlewares/observability/semconv_test.go b/pkg/middlewares/observability/semconv_test.go
index 960d4b98af..1494e3aeb1 100644
--- a/pkg/middlewares/observability/semconv_test.go
+++ b/pkg/middlewares/observability/semconv_test.go
@@ -52,8 +52,6 @@ func TestSemConvServerMetrics(t *testing.T) {
for _, test := range tests {
t.Run(test.desc, func(t *testing.T) {
- t.Parallel()
-
var cfg otypes.OTLP
(&cfg).SetDefaults()
cfg.AddRoutersLabels = true
diff --git a/pkg/middlewares/snicheck/snicheck.go b/pkg/middlewares/snicheck/snicheck.go
index de474997f5..3be15c3b91 100644
--- a/pkg/middlewares/snicheck/snicheck.go
+++ b/pkg/middlewares/snicheck/snicheck.go
@@ -1,24 +1,26 @@
package snicheck
import (
- "net"
"net/http"
- "strings"
"github.com/rs/zerolog/log"
- "github.com/traefik/traefik/v3/pkg/middlewares/requestdecorator"
- traefiktls "github.com/traefik/traefik/v3/pkg/tls"
+ "github.com/traefik/traefik/v3/pkg/tcp"
)
// SNICheck is an HTTP handler that checks whether the TLS configuration for the server name is the same as for the host header.
type SNICheck struct {
- next http.Handler
- tlsOptionsForHost map[string]string
+ next http.Handler
+ routerName string
+ tlsOptionsName string
}
// New creates a new SNICheck.
-func New(tlsOptionsForHost map[string]string, next http.Handler) *SNICheck {
- return &SNICheck{next: next, tlsOptionsForHost: tlsOptionsForHost}
+func New(routerName, tlsOptionsName string, next http.Handler) *SNICheck {
+ return &SNICheck{
+ next: next,
+ routerName: routerName,
+ tlsOptionsName: tlsOptionsName,
+ }
}
func (s SNICheck) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
@@ -27,81 +29,16 @@ func (s SNICheck) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
return
}
- host := getHost(req)
- serverName := strings.TrimSpace(req.TLS.ServerName)
-
- // Domain Fronting
- if !strings.EqualFold(host, serverName) {
- tlsOptionHeader := findTLSOptionName(s.tlsOptionsForHost, host, true)
- tlsOptionSNI := findTLSOptionName(s.tlsOptionsForHost, serverName, false)
-
- if tlsOptionHeader != tlsOptionSNI {
- log.Debug().
- Str("host", host).
- Str("req.Host", req.Host).
- Str("req.TLS.ServerName", req.TLS.ServerName).
- Msgf("TLS options difference: SNI:%s, Header:%s", tlsOptionSNI, tlsOptionHeader)
- http.Error(rw, http.StatusText(http.StatusMisdirectedRequest), http.StatusMisdirectedRequest)
- return
- }
+ tlsOptionsNameUsed := tcp.GetTLSOptionsName(req.Context())
+ if s.tlsOptionsName != tlsOptionsNameUsed {
+ log.Debug().
+ Str("routerName", s.routerName).
+ Str("req.Host", req.Host).
+ Str("req.TLS.ServerName", req.TLS.ServerName).
+ Msgf("TLS options difference: SNI:%s, Header:%s", tlsOptionsNameUsed, s.tlsOptionsName)
+ http.Error(rw, http.StatusText(http.StatusMisdirectedRequest), http.StatusMisdirectedRequest)
+ return
}
s.next.ServeHTTP(rw, req)
}
-
-func getHost(req *http.Request) string {
- h := requestdecorator.GetCNAMEFlatten(req.Context())
- if h != "" {
- return h
- }
-
- h = requestdecorator.GetCanonicalHost(req.Context())
- if h != "" {
- return h
- }
-
- host, _, err := net.SplitHostPort(req.Host)
- if err != nil {
- host = req.Host
- }
-
- return strings.TrimSpace(host)
-}
-
-func findTLSOptionName(tlsOptionsForHost map[string]string, host string, fqdn bool) string {
- name := findTLSOptName(tlsOptionsForHost, host, fqdn)
- if name != "" {
- return name
- }
-
- name = findTLSOptName(tlsOptionsForHost, strings.ToLower(host), fqdn)
- if name != "" {
- return name
- }
-
- return traefiktls.DefaultTLSConfigName
-}
-
-func findTLSOptName(tlsOptionsForHost map[string]string, host string, fqdn bool) string {
- if tlsOptions, ok := tlsOptionsForHost[host]; ok {
- return tlsOptions
- }
-
- if !fqdn {
- return ""
- }
-
- if last := len(host) - 1; last >= 0 && host[last] == '.' {
- if tlsOptions, ok := tlsOptionsForHost[host[:last]]; ok {
- return tlsOptions
- }
-
- return ""
- }
-
- if tlsOptions, ok := tlsOptionsForHost[host+"."]; ok {
- return tlsOptions
- }
-
- return ""
-}
diff --git a/pkg/middlewares/snicheck/snicheck_test.go b/pkg/middlewares/snicheck/snicheck_test.go
deleted file mode 100644
index d7411e555e..0000000000
--- a/pkg/middlewares/snicheck/snicheck_test.go
+++ /dev/null
@@ -1,59 +0,0 @@
-package snicheck
-
-import (
- "net/http"
- "net/http/httptest"
- "testing"
-
- "github.com/stretchr/testify/assert"
-)
-
-func TestSNICheck_ServeHTTP(t *testing.T) {
- testCases := []struct {
- desc string
- tlsOptionsForHost map[string]string
- host string
- expected int
- }{
- {
- desc: "no TLS options",
- expected: http.StatusOK,
- },
- {
- desc: "with TLS options",
- tlsOptionsForHost: map[string]string{
- "example.com": "foo",
- },
- expected: http.StatusOK,
- },
- {
- desc: "server name and host doesn't have the same TLS configuration",
- tlsOptionsForHost: map[string]string{
- "example.com": "foo",
- },
- host: "example.com",
- expected: http.StatusMisdirectedRequest,
- },
- }
-
- for _, test := range testCases {
- t.Run(test.desc, func(t *testing.T) {
- t.Parallel()
-
- next := http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {})
-
- sniCheck := New(test.tlsOptionsForHost, next)
-
- req := httptest.NewRequest(http.MethodGet, "https://localhost", nil)
- if test.host != "" {
- req.Host = test.host
- }
-
- recorder := httptest.NewRecorder()
-
- sniCheck.ServeHTTP(recorder, req)
-
- assert.Equal(t, test.expected, recorder.Code)
- })
- }
-}
diff --git a/pkg/middlewares/stripprefix/strip_prefix.go b/pkg/middlewares/stripprefix/strip_prefix.go
index cc8779ce5f..2934c20613 100644
--- a/pkg/middlewares/stripprefix/strip_prefix.go
+++ b/pkg/middlewares/stripprefix/strip_prefix.go
@@ -54,6 +54,8 @@ func (s *stripPrefix) GetTracingInformation() (string, string) {
}
func (s *stripPrefix) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
+ logger := middlewares.GetLogger(req.Context(), s.name, typeName)
+
for _, prefix := range s.prefixes {
if strings.HasPrefix(req.URL.Path, prefix) {
req.URL.Path = s.getPathStripped(req.URL.Path, prefix)
@@ -64,10 +66,18 @@ func (s *stripPrefix) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
// Here we are sanitizing the URL when the path is not empty,
// as the JoinPath method is adding a leading slash if the path is empty
// to be aligned with ensureLeadingSlash behavior.
- if req.URL.Path != "" {
+ path := req.URL.Path
+ if path != "" {
req.URL = req.URL.JoinPath()
}
+ // Stop here if the normalization of the path produces a different path.
+ if path != req.URL.Path {
+ logger.Debug().Msgf("Rejecting request, sanitized path: %q is not equivalent to stripped path: %q", path, req.URL.Path)
+ http.Error(rw, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
+ return
+ }
+
req.Header.Add(ForwardedPrefixHeader, prefix)
req.RequestURI = req.URL.RequestURI()
break
diff --git a/pkg/middlewares/stripprefix/strip_prefix_test.go b/pkg/middlewares/stripprefix/strip_prefix_test.go
index 7078342c30..587c92da50 100644
--- a/pkg/middlewares/stripprefix/strip_prefix_test.go
+++ b/pkg/middlewares/stripprefix/strip_prefix_test.go
@@ -148,10 +148,7 @@ func TestStripPrefix(t *testing.T) {
Prefixes: []string{"/api"},
},
path: "/api./foo",
- expectedStatusCode: http.StatusOK,
- expectedPath: "/foo",
- expectedRawPath: "",
- expectedHeader: "/api",
+ expectedStatusCode: http.StatusBadRequest,
},
{
desc: "multiple dots in the path not stripped by the prefix",
@@ -159,10 +156,7 @@ func TestStripPrefix(t *testing.T) {
Prefixes: []string{"/api"},
},
path: "/api../foo",
- expectedStatusCode: http.StatusOK,
- expectedPath: "/foo",
- expectedRawPath: "",
- expectedHeader: "/api",
+ expectedStatusCode: http.StatusBadRequest,
},
{
desc: "multiple dots in the path not stripped by the prefix with forceSlash",
@@ -171,10 +165,7 @@ func TestStripPrefix(t *testing.T) {
ForceSlash: ptr.To(true),
},
path: "/api../foo",
- expectedStatusCode: http.StatusOK,
- expectedPath: "/foo",
- expectedRawPath: "",
- expectedHeader: "/api",
+ expectedStatusCode: http.StatusBadRequest,
},
}
@@ -204,6 +195,10 @@ func TestStripPrefix(t *testing.T) {
handler.ServeHTTP(resp, req)
assert.Equal(t, test.expectedStatusCode, resp.Code, "Unexpected status code.")
+ if test.expectedStatusCode != http.StatusOK {
+ return
+ }
+
assert.Equal(t, test.expectedPath, actualPath, "Unexpected path.")
assert.Equal(t, test.expectedRawPath, actualRawPath, "Unexpected raw path.")
assert.Equal(t, test.expectedHeader, actualHeader, "Unexpected '%s' header.", ForwardedPrefixHeader)
diff --git a/pkg/middlewares/stripprefixregex/strip_prefix_regex.go b/pkg/middlewares/stripprefixregex/strip_prefix_regex.go
index f1ee622305..83fd0f2df0 100644
--- a/pkg/middlewares/stripprefixregex/strip_prefix_regex.go
+++ b/pkg/middlewares/stripprefixregex/strip_prefix_regex.go
@@ -47,6 +47,8 @@ func (s *stripPrefixRegex) GetTracingInformation() (string, string) {
}
func (s *stripPrefixRegex) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
+ logger := middlewares.GetLogger(req.Context(), s.name, typeName)
+
for _, exp := range s.expressions {
parts := exp.FindStringSubmatch(req.URL.Path)
if len(parts) > 0 && len(parts[0]) > 0 {
@@ -65,10 +67,18 @@ func (s *stripPrefixRegex) ServeHTTP(rw http.ResponseWriter, req *http.Request)
// Here we are sanitizing the URL when the path is not empty,
// as the JoinPath method is adding a leading slash if the path is empty
// to be aligned with ensureLeadingSlash behavior.
- if req.URL.Path != "" {
+ path := req.URL.Path
+ if path != "" {
req.URL = req.URL.JoinPath()
}
+ // Stop here if the normalization of the path produces a different path.
+ if path != req.URL.Path {
+ logger.Debug().Msgf("Rejecting request, sanitized path: %q is not equivalent to stripped path: %q", path, req.URL.Path)
+ http.Error(rw, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
+ return
+ }
+
req.RequestURI = req.URL.RequestURI()
break
}
diff --git a/pkg/middlewares/stripprefixregex/strip_prefix_regex_test.go b/pkg/middlewares/stripprefixregex/strip_prefix_regex_test.go
index 97bc8269a3..5d1e78fc5b 100644
--- a/pkg/middlewares/stripprefixregex/strip_prefix_regex_test.go
+++ b/pkg/middlewares/stripprefixregex/strip_prefix_regex_test.go
@@ -201,21 +201,13 @@ func TestStripPrefixRegex(t *testing.T) {
desc: "/api./foo",
config: dynamic.StripPrefixRegex{Regex: []string{"/api"}},
path: "/api./foo",
- expectedStatusCode: http.StatusOK,
- expectedPath: "/foo",
- expectedRawPath: "",
- expectedRequestURI: "/foo",
- expectedHeader: "/api",
+ expectedStatusCode: http.StatusBadRequest,
},
{
desc: "/api../foo",
config: dynamic.StripPrefixRegex{Regex: []string{"/api"}},
path: "/api../foo",
- expectedStatusCode: http.StatusOK,
- expectedPath: "/foo",
- expectedRawPath: "",
- expectedRequestURI: "/foo",
- expectedHeader: "/api",
+ expectedStatusCode: http.StatusBadRequest,
},
}
diff --git a/pkg/muxer/tcp/matcher_v2_test.go b/pkg/muxer/tcp/matcher_v2_test.go
index 9f28139a84..362adcd49a 100644
--- a/pkg/muxer/tcp/matcher_v2_test.go
+++ b/pkg/muxer/tcp/matcher_v2_test.go
@@ -491,7 +491,7 @@ func Test_addTCPRouteV2(t *testing.T) {
remoteAddr: fakeAddr{addr: addr},
}
- connData, err := NewConnData(test.serverName, conn, test.protos)
+ connData, err := NewConnData(test.serverName, conn.RemoteAddr(), test.protos)
require.NoError(t, err)
matchingHandler, _ := router.Match(connData)
diff --git a/pkg/muxer/tcp/mux.go b/pkg/muxer/tcp/mux.go
index 083c63a8cb..08436c5bb6 100644
--- a/pkg/muxer/tcp/mux.go
+++ b/pkg/muxer/tcp/mux.go
@@ -22,10 +22,10 @@ type ConnData struct {
}
// NewConnData builds a connData struct from the given parameters.
-func NewConnData(serverName string, conn tcp.WriteCloser, alpnProtos []string) (ConnData, error) {
- remoteIP, _, err := net.SplitHostPort(conn.RemoteAddr().String())
+func NewConnData(serverName string, remoteAddr net.Addr, alpnProtos []string) (ConnData, error) {
+ remoteIP, _, err := net.SplitHostPort(remoteAddr.String())
if err != nil {
- return ConnData{}, fmt.Errorf("error while parsing remote address %q: %w", conn.RemoteAddr().String(), err)
+ return ConnData{}, fmt.Errorf("parsing remote address %q: %w", remoteAddr.String(), err)
}
return ConnData{
diff --git a/pkg/muxer/tcp/mux_test.go b/pkg/muxer/tcp/mux_test.go
index 708ffb7f32..ea1759df3f 100644
--- a/pkg/muxer/tcp/mux_test.go
+++ b/pkg/muxer/tcp/mux_test.go
@@ -293,7 +293,7 @@ func Test_addTCPRoute(t *testing.T) {
remoteAddr: fakeAddr{addr: addr},
}
- connData, err := NewConnData(test.serverName, conn, test.protos)
+ connData, err := NewConnData(test.serverName, conn.RemoteAddr(), test.protos)
require.NoError(t, err)
matchingHandler, _ := router.Match(connData)
diff --git a/pkg/provider/file/file.go b/pkg/provider/file/file.go
index 96c2942884..55d44023a1 100644
--- a/pkg/provider/file/file.go
+++ b/pkg/provider/file/file.go
@@ -72,9 +72,16 @@ func (p *Provider) Provide(configurationChan chan<- dynamic.Message, pool *safe.
// ignore sub-dir
continue
}
+ if !isFileSupported(entry.Name()) {
+ // ignore unsupported file extension
+ continue
+ }
watchItems = append(watchItems, path.Join(p.Directory, entry.Name()))
}
case len(p.Filename) > 0:
+ if !isFileSupported(p.Filename) {
+ return fmt.Errorf("unsupported file extension for file %s", p.Filename)
+ }
watchItems = append(watchItems, filepath.Dir(p.Filename), p.Filename)
default:
return errors.New("error using file configuration provider, neither filename nor directory is defined")
@@ -169,7 +176,7 @@ func (p *Provider) addWatcher(pool *safe.Pool, items []string, configurationChan
log.Debug().Msgf("add watcher on: %s", item)
err = watcher.Add(item)
if err != nil {
- return fmt.Errorf("error adding file watcher: %w", err)
+ return fmt.Errorf("error adding file watcher for %s: %w", item, err)
}
}
@@ -392,6 +399,8 @@ func (p *Provider) collectFileConfigs(ctx context.Context, directory, prefix str
}
for _, item := range fileList {
+ logger := log.Ctx(ctx).With().Str("filename", item.Name()).Logger()
+
itemPath := filepath.Join(directory, item.Name())
filename := item.Name()
if prefix != "" {
@@ -407,10 +416,8 @@ func (p *Provider) collectFileConfigs(ctx context.Context, directory, prefix str
continue
}
- switch strings.ToLower(filepath.Ext(item.Name())) {
- case ".toml", ".yaml", ".yml":
- // noop
- default:
+ if !isFileSupported(item.Name()) {
+ logger.Debug().Msg("Skipping file, unsupported extension")
continue
}
@@ -500,3 +507,12 @@ func readFile(filename string) (string, error) {
}
return "", fmt.Errorf("invalid filename: %s", filename)
}
+
+func isFileSupported(filename string) bool {
+ switch strings.ToLower(filepath.Ext(filename)) {
+ case ".toml", ".yaml", ".yml":
+ return true
+ default:
+ return false
+ }
+}
diff --git a/pkg/provider/file/file_test.go b/pkg/provider/file/file_test.go
index 3aebe1908e..e89e504e3a 100644
--- a/pkg/provider/file/file_test.go
+++ b/pkg/provider/file/file_test.go
@@ -197,6 +197,38 @@ func TestProvideWithWatch(t *testing.T) {
}
}
+func TestProvideWatchWithNonConfigDanglingSymlink(t *testing.T) {
+ tempDir := t.TempDir()
+
+ err := copyFile("./fixtures/yaml/simple_file_01.yml", filepath.Join(tempDir, "simple_file_01.yml"))
+ require.NoError(t, err)
+
+ err = os.Symlink(filepath.Join(tempDir, "non_existent_file.txt"), filepath.Join(tempDir, "dangling_symlink.txt"))
+ require.NoError(t, err)
+
+ provider := &Provider{
+ Directory: tempDir,
+ Watch: true,
+ }
+ configChan := make(chan dynamic.Message)
+ go func() {
+ err := provider.Provide(configChan, safe.NewPool(t.Context()))
+ assert.NoError(t, err)
+ }()
+
+ timeout := time.After(time.Second)
+ select {
+ case conf := <-configChan:
+ require.NotNil(t, conf.Configuration.HTTP)
+ numServices := len(conf.Configuration.HTTP.Services) + len(conf.Configuration.TCP.Services) + len(conf.Configuration.UDP.Services)
+ numRouters := len(conf.Configuration.HTTP.Routers) + len(conf.Configuration.TCP.Routers) + len(conf.Configuration.UDP.Routers)
+ assert.Equal(t, 6, numServices)
+ assert.Equal(t, 3, numRouters)
+ case <-timeout:
+ t.Errorf("timeout while waiting for config")
+ }
+}
+
func getTestCases() []ProvideTestCase {
return []ProvideTestCase{
{
diff --git a/pkg/provider/kubernetes/gateway/client.go b/pkg/provider/kubernetes/gateway/client.go
index 4c1652553a..35603e4476 100644
--- a/pkg/provider/kubernetes/gateway/client.go
+++ b/pkg/provider/kubernetes/gateway/client.go
@@ -49,7 +49,10 @@ type clientWrapper struct {
experimentalChannel bool
}
-func createClientFromConfig(c *rest.Config) (*clientWrapper, error) {
+func createClientFromConfig(c *rest.Config, qps, burst int) (*clientWrapper, error) {
+ c.QPS = float32(qps)
+ c.Burst = burst
+
csGateway, err := gateclientset.NewForConfig(c)
if err != nil {
return nil, err
@@ -75,7 +78,7 @@ func newClientImpl(csKube kclientset.Interface, csGateway gateclientset.Interfac
// newInClusterClient returns a new Provider client that is expected to run
// inside the cluster.
-func newInClusterClient(endpoint string) (*clientWrapper, error) {
+func newInClusterClient(endpoint string, qps, burst int) (*clientWrapper, error) {
config, err := rest.InClusterConfig()
if err != nil {
return nil, fmt.Errorf("failed to create in-cluster configuration: %w", err)
@@ -85,20 +88,20 @@ func newInClusterClient(endpoint string) (*clientWrapper, error) {
config.Host = endpoint
}
- return createClientFromConfig(config)
+ return createClientFromConfig(config, qps, burst)
}
-func newExternalClusterClientFromFile(file string) (*clientWrapper, error) {
+func newExternalClusterClientFromFile(file string, qps, burst int) (*clientWrapper, error) {
configFromFlags, err := clientcmd.BuildConfigFromFlags("", file)
if err != nil {
return nil, err
}
- return createClientFromConfig(configFromFlags)
+ return createClientFromConfig(configFromFlags, qps, burst)
}
// newExternalClusterClient returns a new Provider client that may run outside of the cluster.
// The endpoint parameter must not be empty.
-func newExternalClusterClient(endpoint, caFilePath string, token types.FileOrContent) (*clientWrapper, error) {
+func newExternalClusterClient(endpoint, caFilePath string, token types.FileOrContent, qps, burst int) (*clientWrapper, error) {
if endpoint == "" {
return nil, errors.New("endpoint missing for external cluster client")
}
@@ -122,7 +125,7 @@ func newExternalClusterClient(endpoint, caFilePath string, token types.FileOrCon
config.TLSClientConfig = rest.TLSClientConfig{CAData: caData}
}
- return createClientFromConfig(config)
+ return createClientFromConfig(config, qps, burst)
}
// WatchAll starts namespace-specific controllers for all relevant kinds.
diff --git a/pkg/provider/kubernetes/gateway/kubernetes.go b/pkg/provider/kubernetes/gateway/kubernetes.go
index 629178731f..235fa4e9ec 100644
--- a/pkg/provider/kubernetes/gateway/kubernetes.go
+++ b/pkg/provider/kubernetes/gateway/kubernetes.go
@@ -65,6 +65,8 @@ const (
type Provider struct {
Endpoint string `description:"Kubernetes server endpoint (required for external cluster client)." json:"endpoint,omitempty" toml:"endpoint,omitempty" yaml:"endpoint,omitempty"`
Token types.FileOrContent `description:"Kubernetes bearer token (not needed for in-cluster client). It accepts either a token value or a file path to the token." json:"token,omitempty" toml:"token,omitempty" yaml:"token,omitempty" loggable:"false"`
+ QPS int `description:"Defines the maximum QPS to the Kubernetes API server. Setting this to a negative value will disable client-side ratelimiting." json:"qps,omitempty" toml:"qps,omitempty" yaml:"qps,omitempty" export:"true"`
+ Burst int `description:"Defines the maximum burst of requests to the Kubernetes API server." json:"burst,omitempty" toml:"burst,omitempty" yaml:"burst,omitempty" export:"true"`
CertAuthFilePath string `description:"Kubernetes certificate authority file path (not needed for in-cluster client)." json:"certAuthFilePath,omitempty" toml:"certAuthFilePath,omitempty" yaml:"certAuthFilePath,omitempty"`
Namespaces []string `description:"Kubernetes namespaces." json:"namespaces,omitempty" toml:"namespaces,omitempty" yaml:"namespaces,omitempty" export:"true"`
LabelSelector string `description:"Kubernetes label selector to select specific GatewayClasses." json:"labelSelector,omitempty" toml:"labelSelector,omitempty" yaml:"labelSelector,omitempty" export:"true"`
@@ -86,6 +88,11 @@ type Provider struct {
client *clientWrapper
}
+func (p *Provider) SetDefaults() {
+ p.QPS = 50 // the default value for the QPS is 10x the default Kubernetes client QPS value.
+ p.Burst = 100 // the default value for the Burst is 10x the default Kubernetes client Burst value.
+}
+
// Entrypoint defines the available entry points.
type Entrypoint struct {
Address string
@@ -275,13 +282,13 @@ func (p *Provider) newK8sClient(ctx context.Context) (*clientWrapper, error) {
switch {
case os.Getenv("KUBERNETES_SERVICE_HOST") != "" && os.Getenv("KUBERNETES_SERVICE_PORT") != "":
logger.Info().Str("endpoint", p.Endpoint).Msg("Creating in-cluster Provider client")
- client, err = newInClusterClient(p.Endpoint)
+ client, err = newInClusterClient(p.Endpoint, p.QPS, p.Burst)
case os.Getenv("KUBECONFIG") != "":
logger.Info().Msgf("Creating cluster-external Provider client from KUBECONFIG %s", os.Getenv("KUBECONFIG"))
- client, err = newExternalClusterClientFromFile(os.Getenv("KUBECONFIG"))
+ client, err = newExternalClusterClientFromFile(os.Getenv("KUBECONFIG"), p.QPS, p.Burst)
default:
logger.Info().Str("endpoint", p.Endpoint).Msg("Creating cluster-external Provider client")
- client, err = newExternalClusterClient(p.Endpoint, p.CertAuthFilePath, p.Token)
+ client, err = newExternalClusterClient(p.Endpoint, p.CertAuthFilePath, p.Token, p.QPS, p.Burst)
}
if err != nil {
diff --git a/pkg/provider/kubernetes/ingress/fixtures/Ingress-with-invalid-pathmatcher-annotation.yml b/pkg/provider/kubernetes/ingress/fixtures/Ingress-with-invalid-pathmatcher-annotation.yml
new file mode 100644
index 0000000000..24b310e027
--- /dev/null
+++ b/pkg/provider/kubernetes/ingress/fixtures/Ingress-with-invalid-pathmatcher-annotation.yml
@@ -0,0 +1,50 @@
+---
+kind: Ingress
+apiVersion: networking.k8s.io/v1
+metadata:
+ name: ""
+ namespace: testing
+ annotations:
+ traefik.ingress.kubernetes.io/router.pathmatcher: 'Host("injection") || PathPrefix'
+spec:
+ rules:
+ - http:
+ paths:
+ - path: /bar
+ pathType: ImplementationSpecific
+ backend:
+ service:
+ name: service1
+ port:
+ number: 80
+
+---
+kind: Service
+apiVersion: v1
+metadata:
+ name: service1
+ namespace: testing
+
+spec:
+ ports:
+ - port: 80
+ clusterIP: 10.0.0.1
+
+---
+kind: EndpointSlice
+apiVersion: discovery.k8s.io/v1
+metadata:
+ name: service1
+ namespace: testing
+ labels:
+ kubernetes.io/service-name: service1
+
+addressType: IPv4
+ports:
+ - port: 8080
+ name: ""
+endpoints:
+ - addresses:
+ - 10.10.0.1
+ conditions:
+ ready: true
diff --git a/pkg/provider/kubernetes/ingress/kubernetes.go b/pkg/provider/kubernetes/ingress/kubernetes.go
index 0552956811..439941dd76 100644
--- a/pkg/provider/kubernetes/ingress/kubernetes.go
+++ b/pkg/provider/kubernetes/ingress/kubernetes.go
@@ -395,7 +395,14 @@ func (p *Provider) loadConfigurationFromIngresses(ctx context.Context, client Cl
serviceName := provider.Normalize(ingress.Namespace + "-" + pa.Backend.Service.Name + "-" + portString(pa.Backend.Service.Port))
conf.HTTP.Services[serviceName] = service
- rt := p.loadRouter(ingress, rule, pa, rtConfig, serviceName)
+ rt, err := p.loadRouter(ingress, rule, pa, rtConfig, serviceName)
+ if err != nil {
+ logger.Error().Err(err).
+ Str("serviceName", pa.Backend.Service.Name).
+ Str("path", pa.Path).
+ Msg("Skipping path.")
+ continue
+ }
p.applyRouterTransform(ctxIngress, rt, ingress)
@@ -721,7 +728,7 @@ func (p *Provider) loadService(client Client, namespace string, backend netv1.In
return svc, nil
}
-func (p *Provider) loadRouter(ingress *netv1.Ingress, rule netv1.IngressRule, pa netv1.HTTPIngressPath, rtConfig *RouterConfig, serviceName string) *dynamic.Router {
+func (p *Provider) loadRouter(ingress *netv1.Ingress, rule netv1.IngressRule, pa netv1.HTTPIngressPath, rtConfig *RouterConfig, serviceName string) (*dynamic.Router, error) {
rt := &dynamic.Router{
Service: serviceName,
Observability: &dynamic.RouterObservabilityConfig{
@@ -765,6 +772,12 @@ func (p *Provider) loadRouter(ingress *netv1.Ingress, rule netv1.IngressRule, pa
if pa.PathType == nil || *pa.PathType == "" || *pa.PathType == netv1.PathTypeImplementationSpecific {
if rtConfig != nil && rtConfig.Router != nil && rtConfig.Router.PathMatcher != "" {
+ switch rtConfig.Router.PathMatcher {
+ case "Path", "PathPrefix", "PathRegexp":
+ default:
+ return nil, fmt.Errorf("invalid router path matcher %q: must be one of Path, PathPrefix, PathRegexp", rtConfig.Router.PathMatcher)
+ }
+
matcher = rtConfig.Router.PathMatcher
}
} else if *pa.PathType == netv1.PathTypeExact {
@@ -775,7 +788,7 @@ func (p *Provider) loadRouter(ingress *netv1.Ingress, rule netv1.IngressRule, pa
}
rt.Rule = strings.Join(rules, " && ")
- return rt
+ return rt, nil
}
func buildHostRuleV2(host string) string {
diff --git a/pkg/provider/kubernetes/ingress/kubernetes_test.go b/pkg/provider/kubernetes/ingress/kubernetes_test.go
index 44b8a3a7d2..8adcf6d7a1 100644
--- a/pkg/provider/kubernetes/ingress/kubernetes_test.go
+++ b/pkg/provider/kubernetes/ingress/kubernetes_test.go
@@ -2170,6 +2170,31 @@ func TestLoadConfigurationFromIngresses(t *testing.T) {
},
strictPrefixMatching: true,
},
+ {
+ desc: "Ingress with invalid pathmatcher annotation",
+ expected: &dynamic.Configuration{
+ HTTP: &dynamic.HTTPConfiguration{
+ Middlewares: map[string]*dynamic.Middleware{},
+ Routers: map[string]*dynamic.Router{},
+ Services: map[string]*dynamic.Service{
+ "testing-service1-80": {
+ LoadBalancer: &dynamic.ServersLoadBalancer{
+ Strategy: dynamic.BalancerStrategyWRR,
+ PassHostHeader: pointer(true),
+ ResponseForwarding: &dynamic.ResponseForwarding{
+ FlushInterval: ptypes.Duration(100 * time.Millisecond),
+ },
+ Servers: []dynamic.Server{
+ {
+ URL: "http://10.10.0.1:8080",
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
}
for _, test := range testCases {
diff --git a/pkg/proxy/httputil/observability_test.go b/pkg/proxy/httputil/observability_test.go
index c0ef0e7a3b..117b589bbe 100644
--- a/pkg/proxy/httputil/observability_test.go
+++ b/pkg/proxy/httputil/observability_test.go
@@ -57,8 +57,6 @@ func TestObservabilityRoundTripper_metrics(t *testing.T) {
for _, test := range tests {
t.Run(test.desc, func(t *testing.T) {
- t.Parallel()
-
var cfg otypes.OTLP
(&cfg).SetDefaults()
cfg.AddRoutersLabels = true
diff --git a/pkg/server/aggregator.go b/pkg/server/aggregator.go
index 36326af763..652db41352 100644
--- a/pkg/server/aggregator.go
+++ b/pkg/server/aggregator.go
@@ -1,16 +1,18 @@
package server
import (
+ "context"
"slices"
"strings"
"github.com/go-acme/lego/v4/challenge/tlsalpn01"
"github.com/rs/zerolog/log"
"github.com/traefik/traefik/v3/pkg/config/dynamic"
+ httpmuxer "github.com/traefik/traefik/v3/pkg/muxer/http"
"github.com/traefik/traefik/v3/pkg/observability/logs"
otypes "github.com/traefik/traefik/v3/pkg/observability/types"
"github.com/traefik/traefik/v3/pkg/server/provider"
- "github.com/traefik/traefik/v3/pkg/tls"
+ traefiktls "github.com/traefik/traefik/v3/pkg/tls"
)
func mergeConfiguration(configurations dynamic.Configurations, defaultEntryPoints []string) dynamic.Configuration {
@@ -36,8 +38,8 @@ func mergeConfiguration(configurations dynamic.Configurations, defaultEntryPoint
Services: make(map[string]*dynamic.UDPService),
},
TLS: &dynamic.TLSConfiguration{
- Stores: make(map[string]tls.Store),
- Options: make(map[string]tls.Options),
+ Stores: make(map[string]traefiktls.Store),
+ Options: make(map[string]traefiktls.Options),
},
}
@@ -134,7 +136,7 @@ func mergeConfiguration(configurations dynamic.Configurations, defaultEntryPoint
}
for key, store := range configuration.TLS.Stores {
- if key != tls.DefaultTLSStoreName {
+ if key != traefiktls.DefaultTLSStoreName {
key = provider.MakeQualifiedName(pvd, key)
} else {
defaultTLSStoreProviders = append(defaultTLSStoreProviders, pvd)
@@ -156,19 +158,95 @@ func mergeConfiguration(configurations dynamic.Configurations, defaultEntryPoint
if len(defaultTLSStoreProviders) > 1 {
log.Error().Msgf("Default TLS Store defined in multiple providers: %v", defaultTLSStoreProviders)
- delete(conf.TLS.Stores, tls.DefaultTLSStoreName)
+ delete(conf.TLS.Stores, traefiktls.DefaultTLSStoreName)
}
if len(defaultTLSOptionProviders) == 0 {
- conf.TLS.Options[tls.DefaultTLSConfigName] = tls.DefaultTLSOptions
+ conf.TLS.Options[traefiktls.DefaultTLSConfigName] = traefiktls.DefaultTLSOptions
} else if len(defaultTLSOptionProviders) > 1 {
log.Error().Msgf("Default TLS Options defined in multiple providers %v", defaultTLSOptionProviders)
// We do not set an empty tls.TLS{} as above so that we actually get a "cascading failure" later on,
// i.e. routers depending on this missing TLS option will fail to initialize as well.
- delete(conf.TLS.Options, tls.DefaultTLSConfigName)
+ delete(conf.TLS.Options, traefiktls.DefaultTLSConfigName)
}
- return conf
+ return resolveHTTPTLSOptions(conf)
+}
+
+func resolveHTTPTLSOptions(cfg dynamic.Configuration) dynamic.Configuration {
+ if cfg.HTTP == nil || len(cfg.HTTP.Routers) == 0 {
+ return cfg
+ }
+
+ rts := make(map[string]*dynamic.Router)
+
+ // Keyed by domain, then by options reference.
+ // The actual source of truth for what TLS options will actually be used for the connection.
+ // As opposed to tlsOptionsForHost, it keeps track of all the (different) TLS
+ // options that occur for a given host name, so that later on we can set relevant
+ // errors and logging for all the routers concerned (i.e. wrongly configured).
+ tlsOptionsForHostSNI := map[string]map[string][]string{}
+
+ for routerHTTPName, routerHTTPConfig := range cfg.HTTP.Routers {
+ rts[routerHTTPName] = routerHTTPConfig.DeepCopy()
+
+ if routerHTTPConfig.TLS == nil {
+ continue
+ }
+
+ ctxRouter := provider.AddInContext(context.Background(), routerHTTPName)
+ logger := log.Ctx(ctxRouter).With().Str(logs.RouterName, routerHTTPName).Logger()
+
+ tlsOptionsName := traefiktls.DefaultTLSConfigName
+ if len(routerHTTPConfig.TLS.Options) > 0 && routerHTTPConfig.TLS.Options != traefiktls.DefaultTLSConfigName {
+ tlsOptionsName = provider.GetQualifiedName(ctxRouter, routerHTTPConfig.TLS.Options)
+ }
+
+ domains, err := httpmuxer.ParseDomains(routerHTTPConfig.Rule)
+ if err != nil {
+ logger.Error().Err(err).Msgf("Invalid rule %s", routerHTTPConfig.Rule)
+ continue
+ }
+
+ if len(domains) == 0 {
+ rts[routerHTTPName].TLS.ResolvedOptions = "default"
+ logger.Warn().Msgf("No domain found in rule %v, the TLS options applied for this router will depend on the SNI of each request", routerHTTPConfig.Rule)
+ }
+
+ for _, domain := range domains {
+ // domain is already in lower case thanks to the domain parsing
+ if tlsOptionsForHostSNI[domain] == nil {
+ tlsOptionsForHostSNI[domain] = make(map[string][]string)
+ }
+ tlsOptionsForHostSNI[domain][tlsOptionsName] = append(tlsOptionsForHostSNI[domain][tlsOptionsName], routerHTTPName)
+ }
+ }
+
+ for hostSNI, tlsConfigs := range tlsOptionsForHostSNI {
+ if len(tlsConfigs) == 1 {
+ for optionsName, v := range tlsConfigs {
+ log.Debug().Msgf("Adding route for %s with TLS options %s", hostSNI, optionsName)
+ for _, s := range v {
+ rts[s].TLS.ResolvedOptions = optionsName
+ }
+ }
+ continue
+ }
+
+ // multiple tlsConfigs
+ routers := make([]string, 0, len(tlsConfigs))
+ for _, v := range tlsConfigs {
+ for _, s := range v {
+ rts[s].TLS.ResolvedOptions = traefiktls.DefaultTLSConfigName
+ routers = append(routers, s)
+ }
+ }
+
+ log.Warn().Msgf("Found different TLS options for routers on the same host %v, so using the default TLS options instead for these routers: %#v", hostSNI, routers)
+ }
+
+ cfg.HTTP.Routers = rts
+ return cfg
}
func applyModel(cfg dynamic.Configuration) dynamic.Configuration {
diff --git a/pkg/server/router/router.go b/pkg/server/router/router.go
index 718568b278..9d5623fadd 100644
--- a/pkg/server/router/router.go
+++ b/pkg/server/router/router.go
@@ -19,6 +19,7 @@ import (
metricsMiddle "github.com/traefik/traefik/v3/pkg/middlewares/metrics"
"github.com/traefik/traefik/v3/pkg/middlewares/observability"
"github.com/traefik/traefik/v3/pkg/middlewares/recovery"
+ "github.com/traefik/traefik/v3/pkg/middlewares/snicheck"
httpmuxer "github.com/traefik/traefik/v3/pkg/muxer/http"
"github.com/traefik/traefik/v3/pkg/observability/logs"
"github.com/traefik/traefik/v3/pkg/server/middleware"
@@ -375,6 +376,12 @@ func (m *Manager) buildHTTPHandler(ctx context.Context, router *runtime.RouterIn
})
}
+ if router.TLS != nil {
+ chain = chain.Append(func(next http.Handler) (http.Handler, error) {
+ return snicheck.New(routerName, router.TLS.ResolvedOptions, next), nil
+ })
+ }
+
mHandler := m.middlewaresBuilder.BuildMiddlewareChain(ctx, router.Middlewares)
return chain.Extend(*mHandler).Then(nextHandler)
diff --git a/pkg/server/router/tcp/manager.go b/pkg/server/router/tcp/manager.go
index a580644b02..8ec5c5fa56 100644
--- a/pkg/server/router/tcp/manager.go
+++ b/pkg/server/router/tcp/manager.go
@@ -2,7 +2,6 @@ package tcp
import (
"context"
- "crypto/tls"
"errors"
"fmt"
"math"
@@ -11,7 +10,6 @@ import (
"github.com/rs/zerolog/log"
"github.com/traefik/traefik/v3/pkg/config/runtime"
- "github.com/traefik/traefik/v3/pkg/middlewares/snicheck"
"github.com/traefik/traefik/v3/pkg/muxer"
httpmuxer "github.com/traefik/traefik/v3/pkg/muxer/http"
tcpmuxer "github.com/traefik/traefik/v3/pkg/muxer/tcp"
@@ -97,11 +95,6 @@ func (m *Manager) getHTTPRouters(ctx context.Context, entryPoints []string, tls
return make(map[string]map[string]*runtime.RouterInfo)
}
-type nameAndConfig struct {
- routerName string // just so we have it as additional information when logging
- TLSConfig *tls.Config
-}
-
func (m *Manager) buildEntryPointHandler(ctx context.Context, configs map[string]*runtime.TCPRouterInfo, configsHTTP map[string]*runtime.RouterInfo, handlerHTTP, handlerHTTPS http.Handler) (*Router, error) {
// Build a new Router.
router, err := NewRouter(m.providersPrecedence)
@@ -119,18 +112,6 @@ func (m *Manager) buildEntryPointHandler(ctx context.Context, configs map[string
log.Ctx(ctx).Error().Err(err).Msg("Error during the build of the default TLS configuration")
}
- // Keyed by domain. The source of truth for doing SNI checking (domain fronting).
- // As soon as there's (at least) two different tlsOptions found for the same domain,
- // we set the value to the default TLS conf.
- tlsOptionsForHost := map[string]string{}
-
- // Keyed by domain, then by options reference.
- // The actual source of truth for what TLS options will actually be used for the connection.
- // As opposed to tlsOptionsForHost, it keeps track of all the (different) TLS
- // options that occur for a given host name, so that later on we can set relevant
- // errors and logging for all the routers concerned (i.e. wrongly configured).
- tlsOptionsForHostSNI := map[string]map[string]nameAndConfig{}
-
for routerHTTPName, routerHTTPConfig := range configsHTTP {
if routerHTTPConfig.TLS == nil {
continue
@@ -139,6 +120,8 @@ func (m *Manager) buildEntryPointHandler(ctx context.Context, configs map[string
logger := log.Ctx(ctx).With().Str(logs.RouterName, routerHTTPName).Logger()
ctxRouter := logger.WithContext(provider.AddInContext(ctx, routerHTTPName))
+ // Even if the TLS options mismatch between the configured and the resolved one is handled in the aggregator
+ // we also have to handle it here to be able to mark the router in error.
tlsOptionsName := traefiktls.DefaultTLSConfigName
if len(routerHTTPConfig.TLS.Options) > 0 && routerHTTPConfig.TLS.Options != traefiktls.DefaultTLSConfigName {
tlsOptionsName = provider.GetQualifiedName(ctxRouter, routerHTTPConfig.TLS.Options)
@@ -158,7 +141,7 @@ func (m *Manager) buildEntryPointHandler(ctx context.Context, configs map[string
// This is only about choosing the TLS configuration.
// The actual routing will be done further on by the HTTPS handler.
// See examples below.
- router.AddHTTPTLSConfig("*", defaultTLSConf)
+ router.AddHTTPTLSConfig("*", defaultTLSConf, traefiktls.DefaultTLSConfigName)
// The server name (from a Host(SNI) rule) is the only parameter (available in HTTP routing rules) on which we can map a TLS config,
// because it is the only one accessible before decryption (we obtain it during the ClientHello).
@@ -188,79 +171,36 @@ func (m *Manager) buildEntryPointHandler(ctx context.Context, configs map[string
}
}
+ if routerHTTPConfig.TLS.ResolvedOptions != tlsOptionsName {
+ routerHTTPConfig.AddError(errors.New("found different TLS options for routers on the same host, so using the default TLS options instead"), false)
+ }
+
// Even though the error is seemingly ignored (aside from logging it),
// we actually rely later on the fact that a tls config is nil (which happens when an error is returned) to take special steps
// when assigning a handler to a route.
- tlsConf, tlsConfErr := m.tlsManager.Get(traefiktls.DefaultTLSStoreName, tlsOptionsName)
+ tlsConf, tlsConfErr := m.tlsManager.Get(traefiktls.DefaultTLSStoreName, routerHTTPConfig.TLS.ResolvedOptions)
if tlsConfErr != nil {
// Note: we do not call AddError here because we already did so when buildRouterHandler errored for the same reason.
logger.Error().Err(tlsConfErr).Send()
}
for _, domain := range domains {
- // domain is already in lower case thanks to the domain parsing
- if tlsOptionsForHostSNI[domain] == nil {
- tlsOptionsForHostSNI[domain] = make(map[string]nameAndConfig)
- }
- tlsOptionsForHostSNI[domain][tlsOptionsName] = nameAndConfig{
- routerName: routerHTTPName,
- TLSConfig: tlsConf,
- }
-
- if name, ok := tlsOptionsForHost[domain]; ok && name != tlsOptionsName {
- // Different tlsOptions on the same domain, so fallback to default
- tlsOptionsForHost[domain] = traefiktls.DefaultTLSConfigName
- } else {
- tlsOptionsForHost[domain] = tlsOptionsName
- }
- }
- }
-
- sniCheck := snicheck.New(tlsOptionsForHost, handlerHTTPS)
-
- // Keep in mind that defaultTLSConf might be nil here.
- router.SetHTTPSHandler(sniCheck, defaultTLSConf)
-
- logger := log.Ctx(ctx)
- for hostSNI, tlsConfigs := range tlsOptionsForHostSNI {
- if len(tlsConfigs) == 1 {
- var optionsName string
- var config *tls.Config
- for k, v := range tlsConfigs {
- optionsName = k
- config = v.TLSConfig
- break
- }
-
- if config == nil {
+ if tlsConf == nil {
// we use nil config as a signal to insert a handler
// that enforces that TLS connection attempts to the corresponding (broken) router should fail.
- logger.Debug().Msgf("Adding special closing route for %s because broken TLS options %s", hostSNI, optionsName)
- router.AddHTTPTLSConfig(hostSNI, nil)
+ logger.Debug().Msgf("Adding special closing route for %s because of a broken TLS options %s", domain, routerHTTPConfig.TLS.ResolvedOptions)
+ router.AddHTTPTLSConfig(domain, nil, "")
continue
}
- logger.Debug().Msgf("Adding route for %s with TLS options %s", hostSNI, optionsName)
- router.AddHTTPTLSConfig(hostSNI, config)
- continue
+ logger.Debug().Msgf("Adding route for %s with TLS options %s", domain, routerHTTPConfig.TLS.ResolvedOptions)
+ router.AddHTTPTLSConfig(domain, tlsConf, routerHTTPConfig.TLS.ResolvedOptions)
}
-
- // multiple tlsConfigs
-
- routers := make([]string, 0, len(tlsConfigs))
- for _, v := range tlsConfigs {
- configsHTTP[v.routerName].AddError(fmt.Errorf("found different TLS options for routers on the same host %v, so using the default TLS options instead", hostSNI), false)
- routers = append(routers, v.routerName)
- }
-
- logger.Warn().Msgf("Found different TLS options for routers on the same host %v, so using the default TLS options instead for these routers: %#v", hostSNI, routers)
- if defaultTLSConf == nil {
- logger.Debug().Msgf("Adding special closing route for %s because broken default TLS options", hostSNI)
- }
-
- router.AddHTTPTLSConfig(hostSNI, defaultTLSConf)
}
+ // Keep in mind that defaultTLSConf might be nil here.
+ router.SetHTTPSHandler(handlerHTTPS, defaultTLSConf)
+
m.addTCPHandlers(ctx, configs, router)
return router, nil
@@ -399,8 +339,9 @@ func (m *Manager) addTCPHandlers(ctx context.Context, configs map[string]*runtim
}
handler = &tcp.TLSHandler{
- Next: handler,
- Config: tlsConf,
+ Next: handler,
+ Config: tlsConf,
+ TLSOptionsName: tlsOptionsName,
}
logger.Debug().Msgf("Adding TLS route for %q", routerConfig.Rule)
diff --git a/pkg/server/router/tcp/manager_test.go b/pkg/server/router/tcp/manager_test.go
index adcff5ef04..50301b3546 100644
--- a/pkg/server/router/tcp/manager_test.go
+++ b/pkg/server/router/tcp/manager_test.go
@@ -1,14 +1,10 @@
package tcp
import (
- "crypto/tls"
"math"
- "net/http"
- "net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
"github.com/traefik/traefik/v3/pkg/config/dynamic"
"github.com/traefik/traefik/v3/pkg/config/runtime"
tcpmiddleware "github.com/traefik/traefik/v3/pkg/server/middleware/tcp"
@@ -130,7 +126,8 @@ func TestRuntimeConfiguration(t *testing.T) {
Service: "foo-service",
Rule: "Host(`bar.foo`)",
TLS: &dynamic.RouterTLSConfig{
- Options: "foo",
+ Options: "foo",
+ ResolvedOptions: "default",
},
},
},
@@ -140,7 +137,8 @@ func TestRuntimeConfiguration(t *testing.T) {
Service: "foo-service",
Rule: "Host(`bar.foo`) && PathPrefix(`/path`)",
TLS: &dynamic.RouterTLSConfig{
- Options: "bar",
+ Options: "bar",
+ ResolvedOptions: "default",
},
},
},
@@ -399,293 +397,3 @@ func TestRuntimeConfiguration(t *testing.T) {
})
}
}
-
-func TestDomainFronting(t *testing.T) {
- tlsOptionsBase := map[string]traefiktls.Options{
- "default": {
- MinVersion: "VersionTLS10",
- },
- "host1@file": {
- MinVersion: "VersionTLS12",
- },
- "host1@crd": {
- MinVersion: "VersionTLS12",
- },
- }
-
- entryPoints := []string{"web"}
-
- tests := []struct {
- desc string
- routers map[string]*runtime.RouterInfo
- tlsOptions map[string]traefiktls.Options
- host string
- ServerName string
- expectedStatus int
- }{
- {
- desc: "Request is misdirected when TLS options are different",
- routers: map[string]*runtime.RouterInfo{
- "router-1@file": {
- Router: &dynamic.Router{
- EntryPoints: entryPoints,
- Rule: "Host(`host1.local`)",
- TLS: &dynamic.RouterTLSConfig{
- Options: "host1",
- },
- },
- },
- "router-2@file": {
- Router: &dynamic.Router{
- EntryPoints: entryPoints,
- Rule: "Host(`host2.local`)",
- TLS: &dynamic.RouterTLSConfig{},
- },
- },
- },
- tlsOptions: tlsOptionsBase,
- host: "host1.local",
- ServerName: "host2.local",
- expectedStatus: http.StatusMisdirectedRequest,
- },
- {
- desc: "Request is OK when TLS options are the same",
- routers: map[string]*runtime.RouterInfo{
- "router-1@file": {
- Router: &dynamic.Router{
- EntryPoints: entryPoints,
- Rule: "Host(`host1.local`)",
- TLS: &dynamic.RouterTLSConfig{
- Options: "host1",
- },
- },
- },
- "router-2@file": {
- Router: &dynamic.Router{
- EntryPoints: entryPoints,
- Rule: "Host(`host2.local`)",
- TLS: &dynamic.RouterTLSConfig{
- Options: "host1",
- },
- },
- },
- },
- tlsOptions: tlsOptionsBase,
- host: "host1.local",
- ServerName: "host2.local",
- expectedStatus: http.StatusOK,
- },
- {
- desc: "Default TLS options is used when options are ambiguous for the same host",
- routers: map[string]*runtime.RouterInfo{
- "router-1@file": {
- Router: &dynamic.Router{
- EntryPoints: entryPoints,
- Rule: "Host(`host1.local`)",
- TLS: &dynamic.RouterTLSConfig{
- Options: "host1",
- },
- },
- },
- "router-2@file": {
- Router: &dynamic.Router{
- EntryPoints: entryPoints,
- Rule: "Host(`host1.local`) && PathPrefix(`/foo`)",
- TLS: &dynamic.RouterTLSConfig{
- Options: "default",
- },
- },
- },
- "router-3@file": {
- Router: &dynamic.Router{
- EntryPoints: entryPoints,
- Rule: "Host(`host2.local`)",
- TLS: &dynamic.RouterTLSConfig{
- Options: "host1",
- },
- },
- },
- },
- tlsOptions: tlsOptionsBase,
- host: "host1.local",
- ServerName: "host2.local",
- expectedStatus: http.StatusMisdirectedRequest,
- },
- {
- desc: "Default TLS options should not be used when options are the same for the same host",
- routers: map[string]*runtime.RouterInfo{
- "router-1@file": {
- Router: &dynamic.Router{
- EntryPoints: entryPoints,
- Rule: "Host(`host1.local`)",
- TLS: &dynamic.RouterTLSConfig{
- Options: "host1",
- },
- },
- },
- "router-2@file": {
- Router: &dynamic.Router{
- EntryPoints: entryPoints,
- Rule: "Host(`host1.local`) && PathPrefix(`/bar`)",
- TLS: &dynamic.RouterTLSConfig{
- Options: "host1",
- },
- },
- },
- "router-3@file": {
- Router: &dynamic.Router{
- EntryPoints: entryPoints,
- Rule: "Host(`host2.local`)",
- TLS: &dynamic.RouterTLSConfig{
- Options: "host1",
- },
- },
- },
- },
- tlsOptions: tlsOptionsBase,
- host: "host1.local",
- ServerName: "host2.local",
- expectedStatus: http.StatusOK,
- },
- {
- desc: "Request is misdirected when TLS options have the same name but from different providers",
- routers: map[string]*runtime.RouterInfo{
- "router-1@file": {
- Router: &dynamic.Router{
- EntryPoints: entryPoints,
- Rule: "Host(`host1.local`)",
- TLS: &dynamic.RouterTLSConfig{
- Options: "host1",
- },
- },
- },
- "router-2@crd": {
- Router: &dynamic.Router{
- EntryPoints: entryPoints,
- Rule: "Host(`host2.local`)",
- TLS: &dynamic.RouterTLSConfig{
- Options: "host1",
- },
- },
- },
- },
- tlsOptions: tlsOptionsBase,
- host: "host1.local",
- ServerName: "host2.local",
- expectedStatus: http.StatusMisdirectedRequest,
- },
- {
- desc: "Request is OK when TLS options reference from a different provider is the same",
- routers: map[string]*runtime.RouterInfo{
- "router-1@file": {
- Router: &dynamic.Router{
- EntryPoints: entryPoints,
- Rule: "Host(`host1.local`)",
- TLS: &dynamic.RouterTLSConfig{
- Options: "host1@crd",
- },
- },
- },
- "router-2@crd": {
- Router: &dynamic.Router{
- EntryPoints: entryPoints,
- Rule: "Host(`host2.local`)",
- TLS: &dynamic.RouterTLSConfig{
- Options: "host1@crd",
- },
- },
- },
- },
- tlsOptions: tlsOptionsBase,
- host: "host1.local",
- ServerName: "host2.local",
- expectedStatus: http.StatusOK,
- },
- {
- desc: "Request is misdirected when server name is empty and the host name is an FQDN, but router's rule is not",
- routers: map[string]*runtime.RouterInfo{
- "router-1@file": {
- Router: &dynamic.Router{
- EntryPoints: entryPoints,
- Rule: "Host(`host1.local`)",
- TLS: &dynamic.RouterTLSConfig{
- Options: "host1@file",
- },
- },
- },
- },
- tlsOptions: map[string]traefiktls.Options{
- "default": {
- MinVersion: "VersionTLS13",
- },
- "host1@file": {
- MinVersion: "VersionTLS12",
- },
- },
- host: "host1.local.",
- expectedStatus: http.StatusMisdirectedRequest,
- },
- {
- desc: "Request is misdirected when server name is empty and the host name is not FQDN, but router's rule is",
- routers: map[string]*runtime.RouterInfo{
- "router-1@file": {
- Router: &dynamic.Router{
- EntryPoints: entryPoints,
- Rule: "Host(`host1.local.`)",
- TLS: &dynamic.RouterTLSConfig{
- Options: "host1@file",
- },
- },
- },
- },
- tlsOptions: map[string]traefiktls.Options{
- "default": {
- MinVersion: "VersionTLS13",
- },
- "host1@file": {
- MinVersion: "VersionTLS12",
- },
- },
- host: "host1.local",
- expectedStatus: http.StatusMisdirectedRequest,
- },
- }
-
- for _, test := range tests {
- t.Run(test.desc, func(t *testing.T) {
- conf := &runtime.Configuration{
- Routers: test.routers,
- }
-
- serviceManager := tcp.NewManager(conf, tcp2.NewDialerManager(nil))
-
- tlsManager := traefiktls.NewManager(nil)
- tlsManager.UpdateConfigs(t.Context(), map[string]traefiktls.Store{}, test.tlsOptions, []*traefiktls.CertAndStores{})
-
- httpsHandler := map[string]http.Handler{
- "web": http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {}),
- }
-
- middlewaresBuilder := tcpmiddleware.NewBuilder(conf.TCPMiddlewares)
-
- routerManager := NewManager(conf, serviceManager, middlewaresBuilder, nil, httpsHandler, tlsManager, nil)
-
- routers := routerManager.BuildHandlers(t.Context(), entryPoints)
-
- router, ok := routers["web"]
- require.True(t, ok)
-
- req := httptest.NewRequest(http.MethodGet, "/", nil)
- req.Host = test.host
- req.TLS = &tls.ConnectionState{
- ServerName: test.ServerName,
- }
-
- rw := httptest.NewRecorder()
-
- router.GetHTTPSHandler().ServeHTTP(rw, req)
-
- assert.Equal(t, test.expectedStatus, rw.Code)
- })
- }
-}
diff --git a/pkg/server/router/tcp/postgres.go b/pkg/server/router/tcp/postgres.go
index 10b18a50dc..a91e531e69 100644
--- a/pkg/server/router/tcp/postgres.go
+++ b/pkg/server/router/tcp/postgres.go
@@ -64,7 +64,7 @@ func (r *Router) servePostgres(conn *peekConn) error {
log.Error().Err(err).Msg("Error while setting deadline")
}
- connData, err := tcpmuxer.NewConnData(hello.serverName, conn, hello.protos)
+ connData, err := tcpmuxer.NewConnData(hello.serverName, conn.RemoteAddr(), hello.protos)
if err != nil {
log.Error().Err(err).Msg("Error while reading TCP connection data")
return nil
diff --git a/pkg/server/router/tcp/router.go b/pkg/server/router/tcp/router.go
index 0cce26e6fe..8b6c65854c 100644
--- a/pkg/server/router/tcp/router.go
+++ b/pkg/server/router/tcp/router.go
@@ -16,11 +16,17 @@ import (
"github.com/rs/zerolog/log"
tcpmuxer "github.com/traefik/traefik/v3/pkg/muxer/tcp"
"github.com/traefik/traefik/v3/pkg/tcp"
+ traefiktls "github.com/traefik/traefik/v3/pkg/tls"
)
// errClientHelloRead is used as a sentinel error to break the TLS handshake once we have read the ClientHello.
var errClientHelloRead = errors.New("client hello successfully read")
+type tlsConfigWithOptionsName struct {
+ cfg *tls.Config
+ optionsName string
+}
+
// Router is a TCP router.
type Router struct {
acmeTLSPassthrough bool
@@ -47,7 +53,7 @@ type Router struct {
httpsTLSConfig *tls.Config // default TLS config
// hostHTTPTLSConfig contains TLS configs keyed by SNI.
// A nil config is the hint to set up a brokenTLSRouter.
- hostHTTPTLSConfig map[string]*tls.Config // TLS configs keyed by SNI
+ hostHTTPTLSConfig map[string]tlsConfigWithOptionsName // TLS configs keyed by SNI
}
// NewRouter returns a new TCP router.
@@ -74,14 +80,20 @@ func NewRouter(providersPrecedence []string) (*Router, error) {
}, nil
}
-// GetTLSGetClientInfo is called after a ClientHello is received from a client.
-func (r *Router) GetTLSGetClientInfo() func(info *tls.ClientHelloInfo) (*tls.Config, error) {
- return func(info *tls.ClientHelloInfo) (*tls.Config, error) {
- if tlsConfig, ok := r.hostHTTPTLSConfig[info.ServerName]; ok {
- return tlsConfig, nil
+// HTTP3TLSConfigMatcherFunc returns a matcher func for HTTP/3 which returns a tls.Config with its corresponding
+// TLSOptionName matching the given HostSNI in the connection data, or the default TLS config if there is no match.
+func (r *Router) HTTP3TLSConfigMatcherFunc() func(connData tcpmuxer.ConnData) (*tls.Config, string, error) {
+ return func(connData tcpmuxer.ConnData) (*tls.Config, string, error) {
+ h, _ := r.muxerHTTPS.Match(connData)
+ if h == nil {
+ return r.httpsTLSConfig, traefiktls.DefaultTLSConfigName, nil
}
- return r.httpsTLSConfig, nil
+ if tlsHandler, ok := h.(*tcp.TLSHandler); ok {
+ return tlsHandler.Config, tlsHandler.TLSOptionsName, nil
+ }
+
+ return nil, "", errors.New("matching handler is not a TLSHandler")
}
}
@@ -93,7 +105,7 @@ func (r *Router) ServeTCP(conn tcp.WriteCloser) {
// we would block forever on clientHelloInfo,
// which is why we want to detect and handle that case first and foremost.
if r.muxerTCP.HasRoutes() && !r.muxerTCPTLS.HasRoutes() && !r.muxerHTTPS.HasRoutes() {
- connData, err := tcpmuxer.NewConnData("", conn, nil)
+ connData, err := tcpmuxer.NewConnData("", conn.RemoteAddr(), nil)
if err != nil {
log.Error().Err(err).Msg("Error while reading TCP connection data")
conn.Close()
@@ -159,7 +171,7 @@ func (r *Router) ServeTCP(conn tcp.WriteCloser) {
log.Error().Err(err).Msg("Error while setting deadline")
}
- connData, err := tcpmuxer.NewConnData(hello.serverName, pConn, hello.protos)
+ connData, err := tcpmuxer.NewConnData(hello.serverName, pConn.RemoteAddr(), hello.protos)
if err != nil {
log.Error().Err(err).Msg("Error while reading TCP connection data")
_ = pConn.Close()
@@ -235,12 +247,15 @@ func (r *Router) AddTCPRoute(rule string, priority int, providerName string, tar
}
// AddHTTPTLSConfig defines a handler for a given sniHost and sets the matching tlsConfig.
-func (r *Router) AddHTTPTLSConfig(sniHost string, config *tls.Config) {
+func (r *Router) AddHTTPTLSConfig(sniHost string, config *tls.Config, optionsName string) {
if r.hostHTTPTLSConfig == nil {
- r.hostHTTPTLSConfig = map[string]*tls.Config{}
+ r.hostHTTPTLSConfig = map[string]tlsConfigWithOptionsName{}
}
- r.hostHTTPTLSConfig[sniHost] = config
+ r.hostHTTPTLSConfig[sniHost] = tlsConfigWithOptionsName{
+ cfg: config,
+ optionsName: optionsName,
+ }
}
// GetHTTPHandler gets the attached http handler.
@@ -264,12 +279,13 @@ func (r *Router) SetHTTPForwarder(handler tcp.Handler) {
func (r *Router) SetHTTPSForwarder(handler tcp.Handler) {
for sniHost, tlsConf := range r.hostHTTPTLSConfig {
var tcpHandler tcp.Handler
- if tlsConf == nil {
+ if tlsConf.cfg == nil {
tcpHandler = &brokenTLSRouter{}
} else {
tcpHandler = &tcp.TLSHandler{
- Next: handler,
- Config: tlsConf,
+ Next: handler,
+ Config: tlsConf.cfg,
+ TLSOptionsName: tlsConf.optionsName,
}
}
@@ -287,8 +303,9 @@ func (r *Router) SetHTTPSForwarder(handler tcp.Handler) {
}
r.httpsForwarder = &tcp.TLSHandler{
- Next: handler,
- Config: r.httpsTLSConfig,
+ Next: handler,
+ Config: r.httpsTLSConfig,
+ TLSOptionsName: "default",
}
}
diff --git a/pkg/server/router/tcp/router_test.go b/pkg/server/router/tcp/router_test.go
index a7d7034994..a0bcf956d3 100644
--- a/pkg/server/router/tcp/router_test.go
+++ b/pkg/server/router/tcp/router_test.go
@@ -3,6 +3,7 @@ package tcp
import (
"bufio"
"bytes"
+ "context"
"crypto/tls"
"errors"
"fmt"
@@ -21,7 +22,7 @@ import (
"github.com/traefik/traefik/v3/pkg/config/runtime"
tcpmiddleware "github.com/traefik/traefik/v3/pkg/server/middleware/tcp"
"github.com/traefik/traefik/v3/pkg/server/service/tcp"
- tcp2 "github.com/traefik/traefik/v3/pkg/tcp"
+ traefiktcp "github.com/traefik/traefik/v3/pkg/tcp"
traefiktls "github.com/traefik/traefik/v3/pkg/tls"
"github.com/traefik/traefik/v3/pkg/tls/generate"
"github.com/traefik/traefik/v3/pkg/types"
@@ -125,7 +126,7 @@ func Test_Routing(t *testing.T) {
},
}
- dialerManager := tcp2.NewDialerManager(nil)
+ dialerManager := traefiktcp.NewDialerManager(nil)
dialerManager.Update(map[string]*dynamic.TCPServersTransport{"default@internal": {}})
serviceManager := tcp.NewManager(conf, dialerManager)
@@ -599,6 +600,16 @@ func Test_Routing(t *testing.T) {
_, err = fmt.Fprint(w, "HTTPS")
require.NoError(t, err)
}),
+
+ ConnContext: func(ctx context.Context, c net.Conn) context.Context {
+ if tlsConn, ok := c.(*tls.Conn); ok {
+ if tlsConnWithOptionsName, ok := tlsConn.NetConn().(traefiktcp.TLSConn); ok {
+ return traefiktcp.AddTLSOptionsNameInContext(ctx, tlsConnWithOptionsName.TLSOptionsName)
+ }
+ }
+
+ return ctx
+ },
}
stoppedHTTPS := make(chan struct{})
@@ -856,9 +867,9 @@ func TestPostgresTLSTermination(t *testing.T) {
// Register a TCPTLS route (TLS termination, not passthrough) with a TLSHandler.
// The TLSHandler wraps the actual handler, performing the TLS handshake.
- err = router.muxerTCPTLS.AddRoute("HostSNI(`test.localhost`)", "", 0, "", &tcp2.TLSHandler{
+ err = router.muxerTCPTLS.AddRoute("HostSNI(`test.localhost`)", "", 0, "", &traefiktcp.TLSHandler{
Config: tlsConf,
- Next: tcp2.HandlerFunc(func(conn tcp2.WriteCloser) {
+ Next: traefiktcp.HandlerFunc(func(conn traefiktcp.WriteCloser) {
_, _ = conn.Write([]byte("OK"))
_ = conn.Close()
}),
@@ -921,7 +932,7 @@ func TestPostgresTLSPassthrough(t *testing.T) {
require.NoError(t, err)
// Register a TCPTLS route (TLS passthrough) with a tcp.Handler.
- err = router.muxerTCPTLS.AddRoute("HostSNI(`test.localhost`)", "", 0, "", tcp2.HandlerFunc(func(conn tcp2.WriteCloser) {
+ err = router.muxerTCPTLS.AddRoute("HostSNI(`test.localhost`)", "", 0, "", traefiktcp.HandlerFunc(func(conn traefiktcp.WriteCloser) {
// First we should receive the PostgresStartTLSMsg.
buf := make([]byte, len(PostgresStartTLSMsg))
_, err := conn.Read(buf)
@@ -1056,7 +1067,8 @@ func routerHTTPSPathPrefix(conf *runtime.Configuration) {
Service: "http",
Rule: "PathPrefix(`/`)",
TLS: &dynamic.RouterTLSConfig{
- Options: "tls10",
+ Options: "tls10",
+ ResolvedOptions: "tls10",
},
},
}
@@ -1070,7 +1082,8 @@ func routerHTTPS(conf *runtime.Configuration) {
Service: "http",
Rule: "Host(`foo.bar`)",
TLS: &dynamic.RouterTLSConfig{
- Options: "tls12",
+ Options: "tls12",
+ ResolvedOptions: "tls12",
},
},
}
@@ -1366,7 +1379,7 @@ func (h *httpForwarder) Close() error {
}
// ServeTCP uses the connection to serve it later in "Accept".
-func (h *httpForwarder) ServeTCP(conn tcp2.WriteCloser) {
+func (h *httpForwarder) ServeTCP(conn traefiktcp.WriteCloser) {
h.connChan <- conn
}
diff --git a/pkg/server/server_entrypoint_tcp.go b/pkg/server/server_entrypoint_tcp.go
index c4f8f2098f..c43786dad9 100644
--- a/pkg/server/server_entrypoint_tcp.go
+++ b/pkg/server/server_entrypoint_tcp.go
@@ -2,6 +2,7 @@ package server
import (
"context"
+ "crypto/tls"
"errors"
"expvar"
"fmt"
@@ -690,6 +691,15 @@ func newHTTPServer(ctx context.Context, ln net.Listener, configuration *static.E
MaxDecoderHeaderTableSize: int(configuration.HTTP2.MaxDecoderHeaderTableSize),
MaxEncoderHeaderTableSize: int(configuration.HTTP2.MaxEncoderHeaderTableSize),
},
+ ConnContext: func(ctx context.Context, c net.Conn) context.Context {
+ if tlsConn, ok := c.(*tls.Conn); ok {
+ if tlsConnWithOptionsName, ok := tlsConn.NetConn().(tcp.TLSConn); ok {
+ return tcp.AddTLSOptionsNameInContext(ctx, tlsConnWithOptionsName.TLSOptionsName)
+ }
+ }
+
+ return ctx
+ },
}
if debugConnection || (configuration.Transport != nil && (configuration.Transport.KeepAliveMaxTime > 0 || configuration.Transport.KeepAliveMaxRequests > 0)) {
serverHTTP.ConnContext = func(ctx context.Context, c net.Conn) context.Context {
diff --git a/pkg/server/server_entrypoint_tcp_http3.go b/pkg/server/server_entrypoint_tcp_http3.go
index 7a2b0cadf4..5cac4c9e08 100644
--- a/pkg/server/server_entrypoint_tcp_http3.go
+++ b/pkg/server/server_entrypoint_tcp_http3.go
@@ -13,7 +13,9 @@ import (
"github.com/quic-go/quic-go/http3"
"github.com/rs/zerolog/log"
"github.com/traefik/traefik/v3/pkg/config/static"
+ tcpmuxer "github.com/traefik/traefik/v3/pkg/muxer/tcp"
tcprouter "github.com/traefik/traefik/v3/pkg/server/router/tcp"
+ "github.com/traefik/traefik/v3/pkg/tcp"
)
type http3server struct {
@@ -22,7 +24,7 @@ type http3server struct {
http3conn net.PacketConn
lock sync.RWMutex
- getter func(info *tls.ClientHelloInfo) (*tls.Config, error)
+ getter func(data tcpmuxer.ConnData) (*tls.Config, string, error)
}
func newHTTP3Server(ctx context.Context, name string, config *static.EntryPoint, httpsServer *httpServer) (*http3server, error) {
@@ -55,8 +57,8 @@ func newHTTP3Server(ctx context.Context, name string, config *static.EntryPoint,
h3 := &http3server{
http3conn: conn,
- getter: func(info *tls.ClientHelloInfo) (*tls.Config, error) {
- return nil, errors.New("no tls config")
+ getter: func(data tcpmuxer.ConnData) (*tls.Config, string, error) {
+ return nil, "", errors.New("no TLS config")
},
}
@@ -64,10 +66,18 @@ func newHTTP3Server(ctx context.Context, name string, config *static.EntryPoint,
Addr: config.GetAddress(),
Port: config.HTTP3.AdvertisedPort,
Handler: httpsServer.Server.(*http.Server).Handler,
- TLSConfig: &tls.Config{GetConfigForClient: h3.getGetConfigForClient},
+ TLSConfig: &tls.Config{GetConfigForClient: h3.getTLSConfigForClient},
QUICConfig: &quic.Config{
Allow0RTT: false,
},
+ ConnContext: func(ctx context.Context, c *quic.Conn) context.Context {
+ tlsOptionsName, err := h3.getTLSOptionsName(c)
+ if err != nil {
+ log.Error().Msgf("Error getting TLS options name for client: %v", err)
+ return ctx
+ }
+ return tcp.AddTLSOptionsNameInContext(ctx, tlsOptionsName)
+ },
}
previousHandler := httpsServer.Server.(*http.Server).Handler
@@ -91,7 +101,7 @@ func (e *http3server) Switch(rt *tcprouter.Router) {
e.lock.Lock()
defer e.lock.Unlock()
- e.getter = rt.GetTLSGetClientInfo()
+ e.getter = rt.HTTP3TLSConfigMatcherFunc()
}
func (e *http3server) Shutdown(_ context.Context) error {
@@ -99,9 +109,28 @@ func (e *http3server) Shutdown(_ context.Context) error {
return e.Server.Close()
}
-func (e *http3server) getGetConfigForClient(info *tls.ClientHelloInfo) (*tls.Config, error) {
+func (e *http3server) getTLSConfigForClient(info *tls.ClientHelloInfo) (*tls.Config, error) {
e.lock.RLock()
defer e.lock.RUnlock()
- return e.getter(info)
+ connData, err := tcpmuxer.NewConnData(info.ServerName, info.Conn.RemoteAddr(), info.SupportedProtos)
+ if err != nil {
+ return nil, fmt.Errorf("creating ConnData from client hello: %w", err)
+ }
+
+ conf, _, err := e.getter(connData)
+ return conf, err
+}
+
+func (e *http3server) getTLSOptionsName(c *quic.Conn) (string, error) {
+ e.lock.RLock()
+ defer e.lock.RUnlock()
+
+ connData, err := tcpmuxer.NewConnData(c.ConnectionState().TLS.ServerName, c.RemoteAddr(), []string{c.ConnectionState().TLS.NegotiatedProtocol})
+ if err != nil {
+ return "", fmt.Errorf("creating ConnData from quic Conn: %w", err)
+ }
+
+ _, name, err := e.getter(connData)
+ return name, err
}
diff --git a/pkg/server/server_entrypoint_tcp_http3_test.go b/pkg/server/server_entrypoint_tcp_http3_test.go
index b4d982303c..d7b1e4a0a6 100644
--- a/pkg/server/server_entrypoint_tcp_http3_test.go
+++ b/pkg/server/server_entrypoint_tcp_http3_test.go
@@ -13,6 +13,7 @@ import (
"github.com/stretchr/testify/require"
"github.com/traefik/traefik/v3/pkg/config/static"
tcprouter "github.com/traefik/traefik/v3/pkg/server/router/tcp"
+ traefiktls "github.com/traefik/traefik/v3/pkg/tls"
"github.com/traefik/traefik/v3/pkg/types"
)
@@ -102,7 +103,7 @@ func TestHTTP3AdvertisedPort(t *testing.T) {
router.AddHTTPTLSConfig("*", &tls.Config{
Certificates: []tls.Certificate{tlsCert},
- })
+ }, traefiktls.DefaultTLSConfigName)
router.SetHTTPSHandler(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
rw.WriteHeader(http.StatusOK)
}), nil)
@@ -164,7 +165,7 @@ func TestHTTP30RTT(t *testing.T) {
router.AddHTTPTLSConfig("example.com", &tls.Config{
Certificates: []tls.Certificate{tlsCert},
- })
+ }, traefiktls.DefaultTLSConfigName)
router.SetHTTPSHandler(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
rw.WriteHeader(http.StatusOK)
}), nil)
diff --git a/pkg/tcp/tls.go b/pkg/tcp/tls.go
index 207aebb150..33914e5e53 100644
--- a/pkg/tcp/tls.go
+++ b/pkg/tcp/tls.go
@@ -1,16 +1,39 @@
package tcp
import (
+ "context"
"crypto/tls"
)
+// TLSConn is a TLS connection that also carries the name of the TLS config used.
+type TLSConn struct {
+ WriteCloser
+
+ TLSOptionsName string
+}
+
// TLSHandler handles TLS connections.
type TLSHandler struct {
- Next Handler
- Config *tls.Config
+ Next Handler
+ Config *tls.Config
+ TLSOptionsName string
}
// ServeTCP terminates the TLS connection.
func (t *TLSHandler) ServeTCP(conn WriteCloser) {
- t.Next.ServeTCP(tls.Server(conn, t.Config))
+ t.Next.ServeTCP(tls.Server(TLSConn{WriteCloser: conn, TLSOptionsName: t.TLSOptionsName}, t.Config))
+}
+
+type tlsOptionsNameKey struct{}
+
+func AddTLSOptionsNameInContext(ctx context.Context, name string) context.Context {
+ return context.WithValue(ctx, tlsOptionsNameKey{}, name)
+}
+
+func GetTLSOptionsName(ctx context.Context) string {
+ if name, ok := ctx.Value(tlsOptionsNameKey{}).(string); ok {
+ return name
+ }
+
+ return ""
}