diff --git a/CHANGELOG.md b/CHANGELOG.md index 369b2989d..41e9f2b3d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +## [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.6.20](https://github.com/traefik/traefik/tree/v3.6.20) (2026-06-05) [All Commits](https://github.com/traefik/traefik/compare/v3.6.19...v3.6.20) diff --git a/integration/fixtures/https/https_tls_options_conflict.toml b/integration/fixtures/https/https_tls_options_conflict.toml new file mode 100644 index 000000000..1d01b7eaa --- /dev/null +++ b/integration/fixtures/https/https_tls_options_conflict.toml @@ -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" diff --git a/integration/https_test.go b/integration/https_test.go index d0e04c021..3f8c5d059 100644 --- a/integration/https_test.go +++ b/integration/https_test.go @@ -259,7 +259,6 @@ func (s *HTTPSSuite) TestWithTLSOptions() { } // TestWithConflictingTLSOptions checks that routers with same SNI but different TLS options get fallbacked to the default TLS options. - func (s *HTTPSSuite) TestWithConflictingTLSOptions() { file := s.adaptFile("fixtures/https/https_tls_options.toml", struct{}{}) s.traefikCmd(withConfigFile(file)) @@ -317,7 +316,7 @@ func (s *HTTPSSuite) TestWithConflictingTLSOptions() { assert.ErrorContains(s.T(), err, "tls: no supported versions satisfy MinVersion and MaxVersion") // 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) } @@ -1174,6 +1173,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. func (s *HTTPSSuite) TestWithInvalidTLSOption() { backend := startTestServer("9010", http.StatusOK, "server1") diff --git a/integration/simple_test.go b/integration/simple_test.go index 87b664ff1..017217f31 100644 --- a/integration/simple_test.go +++ b/integration/simple_test.go @@ -758,7 +758,7 @@ func (s *SimpleSuite) TestRouterConfigErrors() { s.traefikCmd(withConfigFile(file)) // 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) // router3 has an error because it uses an unknown entrypoint @@ -766,11 +766,11 @@ func (s *SimpleSuite) TestRouterConfigErrors() { require.NoError(s.T(), err) // 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) // 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) } diff --git a/pkg/server/aggregator.go b/pkg/server/aggregator.go index 583fa1515..2ac129928 100644 --- a/pkg/server/aggregator.go +++ b/pkg/server/aggregator.go @@ -55,7 +55,7 @@ func mergeConfiguration(configurations dynamic.Configurations, defaultEntryPoint Str(logs.RouterName, routerName). Strs(logs.EntryPointName, defaultEntryPoints). 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. @@ -99,7 +99,7 @@ func mergeConfiguration(configurations dynamic.Configurations, defaultEntryPoint log.Debug(). Str(logs.RouterName, routerName). 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 } @@ -173,80 +173,131 @@ func mergeConfiguration(configurations dynamic.Configurations, defaultEntryPoint return conf } -func resolveHTTPTLSOptions(cfg dynamic.Configuration) dynamic.Configuration { - if cfg.HTTP == nil || len(cfg.HTTP.Routers) == 0 { - return cfg +// resolveHTTPTLSOptions resolves the TLS options for the given routers, on a per +// entryPoint basis. +// +// 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. - // The actual source of truth for what TLS options will actually be used for the connection. - // As opposed to tlsOptionsForHost, it keeps track of all the (different) TLS - // options that occur for a given host name, so that later on we can set relevant - // errors and logging for all the routers concerned (i.e. wrongly configured). - tlsOptionsForHostSNI := map[string]map[string][]string{} - - for routerHTTPName, routerHTTPConfig := range cfg.HTTP.Routers { - rts[routerHTTPName] = routerHTTPConfig.DeepCopy() - - if routerHTTPConfig.TLS == nil { + // Split every router per entryPoint. + // Routers always have at least one entryPoint at this stage, as they are + // defaulted in mergeConfiguration before applyModel and this resolution run. + routersByEntryPoint := map[string]map[string]*dynamic.Router{} + for name, router := range routers { + if router.TLS == nil { + newRouters[name] = router continue } - ctxRouter := provider.AddInContext(context.Background(), routerHTTPName) - logger := log.Ctx(ctxRouter).With().Str(logs.RouterName, routerHTTPName).Logger() - - tlsOptionsName := traefiktls.DefaultTLSConfigName - if len(routerHTTPConfig.TLS.Options) > 0 && routerHTTPConfig.TLS.Options != traefiktls.DefaultTLSConfigName { - tlsOptionsName = provider.GetQualifiedName(ctxRouter, routerHTTPConfig.TLS.Options) + router.TLS.ResolvedOptions = traefiktls.DefaultTLSConfigName + if len(router.TLS.Options) > 0 && router.TLS.Options != traefiktls.DefaultTLSConfigName { + router.TLS.ResolvedOptions = provider.GetQualifiedName(provider.AddInContext(context.Background(), name), router.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 { - logger.Error().Err(err).Msgf("Invalid rule %s", routerHTTPConfig.Rule) continue } - if len(domains) == 0 { - rts[routerHTTPName].TLS.ResolvedOptions = "default" - 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) + // The configured TLSOptions on a router without a domain in its rule cannot be selected when evaluating the SNI, + // so if it is not the default one, it is a conflict. + if len(domains) == 0 && router.TLS.ResolvedOptions != traefiktls.DefaultTLSConfigName { + conflicting = append(conflicting, name) + continue } for _, domain := range domains { - // domain is already in lower case thanks to the domain parsing - if tlsOptionsForHostSNI[domain] == nil { - tlsOptionsForHostSNI[domain] = make(map[string][]string) + if routersByHostAndOption[domain] == nil { + routersByHostAndOption[domain] = 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 { - if len(tlsConfigs) == 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 - } - } + for domain, routersByOption := range routersByHostAndOption { + if len(routersByOption) == 1 { continue } - // multiple tlsConfigs - routers := make([]string, 0, len(tlsConfigs)) - for _, v := range tlsConfigs { - for _, s := range v { - rts[s].TLS.ResolvedOptions = traefiktls.DefaultTLSConfigName - routers = append(routers, s) - } + var routersInConflict []string + for _, names := range routersByOption { + conflicting = append(conflicting, names...) + routersInConflict = append(routersInConflict, names...) } - 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 cfg + return conflicting } func applyModel(cfg dynamic.Configuration) dynamic.Configuration { diff --git a/pkg/server/aggregator_test.go b/pkg/server/aggregator_test.go index 8d78447d2..f169f20ed 100644 --- a/pkg/server/aggregator_test.go +++ b/pkg/server/aggregator_test.go @@ -5,6 +5,7 @@ import ( "github.com/go-acme/lego/v4/challenge/tlsalpn01" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/traefik/traefik/v3/pkg/config/dynamic" otypes "github.com/traefik/traefik/v3/pkg/observability/types" "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) + } + }) + } +} diff --git a/pkg/server/configurationwatcher.go b/pkg/server/configurationwatcher.go index eaffb327a..d11d3fbf9 100644 --- a/pkg/server/configurationwatcher.go +++ b/pkg/server/configurationwatcher.go @@ -167,7 +167,9 @@ func (c *ConfigurationWatcher) applyConfigurations(ctx context.Context) { conf := mergeConfiguration(newConfigs.DeepCopy(), c.defaultEntryPoints) conf = applyModel(conf) - conf = resolveHTTPTLSOptions(conf) + if conf.HTTP != nil { + conf.HTTP.Routers = resolveHTTPTLSOptions(conf.HTTP.Routers) + } for _, listener := range c.configurationListeners { listener(conf) diff --git a/pkg/server/router/tcp/manager.go b/pkg/server/router/tcp/manager.go index 8e5ce9a35..1061869d7 100644 --- a/pkg/server/router/tcp/manager.go +++ b/pkg/server/router/tcp/manager.go @@ -169,8 +169,7 @@ func (m *Manager) buildEntryPointHandler(ctx context.Context, configs map[string } 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("found different TLS options for routers on the same host, so using the default TLS options instead"), false) + 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) } // Even though the error is seemingly ignored (aside from logging it),