diff --git a/.golangci.yml b/.golangci.yml index 16688de6b1..1e7bac8be5 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -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/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 1262ea2917..83301e2565 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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) diff --git a/docs/content/migrate/nginx-to-traefik.md b/docs/content/migrate/nginx-to-traefik.md index 3e838296cd..79499038ba 100644 --- a/docs/content/migrate/nginx-to-traefik.md +++ b/docs/content/migrate/nginx-to-traefik.md @@ -246,7 +246,7 @@ helm repo update ```bash helm upgrade --install traefik traefik/traefik \ --namespace traefik --create-namespace \ - --set providers.kubernetesIngressNginx.enabled=true + --set providers.kubernetesIngressNGINX.enabled=true ``` Or using a [values file](https://github.com/traefik/traefik-helm-chart/blob/master/traefik/VALUES.md) for more configuration: @@ -254,7 +254,7 @@ Or using a [values file](https://github.com/traefik/traefik-helm-chart/blob/mast ```yaml tab="traefik-values.yaml" ... providers: - kubernetesIngressNginx: + kubernetesIngressNGINX: enabled: true ... ``` diff --git a/docs/content/migrate/v3.md b/docs/content/migrate/v3.md index 36a959b3e2..fd65db12df 100644 --- a/docs/content/migrate/v3.md +++ b/docs/content/migrate/v3.md @@ -9,6 +9,43 @@ This guide provides detailed migration steps for upgrading between different Tra --- +## v3.7.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` diff --git a/docs/content/reference/install-configuration/configuration-options.md b/docs/content/reference/install-configuration/configuration-options.md index c4278ccd5a..1076f643fe 100644 --- a/docs/content/reference/install-configuration/configuration-options.md +++ b/docs/content/reference/install-configuration/configuration-options.md @@ -364,6 +364,7 @@ THIS FILE MUST NOT BE EDITED BY HAND | providers.kubernetescrd.throttleduration | Ingress refresh throttle duration | 0 | | providers.kubernetescrd.token | Kubernetes bearer token (not needed for in-cluster client). It accepts either a token value or a file path to the token. | | | providers.kubernetesgateway | Enables Kubernetes Gateway API provider. | false | +| providers.kubernetesgateway.burst | Defines the maximum burst of requests to the Kubernetes API server. | 100 | | providers.kubernetesgateway.certauthfilepath | Kubernetes certificate authority file path (not needed for in-cluster client). | | | providers.kubernetesgateway.crossprovidernamespaces | List of namespaces from which Gateway API routes are allowed to declare TraefikService backendRef references. | | | providers.kubernetesgateway.endpoint | Kubernetes server endpoint (required for external cluster client). | | @@ -371,6 +372,7 @@ THIS FILE MUST NOT BE EDITED BY HAND | providers.kubernetesgateway.labelselector | Kubernetes label selector to select specific GatewayClasses. | | | providers.kubernetesgateway.namespaces | Kubernetes namespaces. | | | providers.kubernetesgateway.nativelbbydefault | Defines whether to use Native Kubernetes load-balancing by default. | false | +| providers.kubernetesgateway.qps | Defines the maximum QPS to the Kubernetes API server. Setting this to a negative value will disable client-side ratelimiting. | 50 | | providers.kubernetesgateway.statusaddress.hostname | Hostname used for Kubernetes Gateway status address. | | | providers.kubernetesgateway.statusaddress.ip | IP used to set Kubernetes Gateway status address. | | | providers.kubernetesgateway.statusaddress.service | Published Kubernetes Service to copy status addresses from. | | diff --git a/docs/content/reference/install-configuration/providers/kubernetes/kubernetes-gateway.md b/docs/content/reference/install-configuration/providers/kubernetes/kubernetes-gateway.md index 4899475dd0..e0b9398b3d 100644 --- a/docs/content/reference/install-configuration/providers/kubernetes/kubernetes-gateway.md +++ b/docs/content/reference/install-configuration/providers/kubernetes/kubernetes-gateway.md @@ -66,22 +66,24 @@ providers: -| Field | Description | Default | Required | -|:----------------------------------------------------------------------|:------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:--------|:---------| -| `providers.providersThrottleDuration` | Minimum amount of time to wait for, after a configuration reload, before taking into account any new configuration refresh event.
If multiple events occur within this time, only the most recent one is taken into account, and all others are discarded.
**This option cannot be set per provider, but the throttling algorithm applies to each of them independently.** | 2s | No | -| `providers.kubernetesGateway.endpoint` | Server endpoint URL.
More information [here](#endpoint). | "" | No | -| `providers.kubernetesGateway.experimentalChannel` | Toggles support for the Experimental Channel resources ([Gateway API release channels documentation](https://gateway-api.sigs.k8s.io/concepts/versioning/#release-channels)).
(ex: `TCPRoute`) | false | No | -| `providers.kubernetesGateway.token` | Bearer token used for the Kubernetes client configuration. | "" | No | -| `providers.kubernetesGateway.certAuthFilePath` | Path to the certificate authority file.
Used for the Kubernetes client configuration. | "" | No | -| `providers.kubernetesGateway.namespaces` | Array of namespaces to watch.
If left empty, watch all namespaces. | [] | No | -| `providers.kubernetesGateway.labelselector` | Allow filtering on `GatewayClass` only. If left empty, Traefik processes all GatewayClass objects in the configured namespaces.
See [label-selectors](https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#label-selectors) for details. | "" | No | -| `providers.kubernetesGateway.throttleDuration` | Minimum amount of time to wait between two Kubernetes events before producing a new configuration.
This prevents a Kubernetes cluster that updates many times per second from continuously changing your Traefik configuration.
If empty, every event is caught. | 0s | No | -| `providers.kubernetesGateway.nativeLBByDefault` | Defines whether to use Native Kubernetes load-balancing mode by default. For more information, please check out the `traefik.io/service.nativelb` service annotation documentation. | false | No | -| `providers.kubernetesGateway.`
`statusAddress.hostname`
| Hostname copied to the Gateway `status.addresses`. | "" | No | -| `providers.kubernetesGateway.`
`statusAddress.ip`
| IP address copied to the Gateway `status.addresses`, and currently only supports one IP value (IPv4 or IPv6). | "" | No | -| `providers.kubernetesGateway.`
`statusAddress.service.namespace`
| The namespace of the Kubernetes service to copy status addresses from.
When using third parties tools like External-DNS, this option can be used to copy the service `loadbalancer.status` (containing the service's endpoints IPs) to the Gateway `status.addresses`. | "" | No | -| `providers.kubernetesGateway.`
`statusAddress.service.name`
| The name of the Kubernetes service to copy status addresses from.
When using third parties tools like External-DNS, this option can be used to copy the service `loadbalancer.status` (containing the service's endpoints IPs) to the Gateway `status.addresses`. | "" | No | -| `providers.kubernetesGateway.crossProviderNamespaces` | List of namespaces from which Gateway API routes (`HTTPRoute`, `TCPRoute`, `TLSRoute`) are allowed to declare a `backendRef` of kind `TraefikService`.
When unset, all namespaces are allowed. When set to `[]`, every such backendRef is rejected and the route is dropped. | [] | No | +| Field | Description | Default | Required | +|:----------------------------------------------------------------------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:--------|:---------| +| `providers.providersThrottleDuration` | Minimum amount of time to wait for, after a configuration reload, before taking into account any new configuration refresh event.
If multiple events occur within this time, only the most recent one is taken into account, and all others are discarded.
**This option cannot be set per provider, but the throttling algorithm applies to each of them independently.** | 2s | No | +| `providers.kubernetesGateway.endpoint` | Server endpoint URL.
More information [here](#endpoint). | "" | No | +| `providers.kubernetesGateway.experimentalChannel` | Toggles support for the Experimental Channel resources ([Gateway API release channels documentation](https://gateway-api.sigs.k8s.io/concepts/versioning/#release-channels)).
(ex: `TCPRoute`) | false | No | +| `providers.kubernetesGateway.token` | Bearer token used for the Kubernetes client configuration. | "" | No | +| `providers.kubernetesGateway.certAuthFilePath` | Path to the certificate authority file.
Used for the Kubernetes client configuration. | "" | No | +| `providers.kubernetesGateway.namespaces` | Array of namespaces to watch.
If left empty, watch all namespaces. | [] | No | +| `providers.kubernetesGateway.labelselector` | Allow filtering on `GatewayClass` only. If left empty, Traefik processes all GatewayClass objects in the configured namespaces.
See [label-selectors](https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#label-selectors) for details. | "" | No | +| `providers.kubernetesGateway.throttleDuration` | Minimum amount of time to wait between two Kubernetes events before producing a new configuration.
This prevents a Kubernetes cluster that updates many times per second from continuously changing your Traefik configuration.
If empty, every event is caught. | 0s | No | +| `providers.kubernetesGateway.nativeLBByDefault` | Defines whether to use Native Kubernetes load-balancing mode by default. For more information, please check out the `traefik.io/service.nativelb` service annotation documentation. | false | No | +| `providers.kubernetesGateway.`
`statusAddress.hostname`
| Hostname copied to the Gateway `status.addresses`. | "" | No | +| `providers.kubernetesGateway.`
`statusAddress.ip`
| IP address copied to the Gateway `status.addresses`, and currently only supports one IP value (IPv4 or IPv6). | "" | No | +| `providers.kubernetesGateway.`
`statusAddress.service.namespace`
| The namespace of the Kubernetes service to copy status addresses from.
When using third parties tools like External-DNS, this option can be used to copy the service `loadbalancer.status` (containing the service's endpoints IPs) to the Gateway `status.addresses`. | "" | No | +| `providers.kubernetesGateway.`
`statusAddress.service.name`
| The name of the Kubernetes service to copy status addresses from.
When using third parties tools like External-DNS, this option can be used to copy the service `loadbalancer.status` (containing the service's endpoints IPs) to the Gateway `status.addresses`. | "" | No | +| `providers.kubernetesGateway.crossProviderNamespaces` | List of namespaces from which Gateway API routes (`HTTPRoute`, `TCPRoute`, `TLSRoute`) are allowed to declare a `backendRef` of kind `TraefikService`.
When unset, all namespaces are allowed. When set to `[]`, every such backendRef is rejected and the route is dropped. | [] | No | +| providers.kubernetesgateway.qps | Defines the maximum QPS to the Kubernetes API server. Setting this to a negative value will disable client-side ratelimiting. | 50 | No | +| providers.kubernetesgateway.burst | Defines the maximum burst of requests to the Kubernetes API server. | 100 | No | diff --git a/docs/content/reference/routing-configuration/kubernetes/ingress-nginx.md b/docs/content/reference/routing-configuration/kubernetes/ingress-nginx.md index c26265b784..64a714e998 100644 --- a/docs/content/reference/routing-configuration/kubernetes/ingress-nginx.md +++ b/docs/content/reference/routing-configuration/kubernetes/ingress-nginx.md @@ -280,6 +280,7 @@ The following annotations are organized by category for easier navigation. | `nginx.ingress.kubernetes.io/auth-snippet` | 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`. | | `nginx.ingress.kubernetes.io/auth-method` | 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. | | `nginx.ingress.kubernetes.io/auth-response-headers` | | +| `nginx.ingress.kubernetes.io/enable-global-auth` | | ### SSL/TLS @@ -456,7 +457,6 @@ In practice, Traefik is slightly more lenient under bursty load, as it smooths o | `nginx.ingress.kubernetes.io/auth-keepalive-requests` | | | `nginx.ingress.kubernetes.io/auth-keepalive-timeout` | | | `nginx.ingress.kubernetes.io/auth-proxy-set-headers` | | -| `nginx.ingress.kubernetes.io/enable-global-auth` | | | `nginx.ingress.kubernetes.io/disable-proxy-intercept-errors` | | | `nginx.ingress.kubernetes.io/limit-rate-after` | | | `nginx.ingress.kubernetes.io/limit-rate` | | diff --git a/go.mod b/go.mod index bbd1cda5c2..c1fe4ca125 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index 781b73a849..131ddef2c3 100644 --- a/go.sum +++ b/go.sum @@ -835,10 +835,12 @@ github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= -github.com/bytedance/sonic v1.12.0 h1:YGPgxF9xzaCNvd/ZKdQ28yRovhfMFZQjuk6fKBzZ3ls= -github.com/bytedance/sonic v1.12.0/go.mod h1:B8Gt/XvtZ3Fqj+iSKMypzymZxw/FVwgIGKzMzT9r/rk= -github.com/bytedance/sonic/loader v0.2.0 h1:zNprn+lsIP06C/IqCHs3gPQIvnvpKbbxyXQP1iU4kWM= -github.com/bytedance/sonic/loader v0.2.0/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= +github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M= +github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM= +github.com/bytedance/sonic v1.15.1 h1:nJD5PmM0vY7J8CT6MxoqbVAAMhkSmV2HgRAUrrpLoOw= +github.com/bytedance/sonic v1.15.1/go.mod h1:mT2NbXunuaEbnZ+mRIX/vYqKISmgEuHFDI4UzmKx2SA= +github.com/bytedance/sonic/loader v0.5.1 h1:Ygpfa9zwRCCKSlrp5bBP/b/Xzc3VxsAW+5NIYXrOOpI= +github.com/bytedance/sonic/loader v0.5.1/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo= github.com/c-bata/go-prompt v0.2.5/go.mod h1:vFnjEGDIIA/Lib7giyE4E9c50Lvl8j0S+7FVlAwDAVw= github.com/c-bata/go-prompt v0.2.6/go.mod h1:/LMAke8wD2FsNu9EXNdHxNLbd9MedkPnCdfpU9wwHfY= github.com/casbin/casbin/v2 v2.37.0/go.mod h1:vByNa/Fchek0KZUgG5wEsl7iFsiviAYKRtgrQfcJqHg= @@ -866,10 +868,8 @@ github.com/clbanning/mxj v1.8.4/go.mod h1:BVjHeAH+rl9rs6f+QIpeRl0tfu10SXn1pUSa5P github.com/clbanning/mxj/v2 v2.7.0 h1:WA/La7UGCanFe5NpHF0Q3DNtnCsVoxbPKuyBNHWRyME= github.com/clbanning/mxj/v2 v2.7.0/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= -github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= -github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= -github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= +github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= +github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= @@ -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= diff --git a/integration/fixtures/https/https_domain_fronting.toml b/integration/fixtures/https/https_domain_fronting.toml index d60883b658..8df317ace3 100644 --- a/integration/fixtures/https/https_domain_fronting.toml +++ b/integration/fixtures/https/https_domain_fronting.toml @@ -8,6 +8,7 @@ [entryPoints.websecure] address = ":4443" + [entryPoints.websecure.http3] [api] insecure = true @@ -33,6 +34,35 @@ [http.routers.router3.tls] options = "mytls" +[http.routers.router4] + rule = "Host(`site4.www.snitest.com`)" + service = "service4" + [http.routers.router4.tls] + +[http.routers.router4path] + rule = "Host(`site4.www.snitest.com`) && PathPrefix(`/foo`)" + service = "service4" + [http.routers.router4path.tls] + options = "mytls" + +[http.routers.router5] + rule = "Host(`site5.www.snitest.com`)" + service = "service5" + [http.routers.router5.tls] + options = "mytls" + +[http.routers.router5path] + rule = "Host(`site5.www.snitest.com`) && PathPrefix(`/bar`)" + service = "service5" + [http.routers.router5path.tls] + options = "mytls" + +[http.routers.router6] + rule = "Host(`site6.www.snitest.com.`)" + service = "service6" + [http.routers.router6.tls] + options = "mytls" + [http.services.service1] [[http.services.service1.loadBalancer.servers]] url = "http://127.0.0.1:9010" @@ -45,10 +75,22 @@ [[http.services.service3.loadBalancer.servers]] url = "http://127.0.0.1:9030" +[http.services.service4] + [[http.services.service4.loadBalancer.servers]] + url = "http://127.0.0.1:9040" + +[http.services.service5] + [[http.services.service5.loadBalancer.servers]] + url = "http://127.0.0.1:9050" + +[http.services.service6] + [[http.services.service6.loadBalancer.servers]] + url = "http://127.0.0.1:9060" + [[tls.certificates]] certFile = "fixtures/https/wildcard.www.snitest.com.cert" keyFile = "fixtures/https/wildcard.www.snitest.com.key" [tls.options] [tls.options.mytls] - maxVersion = "VersionTLS12" + maxVersion = "VersionTLS13" diff --git a/integration/fixtures/https/https_entrypoint_tls.toml b/integration/fixtures/https/https_entrypoint_tls.toml new file mode 100644 index 0000000000..26a85c8128 --- /dev/null +++ b/integration/fixtures/https/https_entrypoint_tls.toml @@ -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" diff --git a/integration/https_test.go b/integration/https_test.go index b9b64e8d67..e962337e67 100644 --- a/integration/https_test.go +++ b/integration/https_test.go @@ -13,6 +13,7 @@ import ( "time" "github.com/BurntSushi/toml" + "github.com/quic-go/quic-go/http3" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" @@ -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) } } diff --git a/integration/simple_test.go b/integration/simple_test.go index 159e08175b..be7bd0c55a 100644 --- a/integration/simple_test.go +++ b/integration/simple_test.go @@ -949,7 +949,7 @@ func (s *SimpleSuite) TestRouterConfigErrors() { s.traefikCmd(withConfigFile(file)) // All errors - err := try.GetRequest("http://127.0.0.1:8080/api/http/routers", 1000*time.Millisecond, try.BodyContains(`["middleware \"unknown@file\" does not exist","found different TLS options for routers on the same host snitest.net, so using the default TLS options instead"]`)) + err := try.GetRequest("http://127.0.0.1:8080/api/http/routers", 1000*time.Millisecond, try.BodyContains(`["middleware \"unknown@file\" does not exist","found different TLS options for routers on the same host, so using the default TLS options instead"]`)) require.NoError(s.T(), err) // router3 has an error because it uses an unknown entrypoint diff --git a/integration/try/try.go b/integration/try/try.go index 5a432a13d3..32d1f03581 100644 --- a/integration/try/try.go +++ b/integration/try/try.go @@ -76,7 +76,7 @@ func Request(req *http.Request, timeout time.Duration, conditions ...ResponseCon // the condition on the response. // ResponseCondition may be nil, in which case only the request against the URL must // succeed. -func RequestWithTransport(req *http.Request, timeout time.Duration, transport *http.Transport, conditions ...ResponseCondition) error { +func RequestWithTransport(req *http.Request, timeout time.Duration, transport http.RoundTripper, conditions ...ResponseCondition) error { resp, err := doTryRequest(req, timeout, transport, conditions...) if resp != nil && resp.Body != nil { @@ -140,12 +140,12 @@ func doTryRequest(request *http.Request, timeout time.Duration, transport http.R func doRequest(action timedAction, timeout time.Duration, request *http.Request, transport http.RoundTripper, conditions ...ResponseCondition) (*http.Response, error) { var resp *http.Response return resp, action(timeout, func() error { - var err error - client := http.DefaultClient + var client http.Client if transport != nil { client.Transport = transport } + var err error resp, err = client.Do(request) if err != nil { return err diff --git a/pkg/config/dynamic/http_config.go b/pkg/config/dynamic/http_config.go index ecca917482..17682ef269 100644 --- a/pkg/config/dynamic/http_config.go +++ b/pkg/config/dynamic/http_config.go @@ -149,9 +149,10 @@ func (r *RouterDeniedEncodedPathCharacters) Map() map[string]struct{} { // RouterTLSConfig holds the TLS configuration for a router. type RouterTLSConfig struct { - Options string `json:"options,omitempty" toml:"options,omitempty" yaml:"options,omitempty" export:"true"` - CertResolver string `json:"certResolver,omitempty" toml:"certResolver,omitempty" yaml:"certResolver,omitempty" export:"true"` - Domains []types.Domain `json:"domains,omitempty" toml:"domains,omitempty" yaml:"domains,omitempty" export:"true"` + Options string `json:"options,omitempty" toml:"options,omitempty" yaml:"options,omitempty" export:"true"` + ResolvedOptions string `json:"-" toml:"-" yaml:"-" label:"-" file:"-" kv:"-" export:"false"` + CertResolver string `json:"certResolver,omitempty" toml:"certResolver,omitempty" yaml:"certResolver,omitempty" export:"true"` + Domains []types.Domain `json:"domains,omitempty" toml:"domains,omitempty" yaml:"domains,omitempty" export:"true"` } // +k8s:deepcopy-gen=true diff --git a/pkg/middlewares/auth/basic_auth.go b/pkg/middlewares/auth/basic_auth.go index 5c7ef27171..42a86b8e9d 100644 --- a/pkg/middlewares/auth/basic_auth.go +++ b/pkg/middlewares/auth/basic_auth.go @@ -43,6 +43,10 @@ func NewBasic(ctx context.Context, next http.Handler, authConfig dynamic.BasicAu return nil, err } + if len(users) == 0 { + return nil, fmt.Errorf("no users found in %s", authConfig.UsersFile) + } + // To prevent timing attacks, we need to compute a hash even if the user is not found. // We assume it to be safe only when the users hashes are all from the same algorithm, // so we can pick the first one as a random hash to compute. diff --git a/pkg/middlewares/auth/basic_auth_test.go b/pkg/middlewares/auth/basic_auth_test.go index 8133461679..777617bede 100644 --- a/pkg/middlewares/auth/basic_auth_test.go +++ b/pkg/middlewares/auth/basic_auth_test.go @@ -16,6 +16,15 @@ import ( "github.com/traefik/traefik/v3/pkg/testhelpers" ) +func TestNewBasicEmpty(t *testing.T) { + auth := dynamic.BasicAuth{ + Users: []string{}, + } + + _, err := NewBasic(t.Context(), nil, auth, "authName") + require.Error(t, err) +} + func TestNewBasicNotFoundSecretIsSet(t *testing.T) { auth := dynamic.BasicAuth{ Users: []string{"test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/"}, diff --git a/pkg/middlewares/ingressnginx/authtlspasscertificatetoupstream/auth_tls_pass_certificate_to_upstream.go b/pkg/middlewares/ingressnginx/authtlspasscertificatetoupstream/auth_tls_pass_certificate_to_upstream.go index b7f1fecbaa..b98e738d32 100644 --- a/pkg/middlewares/ingressnginx/authtlspasscertificatetoupstream/auth_tls_pass_certificate_to_upstream.go +++ b/pkg/middlewares/ingressnginx/authtlspasscertificatetoupstream/auth_tls_pass_certificate_to_upstream.go @@ -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 } diff --git a/pkg/middlewares/ingressnginx/authtlspasscertificatetoupstream/auth_tls_pass_certificate_to_upstream_test.go b/pkg/middlewares/ingressnginx/authtlspasscertificatetoupstream/auth_tls_pass_certificate_to_upstream_test.go index 08f216b0a2..aa96f6d291 100644 --- a/pkg/middlewares/ingressnginx/authtlspasscertificatetoupstream/auth_tls_pass_certificate_to_upstream_test.go +++ b/pkg/middlewares/ingressnginx/authtlspasscertificatetoupstream/auth_tls_pass_certificate_to_upstream_test.go @@ -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 diff --git a/pkg/middlewares/observability/semconv_test.go b/pkg/middlewares/observability/semconv_test.go index 960d4b98af..1494e3aeb1 100644 --- a/pkg/middlewares/observability/semconv_test.go +++ b/pkg/middlewares/observability/semconv_test.go @@ -52,8 +52,6 @@ func TestSemConvServerMetrics(t *testing.T) { for _, test := range tests { t.Run(test.desc, func(t *testing.T) { - t.Parallel() - var cfg otypes.OTLP (&cfg).SetDefaults() cfg.AddRoutersLabels = true diff --git a/pkg/middlewares/ratelimiter/redis_limiter.go b/pkg/middlewares/ratelimiter/redis_limiter.go index 9c49c629a2..29a2dc1221 100644 --- a/pkg/middlewares/ratelimiter/redis_limiter.go +++ b/pkg/middlewares/ratelimiter/redis_limiter.go @@ -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 diff --git a/pkg/middlewares/snicheck/snicheck.go b/pkg/middlewares/snicheck/snicheck.go index de474997f5..3be15c3b91 100644 --- a/pkg/middlewares/snicheck/snicheck.go +++ b/pkg/middlewares/snicheck/snicheck.go @@ -1,24 +1,26 @@ package snicheck import ( - "net" "net/http" - "strings" "github.com/rs/zerolog/log" - "github.com/traefik/traefik/v3/pkg/middlewares/requestdecorator" - traefiktls "github.com/traefik/traefik/v3/pkg/tls" + "github.com/traefik/traefik/v3/pkg/tcp" ) // SNICheck is an HTTP handler that checks whether the TLS configuration for the server name is the same as for the host header. type SNICheck struct { - next http.Handler - tlsOptionsForHost map[string]string + next http.Handler + routerName string + tlsOptionsName string } // New creates a new SNICheck. -func New(tlsOptionsForHost map[string]string, next http.Handler) *SNICheck { - return &SNICheck{next: next, tlsOptionsForHost: tlsOptionsForHost} +func New(routerName, tlsOptionsName string, next http.Handler) *SNICheck { + return &SNICheck{ + next: next, + routerName: routerName, + tlsOptionsName: tlsOptionsName, + } } func (s SNICheck) ServeHTTP(rw http.ResponseWriter, req *http.Request) { @@ -27,81 +29,16 @@ func (s SNICheck) ServeHTTP(rw http.ResponseWriter, req *http.Request) { return } - host := getHost(req) - serverName := strings.TrimSpace(req.TLS.ServerName) - - // Domain Fronting - if !strings.EqualFold(host, serverName) { - tlsOptionHeader := findTLSOptionName(s.tlsOptionsForHost, host, true) - tlsOptionSNI := findTLSOptionName(s.tlsOptionsForHost, serverName, false) - - if tlsOptionHeader != tlsOptionSNI { - log.Debug(). - Str("host", host). - Str("req.Host", req.Host). - Str("req.TLS.ServerName", req.TLS.ServerName). - Msgf("TLS options difference: SNI:%s, Header:%s", tlsOptionSNI, tlsOptionHeader) - http.Error(rw, http.StatusText(http.StatusMisdirectedRequest), http.StatusMisdirectedRequest) - return - } + tlsOptionsNameUsed := tcp.GetTLSOptionsName(req.Context()) + if s.tlsOptionsName != tlsOptionsNameUsed { + log.Debug(). + Str("routerName", s.routerName). + Str("req.Host", req.Host). + Str("req.TLS.ServerName", req.TLS.ServerName). + Msgf("TLS options difference: SNI:%s, Header:%s", tlsOptionsNameUsed, s.tlsOptionsName) + http.Error(rw, http.StatusText(http.StatusMisdirectedRequest), http.StatusMisdirectedRequest) + return } s.next.ServeHTTP(rw, req) } - -func getHost(req *http.Request) string { - h := requestdecorator.GetCNAMEFlatten(req.Context()) - if h != "" { - return h - } - - h = requestdecorator.GetCanonicalHost(req.Context()) - if h != "" { - return h - } - - host, _, err := net.SplitHostPort(req.Host) - if err != nil { - host = req.Host - } - - return strings.TrimSpace(host) -} - -func findTLSOptionName(tlsOptionsForHost map[string]string, host string, fqdn bool) string { - name := findTLSOptName(tlsOptionsForHost, host, fqdn) - if name != "" { - return name - } - - name = findTLSOptName(tlsOptionsForHost, strings.ToLower(host), fqdn) - if name != "" { - return name - } - - return traefiktls.DefaultTLSConfigName -} - -func findTLSOptName(tlsOptionsForHost map[string]string, host string, fqdn bool) string { - if tlsOptions, ok := tlsOptionsForHost[host]; ok { - return tlsOptions - } - - if !fqdn { - return "" - } - - if last := len(host) - 1; last >= 0 && host[last] == '.' { - if tlsOptions, ok := tlsOptionsForHost[host[:last]]; ok { - return tlsOptions - } - - return "" - } - - if tlsOptions, ok := tlsOptionsForHost[host+"."]; ok { - return tlsOptions - } - - return "" -} diff --git a/pkg/middlewares/snicheck/snicheck_test.go b/pkg/middlewares/snicheck/snicheck_test.go deleted file mode 100644 index d7411e555e..0000000000 --- a/pkg/middlewares/snicheck/snicheck_test.go +++ /dev/null @@ -1,59 +0,0 @@ -package snicheck - -import ( - "net/http" - "net/http/httptest" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestSNICheck_ServeHTTP(t *testing.T) { - testCases := []struct { - desc string - tlsOptionsForHost map[string]string - host string - expected int - }{ - { - desc: "no TLS options", - expected: http.StatusOK, - }, - { - desc: "with TLS options", - tlsOptionsForHost: map[string]string{ - "example.com": "foo", - }, - expected: http.StatusOK, - }, - { - desc: "server name and host doesn't have the same TLS configuration", - tlsOptionsForHost: map[string]string{ - "example.com": "foo", - }, - host: "example.com", - expected: http.StatusMisdirectedRequest, - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - next := http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {}) - - sniCheck := New(test.tlsOptionsForHost, next) - - req := httptest.NewRequest(http.MethodGet, "https://localhost", nil) - if test.host != "" { - req.Host = test.host - } - - recorder := httptest.NewRecorder() - - sniCheck.ServeHTTP(recorder, req) - - assert.Equal(t, test.expected, recorder.Code) - }) - } -} diff --git a/pkg/middlewares/stripprefix/strip_prefix.go b/pkg/middlewares/stripprefix/strip_prefix.go index cc8779ce5f..2934c20613 100644 --- a/pkg/middlewares/stripprefix/strip_prefix.go +++ b/pkg/middlewares/stripprefix/strip_prefix.go @@ -54,6 +54,8 @@ func (s *stripPrefix) GetTracingInformation() (string, string) { } func (s *stripPrefix) ServeHTTP(rw http.ResponseWriter, req *http.Request) { + logger := middlewares.GetLogger(req.Context(), s.name, typeName) + for _, prefix := range s.prefixes { if strings.HasPrefix(req.URL.Path, prefix) { req.URL.Path = s.getPathStripped(req.URL.Path, prefix) @@ -64,10 +66,18 @@ func (s *stripPrefix) ServeHTTP(rw http.ResponseWriter, req *http.Request) { // Here we are sanitizing the URL when the path is not empty, // as the JoinPath method is adding a leading slash if the path is empty // to be aligned with ensureLeadingSlash behavior. - if req.URL.Path != "" { + path := req.URL.Path + if path != "" { req.URL = req.URL.JoinPath() } + // Stop here if the normalization of the path produces a different path. + if path != req.URL.Path { + logger.Debug().Msgf("Rejecting request, sanitized path: %q is not equivalent to stripped path: %q", path, req.URL.Path) + http.Error(rw, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) + return + } + req.Header.Add(ForwardedPrefixHeader, prefix) req.RequestURI = req.URL.RequestURI() break diff --git a/pkg/middlewares/stripprefix/strip_prefix_test.go b/pkg/middlewares/stripprefix/strip_prefix_test.go index 7078342c30..587c92da50 100644 --- a/pkg/middlewares/stripprefix/strip_prefix_test.go +++ b/pkg/middlewares/stripprefix/strip_prefix_test.go @@ -148,10 +148,7 @@ func TestStripPrefix(t *testing.T) { Prefixes: []string{"/api"}, }, path: "/api./foo", - expectedStatusCode: http.StatusOK, - expectedPath: "/foo", - expectedRawPath: "", - expectedHeader: "/api", + expectedStatusCode: http.StatusBadRequest, }, { desc: "multiple dots in the path not stripped by the prefix", @@ -159,10 +156,7 @@ func TestStripPrefix(t *testing.T) { Prefixes: []string{"/api"}, }, path: "/api../foo", - expectedStatusCode: http.StatusOK, - expectedPath: "/foo", - expectedRawPath: "", - expectedHeader: "/api", + expectedStatusCode: http.StatusBadRequest, }, { desc: "multiple dots in the path not stripped by the prefix with forceSlash", @@ -171,10 +165,7 @@ func TestStripPrefix(t *testing.T) { ForceSlash: ptr.To(true), }, path: "/api../foo", - expectedStatusCode: http.StatusOK, - expectedPath: "/foo", - expectedRawPath: "", - expectedHeader: "/api", + expectedStatusCode: http.StatusBadRequest, }, } @@ -204,6 +195,10 @@ func TestStripPrefix(t *testing.T) { handler.ServeHTTP(resp, req) assert.Equal(t, test.expectedStatusCode, resp.Code, "Unexpected status code.") + if test.expectedStatusCode != http.StatusOK { + return + } + assert.Equal(t, test.expectedPath, actualPath, "Unexpected path.") assert.Equal(t, test.expectedRawPath, actualRawPath, "Unexpected raw path.") assert.Equal(t, test.expectedHeader, actualHeader, "Unexpected '%s' header.", ForwardedPrefixHeader) diff --git a/pkg/middlewares/stripprefixregex/strip_prefix_regex.go b/pkg/middlewares/stripprefixregex/strip_prefix_regex.go index f1ee622305..83fd0f2df0 100644 --- a/pkg/middlewares/stripprefixregex/strip_prefix_regex.go +++ b/pkg/middlewares/stripprefixregex/strip_prefix_regex.go @@ -47,6 +47,8 @@ func (s *stripPrefixRegex) GetTracingInformation() (string, string) { } func (s *stripPrefixRegex) ServeHTTP(rw http.ResponseWriter, req *http.Request) { + logger := middlewares.GetLogger(req.Context(), s.name, typeName) + for _, exp := range s.expressions { parts := exp.FindStringSubmatch(req.URL.Path) if len(parts) > 0 && len(parts[0]) > 0 { @@ -65,10 +67,18 @@ func (s *stripPrefixRegex) ServeHTTP(rw http.ResponseWriter, req *http.Request) // Here we are sanitizing the URL when the path is not empty, // as the JoinPath method is adding a leading slash if the path is empty // to be aligned with ensureLeadingSlash behavior. - if req.URL.Path != "" { + path := req.URL.Path + if path != "" { req.URL = req.URL.JoinPath() } + // Stop here if the normalization of the path produces a different path. + if path != req.URL.Path { + logger.Debug().Msgf("Rejecting request, sanitized path: %q is not equivalent to stripped path: %q", path, req.URL.Path) + http.Error(rw, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) + return + } + req.RequestURI = req.URL.RequestURI() break } diff --git a/pkg/middlewares/stripprefixregex/strip_prefix_regex_test.go b/pkg/middlewares/stripprefixregex/strip_prefix_regex_test.go index 97bc8269a3..5d1e78fc5b 100644 --- a/pkg/middlewares/stripprefixregex/strip_prefix_regex_test.go +++ b/pkg/middlewares/stripprefixregex/strip_prefix_regex_test.go @@ -201,21 +201,13 @@ func TestStripPrefixRegex(t *testing.T) { desc: "/api./foo", config: dynamic.StripPrefixRegex{Regex: []string{"/api"}}, path: "/api./foo", - expectedStatusCode: http.StatusOK, - expectedPath: "/foo", - expectedRawPath: "", - expectedRequestURI: "/foo", - expectedHeader: "/api", + expectedStatusCode: http.StatusBadRequest, }, { desc: "/api../foo", config: dynamic.StripPrefixRegex{Regex: []string{"/api"}}, path: "/api../foo", - expectedStatusCode: http.StatusOK, - expectedPath: "/foo", - expectedRawPath: "", - expectedRequestURI: "/foo", - expectedHeader: "/api", + expectedStatusCode: http.StatusBadRequest, }, } diff --git a/pkg/muxer/tcp/matcher_v2_test.go b/pkg/muxer/tcp/matcher_v2_test.go index 9f28139a84..362adcd49a 100644 --- a/pkg/muxer/tcp/matcher_v2_test.go +++ b/pkg/muxer/tcp/matcher_v2_test.go @@ -491,7 +491,7 @@ func Test_addTCPRouteV2(t *testing.T) { remoteAddr: fakeAddr{addr: addr}, } - connData, err := NewConnData(test.serverName, conn, test.protos) + connData, err := NewConnData(test.serverName, conn.RemoteAddr(), test.protos) require.NoError(t, err) matchingHandler, _ := router.Match(connData) diff --git a/pkg/muxer/tcp/mux.go b/pkg/muxer/tcp/mux.go index 083c63a8cb..08436c5bb6 100644 --- a/pkg/muxer/tcp/mux.go +++ b/pkg/muxer/tcp/mux.go @@ -22,10 +22,10 @@ type ConnData struct { } // NewConnData builds a connData struct from the given parameters. -func NewConnData(serverName string, conn tcp.WriteCloser, alpnProtos []string) (ConnData, error) { - remoteIP, _, err := net.SplitHostPort(conn.RemoteAddr().String()) +func NewConnData(serverName string, remoteAddr net.Addr, alpnProtos []string) (ConnData, error) { + remoteIP, _, err := net.SplitHostPort(remoteAddr.String()) if err != nil { - return ConnData{}, fmt.Errorf("error while parsing remote address %q: %w", conn.RemoteAddr().String(), err) + return ConnData{}, fmt.Errorf("parsing remote address %q: %w", remoteAddr.String(), err) } return ConnData{ diff --git a/pkg/muxer/tcp/mux_test.go b/pkg/muxer/tcp/mux_test.go index 708ffb7f32..ea1759df3f 100644 --- a/pkg/muxer/tcp/mux_test.go +++ b/pkg/muxer/tcp/mux_test.go @@ -293,7 +293,7 @@ func Test_addTCPRoute(t *testing.T) { remoteAddr: fakeAddr{addr: addr}, } - connData, err := NewConnData(test.serverName, conn, test.protos) + connData, err := NewConnData(test.serverName, conn.RemoteAddr(), test.protos) require.NoError(t, err) matchingHandler, _ := router.Match(connData) diff --git a/pkg/provider/file/file.go b/pkg/provider/file/file.go index 96c2942884..55d44023a1 100644 --- a/pkg/provider/file/file.go +++ b/pkg/provider/file/file.go @@ -72,9 +72,16 @@ func (p *Provider) Provide(configurationChan chan<- dynamic.Message, pool *safe. // ignore sub-dir continue } + if !isFileSupported(entry.Name()) { + // ignore unsupported file extension + continue + } watchItems = append(watchItems, path.Join(p.Directory, entry.Name())) } case len(p.Filename) > 0: + if !isFileSupported(p.Filename) { + return fmt.Errorf("unsupported file extension for file %s", p.Filename) + } watchItems = append(watchItems, filepath.Dir(p.Filename), p.Filename) default: return errors.New("error using file configuration provider, neither filename nor directory is defined") @@ -169,7 +176,7 @@ func (p *Provider) addWatcher(pool *safe.Pool, items []string, configurationChan log.Debug().Msgf("add watcher on: %s", item) err = watcher.Add(item) if err != nil { - return fmt.Errorf("error adding file watcher: %w", err) + return fmt.Errorf("error adding file watcher for %s: %w", item, err) } } @@ -392,6 +399,8 @@ func (p *Provider) collectFileConfigs(ctx context.Context, directory, prefix str } for _, item := range fileList { + logger := log.Ctx(ctx).With().Str("filename", item.Name()).Logger() + itemPath := filepath.Join(directory, item.Name()) filename := item.Name() if prefix != "" { @@ -407,10 +416,8 @@ func (p *Provider) collectFileConfigs(ctx context.Context, directory, prefix str continue } - switch strings.ToLower(filepath.Ext(item.Name())) { - case ".toml", ".yaml", ".yml": - // noop - default: + if !isFileSupported(item.Name()) { + logger.Debug().Msg("Skipping file, unsupported extension") continue } @@ -500,3 +507,12 @@ func readFile(filename string) (string, error) { } return "", fmt.Errorf("invalid filename: %s", filename) } + +func isFileSupported(filename string) bool { + switch strings.ToLower(filepath.Ext(filename)) { + case ".toml", ".yaml", ".yml": + return true + default: + return false + } +} diff --git a/pkg/provider/file/file_test.go b/pkg/provider/file/file_test.go index 3aebe1908e..e89e504e3a 100644 --- a/pkg/provider/file/file_test.go +++ b/pkg/provider/file/file_test.go @@ -197,6 +197,38 @@ func TestProvideWithWatch(t *testing.T) { } } +func TestProvideWatchWithNonConfigDanglingSymlink(t *testing.T) { + tempDir := t.TempDir() + + err := copyFile("./fixtures/yaml/simple_file_01.yml", filepath.Join(tempDir, "simple_file_01.yml")) + require.NoError(t, err) + + err = os.Symlink(filepath.Join(tempDir, "non_existent_file.txt"), filepath.Join(tempDir, "dangling_symlink.txt")) + require.NoError(t, err) + + provider := &Provider{ + Directory: tempDir, + Watch: true, + } + configChan := make(chan dynamic.Message) + go func() { + err := provider.Provide(configChan, safe.NewPool(t.Context())) + assert.NoError(t, err) + }() + + timeout := time.After(time.Second) + select { + case conf := <-configChan: + require.NotNil(t, conf.Configuration.HTTP) + numServices := len(conf.Configuration.HTTP.Services) + len(conf.Configuration.TCP.Services) + len(conf.Configuration.UDP.Services) + numRouters := len(conf.Configuration.HTTP.Routers) + len(conf.Configuration.TCP.Routers) + len(conf.Configuration.UDP.Routers) + assert.Equal(t, 6, numServices) + assert.Equal(t, 3, numRouters) + case <-timeout: + t.Errorf("timeout while waiting for config") + } +} + func getTestCases() []ProvideTestCase { return []ProvideTestCase{ { diff --git a/pkg/provider/kubernetes/gateway/client.go b/pkg/provider/kubernetes/gateway/client.go index 4c1652553a..34d15d5724 100644 --- a/pkg/provider/kubernetes/gateway/client.go +++ b/pkg/provider/kubernetes/gateway/client.go @@ -49,7 +49,10 @@ type clientWrapper struct { experimentalChannel bool } -func createClientFromConfig(c *rest.Config) (*clientWrapper, error) { +func createClientFromConfig(c *rest.Config, qps, burst int) (*clientWrapper, error) { + c.QPS = float32(qps) + c.Burst = burst + csGateway, err := gateclientset.NewForConfig(c) if err != nil { return nil, err @@ -75,7 +78,7 @@ func newClientImpl(csKube kclientset.Interface, csGateway gateclientset.Interfac // newInClusterClient returns a new Provider client that is expected to run // inside the cluster. -func newInClusterClient(endpoint string) (*clientWrapper, error) { +func newInClusterClient(endpoint string, qps, burst int) (*clientWrapper, error) { config, err := rest.InClusterConfig() if err != nil { return nil, fmt.Errorf("failed to create in-cluster configuration: %w", err) @@ -85,20 +88,20 @@ func newInClusterClient(endpoint string) (*clientWrapper, error) { config.Host = endpoint } - return createClientFromConfig(config) + return createClientFromConfig(config, qps, burst) } -func newExternalClusterClientFromFile(file string) (*clientWrapper, error) { +func newExternalClusterClientFromFile(file string, qps, burst int) (*clientWrapper, error) { configFromFlags, err := clientcmd.BuildConfigFromFlags("", file) if err != nil { return nil, err } - return createClientFromConfig(configFromFlags) + return createClientFromConfig(configFromFlags, qps, burst) } // newExternalClusterClient returns a new Provider client that may run outside of the cluster. // The endpoint parameter must not be empty. -func newExternalClusterClient(endpoint, caFilePath string, token types.FileOrContent) (*clientWrapper, error) { +func newExternalClusterClient(endpoint, caFilePath string, token types.FileOrContent, qps, burst int) (*clientWrapper, error) { if endpoint == "" { return nil, errors.New("endpoint missing for external cluster client") } @@ -122,7 +125,7 @@ func newExternalClusterClient(endpoint, caFilePath string, token types.FileOrCon config.TLSClientConfig = rest.TLSClientConfig{CAData: caData} } - return createClientFromConfig(config) + return createClientFromConfig(config, qps, burst) } // WatchAll starts namespace-specific controllers for all relevant kinds. @@ -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 } diff --git a/pkg/provider/kubernetes/gateway/httproute.go b/pkg/provider/kubernetes/gateway/httproute.go index a8be580f62..28a49a539f 100644 --- a/pkg/provider/kubernetes/gateway/httproute.go +++ b/pkg/provider/kubernetes/gateway/httproute.go @@ -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 } diff --git a/pkg/provider/kubernetes/gateway/kubernetes.go b/pkg/provider/kubernetes/gateway/kubernetes.go index 629178731f..235fa4e9ec 100644 --- a/pkg/provider/kubernetes/gateway/kubernetes.go +++ b/pkg/provider/kubernetes/gateway/kubernetes.go @@ -65,6 +65,8 @@ const ( type Provider struct { Endpoint string `description:"Kubernetes server endpoint (required for external cluster client)." json:"endpoint,omitempty" toml:"endpoint,omitempty" yaml:"endpoint,omitempty"` Token types.FileOrContent `description:"Kubernetes bearer token (not needed for in-cluster client). It accepts either a token value or a file path to the token." json:"token,omitempty" toml:"token,omitempty" yaml:"token,omitempty" loggable:"false"` + QPS int `description:"Defines the maximum QPS to the Kubernetes API server. Setting this to a negative value will disable client-side ratelimiting." json:"qps,omitempty" toml:"qps,omitempty" yaml:"qps,omitempty" export:"true"` + Burst int `description:"Defines the maximum burst of requests to the Kubernetes API server." json:"burst,omitempty" toml:"burst,omitempty" yaml:"burst,omitempty" export:"true"` CertAuthFilePath string `description:"Kubernetes certificate authority file path (not needed for in-cluster client)." json:"certAuthFilePath,omitempty" toml:"certAuthFilePath,omitempty" yaml:"certAuthFilePath,omitempty"` Namespaces []string `description:"Kubernetes namespaces." json:"namespaces,omitempty" toml:"namespaces,omitempty" yaml:"namespaces,omitempty" export:"true"` LabelSelector string `description:"Kubernetes label selector to select specific GatewayClasses." json:"labelSelector,omitempty" toml:"labelSelector,omitempty" yaml:"labelSelector,omitempty" export:"true"` @@ -86,6 +88,11 @@ type Provider struct { client *clientWrapper } +func (p *Provider) SetDefaults() { + p.QPS = 50 // the default value for the QPS is 10x the default Kubernetes client QPS value. + p.Burst = 100 // the default value for the Burst is 10x the default Kubernetes client Burst value. +} + // Entrypoint defines the available entry points. type Entrypoint struct { Address string @@ -275,13 +282,13 @@ func (p *Provider) newK8sClient(ctx context.Context) (*clientWrapper, error) { switch { case os.Getenv("KUBERNETES_SERVICE_HOST") != "" && os.Getenv("KUBERNETES_SERVICE_PORT") != "": logger.Info().Str("endpoint", p.Endpoint).Msg("Creating in-cluster Provider client") - client, err = newInClusterClient(p.Endpoint) + client, err = newInClusterClient(p.Endpoint, p.QPS, p.Burst) case os.Getenv("KUBECONFIG") != "": logger.Info().Msgf("Creating cluster-external Provider client from KUBECONFIG %s", os.Getenv("KUBECONFIG")) - client, err = newExternalClusterClientFromFile(os.Getenv("KUBECONFIG")) + client, err = newExternalClusterClientFromFile(os.Getenv("KUBECONFIG"), p.QPS, p.Burst) default: logger.Info().Str("endpoint", p.Endpoint).Msg("Creating cluster-external Provider client") - client, err = newExternalClusterClient(p.Endpoint, p.CertAuthFilePath, p.Token) + client, err = newExternalClusterClient(p.Endpoint, p.CertAuthFilePath, p.Token, p.QPS, p.Burst) } if err != nil { diff --git a/pkg/provider/kubernetes/ingress/fixtures/Ingress-with-invalid-pathmatcher-annotation.yml b/pkg/provider/kubernetes/ingress/fixtures/Ingress-with-invalid-pathmatcher-annotation.yml new file mode 100644 index 0000000000..24b310e027 --- /dev/null +++ b/pkg/provider/kubernetes/ingress/fixtures/Ingress-with-invalid-pathmatcher-annotation.yml @@ -0,0 +1,50 @@ +--- +kind: Ingress +apiVersion: networking.k8s.io/v1 +metadata: + name: "" + namespace: testing + annotations: + traefik.ingress.kubernetes.io/router.pathmatcher: 'Host("injection") || PathPrefix' +spec: + rules: + - http: + paths: + - path: /bar + pathType: ImplementationSpecific + backend: + service: + name: service1 + port: + number: 80 + +--- +kind: Service +apiVersion: v1 +metadata: + name: service1 + namespace: testing + +spec: + ports: + - port: 80 + clusterIP: 10.0.0.1 + +--- +kind: EndpointSlice +apiVersion: discovery.k8s.io/v1 +metadata: + name: service1 + namespace: testing + labels: + kubernetes.io/service-name: service1 + +addressType: IPv4 +ports: + - port: 8080 + name: "" +endpoints: + - addresses: + - 10.10.0.1 + conditions: + ready: true diff --git a/pkg/provider/kubernetes/ingress/kubernetes.go b/pkg/provider/kubernetes/ingress/kubernetes.go index 0552956811..439941dd76 100644 --- a/pkg/provider/kubernetes/ingress/kubernetes.go +++ b/pkg/provider/kubernetes/ingress/kubernetes.go @@ -395,7 +395,14 @@ func (p *Provider) loadConfigurationFromIngresses(ctx context.Context, client Cl serviceName := provider.Normalize(ingress.Namespace + "-" + pa.Backend.Service.Name + "-" + portString(pa.Backend.Service.Port)) conf.HTTP.Services[serviceName] = service - rt := p.loadRouter(ingress, rule, pa, rtConfig, serviceName) + rt, err := p.loadRouter(ingress, rule, pa, rtConfig, serviceName) + if err != nil { + logger.Error().Err(err). + Str("serviceName", pa.Backend.Service.Name). + Str("path", pa.Path). + Msg("Skipping path.") + continue + } p.applyRouterTransform(ctxIngress, rt, ingress) @@ -721,7 +728,7 @@ func (p *Provider) loadService(client Client, namespace string, backend netv1.In return svc, nil } -func (p *Provider) loadRouter(ingress *netv1.Ingress, rule netv1.IngressRule, pa netv1.HTTPIngressPath, rtConfig *RouterConfig, serviceName string) *dynamic.Router { +func (p *Provider) loadRouter(ingress *netv1.Ingress, rule netv1.IngressRule, pa netv1.HTTPIngressPath, rtConfig *RouterConfig, serviceName string) (*dynamic.Router, error) { rt := &dynamic.Router{ Service: serviceName, Observability: &dynamic.RouterObservabilityConfig{ @@ -765,6 +772,12 @@ func (p *Provider) loadRouter(ingress *netv1.Ingress, rule netv1.IngressRule, pa if pa.PathType == nil || *pa.PathType == "" || *pa.PathType == netv1.PathTypeImplementationSpecific { if rtConfig != nil && rtConfig.Router != nil && rtConfig.Router.PathMatcher != "" { + switch rtConfig.Router.PathMatcher { + case "Path", "PathPrefix", "PathRegexp": + default: + return nil, fmt.Errorf("invalid router path matcher %q: must be one of Path, PathPrefix, PathRegexp", rtConfig.Router.PathMatcher) + } + matcher = rtConfig.Router.PathMatcher } } else if *pa.PathType == netv1.PathTypeExact { @@ -775,7 +788,7 @@ func (p *Provider) loadRouter(ingress *netv1.Ingress, rule netv1.IngressRule, pa } rt.Rule = strings.Join(rules, " && ") - return rt + return rt, nil } func buildHostRuleV2(host string) string { diff --git a/pkg/provider/kubernetes/ingress/kubernetes_test.go b/pkg/provider/kubernetes/ingress/kubernetes_test.go index 44b8a3a7d2..8adcf6d7a1 100644 --- a/pkg/provider/kubernetes/ingress/kubernetes_test.go +++ b/pkg/provider/kubernetes/ingress/kubernetes_test.go @@ -2170,6 +2170,31 @@ func TestLoadConfigurationFromIngresses(t *testing.T) { }, strictPrefixMatching: true, }, + { + desc: "Ingress with invalid pathmatcher annotation", + expected: &dynamic.Configuration{ + HTTP: &dynamic.HTTPConfiguration{ + Middlewares: map[string]*dynamic.Middleware{}, + Routers: map[string]*dynamic.Router{}, + Services: map[string]*dynamic.Service{ + "testing-service1-80": { + LoadBalancer: &dynamic.ServersLoadBalancer{ + Strategy: dynamic.BalancerStrategyWRR, + PassHostHeader: pointer(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, + Servers: []dynamic.Server{ + { + URL: "http://10.10.0.1:8080", + }, + }, + }, + }, + }, + }, + }, + }, } for _, test := range testCases { diff --git a/pkg/proxy/httputil/observability_test.go b/pkg/proxy/httputil/observability_test.go index c0ef0e7a3b..117b589bbe 100644 --- a/pkg/proxy/httputil/observability_test.go +++ b/pkg/proxy/httputil/observability_test.go @@ -57,8 +57,6 @@ func TestObservabilityRoundTripper_metrics(t *testing.T) { for _, test := range tests { t.Run(test.desc, func(t *testing.T) { - t.Parallel() - var cfg otypes.OTLP (&cfg).SetDefaults() cfg.AddRoutersLabels = true diff --git a/pkg/server/aggregator.go b/pkg/server/aggregator.go index 36326af763..583fa15157 100644 --- a/pkg/server/aggregator.go +++ b/pkg/server/aggregator.go @@ -1,16 +1,18 @@ package server import ( + "context" "slices" "strings" "github.com/go-acme/lego/v4/challenge/tlsalpn01" "github.com/rs/zerolog/log" "github.com/traefik/traefik/v3/pkg/config/dynamic" + httpmuxer "github.com/traefik/traefik/v3/pkg/muxer/http" "github.com/traefik/traefik/v3/pkg/observability/logs" otypes "github.com/traefik/traefik/v3/pkg/observability/types" "github.com/traefik/traefik/v3/pkg/server/provider" - "github.com/traefik/traefik/v3/pkg/tls" + traefiktls "github.com/traefik/traefik/v3/pkg/tls" ) func mergeConfiguration(configurations dynamic.Configurations, defaultEntryPoints []string) dynamic.Configuration { @@ -36,8 +38,8 @@ func mergeConfiguration(configurations dynamic.Configurations, defaultEntryPoint Services: make(map[string]*dynamic.UDPService), }, TLS: &dynamic.TLSConfiguration{ - Stores: make(map[string]tls.Store), - Options: make(map[string]tls.Options), + Stores: make(map[string]traefiktls.Store), + Options: make(map[string]traefiktls.Options), }, } @@ -134,7 +136,7 @@ func mergeConfiguration(configurations dynamic.Configurations, defaultEntryPoint } for key, store := range configuration.TLS.Stores { - if key != tls.DefaultTLSStoreName { + if key != traefiktls.DefaultTLSStoreName { key = provider.MakeQualifiedName(pvd, key) } else { defaultTLSStoreProviders = append(defaultTLSStoreProviders, pvd) @@ -156,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) diff --git a/pkg/server/configurationwatcher.go b/pkg/server/configurationwatcher.go index 747f476f65..919c75d0bf 100644 --- a/pkg/server/configurationwatcher.go +++ b/pkg/server/configurationwatcher.go @@ -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) diff --git a/pkg/server/configurationwatcher_test.go b/pkg/server/configurationwatcher_test.go index 40df7b5aad..a9f5920948 100644 --- a/pkg/server/configurationwatcher_test.go +++ b/pkg/server/configurationwatcher_test.go @@ -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 +} diff --git a/pkg/server/conncontext.go b/pkg/server/conncontext.go new file mode 100644 index 0000000000..a93290c7a2 --- /dev/null +++ b/pkg/server/conncontext.go @@ -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 + } +} diff --git a/pkg/server/conncontext_test.go b/pkg/server/conncontext_test.go new file mode 100644 index 0000000000..6160ea358e --- /dev/null +++ b/pkg/server/conncontext_test.go @@ -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"))) +} diff --git a/pkg/server/router/router.go b/pkg/server/router/router.go index 718568b278..9d5623fadd 100644 --- a/pkg/server/router/router.go +++ b/pkg/server/router/router.go @@ -19,6 +19,7 @@ import ( metricsMiddle "github.com/traefik/traefik/v3/pkg/middlewares/metrics" "github.com/traefik/traefik/v3/pkg/middlewares/observability" "github.com/traefik/traefik/v3/pkg/middlewares/recovery" + "github.com/traefik/traefik/v3/pkg/middlewares/snicheck" httpmuxer "github.com/traefik/traefik/v3/pkg/muxer/http" "github.com/traefik/traefik/v3/pkg/observability/logs" "github.com/traefik/traefik/v3/pkg/server/middleware" @@ -375,6 +376,12 @@ func (m *Manager) buildHTTPHandler(ctx context.Context, router *runtime.RouterIn }) } + if router.TLS != nil { + chain = chain.Append(func(next http.Handler) (http.Handler, error) { + return snicheck.New(routerName, router.TLS.ResolvedOptions, next), nil + }) + } + mHandler := m.middlewaresBuilder.BuildMiddlewareChain(ctx, router.Middlewares) return chain.Extend(*mHandler).Then(nextHandler) diff --git a/pkg/server/router/tcp/manager.go b/pkg/server/router/tcp/manager.go index a580644b02..2cdba0aef7 100644 --- a/pkg/server/router/tcp/manager.go +++ b/pkg/server/router/tcp/manager.go @@ -2,7 +2,6 @@ package tcp import ( "context" - "crypto/tls" "errors" "fmt" "math" @@ -11,7 +10,6 @@ import ( "github.com/rs/zerolog/log" "github.com/traefik/traefik/v3/pkg/config/runtime" - "github.com/traefik/traefik/v3/pkg/middlewares/snicheck" "github.com/traefik/traefik/v3/pkg/muxer" httpmuxer "github.com/traefik/traefik/v3/pkg/muxer/http" tcpmuxer "github.com/traefik/traefik/v3/pkg/muxer/tcp" @@ -97,11 +95,6 @@ func (m *Manager) getHTTPRouters(ctx context.Context, entryPoints []string, tls return make(map[string]map[string]*runtime.RouterInfo) } -type nameAndConfig struct { - routerName string // just so we have it as additional information when logging - TLSConfig *tls.Config -} - func (m *Manager) buildEntryPointHandler(ctx context.Context, configs map[string]*runtime.TCPRouterInfo, configsHTTP map[string]*runtime.RouterInfo, handlerHTTP, handlerHTTPS http.Handler) (*Router, error) { // Build a new Router. router, err := NewRouter(m.providersPrecedence) @@ -119,18 +112,6 @@ func (m *Manager) buildEntryPointHandler(ctx context.Context, configs map[string log.Ctx(ctx).Error().Err(err).Msg("Error during the build of the default TLS configuration") } - // Keyed by domain. The source of truth for doing SNI checking (domain fronting). - // As soon as there's (at least) two different tlsOptions found for the same domain, - // we set the value to the default TLS conf. - tlsOptionsForHost := map[string]string{} - - // Keyed by domain, then by options reference. - // The actual source of truth for what TLS options will actually be used for the connection. - // As opposed to tlsOptionsForHost, it keeps track of all the (different) TLS - // options that occur for a given host name, so that later on we can set relevant - // errors and logging for all the routers concerned (i.e. wrongly configured). - tlsOptionsForHostSNI := map[string]map[string]nameAndConfig{} - for routerHTTPName, routerHTTPConfig := range configsHTTP { if routerHTTPConfig.TLS == nil { continue @@ -139,6 +120,8 @@ func (m *Manager) buildEntryPointHandler(ctx context.Context, configs map[string logger := log.Ctx(ctx).With().Str(logs.RouterName, routerHTTPName).Logger() ctxRouter := logger.WithContext(provider.AddInContext(ctx, routerHTTPName)) + // Even if the TLS options mismatch between the configured and the resolved one is handled in the aggregator + // we also have to handle it here to be able to mark the router in error. tlsOptionsName := traefiktls.DefaultTLSConfigName if len(routerHTTPConfig.TLS.Options) > 0 && routerHTTPConfig.TLS.Options != traefiktls.DefaultTLSConfigName { tlsOptionsName = provider.GetQualifiedName(ctxRouter, routerHTTPConfig.TLS.Options) @@ -158,7 +141,7 @@ func (m *Manager) buildEntryPointHandler(ctx context.Context, configs map[string // This is only about choosing the TLS configuration. // The actual routing will be done further on by the HTTPS handler. // See examples below. - router.AddHTTPTLSConfig("*", defaultTLSConf) + router.AddHTTPTLSConfig("*", defaultTLSConf, traefiktls.DefaultTLSConfigName) // The server name (from a Host(SNI) rule) is the only parameter (available in HTTP routing rules) on which we can map a TLS config, // because it is the only one accessible before decryption (we obtain it during the ClientHello). @@ -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) diff --git a/pkg/server/router/tcp/manager_test.go b/pkg/server/router/tcp/manager_test.go index adcff5ef04..50301b3546 100644 --- a/pkg/server/router/tcp/manager_test.go +++ b/pkg/server/router/tcp/manager_test.go @@ -1,14 +1,10 @@ package tcp import ( - "crypto/tls" "math" - "net/http" - "net/http/httptest" "testing" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" "github.com/traefik/traefik/v3/pkg/config/dynamic" "github.com/traefik/traefik/v3/pkg/config/runtime" tcpmiddleware "github.com/traefik/traefik/v3/pkg/server/middleware/tcp" @@ -130,7 +126,8 @@ func TestRuntimeConfiguration(t *testing.T) { Service: "foo-service", Rule: "Host(`bar.foo`)", TLS: &dynamic.RouterTLSConfig{ - Options: "foo", + Options: "foo", + ResolvedOptions: "default", }, }, }, @@ -140,7 +137,8 @@ func TestRuntimeConfiguration(t *testing.T) { Service: "foo-service", Rule: "Host(`bar.foo`) && PathPrefix(`/path`)", TLS: &dynamic.RouterTLSConfig{ - Options: "bar", + Options: "bar", + ResolvedOptions: "default", }, }, }, @@ -399,293 +397,3 @@ func TestRuntimeConfiguration(t *testing.T) { }) } } - -func TestDomainFronting(t *testing.T) { - tlsOptionsBase := map[string]traefiktls.Options{ - "default": { - MinVersion: "VersionTLS10", - }, - "host1@file": { - MinVersion: "VersionTLS12", - }, - "host1@crd": { - MinVersion: "VersionTLS12", - }, - } - - entryPoints := []string{"web"} - - tests := []struct { - desc string - routers map[string]*runtime.RouterInfo - tlsOptions map[string]traefiktls.Options - host string - ServerName string - expectedStatus int - }{ - { - desc: "Request is misdirected when TLS options are different", - routers: map[string]*runtime.RouterInfo{ - "router-1@file": { - Router: &dynamic.Router{ - EntryPoints: entryPoints, - Rule: "Host(`host1.local`)", - TLS: &dynamic.RouterTLSConfig{ - Options: "host1", - }, - }, - }, - "router-2@file": { - Router: &dynamic.Router{ - EntryPoints: entryPoints, - Rule: "Host(`host2.local`)", - TLS: &dynamic.RouterTLSConfig{}, - }, - }, - }, - tlsOptions: tlsOptionsBase, - host: "host1.local", - ServerName: "host2.local", - expectedStatus: http.StatusMisdirectedRequest, - }, - { - desc: "Request is OK when TLS options are the same", - routers: map[string]*runtime.RouterInfo{ - "router-1@file": { - Router: &dynamic.Router{ - EntryPoints: entryPoints, - Rule: "Host(`host1.local`)", - TLS: &dynamic.RouterTLSConfig{ - Options: "host1", - }, - }, - }, - "router-2@file": { - Router: &dynamic.Router{ - EntryPoints: entryPoints, - Rule: "Host(`host2.local`)", - TLS: &dynamic.RouterTLSConfig{ - Options: "host1", - }, - }, - }, - }, - tlsOptions: tlsOptionsBase, - host: "host1.local", - ServerName: "host2.local", - expectedStatus: http.StatusOK, - }, - { - desc: "Default TLS options is used when options are ambiguous for the same host", - routers: map[string]*runtime.RouterInfo{ - "router-1@file": { - Router: &dynamic.Router{ - EntryPoints: entryPoints, - Rule: "Host(`host1.local`)", - TLS: &dynamic.RouterTLSConfig{ - Options: "host1", - }, - }, - }, - "router-2@file": { - Router: &dynamic.Router{ - EntryPoints: entryPoints, - Rule: "Host(`host1.local`) && PathPrefix(`/foo`)", - TLS: &dynamic.RouterTLSConfig{ - Options: "default", - }, - }, - }, - "router-3@file": { - Router: &dynamic.Router{ - EntryPoints: entryPoints, - Rule: "Host(`host2.local`)", - TLS: &dynamic.RouterTLSConfig{ - Options: "host1", - }, - }, - }, - }, - tlsOptions: tlsOptionsBase, - host: "host1.local", - ServerName: "host2.local", - expectedStatus: http.StatusMisdirectedRequest, - }, - { - desc: "Default TLS options should not be used when options are the same for the same host", - routers: map[string]*runtime.RouterInfo{ - "router-1@file": { - Router: &dynamic.Router{ - EntryPoints: entryPoints, - Rule: "Host(`host1.local`)", - TLS: &dynamic.RouterTLSConfig{ - Options: "host1", - }, - }, - }, - "router-2@file": { - Router: &dynamic.Router{ - EntryPoints: entryPoints, - Rule: "Host(`host1.local`) && PathPrefix(`/bar`)", - TLS: &dynamic.RouterTLSConfig{ - Options: "host1", - }, - }, - }, - "router-3@file": { - Router: &dynamic.Router{ - EntryPoints: entryPoints, - Rule: "Host(`host2.local`)", - TLS: &dynamic.RouterTLSConfig{ - Options: "host1", - }, - }, - }, - }, - tlsOptions: tlsOptionsBase, - host: "host1.local", - ServerName: "host2.local", - expectedStatus: http.StatusOK, - }, - { - desc: "Request is misdirected when TLS options have the same name but from different providers", - routers: map[string]*runtime.RouterInfo{ - "router-1@file": { - Router: &dynamic.Router{ - EntryPoints: entryPoints, - Rule: "Host(`host1.local`)", - TLS: &dynamic.RouterTLSConfig{ - Options: "host1", - }, - }, - }, - "router-2@crd": { - Router: &dynamic.Router{ - EntryPoints: entryPoints, - Rule: "Host(`host2.local`)", - TLS: &dynamic.RouterTLSConfig{ - Options: "host1", - }, - }, - }, - }, - tlsOptions: tlsOptionsBase, - host: "host1.local", - ServerName: "host2.local", - expectedStatus: http.StatusMisdirectedRequest, - }, - { - desc: "Request is OK when TLS options reference from a different provider is the same", - routers: map[string]*runtime.RouterInfo{ - "router-1@file": { - Router: &dynamic.Router{ - EntryPoints: entryPoints, - Rule: "Host(`host1.local`)", - TLS: &dynamic.RouterTLSConfig{ - Options: "host1@crd", - }, - }, - }, - "router-2@crd": { - Router: &dynamic.Router{ - EntryPoints: entryPoints, - Rule: "Host(`host2.local`)", - TLS: &dynamic.RouterTLSConfig{ - Options: "host1@crd", - }, - }, - }, - }, - tlsOptions: tlsOptionsBase, - host: "host1.local", - ServerName: "host2.local", - expectedStatus: http.StatusOK, - }, - { - desc: "Request is misdirected when server name is empty and the host name is an FQDN, but router's rule is not", - routers: map[string]*runtime.RouterInfo{ - "router-1@file": { - Router: &dynamic.Router{ - EntryPoints: entryPoints, - Rule: "Host(`host1.local`)", - TLS: &dynamic.RouterTLSConfig{ - Options: "host1@file", - }, - }, - }, - }, - tlsOptions: map[string]traefiktls.Options{ - "default": { - MinVersion: "VersionTLS13", - }, - "host1@file": { - MinVersion: "VersionTLS12", - }, - }, - host: "host1.local.", - expectedStatus: http.StatusMisdirectedRequest, - }, - { - desc: "Request is misdirected when server name is empty and the host name is not FQDN, but router's rule is", - routers: map[string]*runtime.RouterInfo{ - "router-1@file": { - Router: &dynamic.Router{ - EntryPoints: entryPoints, - Rule: "Host(`host1.local.`)", - TLS: &dynamic.RouterTLSConfig{ - Options: "host1@file", - }, - }, - }, - }, - tlsOptions: map[string]traefiktls.Options{ - "default": { - MinVersion: "VersionTLS13", - }, - "host1@file": { - MinVersion: "VersionTLS12", - }, - }, - host: "host1.local", - expectedStatus: http.StatusMisdirectedRequest, - }, - } - - for _, test := range tests { - t.Run(test.desc, func(t *testing.T) { - conf := &runtime.Configuration{ - Routers: test.routers, - } - - serviceManager := tcp.NewManager(conf, tcp2.NewDialerManager(nil)) - - tlsManager := traefiktls.NewManager(nil) - tlsManager.UpdateConfigs(t.Context(), map[string]traefiktls.Store{}, test.tlsOptions, []*traefiktls.CertAndStores{}) - - httpsHandler := map[string]http.Handler{ - "web": http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {}), - } - - middlewaresBuilder := tcpmiddleware.NewBuilder(conf.TCPMiddlewares) - - routerManager := NewManager(conf, serviceManager, middlewaresBuilder, nil, httpsHandler, tlsManager, nil) - - routers := routerManager.BuildHandlers(t.Context(), entryPoints) - - router, ok := routers["web"] - require.True(t, ok) - - req := httptest.NewRequest(http.MethodGet, "/", nil) - req.Host = test.host - req.TLS = &tls.ConnectionState{ - ServerName: test.ServerName, - } - - rw := httptest.NewRecorder() - - router.GetHTTPSHandler().ServeHTTP(rw, req) - - assert.Equal(t, test.expectedStatus, rw.Code) - }) - } -} diff --git a/pkg/server/router/tcp/postgres.go b/pkg/server/router/tcp/postgres.go index 10b18a50dc..a91e531e69 100644 --- a/pkg/server/router/tcp/postgres.go +++ b/pkg/server/router/tcp/postgres.go @@ -64,7 +64,7 @@ func (r *Router) servePostgres(conn *peekConn) error { log.Error().Err(err).Msg("Error while setting deadline") } - connData, err := tcpmuxer.NewConnData(hello.serverName, conn, hello.protos) + connData, err := tcpmuxer.NewConnData(hello.serverName, conn.RemoteAddr(), hello.protos) if err != nil { log.Error().Err(err).Msg("Error while reading TCP connection data") return nil diff --git a/pkg/server/router/tcp/router.go b/pkg/server/router/tcp/router.go index 0cce26e6fe..8b6c65854c 100644 --- a/pkg/server/router/tcp/router.go +++ b/pkg/server/router/tcp/router.go @@ -16,11 +16,17 @@ import ( "github.com/rs/zerolog/log" tcpmuxer "github.com/traefik/traefik/v3/pkg/muxer/tcp" "github.com/traefik/traefik/v3/pkg/tcp" + traefiktls "github.com/traefik/traefik/v3/pkg/tls" ) // errClientHelloRead is used as a sentinel error to break the TLS handshake once we have read the ClientHello. var errClientHelloRead = errors.New("client hello successfully read") +type tlsConfigWithOptionsName struct { + cfg *tls.Config + optionsName string +} + // Router is a TCP router. type Router struct { acmeTLSPassthrough bool @@ -47,7 +53,7 @@ type Router struct { httpsTLSConfig *tls.Config // default TLS config // hostHTTPTLSConfig contains TLS configs keyed by SNI. // A nil config is the hint to set up a brokenTLSRouter. - hostHTTPTLSConfig map[string]*tls.Config // TLS configs keyed by SNI + hostHTTPTLSConfig map[string]tlsConfigWithOptionsName // TLS configs keyed by SNI } // NewRouter returns a new TCP router. @@ -74,14 +80,20 @@ func NewRouter(providersPrecedence []string) (*Router, error) { }, nil } -// GetTLSGetClientInfo is called after a ClientHello is received from a client. -func (r *Router) GetTLSGetClientInfo() func(info *tls.ClientHelloInfo) (*tls.Config, error) { - return func(info *tls.ClientHelloInfo) (*tls.Config, error) { - if tlsConfig, ok := r.hostHTTPTLSConfig[info.ServerName]; ok { - return tlsConfig, nil +// HTTP3TLSConfigMatcherFunc returns a matcher func for HTTP/3 which returns a tls.Config with its corresponding +// TLSOptionName matching the given HostSNI in the connection data, or the default TLS config if there is no match. +func (r *Router) HTTP3TLSConfigMatcherFunc() func(connData tcpmuxer.ConnData) (*tls.Config, string, error) { + return func(connData tcpmuxer.ConnData) (*tls.Config, string, error) { + h, _ := r.muxerHTTPS.Match(connData) + if h == nil { + return r.httpsTLSConfig, traefiktls.DefaultTLSConfigName, nil } - return r.httpsTLSConfig, nil + if tlsHandler, ok := h.(*tcp.TLSHandler); ok { + return tlsHandler.Config, tlsHandler.TLSOptionsName, nil + } + + return nil, "", errors.New("matching handler is not a TLSHandler") } } @@ -93,7 +105,7 @@ func (r *Router) ServeTCP(conn tcp.WriteCloser) { // we would block forever on clientHelloInfo, // which is why we want to detect and handle that case first and foremost. if r.muxerTCP.HasRoutes() && !r.muxerTCPTLS.HasRoutes() && !r.muxerHTTPS.HasRoutes() { - connData, err := tcpmuxer.NewConnData("", conn, nil) + connData, err := tcpmuxer.NewConnData("", conn.RemoteAddr(), nil) if err != nil { log.Error().Err(err).Msg("Error while reading TCP connection data") conn.Close() @@ -159,7 +171,7 @@ func (r *Router) ServeTCP(conn tcp.WriteCloser) { log.Error().Err(err).Msg("Error while setting deadline") } - connData, err := tcpmuxer.NewConnData(hello.serverName, pConn, hello.protos) + connData, err := tcpmuxer.NewConnData(hello.serverName, pConn.RemoteAddr(), hello.protos) if err != nil { log.Error().Err(err).Msg("Error while reading TCP connection data") _ = pConn.Close() @@ -235,12 +247,15 @@ func (r *Router) AddTCPRoute(rule string, priority int, providerName string, tar } // AddHTTPTLSConfig defines a handler for a given sniHost and sets the matching tlsConfig. -func (r *Router) AddHTTPTLSConfig(sniHost string, config *tls.Config) { +func (r *Router) AddHTTPTLSConfig(sniHost string, config *tls.Config, optionsName string) { if r.hostHTTPTLSConfig == nil { - r.hostHTTPTLSConfig = map[string]*tls.Config{} + r.hostHTTPTLSConfig = map[string]tlsConfigWithOptionsName{} } - r.hostHTTPTLSConfig[sniHost] = config + r.hostHTTPTLSConfig[sniHost] = tlsConfigWithOptionsName{ + cfg: config, + optionsName: optionsName, + } } // GetHTTPHandler gets the attached http handler. @@ -264,12 +279,13 @@ func (r *Router) SetHTTPForwarder(handler tcp.Handler) { func (r *Router) SetHTTPSForwarder(handler tcp.Handler) { for sniHost, tlsConf := range r.hostHTTPTLSConfig { var tcpHandler tcp.Handler - if tlsConf == nil { + if tlsConf.cfg == nil { tcpHandler = &brokenTLSRouter{} } else { tcpHandler = &tcp.TLSHandler{ - Next: handler, - Config: tlsConf, + Next: handler, + Config: tlsConf.cfg, + TLSOptionsName: tlsConf.optionsName, } } @@ -287,8 +303,9 @@ func (r *Router) SetHTTPSForwarder(handler tcp.Handler) { } r.httpsForwarder = &tcp.TLSHandler{ - Next: handler, - Config: r.httpsTLSConfig, + Next: handler, + Config: r.httpsTLSConfig, + TLSOptionsName: "default", } } diff --git a/pkg/server/router/tcp/router_test.go b/pkg/server/router/tcp/router_test.go index a7d7034994..a0bcf956d3 100644 --- a/pkg/server/router/tcp/router_test.go +++ b/pkg/server/router/tcp/router_test.go @@ -3,6 +3,7 @@ package tcp import ( "bufio" "bytes" + "context" "crypto/tls" "errors" "fmt" @@ -21,7 +22,7 @@ import ( "github.com/traefik/traefik/v3/pkg/config/runtime" tcpmiddleware "github.com/traefik/traefik/v3/pkg/server/middleware/tcp" "github.com/traefik/traefik/v3/pkg/server/service/tcp" - tcp2 "github.com/traefik/traefik/v3/pkg/tcp" + traefiktcp "github.com/traefik/traefik/v3/pkg/tcp" traefiktls "github.com/traefik/traefik/v3/pkg/tls" "github.com/traefik/traefik/v3/pkg/tls/generate" "github.com/traefik/traefik/v3/pkg/types" @@ -125,7 +126,7 @@ func Test_Routing(t *testing.T) { }, } - dialerManager := tcp2.NewDialerManager(nil) + dialerManager := traefiktcp.NewDialerManager(nil) dialerManager.Update(map[string]*dynamic.TCPServersTransport{"default@internal": {}}) serviceManager := tcp.NewManager(conf, dialerManager) @@ -599,6 +600,16 @@ func Test_Routing(t *testing.T) { _, err = fmt.Fprint(w, "HTTPS") require.NoError(t, err) }), + + ConnContext: func(ctx context.Context, c net.Conn) context.Context { + if tlsConn, ok := c.(*tls.Conn); ok { + if tlsConnWithOptionsName, ok := tlsConn.NetConn().(traefiktcp.TLSConn); ok { + return traefiktcp.AddTLSOptionsNameInContext(ctx, tlsConnWithOptionsName.TLSOptionsName) + } + } + + return ctx + }, } stoppedHTTPS := make(chan struct{}) @@ -856,9 +867,9 @@ func TestPostgresTLSTermination(t *testing.T) { // Register a TCPTLS route (TLS termination, not passthrough) with a TLSHandler. // The TLSHandler wraps the actual handler, performing the TLS handshake. - err = router.muxerTCPTLS.AddRoute("HostSNI(`test.localhost`)", "", 0, "", &tcp2.TLSHandler{ + err = router.muxerTCPTLS.AddRoute("HostSNI(`test.localhost`)", "", 0, "", &traefiktcp.TLSHandler{ Config: tlsConf, - Next: tcp2.HandlerFunc(func(conn tcp2.WriteCloser) { + Next: traefiktcp.HandlerFunc(func(conn traefiktcp.WriteCloser) { _, _ = conn.Write([]byte("OK")) _ = conn.Close() }), @@ -921,7 +932,7 @@ func TestPostgresTLSPassthrough(t *testing.T) { require.NoError(t, err) // Register a TCPTLS route (TLS passthrough) with a tcp.Handler. - err = router.muxerTCPTLS.AddRoute("HostSNI(`test.localhost`)", "", 0, "", tcp2.HandlerFunc(func(conn tcp2.WriteCloser) { + err = router.muxerTCPTLS.AddRoute("HostSNI(`test.localhost`)", "", 0, "", traefiktcp.HandlerFunc(func(conn traefiktcp.WriteCloser) { // First we should receive the PostgresStartTLSMsg. buf := make([]byte, len(PostgresStartTLSMsg)) _, err := conn.Read(buf) @@ -1056,7 +1067,8 @@ func routerHTTPSPathPrefix(conf *runtime.Configuration) { Service: "http", Rule: "PathPrefix(`/`)", TLS: &dynamic.RouterTLSConfig{ - Options: "tls10", + Options: "tls10", + ResolvedOptions: "tls10", }, }, } @@ -1070,7 +1082,8 @@ func routerHTTPS(conf *runtime.Configuration) { Service: "http", Rule: "Host(`foo.bar`)", TLS: &dynamic.RouterTLSConfig{ - Options: "tls12", + Options: "tls12", + ResolvedOptions: "tls12", }, }, } @@ -1366,7 +1379,7 @@ func (h *httpForwarder) Close() error { } // ServeTCP uses the connection to serve it later in "Accept". -func (h *httpForwarder) ServeTCP(conn tcp2.WriteCloser) { +func (h *httpForwarder) ServeTCP(conn traefiktcp.WriteCloser) { h.connChan <- conn } diff --git a/pkg/server/server_entrypoint_tcp.go b/pkg/server/server_entrypoint_tcp.go index c4f8f2098f..52e47a7852 100644 --- a/pkg/server/server_entrypoint_tcp.go +++ b/pkg/server/server_entrypoint_tcp.go @@ -2,6 +2,7 @@ package server import ( "context" + "crypto/tls" "errors" "expvar" "fmt" @@ -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) diff --git a/pkg/server/server_entrypoint_tcp_http3.go b/pkg/server/server_entrypoint_tcp_http3.go index 7a2b0cadf4..5cac4c9e08 100644 --- a/pkg/server/server_entrypoint_tcp_http3.go +++ b/pkg/server/server_entrypoint_tcp_http3.go @@ -13,7 +13,9 @@ import ( "github.com/quic-go/quic-go/http3" "github.com/rs/zerolog/log" "github.com/traefik/traefik/v3/pkg/config/static" + tcpmuxer "github.com/traefik/traefik/v3/pkg/muxer/tcp" tcprouter "github.com/traefik/traefik/v3/pkg/server/router/tcp" + "github.com/traefik/traefik/v3/pkg/tcp" ) type http3server struct { @@ -22,7 +24,7 @@ type http3server struct { http3conn net.PacketConn lock sync.RWMutex - getter func(info *tls.ClientHelloInfo) (*tls.Config, error) + getter func(data tcpmuxer.ConnData) (*tls.Config, string, error) } func newHTTP3Server(ctx context.Context, name string, config *static.EntryPoint, httpsServer *httpServer) (*http3server, error) { @@ -55,8 +57,8 @@ func newHTTP3Server(ctx context.Context, name string, config *static.EntryPoint, h3 := &http3server{ http3conn: conn, - getter: func(info *tls.ClientHelloInfo) (*tls.Config, error) { - return nil, errors.New("no tls config") + getter: func(data tcpmuxer.ConnData) (*tls.Config, string, error) { + return nil, "", errors.New("no TLS config") }, } @@ -64,10 +66,18 @@ func newHTTP3Server(ctx context.Context, name string, config *static.EntryPoint, Addr: config.GetAddress(), Port: config.HTTP3.AdvertisedPort, Handler: httpsServer.Server.(*http.Server).Handler, - TLSConfig: &tls.Config{GetConfigForClient: h3.getGetConfigForClient}, + TLSConfig: &tls.Config{GetConfigForClient: h3.getTLSConfigForClient}, QUICConfig: &quic.Config{ Allow0RTT: false, }, + ConnContext: func(ctx context.Context, c *quic.Conn) context.Context { + tlsOptionsName, err := h3.getTLSOptionsName(c) + if err != nil { + log.Error().Msgf("Error getting TLS options name for client: %v", err) + return ctx + } + return tcp.AddTLSOptionsNameInContext(ctx, tlsOptionsName) + }, } previousHandler := httpsServer.Server.(*http.Server).Handler @@ -91,7 +101,7 @@ func (e *http3server) Switch(rt *tcprouter.Router) { e.lock.Lock() defer e.lock.Unlock() - e.getter = rt.GetTLSGetClientInfo() + e.getter = rt.HTTP3TLSConfigMatcherFunc() } func (e *http3server) Shutdown(_ context.Context) error { @@ -99,9 +109,28 @@ func (e *http3server) Shutdown(_ context.Context) error { return e.Server.Close() } -func (e *http3server) getGetConfigForClient(info *tls.ClientHelloInfo) (*tls.Config, error) { +func (e *http3server) getTLSConfigForClient(info *tls.ClientHelloInfo) (*tls.Config, error) { e.lock.RLock() defer e.lock.RUnlock() - return e.getter(info) + connData, err := tcpmuxer.NewConnData(info.ServerName, info.Conn.RemoteAddr(), info.SupportedProtos) + if err != nil { + return nil, fmt.Errorf("creating ConnData from client hello: %w", err) + } + + conf, _, err := e.getter(connData) + return conf, err +} + +func (e *http3server) getTLSOptionsName(c *quic.Conn) (string, error) { + e.lock.RLock() + defer e.lock.RUnlock() + + connData, err := tcpmuxer.NewConnData(c.ConnectionState().TLS.ServerName, c.RemoteAddr(), []string{c.ConnectionState().TLS.NegotiatedProtocol}) + if err != nil { + return "", fmt.Errorf("creating ConnData from quic Conn: %w", err) + } + + _, name, err := e.getter(connData) + return name, err } diff --git a/pkg/server/server_entrypoint_tcp_http3_test.go b/pkg/server/server_entrypoint_tcp_http3_test.go index b4d982303c..d7b1e4a0a6 100644 --- a/pkg/server/server_entrypoint_tcp_http3_test.go +++ b/pkg/server/server_entrypoint_tcp_http3_test.go @@ -13,6 +13,7 @@ import ( "github.com/stretchr/testify/require" "github.com/traefik/traefik/v3/pkg/config/static" tcprouter "github.com/traefik/traefik/v3/pkg/server/router/tcp" + traefiktls "github.com/traefik/traefik/v3/pkg/tls" "github.com/traefik/traefik/v3/pkg/types" ) @@ -102,7 +103,7 @@ func TestHTTP3AdvertisedPort(t *testing.T) { router.AddHTTPTLSConfig("*", &tls.Config{ Certificates: []tls.Certificate{tlsCert}, - }) + }, traefiktls.DefaultTLSConfigName) router.SetHTTPSHandler(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { rw.WriteHeader(http.StatusOK) }), nil) @@ -164,7 +165,7 @@ func TestHTTP30RTT(t *testing.T) { router.AddHTTPTLSConfig("example.com", &tls.Config{ Certificates: []tls.Certificate{tlsCert}, - }) + }, traefiktls.DefaultTLSConfigName) router.SetHTTPSHandler(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { rw.WriteHeader(http.StatusOK) }), nil) diff --git a/pkg/tcp/tls.go b/pkg/tcp/tls.go index 207aebb150..33914e5e53 100644 --- a/pkg/tcp/tls.go +++ b/pkg/tcp/tls.go @@ -1,16 +1,39 @@ package tcp import ( + "context" "crypto/tls" ) +// TLSConn is a TLS connection that also carries the name of the TLS config used. +type TLSConn struct { + WriteCloser + + TLSOptionsName string +} + // TLSHandler handles TLS connections. type TLSHandler struct { - Next Handler - Config *tls.Config + Next Handler + Config *tls.Config + TLSOptionsName string } // ServeTCP terminates the TLS connection. func (t *TLSHandler) ServeTCP(conn WriteCloser) { - t.Next.ServeTCP(tls.Server(conn, t.Config)) + t.Next.ServeTCP(tls.Server(TLSConn{WriteCloser: conn, TLSOptionsName: t.TLSOptionsName}, t.Config)) +} + +type tlsOptionsNameKey struct{} + +func AddTLSOptionsNameInContext(ctx context.Context, name string) context.Context { + return context.WithValue(ctx, tlsOptionsNameKey{}, name) +} + +func GetTLSOptionsName(ctx context.Context) string { + if name, ok := ctx.Value(tlsOptionsNameKey{}).(string); ok { + return name + } + + return "" } diff --git a/script/gcg/traefik-bugfix.toml b/script/gcg/traefik-bugfix.toml index 483676a2c4..80f7df1e50 100644 --- a/script/gcg/traefik-bugfix.toml +++ b/script/gcg/traefik-bugfix.toml @@ -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 diff --git a/webui/package.json b/webui/package.json index 186dcaff82..b3e7c7982e 100644 --- a/webui/package.json +++ b/webui/package.json @@ -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" } } diff --git a/webui/yarn.lock b/webui/yarn.lock index 6de5380e6f..8b5bded15c 100644 --- a/webui/yarn.lock +++ b/webui/yarn.lock @@ -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"