From 84b125bdde3ff5c10e3a91a8aac9951114e1bc7a Mon Sep 17 00:00:00 2001 From: Matthias Schneider Date: Tue, 17 Nov 2020 13:04:04 +0100 Subject: [PATCH] added support for tcp proxyProtocol v1&v2 to backend --- .../dynamic-configuration/docker-labels.yml | 1 + .../reference/dynamic-configuration/file.toml | 2 + .../reference/dynamic-configuration/file.yaml | 2 + .../reference/dynamic-configuration/kv-ref.md | 1 + .../marathon-labels.json | 1 + .../routing/providers/consul-catalog.md | 8 ++ docs/content/routing/providers/docker.md | 8 ++ docs/content/routing/providers/ecs.md | 8 ++ .../routing/providers/kubernetes-crd.md | 64 +++++++------ docs/content/routing/providers/kv.md | 8 ++ docs/content/routing/providers/marathon.md | 8 ++ docs/content/routing/providers/rancher.md | 8 ++ docs/content/routing/services/index.md | 33 +++++++ go.mod | 2 +- go.sum | 4 +- integration/proxy_protocol_test.go | 8 +- pkg/config/dynamic/tcp_config.go | 17 +++- pkg/config/dynamic/zz_generated.deepcopy.go | 21 ++++ pkg/config/label/label_test.go | 4 + .../crd/fixtures/tcp/with_proxyprotocol.yml | 17 ++++ pkg/provider/kubernetes/crd/kubernetes_tcp.go | 9 ++ .../kubernetes/crd/kubernetes_test.go | 43 +++++++++ .../crd/traefik/v1alpha1/ingressroutetcp.go | 12 ++- .../traefik/v1alpha1/zz_generated.deepcopy.go | 5 + pkg/server/server_entrypoint_tcp.go | 57 +++++------ pkg/server/service/tcp/service.go | 2 +- pkg/tcp/proxy.go | 22 ++++- pkg/tcp/proxy_test.go | 96 ++++++++++++++++++- 28 files changed, 388 insertions(+), 83 deletions(-) create mode 100644 pkg/provider/kubernetes/crd/fixtures/tcp/with_proxyprotocol.yml diff --git a/docs/content/reference/dynamic-configuration/docker-labels.yml b/docs/content/reference/dynamic-configuration/docker-labels.yml index 3f5c05189c..885706a0fe 100644 --- a/docs/content/reference/dynamic-configuration/docker-labels.yml +++ b/docs/content/reference/dynamic-configuration/docker-labels.yml @@ -183,6 +183,7 @@ - "traefik.tcp.routers.tcprouter1.tls.passthrough=true" - "traefik.tcp.services.tcpservice01.loadbalancer.terminationdelay=42" - "traefik.tcp.services.tcpservice01.loadbalancer.server.port=foobar" +- "traefik.tcp.services.tcpservice01.loadbalancer.proxyprotocol.version=42" - "traefik.udp.routers.udprouter0.entrypoints=foobar, foobar" - "traefik.udp.routers.udprouter0.service=foobar" - "traefik.udp.routers.udprouter1.entrypoints=foobar, foobar" diff --git a/docs/content/reference/dynamic-configuration/file.toml b/docs/content/reference/dynamic-configuration/file.toml index 353cb7285b..ea5e29fcf2 100644 --- a/docs/content/reference/dynamic-configuration/file.toml +++ b/docs/content/reference/dynamic-configuration/file.toml @@ -343,6 +343,8 @@ [tcp.services.TCPService01] [tcp.services.TCPService01.loadBalancer] terminationDelay = 42 + [tcp.services.TCPService01.loadBalancer.proxyProtocol] + version = 42 [[tcp.services.TCPService01.loadBalancer.servers]] address = "foobar" diff --git a/docs/content/reference/dynamic-configuration/file.yaml b/docs/content/reference/dynamic-configuration/file.yaml index 456272ec14..f707c4a03b 100644 --- a/docs/content/reference/dynamic-configuration/file.yaml +++ b/docs/content/reference/dynamic-configuration/file.yaml @@ -387,6 +387,8 @@ tcp: TCPService01: loadBalancer: terminationDelay: 42 + proxyProtocol: + version: 42 servers: - address: foobar - address: foobar diff --git a/docs/content/reference/dynamic-configuration/kv-ref.md b/docs/content/reference/dynamic-configuration/kv-ref.md index 64199fc3b2..1f7490fb5e 100644 --- a/docs/content/reference/dynamic-configuration/kv-ref.md +++ b/docs/content/reference/dynamic-configuration/kv-ref.md @@ -247,6 +247,7 @@ | `traefik/tcp/routers/TCPRouter1/tls/domains/1/sans/1` | `foobar` | | `traefik/tcp/routers/TCPRouter1/tls/options` | `foobar` | | `traefik/tcp/routers/TCPRouter1/tls/passthrough` | `true` | +| `traefik/tcp/services/TCPService01/loadBalancer/proxyProtocol/version` | `42` | | `traefik/tcp/services/TCPService01/loadBalancer/servers/0/address` | `foobar` | | `traefik/tcp/services/TCPService01/loadBalancer/servers/1/address` | `foobar` | | `traefik/tcp/services/TCPService01/loadBalancer/terminationDelay` | `42` | diff --git a/docs/content/reference/dynamic-configuration/marathon-labels.json b/docs/content/reference/dynamic-configuration/marathon-labels.json index 1698cef1a5..3e9644840e 100644 --- a/docs/content/reference/dynamic-configuration/marathon-labels.json +++ b/docs/content/reference/dynamic-configuration/marathon-labels.json @@ -177,6 +177,7 @@ "traefik.tcp.routers.tcprouter1.tls.options": "foobar", "traefik.tcp.routers.tcprouter1.tls.passthrough": "true", "traefik.tcp.services.tcpservice01.loadbalancer.terminationdelay": "42", +"traefik.tcp.services.tcpservice01.loadbalancer.proxyprotocol.version": "42", "traefik.tcp.services.tcpservice01.loadbalancer.server.port": "foobar", "traefik.udp.routers.udprouter0.entrypoints": "foobar, foobar", "traefik.udp.routers.udprouter0.service": "foobar", diff --git a/docs/content/routing/providers/consul-catalog.md b/docs/content/routing/providers/consul-catalog.md index 425bb369e4..735a55f685 100644 --- a/docs/content/routing/providers/consul-catalog.md +++ b/docs/content/routing/providers/consul-catalog.md @@ -381,6 +381,14 @@ You can declare TCP Routers and/or Services using tags. traefik.tcp.services.mytcpservice.loadbalancer.terminationdelay=100 ``` +??? info "`traefik.tcp.services..loadbalancer.proxyprotocol.version`" + + See [PROXY protocol](../services/index.md#proxy-protocol) for more information. + + ```yaml + traefik.tcp.services.mytcpservice.loadbalancer.proxyprotocol.version=1 + ``` + ### UDP You can declare UDP Routers and/or Services using tags. diff --git a/docs/content/routing/providers/docker.md b/docs/content/routing/providers/docker.md index 6cc0cf2a8f..071de64159 100644 --- a/docs/content/routing/providers/docker.md +++ b/docs/content/routing/providers/docker.md @@ -527,6 +527,14 @@ You can declare TCP Routers and/or Services using labels. - "traefik.tcp.services.mytcpservice.loadbalancer.terminationdelay=100" ``` +??? info "`traefik.tcp.services..loadbalancer.proxyprotocol.version`" + + See [PROXY protocol](../services/index.md#proxy-protocol) for more information. + + ```yaml + - "traefik.tcp.services.mytcpservice.loadbalancer.proxyprotocol.version=1" + ``` + ### UDP You can declare UDP Routers and/or Services using labels. diff --git a/docs/content/routing/providers/ecs.md b/docs/content/routing/providers/ecs.md index 62feefa95b..e53646fbf9 100644 --- a/docs/content/routing/providers/ecs.md +++ b/docs/content/routing/providers/ecs.md @@ -388,6 +388,14 @@ You can declare TCP Routers and/or Services using labels. traefik.tcp.services.mytcpservice.loadbalancer.terminationdelay=100 ``` +??? info "`traefik.tcp.services..loadbalancer.proxyprotocol.version`" + + See [PROXY protocol](../services/index.md#proxy-protocol) for more information. + + ```yaml + traefik.tcp.services.mytcpservice.loadbalancer.proxyprotocol.version=1 + ``` + ### UDP You can declare UDP Routers and/or Services using tags. diff --git a/docs/content/routing/providers/kubernetes-crd.md b/docs/content/routing/providers/kubernetes-crd.md index 43c063d4a2..302a280208 100644 --- a/docs/content/routing/providers/kubernetes-crd.md +++ b/docs/content/routing/providers/kubernetes-crd.md @@ -1090,40 +1090,44 @@ Register the `IngressRouteTCP` [kind](../../reference/dynamic-configuration/kube port: 8080 # [6] weight: 10 # [7] terminationDelay: 400 # [8] - tls: # [9] - secretName: supersecret # [10] - options: # [11] - name: opt # [12] - namespace: default # [13] - certResolver: foo # [14] - domains: # [15] - - main: example.net # [16] - sans: # [17] + proxyProtocol: # [9] + version: 1 # [10] + tls: # [11] + secretName: supersecret # [12] + options: # [13] + name: opt # [14] + namespace: default # [15] + certResolver: foo # [16] + domains: # [17] + - main: example.net # [18] + sans: # [19] - a.example.net - b.example.net - passthrough: false # [18] + passthrough: false # [20] ``` -| Ref | Attribute | Purpose | -|------|--------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| [1] | `entryPoints` | List of [entrypoints](../routers/index.md#entrypoints_1) names | -| [2] | `routes` | List of routes | -| [3] | `routes[n].match` | Defines the [rule](../routers/index.md#rule_1) corresponding to an underlying router | -| [4] | `routes[n].services` | List of [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) definitions (See below for `ExternalName Service` setup) | -| [5] | `services[n].name` | Defines the name of a [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) | -| [6] | `services[n].port` | Defines the port of a [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) | -| [7] | `services[n].weight` | Defines the weight to apply to the server load balancing | -| [8] | `services[n].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 connection.
It is a duration in milliseconds, defaulting to 100. A negative value means an infinite deadline (i.e. the reading capability is never closed). | -| [9] | `tls` | Defines [TLS](../routers/index.md#tls_1) certificate configuration | -| [10] | `tls.secretName` | Defines the [secret](https://kubernetes.io/docs/concepts/configuration/secret/) name used to store the certificate (in the `IngressRoute` namespace) | -| [11] | `tls.options` | Defines the reference to a [TLSOption](#kind-tlsoption) | -| [12] | `options.name` | Defines the [TLSOption](#kind-tlsoption) name | -| [13] | `options.namespace` | Defines the [TLSOption](#kind-tlsoption) namespace | -| [14] | `tls.certResolver` | Defines the reference to a [CertResolver](../routers/index.md#certresolver_1) | -| [15] | `tls.domains` | List of [domains](../routers/index.md#domains_1) | -| [16] | `domains[n].main` | Defines the main domain name | -| [17] | `domains[n].sans` | List of SANs (alternative domains) | -| [18] | `tls.passthrough` | If `true`, delegates the TLS termination to the backend | +| Ref | Attribute | Purpose | +|------|--------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| [1] | `entryPoints` | List of [entrypoints](../routers/index.md#entrypoints_1) names | +| [2] | `routes` | List of routes | +| [3] | `routes[n].match` | Defines the [rule](../routers/index.md#rule_1) corresponding to an underlying router | +| [4] | `routes[n].services` | List of [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) definitions (See below for `ExternalName Service` setup) | +| [5] | `services[n].name` | Defines the name of a [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) | +| [6] | `services[n].port` | Defines the port of a [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) | +| [7] | `services[n].weight` | Defines the weight to apply to the server load balancing | +| [8] | `services[n].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 connection. It is a duration in milliseconds, defaulting to 100. A negative value means an infinite deadline (i.e. the reading capability is never closed). | +| [9] | `proxyProtocol` | Defines the [PROXY protocol](../services/index.md#proxy-protocol) configuration | +| [10] | `version` | Defines the [PROXY protocol](../services/index.md#proxy-protocol) version | +| [11] | `tls` | Defines [TLS](../routers/index.md#tls_1) certificate configuration | +| [12] | `tls.secretName` | Defines the [secret](https://kubernetes.io/docs/concepts/configuration/secret/) name used to store the certificate (in the `IngressRoute` namespace) | +| [13] | `tls.options` | Defines the reference to a [TLSOption](#kind-tlsoption) | +| [14] | `options.name` | Defines the [TLSOption](#kind-tlsoption) name | +| [15] | `options.namespace` | Defines the [TLSOption](#kind-tlsoption) namespace | +| [16] | `tls.certResolver` | Defines the reference to a [CertResolver](../routers/index.md#certresolver_1) | +| [17] | `tls.domains` | List of [domains](../routers/index.md#domains_1) | +| [18] | `domains[n].main` | Defines the main domain name | +| [19] | `domains[n].sans` | List of SANs (alternative domains) | +| [20] | `tls.passthrough` | If `true`, delegates the TLS termination to the backend | ??? example "Declaring an IngressRouteTCP" diff --git a/docs/content/routing/providers/kv.md b/docs/content/routing/providers/kv.md index c3bd4cdac7..b7aa8a4d75 100644 --- a/docs/content/routing/providers/kv.md +++ b/docs/content/routing/providers/kv.md @@ -384,6 +384,14 @@ You can declare TCP Routers and/or Services using KV. | Key (Path) | Value | |-------------------------------------------------------------------|-------| | `traefik/tcp/services/mytcpservice/loadbalancer/terminationdelay` | `100` | + +??? info "`traefik/tcp/services//loadbalancer/proxyprotocol/version`" + + See [PROXY protocol](../services/index.md#proxy-protocol) for more information. + + | Key (Path) | Value | + |------------------------------------------------------------------------|-------| + | `traefik/tcp/services/mytcpservice/loadbalancer/proxyprotocol/version` | `1` | ??? info "`traefik/tcp/services//weighted/services//name`" diff --git a/docs/content/routing/providers/marathon.md b/docs/content/routing/providers/marathon.md index ccb0a068ef..bef9e2b57a 100644 --- a/docs/content/routing/providers/marathon.md +++ b/docs/content/routing/providers/marathon.md @@ -421,6 +421,14 @@ You can declare TCP Routers and/or Services using labels. "traefik.tcp.services.mytcpservice.loadbalancer.terminationdelay": "100" ``` +??? info "`traefik.tcp.services..loadbalancer.proxyprotocol.version`" + + See [PROXY protocol](../services/index.md#proxy-protocol) for more information. + + ```json + "traefik.tcp.services.mytcpservice.loadbalancer.proxyprotocol.version": "1" + ``` + ### UDP You can declare UDP Routers and/or Services using labels. diff --git a/docs/content/routing/providers/rancher.md b/docs/content/routing/providers/rancher.md index 3778561157..41c59836a2 100644 --- a/docs/content/routing/providers/rancher.md +++ b/docs/content/routing/providers/rancher.md @@ -424,6 +424,14 @@ You can declare TCP Routers and/or Services using labels. - "traefik.tcp.services.mytcpservice.loadbalancer.terminationdelay=100" ``` +??? info "`traefik.tcp.services..loadbalancer.proxyprotocol.version`" + + See [PROXY protocol](../services/index.md#proxy-protocol) for more information. + + ```yaml + - "traefik.tcp.services.mytcpservice.loadbalancer.proxyprotocol.version=1" + ``` + ### UDP You can declare UDP Routers and/or Services using labels. diff --git a/docs/content/routing/services/index.md b/docs/content/routing/services/index.md index 43920312ed..921009894b 100644 --- a/docs/content/routing/services/index.md +++ b/docs/content/routing/services/index.md @@ -991,6 +991,39 @@ The `address` option (IP:Port) point to a specific instance. - address: "xx.xx.xx.xx:xx" ``` +#### 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)" + + ```toml tab="TOML" + ## Dynamic configuration + [tcp.services] + [tcp.services.my-service.loadBalancer] + [tcp.services.my-service.loadBalancer.proxyProtocol] + version = 1 + ``` + + ```yaml tab="YAML" + ## Dynamic configuration + tcp: + services: + my-service: + loadBalancer: + proxyProtocol: + version: 1 + ``` + #### Termination Delay 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). diff --git a/go.mod b/go.mod index 8d1dfacdc5..1312a764be 100644 --- a/go.mod +++ b/go.mod @@ -14,7 +14,6 @@ require ( github.com/abbot/go-http-auth v0.0.0-00010101000000-000000000000 github.com/abronan/valkeyrie v0.0.0-20200127174252-ef4277a138cd github.com/aws/aws-sdk-go v1.30.20 - github.com/c0va23/go-proxyprotocol v0.9.1 github.com/cenkalti/backoff/v4 v4.0.2 github.com/containerd/containerd v1.3.2 // indirect github.com/containous/alice v0.0.0-20181107144136-d83ebdd94cbd @@ -63,6 +62,7 @@ require ( github.com/openzipkin/zipkin-go v0.2.2 github.com/patrickmn/go-cache v2.1.0+incompatible github.com/philhofer/fwd v1.0.0 // indirect + github.com/pires/go-proxyproto v0.3.1 github.com/pmezard/go-difflib v1.0.0 github.com/prometheus/client_golang v1.3.0 github.com/prometheus/client_model v0.1.0 diff --git a/go.sum b/go.sum index 91d54bece8..a927ab439d 100644 --- a/go.sum +++ b/go.sum @@ -141,8 +141,6 @@ github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kB github.com/blang/semver v3.1.0+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc h1:biVzkmvwrH8WK8raXaxBx6fRVTlJILwEwQGL1I/ByEI= github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= -github.com/c0va23/go-proxyprotocol v0.9.1 h1:5BCkp0fDJOhzzH1lhjUgHhmZz9VvRMMif1U2D31hb34= -github.com/c0va23/go-proxyprotocol v0.9.1/go.mod h1:TNjUV+llvk8TvWJxlPYAeAYZgSzT/iicNr3nWBWX320= github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ= github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4= github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= @@ -680,6 +678,8 @@ github.com/pierrec/lz4 v0.0.0-20190327172049-315a67e90e41/go.mod h1:3/3N9NVKO0je github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc= github.com/pierrec/lz4 v2.0.5+incompatible h1:2xWsjqPFWcplujydGg4WmhC/6fZqK42wMM8aXeqhl0I= github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= +github.com/pires/go-proxyproto v0.3.1 h1:eWb52zeDUbSUDBV+8aVCfOy0pnEG6DrDW3cJ/WKdQsk= +github.com/pires/go-proxyproto v0.3.1/go.mod h1:Odh9VFOZJCf9G8cLW5o435Xf1J95Jw9Gw5rnCjcwzAY= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= diff --git a/integration/proxy_protocol_test.go b/integration/proxy_protocol_test.go index 15588d868a..6a761c071e 100644 --- a/integration/proxy_protocol_test.go +++ b/integration/proxy_protocol_test.go @@ -34,7 +34,7 @@ func (s *ProxyProtocolSuite) TestProxyProtocolTrusted(c *check.C) { c.Assert(err, checker.IsNil) defer s.killCmd(cmd) - err = try.GetRequest("http://"+haproxyIP+"/whoami", 500*time.Millisecond, + err = try.GetRequest("http://"+haproxyIP+"/whoami", 1*time.Second, try.StatusCodeIs(http.StatusOK), try.BodyContains("X-Forwarded-For: "+gatewayIP)) c.Assert(err, checker.IsNil) @@ -57,7 +57,7 @@ func (s *ProxyProtocolSuite) TestProxyProtocolV2Trusted(c *check.C) { c.Assert(err, checker.IsNil) defer s.killCmd(cmd) - err = try.GetRequest("http://"+haproxyIP+":81/whoami", 500*time.Millisecond, + err = try.GetRequest("http://"+haproxyIP+":81/whoami", 1*time.Second, try.StatusCodeIs(http.StatusOK), try.BodyContains("X-Forwarded-For: "+gatewayIP)) c.Assert(err, checker.IsNil) @@ -79,7 +79,7 @@ func (s *ProxyProtocolSuite) TestProxyProtocolNotTrusted(c *check.C) { c.Assert(err, checker.IsNil) defer s.killCmd(cmd) - err = try.GetRequest("http://"+haproxyIP+"/whoami", 500*time.Millisecond, + err = try.GetRequest("http://"+haproxyIP+"/whoami", 1*time.Second, try.StatusCodeIs(http.StatusOK), try.BodyContains("X-Forwarded-For: "+haproxyIP)) c.Assert(err, checker.IsNil) @@ -101,7 +101,7 @@ func (s *ProxyProtocolSuite) TestProxyProtocolV2NotTrusted(c *check.C) { c.Assert(err, checker.IsNil) defer s.killCmd(cmd) - err = try.GetRequest("http://"+haproxyIP+":81/whoami", 500*time.Millisecond, + err = try.GetRequest("http://"+haproxyIP+":81/whoami", 1*time.Second, try.StatusCodeIs(http.StatusOK), try.BodyContains("X-Forwarded-For: "+haproxyIP)) c.Assert(err, checker.IsNil) diff --git a/pkg/config/dynamic/tcp_config.go b/pkg/config/dynamic/tcp_config.go index 8e0ac42ee3..e6b604b23e 100644 --- a/pkg/config/dynamic/tcp_config.go +++ b/pkg/config/dynamic/tcp_config.go @@ -72,8 +72,9 @@ type TCPServersLoadBalancer struct { // connection, to close the reading capability as well, hence fully terminating the // connection. It is a duration in milliseconds, defaulting to 100. A negative value // means an infinite deadline (i.e. the reading capability is never closed). - TerminationDelay *int `json:"terminationDelay,omitempty" toml:"terminationDelay,omitempty" yaml:"terminationDelay,omitempty"` - Servers []TCPServer `json:"servers,omitempty" toml:"servers,omitempty" yaml:"servers,omitempty" label-slice-as-struct:"server"` + TerminationDelay *int `json:"terminationDelay,omitempty" toml:"terminationDelay,omitempty" yaml:"terminationDelay,omitempty"` + ProxyProtocol *ProxyProtocol `json:"proxyProtocol,omitempty" toml:"proxyProtocol,omitempty" yaml:"proxyProtocol,omitempty" label:"allowEmpty" file:"allowEmpty"` + Servers []TCPServer `json:"servers,omitempty" toml:"servers,omitempty" yaml:"servers,omitempty" label-slice-as-struct:"server"` } // SetDefaults Default values for a TCPServersLoadBalancer. @@ -106,3 +107,15 @@ type TCPServer struct { Address string `json:"address,omitempty" toml:"address,omitempty" yaml:"address,omitempty" label:"-"` Port string `toml:"-" json:"-" yaml:"-"` } + +// +k8s:deepcopy-gen=true + +// ProxyProtocol holds the ProxyProtocol configuration. +type ProxyProtocol struct { + Version int `json:"version,omitempty" toml:"version,omitempty" yaml:"version,omitempty"` +} + +// SetDefaults Default values for a ProxyProtocol. +func (p *ProxyProtocol) SetDefaults() { + p.Version = 2 +} diff --git a/pkg/config/dynamic/zz_generated.deepcopy.go b/pkg/config/dynamic/zz_generated.deepcopy.go index 06060ea62d..30d60dc885 100644 --- a/pkg/config/dynamic/zz_generated.deepcopy.go +++ b/pkg/config/dynamic/zz_generated.deepcopy.go @@ -880,6 +880,22 @@ func (in *PassTLSClientCert) DeepCopy() *PassTLSClientCert { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ProxyProtocol) DeepCopyInto(out *ProxyProtocol) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProxyProtocol. +func (in *ProxyProtocol) DeepCopy() *ProxyProtocol { + if in == nil { + return nil + } + out := new(ProxyProtocol) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *RateLimit) DeepCopyInto(out *RateLimit) { *out = *in @@ -1373,6 +1389,11 @@ func (in *TCPServersLoadBalancer) DeepCopyInto(out *TCPServersLoadBalancer) { *out = new(int) **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)) diff --git a/pkg/config/label/label_test.go b/pkg/config/label/label_test.go index 5182d53cee..e3cac2099a 100644 --- a/pkg/config/label/label_test.go +++ b/pkg/config/label/label_test.go @@ -182,8 +182,10 @@ func TestDecodeConfiguration(t *testing.T) { "traefik.tcp.routers.Router1.tls.passthrough": "false", "traefik.tcp.services.Service0.loadbalancer.server.Port": "42", "traefik.tcp.services.Service0.loadbalancer.TerminationDelay": "42", + "traefik.tcp.services.Service0.loadbalancer.proxyProtocol.version": "42", "traefik.tcp.services.Service1.loadbalancer.server.Port": "42", "traefik.tcp.services.Service1.loadbalancer.TerminationDelay": "42", + "traefik.tcp.services.Service1.loadbalancer.proxyProtocol": "true", "traefik.udp.routers.Router0.entrypoints": "foobar, fiibar", "traefik.udp.routers.Router0.service": "foobar", @@ -233,6 +235,7 @@ func TestDecodeConfiguration(t *testing.T) { }, }, TerminationDelay: func(i int) *int { return &i }(42), + ProxyProtocol: &dynamic.ProxyProtocol{Version: 42}, }, }, "Service1": { @@ -243,6 +246,7 @@ func TestDecodeConfiguration(t *testing.T) { }, }, TerminationDelay: func(i int) *int { return &i }(42), + ProxyProtocol: &dynamic.ProxyProtocol{Version: 2}, }, }, }, diff --git a/pkg/provider/kubernetes/crd/fixtures/tcp/with_proxyprotocol.yml b/pkg/provider/kubernetes/crd/fixtures/tcp/with_proxyprotocol.yml new file mode 100644 index 0000000000..843f13a716 --- /dev/null +++ b/pkg/provider/kubernetes/crd/fixtures/tcp/with_proxyprotocol.yml @@ -0,0 +1,17 @@ +apiVersion: traefik.containo.us/v1alpha1 +kind: IngressRouteTCP +metadata: + name: test.route + namespace: default + +spec: + entryPoints: + - foo + + routes: + - match: HostSNI(`foo.com`) + services: + - name: whoamitcp + port: 8000 + proxyProtocol: + version: 2 diff --git a/pkg/provider/kubernetes/crd/kubernetes_tcp.go b/pkg/provider/kubernetes/crd/kubernetes_tcp.go index 49cff4e60f..e2234700fe 100644 --- a/pkg/provider/kubernetes/crd/kubernetes_tcp.go +++ b/pkg/provider/kubernetes/crd/kubernetes_tcp.go @@ -140,6 +140,15 @@ func createLoadBalancerServerTCP(client Client, namespace string, service v1alph }, } + if service.ProxyProtocol != nil { + tcpService.LoadBalancer.ProxyProtocol = &dynamic.ProxyProtocol{} + tcpService.LoadBalancer.ProxyProtocol.SetDefaults() + + if service.ProxyProtocol.Version != 0 { + tcpService.LoadBalancer.ProxyProtocol.Version = service.ProxyProtocol.Version + } + } + if service.TerminationDelay != nil { tcpService.LoadBalancer.TerminationDelay = service.TerminationDelay } diff --git a/pkg/provider/kubernetes/crd/kubernetes_test.go b/pkg/provider/kubernetes/crd/kubernetes_test.go index f1fbcc91de..07d3571121 100644 --- a/pkg/provider/kubernetes/crd/kubernetes_test.go +++ b/pkg/provider/kubernetes/crd/kubernetes_test.go @@ -2878,6 +2878,49 @@ func TestLoadIngressRoutes(t *testing.T) { TLS: &dynamic.TLSConfiguration{}, }, }, + { + desc: "TCP with proxyProtocol Version", + paths: []string{"tcp/services.yml", "tcp/with_proxyprotocol.yml"}, + expected: &dynamic.Configuration{ + TLS: &dynamic.TLSConfiguration{}, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{ + "default-test.route-fdd3e9338e47a45efefc": { + EntryPoints: []string{"foo"}, + Service: "default-test.route-fdd3e9338e47a45efefc", + Rule: "HostSNI(`foo.com`)", + }, + }, + Services: map[string]*dynamic.TCPService{ + "default-test.route-fdd3e9338e47a45efefc": { + LoadBalancer: &dynamic.TCPServersLoadBalancer{ + Servers: []dynamic.TCPServer{ + { + Address: "10.10.0.1:8000", + Port: "", + }, + { + Address: "10.10.0.2:8000", + Port: "", + }, + }, + ProxyProtocol: &dynamic.ProxyProtocol{Version: 2}, + }, + }, + }, + }, + HTTP: &dynamic.HTTPConfiguration{ + ServersTransports: map[string]*dynamic.ServersTransport{}, + Routers: map[string]*dynamic.Router{}, + Middlewares: map[string]*dynamic.Middleware{}, + Services: map[string]*dynamic.Service{}, + }, + }, + }, { desc: "TLS with tls store", paths: []string{"services.yml", "with_tls_store.yml"}, diff --git a/pkg/provider/kubernetes/crd/traefik/v1alpha1/ingressroutetcp.go b/pkg/provider/kubernetes/crd/traefik/v1alpha1/ingressroutetcp.go index d30b1b572a..ab38c16ea7 100644 --- a/pkg/provider/kubernetes/crd/traefik/v1alpha1/ingressroutetcp.go +++ b/pkg/provider/kubernetes/crd/traefik/v1alpha1/ingressroutetcp.go @@ -1,6 +1,7 @@ package v1alpha1 import ( + "github.com/traefik/traefik/v2/pkg/config/dynamic" "github.com/traefik/traefik/v2/pkg/types" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -53,11 +54,12 @@ type TLSStoreTCPRef struct { // ServiceTCP defines an upstream to proxy traffic. type ServiceTCP struct { - Name string `json:"name"` - Namespace string `json:"namespace"` - Port int32 `json:"port"` - Weight *int `json:"weight,omitempty"` - TerminationDelay *int `json:"terminationDelay,omitempty"` + Name string `json:"name"` + Namespace string `json:"namespace"` + Port int32 `json:"port"` + Weight *int `json:"weight,omitempty"` + TerminationDelay *int `json:"terminationDelay,omitempty"` + ProxyProtocol *dynamic.ProxyProtocol `json:"proxyProtocol,omitempty"` } // +genclient diff --git a/pkg/provider/kubernetes/crd/traefik/v1alpha1/zz_generated.deepcopy.go b/pkg/provider/kubernetes/crd/traefik/v1alpha1/zz_generated.deepcopy.go index d5d6e6ad9e..b26c117469 100644 --- a/pkg/provider/kubernetes/crd/traefik/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/provider/kubernetes/crd/traefik/v1alpha1/zz_generated.deepcopy.go @@ -1011,6 +1011,11 @@ func (in *ServiceTCP) DeepCopyInto(out *ServiceTCP) { *out = new(int) **out = **in } + if in.ProxyProtocol != nil { + in, out := &in.ProxyProtocol, &out.ProxyProtocol + *out = new(dynamic.ProxyProtocol) + **out = **in + } return } diff --git a/pkg/server/server_entrypoint_tcp.go b/pkg/server/server_entrypoint_tcp.go index a9f1fdf948..1f001c9c83 100644 --- a/pkg/server/server_entrypoint_tcp.go +++ b/pkg/server/server_entrypoint_tcp.go @@ -11,7 +11,7 @@ import ( "syscall" "time" - proxyprotocol "github.com/c0va23/go-proxyprotocol" + "github.com/pires/go-proxyproto" "github.com/sirupsen/logrus" "github.com/traefik/traefik/v2/pkg/config/static" "github.com/traefik/traefik/v2/pkg/ip" @@ -316,10 +316,10 @@ func (c *writeCloserWrapper) CloseWrite() error { // implementation, if any was found within the underlying conn. func writeCloser(conn net.Conn) (tcp.WriteCloser, error) { switch typedConn := conn.(type) { - case *proxyprotocol.Conn: - underlying, err := writeCloser(typedConn.Conn) - if err != nil { - return nil, err + case *proxyproto.Conn: + underlying, ok := typedConn.TCPConn() + if !ok { + return nil, fmt.Errorf("underlying connection is not a tcp connection") } return &writeCloserWrapper{writeCloser: underlying, Conn: typedConn}, nil case *net.TCPConn: @@ -356,42 +356,35 @@ func (ln tcpKeepAliveListener) Accept() (net.Conn, error) { return tc, nil } -type proxyProtocolLogger struct { - log.Logger -} - -// Printf force log level to debug. -func (p proxyProtocolLogger) Printf(format string, v ...interface{}) { - p.Debugf(format, v...) -} - func buildProxyProtocolListener(ctx context.Context, entryPoint *static.EntryPoint, listener net.Listener) (net.Listener, error) { - var sourceCheck func(addr net.Addr) (bool, error) + proxyListener := &proxyproto.Listener{Listener: listener} + if entryPoint.ProxyProtocol.Insecure { - sourceCheck = func(_ net.Addr) (bool, error) { - return true, nil - } - } else { - checker, err := ip.NewChecker(entryPoint.ProxyProtocol.TrustedIPs) - if err != nil { - return nil, err + log.FromContext(ctx).Infof("Enabling ProxyProtocol without trusted IPs: Insecure") + return proxyListener, nil + } + + checker, err := ip.NewChecker(entryPoint.ProxyProtocol.TrustedIPs) + if err != nil { + return nil, err + } + + proxyListener.Policy = func(upstream net.Addr) (proxyproto.Policy, error) { + ipAddr, ok := upstream.(*net.TCPAddr) + if !ok { + return proxyproto.REJECT, fmt.Errorf("type error %v", upstream) } - sourceCheck = func(addr net.Addr) (bool, error) { - ipAddr, ok := addr.(*net.TCPAddr) - if !ok { - return false, fmt.Errorf("type error %v", addr) - } - - return checker.ContainsIP(ipAddr.IP), nil + if !checker.ContainsIP(ipAddr.IP) { + log.FromContext(ctx).Debugf("IP %s is not in trusted IPs list, ignoring ProxyProtocol Headers and bypass connection", ipAddr.IP) + return proxyproto.IGNORE, nil } + return proxyproto.USE, nil } log.FromContext(ctx).Infof("Enabling ProxyProtocol for trusted IPs %v", entryPoint.ProxyProtocol.TrustedIPs) - return proxyprotocol.NewDefaultListener(listener). - WithSourceChecker(sourceCheck). - WithLogger(proxyProtocolLogger{Logger: log.FromContext(ctx)}), nil + return proxyListener, nil } func buildListener(ctx context.Context, entryPoint *static.EntryPoint) (net.Listener, error) { diff --git a/pkg/server/service/tcp/service.go b/pkg/server/service/tcp/service.go index 96913e941b..69c9ccb312 100644 --- a/pkg/server/service/tcp/service.go +++ b/pkg/server/service/tcp/service.go @@ -59,7 +59,7 @@ func (m *Manager) BuildTCP(rootCtx context.Context, serviceName string) (tcp.Han continue } - handler, err := tcp.NewProxy(server.Address, duration) + handler, err := tcp.NewProxy(server.Address, duration, conf.LoadBalancer.ProxyProtocol) if err != nil { logger.Errorf("In service %q server %q: %v", serviceQualifiedName, server.Address, err) continue diff --git a/pkg/tcp/proxy.go b/pkg/tcp/proxy.go index d3401a874a..7d07c742b2 100644 --- a/pkg/tcp/proxy.go +++ b/pkg/tcp/proxy.go @@ -1,10 +1,13 @@ package tcp import ( + "fmt" "io" "net" "time" + "github.com/pires/go-proxyproto" + "github.com/traefik/traefik/v2/pkg/config/dynamic" "github.com/traefik/traefik/v2/pkg/log" ) @@ -12,16 +15,21 @@ import ( type Proxy struct { target *net.TCPAddr terminationDelay time.Duration + proxyProtocol *dynamic.ProxyProtocol } // NewProxy creates a new Proxy. -func NewProxy(address string, terminationDelay time.Duration) (*Proxy, error) { +func NewProxy(address string, terminationDelay time.Duration, proxyProtocol *dynamic.ProxyProtocol) (*Proxy, error) { tcpAddr, err := net.ResolveTCPAddr("tcp", address) if err != nil { return nil, err } - return &Proxy{target: tcpAddr, terminationDelay: terminationDelay}, nil + if proxyProtocol != nil && (proxyProtocol.Version < 1 || proxyProtocol.Version > 2) { + return nil, fmt.Errorf("unknown proxyProtocol version: %d", proxyProtocol.Version) + } + + return &Proxy{target: tcpAddr, terminationDelay: terminationDelay, proxyProtocol: proxyProtocol}, nil } // ServeTCP forwards the connection to a service. @@ -39,8 +47,16 @@ func (p *Proxy) ServeTCP(conn WriteCloser) { // maybe not needed, but just in case 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.WithoutContext().Errorf("Error while writing proxy protocol headers to backend connection: %v", err) + return + } + } + go p.connCopy(conn, connBackend, errChan) go p.connCopy(connBackend, conn, errChan) diff --git a/pkg/tcp/proxy_test.go b/pkg/tcp/proxy_test.go index aa7dd1eaa8..3e287629ee 100644 --- a/pkg/tcp/proxy_test.go +++ b/pkg/tcp/proxy_test.go @@ -2,13 +2,17 @@ package tcp import ( "bytes" + "errors" "fmt" "io" "net" "testing" "time" + "github.com/pires/go-proxyproto" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/traefik/traefik/v2/pkg/config/dynamic" ) func fakeRedis(t *testing.T, listener net.Listener) { @@ -16,6 +20,7 @@ func fakeRedis(t *testing.T, listener net.Listener) { conn, err := listener.Accept() fmt.Println("Accept on server") require.NoError(t, err) + for { withErr := false buf := make([]byte, 64) @@ -26,12 +31,13 @@ func fakeRedis(t *testing.T, listener net.Listener) { if string(buf[:4]) == "ping" { time.Sleep(1 * time.Millisecond) if _, err := conn.Write([]byte("PONG")); err != nil { - conn.Close() + _ = conn.Close() return } } + if withErr { - conn.Close() + _ = conn.Close() return } } @@ -46,7 +52,7 @@ func TestCloseWrite(t *testing.T) { _, port, err := net.SplitHostPort(backendListener.Addr().String()) require.NoError(t, err) - proxy, err := NewProxy(":"+port, 10*time.Millisecond) + proxy, err := NewProxy(":"+port, 10*time.Millisecond, nil) require.NoError(t, err) proxyListener, err := net.Listen("tcp", ":0") @@ -79,3 +85,87 @@ func TestCloseWrite(t *testing.T) { 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, + }, + } + + for _, test := range testCases { + test := test + + 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 fakeRedis(t, &proxyBackendListener) + + _, port, err := net.SplitHostPort(proxyBackendListener.Addr().String()) + require.NoError(t, err) + + proxy, err := NewProxy(":"+port, 10*time.Millisecond, &dynamic.ProxyProtocol{Version: test.version}) + 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)) + } + }() + + _, 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) + }) + } +}