This commit is contained in:
Romain 2025-07-31 12:13:33 +00:00 committed by GitHub
commit c7c610bfd0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
26 changed files with 542 additions and 400 deletions

View File

@ -730,7 +730,7 @@ In v3, we renamed the `IPWhiteList` middleware to `IPAllowList` without changing
### TCP LoadBalancer `terminationDelay` option
The TCP LoadBalancer `terminationDelay` option has been removed.
The TCP LoadBalancer `terminationDelay` option has been deprecated.
This option can now be configured directly on the `TCPServersTransport` level, please take a look at this [documentation](../routing/services/index.md#terminationdelay)
### Kubernetes CRDs API Group `traefik.containo.us`

View File

@ -420,3 +420,22 @@ The following table illustrates how path matching behavior has changed:
| `/foo/../bar` | ```PathPrefix(`/bar`)``` | Match | Match | Resolves to `/bar` after sanitization |
| `/foo/%2E%2E/bar` | ```PathPrefix(`/foo`)``` | Match | No match | Encoded dots normalized then sanitized |
| `/foo/%2E%2E/bar` | ```PathPrefix(`/bar`)``` | No match | Match | Resolves to `/bar` after normalization + sanitization |
---
## v3.5.1
### Deprecation of ProxyProtocol option
Starting with `v3.5.1`, the `proxyProtocol` option for TCP LoadBalancer is deprecated.
This option can now be configured at the `TCPServersTransport` level, please check out the [documentation](../reference/routing-configuration/tcp/serverstransport.md) for more details.
#### Kubernetes CRD Provider
To use the new `proxyprotocol` option in the Kubernetes CRD provider, you need to update your CRDs.
**Apply Updated CRDs:**
```shell
kubectl apply -f https://raw.githubusercontent.com/traefik/traefik/v3.5/docs/content/reference/dynamic-configuration/kubernetes-crd-definition-v1.yml
```

View File

@ -454,16 +454,16 @@
[tcp.services.TCPService01.loadBalancer]
serversTransport = "foobar"
terminationDelay = 42
[[tcp.services.TCPService01.loadBalancer.servers]]
address = "foobar"
tls = true
[[tcp.services.TCPService01.loadBalancer.servers]]
address = "foobar"
tls = true
[tcp.services.TCPService01.loadBalancer.proxyProtocol]
version = 42
[[tcp.services.TCPService01.loadBalancer.servers]]
address = "foobar"
tls = true
[[tcp.services.TCPService01.loadBalancer.servers]]
address = "foobar"
tls = true
[tcp.services.TCPService02]
[tcp.services.TCPService02.weighted]
@ -489,6 +489,8 @@
dialKeepAlive = "42s"
dialTimeout = "42s"
terminationDelay = "42s"
[tcp.serversTransports.TCPServersTransport0.proxyProtocol]
version = 42
[tcp.serversTransports.TCPServersTransport0.tls]
serverName = "foobar"
insecureSkipVerify = true
@ -509,6 +511,8 @@
dialKeepAlive = "42s"
dialTimeout = "42s"
terminationDelay = "42s"
[tcp.serversTransports.TCPServersTransport1.proxyProtocol]
version = 42
[tcp.serversTransports.TCPServersTransport1.tls]
serverName = "foobar"
insecureSkipVerify = true

View File

@ -518,14 +518,14 @@ tcp:
services:
TCPService01:
loadBalancer:
proxyProtocol:
version: 42
servers:
- address: foobar
tls: true
- address: foobar
tls: true
serversTransport: foobar
proxyProtocol:
version: 42
terminationDelay: 42
TCPService02:
weighted:
@ -552,6 +552,8 @@ tcp:
TCPServersTransport0:
dialKeepAlive: 42s
dialTimeout: 42s
proxyProtocol:
version: 42
terminationDelay: 42s
tls:
serverName: foobar
@ -573,6 +575,8 @@ tcp:
TCPServersTransport1:
dialKeepAlive: 42s
dialTimeout: 42s
proxyProtocol:
version: 42
terminationDelay: 42s
tls:
serverName: foobar

View File

@ -544,6 +544,7 @@ spec:
description: |-
ProxyProtocol defines the PROXY protocol configuration.
More info: https://doc.traefik.io/traefik/v3.5/routing/services/#proxy-protocol
Deprecated: ProxyProtocol will not be supported in future APIVersions, please use ServersTransport to configure ProxyProtocol instead.
properties:
version:
description: Version defines the PROXY Protocol version
@ -2400,6 +2401,15 @@ spec:
to a backend server can be established.
pattern: ^([0-9]+(ns|us|µs|ms|s|m|h)?)+$
x-kubernetes-int-or-string: true
proxyProtocol:
description: ProxyProtocol holds the PROXY Protocol configuration.
properties:
version:
description: Version defines the PROXY Protocol version to use.
maximum: 2
minimum: 1
type: integer
type: object
terminationDelay:
anyOf:
- type: integer

View File

@ -365,6 +365,7 @@ THIS FILE MUST NOT BE EDITED BY HAND
| `traefik/tcp/routers/TCPRouter1/tls/passthrough` | `true` |
| `traefik/tcp/serversTransports/TCPServersTransport0/dialKeepAlive` | `42s` |
| `traefik/tcp/serversTransports/TCPServersTransport0/dialTimeout` | `42s` |
| `traefik/tcp/serversTransports/TCPServersTransport0/proxyProtocol/version` | `42` |
| `traefik/tcp/serversTransports/TCPServersTransport0/terminationDelay` | `42s` |
| `traefik/tcp/serversTransports/TCPServersTransport0/tls/certificates/0/certFile` | `foobar` |
| `traefik/tcp/serversTransports/TCPServersTransport0/tls/certificates/0/keyFile` | `foobar` |
@ -380,6 +381,7 @@ THIS FILE MUST NOT BE EDITED BY HAND
| `traefik/tcp/serversTransports/TCPServersTransport0/tls/spiffe/trustDomain` | `foobar` |
| `traefik/tcp/serversTransports/TCPServersTransport1/dialKeepAlive` | `42s` |
| `traefik/tcp/serversTransports/TCPServersTransport1/dialTimeout` | `42s` |
| `traefik/tcp/serversTransports/TCPServersTransport1/proxyProtocol/version` | `42` |
| `traefik/tcp/serversTransports/TCPServersTransport1/terminationDelay` | `42s` |
| `traefik/tcp/serversTransports/TCPServersTransport1/tls/certificates/0/certFile` | `foobar` |
| `traefik/tcp/serversTransports/TCPServersTransport1/tls/certificates/0/keyFile` | `foobar` |

View File

@ -123,6 +123,7 @@ spec:
description: |-
ProxyProtocol defines the PROXY protocol configuration.
More info: https://doc.traefik.io/traefik/v3.5/routing/services/#proxy-protocol
Deprecated: ProxyProtocol will not be supported in future APIVersions, please use ServersTransport to configure ProxyProtocol instead.
properties:
version:
description: Version defines the PROXY Protocol version

View File

@ -63,6 +63,15 @@ spec:
to a backend server can be established.
pattern: ^([0-9]+(ns|us|µs|ms|s|m|h)?)+$
x-kubernetes-int-or-string: true
proxyProtocol:
description: ProxyProtocol holds the PROXY Protocol configuration.
properties:
version:
description: Version defines the PROXY Protocol version to use.
maximum: 2
minimum: 1
type: integer
type: object
terminationDelay:
anyOf:
- type: integer

View File

@ -16,7 +16,7 @@ This registers the `IngressRouteTCP` kind and other Traefik-specific resources.
You can declare an `IngressRouteTCP` as detailed below:
```yaml tab="IngressRoute"
```yaml tab="IngressRouteTCP"
apiVersion: traefik.io/v1alpha1
kind: IngressRouteTCP
metadata:
@ -36,8 +36,6 @@ spec:
- name: foo
port: 8080
weight: 10
proxyProtocol:
version: 1
serversTransport: transport
nativeLB: true
nodePortLB: true
@ -59,33 +57,31 @@ spec:
## Configuration Options
| Field | Description | Default | Required |
|-------------------------------------|-----------------------------|-------------------------------------------|-----------------------|
| `entryPoints` | List of entrypoints names. | | No |
| `routes` | List of routes. | | Yes |
| `routes[n].match` | Defines the [rule](../../../tcp/router/rules-and-priority.md#rules) of the underlying router. | | Yes |
| `routes[n].priority` | Defines the [priority](../../../tcp/router/rules-and-priority.md#priority) to disambiguate rules of the same length, for route matching. | | No |
| `routes[n].middlewares[n].name` | Defines the [MiddlewareTCP](./middlewaretcp.md) name. | | Yes |
| `routes[n].middlewares[n].namespace` | Defines the [MiddlewareTCP](./middlewaretcp.md) namespace. | ""| No|
| `routes[n].services` | List of [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) definitions. | | No |
| `routes[n].services[n].name` | Defines the name of a [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/). | | Yes |
| `routes[n].services[n].port` | Defines the port of a [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/). This can be a reference to a named port.| | Yes |
| `routes[n].services[n].weight` | Defines the weight to apply to the server load balancing. | 1 | No |
| `routes[n].services[n].proxyProtocol` | Defines the [PROXY protocol](../../../../install-configuration/entrypoints.md#proxyprotocol-and-load-balancers) configuration. | | No |
| `routes[n].services[n].proxyProtocol.version` | Defines the [PROXY protocol](../../../../install-configuration/entrypoints.md#proxyprotocol-and-load-balancers) version. | | No |
| `routes[n].services[n].serversTransport` | Defines the [ServersTransportTCP](./serverstransporttcp.md).<br />The `ServersTransport` namespace is assumed to be the [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) namespace. | | No |
| `routes[n].services[n].nativeLB` | Controls, when creating the load-balancer, whether the LB's children are directly the pods IPs or if the only child is the Kubernetes Service clusterIP. See [here](#nativelb) for more information. | false | No |
| `routes[n].services[n].nodePortLB` | Controls, when creating the load-balancer, whether the LB's children are directly the nodes internal IPs using the nodePort 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. | false | No |
| `tls` | Defines [TLS](../../../../install-configuration/tls/certificate-resolvers/overview.md) certificate configuration. | | No |
| `tls.secretName` | Defines the [secret](https://kubernetes.io/docs/concepts/configuration/secret/) name used to store the certificate (in the `IngressRoute` namespace). | "" | No |
| `tls.options` | Defines the reference to a [TLSOption](../http/tlsoption.md). | "" | No |
| `tls.options.name` | Defines the [TLSOption](../http/tlsoption.md) name. | "" | No |
| `tls.options.namespace` | Defines the [TLSOption](../http/tlsoption.md) namespace. | "" | No |
| `tls.certResolver` | Defines the reference to a [CertResolver](../../../../install-configuration/tls/certificate-resolvers/overview.md). | "" | No |
| `tls.domains` | List of domains. | "" | No |
| `tls.domains[n].main` | Defines the main domain name. | "" | No |
| `tls.domains[n].sans` | List of SANs (alternative domains). | "" | No |
| `tls.passthrough` | If `true`, delegates the TLS termination to the backend. | false | No |
| Field | Description | Default | Required |
|-----------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------|----------|
| `entryPoints` | List of entrypoints names. | | No |
| `routes` | List of routes. | | Yes |
| `routes[n].match` | Defines the [rule](../../../tcp/router/rules-and-priority.md#rules) of the underlying router. | | Yes |
| `routes[n].priority` | Defines the [priority](../../../tcp/router/rules-and-priority.md#priority) to disambiguate rules of the same length, for route matching. | | No |
| `routes[n].middlewares[n].name` | Defines the [MiddlewareTCP](./middlewaretcp.md) name. | | Yes |
| `routes[n].middlewares[n].namespace` | Defines the [MiddlewareTCP](./middlewaretcp.md) namespace. | "" | No |
| `routes[n].services` | List of [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) definitions. | | No |
| `routes[n].services[n].name` | Defines the name of a [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/). | | Yes |
| `routes[n].services[n].port` | Defines the port of a [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/). This can be a reference to a named port. | | Yes |
| `routes[n].services[n].weight` | Defines the weight to apply to the server load balancing. | 1 | No |
| `routes[n].services[n].serversTransport` | Defines the [ServersTransportTCP](./serverstransporttcp.md).<br />The `ServersTransport` namespace is assumed to be the [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) namespace. | | No |
| `routes[n].services[n].nativeLB` | Controls, when creating the load-balancer, whether the LB's children are directly the pods IPs or if the only child is the Kubernetes Service clusterIP. See [here](#nativelb) for more information. | false | No |
| `routes[n].services[n].nodePortLB` | Controls, when creating the load-balancer, whether the LB's children are directly the nodes internal IPs using the nodePort 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. | false | No |
| `tls` | Defines [TLS](../../../../install-configuration/tls/certificate-resolvers/overview.md) certificate configuration. | | No |
| `tls.secretName` | Defines the [secret](https://kubernetes.io/docs/concepts/configuration/secret/) name used to store the certificate (in the `IngressRoute` namespace). | "" | No |
| `tls.options` | Defines the reference to a [TLSOption](../http/tlsoption.md). | "" | No |
| `tls.options.name` | Defines the [TLSOption](../http/tlsoption.md) name. | "" | No |
| `tls.options.namespace` | Defines the [TLSOption](../http/tlsoption.md) namespace. | "" | No |
| `tls.certResolver` | Defines the reference to a [CertResolver](../../../../install-configuration/tls/certificate-resolvers/overview.md). | "" | No |
| `tls.domains` | List of domains. | "" | No |
| `tls.domains[n].main` | Defines the main domain name. | "" | No |
| `tls.domains[n].sans` | List of SANs (alternative domains). | "" | No |
| `tls.passthrough` | If `true`, delegates the TLS termination to the backend. | false | No |
### ExternalName Service

View File

@ -29,6 +29,9 @@ metadata:
namespace: default
spec:
proxyProtocol:
version: 2
terminationDelay: 100ms
tls:
serverName: example.org
insecureSkipVerify: true
@ -36,16 +39,18 @@ spec:
## Configuration Options
| Field | Description | Default | Required |
|-------------------------------------|-----------------------------|-------------------------------------------|-----------------------|
| `dialTimeout` | The amount of time to wait until a connection to a server can be established. If zero, no timeout exists. | 30s | No |
| `dialKeepAlive` | The interval between keep-alive probes for an active network connection.<br />If this option is set to zero, keep-alive probes are sent with a default value (currently 15 seconds),<br />if supported by the protocol and operating system. Network protocols or operating systems that do not support keep-alives ignore this field.<br />If negative, keep-alive probes are turned off.| 15s | No |
| `terminationDelay` | Defines the delay to wait before fully terminating the connection, after one connected peer has closed its writing capability.| 100ms | No |
| `tls.serverName` | ServerName used to contact the server. | "" | No |
| `tls.insecureSkipVerify` | Controls whether the server's certificate chain and host name is verified. | false | No |
| `tls.peerCertURI` | Defines the URI used to match against SAN URIs during the server's certificate verification. | "" | No |
| `tls.rootCAsSecrets` | Defines the set of root certificate authorities to use when verifying server certificates.<br />The CA secret must contain a base64 encoded certificate under either a `tls.ca` or a `ca.crt` key.| "" | No |
| `tls.certificatesSecrets` | Certificates to present to the server for mTLS.| "" | No |
| `spiffe` | Configures [SPIFFE](../../../../install-configuration/tls/spiffe.md) options. | "" | No |
| `spiffe.ids` | Defines the allowed SPIFFE IDs. This takes precedence over the SPIFFE `trustDomain`. |""| No |
| `spiffe.trustDomain` | Defines the allowed SPIFFE trust domain. | "" | No |
| Field | Description | Default | Required |
|---------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------|----------|
| `dialTimeout` | The amount of time to wait until a connection to a server can be established. If zero, no timeout exists. | 30s | No |
| `dialKeepAlive` | The interval between keep-alive probes for an active network connection.<br />If this option is set to zero, keep-alive probes are sent with a default value (currently 15 seconds),<br />if supported by the protocol and operating system. Network protocols or operating systems that do not support keep-alives ignore this field.<br />If negative, keep-alive probes are turned off. | 15s | No |
| `proxyProtocol` | Defines the Proxy Protocol configuration. An empty `proxyProtocol` section enables Proxy Protocol version 2. | | No |
| `proxyProtocol.version` | Traefik supports PROXY Protocol version 1 and 2 on TCP Services. | | No |
| `terminationDelay` | Defines the delay to wait before fully terminating the connection, after one connected peer has closed its writing capability. | 100ms | No |
| `tls.serverName` | ServerName used to contact the server. | "" | No |
| `tls.insecureSkipVerify` | Controls whether the server's certificate chain and host name is verified. | false | No |
| `tls.peerCertURI` | Defines the URI used to match against SAN URIs during the server's certificate verification. | "" | No |
| `tls.rootCAsSecrets` | Defines the set of root certificate authorities to use when verifying server certificates.<br />The CA secret must contain a base64 encoded certificate under either a `tls.ca` or a `ca.crt` key. | "" | No |
| `tls.certificatesSecrets` | Certificates to present to the server for mTLS. | "" | No |
| `spiffe` | Configures [SPIFFE](../../../../install-configuration/tls/spiffe.md) options. | "" | No |
| `spiffe.ids` | Defines the allowed SPIFFE IDs. This takes precedence over the SPIFFE `trustDomain`. | "" | No |
| `spiffe.trustDomain` | Defines the allowed SPIFFE trust domain. | "" | No |

View File

@ -84,19 +84,21 @@ labels:
## Configuration Options
| Field | Description | Default | Required |
|:------|:----------------------------------------------------------|:---------------------|:---------|
| `serverstransport.`<br />`dialTimeout` | Defines the timeout when dialing the backend TCP service. If zero, no timeout exists. | 30s | No |
| `serverstransport.`<br />`dialKeepAlive` | Defines the interval between keep-alive probes for an active network connection. | 15s | No |
| `serverstransport.`<br />`terminationDelay` | Sets the time limit for the proxy to fully terminate connections on both sides after initiating the termination sequence, with a negative value indicating no deadline. More Information [here](#terminationdelay) | 100ms | No |
| `serverstransport.`<br />`tls` | Defines the TLS configuration. An empty `tls` section enables TLS. | | No |
| `serverstransport.`<br />`tls`<br />`.serverName` | Configures the server name that will be used for SNI. | | No |
| `serverstransport.`<br />`tls`<br />`.certificates` | Defines the list of certificates (as file paths, or data bytes) that will be set as client certificates for mTLS. | | No |
| `serverstransport.`<br />`tls`<br />`.insecureSkipVerify` | Controls whether the server's certificate chain and host name is verified. | false | No |
| `serverstransport.`<br />`tls`<br />`.rootcas` | Defines the root certificate authorities to use when verifying server certificates. (for mTLS connections). | | No |
| `serverstransport.`<br />`tls.`<br />`peerCertURI` | Defines the URI used to match against SAN URIs during the server's certificate verification. | false | No |
| `serverstransport.`<br />`spiffe`<br />`.ids` | Allow SPIFFE IDs.<br />This takes precedence over the SPIFFE TrustDomain. | | No |
| `serverstransport.`<br />`spiffe`<br />`.trustDomain` | Allow SPIFFE trust domain. | "" | No |
| Field | Description | Default | Required |
|:----------------------------------------------------------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:--------|:---------|
| `serverstransport.`<br />`dialTimeout` | Defines the timeout when dialing the backend TCP service. If zero, no timeout exists. | 30s | No |
| `serverstransport.`<br />`dialKeepAlive` | Defines the interval between keep-alive probes for an active network connection. | 15s | No |
| `serverstransport.`<br />`terminationDelay` | Sets the time limit for the proxy to fully terminate connections on both sides after initiating the termination sequence, with a negative value indicating no deadline. More Information [here](#terminationdelay) | 100ms | No |
| `serverstransport.`<br />`proxyProtocol` | Defines the Proxy Protocol configuration. An empty `proxyProtocol` section enables Proxy Protocol version 2. | | No |
| `serverstransport.`<br />`proxyProtocol.version` | Traefik supports PROXY Protocol version 1 and 2 on TCP Services. More Information [here](#proxyprotocolversion) | 2 | No |
| `serverstransport.`<br />`tls` | Defines the TLS configuration. An empty `tls` section enables TLS. | | No |
| `serverstransport.`<br />`tls`<br />`.serverName` | Configures the server name that will be used for SNI. | | No |
| `serverstransport.`<br />`tls`<br />`.certificates` | Defines the list of certificates (as file paths, or data bytes) that will be set as client certificates for mTLS. | | No |
| `serverstransport.`<br />`tls`<br />`.insecureSkipVerify` | Controls whether the server's certificate chain and host name is verified. | false | No |
| `serverstransport.`<br />`tls`<br />`.rootcas` | Defines the root certificate authorities to use when verifying server certificates. (for mTLS connections). | | No |
| `serverstransport.`<br />`tls.`<br />`peerCertURI` | Defines the URI used to match against SAN URIs during the server's certificate verification. | false | No |
| `serverstransport.`<br />`spiffe`<br />`.ids` | Allow SPIFFE IDs.<br />This takes precedence over the SPIFFE TrustDomain. | | No |
| `serverstransport.`<br />`spiffe`<br />`.trustDomain` | Allow SPIFFE trust domain. | "" | No |
!!! note "SPIFFE"
@ -114,3 +116,9 @@ To that end, as soon as the proxy enters this termination sequence, it sets a de
The termination delay controls that deadline.
A negative value means an infinite deadline (i.e. the connection is never fully terminated by the proxy itself).
### `proxyProtocol.version`
Traefik supports [PROXY Protocol](https://www.haproxy.org/download/2.0/doc/proxy-protocol.txt) version 1 and 2 on TCP Services.
It can be configured by setting `proxyProtocol.version` on the serversTransport.
The option specifies the version of the protocol to be used. Either 1 or 2.

View File

@ -42,12 +42,6 @@ tcp:
| `servers.address` | The address option (IP:Port) point to a specific instance. | "" |
| `servers.tls` | The `tls` option determines whether to use TLS when dialing with the backend. | false |
| `servers.serversTransport` | `serversTransport` allows to reference a TCP [ServersTransport](./serverstransport.md configuration for the communication between Traefik and your servers. If no serversTransport is specified, the default@internal will be used. | "" |
| `servers.proxyProtocol.version` | Traefik supports PROXY Protocol version 1 and 2 on TCP Services. More Information [here](#serversproxyprotocolversion) | 2 |
### servers.proxyProtocol.version
Traefik supports [PROXY Protocol](https://www.haproxy.org/download/2.0/doc/proxy-protocol.txt) version 1 and 2 on TCP Services. It can be enabled by setting `proxyProtocol` on the load balancer.
The option specifies the version of the protocol to be used. Either 1 or 2.
## Weighted Round Robin

View File

@ -1648,79 +1648,6 @@ The `tls` determines whether to use TLS when dialing with the backend.
If no serversTransport is specified, the `default@internal` will be used.
The `default@internal` serversTransport is created from the [static configuration](../overview.md#tcp-servers-transports).
#### PROXY Protocol
Traefik supports [PROXY Protocol](https://www.haproxy.org/download/2.0/doc/proxy-protocol.txt) version 1 and 2 on TCP Services.
It can be enabled by setting `proxyProtocol` on the load balancer.
Below are the available options for the PROXY protocol:
- `version` specifies the version of the protocol to be used. Either `1` or `2`.
!!! info "Version"
Specifying a version is optional. By default the version 2 will be used.
??? example "A Service with Proxy Protocol v1 -- Using the [File Provider](../../providers/file.md)"
```yaml tab="YAML"
## Dynamic configuration
tcp:
services:
my-service:
loadBalancer:
proxyProtocol:
version: 1
```
```toml tab="TOML"
## Dynamic configuration
[tcp.services]
[tcp.services.my-service.loadBalancer]
[tcp.services.my-service.loadBalancer.proxyProtocol]
version = 1
```
#### Termination Delay
!!! warning
Deprecated in favor of [`serversTransport.terminationDelay`](#terminationdelay).
Please note that if any `serversTransport` configuration on the servers load balancer is found,
it will take precedence over the servers load balancer `terminationDelay` value,
even if the `serversTransport.terminationDelay` is undefined.
As a proxy between a client and a server, it can happen that either side (e.g. client side) decides to terminate its writing capability on the connection (i.e. issuance of a FIN packet).
The proxy needs to propagate that intent to the other side, and so when that happens, it also does the same on its connection with the other side (e.g. backend side).
However, if for some reason (bad implementation, or malicious intent) the other side does not eventually do the same as well,
the connection would stay half-open, which would lock resources for however long.
To that end, as soon as the proxy enters this termination sequence, it sets a deadline on fully terminating the connections on both sides.
The termination delay controls that deadline.
It is a duration in milliseconds, defaulting to 100.
A negative value means an infinite deadline (i.e. the connection is never fully terminated by the proxy itself).
??? example "A Service with a termination delay -- Using the [File Provider](../../providers/file.md)"
```yaml tab="YAML"
## Dynamic configuration
tcp:
services:
my-service:
loadBalancer:
terminationDelay: 200
```
```toml tab="TOML"
## Dynamic configuration
[tcp.services]
[tcp.services.my-service.loadBalancer]
[[tcp.services.my-service.loadBalancer]]
terminationDelay = 200
```
### Weighted Round Robin
The Weighted Round Robin (alias `WRR`) load-balancer of services is in charge of balancing the requests between multiple services based on provided weights.

View File

@ -544,6 +544,7 @@ spec:
description: |-
ProxyProtocol defines the PROXY protocol configuration.
More info: https://doc.traefik.io/traefik/v3.5/routing/services/#proxy-protocol
Deprecated: ProxyProtocol will not be supported in future APIVersions, please use ServersTransport to configure ProxyProtocol instead.
properties:
version:
description: Version defines the PROXY Protocol version
@ -2400,6 +2401,15 @@ spec:
to a backend server can be established.
pattern: ^([0-9]+(ns|us|µs|ms|s|m|h)?)+$
x-kubernetes-int-or-string: true
proxyProtocol:
description: ProxyProtocol holds the PROXY Protocol configuration.
properties:
version:
description: Version defines the PROXY Protocol version to use.
maximum: 2
minimum: 1
type: integer
type: object
terminationDelay:
anyOf:
- type: integer

View File

@ -84,10 +84,12 @@ type RouterTCPTLSConfig struct {
// TCPServersLoadBalancer holds the LoadBalancerService configuration.
type TCPServersLoadBalancer struct {
ProxyProtocol *ProxyProtocol `json:"proxyProtocol,omitempty" toml:"proxyProtocol,omitempty" yaml:"proxyProtocol,omitempty" label:"allowEmpty" file:"allowEmpty" kv:"allowEmpty" export:"true"`
Servers []TCPServer `json:"servers,omitempty" toml:"servers,omitempty" yaml:"servers,omitempty" label-slice-as-struct:"server" export:"true"`
ServersTransport string `json:"serversTransport,omitempty" toml:"serversTransport,omitempty" yaml:"serversTransport,omitempty" export:"true"`
Servers []TCPServer `json:"servers,omitempty" toml:"servers,omitempty" yaml:"servers,omitempty" label-slice-as-struct:"server" export:"true"`
ServersTransport string `json:"serversTransport,omitempty" toml:"serversTransport,omitempty" yaml:"serversTransport,omitempty" export:"true"`
// ProxyProtocol holds the PROXY Protocol configuration.
// Deprecated: use ServersTransport to configure ProxyProtocol instead.
ProxyProtocol *ProxyProtocol `json:"proxyProtocol,omitempty" toml:"proxyProtocol,omitempty" yaml:"proxyProtocol,omitempty" label:"allowEmpty" file:"allowEmpty" kv:"allowEmpty" export:"true"`
// TerminationDelay, corresponds to the deadline that the proxy sets, after one
// of its connected peers indicates it has closed the writing capability of its
// connection, to close the reading capability as well, hence fully terminating the
@ -131,7 +133,7 @@ type ProxyProtocol struct {
// Version defines the PROXY Protocol version to use.
// +kubebuilder:validation:Minimum=1
// +kubebuilder:validation:Maximum=2
Version int `json:"version,omitempty" toml:"version,omitempty" yaml:"version,omitempty" export:"true"`
Version int `description:"Defines the PROXY Protocol version to use." json:"version,omitempty" toml:"version,omitempty" yaml:"version,omitempty" export:"true"`
}
// SetDefaults Default values for a ProxyProtocol.
@ -145,6 +147,8 @@ func (p *ProxyProtocol) SetDefaults() {
type TCPServersTransport struct {
DialKeepAlive ptypes.Duration `description:"Defines the interval between keep-alive probes for an active network connection. If zero, keep-alive probes are sent with a default value (currently 15 seconds), if supported by the protocol and operating system. Network protocols or operating systems that do not support keep-alives ignore this field. If negative, keep-alive probes are disabled" json:"dialKeepAlive,omitempty" toml:"dialKeepAlive,omitempty" yaml:"dialKeepAlive,omitempty" export:"true"`
DialTimeout ptypes.Duration `description:"Defines the amount of time to wait until a connection to a backend server can be established. If zero, no timeout exists." json:"dialTimeout,omitempty" toml:"dialTimeout,omitempty" yaml:"dialTimeout,omitempty" export:"true"`
// ProxyProtocol holds the PROXY Protocol configuration.
ProxyProtocol *ProxyProtocol `description:"Defines the PROXY Protocol configuration." json:"proxyProtocol,omitempty" toml:"proxyProtocol,omitempty" yaml:"proxyProtocol,omitempty" label:"allowEmpty" file:"allowEmpty" kv:"allowEmpty" export:"true"`
// TerminationDelay, corresponds to the deadline that the proxy sets, after one
// of its connected peers indicates it has closed the writing capability of its
// connection, to close the reading capability as well, hence fully terminating the
@ -154,6 +158,13 @@ type TCPServersTransport struct {
TLS *TLSClientConfig `description:"Defines the TLS configuration." json:"tls,omitempty" toml:"tls,omitempty" yaml:"tls,omitempty" label:"allowEmpty" file:"allowEmpty" kv:"allowEmpty" export:"true"`
}
// SetDefaults sets the default values for a TCPServersTransport.
func (t *TCPServersTransport) SetDefaults() {
t.DialTimeout = ptypes.Duration(30 * time.Second)
t.DialKeepAlive = ptypes.Duration(15 * time.Second)
t.TerminationDelay = ptypes.Duration(100 * time.Millisecond)
}
// +k8s:deepcopy-gen=true
// TLSClientConfig options to configure TLS communication between Traefik and the servers.
@ -165,10 +176,3 @@ type TLSClientConfig struct {
PeerCertURI string `description:"Defines the URI used to match against SAN URI during the peer certificate verification." json:"peerCertURI,omitempty" toml:"peerCertURI,omitempty" yaml:"peerCertURI,omitempty" export:"true"`
Spiffe *Spiffe `description:"Defines the SPIFFE TLS configuration." json:"spiffe,omitempty" toml:"spiffe,omitempty" yaml:"spiffe,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"`
}
// SetDefaults sets the default values for a TCPServersTransport.
func (t *TCPServersTransport) SetDefaults() {
t.DialTimeout = ptypes.Duration(30 * time.Second)
t.DialKeepAlive = ptypes.Duration(15 * time.Second)
t.TerminationDelay = ptypes.Duration(100 * time.Millisecond)
}

View File

@ -1929,16 +1929,16 @@ func (in *TCPServer) DeepCopy() *TCPServer {
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *TCPServersLoadBalancer) DeepCopyInto(out *TCPServersLoadBalancer) {
*out = *in
if in.ProxyProtocol != nil {
in, out := &in.ProxyProtocol, &out.ProxyProtocol
*out = new(ProxyProtocol)
**out = **in
}
if in.Servers != nil {
in, out := &in.Servers, &out.Servers
*out = make([]TCPServer, len(*in))
copy(*out, *in)
}
if in.ProxyProtocol != nil {
in, out := &in.ProxyProtocol, &out.ProxyProtocol
*out = new(ProxyProtocol)
**out = **in
}
if in.TerminationDelay != nil {
in, out := &in.TerminationDelay, &out.TerminationDelay
*out = new(int)
@ -1960,6 +1960,11 @@ func (in *TCPServersLoadBalancer) DeepCopy() *TCPServersLoadBalancer {
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *TCPServersTransport) DeepCopyInto(out *TCPServersTransport) {
*out = *in
if in.ProxyProtocol != nil {
in, out := &in.ProxyProtocol, &out.ProxyProtocol
*out = new(ProxyProtocol)
**out = **in
}
if in.TLS != nil {
in, out := &in.TLS, &out.TLS
*out = new(TLSClientConfig)

View File

@ -490,6 +490,10 @@ func (p *Provider) loadConfigurationFromCRD(ctx context.Context, client Client)
}
}
if serversTransportTCP.Spec.ProxyProtocol != nil {
tcpServerTransport.ProxyProtocol = serversTransportTCP.Spec.ProxyProtocol
}
if serversTransportTCP.Spec.TLS != nil {
if len(serversTransportTCP.Spec.TLS.RootCAsSecrets) > 0 {
logger.Warn().Msg("RootCAsSecrets option is deprecated, please use the RootCA option instead.")

View File

@ -86,6 +86,7 @@ type ServiceTCP struct {
TerminationDelay *int `json:"terminationDelay,omitempty"`
// ProxyProtocol defines the PROXY protocol configuration.
// More info: https://doc.traefik.io/traefik/v3.5/routing/services/#proxy-protocol
// Deprecated: ProxyProtocol will not be supported in future APIVersions, please use ServersTransport to configure ProxyProtocol instead.
ProxyProtocol *dynamic.ProxyProtocol `json:"proxyProtocol,omitempty"`
// ServersTransport defines the name of ServersTransportTCP resource to use.
// It allows to configure the transport between Traefik and your servers.

View File

@ -35,12 +35,14 @@ type ServersTransportTCPSpec struct {
// +kubebuilder:validation:Pattern="^([0-9]+(ns|us|µs|ms|s|m|h)?)+$"
// +kubebuilder:validation:XIntOrString
DialKeepAlive *intstr.IntOrString `json:"dialKeepAlive,omitempty"`
// ProxyProtocol holds the PROXY Protocol configuration.
ProxyProtocol *dynamic.ProxyProtocol `json:"proxyProtocol,omitempty"`
// TerminationDelay defines the delay to wait before fully terminating the connection, after one connected peer has closed its writing capability.
// +kubebuilder:validation:Pattern="^([0-9]+(ns|us|µs|ms|s|m|h)?)+$"
// +kubebuilder:validation:XIntOrString
TerminationDelay *intstr.IntOrString `json:"terminationDelay,omitempty"`
// TLS defines the TLS configuration
TLS *TLSClientConfig `description:"Defines the TLS configuration." json:"tls,omitempty"`
TLS *TLSClientConfig `json:"tls,omitempty"`
}
// TLSClientConfig defines the desired state of a TLSClientConfig.

View File

@ -1491,6 +1491,11 @@ func (in *ServersTransportTCPSpec) DeepCopyInto(out *ServersTransportTCPSpec) {
*out = new(intstr.IntOrString)
**out = **in
}
if in.ProxyProtocol != nil {
in, out := &in.ProxyProtocol, &out.ProxyProtocol
*out = new(dynamic.ProxyProtocol)
**out = **in
}
if in.TerminationDelay != nil {
in, out := &in.TerminationDelay, &out.TerminationDelay
*out = new(intstr.IntOrString)

View File

@ -13,7 +13,6 @@ import (
"github.com/traefik/traefik/v3/pkg/logs"
"github.com/traefik/traefik/v3/pkg/server/provider"
"github.com/traefik/traefik/v3/pkg/tcp"
"golang.org/x/net/proxy"
)
// Manager is the TCPHandlers factory.
@ -58,6 +57,10 @@ func (m *Manager) BuildTCP(rootCtx context.Context, serviceName string) (tcp.Han
log.Ctx(ctx).Warn().Msgf("Service %q load balancer uses `TerminationDelay`, but this option is deprecated, please use ServersTransport configuration instead.", serviceName)
}
if conf.LoadBalancer.ProxyProtocol != nil {
log.Ctx(ctx).Warn().Msgf("Service %q load balancer uses `ProxyProtocol`, but this option is deprecated, please use ServersTransport configuration instead.", serviceName)
}
if len(conf.LoadBalancer.ServersTransport) > 0 {
conf.LoadBalancer.ServersTransport = provider.GetQualifiedName(ctx, conf.LoadBalancer.ServersTransport)
}
@ -72,20 +75,12 @@ func (m *Manager) BuildTCP(rootCtx context.Context, serviceName string) (tcp.Han
continue
}
dialer, err := m.dialerManager.Get(conf.LoadBalancer.ServersTransport, server.TLS)
dialer, err := m.dialerManager.Build(conf.LoadBalancer, server.TLS)
if err != nil {
return nil, err
}
// Handle TerminationDelay deprecated option.
if conf.LoadBalancer.ServersTransport == "" && conf.LoadBalancer.TerminationDelay != nil {
dialer = &dialerWrapper{
Dialer: dialer,
terminationDelay: time.Duration(*conf.LoadBalancer.TerminationDelay),
}
}
handler, err := tcp.NewProxy(server.Address, conf.LoadBalancer.ProxyProtocol, dialer)
handler, err := tcp.NewProxy(server.Address, dialer)
if err != nil {
srvLogger.Error().Err(err).Msg("Failed to create server")
continue
@ -126,13 +121,3 @@ func shuffle[T any](values []T, r *rand.Rand) []T {
return shuffled
}
// dialerWrapper is only used to handle TerminationDelay deprecated option on TCPServersLoadBalancer.
type dialerWrapper struct {
proxy.Dialer
terminationDelay time.Duration
}
func (d dialerWrapper) TerminationDelay() time.Duration {
return d.terminationDelay
}

View File

@ -231,7 +231,7 @@ func TestManager_BuildTCP(t *testing.T) {
},
},
providerName: "provider-1",
expectedError: "TCP dialer not found myServersTransport@provider-1",
expectedError: "no transport configuration found for \"myServersTransport@provider-1\"",
},
}

View File

@ -9,11 +9,13 @@ import (
"sync"
"time"
"github.com/pires/go-proxyproto"
"github.com/rs/zerolog/log"
"github.com/spiffe/go-spiffe/v2/bundle/x509bundle"
"github.com/spiffe/go-spiffe/v2/spiffeid"
"github.com/spiffe/go-spiffe/v2/spiffetls/tlsconfig"
"github.com/spiffe/go-spiffe/v2/svid/x509svid"
ptypes "github.com/traefik/paerser/types"
"github.com/traefik/traefik/v3/pkg/config/dynamic"
traefiktls "github.com/traefik/traefik/v3/pkg/tls"
"github.com/traefik/traefik/v3/pkg/types"
@ -27,15 +29,54 @@ type Dialer interface {
}
type tcpDialer struct {
proxy.Dialer
dialer *net.Dialer
terminationDelay time.Duration
proxyProtocol *dynamic.ProxyProtocol
}
func (d tcpDialer) TerminationDelay() time.Duration {
return d.terminationDelay
}
// SpiffeX509Source allows to retrieve a x509 SVID and bundle.
func (d tcpDialer) Dial(network, addr string) (net.Conn, error) {
conn, err := d.dialer.Dial(network, addr)
if err != nil {
return nil, err
}
if d.proxyProtocol != nil && d.proxyProtocol.Version > 0 && d.proxyProtocol.Version < 3 {
header := proxyproto.HeaderProxyFromAddrs(byte(d.proxyProtocol.Version), conn.RemoteAddr(), conn.LocalAddr())
if _, err := header.WriteTo(conn); err != nil {
_ = conn.Close()
return nil, fmt.Errorf("writing PROXY Protocol header: %w", err)
}
}
return conn, nil
}
type tcpTLSDialer struct {
tcpDialer
tlsConfig *tls.Config
}
func (d tcpTLSDialer) Dial(network, addr string) (net.Conn, error) {
conn, err := d.tcpDialer.Dial(network, addr)
if err != nil {
return nil, err
}
// Now perform TLS handshake on the connection
tlsConn := tls.Client(conn, d.tlsConfig)
if err := tlsConn.Handshake(); err != nil {
_ = conn.Close()
return nil, fmt.Errorf("TLS handshake failed: %w", err)
}
return tlsConn, nil
}
// SpiffeX509Source allows retrieving a x509 SVID and bundle.
type SpiffeX509Source interface {
x509svid.Source
x509bundle.Source
@ -43,118 +84,106 @@ type SpiffeX509Source interface {
// DialerManager handles dialer for the reverse proxy.
type DialerManager struct {
rtLock sync.RWMutex
dialers map[string]Dialer
dialersTLS map[string]Dialer
spiffeX509Source SpiffeX509Source
serversTransportsMu sync.RWMutex
serversTransports map[string]*dynamic.TCPServersTransport
spiffeX509Source SpiffeX509Source
}
// NewDialerManager creates a new DialerManager.
func NewDialerManager(spiffeX509Source SpiffeX509Source) *DialerManager {
return &DialerManager{
dialers: make(map[string]Dialer),
dialersTLS: make(map[string]Dialer),
spiffeX509Source: spiffeX509Source,
serversTransports: make(map[string]*dynamic.TCPServersTransport),
spiffeX509Source: spiffeX509Source,
}
}
// Update updates the dialers configurations.
// Update updates the TCP serversTransport configurations.
func (d *DialerManager) Update(configs map[string]*dynamic.TCPServersTransport) {
d.rtLock.Lock()
defer d.rtLock.Unlock()
d.serversTransportsMu.Lock()
defer d.serversTransportsMu.Unlock()
d.dialers = make(map[string]Dialer)
d.dialersTLS = make(map[string]Dialer)
for configName, config := range configs {
if err := d.createDialers(configName, config); err != nil {
log.Debug().
Str("dialer", configName).
Err(err).
Msg("Create TCP Dialer")
}
}
d.serversTransports = configs
}
// Get gets a dialer by name.
func (d *DialerManager) Get(name string, tls bool) (Dialer, error) {
if len(name) == 0 {
name = "default@internal"
// Build builds a dialer by name.
func (d *DialerManager) Build(config *dynamic.TCPServersLoadBalancer, isTLS bool) (Dialer, error) {
name := "default@internal"
if len(config.ServersTransport) > 0 {
name = config.ServersTransport
}
d.rtLock.RLock()
defer d.rtLock.RUnlock()
if tls {
if rt, ok := d.dialersTLS[name]; ok {
return rt, nil
}
return nil, fmt.Errorf("TCP dialer not found %s", name)
var st *dynamic.TCPServersTransport
d.serversTransportsMu.RLock()
st, ok := d.serversTransports[name]
d.serversTransportsMu.RUnlock()
if !ok || st == nil {
return nil, fmt.Errorf("no transport configuration found for %q", name)
}
if rt, ok := d.dialers[name]; ok {
return rt, nil
// Handle TerminationDelay and ProxyProtocol deprecated options.
var terminationDelay ptypes.Duration
if config.TerminationDelay != nil {
terminationDelay = ptypes.Duration(*config.TerminationDelay)
}
proxyProtocol := config.ProxyProtocol
if len(config.ServersTransport) > 0 {
terminationDelay = st.TerminationDelay
proxyProtocol = st.ProxyProtocol
}
return nil, fmt.Errorf("TCP dialer not found %s", name)
}
// createDialers creates the dialers according to the TCPServersTransport configuration.
func (d *DialerManager) createDialers(name string, cfg *dynamic.TCPServersTransport) error {
if cfg == nil {
return errors.New("no transport configuration given")
}
dialer := &net.Dialer{
Timeout: time.Duration(cfg.DialTimeout),
KeepAlive: time.Duration(cfg.DialKeepAlive),
if proxyProtocol != nil && (proxyProtocol.Version < 1 || proxyProtocol.Version > 2) {
return nil, fmt.Errorf("unknown proxyProtocol version: %d", proxyProtocol.Version)
}
var tlsConfig *tls.Config
if cfg.TLS != nil {
if cfg.TLS.Spiffe != nil {
if st.TLS != nil {
if st.TLS.Spiffe != nil {
if d.spiffeX509Source == nil {
return errors.New("SPIFFE is enabled for this transport, but not configured")
return nil, errors.New("SPIFFE is enabled for this transport, but not configured")
}
authorizer, err := buildSpiffeAuthorizer(cfg.TLS.Spiffe)
authorizer, err := buildSpiffeAuthorizer(st.TLS.Spiffe)
if err != nil {
return fmt.Errorf("unable to build SPIFFE authorizer: %w", err)
return nil, fmt.Errorf("unable to build SPIFFE authorizer: %w", err)
}
tlsConfig = tlsconfig.MTLSClientConfig(d.spiffeX509Source, d.spiffeX509Source, authorizer)
}
if cfg.TLS.InsecureSkipVerify || len(cfg.TLS.RootCAs) > 0 || len(cfg.TLS.ServerName) > 0 || len(cfg.TLS.Certificates) > 0 || cfg.TLS.PeerCertURI != "" {
if st.TLS.InsecureSkipVerify || len(st.TLS.RootCAs) > 0 || len(st.TLS.ServerName) > 0 || len(st.TLS.Certificates) > 0 || st.TLS.PeerCertURI != "" {
if tlsConfig != nil {
return errors.New("TLS and SPIFFE configuration cannot be defined at the same time")
return nil, errors.New("TLS and SPIFFE configuration cannot be defined at the same time")
}
tlsConfig = &tls.Config{
ServerName: cfg.TLS.ServerName,
InsecureSkipVerify: cfg.TLS.InsecureSkipVerify,
RootCAs: createRootCACertPool(cfg.TLS.RootCAs),
Certificates: cfg.TLS.Certificates.GetCertificates(),
ServerName: st.TLS.ServerName,
InsecureSkipVerify: st.TLS.InsecureSkipVerify,
RootCAs: createRootCACertPool(st.TLS.RootCAs),
Certificates: st.TLS.Certificates.GetCertificates(),
}
if cfg.TLS.PeerCertURI != "" {
if st.TLS.PeerCertURI != "" {
tlsConfig.VerifyPeerCertificate = func(rawCerts [][]byte, _ [][]*x509.Certificate) error {
return traefiktls.VerifyPeerCertificate(cfg.TLS.PeerCertURI, tlsConfig, rawCerts)
return traefiktls.VerifyPeerCertificate(st.TLS.PeerCertURI, tlsConfig, rawCerts)
}
}
}
}
tlsDialer := &tls.Dialer{
NetDialer: dialer,
Config: tlsConfig,
dialer := tcpDialer{
dialer: &net.Dialer{
Timeout: time.Duration(st.DialTimeout),
KeepAlive: time.Duration(st.DialKeepAlive),
},
terminationDelay: time.Duration(terminationDelay),
proxyProtocol: proxyProtocol,
}
d.dialers[name] = tcpDialer{dialer, time.Duration(cfg.TerminationDelay)}
d.dialersTLS[name] = tcpDialer{tlsDialer, time.Duration(cfg.TerminationDelay)}
return nil
if !isTLS {
return dialer, nil
}
return tcpTLSDialer{dialer, tlsConfig}, nil
}
func createRootCACertPool(rootCAs []types.FileOrContent) *x509.CertPool {

View File

@ -7,6 +7,7 @@ import (
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"errors"
"io"
"math/big"
"net"
@ -14,6 +15,7 @@ import (
"testing"
"time"
"github.com/pires/go-proxyproto"
"github.com/spiffe/go-spiffe/v2/bundle/x509bundle"
"github.com/spiffe/go-spiffe/v2/spiffeid"
"github.com/spiffe/go-spiffe/v2/spiffetls/tlsconfig"
@ -131,7 +133,7 @@ func TestConflictingConfig(t *testing.T) {
dialerManager.Update(dynamicConf)
_, err := dialerManager.Get("test", false)
_, err := dialerManager.Build(&dynamic.TCPServersLoadBalancer{ServersTransport: "test"}, false)
require.Error(t, err)
}
@ -140,7 +142,7 @@ func TestNoTLS(t *testing.T) {
require.NoError(t, err)
defer backendListener.Close()
go fakeRedis(t, backendListener)
go fakeServer(t, backendListener)
_, port, err := net.SplitHostPort(backendListener.Addr().String())
require.NoError(t, err)
@ -155,7 +157,7 @@ func TestNoTLS(t *testing.T) {
dialerManager.Update(dynamicConf)
dialer, err := dialerManager.Get("test", false)
dialer, err := dialerManager.Build(&dynamic.TCPServersLoadBalancer{ServersTransport: "test"}, false)
require.NoError(t, err)
conn, err := dialer.Dial("tcp", ":"+port)
@ -186,7 +188,7 @@ func TestTLS(t *testing.T) {
tlsListener := tls.NewListener(backendListener, &tls.Config{Certificates: []tls.Certificate{cert}})
defer tlsListener.Close()
go fakeRedis(t, tlsListener)
go fakeServer(t, tlsListener)
_, port, err := net.SplitHostPort(tlsListener.Addr().String())
require.NoError(t, err)
@ -204,7 +206,7 @@ func TestTLS(t *testing.T) {
dialerManager.Update(dynamicConf)
dialer, err := dialerManager.Get("test", true)
dialer, err := dialerManager.Build(&dynamic.TCPServersLoadBalancer{ServersTransport: "test"}, true)
require.NoError(t, err)
conn, err := dialer.Dial("tcp", ":"+port)
@ -236,7 +238,7 @@ func TestTLSWithInsecureSkipVerify(t *testing.T) {
tlsListener := tls.NewListener(backendListener, &tls.Config{Certificates: []tls.Certificate{cert}})
defer tlsListener.Close()
go fakeRedis(t, tlsListener)
go fakeServer(t, tlsListener)
_, port, err := net.SplitHostPort(tlsListener.Addr().String())
require.NoError(t, err)
@ -255,7 +257,7 @@ func TestTLSWithInsecureSkipVerify(t *testing.T) {
dialerManager.Update(dynamicConf)
dialer, err := dialerManager.Get("test", true)
dialer, err := dialerManager.Build(&dynamic.TCPServersLoadBalancer{ServersTransport: "test"}, true)
require.NoError(t, err)
conn, err := dialer.Dial("tcp", ":"+port)
@ -297,7 +299,7 @@ func TestMTLS(t *testing.T) {
})
defer tlsListener.Close()
go fakeRedis(t, tlsListener)
go fakeServer(t, tlsListener)
_, port, err := net.SplitHostPort(tlsListener.Addr().String())
require.NoError(t, err)
@ -324,7 +326,7 @@ func TestMTLS(t *testing.T) {
dialerManager.Update(dynamicConf)
dialer, err := dialerManager.Get("test", true)
dialer, err := dialerManager.Build(&dynamic.TCPServersLoadBalancer{ServersTransport: "test"}, true)
require.NoError(t, err)
conn, err := dialer.Dial("tcp", ":"+port)
@ -444,7 +446,7 @@ func TestSpiffeMTLS(t *testing.T) {
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
go fakeRedis(t, tlsListener)
go fakeServer(t, tlsListener)
dialerManager := NewDialerManager(test.clientSource)
@ -458,7 +460,7 @@ func TestSpiffeMTLS(t *testing.T) {
dialerManager.Update(dynamicConf)
dialer, err := dialerManager.Get("test", true)
dialer, err := dialerManager.Build(&dynamic.TCPServersLoadBalancer{ServersTransport: "test"}, true)
require.NoError(t, err)
conn, err := dialer.Dial("tcp", ":"+port)
@ -487,6 +489,226 @@ func TestSpiffeMTLS(t *testing.T) {
}
}
func TestProxyProtocol(t *testing.T) {
testCases := []struct {
desc string
version int
}{
{
desc: "proxy protocol v1",
version: 1,
},
{
desc: "proxy protocol v2",
version: 2,
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
backendListener, err := net.Listen("tcp", ":0")
require.NoError(t, err)
var version int
proxyBackendListener := proxyproto.Listener{
Listener: backendListener,
ValidateHeader: func(h *proxyproto.Header) error {
version = int(h.Version)
return nil
},
Policy: func(upstream net.Addr) (proxyproto.Policy, error) {
switch test.version {
case 1, 2:
return proxyproto.USE, nil
default:
return proxyproto.REQUIRE, errors.New("unsupported version")
}
},
}
defer proxyBackendListener.Close()
go fakeServer(t, &proxyBackendListener)
_, port, err := net.SplitHostPort(backendListener.Addr().String())
require.NoError(t, err)
dialerManager := NewDialerManager(nil)
dialerManager.Update(map[string]*dynamic.TCPServersTransport{
"test": {
ProxyProtocol: &dynamic.ProxyProtocol{
Version: test.version,
},
},
})
dialer, err := dialerManager.Build(&dynamic.TCPServersLoadBalancer{ServersTransport: "test"}, false)
require.NoError(t, err)
conn, err := dialer.Dial("tcp", ":"+port)
require.NoError(t, err)
defer conn.Close()
_, err = conn.Write([]byte("ping"))
require.NoError(t, err)
buf := make([]byte, 64)
n, err := conn.Read(buf)
require.NoError(t, err)
assert.Equal(t, 4, n)
assert.Equal(t, "PONG", string(buf[:4]))
assert.Equal(t, test.version, version)
})
}
}
func TestProxyProtocolWithTLS(t *testing.T) {
testCases := []struct {
desc string
version int
}{
{
desc: "proxy protocol v1",
version: 1,
},
{
desc: "proxy protocol v2",
version: 2,
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
cert, err := tls.X509KeyPair(LocalhostCert, LocalhostKey)
require.NoError(t, err)
backendListener, err := net.Listen("tcp", ":0")
require.NoError(t, err)
var version int
proxyBackendListener := proxyproto.Listener{
Listener: backendListener,
ValidateHeader: func(h *proxyproto.Header) error {
version = int(h.Version)
return nil
},
Policy: func(upstream net.Addr) (proxyproto.Policy, error) {
switch test.version {
case 1, 2:
return proxyproto.USE, nil
default:
return proxyproto.REQUIRE, errors.New("unsupported version")
}
},
}
defer proxyBackendListener.Close()
go func() {
conn, err := proxyBackendListener.Accept()
require.NoError(t, err)
defer conn.Close()
// Now wrap with TLS and perform handshake
tlsConn := tls.Server(conn, &tls.Config{Certificates: []tls.Certificate{cert}})
defer tlsConn.Close()
err = tlsConn.Handshake()
require.NoError(t, err)
buf := make([]byte, 64)
n, err := tlsConn.Read(buf)
require.NoError(t, err)
if bytes.Equal(buf[:n], []byte("ping")) {
_, _ = tlsConn.Write([]byte("PONG"))
}
}()
_, port, err := net.SplitHostPort(backendListener.Addr().String())
require.NoError(t, err)
dialerManager := NewDialerManager(nil)
dialerManager.Update(map[string]*dynamic.TCPServersTransport{
"test": {
TLS: &dynamic.TLSClientConfig{
ServerName: "example.com",
RootCAs: []types.FileOrContent{types.FileOrContent(LocalhostCert)},
InsecureSkipVerify: true,
},
ProxyProtocol: &dynamic.ProxyProtocol{
Version: test.version,
},
},
})
dialer, err := dialerManager.Build(&dynamic.TCPServersLoadBalancer{
ServersTransport: "test",
}, true)
require.NoError(t, err)
conn, err := dialer.Dial("tcp", ":"+port)
require.NoError(t, err)
defer conn.Close()
_, err = conn.Write([]byte("ping"))
require.NoError(t, err)
buf := make([]byte, 64)
n, err := conn.Read(buf)
require.NoError(t, err)
assert.Equal(t, 4, n)
assert.Equal(t, "PONG", string(buf[:4]))
assert.Equal(t, test.version, version)
})
}
}
func TestProxyProtocolDisabled(t *testing.T) {
backendListener, err := net.Listen("tcp", ":0")
require.NoError(t, err)
defer backendListener.Close()
go func() {
conn, err := backendListener.Accept()
require.NoError(t, err)
defer conn.Close()
buf := make([]byte, 64)
n, err := conn.Read(buf)
require.NoError(t, err)
if bytes.Equal(buf[:n], []byte("ping")) {
_, _ = conn.Write([]byte("PONG"))
}
}()
_, port, err := net.SplitHostPort(backendListener.Addr().String())
require.NoError(t, err)
// No proxy protocol configuration.
dialerManager := NewDialerManager(nil)
dialerManager.Update(map[string]*dynamic.TCPServersTransport{
"test": {},
})
dialer, err := dialerManager.Build(&dynamic.TCPServersLoadBalancer{ServersTransport: "test"}, false)
require.NoError(t, err)
conn, err := dialer.Dial("tcp", ":"+port)
require.NoError(t, err)
_, err = conn.Write([]byte("ping"))
require.NoError(t, err)
buf := make([]byte, 64)
n, err := conn.Read(buf)
require.NoError(t, err)
assert.Equal(t, 4, n)
assert.Equal(t, "PONG", string(buf[:4]))
}
// fakeSpiffePKI simulates a SPIFFE aware PKI and allows generating multiple valid SVIDs.
type fakeSpiffePKI struct {
caPrivateKey *rsa.PrivateKey

View File

@ -2,34 +2,25 @@ package tcp
import (
"errors"
"fmt"
"io"
"net"
"syscall"
"time"
"github.com/pires/go-proxyproto"
"github.com/rs/zerolog/log"
"github.com/traefik/traefik/v3/pkg/config/dynamic"
)
// Proxy forwards a TCP request to a TCP service.
type Proxy struct {
address string
proxyProtocol *dynamic.ProxyProtocol
dialer Dialer
address string
dialer Dialer
}
// NewProxy creates a new Proxy.
func NewProxy(address string, proxyProtocol *dynamic.ProxyProtocol, dialer Dialer) (*Proxy, error) {
if proxyProtocol != nil && (proxyProtocol.Version < 1 || proxyProtocol.Version > 2) {
return nil, fmt.Errorf("unknown proxyProtocol version: %d", proxyProtocol.Version)
}
func NewProxy(address string, dialer Dialer) (*Proxy, error) {
return &Proxy{
address: address,
proxyProtocol: proxyProtocol,
dialer: dialer,
address: address,
dialer: dialer,
}, nil
}
@ -53,14 +44,6 @@ func (p *Proxy) ServeTCP(conn WriteCloser) {
defer connBackend.Close()
errChan := make(chan error)
if p.proxyProtocol != nil && p.proxyProtocol.Version > 0 && p.proxyProtocol.Version < 3 {
header := proxyproto.HeaderProxyFromAddrs(byte(p.proxyProtocol.Version), conn.RemoteAddr(), conn.LocalAddr())
if _, err := header.WriteTo(connBackend); err != nil {
log.Error().Err(err).Msg("Error while writing TCP proxy protocol headers to backend connection")
return
}
}
go p.connCopy(conn, connBackend, errChan)
go p.connCopy(connBackend, conn, errChan)

View File

@ -2,59 +2,25 @@ package tcp
import (
"bytes"
"errors"
"io"
"net"
"testing"
"time"
"github.com/pires/go-proxyproto"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/traefik/traefik/v3/pkg/config/dynamic"
)
func fakeRedis(t *testing.T, listener net.Listener) {
t.Helper()
for {
conn, err := listener.Accept()
require.NoError(t, err)
for {
withErr := false
buf := make([]byte, 64)
if _, err := conn.Read(buf); err != nil {
withErr = true
}
if string(buf[:4]) == "ping" {
time.Sleep(1 * time.Millisecond)
if _, err := conn.Write([]byte("PONG")); err != nil {
_ = conn.Close()
return
}
}
if withErr {
_ = conn.Close()
return
}
}
}
}
func TestCloseWrite(t *testing.T) {
backendListener, err := net.Listen("tcp", ":0")
require.NoError(t, err)
go fakeRedis(t, backendListener)
go fakeServer(t, backendListener)
_, port, err := net.SplitHostPort(backendListener.Addr().String())
require.NoError(t, err)
dialer := tcpDialer{&net.Dialer{}, 10 * time.Millisecond}
dialer := tcpDialer{&net.Dialer{}, 10 * time.Millisecond, nil}
proxy, err := NewProxy(":"+port, nil, dialer)
proxy, err := NewProxy(":"+port, dialer)
require.NoError(t, err)
proxyListener, err := net.Listen("tcp", ":0")
@ -84,90 +50,37 @@ func TestCloseWrite(t *testing.T) {
buffer := bytes.NewBuffer(buf)
n, err := io.Copy(buffer, conn)
require.NoError(t, err)
require.Equal(t, int64(4), n)
require.Equal(t, "PONG", buffer.String())
}
func TestProxyProtocol(t *testing.T) {
testCases := []struct {
desc string
version int
}{
{
desc: "PROXY protocol v1",
version: 1,
},
{
desc: "PROXY protocol v2",
version: 2,
},
}
func fakeServer(t *testing.T, listener net.Listener) {
t.Helper()
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
backendListener, err := net.Listen("tcp", ":0")
require.NoError(t, err)
for {
conn, err := listener.Accept()
require.NoError(t, err)
var version int
proxyBackendListener := proxyproto.Listener{
Listener: backendListener,
ValidateHeader: func(h *proxyproto.Header) error {
version = int(h.Version)
return nil
},
Policy: func(upstream net.Addr) (proxyproto.Policy, error) {
switch test.version {
case 1, 2:
return proxyproto.USE, nil
default:
return proxyproto.REQUIRE, errors.New("unsupported version")
}
},
for {
withErr := false
buf := make([]byte, 64)
if _, err := conn.Read(buf); err != nil {
withErr = true
}
defer proxyBackendListener.Close()
go fakeRedis(t, &proxyBackendListener)
_, port, err := net.SplitHostPort(proxyBackendListener.Addr().String())
require.NoError(t, err)
dialer := tcpDialer{&net.Dialer{}, 10 * time.Millisecond}
proxy, err := NewProxy(":"+port, &dynamic.ProxyProtocol{Version: test.version}, dialer)
require.NoError(t, err)
proxyListener, err := net.Listen("tcp", ":0")
require.NoError(t, err)
go func() {
for {
conn, err := proxyListener.Accept()
require.NoError(t, err)
proxy.ServeTCP(conn.(*net.TCPConn))
if string(buf[:4]) == "ping" {
time.Sleep(1 * time.Millisecond)
if _, err := conn.Write([]byte("PONG")); err != nil {
_ = conn.Close()
return
}
}()
}
_, port, err = net.SplitHostPort(proxyListener.Addr().String())
require.NoError(t, err)
conn, err := net.Dial("tcp", ":"+port)
require.NoError(t, err)
_, err = conn.Write([]byte("ping\n"))
require.NoError(t, err)
err = conn.(*net.TCPConn).CloseWrite()
require.NoError(t, err)
var buf []byte
buffer := bytes.NewBuffer(buf)
n, err := io.Copy(buffer, conn)
require.NoError(t, err)
assert.Equal(t, int64(4), n)
assert.Equal(t, "PONG", buffer.String())
assert.Equal(t, test.version, version)
})
if withErr {
_ = conn.Close()
return
}
}
}
}