From a513a05b7a45ae79c6e5acdb02aeb0c7c5f2ba6d Mon Sep 17 00:00:00 2001 From: Romain Date: Mon, 22 Mar 2021 21:16:04 +0100 Subject: [PATCH] Raise errors for non-ASCII domain names in a router's rules --- docs/content/migration/v2.md | 15 +++++++++ docs/content/routing/routers/index.md | 19 ++++++++--- pkg/rules/rules.go | 20 ++++++++++++ pkg/rules/rules_test.go | 10 ++++++ pkg/server/router/tcp/router.go | 46 ++++++++++++++++----------- pkg/server/router/tcp/router_test.go | 31 ++++++++++++++++++ 6 files changed, 118 insertions(+), 23 deletions(-) diff --git a/docs/content/migration/v2.md b/docs/content/migration/v2.md index 3d936cb94..d48a50ca4 100644 --- a/docs/content/migration/v2.md +++ b/docs/content/migration/v2.md @@ -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. diff --git a/docs/content/routing/routers/index.md b/docs/content/routing/routers/index.md index 111302772..8592ecc1e 100644 --- a/docs/content/routing/routers/index.md +++ b/docs/content/routing/routers/index.md @@ -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. diff --git a/pkg/rules/rules.go b/pkg/rules/rules.go index a6bfedb1b..08b0ebaae 100644 --- a/pkg/rules/rules.go +++ b/pkg/rules/rules.go @@ -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 +} diff --git a/pkg/rules/rules_test.go b/pkg/rules/rules_test.go index ca38ef28f..a73f78f10 100644 --- a/pkg/rules/rules_test.go +++ b/pkg/rules/rules_test.go @@ -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`)", diff --git a/pkg/server/router/tcp/router.go b/pkg/server/router/tcp/router.go index 69a04fc4f..397a4bf3c 100644 --- a/pkg/server/router/tcp/router.go +++ b/pkg/server/router/tcp/router.go @@ -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: diff --git a/pkg/server/router/tcp/router_test.go b/pkg/server/router/tcp/router_test.go index a00465a86..ab261377b 100644 --- a/pkg/server/router/tcp/router_test.go +++ b/pkg/server/router/tcp/router_test.go @@ -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{