diff --git a/CHANGELOG.md b/CHANGELOG.md index 135a288b8c..c0472ec1cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,61 @@ +## [v3.6.8](https://github.com/traefik/traefik/tree/v3.6.8) (2026-02-11) +[All Commits](https://github.com/traefik/traefik/compare/v3.6.7...v3.6.8) + +**Bug fixes:** +- **[acme]** Remove invalid private key in log ([#12574](https://github.com/traefik/traefik/pull/12574) by [juliens](https://github.com/juliens)) +- **[acme]** Alter TLS renewal period ([#12479](https://github.com/traefik/traefik/pull/12479) by [LtHummus](https://github.com/LtHummus)) +- **[healthcheck]** Reject absolute URL in healthcheck path configuration ([#12653](https://github.com/traefik/traefik/pull/12653) by [rtribotte](https://github.com/rtribotte)) +- **[http3]** Bump github.com/quic-go/quic-go to v0.59.0 ([#12553](https://github.com/traefik/traefik/pull/12553) by [jnoordsij](https://github.com/jnoordsij)) +- **[metrics,tracing,accesslogs]** Fix ObservabilityConfig SetDefaults ([#12636](https://github.com/traefik/traefik/pull/12636) by [mmatur](https://github.com/mmatur)) +- **[server]** Remove conn deadline after STARTTLS negociation ([#12639](https://github.com/traefik/traefik/pull/12639) by [rtribotte](https://github.com/rtribotte)) +- **[tls]** Fix verifyServerCertMatchesURI function behavior ([#12575](https://github.com/traefik/traefik/pull/12575) by [kevinpollet](https://github.com/kevinpollet)) +- **[tracing,otel]** Use ParentBased sampler to respect parent span sampling decision ([#12403](https://github.com/traefik/traefik/pull/12403) by [xe-leon](https://github.com/xe-leon)) +- **[webui]** Use url.Parse to validate X-Forwarded-Prefix value ([#12643](https://github.com/traefik/traefik/pull/12643) by [kevinpollet](https://github.com/kevinpollet)) +- **[healthcheck]** Validate healthcheck path configuration (#12642 by @rtribotte) +- **[tls, server]** Cap TLS record length to RFC 8446 limit in ClientHello peeking (#12638 by @mmatur) +- **[service]** Avoid recursion with services ([#12591](https://github.com/traefik/traefik/pull/12591) by [juliens](https://github.com/juliens)) +- **[webui]** Bump dependencies of documentation and webui ([#12581](https://github.com/traefik/traefik/pull/12581) by [gndz07](https://github.com/gndz07)) + +**Documentation:** +- **[k8s]** Fix kubernetes.md with correct http redirections ([#12603](https://github.com/traefik/traefik/pull/12603) by [MartenM](https://github.com/MartenM)) +- **[middleware,k8s/crd]** Fix the errors middleware's document for Kubernetes CRD ([#12600](https://github.com/traefik/traefik/pull/12600) by [yuito-it](https://github.com/yuito-it)) +- **[tls]** Clarify SNI selection ([#12482](https://github.com/traefik/traefik/pull/12482) by [AnuragEkkati](https://github.com/AnuragEkkati)) +- Fix typo on JWT documentation ([#12616](https://github.com/traefik/traefik/pull/12616) by [mdevino](https://github.com/mdevino)) +- Add @gndz07 as a current maintainer ([#12594](https://github.com/traefik/traefik/pull/12594) by [emilevauge](https://github.com/emilevauge)) +- Remove extraneous dots in migration guide ([#12571](https://github.com/traefik/traefik/pull/12571) by [dathbe](https://github.com/dathbe)) +- Document Path matcher placeholder removal in v3 migration guide ([#12570](https://github.com/traefik/traefik/pull/12570) by [sheddy-traefik](https://github.com/sheddy-traefik)) +- Improve Service Reference page ([#12541](https://github.com/traefik/traefik/pull/12541) by [sheddy-traefik](https://github.com/sheddy-traefik)) +- Document negative priority support for routers ([#12505](https://github.com/traefik/traefik/pull/12505) by [understood-the-assignment](https://github.com/understood-the-assignment)) +- Improve the structure of the routing reference pages ([#12429](https://github.com/traefik/traefik/pull/12429) by [sheddy-traefik](https://github.com/sheddy-traefik)) +- Clean Up Menu Entries & Update Expose Overview ([#12405](https://github.com/traefik/traefik/pull/12405) by [sheddy-traefik](https://github.com/sheddy-traefik)) +- Split Expose User Guides & Add Multi-Layer Routing Section ([#12238](https://github.com/traefik/traefik/pull/12238) by [sheddy-traefik](https://github.com/sheddy-traefik)) +- Remove extra dots in migration guide ([#12573](https://github.com/traefik/traefik/pull/12573) by [rtribotte](https://github.com/rtribotte)) + +**Misc:** +- Merge v2.11 into v3.6 ([#12652](https://github.com/traefik/traefik/pull/12652) by [mmatur](https://github.com/mmatur)) +- Merge v2.11 into v3.6 ([#12644](https://github.com/traefik/traefik/pull/12644) by [mmatur](https://github.com/mmatur)) +- Merge branch v2.11 into v3.6 ([#12617](https://github.com/traefik/traefik/pull/12617) by [mmatur](https://github.com/mmatur)) +- Merge v2.11 into v3.6 ([#12605](https://github.com/traefik/traefik/pull/12605) by [mmatur](https://github.com/mmatur)) +- Merge v2.11 into v3.6 ([#12601](https://github.com/traefik/traefik/pull/12601) by [mmatur](https://github.com/mmatur)) +- Merge branch v2.11 into v3.6 ([#12556](https://github.com/traefik/traefik/pull/12556) by [mmatur](https://github.com/mmatur)) + +## [v2.11.37](https://github.com/traefik/traefik/tree/v2.11.37) (2026-02-11) +[All Commits](https://github.com/traefik/traefik/compare/v2.11.36...v2.11.37) + +**Bug fixes:** +- **[healthcheck]** Validate healthcheck path configuration (#12642 by @rtribotte) +- **[tls, server]** Cap TLS record length to RFC 8446 limit in ClientHello peeking (#12638 by @mmatur) + +## [v2.11.36](https://github.com/traefik/traefik/tree/v2.11.36) (2026-02-02) +[All Commits](https://github.com/traefik/traefik/compare/v2.11.35...v2.11.36) + +**Bug fixes:** +- **[service]** Avoid recursion with services ([#12591](https://github.com/traefik/traefik/pull/12591) by [juliens](https://github.com/juliens)) +- **[webui]** Bump dependencies of documentation and webui ([#12581](https://github.com/traefik/traefik/pull/12581) by [gndz07](https://github.com/gndz07)) + +**Documentation:** +- Remove extra dots in migration guide ([#12573](https://github.com/traefik/traefik/pull/12573) by [rtribotte](https://github.com/rtribotte)) + ## [v3.6.7](https://github.com/traefik/traefik/tree/v3.6.7) (2026-01-14) [All Commits](https://github.com/traefik/traefik/compare/v3.6.6...v3.6.7) diff --git a/docs/content/assets/css/content-width.css b/docs/content/assets/css/content-width.css new file mode 100644 index 0000000000..e9c7141c88 --- /dev/null +++ b/docs/content/assets/css/content-width.css @@ -0,0 +1,4 @@ +/* Use a wider grid to accommodate table content and code blocks. */ +.md-grid { + max-width: 1650px; +} diff --git a/docs/content/migrate/v3.md b/docs/content/migrate/v3.md index c52d8aab0e..1b7ebb0223 100644 --- a/docs/content/migrate/v3.md +++ b/docs/content/migrate/v3.md @@ -604,6 +604,12 @@ in [RFC3986 section-3](https://datatracker.ietf.org/doc/html/rfc3986#section-3). Please check out the entrypoint [encodedCharacters option](../routing/entrypoints.md#encoded-characters) documentation for more details. +## v3.6.8 + +### Health Check Request Path + +Since `v3.6.8`, the configured path for the health check request is now verified to be a relative URL, and the health check will fail if it is not. + ## v3.7.0 ### Ingress NGINX Provider diff --git a/docs/content/providers/kubernetes-gateway.md b/docs/content/providers/kubernetes-gateway.md index 7842bb7217..419cb8cbd4 100644 --- a/docs/content/providers/kubernetes-gateway.md +++ b/docs/content/providers/kubernetes-gateway.md @@ -290,13 +290,13 @@ See [label-selectors](https://kubernetes.io/docs/concepts/overview/working-with- ```yaml tab="File (YAML)" providers: kubernetesGateway: - labelselector: "app=traefik" + labelSelector: "app=traefik" # ... ``` ```toml tab="File (TOML)" [providers.kubernetesGateway] - labelselector = "app=traefik" + labelSelector = "app=traefik" # ... ``` diff --git a/docs/content/reference/install-configuration/providers/kubernetes/knative.md b/docs/content/reference/install-configuration/providers/kubernetes/knative.md index 810bf2335c..a4626304fc 100644 --- a/docs/content/reference/install-configuration/providers/kubernetes/knative.md +++ b/docs/content/reference/install-configuration/providers/kubernetes/knative.md @@ -91,7 +91,7 @@ The provider then watches for incoming Knative events and derives the correspond | providers.knative.token | Bearer token used for the Kubernetes client configuration. | | | providers.knative.certauthfilepath | Path to the certificate authority file.
Used for the Kubernetes client configuration. | | | providers.knative.namespaces | Array of namespaces to watch.
If left empty, watch all namespaces. | | -| providers.knative.labelselector | Allow filtering Knative Ingress objects using label selectors. | | +| providers.knative.labelSelector | Allow filtering Knative Ingress objects using label selectors. | | | providers.knative.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. | 0 | | providers.knative.privateentrypoints | Entrypoint names used to expose the Ingress privately. If empty local Ingresses are skipped. | | | providers.knative.privateservice | Kubernetes service used to expose the networking controller privately. | | diff --git a/docs/content/reference/install-configuration/providers/kubernetes/kubernetes-crd.md b/docs/content/reference/install-configuration/providers/kubernetes/kubernetes-crd.md index 5c85e5f09c..0c17c45b06 100644 --- a/docs/content/reference/install-configuration/providers/kubernetes/kubernetes-crd.md +++ b/docs/content/reference/install-configuration/providers/kubernetes/kubernetes-crd.md @@ -59,7 +59,7 @@ providers: | `providers.kubernetesCRD.token` | Bearer token used for the Kubernetes client configuration. | "" | No | | `providers.kubernetesCRD.certAuthFilePath` | Path to the certificate authority file.
Used for the Kubernetes client configuration. | "" | No | | `providers.kubernetesCRD.namespaces` | Array of namespaces to watch.
If left empty, watch all namespaces. | [] | No | -| `providers.kubernetesCRD.labelselector` | Allow filtering on specific resource objects only using label selectors.
Only to Traefik [Custom Resources](#list-of-resources) (they all must match the filter).
No effect on Kubernetes `Secrets`, `EndpointSlices` and `Services`.
See [label-selectors](https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#label-selectors) for details. | "" | No | +| `providers.kubernetesCRD.labelSelector` | Allow filtering on specific resource objects only using label selectors.
Only to Traefik [Custom Resources](#list-of-resources) (they all must match the filter).
No effect on Kubernetes `Secrets`, `EndpointSlices` and `Services`.
See [label-selectors](https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#label-selectors) for details. | "" | No | | `providers.kubernetesCRD.ingressClass` | Value of `spec.ingressClassName` field (or the deprecated `kubernetes.io/ingress.class` annotation) that identifies resource objects to be processed.
If empty, resources missing the field/annotation, having an empty value, or the value `traefik` are processed.
The `spec.ingressClassName` field takes precedence over the annotation. | "" | No | | `providers.kubernetesCRD.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.kubernetesCRD.allowEmptyServices` | Allows creating a route to reach a service that has no endpoint available.
It allows Traefik to handle the requests and responses targeting this service (applying middleware or observability operations) before returning a `503` HTTP Status. | false | No | 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 2a59a8300f..6992eb3d2b 100644 --- a/docs/content/reference/install-configuration/providers/kubernetes/kubernetes-gateway.md +++ b/docs/content/reference/install-configuration/providers/kubernetes/kubernetes-gateway.md @@ -75,7 +75,7 @@ providers: | `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 specific resource objects only using label selectors.
Only to Traefik [Custom Resources](./kubernetes-crd.md#list-of-resources) (they all must match the filter).
No effect on Kubernetes `Secrets`, `EndpointSlices` and `Services`.
See [label-selectors](https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#label-selectors) for details. | "" | No | +| `providers.kubernetesGateway.labelSelector` | Allow filtering on specific resource objects only using label selectors.
Only to Traefik [Custom Resources](./kubernetes-crd.md#list-of-resources) (they all must match the filter).
No effect on Kubernetes `Secrets`, `EndpointSlices` and `Services`.
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 | diff --git a/docs/content/reference/install-configuration/providers/kubernetes/kubernetes-ingress.md b/docs/content/reference/install-configuration/providers/kubernetes/kubernetes-ingress.md index b9df965b79..c601e8166b 100644 --- a/docs/content/reference/install-configuration/providers/kubernetes/kubernetes-ingress.md +++ b/docs/content/reference/install-configuration/providers/kubernetes/kubernetes-ingress.md @@ -52,7 +52,7 @@ which in turn creates the resulting routers, services, handlers, etc. | `providers.kubernetesIngress.token` | Bearer token used for the Kubernetes client configuration. | "" | No | | `providers.kubernetesIngress.certAuthFilePath` | Path to the certificate authority file.
Used for the Kubernetes client configuration. | "" | No | | `providers.kubernetesIngress.namespaces` | Array of namespaces to watch.
If left empty, watch all namespaces. | | No | -| `providers.kubernetesIngress.labelselector` | Allow filtering on Ingress objects using label selectors.
No effect on Kubernetes `Secrets`, `EndpointSlices` and `Services`.
See [label-selectors](https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#label-selectors) for details. | "" | No | +| `providers.kubernetesIngress.labelSelector` | Allow filtering on Ingress objects using label selectors.
No effect on Kubernetes `Secrets`, `EndpointSlices` and `Services`.
See [label-selectors](https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#label-selectors) for details. | "" | No | | `providers.kubernetesIngress.ingressClass` | The `IngressClass` resource name or the `kubernetes.io/ingress.class` annotation value that identifies resource objects to be processed.
If empty, resources missing the annotation, having an empty value, or the value `traefik` are processed. | "" | No | | `providers.kubernetesIngress.disableIngressClassLookup` | Prevent to discover IngressClasses in the cluster.
It alleviates the requirement of giving Traefik the rights to look IngressClasses up.
Ignore Ingresses with IngressClass.
Annotations are not affected by this option. | false | No | | `providers.kubernetesIngress.`
`ingressEndpoint.hostname`
| Hostname used for Kubernetes Ingress endpoints. | "" | No | diff --git a/docs/content/reference/routing-configuration/http/load-balancing/service.md b/docs/content/reference/routing-configuration/http/load-balancing/service.md index 131fc2691e..2512b98a39 100644 --- a/docs/content/reference/routing-configuration/http/load-balancing/service.md +++ b/docs/content/reference/routing-configuration/http/load-balancing/service.md @@ -292,9 +292,9 @@ To propagate status changes (e.g. all servers of this service are down) upwards, Below are the available options for the health check mechanism: -| Field | Description | Default | Required | -|---------------------|-------------------------------------------------------------------------------------------------------------------------------|---------|----------| -| `path` | Defines the server URL path for the health check endpoint. | "" | Yes | +| Field | Description | Default | Required | +|--------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------|---------|----------| +| `path` | Defines the server URL path for the health check endpoint. The configured path must be relative URL. | "" | Yes | | `scheme` | Replaces the server URL scheme for the health check endpoint. | | No | | `mode` | If defined to `grpc`, will use the gRPC health check protocol to probe the server. | http | No | | `hostname` | Defines the value of hostname in the Host header of the health check request. | "" | No | diff --git a/docs/content/reference/routing-configuration/kubernetes/crd/http/service.md b/docs/content/reference/routing-configuration/kubernetes/crd/http/service.md index 429b654c1f..0d701d9a3d 100644 --- a/docs/content/reference/routing-configuration/kubernetes/crd/http/service.md +++ b/docs/content/reference/routing-configuration/kubernetes/crd/http/service.md @@ -80,36 +80,36 @@ spec: ## Configuration Options -| Field | Description | Default | Required | -|:---------------------------------------------------------------------------------|:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:---------------------------------------------------------------------|:---------| -| `kind` | Kind of the service targeted.
Two values allowed:
- **Service**: Kubernetes Service
**TraefikService**: Traefik Service.
More information [here](#externalname-service). | "Service" | No | -| `name` | Service name.
The character `@` is not authorized.
More information [here](#middleware). | | Yes | -| `namespace` | Service namespace.
Can be empty if the service belongs to the same namespace as the IngressRoute.
More information [here](#externalname-service). | | No | -| `port` | Service port (number or port name).
Evaluated only if the kind is **Service**. | | No | -| `responseForwarding.`
`flushInterval`
| Interval, in milliseconds, in between flushes to the client while copying the response body.
A negative value means to flush immediately after each write to the client.
This configuration is ignored when a response is a streaming response; for such responses, writes are flushed to the client immediately.
Evaluated only if the kind is **Service**. | 100ms | No | -| `scheme` | Scheme to use for the request to the upstream Kubernetes Service.
Evaluated only if the kind is **Service**. | "http"
"https" if `port` is 443 or contains the string *https*. | No | -| `serversTransport` | Name of ServersTransport resource to use to configure the transport between Traefik and your servers.
Evaluated only if the kind is **Service**. | "" | No | -| `passHostHeader` | Forward client Host header to server.
Evaluated only if the kind is **Service**. | true | No | -| `healthCheck.scheme` | Server URL scheme for the health check endpoint.
Evaluated only if the kind is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type [ExternalName](#externalname-service). | "" | No | -| `healthCheck.mode` | Health check mode.
If defined to grpc, will use the gRPC health check protocol to probe the server.
Evaluated only if the kind is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type [ExternalName](#externalname-service). | "http" | No | -| `healthCheck.path` | Server URL path for the health check endpoint.
Evaluated only if the kind is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type [ExternalName](#externalname-service). | "" | No | -| `healthCheck.interval` | Frequency of the health check calls for healthy targets.
Evaluated only if the kind is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type [ExternalName](#externalname-service). | "100ms" | No | -| `healthCheck.unhealthyInterval` | Frequency of the health check calls for unhealthy targets.
When not defined, it defaults to the `interval` value.
Evaluated only if the kind is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type [ExternalName](#externalname-service). | "100ms" | No | -| `healthCheck.method` | HTTP method for the health check endpoint.
Evaluated only if the kind is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type [ExternalName](#externalname-service). | "GET" | No | -| `healthCheck.status` | Expected HTTP status code of the response to the health check request.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type ExternalName.
If not set, expect a status between 200 and 399.
Evaluated only if the kind is **Service**. | | No | -| `healthCheck.port` | URL port for the health check endpoint.
Evaluated only if the kind is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type [ExternalName](#externalname-service). | | No | -| `healthCheck.timeout` | Maximum duration to wait before considering the server unhealthy.
Evaluated only if the kind is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type [ExternalName](#externalname-service). | "5s" | No | -| `healthCheck.hostname` | Value in the Host header of the health check request.
Evaluated only if the kind is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type [ExternalName](#externalname-service). | "" | No | -| `healthCheck.`
`followRedirect`
| Follow the redirections during the healtchcheck.
Evaluated only if the kind is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type [ExternalName](#externalname-service). | true | No | -| `healthCheck.headers` | Map of header to send to the health check endpoint
Evaluated only if the kind is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type [ExternalName](#externalname-service)). | | No | -| `sticky.`
`cookie.name`
| Name of the cookie used for the stickiness.
When sticky sessions are enabled, a `Set-Cookie` header is set on the initial response to let the client know which server handles the first response.
On subsequent requests, to keep the session alive with the same server, the client should send the cookie with the value set.
If the server pecified in the cookie becomes unhealthy, the request will be forwarded to a new server (and the cookie will keep track of the new server).
Evaluated only if the kind is **Service**. | "" | No | -| `sticky.`
`cookie.httpOnly`
| Allow the cookie can be accessed by client-side APIs, such as JavaScript.
Evaluated only if the kind is **Service**. | false | No | -| `sticky.`
`cookie.secure`
| Allow the cookie can only be transmitted over an encrypted connection (i.e. HTTPS).
Evaluated only if the kind is **Service**. | false | No | -| `sticky.`
`cookie.sameSite`
| [SameSite](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite) policy
Allowed values:
-`none`
-`lax`
`strict`
Evaluated only if the kind is **Service**. | "" | No | -| `sticky.`
`cookie.maxAge`
| Number of seconds until the cookie expires.
Negative number, the cookie expires immediately.
0, the cookie never expires.
Evaluated only if the kind is **Service**. | 0 | No | -| `strategy` | Strategy defines the load balancing strategy between the servers.
Supported values are: wrr (Weighed round-robin), p2c (Power of two choices), hrw (Highest Random Weight), and leasttime (Least-Time).
Evaluated only if the kind is **Service**. | "RoundRobin" | No | -| `nativeLB` | Allow using the Kubernetes Service load balancing between the pods instead of the one provided by Traefik.
Evaluated only if the kind is **Service**. | false | No | -| `nodePortLB` | Use the nodePort IP address when the service type is NodePort.
It allows services to be reachable when Traefik runs externally from the Kubernetes cluster but within the same network of the nodes.
Evaluated only if the kind is **Service**. | false | No | +| Field | Description | Default | Required | +|:---------------------------------------------------------------------------------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:---------------------------------------------------------------------|:---------| +| `kind` | Kind of the service targeted.
Two values allowed:
- **Service**: Kubernetes Service
**TraefikService**: Traefik Service.
More information [here](#externalname-service). | "Service" | No | +| `name` | Service name.
The character `@` is not authorized.
More information [here](#middleware). | | Yes | +| `namespace` | Service namespace.
Can be empty if the service belongs to the same namespace as the IngressRoute.
More information [here](#externalname-service). | | No | +| `port` | Service port (number or port name).
Evaluated only if the kind is **Service**. | | No | +| `responseForwarding.`
`flushInterval`
| Interval, in milliseconds, in between flushes to the client while copying the response body.
A negative value means to flush immediately after each write to the client.
This configuration is ignored when a response is a streaming response; for such responses, writes are flushed to the client immediately.
Evaluated only if the kind is **Service**. | 100ms | No | +| `scheme` | Scheme to use for the request to the upstream Kubernetes Service.
Evaluated only if the kind is **Service**. | "http"
"https" if `port` is 443 or contains the string *https*. | No | +| `serversTransport` | Name of ServersTransport resource to use to configure the transport between Traefik and your servers.
Evaluated only if the kind is **Service**. | "" | No | +| `passHostHeader` | Forward client Host header to server.
Evaluated only if the kind is **Service**. | true | No | +| `healthCheck.scheme` | Server URL scheme for the health check endpoint.
Evaluated only if the kind is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type [ExternalName](#externalname-service). | "" | No | +| `healthCheck.mode` | Health check mode.
If defined to grpc, will use the gRPC health check protocol to probe the server.
Evaluated only if the kind is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type [ExternalName](#externalname-service). | "http" | No | +| `healthCheck.path` | Server URL path for the health check endpoint.
The configured path must be relative URL.
Evaluated only if the kind is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type [ExternalName](#externalname-service). | "" | No | +| `healthCheck.interval` | Frequency of the health check calls for healthy targets.
Evaluated only if the kind is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type [ExternalName](#externalname-service). | "100ms" | No | +| `healthCheck.unhealthyInterval` | Frequency of the health check calls for unhealthy targets.
When not defined, it defaults to the `interval` value.
Evaluated only if the kind is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type [ExternalName](#externalname-service). | "100ms" | No | +| `healthCheck.method` | HTTP method for the health check endpoint.
Evaluated only if the kind is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type [ExternalName](#externalname-service). | "GET" | No | +| `healthCheck.status` | Expected HTTP status code of the response to the health check request.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type ExternalName.
If not set, expect a status between 200 and 399.
Evaluated only if the kind is **Service**. | | No | +| `healthCheck.port` | URL port for the health check endpoint.
Evaluated only if the kind is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type [ExternalName](#externalname-service). | | No | +| `healthCheck.timeout` | Maximum duration to wait before considering the server unhealthy.
Evaluated only if the kind is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type [ExternalName](#externalname-service). | "5s" | No | +| `healthCheck.hostname` | Value in the Host header of the health check request.
Evaluated only if the kind is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type [ExternalName](#externalname-service). | "" | No | +| `healthCheck.`
`followRedirect`
| Follow the redirections during the healtchcheck.
Evaluated only if the kind is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type [ExternalName](#externalname-service). | true | No | +| `healthCheck.headers` | Map of header to send to the health check endpoint
Evaluated only if the kind is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type [ExternalName](#externalname-service)). | | No | +| `sticky.`
`cookie.name`
| Name of the cookie used for the stickiness.
When sticky sessions are enabled, a `Set-Cookie` header is set on the initial response to let the client know which server handles the first response.
On subsequent requests, to keep the session alive with the same server, the client should send the cookie with the value set.
If the server pecified in the cookie becomes unhealthy, the request will be forwarded to a new server (and the cookie will keep track of the new server).
Evaluated only if the kind is **Service**. | "" | No | +| `sticky.`
`cookie.httpOnly`
| Allow the cookie can be accessed by client-side APIs, such as JavaScript.
Evaluated only if the kind is **Service**. | false | No | +| `sticky.`
`cookie.secure`
| Allow the cookie can only be transmitted over an encrypted connection (i.e. HTTPS).
Evaluated only if the kind is **Service**. | false | No | +| `sticky.`
`cookie.sameSite`
| [SameSite](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite) policy
Allowed values:
-`none`
-`lax`
`strict`
Evaluated only if the kind is **Service**. | "" | No | +| `sticky.`
`cookie.maxAge`
| Number of seconds until the cookie expires.
Negative number, the cookie expires immediately.
0, the cookie never expires.
Evaluated only if the kind is **Service**. | 0 | No | +| `strategy` | Strategy defines the load balancing strategy between the servers.
Supported values are: wrr (Weighed round-robin), p2c (Power of two choices), hrw (Highest Random Weight), and leasttime (Least-Time).
Evaluated only if the kind is **Service**. | "RoundRobin" | No | +| `nativeLB` | Allow using the Kubernetes Service load balancing between the pods instead of the one provided by Traefik.
Evaluated only if the kind is **Service**. | false | No | +| `nodePortLB` | Use the nodePort IP address when the service type is NodePort.
It allows services to be reachable when Traefik runs externally from the Kubernetes cluster but within the same network of the nodes.
Evaluated only if the kind is **Service**. | false | No | ### ExternalName Service diff --git a/docs/content/routing/services/index.md b/docs/content/routing/services/index.md index 0ea0fd4cce..db363bd63d 100644 --- a/docs/content/routing/services/index.md +++ b/docs/content/routing/services/index.md @@ -439,7 +439,7 @@ To propagate status changes (e.g. all servers of this service are down) upwards, Below are the available options for the health check mechanism: -- `path` (required), defines the server URL path for the health check endpoint . +- `path` (required), defines the server URL path for the health check endpoint (must be a relative URL). - `scheme` (optional), replaces the server URL `scheme` for the health check endpoint. - `mode` (default: http), if defined to `grpc`, will use the gRPC health check protocol to probe the server. - `hostname` (optional), sets the value of `hostname` in the `Host` header of the health check request. diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index 27a630d289..b5998922c2 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -39,6 +39,7 @@ extra_javascript: extra_css: - assets/css/menu-icons.css - assets/css/code-copy.css + - assets/css/content-width.css plugins: - search diff --git a/integration/docker_test.go b/integration/docker_test.go index 8ae24d9212..88d185454f 100644 --- a/integration/docker_test.go +++ b/integration/docker_test.go @@ -1,7 +1,6 @@ package integration import ( - "encoding/json" "io" "net/http" "testing" @@ -75,16 +74,8 @@ func (s *DockerSuite) TestDefaultDockerContainers() { require.NoError(s.T(), err) req.Host = "simple.docker.localhost" - resp, err := try.ResponseUntilStatusCode(req, 3*time.Second, http.StatusOK) + _, err = try.ResponseUntilStatusCode(req, 3*time.Second, http.StatusOK) require.NoError(s.T(), err) - - body, err := io.ReadAll(resp.Body) - require.NoError(s.T(), err) - - var version map[string]any - - assert.NoError(s.T(), json.Unmarshal(body, &version)) - assert.Equal(s.T(), "swarm/1.0.0", version["Version"]) } func (s *DockerSuite) TestDockerContainersWithTCPLabels() { @@ -139,16 +130,8 @@ func (s *DockerSuite) TestDockerContainersWithLabels() { require.NoError(s.T(), err) req.Host = "my.super.host" - resp, err := try.ResponseUntilStatusCode(req, 3*time.Second, http.StatusOK) + _, err = try.ResponseUntilStatusCode(req, 3*time.Second, http.StatusOK) require.NoError(s.T(), err) - - body, err := io.ReadAll(resp.Body) - require.NoError(s.T(), err) - - var version map[string]any - - assert.NoError(s.T(), json.Unmarshal(body, &version)) - assert.Equal(s.T(), "swarm/1.0.0", version["Version"]) } func (s *DockerSuite) TestDockerContainersWithOneMissingLabels() { @@ -197,17 +180,9 @@ func (s *DockerSuite) TestRestartDockerContainers() { req.Host = "my.super.host" // TODO Need to wait than 500 milliseconds more (for swarm or traefik to boot up ?) - resp, err := try.ResponseUntilStatusCode(req, 1500*time.Millisecond, http.StatusOK) + _, err = try.ResponseUntilStatusCode(req, 1500*time.Millisecond, http.StatusOK) require.NoError(s.T(), err) - body, err := io.ReadAll(resp.Body) - require.NoError(s.T(), err) - - var version map[string]any - - assert.NoError(s.T(), json.Unmarshal(body, &version)) - assert.Equal(s.T(), "swarm/1.0.0", version["Version"]) - err = try.GetRequest("http://127.0.0.1:8080/api/rawdata", 60*time.Second, try.BodyContains("powpow")) require.NoError(s.T(), err) diff --git a/integration/resources/compose/docker.yml b/integration/resources/compose/docker.yml index 783d5be385..76d1ad5752 100644 --- a/integration/resources/compose/docker.yml +++ b/integration/resources/compose/docker.yml @@ -1,7 +1,6 @@ services: simple: - image: swarm:1.0.0 - command: [ "manage", "token://blablabla" ] + image: traefik/whoami withtcplabels: image: traefik/whoamitcp @@ -12,26 +11,24 @@ services: traefik.tcp.Services.Super.Loadbalancer.server.port: 8080 withlabels1: - image: swarm:1.0.0 - command: [ "manage", "token://blabla" ] + image: traefik/whoami labels: traefik.http.Routers.Super.Rule: Host(`my.super.host`) withlabels2: - image: swarm:1.0.0 - command: [ "manage", "token://blablabla" ] + image: traefik/whoami labels: traefik.http.Routers.SuperHost.Rule: Host(`my-super.host`) withonelabelmissing: - image: swarm:1.0.0 - command: [ "manage", "token://blabla" ] + image: traefik/whoami labels: traefik.random.value: my.super.host powpow: - image: swarm:1.0.0 - command: [ "manage", "token://blabla" ] + image: traefik/whoami + command: + - --port=2375 labels: traefik.http.Routers.Super.Rule: Host(`my.super.host`) traefik.http.Services.powpow.LoadBalancer.server.Port: 2375 diff --git a/pkg/api/dashboard/dashboard.go b/pkg/api/dashboard/dashboard.go index dd8c335b5d..8c344bbd5d 100644 --- a/pkg/api/dashboard/dashboard.go +++ b/pkg/api/dashboard/dashboard.go @@ -4,6 +4,7 @@ import ( "fmt" "io/fs" "net/http" + "net/url" "strings" "text/template" @@ -80,7 +81,9 @@ func Append(router *mux.Router, basePath string, customAssets fs.FS) error { Path(basePath). HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { xfPrefix := req.Header.Get("X-Forwarded-Prefix") - if strings.Contains(xfPrefix, "//") { + + // Validates that the X-Forwarded-Prefix value contains a relative URL. + if u, err := url.Parse(xfPrefix); err != nil || u.Host != "" || u.Scheme != "" { log.Error().Msgf("X-Forwarded-Prefix contains an invalid value: %s, defaulting to empty prefix", xfPrefix) xfPrefix = "" } diff --git a/pkg/healthcheck/healthcheck.go b/pkg/healthcheck/healthcheck.go index a0d1a833ab..d8b7c81294 100644 --- a/pkg/healthcheck/healthcheck.go +++ b/pkg/healthcheck/healthcheck.go @@ -246,6 +246,15 @@ func (shc *ServiceHealthChecker) checkHealthHTTP(ctx context.Context, target *ur } func (shc *ServiceHealthChecker) newRequest(ctx context.Context, target *url.URL) (*http.Request, error) { + pathURL, err := url.Parse(shc.config.Path) + if err != nil { + return nil, fmt.Errorf("parsing health check path: %w", err) + } + + if pathURL.Host != "" || pathURL.Scheme != "" { + return nil, fmt.Errorf("health check path must be a relative URL, got: %q", shc.config.Path) + } + u, err := target.Parse(shc.config.Path) if err != nil { return nil, err diff --git a/pkg/healthcheck/healthcheck_test.go b/pkg/healthcheck/healthcheck_test.go index cba259ce6e..9f39eaaaf4 100644 --- a/pkg/healthcheck/healthcheck_test.go +++ b/pkg/healthcheck/healthcheck_test.go @@ -147,6 +147,14 @@ func TestServiceHealthChecker_newRequest(t *testing.T) { expHostname: "backend1:80", expMethod: http.MethodGet, }, + { + desc: "path is an ablsolute URL", + targetURL: "http://backend1:80", + config: dynamic.ServerHealthCheck{ + Path: "http://backend2/health?powpow=do", + }, + expError: true, + }, { desc: "path with param", targetURL: "http://backend1:80", diff --git a/pkg/middlewares/auth/forward.go b/pkg/middlewares/auth/forward.go index 2a84c581b5..6f49c25671 100644 --- a/pkg/middlewares/auth/forward.go +++ b/pkg/middlewares/auth/forward.go @@ -44,6 +44,8 @@ var hopHeaders = []string{ forward.Upgrade, } +var userAgentHeader = http.CanonicalHeaderKey("User-Agent") + type forwardAuth struct { address string authResponseHeaders []string @@ -369,6 +371,12 @@ func writeHeader(req, forwardReq *http.Request, trustForwardHeader bool, allowed RemoveConnectionHeaders(forwardReq) utils.RemoveHeaders(forwardReq.Header, hopHeaders...) + if _, ok := req.Header[userAgentHeader]; !ok { + // If the incoming request doesn't have a User-Agent header set, + // don't send the default Go HTTP client User-Agent for the forwarded request. + forwardReq.Header.Set(userAgentHeader, "") + } + forwardReq.Header = filterForwardRequestHeaders(forwardReq.Header, allowedHeaders) if clientIP, _, err := net.SplitHostPort(req.RemoteAddr); err == nil { diff --git a/pkg/middlewares/auth/forward_test.go b/pkg/middlewares/auth/forward_test.go index 24ce2aaa3b..4c75148fa7 100644 --- a/pkg/middlewares/auth/forward_test.go +++ b/pkg/middlewares/auth/forward_test.go @@ -536,8 +536,7 @@ func Test_writeHeader(t *testing.T) { trustForwardHeader: false, emptyHost: true, expectedHeaders: map[string]string{ - "Accept": "application/json", - "X-Forwarded-Host": "", + "Accept": "application/json", }, }, { @@ -610,6 +609,7 @@ func Test_writeHeader(t *testing.T) { "X-Forwarded-Method": "GET", forward.ProxyAuthenticate: "ProxyAuthenticate", forward.ProxyAuthorization: "ProxyAuthorization", + "User-Agent": "", }, checkForUnexpectedHeaders: true, }, @@ -652,6 +652,65 @@ func Test_writeHeader(t *testing.T) { }, checkForUnexpectedHeaders: true, }, + { + name: "set empty User-Agent header if header is allowed but missing", + headers: map[string]string{ + "X-CustomHeader": "CustomHeader", + "Accept": "application/json", + }, + authRequestHeaders: []string{ + "X-CustomHeader", + "Accept", + "User-Agent", + }, + expectedHeaders: map[string]string{ + "X-CustomHeader": "CustomHeader", + "Accept": "application/json", + "X-Forwarded-Proto": "http", + "X-Forwarded-Host": "foo.bar", + "X-Forwarded-Uri": "/path?q=1", + "X-Forwarded-Method": "GET", + "User-Agent": "", + }, + checkForUnexpectedHeaders: true, + }, + { + name: "ignore User-Agent header if header is not allowed and missing", + headers: map[string]string{ + "X-CustomHeader": "CustomHeader", + "Accept": "application/json", + }, + authRequestHeaders: []string{ + "X-CustomHeader", + "Accept", + }, + expectedHeaders: map[string]string{ + "X-CustomHeader": "CustomHeader", + "Accept": "application/json", + "X-Forwarded-Proto": "http", + "X-Forwarded-Host": "foo.bar", + "X-Forwarded-Uri": "/path?q=1", + "X-Forwarded-Method": "GET", + }, + checkForUnexpectedHeaders: true, + }, + { + name: "set empty User-Agent header if header is missing", + headers: map[string]string{ + "X-CustomHeader": "CustomHeader", + "Accept": "application/json", + }, + expectedHeaders: map[string]string{ + "X-CustomHeader": "CustomHeader", + "Accept": "application/json", + "X-Forwarded-Proto": "http", + "X-Forwarded-Host": "foo.bar", + "X-Forwarded-Uri": "/path?q=1", + "X-Forwarded-Method": "GET", + "User-Agent": "", + }, + checkForUnexpectedHeaders: true, + }, } for _, test := range testCases { @@ -673,9 +732,14 @@ func Test_writeHeader(t *testing.T) { expectedHeaders := test.expectedHeaders for key, value := range expectedHeaders { + _, headerExists := actualHeaders[http.CanonicalHeaderKey(key)] + + assert.True(t, headerExists, "Expected header %s not found", key) assert.Equal(t, value, actualHeaders.Get(key)) + actualHeaders.Del(key) } + if test.checkForUnexpectedHeaders { for key := range actualHeaders { assert.Fail(t, "Unexpected header found", key) diff --git a/pkg/server/router/tcp/postgres.go b/pkg/server/router/tcp/postgres.go index 5d773704fb..dedb7d6f4e 100644 --- a/pkg/server/router/tcp/postgres.go +++ b/pkg/server/router/tcp/postgres.go @@ -7,6 +7,7 @@ import ( "io" "net" "sync" + "time" "github.com/rs/zerolog/log" tcpmuxer "github.com/traefik/traefik/v3/pkg/muxer/tcp" @@ -46,7 +47,7 @@ func isPostgres(br *bufio.Reader) (bool, error) { func (r *Router) servePostgres(conn tcp.WriteCloser) { _, err := conn.Write(PostgresStartTLSReply) if err != nil { - conn.Close() + _ = conn.Close() return } @@ -55,32 +56,39 @@ func (r *Router) servePostgres(conn tcp.WriteCloser) { b := make([]byte, len(PostgresStartTLSMsg)) _, err = br.Read(b) if err != nil { - conn.Close() + _ = conn.Close() return } hello, err := clientHelloInfo(br) if err != nil { - conn.Close() + _ = conn.Close() return } if !hello.isTLS { - conn.Close() + _ = conn.Close() return } + // The deadline was there to prevent hanging connections while waiting for the client, + // now that the STARTTLS message and Client Hello have been read, + // we can remove it and leave its handling to the TCP reverse proxy eventually. + if err := conn.SetDeadline(time.Time{}); err != nil { + log.Error().Err(err).Msg("Error while setting deadline") + } + connData, err := tcpmuxer.NewConnData(hello.serverName, conn, hello.protos) if err != nil { log.Error().Err(err).Msg("Error while reading TCP connection data") - conn.Close() + _ = conn.Close() return } // Contains also TCP TLS passthrough routes. handlerTCPTLS, _ := r.muxerTCPTLS.Match(connData) if handlerTCPTLS == nil { - conn.Close() + _ = conn.Close() return } diff --git a/pkg/server/router/tcp/router.go b/pkg/server/router/tcp/router.go index b2b4ec93f8..872aa19d14 100644 --- a/pkg/server/router/tcp/router.go +++ b/pkg/server/router/tcp/router.go @@ -18,7 +18,15 @@ import ( "github.com/traefik/traefik/v3/pkg/tcp" ) -const defaultBufSize = 4096 +const ( + defaultBufSize = 4096 + // Per RFC 8446 Section 5.1, the maximum TLS record payload length is 2^14 (16384) bytes. + // A ClientHello is always a plaintext record, so any value exceeding this limit is invalid + // and likely indicates an attack attempting to force oversized per-connection buffer allocations. + // However, in practice the go server handshake can read up to 16384 + 2048 bytes, + // so we need to allow for some extra bytes to avoid rejecting valid handshakes. + maxTLSRecordLen = 16384 + 2048 +) // Router is a TCP router. type Router struct { @@ -126,11 +134,6 @@ func (r *Router) ServeTCP(conn tcp.WriteCloser) { } if postgres { - // Remove read/write deadline and delegate this to underlying TCP server. - if err := conn.SetDeadline(time.Time{}); err != nil { - log.Error().Err(err).Msg("Error while setting deadline") - } - r.servePostgres(r.GetConn(conn, getPeeked(br))) return } @@ -141,7 +144,9 @@ func (r *Router) ServeTCP(conn tcp.WriteCloser) { return } - // Remove read/write deadline and delegate this to underlying TCP server (for now only handled by HTTP Server) + // The deadline was set to avoid blocking on the initial read of the ClientHello, + // but now that we have it, we can remove it, + // and delegate this to underlying TCP server (for now only handled by HTTP Server). if err := conn.SetDeadline(time.Time{}); err != nil { log.Error().Err(err).Msg("Error while setting deadline") } @@ -409,6 +414,14 @@ func clientHelloInfo(br *bufio.Reader) (*clientHello, error) { recLen := int(hdr[3])<<8 | int(hdr[4]) // ignoring version in hdr[1:3] + if recLen > maxTLSRecordLen { + log.Debug().Msgf("Error while peeking client hello bytes, oversized record: %d", recLen) + return &clientHello{ + isTLS: true, + peeked: getPeeked(br), + }, nil + } + if recordHeaderLen+recLen > defaultBufSize { br = bufio.NewReaderSize(br, recordHeaderLen+recLen) } diff --git a/pkg/server/router/tcp/router_test.go b/pkg/server/router/tcp/router_test.go index 12bb4a378d..6c22c6a4cb 100644 --- a/pkg/server/router/tcp/router_test.go +++ b/pkg/server/router/tcp/router_test.go @@ -1,6 +1,7 @@ package tcp import ( + "bufio" "bytes" "crypto/tls" "errors" @@ -648,6 +649,7 @@ func Test_Routing(t *testing.T) { _ = serverHTTPS.Serve(httpsForwarder) }() + // The HTTPS forwarder will be added as tcp.TLSHandler (to handle TLS). router.SetHTTPSForwarder(httpsForwarder) stoppedTCP := make(chan struct{}) @@ -1082,6 +1084,118 @@ func checkHTTPSTLS12(addr string, timeout time.Duration) error { return checkHTTPS(addr, timeout, tls.VersionTLS12) } +// Test_clientHelloInfo_oversizedRecordLength verifies that clientHelloInfo +// does not block or allocate excessive memory when a client sends a TLS +// record header with a maliciously large record length (up to 0xFFFF). +// +// Without the fix, clientHelloInfo allocates a ~65KB bufio.Reader and blocks +// on Peek(65540), waiting for bytes that never arrive (until readTimeout). +// With the fix, records exceeding the TLS maximum plaintext size (16384) +// are rejected immediately. +func Test_clientHelloInfo_oversizedRecordLength(t *testing.T) { + testCases := []struct { + desc string + recLen uint16 + }{ + { + desc: "max uint16 record length (0xFFFF)", + recLen: 0xFFFF, + }, + { + desc: "just above TLS maximum (18433)", + recLen: 18433, + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + serverConn, clientConn := net.Pipe() + defer serverConn.Close() + defer clientConn.Close() + + type result struct { + hello *clientHello + err error + } + resultCh := make(chan result, 1) + + go func() { + br := bufio.NewReader(serverConn) + hello, err := clientHelloInfo(br) + resultCh <- result{hello, err} + }() + + // Send a TLS record header with an oversized record length. + // Only the 5-byte header is sent; the client then stalls. + hdr := []byte{ + 0x16, // Content Type: Handshake + 0x03, 0x03, // Version: TLS 1.2 + byte(test.recLen >> 8), // Length high byte + byte(test.recLen & 0xFF), // Length low byte + } + _, err := clientConn.Write(hdr) + require.NoError(t, err) + + // Without the fix, clientHelloInfo blocks on Peek(recLen+5) + // since only 5 bytes are available. The test would time out. + // With the fix, it returns immediately. + select { + case r := <-resultCh: + require.NoError(t, r.err) + require.NotNil(t, r.hello) + assert.True(t, r.hello.isTLS) + case <-time.After(5 * time.Second): + t.Fatal("clientHelloInfo blocked on oversized TLS record length — recLen is not capped") + } + }) + } +} + +// Test_clientHelloInfo_validRecordLength verifies that clientHelloInfo +// still works correctly with legitimate TLS record sizes. +func Test_clientHelloInfo_validRecordLength(t *testing.T) { + serverConn, clientConn := net.Pipe() + defer serverConn.Close() + defer clientConn.Close() + + type result struct { + hello *clientHello + err error + } + resultCh := make(chan result, 1) + + go func() { + br := bufio.NewReader(serverConn) + hello, err := clientHelloInfo(br) + resultCh <- result{hello, err} + }() + + // Build a TLS record header with a small (valid) record length. + recLen := 100 + hdr := []byte{ + 0x16, // Content Type: Handshake + 0x03, 0x03, // Version: TLS 1.2 + byte(recLen >> 8), // Length high byte + byte(recLen & 0xFF), // Length low byte + } + payload := make([]byte, recLen) + + _, err := clientConn.Write(append(hdr, payload...)) + require.NoError(t, err) + clientConn.Close() + + select { + case r := <-resultCh: + require.NoError(t, r.err) + require.NotNil(t, r.hello) + assert.True(t, r.hello.isTLS) + case <-time.After(5 * time.Second): + t.Fatal("clientHelloInfo blocked on valid TLS record") + } +} + func TestPostgres(t *testing.T) { router, err := NewRouter() require.NoError(t, err) diff --git a/script/gcg/traefik-bugfix.toml b/script/gcg/traefik-bugfix.toml index a893c77c61..92354ec43a 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.6.7 +# example new bugfix v3.6.8 CurrentRef = "v3.6" -PreviousRef = "v3.6.6" +PreviousRef = "v3.6.7" BaseBranch = "v3.6" -FutureCurrentRefName = "v3.6.7" +FutureCurrentRefName = "v3.6.8" ThresholdPreviousRef = 10000 ThresholdCurrentRef = 10000