From 25b74ce1f31764ae6aebce79589b8953f3ece72d Mon Sep 17 00:00:00 2001 From: Simon Heimberg Date: Thu, 16 Jul 2020 12:38:03 +0200 Subject: [PATCH 1/7] Add example for entrypoint on one ip address --- docs/content/routing/entrypoints.md | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) 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-*`). From fae2d93525a381b7b494baf10d6deb4afd9a9966 Mon Sep 17 00:00:00 2001 From: Manuel Zapf Date: Thu, 16 Jul 2020 17:18:03 +0200 Subject: [PATCH 2/7] Get Entrypoints Port Address without protocol for redirect --- .../fixtures/redirection_with_protocol.json | 30 +++++++++++++++++++ pkg/provider/traefik/internal.go | 2 +- pkg/provider/traefik/internal_test.go | 22 ++++++++++++++ 3 files changed, 53 insertions(+), 1 deletion(-) create mode 100644 pkg/provider/traefik/fixtures/redirection_with_protocol.json 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 { From 45f52ca29cc178dda500f7136c359ea3707ce068 Mon Sep 17 00:00:00 2001 From: Mickael Jeanroy <1067361+mjeanroy@users.noreply.github.com> Date: Thu, 16 Jul 2020 17:36:04 +0200 Subject: [PATCH 3/7] fix: access logs header names filtering is case insensitive --- integration/helloworld/helloworld.pb.go | 3 +- pkg/middlewares/accesslog/logger.go | 12 +++ pkg/middlewares/accesslog/logger_test.go | 109 +++++++++++++++++++++-- 3 files changed, 115 insertions(+), 9 deletions(-) diff --git a/integration/helloworld/helloworld.pb.go b/integration/helloworld/helloworld.pb.go index 7b3335444..96320f02a 100644 --- a/integration/helloworld/helloworld.pb.go +++ b/integration/helloworld/helloworld.pb.go @@ -16,9 +16,10 @@ It has these top-level messages: package helloworld import ( - proto "github.com/golang/protobuf/proto" fmt "fmt" math "math" + + proto "github.com/golang/protobuf/proto" ) import ( 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 } From 0b7aaa3643cafa105c5a63ce7b41ffe91347d2c9 Mon Sep 17 00:00:00 2001 From: Julien Salleyron Date: Fri, 17 Jul 2020 15:38:04 +0200 Subject: [PATCH 4/7] Fix domain fronting Co-authored-by: Ludovic Fernandez --- docs/content/routing/routers/index.md | 5 + integration/acme_test.go | 2 +- .../fixtures/https/https_domain_fronting.toml | 53 +++++++++ integration/headers_test.go | 4 +- integration/https_test.go | 109 ++++++++++++++++-- pkg/server/router/tcp/router.go | 41 ++++++- 6 files changed, 197 insertions(+), 17 deletions(-) create mode 100644 integration/fixtures/https/https_domain_fronting.toml 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 d598873ea..a5cc4259a 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)) diff --git a/integration/https_test.go b/integration/https_test.go index 9ac854eba..6b39e343f 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,85 @@ 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: "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/server/router/tcp/router.go b/pkg/server/router/tcp/router.go index e5cee5065..8d8dbc7b8 100644 --- a/pkg/server/router/tcp/router.go +++ b/pkg/server/router/tcp/router.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" "net/http" + "strings" "github.com/containous/traefik/v2/pkg/config/runtime" "github.com/containous/traefik/v2/pkg/log" @@ -99,14 +100,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 @@ -148,10 +148,33 @@ func (m *Manager) buildEntryPointHandler(ctx context.Context, configs map[string routerName: routerHTTPName, TLSConfig: tlsConf, } + + lowerDomain := strings.ToLower(domain) + if _, ok := tlsOptionsForHost[lowerDomain]; ok { + // Multiple tlsOptions fallback to default + tlsOptionsForHost[lowerDomain] = "default" + } else { + tlsOptionsForHost[lowerDomain] = routerHTTPConfig.TLS.Options + } } } } + sniCheck := http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + if req.TLS != nil && !strings.EqualFold(req.Host, req.TLS.ServerName) { + tlsOptionSNI := findTLSOptionName(tlsOptionsForHost, req.TLS.ServerName) + tlsOptionHeader := findTLSOptionName(tlsOptionsForHost, req.Host) + + if tlsOptionHeader != tlsOptionSNI { + 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 +271,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" +} From ff16925f63cdaf2837392399b8a8b3dce7cc7b0d Mon Sep 17 00:00:00 2001 From: Romain Date: Fri, 17 Jul 2020 17:54:04 +0200 Subject: [PATCH 5/7] Prepare release v2.2.6 --- CHANGELOG.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 57ee548bd..129ddfa3b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,15 @@ +## [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.2.5](https://github.com/containous/traefik/tree/v2.2.5) (2020-07-13) [All Commits](https://github.com/containous/traefik/compare/v2.2.4...v2.2.5) From 2c7f6e4def0436a42a06118a455fcdf7b2e2fcb3 Mon Sep 17 00:00:00 2001 From: Ludovic Fernandez Date: Mon, 20 Jul 2020 18:32:03 +0200 Subject: [PATCH 6/7] fix: drop host port to compare with SNI. --- integration/https_test.go | 30 ++++++++++++++++++++++++++++ pkg/server/router/tcp/router.go | 35 ++++++++++++++++++++++++++------- pkg/tcp/router.go | 3 ++- 3 files changed, 60 insertions(+), 8 deletions(-) diff --git a/integration/https_test.go b/integration/https_test.go index 6b39e343f..e6c6c0553 100644 --- a/integration/https_test.go +++ b/integration/https_test.go @@ -1150,6 +1150,34 @@ func (s *HTTPSSuite) TestWithDomainFronting(c *check.C) { 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", @@ -1189,9 +1217,11 @@ func (s *HTTPSSuite) TestWithDomainFronting(c *check.C) { 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/server/router/tcp/router.go b/pkg/server/router/tcp/router.go index 8d8dbc7b8..f164039c7 100644 --- a/pkg/server/router/tcp/router.go +++ b/pkg/server/router/tcp/router.go @@ -5,6 +5,7 @@ import ( "crypto/tls" "errors" "fmt" + "net" "net/http" "strings" @@ -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) } @@ -149,27 +151,46 @@ func (m *Manager) buildEntryPointHandler(ctx context.Context, configs map[string TLSConfig: tlsConf, } - lowerDomain := strings.ToLower(domain) - if _, ok := tlsOptionsForHost[lowerDomain]; ok { + if _, ok := tlsOptionsForHost[domain]; ok { // Multiple tlsOptions fallback to default - tlsOptionsForHost[lowerDomain] = "default" + tlsOptionsForHost[domain] = "default" } else { - tlsOptionsForHost[lowerDomain] = routerHTTPConfig.TLS.Options + tlsOptionsForHost[domain] = routerHTTPConfig.TLS.Options } } } } sniCheck := http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { - if req.TLS != nil && !strings.EqualFold(req.Host, req.TLS.ServerName) { - tlsOptionSNI := findTLSOptionName(tlsOptionsForHost, req.TLS.ServerName) - tlsOptionHeader := findTLSOptionName(tlsOptionsForHost, req.Host) + 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) }) 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)) From bbbc18fd843ee3774569a29a2d0d293ccc3f14f4 Mon Sep 17 00:00:00 2001 From: Romain Date: Mon, 20 Jul 2020 18:48:04 +0200 Subject: [PATCH 7/7] Prepare release 2.2.7 --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 129ddfa3b..585647ac7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## [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)