Add muxer for TCP Routers

This commit is contained in:
Daniel Tomcej 2022-03-17 11:02:08 -06:00 committed by GitHub
parent 79aab5aab8
commit dad76e0478
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
39 changed files with 2661 additions and 901 deletions

View file

@ -214,3 +214,6 @@
[[issues.exclude-rules]]
path = "(.+)\\.go"
text = "struct-tag: unknown option 'inline' in JSON tag"
[[issues.exclude-rules]]
path = "pkg/server/router/tcp/manager.go"
text = "Function 'buildEntryPointHandler' is too long (.+)"

View file

@ -165,6 +165,7 @@
- "traefik.tcp.routers.tcprouter0.entrypoints=foobar, foobar"
- "traefik.tcp.routers.tcprouter0.middlewares=foobar, foobar"
- "traefik.tcp.routers.tcprouter0.rule=foobar"
- "traefik.tcp.routers.tcprouter0.priority=42"
- "traefik.tcp.routers.tcprouter0.service=foobar"
- "traefik.tcp.routers.tcprouter0.tls=true"
- "traefik.tcp.routers.tcprouter0.tls.certresolver=foobar"
@ -177,6 +178,7 @@
- "traefik.tcp.routers.tcprouter1.entrypoints=foobar, foobar"
- "traefik.tcp.routers.tcprouter1.middlewares=foobar, foobar"
- "traefik.tcp.routers.tcprouter1.rule=foobar"
- "traefik.tcp.routers.tcprouter1.priority=42"
- "traefik.tcp.routers.tcprouter1.service=foobar"
- "traefik.tcp.routers.tcprouter1.tls=true"
- "traefik.tcp.routers.tcprouter1.tls.certresolver=foobar"

View file

@ -326,6 +326,7 @@
middlewares = ["foobar", "foobar"]
service = "foobar"
rule = "foobar"
priority = 42
[tcp.routers.TCPRouter0.tls]
passthrough = true
options = "foobar"
@ -343,6 +344,7 @@
middlewares = ["foobar", "foobar"]
service = "foobar"
rule = "foobar"
priority = 42
[tcp.routers.TCPRouter1.tls]
passthrough = true
options = "foobar"

View file

@ -366,6 +366,7 @@ tcp:
- foobar
service: foobar
rule: foobar
priority: 42
tls:
passthrough: true
options: foobar
@ -388,6 +389,7 @@ tcp:
- foobar
service: foobar
rule: foobar
priority: 42
tls:
passthrough: true
options: foobar

View file

@ -237,6 +237,7 @@
| `traefik/tcp/routers/TCPRouter0/entryPoints/1` | `foobar` |
| `traefik/tcp/routers/TCPRouter0/middlewares/0` | `foobar` |
| `traefik/tcp/routers/TCPRouter0/middlewares/1` | `foobar` |
| `traefik/tcp/routers/TCPRouter0/priority` | `42` |
| `traefik/tcp/routers/TCPRouter0/rule` | `foobar` |
| `traefik/tcp/routers/TCPRouter0/service` | `foobar` |
| `traefik/tcp/routers/TCPRouter0/tls/certResolver` | `foobar` |
@ -252,6 +253,7 @@
| `traefik/tcp/routers/TCPRouter1/entryPoints/1` | `foobar` |
| `traefik/tcp/routers/TCPRouter1/middlewares/0` | `foobar` |
| `traefik/tcp/routers/TCPRouter1/middlewares/1` | `foobar` |
| `traefik/tcp/routers/TCPRouter1/priority` | `42` |
| `traefik/tcp/routers/TCPRouter1/rule` | `foobar` |
| `traefik/tcp/routers/TCPRouter1/service` | `foobar` |
| `traefik/tcp/routers/TCPRouter1/tls/certResolver` | `foobar` |

View file

@ -163,6 +163,7 @@
"traefik.http.services.service01.loadbalancer.serverstransport": "foobar",
"traefik.tcp.routers.tcprouter0.entrypoints": "foobar, foobar",
"traefik.tcp.routers.tcprouter0.rule": "foobar",
"traefik.tcp.routers.tcprouter0.priority": "42",
"traefik.tcp.routers.tcprouter0.service": "foobar",
"traefik.tcp.routers.tcprouter0.tls": "true",
"traefik.tcp.routers.tcprouter0.tls.certresolver": "foobar",
@ -174,6 +175,7 @@
"traefik.tcp.routers.tcprouter0.tls.passthrough": "true",
"traefik.tcp.routers.tcprouter1.entrypoints": "foobar, foobar",
"traefik.tcp.routers.tcprouter1.rule": "foobar",
"traefik.tcp.routers.tcprouter1.priority": "42",
"traefik.tcp.routers.tcprouter1.service": "foobar",
"traefik.tcp.routers.tcprouter1.tls": "true",
"traefik.tcp.routers.tcprouter1.tls.certresolver": "foobar",

View file

@ -62,6 +62,8 @@ spec:
- name
type: object
type: array
priority:
type: integer
services:
items:
description: ServiceTCP defines an upstream to proxy traffic.

View file

