Merge branch v3.7 into master
Some checks failed
CodeQL / Analyze (push) Has been cancelled
Build and Publish Documentation / Doc Process (push) Has been cancelled
Build experimental image on branch / build-webui (push) Has been cancelled
Build experimental image on branch / Build experimental image on branch (push) Has been cancelled

This commit is contained in:
kevinpollet 2026-06-05 16:01:41 +02:00
commit 8773d7ead4
No known key found for this signature in database
GPG key ID: 0C9A5DDD1B292453
57 changed files with 1383 additions and 970 deletions

View file

@ -348,6 +348,9 @@ linters:
text: 'appendAssign: append result not assigned to the same slice'
linters:
- gocritic
- path: pkg/server/conncontext.go
linters:
- fatcontext
paths:
- pkg/provider/kubernetes/crd/generated/

View file

@ -1,3 +1,122 @@
## [v3.7.4](https://github.com/traefik/traefik/tree/v3.7.4) (2026-06-05)
[All Commits](https://github.com/traefik/traefik/compare/v3.7.3...v3.7.4)
**Bug fixes:**
- **[middleware]** Fix redis write timeout option configuration ([#13273](https://github.com/traefik/traefik/pull/13273) @bzyy1024)
- **[webui]** Bump react-router and jsdom ([#13301](https://github.com/traefik/traefik/pull/13301) @gndz07)
- **[k8s/gatewayapi]** Fix BackendTLSPolicy status update ([#13306](https://github.com/traefik/traefik/pull/13306) @AnatoleLucet)
- **[http3]** Bump github.com/quic-go/quic-go to v0.59.1 ([#13300](https://github.com/traefik/traefik/pull/13300) @rtribotte)
- **[webui]** Bump axios to v1.17.0 ([#13299](https://github.com/traefik/traefik/pull/13299) @gndz07)
- **[tls]** Fix snicheck with keepalive ([#13305](https://github.com/traefik/traefik/pull/13305) @juliens)
## [v3.6.20](https://github.com/traefik/traefik/tree/v3.6.20) (2026-06-05)
[All Commits](https://github.com/traefik/traefik/compare/v3.6.19...v3.6.20)
**Bug fixes:**
- **[middleware]** Fix redis write timeout option configuration ([#13273](https://github.com/traefik/traefik/pull/13273) @bzyy1024)
- **[webui]** Bump react-router and jsdom ([#13301](https://github.com/traefik/traefik/pull/13301) @gndz07)
- **[k8s/gatewayapi]** Fix BackendTLSPolicy status update ([#13306](https://github.com/traefik/traefik/pull/13306) @AnatoleLucet)
- **[http3]** Bump github.com/quic-go/quic-go to v0.59.1 ([#13300](https://github.com/traefik/traefik/pull/13300) @rtribotte)
- **[webui]** Bump axios to v1.17.0 ([#13299](https://github.com/traefik/traefik/pull/13299) @gndz07)
- **[tls]** Fix snicheck with keepalive ([#13305](https://github.com/traefik/traefik/pull/13305) @juliens)
## [v2.11.49](https://github.com/traefik/traefik/tree/v2.11.49) (2026-06-05)
[All Commits](https://github.com/traefik/traefik/compare/v2.11.48...v2.11.49)
**Bug fixes:**
- **[http3]** Bump github.com/quic-go/quic-go to v0.59.1 ([#13300](https://github.com/traefik/traefik/pull/13300) @rtribotte)
- **[webui]** Bump axios to v1.17.0 ([#13299](https://github.com/traefik/traefik/pull/13299) @gndz07)
- **[tls]** Fix snicheck with keepalive ([#13305](https://github.com/traefik/traefik/pull/13305) @juliens)
## [v3.7.3](https://github.com/traefik/traefik/tree/v3.7.3) (2026-06-04)
[All Commits](https://github.com/traefik/traefik/compare/v3.7.1...v3.7.3)
**Bug fixes:**
- **[tls]** Compute resolved tlsOptions after applying models ([#13291](https://github.com/traefik/traefik/pull/13291) @rtribotte)
- **[webui, tcp]** Fix TCP router service resolution in dashboard flow diagram ([#13155](https://github.com/traefik/traefik/pull/13155) @aliamerj)
- **[k8s/ingress-nginx]** Trim quotes from proxy_set_header header name ([#13203](https://github.com/traefik/traefik/pull/13203) @crisbal)
- **[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/ingress-nginx]** Clear Ssl-Client-* headers when no client certificate is present ([#13260](https://github.com/traefik/traefik/pull/13260) @gndz07)
- **[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:**
- **[k8s]** Document new chart behavior on Gateway API ([#13167](https://github.com/traefik/traefik/pull/13167) @mloiseleur)
- **[file]** Replace generated File routing reference page ([#13170](https://github.com/traefik/traefik/pull/13170) @sheddy-traefik)
- **[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)
- Polish grammar in migration guides ([#13174](https://github.com/traefik/traefik/pull/13174) @quyentonndbs)
- **[middleware]** Remove whitespace in HTML tag ([#13160](https://github.com/traefik/traefik/pull/13160) @marbon87)
- Add @LBF38 as a current maintainer ([#13225](https://github.com/traefik/traefik/pull/13225) @emilevauge)
- Add ingressClassName to Kubernetes CRD provider migration guide ([#13248](https://github.com/traefik/traefik/pull/13248) @kevinpollet)
- **[k8s/ingress-nginx]** Add nginx.ingress.kubernetes.io/enable-global-auth to the list of supported annotations ([#13219](https://github.com/traefik/traefik/pull/13219) @filip2mac)
- **[k8s/ingress-nginx]** Capitalize NGINX in kubernetesIngressNGINX ([#13236](https://github.com/traefik/traefik/pull/13236) @smellems)
## [v3.6.19](https://github.com/traefik/traefik/tree/v3.6.19) (2026-06-04)
[All Commits](https://github.com/traefik/traefik/compare/v3.6.17...v3.6.19)
**Bug fixes:**
- **[tls]** Compute resolved tlsOptions after applying models ([#13291](https://github.com/traefik/traefik/pull/13291) @rtribotte)
- **[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)
- **[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)
- Polish grammar in migration guides ([#13174](https://github.com/traefik/traefik/pull/13174) @quyentonndbs)
- **[middleware]** Remove whitespace in HTML tag ([#13160](https://github.com/traefik/traefik/pull/13160) @marbon87)
- Add @LBF38 as a current maintainer ([#13225](https://github.com/traefik/traefik/pull/13225) @emilevauge)
- **[k8s/ingress-nginx]** Capitalize NGINX in kubernetesIngressNGINX ([#13236](https://github.com/traefik/traefik/pull/13236) @smellems)
## [v2.11.48](https://github.com/traefik/traefik/tree/v2.11.48) (2026-06-04)
[All Commits](https://github.com/traefik/traefik/compare/v2.11.46...v2.11.48)
**Bug fixes:**
- **[tls]** Compute resolved tlsOptions after applying models ([#13291](https://github.com/traefik/traefik/pull/13291) @rtribotte)
- **[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.2](https://github.com/traefik/traefik/tree/v3.7.2) (2026-06-03)
[All Commits](https://github.com/traefik/traefik/compare/v3.7.1...v3.7.2)
Release canceled.
## [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)
Release canceled.
## [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)
Release canceled.
## [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)

View file

@ -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
...
```

View file

@ -9,6 +9,43 @@ This guide provides detailed migration steps for upgrading between different Tra
---
## v3.7.3
### Kubernetes Gateway API Provider
Starting with `v3.7.3`, 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.3` 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.3` 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`
@ -80,7 +117,7 @@ kubectl apply -f https://github.com/kubernetes-sigs/gateway-api/releases/downloa
### Kubernetes CRD Provider
To use the new options of the `retry` middleware with the Kubernetes CRD provider, you need to update your CRDs.
To use the new options of the `retry` middleware or the new `ingressClassName` field with the Kubernetes CRD provider, you need to update your CRDs.
**Apply Updated CRDs:**
@ -107,6 +144,43 @@ Note: TLSOptions for `HostRegexp` matchers remains unsupported. Use wildcard `Ho
---
## v3.6.19
### Kubernetes Gateway API Provider
Starting with `v3.6.19`, 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.19` 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.19` 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`

View file

@ -364,6 +364,7 @@ THIS FILE MUST NOT BE EDITED BY HAND
| <a id="opt-providers-kubernetescrd-throttleduration" href="#opt-providers-kubernetescrd-throttleduration" title="#opt-providers-kubernetescrd-throttleduration">providers.kubernetescrd.throttleduration</a> | Ingress refresh throttle duration | 0 |
| <a id="opt-providers-kubernetescrd-token" href="#opt-providers-kubernetescrd-token" title="#opt-providers-kubernetescrd-token">providers.kubernetescrd.token</a> | Kubernetes bearer token (not needed for in-cluster client). It accepts either a token value or a file path to the token. | |
| <a id="opt-providers-kubernetesgateway" href="#opt-providers-kubernetesgateway" title="#opt-providers-kubernetesgateway">providers.kubernetesgateway</a> | Enables Kubernetes Gateway API provider. | false |
| <a id="opt-providers-kubernetesgateway-burst" href="#opt-providers-kubernetesgateway-burst" title="#opt-providers-kubernetesgateway-burst">providers.kubernetesgateway.burst</a> | Defines the maximum burst of requests to the Kubernetes API server. | 100 |
| <a id="opt-providers-kubernetesgateway-certauthfilepath" href="#opt-providers-kubernetesgateway-certauthfilepath" title="#opt-providers-kubernetesgateway-certauthfilepath">providers.kubernetesgateway.certauthfilepath</a> | Kubernetes certificate authority file path (not needed for in-cluster client). | |
| <a id="opt-providers-kubernetesgateway-crossprovidernamespaces" href="#opt-providers-kubernetesgateway-crossprovidernamespaces" title="#opt-providers-kubernetesgateway-crossprovidernamespaces">providers.kubernetesgateway.crossprovidernamespaces</a> | List of namespaces from which Gateway API routes are allowed to declare TraefikService backendRef references. | |
| <a id="opt-providers-kubernetesgateway-endpoint" href="#opt-providers-kubernetesgateway-endpoint" title="#opt-providers-kubernetesgateway-endpoint">providers.kubernetesgateway.endpoint</a> | Kubernetes server endpoint (required for external cluster client). | |
@ -371,6 +372,7 @@ THIS FILE MUST NOT BE EDITED BY HAND
| <a id="opt-providers-kubernetesgateway-labelselector" href="#opt-providers-kubernetesgateway-labelselector" title="#opt-providers-kubernetesgateway-labelselector">providers.kubernetesgateway.labelselector</a> | Kubernetes label selector to select specific GatewayClasses. | |
| <a id="opt-providers-kubernetesgateway-namespaces" href="#opt-providers-kubernetesgateway-namespaces" title="#opt-providers-kubernetesgateway-namespaces">providers.kubernetesgateway.namespaces</a> | Kubernetes namespaces. | |
| <a id="opt-providers-kubernetesgateway-nativelbbydefault" href="#opt-providers-kubernetesgateway-nativelbbydefault" title="#opt-providers-kubernetesgateway-nativelbbydefault">providers.kubernetesgateway.nativelbbydefault</a> | Defines whether to use Native Kubernetes load-balancing by default. | false |
| <a id="opt-providers-kubernetesgateway-qps" href="#opt-providers-kubernetesgateway-qps" title="#opt-providers-kubernetesgateway-qps">providers.kubernetesgateway.qps</a> | Defines the maximum QPS to the Kubernetes API server. Setting this to a negative value will disable client-side ratelimiting. | 50 |
| <a id="opt-providers-kubernetesgateway-statusaddress-hostname" href="#opt-providers-kubernetesgateway-statusaddress-hostname" title="#opt-providers-kubernetesgateway-statusaddress-hostname">providers.kubernetesgateway.statusaddress.hostname</a> | Hostname used for Kubernetes Gateway status address. | |
| <a id="opt-providers-kubernetesgateway-statusaddress-ip" href="#opt-providers-kubernetesgateway-statusaddress-ip" title="#opt-providers-kubernetesgateway-statusaddress-ip">providers.kubernetesgateway.statusaddress.ip</a> | IP used to set Kubernetes Gateway status address. | |
| <a id="opt-providers-kubernetesgateway-statusaddress-service" href="#opt-providers-kubernetesgateway-statusaddress-service" title="#opt-providers-kubernetesgateway-statusaddress-service">providers.kubernetesgateway.statusaddress.service</a> | Published Kubernetes Service to copy status addresses from. | |

View file

@ -66,22 +66,24 @@ providers:
<!-- markdownlint-disable MD013 -->
| Field | Description | Default | Required |
|:----------------------------------------------------------------------|:------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:--------|:---------|
| <a id="opt-providers-providersThrottleDuration" href="#opt-providers-providersThrottleDuration" title="#opt-providers-providersThrottleDuration">`providers.providersThrottleDuration`</a> | Minimum amount of time to wait for, after a configuration reload, before taking into account any new configuration refresh event.<br />If multiple events occur within this time, only the most recent one is taken into account, and all others are discarded.<br />**This option cannot be set per provider, but the throttling algorithm applies to each of them independently.** | 2s | No |
| <a id="opt-providers-kubernetesGateway-endpoint" href="#opt-providers-kubernetesGateway-endpoint" title="#opt-providers-kubernetesGateway-endpoint">`providers.kubernetesGateway.endpoint`</a> | Server endpoint URL.<br />More information [here](#endpoint). | "" | No |
| <a id="opt-providers-kubernetesGateway-experimentalChannel" href="#opt-providers-kubernetesGateway-experimentalChannel" title="#opt-providers-kubernetesGateway-experimentalChannel">`providers.kubernetesGateway.experimentalChannel`</a> | Toggles support for the Experimental Channel resources ([Gateway API release channels documentation](https://gateway-api.sigs.k8s.io/concepts/versioning/#release-channels)).<br />(ex: `TCPRoute`) | false | No |
| <a id="opt-providers-kubernetesGateway-token" href="#opt-providers-kubernetesGateway-token" title="#opt-providers-kubernetesGateway-token">`providers.kubernetesGateway.token`</a> | Bearer token used for the Kubernetes client configuration. | "" | No |
| <a id="opt-providers-kubernetesGateway-certAuthFilePath" href="#opt-providers-kubernetesGateway-certAuthFilePath" title="#opt-providers-kubernetesGateway-certAuthFilePath">`providers.kubernetesGateway.certAuthFilePath`</a> | Path to the certificate authority file.<br />Used for the Kubernetes client configuration. | "" | No |
| <a id="opt-providers-kubernetesGateway-namespaces" href="#opt-providers-kubernetesGateway-namespaces" title="#opt-providers-kubernetesGateway-namespaces">`providers.kubernetesGateway.namespaces`</a> | Array of namespaces to watch.<br />If left empty, watch all namespaces. | [] | No |
| <a id="opt-providers-kubernetesGateway-labelselector" href="#opt-providers-kubernetesGateway-labelselector" title="#opt-providers-kubernetesGateway-labelselector">`providers.kubernetesGateway.labelselector`</a> | Allow filtering on `GatewayClass` only. If left empty, Traefik processes all GatewayClass objects in the configured namespaces.<br />See [label-selectors](https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#label-selectors) for details. | "" | No |
| <a id="opt-providers-kubernetesGateway-throttleDuration" href="#opt-providers-kubernetesGateway-throttleDuration" title="#opt-providers-kubernetesGateway-throttleDuration">`providers.kubernetesGateway.throttleDuration`</a> | Minimum amount of time to wait between two Kubernetes events before producing a new configuration.<br />This prevents a Kubernetes cluster that updates many times per second from continuously changing your Traefik configuration.<br />If empty, every event is caught. | 0s | No |
| <a id="opt-providers-kubernetesGateway-nativeLBByDefault" href="#opt-providers-kubernetesGateway-nativeLBByDefault" title="#opt-providers-kubernetesGateway-nativeLBByDefault">`providers.kubernetesGateway.nativeLBByDefault`</a> | 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 |
| <a id="opt-providers-kubernetesGateway-statusAddress-hostname" href="#opt-providers-kubernetesGateway-statusAddress-hostname" title="#opt-providers-kubernetesGateway-statusAddress-hostname">`providers.kubernetesGateway.`<br />`statusAddress.hostname`</a> | Hostname copied to the Gateway `status.addresses`. | "" | No |
| <a id="opt-providers-kubernetesGateway-statusAddress-ip" href="#opt-providers-kubernetesGateway-statusAddress-ip" title="#opt-providers-kubernetesGateway-statusAddress-ip">`providers.kubernetesGateway.`<br />`statusAddress.ip`</a> | IP address copied to the Gateway `status.addresses`, and currently only supports one IP value (IPv4 or IPv6). | "" | No |
| <a id="opt-providers-kubernetesGateway-statusAddress-service-namespace" href="#opt-providers-kubernetesGateway-statusAddress-service-namespace" title="#opt-providers-kubernetesGateway-statusAddress-service-namespace">`providers.kubernetesGateway.`<br />`statusAddress.service.namespace`</a> | The namespace of the Kubernetes service to copy status addresses from.<br />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 |
| <a id="opt-providers-kubernetesGateway-statusAddress-service-name" href="#opt-providers-kubernetesGateway-statusAddress-service-name" title="#opt-providers-kubernetesGateway-statusAddress-service-name">`providers.kubernetesGateway.`<br />`statusAddress.service.name`</a> | The name of the Kubernetes service to copy status addresses from.<br />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 |
| <a id="opt-providers-kubernetesGateway-crossProviderNamespaces" href="#opt-providers-kubernetesGateway-crossProviderNamespaces" title="#opt-providers-kubernetesGateway-crossProviderNamespaces">`providers.kubernetesGateway.crossProviderNamespaces`</a> | List of namespaces from which Gateway API routes (`HTTPRoute`, `TCPRoute`, `TLSRoute`) are allowed to declare a `backendRef` of kind `TraefikService`.<br />When unset, all namespaces are allowed. When set to `[]`, every such backendRef is rejected and the route is dropped. | [] | No |
| Field | Description | Default | Required |
|:----------------------------------------------------------------------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:--------|:---------|
| <a id="opt-providers-providersThrottleDuration" href="#opt-providers-providersThrottleDuration" title="#opt-providers-providersThrottleDuration">`providers.providersThrottleDuration`</a> | Minimum amount of time to wait for, after a configuration reload, before taking into account any new configuration refresh event.<br />If multiple events occur within this time, only the most recent one is taken into account, and all others are discarded.<br />**This option cannot be set per provider, but the throttling algorithm applies to each of them independently.** | 2s | No |
| <a id="opt-providers-kubernetesGateway-endpoint" href="#opt-providers-kubernetesGateway-endpoint" title="#opt-providers-kubernetesGateway-endpoint">`providers.kubernetesGateway.endpoint`</a> | Server endpoint URL.<br />More information [here](#endpoint). | "" | No |
| <a id="opt-providers-kubernetesGateway-experimentalChannel" href="#opt-providers-kubernetesGateway-experimentalChannel" title="#opt-providers-kubernetesGateway-experimentalChannel">`providers.kubernetesGateway.experimentalChannel`</a> | Toggles support for the Experimental Channel resources ([Gateway API release channels documentation](https://gateway-api.sigs.k8s.io/concepts/versioning/#release-channels)).<br />(ex: `TCPRoute`) | false | No |
| <a id="opt-providers-kubernetesGateway-token" href="#opt-providers-kubernetesGateway-token" title="#opt-providers-kubernetesGateway-token">`providers.kubernetesGateway.token`</a> | Bearer token used for the Kubernetes client configuration. | "" | No |
| <a id="opt-providers-kubernetesGateway-certAuthFilePath" href="#opt-providers-kubernetesGateway-certAuthFilePath" title="#opt-providers-kubernetesGateway-certAuthFilePath">`providers.kubernetesGateway.certAuthFilePath`</a> | Path to the certificate authority file.<br />Used for the Kubernetes client configuration. | "" | No |
| <a id="opt-providers-kubernetesGateway-namespaces" href="#opt-providers-kubernetesGateway-namespaces" title="#opt-providers-kubernetesGateway-namespaces">`providers.kubernetesGateway.namespaces`</a> | Array of namespaces to watch.<br />If left empty, watch all namespaces. | [] | No |
| <a id="opt-providers-kubernetesGateway-labelselector" href="#opt-providers-kubernetesGateway-labelselector" title="#opt-providers-kubernetesGateway-labelselector">`providers.kubernetesGateway.labelselector`</a> | Allow filtering on `GatewayClass` only. If left empty, Traefik processes all GatewayClass objects in the configured namespaces.<br />See [label-selectors](https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#label-selectors) for details. | "" | No |
| <a id="opt-providers-kubernetesGateway-throttleDuration" href="#opt-providers-kubernetesGateway-throttleDuration" title="#opt-providers-kubernetesGateway-throttleDuration">`providers.kubernetesGateway.throttleDuration`</a> | Minimum amount of time to wait between two Kubernetes events before producing a new configuration.<br />This prevents a Kubernetes cluster that updates many times per second from continuously changing your Traefik configuration.<br />If empty, every event is caught. | 0s | No |
| <a id="opt-providers-kubernetesGateway-nativeLBByDefault" href="#opt-providers-kubernetesGateway-nativeLBByDefault" title="#opt-providers-kubernetesGateway-nativeLBByDefault">`providers.kubernetesGateway.nativeLBByDefault`</a> | 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 |
| <a id="opt-providers-kubernetesGateway-statusAddress-hostname" href="#opt-providers-kubernetesGateway-statusAddress-hostname" title="#opt-providers-kubernetesGateway-statusAddress-hostname">`providers.kubernetesGateway.`<br />`statusAddress.hostname`</a> | Hostname copied to the Gateway `status.addresses`. | "" | No |
| <a id="opt-providers-kubernetesGateway-statusAddress-ip" href="#opt-providers-kubernetesGateway-statusAddress-ip" title="#opt-providers-kubernetesGateway-statusAddress-ip">`providers.kubernetesGateway.`<br />`statusAddress.ip`</a> | IP address copied to the Gateway `status.addresses`, and currently only supports one IP value (IPv4 or IPv6). | "" | No |
| <a id="opt-providers-kubernetesGateway-statusAddress-service-namespace" href="#opt-providers-kubernetesGateway-statusAddress-service-namespace" title="#opt-providers-kubernetesGateway-statusAddress-service-namespace">`providers.kubernetesGateway.`<br />`statusAddress.service.namespace`</a> | The namespace of the Kubernetes service to copy status addresses from.<br />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 |
| <a id="opt-providers-kubernetesGateway-statusAddress-service-name" href="#opt-providers-kubernetesGateway-statusAddress-service-name" title="#opt-providers-kubernetesGateway-statusAddress-service-name">`providers.kubernetesGateway.`<br />`statusAddress.service.name`</a> | The name of the Kubernetes service to copy status addresses from.<br />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 |
| <a id="opt-providers-kubernetesGateway-crossProviderNamespaces" href="#opt-providers-kubernetesGateway-crossProviderNamespaces" title="#opt-providers-kubernetesGateway-crossProviderNamespaces">`providers.kubernetesGateway.crossProviderNamespaces`</a> | List of namespaces from which Gateway API routes (`HTTPRoute`, `TCPRoute`, `TLSRoute`) are allowed to declare a `backendRef` of kind `TraefikService`.<br />When unset, all namespaces are allowed. When set to `[]`, every such backendRef is rejected and the route is dropped. | [] | No |
| <a id="opt-providers-kubernetesgateway-qps" href="#opt-providers-kubernetesgateway-qps" title="#opt-providers-kubernetesgateway-qps">providers.kubernetesgateway.qps</a> | Defines the maximum QPS to the Kubernetes API server. Setting this to a negative value will disable client-side ratelimiting. | 50 | No |
| <a id="opt-providers-kubernetesgateway-burst" href="#opt-providers-kubernetesgateway-burst" title="#opt-providers-kubernetesgateway-burst">providers.kubernetesgateway.burst</a> | Defines the maximum burst of requests to the Kubernetes API server. | 100 | No |
<!-- markdownlint-enable MD013 -->

View file

@ -280,6 +280,7 @@ The following annotations are organized by category for easier navigation.
| <a id="opt-nginx-ingress-kubernetes-ioauth-snippet" href="#opt-nginx-ingress-kubernetes-ioauth-snippet" title="#opt-nginx-ingress-kubernetes-ioauth-snippet">`nginx.ingress.kubernetes.io/auth-snippet`</a> | Supported directives: `proxy_method`, `more_set_headers`, `proxy_set_header`, `more_set_input_headers`, `set`, `if`, `return code [text]`. It supports minimal variable interpolation by using the following NGINX variables: `$scheme`, `$host`, `$http_*`, `$hostname`, `$request_uri`, `$request_method`, `$query_string`, `$args`, `$arg_*`, `$remote_addr`, `$uri`, `$document_uri`, `$server_name`, `$server_port`, `$content_type`, `$content_length`, `$cookie_*`, `$is_args`, `$best_http_host`, `$escaped_request_uri`, `$proxy_add_x_forwarded_for`. |
| <a id="opt-nginx-ingress-kubernetes-ioauth-method" href="#opt-nginx-ingress-kubernetes-ioauth-method" title="#opt-nginx-ingress-kubernetes-ioauth-method">`nginx.ingress.kubernetes.io/auth-method`</a> | This annotation uses the `proxy_method` directive in Nginx. Thus, it can't be defined on an ingress that already have an `auth-snippet` annotation with the `proxy_method` directive. |
| <a id="opt-nginx-ingress-kubernetes-ioauth-response-headers" href="#opt-nginx-ingress-kubernetes-ioauth-response-headers" title="#opt-nginx-ingress-kubernetes-ioauth-response-headers">`nginx.ingress.kubernetes.io/auth-response-headers`</a> | |
| <a id="opt-nginx-ingress-kubernetes-ioenable-global-auth" href="#opt-nginx-ingress-kubernetes-ioenable-global-auth" title="#opt-nginx-ingress-kubernetes-ioenable-global-auth">`nginx.ingress.kubernetes.io/enable-global-auth`</a> | |
### SSL/TLS
@ -456,7 +457,6 @@ In practice, Traefik is slightly more lenient under bursty load, as it smooths o
| <a id="opt-nginx-ingress-kubernetes-ioauth-keepalive-requests" href="#opt-nginx-ingress-kubernetes-ioauth-keepalive-requests" title="#opt-nginx-ingress-kubernetes-ioauth-keepalive-requests">`nginx.ingress.kubernetes.io/auth-keepalive-requests`</a> | |
| <a id="opt-nginx-ingress-kubernetes-ioauth-keepalive-timeout" href="#opt-nginx-ingress-kubernetes-ioauth-keepalive-timeout" title="#opt-nginx-ingress-kubernetes-ioauth-keepalive-timeout">`nginx.ingress.kubernetes.io/auth-keepalive-timeout`</a> | |
| <a id="opt-nginx-ingress-kubernetes-ioauth-proxy-set-headers" href="#opt-nginx-ingress-kubernetes-ioauth-proxy-set-headers" title="#opt-nginx-ingress-kubernetes-ioauth-proxy-set-headers">`nginx.ingress.kubernetes.io/auth-proxy-set-headers`</a> | |
| <a id="opt-nginx-ingress-kubernetes-ioenable-global-auth" href="#opt-nginx-ingress-kubernetes-ioenable-global-auth" title="#opt-nginx-ingress-kubernetes-ioenable-global-auth">`nginx.ingress.kubernetes.io/enable-global-auth`</a> | |
| <a id="opt-nginx-ingress-kubernetes-iodisable-proxy-intercept-errors" href="#opt-nginx-ingress-kubernetes-iodisable-proxy-intercept-errors" title="#opt-nginx-ingress-kubernetes-iodisable-proxy-intercept-errors">`nginx.ingress.kubernetes.io/disable-proxy-intercept-errors`</a> | |
| <a id="opt-nginx-ingress-kubernetes-iolimit-rate-after" href="#opt-nginx-ingress-kubernetes-iolimit-rate-after" title="#opt-nginx-ingress-kubernetes-iolimit-rate-after">`nginx.ingress.kubernetes.io/limit-rate-after`</a> | |
| <a id="opt-nginx-ingress-kubernetes-iolimit-rate" href="#opt-nginx-ingress-kubernetes-iolimit-rate" title="#opt-nginx-ingress-kubernetes-iolimit-rate">`nginx.ingress.kubernetes.io/limit-rate`</a> | |

19
go.mod
View file

@ -22,7 +22,7 @@ require (
github.com/docker/cli v29.4.0+incompatible
github.com/docker/go-connections v0.6.0
github.com/fatih/structs v1.1.0
github.com/fsnotify/fsnotify v1.9.0
github.com/fsnotify/fsnotify v1.10.1
github.com/go-acme/lego/v4 v4.35.2
github.com/go-kit/kit v0.13.0
github.com/go-kit/log v0.2.1
@ -57,7 +57,7 @@ require (
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // No tag on the repo.
github.com/prometheus/client_golang v1.23.2
github.com/prometheus/client_model v0.6.2
github.com/quic-go/quic-go v0.59.0
github.com/quic-go/quic-go v0.59.1
github.com/redis/go-redis/v9 v9.8.0
github.com/rs/zerolog v1.33.0
github.com/sirupsen/logrus v1.9.4
@ -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

52
go.sum
View file

@ -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=
@ -1004,8 +1004,8 @@ github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMo
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU=
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx59Ho=
github.com/fsnotify/fsnotify v1.10.1/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo=
github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM=
@ -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=
@ -1811,8 +1811,8 @@ github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05Zp
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw=
github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
github.com/quic-go/quic-go v0.59.1 h1:0Gmua0HW1Tv7ANR7hUYwRyD0MG5OJfgvYSZasGZzBic=
github.com/quic-go/quic-go v0.59.1/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
github.com/redis/go-redis/v9 v9.8.0 h1:q3nRvjrlge/6UD7eTu/DSg2uYiU2mCL0G/uzBWqhicI=
@ -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=

View file

@ -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"

View file

@ -0,0 +1,53 @@
[global]
checkNewVersion = false
sendAnonymousUsage = false
[log]
level = "DEBUG"
[entryPoints]
[entryPoints.websecure]
address = ":4443"
[entryPoints.websecure.http.tls]
[entryPoints.websecure-options]
address = ":4444"
[entryPoints.websecure-options.http.tls]
options = "foo"
[api]
insecure = true
[providers.file]
filename = "{{ .SelfFilename }}"
## dynamic configuration ##
[http.routers]
[http.routers.router1]
entryPoints = ["websecure"]
service = "service1"
rule = "Host(`snitest.com`)"
[http.routers.router2]
entryPoints = ["websecure-options"]
service = "service1"
rule = "Host(`snitest.org`)"
[http.services]
[http.services.service1]
[http.services.service1.loadBalancer]
[[http.services.service1.loadBalancer.servers]]
url = "http://127.0.0.1:9010"
[[tls.certificates]]
certFile = "fixtures/https/snitest.com.cert"
keyFile = "fixtures/https/snitest.com.key"
[[tls.certificates]]
certFile = "fixtures/https/snitest.org.cert"
keyFile = "fixtures/https/snitest.org.key"
[tls.options]
[tls.options.foo]
maxVersion = "VersionTLS12"

View file

@ -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"
@ -149,7 +150,69 @@ func (s *HTTPSSuite) TestWithSNIConfigRoute() {
require.NoError(s.T(), err)
}
// TestWithTLSOptions verifies that traefik routes the requests with the associated tls options.
// TestWithEntryPointTLSConfig verifies that a router relying on the entry point
// TLS configuration (without an explicit router TLS section) is served over HTTPS,
// including when the entry point references user-defined TLS options.
// Regression test for https://github.com/traefik/traefik/issues/13289.
func (s *HTTPSSuite) TestWithEntryPointTLSConfig() {
file := s.adaptFile("fixtures/https/https_entrypoint_tls.toml", struct{}{})
s.traefikCmd(withConfigFile(file))
// wait for Traefik
err := try.GetRequest("http://127.0.0.1:8080/api/rawdata", 1*time.Second, try.BodyContains("Host(`snitest.com`)"))
require.NoError(s.T(), err)
backend := startTestServer("9010", http.StatusNoContent, "")
defer backend.Close()
err = try.GetRequest(backend.URL, 1*time.Second, try.StatusCodeIs(http.StatusNoContent))
require.NoError(s.T(), err)
tr := &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
ServerName: "snitest.com",
},
}
req, err := http.NewRequest(http.MethodGet, "https://127.0.0.1:4443/", nil)
require.NoError(s.T(), err)
req.Host = tr.TLSClientConfig.ServerName
req.Header.Set("Host", tr.TLSClientConfig.ServerName)
req.Header.Set("Accept", "*/*")
err = try.RequestWithTransport(req, 30*time.Second, tr, try.HasCn(tr.TLSClientConfig.ServerName), try.StatusCodeIs(http.StatusNoContent))
require.NoError(s.T(), err)
// The websecure-options entry point references the user-defined "foo" TLS options (maxVersion VersionTLS12).
// A request with no router-level TLS must still have these options resolved and applied.
trOptions := &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
ServerName: "snitest.org",
},
}
req, err = http.NewRequest(http.MethodGet, "https://127.0.0.1:4444/", nil)
require.NoError(s.T(), err)
req.Host = trOptions.TLSClientConfig.ServerName
req.Header.Set("Host", trOptions.TLSClientConfig.ServerName)
req.Header.Set("Accept", "*/*")
err = try.RequestWithTransport(req, 30*time.Second, trOptions, try.HasCn(trOptions.TLSClientConfig.ServerName), try.StatusCodeIs(http.StatusNoContent))
require.NoError(s.T(), err)
// A TLS 1.3-only client must fail the handshake, proving the "foo" options
// (resolved from the entry point) are effectively enforced.
_, err = tls.Dial("tcp", "127.0.0.1:4444", &tls.Config{
InsecureSkipVerify: true,
ServerName: "snitest.org",
MinVersion: tls.VersionTLS13,
})
assert.Error(s.T(), err)
}
// TestWithTLSOptions verifies that traefik routes the requests with the associated tls options.
func (s *HTTPSSuite) TestWithTLSOptions() {
file := s.adaptFile("fixtures/https/https_tls_options.toml", struct{}{})
s.traefikCmd(withConfigFile(file))
@ -352,7 +415,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 +1146,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 +1177,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 +1184,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 +1219,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 +1255,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)
}
}

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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.

View file

@ -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/"},

View file

@ -65,6 +65,10 @@ func (p *authTLSPassCertificateToUpstream) ServeHTTP(rw http.ResponseWriter, req
if req.TLS == nil || len(req.TLS.PeerCertificates) == 0 {
logger.Debug().Msg("Tried to extract a certificate on a request without mutual TLS")
req.Header.Set(sslClientVerify, "NONE")
// Prevent client-supplied values from reaching the upstream on the no-mTLS path.
req.Header.Del(sslClientCert)
req.Header.Del(sslClientSubjectDN)
req.Header.Del(sslClientIssuerDN)
p.next.ServeHTTP(rw, req)
return
}

View file

@ -360,6 +360,26 @@ func TestAuthTLSPassCertificateToUpstream(t *testing.T) {
}
}
func TestAuthTLSNoMTLSClearsCertHeaders(t *testing.T) {
config := dynamic.AuthTLSPassCertificateToUpstream{
ClientAuthType: tls.VerifyClientCertIfGiven,
}
handler, err := NewAuthTLSPassCertificateToUpstream(t.Context(), next, config, "test")
require.NoError(t, err)
req := testhelpers.MustNewRequest(http.MethodGet, "http://example.com/foo", nil)
req.Header.Set(sslClientCert, "client-cert")
req.Header.Set(sslClientSubjectDN, "CN=client")
req.Header.Set(sslClientIssuerDN, "CN=client-CA")
handler.ServeHTTP(httptest.NewRecorder(), req)
assert.Equal(t, "NONE", req.Header.Get(sslClientVerify))
assert.Empty(t, req.Header.Get(sslClientCert))
assert.Empty(t, req.Header.Get(sslClientSubjectDN))
assert.Empty(t, req.Header.Get(sslClientIssuerDN))
}
func buildTLSWith(certContents []string) *cryptoTLS.ConnectionState {
var peerCertificates []*x509.Certificate

View file

@ -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

View file

@ -49,7 +49,7 @@ func newRedisLimiter(ctx context.Context, rate rate.Limit, burst int64, maxDelay
}
if config.Redis.WriteTimeout != nil {
if *config.Redis.ReadTimeout > 0 {
if *config.Redis.WriteTimeout > 0 {
options.WriteTimeout = time.Duration(*config.Redis.WriteTimeout)
} else {
options.WriteTimeout = -1

View file

@ -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 ""
}

View file

@ -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)
})
}
}

View file

@ -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

View file

@ -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)

View file

@ -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
}

View file

@ -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,
},
}

View file

@ -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)

View file

@ -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{

View file

@ -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)

View file

@ -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
}
}

View file

@ -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{
{

View file

@ -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.
@ -718,20 +721,27 @@ func (c *clientWrapper) UpdateBackendTLSPolicyStatus(ctx context.Context, policy
ancestorStatuses := make([]gatev1.PolicyAncestorStatus, len(status.Ancestors))
copy(ancestorStatuses, status.Ancestors)
// keep statuses added by other gateway controllers,
// and statuses for Traefik gateway controller but not for the same Gateway as the one in parameter (AncestorRef).
for _, ancestorStatus := range currentPolicy.Status.Ancestors {
// Keep statuses added by other gateway controllers.
if ancestorStatus.ControllerName != controllerName {
ancestorStatuses = append(ancestorStatuses, ancestorStatus)
continue
}
// Keep statuses added by Traefik for other ancestors.
// A BackendTLSPolicy can target services attached to different listeners.
if !slices.ContainsFunc(status.Ancestors, func(s gatev1.PolicyAncestorStatus) bool {
return reflect.DeepEqual(s.AncestorRef, ancestorStatus.AncestorRef)
}) {
ancestorStatuses = append(ancestorStatuses, ancestorStatus)
}
}
if len(ancestorStatuses) > 16 {
return fmt.Errorf("failed to update BackendTLSPolicy %s/%s status: PolicyAncestor statuses count exceeds 16", policy.Namespace, policy.Name)
}
// do not update status when nothing has changed.
// Do not update status when nothing has changed.
if policyAncestorStatusesEqual(currentPolicy.Status.Ancestors, ancestorStatuses) {
return nil
}

View file

@ -468,6 +468,12 @@ func (p *Provider) loadHTTPServers(ctx context.Context, namespace string, route
var serversTransport *dynamic.ServersTransport
for _, policy := range backendTLSPolicies {
for _, targetRef := range policy.Spec.TargetRefs {
// Skip targetRefs that doesn't match the backendRef,
// since a BackendTLSPolicy can select multiple services.
if targetRef.Name != backendRef.Name {
continue
}
// Skip the targetRef if the sectionName doesn't match the backendRef port.
if targetRef.SectionName != nil && svcPort.Name != string(*targetRef.SectionName) {
continue
}

View file

@ -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 {

View file

@ -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

View file

@ -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 {

View file

@ -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 {

View file

@ -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

View file

@ -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,21 +158,97 @@ 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
}
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 {
if cfg.HTTP != nil && len(cfg.HTTP.Models) > 0 {
rts := make(map[string]*dynamic.Router)

View file

@ -176,6 +176,7 @@ func (c *ConfigurationWatcher) applyConfigurations(ctx context.Context) {
conf := mergeConfiguration(newConfigs.DeepCopy(), c.defaultEntryPoints)
conf = applyModel(conf)
conf = resolveHTTPTLSOptions(conf)
for _, listener := range c.configurationListeners {
listener(conf)

View file

@ -992,3 +992,52 @@ func TestConfigurationWatcher_MultipleTransformers(t *testing.T) {
assert.Equal(t, 1, callCount1)
assert.Equal(t, 1, callCount2)
}
// TestEntryPointTLSResolvedOptions is a regression test for
// https://github.com/traefik/traefik/issues/13289: a router whose TLS
// configuration comes from the entry point (and not from an explicit router TLS
// section) must still have its TLS options resolved in the published configuration.
func TestEntryPointTLSResolvedOptions(t *testing.T) {
routinesPool := safe.NewPool(t.Context())
t.Cleanup(routinesPool.Stop)
pvd := &mockProvider{
messages: []dynamic.Message{{
ProviderName: "internal",
Configuration: &dynamic.Configuration{
HTTP: &dynamic.HTTPConfiguration{
Routers: map[string]*dynamic.Router{
"foo": {
EntryPoints: []string{"websecure"},
Rule: "Host(`foo.example.com`)",
Service: "service",
},
},
Models: map[string]*dynamic.Model{
"websecure": {
TLS: &dynamic.RouterTLSConfig{},
},
},
},
},
}},
}
watcher := NewConfigurationWatcher(routinesPool, pvd, []string{}, "")
run := make(chan struct{})
watcher.AddListener(func(conf dynamic.Configuration) {
router := conf.HTTP.Routers["foo@internal"]
if router == nil || router.TLS == nil {
return
}
assert.Equal(t, "default", router.TLS.ResolvedOptions)
close(run)
})
watcher.Start()
t.Cleanup(watcher.Stop)
<-run
}

29
pkg/server/conncontext.go Normal file
View file

@ -0,0 +1,29 @@
package server
import (
"context"
"net"
)
type connContextFunc func(context.Context, net.Conn) context.Context
type multipleConnContext struct {
fns []connContextFunc
}
func (m *multipleConnContext) AddConnContextFunc(fn connContextFunc) {
m.fns = append(m.fns, fn)
}
func (m *multipleConnContext) Build() connContextFunc {
if len(m.fns) == 0 {
return nil
}
return func(ctx context.Context, c net.Conn) context.Context {
for _, contextFunc := range m.fns {
ctx = contextFunc(ctx, c)
}
return ctx
}
}

View file

@ -0,0 +1,26 @@
package server
import (
"context"
"net"
"testing"
"github.com/stretchr/testify/require"
)
type keyTest string
func TestConnContext(t *testing.T) {
var connContext multipleConnContext
connContext.AddConnContextFunc(func(ctx context.Context, _ net.Conn) context.Context {
return context.WithValue(ctx, keyTest("test"), "test")
})
connContext.AddConnContextFunc(func(ctx context.Context, _ net.Conn) context.Context {
return context.WithValue(ctx, keyTest("test2"), "test2")
})
ctx := connContext.Build()(context.Background(), nil)
require.Equal(t, "test", ctx.Value(keyTest("test")))
require.Equal(t, "test2", ctx.Value(keyTest("test2")))
}

View file

@ -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)

View file

@ -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).
@ -185,82 +168,41 @@ func (m *Manager) buildEntryPointHandler(ctx context.Context, configs map[string
// # Otherwise, it will fallback to the default TLS config.
if tlsOptionsName != traefiktls.DefaultTLSConfigName {
logger.Warn().Msgf("No domain found in rule %v, the TLS option %s cannot be applied", routerHTTPConfig.Rule, tlsOptionsName)
routerHTTPConfig.AddError(fmt.Errorf("no domain found in rule %v, the TLS option %s cannot be applied", routerHTTPConfig.Rule, tlsOptionsName), false)
}
}
if len(domains) > 0 && routerHTTPConfig.TLS.ResolvedOptions != tlsOptionsName {
logger.Warn().Msg("Found different TLS options for routers on the same host, so using the default TLS options instead.")
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 +341,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)

View file

@ -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)
})
}
}

View file

@ -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

View file

@ -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",
}
}

View file

@ -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
}

View file

@ -2,6 +2,7 @@ package server
import (
"context"
"crypto/tls"
"errors"
"expvar"
"fmt"
@ -677,6 +678,44 @@ func newHTTPServer(ctx context.Context, ln net.Listener, configuration *static.E
handler = denyFragment(handler)
var connContext multipleConnContext
connContext.AddConnContextFunc(func(ctx context.Context, c net.Conn) context.Context {
// This adds an empty struct in order to store a RoundTripper in the ConnContext in case of Kerberos or NTLM.
ctx = service.AddTransportOnContext(ctx)
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)) {
connContext.AddConnContextFunc(func(ctx context.Context, c net.Conn) context.Context {
cState := &connState{Start: time.Now()}
if debugConnection {
clientConnectionStatesMu.Lock()
clientConnectionStates[getConnKey(c)] = cState
clientConnectionStatesMu.Unlock()
}
return context.WithValue(ctx, connStateKey, cState)
})
}
var connState func(c net.Conn, state http.ConnState)
if debugConnection {
connState = func(c net.Conn, state http.ConnState) {
clientConnectionStatesMu.Lock()
if clientConnectionStates[getConnKey(c)] != nil {
clientConnectionStates[getConnKey(c)].State = state.String()
}
clientConnectionStatesMu.Unlock()
}
}
serverHTTP := &http.Server{
Protocols: &protocols,
Handler: handler,
@ -690,37 +729,8 @@ func newHTTPServer(ctx context.Context, ln net.Listener, configuration *static.E
MaxDecoderHeaderTableSize: int(configuration.HTTP2.MaxDecoderHeaderTableSize),
MaxEncoderHeaderTableSize: int(configuration.HTTP2.MaxEncoderHeaderTableSize),
},
}
if debugConnection || (configuration.Transport != nil && (configuration.Transport.KeepAliveMaxTime > 0 || configuration.Transport.KeepAliveMaxRequests > 0)) {
serverHTTP.ConnContext = func(ctx context.Context, c net.Conn) context.Context {
cState := &connState{Start: time.Now()}
if debugConnection {
clientConnectionStatesMu.Lock()
clientConnectionStates[getConnKey(c)] = cState
clientConnectionStatesMu.Unlock()
}
return context.WithValue(ctx, connStateKey, cState)
}
if debugConnection {
serverHTTP.ConnState = func(c net.Conn, state http.ConnState) {
clientConnectionStatesMu.Lock()
if clientConnectionStates[getConnKey(c)] != nil {
clientConnectionStates[getConnKey(c)].State = state.String()
}
clientConnectionStatesMu.Unlock()
}
}
}
prevConnContext := serverHTTP.ConnContext
serverHTTP.ConnContext = func(ctx context.Context, c net.Conn) context.Context {
// This adds an empty struct in order to store a RoundTripper in the ConnContext in case of Kerberos or NTLM.
ctx = service.AddTransportOnContext(ctx)
if prevConnContext != nil {
return prevConnContext(ctx, c)
}
return ctx
ConnContext: connContext.Build(),
ConnState: connState,
}
listener := newHTTPForwarder(ln)

View file

@ -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
}

View file

@ -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)

View file

@ -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 ""
}

View file

@ -4,11 +4,11 @@ RepositoryName = "traefik"
OutputType = "file"
FileName = "traefik_changelog.md"
# example new bugfix v3.7.1
# example new bugfix v3.7.4
CurrentRef = "v3.7"
PreviousRef = "v3.7.0"
PreviousRef = "v3.7.3"
BaseBranch = "v3.7"
FutureCurrentRefName = "v3.7.1"
FutureCurrentRefName = "v3.7.4"
ThresholdPreviousRef = 10000
ThresholdCurrentRef = 10000

View file

@ -64,7 +64,7 @@
"framer-motion": "^11.18.2",
"globals": "^16.0.0",
"jest-extended": "^4.0.2",
"jsdom": "^24.0.0",
"jsdom": "29.1.1",
"lodash": "4.18.1",
"msw": "^2.1.7",
"query-string": "^6.9.0",
@ -75,7 +75,7 @@
"react-helmet-async": "^2.0.4",
"react-icons": "^5.0.1",
"react-infinite-scroll-hook": "^4.1.1",
"react-router-dom": "6.30.2",
"react-router-dom": "6.30.4",
"swr": "^2.2.4",
"typescript": "^5.2.2",
"typescript-eslint": "^8.38.0",
@ -98,6 +98,8 @@
"packageManager": "yarn@4.13.0",
"resolutions": {
"form-data": "4.0.4",
"js-yaml": "4.1.1"
"js-yaml": "4.1.1",
"@csstools/css-syntax-patches-for-csstree": "1.1.4",
"undici": "7.27.0"
}
}

View file

@ -22,16 +22,43 @@ __metadata:
languageName: node
linkType: hard
"@asamuzakjp/css-color@npm:^3.1.1":
version: 3.1.1
resolution: "@asamuzakjp/css-color@npm:3.1.1"
"@asamuzakjp/css-color@npm:^5.1.11":
version: 5.1.11
resolution: "@asamuzakjp/css-color@npm:5.1.11"
dependencies:
"@csstools/css-calc": "npm:^2.1.2"
"@csstools/css-color-parser": "npm:^3.0.8"
"@csstools/css-parser-algorithms": "npm:^3.0.4"
"@csstools/css-tokenizer": "npm:^3.0.3"
lru-cache: "npm:^10.4.3"
checksum: 10c0/4abb010fd29de8acae8571eba738468c22cb45a1f77647df3c59a80f1c83d83d728cae3ebbf99e5c73f2517761abaaffbe5e4176fc46b5f9bf60f1478463b51e
"@asamuzakjp/generational-cache": "npm:^1.0.1"
"@csstools/css-calc": "npm:^3.2.0"
"@csstools/css-color-parser": "npm:^4.1.0"
"@csstools/css-parser-algorithms": "npm:^4.0.0"
"@csstools/css-tokenizer": "npm:^4.0.0"
checksum: 10c0/32720bdff8daea6a8847aba6cdfae55baa3b4a2690b51d21db7f0382bbd183f3d9f2d5126df50afd889062635684b2819e47113629ee2e80c99389e75f48d060
languageName: node
linkType: hard
"@asamuzakjp/dom-selector@npm:^7.1.1":
version: 7.1.1
resolution: "@asamuzakjp/dom-selector@npm:7.1.1"
dependencies:
"@asamuzakjp/generational-cache": "npm:^1.0.1"
"@asamuzakjp/nwsapi": "npm:^2.3.9"
bidi-js: "npm:^1.0.3"
css-tree: "npm:^3.2.1"
is-potential-custom-element-name: "npm:^1.0.1"
checksum: 10c0/8cec1c618781c94de5836a215bbe5aafb4d8b835b18c51faf8547f4574afa39f92def3951e40123860062467613dd825f1e1600ff32e8045cc099a91796dcfb8
languageName: node
linkType: hard
"@asamuzakjp/generational-cache@npm:^1.0.1":
version: 1.0.1
resolution: "@asamuzakjp/generational-cache@npm:1.0.1"
checksum: 10c0/1de62de43764e13fca3b9a31b7ea9b1bf0780fe053d266e40378a19ff8c66b543e011e6a0df02d410cd59bf981126706f176cdbb938985165202c4a079fe1057
languageName: node
linkType: hard
"@asamuzakjp/nwsapi@npm:^2.3.9":
version: 2.3.9
resolution: "@asamuzakjp/nwsapi@npm:2.3.9"
checksum: 10c0/869b81382e775499c96c45c6dbe0d0766a6da04bcf0abb79f5333535c4e19946851acaa43398f896e2ecc5a1de9cf3db7cf8c4b1afac1ee3d15e21584546d74d
languageName: node
linkType: hard
@ -301,6 +328,17 @@ __metadata:
languageName: node
linkType: hard
"@bramus/specificity@npm:^2.4.2":
version: 2.4.2
resolution: "@bramus/specificity@npm:2.4.2"
dependencies:
css-tree: "npm:^3.0.0"
bin:
specificity: bin/cli.js
checksum: 10c0/c5f4e04e0bca0d2202598207a5eb0733c8109d12a68a329caa26373bec598d99db5bb785b8865fefa00fc01b08c6068138807ceb11a948fe15e904ed6cf4ba72
languageName: node
linkType: hard
"@bundled-es-modules/cookie@npm:^2.0.1":
version: 2.0.1
resolution: "@bundled-es-modules/cookie@npm:2.0.1"
@ -329,49 +367,61 @@ __metadata:
languageName: node
linkType: hard
"@csstools/color-helpers@npm:^5.0.2":
version: 5.0.2
resolution: "@csstools/color-helpers@npm:5.0.2"
checksum: 10c0/bebaddb28b9eb58b0449edd5d0c0318fa88f3cb079602ee27e88c9118070d666dcc4e09a5aa936aba2fde6ba419922ade07b7b506af97dd7051abd08dfb2959b
"@csstools/color-helpers@npm:^6.0.2":
version: 6.0.2
resolution: "@csstools/color-helpers@npm:6.0.2"
checksum: 10c0/4c66574563d7c960010c11e41c2673675baff07c427cca6e8dddffa5777de45770d13ff3efce1c0642798089ad55de52870d9d8141f78db3fa5bba012f2d3789
languageName: node
linkType: hard
"@csstools/css-calc@npm:^2.1.2":
version: 2.1.2
resolution: "@csstools/css-calc@npm:2.1.2"
"@csstools/css-calc@npm:^3.2.0, @csstools/css-calc@npm:^3.2.1":
version: 3.2.1
resolution: "@csstools/css-calc@npm:3.2.1"
peerDependencies:
"@csstools/css-parser-algorithms": ^3.0.4
"@csstools/css-tokenizer": ^3.0.3
checksum: 10c0/34ced30553968ef5d5f9e00e3b90b48c47480cf130e282e99d57ec9b09f803aab8bc06325683e72a1518b5e7180a3da8b533f1b462062757c21989a53b482e1a
"@csstools/css-parser-algorithms": ^4.0.0
"@csstools/css-tokenizer": ^4.0.0
checksum: 10c0/0191c8d1cd4dffa0d3b6bfd1e78a721934b1d7a6c972966e4fdaa72208c6789e8ff443ee81764a32f1e6107825695b5524ef2b4dc1681b5b29230f2a1277e5df
languageName: node
linkType: hard
"@csstools/css-color-parser@npm:^3.0.8":
version: 3.0.8
resolution: "@csstools/css-color-parser@npm:3.0.8"
"@csstools/css-color-parser@npm:^4.1.0":
version: 4.1.1
resolution: "@csstools/css-color-parser@npm:4.1.1"
dependencies:
"@csstools/color-helpers": "npm:^5.0.2"
"@csstools/css-calc": "npm:^2.1.2"
"@csstools/color-helpers": "npm:^6.0.2"
"@csstools/css-calc": "npm:^3.2.1"
peerDependencies:
"@csstools/css-parser-algorithms": ^3.0.4
"@csstools/css-tokenizer": ^3.0.3
checksum: 10c0/90722c5a62ca94e9d578ddf59be604a76400b932bd3d4bd23cb1ae9b7ace8fcf83c06995d2b31f96f4afef24a7cefba79beb11ed7ee4999d7ecfec3869368359
"@csstools/css-parser-algorithms": ^4.0.0
"@csstools/css-tokenizer": ^4.0.0
checksum: 10c0/427bd32f1a8917342a70a6fd97b93bb492aae7c8790e7782b5d6edc8c08064bb8aef0a86099f286db00288f9afea85eb92c46350e9057f5fea058e03a2a09203
languageName: node
linkType: hard
"@csstools/css-parser-algorithms@npm:^3.0.4":
version: 3.0.4
resolution: "@csstools/css-parser-algorithms@npm:3.0.4"
"@csstools/css-parser-algorithms@npm:^4.0.0":
version: 4.0.0
resolution: "@csstools/css-parser-algorithms@npm:4.0.0"
peerDependencies:
"@csstools/css-tokenizer": ^3.0.3
checksum: 10c0/d411f07765e14eede17bccc6bd4f90ff303694df09aabfede3fd104b2dfacfd4fe3697cd25ddad14684c850328f3f9420ebfa9f78380892492974db24ae47dbd
"@csstools/css-tokenizer": ^4.0.0
checksum: 10c0/94558c2428d6ef0ddef542e86e0a8376aa1263a12a59770abb13ba50d7b83086822c75433f32aa2e7fef00555e1cc88292f9ca5bce79aed232bb3fed73b1528d
languageName: node
linkType: hard
"@csstools/css-tokenizer@npm:^3.0.3":
version: 3.0.3
resolution: "@csstools/css-tokenizer@npm:3.0.3"
checksum: 10c0/c31bf410e1244b942e71798e37c54639d040cb59e0121b21712b40015fced2b0fb1ffe588434c5f8923c9cd0017cfc1c1c8f3921abc94c96edf471aac2eba5e5
"@csstools/css-syntax-patches-for-csstree@npm:1.1.4":
version: 1.1.4
resolution: "@csstools/css-syntax-patches-for-csstree@npm:1.1.4"
peerDependencies:
css-tree: ^3.2.1
peerDependenciesMeta:
css-tree:
optional: true
checksum: 10c0/3872a7befb553c53249c87e964ac00f55d059f4574d2cc023e03e1dafc86a5ad19f6a6d05fa2c14fb192e6a4538a73158104cc2e32e0688f27fd841b9ba76568
languageName: node
linkType: hard
"@csstools/css-tokenizer@npm:^4.0.0":
version: 4.0.0
resolution: "@csstools/css-tokenizer@npm:4.0.0"
checksum: 10c0/669cf3d0f9c8e1ffdf8c9955ad8beba0c8cfe03197fe29a4fcbd9ee6f7a18856cfa42c62670021a75183d9ab37f5d14a866e6a9df753a6c07f59e36797a9ea9f
languageName: node
linkType: hard
@ -850,6 +900,18 @@ __metadata:
languageName: node
linkType: hard
"@exodus/bytes@npm:^1.11.0, @exodus/bytes@npm:^1.15.0, @exodus/bytes@npm:^1.6.0":
version: 1.15.1
resolution: "@exodus/bytes@npm:1.15.1"
peerDependencies:
"@noble/hashes": ^1.8.0 || ^2.0.0
peerDependenciesMeta:
"@noble/hashes":
optional: true
checksum: 10c0/333056a6953bbf875d9f3b86c32314de29458d842e5f56f6ef8034b18c2d9660184550093d1bae5de0064043d5e23f54cc03148798d9d29cf5167ac03f2e9f8c
languageName: node
linkType: hard
"@floating-ui/core@npm:^1.6.0":
version: 1.6.9
resolution: "@floating-ui/core@npm:1.6.9"
@ -2417,10 +2479,10 @@ __metadata:
languageName: node
linkType: hard
"@remix-run/router@npm:1.23.1":
version: 1.23.1
resolution: "@remix-run/router@npm:1.23.1"
checksum: 10c0/94ac9632c0070199b8275cd6dffe78eb4c02e8926328937c65561c5c30d7ddf842743df3c8f7df302f00a593dd204846d93667fbbbe3c3641437d7b8f333ed90
"@remix-run/router@npm:1.23.3":
version: 1.23.3
resolution: "@remix-run/router@npm:1.23.3"
checksum: 10c0/73622465dd9e9a2c5a7ced7d1151ea6e9195a8a979c99b4f70a67093eeff7f339daf03a7c6304709a9c27deb2081a8fff6737663aacb33529c1a12a0a0827a17
languageName: node
linkType: hard
@ -3841,13 +3903,6 @@ __metadata:
languageName: node
linkType: hard
"asynckit@npm:^0.4.0":
version: 0.4.0
resolution: "asynckit@npm:0.4.0"
checksum: 10c0/d73e2ddf20c4eb9337e1b3df1a0f6159481050a5de457c55b14ea2e5cb6d90bb69e004c9af54737a5ee0917fcf2c9e25de67777bbe58261847846066ba75bc9d
languageName: node
linkType: hard
"available-typed-arrays@npm:^1.0.7":
version: 1.0.7
resolution: "available-typed-arrays@npm:1.0.7"
@ -3878,6 +3933,15 @@ __metadata:
languageName: node
linkType: hard
"bidi-js@npm:^1.0.3":
version: 1.0.3
resolution: "bidi-js@npm:1.0.3"
dependencies:
require-from-string: "npm:^2.0.2"
checksum: 10c0/fdddea4aa4120a34285486f2267526cd9298b6e8b773ad25e765d4f104b6d7437ab4ba542e6939e3ac834a7570bcf121ee2cf6d3ae7cd7082c4b5bedc8f271e1
languageName: node
linkType: hard
"brace-expansion@npm:^1.1.7":
version: 1.1.11
resolution: "brace-expansion@npm:1.1.11"
@ -4103,15 +4167,6 @@ __metadata:
languageName: node
linkType: hard
"combined-stream@npm:^1.0.8":
version: 1.0.8
resolution: "combined-stream@npm:1.0.8"
dependencies:
delayed-stream: "npm:~1.0.0"
checksum: 10c0/0dbb829577e1b1e839fa82b40c07ffaf7de8a09b935cadd355a73652ae70a88b4320db322f6634a4ad93424292fa80973ac6480986247f1734a1137debf271d5
languageName: node
linkType: hard
"commander@npm:^13.1.0":
version: 13.1.0
resolution: "commander@npm:13.1.0"
@ -4151,6 +4206,16 @@ __metadata:
languageName: node
linkType: hard
"css-tree@npm:^3.0.0, css-tree@npm:^3.2.1":
version: 3.2.1
resolution: "css-tree@npm:3.2.1"
dependencies:
mdn-data: "npm:2.27.1"
source-map-js: "npm:^1.2.1"
checksum: 10c0/1f65e9ccaa56112a4706d6f003dd43d777f0dbcf848e66fd320f823192533581f8dd58daa906cb80622658332d50284d6be13b87a6ab4556cbbfe9ef535bbf7e
languageName: node
linkType: hard
"css-what@npm:^6.1.0":
version: 6.2.2
resolution: "css-what@npm:6.2.2"
@ -4172,16 +4237,6 @@ __metadata:
languageName: node
linkType: hard
"cssstyle@npm:^4.0.1":
version: 4.3.0
resolution: "cssstyle@npm:4.3.0"
dependencies:
"@asamuzakjp/css-color": "npm:^3.1.1"
rrweb-cssom: "npm:^0.8.0"
checksum: 10c0/770ccb288a99257fd0d5b129e03878f848e922d3b017358acb02e8dd530e8f0c7c6f74e6ae5367d715e2da36a490a734b4177fc1b78f3f08eca25f204a56a692
languageName: node
linkType: hard
"csstype@npm:^3.0.2":
version: 3.1.3
resolution: "csstype@npm:3.1.3"
@ -4203,13 +4258,13 @@ __metadata:
languageName: node
linkType: hard
"data-urls@npm:^5.0.0":
version: 5.0.0
resolution: "data-urls@npm:5.0.0"
"data-urls@npm:^7.0.0":
version: 7.0.0
resolution: "data-urls@npm:7.0.0"
dependencies:
whatwg-mimetype: "npm:^4.0.0"
whatwg-url: "npm:^14.0.0"
checksum: 10c0/1b894d7d41c861f3a4ed2ae9b1c3f0909d4575ada02e36d3d3bc584bdd84278e20709070c79c3b3bff7ac98598cb191eb3e86a89a79ea4ee1ef360e1694f92ad
whatwg-mimetype: "npm:^5.0.0"
whatwg-url: "npm:^16.0.0"
checksum: 10c0/08d88ef50d8966a070ffdaa703e1e4b29f01bb2da364dfbc1612b1c2a4caa8045802c9532d81347b21781100132addb36a585071c8323b12cce97973961dee9f
languageName: node
linkType: hard
@ -4298,10 +4353,10 @@ __metadata:
languageName: node
linkType: hard
"decimal.js@npm:^10.4.3":
version: 10.5.0
resolution: "decimal.js@npm:10.5.0"
checksum: 10c0/785c35279df32762143914668df35948920b6c1c259b933e0519a69b7003fc0a5ed2a766b1e1dda02574450c566b21738a45f15e274b47c2ac02072c0d1f3ac3
"decimal.js@npm:^10.6.0":
version: 10.6.0
resolution: "decimal.js@npm:10.6.0"
checksum: 10c0/07d69fbcc54167a340d2d97de95f546f9ff1f69d2b45a02fd7a5292412df3cd9eb7e23065e532a318f5474a2e1bccf8392fdf0443ef467f97f3bf8cb0477e5aa
languageName: node
linkType: hard
@ -4393,13 +4448,6 @@ __metadata:
languageName: node
linkType: hard
"delayed-stream@npm:~1.0.0":
version: 1.0.0
resolution: "delayed-stream@npm:1.0.0"
checksum: 10c0/d758899da03392e6712f042bec80aa293bbe9e9ff1b2634baae6a360113e708b91326594c8a486d475c69d6259afb7efacdc3537bfcda1c6c648e390ce601b19
languageName: node
linkType: hard
"dequal@npm:^2.0.3":
version: 2.0.3
resolution: "dequal@npm:2.0.3"
@ -4499,10 +4547,10 @@ __metadata:
languageName: node
linkType: hard
"entities@npm:^4.5.0":
version: 4.5.0
resolution: "entities@npm:4.5.0"
checksum: 10c0/5b039739f7621f5d1ad996715e53d964035f75ad3b9a4d38c6b3804bb226e282ffeae2443624d8fdd9c47d8e926ae9ac009c54671243f0c3294c26af7cc85250
"entities@npm:^8.0.0":
version: 8.0.0
resolution: "entities@npm:8.0.0"
checksum: 10c0/938e631664c19451823344a351aeeafd74fae2d5fa51e4d5b6ff635afaefd4bacf0f609989888c04c42733f46ffdac15211608267ebb02488005891a4793e94d
languageName: node
linkType: hard
@ -5430,19 +5478,6 @@ __metadata:
languageName: node
linkType: hard
"form-data@npm:4.0.4":
version: 4.0.4
resolution: "form-data@npm:4.0.4"
dependencies:
asynckit: "npm:^0.4.0"
combined-stream: "npm:^1.0.8"
es-set-tostringtag: "npm:^2.1.0"
hasown: "npm:^2.0.2"
mime-types: "npm:^2.1.12"
checksum: 10c0/373525a9a034b9d57073e55eab79e501a714ffac02e7a9b01be1c820780652b16e4101819785e1e18f8d98f0aee866cc654d660a435c378e16a72f2e7cac9695
languageName: node
linkType: hard
"framer-motion@npm:^11.18.2":
version: 11.18.2
resolution: "framer-motion@npm:11.18.2"
@ -5761,12 +5796,12 @@ __metadata:
languageName: node
linkType: hard
"html-encoding-sniffer@npm:^4.0.0":
version: 4.0.0
resolution: "html-encoding-sniffer@npm:4.0.0"
"html-encoding-sniffer@npm:^6.0.0":
version: 6.0.0
resolution: "html-encoding-sniffer@npm:6.0.0"
dependencies:
whatwg-encoding: "npm:^3.1.1"
checksum: 10c0/523398055dc61ac9b34718a719cb4aa691e4166f29187e211e1607de63dc25ac7af52ca7c9aead0c4b3c0415ffecb17326396e1202e2e86ff4bca4c0ee4c6140
"@exodus/bytes": "npm:^1.6.0"
checksum: 10c0/66dc3f6f5539cc3beb814fcbfae7eacf4ec38cf824d6e1425b72039b51a40f4456bd8541ba66f4f4fe09cdf885ab5cd5bae6ec6339d6895a930b2fdb83c53025
languageName: node
linkType: hard
@ -5784,7 +5819,7 @@ __metadata:
languageName: node
linkType: hard
"http-proxy-agent@npm:^7.0.0, http-proxy-agent@npm:^7.0.2":
"http-proxy-agent@npm:^7.0.0":
version: 7.0.2
resolution: "http-proxy-agent@npm:7.0.2"
dependencies:
@ -5794,7 +5829,7 @@ __metadata:
languageName: node
linkType: hard
"https-proxy-agent@npm:^7.0.1, https-proxy-agent@npm:^7.0.5":
"https-proxy-agent@npm:^7.0.1":
version: 7.0.6
resolution: "https-proxy-agent@npm:7.0.6"
dependencies:
@ -5820,7 +5855,7 @@ __metadata:
languageName: node
linkType: hard
"iconv-lite@npm:0.6.3, iconv-lite@npm:^0.6.2":
"iconv-lite@npm:^0.6.2":
version: 0.6.3
resolution: "iconv-lite@npm:0.6.3"
dependencies:
@ -6355,37 +6390,37 @@ __metadata:
languageName: node
linkType: hard
"jsdom@npm:^24.0.0":
version: 24.1.3
resolution: "jsdom@npm:24.1.3"
"jsdom@npm:29.1.1":
version: 29.1.1
resolution: "jsdom@npm:29.1.1"
dependencies:
cssstyle: "npm:^4.0.1"
data-urls: "npm:^5.0.0"
decimal.js: "npm:^10.4.3"
form-data: "npm:^4.0.0"
html-encoding-sniffer: "npm:^4.0.0"
http-proxy-agent: "npm:^7.0.2"
https-proxy-agent: "npm:^7.0.5"
"@asamuzakjp/css-color": "npm:^5.1.11"
"@asamuzakjp/dom-selector": "npm:^7.1.1"
"@bramus/specificity": "npm:^2.4.2"
"@csstools/css-syntax-patches-for-csstree": "npm:^1.1.3"
"@exodus/bytes": "npm:^1.15.0"
css-tree: "npm:^3.2.1"
data-urls: "npm:^7.0.0"
decimal.js: "npm:^10.6.0"
html-encoding-sniffer: "npm:^6.0.0"
is-potential-custom-element-name: "npm:^1.0.1"
nwsapi: "npm:^2.2.12"
parse5: "npm:^7.1.2"
rrweb-cssom: "npm:^0.7.1"
lru-cache: "npm:^11.3.5"
parse5: "npm:^8.0.1"
saxes: "npm:^6.0.0"
symbol-tree: "npm:^3.2.4"
tough-cookie: "npm:^4.1.4"
tough-cookie: "npm:^6.0.1"
undici: "npm:^7.25.0"
w3c-xmlserializer: "npm:^5.0.0"
webidl-conversions: "npm:^7.0.0"
whatwg-encoding: "npm:^3.1.1"
whatwg-mimetype: "npm:^4.0.0"
whatwg-url: "npm:^14.0.0"
ws: "npm:^8.18.0"
webidl-conversions: "npm:^8.0.1"
whatwg-mimetype: "npm:^5.0.0"
whatwg-url: "npm:^16.0.1"
xml-name-validator: "npm:^5.0.0"
peerDependencies:
canvas: ^2.11.2
canvas: ^3.0.0
peerDependenciesMeta:
canvas:
optional: true
checksum: 10c0/e48b342afacd7418a23dac204a62deea729c50f4d072a7c04c09fd32355fdb4335f8779fa79fd0277a2dbeb2d356250a950955719d00047324b251233b11277f
checksum: 10c0/20e2174b09d9d06393cb48e1392b7a1cb7191d6656a6f7b3b8fbf9853b4ab0ef60b4a42c2c55f71b55ca5da50ffa75bcdc6986210963182e7993c6f9cd4f499b
languageName: node
linkType: hard
@ -6595,6 +6630,13 @@ __metadata:
languageName: node
linkType: hard
"lru-cache@npm:^11.3.5":
version: 11.5.1
resolution: "lru-cache@npm:11.5.1"
checksum: 10c0/7b341cea79a8efe9c6a6f20c8757a77eca5b25d7ff983ccf4e11e547b81f6787824baa1c84705251dff84ab4ffac85717ac354b9d02e465f86a9f8b166409979
languageName: node
linkType: hard
"lru-cache@npm:^5.1.1":
version: 5.1.1
resolution: "lru-cache@npm:5.1.1"
@ -6677,6 +6719,13 @@ __metadata:
languageName: node
linkType: hard
"mdn-data@npm:2.27.1":
version: 2.27.1
resolution: "mdn-data@npm:2.27.1"
checksum: 10c0/eb8abf5d22e4d1e090346f5e81b67d23cef14c83940e445da5c44541ad874dc8fb9f6ca236e8258c3a489d9fb5884188a4d7d58773adb9089ac2c0b966796393
languageName: node
linkType: hard
"media-query-parser@npm:^2.0.2":
version: 2.0.2
resolution: "media-query-parser@npm:2.0.2"
@ -6710,22 +6759,6 @@ __metadata:
languageName: node
linkType: hard
"mime-db@npm:1.52.0":
version: 1.52.0
resolution: "mime-db@npm:1.52.0"
checksum: 10c0/0557a01deebf45ac5f5777fe7740b2a5c309c6d62d40ceab4e23da9f821899ce7a900b7ac8157d4548ddbb7beffe9abc621250e6d182b0397ec7f10c7b91a5aa
languageName: node
linkType: hard
"mime-types@npm:^2.1.12":
version: 2.1.35
resolution: "mime-types@npm:2.1.35"
dependencies:
mime-db: "npm:1.52.0"
checksum: 10c0/82fb07ec56d8ff1fc999a84f2f217aa46cb6ed1033fefaabd5785b9a974ed225c90dc72fff460259e66b95b73648596dbcc50d51ed69cdf464af2d237d3149b2
languageName: node
linkType: hard
"mimic-fn@npm:^4.0.0":
version: 4.0.0
resolution: "mimic-fn@npm:4.0.0"
@ -7015,13 +7048,6 @@ __metadata:
languageName: node
linkType: hard
"nwsapi@npm:^2.2.12":
version: 2.2.20
resolution: "nwsapi@npm:2.2.20"
checksum: 10c0/07f4dafa3186aef7c007863e90acd4342a34ba9d44b22f14f644fdb311f6086887e21c2fc15efaa826c2bc39ab2bc841364a1a630e7c87e0cb723ba59d729297
languageName: node
linkType: hard
"object-assign@npm:^4.1.1":
version: 4.1.1
resolution: "object-assign@npm:4.1.1"
@ -7205,12 +7231,12 @@ __metadata:
languageName: node
linkType: hard
"parse5@npm:^7.1.2":
version: 7.2.1
resolution: "parse5@npm:7.2.1"
"parse5@npm:^8.0.1":
version: 8.0.1
resolution: "parse5@npm:8.0.1"
dependencies:
entities: "npm:^4.5.0"
checksum: 10c0/829d37a0c709215a887e410a7118d754f8e1afd7edb529db95bc7bbf8045fb0266a7b67801331d8e8d9d073ea75793624ec27ce9ff3b96862c3b9008f4d68e80
entities: "npm:^8.0.0"
checksum: 10c0/c3c1c5aab55f6e4be5245599790e56e64be7764a4a0edd7f98db4fe3bb380f63add752fa047dff0496446c25f4104f0c7c1967723de640bde92306a7bb67ed2f
languageName: node
linkType: hard
@ -7604,27 +7630,27 @@ __metadata:
languageName: node
linkType: hard
"react-router-dom@npm:6.30.2":
version: 6.30.2
resolution: "react-router-dom@npm:6.30.2"
"react-router-dom@npm:6.30.4":
version: 6.30.4
resolution: "react-router-dom@npm:6.30.4"
dependencies:
"@remix-run/router": "npm:1.23.1"
react-router: "npm:6.30.2"
"@remix-run/router": "npm:1.23.3"
react-router: "npm:6.30.4"
peerDependencies:
react: ">=16.8"
react-dom: ">=16.8"
checksum: 10c0/d0c6edf4e2aa7639b4a4f64a7747f03d8861bdf4857e8981b1cff1451b7cb91fcdcd7e315a6e3df910271b2f5071825d2aec218d5f7890f2269fc074f198e42a
checksum: 10c0/1b25ab26a288da852f7b58eec5c14b3f37919e6773a557cd846d3a05d7a7d890c8d49bda93c0a7f497f240df1df8b0ad50635f990aafb55f2fc545ad8269a822
languageName: node
linkType: hard
"react-router@npm:6.30.2":
version: 6.30.2
resolution: "react-router@npm:6.30.2"
"react-router@npm:6.30.4":
version: 6.30.4
resolution: "react-router@npm:6.30.4"
dependencies:
"@remix-run/router": "npm:1.23.1"
"@remix-run/router": "npm:1.23.3"
peerDependencies:
react: ">=16.8"
checksum: 10c0/cff5ea92d248d2230adc46d4e2ed3fbeddfaf1ae2e63411da8b7ea6ddc207d71dbc522c05c492e671e553e2153934f4ab180ac02bd36205b274e097f2cfe6fc4
checksum: 10c0/fb6de7d1002bcab9ea12c4072d93792eca494f1e4f30cfec334ccb6523756d23ce3e093c7c1241399296cebed94789a3fb89f96ee76004e0e746458a8f6bab33
languageName: node
linkType: hard
@ -7707,6 +7733,13 @@ __metadata:
languageName: node
linkType: hard
"require-from-string@npm:^2.0.2":
version: 2.0.2
resolution: "require-from-string@npm:2.0.2"
checksum: 10c0/aaa267e0c5b022fc5fd4eef49d8285086b15f2a1c54b28240fdf03599cbd9c26049fee3eab894f2e1f6ca65e513b030a7c264201e3f005601e80c49fb2937ce2
languageName: node
linkType: hard
"requires-port@npm:^1.0.0":
version: 1.0.0
resolution: "requires-port@npm:1.0.0"
@ -7967,20 +8000,6 @@ __metadata:
languageName: node
linkType: hard
"rrweb-cssom@npm:^0.7.1":
version: 0.7.1
resolution: "rrweb-cssom@npm:0.7.1"
checksum: 10c0/127b8ca6c8aac45e2755abbae6138d4a813b1bedc2caabf79466ae83ab3cfc84b5bfab513b7033f0aa4561c7753edf787d0dd01163ceacdee2e8eb1b6bf7237e
languageName: node
linkType: hard
"rrweb-cssom@npm:^0.8.0":
version: 0.8.0
resolution: "rrweb-cssom@npm:0.8.0"
checksum: 10c0/56f2bfd56733adb92c0b56e274c43f864b8dd48784d6fe946ef5ff8d438234015e59ad837fc2ad54714b6421384141c1add4eb569e72054e350d1f8a50b8ac7b
languageName: node
linkType: hard
"run-parallel@npm:^1.1.9":
version: 1.2.0
resolution: "run-parallel@npm:1.2.0"
@ -8628,6 +8647,24 @@ __metadata:
languageName: node
linkType: hard
"tldts-core@npm:^7.4.2":
version: 7.4.2
resolution: "tldts-core@npm:7.4.2"
checksum: 10c0/e8e02a43f6823ea1beab8f5a9da370b9a6cbf1a942d4f7d828700e65f03a119348f8d19faa95b599f3ca76fcb5fe5c4611d5b9c274f5a58c7487d342f6083a06
languageName: node
linkType: hard
"tldts@npm:^7.0.5":
version: 7.4.2
resolution: "tldts@npm:7.4.2"
dependencies:
tldts-core: "npm:^7.4.2"
bin:
tldts: bin/cli.js
checksum: 10c0/68f7ec58c9ea41f1583a6384f0fdf1b33d2d8ee55e7d403ec5cf9de4a7226a25c585b5ce1a73de8ddc9abbbe7b783bb3554d1c811fd47fceb0d5511e06d0d766
languageName: node
linkType: hard
"to-regex-range@npm:^5.0.1":
version: 5.0.1
resolution: "to-regex-range@npm:5.0.1"
@ -8649,12 +8686,21 @@ __metadata:
languageName: node
linkType: hard
"tr46@npm:^5.1.0":
version: 5.1.0
resolution: "tr46@npm:5.1.0"
"tough-cookie@npm:^6.0.1":
version: 6.0.1
resolution: "tough-cookie@npm:6.0.1"
dependencies:
tldts: "npm:^7.0.5"
checksum: 10c0/ec70bd6b1215efe4ed31a158f0be3e4c9088fcbd8620edc23a5860d4f3d85c757b77e274baaa700f7b25e409f4181552ed189603c2b2e1a9f88104da3a61a37d
languageName: node
linkType: hard
"tr46@npm:^6.0.0":
version: 6.0.0
resolution: "tr46@npm:6.0.0"
dependencies:
punycode: "npm:^2.3.1"
checksum: 10c0/d761f7144e0cb296187674ef245c74f761e334d7cf25ca73ef60e4c72c097c75051031c093fa1a2fee04b904977b316716a7915854bcae8fb1a371746513cbe8
checksum: 10c0/83130df2f649228aa91c17754b66248030a3af34911d713b5ea417066fa338aa4bc8668d06bd98aa21a2210f43fc0a3db8b9099e7747fb5830e40e39a6a1058e
languageName: node
linkType: hard
@ -8691,7 +8737,7 @@ __metadata:
globals: "npm:^16.0.0"
husky: "npm:^9.0.0"
jest-extended: "npm:^4.0.2"
jsdom: "npm:^24.0.0"
jsdom: "npm:29.1.1"
lint-staged: "npm:^15.0.0"
lodash: "npm:4.18.1"
msw: "npm:^2.1.7"
@ -8704,7 +8750,7 @@ __metadata:
react-helmet-async: "npm:^2.0.4"
react-icons: "npm:^5.0.1"
react-infinite-scroll-hook: "npm:^4.1.1"
react-router-dom: "npm:6.30.2"
react-router-dom: "npm:6.30.4"
swr: "npm:^2.2.4"
typescript: "npm:^5.2.2"
typescript-eslint: "npm:^8.38.0"
@ -8888,6 +8934,13 @@ __metadata:
languageName: node
linkType: hard
"undici@npm:7.27.0":
version: 7.27.0
resolution: "undici@npm:7.27.0"
checksum: 10c0/6fd15a81b0ca177b2667738b830ed175363e5e2164e992251d03aaee6c6517098b930085bd68b8fe5911920371076526657de035e07dc72377b9c5c731b90f0b
languageName: node
linkType: hard
"unique-filename@npm:^4.0.0":
version: 4.0.0
resolution: "unique-filename@npm:4.0.0"
@ -9266,36 +9319,28 @@ __metadata:
languageName: node
linkType: hard
"webidl-conversions@npm:^7.0.0":
version: 7.0.0
resolution: "webidl-conversions@npm:7.0.0"
checksum: 10c0/228d8cb6d270c23b0720cb2d95c579202db3aaf8f633b4e9dd94ec2000a04e7e6e43b76a94509cdb30479bd00ae253ab2371a2da9f81446cc313f89a4213a2c4
"webidl-conversions@npm:^8.0.1":
version: 8.0.1
resolution: "webidl-conversions@npm:8.0.1"
checksum: 10c0/3f6f327ca5fa0c065ed8ed0ef3b72f33623376e68f958e9b7bd0df49fdb0b908139ac2338d19fb45bd0e05595bda96cb6d1622222a8b413daa38a17aacc4dd46
languageName: node
linkType: hard
"whatwg-encoding@npm:^3.1.1":
version: 3.1.1
resolution: "whatwg-encoding@npm:3.1.1"
"whatwg-mimetype@npm:^5.0.0":
version: 5.0.0
resolution: "whatwg-mimetype@npm:5.0.0"
checksum: 10c0/eead164fe73a00dd82f817af6fc0bd22e9c273e1d55bf4bc6bdf2da7ad8127fca82ef00ea6a37892f5f5641f8e34128e09508f92126086baba126b9e0d57feb4
languageName: node
linkType: hard
"whatwg-url@npm:^16.0.0, whatwg-url@npm:^16.0.1":
version: 16.0.1
resolution: "whatwg-url@npm:16.0.1"
dependencies:
iconv-lite: "npm:0.6.3"
checksum: 10c0/273b5f441c2f7fda3368a496c3009edbaa5e43b71b09728f90425e7f487e5cef9eb2b846a31bd760dd8077739c26faf6b5ca43a5f24033172b003b72cf61a93e
languageName: node
linkType: hard
"whatwg-mimetype@npm:^4.0.0":
version: 4.0.0
resolution: "whatwg-mimetype@npm:4.0.0"
checksum: 10c0/a773cdc8126b514d790bdae7052e8bf242970cebd84af62fb2f35a33411e78e981f6c0ab9ed1fe6ec5071b09d5340ac9178e05b52d35a9c4bcf558ba1b1551df
languageName: node
linkType: hard
"whatwg-url@npm:^14.0.0":
version: 14.2.0
resolution: "whatwg-url@npm:14.2.0"
dependencies:
tr46: "npm:^5.1.0"
webidl-conversions: "npm:^7.0.0"
checksum: 10c0/f746fc2f4c906607d09537de1227b13f9494c171141e5427ed7d2c0dd0b6a48b43d8e71abaae57d368d0c06b673fd8ec63550b32ad5ed64990c7b0266c2b4272
"@exodus/bytes": "npm:^1.11.0"
tr46: "npm:^6.0.0"
webidl-conversions: "npm:^8.0.1"
checksum: 10c0/e75565566abf3a2cdbd9f06c965dbcccee6ec4e9f0d3728ad5e08ceb9944279848bcaa211d35a29cb6d2df1e467dd05cfb59fbddf8a0adcd7d0bce9ffb703fd2
languageName: node
linkType: hard
@ -9445,21 +9490,6 @@ __metadata:
languageName: node
linkType: hard
"ws@npm:^8.18.0":
version: 8.18.1
resolution: "ws@npm:8.18.1"
peerDependencies:
bufferutil: ^4.0.1
utf-8-validate: ">=5.0.2"
peerDependenciesMeta:
bufferutil:
optional: true
utf-8-validate:
optional: true
checksum: 10c0/e498965d6938c63058c4310ffb6967f07d4fa06789d3364829028af380d299fe05762961742971c764973dce3d1f6a2633fe8b2d9410c9b52e534b4b882a99fa
languageName: node
linkType: hard
"xml-name-validator@npm:^5.0.0":
version: 5.0.0
resolution: "xml-name-validator@npm:5.0.0"