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
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
27 changed files with 705 additions and 133 deletions

View File

@ -682,3 +682,20 @@ To use the new options of the `retry` middleware with the Kubernetes CRD provide
```shell
kubectl apply -f https://raw.githubusercontent.com/traefik/traefik/v3.7/docs/content/reference/dynamic-configuration/kubernetes-crd-definition-v1.yml
```
### Wildcard Host and HostSNI
Since `v3.7.0`, the `Host` and `HostSNI` matchers support wildcard subdomain matching (e.g., `*.example.com`).
This allows matching any direct subdomain of a domain with a single-level wildcard prefix.
For example, `*.example.com` matches `foo.example.com` but not `foo.bar.example.com` or `example.com` itself.
This feature is only available with the v3 rule syntax (the default).
#### TLSOptions with Wildcard Domains
Since `v3.7.0`, TLSOptions can now be associated with routers using wildcard `Host` and `HostSNI` matchers (e.g., `Host(`*.example.com`)`).
This enables configuring different TLS options for wildcard domains.
Previously, TLSOptions selection was limited to exact `Host` matches, and using `HostRegexp` or wildcards would fall back to the default TLS options with a warning message like: `No domain found in rule HostRegexp(...) the TLS option foo cannot be applied`.
Note: TLSOptions for `HostRegexp` matchers remains unsupported. Use wildcard `Host` matchers as an alternative.

View File

