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" +}