Fix domain fronting

Co-authored-by: Ludovic Fernandez <ldez@users.noreply.github.com>
This commit is contained in:
Julien Salleyron 2020-07-17 15:38:04 +02:00 committed by GitHub
parent 45f52ca29c
commit 0b7aaa3643
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 197 additions and 17 deletions

View file

@ -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)"

View file

@ -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 {

View file

@ -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"

View file

@ -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))

View file

@ -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)
}
}

View file

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