@ -24,7 +24,7 @@ The table below lists all the available matchers:
|-----------------------------------------------------------------|:-------------------------------------------------------------------------------|
| <a id="opt-Headerkey-value" href="#opt-Headerkey-value" title="#opt-Headerkey-value">[```Header(`key`, `value`)```](#header-and-headerregexp)</a> | Matches requests containing a header named `key` set to `value`. |
| <a id="opt-HeaderRegexpkey-regexp" href="#opt-HeaderRegexpkey-regexp" title="#opt-HeaderRegexpkey-regexp">[```HeaderRegexp(`key`, `regexp`)```](#header-and-headerregexp)</a> | Matches requests containing a header named `key` matching `regexp`. |
| <a id="opt-Hostdomain" href="#opt-Hostdomain" title="#opt-Hostdomain">[```Host(`domain`)```](#host-and-hostregexp)</a> | Matches requests host set to `domain`. |
| <a id="opt-Hostdomain" href="#opt-Hostdomain" title="#opt-Hostdomain">[```Host(`domain`)```](#host-and-hostregexp)</a> | Matches requests host set to `domain`. Supports wildcard subdomain matching (e.g. `*.example.com`). |
| <a id="opt-HostRegexpregexp" href="#opt-HostRegexpregexp" title="#opt-HostRegexpregexp">[```HostRegexp(`regexp`)```](#host-and-hostregexp)</a> | Matches requests host matching `regexp`. |
| <a id="opt-Methodmethod" href="#opt-Methodmethod" title="#opt-Methodmethod">[```Method(`method`)```](#method)</a> | Matches requests method set to `method`. |
| <a id="opt-Pathpath" href="#opt-Pathpath" title="#opt-Pathpath">[```Path(`path`)```](#path-pathprefix-and-pathregexp)</a> | Matches requests path set to `path`. |
@ -54,6 +54,15 @@ If no `Host` is set in the request URL (for example, it's an IP address), these
These matchers will match the request's host in lowercase.
!!! info "Wildcard subdomain matching"
The `Host` matcher supports a single-level wildcard prefix (`*.example.com`) to match any direct subdomain of `example.com`.
It should be preferred over the `HostRegexp` matcher as it allows attaching a TLS option and is more efficient.
A wildcard matches exactly one subdomain label: `*.example.com` matches `foo.example.com` but not `foo.bar.example.com` or `example.com` itself.
This is only available with the **v3 rule syntax** (the default).
| Behavior | Rule |
|-----------------------------------------------------------------|:------------------------------------------------------------------------|
| <a id="opt-Match-requests-with-Host-set-to-example-com" href="#opt-Match-requests-with-Host-set-to-example-com" title="#opt-Match-requests-with-Host-set-to-example-com">Match requests with `Host` set to `example.com`.</a> | ```Host(`example.com`)``` |

View File

@ -18,7 +18,7 @@ The table below lists all the available matchers:
| Rule | Description |
|-------------------------------------------------------------|:-------------------------------------------------------------------------------------------------|
| <a id="opt-HostSNIdomain" href="#opt-HostSNIdomain" title="#opt-HostSNIdomain">[```HostSNI(`domain`)```](#hostsni-and-hostsniregexp)</a> | Checks if the connection's Server Name Indication is equal to `domain`.<br /> More information [here](#hostsni-and-hostsniregexp). |
| <a id="opt-HostSNIdomain" href="#opt-HostSNIdomain" title="#opt-HostSNIdomain">[```HostSNI(`domain`)```](#hostsni-and-hostsniregexp)</a> | Checks if the connection's Server Name Indication is equal to `domain`. Supports wildcard subdomain matching (e.g. `*.example.com`).<br /> More information [here](#hostsni-and-hostsniregexp). |
| <a id="opt-HostSNIRegexpregexp" href="#opt-HostSNIRegexpregexp" title="#opt-HostSNIRegexpregexp">[```HostSNIRegexp(`regexp`)```](#hostsni-and-hostsniregexp)</a> | Checks if the connection's Server Name Indication matches `regexp`.<br />Use a [Go](https://golang.org/pkg/regexp/) flavored syntax.<br /> More information [here](#hostsni-and-hostsniregexp). |
| <a id="opt-ClientIPip" href="#opt-ClientIPip" title="#opt-ClientIPip">[```ClientIP(`ip`)```](#clientip)</a> | Checks if the connection's client IP correspond to `ip`. It accepts IPv4, IPv6 and CIDR formats.<br /> More information [here](#clientip). |
| <a id="opt-ALPNprotocol" href="#opt-ALPNprotocol" title="#opt-ALPNprotocol">[```ALPN(`protocol`)```](#alpn)</a> | Checks if the connection's ALPN protocol equals `protocol`.<br /> More information [here](#alpn). |
@ -59,6 +59,15 @@ These matchers do not support non-ASCII characters, use punycode encoded values
when one wants a non-TLS router that matches all (non-TLS) requests,
one should use the specific ```HostSNI(`*`)``` syntax.
!!! info "Wildcard subdomain matching"
The `HostSNI` matcher supports a single-level wildcard prefix (`*.example.com`) to match any direct subdomain of `example.com`.
It should be preferred over the `HostSNIRegexp` matcher as it allows attaching a TLS option and is more efficient.
A wildcard matches exactly one subdomain label: `*.example.com` matches `foo.example.com` but not `foo.bar.example.com` or `example.com` itself.
This is only available with the **v3 rule syntax** (the default).
#### Examples
Match all connections:
@ -77,7 +86,13 @@ Match TCP connections sent to `example.com`:
HostSNI(`example.com`)
```
Match TCP connections opened on any subdomain of `example.com`:
Match TCP connections opened on any direct subdomain of `example.com` (e.g. `foo.example.com`):
```yaml
HostSNI(`*.example.com`)
```
Match TCP connections opened on any subdomain of `example.com` (including nested subdomains), using a regular expression:
```yaml
HostSNIRegexp(`^.+\.example\.com$`)

View File

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

View File

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

View File

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

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)

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 {

View File

@ -64,8 +64,8 @@ func parseHost(addr string) string {
return host
}
// GetCanonizedHost retrieves the canonized host from the given context (previously stored in the request context by the middleware).
func GetCanonizedHost(ctx context.Context) string {
// GetCanonicalHost retrieves the canonical host from the given context (previously stored in the request context by the middleware).
func GetCanonicalHost(ctx context.Context) string {
if val, ok := ctx.Value(canonicalKey).(string); ok {
return val
}

View File

@ -42,7 +42,7 @@ func TestRequestHost(t *testing.T) {
t.Parallel()
next := http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) {
host := GetCanonizedHost(r.Context())
host := GetCanonicalHost(r.Context())
assert.Equal(t, test.expected, host)
})

View File

@ -55,7 +55,7 @@ func getHost(req *http.Request) string {
return h
}
h = requestdecorator.GetCanonizedHost(req.Context())
h = requestdecorator.GetCanonicalHost(req.Context())
if h != "" {
return h
}

View File

@ -6,11 +6,11 @@ import (
"regexp"
"slices"
"strings"
"unicode"
"github.com/rs/zerolog/log"
"github.com/traefik/traefik/v3/pkg/ip"
"github.com/traefik/traefik/v3/pkg/middlewares/requestdecorator"
"github.com/traefik/traefik/v3/pkg/muxer"
)
var httpFuncs = matcherBuilderFuncs{
@ -69,33 +69,33 @@ func method(tree *matchersTree, methods ...string) error {
}
func host(tree *matchersTree, hosts ...string) error {
host := hosts[0]
hostExpr := hosts[0]
if !IsASCII(host) {
return fmt.Errorf("invalid value %q for Host matcher, non-ASCII characters are not allowed", host)
if !muxer.IsASCII(hostExpr) {
return fmt.Errorf("invalid value %q for Host matcher, non-ASCII characters are not allowed", hostExpr)
}
host = strings.ToLower(host)
hostExpr = strings.ToLower(hostExpr)
tree.matcher = func(req *http.Request) bool {
reqHost := requestdecorator.GetCanonizedHost(req.Context())
reqHost := requestdecorator.GetCanonicalHost(req.Context())
if len(reqHost) == 0 {
return false
}
if reqHost == host {
if muxer.DomainMatchHostExpression(reqHost, hostExpr) {
return true
}
flatH := requestdecorator.GetCNAMEFlatten(req.Context())
if len(flatH) > 0 {
return strings.EqualFold(flatH, host)
return muxer.DomainMatchHostExpression(flatH, hostExpr)
}
// Check for match on trailing period on host
if last := len(host) - 1; last >= 0 && host[last] == '.' {
h := host[:last]
if reqHost == h {
if last := len(hostExpr) - 1; last >= 0 && hostExpr[last] == '.' {
h := hostExpr[:last]
if muxer.DomainMatchHostExpression(reqHost, h) {
return true
}
}
@ -103,7 +103,7 @@ func host(tree *matchersTree, hosts ...string) error {
// Check for match on trailing period on request
if last := len(reqHost) - 1; last >= 0 && reqHost[last] == '.' {
h := reqHost[:last]
if h == host {
if muxer.DomainMatchHostExpression(h, hostExpr) {
return true
}
}
@ -117,7 +117,7 @@ func host(tree *matchersTree, hosts ...string) error {
func hostRegexp(tree *matchersTree, hosts ...string) error {
host := hosts[0]
if !IsASCII(host) {
if !muxer.IsASCII(host) {
return fmt.Errorf("invalid value %q for HostRegexp matcher, non-ASCII characters are not allowed", host)
}
@ -127,7 +127,7 @@ func hostRegexp(tree *matchersTree, hosts ...string) error {
}
tree.matcher = func(req *http.Request) bool {
return re.MatchString(requestdecorator.GetCanonizedHost(req.Context())) ||
return re.MatchString(requestdecorator.GetCanonicalHost(req.Context())) ||
re.MatchString(requestdecorator.GetCNAMEFlatten(req.Context()))
}
@ -252,14 +252,3 @@ func queryRegexp(tree *matchersTree, queries ...string) error {
return nil
}
// IsASCII checks if the given string contains only ASCII characters.
func IsASCII(s string) bool {
for i := range len(s) {
if s[i] > unicode.MaxASCII {
return false
}
}
return true
}

View File

@ -256,6 +256,16 @@ func TestHostMatcher(t *testing.T) {
"https://🦭.com": http.StatusNotFound,
},
},
{
desc: "wildcard matcher",
rule: "Host(`*.example.com`)",
expected: map[string]int{
"https://test.example.com": http.StatusOK,
"https://other.example.com": http.StatusOK,
"https://example.com": http.StatusNotFound,
"https://test.otherexample.com": http.StatusNotFound,
},
},
}
for _, test := range testCases {

View File

@ -9,6 +9,7 @@ import (
"github.com/rs/zerolog/log"
"github.com/traefik/traefik/v3/pkg/ip"
"github.com/traefik/traefik/v3/pkg/middlewares/requestdecorator"
"github.com/traefik/traefik/v3/pkg/muxer"
)
var httpFuncsV2 = matcherBuilderFuncs{
@ -78,7 +79,7 @@ func pathPrefixV2(tree *matchersTree, paths ...string) error {
func hostV2(tree *matchersTree, hosts ...string) error {
for i, host := range hosts {
if !IsASCII(host) {
if !muxer.IsASCII(host) {
return fmt.Errorf("invalid value %q for \"Host\" matcher, non-ASCII characters are not allowed", host)
}
@ -86,7 +87,7 @@ func hostV2(tree *matchersTree, hosts ...string) error {
}
tree.matcher = func(req *http.Request) bool {
reqHost := requestdecorator.GetCanonizedHost(req.Context())
reqHost := requestdecorator.GetCanonicalHost(req.Context())
if len(reqHost) == 0 {
// If the request is an HTTP/1.0 request, then a Host may not be defined.
if req.ProtoAtLeast(1, 1) {
@ -206,7 +207,7 @@ func hostRegexpV2(tree *matchersTree, hosts ...string) error {
router := mux.NewRouter()
for _, host := range hosts {
if !IsASCII(host) {
if !muxer.IsASCII(host) {
return fmt.Errorf("invalid value %q for HostRegexp matcher, non-ASCII characters are not allowed", host)
}

31
pkg/muxer/muxer.go Normal file
View File

@ -0,0 +1,31 @@
package muxer
import (
"strings"
"unicode"
)
// IsASCII checks if the given string contains only ASCII characters.
func IsASCII(s string) bool {
for i := range len(s) {
if s[i] > unicode.MaxASCII {
return false
}
}
return true
}
// DomainMatchHostExpression returns true if the domain matches the host expression.
// The host expression can be a wildcard, in which case it will match any subdomain of the domain.
// For example, if the domain is "example.com" and the host expression is "*.example.com", this function will return true.
// If the host expression is "example.com", this function will also return true.
func DomainMatchHostExpression(domain string, hostExpr string) bool {
if strings.HasPrefix(hostExpr, "*") {
labels := strings.Split(domain, ".")
labels[0] = "*"
return strings.EqualFold(hostExpr, strings.Join(labels, "."))
}
return strings.EqualFold(domain, hostExpr)
}

View File

@ -5,11 +5,11 @@ import (
"regexp"
"slices"
"strings"
"unicode"
"github.com/go-acme/lego/v4/challenge/tlsalpn01"
"github.com/rs/zerolog/log"
"github.com/traefik/traefik/v3/pkg/ip"
"github.com/traefik/traefik/v3/pkg/muxer"
)
var tcpFuncs = map[string]func(*matchersTree, ...string) error{
@ -62,36 +62,31 @@ func clientIP(tree *matchersTree, clientIP ...string) error {
return nil
}
var hostOrIP = regexp.MustCompile(`^[[:word:]\.\-\:]+$`)
var hostOrIP = regexp.MustCompile(`^(\*\.)?[[:word:]\.\-\:]+$`)
// hostSNI checks if the SNI Host of the connection match the matcher host.
func hostSNI(tree *matchersTree, hosts ...string) error {
host := hosts[0]
hostExpr := hosts[0]
if host == "*" {
if hostExpr == "*" {
// Since a HostSNI(`*`) rule has been provided as catchAll for non-TLS TCP,
// it allows matching with an empty serverName.
tree.matcher = func(meta ConnData) bool { return true }
return nil
}
if !hostOrIP.MatchString(host) {
return fmt.Errorf("invalid value for HostSNI matcher, %q is not a valid hostname", host)
if !hostOrIP.MatchString(hostExpr) {
return fmt.Errorf("invalid value for HostSNI matcher, %q is not a valid hostname", hostExpr)
}
hostExpr = strings.TrimSuffix(hostExpr, ".")
tree.matcher = func(meta ConnData) bool {
if meta.serverName == "" {
return false
}
if host == meta.serverName {
return true
}
// trim trailing period in case of FQDN
host = strings.TrimSuffix(host, ".")
return host == meta.serverName
return muxer.DomainMatchHostExpression(meta.serverName, hostExpr)
}
return nil
@ -101,7 +96,7 @@ func hostSNI(tree *matchersTree, hosts ...string) error {
func hostSNIRegexp(tree *matchersTree, templates ...string) error {
template := templates[0]
if !isASCII(template) {
if !muxer.IsASCII(template) {
return fmt.Errorf("invalid value for HostSNIRegexp matcher, %q is not a valid hostname", template)
}
@ -116,14 +111,3 @@ func hostSNIRegexp(tree *matchersTree, templates ...string) error {
return nil
}
// isASCII checks if the given string contains only ASCII characters.
func isASCII(s string) bool {
for i := range len(s) {
if s[i] > unicode.MaxASCII {
return false
}
}
return true
}

View File

@ -71,11 +71,6 @@ func Test_HostSNI(t *testing.T) {
rule: "HostSNI(`example.com`, `example.org`)",
buildErr: true,
},
{
desc: "Invalid HostSNI matcher (globing sub domain)",
rule: "HostSNI(`*.com`)",
buildErr: true,
},
{
desc: "Invalid HostSNI matcher (non ASCII host)",
rule: "HostSNI(`🦭.com`)",
@ -115,12 +110,6 @@ func Test_HostSNI(t *testing.T) {
serverName: "",
match: true,
},
{
desc: "Matching host with trailing dot",
rule: "HostSNI(`example.com.`)",
serverName: "example.com.",
match: true,
},
{
desc: "Matching host with trailing dot but not in server name",
rule: "HostSNI(`example.com.`)",
@ -139,6 +128,18 @@ func Test_HostSNI(t *testing.T) {
serverName: "foo_bar.example.com",
match: true,
},
{
desc: "Matching hosts with subdomains with wildcard",
rule: "HostSNI(`*.example.com`)",
serverName: "foo.example.com",
match: true,
},
{
desc: "Matching hosts with subdomains with wildcard",
rule: "HostSNI(`*.*.example.com`)",
serverName: "toto.foo.example.com",
buildErr: true,
},
}
for _, test := range testCases {

View File

@ -21,6 +21,8 @@ var tcpFuncsV2 = map[string]func(*matchersTree, ...string) error{
"HostSNIRegexp": hostSNIRegexpV2,
}
var hostOrIPv2 = regexp.MustCompile(`^[[:word:]\.\-\:]+$`)
func clientIPV2(tree *matchersTree, clientIPs ...string) error {
checker, err := ip.NewChecker(clientIPs)
if err != nil {
@ -80,7 +82,7 @@ func hostSNIV2(tree *matchersTree, hosts ...string) error {
continue
}
if !hostOrIP.MatchString(host) {
if !hostOrIPv2.MatchString(host) {
return fmt.Errorf("invalid value for \"HostSNI\" matcher, %q is not a valid hostname or IP", host)
}

View File

@ -27,12 +27,10 @@ func NewConnData(serverName string, conn tcp.WriteCloser, alpnProtos []string) (
return ConnData{}, fmt.Errorf("error while parsing remote address %q: %w", conn.RemoteAddr().String(), err)
}
// as per https://datatracker.ietf.org/doc/html/rfc6066:
// > The hostname is represented as a byte string using ASCII encoding without a trailing dot.
// so there is no need to trim a potential trailing dot
serverName = types.CanonicalDomain(serverName)
return ConnData{
// As per https://datatracker.ietf.org/doc/html/rfc6066:
// > The hostname is represented as a byte string using ASCII encoding without a trailing dot.
// so there is no need to trim a potential trailing dot
serverName: types.CanonicalDomain(serverName),
remoteIP: remoteIP,
alpnProtos: alpnProtos,

View File

@ -0,0 +1,23 @@
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: ingress-with-wildcard-host-tls
namespace: default
spec:
ingressClassName: nginx
rules:
- host: "*.localhost"
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: whoami
port:
number: 80
tls:
- hosts:
- "*.localhost"
secretName: whoami-tls

View File

@ -0,0 +1,19 @@
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: ingress-with-wildcard-host
namespace: default
spec:
ingressClassName: nginx
rules:
- host: "*.localhost"
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: whoami
port:
number: 80

View File

@ -692,7 +692,7 @@ func (p *Provider) loadConfiguration(ctx context.Context) *dynamic.Configuration
rt := &dynamic.Router{
EntryPoints: p.NonTLSEntryPoints,
Rule: buildHostRule(rule.Host),
Rule: fmt.Sprintf("Host(%q)", rule.Host),
// "default" stands for the default rule syntax in Traefik v3, i.e. the v3 syntax.
RuleSyntax: "default",
Service: key,
@ -706,7 +706,7 @@ func (p *Provider) loadConfiguration(ctx context.Context) *dynamic.Configuration
rtTLS := &dynamic.Router{
EntryPoints: p.TLSEntryPoints,
Rule: buildHostRule(rule.Host),
Rule: fmt.Sprintf("Host(%q)", rule.Host),
// "default" stands for the default rule syntax in Traefik v3, i.e. the v3 syntax.
RuleSyntax: "default",
Service: key,
@ -2156,7 +2156,7 @@ func buildRule(ctx context.Context, host string, pa netv1.HTTPIngressPath, confi
var hostRules []string
for _, h := range hosts {
hostRules = append(hostRules, buildHostRule(h))
hostRules = append(hostRules, fmt.Sprintf("Host(%q)", h))
}
if len(hostRules) > 1 {
@ -2189,15 +2189,6 @@ func buildRule(ctx context.Context, host string, pa netv1.HTTPIngressPath, confi
return strings.Join(rules, " && ")
}
func buildHostRule(host string) string {
if strings.HasPrefix(host, "*.") {
host = strings.Replace(regexp.QuoteMeta(host), `\*\.`, `[a-zA-Z0-9-]+\.`, 1)
return fmt.Sprintf("HostRegexp(%q)", fmt.Sprintf("^%s$", host))
}
return fmt.Sprintf("Host(%q)", host)
}
// buildPrefixRule is a helper function to build a path prefix rule that matches path prefix split by `/`.
// For example, the paths `/abc`, `/abc/`, and `/abc/def` would all match the prefix `/abc`,
// but the path `/abcd` would not. See TestStrictPrefixMatchingRule() for more examples.

View File

@ -9730,6 +9730,160 @@ func TestLoadIngresses(t *testing.T) {
TLS: &dynamic.TLSConfiguration{},
},
},
{
desc: "Wildcard host",
paths: []string{
"services.yml",
"ingressclasses.yml",
"ingresses/ingress-with-wildcard-host.yml",
},
expected: &dynamic.Configuration{
TCP: &dynamic.TCPConfiguration{
Routers: map[string]*dynamic.TCPRouter{},
Services: map[string]*dynamic.TCPService{},
},
HTTP: &dynamic.HTTPConfiguration{
Routers: map[string]*dynamic.Router{
"default-ingress-with-wildcard-host-rule-0-path-0": {
EntryPoints: []string{"http"},
Rule: `Host("*.localhost") && PathPrefix("/")`,
RuleSyntax: "default",
Middlewares: []string{"default-ingress-with-wildcard-host-rule-0-path-0-retry"},
Service: "default-ingress-with-wildcard-host-whoami-80",
},
"default-ingress-with-wildcard-host-rule-0-path-0-tls": {
EntryPoints: []string{"https"},
Rule: `Host("*.localhost") && PathPrefix("/")`,
RuleSyntax: "default",
TLS: &dynamic.RouterTLSConfig{},
Middlewares: []string{"default-ingress-with-wildcard-host-rule-0-path-0-tls-retry"},
Service: "default-ingress-with-wildcard-host-whoami-80",
},
},
Middlewares: map[string]*dynamic.Middleware{
"default-ingress-with-wildcard-host-rule-0-path-0-retry": {
Retry: &dynamic.Retry{
Attempts: 3,
},
},
"default-ingress-with-wildcard-host-rule-0-path-0-tls-retry": {
Retry: &dynamic.Retry{
Attempts: 3,
},
},
},
Services: map[string]*dynamic.Service{
"default-ingress-with-wildcard-host-whoami-80": {
LoadBalancer: &dynamic.ServersLoadBalancer{
Servers: []dynamic.Server{
{URL: "http://10.10.0.1:80"},
{URL: "http://10.10.0.2:80"},
},
Strategy: "wrr",
PassHostHeader: ptr.To(true),
ServersTransport: "default-ingress-with-wildcard-host",
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: dynamic.DefaultFlushInterval,
},
},
},
},
ServersTransports: map[string]*dynamic.ServersTransport{
"default-ingress-with-wildcard-host": {
ForwardingTimeouts: &dynamic.ForwardingTimeouts{
DialTimeout: ptypes.Duration(60 * time.Second),
ReadTimeout: ptypes.Duration(60 * time.Second),
WriteTimeout: ptypes.Duration(60 * time.Second),
IdleConnTimeout: ptypes.Duration(60 * time.Second),
},
},
},
},
TLS: &dynamic.TLSConfiguration{},
},
},
{
desc: "Wildcard host with TLS",
paths: []string{
"services.yml",
"ingressclasses.yml",
"secrets.yml",
"ingresses/ingress-with-wildcard-host-tls.yml",
},
expected: &dynamic.Configuration{
TCP: &dynamic.TCPConfiguration{
Routers: map[string]*dynamic.TCPRouter{},
Services: map[string]*dynamic.TCPService{},
},
HTTP: &dynamic.HTTPConfiguration{
Routers: map[string]*dynamic.Router{
"default-ingress-with-wildcard-host-tls-rule-0-path-0": {
EntryPoints: []string{"http"},
Rule: `Host("*.localhost") && PathPrefix("/")`,
RuleSyntax: "default",
Middlewares: []string{"default-ingress-with-wildcard-host-tls-rule-0-path-0-retry"},
Service: "default-ingress-with-wildcard-host-tls-whoami-80",
},
"default-ingress-with-wildcard-host-tls-rule-0-path-0-tls": {
EntryPoints: []string{"https"},
Rule: `Host("*.localhost") && PathPrefix("/")`,
RuleSyntax: "default",
TLS: &dynamic.RouterTLSConfig{},
Middlewares: []string{"default-ingress-with-wildcard-host-tls-rule-0-path-0-tls-retry"},
Service: "default-ingress-with-wildcard-host-tls-whoami-80",
},
},
Middlewares: map[string]*dynamic.Middleware{
"default-ingress-with-wildcard-host-tls-rule-0-path-0-retry": {
Retry: &dynamic.Retry{
Attempts: 3,
},
},
"default-ingress-with-wildcard-host-tls-rule-0-path-0-tls-retry": {
Retry: &dynamic.Retry{
Attempts: 3,
},
},
},
Services: map[string]*dynamic.Service{
"default-ingress-with-wildcard-host-tls-whoami-80": {
LoadBalancer: &dynamic.ServersLoadBalancer{
Servers: []dynamic.Server{
{URL: "http://10.10.0.1:80"},
{URL: "http://10.10.0.2:80"},
},
Strategy: "wrr",
PassHostHeader: ptr.To(true),
ServersTransport: "default-ingress-with-wildcard-host-tls",
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: dynamic.DefaultFlushInterval,
},
},
},
},
ServersTransports: map[string]*dynamic.ServersTransport{
"default-ingress-with-wildcard-host-tls": {
ForwardingTimeouts: &dynamic.ForwardingTimeouts{
DialTimeout: ptypes.Duration(60 * time.Second),
ReadTimeout: ptypes.Duration(60 * time.Second),
WriteTimeout: ptypes.Duration(60 * time.Second),
IdleConnTimeout: ptypes.Duration(60 * time.Second),
},
},
},
},
TLS: &dynamic.TLSConfiguration{
Certificates: []*tls.CertAndStores{
{
Certificate: tls.Certificate{
CertFile: "-----BEGIN CERTIFICATE-----",
KeyFile: "-----BEGIN CERTIFICATE-----",
},
},
},
},
},
},
{
desc: "Ingress with multiple paths and one invalid path with StrictValidatePathType drops the whole ingress",
paths: []string{

View File

@ -8,7 +8,6 @@ import (
"math"
"net"
"os"
"regexp"
"slices"
"sort"
"strconv"
@ -706,7 +705,7 @@ func (p *Provider) loadRouter(rule netv1.IngressRule, pa netv1.HTTPIngressPath,
if rt.RuleSyntax == "v2" || (rt.RuleSyntax == "" && p.DefaultRuleSyntax == "v2") {
rules = append(rules, buildHostRuleV2(rule.Host))
} else {
rules = append(rules, buildHostRule(rule.Host))
rules = append(rules, fmt.Sprintf("Host(%q)", rule.Host))
}
}
@ -737,15 +736,6 @@ func buildHostRuleV2(host string) string {
return fmt.Sprintf("Host(%q)", host)
}
func buildHostRule(host string) string {
if strings.HasPrefix(host, "*.") {
host = strings.Replace(regexp.QuoteMeta(host), `\*\.`, `[a-zA-Z0-9-]+\.`, 1)
return fmt.Sprintf("HostRegexp(%q)", fmt.Sprintf("^%s$", host))
}
return fmt.Sprintf("Host(%q)", host)
}
func getCertificates(ctx context.Context, ingress *netv1.Ingress, k8sClient Client, tlsConfigs map[string]*tls.CertAndStores) error {
for _, t := range ingress.Spec.TLS {
if t.SecretName == "" {

View File

@ -206,8 +206,8 @@ func TestLoadConfigurationFromIngresses(t *testing.T) {
HTTP: &dynamic.HTTPConfiguration{
Middlewares: map[string]*dynamic.Middleware{},
Routers: map[string]*dynamic.Router{
"testing-bar-bar-97cb2ba265f7a5df4ab9": {
Rule: `HostRegexp("^[a-zA-Z0-9-]+\\.bar$") && PathPrefix("/bar")`,
"testing-bar-bar-41871576e140babe40bd": {
Rule: `Host("*.bar") && PathPrefix("/bar")`,
Service: "testing-service1-80",
},
"testing-bar-bar-605945111a3c9f84dc65": {
@ -1161,7 +1161,7 @@ func TestLoadConfigurationFromIngresses(t *testing.T) {
Middlewares: map[string]*dynamic.Middleware{},
Routers: map[string]*dynamic.Router{
"testing-foobar-com-bar": {
Rule: `HostRegexp("^[a-zA-Z0-9-]+\\.foobar\\.com$") && PathPrefix("/bar")`,
Rule: `Host("*.foobar.com") && PathPrefix("/bar")`,
Service: "testing-service1-80",
},
},

View File

@ -12,6 +12,7 @@ import (
"github.com/rs/zerolog/log"
"github.com/traefik/traefik/v3/pkg/config/runtime"
"github.com/traefik/traefik/v3/pkg/middlewares/snicheck"
"github.com/traefik/traefik/v3/pkg/muxer"
httpmuxer "github.com/traefik/traefik/v3/pkg/muxer/http"
tcpmuxer "github.com/traefik/traefik/v3/pkg/muxer/tcp"
"github.com/traefik/traefik/v3/pkg/observability/logs"
@ -179,7 +180,9 @@ func (m *Manager) buildEntryPointHandler(ctx context.Context, configs map[string
// # When a request for "/foo" comes, even though it won't be routed by httpRouter2,
// # if its SNI is set to foo.com, myTLSOptions will be used for the TLS connection.
// # Otherwise, it will fallback to the default TLS config.
logger.Warn().Msgf("No domain found in rule %v, the TLS options applied for this router will depend on the SNI of each request", routerHTTPConfig.Rule)
if tlsOptionsName != traefiktls.DefaultTLSConfigName {
logger.Warn().Msgf("No domain found in rule %v, the TLS option %s cannot be applied", routerHTTPConfig.Rule, tlsOptionsName)
}
}
// Even though the error is seemingly ignored (aside from logging it),
@ -339,7 +342,7 @@ func (m *Manager) addTCPHandlers(ctx context.Context, configs map[string]*runtim
}
for _, domain := range domains {
if httpmuxer.IsASCII(domain) {
if muxer.IsASCII(domain) {
continue
}

View File

@ -281,12 +281,6 @@ func matchDomain(serverName, certDomain string) bool {
}
labels := strings.Split(serverName, ".")
for i := range labels {
labels[i] = "*"
candidate := strings.Join(labels, ".")
if certDomain == candidate {
return true
}
}
return false
labels[0] = "*"
return certDomain == strings.Join(labels, ".")
}