Add providers routing precedence configuration

Co-authored-by: Mathis Urien <contact.lbf38@gmail.com>
This commit is contained in:
Julien Salleyron
2026-04-02 09:08:05 +02:00
committed by GitHub
parent ea7f300c85
commit 9d9f0d465d
51 changed files with 1278 additions and 571 deletions
@@ -448,6 +448,7 @@ THIS FILE MUST NOT BE EDITED BY HAND
| <a id="opt-providers-nomad-throttleduration" href="#opt-providers-nomad-throttleduration" title="#opt-providers-nomad-throttleduration">providers.nomad.throttleduration</a> | Watch throttle duration. | 0 |
| <a id="opt-providers-nomad-watch" href="#opt-providers-nomad-watch" title="#opt-providers-nomad-watch">providers.nomad.watch</a> | Watch Nomad Service events. | false |
| <a id="opt-providers-plugin-name" href="#opt-providers-plugin-name" title="#opt-providers-plugin-name">providers.plugin._name_</a> | Plugins configuration. | |
| <a id="opt-providers-precedence" href="#opt-providers-precedence" title="#opt-providers-precedence">providers.precedence</a> | Defines the routing precedence between providers. | kubernetesgateway, kubernetescrd, kubernetes, kubernetesingressnginx, swarm, docker, file, redis, knative, consul, consulcatalog, nomad, etcd, ecs, http, zookeeper, rest |
| <a id="opt-providers-providersthrottleduration" href="#opt-providers-providersthrottleduration" title="#opt-providers-providersthrottleduration">providers.providersthrottleduration</a> | Backends throttle duration: minimum duration between 2 events from providers before applying a new configuration. It avoids unnecessary reloads if multiples events are sent in a short amount of time. | 2 |
| <a id="opt-providers-redis" href="#opt-providers-redis" title="#opt-providers-redis">providers.redis</a> | Enables Redis provider. | false |
| <a id="opt-providers-redis-db" href="#opt-providers-redis-db" title="#opt-providers-redis-db">providers.redis.db</a> | Database to be selected after connecting to the server. | 0 |
@@ -142,6 +142,64 @@ you can do so in two different ways:
- the generic configuration option `exposedByDefault`,
- a finer granularity mechanism based on constraints.
## Providers Precedence
### `providers.precedence`
_Optional_
When two routers from **different providers** define the same rule with equal numeric [priority](../../routing-configuration/http/routing/rules-and-priority.md#priority-calculation),
the `precedence` option determines which provider's route takes precedence.
The list is ordered from highest to lowest precedence: a provider listed first wins over providers listed later.
```yaml tab="File (YAML)"
providers:
precedence:
- kubernetescrd
- kubernetes
- file
```
```toml tab="File (TOML)"
[providers]
precedence = ["kubernetescrd", "kubernetes", "file"]
```
```bash tab="CLI"
--providers.precedence=kubernetescrd,kubernetes,file
```
#### Default precedence
When `precedence` is not set, Traefik uses the following default order (highest precedence first):
| Position | Provider name |
|----------|--------------------------|
| <a id="opt-1" href="#opt-1" title="#opt-1">1</a> | `kubernetesgateway` |
| <a id="opt-2" href="#opt-2" title="#opt-2">2</a> | `kubernetescrd` |
| <a id="opt-3" href="#opt-3" title="#opt-3">3</a> | `kubernetes` |
| <a id="opt-4" href="#opt-4" title="#opt-4">4</a> | `kubernetesingressnginx` |
| <a id="opt-5" href="#opt-5" title="#opt-5">5</a> | `swarm` |
| <a id="opt-6" href="#opt-6" title="#opt-6">6</a> | `docker` |
| <a id="opt-7" href="#opt-7" title="#opt-7">7</a> | `file` |
| <a id="opt-8" href="#opt-8" title="#opt-8">8</a> | `redis` |
| <a id="opt-9" href="#opt-9" title="#opt-9">9</a> | `knative` |
| <a id="opt-10" href="#opt-10" title="#opt-10">10</a> | `consul` |
| <a id="opt-11" href="#opt-11" title="#opt-11">11</a> | `consulcatalog` |
| <a id="opt-12" href="#opt-12" title="#opt-12">12</a> | `nomad` |
| <a id="opt-13" href="#opt-13" title="#opt-13">13</a> | `etcd` |
| <a id="opt-14" href="#opt-14" title="#opt-14">14</a> | `ecs` |
| <a id="opt-15" href="#opt-15" title="#opt-15">15</a> | `http` |
| <a id="opt-16" href="#opt-16" title="#opt-16">16</a> | `zookeeper` |
| <a id="opt-17" href="#opt-17" title="#opt-17">17</a> | `rest` |
!!! note
- `precedence` only acts as a **tiebreaker**: it is applied only when two routes from different providers share the same numeric `priority` value. An explicit router priority always takes precedence.
- A provider absent from `precedence` loses to any listed provider.
- Provider names are case-insensitive.
### `exposedByDefault` and `traefik.enable`
List of providers that support these features:
@@ -241,6 +241,12 @@ Traefik reserves a range of priorities for its internal routers, the maximum use
- `(MaxInt32 - 1000)` = `2147482647` for 32-bit platforms,
- `(MaxInt64 - 1000)` = `9223372036854774807` for 64-bit platforms.
!!! info "Providers Precedence"
When two routes from **different providers** share the same numeric priority,
Traefik uses the [`providers.precedence`](../../../install-configuration/providers/overview.md#providers-precedence) install configuration option to determine which route takes precedence.
The provider listed first in `precedence` wins the tie.
### Example
```yaml tab="Structured (YAML)"
@@ -216,3 +216,9 @@ Traefik reserves a range of priorities for its internal routers, the maximum use
- `(MaxInt32 - 1000)` for 32-bit platforms,
- `(MaxInt64 - 1000)` for 64-bit platforms.
!!! info "Providers Precedence"
When two routes from **different providers** share the same numeric priority,
Traefik uses the [`providers.precedence`](../../../install-configuration/providers/overview.md#providers-precedence) install configuration option to determine which route takes precedence.
The provider listed first in `precedence` wins the tie.
@@ -0,0 +1,48 @@
[global]
checkNewVersion = false
sendAnonymousUsage = false
[log]
level = "DEBUG"
noColor = true
[entryPoints]
[entryPoints.web]
address = ":8000"
[api]
insecure = true
[providers]
precedence = {{ .Precedence }}
[providers.file]
filename = "{{ .SelfFilename }}"
[providers.docker]
endpoint = "{{ .DockerHost }}"
exposedByDefault = false
## dynamic configuration ##
[http.routers]
[http.routers.file-router]
rule = "PathPrefix(`/http`)"
service = "file-service"
entryPoints = ["web"]
[http.services]
[http.services.file-service.loadBalancer]
[[http.services.file-service.loadBalancer.servers]]
url = "http://{{ .FileBackendAddress }}"
[tcp.routers]
[tcp.routers.file-router]
rule = "HostSNI(`*`)"
service = "file-service"
entryPoints = ["web"]
[tcp.services]
[tcp.services.file-service.loadBalancer]
[[tcp.services.file-service.loadBalancer.servers]]
address = "{{ .FileBackendAddress }}"
+1 -1
View File
@@ -577,7 +577,7 @@ func (s *HealthCheckSuite) TestPropagateNoHealthCheck() {
s.traefikCmd(withConfigFile(file))
// wait for traefik
err := try.GetRequest("http://127.0.0.1:8080/api/rawdata", 60*time.Second, try.BodyContains("Host(`noop.localhost`)"), try.BodyNotContains("Host(`root.localhost`)"))
err := try.GetRequest("http://127.0.0.1:8080/api/rawdata", 60*time.Second, try.BodyContains("Host(`noop.localhost`)"), try.BodyContains("cannot register wsp1 as updater for wsp-service1@file"))
require.NoError(s.T(), err)
rootReq, err := http.NewRequest(http.MethodGet, "http://127.0.0.1:8000", nil)
@@ -0,0 +1,9 @@
services:
whoami:
image: traefik/whoami
labels:
traefik.enable: "true"
traefik.http.routers.docker-router.rule: "PathPrefix(`/http`)"
traefik.http.routers.docker-router.entryPoints: web
traefik.tcp.routers.docker-router.rule: "HostSNI(`*`)"
traefik.tcp.routers.docker-router.entryPoints: web
+85
View File
@@ -2656,3 +2656,88 @@ func (s *SimpleSuite) TestServiceMiddleware() {
// The whoami service should have received the X-Custom-Header that was added by the service middleware
assert.Contains(s.T(), string(body), "X-Custom-Header: service-middleware-test")
}
// TestProviderPrecedenceFileWins verifies that, when two providers define
// routes with the same rule and auto-computed priority, the provider listed
// first in providers.precedence takes precedence (lower index = higher
// provider priority).
//
// Setup:
// - providers.file → file-router → fileBackend (body: "from-file")
// - providers.docker → docker-router → whoami container
// - precedence = ["file", "docker"] → file is index 0 → wins
func (s *SimpleSuite) TestProviderPrecedenceFileWins() {
s.createComposeProject("providers-precedence")
s.composeUp("whoami")
defer s.composeDown()
fileBackend := startTestServer("9042", http.StatusOK, "from-file")
defer fileBackend.Close()
file := s.adaptFile("fixtures/providers-precedence.toml", struct {
Precedence string
FileBackendAddress string
DockerHost string
}{
Precedence: `["file", "docker"]`,
FileBackendAddress: "127.0.0.1:9042",
DockerHost: s.getDockerHost(),
})
s.traefikCmd(withConfigFile(file))
// Wait for both providers to have loaded their routers.
err := try.GetRequest("http://127.0.0.1:8080/api/rawdata", 10*time.Second,
try.StatusCodeIs(http.StatusOK),
try.BodyContains("file-router@file"),
try.BodyContains("docker-router@docker"))
require.NoError(s.T(), err)
// The file provider has higher priority → requests must reach the file backend.
err = try.GetRequest("http://127.0.0.1:8000/http", 5*time.Second, try.BodyContains("from-file"))
require.NoError(s.T(), err)
// This request should be handled by the TCP route.
err = try.GetRequest("http://127.0.0.1:8000/tcp", 5*time.Second, try.BodyContains("from-file"))
require.NoError(s.T(), err)
}
// TestProviderPrecedenceDockerWins mirrors TestProviderPrecedenceFileWins
// but reverses the precedence so that the Docker provider wins instead.
//
// Setup:
// - providers.file → file-router → fileBackend (body: "from-file")
// - providers.docker → docker-router → whoami container
// - precedence = ["docker", "file"] → docker is index 0 → wins
func (s *SimpleSuite) TestProviderPrecedenceDockerWins() {
s.createComposeProject("providers-precedence")
s.composeUp("whoami")
defer s.composeDown()
fileBackend := startTestServer("9042", http.StatusOK, "from-file")
defer fileBackend.Close()
file := s.adaptFile("fixtures/providers-precedence.toml", struct {
Precedence string
FileBackendAddress string
DockerHost string
}{
Precedence: `["docker", "file"]`,
FileBackendAddress: "127.0.0.1:9042",
DockerHost: s.getDockerHost(),
})
s.traefikCmd(withConfigFile(file))
// Wait for both providers to have loaded their routers.
err := try.GetRequest("http://127.0.0.1:8080/api/rawdata", 10*time.Second,
try.BodyContains("file-router@file"),
try.BodyContains("docker-router@docker"))
require.NoError(s.T(), err)
// The Docker provider has higher priority → requests must reach the whoami container.
err = try.GetRequest("http://127.0.0.1:8000/http", 5*time.Second, try.BodyContains("Hostname:"))
require.NoError(s.T(), err)
// This request should be handled by the TCP route.
err = try.GetRequest("http://127.0.0.1:8000/tcp", 5*time.Second, try.BodyContains("Hostname:"))
require.NoError(s.T(), err)
}
+9
View File
@@ -1,6 +1,7 @@
package try
import (
"bytes"
"context"
"errors"
"fmt"
@@ -25,6 +26,8 @@ func BodyContains(values ...string) ResponseCondition {
return fmt.Errorf("failed to read response body: %w", err)
}
res.Body = io.NopCloser(bytes.NewBuffer(body))
for _, value := range values {
if !strings.Contains(string(body), value) {
return fmt.Errorf("could not find '%s' in body '%s'", value, string(body))
@@ -43,6 +46,8 @@ func BodyNotContains(values ...string) ResponseCondition {
return fmt.Errorf("failed to read response body: %w", err)
}
res.Body = io.NopCloser(bytes.NewBuffer(body))
for _, value := range values {
if strings.Contains(string(body), value) {
return fmt.Errorf("find '%s' in body '%s'", value, string(body))
@@ -61,6 +66,8 @@ func BodyContainsOr(values ...string) ResponseCondition {
return fmt.Errorf("failed to read response body: %w", err)
}
res.Body = io.NopCloser(bytes.NewBuffer(body))
for _, value := range values {
if strings.Contains(string(body), value) {
return nil
@@ -79,6 +86,8 @@ func HasBody() ResponseCondition {
return fmt.Errorf("failed to read response body: %w", err)
}
res.Body = io.NopCloser(bytes.NewBuffer(body))
if len(body) == 0 {
return errors.New("response doesn't have body content")
}
+1
View File
@@ -232,6 +232,7 @@ func TestHandler_Overview(t *testing.T) {
Global: &static.Global{},
API: &static.API{},
Providers: &static.Providers{
Precedence: []string{"foo"},
Docker: &docker.Provider{},
Swarm: &docker.SwarmProvider{},
File: &file.Provider{},
+40
View File
@@ -4,6 +4,7 @@ import (
"errors"
"fmt"
"regexp"
"slices"
"strings"
"time"
@@ -57,6 +58,27 @@ const (
DefaultUDPTimeout = 3 * time.Second
)
// providerNames is the ordered list of the Traefik provider names.
var providerNames = []string{
gateway.ProviderName,
crd.ProviderName,
ingress.ProviderName,
ingressnginx.ProviderName,
docker.SwarmName,
docker.DockerName,
file.ProviderName,
redis.ProviderName,
knative.ProviderName,
consul.ProviderName,
consulcatalog.ProviderName,
nomad.ProviderName,
etcd.ProviderName,
ecs.ProviderName,
http.ProviderName,
zk.ProviderName,
rest.ProviderName,
}
// Allowed characters in URL following RFC 3986 (https://www.rfc-editor.org/rfc/rfc3986#section-2)
var validBasePath = regexp.MustCompile(`^/[a-zA-Z0-9/_.:~-]*$`)
@@ -238,6 +260,7 @@ func (t *Tracing) SetDefaults() {
// Providers contains providers configuration.
type Providers struct {
ProvidersThrottleDuration ptypes.Duration `description:"Backends throttle duration: minimum duration between 2 events from providers before applying a new configuration. It avoids unnecessary reloads if multiples events are sent in a short amount of time." json:"providersThrottleDuration,omitempty" toml:"providersThrottleDuration,omitempty" yaml:"providersThrottleDuration,omitempty" export:"true"`
Precedence []string `description:"Defines the routing precedence between providers." json:"precedence,omitempty" toml:"precedence,omitempty" yaml:"precedence,omitempty" export:"true"`
Docker *docker.Provider `description:"Enables Docker provider." json:"docker,omitempty" toml:"docker,omitempty" yaml:"docker,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"`
Swarm *docker.SwarmProvider `description:"Enables Docker Swarm provider." json:"swarm,omitempty" toml:"swarm,omitempty" yaml:"swarm,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"`
@@ -260,6 +283,11 @@ type Providers struct {
Plugin map[string]PluginConf `description:"Plugins configuration." json:"plugin,omitempty" toml:"plugin,omitempty" yaml:"plugin,omitempty"`
}
// SetDefaults sets the default values.
func (p *Providers) SetDefaults() {
p.Precedence = providerNames
}
// SetEffectiveConfiguration adds missing configuration parameters derived from existing ones.
// It also takes care of maintaining backwards compatibility.
func (c *Configuration) SetEffectiveConfiguration() {
@@ -290,6 +318,10 @@ func (c *Configuration) SetEffectiveConfiguration() {
c.Tracing.ResourceAttributes = c.Tracing.GlobalAttributes
}
for i, providerName := range c.Providers.Precedence {
c.Providers.Precedence[i] = strings.ToLower(providerName)
}
if c.Providers.Docker != nil {
if c.Providers.Docker.HTTPClientTimeout < 0 {
c.Providers.Docker.HTTPClientTimeout = 0
@@ -426,6 +458,14 @@ func (c *Configuration) ValidateConfiguration() error {
}
}
if c.Providers != nil {
for _, providerName := range c.Providers.Precedence {
if !slices.Contains(providerNames, providerName) {
return fmt.Errorf("provider %q is not a valid provider name", providerName)
}
}
}
if c.Providers != nil && c.Providers.KubernetesIngressNGINX != nil {
if c.Providers.KubernetesIngressNGINX.WatchNamespace != "" && c.Providers.KubernetesIngressNGINX.WatchNamespaceSelector != "" {
return errors.New("watchNamespace and watchNamespaceSelector options are mutually exclusive")
+61 -8
View File
@@ -4,6 +4,7 @@ import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/traefik/traefik/v3/pkg/provider/acme"
)
@@ -50,7 +51,7 @@ func TestConfiguration_SetEffectiveConfiguration(t *testing.T) {
{
desc: "empty",
conf: &Configuration{
Providers: &Providers{},
Providers: &Providers{Precedence: providerNames},
},
expected: &Configuration{
EntryPoints: EntryPoints{"http": &EntryPoint{
@@ -83,13 +84,13 @@ func TestConfiguration_SetEffectiveConfiguration(t *testing.T) {
Timeout: 3000000000,
},
}},
Providers: &Providers{},
Providers: &Providers{Precedence: providerNames},
},
},
{
desc: "ACME simple",
conf: &Configuration{
Providers: &Providers{},
Providers: &Providers{Precedence: providerNames},
CertificatesResolvers: map[string]CertificateResolver{
"foo": {
ACME: &acme.Configuration{
@@ -131,7 +132,7 @@ func TestConfiguration_SetEffectiveConfiguration(t *testing.T) {
Timeout: 3000000000,
},
}},
Providers: &Providers{},
Providers: &Providers{Precedence: providerNames},
CertificatesResolvers: map[string]CertificateResolver{
"foo": {
ACME: &acme.Configuration{
@@ -147,7 +148,7 @@ func TestConfiguration_SetEffectiveConfiguration(t *testing.T) {
{
desc: "ACME deprecation DelayBeforeCheck",
conf: &Configuration{
Providers: &Providers{},
Providers: &Providers{Precedence: providerNames},
CertificatesResolvers: map[string]CertificateResolver{
"foo": {
ACME: &acme.Configuration{
@@ -190,7 +191,7 @@ func TestConfiguration_SetEffectiveConfiguration(t *testing.T) {
Timeout: 3000000000,
},
}},
Providers: &Providers{},
Providers: &Providers{Precedence: providerNames},
CertificatesResolvers: map[string]CertificateResolver{
"foo": {
ACME: &acme.Configuration{
@@ -210,7 +211,7 @@ func TestConfiguration_SetEffectiveConfiguration(t *testing.T) {
{
desc: "ACME deprecation DisablePropagationCheck",
conf: &Configuration{
Providers: &Providers{},
Providers: &Providers{Precedence: providerNames},
CertificatesResolvers: map[string]CertificateResolver{
"foo": {
ACME: &acme.Configuration{
@@ -253,7 +254,7 @@ func TestConfiguration_SetEffectiveConfiguration(t *testing.T) {
Timeout: 3000000000,
},
}},
Providers: &Providers{},
Providers: &Providers{Precedence: providerNames},
CertificatesResolvers: map[string]CertificateResolver{
"foo": {
ACME: &acme.Configuration{
@@ -378,3 +379,55 @@ func TestValidateConfiguration_BasePath(t *testing.T) {
})
}
}
func TestProvidersPrecedence(t *testing.T) {
testCases := []struct {
desc string
cfg *Configuration
expectedError bool
expected []string
}{
{
desc: "No precedence",
cfg: &Configuration{
Providers: &Providers{
Precedence: providerNames,
},
},
expected: providerNames,
},
{
desc: "Precedence with non existing provider",
cfg: &Configuration{
Providers: &Providers{
Precedence: []string{"unknown"},
},
},
expectedError: true,
},
{
desc: "Precedence with upper case provider",
cfg: &Configuration{
Providers: &Providers{
Precedence: []string{"DOCKER"},
},
},
expected: []string{"docker"},
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
test.cfg.SetEffectiveConfiguration()
err := test.cfg.ValidateConfiguration()
if test.expectedError {
require.Error(t, err)
} else {
require.NoError(t, err)
assert.Equal(t, test.expected, test.cfg.Providers.Precedence)
}
})
}
}
+22 -22
View File
@@ -72,9 +72,9 @@ func TestClientIPMatcher(t *testing.T) {
parser, err := NewSyntaxParser()
require.NoError(t, err)
muxer := NewMuxer(parser)
muxer := NewMuxer(parser, nil)
err = muxer.AddRoute(test.rule, "", 0, handler)
err = muxer.AddRoute(test.rule, "", 0, "", handler)
if test.expectedError {
require.Error(t, err)
return
@@ -147,9 +147,9 @@ func TestMethodMatcher(t *testing.T) {
parser, err := NewSyntaxParser()
require.NoError(t, err)
muxer := NewMuxer(parser)
muxer := NewMuxer(parser, nil)
err = muxer.AddRoute(test.rule, "", 0, handler)
err = muxer.AddRoute(test.rule, "", 0, "", handler)
if test.expectedError {
require.Error(t, err)
return
@@ -276,9 +276,9 @@ func TestHostMatcher(t *testing.T) {
parser, err := NewSyntaxParser()
require.NoError(t, err)
muxer := NewMuxer(parser)
muxer := NewMuxer(parser, nil)
err = muxer.AddRoute(test.rule, "", 0, handler)
err = muxer.AddRoute(test.rule, "", 0, "", handler)
if test.expectedError {
require.Error(t, err)
return
@@ -377,9 +377,9 @@ func TestHostRegexpMatcher(t *testing.T) {
parser, err := NewSyntaxParser()
require.NoError(t, err)
muxer := NewMuxer(parser)
muxer := NewMuxer(parser, nil)
err = muxer.AddRoute(test.rule, "", 0, handler)
err = muxer.AddRoute(test.rule, "", 0, "", handler)
if test.expectedError {
require.Error(t, err)
return
@@ -452,9 +452,9 @@ func TestPathMatcher(t *testing.T) {
parser, err := NewSyntaxParser()
require.NoError(t, err)
muxer := NewMuxer(parser)
muxer := NewMuxer(parser, nil)
err = muxer.AddRoute(test.rule, "", 0, handler)
err = muxer.AddRoute(test.rule, "", 0, "", handler)
if test.expectedError {
require.Error(t, err)
return
@@ -546,9 +546,9 @@ func TestPathRegexpMatcher(t *testing.T) {
parser, err := NewSyntaxParser()
require.NoError(t, err)
muxer := NewMuxer(parser)
muxer := NewMuxer(parser, nil)
err = muxer.AddRoute(test.rule, "", 0, handler)
err = muxer.AddRoute(test.rule, "", 0, "", handler)
if test.expectedError {
require.Error(t, err)
return
@@ -619,9 +619,9 @@ func TestPathPrefixMatcher(t *testing.T) {
parser, err := NewSyntaxParser()
require.NoError(t, err)
muxer := NewMuxer(parser)
muxer := NewMuxer(parser, nil)
err = muxer.AddRoute(test.rule, "", 0, handler)
err = muxer.AddRoute(test.rule, "", 0, "", handler)
if test.expectedError {
require.Error(t, err)
return
@@ -707,9 +707,9 @@ func TestHeaderMatcher(t *testing.T) {
parser, err := NewSyntaxParser()
require.NoError(t, err)
muxer := NewMuxer(parser)
muxer := NewMuxer(parser, nil)
err = muxer.AddRoute(test.rule, "", 0, handler)
err = muxer.AddRoute(test.rule, "", 0, "", handler)
if test.expectedError {
require.Error(t, err)
return
@@ -816,9 +816,9 @@ func TestHeaderRegexpMatcher(t *testing.T) {
parser, err := NewSyntaxParser()
require.NoError(t, err)
muxer := NewMuxer(parser)
muxer := NewMuxer(parser, nil)
err = muxer.AddRoute(test.rule, "", 0, handler)
err = muxer.AddRoute(test.rule, "", 0, "", handler)
if test.expectedError {
require.Error(t, err)
return
@@ -906,9 +906,9 @@ func TestQueryMatcher(t *testing.T) {
parser, err := NewSyntaxParser()
require.NoError(t, err)
muxer := NewMuxer(parser)
muxer := NewMuxer(parser, nil)
err = muxer.AddRoute(test.rule, "", 0, handler)
err = muxer.AddRoute(test.rule, "", 0, "", handler)
if test.expectedError {
require.Error(t, err)
return
@@ -1021,9 +1021,9 @@ func TestQueryRegexpMatcher(t *testing.T) {
parser, err := NewSyntaxParser()
require.NoError(t, err)
muxer := NewMuxer(parser)
muxer := NewMuxer(parser, nil)
err = muxer.AddRoute(test.rule, "", 0, handler)
err = muxer.AddRoute(test.rule, "", 0, "", handler)
if test.expectedError {
require.Error(t, err)
return
+20 -20
View File
@@ -76,9 +76,9 @@ func TestClientIPV2Matcher(t *testing.T) {
parser, err := NewSyntaxParser()
require.NoError(t, err)
muxer := NewMuxer(parser)
muxer := NewMuxer(parser, nil)
err = muxer.AddRoute(test.rule, "v2", 0, handler)
err = muxer.AddRoute(test.rule, "v2", 0, "", handler)
if test.expectedError {
require.Error(t, err)
return
@@ -154,9 +154,9 @@ func TestMethodV2Matcher(t *testing.T) {
parser, err := NewSyntaxParser()
require.NoError(t, err)
muxer := NewMuxer(parser)
muxer := NewMuxer(parser, nil)
err = muxer.AddRoute(test.rule, "v2", 0, handler)
err = muxer.AddRoute(test.rule, "v2", 0, "", handler)
if test.expectedError {
require.Error(t, err)
return
@@ -280,9 +280,9 @@ func TestHostV2Matcher(t *testing.T) {
parser, err := NewSyntaxParser()
require.NoError(t, err)
muxer := NewMuxer(parser)
muxer := NewMuxer(parser, nil)
err = muxer.AddRoute(test.rule, "v2", 0, handler)
err = muxer.AddRoute(test.rule, "v2", 0, "", handler)
if test.expectedError {
require.Error(t, err)
return
@@ -384,9 +384,9 @@ func TestHostRegexpV2Matcher(t *testing.T) {
parser, err := NewSyntaxParser()
require.NoError(t, err)
muxer := NewMuxer(parser)
muxer := NewMuxer(parser, nil)
err = muxer.AddRoute(test.rule, "v2", 0, handler)
err = muxer.AddRoute(test.rule, "v2", 0, "", handler)
if test.expectedError {
require.Error(t, err)
return
@@ -480,9 +480,9 @@ func TestPathV2Matcher(t *testing.T) {
parser, err := NewSyntaxParser()
require.NoError(t, err)
muxer := NewMuxer(parser)
muxer := NewMuxer(parser, nil)
err = muxer.AddRoute(test.rule, "v2", 0, handler)
err = muxer.AddRoute(test.rule, "v2", 0, "", handler)
if test.expectedError {
require.Error(t, err)
return
@@ -574,9 +574,9 @@ func TestPathPrefixV2Matcher(t *testing.T) {
parser, err := NewSyntaxParser()
require.NoError(t, err)
muxer := NewMuxer(parser)
muxer := NewMuxer(parser, nil)
err = muxer.AddRoute(test.rule, "v2", 0, handler)
err = muxer.AddRoute(test.rule, "v2", 0, "", handler)
if test.expectedError {
require.Error(t, err)
return
@@ -662,9 +662,9 @@ func TestHeadersMatcher(t *testing.T) {
parser, err := NewSyntaxParser()
require.NoError(t, err)
muxer := NewMuxer(parser)
muxer := NewMuxer(parser, nil)
err = muxer.AddRoute(test.rule, "v2", 0, handler)
err = muxer.AddRoute(test.rule, "v2", 0, "", handler)
if test.expectedError {
require.Error(t, err)
return
@@ -771,9 +771,9 @@ func TestHeaderRegexpV2Matcher(t *testing.T) {
parser, err := NewSyntaxParser()
require.NoError(t, err)
muxer := NewMuxer(parser)
muxer := NewMuxer(parser, nil)
err = muxer.AddRoute(test.rule, "v2", 0, handler)
err = muxer.AddRoute(test.rule, "v2", 0, "", handler)
if test.expectedError {
require.Error(t, err)
return
@@ -865,9 +865,9 @@ func TestHostRegexp(t *testing.T) {
parser, err := NewSyntaxParser()
require.NoError(t, err)
muxer := NewMuxer(parser)
muxer := NewMuxer(parser, nil)
err = muxer.AddRoute(test.hostExp, "v2", 0, handler)
err = muxer.AddRoute(test.hostExp, "v2", 0, "", handler)
require.NoError(t, err)
results := make(map[string]int)
@@ -1534,9 +1534,9 @@ func Test_addRoute(t *testing.T) {
parser, err := NewSyntaxParser()
require.NoError(t, err)
muxer := NewMuxer(parser)
muxer := NewMuxer(parser, nil)
err = muxer.AddRoute(test.rule, "v2", 0, handler)
err = muxer.AddRoute(test.rule, "v2", 0, "", handler)
if test.expectedError {
require.Error(t, err)
} else {
+22 -10
View File
@@ -6,6 +6,7 @@ import (
"fmt"
"net/http"
"net/url"
"slices"
"sort"
"strings"
@@ -24,15 +25,20 @@ type MatcherFunc func(*http.Request) bool
type Muxer struct {
routes routes
parser SyntaxParser
defaultHandler http.Handler
parser SyntaxParser
defaultHandler http.Handler
providersPrecedence []string
}
// NewMuxer returns a new muxer instance.
func NewMuxer(parser SyntaxParser) *Muxer {
func NewMuxer(parser SyntaxParser, providersPrecedence []string) *Muxer {
providersPrecedence = slices.Clone(providersPrecedence)
slices.Reverse(providersPrecedence)
return &Muxer{
parser: parser,
defaultHandler: http.NotFoundHandler(),
parser: parser,
defaultHandler: http.NotFoundHandler(),
providersPrecedence: providersPrecedence,
}
}
@@ -71,16 +77,17 @@ func GetRulePriority(rule string) int {
}
// AddRoute add a new route to the router.
func (m *Muxer) AddRoute(rule string, syntax string, priority int, handler http.Handler) error {
func (m *Muxer) AddRoute(rule string, syntax string, priority int, providerName string, handler http.Handler) error {
matchers, err := m.parser.parse(syntax, rule)
if err != nil {
return fmt.Errorf("error while parsing rule %s: %w", rule, err)
}
m.routes = append(m.routes, &route{
handler: handler,
matchers: matchers,
priority: priority,
handler: handler,
matchers: matchers,
priority: priority,
providerPriority: slices.Index(m.providersPrecedence, providerName),
})
sort.Sort(m.routes)
@@ -206,7 +213,10 @@ func (r routes) Len() int { return len(r) }
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 }
func (r routes) Less(i, j int) bool {
return r[i].priority > r[j].priority ||
(r[i].priority == r[j].priority && r[i].providerPriority > r[j].providerPriority)
}
// route holds the matchers to match HTTP route,
// and the handler that will serve the request.
@@ -218,6 +228,8 @@ type route struct {
// priority is used to disambiguate between two (or more) rules that would all match for a given request.
// Computed from the matching rule length, if not user-set.
priority int
// providerPriority is used to disambiguate between two (or more) rules that would all match for a given request and have the same priority.
providerPriority int
}
// matchersTree represents the matchers tree structure.
+124 -9
View File
@@ -228,10 +228,10 @@ func TestMuxer(t *testing.T) {
parser, err := NewSyntaxParser()
require.NoError(t, err)
muxer := NewMuxer(parser)
muxer := NewMuxer(parser, nil)
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})
err = muxer.AddRoute(test.rule, "", 0, handler)
err = muxer.AddRoute(test.rule, "", 0, "", handler)
if test.expectedError {
require.Error(t, err)
return
@@ -264,9 +264,10 @@ func TestMuxer(t *testing.T) {
func Test_addRoutePriority(t *testing.T) {
type Case struct {
xFrom string
rule string
priority int
xFrom string
rule string
priority int
providerName string
}
testCases := []struct {
@@ -375,6 +376,120 @@ func Test_addRoutePriority(t *testing.T) {
},
expected: "header3",
},
{
desc: "Same priority and rule, kubernetescrd wins over kubernetes",
path: "/my",
cases: []Case{
{
xFrom: "header1",
rule: "PathPrefix(`/my`)",
priority: 10,
providerName: "kubernetes",
},
{
xFrom: "header2",
rule: "PathPrefix(`/my`)",
priority: 10,
providerName: "kubernetescrd",
},
},
expected: "header2",
},
{
desc: "Same priority and rule, kubernetesgateway wins over kubernetescrd",
path: "/my",
cases: []Case{
{
xFrom: "header1",
rule: "PathPrefix(`/my`)",
priority: 10,
providerName: "kubernetescrd",
},
{
xFrom: "header2",
rule: "PathPrefix(`/my`)",
priority: 10,
providerName: "kubernetesgateway",
},
},
expected: "header2",
},
{
desc: "Same priority and rule, kubernetesgateway wins over kubernetes",
path: "/my",
cases: []Case{
{
xFrom: "header1",
rule: "PathPrefix(`/my`)",
priority: 10,
providerName: "kubernetesgateway",
},
{
xFrom: "header2",
rule: "PathPrefix(`/my`)",
priority: 10,
providerName: "kubernetes",
},
},
expected: "header1",
},
{
desc: "Same priority and rule, kubernetescrd wins over kubernetesingressnginx",
path: "/my",
cases: []Case{
{
xFrom: "header1",
rule: "PathPrefix(`/my`)",
priority: 10,
providerName: "kubernetesingressnginx",
},
{
xFrom: "header2",
rule: "PathPrefix(`/my`)",
priority: 10,
providerName: "kubernetescrd",
},
},
expected: "header2",
},
{
desc: "Same priority and rule, known provider wins over unknown provider",
path: "/my",
cases: []Case{
{
xFrom: "header1",
rule: "PathPrefix(`/my`)",
priority: 10,
providerName: "unknownprovider",
},
{
xFrom: "header2",
rule: "PathPrefix(`/my`)",
priority: 10,
providerName: "kubernetes",
},
},
expected: "header2",
},
{
desc: "Higher numeric priority wins regardless of provider",
path: "/my",
cases: []Case{
{
xFrom: "header1",
rule: "PathPrefix(`/my`)",
priority: 20,
providerName: "kubernetesingressnginx",
},
{
xFrom: "header2",
rule: "PathPrefix(`/my`)",
priority: 10,
providerName: "kubernetesgateway",
},
},
expected: "header1",
},
}
for _, test := range testCases {
@@ -383,7 +498,7 @@ func Test_addRoutePriority(t *testing.T) {
parser, err := NewSyntaxParser()
require.NoError(t, err)
muxer := NewMuxer(parser)
muxer := NewMuxer(parser, []string{"kubernetesgateway", "kubernetescrd", "kubernetes", "kubernetesingressnginx"})
for _, route := range test.cases {
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
@@ -394,7 +509,7 @@ func Test_addRoutePriority(t *testing.T) {
route.priority = GetRulePriority(route.rule)
}
err := muxer.AddRoute(route.rule, "", route.priority, handler)
err := muxer.AddRoute(route.rule, "", route.priority, route.providerName, handler)
require.NoError(t, err, route.rule)
}
@@ -517,9 +632,9 @@ func TestEmptyHost(t *testing.T) {
parser, err := NewSyntaxParser()
require.NoError(t, err)
muxer := NewMuxer(parser)
muxer := NewMuxer(parser, nil)
err = muxer.AddRoute(test.rule, "", 0, handler)
err = muxer.AddRoute(test.rule, "", 0, "", handler)
require.NoError(t, err)
// RequestDecorator is necessary for the host rule
+10 -10
View File
@@ -34,10 +34,10 @@ func Test_HostSNICatchAll(t *testing.T) {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
muxer, err := NewMuxer()
muxer, err := NewMuxer(nil)
require.NoError(t, err)
err = muxer.AddRoute(test.rule, "", 0, tcp.HandlerFunc(func(conn tcp.WriteCloser) {}))
err = muxer.AddRoute(test.rule, "", 0, "", tcp.HandlerFunc(func(conn tcp.WriteCloser) {}))
require.NoError(t, err)
handler, catchAll := muxer.Match(ConnData{
@@ -146,10 +146,10 @@ func Test_HostSNI(t *testing.T) {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
muxer, err := NewMuxer()
muxer, err := NewMuxer(nil)
require.NoError(t, err)
err = muxer.AddRoute(test.rule, "", 0, tcp.HandlerFunc(func(conn tcp.WriteCloser) {}))
err = muxer.AddRoute(test.rule, "", 0, "", tcp.HandlerFunc(func(conn tcp.WriteCloser) {}))
if test.buildErr {
require.Error(t, err)
return
@@ -228,10 +228,10 @@ func Test_HostSNIRegexp(t *testing.T) {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
muxer, err := NewMuxer()
muxer, err := NewMuxer(nil)
require.NoError(t, err)
err = muxer.AddRoute(test.rule, "", 0, tcp.HandlerFunc(func(conn tcp.WriteCloser) {}))
err = muxer.AddRoute(test.rule, "", 0, "", tcp.HandlerFunc(func(conn tcp.WriteCloser) {}))
if test.buildErr {
require.Error(t, err)
return
@@ -299,10 +299,10 @@ func Test_ClientIP(t *testing.T) {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
muxer, err := NewMuxer()
muxer, err := NewMuxer(nil)
require.NoError(t, err)
err = muxer.AddRoute(test.rule, "", 0, tcp.HandlerFunc(func(conn tcp.WriteCloser) {}))
err = muxer.AddRoute(test.rule, "", 0, "", tcp.HandlerFunc(func(conn tcp.WriteCloser) {}))
if test.buildErr {
require.Error(t, err)
return
@@ -362,10 +362,10 @@ func Test_ALPN(t *testing.T) {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
muxer, err := NewMuxer()
muxer, err := NewMuxer(nil)
require.NoError(t, err)
err = muxer.AddRoute(test.rule, "", 0, tcp.HandlerFunc(func(conn tcp.WriteCloser) {}))
err = muxer.AddRoute(test.rule, "", 0, "", tcp.HandlerFunc(func(conn tcp.WriteCloser) {}))
if test.buildErr {
require.Error(t, err)
return
+4 -4
View File
@@ -470,10 +470,10 @@ func Test_addTCPRouteV2(t *testing.T) {
require.NoError(t, err)
})
router, err := NewMuxer()
router, err := NewMuxer(nil)
require.NoError(t, err)
err = router.AddRoute(test.rule, "v2", 0, handler)
err = router.AddRoute(test.rule, "v2", 0, "", handler)
if test.routeErr {
require.Error(t, err)
return
@@ -606,10 +606,10 @@ func Test_HostSNICatchAllV2(t *testing.T) {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
muxer, err := NewMuxer()
muxer, err := NewMuxer(nil)
require.NoError(t, err)
err = muxer.AddRoute(test.rule, "v2", 0, tcp.HandlerFunc(func(conn tcp.WriteCloser) {}))
err = muxer.AddRoute(test.rule, "v2", 0, "", tcp.HandlerFunc(func(conn tcp.WriteCloser) {}))
require.NoError(t, err)
handler, catchAll := muxer.Match(ConnData{
+25 -12
View File
@@ -3,6 +3,7 @@ package tcp
import (
"fmt"
"net"
"slices"
"sort"
"strings"
@@ -39,13 +40,15 @@ func NewConnData(serverName string, conn tcp.WriteCloser, alpnProtos []string) (
// Muxer defines a muxer that handles TCP routing with rules.
type Muxer struct {
routes routes
parser predicate.Parser
parserV2 predicate.Parser
routes routes
parser predicate.Parser
parserV2 predicate.Parser
providersPrecedence []string
}
// NewMuxer returns a TCP muxer.
func NewMuxer() (*Muxer, error) {
func NewMuxer(providersPrecedence []string) (*Muxer, error) {
var matcherNames []string
for matcherName := range tcpFuncs {
matcherNames = append(matcherNames, matcherName)
@@ -66,9 +69,13 @@ func NewMuxer() (*Muxer, error) {
return nil, fmt.Errorf("error while creating v2 rules parser: %w", err)
}
providersPrecedence = slices.Clone(providersPrecedence)
slices.Reverse(providersPrecedence)
return &Muxer{
parser: parser,
parserV2: parserV2,
parser: parser,
parserV2: parserV2,
providersPrecedence: providersPrecedence,
}, nil
}
@@ -118,7 +125,7 @@ func GetRulePriority(rule string) int {
// AddRoute adds a new route, associated to the given handler, at the given
// priority, to the muxer.
func (m *Muxer) AddRoute(rule string, syntax string, priority int, handler tcp.Handler) error {
func (m *Muxer) AddRoute(rule string, syntax string, priority int, providerName string, handler tcp.Handler) error {
var parse any
var err error
var matcherFuncs map[string]func(*matchersTree, ...string) error
@@ -159,10 +166,11 @@ func (m *Muxer) AddRoute(rule string, syntax string, priority int, handler tcp.H
}
newRoute := &route{
handler: handler,
matchers: matchers,
catchAll: catchAll,
priority: priority,
handler: handler,
matchers: matchers,
catchAll: catchAll,
priority: priority,
providerPriority: slices.Index(m.providersPrecedence, providerName),
}
m.routes = append(m.routes, newRoute)
@@ -215,7 +223,10 @@ func (r routes) Len() int { return len(r) }
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 }
func (r routes) Less(i, j int) bool {
return r[i].priority > r[j].priority ||
(r[i].priority == r[j].priority && r[i].providerPriority > r[j].providerPriority)
}
// route holds the matchers to match TCP route,
// and the handler that will serve the connection.
@@ -230,6 +241,8 @@ type route struct {
// all match for a given request.
// Computed from the matching rule length, if not user-set.
priority int
// providerPriority is used to disambiguate between two (or more) rules that would all match for a given request and have the same priority.
providerPriority int
}
// matchersTree represents the matchers tree structure.
+56 -32
View File
@@ -272,10 +272,10 @@ func Test_addTCPRoute(t *testing.T) {
require.NoError(t, err)
})
router, err := NewMuxer()
router, err := NewMuxer(nil)
require.NoError(t, err)
err = router.AddRoute(test.rule, "", 0, handler)
err = router.AddRoute(test.rule, "", 0, "", handler)
if test.routeErr {
require.Error(t, err)
return
@@ -388,47 +388,66 @@ func TestParseHostSNI(t *testing.T) {
}
func Test_Priority(t *testing.T) {
type rule struct {
rule string
priority int
provider string
}
testCases := []struct {
desc string
rules map[string]int
serverName string
expectedRule string
desc string
rules []rule
serverName string
expectedRule string
expectedProvider string
}{
{
desc: "One matching rule, calculated priority",
rules: map[string]int{
"HostSNI(`example.com`)": 0,
"HostSNI(`example.org`)": 0,
desc: "One matching rule, same priority",
rules: []rule{
{rule: "HostSNI(`example.com`)"},
{rule: "HostSNI(`example.org`)"},
},
expectedRule: "HostSNI(`example.com`)",
serverName: "example.com",
expectedRule: "HostSNI(`example.com`)",
},
{
desc: "One matching rule, custom priority",
rules: map[string]int{
"HostSNI(`example.org`)": 0,
"HostSNI(`example.com`)": 10000,
rules: []rule{
{rule: "HostSNI(`example.com`)", priority: 10000},
{rule: "HostSNI(`example.org`)"},
},
expectedRule: "HostSNI(`example.org`)",
serverName: "example.org",
expectedRule: "HostSNI(`example.org`)",
},
{
desc: "Two matching rules, calculated priority",
rules: map[string]int{
"HostSNI(`example.org`)": 0,
"HostSNI(`example.com`)": 0,
desc: "Same rule and priority, kubernetescrd wins over kubernetes",
rules: []rule{
{rule: "HostSNI(`example.org`)", provider: "kubernetescrd"},
{rule: "HostSNI(`example.org`)", provider: "kubernetes"},
},
expectedRule: "HostSNI(`example.org`)",
serverName: "example.org",
serverName: "example.org",
expectedRule: "HostSNI(`example.org`)",
expectedProvider: "kubernetescrd",
},
{
desc: "Two matching rules, custom priority",
rules: map[string]int{
"HostSNI(`example.com`)": 10000,
"HostSNI(`example.org`)": 0,
desc: "Same rule and priority, known provider wins over unknown provider",
rules: []rule{
{rule: "HostSNI(`example.org`)", provider: "kubernetescrd"},
{rule: "HostSNI(`example.org`)", provider: "foo"},
},
expectedRule: "HostSNI(`example.com`)",
serverName: "example.com",
serverName: "example.org",
expectedRule: "HostSNI(`example.org`)",
expectedProvider: "kubernetescrd",
},
{
desc: "Same rule, higher numeric priority wins regardless of provider",
rules: []rule{
{rule: "HostSNI(`example.org`)", priority: 10, provider: "kubernetescrd"},
{rule: "HostSNI(`example.org`)", priority: 20, provider: "kubernetes"},
},
serverName: "example.org",
expectedRule: "HostSNI(`example.org`)",
expectedProvider: "kubernetes",
},
}
@@ -436,13 +455,17 @@ func Test_Priority(t *testing.T) {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
muxer, err := NewMuxer()
muxer, err := NewMuxer([]string{"kubernetescrd", "kubernetes"})
require.NoError(t, err)
matchedRule := ""
for rule, priority := range test.rules {
err := muxer.AddRoute(rule, "", priority, tcp.HandlerFunc(func(conn tcp.WriteCloser) {
matchedRule = rule
var (
matchedRule string
matchedProvider string
)
for _, r := range test.rules {
err := muxer.AddRoute(r.rule, "", r.priority, r.provider, tcp.HandlerFunc(func(conn tcp.WriteCloser) {
matchedRule = r.rule
matchedProvider = r.provider
}))
require.NoError(t, err)
}
@@ -454,6 +477,7 @@ func Test_Priority(t *testing.T) {
handler.ServeTCP(nil)
assert.Equal(t, test.expectedRule, matchedRule)
assert.Equal(t, test.expectedProvider, matchedProvider)
})
}
}
+5 -5
View File
@@ -27,8 +27,8 @@ import (
// defaultTemplateRule is the default template for the default rule.
const defaultTemplateRule = "Host(`{{ normalize .Name }}`)"
// providerName is the Consul Catalog provider name.
const providerName = "consulcatalog"
// ProviderName is the Consul Catalog provider name.
const ProviderName = "consulcatalog"
var _ provider.Provider = (*Provider)(nil)
@@ -58,7 +58,7 @@ func (p *ProviderBuilder) BuildProviders() []*Provider {
if len(p.Namespaces) == 0 {
return []*Provider{{
Configuration: p.Configuration,
name: providerName,
name: ProviderName,
}}
}
@@ -66,7 +66,7 @@ func (p *ProviderBuilder) BuildProviders() []*Provider {
for _, namespace := range p.Namespaces {
providers = append(providers, &Provider{
Configuration: p.Configuration,
name: providerName + "-" + namespace,
name: ProviderName + "-" + namespace,
namespace: namespace,
})
}
@@ -145,7 +145,7 @@ func (p *Provider) Init() error {
// In case they didn't initialize Provider with BuildProviders.
if p.name == "" {
p.name = providerName
p.name = ProviderName
}
return nil
+6 -5
View File
@@ -21,7 +21,8 @@ import (
"github.com/traefik/traefik/v3/pkg/safe"
)
const dockerName = "docker"
// DockerName is the docker provider name.
const DockerName = "docker"
var _ provider.Provider = (*Provider)(nil)
@@ -53,7 +54,7 @@ func (p *Provider) Init() error {
// Provide allows the docker provider to provide configurations to traefik using the given configuration channel.
func (p *Provider) Provide(configurationChan chan<- dynamic.Message, pool *safe.Pool) error {
pool.GoCtx(func(routineCtx context.Context) {
logger := log.Ctx(routineCtx).With().Str(logs.ProviderName, dockerName).Logger()
logger := log.Ctx(routineCtx).With().Str(logs.ProviderName, DockerName).Logger()
ctxLog := logger.WithContext(routineCtx)
operation := func() error {
@@ -61,7 +62,7 @@ func (p *Provider) Provide(configurationChan chan<- dynamic.Message, pool *safe.
ctx, cancel := context.WithCancel(ctxLog)
defer cancel()
ctx = log.Ctx(ctx).With().Str(logs.ProviderName, dockerName).Logger().WithContext(ctx)
ctx = log.Ctx(ctx).With().Str(logs.ProviderName, DockerName).Logger().WithContext(ctx)
dockerClient, err := p.createClient(ctxLog)
if err != nil {
@@ -88,7 +89,7 @@ func (p *Provider) Provide(configurationChan chan<- dynamic.Message, pool *safe.
configuration := builder.build(ctxLog, dockerDataList)
configurationChan <- dynamic.Message{
ProviderName: dockerName,
ProviderName: DockerName,
Configuration: configuration,
}
@@ -111,7 +112,7 @@ func (p *Provider) Provide(configurationChan chan<- dynamic.Message, pool *safe.
configuration := builder.build(ctx, containers)
if configuration != nil {
message := dynamic.Message{
ProviderName: dockerName,
ProviderName: DockerName,
Configuration: configuration,
}
select {
+7 -6
View File
@@ -22,7 +22,8 @@ import (
"github.com/traefik/traefik/v3/pkg/safe"
)
const swarmName = "swarm"
// SwarmName is the swarm provider name.
const SwarmName = "swarm"
var _ provider.Provider = (*SwarmProvider)(nil)
@@ -57,7 +58,7 @@ func (p *SwarmProvider) Init() error {
// Provide allows the docker provider to provide configurations to traefik using the given configuration channel.
func (p *SwarmProvider) Provide(configurationChan chan<- dynamic.Message, pool *safe.Pool) error {
pool.GoCtx(func(routineCtx context.Context) {
logger := log.Ctx(routineCtx).With().Str(logs.ProviderName, swarmName).Logger()
logger := log.Ctx(routineCtx).With().Str(logs.ProviderName, SwarmName).Logger()
ctxLog := logger.WithContext(routineCtx)
operation := func() error {
@@ -65,7 +66,7 @@ func (p *SwarmProvider) Provide(configurationChan chan<- dynamic.Message, pool *
ctx, cancel := context.WithCancel(ctxLog)
defer cancel()
ctx = log.Ctx(ctx).With().Str(logs.ProviderName, swarmName).Logger().WithContext(ctx)
ctx = log.Ctx(ctx).With().Str(logs.ProviderName, SwarmName).Logger().WithContext(ctx)
dockerClient, err := p.createClient(ctx)
if err != nil {
@@ -92,7 +93,7 @@ func (p *SwarmProvider) Provide(configurationChan chan<- dynamic.Message, pool *
configuration := builder.build(ctxLog, dockerDataList)
configurationChan <- dynamic.Message{
ProviderName: swarmName,
ProviderName: SwarmName,
Configuration: configuration,
}
if p.Watch {
@@ -102,7 +103,7 @@ func (p *SwarmProvider) Provide(configurationChan chan<- dynamic.Message, pool *
ticker := time.NewTicker(time.Duration(p.RefreshSeconds))
pool.GoCtx(func(ctx context.Context) {
logger := log.Ctx(ctx).With().Str(logs.ProviderName, swarmName).Logger()
logger := log.Ctx(ctx).With().Str(logs.ProviderName, SwarmName).Logger()
ctx = logger.WithContext(ctx)
defer close(errChan)
@@ -119,7 +120,7 @@ func (p *SwarmProvider) Provide(configurationChan chan<- dynamic.Message, pool *
configuration := builder.build(ctx, services)
if configuration != nil {
configurationChan <- dynamic.Message{
ProviderName: swarmName,
ProviderName: SwarmName,
Configuration: configuration,
}
}
+4 -1
View File
@@ -29,6 +29,9 @@ import (
"github.com/traefik/traefik/v3/pkg/safe"
)
// ProviderName is the ECS provider name.
const ProviderName = "ecs"
// Provider holds configurations of the provider.
type Provider struct {
Constraints string `description:"Constraints is an expression that Traefik matches against the container's labels to determine whether to create any route for that container." json:"constraints,omitempty" toml:"constraints,omitempty" yaml:"constraints,omitempty" export:"true"`
@@ -107,7 +110,7 @@ func (p *Provider) Init() error {
// Provide configuration to traefik from ECS.
func (p *Provider) Provide(configurationChan chan<- dynamic.Message, pool *safe.Pool) error {
pool.GoCtx(func(routineCtx context.Context) {
logger := log.Ctx(routineCtx).With().Str(logs.ProviderName, "ecs").Logger()
logger := log.Ctx(routineCtx).With().Str(logs.ProviderName, ProviderName).Logger()
ctxLog := logger.WithContext(routineCtx)
operation := func() error {
+5 -4
View File
@@ -26,7 +26,8 @@ import (
"github.com/traefik/traefik/v3/pkg/types"
)
const providerName = "file"
// ProviderName is the file provider name.
const ProviderName = "file"
var _ provider.Provider = (*Provider)(nil)
@@ -52,7 +53,7 @@ func (p *Provider) Init() error {
// Provide allows the file provider to provide configurations to traefik
// using the given configuration channel.
func (p *Provider) Provide(configurationChan chan<- dynamic.Message, pool *safe.Pool) error {
logger := log.With().Str(logs.ProviderName, providerName).Logger()
logger := log.With().Str(logs.ProviderName, ProviderName).Logger()
if p.Watch {
var watchItems []string
@@ -174,7 +175,7 @@ func (p *Provider) addWatcher(pool *safe.Pool, items []string, configurationChan
// Process events
pool.GoCtx(func(ctx context.Context) {
logger := log.With().Str(logs.ProviderName, providerName).Logger()
logger := log.With().Str(logs.ProviderName, ProviderName).Logger()
defer watcher.Close()
for {
select {
@@ -218,7 +219,7 @@ func (p *Provider) applyConfiguration(configurationChan chan<- dynamic.Message)
// buildConfiguration loads configuration either from file or a directory
// specified by 'Filename'/'Directory' and returns a 'Configuration' object.
func (p *Provider) buildConfiguration() (*dynamic.Configuration, error) {
ctx := log.With().Str(logs.ProviderName, providerName).Logger().WithContext(context.Background())
ctx := log.With().Str(logs.ProviderName, ProviderName).Logger().WithContext(context.Background())
if len(p.Directory) > 0 {
configurations, err := p.collectFileConfigs(ctx, p.Directory, "")
+4 -1
View File
@@ -25,6 +25,9 @@ import (
var _ provider.Provider = (*Provider)(nil)
// ProviderName is the http provider name.
const ProviderName = "http"
const defaultMaxResponseBodySize = -1
// Provider is a provider.Provider implementation that queries an HTTP(s) endpoint for a configuration.
@@ -78,7 +81,7 @@ func (p *Provider) Init() error {
// Provide allows the provider to provide configurations to traefik using the given configuration channel.
func (p *Provider) Provide(configurationChan chan<- dynamic.Message, pool *safe.Pool) error {
pool.GoCtx(func(routineCtx context.Context) {
logger := log.Ctx(routineCtx).With().Str(logs.ProviderName, "http").Logger()
logger := log.Ctx(routineCtx).With().Str(logs.ProviderName, ProviderName).Logger()
ctxLog := logger.WithContext(routineCtx)
operation := func() error {
+6 -5
View File
@@ -44,7 +44,8 @@ const (
)
const (
providerName = "kubernetescrd"
// ProviderName is the Kubernetes CRD provider name.
ProviderName = "kubernetescrd"
providerNamespaceSeparator = "@"
)
@@ -76,7 +77,7 @@ func (p *Provider) Init() error {
// Provide allows the k8s provider to provide configurations to traefik
// using the given configuration channel.
func (p *Provider) Provide(configurationChan chan<- dynamic.Message, pool *safe.Pool) error {
logger := log.With().Str(logs.ProviderName, providerName).Logger()
logger := log.With().Str(logs.ProviderName, ProviderName).Logger()
ctxLog := logger.WithContext(context.Background())
k8sClient, err := p.newK8sClient(ctxLog)
@@ -131,7 +132,7 @@ func (p *Provider) Provide(configurationChan chan<- dynamic.Message, pool *safe.
default:
p.lastConfiguration.Set(confHash)
configurationChan <- dynamic.Message{
ProviderName: providerName,
ProviderName: ProviderName,
Configuration: conf,
}
}
@@ -166,7 +167,7 @@ func (p *Provider) FillExtensionBuilderRegistry(registry gateway.ExtensionBuilde
return "", nil, fmt.Errorf("namespace %q is not allowed", namespace)
}
return makeID(namespace, name) + providerNamespaceSeparator + providerName, nil, nil
return makeID(namespace, name) + providerNamespaceSeparator + ProviderName, nil, nil
})
registry.RegisterBackendFuncs(traefikv1alpha1.GroupName, "TraefikService", func(name, namespace string) (string, *dynamic.Service, error) {
@@ -174,7 +175,7 @@ func (p *Provider) FillExtensionBuilderRegistry(registry gateway.ExtensionBuilde
return "", nil, fmt.Errorf("namespace %q is not allowed", namespace)
}
return makeID(namespace, name) + providerNamespaceSeparator + providerName, nil, nil
return makeID(namespace, name) + providerNamespaceSeparator + ProviderName, nil, nil
})
}
@@ -178,7 +178,7 @@ func makeMiddlewareKeys(ctx context.Context, namespace string, middlewares []tra
for _, mi := range middlewares {
name := mi.Name
if !allowCrossNamespace && strings.HasSuffix(mi.Name, providerNamespaceSeparator+providerName) {
if !allowCrossNamespace && strings.HasSuffix(mi.Name, providerNamespaceSeparator+ProviderName) {
// Since we are not able to know if another namespace is in the name (namespace-name@kubernetescrd),
// if the provider namespace kubernetescrd is used,
// we don't allow this format to avoid cross namespace references.
@@ -521,7 +521,7 @@ func (c configBuilder) makeServersTransportKey(parentNamespace string, serversTr
return "", nil
}
if !c.allowCrossNamespace && strings.HasSuffix(serversTransportName, providerNamespaceSeparator+providerName) {
if !c.allowCrossNamespace && strings.HasSuffix(serversTransportName, providerNamespaceSeparator+ProviderName) {
// Since we are not able to know if another namespace is in the name (namespace-name@kubernetescrd),
// if the provider namespace kubernetescrd is used,
// we don't allow this format to avoid cross namespace references.
@@ -543,7 +543,7 @@ func (c configBuilder) loadServers(parentNamespace string, svc traefikv1alpha1.L
}
// If the service uses explicitly the provider suffix
sanitizedName := strings.TrimSuffix(svc.Name, providerNamespaceSeparator+providerName)
sanitizedName := strings.TrimSuffix(svc.Name, providerNamespaceSeparator+ProviderName)
service, exists, err := c.client.GetService(namespace, sanitizedName)
if err != nil {
return nil, err
@@ -799,7 +799,7 @@ func fullServiceName(ctx context.Context, namespace string, service traefikv1alp
}
name, pName := splitSvcNameProvider(service.Name)
if pName == providerName {
if pName == ProviderName {
return provider.Normalize(fmt.Sprintf("%s-%s", namespace, name))
}
@@ -339,7 +339,7 @@ func (p *Provider) makeTCPServersTransportKey(parentNamespace string, serversTra
return "", nil
}
if !p.AllowCrossNamespace && strings.HasSuffix(serversTransportName, providerNamespaceSeparator+providerName) {
if !p.AllowCrossNamespace && strings.HasSuffix(serversTransportName, providerNamespaceSeparator+ProviderName) {
// Since we are not able to know if another namespace is in the name (namespace-name@kubernetescrd),
// if the provider namespace kubernetescrd is used,
// we don't allow this format to avoid cross namespace references.
@@ -34,7 +34,8 @@ import (
)
const (
providerName = "kubernetesgateway"
// ProviderName is the Kubernetes Gateway API provider name.
ProviderName = "kubernetesgateway"
controllerName = "traefik.io/gateway-controller"
@@ -166,7 +167,7 @@ func (p *Provider) SetRouterTransform(routerTransform k8s.RouterTransform) {
// Init the provider.
func (p *Provider) Init() error {
logger := log.With().Str(logs.ProviderName, providerName).Logger()
logger := log.With().Str(logs.ProviderName, ProviderName).Logger()
var err error
p.client, err = p.newK8sClient(logger.WithContext(context.Background()))
@@ -179,7 +180,7 @@ func (p *Provider) Init() error {
// Provide allows the k8s provider to provide configurations to traefik using the given configuration channel.
func (p *Provider) Provide(configurationChan chan<- dynamic.Message, pool *safe.Pool) error {
logger := log.With().Str(logs.ProviderName, providerName).Logger()
logger := log.With().Str(logs.ProviderName, ProviderName).Logger()
ctxLog := logger.WithContext(context.Background())
pool.GoCtx(func(ctxPool context.Context) {
@@ -221,7 +222,7 @@ func (p *Provider) Provide(configurationChan chan<- dynamic.Message, pool *safe.
default:
p.lastConfiguration.Set(confHash)
configurationChan <- dynamic.Message{
ProviderName: providerName,
ProviderName: ProviderName,
Configuration: conf,
}
}
@@ -32,7 +32,8 @@ import (
)
const (
providerName = "kubernetesingressnginx"
// ProviderName is the Kubernetes Ingress NGINX provider name.
ProviderName = "kubernetesingressnginx"
// NGINX default values.
annotationIngressClass = "kubernetes.io/ingress.class"
@@ -279,7 +280,7 @@ func (p *Provider) Init() error {
// Provide allows the k8s provider to provide configurations to traefik using the given configuration channel.
func (p *Provider) Provide(configurationChan chan<- dynamic.Message, pool *safe.Pool) error {
logger := log.With().Str(logs.ProviderName, providerName).Logger()
logger := log.With().Str(logs.ProviderName, ProviderName).Logger()
ctxLog := logger.WithContext(context.Background())
pool.GoCtx(func(ctxPool context.Context) {
@@ -323,7 +324,7 @@ func (p *Provider) Provide(configurationChan chan<- dynamic.Message, pool *safe.
default:
p.lastConfiguration.Set(confHash)
configurationChan <- dynamic.Message{
ProviderName: providerName,
ProviderName: ProviderName,
Configuration: conf,
}
}
@@ -74,10 +74,13 @@ func (p *Provider) Init() error {
return nil
}
// ProviderName is the Kubernetes Ingress provider name.
const ProviderName = "kubernetes"
// Provide allows the k8s provider to provide configurations to traefik
// using the given configuration channel.
func (p *Provider) Provide(configurationChan chan<- dynamic.Message, pool *safe.Pool) error {
logger := log.With().Str(logs.ProviderName, "kubernetes").Logger()
logger := log.With().Str(logs.ProviderName, ProviderName).Logger()
ctxLog := logger.WithContext(context.Background())
k8sClient, err := p.newK8sClient(ctxLog)
@@ -130,7 +133,7 @@ func (p *Provider) Provide(configurationChan chan<- dynamic.Message, pool *safe.
default:
p.lastConfiguration.Set(confHash)
configurationChan <- dynamic.Message{
ProviderName: "kubernetes",
ProviderName: ProviderName,
Configuration: conf,
}
}
@@ -2465,10 +2465,10 @@ func TestStrictPrefixMatchingRule(t *testing.T) {
parser, err := traefikhttp.NewSyntaxParser()
require.NoError(t, err)
muxer := traefikhttp.NewMuxer(parser)
muxer := traefikhttp.NewMuxer(parser, nil)
rule := buildStrictPrefixMatchingRule(tt.path)
err = muxer.AddRoute(rule, "", 0, handler)
err = muxer.AddRoute(rule, "", 0, "", handler)
require.NoError(t, err)
w := httptest.NewRecorder()
@@ -32,7 +32,8 @@ import (
)
const (
providerName = "knative"
// ProviderName is the Knative provider name.
ProviderName = "knative"
traefikIngressClassName = "traefik.ingress.networking.knative.dev"
)
@@ -61,7 +62,7 @@ type Provider struct {
// Init the provider.
func (p *Provider) Init() error {
logger := log.With().Str(logs.ProviderName, providerName).Logger()
logger := log.With().Str(logs.ProviderName, ProviderName).Logger()
// Initializes Kubernetes client.
var err error
@@ -75,7 +76,7 @@ func (p *Provider) Init() error {
// Provide allows the knative provider to provide configurations to traefik using the given configuration channel.
func (p *Provider) Provide(configurationChan chan<- dynamic.Message, pool *safe.Pool) error {
logger := log.With().Str(logs.ProviderName, providerName).Logger()
logger := log.With().Str(logs.ProviderName, ProviderName).Logger()
ctxLog := logger.WithContext(context.Background())
pool.GoCtx(func(ctxPool context.Context) {
@@ -117,7 +118,7 @@ func (p *Provider) Provide(configurationChan chan<- dynamic.Message, pool *safe.
default:
p.lastConfiguration.Set(confHash)
configurationChan <- dynamic.Message{
ProviderName: providerName,
ProviderName: ProviderName,
Configuration: conf,
}
}
+5 -5
View File
@@ -12,8 +12,8 @@ import (
"github.com/traefik/traefik/v3/pkg/types"
)
// providerName is the Consul provider name.
const providerName = "consul"
// ProviderName is the Consul provider name.
const ProviderName = "consul"
var _ provider.Provider = (*Provider)(nil)
@@ -38,7 +38,7 @@ func (p *ProviderBuilder) BuildProviders() []*Provider {
if len(p.Namespaces) == 0 {
return []*Provider{{
Provider: p.Provider,
name: providerName,
name: ProviderName,
token: p.Token,
tls: p.TLS,
}}
@@ -48,7 +48,7 @@ func (p *ProviderBuilder) BuildProviders() []*Provider {
for _, namespace := range p.Namespaces {
providers = append(providers, &Provider{
Provider: p.Provider,
name: providerName + "-" + namespace,
name: ProviderName + "-" + namespace,
namespace: namespace,
token: p.Token,
tls: p.TLS,
@@ -78,7 +78,7 @@ func (p *Provider) Init() error {
// In case they didn't initialize with BuildProviders.
if p.name == "" {
p.name = providerName
p.name = ProviderName
}
config := &consul.Config{
+4 -1
View File
@@ -11,6 +11,9 @@ import (
"github.com/traefik/traefik/v3/pkg/types"
)
// ProviderName is the Etcd provider name.
const ProviderName = "etcd"
var _ provider.Provider = (*Provider)(nil)
// Provider holds configurations of the provider.
@@ -44,5 +47,5 @@ func (p *Provider) Init() error {
}
}
return p.Provider.Init(etcdv3.StoreName, "etcd", config)
return p.Provider.Init(etcdv3.StoreName, ProviderName, config)
}
+4 -1
View File
@@ -11,6 +11,9 @@ import (
"github.com/traefik/traefik/v3/pkg/types"
)
// ProviderName is the Redis provider name.
const ProviderName = "redis"
var _ provider.Provider = (*Provider)(nil)
// Provider holds configurations of the provider.
@@ -90,5 +93,5 @@ func (p *Provider) Init() error {
}
}
return p.Provider.Init(redis.StoreName, "redis", config)
return p.Provider.Init(redis.StoreName, ProviderName, config)
}
+4 -1
View File
@@ -8,6 +8,9 @@ import (
"github.com/traefik/traefik/v3/pkg/provider/kv"
)
// ProviderName is the ZooKeeper provider name.
const ProviderName = "zookeeper"
var _ provider.Provider = (*Provider)(nil)
// Provider holds configurations of the provider.
@@ -32,5 +35,5 @@ func (p *Provider) Init() error {
Password: p.Password,
}
return p.Provider.Init(zookeeper.StoreName, "zookeeper", config)
return p.Provider.Init(zookeeper.StoreName, ProviderName, config)
}
+5 -5
View File
@@ -23,8 +23,8 @@ import (
)
const (
// providerName is the name of this provider.
providerName = "nomad"
// ProviderName is the Nomad provider name.
ProviderName = "nomad"
// defaultTemplateRule is the default template for the default rule.
defaultTemplateRule = "Host(`{{ normalize .Name }}`)"
@@ -68,7 +68,7 @@ func (p *ProviderBuilder) BuildProviders() []*Provider {
if len(p.Namespaces) == 0 {
return []*Provider{{
Configuration: p.Configuration,
name: providerName,
name: ProviderName,
}}
}
@@ -76,7 +76,7 @@ func (p *ProviderBuilder) BuildProviders() []*Provider {
for _, namespace := range p.Namespaces {
providers = append(providers, &Provider{
Configuration: p.Configuration,
name: providerName + "-" + namespace,
name: ProviderName + "-" + namespace,
namespace: namespace,
})
}
@@ -169,7 +169,7 @@ func (p *Provider) Init() error {
// In case they didn't initialize Provider with BuildProviders
if p.name == "" {
p.name = providerName
p.name = ProviderName
}
return nil
+5 -2
View File
@@ -13,6 +13,9 @@ import (
"github.com/unrolled/render"
)
// ProviderName is the REST provider name.
const ProviderName = "rest"
var _ provider.Provider = (*Provider)(nil)
// Provider is a provider.Provider implementation that provides a Rest API.
@@ -40,7 +43,7 @@ func (p *Provider) CreateRouter() *mux.Router {
func (p *Provider) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
vars := mux.Vars(req)
if vars["provider"] != "rest" {
if vars["provider"] != ProviderName {
http.Error(rw, "Only 'rest' provider can be updated through the REST API", http.StatusBadRequest)
return
}
@@ -53,7 +56,7 @@ func (p *Provider) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
return
}
p.configurationChan <- dynamic.Message{ProviderName: "rest", Configuration: configuration}
p.configurationChan <- dynamic.Message{ProviderName: ProviderName, Configuration: configuration}
if err := templatesRenderer.JSON(rw, http.StatusOK, configuration); err != nil {
log.Error().Err(err).Send()
}
+8 -3
View File
@@ -17,7 +17,12 @@ import (
"github.com/traefik/traefik/v3/pkg/tls"
)
const defaultInternalEntryPointName = "traefik"
const (
// ProviderName is the internal Traefik provider name.
ProviderName = "internal"
defaultInternalEntryPointName = "traefik"
)
var _ provider.Provider = (*Provider)(nil)
@@ -38,10 +43,10 @@ func (i *Provider) ThrottleDuration() time.Duration {
// Provide allows the provider to provide configurations to traefik using the given configuration channel.
func (i *Provider) Provide(configurationChan chan<- dynamic.Message, _ *safe.Pool) error {
ctx := log.With().Str(logs.ProviderName, "internal").Logger().WithContext(context.Background())
ctx := log.With().Str(logs.ProviderName, ProviderName).Logger().WithContext(context.Background())
configurationChan <- dynamic.Message{
ProviderName: "internal",
ProviderName: ProviderName,
Configuration: i.createConfiguration(ctx),
}
+29 -18
View File
@@ -39,13 +39,14 @@ type serviceManager interface {
// Manager A route/router manager.
type Manager struct {
routerHandlers map[string]http.Handler
serviceManager serviceManager
observabilityMgr *middleware.ObservabilityMgr
middlewaresBuilder middlewareChainBuilder
conf *runtime.Configuration
tlsManager *tls.Manager
parser httpmuxer.SyntaxParser
routerHandlers map[string]http.Handler
serviceManager serviceManager
observabilityMgr *middleware.ObservabilityMgr
middlewaresBuilder middlewareChainBuilder
conf *runtime.Configuration
tlsManager *tls.Manager
parser httpmuxer.SyntaxParser
providersPrecedence []string
}
// NewManager creates a new Manager.
@@ -55,15 +56,17 @@ func NewManager(conf *runtime.Configuration,
observabilityMgr *middleware.ObservabilityMgr,
tlsManager *tls.Manager,
parser httpmuxer.SyntaxParser,
providersPrecedence []string,
) *Manager {
return &Manager{
routerHandlers: make(map[string]http.Handler),
serviceManager: serviceManager,
observabilityMgr: observabilityMgr,
middlewaresBuilder: middlewaresBuilder,
conf: conf,
tlsManager: tlsManager,
parser: parser,
routerHandlers: make(map[string]http.Handler),
serviceManager: serviceManager,
observabilityMgr: observabilityMgr,
middlewaresBuilder: middlewaresBuilder,
conf: conf,
tlsManager: tlsManager,
parser: parser,
providersPrecedence: providersPrecedence,
}
}
@@ -225,7 +228,7 @@ func (m *Manager) getHTTPRouters(ctx context.Context, entryPoints []string, tls
}
func (m *Manager) buildEntryPointHandler(ctx context.Context, entryPointName string, configs map[string]*runtime.RouterInfo, config dynamic.RouterObservabilityConfig) (http.Handler, error) {
muxer := httpmuxer.NewMuxer(m.parser)
muxer := httpmuxer.NewMuxer(m.parser, m.providersPrecedence)
defaultHandler, err := m.observabilityMgr.BuildEPChain(ctx, entryPointName, false, config).Then(http.NotFoundHandler())
if err != nil {
@@ -274,7 +277,7 @@ func (m *Manager) buildEntryPointHandler(ctx context.Context, entryPointName str
continue
}
if err = muxer.AddRoute(routerConfig.Rule, routerConfig.RuleSyntax, routerConfig.Priority, handler); err != nil {
if err = muxer.AddRoute(routerConfig.Rule, routerConfig.RuleSyntax, routerConfig.Priority, providerName(routerName), handler); err != nil {
routerConfig.AddError(err, true)
logger.Error().Err(err).Send()
continue
@@ -456,7 +459,7 @@ func (m *Manager) handleCycle(victimRouter string, path []string) {
// buildChildRoutersMuxer creates a muxer for child routers.
func (m *Manager) buildChildRoutersMuxer(ctx context.Context, entryPointName string, childRefs []string) (http.Handler, error) {
childMuxer := httpmuxer.NewMuxer(m.parser)
childMuxer := httpmuxer.NewMuxer(m.parser, m.providersPrecedence)
// Set a default handler for the child muxer (404 Not Found).
childMuxer.SetDefaultHandler(http.NotFoundHandler())
@@ -490,7 +493,7 @@ func (m *Manager) buildChildRoutersMuxer(ctx context.Context, entryPointName str
}
// Add the child router to the muxer.
if err = childMuxer.AddRoute(childRouter.Rule, childRouter.RuleSyntax, childRouter.Priority, childHandler); err != nil {
if err = childMuxer.AddRoute(childRouter.Rule, childRouter.RuleSyntax, childRouter.Priority, providerName(childName), childHandler); err != nil {
childRouter.AddError(err, true)
logger.Error().Err(err).Send()
continue
@@ -506,3 +509,11 @@ func (m *Manager) buildChildRoutersMuxer(ctx context.Context, entryPointName str
return childMuxer, nil
}
func providerName(routerName string) string {
parts := strings.Split(routerName, "@")
if len(parts) == 2 {
return parts[1]
}
return ""
}
+171 -8
View File
@@ -332,7 +332,7 @@ func TestRouterManager_Get(t *testing.T) {
parser, err := httpmuxer.NewSyntaxParser()
require.NoError(t, err)
routerManager := NewManager(rtConf, serviceManager, middlewaresBuilder, nil, tlsManager, parser)
routerManager := NewManager(rtConf, serviceManager, middlewaresBuilder, nil, tlsManager, parser, []string{})
handlers := routerManager.BuildHandlers(t.Context(), test.entryPoints, false)
@@ -720,7 +720,7 @@ func TestRuntimeConfiguration(t *testing.T) {
parser, err := httpmuxer.NewSyntaxParser()
require.NoError(t, err)
routerManager := NewManager(rtConf, serviceManager, middlewaresBuilder, nil, tlsManager, parser)
routerManager := NewManager(rtConf, serviceManager, middlewaresBuilder, nil, tlsManager, parser, []string{})
_ = routerManager.BuildHandlers(t.Context(), entryPoints, false)
_ = routerManager.BuildHandlers(t.Context(), entryPoints, true)
@@ -801,7 +801,7 @@ func TestProviderOnMiddlewares(t *testing.T) {
parser, err := httpmuxer.NewSyntaxParser()
require.NoError(t, err)
routerManager := NewManager(rtConf, serviceManager, middlewaresBuilder, nil, tlsManager, parser)
routerManager := NewManager(rtConf, serviceManager, middlewaresBuilder, nil, tlsManager, parser, []string{})
_ = routerManager.BuildHandlers(t.Context(), entryPoints, false)
@@ -856,7 +856,7 @@ func BenchmarkRouterServe(b *testing.B) {
parser, err := httpmuxer.NewSyntaxParser()
require.NoError(b, err)
routerManager := NewManager(rtConf, serviceManager, middlewaresBuilder, nil, tlsManager, parser)
routerManager := NewManager(rtConf, serviceManager, middlewaresBuilder, nil, tlsManager, parser, []string{})
handlers := routerManager.BuildHandlers(b.Context(), entryPoints, false)
@@ -905,6 +905,155 @@ func BenchmarkService(b *testing.B) {
}
}
func TestProvidersPrecedence(t *testing.T) {
// Each provider gets its own service with a fake URL whose host encodes the
// provider label. labellingProxyBuilder writes the host back as the X-From
// response header so the test can identify which backend was selected.
//
// Service names must be fully qualified ("svc@<provider>") because the
// router manager qualifies every unqualified name with the provider embedded
// in the router's own name ("router@<provider>").
svcFor := func(provider string) *dynamic.Service {
return &dynamic.Service{
LoadBalancer: &dynamic.ServersLoadBalancer{
Strategy: dynamic.BalancerStrategyWRR,
Servers: []dynamic.Server{{URL: "http://" + provider}},
},
}
}
testCases := []struct {
desc string
providersPrecedence []string
routersConfig map[string]*dynamic.Router
serviceConfig map[string]*dynamic.Service
expectedFrom string
}{
{
desc: "kubernetescrd beats kubernetes when listed after it",
providersPrecedence: []string{"kubernetescrd", "kubernetes"},
routersConfig: map[string]*dynamic.Router{
// Service names are bare; the manager qualifies them with the
// provider extracted from the router key (@kubernetes / @kubernetescrd).
"router@kubernetes": {
EntryPoints: []string{"web"},
Rule: "Host(`foo.bar`)",
Service: "svc",
},
"router@kubernetescrd": {
EntryPoints: []string{"web"},
Rule: "Host(`foo.bar`)",
Service: "svc",
},
},
serviceConfig: map[string]*dynamic.Service{
"svc@kubernetes": svcFor("kubernetes"),
"svc@kubernetescrd": svcFor("kubernetescrd"),
},
expectedFrom: "kubernetescrd",
},
{
desc: "kubernetes beats kubernetescrd when listed after it",
providersPrecedence: []string{"kubernetes", "kubernetescrd"},
routersConfig: map[string]*dynamic.Router{
"router@kubernetes": {
EntryPoints: []string{"web"},
Rule: "Host(`foo.bar`)",
Service: "svc",
},
"router@kubernetescrd": {
EntryPoints: []string{"web"},
Rule: "Host(`foo.bar`)",
Service: "svc",
},
},
serviceConfig: map[string]*dynamic.Service{
"svc@kubernetes": svcFor("kubernetes"),
"svc@kubernetescrd": svcFor("kubernetescrd"),
},
expectedFrom: "kubernetes",
},
{
desc: "higher numeric priority wins regardless of providersPrecedence",
providersPrecedence: []string{"kubernetescrd", "kubernetes"},
routersConfig: map[string]*dynamic.Router{
"router@kubernetes": {
EntryPoints: []string{"web"},
Rule: "Host(`foo.bar`)",
Priority: 100,
Service: "svc",
},
"router@kubernetescrd": {
EntryPoints: []string{"web"},
Rule: "Host(`foo.bar`)",
Priority: 10,
Service: "svc",
},
},
serviceConfig: map[string]*dynamic.Service{
"svc@kubernetes": svcFor("kubernetes"),
"svc@kubernetescrd": svcFor("kubernetescrd"),
},
expectedFrom: "kubernetes",
},
{
desc: "provider not in providersPrecedence loses to any listed provider",
providersPrecedence: []string{"kubernetes"},
routersConfig: map[string]*dynamic.Router{
"router@file": {
EntryPoints: []string{"web"},
Rule: "Host(`foo.bar`)",
Service: "svc",
},
"router@kubernetes": {
EntryPoints: []string{"web"},
Rule: "Host(`foo.bar`)",
Service: "svc",
},
},
serviceConfig: map[string]*dynamic.Service{
"svc@file": svcFor("file"),
"svc@kubernetes": svcFor("kubernetes"),
},
expectedFrom: "kubernetes",
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
rtConf := runtime.NewConfig(dynamic.Configuration{
HTTP: &dynamic.HTTPConfiguration{
Services: test.serviceConfig,
Routers: test.routersConfig,
Middlewares: map[string]*dynamic.Middleware{},
},
})
transportManager := service.NewTransportManager(nil)
transportManager.Update(map[string]*dynamic.ServersTransport{"default@internal": {}})
serviceManager := service.NewManager(rtConf.Services, nil, nil, transportManager, labellingProxyBuilder{})
middlewaresBuilder := middleware.NewBuilder(rtConf.Middlewares, serviceManager, nil)
tlsManager := traefiktls.NewManager(nil)
parser, err := httpmuxer.NewSyntaxParser()
require.NoError(t, err)
routerManager := NewManager(rtConf, serviceManager, middlewaresBuilder, nil, tlsManager, parser, test.providersPrecedence)
handlers := routerManager.BuildHandlers(t.Context(), []string{"web"}, false)
w := httptest.NewRecorder()
req := testhelpers.MustNewRequest(http.MethodGet, "http://foo.bar/", nil)
requestdecorator.New(nil).ServeHTTP(w, req, handlers["web"].ServeHTTP)
assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, test.expectedFrom, w.Header().Get("X-From"), "wrong provider won the route")
})
}
}
func TestManager_ComputeMultiLayerRouting(t *testing.T) {
testCases := []struct {
desc string
@@ -1449,7 +1598,7 @@ func TestManager_buildChildRoutersMuxer(t *testing.T) {
parser, err := httpmuxer.NewSyntaxParser()
require.NoError(t, err)
manager := NewManager(conf, serviceManager, middlewareBuilder, nil, nil, parser)
manager := NewManager(conf, serviceManager, middlewareBuilder, nil, nil, parser, []string{})
// Compute multi-layer routing to populate ChildRefs
manager.ParseRouterTree()
@@ -1640,7 +1789,7 @@ func TestManager_buildHTTPHandler_WithChildRouters(t *testing.T) {
parser, err := httpmuxer.NewSyntaxParser()
require.NoError(t, err)
manager := NewManager(conf, serviceManager, middlewareBuilder, nil, nil, parser)
manager := NewManager(conf, serviceManager, middlewareBuilder, nil, nil, parser, []string{})
// Run ParseRouterTree to validate configuration and populate ChildRefs/errors
manager.ParseRouterTree()
@@ -1787,7 +1936,7 @@ func TestManager_BuildHandlers_WithChildRouters(t *testing.T) {
parser, err := httpmuxer.NewSyntaxParser()
require.NoError(t, err)
manager := NewManager(conf, serviceManager, middlewareBuilder, nil, nil, parser)
manager := NewManager(conf, serviceManager, middlewareBuilder, nil, nil, parser, []string{})
// Compute multi-layer routing to set up parent-child relationships
manager.ParseRouterTree()
@@ -1942,7 +2091,7 @@ func TestManager_BuildHandlers_Deny(t *testing.T) {
parser, err := httpmuxer.NewSyntaxParser()
require.NoError(t, err)
manager := NewManager(conf, serviceManager, middlewareBuilder, nil, nil, parser)
manager := NewManager(conf, serviceManager, middlewareBuilder, nil, nil, parser, []string{})
// Compute multi-layer routing to set up parent-child relationships
manager.ParseRouterTree()
@@ -2014,3 +2163,17 @@ func (p proxyBuilderMock) Build(_ string, _ *url.URL, _, _ bool, _ time.Duration
func (p proxyBuilderMock) Update(_ map[string]*dynamic.ServersTransport) {
panic("implement me")
}
// labellingProxyBuilder builds a handler that writes the target URL host as
// the X-From response header, allowing tests to identify which backend was
// selected by the router.
type labellingProxyBuilder struct{}
func (l labellingProxyBuilder) Build(_ string, target *url.URL, _, _ bool, _ time.Duration) (http.Handler, error) {
label := target.Host
return http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("X-From", label)
}), nil
}
func (l labellingProxyBuilder) Update(_ map[string]*dynamic.ServersTransport) {}
+28 -17
View File
@@ -30,12 +30,13 @@ type middlewareBuilder interface {
// 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
serviceManager *tcpservice.Manager
middlewaresBuilder middlewareBuilder
httpHandlers map[string]http.Handler
httpsHandlers map[string]http.Handler
tlsManager *traefiktls.Manager
conf *runtime.Configuration
providersPrecedence []string
}
// NewManager Creates a new Manager.
@@ -45,14 +46,16 @@ func NewManager(conf *runtime.Configuration,
httpHandlers map[string]http.Handler,
httpsHandlers map[string]http.Handler,
tlsManager *traefiktls.Manager,
providersPrecedence []string,
) *Manager {
return &Manager{
serviceManager: serviceManager,
middlewaresBuilder: middlewaresBuilder,
httpHandlers: httpHandlers,
httpsHandlers: httpsHandlers,
tlsManager: tlsManager,
conf: conf,
serviceManager: serviceManager,
middlewaresBuilder: middlewaresBuilder,
httpHandlers: httpHandlers,
httpsHandlers: httpsHandlers,
tlsManager: tlsManager,
conf: conf,
providersPrecedence: providersPrecedence,
}
}
@@ -101,7 +104,7 @@ type nameAndConfig struct {
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()
router, err := NewRouter(m.providersPrecedence)
if err != nil {
return nil, err
}
@@ -324,7 +327,7 @@ func (m *Manager) addTCPHandlers(ctx context.Context, configs map[string]*runtim
if routerConfig.TLS == nil {
logger.Debug().Msgf("Adding route for %q", routerConfig.Rule)
if err := router.muxerTCP.AddRoute(routerConfig.Rule, routerConfig.RuleSyntax, routerConfig.Priority, handler); err != nil {
if err := router.muxerTCP.AddRoute(routerConfig.Rule, routerConfig.RuleSyntax, routerConfig.Priority, providerName(routerName), handler); err != nil {
routerConfig.AddError(err, true)
logger.Error().Err(err).Send()
}
@@ -334,7 +337,7 @@ func (m *Manager) addTCPHandlers(ctx context.Context, configs map[string]*runtim
if routerConfig.TLS.Passthrough {
logger.Debug().Msgf("Adding Passthrough route for %q", routerConfig.Rule)
if err := router.muxerTCPTLS.AddRoute(routerConfig.Rule, routerConfig.RuleSyntax, routerConfig.Priority, handler); err != nil {
if err := router.muxerTCPTLS.AddRoute(routerConfig.Rule, routerConfig.RuleSyntax, routerConfig.Priority, providerName(routerName), handler); err != nil {
routerConfig.AddError(err, true)
logger.Error().Err(err).Send()
}
@@ -368,7 +371,7 @@ func (m *Manager) addTCPHandlers(ctx context.Context, configs map[string]*runtim
logger.Debug().Msgf("Adding special TLS closing route for %q because broken TLS options %s", routerConfig.Rule, tlsOptionsName)
if err := router.muxerTCPTLS.AddRoute(routerConfig.Rule, routerConfig.RuleSyntax, routerConfig.Priority, &brokenTLSRouter{}); err != nil {
if err := router.muxerTCPTLS.AddRoute(routerConfig.Rule, routerConfig.RuleSyntax, routerConfig.Priority, providerName(routerName), &brokenTLSRouter{}); err != nil {
routerConfig.AddError(err, true)
logger.Error().Err(err).Send()
}
@@ -402,7 +405,7 @@ func (m *Manager) addTCPHandlers(ctx context.Context, configs map[string]*runtim
logger.Debug().Msgf("Adding TLS route for %q", routerConfig.Rule)
if err := router.muxerTCPTLS.AddRoute(routerConfig.Rule, routerConfig.RuleSyntax, routerConfig.Priority, handler); err != nil {
if err := router.muxerTCPTLS.AddRoute(routerConfig.Rule, routerConfig.RuleSyntax, routerConfig.Priority, providerName(routerName), handler); err != nil {
routerConfig.AddError(err, true)
logger.Error().Err(err).Send()
continue
@@ -430,3 +433,11 @@ func (m *Manager) buildTCPHandler(ctx context.Context, router *runtime.TCPRouter
return tcp.NewChain().Extend(*mHandler).Then(sHandler)
}
func providerName(routerName string) string {
parts := strings.Split(routerName, "@")
if len(parts) == 2 {
return parts[1]
}
return ""
}
+2 -2
View File
@@ -367,7 +367,7 @@ func TestRuntimeConfiguration(t *testing.T) {
middlewaresBuilder := tcpmiddleware.NewBuilder(conf.TCPMiddlewares)
routerManager := NewManager(conf, serviceManager, middlewaresBuilder,
nil, nil, tlsManager)
nil, nil, tlsManager, nil)
_ = routerManager.BuildHandlers(t.Context(), entryPoints)
@@ -668,7 +668,7 @@ func TestDomainFronting(t *testing.T) {
middlewaresBuilder := tcpmiddleware.NewBuilder(conf.TCPMiddlewares)
routerManager := NewManager(conf, serviceManager, middlewaresBuilder, nil, httpsHandler, tlsManager)
routerManager := NewManager(conf, serviceManager, middlewaresBuilder, nil, httpsHandler, tlsManager, nil)
routers := routerManager.BuildHandlers(t.Context(), entryPoints)
+10 -8
View File
@@ -51,18 +51,18 @@ type Router struct {
}
// NewRouter returns a new TCP router.
func NewRouter() (*Router, error) {
muxTCP, err := tcpmuxer.NewMuxer()
func NewRouter(providersPrecedence []string) (*Router, error) {
muxTCP, err := tcpmuxer.NewMuxer(providersPrecedence)
if err != nil {
return nil, err
}
muxTCPTLS, err := tcpmuxer.NewMuxer()
muxTCPTLS, err := tcpmuxer.NewMuxer(providersPrecedence)
if err != nil {
return nil, err
}
muxHTTPS, err := tcpmuxer.NewMuxer()
muxHTTPS, err := tcpmuxer.NewMuxer(providersPrecedence)
if err != nil {
return nil, err
}
@@ -230,8 +230,8 @@ func (r *Router) ServeTCP(conn tcp.WriteCloser) {
}
// AddTCPRoute defines a handler for the given rule.
func (r *Router) AddTCPRoute(rule string, priority int, target tcp.Handler) error {
return r.muxerTCP.AddRoute(rule, "", priority, target)
func (r *Router) AddTCPRoute(rule string, priority int, providerName string, target tcp.Handler) error {
return r.muxerTCP.AddRoute(rule, "", priority, providerName, target)
}
// AddHTTPTLSConfig defines a handler for a given sniHost and sets the matching tlsConfig.
@@ -273,8 +273,10 @@ func (r *Router) SetHTTPSForwarder(handler tcp.Handler) {
}
}
rule := "HostSNI(`" + sniHost + "`)"
if err := r.muxerHTTPS.AddRoute(rule, "", tcpmuxer.GetRulePriority(rule), tcpHandler); err != nil {
rule := fmt.Sprintf(`HostSNI(%q)`, sniHost)
// As the hostHTTPTLSConfig contains only one TLS config per SNI,
// there is no conflict thus the provider name can be passed as empty as no tie-break is needed.
if err := r.muxerHTTPS.AddRoute(rule, "", tcpmuxer.GetRulePriority(rule), "", tcpHandler); err != nil {
log.Error().Err(err).Msg("Error while adding route for host")
}
}
+301 -301
View File
@@ -27,47 +27,6 @@ import (
"github.com/traefik/traefik/v3/pkg/types"
)
type applyRouter func(conf *runtime.Configuration)
type checkRouter func(addr string, timeout time.Duration) error
type httpForwarder struct {
net.Listener
connChan chan net.Conn
errChan chan error
}
func newHTTPForwarder(ln net.Listener) *httpForwarder {
return &httpForwarder{
Listener: ln,
connChan: make(chan net.Conn),
errChan: make(chan error),
}
}
// Close closes the Listener.
func (h *httpForwarder) Close() error {
h.errChan <- http.ErrServerClosed
return nil
}
// ServeTCP uses the connection to serve it later in "Accept".
func (h *httpForwarder) ServeTCP(conn tcp2.WriteCloser) {
h.connChan <- conn
}
// Accept retrieves a served connection in ServeTCP.
func (h *httpForwarder) Accept() (net.Conn, error) {
select {
case conn := <-h.connChan:
return conn, nil
case err := <-h.errChan:
return nil, err
}
}
// Test_Routing aims to settle the behavior between routers of different types on the same TCP entryPoint.
// It has been introduced as a regression test following a fix on the v2.7 TCP Muxer.
//
@@ -202,7 +161,7 @@ func Test_Routing(t *testing.T) {
middlewaresBuilder := tcpmiddleware.NewBuilder(conf.TCPMiddlewares)
manager := NewManager(conf, serviceManager, middlewaresBuilder,
nil, nil, tlsManager)
nil, nil, tlsManager, nil)
type checkCase struct {
checkRouter
@@ -701,7 +660,7 @@ func Test_Routing(t *testing.T) {
}
func Test_Router_acmeTLSALPNHandlerTimeout(t *testing.T) {
router, err := NewRouter()
router, err := NewRouter(nil)
require.NoError(t, err)
router.httpsTLSConfig = &tls.Config{}
@@ -758,6 +717,272 @@ func Test_Router_acmeTLSALPNHandlerTimeout(t *testing.T) {
}
}
// Test_clientHelloInfo_oversizedRecordLength verifies that clientHelloInfo
// does not block or allocate excessive memory when a client sends a TLS
// record header with a maliciously large record length (up to 0xFFFF).
//
// Without the fix, clientHelloInfo allocates a ~65KB bufio.Reader and blocks
// on Peek(65540), waiting for bytes that never arrive (until readTimeout).
// With the fix, records exceeding the TLS maximum plaintext size (16384)
// are rejected immediately.
func Test_clientHelloInfo_oversizedRecordLength(t *testing.T) {
testCases := []struct {
desc string
recLen uint16
}{
{
desc: "max uint16 record length (0xFFFF)",
recLen: 0xFFFF,
},
{
desc: "just above TLS maximum (18433)",
recLen: 18433,
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
serverConn, clientConn := net.Pipe()
defer serverConn.Close()
defer clientConn.Close()
type result struct {
hello *clientHello
err error
}
resultCh := make(chan result, 1)
go func() {
pConn := &peekConn{reader: bufio.NewReader(serverConn)}
hello, err := clientHelloInfo(pConn)
resultCh <- result{hello, err}
}()
// Send a TLS record header with an oversized record length.
// Only the 5-byte header is sent; the client then stalls.
hdr := []byte{
0x16, // Content Type: Handshake
0x03, 0x03, // Version: TLS 1.2
byte(test.recLen >> 8), // Length high byte
byte(test.recLen & 0xFF), // Length low byte
}
_, err := clientConn.Write(hdr)
require.NoError(t, err)
// Without the fix, clientHelloInfo blocks on Peek(recLen+5)
// since only 5 bytes are available. The test would time out.
// With the fix, it returns immediately.
select {
case r := <-resultCh:
require.Error(t, r.err)
case <-time.After(5 * time.Second):
t.Fatal("clientHelloInfo blocked on oversized TLS record length — recLen is not capped")
}
})
}
}
// Test_clientHelloInfo_tlsRecordFragmentation documents a known limitation:
// clientHelloInfo only reads a single TLS record. When a ClientHello handshake
// message is split across multiple TLS records (RFC 5246 §6.2.1), the SNI cannot
// be extracted, leaving serverName empty and allowing SNI-based routing to be bypassed.
func Test_clientHelloInfo_tlsRecordFragmentation(t *testing.T) {
serverName := "foo.example.com"
record := buildClientHelloRecord(t, serverName)
const hdrLen = 5
payload := record[hdrLen:]
ver1, ver2 := record[1], record[2]
var recordsData bytes.Buffer
for _, part := range [][]byte{payload[:len(serverName)/2], payload[len(serverName)/2:]} {
recordsData.WriteByte(0x16)
recordsData.WriteByte(ver1)
recordsData.WriteByte(ver2)
recordsData.WriteByte(byte(len(part) >> 8))
recordsData.WriteByte(byte(len(part)))
recordsData.Write(part)
}
serverConn, clientConn := net.Pipe()
t.Cleanup(func() {
_ = serverConn.Close()
_ = clientConn.Close()
})
type result struct {
hello *clientHello
err error
}
resultCh := make(chan result, 1)
go func() {
pConn := &peekConn{reader: bufio.NewReader(serverConn)}
hello, err := clientHelloInfo(pConn)
resultCh <- result{hello, err}
}()
_, err := clientConn.Write(recordsData.Bytes())
require.NoError(t, err)
_ = clientConn.Close()
select {
case r := <-resultCh:
require.NoError(t, r.err)
require.NotNil(t, r.hello)
assert.True(t, r.hello.isTLS)
assert.Equal(t, serverName, r.hello.serverName)
case <-time.After(5 * time.Second):
t.Fatal("clientHelloInfo blocked")
}
}
func TestPostgresTLSTermination(t *testing.T) {
certPEM, keyPEM, err := generate.KeyPair("test.localhost", time.Time{})
require.NoError(t, err)
cert, err := tls.X509KeyPair(certPEM, keyPEM)
require.NoError(t, err)
tlsConf := &tls.Config{
Certificates: []tls.Certificate{cert},
}
router, err := NewRouter(nil)
require.NoError(t, err)
// Register a TCPTLS route (TLS termination, not passthrough) with a TLSHandler.
// The TLSHandler wraps the actual handler, performing the TLS handshake.
err = router.muxerTCPTLS.AddRoute("HostSNI(`test.localhost`)", "", 0, "", &tcp2.TLSHandler{
Config: tlsConf,
Next: tcp2.HandlerFunc(func(conn tcp2.WriteCloser) {
_, _ = conn.Write([]byte("OK"))
_ = conn.Close()
}),
})
require.NoError(t, err)
ln, err := net.Listen("tcp", "127.0.0.1:0")
require.NoError(t, err)
t.Cleanup(func() { _ = ln.Close() })
go func() {
conn, err := ln.Accept()
require.NoError(t, err)
tcpConn := conn.(*net.TCPConn)
router.ServeTCP(tcpConn)
}()
clientConn, err := net.Dial("tcp", ln.Addr().String())
require.NoError(t, err)
t.Cleanup(func() { _ = clientConn.Close() })
// Step 1: Client sends PostgresStartTLSMsg (SSLRequest).
_, err = clientConn.Write(PostgresStartTLSMsg)
require.NoError(t, err)
// Step 2: Client receives PostgresStartTLSReply ('S').
reply := make([]byte, 1)
_, err = io.ReadFull(clientConn, reply)
require.NoError(t, err)
require.Equal(t, PostgresStartTLSReply, reply)
// Step 3: Client performs TLS handshake.
tlsClient := tls.Client(clientConn, &tls.Config{
ServerName: "test.localhost",
InsecureSkipVerify: true,
})
require.NoError(t, tlsClient.Handshake())
t.Cleanup(func() { _ = tlsClient.Close() })
// Step 4: Read the response from the handler through the TLS connection.
buf := make([]byte, 256)
n, err := tlsClient.Read(buf)
require.NoError(t, err)
assert.Equal(t, "OK", string(buf[:n]))
}
func TestPostgresTLSPassthrough(t *testing.T) {
certPEM, keyPEM, err := generate.KeyPair("test.localhost", time.Time{})
require.NoError(t, err)
cert, err := tls.X509KeyPair(certPEM, keyPEM)
require.NoError(t, err)
tlsConf := &tls.Config{
Certificates: []tls.Certificate{cert},
}
router, err := NewRouter(nil)
require.NoError(t, err)
// Register a TCPTLS route (TLS passthrough) with a tcp.Handler.
err = router.muxerTCPTLS.AddRoute("HostSNI(`test.localhost`)", "", 0, "", tcp2.HandlerFunc(func(conn tcp2.WriteCloser) {
// First we should receive the PostgresStartTLSMsg.
buf := make([]byte, len(PostgresStartTLSMsg))
_, err := conn.Read(buf)
require.NoError(t, err)
assert.Equal(t, PostgresStartTLSMsg, buf)
// Next we should answer with the PostgresStartTLSReply.
_, err = conn.Write(PostgresStartTLSReply)
require.NoError(t, err)
// Then we should do the TLS handshake.
tlsConn := tls.Server(conn, tlsConf)
require.NoError(t, tlsConn.Handshake())
// Finally we write the response through the TLS connection.
_, err = tlsConn.Write([]byte("OK"))
require.NoError(t, err)
}))
require.NoError(t, err)
ln, err := net.Listen("tcp", "127.0.0.1:0")
require.NoError(t, err)
t.Cleanup(func() { _ = ln.Close() })
go func() {
conn, err := ln.Accept()
require.NoError(t, err)
tcpConn := conn.(*net.TCPConn)
router.ServeTCP(tcpConn)
}()
clientConn, err := net.Dial("tcp", ln.Addr().String())
require.NoError(t, err)
t.Cleanup(func() { _ = clientConn.Close() })
// Step 1: Client sends PostgresStartTLSMsg (SSLRequest).
_, err = clientConn.Write(PostgresStartTLSMsg)
require.NoError(t, err)
// Step 2: Client receives PostgresStartTLSReply ('S').
reply := make([]byte, 1)
_, err = io.ReadFull(clientConn, reply)
require.NoError(t, err)
require.Equal(t, PostgresStartTLSReply, reply)
// Step 3: Client performs TLS handshake.
tlsClient := tls.Client(clientConn, &tls.Config{
ServerName: "test.localhost",
InsecureSkipVerify: true,
})
require.NoError(t, tlsClient.Handshake())
t.Cleanup(func() { _ = tlsClient.Close() })
// Step 4: Read the response from the handler through the TLS connection.
buf := make([]byte, 256)
n, err := tlsClient.Read(buf)
require.NoError(t, err)
assert.Equal(t, "OK", string(buf[:n]))
}
// routerTCPCatchAll configures a TCP CatchAll No TLS - HostSNI(`*`) router.
func routerTCPCatchAll(conf *runtime.Configuration) {
conf.TCPRouters["tcp-catchall"] = &runtime.TCPRouterInfo{
@@ -1084,129 +1309,6 @@ func checkHTTPSTLS12(addr string, timeout time.Duration) error {
return checkHTTPS(addr, timeout, tls.VersionTLS12)
}
// Test_clientHelloInfo_oversizedRecordLength verifies that clientHelloInfo
// does not block or allocate excessive memory when a client sends a TLS
// record header with a maliciously large record length (up to 0xFFFF).
//
// Without the fix, clientHelloInfo allocates a ~65KB bufio.Reader and blocks
// on Peek(65540), waiting for bytes that never arrive (until readTimeout).
// With the fix, records exceeding the TLS maximum plaintext size (16384)
// are rejected immediately.
func Test_clientHelloInfo_oversizedRecordLength(t *testing.T) {
testCases := []struct {
desc string
recLen uint16
}{
{
desc: "max uint16 record length (0xFFFF)",
recLen: 0xFFFF,
},
{
desc: "just above TLS maximum (18433)",
recLen: 18433,
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
serverConn, clientConn := net.Pipe()
defer serverConn.Close()
defer clientConn.Close()
type result struct {
hello *clientHello
err error
}
resultCh := make(chan result, 1)
go func() {
pConn := &peekConn{reader: bufio.NewReader(serverConn)}
hello, err := clientHelloInfo(pConn)
resultCh <- result{hello, err}
}()
// Send a TLS record header with an oversized record length.
// Only the 5-byte header is sent; the client then stalls.
hdr := []byte{
0x16, // Content Type: Handshake
0x03, 0x03, // Version: TLS 1.2
byte(test.recLen >> 8), // Length high byte
byte(test.recLen & 0xFF), // Length low byte
}
_, err := clientConn.Write(hdr)
require.NoError(t, err)
// Without the fix, clientHelloInfo blocks on Peek(recLen+5)
// since only 5 bytes are available. The test would time out.
// With the fix, it returns immediately.
select {
case r := <-resultCh:
require.Error(t, r.err)
case <-time.After(5 * time.Second):
t.Fatal("clientHelloInfo blocked on oversized TLS record length — recLen is not capped")
}
})
}
}
// Test_clientHelloInfo_tlsRecordFragmentation documents a known limitation:
// clientHelloInfo only reads a single TLS record. When a ClientHello handshake
// message is split across multiple TLS records (RFC 5246 §6.2.1), the SNI cannot
// be extracted, leaving serverName empty and allowing SNI-based routing to be bypassed.
func Test_clientHelloInfo_tlsRecordFragmentation(t *testing.T) {
serverName := "foo.example.com"
record := buildClientHelloRecord(t, serverName)
const hdrLen = 5
payload := record[hdrLen:]
ver1, ver2 := record[1], record[2]
var recordsData bytes.Buffer
for _, part := range [][]byte{payload[:len(serverName)/2], payload[len(serverName)/2:]} {
recordsData.WriteByte(0x16)
recordsData.WriteByte(ver1)
recordsData.WriteByte(ver2)
recordsData.WriteByte(byte(len(part) >> 8))
recordsData.WriteByte(byte(len(part)))
recordsData.Write(part)
}
serverConn, clientConn := net.Pipe()
t.Cleanup(func() {
_ = serverConn.Close()
_ = clientConn.Close()
})
type result struct {
hello *clientHello
err error
}
resultCh := make(chan result, 1)
go func() {
pConn := &peekConn{reader: bufio.NewReader(serverConn)}
hello, err := clientHelloInfo(pConn)
resultCh <- result{hello, err}
}()
_, err := clientConn.Write(recordsData.Bytes())
require.NoError(t, err)
_ = clientConn.Close()
select {
case r := <-resultCh:
require.NoError(t, r.err)
require.NotNil(t, r.hello)
assert.True(t, r.hello.isTLS)
assert.Equal(t, serverName, r.hello.serverName)
case <-time.After(5 * time.Second):
t.Fatal("clientHelloInfo blocked")
}
}
// buildClientHelloRecord captures a real TLS ClientHello record from Go's TLS stack
// for the given serverName.
// It returns the raw record bytes and the byte offset of the SNI value within those bytes.
@@ -1237,145 +1339,43 @@ func buildClientHelloRecord(t *testing.T, serverName string) []byte {
return record
}
func TestPostgresTLSTermination(t *testing.T) {
certPEM, keyPEM, err := generate.KeyPair("test.localhost", time.Time{})
require.NoError(t, err)
type applyRouter func(conf *runtime.Configuration)
cert, err := tls.X509KeyPair(certPEM, keyPEM)
require.NoError(t, err)
type checkRouter func(addr string, timeout time.Duration) error
tlsConf := &tls.Config{
Certificates: []tls.Certificate{cert},
}
type httpForwarder struct {
net.Listener
router, err := NewRouter()
require.NoError(t, err)
// Register a TCPTLS route (TLS termination, not passthrough) with a TLSHandler.
// The TLSHandler wraps the actual handler, performing the TLS handshake.
err = router.muxerTCPTLS.AddRoute("HostSNI(`test.localhost`)", "", 0, &tcp2.TLSHandler{
Config: tlsConf,
Next: tcp2.HandlerFunc(func(conn tcp2.WriteCloser) {
_, _ = conn.Write([]byte("OK"))
_ = conn.Close()
}),
})
require.NoError(t, err)
ln, err := net.Listen("tcp", "127.0.0.1:0")
require.NoError(t, err)
t.Cleanup(func() { _ = ln.Close() })
go func() {
conn, err := ln.Accept()
require.NoError(t, err)
tcpConn := conn.(*net.TCPConn)
router.ServeTCP(tcpConn)
}()
clientConn, err := net.Dial("tcp", ln.Addr().String())
require.NoError(t, err)
t.Cleanup(func() { _ = clientConn.Close() })
// Step 1: Client sends PostgresStartTLSMsg (SSLRequest).
_, err = clientConn.Write(PostgresStartTLSMsg)
require.NoError(t, err)
// Step 2: Client receives PostgresStartTLSReply ('S').
reply := make([]byte, 1)
_, err = io.ReadFull(clientConn, reply)
require.NoError(t, err)
require.Equal(t, PostgresStartTLSReply, reply)
// Step 3: Client performs TLS handshake.
tlsClient := tls.Client(clientConn, &tls.Config{
ServerName: "test.localhost",
InsecureSkipVerify: true,
})
require.NoError(t, tlsClient.Handshake())
t.Cleanup(func() { _ = tlsClient.Close() })
// Step 4: Read the response from the handler through the TLS connection.
buf := make([]byte, 256)
n, err := tlsClient.Read(buf)
require.NoError(t, err)
assert.Equal(t, "OK", string(buf[:n]))
connChan chan net.Conn
errChan chan error
}
func TestPostgresTLSPassthrough(t *testing.T) {
certPEM, keyPEM, err := generate.KeyPair("test.localhost", time.Time{})
require.NoError(t, err)
cert, err := tls.X509KeyPair(certPEM, keyPEM)
require.NoError(t, err)
tlsConf := &tls.Config{
Certificates: []tls.Certificate{cert},
func newHTTPForwarder(ln net.Listener) *httpForwarder {
return &httpForwarder{
Listener: ln,
connChan: make(chan net.Conn),
errChan: make(chan error),
}
}
// Close closes the Listener.
func (h *httpForwarder) Close() error {
h.errChan <- http.ErrServerClosed
return nil
}
// ServeTCP uses the connection to serve it later in "Accept".
func (h *httpForwarder) ServeTCP(conn tcp2.WriteCloser) {
h.connChan <- conn
}
// Accept retrieves a served connection in ServeTCP.
func (h *httpForwarder) Accept() (net.Conn, error) {
select {
case conn := <-h.connChan:
return conn, nil
case err := <-h.errChan:
return nil, err
}
router, err := NewRouter()
require.NoError(t, err)
// Register a TCPTLS route (TLS passthrough) with a tcp.Handler.
err = router.muxerTCPTLS.AddRoute("HostSNI(`test.localhost`)", "", 0, tcp2.HandlerFunc(func(conn tcp2.WriteCloser) {
// First we should receive the PostgresStartTLSMsg.
buf := make([]byte, len(PostgresStartTLSMsg))
_, err := conn.Read(buf)
require.NoError(t, err)
assert.Equal(t, PostgresStartTLSMsg, buf)
// Next we should answer with the PostgresStartTLSReply.
_, err = conn.Write(PostgresStartTLSReply)
require.NoError(t, err)
// Then we should do the TLS handshake.
tlsConn := tls.Server(conn, tlsConf)
require.NoError(t, tlsConn.Handshake())
// Finally we write the response through the TLS connection.
_, err = tlsConn.Write([]byte("OK"))
require.NoError(t, err)
}))
require.NoError(t, err)
ln, err := net.Listen("tcp", "127.0.0.1:0")
require.NoError(t, err)
t.Cleanup(func() { _ = ln.Close() })
go func() {
conn, err := ln.Accept()
require.NoError(t, err)
tcpConn := conn.(*net.TCPConn)
router.ServeTCP(tcpConn)
}()
clientConn, err := net.Dial("tcp", ln.Addr().String())
require.NoError(t, err)
t.Cleanup(func() { _ = clientConn.Close() })
// Step 1: Client sends PostgresStartTLSMsg (SSLRequest).
_, err = clientConn.Write(PostgresStartTLSMsg)
require.NoError(t, err)
// Step 2: Client receives PostgresStartTLSReply ('S').
reply := make([]byte, 1)
_, err = io.ReadFull(clientConn, reply)
require.NoError(t, err)
require.Equal(t, PostgresStartTLSReply, reply)
// Step 3: Client performs TLS handshake.
tlsClient := tls.Client(clientConn, &tls.Config{
ServerName: "test.localhost",
InsecureSkipVerify: true,
})
require.NoError(t, tlsClient.Handshake())
t.Cleanup(func() { _ = tlsClient.Close() })
// Step 4: Read the response from the handler through the TLS connection.
buf := make([]byte, 256)
n, err := tlsClient.Read(buf)
require.NoError(t, err)
assert.Equal(t, "OK", string(buf[:n]))
}
+19 -12
View File
@@ -39,7 +39,8 @@ type RouterFactory struct {
cancelPrevState func()
parser httpmuxer.SyntaxParser
parser httpmuxer.SyntaxParser
providersPrecedence []string
}
// NewRouterFactory creates a new RouterFactory.
@@ -77,16 +78,22 @@ func NewRouterFactory(staticConfiguration static.Configuration, managerFactory *
return nil, fmt.Errorf("creating parser: %w", err)
}
var providersPrecedence []string
if staticConfiguration.Providers != nil {
providersPrecedence = staticConfiguration.Providers.Precedence
}
return &RouterFactory{
entryPointsTCP: entryPointsTCP,
entryPointsUDP: entryPointsUDP,
managerFactory: managerFactory,
observabilityMgr: observabilityMgr,
tlsManager: tlsManager,
pluginBuilder: pluginBuilder,
dialerManager: dialerManager,
allowACMEByPass: allowACMEByPass,
parser: parser,
entryPointsTCP: entryPointsTCP,
entryPointsUDP: entryPointsUDP,
managerFactory: managerFactory,
observabilityMgr: observabilityMgr,
tlsManager: tlsManager,
pluginBuilder: pluginBuilder,
dialerManager: dialerManager,
allowACMEByPass: allowACMEByPass,
parser: parser,
providersPrecedence: providersPrecedence,
}, nil
}
@@ -106,7 +113,7 @@ func (f *RouterFactory) CreateRouters(rtConf *runtime.Configuration) (map[string
serviceManager.SetMiddlewareChainBuilder(middlewaresBuilder)
routerManager := router.NewManager(rtConf, serviceManager, middlewaresBuilder, f.observabilityMgr, f.tlsManager, f.parser)
routerManager := router.NewManager(rtConf, serviceManager, middlewaresBuilder, f.observabilityMgr, f.tlsManager, f.parser, f.providersPrecedence)
routerManager.ParseRouterTree()
@@ -120,7 +127,7 @@ func (f *RouterFactory) CreateRouters(rtConf *runtime.Configuration) (map[string
middlewaresTCPBuilder := tcpmiddleware.NewBuilder(rtConf.TCPMiddlewares)
rtTCPManager := tcprouter.NewManager(rtConf, svcTCPManager, middlewaresTCPBuilder, handlersNonTLS, handlersTLS, f.tlsManager)
rtTCPManager := tcprouter.NewManager(rtConf, svcTCPManager, middlewaresTCPBuilder, handlersNonTLS, handlersTLS, f.tlsManager, f.providersPrecedence)
routersTCP := rtTCPManager.BuildHandlers(ctx, f.entryPointsTCP)
for ep, r := range routersTCP {
+1 -1
View File
@@ -187,7 +187,7 @@ func NewTCPEntryPoint(ctx context.Context, name string, config *static.EntryPoin
return nil, fmt.Errorf("building listener: %w", err)
}
rt, err := tcprouter.NewRouter()
rt, err := tcprouter.NewRouter(nil)
if err != nil {
return nil, fmt.Errorf("creating TCP router: %w", err)
}
@@ -97,7 +97,7 @@ func TestHTTP3AdvertisedPort(t *testing.T) {
}, nil, nil)
require.NoError(t, err)
router, err := tcprouter.NewRouter()
router, err := tcprouter.NewRouter(nil)
require.NoError(t, err)
router.AddHTTPTLSConfig("*", &tls.Config{
@@ -159,7 +159,7 @@ func TestHTTP30RTT(t *testing.T) {
}, nil, nil)
require.NoError(t, err)
router, err := tcprouter.NewRouter()
router, err := tcprouter.NewRouter(nil)
require.NoError(t, err)
router.AddHTTPTLSConfig("example.com", &tls.Config{
+9 -9
View File
@@ -24,7 +24,7 @@ import (
)
func TestShutdownHijacked(t *testing.T) {
router, err := tcprouter.NewRouter()
router, err := tcprouter.NewRouter(nil)
require.NoError(t, err)
router.SetHTTPHandler(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
@@ -40,7 +40,7 @@ func TestShutdownHijacked(t *testing.T) {
}
func TestShutdownHTTP(t *testing.T) {
router, err := tcprouter.NewRouter()
router, err := tcprouter.NewRouter(nil)
require.NoError(t, err)
router.SetHTTPHandler(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
@@ -52,10 +52,10 @@ func TestShutdownHTTP(t *testing.T) {
}
func TestShutdownTCP(t *testing.T) {
router, err := tcprouter.NewRouter()
router, err := tcprouter.NewRouter(nil)
require.NoError(t, err)
err = router.AddTCPRoute("HostSNI(`*`)", 0, tcp.HandlerFunc(func(conn tcp.WriteCloser) {
err = router.AddTCPRoute("HostSNI(`*`)", 0, "", tcp.HandlerFunc(func(conn tcp.WriteCloser) {
_, err := http.ReadRequest(bufio.NewReader(conn))
if err != nil {
return
@@ -177,7 +177,7 @@ func TestReadTimeoutWithoutFirstByte(t *testing.T) {
}, nil, nil)
require.NoError(t, err)
router, err := tcprouter.NewRouter()
router, err := tcprouter.NewRouter(nil)
require.NoError(t, err)
router.SetHTTPHandler(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
@@ -216,7 +216,7 @@ func TestReadTimeoutWithFirstByte(t *testing.T) {
}, nil, nil)
require.NoError(t, err)
router, err := tcprouter.NewRouter()
router, err := tcprouter.NewRouter(nil)
require.NoError(t, err)
router.SetHTTPHandler(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
@@ -258,7 +258,7 @@ func TestKeepAliveMaxRequests(t *testing.T) {
}, nil, nil)
require.NoError(t, err)
router, err := tcprouter.NewRouter()
router, err := tcprouter.NewRouter(nil)
require.NoError(t, err)
router.SetHTTPHandler(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
@@ -306,7 +306,7 @@ func TestKeepAliveMaxTime(t *testing.T) {
}, nil, nil)
require.NoError(t, err)
router, err := tcprouter.NewRouter()
router, err := tcprouter.NewRouter(nil)
require.NoError(t, err)
router.SetHTTPHandler(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
@@ -350,7 +350,7 @@ func TestKeepAliveH2c(t *testing.T) {
}, nil, nil)
require.NoError(t, err)
router, err := tcprouter.NewRouter()
router, err := tcprouter.NewRouter(nil)
require.NoError(t, err)
router.SetHTTPHandler(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {