diff --git a/CHANGELOG.md b/CHANGELOG.md index 64b7382de..f26f6a1ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,21 @@ +## [v2.2.7](https://github.com/containous/traefik/tree/v2.2.7) (2020-07-20) +[All Commits](https://github.com/containous/traefik/compare/v2.2.6...v2.2.7) + +**Bug fixes:** +- **[server,tls]** fix: drop host port to compare with SNI. ([#7071](https://github.com/containous/traefik/pull/7071) by [ldez](https://github.com/ldez)) + +## [v2.2.6](https://github.com/containous/traefik/tree/v2.2.6) (2020-07-17) +[All Commits](https://github.com/containous/traefik/compare/v2.2.5...v2.2.6) + +**Bug fixes:** +- **[logs]** fix: access logs header names filtering is case insensitive ([#6900](https://github.com/containous/traefik/pull/6900) by [mjeanroy](https://github.com/mjeanroy)) +- **[provider]** Get Entrypoints Port Address without protocol for redirect ([#7047](https://github.com/containous/traefik/pull/7047) by [SantoDE](https://github.com/SantoDE)) +- **[tls]** Fix domain fronting ([#7064](https://github.com/containous/traefik/pull/7064) by [juliens](https://github.com/juliens)) + +**Documentation:** +- fix: documentation references. ([#7049](https://github.com/containous/traefik/pull/7049) by [ldez](https://github.com/ldez)) +- Add example for entrypoint on one ip address ([#6483](https://github.com/containous/traefik/pull/6483) by [SimonHeimberg](https://github.com/SimonHeimberg)) + ## [v2.3.0-rc2](https://github.com/containous/traefik/tree/v2.3.0-rc2) (2020-07-15) [All Commits](https://github.com/containous/traefik/compare/v2.3.0-rc1...v2.3.0-rc2) diff --git a/docs/content/https/acme.md b/docs/content/https/acme.md index de1efe94b..34aa0a49d 100644 --- a/docs/content/https/acme.md +++ b/docs/content/https/acme.md @@ -362,7 +362,7 @@ For complete details, refer to your provider's _Additional configuration_ link. | [Zonomi](https://zonomi.com) | `zonomi` | `ZONOMI_API_KEY` | [Additional configuration](https://go-acme.github.io/lego/dns/zonomi) | [^1]: more information about the HTTP message format can be found [here](https://go-acme.github.io/lego/dns/httpreq/) -[^2]: [providing_credentials_to_your_application](https://cloud.google.com/docs/authentication/production#providing_credentials_to_your_application) +[^2]: [providing_credentials_to_your_application](https://cloud.google.com/docs/authentication/production) [^3]: [google/default.go](https://github.com/golang/oauth2/blob/36a7019397c4c86cf59eeab3bc0d188bac444277/google/default.go#L61-L76) [^4]: `docker stack` remark: there is no way to support terminal attached to container when deploying with `docker stack`, so you might need to run container with `docker run -it` to generate certificates using `manual` provider. [^5]: The `Global API Key` needs to be used, not the `Origin CA Key`. diff --git a/docs/content/routing/entrypoints.md b/docs/content/routing/entrypoints.md index 52290000e..dc6d97d55 100644 --- a/docs/content/routing/entrypoints.md +++ b/docs/content/routing/entrypoints.md @@ -168,7 +168,7 @@ The format is: If both TCP and UDP are wanted for the same port, two entryPoints definitions are needed, such as in the example below. -??? example "Both TCP and UDP on port 3179" +??? example "Both TCP and UDP on Port 3179" ```toml tab="File (TOML)" ## Static configuration @@ -194,6 +194,30 @@ If both TCP and UDP are wanted for the same port, two entryPoints definitions ar --entryPoints.udpep.address=:3179/udp ``` +??? example "Listen on Specific IP Addresses Only" + + ```toml tab="File (TOML)" + [entryPoints.specificIPv4] + address = "192.168.2.7:8888" + [entryPoints.specificIPv6] + address = "[2001:db8::1]:8888" + ``` + + ```yaml tab="File (yaml)" + entryPoints: + specificIPv4: + address: "192.168.2.7:8888" + specificIPv6: + address: "[2001:db8::1]:8888" + ``` + + ```bash tab="CLI" + entrypoints.specificIPv4.address=192.168.2.7:8888 + entrypoints.specificIPv6.address=[2001:db8::1]:8888 + ``` + + Full details for how to specify `address` can be found in [net.Listen](https://golang.org/pkg/net/#Listen) (and [net.Dial](https://golang.org/pkg/net/#Dial)) of the doc for go. + ### Forwarded Headers You can configure Traefik to trust the forwarded headers information (`X-Forwarded-*`). diff --git a/docs/content/routing/routers/index.md b/docs/content/routing/routers/index.md index 81d5a4083..d97e079a7 100644 --- a/docs/content/routing/routers/index.md +++ b/docs/content/routing/routers/index.md @@ -472,6 +472,11 @@ It refers to a [TLS Options](../../https/tls.md#tls-options) and will be applied the TLS option is picked from the mapping mentioned above and based on the server name provided during the TLS handshake, and it all happens before routing actually occurs. +!!! info "Domain Fronting" + + In the case of domain fronting, + if the TLS options associated with the Host Header and the SNI are different then Traefik will respond with a status code `421`. + ??? example "Configuring the TLS options" ```toml tab="File (TOML)" diff --git a/integration/acme_test.go b/integration/acme_test.go index a5e34e695..9c757b61d 100644 --- a/integration/acme_test.go +++ b/integration/acme_test.go @@ -444,7 +444,7 @@ func (s *AcmeSuite) retrieveAcmeCertificate(c *check.C, testCase acmeTestCase) { // A real file is needed to have the right mode on acme.json file defer os.Remove("/tmp/acme.json") - backend := startTestServer("9010", http.StatusOK) + backend := startTestServer("9010", http.StatusOK, "") defer backend.Close() for _, sub := range testCase.subCases { diff --git a/integration/fixtures/https/https_domain_fronting.toml b/integration/fixtures/https/https_domain_fronting.toml new file mode 100644 index 000000000..80011ee8e --- /dev/null +++ b/integration/fixtures/https/https_domain_fronting.toml @@ -0,0 +1,53 @@ +[global] + checkNewVersion = false + sendAnonymousUsage = false + +[log] + level = "DEBUG" + +[entryPoints.websecure] + address = ":4443" + +[api] + insecure = true + +[providers.file] + filename = "{{ .SelfFilename }}" + +## dynamic configuration ## + +[http.routers.router1] + rule = "Host(`site1.www.snitest.com`)" + service = "service1" + [http.routers.router1.tls] + +[http.routers.router2] + rule = "Host(`site2.www.snitest.com`)" + service = "service2" + [http.routers.router2.tls] + +[http.routers.router3] + rule = "Host(`site3.www.snitest.com`)" + service = "service3" + [http.routers.router3.tls] + options = "mytls" + +[http.services.service1] + [[http.services.service1.loadBalancer.servers]] + url = "http://127.0.0.1:9010" + +[http.services.service2] + [[http.services.service2.loadBalancer.servers]] + url = "http://127.0.0.1:9020" + +[http.services.service3] + [[http.services.service3.loadBalancer.servers]] + url = "http://127.0.0.1:9030" + +[[tls.certificates]] + certFile = "fixtures/https/wildcard.www.snitest.com.cert" + keyFile = "fixtures/https/wildcard.www.snitest.com.key" + +[tls.options] + [tls.options.mytls] + maxVersion = "VersionTLS12" diff --git a/integration/headers_test.go b/integration/headers_test.go index b2e34beff..c464bf97c 100644 --- a/integration/headers_test.go +++ b/integration/headers_test.go @@ -35,7 +35,7 @@ func (s *HeadersSuite) TestCorsResponses(c *check.C) { c.Assert(err, checker.IsNil) defer cmd.Process.Kill() - backend := startTestServer("9000", http.StatusOK) + backend := startTestServer("9000", http.StatusOK, "") defer backend.Close() err = try.GetRequest(backend.URL, 500*time.Millisecond, try.StatusCodeIs(http.StatusOK)) @@ -124,7 +124,7 @@ func (s *HeadersSuite) TestSecureHeadersResponses(c *check.C) { c.Assert(err, checker.IsNil) defer cmd.Process.Kill() - backend := startTestServer("9000", http.StatusOK) + backend := startTestServer("9000", http.StatusOK, "") defer backend.Close() err = try.GetRequest(backend.URL, 500*time.Millisecond, try.StatusCodeIs(http.StatusOK)) @@ -173,7 +173,7 @@ func (s *HeadersSuite) TestMultipleSecureHeadersResponses(c *check.C) { c.Assert(err, checker.IsNil) defer cmd.Process.Kill() - backend := startTestServer("9000", http.StatusOK) + backend := startTestServer("9000", http.StatusOK, "") defer backend.Close() err = try.GetRequest(backend.URL, 500*time.Millisecond, try.StatusCodeIs(http.StatusOK)) diff --git a/integration/https_test.go b/integration/https_test.go index 9ac854eba..e6c6c0553 100644 --- a/integration/https_test.go +++ b/integration/https_test.go @@ -73,8 +73,8 @@ func (s *HTTPSSuite) TestWithSNIConfigRoute(c *check.C) { err = try.GetRequest("http://127.0.0.1:8080/api/rawdata", 1*time.Second, try.BodyContains("Host(`snitest.org`)")) c.Assert(err, checker.IsNil) - backend1 := startTestServer("9010", http.StatusNoContent) - backend2 := startTestServer("9020", http.StatusResetContent) + backend1 := startTestServer("9010", http.StatusNoContent, "") + backend2 := startTestServer("9020", http.StatusResetContent, "") defer backend1.Close() defer backend2.Close() @@ -129,8 +129,8 @@ func (s *HTTPSSuite) TestWithTLSOptions(c *check.C) { err = try.GetRequest("http://127.0.0.1:8080/api/rawdata", 1*time.Second, try.BodyContains("Host(`snitest.org`)")) c.Assert(err, checker.IsNil) - backend1 := startTestServer("9010", http.StatusNoContent) - backend2 := startTestServer("9020", http.StatusResetContent) + backend1 := startTestServer("9010", http.StatusNoContent, "") + backend2 := startTestServer("9020", http.StatusResetContent, "") defer backend1.Close() defer backend2.Close() @@ -215,8 +215,8 @@ func (s *HTTPSSuite) TestWithConflictingTLSOptions(c *check.C) { err = try.GetRequest("http://127.0.0.1:8080/api/rawdata", 1*time.Second, try.BodyContains("Host(`snitest.net`)")) c.Assert(err, checker.IsNil) - backend1 := startTestServer("9010", http.StatusNoContent) - backend2 := startTestServer("9020", http.StatusResetContent) + backend1 := startTestServer("9010", http.StatusNoContent, "") + backend2 := startTestServer("9020", http.StatusResetContent, "") defer backend1.Close() defer backend2.Close() @@ -733,9 +733,12 @@ func (s *HTTPSSuite) TestWithRootCAsFileForHTTPSOnBackend(c *check.C) { c.Assert(err, checker.IsNil) } -func startTestServer(port string, statusCode int) (ts *httptest.Server) { +func startTestServer(port string, statusCode int, textContent string) (ts *httptest.Server) { handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(statusCode) + if textContent != "" { + _, _ = w.Write([]byte(textContent)) + } }) listener, err := net.Listen("tcp", "127.0.0.1:"+port) if err != nil { @@ -787,8 +790,8 @@ func (s *HTTPSSuite) TestWithSNIDynamicConfigRouteWithNoChange(c *check.C) { err = try.GetRequest("http://127.0.0.1:8080/api/rawdata", 1*time.Second, try.BodyContains("Host(`"+tr1.TLSClientConfig.ServerName+"`)")) c.Assert(err, checker.IsNil) - backend1 := startTestServer("9010", http.StatusNoContent) - backend2 := startTestServer("9020", http.StatusResetContent) + backend1 := startTestServer("9010", http.StatusNoContent, "") + backend2 := startTestServer("9020", http.StatusResetContent, "") defer backend1.Close() defer backend2.Close() @@ -856,8 +859,8 @@ func (s *HTTPSSuite) TestWithSNIDynamicConfigRouteWithChange(c *check.C) { err = try.GetRequest("http://127.0.0.1:8080/api/rawdata", 1*time.Second, try.BodyContains("Host(`"+tr2.TLSClientConfig.ServerName+"`)")) c.Assert(err, checker.IsNil) - backend1 := startTestServer("9010", http.StatusNoContent) - backend2 := startTestServer("9020", http.StatusResetContent) + backend1 := startTestServer("9010", http.StatusNoContent, "") + backend2 := startTestServer("9020", http.StatusResetContent, "") defer backend1.Close() defer backend2.Close() @@ -919,7 +922,7 @@ func (s *HTTPSSuite) TestWithSNIDynamicConfigRouteWithTlsConfigurationDeletion(c err = try.GetRequest("http://127.0.0.1:8080/api/rawdata", 1*time.Second, try.BodyContains("Host(`"+tr2.TLSClientConfig.ServerName+"`)")) c.Assert(err, checker.IsNil) - backend2 := startTestServer("9020", http.StatusResetContent) + backend2 := startTestServer("9020", http.StatusResetContent, "") defer backend2.Close() @@ -1111,3 +1114,115 @@ func (s *HTTPSSuite) TestWithSNIDynamicCaseInsensitive(c *check.C) { proto := conn.ConnectionState().NegotiatedProtocol c.Assert(proto, checker.Equals, "h2") } + +// TestWithDomainFronting verify the domain fronting behavior +func (s *HTTPSSuite) TestWithDomainFronting(c *check.C) { + backend := startTestServer("9010", http.StatusOK, "server1") + defer backend.Close() + backend2 := startTestServer("9020", http.StatusOK, "server2") + defer backend2.Close() + backend3 := startTestServer("9030", http.StatusOK, "server3") + defer backend3.Close() + + file := s.adaptFile(c, "fixtures/https/https_domain_fronting.toml", struct{}{}) + defer os.Remove(file) + cmd, display := s.traefikCmd(withConfigFile(file)) + defer display(c) + err := cmd.Start() + c.Assert(err, checker.IsNil) + defer cmd.Process.Kill() + + // wait for Traefik + err = try.GetRequest("http://127.0.0.1:8080/api/rawdata", 500*time.Millisecond, try.BodyContains("Host(`site1.www.snitest.com`)")) + c.Assert(err, checker.IsNil) + + testCases := []struct { + desc string + hostHeader string + serverName string + expectedContent string + expectedStatusCode int + }{ + { + desc: "SimpleCase", + hostHeader: "site1.www.snitest.com", + serverName: "site1.www.snitest.com", + expectedContent: "server1", + expectedStatusCode: http.StatusOK, + }, + { + desc: "Simple case with port in the Host Header", + hostHeader: "site3.www.snitest.com:4443", + serverName: "site3.www.snitest.com", + expectedContent: "server3", + expectedStatusCode: http.StatusOK, + }, + { + desc: "Spaces after the host header", + hostHeader: "site3.www.snitest.com ", + serverName: "site3.www.snitest.com", + expectedContent: "server3", + expectedStatusCode: http.StatusOK, + }, + { + desc: "Spaces after the servername", + hostHeader: "site3.www.snitest.com", + serverName: "site3.www.snitest.com ", + expectedContent: "server3", + expectedStatusCode: http.StatusOK, + }, + { + desc: "Spaces after the servername and host header", + hostHeader: "site3.www.snitest.com ", + serverName: "site3.www.snitest.com ", + expectedContent: "server3", + expectedStatusCode: http.StatusOK, + }, + { + desc: "Domain Fronting with same tlsOptions should follow header", + hostHeader: "site1.www.snitest.com", + serverName: "site2.www.snitest.com", + expectedContent: "server1", + expectedStatusCode: http.StatusOK, + }, + { + desc: "Domain Fronting with same tlsOptions should follow header (2)", + hostHeader: "site2.www.snitest.com", + serverName: "site1.www.snitest.com", + expectedContent: "server2", + expectedStatusCode: http.StatusOK, + }, + { + desc: "Domain Fronting with different tlsOptions should produce a 421", + hostHeader: "site2.www.snitest.com", + serverName: "site3.www.snitest.com", + expectedContent: "", + expectedStatusCode: http.StatusMisdirectedRequest, + }, + { + desc: "Domain Fronting with different tlsOptions should produce a 421 (2)", + hostHeader: "site3.www.snitest.com", + serverName: "site1.www.snitest.com", + expectedContent: "", + expectedStatusCode: http.StatusMisdirectedRequest, + }, + { + desc: "Case insensitive", + hostHeader: "sIte1.www.snitest.com", + serverName: "sitE1.www.snitest.com", + expectedContent: "server1", + expectedStatusCode: http.StatusOK, + }, + } + + for _, test := range testCases { + test := test + + req, err := http.NewRequest(http.MethodGet, "https://127.0.0.1:4443", nil) + c.Assert(err, checker.IsNil) + req.Host = test.hostHeader + + err = try.RequestWithTransport(req, 500*time.Millisecond, &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true, ServerName: test.serverName}}, try.StatusCodeIs(test.expectedStatusCode), try.BodyContains(test.expectedContent)) + c.Assert(err, checker.IsNil) + } +} diff --git a/pkg/middlewares/accesslog/logger.go b/pkg/middlewares/accesslog/logger.go index c876efcb3..13af24c9b 100644 --- a/pkg/middlewares/accesslog/logger.go +++ b/pkg/middlewares/accesslog/logger.go @@ -6,6 +6,7 @@ import ( "io" "net" "net/http" + "net/textproto" "net/url" "os" "path/filepath" @@ -100,6 +101,17 @@ func NewHandler(config *types.AccessLog) (*Handler, error) { Level: logrus.InfoLevel, } + // Transform headers names in config to a canonical form, to be used as is without further transformations. + if config.Fields != nil && config.Fields.Headers != nil && len(config.Fields.Headers.Names) > 0 { + fields := map[string]string{} + + for h, v := range config.Fields.Headers.Names { + fields[textproto.CanonicalMIMEHeaderKey(h)] = v + } + + config.Fields.Headers.Names = fields + } + logHandler := &Handler{ config: config, logger: logger, diff --git a/pkg/middlewares/accesslog/logger_test.go b/pkg/middlewares/accesslog/logger_test.go index 6d1567db0..eeeceba7b 100644 --- a/pkg/middlewares/accesslog/logger_test.go +++ b/pkg/middlewares/accesslog/logger_test.go @@ -41,11 +41,7 @@ var ( ) func TestLogRotation(t *testing.T) { - tempDir, err := ioutil.TempDir("", "traefik_") - if err != nil { - t.Fatalf("Error setting up temporary directory: %s", err) - } - defer os.RemoveAll(tempDir) + tempDir := createTempDir(t, "traefik_") fileName := filepath.Join(tempDir, "traefik.log") rotatedFileName := fileName + ".rotated" @@ -119,9 +115,106 @@ func lineCount(t *testing.T, fileName string) int { return count } +func TestLoggerHeaderFields(t *testing.T) { + tmpDir := createTempDir(t, CommonFormat) + + expectedValue := "expectedValue" + + testCases := []struct { + desc string + accessLogFields types.AccessLogFields + header string + expected string + }{ + { + desc: "with default mode", + header: "User-Agent", + expected: types.AccessLogDrop, + accessLogFields: types.AccessLogFields{ + DefaultMode: types.AccessLogDrop, + Headers: &types.FieldHeaders{ + DefaultMode: types.AccessLogDrop, + Names: map[string]string{}, + }, + }, + }, + { + desc: "with exact header name", + header: "User-Agent", + expected: types.AccessLogKeep, + accessLogFields: types.AccessLogFields{ + DefaultMode: types.AccessLogDrop, + Headers: &types.FieldHeaders{ + DefaultMode: types.AccessLogDrop, + Names: map[string]string{ + "User-Agent": types.AccessLogKeep, + }, + }, + }, + }, + { + desc: "with case insensitive match on header name", + header: "User-Agent", + expected: types.AccessLogKeep, + accessLogFields: types.AccessLogFields{ + DefaultMode: types.AccessLogDrop, + Headers: &types.FieldHeaders{ + DefaultMode: types.AccessLogDrop, + Names: map[string]string{ + "user-agent": types.AccessLogKeep, + }, + }, + }, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + logFile, err := ioutil.TempFile(tmpDir, "*.log") + require.NoError(t, err) + + config := &types.AccessLog{ + FilePath: logFile.Name(), + Format: CommonFormat, + Fields: &test.accessLogFields, + } + + logger, err := NewHandler(config) + require.NoError(t, err) + defer logger.Close() + + if config.FilePath != "" { + _, err = os.Stat(config.FilePath) + require.NoError(t, err, fmt.Sprintf("logger should create %s", config.FilePath)) + } + + req := &http.Request{ + Header: map[string][]string{}, + URL: &url.URL{ + Path: testPath, + }, + } + req.Header.Set(test.header, expectedValue) + + logger.ServeHTTP(httptest.NewRecorder(), req, http.HandlerFunc(func(writer http.ResponseWriter, r *http.Request) { + writer.WriteHeader(http.StatusOK) + })) + + logData, err := ioutil.ReadFile(logFile.Name()) + require.NoError(t, err) + + if test.expected == types.AccessLogDrop { + assert.NotContains(t, string(logData), expectedValue) + } else { + assert.Contains(t, string(logData), expectedValue) + } + }) + } +} + func TestLoggerCLF(t *testing.T) { tmpDir := createTempDir(t, CommonFormat) - defer os.RemoveAll(tmpDir) logFilePath := filepath.Join(tmpDir, logFileNameSuffix) config := &types.AccessLog{FilePath: logFilePath, Format: CommonFormat} @@ -136,7 +229,6 @@ func TestLoggerCLF(t *testing.T) { func TestAsyncLoggerCLF(t *testing.T) { tmpDir := createTempDir(t, CommonFormat) - defer os.RemoveAll(tmpDir) logFilePath := filepath.Join(tmpDir, logFileNameSuffix) config := &types.AccessLog{FilePath: logFilePath, Format: CommonFormat, BufferingSize: 1024} @@ -358,7 +450,6 @@ func TestLoggerJSON(t *testing.T) { t.Parallel() tmpDir := createTempDir(t, JSONFormat) - defer os.RemoveAll(tmpDir) logFilePath := filepath.Join(tmpDir, logFileNameSuffix) @@ -642,6 +733,8 @@ func createTempDir(t *testing.T, prefix string) string { tmpDir, err := ioutil.TempDir("", prefix) require.NoError(t, err, "failed to create temp dir") + t.Cleanup(func() { _ = os.RemoveAll(tmpDir) }) + return tmpDir } diff --git a/pkg/provider/traefik/fixtures/redirection_with_protocol.json b/pkg/provider/traefik/fixtures/redirection_with_protocol.json new file mode 100644 index 000000000..4ffbb756c --- /dev/null +++ b/pkg/provider/traefik/fixtures/redirection_with_protocol.json @@ -0,0 +1,30 @@ +{ + "http": { + "routers": { + "web-to-websecure": { + "entryPoints": [ + "web" + ], + "middlewares": [ + "redirect-web-to-websecure" + ], + "service": "noop@internal", + "rule": "HostRegexp(`{host:.+}`)" + } + }, + "middlewares": { + "redirect-web-to-websecure": { + "redirectScheme": { + "scheme": "https", + "port": "443", + "permanent": true + } + } + }, + "services": { + "noop": {} + } + }, + "tcp": {}, + "tls": {} +} \ No newline at end of file diff --git a/pkg/provider/traefik/internal.go b/pkg/provider/traefik/internal.go index a63a2f49b..4c341f56b 100644 --- a/pkg/provider/traefik/internal.go +++ b/pkg/provider/traefik/internal.go @@ -142,7 +142,7 @@ func (i *Provider) getEntryPointPort(name string, def *static.Redirections) (str return "", fmt.Errorf("'to' entry point field references a non-existing entry point: %s", def.EntryPoint.To) } - _, port, err := net.SplitHostPort(dst.Address) + _, port, err := net.SplitHostPort(dst.GetAddress()) if err != nil { return "", fmt.Errorf("invalid entry point %q address %q: %w", name, i.staticCfg.EntryPoints[def.EntryPoint.To].Address, err) diff --git a/pkg/provider/traefik/internal_test.go b/pkg/provider/traefik/internal_test.go index dd6ef8c6a..b9e0af9c4 100644 --- a/pkg/provider/traefik/internal_test.go +++ b/pkg/provider/traefik/internal_test.go @@ -232,6 +232,28 @@ func Test_createConfiguration(t *testing.T) { }, }, }, + { + desc: "redirection_with_protocol.json", + staticCfg: static.Configuration{ + EntryPoints: map[string]*static.EntryPoint{ + "web": { + Address: ":80", + HTTP: static.HTTPConfig{ + Redirections: &static.Redirections{ + EntryPoint: &static.RedirectEntryPoint{ + To: "websecure", + Scheme: "https", + Permanent: true, + }, + }, + }, + }, + "websecure": { + Address: ":443/tcp", + }, + }, + }, + }, } for _, test := range testCases { diff --git a/pkg/server/router/tcp/router.go b/pkg/server/router/tcp/router.go index e5cee5065..f164039c7 100644 --- a/pkg/server/router/tcp/router.go +++ b/pkg/server/router/tcp/router.go @@ -5,7 +5,9 @@ import ( "crypto/tls" "errors" "fmt" + "net" "net/http" + "strings" "github.com/containous/traefik/v2/pkg/config/runtime" "github.com/containous/traefik/v2/pkg/log" @@ -99,14 +101,13 @@ func (m *Manager) buildEntryPointHandler(ctx context.Context, configs map[string log.FromContext(ctx).Errorf("Error during the build of the default TLS configuration: %v", err) } - router.HTTPSHandler(handlerHTTPS, defaultTLSConf) - if len(configsHTTP) > 0 { router.AddRouteHTTPTLS("*", defaultTLSConf) } // Keyed by domain, then by options reference. tlsOptionsForHostSNI := map[string]map[string]nameAndConfig{} + tlsOptionsForHost := map[string]string{} for routerHTTPName, routerHTTPConfig := range configsHTTP { if len(routerHTTPConfig.TLS.Options) == 0 || routerHTTPConfig.TLS.Options == defaultTLSConfigName { continue @@ -141,6 +142,7 @@ func (m *Manager) buildEntryPointHandler(ctx context.Context, configs map[string continue } + // domain is already in lower case thanks to the domain parsing if tlsOptionsForHostSNI[domain] == nil { tlsOptionsForHostSNI[domain] = make(map[string]nameAndConfig) } @@ -148,10 +150,52 @@ func (m *Manager) buildEntryPointHandler(ctx context.Context, configs map[string routerName: routerHTTPName, TLSConfig: tlsConf, } + + if _, ok := tlsOptionsForHost[domain]; ok { + // Multiple tlsOptions fallback to default + tlsOptionsForHost[domain] = "default" + } else { + tlsOptionsForHost[domain] = routerHTTPConfig.TLS.Options + } } } } + sniCheck := http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + if req.TLS == nil { + handlerHTTPS.ServeHTTP(rw, req) + return + } + + host, _, err := net.SplitHostPort(req.Host) + if err != nil { + host = req.Host + } + + host = strings.TrimSpace(host) + serverName := strings.TrimSpace(req.TLS.ServerName) + + // Domain Fronting + if !strings.EqualFold(host, serverName) { + tlsOptionSNI := findTLSOptionName(tlsOptionsForHost, serverName) + tlsOptionHeader := findTLSOptionName(tlsOptionsForHost, host) + + if tlsOptionHeader != tlsOptionSNI { + log.WithoutContext(). + WithField("host", host). + WithField("req.Host", req.Host). + WithField("req.TLS.ServerName", req.TLS.ServerName). + Debugf("TLS options difference: SNI=%s, Header:%s", tlsOptionSNI, tlsOptionHeader) + http.Error(rw, http.StatusText(http.StatusMisdirectedRequest), http.StatusMisdirectedRequest) + return + } + } + + handlerHTTPS.ServeHTTP(rw, req) + }) + + router.HTTPSHandler(sniCheck, defaultTLSConf) + logger := log.FromContext(ctx) for hostSNI, tlsConfigs := range tlsOptionsForHostSNI { if len(tlsConfigs) == 1 { @@ -248,3 +292,17 @@ func (m *Manager) buildEntryPointHandler(ctx context.Context, configs map[string return router, nil } + +func findTLSOptionName(tlsOptionsForHost map[string]string, host string) string { + tlsOptions, ok := tlsOptionsForHost[host] + if ok { + return tlsOptions + } + + tlsOptions, ok = tlsOptionsForHost[strings.ToLower(host)] + if ok { + return tlsOptions + } + + return "default" +} diff --git a/pkg/tcp/router.go b/pkg/tcp/router.go index c66e987ce..601f18190 100644 --- a/pkg/tcp/router.go +++ b/pkg/tcp/router.go @@ -11,6 +11,7 @@ import ( "time" "github.com/containous/traefik/v2/pkg/log" + "github.com/containous/traefik/v2/pkg/types" ) // Router is a TCP router. @@ -65,7 +66,7 @@ func (r *Router) ServeTCP(conn WriteCloser) { } // FIXME Optimize and test the routing table before helloServerName - serverName = strings.ToLower(serverName) + serverName = types.CanonicalDomain(serverName) if r.routingTable != nil && serverName != "" { if target, ok := r.routingTable[serverName]; ok { target.ServeTCP(r.GetConn(conn, peeked))