@ -99,7 +99,8 @@ For example, to change the rule, you could add the tag ```traefik.http.routers.m
```
??? info "`traefik.http.routers.<router_name>.priority`"
<!-- TODO doc priority in routers page -->
See [priority](../routers/index.md#priority) for more information.
```yaml
traefik.http.routers.myrouter.priority=42

View file

@ -538,6 +538,14 @@ You can declare TCP Routers and/or Services using labels.
- "traefik.tcp.routers.mytcprouter.tls.passthrough=true"
```
??? info "`traefik.tcp.routers.<router_name>.priority`"
See [priority](../routers/index.md#priority_1) for more information.
```yaml
- "traefik.tcp.routers.myrouter.priority=42"
```
#### TCP Services
??? info "`traefik.tcp.services.<service_name>.loadbalancer.server.port`"

View file

@ -379,6 +379,14 @@ You can declare TCP Routers and/or Services using labels.
traefik.tcp.routers.mytcprouter.tls.passthrough=true
```
??? info "`traefik.tcp.routers.<router_name>.priority`"
See [priority](../routers/index.md#priority_1) for more information.
```yaml
traefik.tcp.routers.myrouter.priority=42
```
#### TCP Services
??? info "`traefik.tcp.services.<service_name>.loadbalancer.server.port`"

View file

@ -357,27 +357,27 @@ Register the `IngressRoute` [kind](../../reference/dynamic-configuration/kuberne
- b.example.net
```
| Ref | Attribute | Purpose |
|------|--------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [1] | `entryPoints` | List of [entry points](../routers/index.md#entrypoints) names |
| [2] | `routes` | List of routes |
| [3] | `routes[n].match` | Defines the [rule](../routers/index.md#rule) corresponding to an underlying router. |
| [4] | `routes[n].priority` | [Disambiguate](../routers/index.md#priority) rules of the same length, for route matching |
| [5] | `routes[n].middlewares` | List of reference to [Middleware](#kind-middleware) |
| [6] | `middlewares[n].name` | Defines the [Middleware](#kind-middleware) name |
| [7] | `middlewares[n].namespace` | Defines the [Middleware](#kind-middleware) namespace |
| [8] | `routes[n].services` | List of any combination of [TraefikService](#kind-traefikservice) and reference to a [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) (See below for `ExternalName Service` setup) |
| [9] | `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. |
| Ref | Attribute | Purpose |
|------|--------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [1] | `entryPoints` | List of [entry points](../routers/index.md#entrypoints) names |
| [2] | `routes` | List of routes |
| [3] | `routes[n].match` | Defines the [rule](../routers/index.md#rule) corresponding to an underlying router. |
| [4] | `routes[n].priority` | Defines the [priority](../routers/index.md#priority) to disambiguate rules of the same length, for route matching |
| [5] | `routes[n].middlewares` | List of reference to [Middleware](#kind-middleware) |
| [6] | `middlewares[n].name` | Defines the [Middleware](#kind-middleware) name |
| [7] | `middlewares[n].namespace` | Defines the [Middleware](#kind-middleware) namespace |
| [8] | `routes[n].services` | List of any combination of [TraefikService](#kind-traefikservice) and reference to a [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) (See below for `ExternalName Service` setup) |
| [9] | `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. |
| [10] | `services[n].serversTransport` | Defines the reference to a [ServersTransport](#kind-serverstransport). The ServersTransport namespace is assumed to be the [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) namespace (see [ServersTransport reference](#serverstransport-reference)). |
| [11] | `tls` | Defines [TLS](../routers/index.md#tls) 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) |
| [17] | `tls.domains` | List of [domains](../routers/index.md#domains) |
| [18] | `domains[n].main` | Defines the main domain name |
| [19] | `domains[n].sans` | List of SANs (alternative domains) |
| [11] | `tls` | Defines [TLS](../routers/index.md#tls) 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) |
| [17] | `tls.domains` | List of [domains](../routers/index.md#domains) |
| [18] | `domains[n].main` | Defines the main domain name |
| [19] | `domains[n].sans` | List of SANs (alternative domains) |
??? example "Declaring an IngressRoute"
@ -1088,54 +1088,56 @@ Register the `IngressRouteTCP` [kind](../../reference/dynamic-configuration/kube
- footcp
routes: # [2]
- match: HostSNI(`*`) # [3]
priority: 10 # [4]
middlewares:
- name: middleware1 # [4]
namespace: default # [5]
services: # [6]
- name: foo # [7]
port: 8080 # [8]
weight: 10 # [9]
terminationDelay: 400 # [10]
proxyProtocol: # [11]
version: 1 # [12]
tls: # [13]
secretName: supersecret # [14]
options: # [15]
name: opt # [16]
namespace: default # [17]
certResolver: foo # [18]
domains: # [19]
- main: example.net # [20]
sans: # [21]
- name: middleware1 # [5]
namespace: default # [6]
services: # [7]
- name: foo # [8]
port: 8080 # [9]
weight: 10 # [10]
terminationDelay: 400 # [11]
proxyProtocol: # [12]
version: 1 # [13]
tls: # [14]
secretName: supersecret # [15]
options: # [16]
name: opt # [17]
namespace: default # [18]
certResolver: foo # [19]
domains: # [20]
- main: example.net # [21]
sans: # [22]
- a.example.net
- b.example.net
passthrough: false # [22]
passthrough: false # [23]
```
| 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] | `middlewares[n].name` | Defines the [MiddlewareTCP](#kind-middlewaretcp) name |
| [5] | `middlewares[n].namespace` | Defines the [MiddlewareTCP](#kind-middlewaretcp) namespace |
| [6] | `routes[n].services` | List of [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) definitions (See below for `ExternalName Service` setup) |
| [7] | `services[n].name` | Defines the name of a [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) |
| [8] | `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. |
| [9] | `services[n].weight` | Defines the weight to apply to the server load balancing |
| [10] | `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). |
| [11] | `proxyProtocol` | Defines the [PROXY protocol](../services/index.md#proxy-protocol) configuration |
| [12] | `version` | Defines the [PROXY protocol](../services/index.md#proxy-protocol) version |
| [13] | `tls` | Defines [TLS](../routers/index.md#tls_1) certificate configuration |
| [14] | `tls.secretName` | Defines the [secret](https://kubernetes.io/docs/concepts/configuration/secret/) name used to store the certificate (in the `IngressRoute` namespace) |
| [15] | `tls.options` | Defines the reference to a [TLSOption](#kind-tlsoption) |
| [16] | `options.name` | Defines the [TLSOption](#kind-tlsoption) name |
| [17] | `options.namespace` | Defines the [TLSOption](#kind-tlsoption) namespace |
| [18] | `tls.certResolver` | Defines the reference to a [CertResolver](../routers/index.md#certresolver_1) |
| [19] | `tls.domains` | List of [domains](../routers/index.md#domains_1) |
| [20] | `domains[n].main` | Defines the main domain name |
| [21] | `domains[n].sans` | List of SANs (alternative domains) |
| [22] | `tls.passthrough` | If `true`, delegates the TLS termination to the backend |
| [3] | `routes[n].match` | Defines the [rule](../routers/index.md#rule_1) of the underlying router |
| [4] | `routes[n].priority` | Defines the [priority](../routers/index.md#priority_1) to disambiguate rules of the same length, for route matching |
| [5] | `middlewares[n].name` | Defines the [MiddlewareTCP](#kind-middlewaretcp) name |
| [6] | `middlewares[n].namespace` | Defines the [MiddlewareTCP](#kind-middlewaretcp) namespace |
| [7] | `routes[n].services` | List of [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) definitions (See below for `ExternalName Service` setup) |
| [8] | `services[n].name` | Defines the name of a [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) |
| [9] | `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. |
| [10] | `services[n].weight` | Defines the weight to apply to the server load balancing |
| [11] | `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). |
| [12] | `proxyProtocol` | Defines the [PROXY protocol](../services/index.md#proxy-protocol) configuration |
| [13] | `version` | Defines the [PROXY protocol](../services/index.md#proxy-protocol) version |
| [14] | `tls` | Defines [TLS](../routers/index.md#tls_1) certificate configuration |
| [15] | `tls.secretName` | Defines the [secret](https://kubernetes.io/docs/concepts/configuration/secret/) name used to store the certificate (in the `IngressRoute` namespace) |
| [16] | `tls.options` | Defines the reference to a [TLSOption](#kind-tlsoption) |
| [17] | `options.name` | Defines the [TLSOption](#kind-tlsoption) name |
| [18] | `options.namespace` | Defines the [TLSOption](#kind-tlsoption) namespace |
| [19] | `tls.certResolver` | Defines the reference to a [CertResolver](../routers/index.md#certresolver_1) |
| [20] | `tls.domains` | List of [domains](../routers/index.md#domains_1) |
| [21] | `domains[n].main` | Defines the main domain name |
| [22] | `domains[n].sans` | List of SANs (alternative domains) |
| [23] | `tls.passthrough` | If `true`, delegates the TLS termination to the backend |
??? example "Declaring an IngressRouteTCP"
@ -1151,6 +1153,7 @@ Register the `IngressRouteTCP` [kind](../../reference/dynamic-configuration/kube
routes:
# Match is the rule corresponding to an underlying router.
- match: HostSNI(`*`)
priority: 10
services:
- name: foo
port: 8080

View file

@ -366,7 +366,6 @@ You can declare TCP Routers and/or Services using KV.
| Key (Path) | Value |
|-----------------------------------------------|----------|
| `traefik/tcp/routers/mytcprouter/tls/options` | `foobar` |
??? info "`traefik/tcp/routers/<router_name>/tls/passthrough`"
@ -376,6 +375,14 @@ You can declare TCP Routers and/or Services using KV.
|---------------------------------------------------|--------|
| `traefik/tcp/routers/mytcprouter/tls/passthrough` | `true` |
??? info "`traefik/tcp/routers/<router_name>/priority`"
See [priority](../routers/index.md#priority_1) for more information.
| Key (Path) | Value |
|------------------------------------------|-------|
| `traefik/tcp/routers/myrouter/priority` | `42` |
#### TCP Services
??? info "`traefik/tcp/services/<service_name>/loadbalancer/servers/<n>/url`"

View file

@ -412,6 +412,14 @@ You can declare TCP Routers and/or Services using labels.
"traefik.tcp.routers.mytcprouter.tls.passthrough": "true"
```
??? info "`traefik.tcp.routers.<router_name>.priority`"
See [priority](../routers/index.md#priority_1) for more information.
```json
"traefik.tcp.routers.myrouter.priority": "42"
```
#### TCP Services
??? info "`traefik.tcp.services.<service_name>.loadbalancer.server.port`"

View file

@ -415,6 +415,14 @@ You can declare TCP Routers and/or Services using labels.
- "traefik.tcp.routers.mytcprouter.tls.passthrough=true"
```
??? info "`traefik.tcp.routers.<router_name>.priority`"
See [priority](../routers/index.md#priority_1) for more information.
```yaml
- "traefik.tcp.routers.myrouter.priority=42"
```
#### TCP Services
??? info "`traefik.tcp.services.<service_name>.loadbalancer.server.port`"

View file

@ -212,7 +212,7 @@ If the rule is verified, the router becomes active, calls middlewares, and then
??? tip "Backticks or Quotes?"
To set the value of a rule, use [backticks](https://en.wiktionary.org/wiki/backtick) ``` ` ``` or escaped double-quotes `\"`.
Single quotes `'` are not accepted as values are [Golang's String Literals](https://golang.org/ref/spec#String_literals).
Single quotes `'` are not accepted since the values are [Golang's String Literals](https://golang.org/ref/spec#String_literals).
!!! example "Host is example.com"
@ -257,11 +257,12 @@ The table below lists all the available matchers:
!!! info "Combining Matchers Using Operators and Parenthesis"
You can combine multiple matchers using the AND (`&&`) and OR (`||`) operators. You can also use parenthesis.
The usual AND (`&&`) and OR (`||`) logical operators can be used, with the expected precedence rules,
as well as parentheses.
!!! info "Invert a matcher"
!!! info "Inverting a matcher"
You can invert a matcher by using the `!` operator.
One can invert a matcher by using the `!` operator.
!!! important "Rule, Middleware, and Services"
@ -795,9 +796,33 @@ If you want to limit the router scope to a set of entry points, set the entry po
### Rule
| Rule | Description |
|--------------------------------|-------------------------------------------------------------------------|
| ```HostSNI(`domain-1`, ...)``` | Check if the Server Name Indication corresponds to the given `domains`. |
Rules are a set of matchers configured with values, that determine if a particular request matches specific criteria.
If the rule is verified, the router becomes active, calls middlewares, and then forwards the request to the service.
??? tip "Backticks or Quotes?"
To set the value of a rule, use [backticks](https://en.wiktionary.org/wiki/backtick) ``` ` ``` or escaped double-quotes `\"`.
Single quotes `'` are not accepted since the values are [Golang's String Literals](https://golang.org/ref/spec#String_literals).
!!! example "HostSNI is example.com"
```toml
rule = "HostSNI(`example.com`)"
```
!!! example "HostSNI is example.com OR HostSNI is example.org AND ClientIP is 0.0.0.0"
```toml
rule = "HostSNI(`example.com`) || (HostSNI(`example.org`) && ClientIP(`0.0.0.0`))"
```
The table below lists all the available matchers:
| Rule | Description |
|---------------------------------------------|-----------------------------------------------------------------------------------------------------------|
| ```HostSNI(`domain-1`, ...)``` | Check if the Server Name Indication corresponds to the given `domains`. |
| ```ClientIP(`10.0.0.0/16`, `::1`)``` | Check if the request client IP is one of the given IP/CIDR. It accepts IPv4, IPv6 and CIDR formats. |
!!! important "Non-ASCII Domain Names"
@ -808,7 +833,101 @@ If you want to limit the router scope to a set of entry points, set the entry po
It is important to note that the Server Name Indication is an extension of the TLS protocol.
Hence, only TLS routers will be able to specify a domain name with that rule.
However, non-TLS routers will have to explicitly use that rule with `*` (every domain) to state that every non-TLS request will be handled by the router.
However, there is one special use case for HostSNI with non-TLS routers:
when one wants a non-TLS router that matches all (non-TLS) requests,
one should use the specific `HostSNI(*)` syntax.
!!! info "Combining Matchers Using Operators and Parenthesis"
The usual AND (`&&`) and OR (`||`) logical operators can be used, with the expected precedence rules,
as well as parentheses.
!!! info "Inverting a matcher"
One can invert a matcher by using the `!` operator.
!!! important "Rule, Middleware, and Services"
The rule is evaluated "before" any middleware has the opportunity to work, and "before" the request is forwarded to the service.
### Priority
To avoid path overlap, routes are sorted, by default, in descending order using rules length.
The priority is directly equal to the length of the rule, and so the longest length has the highest priority.
A value of `0` for the priority is ignored: `priority = 0` means that the default rules length sorting is used.
??? info "How default priorities are computed"
```yaml tab="File (YAML)"
## Dynamic configuration
tcp:
routers:
Router-1:
rule: "ClientIP(`192.168.0.12`)"
# ...
Router-2:
rule: "ClientIP(`192.168.0.0/24`)"
# ...
```
```toml tab="File (TOML)"
## Dynamic configuration
[tcp.routers]
[tcp.routers.Router-1]
rule = "ClientIP(`192.168.0.12`)"
# ...
[tcp.routers.Router-2]
rule = "ClientIP(`192.168.0.0/24`)"
# ...
```
The table below shows that `Router-2` has a higher computed priority than `Router-1`.
| Name | Rule | Priority |
|----------|-------------------------------------------------------------|----------|
| Router-1 | ```ClientIP(`192.168.0.12`)``` | 24 |
| Router-2 | ```ClientIP(`192.168.0.0/24`)``` | 26 |
Which means that requests from `192.168.0.12` would go to Router-2 even though Router-1 is intended to specifically handle them.
To achieve this intention, a priority (higher than 26) should be set on Router-1.
??? example "Setting priorities -- using the [File Provider](../../providers/file.md)"
```yaml tab="File (YAML)"
## Dynamic configuration
tcp:
routers:
Router-1:
rule: "ClientIP(`192.168.0.12`)"
entryPoints:
- "web"
service: service-1
priority: 2
Router-2:
rule: "ClientIP(`192.168.0.0/24`)"
entryPoints:
- "web"
priority: 1
service: service-2
```
```toml tab="File (TOML)"
## Dynamic configuration
[tcp.routers]
[tcp.routers.Router-1]
rule = "ClientIP(`192.168.0.12`)"
entryPoints = ["web"]
service = "service-1"
priority = 2
[tcp.routers.Router-2]
rule = "ClientIP(`192.168.0.0/24`)"
entryPoints = ["web"]
priority = 1
service = "service-2"
```
In this configuration, the priority is configured so that `Router-1` will handle requests from `192.168.0.12`.
### Middlewares

View file

@ -260,6 +260,8 @@ spec:
- name
type: object
type: array
priority:
type: integer
services:
items:
description: ServiceTCP defines an upstream to proxy traffic.

View file

@ -663,7 +663,7 @@ func (s *SimpleSuite) TestTCPRouterConfigErrors(c *check.C) {
c.Assert(err, checker.IsNil)
// router4 has an unsupported Rule
err = try.GetRequest("http://127.0.0.1:8080/api/tcp/routers/router4@file", 1000*time.Millisecond, try.BodyContains("unknown rule Host(`mydomain.com`)"))
err = try.GetRequest("http://127.0.0.1:8080/api/tcp/routers/router4@file", 1000*time.Millisecond, try.BodyContains("invalid rule: \\\"Host(`mydomain.com`)\\\""))
c.Assert(err, checker.IsNil)
}

View file

@ -52,6 +52,7 @@ type TCPRouter struct {
Middlewares []string `json:"middlewares,omitempty" toml:"middlewares,omitempty" yaml:"middlewares,omitempty" export:"true"`
Service string `json:"service,omitempty" toml:"service,omitempty" yaml:"service,omitempty" export:"true"`
Rule string `json:"rule,omitempty" toml:"rule,omitempty" yaml:"rule,omitempty"`
Priority int `json:"priority,omitempty" toml:"priority,omitempty,omitzero" yaml:"priority,omitempty" export:"true"`
TLS *RouterTCPTLSConfig `json:"tls,omitempty" toml:"tls,omitempty" yaml:"tls,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"`
}

View file

@ -176,11 +176,13 @@ func TestDecodeConfiguration(t *testing.T) {
"traefik.tcp.middlewares.Middleware0.ipwhitelist.sourcerange": "foobar, fiibar",
"traefik.tcp.middlewares.Middleware2.inflightconn.amount": "42",
"traefik.tcp.routers.Router0.rule": "foobar",
"traefik.tcp.routers.Router0.priority": "42",
"traefik.tcp.routers.Router0.entrypoints": "foobar, fiibar",
"traefik.tcp.routers.Router0.service": "foobar",
"traefik.tcp.routers.Router0.tls.passthrough": "false",
"traefik.tcp.routers.Router0.tls.options": "foo",
"traefik.tcp.routers.Router1.rule": "foobar",
"traefik.tcp.routers.Router1.priority": "42",
"traefik.tcp.routers.Router1.entrypoints": "foobar, fiibar",
"traefik.tcp.routers.Router1.service": "foobar",
"traefik.tcp.routers.Router1.tls.options": "foo",
@ -211,8 +213,9 @@ func TestDecodeConfiguration(t *testing.T) {
"foobar",
"fiibar",
},
Service: "foobar",
Rule: "foobar",
Service: "foobar",
Rule: "foobar",
Priority: 42,
TLS: &dynamic.RouterTCPTLSConfig{
Passthrough: false,
Options: "foo",
@ -223,8 +226,9 @@ func TestDecodeConfiguration(t *testing.T) {
"foobar",
"fiibar",
},
Service: "foobar",
Rule: "foobar",
Service: "foobar",
Rule: "foobar",
Priority: 42,
TLS: &dynamic.RouterTCPTLSConfig{
Passthrough: false,
Options: "foo",
@ -699,8 +703,9 @@ func TestEncodeConfiguration(t *testing.T) {
"foobar",
"fiibar",
},
Service: "foobar",
Rule: "foobar",
Service: "foobar",
Rule: "foobar",
Priority: 42,
TLS: &dynamic.RouterTCPTLSConfig{
Passthrough: false,
Options: "foo",
@ -711,8 +716,9 @@ func TestEncodeConfiguration(t *testing.T) {
"foobar",
"fiibar",
},
Service: "foobar",
Rule: "foobar",
Service: "foobar",
Rule: "foobar",
Priority: 42,
TLS: &dynamic.RouterTCPTLSConfig{
Passthrough: false,
Options: "foo",
@ -1333,11 +1339,13 @@ func TestEncodeConfiguration(t *testing.T) {
"traefik.TCP.Middlewares.Middleware0.IPWhiteList.SourceRange": "foobar, fiibar",
"traefik.TCP.Middlewares.Middleware2.InFlightConn.Amount": "42",
"traefik.TCP.Routers.Router0.Rule": "foobar",
"traefik.TCP.Routers.Router0.Priority": "42",
"traefik.TCP.Routers.Router0.EntryPoints": "foobar, fiibar",
"traefik.TCP.Routers.Router0.Service": "foobar",
"traefik.TCP.Routers.Router0.TLS.Passthrough": "false",
"traefik.TCP.Routers.Router0.TLS.Options": "foo",
"traefik.TCP.Routers.Router1.Rule": "foobar",
"traefik.TCP.Routers.Router1.Priority": "42",
"traefik.TCP.Routers.Router1.EntryPoints": "foobar, fiibar",
"traefik.TCP.Routers.Router1.Service": "foobar",
"traefik.TCP.Routers.Router1.TLS.Passthrough": "false",

View file

@ -1,4 +1,4 @@
package rules
package http
import (
"fmt"
@ -10,11 +10,14 @@ import (
"github.com/traefik/traefik/v2/pkg/ip"
"github.com/traefik/traefik/v2/pkg/log"
"github.com/traefik/traefik/v2/pkg/middlewares/requestdecorator"
"github.com/traefik/traefik/v2/pkg/rules"
"github.com/vulcand/predicate"
)
var funcs = map[string]func(*mux.Route, ...string) error{
"Host": host,
const hostMatcher = "Host"
var httpFuncs = map[string]func(*mux.Route, ...string) error{
hostMatcher: host,
"HostHeader": host,
"HostRegexp": hostRegexp,
"ClientIP": clientIP,
@ -26,33 +29,38 @@ var funcs = map[string]func(*mux.Route, ...string) error{
"Query": query,
}
// Router handle routing with rules.
type Router struct {
// Muxer handles routing with rules.
type Muxer struct {
*mux.Router
parser predicate.Parser
}
// NewRouter returns a new router instance.
func NewRouter() (*Router, error) {
parser, err := newParser()
// NewMuxer returns a new muxer instance.
func NewMuxer() (*Muxer, error) {
var matchers []string
for matcher := range httpFuncs {
matchers = append(matchers, matcher)
}
parser, err := rules.NewParser(matchers)
if err != nil {
return nil, err
}
return &Router{
return &Muxer{
Router: mux.NewRouter().SkipClean(true),
parser: parser,
}, nil
}
// AddRoute add a new route to the router.
func (r *Router) AddRoute(rule string, priority int, handler http.Handler) error {
func (r *Muxer) AddRoute(rule string, priority int, handler http.Handler) error {
parse, err := r.parser.Parse(rule)
if err != nil {
return fmt.Errorf("error while parsing rule %s: %w", rule, err)
}
buildTree, ok := parse.(treeBuilder)
buildTree, ok := parse.(rules.TreeBuilder)
if !ok {
return fmt.Errorf("error while parsing rule %s", rule)
}
@ -72,23 +80,40 @@ func (r *Router) AddRoute(rule string, priority int, handler http.Handler) error
return nil
}
type tree struct {
matcher string
not bool
value []string
ruleLeft *tree
ruleRight *tree
// ParseDomains extract domains from rule.
func ParseDomains(rule string) ([]string, error) {
var matchers []string
for matcher := range httpFuncs {
matchers = append(matchers, matcher)
}
parser, err := rules.NewParser(matchers)
if err != nil {
return nil, err
}
parse, err := parser.Parse(rule)
if err != nil {
return nil, err
}
buildTree, ok := parse.(rules.TreeBuilder)
if !ok {
return nil, fmt.Errorf("error while parsing rule %s", rule)
}
return buildTree().ParseMatchers([]string{hostMatcher}), nil
}
func path(route *mux.Route, paths ...string) error {
rt := route.Subrouter()
for _, path := range paths {
tmpRt := rt.Path(path)
if tmpRt.GetError() != nil {
return tmpRt.GetError()
if err := rt.Path(path).GetError(); err != nil {
return err
}
}
return nil
}
@ -96,11 +121,11 @@ func pathPrefix(route *mux.Route, paths ...string) error {
rt := route.Subrouter()
for _, path := range paths {
tmpRt := rt.PathPrefix(path)
if tmpRt.GetError() != nil {
return tmpRt.GetError()
if err := rt.PathPrefix(path).GetError(); err != nil {
return err
}
}
return nil
}
@ -220,33 +245,34 @@ func query(route *mux.Route, query ...string) error {
return route.GetError()
}
func addRuleOnRouter(router *mux.Router, rule *tree) error {
switch rule.matcher {
func addRuleOnRouter(router *mux.Router, rule *rules.Tree) error {
switch rule.Matcher {
case "and":
route := router.NewRoute()
err := addRuleOnRoute(route, rule.ruleLeft)
err := addRuleOnRoute(route, rule.RuleLeft)
if err != nil {
return err
}
return addRuleOnRoute(route, rule.ruleRight)
return addRuleOnRoute(route, rule.RuleRight)
case "or":
err := addRuleOnRouter(router, rule.ruleLeft)
err := addRuleOnRouter(router, rule.RuleLeft)
if err != nil {
return err
}
return addRuleOnRouter(router, rule.ruleRight)
return addRuleOnRouter(router, rule.RuleRight)
default:
err := checkRule(rule)
err := rules.CheckRule(rule)
if err != nil {
return err
}
if rule.not {
return not(funcs[rule.matcher])(router.NewRoute(), rule.value...)
if rule.Not {
return not(httpFuncs[rule.Matcher])(router.NewRoute(), rule.Value...)
}
return funcs[rule.matcher](router.NewRoute(), rule.value...)
return httpFuncs[rule.Matcher](router.NewRoute(), rule.Value...)
}
}
@ -264,48 +290,36 @@ func not(m func(*mux.Route, ...string) error) func(*mux.Route, ...string) error
}
}
func addRuleOnRoute(route *mux.Route, rule *tree) error {
switch rule.matcher {
func addRuleOnRoute(route *mux.Route, rule *rules.Tree) error {
switch rule.Matcher {
case "and":
err := addRuleOnRoute(route, rule.ruleLeft)
err := addRuleOnRoute(route, rule.RuleLeft)
if err != nil {
return err
}
return addRuleOnRoute(route, rule.ruleRight)
return addRuleOnRoute(route, rule.RuleRight)
case "or":
subRouter := route.Subrouter()
err := addRuleOnRouter(subRouter, rule.ruleLeft)
err := addRuleOnRouter(subRouter, rule.RuleLeft)
if err != nil {
return err
}
return addRuleOnRouter(subRouter, rule.ruleRight)
return addRuleOnRouter(subRouter, rule.RuleRight)
default:
err := checkRule(rule)
err := rules.CheckRule(rule)
if err != nil {
return err
}
if rule.not {
return not(funcs[rule.matcher])(route, rule.value...)
if rule.Not {
return not(httpFuncs[rule.Matcher])(route, rule.Value...)
}
return funcs[rule.matcher](route, rule.value...)
}
}
func checkRule(rule *tree) error {
if len(rule.value) == 0 {
return fmt.Errorf("no args for matcher %s", rule.matcher)
return httpFuncs[rule.Matcher](route, rule.Value...)
}
for _, v := range rule.value {
if len(v) == 0 {
return fmt.Errorf("empty args for matcher %s, %v", rule.matcher, rule.value)
}
}
return nil
}
// IsASCII checks if the given string contains only ASCII characters.

View file

@ -1,4 +1,4 @@
package rules
package http
import (
"net/http"
@ -635,10 +635,10 @@ func Test_addRoute(t *testing.T) {
t.Parallel()
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})
router, err := NewRouter()
muxer, err := NewMuxer()
require.NoError(t, err)
err = router.AddRoute(test.rule, 0, handler)
err = muxer.AddRoute(test.rule, 0, handler)
if test.expectedError {
require.Error(t, err)
} else {
@ -659,7 +659,7 @@ func Test_addRoute(t *testing.T) {
for key, value := range test.headers {
req.Header.Set(key, value)
}
reqHost.ServeHTTP(w, req, router.ServeHTTP)
reqHost.ServeHTTP(w, req, muxer.ServeHTTP)
results[calledURL] = w.Code
}
assert.Equal(t, test.expected, results)
@ -787,7 +787,7 @@ func Test_addRoutePriority(t *testing.T) {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
router, err := NewRouter()
muxer, err := NewMuxer()
require.NoError(t, err)
for _, route := range test.cases {
@ -796,16 +796,16 @@ func Test_addRoutePriority(t *testing.T) {
w.Header().Set("X-From", route.xFrom)
})
err := router.AddRoute(route.rule, route.priority, handler)
err := muxer.AddRoute(route.rule, route.priority, handler)
require.NoError(t, err, route.rule)
}
router.SortRoutes()
muxer.SortRoutes()
w := httptest.NewRecorder()
req := testhelpers.MustNewRequest(http.MethodGet, test.path, nil)
router.ServeHTTP(w, req)
muxer.ServeHTTP(w, req)
assert.Equal(t, test.expected, w.Header().Get("X-From"))
})
@ -900,44 +900,42 @@ func TestParseDomains(t *testing.T) {
errorExpected bool
}{
{
description: "Many host rules",
expression: "Host(`foo.bar`,`test.bar`)",
domain: []string{"foo.bar", "test.bar"},
errorExpected: false,
description: "Unknown rule",
expression: "Foobar(`foo.bar`,`test.bar`)",
errorExpected: true,
},
{
description: "Many host rules upper",
expression: "HOST(`foo.bar`,`test.bar`)",
domain: []string{"foo.bar", "test.bar"},
errorExpected: false,
description: "Several host rules",
expression: "Host(`foo.bar`,`test.bar`)",
domain: []string{"foo.bar", "test.bar"},
},
{
description: "Many host rules lower",
expression: "host(`foo.bar`,`test.bar`)",
domain: []string{"foo.bar", "test.bar"},
errorExpected: false,
description: "Several host rules upper",
expression: "HOST(`foo.bar`,`test.bar`)",
domain: []string{"foo.bar", "test.bar"},
},
{
description: "No host rule",
expression: "Path(`/test`)",
errorExpected: false,
description: "Several host rules lower",
expression: "host(`foo.bar`,`test.bar`)",
domain: []string{"foo.bar", "test.bar"},
},
{
description: "Host rule and another rule",
expression: "Host(`foo.bar`) && Path(`/test`)",
domain: []string{"foo.bar"},
errorExpected: false,
description: "No host rule",
expression: "Path(`/test`)",
},
{
description: "Host rule to trim and another rule",
expression: "Host(`Foo.Bar`) && Path(`/test`)",
domain: []string{"foo.bar"},
errorExpected: false,
description: "Host rule and another rule",
expression: "Host(`foo.bar`) && Path(`/test`)",
domain: []string{"foo.bar"},
},
{
description: "Host rule with no domain",
expression: "Host() && Path(`/test`)",
errorExpected: false,
description: "Host rule to trim and another rule",
expression: "Host(`Foo.Bar`) && Path(`/test`)",
domain: []string{"foo.bar"},
},
{
description: "Host rule with no domain",
expression: "Host() && Path(`/test`)",
},
}

328
pkg/muxer/tcp/mux.go Normal file
View file

@ -0,0 +1,328 @@
package tcp
import (
"errors"
"fmt"
"net"
"regexp"
"sort"
"strings"
"github.com/traefik/traefik/v2/pkg/ip"
"github.com/traefik/traefik/v2/pkg/log"
"github.com/traefik/traefik/v2/pkg/rules"
"github.com/traefik/traefik/v2/pkg/tcp"
"github.com/traefik/traefik/v2/pkg/types"
"github.com/vulcand/predicate"
)
var tcpFuncs = map[string]func(*matchersTree, ...string) error{
"HostSNI": hostSNI,
"ClientIP": clientIP,
}
// ParseHostSNI extracts the HostSNIs declared in a rule.
// This is a first naive implementation used in TCP routing.
func ParseHostSNI(rule string) ([]string, error) {
var matchers []string
for matcher := range tcpFuncs {
matchers = append(matchers, matcher)
}
parser, err := rules.NewParser(matchers)
if err != nil {
return nil, err
}
parse, err := parser.Parse(rule)
if err != nil {
return nil, err
}
buildTree, ok := parse.(rules.TreeBuilder)
if !ok {
return nil, fmt.Errorf("error while parsing rule %s", rule)
}
return buildTree().ParseMatchers([]string{"HostSNI"}), nil
}
// ConnData contains TCP connection metadata.
type ConnData struct {
serverName string
remoteIP string
}
// NewConnData builds a connData struct from the given parameters.
func NewConnData(serverName string, conn tcp.WriteCloser) (ConnData, error) {
remoteIP, _, err := net.SplitHostPort(conn.RemoteAddr().String())
if err != nil {
return ConnData{}, fmt.Errorf("error while parsing remote address %q: %w", conn.RemoteAddr().String(), err)
}
// as per https://datatracker.ietf.org/doc/html/rfc6066:
// > The hostname is represented as a byte string using ASCII encoding without a trailing dot.
// so there is no need to trim a potential trailing dot
serverName = types.CanonicalDomain(serverName)
return ConnData{
serverName: types.CanonicalDomain(serverName),
remoteIP: remoteIP,
}, nil
}
// Muxer defines a muxer that handles TCP routing with rules.
type Muxer struct {
routes []*route
parser predicate.Parser
}
// NewMuxer returns a TCP muxer.
func NewMuxer() (*Muxer, error) {
var matcherNames []string
for matcherName := range tcpFuncs {
matcherNames = append(matcherNames, matcherName)
}
parser, err := rules.NewParser(matcherNames)
if err != nil {
return nil, fmt.Errorf("error while creating rules parser: %w", err)
}
return &Muxer{parser: parser}, nil
}
// Match returns the handler of the first route matching the connection metadata.
func (m Muxer) Match(meta ConnData) tcp.Handler {
for _, route := range m.routes {
if route.matchers.match(meta) {
return route.handler
}
}
return nil
}
// AddRoute adds a new route, associated to the given handler, at the given
// priority, to the muxer.
func (m *Muxer) AddRoute(rule string, priority int, handler tcp.Handler) error {
// Special case for when the catchAll fallback is present.
// When no user-defined priority is found, the lowest computable priority minus one is used,
// in order to make the fallback the last to be evaluated.
if priority == 0 && rule == "HostSNI(`*`)" {
priority = -1
}
// Default value, which means the user has not set it, so we'll compute it.
if priority == 0 {
priority = len(rule)
}
parse, err := m.parser.Parse(rule)
if err != nil {
return fmt.Errorf("error while parsing rule %s: %w", rule, err)
}
buildTree, ok := parse.(rules.TreeBuilder)
if !ok {
return fmt.Errorf("error while parsing rule %s", rule)
}
var matchers matchersTree
err = addRule(&matchers, buildTree())
if err != nil {
return err
}
newRoute := &route{
handler: handler,
priority: priority,
matchers: matchers,
}
m.routes = append(m.routes, newRoute)
sort.Sort(routes(m.routes))
return nil
}
func addRule(tree *matchersTree, rule *rules.Tree) error {
switch rule.Matcher {
case "and", "or":
tree.operator = rule.Matcher
tree.left = &matchersTree{}
err := addRule(tree.left, rule.RuleLeft)
if err != nil {
return err
}
tree.right = &matchersTree{}
return addRule(tree.right, rule.RuleRight)
default:
err := rules.CheckRule(rule)
if err != nil {
return err
}
err = tcpFuncs[rule.Matcher](tree, rule.Value...)
if err != nil {
return err
}
if rule.Not {
matcherFunc := tree.matcher
tree.matcher = func(meta ConnData) bool {
return !matcherFunc(meta)
}
}
}
return nil
}
// HasRoutes returns whether the muxer has routes.
func (m *Muxer) HasRoutes() bool {
return len(m.routes) > 0
}
// routes implements sort.Interface.
type routes []*route
// Len implements sort.Interface.
func (r routes) Len() int { return len(r) }
// Swap implements sort.Interface.
func (r routes) Swap(i, j int) { r[i], r[j] = r[j], r[i] }
// Less implements sort.Interface.
func (r routes) Less(i, j int) bool { return r[i].priority > r[j].priority }
// route holds the matchers to match TCP route,
// and the handler that will serve the connection.
type route struct {
// matchers tree structure reflecting the rule.
matchers matchersTree
// handler responsible for handling the route.
handler tcp.Handler
// Used to disambiguate between two (or more) rules that would both match for a
// given request.
// Computed from the matching rule length, if not user-set.
priority int
}
// matcher is a matcher func used to match connection properties.
type matcher func(meta ConnData) bool
// matchersTree represents the matchers tree structure.
type matchersTree struct {
// If matcher is not nil, it means that this matcherTree is a leaf of the tree.
// It is therefore mutually exclusive with left and right.
matcher matcher
// operator to combine the evaluation of left and right leaves.
operator string
// Mutually exclusive with matcher.
left *matchersTree
right *matchersTree
}
func (m *matchersTree) match(meta ConnData) bool {
if m == nil {
// This should never happen as it should have been detected during parsing.
log.WithoutContext().Warnf("Rule matcher is nil")
return false
}
if m.matcher != nil {
return m.matcher(meta)
}
switch m.operator {
case "or":
return m.left.match(meta) || m.right.match(meta)
case "and":
return m.left.match(meta) && m.right.match(meta)
default:
// This should never happen as it should have been detected during parsing.
log.WithoutContext().Warnf("Invalid rule operator %s", m.operator)
return false
}
}
func clientIP(tree *matchersTree, clientIPs ...string) error {
checker, err := ip.NewChecker(clientIPs)
if err != nil {
return fmt.Errorf("could not initialize IP Checker for \"ClientIP\" matcher: %w", err)
}
tree.matcher = func(meta ConnData) bool {
if meta.remoteIP == "" {
return false
}
ok, err := checker.Contains(meta.remoteIP)
if err != nil {
log.WithoutContext().Warnf("\"ClientIP\" matcher: could not match remote address: %v", err)
return false
}
return ok
}
return nil
}
var almostFQDN = regexp.MustCompile(`^[[:alnum:]\.-]+$`)
// hostSNI checks if the SNI Host of the connection match the matcher host.
func hostSNI(tree *matchersTree, hosts ...string) error {
if len(hosts) == 0 {
return errors.New("empty value for \"HostSNI\" matcher is not allowed")
}
for i, host := range hosts {
// Special case to allow global wildcard
if host == "*" {
continue
}
if !almostFQDN.MatchString(host) {
return fmt.Errorf("invalid value for \"HostSNI\" matcher, %q is not a valid hostname", host)
}
hosts[i] = strings.ToLower(host)
}
tree.matcher = func(meta ConnData) bool {
// Since a HostSNI(`*`) rule has been provided as catchAll for non-TLS TCP,
// it allows matching with an empty serverName.
// Which is why we make sure to take that case into account before
// checking meta.serverName.
if hosts[0] == "*" {
return true
}
if meta.serverName == "" {
return false
}
for _, host := range hosts {
if host == "*" {
return true
}
if host == meta.serverName {
return true
}
// trim trailing period in case of FQDN
host = strings.TrimSuffix(host, ".")
if host == meta.serverName {
return true
}
}
return false
}
return nil
}

776
pkg/muxer/tcp/mux_test.go Normal file
View file

@ -0,0 +1,776 @@
package tcp
import (
"net"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/traefik/traefik/v2/pkg/tcp"
)
type fakeConn struct {
call map[string]int
remoteAddr net.Addr
}
func (f *fakeConn) Read(b []byte) (n int, err error) {
panic("implement me")
}
func (f *fakeConn) Write(b []byte) (n int, err error) {
f.call[string(b)]++
return len(b), nil
}
func (f *fakeConn) Close() error {
panic("implement me")
}
func (f *fakeConn) LocalAddr() net.Addr {
panic("implement me")
}
func (f *fakeConn) RemoteAddr() net.Addr {
return f.remoteAddr
}
func (f *fakeConn) SetDeadline(t time.Time) error {
panic("implement me")
}
func (f *fakeConn) SetReadDeadline(t time.Time) error {
panic("implement me")
}
func (f *fakeConn) SetWriteDeadline(t time.Time) error {
panic("implement me")
}
func (f *fakeConn) CloseWrite() error {
panic("implement me")
}
func Test_addTCPRoute(t *testing.T) {
testCases := []struct {
desc string
rule string
serverName string
remoteAddr string
routeErr bool
matchErr bool
}{
{
desc: "no tree",
routeErr: true,
},
{
desc: "Rule with no matcher",
rule: "rulewithnotmatcher",
routeErr: true,
},
{
desc: "Empty HostSNI rule",
rule: "HostSNI()",
serverName: "foobar",
routeErr: true,
},
{
desc: "Empty HostSNI rule",
rule: "HostSNI(``)",
serverName: "foobar",
routeErr: true,
},
{
desc: "Valid HostSNI rule matching",
rule: "HostSNI(`foobar`)",
serverName: "foobar",
},
{
desc: "Valid negative HostSNI rule matching",
rule: "!HostSNI(`bar`)",
serverName: "foobar",
},
{
desc: "Valid HostSNI rule matching with alternative case",
rule: "hostsni(`foobar`)",
serverName: "foobar",
},
{
desc: "Valid HostSNI rule matching with alternative case",
rule: "HOSTSNI(`foobar`)",
serverName: "foobar",
},
{
desc: "Valid HostSNI rule not matching",
rule: "HostSNI(`foobar`)",
serverName: "bar",
matchErr: true,
},
{
desc: "Valid negative HostSNI rule not matching",
rule: "!HostSNI(`bar`)",
serverName: "bar",
matchErr: true,
},
{
desc: "Empty ClientIP rule",
rule: "ClientIP()",
routeErr: true,
},
{
desc: "Empty ClientIP rule",
rule: "ClientIP(``)",
routeErr: true,
},
{
desc: "Invalid ClientIP",
rule: "ClientIP(`invalid`)",
routeErr: true,
},
{
desc: "Invalid remoteAddr",
rule: "ClientIP(`10.0.0.1`)",
remoteAddr: "not.an.IP:80",
matchErr: true,
},
{
desc: "Valid ClientIP rule matching",
rule: "ClientIP(`10.0.0.1`)",
remoteAddr: "10.0.0.1:80",
},
{
desc: "Valid negative ClientIP rule matching",
rule: "!ClientIP(`20.0.0.1`)",
remoteAddr: "10.0.0.1:80",
},
{
desc: "Valid ClientIP rule matching with alternative case",
rule: "clientip(`10.0.0.1`)",
remoteAddr: "10.0.0.1:80",
},
{
desc: "Valid ClientIP rule matching with alternative case",
rule: "CLIENTIP(`10.0.0.1`)",
remoteAddr: "10.0.0.1:80",
},
{
desc: "Valid ClientIP rule not matching",
rule: "ClientIP(`10.0.0.1`)",
remoteAddr: "10.0.0.2:80",
matchErr: true,
},
{
desc: "Valid negative ClientIP rule not matching",
rule: "!ClientIP(`10.0.0.2`)",
remoteAddr: "10.0.0.2:80",
matchErr: true,
},
{
desc: "Valid ClientIP rule matching IPv6",
rule: "ClientIP(`10::10`)",
remoteAddr: "[10::10]:80",
},
{
desc: "Valid negative ClientIP rule matching IPv6",
rule: "!ClientIP(`10::10`)",
remoteAddr: "[::1]:80",
},
{
desc: "Valid ClientIP rule not matching IPv6",
rule: "ClientIP(`10::10`)",
remoteAddr: "[::1]:80",
matchErr: true,
},
{
desc: "Valid ClientIP rule matching multiple IPs",
rule: "ClientIP(`10.0.0.1`, `10.0.0.0`)",
remoteAddr: "10.0.0.0:80",
},
{
desc: "Valid ClientIP rule matching CIDR",
rule: "ClientIP(`11.0.0.0/24`)",
remoteAddr: "11.0.0.0:80",
},
{
desc: "Valid ClientIP rule not matching CIDR",
rule: "ClientIP(`11.0.0.0/24`)",
remoteAddr: "10.0.0.0:80",
matchErr: true,
},
{
desc: "Valid ClientIP rule matching CIDR IPv6",
rule: "ClientIP(`11::/16`)",
remoteAddr: "[11::]:80",
},
{
desc: "Valid ClientIP rule not matching CIDR IPv6",
rule: "ClientIP(`11::/16`)",
remoteAddr: "[10::]:80",
matchErr: true,
},
{
desc: "Valid ClientIP rule matching multiple CIDR",
rule: "ClientIP(`11.0.0.0/16`, `10.0.0.0/16`)",
remoteAddr: "10.0.0.0:80",
},
{
desc: "Valid ClientIP rule not matching CIDR and matching IP",
rule: "ClientIP(`11.0.0.0/16`, `10.0.0.0`)",
remoteAddr: "10.0.0.0:80",
},
{
desc: "Valid ClientIP rule matching CIDR and not matching IP",
rule: "ClientIP(`11.0.0.0`, `10.0.0.0/16`)",
remoteAddr: "10.0.0.0:80",
},
{
desc: "Valid HostSNI and ClientIP rule matching",
rule: "HostSNI(`foobar`) && ClientIP(`10.0.0.1`)",
serverName: "foobar",
remoteAddr: "10.0.0.1:80",
},
{
desc: "Valid negative HostSNI and ClientIP rule matching",
rule: "!HostSNI(`bar`) && ClientIP(`10.0.0.1`)",
serverName: "foobar",
remoteAddr: "10.0.0.1:80",
},
{
desc: "Valid HostSNI and negative ClientIP rule matching",
rule: "HostSNI(`foobar`) && !ClientIP(`10.0.0.2`)",
serverName: "foobar",
remoteAddr: "10.0.0.1:80",
},
{
desc: "Valid negative HostSNI and negative ClientIP rule matching",
rule: "!HostSNI(`bar`) && !ClientIP(`10.0.0.2`)",
serverName: "foobar",
remoteAddr: "10.0.0.1:80",
},
{
desc: "Valid negative HostSNI or negative ClientIP rule matching",
rule: "!(HostSNI(`bar`) || ClientIP(`10.0.0.2`))",
serverName: "foobar",
remoteAddr: "10.0.0.1:80",
},
{
desc: "Valid negative HostSNI and negative ClientIP rule matching",
rule: "!(HostSNI(`bar`) && ClientIP(`10.0.0.2`))",
serverName: "foobar",
remoteAddr: "10.0.0.2:80",
},
{
desc: "Valid negative HostSNI and negative ClientIP rule matching",
rule: "!(HostSNI(`bar`) && ClientIP(`10.0.0.2`))",
serverName: "bar",
remoteAddr: "10.0.0.1:80",
},
{
desc: "Valid negative HostSNI and negative ClientIP rule matching",
rule: "!(HostSNI(`bar`) && ClientIP(`10.0.0.2`))",
serverName: "bar",
remoteAddr: "10.0.0.2:80",
matchErr: true,
},
{
desc: "Valid negative HostSNI and negative ClientIP rule matching",
rule: "!(HostSNI(`bar`) && ClientIP(`10.0.0.2`))",
serverName: "foobar",
remoteAddr: "10.0.0.1:80",
},
{
desc: "Valid HostSNI and ClientIP rule not matching",
rule: "HostSNI(`foobar`) && ClientIP(`10.0.0.1`)",
serverName: "bar",
remoteAddr: "10.0.0.1:80",
matchErr: true,
},
{
desc: "Valid HostSNI and ClientIP rule not matching",
rule: "HostSNI(`foobar`) && ClientIP(`10.0.0.1`)",
serverName: "foobar",
remoteAddr: "10.0.0.2:80",
matchErr: true,
},
{
desc: "Valid HostSNI or ClientIP rule matching",
rule: "HostSNI(`foobar`) || ClientIP(`10.0.0.1`)",
serverName: "foobar",
remoteAddr: "10.0.0.1:80",
},
{
desc: "Valid HostSNI or ClientIP rule matching",
rule: "HostSNI(`foobar`) || ClientIP(`10.0.0.1`)",
serverName: "bar",
remoteAddr: "10.0.0.1:80",
},
{
desc: "Valid HostSNI or ClientIP rule matching",
rule: "HostSNI(`foobar`) || ClientIP(`10.0.0.1`)",
serverName: "foobar",
remoteAddr: "10.0.0.2:80",
},
{
desc: "Valid HostSNI or ClientIP rule not matching",
rule: "HostSNI(`foobar`) || ClientIP(`10.0.0.1`)",
serverName: "bar",
remoteAddr: "10.0.0.2:80",
matchErr: true,
},
{
desc: "Valid HostSNI x 3 OR rule matching",
rule: "HostSNI(`foobar`) || HostSNI(`foo`) || HostSNI(`bar`)",
serverName: "foobar",
},
{
desc: "Valid HostSNI x 3 OR rule not matching",
rule: "HostSNI(`foobar`) || HostSNI(`foo`) || HostSNI(`bar`)",
serverName: "baz",
matchErr: true,
},
{
desc: "Valid HostSNI and ClientIP Combined rule matching",
rule: "HostSNI(`foobar`) || HostSNI(`bar`) && ClientIP(`10.0.0.1`)",
serverName: "foobar",
remoteAddr: "10.0.0.2:80",
},
{
desc: "Valid HostSNI and ClientIP Combined rule matching",
rule: "HostSNI(`foobar`) || HostSNI(`bar`) && ClientIP(`10.0.0.1`)",
serverName: "bar",
remoteAddr: "10.0.0.1:80",
},
{
desc: "Valid HostSNI and ClientIP Combined rule not matching",
rule: "HostSNI(`foobar`) || HostSNI(`bar`) && ClientIP(`10.0.0.1`)",
serverName: "bar",
remoteAddr: "10.0.0.2:80",
matchErr: true,
},
{
desc: "Valid HostSNI and ClientIP Combined rule not matching",
rule: "HostSNI(`foobar`) || HostSNI(`bar`) && ClientIP(`10.0.0.1`)",
serverName: "baz",
remoteAddr: "10.0.0.1:80",
matchErr: true,
},
{
desc: "Valid HostSNI and ClientIP complex combined rule matching",
rule: "(HostSNI(`foobar`) || HostSNI(`bar`)) && (ClientIP(`10.0.0.1`) || ClientIP(`10.0.0.2`))",
serverName: "bar",
remoteAddr: "10.0.0.1:80",
},
{
desc: "Valid HostSNI and ClientIP complex combined rule not matching",
rule: "(HostSNI(`foobar`) || HostSNI(`bar`)) && (ClientIP(`10.0.0.1`) || ClientIP(`10.0.0.2`))",
serverName: "baz",
remoteAddr: "10.0.0.1:80",
matchErr: true,
},
{
desc: "Valid HostSNI and ClientIP complex combined rule not matching",
rule: "(HostSNI(`foobar`) || HostSNI(`bar`)) && (ClientIP(`10.0.0.1`) || ClientIP(`10.0.0.2`))",
serverName: "bar",
remoteAddr: "10.0.0.3:80",
matchErr: true,
},
{
desc: "Valid HostSNI and ClientIP more complex (but absurd) combined rule matching",
rule: "(HostSNI(`foobar`) || (HostSNI(`bar`) && !HostSNI(`foobar`))) && ((ClientIP(`10.0.0.1`) && !ClientIP(`10.0.0.2`)) || ClientIP(`10.0.0.2`)) ",
serverName: "bar",
remoteAddr: "10.0.0.1:80",
},
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
msg := "BYTES"
handler := tcp.HandlerFunc(func(conn tcp.WriteCloser) {
_, err := conn.Write([]byte(msg))
require.NoError(t, err)
})
router, err := NewMuxer()
require.NoError(t, err)
err = router.AddRoute(test.rule, 0, handler)
if test.routeErr {
require.Error(t, err)
return
}
require.NoError(t, err)
addr := "0.0.0.0:0"
if test.remoteAddr != "" {
addr = test.remoteAddr
}
conn := &fakeConn{
call: map[string]int{},
remoteAddr: fakeAddr{addr: addr},
}
connData, err := NewConnData(test.serverName, conn)
require.NoError(t, err)
matchingHandler := router.Match(connData)
if test.matchErr {
require.Nil(t, matchingHandler)
return
}
require.NotNil(t, matchingHandler)
matchingHandler.ServeTCP(conn)
n, ok := conn.call[msg]
assert.Equal(t, n, 1)
assert.True(t, ok)
})
}
}
type fakeAddr struct {
addr string
}
func (f fakeAddr) String() string {
return f.addr
}
func (f fakeAddr) Network() string {
panic("Implement me")
}
func TestParseHostSNI(t *testing.T) {
testCases := []struct {
description string
expression string
domain []string
errorExpected bool
}{
{
description: "Unknown rule",
expression: "Foobar(`foo.bar`,`test.bar`)",
errorExpected: true,
},
{
description: "Many hostSNI rules",
expression: "HostSNI(`foo.bar`,`test.bar`)",
domain: []string{"foo.bar", "test.bar"},
},
{
description: "Many hostSNI rules upper",
expression: "HOSTSNI(`foo.bar`,`test.bar`)",
domain: []string{"foo.bar", "test.bar"},
},
{
description: "Many hostSNI rules lower",
expression: "hostsni(`foo.bar`,`test.bar`)",
domain: []string{"foo.bar", "test.bar"},
},
{
description: "No hostSNI rule",
expression: "ClientIP(`10.1`)",
},
{
description: "HostSNI rule and another rule",
expression: "HostSNI(`foo.bar`) && ClientIP(`10.1`)",
domain: []string{"foo.bar"},
},
{
description: "HostSNI rule to lower and another rule",
expression: "HostSNI(`Foo.Bar`) && ClientIP(`10.1`)",
domain: []string{"foo.bar"},
},
{
description: "HostSNI rule with no domain",
expression: "HostSNI() && ClientIP(`10.1`)",
},
}
for _, test := range testCases {
test := test
t.Run(test.expression, func(t *testing.T) {
t.Parallel()
domains, err := ParseHostSNI(test.expression)
if test.errorExpected {
require.Errorf(t, err, "unable to parse correctly the domains in the HostSNI rule from %q", test.expression)
} else {
require.NoError(t, err, "%s: Error while parsing domain.", test.expression)
}
assert.EqualValues(t, test.domain, domains, "%s: Error parsing domains from expression.", test.expression)
})
}
}
func Test_HostSNI(t *testing.T) {
testCases := []struct {
desc string
ruleHosts []string
serverName string
buildErr bool
matchErr bool
}{
{
desc: "Empty",
buildErr: true,
},
{
desc: "Non ASCII host",
ruleHosts: []string{"héhé"},
buildErr: true,
},
{
desc: "Not Matching hosts",
ruleHosts: []string{"foobar"},
serverName: "bar",
matchErr: true,
},
{
desc: "Matching globing host `*`",
ruleHosts: []string{"*"},
serverName: "foobar",
},
{
desc: "Matching globing host `*` and empty serverName",
ruleHosts: []string{"*"},
serverName: "",
},
{
desc: "Matching globing host `*` and another non matching host",
ruleHosts: []string{"foo", "*"},
serverName: "bar",
},
{
desc: "Matching globing host `*` and another non matching host, and empty servername",
ruleHosts: []string{"foo", "*"},
serverName: "",
matchErr: true,
},
{
desc: "Not Matching globing host with subdomain",
ruleHosts: []string{"*.bar"},
buildErr: true,
},
{
desc: "Not Matching host with trailing dot with ",
ruleHosts: []string{"foobar."},
serverName: "foobar.",
},
{
desc: "Matching host with trailing dot",
ruleHosts: []string{"foobar."},
serverName: "foobar",
},
{
desc: "Matching hosts",
ruleHosts: []string{"foobar"},
serverName: "foobar",
},
{
desc: "Matching hosts with subdomains",
ruleHosts: []string{"foo.bar"},
serverName: "foo.bar",
},
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
matcherTree := &matchersTree{}
err := hostSNI(matcherTree, test.ruleHosts...)
if test.buildErr {
require.Error(t, err)
return
}
require.NoError(t, err)
meta := ConnData{
serverName: test.serverName,
}
assert.Equal(t, test.matchErr, !matcherTree.match(meta))
})
}
}
func Test_ClientIP(t *testing.T) {
testCases := []struct {
desc string
ruleCIDRs []string
remoteIP string
buildErr bool
matchErr bool
}{
{
desc: "Empty",
buildErr: true,
},
{
desc: "Malformed CIDR",
ruleCIDRs: []string{"héhé"},
buildErr: true,
},
{
desc: "Not matching empty remote IP",
ruleCIDRs: []string{"20.20.20.20"},
matchErr: true,
},
{
desc: "Not matching IP",
ruleCIDRs: []string{"20.20.20.20"},
remoteIP: "10.10.10.10",
matchErr: true,
},
{
desc: "Matching IP",
ruleCIDRs: []string{"10.10.10.10"},
remoteIP: "10.10.10.10",
},
{
desc: "Not matching multiple IPs",
ruleCIDRs: []string{"20.20.20.20", "30.30.30.30"},
remoteIP: "10.10.10.10",
matchErr: true,
},
{
desc: "Matching multiple IPs",
ruleCIDRs: []string{"10.10.10.10", "20.20.20.20", "30.30.30.30"},
remoteIP: "20.20.20.20",
},
{
desc: "Not matching CIDR",
ruleCIDRs: []string{"20.0.0.0/24"},
remoteIP: "10.10.10.10",
matchErr: true,
},
{
desc: "Matching CIDR",
ruleCIDRs: []string{"20.0.0.0/8"},
remoteIP: "20.10.10.10",
},
{
desc: "Not matching multiple CIDRs",
ruleCIDRs: []string{"10.0.0.0/24", "20.0.0.0/24"},
remoteIP: "10.10.10.10",
matchErr: true,
},
{
desc: "Matching multiple CIDRs",
ruleCIDRs: []string{"10.0.0.0/8", "20.0.0.0/8"},
remoteIP: "20.10.10.10",
},
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
matchersTree := &matchersTree{}
err := clientIP(matchersTree, test.ruleCIDRs...)
if test.buildErr {
require.Error(t, err)
return
}
require.NoError(t, err)
meta := ConnData{
remoteIP: test.remoteIP,
}
assert.Equal(t, test.matchErr, !matchersTree.match(meta))
})
}
}
func Test_Priority(t *testing.T) {
testCases := []struct {
desc string
rules map[string]int
serverName string
remoteIP string
expectedRule string
}{
{
desc: "One matching rule, calculated priority",
rules: map[string]int{
"HostSNI(`bar`)": 0,
"HostSNI(`foobar`)": 0,
},
expectedRule: "HostSNI(`bar`)",
serverName: "bar",
},
{
desc: "One matching rule, custom priority",
rules: map[string]int{
"HostSNI(`foobar`)": 0,
"HostSNI(`bar`)": 10000,
},
expectedRule: "HostSNI(`foobar`)",
serverName: "foobar",
},
{
desc: "Two matching rules, calculated priority",
rules: map[string]int{
"HostSNI(`foobar`)": 0,
"HostSNI(`foobar`, `bar`)": 0,
},
expectedRule: "HostSNI(`foobar`, `bar`)",
serverName: "foobar",
},
{
desc: "Two matching rules, custom priority",
rules: map[string]int{
"HostSNI(`foobar`)": 10000,
"HostSNI(`foobar`, `bar`)": 0,
},
expectedRule: "HostSNI(`foobar`)",
serverName: "foobar",
},
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
muxer, err := NewMuxer()
require.NoError(t, err)
matchedRule := ""
for rule, priority := range test.rules {
rule := rule
err := muxer.AddRoute(rule, priority, tcp.HandlerFunc(func(conn tcp.WriteCloser) {
matchedRule = rule
}))
require.NoError(t, err)
}
handler := muxer.Match(ConnData{
serverName: test.serverName,
remoteIP: test.remoteIP,
})
require.NotNil(t, handler)
handler.ServeTCP(nil)
assert.Equal(t, test.expectedRule, matchedRule)
})
}
}

View file

@ -21,7 +21,8 @@ import (
ptypes "github.com/traefik/paerser/types"
"github.com/traefik/traefik/v2/pkg/config/dynamic"
"github.com/traefik/traefik/v2/pkg/log"
"github.com/traefik/traefik/v2/pkg/rules"
httpmuxer "github.com/traefik/traefik/v2/pkg/muxer/http"
tcpmuxer "github.com/traefik/traefik/v2/pkg/muxer/tcp"
"github.com/traefik/traefik/v2/pkg/safe"
traefiktls "github.com/traefik/traefik/v2/pkg/tls"
"github.com/traefik/traefik/v2/pkg/types"
@ -423,7 +424,7 @@ func (p *Provider) watchNewDomains(ctx context.Context) {
})
}
} else {
domains, err := rules.ParseHostSNI(route.Rule)
domains, err := tcpmuxer.ParseHostSNI(route.Rule)
if err != nil {
logger.Errorf("Error parsing domains in provider ACME: %v", err)
continue
@ -452,7 +453,7 @@ func (p *Provider) watchNewDomains(ctx context.Context) {
})
}
} else {
domains, err := rules.ParseDomains(route.Rule)
domains, err := httpmuxer.ParseDomains(route.Rule)
if err != nil {
log.FromContext(ctxRouter).Errorf("Error parsing domains in provider ACME: %v", err)
continue

View file

@ -98,6 +98,7 @@ func (p *Provider) loadIngressRouteTCPConfiguration(ctx context.Context, client
EntryPoints: ingressRouteTCP.Spec.EntryPoints,
Middlewares: mds,
Rule: route.Match,
Priority: route.Priority,
Service: serviceName,
}

View file

@ -17,6 +17,7 @@ type IngressRouteTCPSpec struct {
// RouteTCP contains the set of routes.
type RouteTCP struct {
Match string `json:"match"`
Priority int `json:"priority,omitempty"`
Services []ServiceTCP `json:"services,omitempty"`
// Middlewares contains references to MiddlewareTCP resources.
Middlewares []ObjectReference `json:"middlewares,omitempty"`

View file

@ -1,7 +1,7 @@
package rules
import (
"errors"
"fmt"
"strings"
"github.com/vulcand/predicate"
@ -12,121 +12,29 @@ const (
or = "or"
)
type treeBuilder func() *tree
// TreeBuilder defines the type for a Tree builder.
type TreeBuilder func() *Tree
// ParseDomains extract domains from rule.
func ParseDomains(rule string) ([]string, error) {
parser, err := newParser()
if err != nil {
return nil, err
}
parse, err := parser.Parse(rule)
if err != nil {
return nil, err
}
buildTree, ok := parse.(treeBuilder)
if !ok {
return nil, errors.New("cannot parse")
}
return lower(parseDomain(buildTree())), nil
// Tree represents the rules' tree structure.
type Tree struct {
Matcher string
Not bool
Value []string
RuleLeft *Tree
RuleRight *Tree
}
// ParseHostSNI extracts the HostSNIs declared in a rule.
// This is a first naive implementation used in TCP routing.
func ParseHostSNI(rule string) ([]string, error) {
parser, err := newTCPParser()
if err != nil {
return nil, err
}
parse, err := parser.Parse(rule)
if err != nil {
return nil, err
}
buildTree, ok := parse.(treeBuilder)
if !ok {
return nil, errors.New("cannot parse")
}
return lower(parseDomain(buildTree())), nil
}
func lower(slice []string) []string {
var lowerStrings []string
for _, value := range slice {
lowerStrings = append(lowerStrings, strings.ToLower(value))
}
return lowerStrings
}
func parseDomain(tree *tree) []string {
switch tree.matcher {
case and, or:
return append(parseDomain(tree.ruleLeft), parseDomain(tree.ruleRight)...)
case "Host", "HostSNI":
return tree.value
default:
return nil
}
}
func andFunc(left, right treeBuilder) treeBuilder {
return func() *tree {
return &tree{
matcher: and,
ruleLeft: left(),
ruleRight: right(),
}
}
}
func orFunc(left, right treeBuilder) treeBuilder {
return func() *tree {
return &tree{
matcher: or,
ruleLeft: left(),
ruleRight: right(),
}
}
}
func invert(t *tree) *tree {
switch t.matcher {
case or:
t.matcher = and
t.ruleLeft = invert(t.ruleLeft)
t.ruleRight = invert(t.ruleRight)
case and:
t.matcher = or
t.ruleLeft = invert(t.ruleLeft)
t.ruleRight = invert(t.ruleRight)
default:
t.not = !t.not
}
return t
}
func notFunc(elem treeBuilder) treeBuilder {
return func() *tree {
return invert(elem())
}
}
func newParser() (predicate.Parser, error) {
// NewParser constructs a parser for the given matchers.
func NewParser(matchers []string) (predicate.Parser, error) {
parserFuncs := make(map[string]interface{})
for matcherName := range funcs {
for _, matcherName := range matchers {
matcherName := matcherName
fn := func(value ...string) treeBuilder {
return func() *tree {
return &tree{
matcher: matcherName,
value: value,
fn := func(value ...string) TreeBuilder {
return func() *Tree {
return &Tree{
Matcher: matcherName,
Value: value,
}
}
}
@ -146,28 +54,85 @@ func newParser() (predicate.Parser, error) {
})
}
func newTCPParser() (predicate.Parser, error) {
parserFuncs := make(map[string]interface{})
// FIXME quircky way of waiting for new rules
matcherName := "HostSNI"
fn := func(value ...string) treeBuilder {
return func() *tree {
return &tree{
matcher: matcherName,
value: value,
}
func andFunc(left, right TreeBuilder) TreeBuilder {
return func() *Tree {
return &Tree{
Matcher: and,
RuleLeft: left(),
RuleRight: right(),
}
}
parserFuncs[matcherName] = fn
parserFuncs[strings.ToLower(matcherName)] = fn
parserFuncs[strings.ToUpper(matcherName)] = fn
parserFuncs[strings.Title(strings.ToLower(matcherName))] = fn
return predicate.NewParser(predicate.Def{
Operators: predicate.Operators{
OR: orFunc,
},
Functions: parserFuncs,
})
}
func orFunc(left, right TreeBuilder) TreeBuilder {
return func() *Tree {
return &Tree{
Matcher: or,
RuleLeft: left(),
RuleRight: right(),
}
}
}
func invert(t *Tree) *Tree {
switch t.Matcher {
case or:
t.Matcher = and
t.RuleLeft = invert(t.RuleLeft)
t.RuleRight = invert(t.RuleRight)
case and:
t.Matcher = or
t.RuleLeft = invert(t.RuleLeft)
t.RuleRight = invert(t.RuleRight)
default:
t.Not = !t.Not
}
return t
}
func notFunc(elem TreeBuilder) TreeBuilder {
return func() *Tree {
return invert(elem())
}
}
// ParseMatchers returns the subset of matchers in the Tree matching the given matchers.
func (tree *Tree) ParseMatchers(matchers []string) []string {
switch tree.Matcher {
case and, or:
return append(tree.RuleLeft.ParseMatchers(matchers), tree.RuleRight.ParseMatchers(matchers)...)
default:
for _, matcher := range matchers {
if tree.Matcher == matcher {
return lower(tree.Value)
}
}
return nil
}
}
// CheckRule validates the given rule.
func CheckRule(rule *Tree) error {
if len(rule.Value) == 0 {
return fmt.Errorf("no args for matcher %s", rule.Matcher)
}
for _, v := range rule.Value {
if len(v) == 0 {
return fmt.Errorf("empty args for matcher %s, %v", rule.Matcher, rule.Value)
}
}
return nil
}
func lower(slice []string) []string {
var lowerStrings []string
for _, value := range slice {
lowerStrings = append(lowerStrings, strings.ToLower(value))
}
return lowerStrings
}

301
pkg/rules/parser_test.go Normal file
View file

@ -0,0 +1,301 @@
package rules
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// testTree = Tree + CheckErr
type testTree struct {
Matcher string
Not bool
Value []string
RuleLeft *testTree
RuleRight *testTree
// CheckErr allow knowing if a Tree has a rule error.
CheckErr bool
}
func TestRuleMatch(t *testing.T) {
matchers := []string{"m"}
testCases := []struct {
desc string
rule string
tree testTree
matchers []string
values []string
expectParseErr bool
}{
{
desc: "No rule",
rule: "",
expectParseErr: true,
},
{
desc: "No matcher in rule",
rule: "m",
expectParseErr: true,
},
{
desc: "No value in rule",
rule: "m()",
tree: testTree{
Matcher: "m",
Value: []string{},
CheckErr: true,
},
},
{
desc: "Empty value in rule",
rule: "m(``)",
tree: testTree{
Matcher: "m",
Value: []string{""},
CheckErr: true,
},
matchers: []string{"m"},
values: []string{""},
},
{
desc: "One value in rule with and",
rule: "m(`1`) &&",
expectParseErr: true,
},
{
desc: "One value in rule with or",
rule: "m(`1`) ||",
expectParseErr: true,
},
{
desc: "One value in rule with missing back tick",
rule: "m(`1)",
expectParseErr: true,
},
{
desc: "One value in rule with missing opening parenthesis",
rule: "m(`1`))",
expectParseErr: true,
},
{
desc: "One value in rule with missing closing parenthesis",
rule: "(m(`1`)",
expectParseErr: true,
},
{
desc: "One value in rule",
rule: "m(`1`)",
tree: testTree{
Matcher: "m",
Value: []string{"1"},
},
matchers: []string{"m"},
values: []string{"1"},
},
{
desc: "One value in rule with superfluous parenthesis",
rule: "(m(`1`))",
tree: testTree{
Matcher: "m",
Value: []string{"1"},
},
matchers: []string{"m"},
values: []string{"1"},
},
{
desc: "Rule with CAPS matcher",
rule: "M(`1`)",
tree: testTree{
Matcher: "m",
Value: []string{"1"},
},
matchers: []string{"m"},
values: []string{"1"},
},
{
desc: "Invalid matcher in rule",
rule: "w(`1`)",
expectParseErr: true,
},
{
desc: "Invalid matchers",
rule: "m(`1`)",
tree: testTree{
Matcher: "m",
Value: []string{"1"},
},
matchers: []string{"not-m"},
},
{
desc: "Two value in rule",
rule: "m(`1`, `2`)",
tree: testTree{
Matcher: "m",
Value: []string{"1", "2"},
},
matchers: []string{"m"},
values: []string{"1", "2"},
},
{
desc: "Not one value in rule",
rule: "!m(`1`)",
tree: testTree{
Matcher: "m",
Not: true,
Value: []string{"1"},
},
matchers: []string{"m"},
values: []string{"1"},
},
{
desc: "Two value in rule with and",
rule: "m(`1`) && m(`2`)",
tree: testTree{
Matcher: "and",
CheckErr: true,
RuleLeft: &testTree{
Matcher: "m",
Value: []string{"1"},
},
RuleRight: &testTree{
Matcher: "m",
Value: []string{"2"},
},
},
matchers: []string{"m"},
values: []string{"1", "2"},
},
{
desc: "Two value in rule with or",
rule: "m(`1`) || m(`2`)",
tree: testTree{
Matcher: "or",
CheckErr: true,
RuleLeft: &testTree{
Matcher: "m",
Value: []string{"1"},
},
RuleRight: &testTree{
Matcher: "m",
Value: []string{"2"},
},
},
matchers: []string{"m"},
values: []string{"1", "2"},
},
{
desc: "Two value in rule with and negated",
rule: "!(m(`1`) && m(`2`))",
tree: testTree{
Matcher: "or",
CheckErr: true,
RuleLeft: &testTree{
Matcher: "m",
Not: true,
Value: []string{"1"},
},
RuleRight: &testTree{
Matcher: "m",
Not: true,
Value: []string{"2"},
},
},
matchers: []string{"m"},
values: []string{"1", "2"},
},
{
desc: "Two value in rule with or negated",
rule: "!(m(`1`) || m(`2`))",
tree: testTree{
Matcher: "and",
CheckErr: true,
RuleLeft: &testTree{
Matcher: "m",
Not: true,
Value: []string{"1"},
},
RuleRight: &testTree{
Matcher: "m",
Not: true,
Value: []string{"2"},
},
},
matchers: []string{"m"},
values: []string{"1", "2"},
},
{
desc: "No value in rule",
rule: "m(`1`) && m()",
tree: testTree{
Matcher: "and",
CheckErr: true,
RuleLeft: &testTree{
Matcher: "m",
Value: []string{"1"},
},
RuleRight: &testTree{
Matcher: "m",
Value: []string{},
CheckErr: true,
},
},
matchers: []string{"m"},
values: []string{"1"},
},
}
parser, err := NewParser(matchers)
require.NoError(t, err)
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
parse, err := parser.Parse(test.rule)
if test.expectParseErr {
require.Error(t, err)
return
}
require.NoError(t, err)
treeBuilder, ok := parse.(TreeBuilder)
require.True(t, ok)
tree := treeBuilder()
checkEquivalence(t, &test.tree, tree)
assert.Equal(t, test.values, tree.ParseMatchers(test.matchers))
})
}
}
func checkEquivalence(t *testing.T, expected *testTree, actual *Tree) {
t.Helper()
if actual == nil {
return
}
if actual.RuleLeft != nil {
checkEquivalence(t, expected.RuleLeft, actual.RuleLeft)
}
if actual.RuleRight != nil {
checkEquivalence(t, expected.RuleRight, actual.RuleRight)
}
assert.Equal(t, expected.Matcher, actual.Matcher)
assert.Equal(t, expected.Not, actual.Not)
assert.Equal(t, expected.Value, actual.Value)
t.Logf("%+v -> %v", actual, CheckRule(actual))
if expected.CheckErr {
assert.Error(t, CheckRule(actual))
} else {
assert.NoError(t, CheckRule(actual))
}
}

View file

@ -13,7 +13,7 @@ import (
metricsMiddle "github.com/traefik/traefik/v2/pkg/middlewares/metrics"
"github.com/traefik/traefik/v2/pkg/middlewares/recovery"
"github.com/traefik/traefik/v2/pkg/middlewares/tracing"
"github.com/traefik/traefik/v2/pkg/rules"
httpmuxer "github.com/traefik/traefik/v2/pkg/muxer/http"
"github.com/traefik/traefik/v2/pkg/server/middleware"
"github.com/traefik/traefik/v2/pkg/server/provider"
)
@ -102,7 +102,7 @@ func (m *Manager) BuildHandlers(rootCtx context.Context, entryPoints []string, t
}
func (m *Manager) buildEntryPointHandler(ctx context.Context, configs map[string]*runtime.RouterInfo) (http.Handler, error) {
router, err := rules.NewRouter()
muxer, err := httpmuxer.NewMuxer()
if err != nil {
return nil, err
}
@ -118,7 +118,7 @@ func (m *Manager) buildEntryPointHandler(ctx context.Context, configs map[string
continue
}
err = router.AddRoute(routerConfig.Rule, routerConfig.Priority, handler)
err = muxer.AddRoute(routerConfig.Rule, routerConfig.Priority, handler)
if err != nil {
routerConfig.AddError(err, true)
logger.Error(err)
@ -126,14 +126,14 @@ func (m *Manager) buildEntryPointHandler(ctx context.Context, configs map[string
}
}
router.SortRoutes()
muxer.SortRoutes()
chain := alice.New()
chain = chain.Append(func(next http.Handler) (http.Handler, error) {
return recovery.New(ctx, next)
})
return chain.Then(router)
return chain.Then(muxer)
}
func (m *Manager) buildRouterHandler(ctx context.Context, routerName string, routerConfig *runtime.RouterInfo) (http.Handler, error) {

View file

@ -0,0 +1,365 @@
package tcp
import (
"context"
"crypto/tls"
"errors"
"fmt"
"net/http"
"github.com/traefik/traefik/v2/pkg/config/runtime"
"github.com/traefik/traefik/v2/pkg/log"
"github.com/traefik/traefik/v2/pkg/middlewares/snicheck"
httpmuxer "github.com/traefik/traefik/v2/pkg/muxer/http"
tcpmuxer "github.com/traefik/traefik/v2/pkg/muxer/tcp"
"github.com/traefik/traefik/v2/pkg/server/provider"
tcpservice "github.com/traefik/traefik/v2/pkg/server/service/tcp"
"github.com/traefik/traefik/v2/pkg/tcp"
traefiktls "github.com/traefik/traefik/v2/pkg/tls"
)
type middlewareBuilder interface {
BuildChain(ctx context.Context, names []string) *tcp.Chain
}
// NewManager Creates a new Manager.
func NewManager(conf *runtime.Configuration,
serviceManager *tcpservice.Manager,
middlewaresBuilder middlewareBuilder,
httpHandlers map[string]http.Handler,
httpsHandlers map[string]http.Handler,
tlsManager *traefiktls.Manager,
) *Manager {
return &Manager{
serviceManager: serviceManager,
middlewaresBuilder: middlewaresBuilder,
httpHandlers: httpHandlers,
httpsHandlers: httpsHandlers,
tlsManager: tlsManager,
conf: conf,
}
}
// Manager is a route/router manager.
type Manager struct {
serviceManager *tcpservice.Manager
middlewaresBuilder middlewareBuilder
httpHandlers map[string]http.Handler
httpsHandlers map[string]http.Handler
tlsManager *traefiktls.Manager
conf *runtime.Configuration
}
func (m *Manager) getTCPRouters(ctx context.Context, entryPoints []string) map[string]map[string]*runtime.TCPRouterInfo {
if m.conf != nil {
return m.conf.GetTCPRoutersByEntryPoints(ctx, entryPoints)
}
return make(map[string]map[string]*runtime.TCPRouterInfo)
}
func (m *Manager) getHTTPRouters(ctx context.Context, entryPoints []string, tls bool) map[string]map[string]*runtime.RouterInfo {
if m.conf != nil {
return m.conf.GetRoutersByEntryPoints(ctx, entryPoints, tls)
}
return make(map[string]map[string]*runtime.RouterInfo)
}
// BuildHandlers builds the handlers for the given entrypoints.
func (m *Manager) BuildHandlers(rootCtx context.Context, entryPoints []string) map[string]*Router {
entryPointsRouters := m.getTCPRouters(rootCtx, entryPoints)
entryPointsRoutersHTTP := m.getHTTPRouters(rootCtx, entryPoints, true)
entryPointHandlers := make(map[string]*Router)
for _, entryPointName := range entryPoints {
entryPointName := entryPointName
routers := entryPointsRouters[entryPointName]
ctx := log.With(rootCtx, log.Str(log.EntryPointName, entryPointName))
handler, err := m.buildEntryPointHandler(ctx, routers, entryPointsRoutersHTTP[entryPointName], m.httpHandlers[entryPointName], m.httpsHandlers[entryPointName])
if err != nil {
log.FromContext(ctx).Error(err)
continue
}
entryPointHandlers[entryPointName] = handler
}
return entryPointHandlers
}
type nameAndConfig struct {
routerName string // just so we have it as additional information when logging
TLSConfig *tls.Config
}
func (m *Manager) buildEntryPointHandler(ctx context.Context, configs map[string]*runtime.TCPRouterInfo, configsHTTP map[string]*runtime.RouterInfo, handlerHTTP, handlerHTTPS http.Handler) (*Router, error) {
// Build a new Router.
router, err := NewRouter()
if err != nil {
return nil, err
}
router.SetHTTPHandler(handlerHTTP)
defaultTLSConf, err := m.tlsManager.Get(traefiktls.DefaultTLSStoreName, traefiktls.DefaultTLSConfigName)
if err != nil {
log.FromContext(ctx).Errorf("Error during the build of the default TLS configuration: %v", err)
}
// Keyed by domain. The source of truth for doing SNI checking, and for what TLS
// options will actually be used for the connection.
// As soon as there's (at least) two different tlsOptions found for the same domain,
// we set the value to the default TLS conf.
tlsOptionsForHost := map[string]string{}
// Keyed by domain, then by options reference.
// As opposed to tlsOptionsForHost, it keeps track of all the (different) TLS
// options that occur for a given host name, so that later on we can set relevant
// errors and logging for all the routers concerned (i.e. wrongly configured).
tlsOptionsForHostSNI := map[string]map[string]nameAndConfig{}
for routerHTTPName, routerHTTPConfig := range configsHTTP {
if routerHTTPConfig.TLS == nil {
continue
}
ctxRouter := log.With(provider.AddInContext(ctx, routerHTTPName), log.Str(log.RouterName, routerHTTPName))
logger := log.FromContext(ctxRouter)
tlsOptionsName := traefiktls.DefaultTLSConfigName
if len(routerHTTPConfig.TLS.Options) > 0 && routerHTTPConfig.TLS.Options != traefiktls.DefaultTLSConfigName {
tlsOptionsName = provider.GetQualifiedName(ctxRouter, routerHTTPConfig.TLS.Options)
}
domains, err := httpmuxer.ParseDomains(routerHTTPConfig.Rule)
if err != nil {
routerErr := fmt.Errorf("invalid rule %s, error: %w", routerHTTPConfig.Rule, err)
routerHTTPConfig.AddError(routerErr, true)
logger.Error(routerErr)
continue
}
if len(domains) == 0 {
// Extra Host(*) rule, for HTTPS routers with no Host rule, and for requests for
// which the SNI does not match _any_ of the other existing routers Host. This is
// only about choosing the TLS configuration. The actual routing will be done
// further on by the HTTPS handler. See examples below.
router.AddHTTPTLSConfig("*", defaultTLSConf)
// The server name (from a Host(SNI) rule) is the only parameter (available in
// HTTP routing rules) on which we can map a TLS config, because it is the only one
// accessible before decryption (we obtain it during the ClientHello). Therefore,
// when a router has no Host rule, it does not make any sense to specify some TLS
// options. Consequently, when it comes to deciding what TLS config will be used,
// for a request that will match an HTTPS router with no Host rule, the result will
// depend on the _others_ existing routers (their Host rule, to be precise), and
// the TLS options associated with them, even though they don't match the incoming
// request. Consider the following examples:
// # conf1
// httpRouter1:
// rule: PathPrefix("/foo")
// # Wherever the request comes from, the TLS config used will be the default one, because of the Host(*) fallback.
// # conf2
// httpRouter1:
// rule: PathPrefix("/foo")
//
// httpRouter2:
// rule: Host("foo.com") && PathPrefix("/bar")
// tlsoptions: myTLSOptions
// # When a request for "/foo" comes, even though it won't be routed by
// httpRouter2, if its SNI is set to foo.com, myTLSOptions will be used for the TLS
// connection. Otherwise, it will fallback to the default TLS config.
logger.Warnf("No domain found in rule %v, the TLS options applied for this router will depend on the SNI of each request", routerHTTPConfig.Rule)
}
tlsConf, err := m.tlsManager.Get(traefiktls.DefaultTLSStoreName, tlsOptionsName)
if err != nil {
routerHTTPConfig.AddError(err, true)
logger.Error(err)
continue
}
for _, domain := range domains {
// domain is already in lower case thanks to the domain parsing
if tlsOptionsForHostSNI[domain] == nil {
tlsOptionsForHostSNI[domain] = make(map[string]nameAndConfig)
}
tlsOptionsForHostSNI[domain][tlsOptionsName] = nameAndConfig{
routerName: routerHTTPName,
TLSConfig: tlsConf,
}
if name, ok := tlsOptionsForHost[domain]; ok && name != tlsOptionsName {
// Different tlsOptions on the same domain, so fallback to default
tlsOptionsForHost[domain] = traefiktls.DefaultTLSConfigName
} else {
tlsOptionsForHost[domain] = tlsOptionsName
}
}
}
sniCheck := snicheck.New(tlsOptionsForHost, handlerHTTPS)
router.SetHTTPSHandler(sniCheck, defaultTLSConf)
logger := log.FromContext(ctx)
for hostSNI, tlsConfigs := range tlsOptionsForHostSNI {
if len(tlsConfigs) == 1 {
var optionsName string
var config *tls.Config
for k, v := range tlsConfigs {
optionsName = k
config = v.TLSConfig
break
}
logger.Debugf("Adding route for %s with TLS options %s", hostSNI, optionsName)
router.AddHTTPTLSConfig(hostSNI, config)
} else {
routers := make([]string, 0, len(tlsConfigs))
for _, v := range tlsConfigs {
configsHTTP[v.routerName].AddError(fmt.Errorf("found different TLS options for routers on the same host %v, so using the default TLS options instead", hostSNI), false)
routers = append(routers, v.routerName)
}
logger.Warnf("Found different TLS options for routers on the same host %v, so using the default TLS options instead for these routers: %#v", hostSNI, routers)
router.AddHTTPTLSConfig(hostSNI, defaultTLSConf)
}
}
for routerName, routerConfig := range configs {
ctxRouter := log.With(provider.AddInContext(ctx, routerName), log.Str(log.RouterName, routerName))
logger := log.FromContext(ctxRouter)
if routerConfig.Service == "" {
err := errors.New("the service is missing on the router")
routerConfig.AddError(err, true)
logger.Error(err)
continue
}
if routerConfig.Rule == "" {
err := errors.New("router has no rule")
routerConfig.AddError(err, true)
logger.Error(err)
continue
}
handler, err := m.buildTCPHandler(ctxRouter, routerConfig)
if err != nil {
routerConfig.AddError(err, true)
logger.Error(err)
continue
}
domains, err := tcpmuxer.ParseHostSNI(routerConfig.Rule)
if err != nil {
routerErr := fmt.Errorf("invalid rule: %q , %w", routerConfig.Rule, err)
routerConfig.AddError(routerErr, true)
logger.Error(routerErr)
continue
}
// HostSNI Rule, but TLS not set on the router, which is an error.
// However, we allow the HostSNI(*) exception.
if len(domains) > 0 && routerConfig.TLS == nil && domains[0] != "*" {
routerErr := fmt.Errorf("invalid rule: %q , has HostSNI matcher, but no TLS on router", routerConfig.Rule)
routerConfig.AddError(routerErr, true)
logger.Error(routerErr)
}
if routerConfig.TLS == nil {
logger.Debugf("Adding route for %q", routerConfig.Rule)
if err := router.AddRoute(routerConfig.Rule, routerConfig.Priority, handler); err != nil {
routerConfig.AddError(err, true)
logger.Error(err)
}
continue
}
if routerConfig.TLS.Passthrough {
logger.Debugf("Adding Passthrough route for %q", routerConfig.Rule)
if err := router.AddRouteTLS(routerConfig.Rule, routerConfig.Priority, handler, nil); err != nil {
routerConfig.AddError(err, true)
logger.Error(err)
}
continue
}
for _, domain := range domains {
if httpmuxer.IsASCII(domain) {
continue
}
asciiError := fmt.Errorf("invalid domain name value %q, non-ASCII characters are not allowed", domain)
routerConfig.AddError(asciiError, true)
logger.Error(asciiError)
}
tlsOptionsName := routerConfig.TLS.Options
if len(tlsOptionsName) == 0 {
tlsOptionsName = traefiktls.DefaultTLSConfigName
}
if tlsOptionsName != traefiktls.DefaultTLSConfigName {
tlsOptionsName = provider.GetQualifiedName(ctxRouter, tlsOptionsName)
}
tlsConf, err := m.tlsManager.Get(traefiktls.DefaultTLSStoreName, tlsOptionsName)
if err != nil {
routerConfig.AddError(err, true)
logger.Error(err)
continue
}
// Now that the Rule is not just about the Host, we could theoretically have a config like:
// router1:
// rule: HostSNI(foo.com) && ClientIP(IP1)
// tlsOption: tlsOne
// router2:
// rule: HostSNI(foo.com) && ClientIP(IP2)
// tlsOption: tlsTwo
// i.e. same HostSNI but different tlsOptions
// This is only applicable if the muxer can decide about the routing _before_
// telling the client about the tlsConf (i.e. before the TLS HandShake). This seems
// to be the case so far with the existing matchers (HostSNI, and ClientIP), so
// it's all good. Otherwise, we would have to do as for HTTPS, i.e. disallow
// different TLS configs for the same HostSNIs.
logger.Debugf("Adding TLS route for %q", routerConfig.Rule)
if err := router.AddRouteTLS(routerConfig.Rule, routerConfig.Priority, handler, tlsConf); err != nil {
routerConfig.AddError(err, true)
logger.Error(err)
}
}
return router, nil
}
func (m *Manager) buildTCPHandler(ctx context.Context, router *runtime.TCPRouterInfo) (tcp.Handler, error) {
var qualifiedNames []string
for _, name := range router.Middlewares {
qualifiedNames = append(qualifiedNames, provider.GetQualifiedName(ctx, name))
}
router.Middlewares = qualifiedNames
if router.Service == "" {
return nil, errors.New("the service is missing on the router")
}
sHandler, err := m.serviceManager.BuildTCP(ctx, router.Service)
if err != nil {
return nil, err
}
mHandler := m.middlewaresBuilder.BuildChain(ctx, router.Middlewares)
return tcp.NewChain().Extend(*mHandler).Then(sHandler)
}

View file

@ -175,6 +175,7 @@ func TestRuntimeConfiguration(t *testing.T) {
EntryPoints: []string{"web"},
Service: "foo-service",
Rule: "HostSNI(`foo.bar`)",
TLS: &dynamic.RouterTCPTLSConfig{},
},
},
},
@ -234,6 +235,7 @@ func TestRuntimeConfiguration(t *testing.T) {
EntryPoints: []string{"web"},
Service: "wrong-service",
Rule: "HostSNI(`bar.foo`)",
TLS: &dynamic.RouterTCPTLSConfig{},
},
},
"bar": {
@ -241,6 +243,7 @@ func TestRuntimeConfiguration(t *testing.T) {
EntryPoints: []string{"web"},
Service: "foo-service",
Rule: "HostSNI(`foo.bar`)",
TLS: &dynamic.RouterTCPTLSConfig{},
},
},
},
@ -266,6 +269,32 @@ func TestRuntimeConfiguration(t *testing.T) {
},
expectedError: 2,
},
{
desc: "Router with HostSNI but no TLS",
tcpServiceConfig: map[string]*runtime.TCPServiceInfo{
"foo-service": {
TCPService: &dynamic.TCPService{
LoadBalancer: &dynamic.TCPServersLoadBalancer{
Servers: []dynamic.TCPServer{
{
Address: "127.0.0.1:80",
},
},
},
},
},
},
tcpRouterConfig: map[string]*runtime.TCPRouterInfo{
"foo": {
TCPRouter: &dynamic.TCPRouter{
EntryPoints: []string{"web"},
Service: "foo-service",
Rule: "HostSNI(`bar.foo`)",
},
},
},
expectedError: 1,
},
}
for _, test := range testCases {

View file

@ -1,291 +1,364 @@
package tcp
import (
"context"
"bufio"
"bytes"
"crypto/tls"
"errors"
"fmt"
"io"
"net"
"net/http"
"time"
"github.com/traefik/traefik/v2/pkg/config/runtime"
"github.com/traefik/traefik/v2/pkg/log"
"github.com/traefik/traefik/v2/pkg/middlewares/snicheck"
"github.com/traefik/traefik/v2/pkg/rules"
"github.com/traefik/traefik/v2/pkg/server/provider"
tcpservice "github.com/traefik/traefik/v2/pkg/server/service/tcp"
tcpmuxer "github.com/traefik/traefik/v2/pkg/muxer/tcp"
"github.com/traefik/traefik/v2/pkg/tcp"
traefiktls "github.com/traefik/traefik/v2/pkg/tls"
)
type middlewareBuilder interface {
BuildChain(ctx context.Context, names []string) *tcp.Chain
const defaultBufSize = 4096
// Router is a TCP router.
type Router struct {
// Contains TCP routes.
muxerTCP tcpmuxer.Muxer
// Contains TCP TLS routes.
muxerTCPTLS tcpmuxer.Muxer
// Contains HTTPS routes.
muxerHTTPS tcpmuxer.Muxer
// Forwarder handlers.
// Handles all HTTP requests.
httpForwarder tcp.Handler
// Handles (indirectly through muxerHTTPS, or directly) all HTTPS requests.
httpsForwarder tcp.Handler
// Neither is used directly, but they are held here, and recreated on config
// reload, so that they can be passed to the Switcher at the end of the config
// reload phase.
httpHandler http.Handler
httpsHandler http.Handler
// TLS configs.
httpsTLSConfig *tls.Config // default TLS config
hostHTTPTLSConfig map[string]*tls.Config // TLS configs keyed by SNI
}
// NewManager Creates a new Manager.
func NewManager(conf *runtime.Configuration,
serviceManager *tcpservice.Manager,
middlewaresBuilder middlewareBuilder,
httpHandlers map[string]http.Handler,
httpsHandlers map[string]http.Handler,
tlsManager *traefiktls.Manager,
) *Manager {
return &Manager{
serviceManager: serviceManager,
middlewaresBuilder: middlewaresBuilder,
httpHandlers: httpHandlers,
httpsHandlers: httpsHandlers,
tlsManager: tlsManager,
conf: conf,
}
}
// Manager is a route/router manager.
type Manager struct {
serviceManager *tcpservice.Manager
middlewaresBuilder middlewareBuilder
httpHandlers map[string]http.Handler
httpsHandlers map[string]http.Handler
tlsManager *traefiktls.Manager
conf *runtime.Configuration
}
func (m *Manager) getTCPRouters(ctx context.Context, entryPoints []string) map[string]map[string]*runtime.TCPRouterInfo {
if m.conf != nil {
return m.conf.GetTCPRoutersByEntryPoints(ctx, entryPoints)
}
return make(map[string]map[string]*runtime.TCPRouterInfo)
}
func (m *Manager) getHTTPRouters(ctx context.Context, entryPoints []string, tls bool) map[string]map[string]*runtime.RouterInfo {
if m.conf != nil {
return m.conf.GetRoutersByEntryPoints(ctx, entryPoints, tls)
}
return make(map[string]map[string]*runtime.RouterInfo)
}
// BuildHandlers builds the handlers for the given entrypoints.
func (m *Manager) BuildHandlers(rootCtx context.Context, entryPoints []string) map[string]*tcp.Router {
entryPointsRouters := m.getTCPRouters(rootCtx, entryPoints)
entryPointsRoutersHTTP := m.getHTTPRouters(rootCtx, entryPoints, true)
entryPointHandlers := make(map[string]*tcp.Router)
for _, entryPointName := range entryPoints {
entryPointName := entryPointName
routers := entryPointsRouters[entryPointName]
ctx := log.With(rootCtx, log.Str(log.EntryPointName, entryPointName))
handler, err := m.buildEntryPointHandler(ctx, routers, entryPointsRoutersHTTP[entryPointName], m.httpHandlers[entryPointName], m.httpsHandlers[entryPointName])
if err != nil {
log.FromContext(ctx).Error(err)
continue
}
entryPointHandlers[entryPointName] = handler
}
return entryPointHandlers
}
type nameAndConfig struct {
routerName string // just so we have it as additional information when logging
TLSConfig *tls.Config
}
func (m *Manager) buildEntryPointHandler(ctx context.Context, configs map[string]*runtime.TCPRouterInfo, configsHTTP map[string]*runtime.RouterInfo, handlerHTTP, handlerHTTPS http.Handler) (*tcp.Router, error) {
router := &tcp.Router{}
router.HTTPHandler(handlerHTTP)
defaultTLSConf, err := m.tlsManager.Get(traefiktls.DefaultTLSStoreName, traefiktls.DefaultTLSConfigName)
if err != nil {
log.FromContext(ctx).Errorf("Error during the build of the default TLS configuration: %v", err)
}
if len(configsHTTP) > 0 {
router.AddRouteHTTPTLS("*", defaultTLSConf)
}
// Keyed by domain, then by options reference.
tlsOptionsForHostSNI := map[string]map[string]nameAndConfig{}
tlsOptionsForHost := map[string]string{}
for routerHTTPName, routerHTTPConfig := range configsHTTP {
if routerHTTPConfig.TLS == nil {
continue
}
ctxRouter := log.With(provider.AddInContext(ctx, routerHTTPName), log.Str(log.RouterName, routerHTTPName))
logger := log.FromContext(ctxRouter)
tlsOptionsName := traefiktls.DefaultTLSConfigName
if len(routerHTTPConfig.TLS.Options) > 0 && routerHTTPConfig.TLS.Options != traefiktls.DefaultTLSConfigName {
tlsOptionsName = provider.GetQualifiedName(ctxRouter, routerHTTPConfig.TLS.Options)
}
domains, err := rules.ParseDomains(routerHTTPConfig.Rule)
if err != nil {
routerErr := fmt.Errorf("invalid rule %s, error: %w", routerHTTPConfig.Rule, err)
routerHTTPConfig.AddError(routerErr, true)
logger.Debug(routerErr)
continue
}
if len(domains) == 0 {
logger.Warnf("No domain found in rule %v, the TLS options applied for this router will depend on the hostSNI of each request", routerHTTPConfig.Rule)
}
for _, domain := range domains {
tlsConf, err := m.tlsManager.Get(traefiktls.DefaultTLSStoreName, tlsOptionsName)
if err != nil {
routerHTTPConfig.AddError(err, true)
logger.Debug(err)
continue
}
// domain is already in lower case thanks to the domain parsing
if tlsOptionsForHostSNI[domain] == nil {
tlsOptionsForHostSNI[domain] = make(map[string]nameAndConfig)
}
tlsOptionsForHostSNI[domain][tlsOptionsName] = nameAndConfig{
routerName: routerHTTPName,
TLSConfig: tlsConf,
}
if name, ok := tlsOptionsForHost[domain]; ok && name != tlsOptionsName {
// Different tlsOptions on the same domain fallback to default
tlsOptionsForHost[domain] = traefiktls.DefaultTLSConfigName
} else {
tlsOptionsForHost[domain] = tlsOptionsName
}
}
}
sniCheck := snicheck.New(tlsOptionsForHost, handlerHTTPS)
router.HTTPSHandler(sniCheck, defaultTLSConf)
logger := log.FromContext(ctx)
for hostSNI, tlsConfigs := range tlsOptionsForHostSNI {
if len(tlsConfigs) == 1 {
var optionsName string
var config *tls.Config
for k, v := range tlsConfigs {
optionsName = k
config = v.TLSConfig
break
}
logger.Debugf("Adding route for %s with TLS options %s", hostSNI, optionsName)
router.AddRouteHTTPTLS(hostSNI, config)
} else {
routers := make([]string, 0, len(tlsConfigs))
for _, v := range tlsConfigs {
configsHTTP[v.routerName].AddError(fmt.Errorf("found different TLS options for routers on the same host %v, so using the default TLS options instead", hostSNI), false)
routers = append(routers, v.routerName)
}
logger.Warnf("Found different TLS options for routers on the same host %v, so using the default TLS options instead for these routers: %#v", hostSNI, routers)
router.AddRouteHTTPTLS(hostSNI, defaultTLSConf)
}
}
for routerName, routerConfig := range configs {
ctxRouter := log.With(provider.AddInContext(ctx, routerName), log.Str(log.RouterName, routerName))
logger := log.FromContext(ctxRouter)
if routerConfig.Service == "" {
err := errors.New("the service is missing on the router")
routerConfig.AddError(err, true)
logger.Error(err)
continue
}
if routerConfig.Rule == "" {
err := errors.New("router has no rule")
routerConfig.AddError(err, true)
logger.Error(err)
continue
}
handler, err := m.buildTCPHandler(ctxRouter, routerConfig)
if err != nil {
routerConfig.AddError(err, true)
logger.Error(err)
continue
}
domains, err := rules.ParseHostSNI(routerConfig.Rule)
if err != nil {
routerErr := fmt.Errorf("unknown rule %s", routerConfig.Rule)
routerConfig.AddError(routerErr, true)
logger.Error(routerErr)
continue
}
for _, domain := range domains {
logger.Debugf("Adding route %s on TCP", domain)
switch {
case routerConfig.TLS != nil:
if !rules.IsASCII(domain) {
asciiError := fmt.Errorf("invalid domain name value %q, non-ASCII characters are not allowed", domain)
routerConfig.AddError(asciiError, true)
logger.Debug(asciiError)
continue
}
if routerConfig.TLS.Passthrough {
router.AddRoute(domain, handler)
continue
}
tlsOptionsName := routerConfig.TLS.Options
if len(tlsOptionsName) == 0 {
tlsOptionsName = traefiktls.DefaultTLSConfigName
}
if tlsOptionsName != traefiktls.DefaultTLSConfigName {
tlsOptionsName = provider.GetQualifiedName(ctxRouter, tlsOptionsName)
}
tlsConf, err := m.tlsManager.Get(traefiktls.DefaultTLSStoreName, tlsOptionsName)
if err != nil {
routerConfig.AddError(err, true)
logger.Debug(err)
continue
}
router.AddRouteTLS(domain, handler, tlsConf)
case domain == "*":
router.AddCatchAllNoTLS(handler)
default:
logger.Warn("TCP Router ignored, cannot specify a Host rule without TLS")
}
}
}
return router, nil
}
func (m *Manager) buildTCPHandler(ctx context.Context, router *runtime.TCPRouterInfo) (tcp.Handler, error) {
var qualifiedNames []string
for _, name := range router.Middlewares {
qualifiedNames = append(qualifiedNames, provider.GetQualifiedName(ctx, name))
}
router.Middlewares = qualifiedNames
if router.Service == "" {
return nil, errors.New("the service is missing on the router")
}
sHandler, err := m.serviceManager.BuildTCP(ctx, router.Service)
// NewRouter returns a new TCP router.
func NewRouter() (*Router, error) {
muxTCP, err := tcpmuxer.NewMuxer()
if err != nil {
return nil, err
}
mHandler := m.middlewaresBuilder.BuildChain(ctx, router.Middlewares)
muxTCPTLS, err := tcpmuxer.NewMuxer()
if err != nil {
return nil, err
}
return tcp.NewChain().Extend(*mHandler).Then(sHandler)
muxHTTPS, err := tcpmuxer.NewMuxer()
if err != nil {
return nil, err
}
return &Router{
muxerTCP: *muxTCP,
muxerTCPTLS: *muxTCPTLS,
muxerHTTPS: *muxHTTPS,
}, nil
}
// GetTLSGetClientInfo is called after a ClientHello is received from a client.
func (r *Router) GetTLSGetClientInfo() func(info *tls.ClientHelloInfo) (*tls.Config, error) {
return func(info *tls.ClientHelloInfo) (*tls.Config, error) {
if tlsConfig, ok := r.hostHTTPTLSConfig[info.ServerName]; ok {
return tlsConfig, nil
}
return r.httpsTLSConfig, nil
}
}
// ServeTCP forwards the connection to the right TCP/HTTP handler.
func (r *Router) ServeTCP(conn tcp.WriteCloser) {
// Handling Non-TLS TCP connection early if there is neither HTTP(S) nor TLS
// routers on the entryPoint, and if there is at least one non-TLS TCP router.
// In the case of a non-TLS TCP client (that does not "send" first), we would
// block forever on clientHelloServerName, which is why we want to detect and
// handle that case first and foremost.
if r.muxerTCP.HasRoutes() && !r.muxerTCPTLS.HasRoutes() && !r.muxerHTTPS.HasRoutes() {
connData, err := tcpmuxer.NewConnData("", conn)
if err != nil {
log.WithoutContext().Errorf("Error while reading TCP connection data: %v", err)
conn.Close()
return
}
handler := r.muxerTCP.Match(connData)
// If there is a handler matching the connection metadata,
// we let it handle the connection.
if handler != nil {
handler.ServeTCP(conn)
return
}
// Otherwise, we keep going because:
// 1) we could be in the case where we have HTTP routers.
// 2) if it is an HTTPS request, even though we do not have any TLS routers,
// we still need to reply with a 404.
}
// FIXME -- Check if ProxyProtocol changes the first bytes of the request
br := bufio.NewReader(conn)
serverName, tls, peeked, err := clientHelloServerName(br)
if err != nil {
conn.Close()
return
}
// Remove read/write deadline and delegate this to underlying tcp server (for now only handled by HTTP Server)
err = conn.SetReadDeadline(time.Time{})
if err != nil {
log.WithoutContext().Errorf("Error while setting read deadline: %v", err)
}
err = conn.SetWriteDeadline(time.Time{})
if err != nil {
log.WithoutContext().Errorf("Error while setting write deadline: %v", err)
}
connData, err := tcpmuxer.NewConnData(serverName, conn)
if err != nil {
log.WithoutContext().Errorf("Error while reading TCP connection data: %v", err)
conn.Close()
return
}
if !tls {
handler := r.muxerTCP.Match(connData)
switch {
case handler != nil:
handler.ServeTCP(r.GetConn(conn, peeked))
case r.httpForwarder != nil:
r.httpForwarder.ServeTCP(r.GetConn(conn, peeked))
default:
conn.Close()
}
return
}
handler := r.muxerTCPTLS.Match(connData)
if handler != nil {
handler.ServeTCP(r.GetConn(conn, peeked))
return
}
// for real, the handler returned here is (almost) always the same:
// it is the httpsForwarder that is used for all HTTPS connections that match
// (which is also incidentally the same used in the last block below for 404s).
// The added value from doing Match, is to find and use the specific TLS config
// requested for the given HostSNI.
handler = r.muxerHTTPS.Match(connData)
if handler != nil {
handler.ServeTCP(r.GetConn(conn, peeked))
return
}
// needed to handle 404s for HTTPS, as well as all non-Host (e.g. PathPrefix) matches.
if r.httpsForwarder != nil {
r.httpsForwarder.ServeTCP(r.GetConn(conn, peeked))
return
}
conn.Close()
}
// AddRoute defines a handler for the given rule.
func (r *Router) AddRoute(rule string, priority int, target tcp.Handler) error {
return r.muxerTCP.AddRoute(rule, priority, target)
}
// AddRouteTLS defines a handler for a given rule and sets the matching tlsConfig.
func (r *Router) AddRouteTLS(rule string, priority int, target tcp.Handler, config *tls.Config) error {
// TLS PassThrough
if config == nil {
return r.muxerTCPTLS.AddRoute(rule, priority, target)
}
return r.muxerTCPTLS.AddRoute(rule, priority, &tcp.TLSHandler{
Next: target,
Config: config,
})
}
// AddHTTPTLSConfig defines a handler for a given sniHost and sets the matching tlsConfig.
func (r *Router) AddHTTPTLSConfig(sniHost string, config *tls.Config) {
if r.hostHTTPTLSConfig == nil {
r.hostHTTPTLSConfig = map[string]*tls.Config{}
}
r.hostHTTPTLSConfig[sniHost] = config
}
// GetConn creates a connection proxy with a peeked string.
func (r *Router) GetConn(conn tcp.WriteCloser, peeked string) tcp.WriteCloser {
// FIXME should it really be on Router ?
conn = &Conn{
Peeked: []byte(peeked),
WriteCloser: conn,
}
return conn
}
// GetHTTPHandler gets the attached http handler.
func (r *Router) GetHTTPHandler() http.Handler {
return r.httpHandler
}
// GetHTTPSHandler gets the attached https handler.
func (r *Router) GetHTTPSHandler() http.Handler {
return r.httpsHandler
}
// SetHTTPForwarder sets the tcp handler that will forward the connections to an http handler.
func (r *Router) SetHTTPForwarder(handler tcp.Handler) {
r.httpForwarder = handler
}
// SetHTTPSForwarder sets the tcp handler that will forward the TLS connections to an http handler.
func (r *Router) SetHTTPSForwarder(handler tcp.Handler) {
for sniHost, tlsConf := range r.hostHTTPTLSConfig {
// muxerHTTPS only contains single HostSNI rules (and no other kind of rules),
// so there's no need for specifying a priority for them.
err := r.muxerHTTPS.AddRoute("HostSNI(`"+sniHost+"`)", 0, &tcp.TLSHandler{
Next: handler,
Config: tlsConf,
})
if err != nil {
log.WithoutContext().Errorf("Error while adding route for host: %w", err)
}
}
r.httpsForwarder = &tcp.TLSHandler{
Next: handler,
Config: r.httpsTLSConfig,
}
}
// SetHTTPHandler attaches http handlers on the router.
func (r *Router) SetHTTPHandler(handler http.Handler) {
r.httpHandler = handler
}
// SetHTTPSHandler attaches https handlers on the router.
func (r *Router) SetHTTPSHandler(handler http.Handler, config *tls.Config) {
r.httpsHandler = handler
r.httpsTLSConfig = config
}
// Conn is a connection proxy that handles Peeked bytes.
type Conn struct {
// Peeked are the bytes that have been read from Conn for the
// purposes of route matching, but have not yet been consumed
// by Read calls. It set to nil by Read when fully consumed.
Peeked []byte
// Conn is the underlying connection.
// It can be type asserted against *net.TCPConn or other types
// as needed. It should not be read from directly unless
// Peeked is nil.
tcp.WriteCloser
}
// Read reads bytes from the connection (using the buffer prior to actually reading).
func (c *Conn) Read(p []byte) (n int, err error) {
if len(c.Peeked) > 0 {
n = copy(p, c.Peeked)
c.Peeked = c.Peeked[n:]
if len(c.Peeked) == 0 {
c.Peeked = nil
}
return n, nil
}
return c.WriteCloser.Read(p)
}
// clientHelloServerName returns the SNI server name inside the TLS ClientHello,
// without consuming any bytes from br.
// On any error, the empty string is returned.
func clientHelloServerName(br *bufio.Reader) (string, bool, string, error) {
hdr, err := br.Peek(1)
if err != nil {
var opErr *net.OpError
if !errors.Is(err, io.EOF) && (!errors.As(err, &opErr) || opErr.Timeout()) {
log.WithoutContext().Errorf("Error while Peeking first byte: %s", err)
}
return "", false, "", err
}
// No valid TLS record has a type of 0x80, however SSLv2 handshakes
// start with a uint16 length where the MSB is set and the first record
// is always < 256 bytes long. Therefore typ == 0x80 strongly suggests
// an SSLv2 client.
const recordTypeSSLv2 = 0x80
const recordTypeHandshake = 0x16
if hdr[0] != recordTypeHandshake {
if hdr[0] == recordTypeSSLv2 {
// we consider SSLv2 as TLS and it will be refused by real TLS handshake.
return "", true, getPeeked(br), nil
}
return "", false, getPeeked(br), nil // Not TLS.
}
const recordHeaderLen = 5
hdr, err = br.Peek(recordHeaderLen)
if err != nil {
log.Errorf("Error while Peeking hello: %s", err)
return "", false, getPeeked(br), nil
}
recLen := int(hdr[3])<<8 | int(hdr[4]) // ignoring version in hdr[1:3]
if recordHeaderLen+recLen > defaultBufSize {
br = bufio.NewReaderSize(br, recordHeaderLen+recLen)
}
helloBytes, err := br.Peek(recordHeaderLen + recLen)
if err != nil {
log.Errorf("Error while Hello: %s", err)
return "", true, getPeeked(br), nil
}
sni := ""
server := tls.Server(sniSniffConn{r: bytes.NewReader(helloBytes)}, &tls.Config{
GetConfigForClient: func(hello *tls.ClientHelloInfo) (*tls.Config, error) {
sni = hello.ServerName
return nil, nil
},
})
_ = server.Handshake()
return sni, true, getPeeked(br), nil
}
func getPeeked(br *bufio.Reader) string {
peeked, err := br.Peek(br.Buffered())
if err != nil {
log.Errorf("Could not get anything: %s", err)
return ""
}
return string(peeked)
}
// sniSniffConn is a net.Conn that reads from r, fails on Writes,
// and crashes otherwise.
type sniSniffConn struct {
r io.Reader
net.Conn // nil; crash on any unexpected use
}
// Read reads from the underlying reader.
func (c sniSniffConn) Read(p []byte) (int, error) { return c.r.Read(p) }
// Write crashes all the time.
func (sniSniffConn) Write(p []byte) (int, error) { return 0, io.EOF }

View file

@ -8,16 +8,15 @@ import (
"github.com/traefik/traefik/v2/pkg/log"
"github.com/traefik/traefik/v2/pkg/metrics"
"github.com/traefik/traefik/v2/pkg/server/middleware"
middlewaretcp "github.com/traefik/traefik/v2/pkg/server/middleware/tcp"
tcpmiddleware "github.com/traefik/traefik/v2/pkg/server/middleware/tcp"
"github.com/traefik/traefik/v2/pkg/server/router"
routertcp "github.com/traefik/traefik/v2/pkg/server/router/tcp"
routerudp "github.com/traefik/traefik/v2/pkg/server/router/udp"
tcprouter "github.com/traefik/traefik/v2/pkg/server/router/tcp"
udprouter "github.com/traefik/traefik/v2/pkg/server/router/udp"
"github.com/traefik/traefik/v2/pkg/server/service"
"github.com/traefik/traefik/v2/pkg/server/service/tcp"
"github.com/traefik/traefik/v2/pkg/server/service/udp"
tcpCore "github.com/traefik/traefik/v2/pkg/tcp"
"github.com/traefik/traefik/v2/pkg/tls"
udpCore "github.com/traefik/traefik/v2/pkg/udp"
udptypes "github.com/traefik/traefik/v2/pkg/udp"
)
// RouterFactory the factory of TCP/UDP routers.
@ -64,7 +63,7 @@ func NewRouterFactory(staticConfiguration static.Configuration, managerFactory *
}
// CreateRouters creates new TCPRouters and UDPRouters.
func (f *RouterFactory) CreateRouters(rtConf *runtime.Configuration) (map[string]*tcpCore.Router, map[string]udpCore.Handler) {
func (f *RouterFactory) CreateRouters(rtConf *runtime.Configuration) (map[string]*tcprouter.Router, map[string]udptypes.Handler) {
ctx := context.Background()
// HTTP
@ -82,14 +81,14 @@ func (f *RouterFactory) CreateRouters(rtConf *runtime.Configuration) (map[string
// TCP
svcTCPManager := tcp.NewManager(rtConf)
middlewaresTCPBuilder := middlewaretcp.NewBuilder(rtConf.TCPMiddlewares)
middlewaresTCPBuilder := tcpmiddleware.NewBuilder(rtConf.TCPMiddlewares)
rtTCPManager := routertcp.NewManager(rtConf, svcTCPManager, middlewaresTCPBuilder, handlersNonTLS, handlersTLS, f.tlsManager)
rtTCPManager := tcprouter.NewManager(rtConf, svcTCPManager, middlewaresTCPBuilder, handlersNonTLS, handlersTLS, f.tlsManager)
routersTCP := rtTCPManager.BuildHandlers(ctx, f.entryPointsTCP)
// UDP
svcUDPManager := udp.NewManager(rtConf)
rtUDPManager := routerudp.NewManager(rtConf, svcUDPManager)
rtUDPManager := udprouter.NewManager(rtConf, svcUDPManager)
routersUDP := rtUDPManager.BuildHandlers(ctx, f.entryPointsUDP)
rtConf.PopulateUsedBy()

View file

@ -22,6 +22,7 @@ import (
"github.com/traefik/traefik/v2/pkg/middlewares/requestdecorator"
"github.com/traefik/traefik/v2/pkg/safe"
"github.com/traefik/traefik/v2/pkg/server/router"
tcprouter "github.com/traefik/traefik/v2/pkg/server/router/tcp"
"github.com/traefik/traefik/v2/pkg/tcp"
"github.com/traefik/traefik/v2/pkg/types"
"golang.org/x/net/http2"
@ -114,7 +115,7 @@ func (eps TCPEntryPoints) Stop() {
}
// Switch the TCP routers.
func (eps TCPEntryPoints) Switch(routersTCP map[string]*tcp.Router) {
func (eps TCPEntryPoints) Switch(routersTCP map[string]*tcprouter.Router) {
for entryPointName, rt := range routersTCP {
eps[entryPointName].SwitchRouter(rt)
}
@ -141,7 +142,7 @@ func NewTCPEntryPoint(ctx context.Context, configuration *static.EntryPoint, hos
return nil, fmt.Errorf("error preparing server: %w", err)
}
rt := &tcp.Router{}
rt := &tcprouter.Router{}
reqDecorator := requestdecorator.New(hostResolverConfig)
@ -150,7 +151,7 @@ func NewTCPEntryPoint(ctx context.Context, configuration *static.EntryPoint, hos
return nil, fmt.Errorf("error preparing http server: %w", err)
}
rt.HTTPForwarder(httpServer.Forwarder)
rt.SetHTTPForwarder(httpServer.Forwarder)
httpsServer, err := createHTTPServer(ctx, listener, configuration, false, reqDecorator)
if err != nil {
@ -162,7 +163,7 @@ func NewTCPEntryPoint(ctx context.Context, configuration *static.EntryPoint, hos
return nil, fmt.Errorf("error preparing http3 server: %w", err)
}
rt.HTTPSForwarder(httpsServer.Forwarder)
rt.SetHTTPSForwarder(httpsServer.Forwarder)
tcpSwitcher := &tcp.HandlerSwitcher{}
tcpSwitcher.Switch(rt)
@ -181,7 +182,7 @@ func NewTCPEntryPoint(ctx context.Context, configuration *static.EntryPoint, hos
// Start starts the TCP server.
func (e *TCPEntryPoint) Start(ctx context.Context) {
logger := log.FromContext(ctx)
logger.Debugf("Start TCP Server")
logger.Debug("Starting TCP Server")
if e.http3Server != nil {
go func() { _ = e.http3Server.Start() }()
@ -301,8 +302,8 @@ func (e *TCPEntryPoint) Shutdown(ctx context.Context) {
}
// SwitchRouter switches the TCP router handler.
func (e *TCPEntryPoint) SwitchRouter(rt *tcp.Router) {
rt.HTTPForwarder(e.httpServer.Forwarder)
func (e *TCPEntryPoint) SwitchRouter(rt *tcprouter.Router) {
rt.SetHTTPForwarder(e.httpServer.Forwarder)
httpHandler := rt.GetHTTPHandler()
if httpHandler == nil {
@ -311,7 +312,7 @@ func (e *TCPEntryPoint) SwitchRouter(rt *tcp.Router) {
e.httpServer.Switcher.UpdateHandler(httpHandler)
rt.HTTPSForwarder(e.httpsServer.Forwarder)
rt.SetHTTPSForwarder(e.httpsServer.Forwarder)
httpsHandler := rt.GetHTTPSHandler()
if httpsHandler == nil {

View file

@ -13,7 +13,7 @@ import (
"github.com/lucas-clemente/quic-go/http3"
"github.com/traefik/traefik/v2/pkg/config/static"
"github.com/traefik/traefik/v2/pkg/log"
"github.com/traefik/traefik/v2/pkg/tcp"
tcprouter "github.com/traefik/traefik/v2/pkg/server/router/tcp"
)
type http3server struct {
@ -77,7 +77,7 @@ func (e *http3server) Start() error {
return e.Serve(e.http3conn)
}
func (e *http3server) Switch(rt *tcp.Router) {
func (e *http3server) Switch(rt *tcprouter.Router) {
e.lock.Lock()
defer e.lock.Unlock()

View file

@ -10,7 +10,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/traefik/traefik/v2/pkg/config/static"
"github.com/traefik/traefik/v2/pkg/tcp"
tcprouter "github.com/traefik/traefik/v2/pkg/server/router/tcp"
traefiktls "github.com/traefik/traefik/v2/pkg/tls"
)
@ -94,11 +94,13 @@ func TestHTTP3AdvertisedPort(t *testing.T) {
}, nil)
require.NoError(t, err)
router := &tcp.Router{}
router.AddRouteHTTPTLS("*", &tls.Config{
router, err := tcprouter.NewRouter()
require.NoError(t, err)
router.AddHTTPTLSConfig("*", &tls.Config{
Certificates: []tls.Certificate{tlsCert},
})
router.HTTPSHandler(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
router.SetHTTPSHandler(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
rw.WriteHeader(http.StatusOK)
}), nil)

View file

@ -15,12 +15,13 @@ import (
"github.com/stretchr/testify/require"
ptypes "github.com/traefik/paerser/types"
"github.com/traefik/traefik/v2/pkg/config/static"
tcprouter "github.com/traefik/traefik/v2/pkg/server/router/tcp"
"github.com/traefik/traefik/v2/pkg/tcp"
)
func TestShutdownHijacked(t *testing.T) {
router := &tcp.Router{}
router.HTTPHandler(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
router := &tcprouter.Router{}
router.SetHTTPHandler(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
conn, _, err := rw.(http.Hijacker).Hijack()
require.NoError(t, err)
@ -33,8 +34,8 @@ func TestShutdownHijacked(t *testing.T) {
}
func TestShutdownHTTP(t *testing.T) {
router := &tcp.Router{}
router.HTTPHandler(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
router := &tcprouter.Router{}
router.SetHTTPHandler(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
rw.WriteHeader(http.StatusOK)
time.Sleep(time.Second)
}))
@ -43,8 +44,10 @@ func TestShutdownHTTP(t *testing.T) {
}
func TestShutdownTCP(t *testing.T) {
router := &tcp.Router{}
router.AddCatchAllNoTLS(tcp.HandlerFunc(func(conn tcp.WriteCloser) {
router, err := tcprouter.NewRouter()
require.NoError(t, err)
err = router.AddRoute("HostSNI(`*`)", 0, tcp.HandlerFunc(func(conn tcp.WriteCloser) {
for {
_, err := http.ReadRequest(bufio.NewReader(conn))
@ -58,11 +61,12 @@ func TestShutdownTCP(t *testing.T) {
require.NoError(t, err)
}
}))
require.NoError(t, err)
testShutdown(t, router)
}
func testShutdown(t *testing.T, router *tcp.Router) {
func testShutdown(t *testing.T, router *tcprouter.Router) {
t.Helper()
epConfig := &static.EntryPointsTransport{}
@ -135,7 +139,7 @@ func testShutdown(t *testing.T, router *tcp.Router) {
assert.Equal(t, http.StatusOK, resp.StatusCode)
}
func startEntrypoint(entryPoint *TCPEntryPoint, router *tcp.Router) (net.Conn, error) {
func startEntrypoint(entryPoint *TCPEntryPoint, router *tcprouter.Router) (net.Conn, error) {
go entryPoint.Start(context.Background())
entryPoint.SwitchRouter(router)
@ -165,8 +169,8 @@ func TestReadTimeoutWithoutFirstByte(t *testing.T) {
}, nil)
require.NoError(t, err)
router := &tcp.Router{}
router.HTTPHandler(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
router := &tcprouter.Router{}
router.SetHTTPHandler(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
rw.WriteHeader(http.StatusOK)
}))
@ -201,8 +205,8 @@ func TestReadTimeoutWithFirstByte(t *testing.T) {
}, nil)
require.NoError(t, err)
router := &tcp.Router{}
router.HTTPHandler(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
router := &tcprouter.Router{}
router.SetHTTPHandler(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
rw.WriteHeader(http.StatusOK)
}))

View file

@ -93,7 +93,7 @@ func (m *Mirroring) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
if errors.Is(err, errBodyTooLarge) {
req.Body = io.NopCloser(io.MultiReader(bytes.NewReader(bytesRead), req.Body))
m.handler.ServeHTTP(rw, req)
logger.Debugf("no mirroring, request body larger than allowed size")
logger.Debug("no mirroring, request body larger than allowed size")
return
}

View file

@ -1,286 +0,0 @@
package tcp
import (
"bufio"
"bytes"
"crypto/tls"
"errors"
"io"
"net"
"net/http"
"strings"
"time"
"github.com/traefik/traefik/v2/pkg/log"
"github.com/traefik/traefik/v2/pkg/types"
)
const defaultBufSize = 4096
// Router is a TCP router.
type Router struct {
routingTable map[string]Handler
httpForwarder Handler
httpsForwarder Handler
httpHandler http.Handler
httpsHandler http.Handler
httpsTLSConfig *tls.Config // default TLS config
catchAllNoTLS Handler
hostHTTPTLSConfig map[string]*tls.Config // TLS configs keyed by SNI
}
// GetTLSGetClientInfo is called after a ClientHello is received from a client.
func (r *Router) GetTLSGetClientInfo() func(info *tls.ClientHelloInfo) (*tls.Config, error) {
return func(info *tls.ClientHelloInfo) (*tls.Config, error) {
if tlsConfig, ok := r.hostHTTPTLSConfig[info.ServerName]; ok {
return tlsConfig, nil
}
return r.httpsTLSConfig, nil
}
}
// ServeTCP forwards the connection to the right TCP/HTTP handler.
func (r *Router) ServeTCP(conn WriteCloser) {
// FIXME -- Check if ProxyProtocol changes the first bytes of the request
if r.catchAllNoTLS != nil && len(r.routingTable) == 0 {
r.catchAllNoTLS.ServeTCP(conn)
return
}
br := bufio.NewReader(conn)
serverName, tls, peeked, err := clientHelloServerName(br)
if err != nil {
conn.Close()
return
}
// Remove read/write deadline and delegate this to underlying tcp server (for now only handled by HTTP Server)
err = conn.SetReadDeadline(time.Time{})
if err != nil {
log.WithoutContext().Errorf("Error while setting read deadline: %v", err)
}
err = conn.SetWriteDeadline(time.Time{})
if err != nil {
log.WithoutContext().Errorf("Error while setting write deadline: %v", err)
}
if !tls {
switch {
case r.catchAllNoTLS != nil:
r.catchAllNoTLS.ServeTCP(r.GetConn(conn, peeked))
case r.httpForwarder != nil:
r.httpForwarder.ServeTCP(r.GetConn(conn, peeked))
default:
conn.Close()
}
return
}
// FIXME Optimize and test the routing table before helloServerName
serverName = types.CanonicalDomain(serverName)
if r.routingTable != nil && serverName != "" {
if target, ok := r.routingTable[serverName]; ok {
target.ServeTCP(r.GetConn(conn, peeked))
return
}
}
// FIXME Needs tests
if target, ok := r.routingTable["*"]; ok {
target.ServeTCP(r.GetConn(conn, peeked))
return
}
if r.httpsForwarder != nil {
r.httpsForwarder.ServeTCP(r.GetConn(conn, peeked))
} else {
conn.Close()
}
}
// AddRoute defines a handler for a given sniHost (* is the only valid option).
func (r *Router) AddRoute(sniHost string, target Handler) {
if r.routingTable == nil {
r.routingTable = map[string]Handler{}
}
r.routingTable[strings.ToLower(sniHost)] = target
}
// AddRouteTLS defines a handler for a given sniHost and sets the matching tlsConfig.
func (r *Router) AddRouteTLS(sniHost string, target Handler, config *tls.Config) {
r.AddRoute(sniHost, &TLSHandler{
Next: target,
Config: config,
})
}
// AddRouteHTTPTLS defines a handler for a given sniHost and sets the matching tlsConfig.
func (r *Router) AddRouteHTTPTLS(sniHost string, config *tls.Config) {
if r.hostHTTPTLSConfig == nil {
r.hostHTTPTLSConfig = map[string]*tls.Config{}
}
r.hostHTTPTLSConfig[sniHost] = config
}
// AddCatchAllNoTLS defines the fallback tcp handler.
func (r *Router) AddCatchAllNoTLS(handler Handler) {
r.catchAllNoTLS = handler
}
// GetConn creates a connection proxy with a peeked string.
func (r *Router) GetConn(conn WriteCloser, peeked string) WriteCloser {
// FIXME should it really be on Router ?
conn = &Conn{
Peeked: []byte(peeked),
WriteCloser: conn,
}
return conn
}
// GetHTTPHandler gets the attached http handler.
func (r *Router) GetHTTPHandler() http.Handler {
return r.httpHandler
}
// GetHTTPSHandler gets the attached https handler.
func (r *Router) GetHTTPSHandler() http.Handler {
return r.httpsHandler
}
// HTTPForwarder sets the tcp handler that will forward the connections to an http handler.
func (r *Router) HTTPForwarder(handler Handler) {
r.httpForwarder = handler
}
// HTTPSForwarder sets the tcp handler that will forward the TLS connections to an http handler.
func (r *Router) HTTPSForwarder(handler Handler) {
for sniHost, tlsConf := range r.hostHTTPTLSConfig {
r.AddRouteTLS(sniHost, handler, tlsConf)
}
r.httpsForwarder = &TLSHandler{
Next: handler,
Config: r.httpsTLSConfig,
}
}
// HTTPHandler attaches http handlers on the router.
func (r *Router) HTTPHandler(handler http.Handler) {
r.httpHandler = handler
}
// HTTPSHandler attaches https handlers on the router.
func (r *Router) HTTPSHandler(handler http.Handler, config *tls.Config) {
r.httpsHandler = handler
r.httpsTLSConfig = config
}
// Conn is a connection proxy that handles Peeked bytes.
type Conn struct {
// Peeked are the bytes that have been read from Conn for the
// purposes of route matching, but have not yet been consumed
// by Read calls. It set to nil by Read when fully consumed.
Peeked []byte
// Conn is the underlying connection.
// It can be type asserted against *net.TCPConn or other types
// as needed. It should not be read from directly unless
// Peeked is nil.
WriteCloser
}
// Read reads bytes from the connection (using the buffer prior to actually reading).
func (c *Conn) Read(p []byte) (n int, err error) {
if len(c.Peeked) > 0 {
n = copy(p, c.Peeked)
c.Peeked = c.Peeked[n:]
if len(c.Peeked) == 0 {
c.Peeked = nil
}
return n, nil
}
return c.WriteCloser.Read(p)
}
// clientHelloServerName returns the SNI server name inside the TLS ClientHello,
// without consuming any bytes from br.
// On any error, the empty string is returned.
func clientHelloServerName(br *bufio.Reader) (string, bool, string, error) {
hdr, err := br.Peek(1)
if err != nil {
var opErr *net.OpError
if !errors.Is(err, io.EOF) && (!errors.As(err, &opErr) || opErr.Timeout()) {
log.WithoutContext().Debugf("Error while Peeking first byte: %s", err)
}
return "", false, "", err
}
// No valid TLS record has a type of 0x80, however SSLv2 handshakes
// start with a uint16 length where the MSB is set and the first record
// is always < 256 bytes long. Therefore typ == 0x80 strongly suggests
// an SSLv2 client.
const recordTypeSSLv2 = 0x80
const recordTypeHandshake = 0x16
if hdr[0] != recordTypeHandshake {
if hdr[0] == recordTypeSSLv2 {
// we consider SSLv2 as TLS and it will be refuse by real TLS handshake.
return "", true, getPeeked(br), nil
}
return "", false, getPeeked(br), nil // Not TLS.
}
const recordHeaderLen = 5
hdr, err = br.Peek(recordHeaderLen)
if err != nil {
log.Errorf("Error while Peeking hello: %s", err)
return "", false, getPeeked(br), nil
}
recLen := int(hdr[3])<<8 | int(hdr[4]) // ignoring version in hdr[1:3]
if recordHeaderLen+recLen > defaultBufSize {
br = bufio.NewReaderSize(br, recordHeaderLen+recLen)
}
helloBytes, err := br.Peek(recordHeaderLen + recLen)
if err != nil {
log.Errorf("Error while Hello: %s", err)
return "", true, getPeeked(br), nil
}
sni := ""
server := tls.Server(sniSniffConn{r: bytes.NewReader(helloBytes)}, &tls.Config{
GetConfigForClient: func(hello *tls.ClientHelloInfo) (*tls.Config, error) {
sni = hello.ServerName
return nil, nil
},
})
_ = server.Handshake()
return sni, true, getPeeked(br), nil
}
func getPeeked(br *bufio.Reader) string {
peeked, err := br.Peek(br.Buffered())
if err != nil {
log.Errorf("Could not get anything: %s", err)
return ""
}
return string(peeked)
}
// sniSniffConn is a net.Conn that reads from r, fails on Writes,
// and crashes otherwise.
type sniSniffConn struct {
r io.Reader
net.Conn // nil; crash on any unexpected use
}
// Read reads from the underlying reader.
func (c sniSniffConn) Read(p []byte) (int, error) { return c.r.Read(p) }
// Write crashes all the time.
func (sniSniffConn) Write(p []byte) (int, error) { return 0, io.EOF }