Raise errors for non-ASCII domain names in a router's rules

This commit is contained in:
Romain 2021-03-22 21:16:04 +01:00 committed by GitHub
parent 1e716a93ff
commit a513a05b7a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 118 additions and 23 deletions

View file

@ -336,3 +336,18 @@ The file parser has been changed, since v2.3 the unknown options/fields in a dyn
In `v2.3`, the support of `IngressClass`, which is available since Kubernetes version `1.18`, has been introduced.
In order to be able to use this new resource the [Kubernetes RBAC](../reference/dynamic-configuration/kubernetes-crd.md#rbac) must be updated.
## v2.4.7 to v2.4.8
### Non-ASCII Domain Names
In `v2.4.8` we introduced a new check on domain names used in HTTP router rule `Host` and `HostRegexp` expressions,
and in TCP router rule `HostSNI` expression.
This check ensures that provided domain names don't contain non-ASCII characters.
If not, an error is raised, and the associated router will be shown as invalid in the dashboard.
This new behavior is intended to show what was failing silently previously and to help troubleshooting configuration issues.
It doesn't change the support for non-ASCII domain names in routers rules, which is not part of the Traefik feature set so far.
In order to use non-ASCII domain names in a router's rule, one should use the Punycode form of the domain name.
For more information, please read the [HTTP routers rule](../routing/routers/index.md#rule) part or [TCP router rules](../routing/routers/index.md#rule_1) part of the documentation.

View file

@ -235,16 +235,22 @@ The table below lists all the available matchers:
| ```Host(`example.com`, ...)``` | Check if the request domain (host header value) targets one of the given `domains`. |
| ```HostHeader(`example.com`, ...)``` | Check if the request domain (host header value) targets one of the given `domains`. |
| ```HostRegexp(`example.com`, `{subdomain:[a-z]+}.example.com`, ...)``` | Check if the request domain matches the given `regexp`. |
| ```Method(`GET`, ...)``` | Check if the request method is one of the given `methods` (`GET`, `POST`, `PUT`, `DELETE`, `PATCH`, `HEAD`) |
| ```Method(`GET`, ...)``` | Check if the request method is one of the given `methods` (`GET`, `POST`, `PUT`, `DELETE`, `PATCH`, `HEAD`) |
| ```Path(`/path`, `/articles/{cat:[a-z]+}/{id:[0-9]+}`, ...)``` | Match exact request path. It accepts a sequence of literal and regular expression paths. |
| ```PathPrefix(`/products/`, `/articles/{cat:[a-z]+}/{id:[0-9]+}`)``` | Match request prefix path. It accepts a sequence of literal and regular expression prefix paths. |
| ```Query(`foo=bar`, `bar=baz`)``` | Match Query String parameters. It accepts a sequence of key=value pairs. |
!!! important "Non-ASCII Domain Names"
Non-ASCII characters are not supported in `Host` and `HostRegexp` expressions, and by doing so the associated router will be invalid.
For the `Host` expression, domain names containing non-ASCII characters must be provided as punycode encoded values ([rfc 3492](https://tools.ietf.org/html/rfc3492)).
As well, when using the `HostRegexp` expressions, in order to match domain names containing non-ASCII characters, the regular expression should match a punycode encoded domain name.
!!! important "Regexp Syntax"
In order to use regular expressions with `Host` and `Path` expressions,
you must declare an arbitrarily named variable followed by the colon-separated regular expression, all enclosed in curly braces.
Any pattern supported by [Go's regexp package](https://golang.org/pkg/regexp/) may be used (example: `/posts/{id:[0-9]+}`).
`HostRegexp` and `Path` accept an expression with zero or more groups enclosed by curly braces.
Named groups can be like `{name:pattern}` that matches the given regexp pattern or like `{name}` that matches anything until the next dot.
Any pattern supported by [Go's regexp package](https://golang.org/pkg/regexp/) may be used (example: `{subdomain:[a-z]+}.{domain}.com`).
!!! info "Combining Matchers Using Operators and Parenthesis"
@ -782,6 +788,11 @@ If you want to limit the router scope to a set of entry points, set the entry po
|--------------------------------|-------------------------------------------------------------------------|
| ```HostSNI(`domain-1`, ...)``` | Check if the Server Name Indication corresponds to the given `domains`. |
!!! important "Non-ASCII Domain Names"
Non-ASCII characters are not supported in the `HostSNI` expression, and by doing so the associated TCP router will be invalid.
Domain names containing non-ASCII characters must be provided as punycode encoded values ([rfc 3492](https://tools.ietf.org/html/rfc3492)).
!!! important "HostSNI & TLS"
It is important to note that the Server Name Indication is an extension of the TLS protocol.

View file

@ -4,6 +4,7 @@ import (
"fmt"
"net/http"
"strings"
"unicode/utf8"
"github.com/gorilla/mux"
"github.com/traefik/traefik/v2/pkg/log"
@ -102,6 +103,10 @@ func pathPrefix(route *mux.Route, paths ...string) error {
func host(route *mux.Route, hosts ...string) error {
for i, host := range hosts {
if !IsASCII(host) {
return fmt.Errorf("invalid value %q for \"Host\" matcher, non-ASCII characters are not allowed", host)
}
hosts[i] = strings.ToLower(host)
}
@ -152,6 +157,10 @@ func host(route *mux.Route, hosts ...string) error {
func hostRegexp(route *mux.Route, hosts ...string) error {
router := route.Subrouter()
for _, host := range hosts {
if !IsASCII(host) {
return fmt.Errorf("invalid value %q for HostRegexp matcher, non-ASCII characters are not allowed", host)
}
tmpRt := router.Host(host)
if tmpRt.GetError() != nil {
return tmpRt.GetError()
@ -250,3 +259,14 @@ func checkRule(rule *tree) error {
}
return nil
}
// IsASCII checks if the given string contains only ASCII characters.
func IsASCII(s string) bool {
for i := 0; i < len(s); i++ {
if s[i] >= utf8.RuneSelf {
return false
}
}
return true
}

View file

@ -60,6 +60,16 @@ func Test_addRoute(t *testing.T) {
"http://localhost/foo": http.StatusOK,
},
},
{
desc: "Non-ASCII Host",
rule: "Host(`locàlhost`)",
expectedError: true,
},
{
desc: "Non-ASCII HostRegexp",
rule: "HostRegexp(`locàlhost`)",
expectedError: true,
},
{
desc: "HostHeader equivalent to Host",
rule: "HostHeader(`localhost`)",

View file

@ -258,28 +258,36 @@ func (m *Manager) buildEntryPointHandler(ctx context.Context, configs map[string
logger.Debugf("Adding route %s on TCP", domain)
switch {
case routerConfig.TLS != nil:
if !rules.IsASCII(domain) {
asciiError := fmt.Errorf("invalid domain name value %q, non-ASCII characters are not allowed", domain)
routerConfig.AddError(asciiError, true)
logger.Debug(asciiError)
continue
}
if routerConfig.TLS.Passthrough {
router.AddRoute(domain, handler)
} else {
tlsOptionsName := routerConfig.TLS.Options
if len(tlsOptionsName) == 0 {
tlsOptionsName = defaultTLSConfigName
}
if tlsOptionsName != defaultTLSConfigName {
tlsOptionsName = provider.GetQualifiedName(ctxRouter, tlsOptionsName)
}
tlsConf, err := m.tlsManager.Get(defaultTLSStoreName, tlsOptionsName)
if err != nil {
routerConfig.AddError(err, true)
logger.Debug(err)
continue
}
router.AddRouteTLS(domain, handler, tlsConf)
continue
}
tlsOptionsName := routerConfig.TLS.Options
if len(tlsOptionsName) == 0 {
tlsOptionsName = defaultTLSConfigName
}
if tlsOptionsName != defaultTLSConfigName {
tlsOptionsName = provider.GetQualifiedName(ctxRouter, tlsOptionsName)
}
tlsConf, err := m.tlsManager.Get(defaultTLSStoreName, tlsOptionsName)
if err != nil {
routerConfig.AddError(err, true)
logger.Debug(err)
continue
}
router.AddRouteTLS(domain, handler, tlsConf)
case domain == "*":
router.AddCatchAllNoTLS(handler)
default:

View file

@ -71,6 +71,37 @@ func TestRuntimeConfiguration(t *testing.T) {
},
expectedError: 0,
},
{
desc: "Non-ASCII domain error",
tcpServiceConfig: map[string]*runtime.TCPServiceInfo{
"foo-service": {
TCPService: &dynamic.TCPService{
LoadBalancer: &dynamic.TCPServersLoadBalancer{
Servers: []dynamic.TCPServer{
{
Port: "8085",
Address: "127.0.0.1:8085",
},
},
},
},
},
},
tcpRouterConfig: map[string]*runtime.TCPRouterInfo{
"foo": {
TCPRouter: &dynamic.TCPRouter{
EntryPoints: []string{"web"},
Service: "foo-service",
Rule: "HostSNI(`bàr.foo`)",
TLS: &dynamic.RouterTCPTLSConfig{
Passthrough: false,
Options: "foo",
},
},
},
},
expectedError: 1,
},
{
desc: "HTTP routers with same domain but different TLS options",
httpServiceConfig: map[string]*runtime.ServiceInfo{