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,
|
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)"
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
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)
|
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))
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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"
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue