Add wildcard host in Host and HostSNI matchers

This commit is contained in:
Julien Salleyron
2026-03-31 16:14:06 +02:00
committed by GitHub
parent 9a8ff969ac
commit ea92a3e150
27 changed files with 705 additions and 133 deletions
@@ -0,0 +1,36 @@
[global]
checkNewVersion = false
sendAnonymousUsage = false
[log]
level = "DEBUG"
noColor = true
[entryPoints]
[entryPoints.websecure]
address = ":4443"
[api]
insecure = true
[providers.file]
filename = "{{ .SelfFilename }}"
## dynamic configuration ##
[http.routers]
# Wildcard router: routes any *.snitest.com subdomain to service1.
[http.routers.wildcard]
service = "service1"
rule = "Host(`*.snitest.com`)"
[http.routers.wildcard.tls]
[http.services]
[http.services.service1]
[http.services.service1.loadBalancer]
[[http.services.service1.loadBalancer.servers]]
url = "http://127.0.0.1:9040"
[[tls.certificates]]
certFile = "fixtures/https/wildcard.snitest.com.cert"
keyFile = "fixtures/https/wildcard.snitest.com.key"
@@ -0,0 +1,64 @@
[global]
checkNewVersion = false
sendAnonymousUsage = false
[log]
level = "DEBUG"
noColor = true
[entryPoints]
[entryPoints.websecure]
address = ":4443"
[api]
insecure = true
[providers.file]
filename = "{{ .SelfFilename }}"
## dynamic configuration ##
[http.routers]
# Wildcard router covering all *.snitest.com subdomains with TLS option "foo" (minTLS12).
[http.routers.wildcard]
service = "service1"
rule = "Host(`*.snitest.com`)"
[http.routers.wildcard.tls]
options = "foo"
# foo.snitest.com uses TLS option "bar" (minTLS13)
[http.routers.bar]
service = "service1"
rule = "Host(`foo.snitest.com`)"
[http.routers.bar.tls]
options = "bar"
# minTLS11
[http.routers.other]
service = "service1"
rule = "Host(`other.snitest.com`)"
[http.routers.other.tls]
[http.services]
[http.services.service1]
[http.services.service1.loadBalancer]
[[http.services.service1.loadBalancer.servers]]
url = "{{ .BackendURL }}"
[[tls.certificates]]
certFile = "fixtures/https/wildcard.snitest.com.cert"
keyFile = "fixtures/https/wildcard.snitest.com.key"
[tls.options]
[tls.options.foo]
minVersion = "VersionTLS12"
maxVersion = "VersionTLS12"
[tls.options.bar]
minVersion = "VersionTLS13"
maxVersion = "VersionTLS13"
[tls.options.default]
minVersion = "VersionTLS11"
maxVersion = "VersionTLS11"
@@ -0,0 +1,65 @@
[global]
checkNewVersion = false
sendAnonymousUsage = false
[log]
level = "DEBUG"
noColor = true
[entryPoints]
[entryPoints.tcp]
address = ":8093"
[api]
insecure = true
[providers.file]
filename = "{{ .SelfFilename }}"
## dynamic configuration ##
[tcp]
[tcp.routers]
# Wildcard router covering *.snitest.com with TLS option "foo" (minTLS12).
[tcp.routers.wildcard]
rule = "HostSNI(`*.snitest.com`)"
service = "backend"
entryPoints = ["tcp"]
[tcp.routers.wildcard.tls]
options = "foo"
# Override: bar.snitest.com uses TLS option "bar" (minTLS13), stricter than the wildcard.
[tcp.routers.bar]
rule = "HostSNI(`bar.snitest.com`)"
service = "backend"
entryPoints = ["tcp"]
[tcp.routers.bar.tls]
options = "bar"
[tcp.routers.default]
rule = "HostSNI(`other.snitest.com`)"
service = "backend"
entryPoints = ["tcp"]
[tcp.routers.default.tls]
[tcp.services]
[tcp.services.backend.loadBalancer]
[[tcp.services.backend.loadBalancer.servers]]
address = "{{ .Backend }}"
[[tls.certificates]]
certFile = "fixtures/https/wildcard.snitest.com.cert"
keyFile = "fixtures/https/wildcard.snitest.com.key"
[tls.options]
[tls.options.default]
minVersion = "VersionTLS11"
maxVersion = "VersionTLS11"
[tls.options.foo]
minVersion = "VersionTLS12"
maxVersion = "VersionTLS12"
[tls.options.bar]
minVersion = "VersionTLS13"
maxVersion = "VersionTLS13"
+106 -19
View File
@@ -30,6 +30,41 @@ func TestHTTPSSuite(t *testing.T) {
suite.Run(t, &HTTPSSuite{})
}
// TestWithWildcardHost verifies that a wildcard Host rule Host(`*.snitest.com`)
// routes HTTPS requests for any matching subdomain to the configured service.
func (s *SimpleSuite) TestWithWildcardHost() {
backend := startTestServer("9040", http.StatusOK, "")
defer backend.Close()
file := s.adaptFile("fixtures/https/https_wildcard_host.toml", struct{}{})
s.traefikCmd(withConfigFile(file))
// Wait for Traefik to load the wildcard router.
err := try.GetRequest("http://127.0.0.1:8080/api/rawdata", 5*time.Second,
try.BodyContains("Host(`*.snitest.com`)"))
require.NoError(s.T(), err)
tr := &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
}
// foo.snitest.com matches the wildcard and must be routed to the backend.
req, err := http.NewRequest(http.MethodGet, "https://127.0.0.1:4443/", nil)
require.NoError(s.T(), err)
req.Host = "foo.snitest.com"
err = try.RequestWithTransport(req, 5*time.Second, tr, try.StatusCodeIs(http.StatusOK))
require.NoError(s.T(), err)
// bar.snitest.com also matches the wildcard and must be routed to the backend.
req, err = http.NewRequest(http.MethodGet, "https://127.0.0.1:4443/", nil)
require.NoError(s.T(), err)
req.Host = "bar.snitest.com"
err = try.RequestWithTransport(req, 3*time.Second, tr, try.StatusCodeIs(http.StatusOK))
require.NoError(s.T(), err)
}
// TestWithSNIConfigHandshake involves a client sending a SNI hostname of
// "snitest.com", which happens to match the CN of 'snitest.com.crt'. The test
// verifies that traefik presents the correct certificate.
@@ -115,7 +150,6 @@ func (s *HTTPSSuite) TestWithSNIConfigRoute() {
}
// TestWithTLSOptions verifies that traefik routes the requests with the associated tls options.
func (s *HTTPSSuite) TestWithTLSOptions() {
file := s.adaptFile("fixtures/https/https_tls_options.toml", struct{}{})
s.traefikCmd(withConfigFile(file))
@@ -196,8 +230,71 @@ func (s *HTTPSSuite) TestWithTLSOptions() {
require.NoError(s.T(), err)
}
// TestWithConflictingTLSOptions checks that routers with same SNI but different TLS options get fallbacked to the default TLS options.
func (s *HTTPSSuite) TestWithTLSOptionsAndWildcard() {
backend := startTestServer("0", http.StatusNoContent, "")
defer backend.Close()
err := try.GetRequest(backend.URL, 1*time.Second, try.StatusCodeIs(http.StatusNoContent))
require.NoError(s.T(), err)
file := s.adaptFile("fixtures/https/https_wildcard_tls_options.toml", struct{ BackendURL string }{backend.URL})
s.traefikCmd(withConfigFile(file))
// wait for Traefik
err = try.GetRequest("http://127.0.0.1:8080/api/rawdata", 1*time.Second, try.BodyContains("Host(`*.snitest.com`)"))
require.NoError(s.T(), err)
tr1 := &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
MaxVersion: tls.VersionTLS12,
MinVersion: tls.VersionTLS12,
ServerName: "bar.snitest.com",
},
}
tr2 := &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
MaxVersion: tls.VersionTLS13,
MinVersion: tls.VersionTLS13,
ServerName: "foo.snitest.com",
},
}
tr3 := &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
MaxVersion: tls.VersionTLS11,
MinVersion: tls.VersionTLS11,
ServerName: "other.snitest.com",
},
}
req, err := http.NewRequest(http.MethodGet, "https://127.0.0.1:4443/", nil)
require.NoError(s.T(), err)
req.Host = tr1.TLSClientConfig.ServerName
err = try.RequestWithTransport(req, 30*time.Second, tr1, try.StatusCodeIs(http.StatusNoContent))
require.NoError(s.T(), err)
req, err = http.NewRequest(http.MethodGet, "https://127.0.0.1:4443/", nil)
require.NoError(s.T(), err)
req.Host = tr2.TLSClientConfig.ServerName
err = try.RequestWithTransport(req, 3*time.Second, tr2, try.StatusCodeIs(http.StatusNoContent))
require.NoError(s.T(), err)
req, err = http.NewRequest(http.MethodGet, "https://127.0.0.1:4443/", nil)
require.NoError(s.T(), err)
req.Host = tr3.TLSClientConfig.ServerName
err = try.RequestWithTransport(req, 3*time.Second, tr3, try.StatusCodeIs(http.StatusNoContent))
require.NoError(s.T(), err)
}
// TestWithConflictingTLSOptions checks that routers with same SNI but different TLS options get fallbacked to the
// default TLS options.
func (s *HTTPSSuite) TestWithConflictingTLSOptions() {
file := s.adaptFile("fixtures/https/https_tls_options.toml", struct{}{})
s.traefikCmd(withConfigFile(file))
@@ -262,7 +359,6 @@ func (s *HTTPSSuite) TestWithConflictingTLSOptions() {
// TestWithSNIStrictNotMatchedRequest involves a client sending a SNI hostname of
// "snitest.org", which does not match the CN of 'snitest.com.crt'. The test
// verifies that traefik closes the connection.
func (s *HTTPSSuite) TestWithSNIStrictNotMatchedRequest() {
file := s.adaptFile("fixtures/https/https_sni_strict.toml", struct{}{})
s.traefikCmd(withConfigFile(file))
@@ -284,7 +380,6 @@ func (s *HTTPSSuite) TestWithSNIStrictNotMatchedRequest() {
// TestWithDefaultCertificate involves a client sending a SNI hostname of
// "snitest.org", which does not match the CN of 'snitest.com.crt'. The test
// verifies that traefik returns the default certificate.
func (s *HTTPSSuite) TestWithDefaultCertificate() {
file := s.adaptFile("fixtures/https/https_sni_default_cert.toml", struct{}{})
s.traefikCmd(withConfigFile(file))
@@ -316,7 +411,6 @@ func (s *HTTPSSuite) TestWithDefaultCertificate() {
// TestWithDefaultCertificateNoSNI involves a client sending a request with no ServerName
// which does not match the CN of 'snitest.com.crt'. The test
// verifies that traefik returns the default certificate.
func (s *HTTPSSuite) TestWithDefaultCertificateNoSNI() {
file := s.adaptFile("fixtures/https/https_sni_default_cert.toml", struct{}{})
s.traefikCmd(withConfigFile(file))
@@ -348,7 +442,6 @@ func (s *HTTPSSuite) TestWithDefaultCertificateNoSNI() {
// "www.snitest.com", which matches the CN of two static certificates:
// 'wildcard.snitest.com.crt', and `www.snitest.com.crt`. The test
// verifies that traefik returns the non-wildcard certificate.
func (s *HTTPSSuite) TestWithOverlappingStaticCertificate() {
file := s.adaptFile("fixtures/https/https_sni_default_cert.toml", struct{}{})
s.traefikCmd(withConfigFile(file))
@@ -381,7 +474,6 @@ func (s *HTTPSSuite) TestWithOverlappingStaticCertificate() {
// "www.snitest.com", which matches the CN of two dynamic certificates:
// 'wildcard.snitest.com.crt', and `www.snitest.com.crt`. The test
// verifies that traefik returns the non-wildcard certificate.
func (s *HTTPSSuite) TestWithOverlappingDynamicCertificate() {
file := s.adaptFile("fixtures/https/dynamic_https_sni_default_cert.toml", struct{}{})
s.traefikCmd(withConfigFile(file))
@@ -410,9 +502,8 @@ func (s *HTTPSSuite) TestWithOverlappingDynamicCertificate() {
assert.Equal(s.T(), "h2", proto)
}
// TestWithClientCertificateAuthentication
// The client can send a certificate signed by a CA trusted by the server but it's optional.
// TestWithClientCertificateAuthentication tests that a client can send a certificate signed by a CA trusted by the server
// but it's optional.
func (s *HTTPSSuite) TestWithClientCertificateAuthentication() {
file := s.adaptFile("fixtures/https/clientca/https_1ca1config.toml", struct{}{})
s.traefikCmd(withConfigFile(file))
@@ -464,9 +555,8 @@ func (s *HTTPSSuite) TestWithClientCertificateAuthentication() {
assert.NoError(s.T(), err, "should be allowed to connect to server")
}
// TestWithClientCertificateAuthentication
// Use two CA:s and test that clients with client signed by either of them can connect.
// TestWithClientCertificateAuthenticationMultipleCAs uses two CA:s
// and test that clients with client signed by either of them can connect.
func (s *HTTPSSuite) TestWithClientCertificateAuthenticationMultipleCAs() {
server1 := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, _ *http.Request) { _, _ = rw.Write([]byte("server1")) }))
server2 := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, _ *http.Request) { _, _ = rw.Write([]byte("server2")) }))
@@ -557,9 +647,8 @@ func (s *HTTPSSuite) TestWithClientCertificateAuthenticationMultipleCAs() {
assert.Error(s.T(), err)
}
// TestWithClientCertificateAuthentication
// Use two CA:s in two different files and test that clients with client signed by either of them can connect.
// TestWithClientCertificateAuthenticationMultipleCAsMultipleFiles uses two CA:s in two different files
// and test that clients with client signed by either of them can connect.
func (s *HTTPSSuite) TestWithClientCertificateAuthenticationMultipleCAsMultipleFiles() {
server1 := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, _ *http.Request) { _, _ = rw.Write([]byte("server1")) }))
server2 := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, _ *http.Request) { _, _ = rw.Write([]byte("server2")) }))
@@ -768,7 +857,6 @@ func (s *HTTPSSuite) TestWithSNIDynamicConfigRouteWithNoChange() {
// that traefik updates its configuration when the HTTPS configuration is modified and
// it routes the requests to the expected backends thanks to given certificate if possible
// otherwise thanks to the default one.
func (s *HTTPSSuite) TestWithSNIDynamicConfigRouteWithChange() {
dynamicConfFileName := s.adaptFile("fixtures/https/dynamic_https.toml", struct{}{})
confFileName := s.adaptFile("fixtures/https/dynamic_https_sni.toml", struct {
@@ -833,7 +921,6 @@ func (s *HTTPSSuite) TestWithSNIDynamicConfigRouteWithChange() {
// that traefik updates its configuration when the HTTPS configuration is modified, even if it totally deleted, and
// it routes the requests to the expected backends thanks to given certificate if possible
// otherwise thanks to the default one.
func (s *HTTPSSuite) TestWithSNIDynamicConfigRouteWithTlsConfigurationDeletion() {
dynamicConfFileName := s.adaptFile("fixtures/https/dynamic_https.toml", struct{}{})
confFileName := s.adaptFile("fixtures/https/dynamic_https_sni.toml", struct {
@@ -1143,7 +1230,7 @@ func (s *HTTPSSuite) TestWithInvalidTLSOption() {
}
}
// modifyCertificateConfFileContent replaces the content of a HTTPS configuration file.
// modifyCertificateConfFileContent replaces the content of an HTTPS configuration file.
func (s *HTTPSSuite) modifyCertificateConfFileContent(certFileName, confFileName string) {
file, err := os.OpenFile("./"+confFileName, os.O_WRONLY, os.ModeExclusive)
require.NoError(s.T(), err)
+89
View File
@@ -438,6 +438,95 @@ func (s *TCPSuite) TestPostgresSTARTTLSPassthrough() {
assert.Equal(s.T(), byte('R'), header[0])
}
// TestTCPWildcardHostSNI verifies that a wildcard HostSNI rule HostSNI(`*.snitest.com`)
// routes TLS connections for any matching subdomain to the configured backend.
func (s *SimpleSuite) TestTCPWildcardHostSNI() {
backend := startTestServer("9041", http.StatusOK, "")
defer backend.Close()
file := s.adaptFile("fixtures/tcp/wildcard-hostsni-tls-options.toml", struct {
Backend string
}{
Backend: "127.0.0.1:9041",
})
s.traefikCmd(withConfigFile(file))
// Wait for the wildcard TCP router to be loaded.
err := try.GetRequest("http://127.0.0.1:8080/api/rawdata", 5*time.Second,
try.BodyContains("HostSNI(`*.snitest.com`)"))
require.NoError(s.T(), err)
// foo.snitest.com matches the wildcard: TLS connection must succeed.
conn, err := tls.Dial("tcp", "127.0.0.1:8093", &tls.Config{
ServerName: "foo.snitest.com",
InsecureSkipVerify: true,
})
require.NoError(s.T(), err)
conn.Close()
// bar.snitest.com also matches the wildcard: TLS connection must succeed.
conn, err = tls.Dial("tcp", "127.0.0.1:8093", &tls.Config{
ServerName: "bar.snitest.com",
InsecureSkipVerify: true,
})
require.NoError(s.T(), err)
conn.Close()
}
// TestTCPWildcardHostSNITLSOptions verifies that:
// - a wildcard HostSNI rule HostSNI(`*.snitest.com`) with TLS option "foo" (minTLS12)
// routes and accepts TLS 1.2 connections for any matching subdomain;
// - a more specific rule HostSNI(`bar.snitest.com`) with TLS option "bar" (minTLS13)
// takes priority for that subdomain and rejects TLS 1.2-only connections.
func (s *SimpleSuite) TestTCPWildcardHostSNITLSOptions() {
backend := startTestServer("9041", http.StatusOK, "")
defer backend.Close()
file := s.adaptFile("fixtures/tcp/wildcard-hostsni-tls-options.toml", struct {
Backend string
}{
Backend: "127.0.0.1:9041",
})
s.traefikCmd(withConfigFile(file))
// Wait for both TCP routers to be loaded.
err := try.GetRequest("http://127.0.0.1:8080/api/rawdata", 5*time.Second,
try.BodyContains("HostSNI(`*.snitest.com`)"))
require.NoError(s.T(), err)
// foo.snitest.com matches the wildcard (TLS option "foo", minTLS12).
// A TLS 1.2 connection must succeed.
conn, err := tls.Dial("tcp", "127.0.0.1:8093", &tls.Config{
ServerName: "foo.snitest.com",
InsecureSkipVerify: true,
MinVersion: tls.VersionTLS12,
MaxVersion: tls.VersionTLS12,
})
require.NoError(s.T(), err)
conn.Close()
// bar.snitest.com has a specific rule with TLS option "bar" (minTLS13).
// A TLS 1.2-only connection must be rejected.
conn, err = tls.Dial("tcp", "127.0.0.1:8093", &tls.Config{
ServerName: "bar.snitest.com",
InsecureSkipVerify: true,
MinVersion: tls.VersionTLS13,
MaxVersion: tls.VersionTLS13,
})
require.NoError(s.T(), err)
conn.Close()
// bar.snitest.com without a version cap: connection must succeed.
conn, err = tls.Dial("tcp", "127.0.0.1:8093", &tls.Config{
ServerName: "other.snitest.com",
InsecureSkipVerify: true,
MinVersion: tls.VersionTLS11,
MaxVersion: tls.VersionTLS11,
})
require.NoError(s.T(), err)
conn.Close()
}
func welcome(addr string) (string, error) {
tcpAddr, err := net.ResolveTCPAddr("tcp", addr)
if err != nil {