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, 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. 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" ??? example "Configuring the TLS options"
```toml tab="File (TOML)" ```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 // A real file is needed to have the right mode on acme.json file
defer os.Remove("/tmp/acme.json") defer os.Remove("/tmp/acme.json")
backend := startTestServer("9010", http.StatusOK) backend := startTestServer("9010", http.StatusOK, "")
defer backend.Close() defer backend.Close()
for _, sub := range testCase.subCases { 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) c.Assert(err, checker.IsNil)
defer cmd.Process.Kill() defer cmd.Process.Kill()
backend := startTestServer("9000", http.StatusOK) backend := startTestServer("9000", http.StatusOK, "")
defer backend.Close() defer backend.Close()
err = try.GetRequest(backend.URL, 500*time.Millisecond, try.StatusCodeIs(http.StatusOK)) 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) c.Assert(err, checker.IsNil)
defer cmd.Process.Kill() defer cmd.Process.Kill()
backend := startTestServer("9000", http.StatusOK) backend := startTestServer("9000", http.StatusOK, "")
defer backend.Close() defer backend.Close()
err = try.GetRequest(backend.URL, 500*time.Millisecond, try.StatusCodeIs(http.StatusOK)) 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`)")) err = try.GetRequest("http://127.0.0.1:8080/api/rawdata", 1*time.Second, try.BodyContains("Host(`snitest.org`)"))
c.Assert(err, checker.IsNil) c.Assert(err, checker.IsNil)
backend1 := startTestServer("9010", http.StatusNoContent) backend1 := startTestServer("9010", http.StatusNoContent, "")
backend2 := startTestServer("9020", http.StatusResetContent) backend2 := startTestServer("9020", http.StatusResetContent, "")
defer backend1.Close() defer backend1.Close()
defer backend2.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`)")) err = try.GetRequest("http://127.0.0.1:8080/api/rawdata", 1*time.Second, try.BodyContains("Host(`snitest.org`)"))
c.Assert(err, checker.IsNil) c.Assert(err, checker.IsNil)
backend1 := startTestServer("9010", http.StatusNoContent) backend1 := startTestServer("9010", http.StatusNoContent, "")
backend2 := startTestServer("9020", http.StatusResetContent) backend2 := startTestServer("9020", http.StatusResetContent, "")
defer backend1.Close() defer backend1.Close()
defer backend2.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`)")) err = try.GetRequest("http://127.0.0.1:8080/api/rawdata", 1*time.Second, try.BodyContains("Host(`snitest.net`)"))
c.Assert(err, checker.IsNil) c.Assert(err, checker.IsNil)
backend1 := startTestServer("9010", http.StatusNoContent) backend1 := startTestServer("9010", http.StatusNoContent, "")
backend2 := startTestServer("9020", http.StatusResetContent) backend2 := startTestServer("9020", http.StatusResetContent, "")
defer backend1.Close() defer backend1.Close()
defer backend2.Close() defer backend2.Close()
@ -733,9 +733,12 @@ func (s *HTTPSSuite) TestWithRootCAsFileForHTTPSOnBackend(c *check.C) {
c.Assert(err, checker.IsNil) 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) { handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(statusCode) w.WriteHeader(statusCode)
if textContent != "" {
_, _ = w.Write([]byte(textContent))
}
}) })
listener, err := net.Listen("tcp", "127.0.0.1:"+port) listener, err := net.Listen("tcp", "127.0.0.1:"+port)
if err != nil { 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+"`)")) 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) c.Assert(err, checker.IsNil)
backend1 := startTestServer("9010", http.StatusNoContent) backend1 := startTestServer("9010", http.StatusNoContent, "")
backend2 := startTestServer("9020", http.StatusResetContent) backend2 := startTestServer("9020", http.StatusResetContent, "")
defer backend1.Close() defer backend1.Close()
defer backend2.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+"`)")) 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) c.Assert(err, checker.IsNil)
backend1 := startTestServer("9010", http.StatusNoContent) backend1 := startTestServer("9010", http.StatusNoContent, "")
backend2 := startTestServer("9020", http.StatusResetContent) backend2 := startTestServer("9020", http.StatusResetContent, "")
defer backend1.Close() defer backend1.Close()
defer backend2.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+"`)")) 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) c.Assert(err, checker.IsNil)
backend2 := startTestServer("9020", http.StatusResetContent) backend2 := startTestServer("9020", http.StatusResetContent, "")
defer backend2.Close() defer backend2.Close()
@ -1111,3 +1114,85 @@ func (s *HTTPSSuite) TestWithSNIDynamicCaseInsensitive(c *check.C) {
proto := conn.ConnectionState().NegotiatedProtocol proto := conn.ConnectionState().NegotiatedProtocol
c.Assert(proto, checker.Equals, "h2") 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" "errors"
"fmt" "fmt"
"net/http" "net/http"
"strings"
"github.com/containous/traefik/v2/pkg/config/runtime" "github.com/containous/traefik/v2/pkg/config/runtime"
"github.com/containous/traefik/v2/pkg/log" "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) log.FromContext(ctx).Errorf("Error during the build of the default TLS configuration: %v", err)
} }
router.HTTPSHandler(handlerHTTPS, defaultTLSConf)
if len(configsHTTP) > 0 { if len(configsHTTP) > 0 {
router.AddRouteHTTPTLS("*", defaultTLSConf) router.AddRouteHTTPTLS("*", defaultTLSConf)
} }
// Keyed by domain, then by options reference. // Keyed by domain, then by options reference.
tlsOptionsForHostSNI := map[string]map[string]nameAndConfig{} tlsOptionsForHostSNI := map[string]map[string]nameAndConfig{}
tlsOptionsForHost := map[string]string{}
for routerHTTPName, routerHTTPConfig := range configsHTTP { for routerHTTPName, routerHTTPConfig := range configsHTTP {
if len(routerHTTPConfig.TLS.Options) == 0 || routerHTTPConfig.TLS.Options == defaultTLSConfigName { if len(routerHTTPConfig.TLS.Options) == 0 || routerHTTPConfig.TLS.Options == defaultTLSConfigName {
continue continue
@ -148,10 +148,33 @@ func (m *Manager) buildEntryPointHandler(ctx context.Context, configs map[string
routerName: routerHTTPName, routerName: routerHTTPName,
TLSConfig: tlsConf, 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) logger := log.FromContext(ctx)
for hostSNI, tlsConfigs := range tlsOptionsForHostSNI { for hostSNI, tlsConfigs := range tlsOptionsForHostSNI {
if len(tlsConfigs) == 1 { if len(tlsConfigs) == 1 {
@ -248,3 +271,17 @@ func (m *Manager) buildEntryPointHandler(ctx context.Context, configs map[string
return router, nil 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"
}