Merge branch v3.6 into v3.7

This commit is contained in:
kevinpollet
2026-06-10 16:06:21 +02:00
17 changed files with 714 additions and 75 deletions
+16
View File
@@ -1,3 +1,19 @@
## [v3.6.21](https://github.com/traefik/traefik/tree/v3.6.21) (2026-06-10)
[All Commits](https://github.com/traefik/traefik/compare/v3.6.20...v3.6.21)
**Bug fixes:**
- **[k8s/gatewayapi]** Reject cross-provider references with backendRefs.namespace ([#13322](https://github.com/traefik/traefik/pull/13322) @youkoulayley)
- **[server]** Bump to github.com/pires/go-proxyproto v0.12.0 ([#13313](https://github.com/traefik/traefik/pull/13313) @timschumi)
- **[tls]** Fix routers with same host, different tlsoptions on different entryPoint ([#13329](https://github.com/traefik/traefik/pull/13329) @juliens)
- **[tls]** Fix snicheck for routers with no hosts ([#13333](https://github.com/traefik/traefik/pull/13333) @rtribotte)
## [v2.11.50](https://github.com/traefik/traefik/tree/v2.11.50) (2026-06-10)
[All Commits](https://github.com/traefik/traefik/compare/v2.11.49...v2.11.50)
**Bug fixes:**
- **[tls]** Fix routers with same host, different tlsoptions on different entryPoint ([#13329](https://github.com/traefik/traefik/pull/13329) @juliens)
- **[tls]** Fix snicheck for routers with no hosts ([#13333](https://github.com/traefik/traefik/pull/13333) @rtribotte)
## [v3.7.4](https://github.com/traefik/traefik/tree/v3.7.4) (2026-06-05) ## [v3.7.4](https://github.com/traefik/traefik/tree/v3.7.4) (2026-06-05)
[All Commits](https://github.com/traefik/traefik/compare/v3.7.3...v3.7.4) [All Commits](https://github.com/traefik/traefik/compare/v3.7.3...v3.7.4)
+1 -1
View File
@@ -53,7 +53,7 @@ require (
github.com/moby/moby/api v1.54.1 github.com/moby/moby/api v1.54.1
github.com/moby/moby/client v0.4.0 github.com/moby/moby/client v0.4.0
github.com/patrickmn/go-cache v2.1.0+incompatible github.com/patrickmn/go-cache v2.1.0+incompatible
github.com/pires/go-proxyproto v0.8.1 github.com/pires/go-proxyproto v0.12.0
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // No tag on the repo. github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // No tag on the repo.
github.com/prometheus/client_golang v1.23.2 github.com/prometheus/client_golang v1.23.2
github.com/prometheus/client_model v0.6.2 github.com/prometheus/client_model v0.6.2
+2 -2
View File
@@ -1748,8 +1748,8 @@ github.com/phpdave11/gofpdi v1.0.13/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk
github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc= github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc=
github.com/pierrec/lz4 v2.6.1+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= github.com/pierrec/lz4 v2.6.1+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
github.com/pierrec/lz4/v4 v4.1.15/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pierrec/lz4/v4 v4.1.15/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pires/go-proxyproto v0.8.1 h1:9KEixbdJfhrbtjpz/ZwCdWDD2Xem0NZ38qMYaASJgp0= github.com/pires/go-proxyproto v0.12.0 h1:TTCxD66dU898tahivkqc3hoceZp7P44FnorWyo9d5vM=
github.com/pires/go-proxyproto v0.8.1/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU= github.com/pires/go-proxyproto v0.12.0/go.mod h1:qUvfqUMEoX7T8g0q7TQLDnhMjdTrxnG0hvpMn+7ePNI=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
@@ -0,0 +1,101 @@
[global]
checkNewVersion = false
sendAnonymousUsage = false
[log]
level = "DEBUG"
[entryPoints.websecure]
address = ":4443"
[entryPoints.websecure2]
address = ":4444"
[api]
insecure = true
[providers.file]
filename = "{{ .SelfFilename }}"
## dynamic configuration ##
# --- Same host, same options, same entryPoint: no conflict, the options are applied. ---
[http.routers.same-1]
rule = "Host(`same.www.snitest.com`)"
entryPoints = ["websecure"]
service = "service1"
[http.routers.same-1.tls]
options = "tls12"
[http.routers.same-2]
rule = "Host(`same.www.snitest.com`) && PathPrefix(`/same`)"
entryPoints = ["websecure"]
service = "service1"
[http.routers.same-2.tls]
options = "tls12"
# --- Same host, different options, same entryPoint: conflict, fallback to default options. ---
[http.routers.conflict-1]
rule = "Host(`conflict.www.snitest.com`)"
entryPoints = ["websecure"]
service = "service1"
[http.routers.conflict-1.tls]
options = "tls12"
[http.routers.conflict-2]
rule = "Host(`conflict.www.snitest.com`) && PathPrefix(`/conflict`)"
entryPoints = ["websecure"]
service = "service1"
[http.routers.conflict-2.tls]
options = "tls13"
# --- Same host, different options, different entryPoints: no conflict, each entryPoint keeps its own options. ---
[http.routers.cross-ep1]
rule = "Host(`cross.www.snitest.com`)"
entryPoints = ["websecure"]
service = "service1"
[http.routers.cross-ep1.tls]
options = "tls12"
[http.routers.cross-ep2]
rule = "Host(`cross.www.snitest.com`)"
entryPoints = ["websecure2"]
service = "service1"
[http.routers.cross-ep2.tls]
options = "tls13"
# --- Domain fronting (Host header != SNI): same options follow the header, different options are rejected. ---
[http.routers.df-a]
rule = "Host(`df-a.www.snitest.com`)"
entryPoints = ["websecure"]
service = "service1"
[http.routers.df-a.tls]
options = "tls12"
[http.routers.df-b]
rule = "Host(`df-b.www.snitest.com`)"
entryPoints = ["websecure"]
service = "service1"
[http.routers.df-b.tls]
options = "tls12"
[http.routers.df-c]
rule = "Host(`df-c.www.snitest.com`)"
entryPoints = ["websecure"]
service = "service1"
[http.routers.df-c.tls]
options = "tls13"
[http.services.service1]
[[http.services.service1.loadBalancer.servers]]
url = "http://127.0.0.1:9010"
[[tls.certificates]]
certFile = "fixtures/https/wildcard.www.snitest.com.cert"
keyFile = "fixtures/https/wildcard.www.snitest.com.key"
[tls.options]
[tls.options.tls12]
maxVersion = "VersionTLS12"
[tls.options.tls13]
minVersion = "VersionTLS13"
+148 -1
View File
@@ -415,7 +415,7 @@ func (s *HTTPSSuite) TestWithConflictingTLSOptions() {
assert.ErrorContains(s.T(), err, "tls: no supported versions satisfy MinVersion and MaxVersion") assert.ErrorContains(s.T(), err, "tls: no supported versions satisfy MinVersion and MaxVersion")
// with unknown tls option // with unknown tls option
err = try.GetRequest("http://127.0.0.1:8080/api/rawdata", 1*time.Second, try.BodyContains("found different TLS options for routers on the same host, so using the default TLS options instead")) err = try.GetRequest("http://127.0.0.1:8080/api/rawdata", 1*time.Second, try.BodyContains("router's TLSOptions configuration is conflicting with other routers on the same entrypoint and host, default TLS options will be used instead"))
require.NoError(s.T(), err) require.NoError(s.T(), err)
} }
@@ -1262,6 +1262,153 @@ func (s *HTTPSSuite) TestWithDomainFronting() {
} }
} }
// TestWithTLSOptionsConflict checks how TLS options are resolved when several routers
// target the same host (SNI), across the different conflict situations:
// - same options on the same entryPoint: no conflict, the options are applied;
// - different options on the same entryPoint: conflict, fallback to the default options;
// - different options on different entryPoints: no conflict, each entryPoint keeps its
// own options (they are selected independently on each listener);
// - domain fronting (Host header != SNI): allowed when both resolve to the same options,
// rejected with a 421 otherwise.
//
// The effective TLS options are probed through the negotiated TLS version: the "tls12"
// options cap the version to TLS 1.2, while the "tls13" options require at least TLS 1.3.
func (s *HTTPSSuite) TestWithTLSOptionsConflict() {
backend := startTestServer("9010", http.StatusOK, "server1")
defer backend.Close()
file := s.adaptFile("fixtures/https/https_tls_options_conflict.toml", struct{}{})
s.traefikCmd(withConfigFile(file))
// wait for Traefik
err := try.GetRequest("http://127.0.0.1:8080/api/rawdata", 1*time.Second, try.BodyContains("Host(`cross.www.snitest.com`)"))
require.NoError(s.T(), err)
testCases := []struct {
desc string
addr string // entryPoint address to reach
hostHeader string
serverName string // SNI
minVersion uint16 // 0 means the crypto/tls library default
maxVersion uint16 // 0 means the crypto/tls library default
// expectHandshakeError is set when the TLS handshake itself is expected to fail
// (i.e. the probed options reject the client's TLS version). Otherwise
// expectedStatusCode is asserted on the HTTP response.
expectHandshakeError bool
expectedStatusCode int
}{
// Same host, same options, same entryPoint: no conflict, the "tls12" options are applied.
{
desc: "same options / same entryPoint: TLS 1.2 client is accepted",
addr: "127.0.0.1:4443",
hostHeader: "same.www.snitest.com",
serverName: "same.www.snitest.com",
maxVersion: tls.VersionTLS12,
expectedStatusCode: http.StatusOK,
},
{
desc: "same options / same entryPoint: TLS 1.3 client is rejected (maxVersion TLS1.2 enforced)",
addr: "127.0.0.1:4443",
hostHeader: "same.www.snitest.com",
serverName: "same.www.snitest.com",
minVersion: tls.VersionTLS13,
expectHandshakeError: true,
},
// Same host, different options, same entryPoint: conflict, both routers fall back to the default options.
{
desc: "conflicting options / same entryPoint: TLS 1.3 client is accepted (default options used)",
addr: "127.0.0.1:4443",
hostHeader: "conflict.www.snitest.com",
serverName: "conflict.www.snitest.com",
minVersion: tls.VersionTLS13,
expectedStatusCode: http.StatusOK,
},
{
desc: "conflicting options / same entryPoint: TLS 1.2 client is accepted (default options used)",
addr: "127.0.0.1:4443",
hostHeader: "conflict.www.snitest.com",
serverName: "conflict.www.snitest.com",
maxVersion: tls.VersionTLS12,
expectedStatusCode: http.StatusOK,
},
// Same host, different options, different entryPoints: no conflict, each entryPoint keeps its own options.
{
desc: "different entryPoints: websecure keeps tls12, TLS 1.2 client is accepted",
addr: "127.0.0.1:4443",
hostHeader: "cross.www.snitest.com",
serverName: "cross.www.snitest.com",
maxVersion: tls.VersionTLS12,
expectedStatusCode: http.StatusOK,
},
{
desc: "different entryPoints: websecure keeps tls12, TLS 1.3 client is rejected",
addr: "127.0.0.1:4443",
hostHeader: "cross.www.snitest.com",
serverName: "cross.www.snitest.com",
minVersion: tls.VersionTLS13,
expectHandshakeError: true,
},
{
desc: "different entryPoints: websecure2 keeps tls13, TLS 1.3 client is accepted",
addr: "127.0.0.1:4444",
hostHeader: "cross.www.snitest.com",
serverName: "cross.www.snitest.com",
minVersion: tls.VersionTLS13,
expectedStatusCode: http.StatusOK,
},
{
desc: "different entryPoints: websecure2 keeps tls13, TLS 1.2 client is rejected",
addr: "127.0.0.1:4444",
hostHeader: "cross.www.snitest.com",
serverName: "cross.www.snitest.com",
maxVersion: tls.VersionTLS12,
expectHandshakeError: true,
},
// Domain fronting (Host header != SNI) on the same entryPoint.
{
desc: "domain fronting / same options: request follows the Host header (200)",
addr: "127.0.0.1:4443",
hostHeader: "df-a.www.snitest.com",
serverName: "df-b.www.snitest.com",
maxVersion: tls.VersionTLS12,
expectedStatusCode: http.StatusOK,
},
{
desc: "domain fronting / different options: request is misdirected (421)",
addr: "127.0.0.1:4443",
hostHeader: "df-a.www.snitest.com",
serverName: "df-c.www.snitest.com",
minVersion: tls.VersionTLS13,
expectedStatusCode: http.StatusMisdirectedRequest,
},
}
for _, test := range testCases {
tlsConfig := &tls.Config{
InsecureSkipVerify: true,
ServerName: test.serverName,
MinVersion: test.minVersion,
MaxVersion: test.maxVersion,
}
req, err := http.NewRequest(http.MethodGet, "https://"+test.addr+"/", nil)
require.NoError(s.T(), err)
req.Host = test.hostHeader
if test.expectHandshakeError {
_, err = (&http.Client{Transport: &http.Transport{TLSClientConfig: tlsConfig}}).Do(req)
assert.ErrorContains(s.T(), err, "tls:", "test %q should fail the TLS handshake", test.desc)
continue
}
err = try.RequestWithTransport(req, 2*time.Second, &http.Transport{TLSClientConfig: tlsConfig}, try.StatusCodeIs(test.expectedStatusCode))
assert.NoError(s.T(), err, "test %q failed with: %v", test.desc, err)
}
}
// TestWithInvalidTLSOption verifies the behavior when using an invalid tlsOption configuration. // TestWithInvalidTLSOption verifies the behavior when using an invalid tlsOption configuration.
func (s *HTTPSSuite) TestWithInvalidTLSOption() { func (s *HTTPSSuite) TestWithInvalidTLSOption() {
backend := startTestServer("9010", http.StatusOK, "server1") backend := startTestServer("9010", http.StatusOK, "server1")
+3 -3
View File
@@ -949,7 +949,7 @@ func (s *SimpleSuite) TestRouterConfigErrors() {
s.traefikCmd(withConfigFile(file)) s.traefikCmd(withConfigFile(file))
// All errors // All errors
err := try.GetRequest("http://127.0.0.1:8080/api/http/routers", 1000*time.Millisecond, try.BodyContains(`["middleware \"unknown@file\" does not exist","found different TLS options for routers on the same host, so using the default TLS options instead"]`)) err := try.GetRequest("http://127.0.0.1:8080/api/http/routers", 1000*time.Millisecond, try.BodyContains(`["middleware \"unknown@file\" does not exist","router's TLSOptions configuration is conflicting with other routers on the same entrypoint and host, default TLS options will be used instead"]`))
require.NoError(s.T(), err) require.NoError(s.T(), err)
// router3 has an error because it uses an unknown entrypoint // router3 has an error because it uses an unknown entrypoint
@@ -957,11 +957,11 @@ func (s *SimpleSuite) TestRouterConfigErrors() {
require.NoError(s.T(), err) require.NoError(s.T(), err)
// router4 is enabled, but in warning state because its tls options conf was messed up // router4 is enabled, but in warning state because its tls options conf was messed up
err = try.GetRequest("http://127.0.0.1:8080/api/http/routers/router4@file", 1000*time.Millisecond, try.BodyContains(`"status":"warning"`)) err = try.GetRequest("http://127.0.0.1:8080/api/http/routers/websecure-conflicted-router4@file", 1000*time.Millisecond, try.BodyContains(`"status":"warning"`))
require.NoError(s.T(), err) require.NoError(s.T(), err)
// router5 is disabled because its middleware conf is broken // router5 is disabled because its middleware conf is broken
err = try.GetRequest("http://127.0.0.1:8080/api/http/routers/router5@file", 1000*time.Millisecond, try.BodyContains()) err = try.GetRequest("http://127.0.0.1:8080/api/http/routers/websecure-conflicted-router5@file", 1000*time.Millisecond, try.BodyContains())
require.NoError(s.T(), err) require.NoError(s.T(), err)
} }
@@ -0,0 +1,55 @@
---
kind: GatewayClass
apiVersion: gateway.networking.k8s.io/v1
metadata:
name: my-gateway-class
spec:
controllerName: traefik.io/gateway-controller
---
kind: Gateway
apiVersion: gateway.networking.k8s.io/v1
metadata:
name: my-gateway
namespace: default
spec:
gatewayClassName: my-gateway-class
listeners: # Use GatewayClass defaults for listener definition.
- name: http
protocol: HTTP
port: 80
allowedRoutes:
namespaces:
from: Same
---
kind: HTTPRoute
apiVersion: gateway.networking.k8s.io/v1
metadata:
name: http-app-1
namespace: default
spec:
parentRefs:
- name: my-gateway
kind: Gateway
group: gateway.networking.k8s.io
hostnames:
- "foo.com"
rules:
- matches:
- path:
type: Exact
value: /bar
backendRefs:
- weight: 1
group: traefik.io
kind: TraefikService
name: service@file
namespace: bar
port: 80
- name: whoami
port: 80
weight: 1
group: ""
kind: Service
@@ -0,0 +1,51 @@
---
kind: GatewayClass
apiVersion: gateway.networking.k8s.io/v1
metadata:
name: my-gateway-class
spec:
controllerName: traefik.io/gateway-controller
---
kind: Gateway
apiVersion: gateway.networking.k8s.io/v1
metadata:
name: my-gateway
namespace: default
spec:
gatewayClassName: my-gateway-class
listeners: # Use GatewayClass defaults for listener definition.
- name: tcp
protocol: TCP
port: 9000
allowedRoutes:
kinds:
- kind: TCPRoute
group: gateway.networking.k8s.io
namespaces:
from: Same
---
kind: TCPRoute
apiVersion: gateway.networking.k8s.io/v1alpha2
metadata:
name: tcp-app-1
namespace: default
spec:
parentRefs:
- name: my-gateway
kind: Gateway
group: gateway.networking.k8s.io
rules:
- backendRefs:
- weight: 1
group: traefik.io
kind: TraefikService
name: service@file
namespace: bar
port: 9000
- name: whoamitcp
port: 9000
weight: 1
group: ""
kind: Service
@@ -0,0 +1,67 @@
---
apiVersion: v1
kind: Secret
metadata:
name: supersecret
namespace: default
data:
tls.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUJxRENDQVU2Z0F3SUJBZ0lVWU9zcjBRZ0hPQnE0a1lSQ0w1K1REZFZ0NmJRd0NnWUlLb1pJemowRUF3SXcKRmpFVU1CSUdBMVVFQXd3TFpYaGhiWEJzWlM1amIyMHdIaGNOTWpVeE1ERXdNRGN4TnpNd1doY05NelV4TURBNApNRGN4TnpNd1dqQVdNUlF3RWdZRFZRUUREQXRsZUdGdGNHeGxMbU52YlRCWk1CTUdCeXFHU000OUFnRUdDQ3FHClNNNDlBd0VIQTBJQUJET3JpdzNaUTd3SWhXcmJQUzZKRlFUM2JUb05DRjAwdlNWNWZhYjZUYlh5TDh0bHNHcmUKVFJJRjJFd2dzdGVNT2t4R0tLU2xEdnVhRHdxOHAvcVYrMHVqZWpCNE1CMEdBMVVkRGdRV0JCUk1Fa3VleFhRaApVdERnUmcxS0J2NzJDRHErRXpBZkJnTlZIU01FR0RBV2dCUk1Fa3VleFhRaFV0RGdSZzFLQnY3MkNEcStFekFQCkJnTlZIUk1CQWY4RUJUQURBUUgvTUNVR0ExVWRFUVFlTUJ5Q0MyVjRZVzF3YkdVdVkyOXRnZzBxTG1WNFlXMXcKYkdVdVkyOXRNQW9HQ0NxR1NNNDlCQU1DQTBnQU1FVUNJUURzODdWazBzd0E2SGdPSmpST3llMW14RDgzcWNHeQpwZUZnb3hWOTNEeStjd0lnVjBNTUVKSmJWc1R5WkszRVErK1hjNXJFTDc4bnJKK1lJRVYrckNVV2o1VT0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQ==
tls.key: LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JR0hBZ0VBTUJNR0J5cUdTTTQ5QWdFR0NDcUdTTTQ5QXdFSEJHMHdhd0lCQVFRZ253Z0w1RFk0VUIxNHNNNmYKRGlrUWR0cWgyUVcxQXJmRjRmYzFVRnppZmRHaFJBTkNBQVF6cTRzTjJVTzhDSVZxMnowdWlSVUU5MjA2RFFoZApOTDBsZVgybStrMjE4aS9MWmJCcTNrMFNCZGhNSUxMWGpEcE1SaWlrcFE3N21nOEt2S2Y2bGZ0TAotLS0tLUVORCBQUklWQVRFIEtFWS0tLS0t
---
kind: GatewayClass
apiVersion: gateway.networking.k8s.io/v1
metadata:
name: my-gateway-class
spec:
controllerName: traefik.io/gateway-controller
---
kind: Gateway
apiVersion: gateway.networking.k8s.io/v1
metadata:
name: my-gateway
namespace: default
spec:
gatewayClassName: my-gateway-class
listeners: # Use GatewayClass defaults for listener definition.
- name: tls
protocol: TLS
port: 9000
tls:
certificateRefs:
- kind: Secret
name: supersecret
group: ""
allowedRoutes:
kinds:
- kind: TLSRoute
group: gateway.networking.k8s.io
namespaces:
from: Same
---
kind: TLSRoute
apiVersion: gateway.networking.k8s.io/v1
metadata:
name: tls-app-1
namespace: default
spec:
parentRefs:
- name: my-gateway
kind: Gateway
group: gateway.networking.k8s.io
rules:
- backendRefs:
- weight: 1
group: traefik.io
kind: TraefikService
name: service@file
namespace: bar
port: 9000
- name: whoamitcp
port: 9000
weight: 1
kind: Service
group: ""
@@ -240,6 +240,17 @@ func (p *Provider) loadService(ctx context.Context, listener gatewayListener, co
namespace := route.Namespace namespace := route.Namespace
if backendRef.Namespace != nil && *backendRef.Namespace != "" { if backendRef.Namespace != nil && *backendRef.Namespace != "" {
namespace = string(*backendRef.Namespace) namespace = string(*backendRef.Namespace)
if strings.Contains(string(backendRef.Name), "@") {
return provider.Normalize(namespace + "-" + string(backendRef.Name) + "-http"), &metav1.Condition{
Type: string(gatev1.RouteConditionResolvedRefs),
Status: metav1.ConditionFalse,
ObservedGeneration: route.Generation,
LastTransitionTime: metav1.Now(),
Reason: string(gatev1.RouteReasonRefNotPermitted),
Message: fmt.Sprintf("Cannot load HTTPBackendRef %s/%s/%s/%s: namespace is not allowed with a cross-provider reference", group, kind, namespace, backendRef.Name),
}
}
} }
serviceName := provider.Normalize(namespace + "-" + string(backendRef.Name) + "-http") serviceName := provider.Normalize(namespace + "-" + string(backendRef.Name) + "-http")
@@ -8425,20 +8425,22 @@ func Test_isCrossProviderNamespaceAllowed(t *testing.T) {
func TestCrossProviderNamespaces_HTTPRoute(t *testing.T) { func TestCrossProviderNamespaces_HTTPRoute(t *testing.T) {
testCases := []struct { testCases := []struct {
desc string desc string
fixture string
crossProviderNamespaces []string crossProviderNamespaces []string
wantError bool wantError bool
}{ }{
{desc: "nil: cross-provider TraefikService backendRefs accepted (backward compatible)", crossProviderNamespaces: nil, wantError: false}, {desc: "nil: cross-provider TraefikService backendRefs accepted (backward compatible)", fixture: "httproute/simple_cross_provider.yml", crossProviderNamespaces: nil, wantError: false},
{desc: "empty list: cross-provider TraefikService backendRefs are rejected, route dropped", crossProviderNamespaces: []string{}, wantError: true}, {desc: "empty list: cross-provider TraefikService backendRefs are rejected, route dropped", fixture: "httproute/simple_cross_provider.yml", crossProviderNamespaces: []string{}, wantError: true},
{desc: "namespace allowed: cross-provider TraefikService backendRefs accepted", crossProviderNamespaces: []string{"default"}, wantError: false}, {desc: "namespace allowed: cross-provider TraefikService backendRefs accepted", fixture: "httproute/simple_cross_provider.yml", crossProviderNamespaces: []string{"default"}, wantError: false},
{desc: "namespace not allowed: cross-provider TraefikService backendRefs rejected, route dropped", crossProviderNamespaces: []string{"other"}, wantError: true}, {desc: "namespace not allowed: cross-provider TraefikService backendRefs rejected, route dropped", fixture: "httproute/simple_cross_provider.yml", crossProviderNamespaces: []string{"other"}, wantError: true},
{desc: "namespace provided with cross-provider backendRef, route dropped", fixture: "httproute/invalid_cross_provider.yml", crossProviderNamespaces: []string{"other"}, wantError: true},
} }
for _, test := range testCases { for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) { t.Run(test.desc, func(t *testing.T) {
t.Parallel() t.Parallel()
k8sObjects, gwObjects := readResources(t, []string{"services.yml", "httproute/simple_cross_provider.yml"}) k8sObjects, gwObjects := readResources(t, []string{"services.yml", test.fixture})
kubeClient := kubefake.NewClientset(k8sObjects...) kubeClient := kubefake.NewClientset(k8sObjects...)
gwClient := newGatewaySimpleClientSet(t, gwObjects...) gwClient := newGatewaySimpleClientSet(t, gwObjects...)
@@ -8488,20 +8490,22 @@ func TestCrossProviderNamespaces_HTTPRoute(t *testing.T) {
func TestCrossProviderNamespaces_TCPRoute(t *testing.T) { func TestCrossProviderNamespaces_TCPRoute(t *testing.T) {
testCases := []struct { testCases := []struct {
desc string desc string
fixture string
crossProviderNamespaces []string crossProviderNamespaces []string
wantError bool wantError bool
}{ }{
{desc: "nil: cross-provider TraefikService backendRefs accepted (backward compatible)", crossProviderNamespaces: nil, wantError: false}, {desc: "nil: cross-provider TraefikService backendRefs accepted (backward compatible)", fixture: "tcproute/simple_cross_provider.yml", crossProviderNamespaces: nil, wantError: false},
{desc: "empty list: cross-provider TraefikService backendRefs are rejected, route dropped", crossProviderNamespaces: []string{}, wantError: true}, {desc: "empty list: cross-provider TraefikService backendRefs are rejected, route dropped", fixture: "tcproute/simple_cross_provider.yml", crossProviderNamespaces: []string{}, wantError: true},
{desc: "namespace allowed: cross-provider TraefikService backendRefs accepted", crossProviderNamespaces: []string{"default"}, wantError: false}, {desc: "namespace allowed: cross-provider TraefikService backendRefs accepted", fixture: "tcproute/simple_cross_provider.yml", crossProviderNamespaces: []string{"default"}, wantError: false},
{desc: "namespace not allowed: cross-provider TraefikService backendRefs rejected, route dropped", crossProviderNamespaces: []string{"other"}, wantError: true}, {desc: "namespace not allowed: cross-provider TraefikService backendRefs rejected, route dropped", fixture: "tcproute/simple_cross_provider.yml", crossProviderNamespaces: []string{"other"}, wantError: true},
{desc: "namespace provided with cross-provider backendRef, route dropped", fixture: "tcproute/invalid_cross_provider.yml", crossProviderNamespaces: []string{"other"}, wantError: true},
} }
for _, test := range testCases { for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) { t.Run(test.desc, func(t *testing.T) {
t.Parallel() t.Parallel()
k8sObjects, gwObjects := readResources(t, []string{"services.yml", "tcproute/simple_cross_provider.yml"}) k8sObjects, gwObjects := readResources(t, []string{"services.yml", test.fixture})
kubeClient := kubefake.NewClientset(k8sObjects...) kubeClient := kubefake.NewClientset(k8sObjects...)
gwClient := newGatewaySimpleClientSet(t, gwObjects...) gwClient := newGatewaySimpleClientSet(t, gwObjects...)
@@ -8560,20 +8564,22 @@ func TestCrossProviderNamespaces_TCPRoute(t *testing.T) {
func TestCrossProviderNamespaces_TLSRoute(t *testing.T) { func TestCrossProviderNamespaces_TLSRoute(t *testing.T) {
testCases := []struct { testCases := []struct {
desc string desc string
fixture string
crossProviderNamespaces []string crossProviderNamespaces []string
wantError bool wantError bool
}{ }{
{desc: "nil: cross-provider TraefikService backendRefs accepted (backward compatible)", crossProviderNamespaces: nil, wantError: false}, {desc: "nil: cross-provider TraefikService backendRefs accepted (backward compatible)", fixture: "tlsroute/simple_cross_provider.yml", crossProviderNamespaces: nil, wantError: false},
{desc: "empty list: cross-provider TraefikService backendRefs are rejected, route dropped", crossProviderNamespaces: []string{}, wantError: true}, {desc: "empty list: cross-provider TraefikService backendRefs are rejected, route dropped", fixture: "tlsroute/simple_cross_provider.yml", crossProviderNamespaces: []string{}, wantError: true},
{desc: "namespace allowed: cross-provider TraefikService backendRefs accepted", crossProviderNamespaces: []string{"default"}, wantError: false}, {desc: "namespace allowed: cross-provider TraefikService backendRefs accepted", fixture: "tlsroute/simple_cross_provider.yml", crossProviderNamespaces: []string{"default"}, wantError: false},
{desc: "namespace not allowed: cross-provider TraefikService backendRefs rejected, route dropped", crossProviderNamespaces: []string{"other"}, wantError: true}, {desc: "namespace not allowed: cross-provider TraefikService backendRefs rejected, route dropped", fixture: "tlsroute/simple_cross_provider.yml", crossProviderNamespaces: []string{"other"}, wantError: true},
{desc: "namespace provided with cross-provider backendRef, route dropped", fixture: "tlsroute/invalid_cross_provider.yml", crossProviderNamespaces: []string{"other"}, wantError: true},
} }
for _, test := range testCases { for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) { t.Run(test.desc, func(t *testing.T) {
t.Parallel() t.Parallel()
k8sObjects, gwObjects := readResources(t, []string{"services.yml", "tlsroute/simple_cross_provider.yml"}) k8sObjects, gwObjects := readResources(t, []string{"services.yml", test.fixture})
kubeClient := kubefake.NewClientset(k8sObjects...) kubeClient := kubefake.NewClientset(k8sObjects...)
gwClient := newGatewaySimpleClientSet(t, gwObjects...) gwClient := newGatewaySimpleClientSet(t, gwObjects...)
@@ -221,6 +221,17 @@ func (p *Provider) loadTCPService(route *gatev1alpha2.TCPRoute, backendRef gatev
namespace := route.Namespace namespace := route.Namespace
if backendRef.Namespace != nil && *backendRef.Namespace != "" { if backendRef.Namespace != nil && *backendRef.Namespace != "" {
namespace = string(*backendRef.Namespace) namespace = string(*backendRef.Namespace)
if strings.Contains(string(backendRef.Name), "@") {
return provider.Normalize(namespace + "-" + string(backendRef.Name)), nil, &metav1.Condition{
Type: string(gatev1.RouteConditionResolvedRefs),
Status: metav1.ConditionFalse,
ObservedGeneration: route.Generation,
LastTransitionTime: metav1.Now(),
Reason: string(gatev1.RouteReasonRefNotPermitted),
Message: fmt.Sprintf("Cannot load TCPRoute BackendRef %s/%s/%s/%s: namespace is not allowed with a cross-provider reference", group, kind, namespace, backendRef.Name),
}
}
} }
serviceName := provider.Normalize(namespace + "-" + string(backendRef.Name)) serviceName := provider.Normalize(namespace + "-" + string(backendRef.Name))
@@ -237,6 +237,17 @@ func (p *Provider) loadTLSService(route *gatev1.TLSRoute, backendRef gatev1.Back
namespace := route.Namespace namespace := route.Namespace
if backendRef.Namespace != nil && *backendRef.Namespace != "" { if backendRef.Namespace != nil && *backendRef.Namespace != "" {
namespace = string(*backendRef.Namespace) namespace = string(*backendRef.Namespace)
if strings.Contains(string(backendRef.Name), "@") {
return provider.Normalize(namespace + "-" + string(backendRef.Name)), nil, &metav1.Condition{
Type: string(gatev1.RouteConditionResolvedRefs),
Status: metav1.ConditionFalse,
ObservedGeneration: route.Generation,
LastTransitionTime: metav1.Now(),
Reason: string(gatev1.RouteReasonRefNotPermitted),
Message: fmt.Sprintf("Cannot load TLSRoute BackendRef %s/%s/%s/%s: namespace is not allowed with a cross-provider reference", group, kind, namespace, backendRef.Name),
}
}
} }
serviceName := provider.Normalize(namespace + "-" + string(backendRef.Name)) serviceName := provider.Normalize(namespace + "-" + string(backendRef.Name))
+101 -50
View File
@@ -55,7 +55,7 @@ func mergeConfiguration(configurations dynamic.Configurations, defaultEntryPoint
Str(logs.RouterName, routerName). Str(logs.RouterName, routerName).
Strs(logs.EntryPointName, defaultEntryPoints). Strs(logs.EntryPointName, defaultEntryPoints).
Msg("No entryPoint defined for this router, using the default one(s) instead") Msg("No entryPoint defined for this router, using the default one(s) instead")
router.EntryPoints = defaultEntryPoints router.EntryPoints = slices.Clone(defaultEntryPoints)
} }
// The `ruleSyntax` option is deprecated. // The `ruleSyntax` option is deprecated.
@@ -99,7 +99,7 @@ func mergeConfiguration(configurations dynamic.Configurations, defaultEntryPoint
log.Debug(). log.Debug().
Str(logs.RouterName, routerName). Str(logs.RouterName, routerName).
Msgf("No entryPoint defined for this TCP router, using the default one(s) instead: %+v", defaultEntryPoints) Msgf("No entryPoint defined for this TCP router, using the default one(s) instead: %+v", defaultEntryPoints)
router.EntryPoints = defaultEntryPoints router.EntryPoints = slices.Clone(defaultEntryPoints)
} }
conf.TCP.Routers[provider.MakeQualifiedName(pvd, routerName)] = router conf.TCP.Routers[provider.MakeQualifiedName(pvd, routerName)] = router
} }
@@ -173,80 +173,131 @@ func mergeConfiguration(configurations dynamic.Configurations, defaultEntryPoint
return conf return conf
} }
func resolveHTTPTLSOptions(cfg dynamic.Configuration) dynamic.Configuration { // resolveHTTPTLSOptions resolves the TLS options for the given routers, on a per
if cfg.HTTP == nil || len(cfg.HTTP.Routers) == 0 { // entryPoint basis.
return cfg //
// TLS options conflicts (i.e. the same host served with different TLS options) can
// only be detected and arbitrated within a single TLS listener, that is to say within
// a single entryPoint. To honor that, routers are grouped per entryPoint and the
// conflict detection is run independently for each entryPoint.
//
// A router keeps its original name, and its resolved TLS options, for the entryPoints
// on which it does not conflict. For each entryPoint on which it conflicts, that
// entryPoint is removed from the router and a dedicated copy is emitted, with its
// TLSOptions reset to the default one, named following the "ep-conflicted-name@provider" pattern.
func resolveHTTPTLSOptions(routers map[string]*dynamic.Router) map[string]*dynamic.Router {
if len(routers) == 0 {
return routers
} }
rts := make(map[string]*dynamic.Router) newRouters := make(map[string]*dynamic.Router)
// Keyed by domain, then by options reference. // Split every router per entryPoint.
// The actual source of truth for what TLS options will actually be used for the connection. // Routers always have at least one entryPoint at this stage, as they are
// As opposed to tlsOptionsForHost, it keeps track of all the (different) TLS // defaulted in mergeConfiguration before applyModel and this resolution run.
// options that occur for a given host name, so that later on we can set relevant routersByEntryPoint := map[string]map[string]*dynamic.Router{}
// errors and logging for all the routers concerned (i.e. wrongly configured). for name, router := range routers {
tlsOptionsForHostSNI := map[string]map[string][]string{} if router.TLS == nil {
newRouters[name] = router
for routerHTTPName, routerHTTPConfig := range cfg.HTTP.Routers {
rts[routerHTTPName] = routerHTTPConfig.DeepCopy()
if routerHTTPConfig.TLS == nil {
continue continue
} }
ctxRouter := provider.AddInContext(context.Background(), routerHTTPName) router.TLS.ResolvedOptions = traefiktls.DefaultTLSConfigName
logger := log.Ctx(ctxRouter).With().Str(logs.RouterName, routerHTTPName).Logger() if len(router.TLS.Options) > 0 && router.TLS.Options != traefiktls.DefaultTLSConfigName {
router.TLS.ResolvedOptions = provider.GetQualifiedName(provider.AddInContext(context.Background(), name), router.TLS.Options)
tlsOptionsName := traefiktls.DefaultTLSConfigName
if len(routerHTTPConfig.TLS.Options) > 0 && routerHTTPConfig.TLS.Options != traefiktls.DefaultTLSConfigName {
tlsOptionsName = provider.GetQualifiedName(ctxRouter, routerHTTPConfig.TLS.Options)
} }
domains, err := httpmuxer.ParseDomains(routerHTTPConfig.Rule) for _, ep := range router.EntryPoints {
if routersByEntryPoint[ep] == nil {
routersByEntryPoint[ep] = map[string]*dynamic.Router{}
}
routersByEntryPoint[ep][name] = router
}
}
// Resolve the TLS options independently for each entryPoint.
conflictingRouters := make(map[string][]string, len(routersByEntryPoint))
for ep, epRouters := range routersByEntryPoint {
conflictingRouters[ep] = findConflictingRouters(ep, epRouters)
}
for name, router := range routers {
router.EntryPoints = slices.DeleteFunc(router.EntryPoints, func(ep string) bool {
deleted := slices.Contains(conflictingRouters[ep], name)
if deleted {
rt := router.DeepCopy()
rt.TLS.ResolvedOptions = traefiktls.DefaultTLSConfigName
rt.EntryPoints = []string{ep}
// The new name is not collision free but has very small possibility to collide.
// TODO: rework this naming whenever we'll introduce a resource reference mechanism not based on a string.
newRouters[ep+"-conflicted-"+name] = rt
}
return deleted
})
if len(router.EntryPoints) > 0 {
newRouters[name] = router
}
}
return newRouters
}
// findConflictingRouters returns the names of the routers, among the given
// single-entryPoint routers, that serve a host (SNI) also served by another router
// with a different resolved TLS option. Such routers are arbitrated by falling back
// to the default TLS options.
func findConflictingRouters(ep string, routers map[string]*dynamic.Router) []string {
var conflicting []string
// For each host (SNI, already lower-cased by the domain parsing), the routers
// serving it grouped by their resolved TLS option. A host with more than one
// group is served with conflicting TLS options.
routersByHostAndOption := map[string]map[string][]string{}
for name, router := range routers {
if router.TLS == nil {
continue
}
domains, err := httpmuxer.ParseDomains(router.Rule)
if err != nil { if err != nil {
logger.Error().Err(err).Msgf("Invalid rule %s", routerHTTPConfig.Rule)
continue continue
} }
if len(domains) == 0 { // The configured TLSOptions on a router without a domain in its rule cannot be selected when evaluating the SNI,
rts[routerHTTPName].TLS.ResolvedOptions = "default" // so if it is not the default one, it is a conflict.
logger.Warn().Msgf("No domain found in rule %v, the TLS options applied for this router will depend on the SNI of each request", routerHTTPConfig.Rule) if len(domains) == 0 && router.TLS.ResolvedOptions != traefiktls.DefaultTLSConfigName {
conflicting = append(conflicting, name)
continue
} }
for _, domain := range domains { for _, domain := range domains {
// domain is already in lower case thanks to the domain parsing if routersByHostAndOption[domain] == nil {
if tlsOptionsForHostSNI[domain] == nil { routersByHostAndOption[domain] = map[string][]string{}
tlsOptionsForHostSNI[domain] = make(map[string][]string)
} }
tlsOptionsForHostSNI[domain][tlsOptionsName] = append(tlsOptionsForHostSNI[domain][tlsOptionsName], routerHTTPName) option := router.TLS.ResolvedOptions
routersByHostAndOption[domain][option] = append(routersByHostAndOption[domain][option], name)
} }
} }
for hostSNI, tlsConfigs := range tlsOptionsForHostSNI { for domain, routersByOption := range routersByHostAndOption {
if len(tlsConfigs) == 1 { if len(routersByOption) == 1 {
for optionsName, v := range tlsConfigs {
log.Debug().Msgf("Adding route for %s with TLS options %s", hostSNI, optionsName)
for _, s := range v {
rts[s].TLS.ResolvedOptions = optionsName
}
}
continue continue
} }
// multiple tlsConfigs var routersInConflict []string
routers := make([]string, 0, len(tlsConfigs)) for _, names := range routersByOption {
for _, v := range tlsConfigs { conflicting = append(conflicting, names...)
for _, s := range v { routersInConflict = append(routersInConflict, names...)
rts[s].TLS.ResolvedOptions = traefiktls.DefaultTLSConfigName
routers = append(routers, s)
}
} }
log.Warn().Msgf("Found different TLS options for routers on the same host %v, so using the default TLS options instead for these routers: %#v", hostSNI, routers) log.Error().Msgf("On EntryPoint %q, Host %q is served by multiple routers with different TLS options, default TLSOptions will be applied for the following routers: %v", ep, domain, routersInConflict)
} }
cfg.HTTP.Routers = rts return conflicting
return cfg
} }
func applyModel(cfg dynamic.Configuration) dynamic.Configuration { func applyModel(cfg dynamic.Configuration) dynamic.Configuration {
+111
View File
@@ -5,6 +5,7 @@ import (
"github.com/go-acme/lego/v4/challenge/tlsalpn01" "github.com/go-acme/lego/v4/challenge/tlsalpn01"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/traefik/traefik/v3/pkg/config/dynamic" "github.com/traefik/traefik/v3/pkg/config/dynamic"
otypes "github.com/traefik/traefik/v3/pkg/observability/types" otypes "github.com/traefik/traefik/v3/pkg/observability/types"
"github.com/traefik/traefik/v3/pkg/tls" "github.com/traefik/traefik/v3/pkg/tls"
@@ -1230,3 +1231,113 @@ func Test_applyModel(t *testing.T) {
}) })
} }
} }
func Test_resolveHTTPTLSOptions(t *testing.T) {
testCases := []struct {
desc string
routers map[string]*dynamic.Router
expected map[string]string // router name -> ResolvedOptions
unexpectedRouters []string
}{
{
desc: "same host, different options, different entryPoints: no conflict",
routers: map[string]*dynamic.Router{
"router-a@file": {EntryPoints: []string{"ep-a"}, Rule: "Host(`example.com`)", TLS: &dynamic.RouterTLSConfig{Options: "optsA"}},
"router-b@file": {EntryPoints: []string{"ep-b"}, Rule: "Host(`example.com`)", TLS: &dynamic.RouterTLSConfig{Options: "optsB"}},
},
expected: map[string]string{
"router-a@file": "optsA@file",
"router-b@file": "optsB@file",
},
},
{
desc: "same host, different options, same entryPoint: conflict falls back to default",
routers: map[string]*dynamic.Router{
"router-a@file": {EntryPoints: []string{"ep-a"}, Rule: "Host(`example.com`)", TLS: &dynamic.RouterTLSConfig{Options: "optsA"}},
"router-b@file": {EntryPoints: []string{"ep-a"}, Rule: "Host(`example.com`)", TLS: &dynamic.RouterTLSConfig{Options: "optsB"}},
},
expected: map[string]string{
"ep-a-conflicted-router-a@file": "default",
"ep-a-conflicted-router-b@file": "default",
},
unexpectedRouters: []string{"router-a@file", "router-b@file"},
},
{
desc: "same host, same options, same entryPoint: keeps the configured options",
routers: map[string]*dynamic.Router{
"router-a@file": {EntryPoints: []string{"ep-a"}, Rule: "Host(`example.com`)", TLS: &dynamic.RouterTLSConfig{Options: "optsA"}},
"router-b@file": {EntryPoints: []string{"ep-a"}, Rule: "Host(`example.com`) && PathPrefix(`/foo`)", TLS: &dynamic.RouterTLSConfig{Options: "optsA"}},
},
expected: map[string]string{
"router-a@file": "optsA@file",
"router-b@file": "optsA@file",
},
},
{
desc: "router spanning two entryPoints, conflict on one only: router is duplicated",
routers: map[string]*dynamic.Router{
"shared@file": {EntryPoints: []string{"ep-a", "ep-b"}, Rule: "Host(`example.com`)", TLS: &dynamic.RouterTLSConfig{Options: "optsX"}},
"other@file": {EntryPoints: []string{"ep-a"}, Rule: "Host(`example.com`)", TLS: &dynamic.RouterTLSConfig{Options: "optsY"}},
},
expected: map[string]string{
"ep-a-conflicted-shared@file": "default", // conflicts with other@file on ep-a
"shared@file": "optsX@file", // alone on ep-b
"ep-a-conflicted-other@file": "default",
},
unexpectedRouters: []string{"other@file"},
},
{
desc: "no domain in rule, non-default options: forced to default and renamed",
routers: map[string]*dynamic.Router{
"router-a@file": {EntryPoints: []string{"ep-a"}, Rule: "PathPrefix(`/foo`)", TLS: &dynamic.RouterTLSConfig{Options: "optsA"}},
},
expected: map[string]string{
"ep-a-conflicted-router-a@file": "default",
},
unexpectedRouters: []string{"router-a@file"},
},
{
desc: "no domain in rule, implicit default options: not conflicting, keeps its name",
routers: map[string]*dynamic.Router{
"router-a@file": {EntryPoints: []string{"ep-a"}, Rule: "PathPrefix(`/foo`)", TLS: &dynamic.RouterTLSConfig{}},
},
expected: map[string]string{
"router-a@file": "default",
},
unexpectedRouters: []string{"ep-a-conflicted-router-a@file"},
},
{
desc: "no domain in rule, explicit default options: not conflicting, keeps its name",
routers: map[string]*dynamic.Router{
"router-a@file": {EntryPoints: []string{"ep-a"}, Rule: "PathPrefix(`/foo`)", TLS: &dynamic.RouterTLSConfig{
Options: "default",
}},
},
expected: map[string]string{
"router-a@file": "default",
},
unexpectedRouters: []string{"ep-a-conflicted-router-a@file"},
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
got := resolveHTTPTLSOptions(test.routers)
for name, want := range test.expected {
rt, ok := got[name]
require.True(t, ok, "router %q is missing", name)
require.NotNil(t, rt.TLS, "router %q has no TLS config", name)
assert.Equal(t, want, rt.TLS.ResolvedOptions, "router %q %v", name, rt.EntryPoints)
}
for _, name := range test.unexpectedRouters {
_, ok := got[name]
require.False(t, ok, "router %q is present", name)
}
})
}
}
+3 -1
View File
@@ -176,7 +176,9 @@ func (c *ConfigurationWatcher) applyConfigurations(ctx context.Context) {
conf := mergeConfiguration(newConfigs.DeepCopy(), c.defaultEntryPoints) conf := mergeConfiguration(newConfigs.DeepCopy(), c.defaultEntryPoints)
conf = applyModel(conf) conf = applyModel(conf)
conf = resolveHTTPTLSOptions(conf) if conf.HTTP != nil {
conf.HTTP.Routers = resolveHTTPTLSOptions(conf.HTTP.Routers)
}
for _, listener := range c.configurationListeners { for _, listener := range c.configurationListeners {
listener(conf) listener(conf)
+1 -2
View File
@@ -173,8 +173,7 @@ func (m *Manager) buildEntryPointHandler(ctx context.Context, configs map[string
} }
if len(domains) > 0 && routerHTTPConfig.TLS.ResolvedOptions != tlsOptionsName { if len(domains) > 0 && routerHTTPConfig.TLS.ResolvedOptions != tlsOptionsName {
logger.Warn().Msg("Found different TLS options for routers on the same host, so using the default TLS options instead.") routerHTTPConfig.AddError(errors.New("router's TLSOptions configuration is conflicting with other routers on the same entrypoint and host, default TLS options will be used instead"), false)
routerHTTPConfig.AddError(errors.New("found different TLS options for routers on the same host, so using the default TLS options instead"), false)
} }
// Even though the error is seemingly ignored (aside from logging it), // Even though the error is seemingly ignored (aside from logging it),