Fix domain fronting
Co-authored-by: Ludovic Fernandez <ldez@users.noreply.github.com>
This commit is contained in:
parent
45f52ca29c
commit
0b7aaa3643
6 changed files with 197 additions and 17 deletions
|
@ -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)"
|
||||
|
|
|
@ -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 {
|
||||
|
|
53
integration/fixtures/https/https_domain_fronting.toml
Normal file
53
integration/fixtures/https/https_domain_fronting.toml
Normal 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"
|
|
@ -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))
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,9 +148,32 @@ 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 {
|
||||
|
@ -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"
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue