Repository: traefik/traefik (language: go)

Issue:
Unify router rule matchers
### Welcome!

- [X] Yes, I've searched similar issues on [GitHub](https://github.com/traefik/traefik/issues) and didn't find any.
- [X] Yes, I've searched similar issues on the [Traefik community forum](https://community.traefik.io) and didn't find any.

### What did you expect to see?

The goal of this proposal is to clarify router rule matchers in Traefik.

We see four issues with the current rule syntax:

1) Regexps are defined using the [gorilla/mux](https://github.com/containous/mux) syntax, which arbitrary enforces naming matching groups.
This adds complexity, and should be removed, as it's not even used by Traefik.
2) It is sometimes unclear which matcher uses regexps (e.g., there are `Host`, and `HostRegexp` matchers, but not `Path`, and `PathRegexp` matchers).
There should be an explicit regexp matcher when appropriate (i.e., `PathRegexp`, and `QueryRegexp`).
3) Matchers that accept multiple values are ORing them when it makes sense (e.g., `Host`, `Path`, etc.), or ANDing them when it doesn't (e.g., `Query`, `Headers`, etc.).
Even thought it's not wrong, this can cause confusion where one would expect those matchers to deal with their values similarly (i.e., ORing them all).
Making every matcher accept only one value, and combining them with logical operators is more verbose, but eliminates this issue.
4) `Query` and `Headers` matchers both expect a key and value as arguments.
These two matchers should get their argument using the same syntax (e.g., using `Headers` matcher one's).

These changes would transform:
```yml
rule: Host(`example.com`, `example.org`) && Path(`/products/{id:[a-z]+}`) || Query(`mobile=true`)
```

into:
```yml
rule: (Host(`example.com`) || Host(`example.org`)) && PathRegexp(`/products/[a-z]+`) || Query(`mobile`, `true`)
```


Discussion / hints:

Fix patch:
diff --git a/docs/content/migration/v2-to-v3.md b/docs/content/migration/v2-to-v3.md
index 31218c4667..4a8a9bf248 100644
--- a/docs/content/migration/v2-to-v3.md
+++ b/docs/content/migration/v2-to-v3.md
@@ -30,3 +30,18 @@ In v3, the reported status code for gRPC requests is now the value of the `Grpc-
 - `sslRedirect`, `sslTemporaryRedirect`, `sslHost`, `sslForceHost` and `featurePolicy` options of the Headers middleware have been removed.
 - The `forceSlash` option of the StripPrefix middleware has been removed.
 - the `preferServerCipherSuites` option has been removed.
+
+## Matchers
+
+In v3, the `Headers` and `HeadersRegexp` matchers have been renamed to `Header` and `HeaderRegexp` respectively.
+
+`QueryRegexp` has been introduced to match query values using a regular expression.
+
+`HeaderRegexp`, `HostRegexp`, `PathRegexp`, `QueryRegexp`, and `HostSNIRegexp` matchers now uses the [Go regexp syntax](https://golang.org/pkg/regexp/syntax/).
+
+All matchers now take a single value (except `Headers`, `HeaderRegexp`, `Query`, and `QueryRegexp` which take two)
+and should be explicitly combined using logical operators to mimic previous behavior.
+
+`Query` can take a single value to match is the query value that has no value (e.g. `/search?mobile`).
+
+`HostHeader` has been removed, use `Host` instead.
diff --git a/docs/content/operations/dashboard.md b/docs/content/operations/dashboard.md
index 61e17e7427..5b79dfc985 100644
--- a/docs/content/operations/dashboard.md
+++ b/docs/content/operations/dashboard.md
@@ -93,12 +93,12 @@ rule = "Host(`traefik.example.com`)"
 
 ```bash tab="Path Prefix Rule"
 # The dashboard can be accessed on http://example.com/dashboard/ or http://traefik.example.com/dashboard/
-rule = "PathPrefix(`/api`, `/dashboard`)"
+rule = "PathPrefix(`/api`) || PathPrefix(`/dashboard`)"
 ```
 
 ```bash tab="Combination of Rules"
 # The dashboard can be accessed on http://traefik.example.com/dashboard/
-rule = "Host(`traefik.example.com`) && PathPrefix(`/api`, `/dashboard`)"
+rule = "Host(`traefik.example.com`) && (PathPrefix(`/api`) || PathPrefix(`/dashboard`))"
 ```
 
 ??? example "Dashboard Dynamic Configuration Examples"
diff --git a/docs/content/routing/routers/index.md b/docs/content/routing/routers/index.md
index 557dcd8efd..14acc44487 100644
--- a/docs/content/routing/routers/index.md
+++ b/docs/content/routing/routers/index.md
@@ -214,78 +214,224 @@ If you want to limit the router scope to a set of entry points, set the `entryPo
 Rules are a set of matchers configured with values, that determine if a particular request matches specific criteria.
 If the rule is verified, the router becomes active, calls middlewares, and then forwards the request to the service.
 
-??? tip "Backticks or Quotes?"
+The table below lists all the available matchers:
+
+| Rule                                                            | Description                                                                    |
+|-----------------------------------------------------------------|:-------------------------------------------------------------------------------|
+| [```Header(`key`, `value`)```](#header-and-headerregexp)        | Matches requests containing a header named `key` set to `value`.               |
+| [```HeaderRegexp(`key`, `regexp`)```](#header-and-headerregexp) | Matches requests containing a header named `key` matching `regexp`.            |
+| [```Host(`domain`)```](#host-and-hostregexp)                    | Matches requests host set to `domain`.                                         |
+| [```HostRegexp(`regexp`)```](#host-and-hostregexp)              | Matches requests host matching `regexp`.                                       |
+| [```Method(`method`)```](#method)                               | Matches requests method set to `method`.                                       |
+| [```Path(`path`)```](#path-pathprefix-and-pathregexp)           | Matches requests path set to `path`.                                           |
+| [```PathPrefix(`prefix`)```](#path-pathprefix-and-pathregexp)   | Matches requests path prefix set to `prefix`.                                  |
+| [```PathRegexp(`regexp`)```](#path-pathprefix-and-pathregexp)   | Matches request path using `regexp`.                                           |
+| [```Query(`key`, `value`)```](#query-and-queryregexp)           | Matches requests query parameters named `key` set to `value`.                  |
+| [```QueryRegexp(`key`, `regexp`)```](#query-and-queryregexp)    | Matches requests query parameters named `key` matching `regexp`.               |
+| [```ClientIP(`ip`)```](#clientip)                               | Matches requests client IP using `ip`. It accepts IPv4, IPv6 and CIDR formats. |
+
+!!! tip "Backticks or Quotes?"
+
     To set the value of a rule, use [backticks](https://en.wiktionary.org/wiki/backtick) ``` ` ``` or escaped double-quotes `\"`.
 
-    Single quotes `'` are not accepted since the values are [Golang's String Literals](https://golang.org/ref/spec#String_literals).
+    Single quotes `'` are not accepted since the values are [Go's String Literals](https://golang.org/ref/spec#String_literals).
+
+!!! important "Regexp Syntax"
+
+    Matchers that accept a regexp as their value use a [Go](https://golang.org/pkg/regexp/) flavored syntax.
 
-!!! example "Host is example.com"
+!!! info "Expressing Complex Rules Using Operators and Parenthesis"
 
-    ```toml
-    rule = "Host(`example.com`)"
+    The usual AND (`&&`) and OR (`||`) logical operators can be used, with the expected precedence rules,
+    as well as parentheses.
+    
+    One can invert a matcher by using the NOT (`!`) operator.
+    
+    The following rule matches requests where:
+    
+    - either host is `example.com` OR,
+    - host is `example.org` AND path is NOT `/traefik`
+    
+    ```yaml
+    Host(`example.com`) || (Host(`example.org`) && !Path(`/traefik`))
     ```
 
-!!! example "Host is example.com OR Host is example.org AND path is /traefik"
+#### Header and HeaderRegexp
+
+The `Header` and `HeaderRegexp` matchers allow to match requests that contain specific header.
 
-    ```toml
-    rule = "Host(`example.com`) || (Host(`example.org`) && Path(`/traefik`))"
+!!! example "Examples"
+    
+    Match requests with a `Content-Type` header set to `application/yaml`:
+    
+    ```yaml
+    Header(`Content-Type`, `application/yaml`)
+    ```
+    
+    Match requests with a `Content-Type` header set to either `application/json` or `application/yaml`:
+    
+    ```yaml
+    HeaderRegexp(`Content-Type`, `^application/(json|yaml)$`)
+    ```
+    
+    To match headers [case-insensitively](https://en.wikipedia.org/wiki/Case_sensitivity), use the `(?i)` option:
+    
+    ```yaml
+    HeaderRegexp(`Content-Type`, `(?i)^application/(json|yaml)$`)
     ```
 
-The table below lists all the available matchers:
+#### Host and HostRegexp
 
-| Rule                                                                   | Description                                                                                                    |
-|------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------|
-| ```Headers(`key`, `value`)```                                          | Check if there is a key `key`defined in the headers, with the value `value`                                    |
-| ```HeadersRegexp(`key`, `regexp`)```                                   | Check if there is a key `key`defined in the headers, with a value that matches the regular expression `regexp` |
-| ```Host(`example.com`, ...)```                                         | Check if the request domain (host header value) targets one of the given `domains`.                            |
-| ```HostHeader(`example.com`, ...)```                                   | Same as `Host`, only exists for historical reasons.                                                            |
-| ```HostRegexp(`example.com`, `{subdomain:[a-z]+}.example.com`, ...)``` | Match the request domain. See "Regexp Syntax" below.                                                           |
-| ```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. See "Regexp Syntax" below.                                                           |
-| ```PathPrefix(`/products/`, `/articles/{cat:[a-z]+}/{id:[0-9]+}`)```   | Match request prefix path. See "Regexp Syntax" below.                                                          |
-| ```Query(`foo=bar`, `bar=baz`)```                                      | Match Query String parameters. It accepts a sequence of key=value pairs.                                       |
-| ```ClientIP(`10.0.0.0/16`, `::1`)```                                   | Match if the request client IP is one of the given IP/CIDR. It accepts IPv4, IPv6 and CIDR formats.            |
-
-!!! 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.
+The `Host` and `HostRegexp` matchers allow to match requests that are targeted to a given host.
 
-!!! important "Regexp Syntax"
+These matchers do not support non-ASCII characters, use punycode encoded values ([rfc 3492](https://tools.ietf.org/html/rfc3492)) to match such domains.
 
-    `HostRegexp`, `PathPrefix`, and `Path` accept an expression with zero or more groups enclosed by curly braces, which are called named regexps.
-    Named regexps, of the form `{name:regexp}`, are the only expressions considered for regexp matching.
-    The regexp name (`name` in the above example) is an arbitrary value, that exists only for historical reasons.
+If no Host is set in the request URL (e.g., it's an IP address), these matchers will look at the `Host` header.
 
-    Any `regexp` supported by [Go's regexp package](https://golang.org/pkg/regexp/) may be used.
-    For example, here is a case insensitive path matcher syntax: ```Path(`/{path:(?i:Products)}`)```.
+!!! example "Examples"
 
-!!! info "Combining Matchers Using Operators and Parenthesis"
+    Match requests with `Host` set to `example.com`:
+    
+    ```yaml
+    Host(`example.com`)
+    ```
+ 
+    Match requests sent to any subdomain of `example.com`:
+    
+    ```yaml
+    HostRegexp(`^.+\.example\.com$`)
+    ```
 
-    The usual AND (`&&`) and OR (`||`) logical operators can be used, with the expected precedence rules,
-    as well as parentheses.
+    Match requests with `Host` set to either `example.com` or `example.org`:
+    
+    ```yaml
+    HostRegexp(`^example\.(com|org)$`)
+    ```
+
+    To match domains [case-insensitively](https://en.wikipedia.org/wiki/Case_sensitivity), use the `(?i)` option:
+
+    ```yaml
+    HostRegexp(`(?i)^example\.(com|org)$`)
+    ```
+
+#### Method
+
+The `Method` matchers allows to match requests sent with the given method.
 
-!!! info "Inverting a matcher"
+!!! example "Example"
 
-    One can invert a matcher by using the `!` operator.
+    Match `OPTIONS` requests:
+    
+    ```yaml
+    Method(`OPTIONS`)
+    ```
+
+#### Path, PathPrefix, and PathRegexp
+
+These matchers allow matching requests based on their URL path.
+
+For exact matches, use `Path` and its prefixed alternative `PathPrefix`, for regexp matches, use `PathRegexp`.
+
+Path are always starting with a `/`, except for `PathRegexp`.
+
+!!! example "Examples"
 
-!!! important "Rule, Middleware, and Services"
+    Match `/products` but neither `/products/shoes` nor `/products/`:
+    
+    ```yaml
+    Path(`/products`)
+    ```
+    
+    Match `/products` as well as everything under `/products`,
+    such as `/products/shoes`, `/products/` but also `/products-for-sale`:
+    
+    ```yaml
+    PathPrefix(`/products`)
+    ```
+
+    Match both `/products/shoes` and `/products/socks` with and ID like `/products/shoes/57`:
 
-    The rule is evaluated "before" any middleware has the opportunity to work, and "before" the request is forwarded to the service.
+    ```yaml
+    PathRegexp(`^/products/(shoes|socks)/[0-9]+$`)
+    ```
 
-!!! info "Path Vs PathPrefix"
+    Match requests with a path ending in either `.jpeg`, `.jpg` or `.png`:
 
-    Use `Path` if your service listens on the exact path only. For instance, ```Path(`/products`)``` would match `/products` but not `/products/shoes`.
+    ```yaml
+    PathRegexp(`\.(jpeg|jpg|png)$`)
+    ```
 
-    Use a `*Prefix*` matcher if your service listens on a particular base path but also serves requests on sub-paths.
-    For instance, ```PathPrefix(`/products`)``` would match `/products` and `/products/shoes`,
-    as well as `/productsforsale`, and `/productsforsale/shoes`.
-    Since the path is forwarded as-is, your service is expected to listen on `/products`.
+    Match `/products` as well as everything under `/products`,
+    such as `/products/shoes`, `/products/` but also `/products-for-sale`,
+    [case-insensitively](https://en.wikipedia.org/wiki/Case_sensitivity):
 
-!!! info "ClientIP matcher"
+    ```yaml
+    HostRegexp(`(?i)^/products`)
+    ```
 
-    The `ClientIP` matcher will only match the request client IP and does not use the `X-Forwarded-For` header for matching.
+#### Query and QueryRegexp
+
+The `Query` and `QueryRegexp` matchers allow to match requests based on query parameters.
+
+!!! example "Examples"
+
+    Match requests with a `mobile` query parameter set to `true`, such as in `/search?mobile=true`:
+    
+    ```yaml
+    Query(`mobile`, `true`)
+    ```
+
+    To match requests with a query parameter `mobile` that has no value, such as in `/search?mobile`, use:
+
+    ```yaml
+    Query(`mobile`)
+    ```
+
+    Match requests with a `mobile` query parameter set to either `true` or `yes`:
+    
+    ```yaml
+    QueryRegexp(`mobile`, `^(true|yes)$`)
+    ```
+
+    Match requests with a `mobile` query parameter set to any value (including the empty value):
+    
+    ```yaml
+    QueryRegexp(`mobile`, `^.*$`)
+    ```
+
+    To match query parameters [case-insensitively](https://en.wikipedia.org/wiki/Case_sensitivity), use the `(?i)` option:
+
+    ```yaml
+    QueryRegexp(`mobile`, `(?i)^(true|yes)$`)
+    ```
+
+#### ClientIP
+
+The `ClientIP` matcher allows matching requests sent from the given client IP.
+
+It only matches the request client IP and does not use the `X-Forwarded-For` header for matching.
+
+!!! example "Examples"
+
+    Match requests coming from a given IP:
+    
+    ```yaml tab="IPv4"
+    ClientIP(`10.76.105.11`)
+    ```
+    
+    ```yaml tab="IPv6"
+    ClientIP(`::1`)
+    ```
+    
+    Match requests coming from a given subnet:
+
+    ```yaml tab="IPv4"
+    ClientIP(`192.168.1.0/24`)
+    ```
+
+    ```yaml tab="IPv6"
+    ClientIP(`fe80::/10`)
+    ```
 
 ### Priority
 
@@ -300,7 +446,7 @@ A value of `0` for the priority is ignored: `priority = 0` means that the defaul
     http:
       routers:
         Router-1:
-          rule: "HostRegexp(`{subdomain:[a-z]+}.traefik.com`)"
+          rule: "HostRegexp(`[a-z]+\.traefik\.com`)"
           # ...
         Router-2:
           rule: "Host(`foobar.traefik.com`)"
@@ -311,7 +457,7 @@ A value of `0` for the priority is ignored: `priority = 0` means that the defaul
     ## Dynamic configuration
     [http.routers]
       [http.routers.Router-1]
-        rule = "HostRegexp(`{subdomain:[a-z]+}.traefik.com`)"
+        rule = "HostRegexp(`[a-z]+\\.traefik\\.com`)"
         # ...
       [http.routers.Router-2]
         rule = "Host(`foobar.traefik.com`)"
@@ -320,10 +466,10 @@ A value of `0` for the priority is ignored: `priority = 0` means that the defaul
 
     In this case, all requests with host `foobar.traefik.com` will be routed through `Router-1` instead of `Router-2`.
 
-    | Name     | Rule                                               | Priority |
-    |----------|----------------------------------------------------|----------|
-    | Router-1 | ```HostRegexp(`{subdomain:[a-z]+}.traefik.com`)``` | 44       |
-    | Router-2 | ```Host(`foobar.traefik.com`)```                   | 26       |
+    | Name     | Rule                                     | Priority |
+    |----------|------------------------------------------|----------|
+    | Router-1 | ```HostRegexp(`[a-z]+\.traefik\.com`)``` | 44       |
+    | Router-2 | ```Host(`foobar.traefik.com`)```         | 26       |
 
     The previous table shows that `Router-1` has a higher priority than `Router-2`.
 
@@ -336,7 +482,7 @@ A value of `0` for the priority is ignored: `priority = 0` means that the defaul
     http:
       routers:
         Router-1:
-          rule: "HostRegexp(`{subdomain:[a-z]+}.traefik.com`)"
+          rule: "HostRegexp(`[a-z]+\\.traefik\\.com`)"
           entryPoints:
           - "web"
           service: service-1
@@ -353,7 +499,7 @@ A value of `0` for the priority is ignored: `priority = 0` means that the defaul
     ## Dynamic configuration
     [http.routers]
       [http.routers.Router-1]
-        rule = "HostRegexp(`{subdomain:[a-z]+}.traefik.com`)"
+        rule = "HostRegexp(`[a-z]+\\.traefik\\.com`)"
         entryPoints = ["web"]
         service = "service-1"
         priority = 1
@@ -818,48 +964,49 @@ If you want to limit the router scope to a set of entry points, set the entry po
 
 ### Rule
 
-Rules are a set of matchers configured with values, that determine if a particular request matches specific criteria.
+Rules are a set of matchers configured with values, that determine if a particular connection matches specific criteria.
 If the rule is verified, the router becomes active, calls middlewares, and then forwards the request to the service.
 
-??? tip "Backticks or Quotes?"
-
-     To set the value of a rule, use [backticks](https://en.wiktionary.org/wiki/backtick) ``` ` ``` or escaped double-quotes `\"`.
-
-    Single quotes `'` are not accepted since the values are [Golang's String Literals](https://golang.org/ref/spec#String_literals).
+The table below lists all the available matchers:
 
-!!! example "HostSNI is example.com"
+| Rule                                                        | Description                                                                                      |
+|-------------------------------------------------------------|:-------------------------------------------------------------------------------------------------|
+| [```HostSNI(`domain`)```](#hostsni-and-hostsniregexp)       | Checks if the connection's Server Name Indication is equal to `domain`.                          |
+| [```HostSNIRegexp(`regexp`)```](#hostsni-and-hostsniregexp) | Checks if the connection's Server Name Indication matches `regexp`.                              |
+| [```ClientIP(`ip`)```](#clientip_1)                         | Checks if the connection's client IP correspond to `ip`. It accepts IPv4, IPv6 and CIDR formats. |
+| [```ALPN(`protocol`)```](#alpn)                             | Checks if the connection's ALPN protocol equals `protocol`.                                      |
 
-    ```toml
-    rule = "HostSNI(`example.com`)"
-    ```
+!!! tip "Backticks or Quotes?"
 
-!!! example "HostSNI is example.com OR HostSNI is example.org AND ClientIP is 0.0.0.0"
+     To set the value of a rule, use [backticks](https://en.wiktionary.org/wiki/backtick) ``` ` ``` or escaped double-quotes `\"`.
 
-    ```toml
-    rule = "HostSNI(`example.com`) || (HostSNI(`example.org`) && ClientIP(`0.0.0.0`))"
-    ```
+    Single quotes `'` are not accepted since the values are [Go's String Literals](https://golang.org/ref/spec#String_literals).
 
-The table below lists all the available matchers:
+!!! important "Regexp Syntax"
 
-| Rule                                                                      | Description                                                                                             |
-|---------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------|
-| ```HostSNI(`domain-1`, ...)```                                            | Checks if the Server Name Indication corresponds to the given `domains`.                                |
-| ```HostSNIRegexp(`example.com`, `{subdomain:[a-z]+}.example.com`, ...)``` | Checks if the Server Name Indication matches the given regular expressions. See "Regexp Syntax" below.  |
-| ```ClientIP(`10.0.0.0/16`, `::1`)```                                      | Checks if the connection client IP is one of the given IP/CIDR. It accepts IPv4, IPv6 and CIDR formats. |
-| ```ALPN(`mqtt`, `h2c`)```                                                 | Checks if any of the connection ALPN protocols is one of the given protocols.                           |
+    Matchers that accept a regexp as their value use a [Go](https://golang.org/pkg/regexp/) flavored syntax.
 
-!!! important "Non-ASCII Domain Names"
+!!! info "Expressing Complex Rules Using Operators and Parenthesis"
 
-    Non-ASCII characters are not supported in the `HostSNI` and `HostSNIRegexp` expressions, and so using them would invalidate the associated TCP router.
-    Domain names containing non-ASCII characters must be provided as punycode encoded values ([rfc 3492](https://tools.ietf.org/html/rfc3492)).
+    The usual AND (`&&`) and OR (`||`) logical operators can be used, with the expected precedence rules,
+    as well as parentheses.
+    
+    One can invert a matcher by using the NOT (`!`) operator.
+    
+    The following rule matches connections where:
+    
+    - either Server Name Indication is `example.com` OR,
+    - Server Name Indication is `example.org` AND ALPN protocol is NOT `h2`
+    
+    ```yaml
+    HostSNI(`example.com`) || (HostSNI(`example.org`) && !ALPN(`h2`))
+    ```
 
-!!! important "Regexp Syntax"
+#### HostSNI and HostSNIRegexp
 
-    `HostSNIRegexp` accepts an expression with zero or more groups enclosed by curly braces, which are called named regexps.
-    Named regexps, of the form `{name:regexp}`, are the only expressions considered for regexp matching.
-    The regexp name (`name` in the above example) is an arbitrary value, that exists only for historical reasons.
+`HostSNI` and `HostSNIRegexp` matchers allow to match connections targeted to a given domain.
 
-    Any `regexp` supported by [Go's regexp package](https://golang.org/pkg/regexp/) may be used.
+These matchers do not support non-ASCII characters, use punycode encoded values ([rfc 3492](https://tools.ietf.org/html/rfc3492)) to match such domains.
 
 !!! important "HostSNI & TLS"
 
@@ -869,25 +1016,72 @@ The table below lists all the available matchers:
     when one wants a non-TLS router that matches all (non-TLS) requests,
     one should use the specific ```HostSNI(`*`)``` syntax.
 
-!!! info "Combining Matchers Using Operators and Parenthesis"
+!!! example "Examples"
 
-    The usual AND (`&&`) and OR (`||`) logical operators can be used, with the expected precedence rules,
-    as well as parentheses.
+    Match all connections:
+    
+    ```yaml tab="HostSNI"
+    HostSNI(`*`)
+    ```
+    
+    ```yaml tab="HostSNIRegexp"
+    HostSNIRegexp(`^.*$`)
+    ```
+
+    Match TCP connections sent to `example.com`:
+    
+    ```yaml
+    HostSNI(`example.com`)
+    ```
 
-!!! info "Inverting a matcher"
+    Match TCP connections openned on any subdomain of `example.com`:
+    
+    ```yaml
+    HostSNIRegexp(`^.+\.example\.com$`)
+    ```
+
+#### ClientIP
 
-    One can invert a matcher by using the `!` operator.
+The `ClientIP` matcher allows matching connections opened by a client with the given IP.
 
-!!! important "Rule, Middleware, and Services"
+!!! example "Examples"
 
-    The rule is evaluated "before" any middleware has the opportunity to work, and "before" the request is forwarded to the service.
+    Match connections opened by a given IP:
+    
+    ```yaml tab="IPv4"
+    ClientIP(`10.76.105.11`)
+    ```
+    
+    ```yaml tab="IPv6"
+    ClientIP(`::1`)
+    ```
+    
+    Match connections coming from a given subnet:
+    
+    ```yaml tab="IPv4"
+    ClientIP(`192.168.1.0/24`)
+    ```
 
-!!! important "ALPN ACME-TLS/1"
+    ```yaml tab="IPv6"
+    ClientIP(`fe80::/10`)
+    ```
 
-    It would be a security issue to let a user-defined router catch the response to
-    an ACME TLS challenge previously initiated by Traefik.
-    For this reason, the `ALPN` matcher is not allowed to match the `ACME-TLS/1`
-    protocol, and Traefik returns an error if this is attempted.
+#### ALPN
+
+The `ALPN` matcher allows matching connections the given protocol.
+
+It would be a security issue to let a user-defined router catch the response to
+an ACME TLS challenge previously initiated by Traefik.
+For this reason, the `ALPN` matcher is not allowed to match the `ACME-TLS/1`
+protocol, and Traefik returns an error if this is attempted.
+
+!!! example "Example"
+
+    Match connections using the ALPN protocol `h2`:
+    
+    ```yaml
+    ALPN(`h2`)
+    ```
 
 ### Priority
 
diff --git a/go.mod b/go.mod
index adf17cfd4c..86872dde85 100644
--- a/go.mod
+++ b/go.mod
@@ -77,11 +77,12 @@ require (
 	github.com/vulcand/predicate v1.2.0
 	go.elastic.co/apm v1.13.1
 	go.elastic.co/apm/module/apmot v1.13.1
-	golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4
+	golang.org/x/exp v0.0.0-20221114191408-850992195362
+	golang.org/x/mod v0.6.0
 	golang.org/x/net v0.1.0
 	golang.org/x/text v0.4.0
 	golang.org/x/time v0.0.0-20220224211638-0e9765cccd65
-	golang.org/x/tools v0.1.12
+	golang.org/x/tools v0.2.0
 	google.golang.org/grpc v1.46.0
 	gopkg.in/DataDog/dd-trace-go.v1 v1.43.1
 	gopkg.in/fsnotify.v1 v1.4.7
@@ -330,7 +331,7 @@ require (
 	go.uber.org/zap v1.18.1 // indirect
 	go4.org/intern v0.0.0-20211027215823-ae77deb06f29 // indirect
 	go4.org/unsafe/assume-no-moving-gc v0.0.0-20220617031537-928513b29760 // indirect
-	golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f // indirect
+	golang.org/x/crypto v0.1.0 // indirect
 	golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 // indirect
 	golang.org/x/oauth2 v0.0.0-20220909003341-f21342109be1 // indirect
 	golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 // indirect
diff --git a/go.sum b/go.sum
index d546c08fe6..25a719ffa1 100644
--- a/go.sum
+++ b/go.sum
@@ -2028,8 +2028,8 @@ golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5y
 golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
 golang.org/x/crypto v0.0.0-20211202192323-5770296d904e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
 golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
-golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f h1:OeJjE6G4dgCY4PIXvIRQbE8+RX+uXZyGhUy/ksMGJoc=
-golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
+golang.org/x/crypto v0.1.0 h1:MDRAIl0xIo9Io2xV565hzXHw3zVseKrJKodhohM5CjU=
+golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
 golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
@@ -2044,7 +2044,8 @@ golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u0
 golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
 golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
 golang.org/x/exp v0.0.0-20200331195152-e8c3332aa8e5/go.mod h1:4M0jN8W1tt0AVLNr8HDosyJCDCDuyL9N9+3m7wDWgKw=
-golang.org/x/exp v0.0.0-20200908183739-ae8ad444f925 h1:5XVKs2rlCg8EFyRcvO8/XFwYxh1oKJO1Q3X5vttIf9c=
+golang.org/x/exp v0.0.0-20221114191408-850992195362 h1:NoHlPRbyl1VFI6FjwHtPQCN7wAMXI6cKcqrmXhOOfBQ=
+golang.org/x/exp v0.0.0-20221114191408-850992195362/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
 golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs=
 golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
 golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
@@ -2074,8 +2075,8 @@ golang.org/x/mod v0.3.1-0.20200828183125-ce943fd02449/go.mod h1:s0Qsj1ACt9ePp/hM
 golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
-golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s=
-golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
+golang.org/x/mod v0.6.0 h1:b9gGHsz9/HhJ3HF5DHQytPpuwocVTChQJK3AvoLRD5I=
+golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI=
 golang.org/x/net v0.0.0-20170114055629-f2499483f923/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20180811021610-c39426892332/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -2417,8 +2418,8 @@ golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
 golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
 golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
 golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
-golang.org/x/tools v0.1.12 h1:VveCTK38A2rkS8ZqFY25HIDFscX5X9OoEhJd3quQmXU=
-golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
+golang.org/x/tools v0.2.0 h1:G6AHpWxTMGY1KyEYoAQ5WTtIekUUvDNjan3ugu60JvE=
+golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
diff --git a/integration/fixtures/https/https_sni_case_insensitive_dynamic.toml b/integration/fixtures/https/https_sni_case_insensitive_dynamic.toml
index 718477cb3b..77c87a5193 100644
--- a/integration/fixtures/https/https_sni_case_insensitive_dynamic.toml
+++ b/integration/fixtures/https/https_sni_case_insensitive_dynamic.toml
@@ -20,12 +20,12 @@
 
 [http.routers]
   [http.routers.router1]
-    rule = "HostRegexp(`{subdomain:[a-z1-9-]+}.snitest.com`)"
+    rule = "HostRegexp(`[a-z1-9-]+\\.snitest\\.com`)"
     service = "service1"
     [http.routers.router1.tls]
 
   [http.routers.router2]
-    rule = "HostRegexp(`{subdomain:[a-z1-9-]+}.www.snitest.com`)"
+    rule = "HostRegexp(`[a-z1-9-]+\\.www\\.snitest\\.com`)"
     service = "service1"
     [http.routers.router2.tls]
 
diff --git a/integration/fixtures/simple_muxer.toml b/integration/fixtures/simple_muxer.toml
index 9ec55ebdcc..92a86edb43 100644
--- a/integration/fixtures/simple_muxer.toml
+++ b/integration/fixtures/simple_muxer.toml
@@ -4,6 +4,7 @@
 
 [log]
   level = "DEBUG"
+  noColor = true
 
 [entryPoints]
   [entryPoints.webHost]
@@ -30,12 +31,12 @@
   [http.routers.router2]
     entryPoints = ["webHostRegexp"]
     service = "service1"
-    rule = "!HostRegexp(`test.localhost`)"
+    rule = "!HostRegexp(`test\\.localhost`)"
 
   [http.routers.router3]
     entryPoints = ["webQuery"]
     service = "service1"
-    rule = "!Query(`foo=`)"
+    rule = "!QueryRegexp(`foo`, `.*`)"
 
 
 [http.services]
diff --git a/integration/fixtures/websocket/config_https.toml b/integration/fixtures/websocket/config_https.toml
index cdc92b8624..71f0f5465b 100644
--- a/integration/fixtures/websocket/config_https.toml
+++ b/integration/fixtures/websocket/config_https.toml
@@ -24,7 +24,7 @@
 [http.routers]
   [http.routers.router1]
     service = "service1"
-    rule = "Path(`/echo`,`/ws`)"
+    rule = "Path(`/echo`) || Path(`/ws`)"
     [http.routers.router1.tls]
 
 [http.services]
diff --git a/pkg/muxer/http/matcher.go b/pkg/muxer/http/matcher.go
new file mode 100644
index 0000000000..ceafbd6b49
--- /dev/null
+++ b/pkg/muxer/http/matcher.go
@@ -0,0 +1,253 @@
+package http
+
+import (
+	"fmt"
+	"net/http"
+	"regexp"
+	"strings"
+	"unicode/utf8"
+
+	"github.com/gorilla/mux"
+	"github.com/rs/zerolog/log"
+	"github.com/traefik/traefik/v2/pkg/ip"
+	"github.com/traefik/traefik/v2/pkg/middlewares/requestdecorator"
+	"golang.org/x/exp/slices"
+)
+
+var httpFuncs = map[string]func(*mux.Route, ...string) error{
+	"ClientIP":     expectNParameters(clientIP, 1),
+	"Method":       expectNParameters(method, 1),
+	"Host":         expectNParameters(host, 1),
+	"HostRegexp":   expectNParameters(hostRegexp, 1),
+	"Path":         expectNParameters(path, 1),
+	"PathRegexp":   expectNParameters(pathRegexp, 1),
+	"PathPrefix":   expectNParameters(pathPrefix, 1),
+	"Header":       expectNParameters(header, 2),
+	"HeaderRegexp": expectNParameters(headerRegexp, 2),
+	"Query":        expectNParameters(query, 1, 2),
+	"QueryRegexp":  expectNParameters(queryRegexp, 1, 2),
+}
+
+func expectNParameters(fn func(*mux.Route, ...string) error, n ...int) func(*mux.Route, ...string) error {
+	return func(route *mux.Route, s ...string) error {
+		if !slices.Contains(n, len(s)) {
+			return fmt.Errorf("unexpected number of parameters; got %d, expected one of %v", len(s), n)
+		}
+
+		return fn(route, s...)
+	}
+}
+
+func clientIP(route *mux.Route, clientIP ...string) error {
+	checker, err := ip.NewChecker(clientIP)
+	if err != nil {
+		return fmt.Errorf("initializing IP checker for ClientIP matcher: %w", err)
+	}
+
+	strategy := ip.RemoteAddrStrategy{}
+
+	route.MatcherFunc(func(req *http.Request, _ *mux.RouteMatch) bool {
+		ok, err := checker.Contains(strategy.GetIP(req))
+		if err != nil {
+			log.Ctx(req.Context()).Warn().Err(err).Msg("ClientIP matcher: could not match remote address")
+			return false
+		}
+
+		return ok
+	})
+
+	return nil
+}
+
+func method(route *mux.Route, methods ...string) error {
+	return route.Methods(methods...).GetError()
+}
+
+func host(route *mux.Route, hosts ...string) error {
+	host := hosts[0]
+
+	if !IsASCII(host) {
+		return fmt.Errorf("invalid value %q for Host matcher, non-ASCII characters are not allowed", host)
+	}
+
+	host = strings.ToLower(host)
+
+	route.MatcherFunc(func(req *http.Request, _ *mux.RouteMatch) bool {
+		reqHost := requestdecorator.GetCanonizedHost(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) {
+				log.Ctx(req.Context()).Warn().Str("host", req.Host).Msg("Could not retrieve CanonizedHost, rejecting")
+			}
+
+			return false
+		}
+
+		flatH := requestdecorator.GetCNAMEFlatten(req.Context())
+		if len(flatH) > 0 {
+			if strings.EqualFold(reqHost, host) || strings.EqualFold(flatH, host) {
+				return true
+			}
+
+			log.Ctx(req.Context()).Debug().
+				Str("host", reqHost).
+				Str("flattenHost", flatH).
+				Str("matcher", host).
+				Msg("CNAMEFlattening: resolved Host does not match")
+			return false
+		}
+
+		if reqHost == host {
+			return true
+		}
+
+		// Check for match on trailing period on host
+		if last := len(host) - 1; last >= 0 && host[last] == '.' {
+			h := host[:last]
+			if reqHost == h {
+				return true
+			}
+		}
+
+		// Check for match on trailing period on request
+		if last := len(reqHost) - 1; last >= 0 && reqHost[last] == '.' {
+			h := reqHost[:last]
+			if h == host {
+				return true
+			}
+		}
+
+		return false
+	})
+
+	return nil
+}
+
+func hostRegexp(route *mux.Route, hosts ...string) error {
+	host := hosts[0]
+
+	if !IsASCII(host) {
+		return fmt.Errorf("invalid value %q for HostRegexp matcher, non-ASCII characters are not allowed", host)
+	}
+
+	re, err := regexp.Compile(host)
+	if err != nil {
+		return fmt.Errorf("compiling HostRegexp matcher: %w", err)
+	}
+
+	route.MatcherFunc(func(req *http.Request, _ *mux.RouteMatch) bool {
+		return re.MatchString(req.Host)
+	})
+
+	return nil
+}
+
+func path(route *mux.Route, paths ...string) error {
+	path := paths[0]
+
+	if !strings.HasPrefix(path, "/") {
+		return fmt.Errorf("path %q does not start with a '/'", path)
+	}
+
+	route.MatcherFunc(func(req *http.Request, _ *mux.RouteMatch) bool {
+		return req.URL.Path == path
+	})
+
+	return nil
+}
+
+func pathRegexp(route *mux.Route, paths ...string) error {
+	path := paths[0]
+
+	re, err := regexp.Compile(path)
+	if err != nil {
+		return fmt.Errorf("compiling PathPrefix matcher: %w", err)
+	}
+
+	route.MatcherFunc(func(req *http.Request, _ *mux.RouteMatch) bool {
+		return re.MatchString(req.URL.Path)
+	})
+
+	return nil
+}
+
+func pathPrefix(route *mux.Route, paths ...string) error {
+	path := paths[0]
+
+	if !strings.HasPrefix(path, "/") {
+		return fmt.Errorf("path %q does not start with a '/'", path)
+	}
+
+	route.MatcherFunc(func(req *http.Request, _ *mux.RouteMatch) bool {
+		return strings.HasPrefix(req.URL.Path, path)
+	})
+
+	return nil
+}
+
+func header(route *mux.Route, headers ...string) error {
+	return route.Headers(headers...).GetError()
+}
+
+func headerRegexp(route *mux.Route, headers ...string) error {
+	return route.HeadersRegexp(headers...).GetError()
+}
+
+func query(route *mux.Route, queries ...string) error {
+	key := queries[0]
+
+	var value string
+	if len(queries) == 2 {
+		value = queries[1]
+	}
+
+	route.MatcherFunc(func(req *http.Request, _ *mux.RouteMatch) bool {
+		values, ok := req.URL.Query()[key]
+		if !ok {
+			return false
+		}
+
+		return slices.Contains(values, value)
+	})
+
+	return nil
+}
+
+func queryRegexp(route *mux.Route, queries ...string) error {
+	if len(queries) == 1 {
+		return query(route, queries...)
+	}
+
+	key, value := queries[0], queries[1]
+
+	re, err := regexp.Compile(value)
+	if err != nil {
+		return fmt.Errorf("compiling QueryRegexp matcher: %w", err)
+	}
+
+	route.MatcherFunc(func(req *http.Request, _ *mux.RouteMatch) bool {
+		values, ok := req.URL.Query()[key]
+		if !ok {
+			return false
+		}
+
+		idx := slices.IndexFunc(values, func(value string) bool {
+			return re.MatchString(value)
+		})
+
+		return idx >= 0
+	})
+
+	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/muxer/http/mux.go b/pkg/muxer/http/mux.go
index 16f6e69fe7..2379770445 100644
--- a/pkg/muxer/http/mux.go
+++ b/pkg/muxer/http/mux.go
@@ -3,32 +3,12 @@ package http
 import (
 	"fmt"
 	"net/http"
-	"strings"
-	"unicode/utf8"
 
 	"github.com/gorilla/mux"
-	"github.com/rs/zerolog/log"
-	"github.com/traefik/traefik/v2/pkg/ip"
-	"github.com/traefik/traefik/v2/pkg/middlewares/requestdecorator"
 	"github.com/traefik/traefik/v2/pkg/rules"
 	"github.com/vulcand/predicate"
 )
 
-const hostMatcher = "Host"
-
-var httpFuncs = map[string]func(*mux.Route, ...string) error{
-	hostMatcher:     host,
-	"HostHeader":    host,
-	"HostRegexp":    hostRegexp,
-	"ClientIP":      clientIP,
-	"Path":          path,
-	"PathPrefix":    pathPrefix,
-	"Method":        methods,
-	"Headers":       headers,
-	"HeadersRegexp": headersRegexp,
-	"Query":         query,
-}
-
 // Muxer handles routing with rules.
 type Muxer struct {
 	*mux.Router
@@ -80,171 +60,6 @@ func (r *Muxer) AddRoute(rule string, priority int, handler http.Handler) error
 	return nil
 }
 
-// ParseDomains extract domains from rule.
-func ParseDomains(rule string) ([]string, error) {
-	var matchers []string
-	for matcher := range httpFuncs {
-		matchers = append(matchers, matcher)
-	}
-
-	parser, err := rules.NewParser(matchers)
-	if err != nil {
-		return nil, err
-	}
-
-	parse, err := parser.Parse(rule)
-	if err != nil {
-		return nil, err
-	}
-
-	buildTree, ok := parse.(rules.TreeBuilder)
-	if !ok {
-		return nil, fmt.Errorf("error while parsing rule %s", rule)
-	}
-
-	return buildTree().ParseMatchers([]string{hostMatcher}), nil
-}
-
-func path(route *mux.Route, paths ...string) error {
-	rt := route.Subrouter()
-
-	for _, path := range paths {
-		if err := rt.Path(path).GetError(); err != nil {
-			return err
-		}
-	}
-
-	return nil
-}
-
-func pathPrefix(route *mux.Route, paths ...string) error {
-	rt := route.Subrouter()
-
-	for _, path := range paths {
-		if err := rt.PathPrefix(path).GetError(); err != nil {
-			return err
-		}
-	}
-
-	return nil
-}
-
-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)
-	}
-
-	route.MatcherFunc(func(req *http.Request, _ *mux.RouteMatch) bool {
-		reqHost := requestdecorator.GetCanonizedHost(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) {
-				log.Ctx(req.Context()).Warn().Msgf("Could not retrieve CanonizedHost, rejecting %s", req.Host)
-			}
-
-			return false
-		}
-
-		flatH := requestdecorator.GetCNAMEFlatten(req.Context())
-		if len(flatH) > 0 {
-			for _, host := range hosts {
-				if strings.EqualFold(reqHost, host) || strings.EqualFold(flatH, host) {
-					return true
-				}
-				log.Ctx(req.Context()).Debug().Msgf("CNAMEFlattening: request %s which resolved to %s, is not matched to route %s", reqHost, flatH, host)
-			}
-			return false
-		}
-
-		for _, host := range hosts {
-			if reqHost == host {
-				return true
-			}
-
-			// Check for match on trailing period on host
-			if last := len(host) - 1; last >= 0 && host[last] == '.' {
-				h := host[:last]
-				if reqHost == h {
-					return true
-				}
-			}
-
-			// Check for match on trailing period on request
-			if last := len(reqHost) - 1; last >= 0 && reqHost[last] == '.' {
-				h := reqHost[:last]
-				if h == host {
-					return true
-				}
-			}
-		}
-		return false
-	})
-	return nil
-}
-
-func clientIP(route *mux.Route, clientIPs ...string) error {
-	checker, err := ip.NewChecker(clientIPs)
-	if err != nil {
-		return fmt.Errorf("could not initialize IP Checker for \"ClientIP\" matcher: %w", err)
-	}
-
-	strategy := ip.RemoteAddrStrategy{}
-
-	route.MatcherFunc(func(req *http.Request, _ *mux.RouteMatch) bool {
-		ok, err := checker.Contains(strategy.GetIP(req))
-		if err != nil {
-			log.Ctx(req.Context()).Warn().Err(err).Msg("\"ClientIP\" matcher: could not match remote address")
-			return false
-		}
-
-		return ok
-	})
-
-	return nil
-}
-
-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()
-		}
-	}
-	return nil
-}
-
-func methods(route *mux.Route, methods ...string) error {
-	return route.Methods(methods...).GetError()
-}
-
-func headers(route *mux.Route, headers ...string) error {
-	return route.Headers(headers...).GetError()
-}
-
-func headersRegexp(route *mux.Route, headers ...string) error {
-	return route.HeadersRegexp(headers...).GetError()
-}
-
-func query(route *mux.Route, query ...string) error {
-	var queries []string
-	for _, elem := range query {
-		queries = append(queries, strings.SplitN(elem, "=", 2)...)
-	}
-
-	route.Queries(queries...)
-	// Queries can return nil so we can't chain the GetError()
-	return route.GetError()
-}
-
 func addRuleOnRouter(router *mux.Router, rule *rules.Tree) error {
 	switch rule.Matcher {
 	case "and":
@@ -276,20 +91,6 @@ func addRuleOnRouter(router *mux.Router, rule *rules.Tree) error {
 	}
 }
 
-func not(m func(*mux.Route, ...string) error) func(*mux.Route, ...string) error {
-	return func(r *mux.Route, v ...string) error {
-		router := mux.NewRouter()
-		err := m(router.NewRoute(), v...)
-		if err != nil {
-			return err
-		}
-		r.MatcherFunc(func(req *http.Request, ma *mux.RouteMatch) bool {
-			return !router.Match(req, ma)
-		})
-		return nil
-	}
-}
-
 func addRuleOnRoute(route *mux.Route, rule *rules.Tree) error {
 	switch rule.Matcher {
 	case "and":
@@ -322,13 +123,44 @@ func addRuleOnRoute(route *mux.Route, rule *rules.Tree) error {
 	}
 }
 
-// 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
+func not(m func(*mux.Route, ...string) error) func(*mux.Route, ...string) error {
+	return func(r *mux.Route, v ...string) error {
+		router := mux.NewRouter()
+
+		err := m(router.NewRoute(), v...)
+		if err != nil {
+			return err
 		}
+
+		r.MatcherFunc(func(req *http.Request, ma *mux.RouteMatch) bool {
+			return !router.Match(req, ma)
+		})
+
+		return nil
+	}
+}
+
+// ParseDomains extract domains from rule.
+func ParseDomains(rule string) ([]string, error) {
+	var matchers []string
+	for matcher := range httpFuncs {
+		matchers = append(matchers, matcher)
+	}
+
+	parser, err := rules.NewParser(matchers)
+	if err != nil {
+		return nil, err
+	}
+
+	parse, err := parser.Parse(rule)
+	if err != nil {
+		return nil, err
+	}
+
+	buildTree, ok := parse.(rules.TreeBuilder)
+	if !ok {
+		return nil, fmt.Errorf("error while parsing rule %s", rule)
 	}
 
-	return true
+	return buildTree().ParseMatchers([]string{"Host"}), nil
 }
diff --git a/pkg/muxer/tcp/matcher.go b/pkg/muxer/tcp/matcher.go
new file mode 100644
index 0000000000..40694c3eae
--- /dev/null
+++ b/pkg/muxer/tcp/matcher.go
@@ -0,0 +1,134 @@
+package tcp
+
+import (
+	"fmt"
+	"regexp"
+	"strings"
+	"unicode/utf8"
+
+	"github.com/go-acme/lego/v4/challenge/tlsalpn01"
+	"github.com/rs/zerolog/log"
+	"github.com/traefik/traefik/v2/pkg/ip"
+)
+
+var tcpFuncs = map[string]func(*matchersTree, ...string) error{
+	"ALPN":          expect1Parameter(alpn),
+	"ClientIP":      expect1Parameter(clientIP),
+	"HostSNI":       expect1Parameter(hostSNI),
+	"HostSNIRegexp": expect1Parameter(hostSNIRegexp),
+}
+
+func expect1Parameter(fn func(*matchersTree, ...string) error) func(*matchersTree, ...string) error {
+	return func(route *matchersTree, s ...string) error {
+		if len(s) != 1 {
+			return fmt.Errorf("unexpected number of parameters; got %d, expected 1", len(s))
+		}
+
+		return fn(route, s...)
+	}
+}
+
+// alpn checks if any of the connection ALPN protocols matches one of the matcher protocols.
+func alpn(tree *matchersTree, protos ...string) error {
+	proto := protos[0]
+
+	if proto == tlsalpn01.ACMETLS1Protocol {
+		return fmt.Errorf("invalid protocol value for ALPN matcher, %q is not allowed", proto)
+	}
+
+	tree.matcher = func(meta ConnData) bool {
+		for _, alpnProto := range meta.alpnProtos {
+			if alpnProto == proto {
+				return true
+			}
+		}
+
+		return false
+	}
+
+	return nil
+}
+
+func clientIP(tree *matchersTree, clientIP ...string) error {
+	checker, err := ip.NewChecker(clientIP)
+	if err != nil {
+		return fmt.Errorf("initializing IP checker for ClientIP matcher: %w", err)
+	}
+
+	tree.matcher = func(meta ConnData) bool {
+		ok, err := checker.Contains(meta.remoteIP)
+		if err != nil {
+			log.Warn().Err(err).Msg("ClientIP matcher: could not match remote address")
+			return false
+		}
+		return ok
+	}
+
+	return nil
+}
+
+var almostFQDN = regexp.MustCompile(`^[[:alnum:]\.-]+$`)
+
+// hostSNI checks if the SNI Host of the connection match the matcher host.
+func hostSNI(tree *matchersTree, hosts ...string) error {
+	host := hosts[0]
+
+	if host == "*" {
+		// 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 !almostFQDN.MatchString(host) {
+		return fmt.Errorf("invalid value for HostSNI matcher, %q is not a valid hostname", host)
+	}
+
+	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 nil
+}
+
+// hostSNIRegexp checks if the SNI Host of the connection matches the matcher host regexp.
+func hostSNIRegexp(tree *matchersTree, templates ...string) error {
+	template := templates[0]
+
+	if !isASCII(template) {
+		return fmt.Errorf("invalid value for HostSNIRegexp matcher, %q is not a valid hostname", template)
+	}
+
+	re, err := regexp.Compile(template)
+	if err != nil {
+		return fmt.Errorf("compiling HostSNIRegexp matcher: %w", err)
+	}
+
+	tree.matcher = func(meta ConnData) bool {
+		return re.MatchString(meta.serverName)
+	}
+
+	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/muxer/tcp/mux.go b/pkg/muxer/tcp/mux.go
index 6d74211192..3c5abffaf7 100644
--- a/pkg/muxer/tcp/mux.go
+++ b/pkg/muxer/tcp/mux.go
@@ -1,31 +1,18 @@
 package tcp
 
 import (
-	"bytes"
-	"errors"
 	"fmt"
 	"net"
-	"regexp"
 	"sort"
-	"strconv"
 	"strings"
 
-	"github.com/go-acme/lego/v4/challenge/tlsalpn01"
 	"github.com/rs/zerolog/log"
-	"github.com/traefik/traefik/v2/pkg/ip"
 	"github.com/traefik/traefik/v2/pkg/rules"
 	"github.com/traefik/traefik/v2/pkg/tcp"
 	"github.com/traefik/traefik/v2/pkg/types"
 	"github.com/vulcand/predicate"
 )
 
-var tcpFuncs = map[string]func(*matchersTree, ...string) error{
-	"HostSNI":       hostSNI,
-	"HostSNIRegexp": hostSNIRegexp,
-	"ClientIP":      clientIP,
-	"ALPN":          alpn,
-}
-
 // ParseHostSNI extracts the HostSNIs declared in a rule.
 // This is a first naive implementation used in TCP routing.
 func ParseHostSNI(rule string) ([]string, error) {
@@ -261,233 +248,7 @@ func (m *matchersTree) match(meta ConnData) bool {
 		return m.left.match(meta) && m.right.match(meta)
 	default:
 		// This should never happen as it should have been detected during parsing.
-		log.Warn().Msgf("Invalid rule operator %s", m.operator)
+		log.Warn().Str("operator", m.operator).Msg("Invalid rule operator")
 		return false
 	}
 }
-
-func clientIP(tree *matchersTree, clientIPs ...string) error {
-	checker, err := ip.NewChecker(clientIPs)
-	if err != nil {
-		return fmt.Errorf("could not initialize IP Checker for \"ClientIP\" matcher: %w", err)
-	}
-
-	tree.matcher = func(meta ConnData) bool {
-		if meta.remoteIP == "" {
-			return false
-		}
-
-		ok, err := checker.Contains(meta.remoteIP)
-		if err != nil {
-			log.Warn().Err(err).Msg("\"ClientIP\" matcher: could not match remote address")
-			return false
-		}
-		return ok
-	}
-
-	return nil
-}
-
-// alpn checks if any of the connection ALPN protocols matches one of the matcher protocols.
-func alpn(tree *matchersTree, protos ...string) error {
-	if len(protos) == 0 {
-		return errors.New("empty value for \"ALPN\" matcher is not allowed")
-	}
-
-	for _, proto := range protos {
-		if proto == tlsalpn01.ACMETLS1Protocol {
-			return fmt.Errorf("invalid protocol value for \"ALPN\" matcher, %q is not allowed", proto)
-		}
-	}
-
-	tree.matcher = func(meta ConnData) bool {
-		for _, proto := range meta.alpnProtos {
-			for _, filter := range protos {
-				if proto == filter {
-					return true
-				}
-			}
-		}
-
-		return false
-	}
-
-	return nil
-}
-
-var almostFQDN = regexp.MustCompile(`^[[:alnum:]\.-]+$`)
-
-// hostSNI checks if the SNI Host of the connection match the matcher host.
-func hostSNI(tree *matchersTree, hosts ...string) error {
-	if len(hosts) == 0 {
-		return errors.New("empty value for \"HostSNI\" matcher is not allowed")
-	}
-
-	for i, host := range hosts {
-		// Special case to allow global wildcard
-		if host == "*" {
-			continue
-		}
-
-		if !almostFQDN.MatchString(host) {
-			return fmt.Errorf("invalid value for \"HostSNI\" matcher, %q is not a valid hostname", host)
-		}
-
-		hosts[i] = strings.ToLower(host)
-	}
-
-	tree.matcher = func(meta ConnData) bool {
-		// Since a HostSNI(`*`) rule has been provided as catchAll for non-TLS TCP,
-		// it allows matching with an empty serverName.
-		// Which is why we make sure to take that case into account before
-		// checking meta.serverName.
-		if hosts[0] == "*" {
-			return true
-		}
-
-		if meta.serverName == "" {
-			return false
-		}
-
-		for _, host := range hosts {
-			if host == "*" {
-				return true
-			}
-
-			if host == meta.serverName {
-				return true
-			}
-
-			// trim trailing period in case of FQDN
-			host = strings.TrimSuffix(host, ".")
-			if host == meta.serverName {
-				return true
-			}
-		}
-
-		return false
-	}
-
-	return nil
-}
-
-// hostSNIRegexp checks if the SNI Host of the connection matches the matcher host regexp.
-func hostSNIRegexp(tree *matchersTree, templates ...string) error {
-	if len(templates) == 0 {
-		return fmt.Errorf("empty value for \"HostSNIRegexp\" matcher is not allowed")
-	}
-
-	var regexps []*regexp.Regexp
-
-	for _, template := range templates {
-		preparedPattern, err := preparePattern(template)
-		if err != nil {
-			return fmt.Errorf("invalid pattern value for \"HostSNIRegexp\" matcher, %q is not a valid pattern: %w", template, err)
-		}
-
-		regexp, err := regexp.Compile(preparedPattern)
-		if err != nil {
-			return err
-		}
-
-		regexps = append(regexps, regexp)
-	}
-
-	tree.matcher = func(meta ConnData) bool {
-		for _, regexp := range regexps {
-			if regexp.MatchString(meta.serverName) {
-				return true
-			}
-		}
-
-		return false
-	}
-
-	return nil
-}
-
-// TODO: expose more of containous/mux fork to get rid of the following copied code (https://github.com/containous/mux/blob/8ffa4f6d063c/regexp.go).
-
-// preparePattern builds a regexp pattern from the initial user defined expression.
-// This function reuses the code dedicated to host matching of the newRouteRegexp func from the gorilla/mux library.
-// https://github.com/containous/mux/tree/8ffa4f6d063c1e2b834a73be6a1515cca3992618.
-func preparePattern(template string) (string, error) {
-	// Check if it is well-formed.
-	idxs, errBraces := braceIndices(template)
-	if errBraces != nil {
-		return "", errBraces
-	}
-
-	defaultPattern := "[^.]+"
-	pattern := bytes.NewBufferString("")
-
-	// Host SNI matching is case-insensitive
-	fmt.Fprint(pattern, "(?i)")
-
-	pattern.WriteByte('^')
-	var end int
-	var err error
-	for i := 0; i < len(idxs); i += 2 {
-		// Set all values we are interested in.
-		raw := template[end:idxs[i]]
-		end = idxs[i+1]
-		parts := strings.SplitN(template[idxs[i]+1:end-1], ":", 2)
-		name := parts[0]
-		patt := defaultPattern
-		if len(parts) == 2 {
-			patt = parts[1]
-		}
-		// Name or pattern can't be empty.
-		if name == "" || patt == "" {
-			return "", fmt.Errorf("mux: missing name or pattern in %q",
-				template[idxs[i]:end])
-		}
-		// Build the regexp pattern.
-		fmt.Fprintf(pattern, "%s(?P<%s>%s)", regexp.QuoteMeta(raw), varGroupName(i/2), patt)
-
-		// Append variable name and compiled pattern.
-		if err != nil {
-			return "", err
-		}
-	}
-
-	// Add the remaining.
-	raw := template[end:]
-	pattern.WriteString(regexp.QuoteMeta(raw))
-	pattern.WriteByte('$')
-
-	return pattern.String(), nil
-}
-
-// varGroupName builds a capturing group name for the indexed variable.
-// This function is a copy of varGroupName func from the gorilla/mux library.
-// https://github.com/containous/mux/tree/8ffa4f6d063c1e2b834a73be6a1515cca3992618.
-func varGroupName(idx int) string {
-	return "v" + strconv.Itoa(idx)
-}
-
-// braceIndices returns the first level curly brace indices from a string.
-// This function is a copy of braceIndices func from the gorilla/mux library.
-// https://github.com/containous/mux/tree/8ffa4f6d063c1e2b834a73be6a1515cca3992618.
-func braceIndices(s string) ([]int, error) {
-	var level, idx int
-	var idxs []int
-	for i := 0; i < len(s); i++ {
-		switch s[i] {
-		case '{':
-			if level++; level == 1 {
-				idx = i
-			}
-		case '}':
-			if level--; level == 0 {
-				idxs = append(idxs, idx, i+1)
-			} else if level < 0 {
-				return nil, fmt.Errorf("mux: unbalanced braces in %q", s)
-			}
-		}
-	}
-	if level != 0 {
-		return nil, fmt.Errorf("mux: unbalanced braces in %q", s)
-	}
-	return idxs, nil
-}
diff --git a/pkg/provider/hub/hub.go b/pkg/provider/hub/hub.go
index 07fc50d303..c44cb7e972 100644
--- a/pkg/provider/hub/hub.go
+++ b/pkg/provider/hub/hub.go
@@ -101,7 +101,7 @@ func patchDynamicConfiguration(cfg *dynamic.Configuration, ep string, port int,
 	cfg.HTTP.Routers["traefik-hub-agent-service"] = &dynamic.Router{
 		EntryPoints: []string{ep},
 		Service:     "traefik-hub-agent-service",
-		Rule:        "Host(`proxy.traefik`) && PathPrefix(`/config`, `/discover-ip`, `/state`)",
+		Rule:        "Host(`proxy.traefik`) && (PathPrefix(`/config`) || PathPrefix(`/discover-ip`) || PathPrefix(`/state`))",
 	}
 
 	cfg.HTTP.Services["traefik-hub-agent-service"] = &dynamic.Service{
diff --git a/pkg/provider/kubernetes/gateway/kubernetes.go b/pkg/provider/kubernetes/gateway/kubernetes.go
index 7bc3dc754d..f06bdac9b4 100644
--- a/pkg/provider/kubernetes/gateway/kubernetes.go
+++ b/pkg/provider/kubernetes/gateway/kubernetes.go
@@ -7,6 +7,7 @@ import (
 	"fmt"
 	"net"
 	"os"
+	"regexp"
 	"sort"
 	"strconv"
 	"strings"
@@ -1163,8 +1164,7 @@ func getRouteBindingSelectorNamespace(client Client, gatewayNamespace string, ro
 }
 
 func hostRule(hostnames []v1alpha2.Hostname) (string, error) {
-	var hostNames []string
-	var hostRegexNames []string
+	var rules []string
 
 	for _, hostname := range hostnames {
 		host := string(hostname)
@@ -1177,7 +1177,7 @@ func hostRule(hostnames []v1alpha2.Hostname) (string, error) {
 
 		wildcard := strings.Count(host, "*")
 		if wildcard == 0 {
-			hostNames = append(hostNames, host)
+			rules = append(rules, fmt.Sprintf("Host(`%s`)", host))
 			continue
 		}
 
@@ -1186,25 +1186,18 @@ func hostRule(hostnames []v1alpha2.Hostname) (string, error) {
 			return "", fmt.Errorf("invalid rule: %q", host)
 		}
 
-		hostRegexNames = append(hostRegexNames, strings.Replace(host, "*.", "{subdomain:[a-zA-Z0-9-]+}.", 1))
+		host = strings.Replace(regexp.QuoteMeta(host), `\*\.`, `[a-zA-Z0-9-]+\.`, 1)
+		rules = append(rules, fmt.Sprintf("HostRegexp(`^%s$`)", host))
 	}
 
-	var res string
-	if len(hostNames) > 0 {
-		res = "Host(`" + strings.Join(hostNames, "`, `") + "`)"
-	}
-
-	if len(hostRegexNames) == 0 {
-		return res, nil
-	}
-
-	hostRegexp := "HostRegexp(`" + strings.Join(hostRegexNames, "`, `") + "`)"
-
-	if len(res) > 0 {
-		return "(" + res + " || " + hostRegexp + ")", nil
+	switch len(rules) {
+	case 0:
+		return "", nil
+	case 1:
+		return rules[0], nil
+	default:
+		return fmt.Sprintf("(%s)", strings.Join(rules, " || ")), nil
 	}
-
-	return hostRegexp, nil
 }
 
 func hostSNIRule(hostnames []v1alpha2.Hostname) (string, error) {
@@ -1227,15 +1220,18 @@ func hostSNIRule(hostnames []v1alpha2.Hostname) (string, error) {
 			return "", fmt.Errorf("wildcard hostname is not supported: %q", h)
 		}
 
-		matchers = append(matchers, "`"+h+"`")
+		matchers = append(matchers, fmt.Sprintf("HostSNI(`%s`)", h))
 		uniqHostnames[hostname] = struct{}{}
 	}
 
-	if len(matchers) == 0 {
+	switch len(matchers) {
+	case 0:
 		return "HostSNI(`*`)", nil
+	case 1:
+		return matchers[0], nil
+	default:
+		return fmt.Sprintf("(%s)", strings.Join(matchers, " || ")), nil
 	}
-
-	return "HostSNI(" + strings.Join(matchers, ",") + ")", nil
 }
 
 func extractRule(routeRule v1alpha2.HTTPRouteRule, hostRule string) (string, error) {
diff --git a/pkg/provider/kubernetes/ingress/kubernetes.go b/pkg/provider/kubernetes/ingress/kubernetes.go
index 502c7cbe9b..206f37d504 100644
--- a/pkg/provider/kubernetes/ingress/kubernetes.go
+++ b/pkg/provider/kubernetes/ingress/kubernetes.go
@@ -8,6 +8,7 @@ import (
 	"math"
 	"net"
 	"os"
+	"regexp"
 	"sort"
 	"strconv"
 	"strings"
@@ -400,10 +401,11 @@ func (p *Provider) shouldProcessIngress(ingress *networkingv1.Ingress, ingressCl
 
 func buildHostRule(host string) string {
 	if strings.HasPrefix(host, "*.") {
-		return "HostRegexp(`" + strings.Replace(host, "*.", "{subdomain:[a-zA-Z0-9-]+}.", 1) + "`)"
+		host = strings.Replace(regexp.QuoteMeta(host), `\*\.`, `[a-zA-Z0-9-]+\.`, 1)
+		return fmt.Sprintf("HostRegexp(`^%s$`)", host)
 	}
 
-	return "Host(`" + host + "`)"
+	return fmt.Sprintf("Host(`%s`)", host)
 }
 
 func getCertificates(ctx context.Context, ingress *networkingv1.Ingress, k8sClient Client, tlsConfigs map[string]*tls.CertAndStores) error {
diff --git a/pkg/provider/traefik/fixtures/redirection.json b/pkg/provider/traefik/fixtures/redirection.json
index 4ffbb756c6..2b3b271fae 100644
--- a/pkg/provider/traefik/fixtures/redirection.json
+++ b/pkg/provider/traefik/fixtures/redirection.json
@@ -9,7 +9,7 @@
           "redirect-web-to-websecure"
         ],
         "service": "noop@internal",
-        "rule": "HostRegexp(`{host:.+}`)"
+        "rule": "HostRegexp(`^.+$`)"
       }
     },
     "middlewares": {
@@ -27,4 +27,4 @@
   },
   "tcp": {},
   "tls": {}
-}
\ No newline at end of file
+}
diff --git a/pkg/provider/traefik/fixtures/redirection_port.json b/pkg/provider/traefik/fixtures/redirection_port.json
index e113d21b39..ead9bc0b1f 100644
--- a/pkg/provider/traefik/fixtures/redirection_port.json
+++ b/pkg/provider/traefik/fixtures/redirection_port.json
@@ -9,7 +9,7 @@
           "redirect-web-to-443"
         ],
         "service": "noop@internal",
-        "rule": "HostRegexp(`{host:.+}`)"
+        "rule": "HostRegexp(`^.+$`)"
       }
     },
     "middlewares": {
@@ -27,4 +27,4 @@
   },
   "tcp": {},
   "tls": {}
-}
\ No newline at end of file
+}
diff --git a/pkg/provider/traefik/fixtures/redirection_with_protocol.json b/pkg/provider/traefik/fixtures/redirection_with_protocol.json
index 4ffbb756c6..2b3b271fae 100644
--- a/pkg/provider/traefik/fixtures/redirection_with_protocol.json
+++ b/pkg/provider/traefik/fixtures/redirection_with_protocol.json
@@ -9,7 +9,7 @@
           "redirect-web-to-websecure"
         ],
         "service": "noop@internal",
-        "rule": "HostRegexp(`{host:.+}`)"
+        "rule": "HostRegexp(`^.+$`)"
       }
     },
     "middlewares": {
@@ -27,4 +27,4 @@
   },
   "tcp": {},
   "tls": {}
-}
\ No newline at end of file
+}
diff --git a/pkg/provider/traefik/internal.go b/pkg/provider/traefik/internal.go
index 175ce07b92..1e1c23becc 100644
--- a/pkg/provider/traefik/internal.go
+++ b/pkg/provider/traefik/internal.go
@@ -137,7 +137,7 @@ func (i *Provider) redirection(ctx context.Context, cfg *dynamic.Configuration)
 		mdName := "redirect-" + rtName
 
 		rt := &dynamic.Router{
-			Rule:        "HostRegexp(`{host:.+}`)",
+			Rule:        "HostRegexp(`^.+$`)",
 			EntryPoints: []string{name},
 			Middlewares: []string{mdName},
 			Service:     "noop@internal",


Test patch:
diff --git a/integration/https_test.go b/integration/https_test.go
index 868d31aff5..ba80a25f00 100644
--- a/integration/https_test.go
+++ b/integration/https_test.go
@@ -1092,7 +1092,7 @@ func (s *HTTPSSuite) TestWithSNIDynamicCaseInsensitive(c *check.C) {
 	defer s.killCmd(cmd)
 
 	// wait for Traefik
-	err = try.GetRequest("http://127.0.0.1:8080/api/rawdata", 500*time.Millisecond, try.BodyContains("HostRegexp(`{subdomain:[a-z1-9-]+}.www.snitest.com`)"))
+	err = try.GetRequest("http://127.0.0.1:8080/api/rawdata", 500*time.Millisecond, try.BodyContains("HostRegexp(`[a-z1-9-]+\\\\.www\\\\.snitest\\\\.com`)"))
 	c.Assert(err, checker.IsNil)
 
 	tlsConfig := &tls.Config{
diff --git a/integration/simple_test.go b/integration/simple_test.go
index 287fe72e0b..8738699a17 100644
--- a/integration/simple_test.go
+++ b/integration/simple_test.go
@@ -1337,7 +1337,7 @@ func (s *SimpleSuite) TestMuxer(c *check.C) {
 			expected: http.StatusOK,
 		},
 		{
-			desc:     "!Query with semicolon, no match",
+			desc:     "!Query with semicolon and empty query param value, no match",
 			request:  "GET /?foo=; HTTP/1.1\r\nHost: other.localhost\r\n\r\n",
 			target:   "127.0.0.1:8002",
 			expected: http.StatusNotFound,
@@ -1367,9 +1367,7 @@ func (s *SimpleSuite) TestMuxer(c *check.C) {
 		resp, err := http.ReadResponse(bufio.NewReader(conn), nil)
 		c.Assert(err, checker.IsNil)
 
-		if resp.StatusCode != test.expected {
-			c.Errorf("%s failed with %d instead of %d", test.desc, resp.StatusCode, test.expected)
-		}
+		c.Assert(resp.StatusCode, checker.Equals, test.expected, check.Commentf(test.desc))
 
 		if test.body != "" {
 			body, err := io.ReadAll(resp.Body)
diff --git a/pkg/muxer/http/matcher_test.go b/pkg/muxer/http/matcher_test.go
new file mode 100644
index 0000000000..231d34f912
--- /dev/null
+++ b/pkg/muxer/http/matcher_test.go
@@ -0,0 +1,974 @@
+package http
+
+import (
+	"net/http"
+	"net/http/httptest"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+	"github.com/traefik/traefik/v2/pkg/middlewares/requestdecorator"
+)
+
+func TestClientIPMatcher(t *testing.T) {
+	testCases := []struct {
+		desc          string
+		rule          string
+		expected      map[string]int
+		expectedError bool
+	}{
+		{
+			desc:          "invalid ClientIP matcher",
+			rule:          "ClientIP(`1`)",
+			expectedError: true,
+		},
+		{
+			desc:          "invalid ClientIP matcher (no parameter)",
+			rule:          "ClientIP()",
+			expectedError: true,
+		},
+		{
+			desc:          "invalid ClientIP matcher (empty parameter)",
+			rule:          "ClientIP(``)",
+			expectedError: true,
+		},
+		{
+			desc:          "invalid ClientIP matcher (too many parameters)",
+			rule:          "ClientIP(`127.0.0.1`, `192.168.1.0/24`)",
+			expectedError: true,
+		},
+		{
+			desc: "valid ClientIP matcher",
+			rule: "ClientIP(`127.0.0.1`)",
+			expected: map[string]int{
+				"127.0.0.1":   http.StatusOK,
+				"192.168.1.1": http.StatusNotFound,
+			},
+		},
+		{
+			desc: "valid ClientIP matcher but invalid remote address",
+			rule: "ClientIP(`127.0.0.1`)",
+			expected: map[string]int{
+				"1": http.StatusNotFound,
+			},
+		},
+		{
+			desc: "valid ClientIP matcher using CIDR",
+			rule: "ClientIP(`192.168.1.0/24`)",
+			expected: map[string]int{
+				"192.168.1.1":   http.StatusOK,
+				"192.168.1.100": http.StatusOK,
+				"192.168.2.1":   http.StatusNotFound,
+			},
+		},
+	}
+
+	for _, test := range testCases {
+		test := test
+		t.Run(test.desc, func(t *testing.T) {
+			t.Parallel()
+
+			handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})
+			muxer, err := NewMuxer()
+			require.NoError(t, err)
+
+			err = muxer.AddRoute(test.rule, 0, handler)
+			if test.expectedError {
+				require.Error(t, err)
+				return
+			}
+
+			require.NoError(t, err)
+
+			results := make(map[string]int)
+			for remoteAddr := range test.expected {
+				w := httptest.NewRecorder()
+
+				req := httptest.NewRequest(http.MethodGet, "https://example.com", http.NoBody)
+				req.RemoteAddr = remoteAddr
+
+				muxer.ServeHTTP(w, req)
+				results[remoteAddr] = w.Code
+			}
+			assert.Equal(t, test.expected, results)
+		})
+	}
+}
+
+func TestMethodMatcher(t *testing.T) {
+	testCases := []struct {
+		desc          string
+		rule          string
+		expected      map[string]int
+		expectedError bool
+	}{
+		{
+			desc:          "invalid Method matcher (no parameter)",
+			rule:          "Method()",
+			expectedError: true,
+		},
+		{
+			desc:          "invalid Method matcher (empty parameter)",
+			rule:          "Method(``)",
+			expectedError: true,
+		},
+		{
+			desc:          "invalid Method matcher (too many parameters)",
+			rule:          "Method(`GET`, `POST`)",
+			expectedError: true,
+		},
+		{
+			desc: "valid Method matcher",
+			rule: "Method(`GET`)",
+			expected: map[string]int{
+				http.MethodGet:  http.StatusOK,
+				http.MethodPost: http.StatusMethodNotAllowed,
+			},
+		},
+		{
+			desc: "valid Method matcher (lower case)",
+			rule: "Method(`get`)",
+			expected: map[string]int{
+				http.MethodGet:  http.StatusOK,
+				http.MethodPost: http.StatusMethodNotAllowed,
+			},
+		},
+	}
+
+	for _, test := range testCases {
+		test := test
+		t.Run(test.desc, func(t *testing.T) {
+			t.Parallel()
+
+			handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})
+			muxer, err := NewMuxer()
+			require.NoError(t, err)
+
+			err = muxer.AddRoute(test.rule, 0, handler)
+			if test.expectedError {
+				require.Error(t, err)
+				return
+			}
+
+			require.NoError(t, err)
+
+			results := make(map[string]int)
+			for method := range test.expected {
+				w := httptest.NewRecorder()
+
+				req := httptest.NewRequest(method, "https://example.com", http.NoBody)
+
+				muxer.ServeHTTP(w, req)
+				results[method] = w.Code
+			}
+			assert.Equal(t, test.expected, results)
+		})
+	}
+}
+
+func TestHostMatcher(t *testing.T) {
+	testCases := []struct {
+		desc          string
+		rule          string
+		expected      map[string]int
+		expectedError bool
+	}{
+		{
+			desc:          "invalid Host matcher (no parameter)",
+			rule:          "Host()",
+			expectedError: true,
+		},
+		{
+			desc:          "invalid Host matcher (empty parameter)",
+			rule:          "Host(``)",
+			expectedError: true,
+		},
+		{
+			desc:          "invalid Host matcher (non-ASCII)",
+			rule:          "Host(`🦭.com`)",
+			expectedError: true,
+		},
+		{
+			desc:          "invalid Host matcher (too many parameters)",
+			rule:          "Host(`example.com`, `example.org`)",
+			expectedError: true,
+		},
+		{
+			desc: "valid Host matcher",
+			rule: "Host(`example.com`)",
+			expected: map[string]int{
+				"https://example.com":      http.StatusOK,
+				"https://example.com/path": http.StatusOK,
+				"https://example.org":      http.StatusNotFound,
+				"https://example.org/path": http.StatusNotFound,
+			},
+		},
+		{
+			desc: "valid Host matcher - matcher ending with a dot",
+			rule: "Host(`example.com.`)",
+			expected: map[string]int{
+				"https://example.com":       http.StatusOK,
+				"https://example.com/path":  http.StatusOK,
+				"https://example.org":       http.StatusNotFound,
+				"https://example.org/path":  http.StatusNotFound,
+				"https://example.com.":      http.StatusOK,
+				"https://example.com./path": http.StatusOK,
+				"https://example.org.":      http.StatusNotFound,
+				"https://example.org./path": http.StatusNotFound,
+			},
+		},
+		{
+			desc: "valid Host matcher - URL ending with a dot",
+			rule: "Host(`example.com`)",
+			expected: map[string]int{
+				"https://example.com.":      http.StatusOK,
+				"https://example.com./path": http.StatusOK,
+				"https://example.org.":      http.StatusNotFound,
+				"https://example.org./path": http.StatusNotFound,
+			},
+		},
+		{
+			desc: "valid Host matcher - puny-coded emoji",
+			rule: "Host(`xn--9t9h.com`)",
+			expected: map[string]int{
+				"https://xn--9t9h.com":      http.StatusOK,
+				"https://xn--9t9h.com/path": http.StatusOK,
+				"https://example.com":       http.StatusNotFound,
+				"https://example.com/path":  http.StatusNotFound,
+				// The request's sender must use puny-code.
+				"https://🦭.com": http.StatusNotFound,
+			},
+		},
+	}
+
+	for _, test := range testCases {
+		test := test
+		t.Run(test.desc, func(t *testing.T) {
+			t.Parallel()
+
+			handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})
+			muxer, err := NewMuxer()
+			require.NoError(t, err)
+
+			err = muxer.AddRoute(test.rule, 0, handler)
+			if test.expectedError {
+				require.Error(t, err)
+				return
+			}
+
+			require.NoError(t, err)
+
+			// RequestDecorator is necessary for the host rule
+			reqHost := requestdecorator.New(nil)
+
+			results := make(map[string]int)
+			for calledURL := range test.expected {
+				w := httptest.NewRecorder()
+
+				req := httptest.NewRequest(http.MethodGet, calledURL, http.NoBody)
+
+				reqHost.ServeHTTP(w, req, muxer.ServeHTTP)
+				results[calledURL] = w.Code
+			}
+			assert.Equal(t, test.expected, results)
+		})
+	}
+}
+
+func TestHostRegexpMatcher(t *testing.T) {
+	testCases := []struct {
+		desc          string
+		rule          string
+		expected      map[string]int
+		expectedError bool
+	}{
+		{
+			desc:          "invalid HostRegexp matcher (no parameter)",
+			rule:          "HostRegexp()",
+			expectedError: true,
+		},
+		{
+			desc:          "invalid HostRegexp matcher (empty parameter)",
+			rule:          "HostRegexp(``)",
+			expectedError: true,
+		},
+		{
+			desc:          "invalid HostRegexp matcher (non-ASCII)",
+			rule:          "HostRegexp(`🦭.com`)",
+			expectedError: true,
+		},
+		{
+			desc:          "invalid HostRegexp matcher (invalid regexp)",
+			rule:          "HostRegexp(`(example.com`)",
+			expectedError: true,
+		},
+		{
+			desc:          "invalid HostRegexp matcher (too many parameters)",
+			rule:          "HostRegexp(`example.com`, `example.org`)",
+			expectedError: true,
+		},
+		{
+			desc: "valid HostRegexp matcher",
+			rule: "HostRegexp(`^[a-zA-Z-]+\\.com$`)",
+			expected: map[string]int{
+				"https://example.com":      http.StatusOK,
+				"https://example.com/path": http.StatusOK,
+				"https://example.org":      http.StatusNotFound,
+				"https://example.org/path": http.StatusNotFound,
+			},
+		},
+		{
+			desc: "valid HostRegexp matcher with Traefik v2 syntax",
+			rule: "HostRegexp(`{domain:[a-zA-Z-]+\\.com}`)",
+			expected: map[string]int{
+				"https://example.com":      http.StatusNotFound,
+				"https://example.com/path": http.StatusNotFound,
+				"https://example.org":      http.StatusNotFound,
+				"https://example.org/path": http.StatusNotFound,
+			},
+		},
+	}
+
+	for _, test := range testCases {
+		test := test
+		t.Run(test.desc, func(t *testing.T) {
+			t.Parallel()
+
+			handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})
+			muxer, err := NewMuxer()
+			require.NoError(t, err)
+
+			err = muxer.AddRoute(test.rule, 0, handler)
+			if test.expectedError {
+				require.Error(t, err)
+				return
+			}
+
+			require.NoError(t, err)
+
+			results := make(map[string]int)
+			for calledURL := range test.expected {
+				w := httptest.NewRecorder()
+
+				req := httptest.NewRequest(http.MethodGet, calledURL, http.NoBody)
+
+				muxer.ServeHTTP(w, req)
+				results[calledURL] = w.Code
+			}
+			assert.Equal(t, test.expected, results)
+		})
+	}
+}
+
+func TestPathMatcher(t *testing.T) {
+	testCases := []struct {
+		desc          string
+		rule          string
+		expected      map[string]int
+		expectedError bool
+	}{
+		{
+			desc:          "invalid Path matcher (no parameter)",
+			rule:          "Path()",
+			expectedError: true,
+		},
+		{
+			desc:          "invalid Path matcher (empty parameter)",
+			rule:          "Path(``)",
+			expectedError: true,
+		},
+		{
+			desc:          "invalid Path matcher (no leading /)",
+			rule:          "Path(`css`)",
+			expectedError: true,
+		},
+		{
+			desc:          "invalid Path matcher (too many parameters)",
+			rule:          "Path(`/css`, `/js`)",
+			expectedError: true,
+		},
+		{
+			desc: "valid Path matcher",
+			rule: "Path(`/css`)",
+			expected: map[string]int{
+				"https://example.com":              http.StatusNotFound,
+				"https://example.com/html":         http.StatusNotFound,
+				"https://example.org/css":          http.StatusOK,
+				"https://example.com/css":          http.StatusOK,
+				"https://example.com/css/":         http.StatusNotFound,
+				"https://example.com/css/main.css": http.StatusNotFound,
+			},
+		},
+	}
+
+	for _, test := range testCases {
+		test := test
+		t.Run(test.desc, func(t *testing.T) {
+			t.Parallel()
+
+			handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})
+			muxer, err := NewMuxer()
+			require.NoError(t, err)
+
+			err = muxer.AddRoute(test.rule, 0, handler)
+			if test.expectedError {
+				require.Error(t, err)
+				return
+			}
+
+			require.NoError(t, err)
+
+			results := make(map[string]int)
+			for calledURL := range test.expected {
+				w := httptest.NewRecorder()
+
+				req := httptest.NewRequest(http.MethodGet, calledURL, http.NoBody)
+
+				muxer.ServeHTTP(w, req)
+				results[calledURL] = w.Code
+			}
+			assert.Equal(t, test.expected, results)
+		})
+	}
+}
+
+func TestPathRegexpMatcher(t *testing.T) {
+	testCases := []struct {
+		desc          string
+		rule          string
+		expected      map[string]int
+		expectedError bool
+	}{
+		{
+			desc:          "invalid PathRegexp matcher (no parameter)",
+			rule:          "PathRegexp()",
+			expectedError: true,
+		},
+		{
+			desc:          "invalid PathRegexp matcher (empty parameter)",
+			rule:          "PathRegexp(``)",
+			expectedError: true,
+		},
+		{
+			desc:          "invalid PathRegexp matcher (invalid regexp)",
+			rule:          "PathRegexp(`/(css`)",
+			expectedError: true,
+		},
+		{
+			desc:          "invalid PathRegexp matcher (too many parameters)",
+			rule:          "PathRegexp(`/css`, `/js`)",
+			expectedError: true,
+		},
+		{
+			desc: "valid PathRegexp matcher",
+			rule: "PathRegexp(`^/(css|js)`)",
+			expected: map[string]int{
+				"https://example.com":              http.StatusNotFound,
+				"https://example.com/html":         http.StatusNotFound,
+				"https://example.org/css":          http.StatusOK,
+				"https://example.com/CSS":          http.StatusNotFound,
+				"https://example.com/css":          http.StatusOK,
+				"https://example.com/css/":         http.StatusOK,
+				"https://example.com/css/main.css": http.StatusOK,
+				"https://example.com/js":           http.StatusOK,
+				"https://example.com/js/":          http.StatusOK,
+				"https://example.com/js/main.js":   http.StatusOK,
+			},
+		},
+		{
+			desc: "valid PathRegexp matcher with Traefik v2 syntax",
+			rule: `PathRegexp("/{path:(css|js)}")`,
+			expected: map[string]int{
+				"https://example.com":                 http.StatusNotFound,
+				"https://example.com/html":            http.StatusNotFound,
+				"https://example.org/css":             http.StatusNotFound,
+				"https://example.com/{path:css}":      http.StatusOK,
+				"https://example.com/{path:css}/":     http.StatusOK,
+				"https://example.com/%7Bpath:css%7D":  http.StatusOK,
+				"https://example.com/%7Bpath:css%7D/": http.StatusOK,
+				"https://example.com/{path:js}":       http.StatusOK,
+				"https://example.com/{path:js}/":      http.StatusOK,
+				"https://example.com/%7Bpath:js%7D":   http.StatusOK,
+				"https://example.com/%7Bpath:js%7D/":  http.StatusOK,
+			},
+		},
+	}
+
+	for _, test := range testCases {
+		test := test
+		t.Run(test.desc, func(t *testing.T) {
+			t.Parallel()
+
+			handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})
+			muxer, err := NewMuxer()
+			require.NoError(t, err)
+
+			err = muxer.AddRoute(test.rule, 0, handler)
+			if test.expectedError {
+				require.Error(t, err)
+				return
+			}
+
+			require.NoError(t, err)
+
+			results := make(map[string]int)
+			for calledURL := range test.expected {
+				w := httptest.NewRecorder()
+
+				req := httptest.NewRequest(http.MethodGet, calledURL, http.NoBody)
+
+				muxer.ServeHTTP(w, req)
+				results[calledURL] = w.Code
+			}
+			assert.Equal(t, test.expected, results)
+		})
+	}
+}
+
+func TestPathPrefixMatcher(t *testing.T) {
+	testCases := []struct {
+		desc          string
+		rule          string
+		expected      map[string]int
+		expectedError bool
+	}{
+		{
+			desc:          "invalid PathPrefix matcher (no parameter)",
+			rule:          "PathPrefix()",
+			expectedError: true,
+		},
+		{
+			desc:          "invalid PathPrefix matcher (empty parameter)",
+			rule:          "PathPrefix(``)",
+			expectedError: true,
+		},
+		{
+			desc:          "invalid PathPrefix matcher (no leading /)",
+			rule:          "PathPrefix(`css`)",
+			expectedError: true,
+		},
+		{
+			desc:          "invalid PathPrefix matcher (too many parameters)",
+			rule:          "PathPrefix(`/css`, `/js`)",
+			expectedError: true,
+		},
+		{
+			desc: "valid PathPrefix matcher",
+			rule: `PathPrefix("/css")`,
+			expected: map[string]int{
+				"https://example.com":              http.StatusNotFound,
+				"https://example.com/html":         http.StatusNotFound,
+				"https://example.org/css":          http.StatusOK,
+				"https://example.com/css":          http.StatusOK,
+				"https://example.com/css/":         http.StatusOK,
+				"https://example.com/css/main.css": http.StatusOK,
+			},
+		},
+	}
+
+	for _, test := range testCases {
+		test := test
+		t.Run(test.desc, func(t *testing.T) {
+			t.Parallel()
+
+			handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})
+			muxer, err := NewMuxer()
+			require.NoError(t, err)
+
+			err = muxer.AddRoute(test.rule, 0, handler)
+			if test.expectedError {
+				require.Error(t, err)
+				return
+			}
+
+			require.NoError(t, err)
+
+			results := make(map[string]int)
+			for calledURL := range test.expected {
+				w := httptest.NewRecorder()
+
+				req := httptest.NewRequest(http.MethodGet, calledURL, http.NoBody)
+
+				muxer.ServeHTTP(w, req)
+				results[calledURL] = w.Code
+			}
+			assert.Equal(t, test.expected, results)
+		})
+	}
+}
+
+func TestHeaderMatcher(t *testing.T) {
+	testCases := []struct {
+		desc          string
+		rule          string
+		expected      map[*http.Header]int
+		expectedError bool
+	}{
+		{
+			desc:          "invalid Header matcher (no parameter)",
+			rule:          "Header()",
+			expectedError: true,
+		},
+		{
+			desc:          "invalid Header matcher (missing value parameter)",
+			rule:          "Header(`X-Forwarded-Host`)",
+			expectedError: true,
+		},
+		{
+			desc:          "invalid Header matcher (missing value parameter)",
+			rule:          "Header(`X-Forwarded-Host`, ``)",
+			expectedError: true,
+		},
+		{
+			desc:          "invalid Header matcher (missing key parameter)",
+			rule:          "Header(``, `example.com`)",
+			expectedError: true,
+		},
+		{
+			desc:          "invalid Header matcher (too many parameters)",
+			rule:          "Header(`X-Forwarded-Host`, `example.com`, `example.org`)",
+			expectedError: true,
+		},
+		{
+			desc: "valid Header matcher",
+			rule: "Header(`X-Forwarded-Proto`, `https`)",
+			expected: map[*http.Header]int{
+				{"X-Forwarded-Proto": []string{"https"}}:         http.StatusOK,
+				{"x-forwarded-proto": []string{"https"}}:         http.StatusNotFound,
+				{"X-Forwarded-Proto": []string{"http", "https"}}: http.StatusOK,
+				{"X-Forwarded-Proto": []string{"https", "http"}}: http.StatusOK,
+				{"X-Forwarded-Host": []string{"example.com"}}:    http.StatusNotFound,
+			},
+		},
+	}
+
+	for _, test := range testCases {
+		test := test
+
+		t.Run(test.desc, func(t *testing.T) {
+			t.Parallel()
+
+			handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})
+			muxer, err := NewMuxer()
+			require.NoError(t, err)
+
+			err = muxer.AddRoute(test.rule, 0, handler)
+			if test.expectedError {
+				require.Error(t, err)
+				return
+			}
+
+			require.NoError(t, err)
+
+			for headers := range test.expected {
+				w := httptest.NewRecorder()
+
+				req := httptest.NewRequest(http.MethodGet, "https://example.com", http.NoBody)
+				req.Header = *headers
+
+				muxer.ServeHTTP(w, req)
+				assert.Equal(t, test.expected[headers], w.Code, headers)
+			}
+		})
+	}
+}
+
+func TestHeaderRegexpMatcher(t *testing.T) {
+	testCases := []struct {
+		desc          string
+		rule          string
+		expected      map[*http.Header]int
+		expectedError bool
+	}{
+		{
+			desc:          "invalid HeaderRegexp matcher (no parameter)",
+			rule:          "HeaderRegexp()",
+			expectedError: true,
+		},
+		{
+			desc:          "invalid HeaderRegexp matcher (missing value parameter)",
+			rule:          "HeaderRegexp(`X-Forwarded-Host`)",
+			expectedError: true,
+		},
+		{
+			desc:          "invalid HeaderRegexp matcher (missing value parameter)",
+			rule:          "HeaderRegexp(`X-Forwarded-Host`, ``)",
+			expectedError: true,
+		},
+		{
+			desc:          "invalid HeaderRegexp matcher (missing key parameter)",
+			rule:          "HeaderRegexp(``, `example.com`)",
+			expectedError: true,
+		},
+		{
+			desc:          "invalid HeaderRegexp matcher (invalid regexp)",
+			rule:          "HeaderRegexp(`X-Forwarded-Host`,`(example.com`)",
+			expectedError: true,
+		},
+		{
+			desc:          "invalid HeaderRegexp matcher (too many parameters)",
+			rule:          "HeaderRegexp(`X-Forwarded-Host`, `example.com`, `example.org`)",
+			expectedError: true,
+		},
+		{
+			desc: "valid HeaderRegexp matcher",
+			rule: "HeaderRegexp(`X-Forwarded-Proto`, `^https?$`)",
+			expected: map[*http.Header]int{
+				{"X-Forwarded-Proto": []string{"http"}}:        http.StatusOK,
+				{"x-forwarded-proto": []string{"http"}}:        http.StatusNotFound,
+				{"X-Forwarded-Proto": []string{"https"}}:       http.StatusOK,
+				{"X-Forwarded-Proto": []string{"HTTPS"}}:       http.StatusNotFound,
+				{"X-Forwarded-Proto": []string{"ws", "https"}}: http.StatusOK,
+				{"X-Forwarded-Host": []string{"example.com"}}:  http.StatusNotFound,
+			},
+		},
+		{
+			desc: "valid HeaderRegexp matcher with Traefik v2 syntax",
+			rule: "HeaderRegexp(`X-Forwarded-Proto`, `http{secure:s?}`)",
+			expected: map[*http.Header]int{
+				{"X-Forwarded-Proto": []string{"http"}}:                 http.StatusNotFound,
+				{"X-Forwarded-Proto": []string{"https"}}:                http.StatusNotFound,
+				{"X-Forwarded-Proto": []string{"http{secure:}"}}:        http.StatusOK,
+				{"X-Forwarded-Proto": []string{"HTTP{secure:}"}}:        http.StatusNotFound,
+				{"X-Forwarded-Proto": []string{"http{secure:s}"}}:       http.StatusOK,
+				{"X-Forwarded-Proto": []string{"http{secure:S}"}}:       http.StatusNotFound,
+				{"X-Forwarded-Proto": []string{"HTTPS"}}:                http.StatusNotFound,
+				{"X-Forwarded-Proto": []string{"ws", "http{secure:s}"}}: http.StatusOK,
+				{"X-Forwarded-Host": []string{"example.com"}}:           http.StatusNotFound,
+			},
+		},
+	}
+
+	for _, test := range testCases {
+		test := test
+		t.Run(test.desc, func(t *testing.T) {
+			t.Parallel()
+
+			handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})
+			muxer, err := NewMuxer()
+			require.NoError(t, err)
+
+			err = muxer.AddRoute(test.rule, 0, handler)
+			if test.expectedError {
+				require.Error(t, err)
+				return
+			}
+
+			require.NoError(t, err)
+
+			for headers := range test.expected {
+				w := httptest.NewRecorder()
+
+				req := httptest.NewRequest(http.MethodGet, "https://example.com", http.NoBody)
+				req.Header = *headers
+
+				muxer.ServeHTTP(w, req)
+				assert.Equal(t, test.expected[headers], w.Code, *headers)
+			}
+		})
+	}
+}
+
+func TestQueryMatcher(t *testing.T) {
+	testCases := []struct {
+		desc          string
+		rule          string
+		expected      map[string]int
+		expectedError bool
+	}{
+		{
+			desc:          "invalid Query matcher (no parameter)",
+			rule:          "Query()",
+			expectedError: true,
+		},
+		{
+			desc:          "invalid Query matcher (empty key, one parameter)",
+			rule:          "Query(``)",
+			expectedError: true,
+		},
+		{
+			desc:          "invalid Query matcher (empty key)",
+			rule:          "Query(``, `traefik`)",
+			expectedError: true,
+		},
+		{
+			desc:          "invalid Query matcher (empty value)",
+			rule:          "Query(`q`, ``)",
+			expectedError: true,
+		},
+		{
+			desc:          "invalid Query matcher (too many parameters)",
+			rule:          "Query(`q`, `traefik`, `proxy`)",
+			expectedError: true,
+		},
+		{
+			desc: "valid Query matcher",
+			rule: "Query(`q`, `traefik`)",
+			expected: map[string]int{
+				"https://example.com":                     http.StatusNotFound,
+				"https://example.com?q=traefik":           http.StatusOK,
+				"https://example.com?rel=ddg&q=traefik":   http.StatusOK,
+				"https://example.com?q=traefik&q=proxy":   http.StatusOK,
+				"https://example.com?q=awesome&q=traefik": http.StatusOK,
+				"https://example.com?q=nginx":             http.StatusNotFound,
+				"https://example.com?rel=ddg":             http.StatusNotFound,
+				"https://example.com?q=TRAEFIK":           http.StatusNotFound,
+				"https://example.com?Q=traefik":           http.StatusNotFound,
+				"https://example.com?rel=traefik":         http.StatusNotFound,
+			},
+		},
+		{
+			desc: "valid Query matcher with empty value",
+			rule: "Query(`mobile`)",
+			expected: map[string]int{
+				"https://example.com":             http.StatusNotFound,
+				"https://example.com?mobile":      http.StatusOK,
+				"https://example.com?mobile=true": http.StatusNotFound,
+			},
+		},
+	}
+
+	for _, test := range testCases {
+		test := test
+		t.Run(test.desc, func(t *testing.T) {
+			t.Parallel()
+
+			handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})
+			muxer, err := NewMuxer()
+			require.NoError(t, err)
+
+			err = muxer.AddRoute(test.rule, 0, handler)
+			if test.expectedError {
+				require.Error(t, err)
+				return
+			}
+
+			require.NoError(t, err)
+
+			results := make(map[string]int)
+			for calledURL := range test.expected {
+				w := httptest.NewRecorder()
+
+				req := httptest.NewRequest(http.MethodGet, calledURL, http.NoBody)
+
+				muxer.ServeHTTP(w, req)
+				results[calledURL] = w.Code
+			}
+			assert.Equal(t, test.expected, results)
+		})
+	}
+}
+
+func TestQueryRegexpMatcher(t *testing.T) {
+	testCases := []struct {
+		desc          string
+		rule          string
+		expected      map[string]int
+		expectedError bool
+	}{
+		{
+			desc:          "invalid QueryRegexp matcher (no parameter)",
+			rule:          "QueryRegexp()",
+			expectedError: true,
+		},
+		{
+			desc:          "invalid QueryRegexp matcher (empty parameter)",
+			rule:          "QueryRegexp(``)",
+			expectedError: true,
+		},
+		{
+			desc:          "invalid QueryRegexp matcher (invalid regexp)",
+			rule:          "QueryRegexp(`q`, `(traefik`)",
+			expectedError: true,
+		},
+		{
+			desc:          "invalid QueryRegexp matcher (too many parameters)",
+			rule:          "QueryRegexp(`q`, `traefik`, `proxy`)",
+			expectedError: true,
+		},
+		{
+			desc: "valid QueryRegexp matcher",
+			rule: "QueryRegexp(`q`, `^(traefik|nginx)$`)",
+			expected: map[string]int{
+				"https://example.com":                     http.StatusNotFound,
+				"https://example.com?q=traefik":           http.StatusOK,
+				"https://example.com?rel=ddg&q=traefik":   http.StatusOK,
+				"https://example.com?q=traefik&q=proxy":   http.StatusOK,
+				"https://example.com?q=awesome&q=traefik": http.StatusOK,
+				"https://example.com?q=TRAEFIK":           http.StatusNotFound,
+				"https://example.com?Q=traefik":           http.StatusNotFound,
+				"https://example.com?rel=traefik":         http.StatusNotFound,
+				"https://example.com?q=nginx":             http.StatusOK,
+				"https://example.com?rel=ddg&q=nginx":     http.StatusOK,
+				"https://example.com?q=nginx&q=proxy":     http.StatusOK,
+				"https://example.com?q=awesome&q=nginx":   http.StatusOK,
+				"https://example.com?q=NGINX":             http.StatusNotFound,
+				"https://example.com?Q=nginx":             http.StatusNotFound,
+				"https://example.com?rel=nginx":           http.StatusNotFound,
+				"https://example.com?q=haproxy":           http.StatusNotFound,
+				"https://example.com?rel=ddg":             http.StatusNotFound,
+			},
+		},
+		{
+			desc: "valid QueryRegexp matcher",
+			rule: "QueryRegexp(`q`, `^.*$`)",
+			expected: map[string]int{
+				"https://example.com":                     http.StatusNotFound,
+				"https://example.com?q=traefik":           http.StatusOK,
+				"https://example.com?rel=ddg&q=traefik":   http.StatusOK,
+				"https://example.com?q=traefik&q=proxy":   http.StatusOK,
+				"https://example.com?q=awesome&q=traefik": http.StatusOK,
+				"https://example.com?q=TRAEFIK":           http.StatusOK,
+				"https://example.com?Q=traefik":           http.StatusNotFound,
+				"https://example.com?rel=traefik":         http.StatusNotFound,
+				"https://example.com?q=nginx":             http.StatusOK,
+				"https://example.com?rel=ddg&q=nginx":     http.StatusOK,
+				"https://example.com?q=nginx&q=proxy":     http.StatusOK,
+				"https://example.com?q=awesome&q=nginx":   http.StatusOK,
+				"https://example.com?q=NGINX":             http.StatusOK,
+				"https://example.com?Q=nginx":             http.StatusNotFound,
+				"https://example.com?rel=nginx":           http.StatusNotFound,
+				"https://example.com?q=haproxy":           http.StatusOK,
+				"https://example.com?rel=ddg":             http.StatusNotFound,
+			},
+		},
+		{
+			desc: "valid QueryRegexp matcher with Traefik v2 syntax",
+			rule: "QueryRegexp(`q`, `{value:(traefik|nginx)}`)",
+			expected: map[string]int{
+				"https://example.com?q=traefik":         http.StatusNotFound,
+				"https://example.com?q={value:traefik}": http.StatusOK,
+			},
+		},
+	}
+
+	for _, test := range testCases {
+		test := test
+		t.Run(test.desc, func(t *testing.T) {
+			t.Parallel()
+
+			handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})
+			muxer, err := NewMuxer()
+			require.NoError(t, err)
+
+			err = muxer.AddRoute(test.rule, 0, handler)
+			if test.expectedError {
+				require.Error(t, err)
+				return
+			}
+
+			require.NoError(t, err)
+
+			results := make(map[string]int)
+			for calledURL := range test.expected {
+				w := httptest.NewRecorder()
+
+				req := httptest.NewRequest(http.MethodGet, calledURL, http.NoBody)
+
+				muxer.ServeHTTP(w, req)
+				results[calledURL] = w.Code
+			}
+			assert.Equal(t, test.expected, results)
+		})
+	}
+}
diff --git a/pkg/muxer/http/mux_test.go b/pkg/muxer/http/mux_test.go
index be8e2de301..bdc98df373 100644
--- a/pkg/muxer/http/mux_test.go
+++ b/pkg/muxer/http/mux_test.go
@@ -7,14 +7,13 @@ import (
 	"net/http/httptest"
 	"testing"
 
-	"github.com/gorilla/mux"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 	"github.com/traefik/traefik/v2/pkg/middlewares/requestdecorator"
 	"github.com/traefik/traefik/v2/pkg/testhelpers"
 )
 
-func Test_addRoute(t *testing.T) {
+func TestMuxer(t *testing.T) {
 	testCases := []struct {
 		desc          string
 		rule          string
@@ -33,607 +32,177 @@ func Test_addRoute(t *testing.T) {
 			expectedError: true,
 		},
 		{
-			desc:          "Host empty",
-			rule:          "Host(``)",
-			expectedError: true,
-		},
-		{
-			desc:          "PathPrefix empty",
-			rule:          "PathPrefix(``)",
-			expectedError: true,
-		},
-		{
-			desc: "PathPrefix",
-			rule: "PathPrefix(`/foo`)",
-			expected: map[string]int{
-				"http://localhost/foo": http.StatusOK,
-			},
-		},
-		{
-			desc: "wrong PathPrefix",
-			rule: "PathPrefix(`/bar`)",
-			expected: map[string]int{
-				"http://localhost/foo": http.StatusNotFound,
-			},
-		},
-		{
-			desc: "Host",
-			rule: "Host(`localhost`)",
-			expected: map[string]int{
-				"http://localhost/foo": http.StatusOK,
-			},
-		},
-		{
-			desc:          "Non-ASCII Host",
-			rule:          "Host(`locàlhost`)",
-			expectedError: true,
-		},
-		{
-			desc:          "Non-ASCII HostRegexp",
-			rule:          "HostRegexp(`locàlhost`)",
+			desc:          "Rule without quote",
+			rule:          "Host(example.com)",
 			expectedError: true,
 		},
-		{
-			desc: "HostHeader equivalent to Host",
-			rule: "HostHeader(`localhost`)",
-			expected: map[string]int{
-				"http://localhost/foo": http.StatusOK,
-				"http://bar/foo":       http.StatusNotFound,
-			},
-		},
-		{
-			desc: "Host with trailing period in rule",
-			rule: "Host(`localhost.`)",
-			expected: map[string]int{
-				"http://localhost/foo": http.StatusOK,
-			},
-		},
-		{
-			desc: "Host with trailing period in domain",
-			rule: "Host(`localhost`)",
-			expected: map[string]int{
-				"http://localhost./foo": http.StatusOK,
-			},
-		},
-		{
-			desc: "Host with trailing period in domain and rule",
-			rule: "Host(`localhost.`)",
-			expected: map[string]int{
-				"http://localhost./foo": http.StatusOK,
-			},
-		},
-		{
-			desc: "wrong Host",
-			rule: "Host(`nope`)",
-			expected: map[string]int{
-				"http://localhost/foo": http.StatusNotFound,
-			},
-		},
 		{
 			desc: "Host and PathPrefix",
-			rule: "Host(`localhost`) && PathPrefix(`/foo`)",
-			expected: map[string]int{
-				"http://localhost/foo": http.StatusOK,
-			},
-		},
-		{
-			desc: "Host and PathPrefix wrong PathPrefix",
-			rule: "Host(`localhost`) && PathPrefix(`/bar`)",
-			expected: map[string]int{
-				"http://localhost/foo": http.StatusNotFound,
-			},
-		},
-		{
-			desc: "Host and PathPrefix wrong Host",
-			rule: "Host(`nope`) && PathPrefix(`/foo`)",
-			expected: map[string]int{
-				"http://localhost/foo": http.StatusNotFound,
-			},
-		},
-		{
-			desc: "Host and PathPrefix Host OR, first host",
-			rule: "Host(`nope`,`localhost`) && PathPrefix(`/foo`)",
-			expected: map[string]int{
-				"http://localhost/foo": http.StatusOK,
-			},
-		},
-		{
-			desc: "Host and PathPrefix Host OR, second host",
-			rule: "Host(`nope`,`localhost`) && PathPrefix(`/foo`)",
-			expected: map[string]int{
-				"http://nope/foo": http.StatusOK,
-			},
-		},
-		{
-			desc: "Host and PathPrefix Host OR, first host and wrong PathPrefix",
-			rule: "Host(`nope,localhost`) && PathPrefix(`/bar`)",
-			expected: map[string]int{
-				"http://localhost/foo": http.StatusNotFound,
-			},
-		},
-		{
-			desc: "HostRegexp with capturing group",
-			rule: "HostRegexp(`{subdomain:(foo\\.)?bar\\.com}`)",
+			rule: "Host(`localhost`) && PathPrefix(`/css`)",
 			expected: map[string]int{
-				"http://foo.bar.com": http.StatusOK,
-				"http://bar.com":     http.StatusOK,
-				"http://fooubar.com": http.StatusNotFound,
-				"http://barucom":     http.StatusNotFound,
-				"http://barcom":      http.StatusNotFound,
-			},
-		},
-		{
-			desc: "HostRegexp with non capturing group",
-			rule: "HostRegexp(`{subdomain:(?:foo\\.)?bar\\.com}`)",
-			expected: map[string]int{
-				"http://foo.bar.com": http.StatusOK,
-				"http://bar.com":     http.StatusOK,
-				"http://fooubar.com": http.StatusNotFound,
-				"http://barucom":     http.StatusNotFound,
-				"http://barcom":      http.StatusNotFound,
-			},
-		},
-		{
-			desc: "Methods with GET",
-			rule: "Method(`GET`)",
-			expected: map[string]int{
-				"http://localhost/foo": http.StatusOK,
-			},
-		},
-		{
-			desc: "Methods with GET and POST",
-			rule: "Method(`GET`,`POST`)",
-			expected: map[string]int{
-				"http://localhost/foo": http.StatusOK,
-			},
-		},
-		{
-			desc: "Methods with POST",
-			rule: "Method(`POST`)",
-			expected: map[string]int{
-				"http://localhost/foo": http.StatusMethodNotAllowed,
-			},
-		},
-		{
-			desc: "Header with matching header",
-			rule: "Headers(`Content-Type`,`application/json`)",
-			headers: map[string]string{
-				"Content-Type": "application/json",
-			},
-			expected: map[string]int{
-				"http://localhost/foo": http.StatusOK,
-			},
-		},
-		{
-			desc: "Header without matching header",
-			rule: "Headers(`Content-Type`,`application/foo`)",
-			headers: map[string]string{
-				"Content-Type": "application/json",
-			},
-			expected: map[string]int{
-				"http://localhost/foo": http.StatusNotFound,
-			},
-		},
-		{
-			desc: "HeaderRegExp with matching header",
-			rule: "HeadersRegexp(`Content-Type`, `application/(text|json)`)",
-			headers: map[string]string{
-				"Content-Type": "application/json",
-			},
-			expected: map[string]int{
-				"http://localhost/foo": http.StatusOK,
-			},
-		},
-		{
-			desc: "HeaderRegExp without matching header",
-			rule: "HeadersRegexp(`Content-Type`, `application/(text|json)`)",
-			headers: map[string]string{
-				"Content-Type": "application/foo",
-			},
-			expected: map[string]int{
-				"http://localhost/foo": http.StatusNotFound,
-			},
-		},
-		{
-			desc: "HeaderRegExp with matching second header",
-			rule: "HeadersRegexp(`Content-Type`, `application/(text|json)`)",
-			headers: map[string]string{
-				"Content-Type": "application/text",
-			},
-			expected: map[string]int{
-				"http://localhost/foo": http.StatusOK,
-			},
-		},
-		{
-			desc: "Query with multiple params",
-			rule: "Query(`foo=bar`, `bar=baz`)",
-			expected: map[string]int{
-				"http://localhost/foo?foo=bar&bar=baz": http.StatusOK,
-				"http://localhost/foo?bar=baz":         http.StatusNotFound,
-			},
-		},
-		{
-			desc: "Query with multiple equals",
-			rule: "Query(`foo=b=ar`)",
-			expected: map[string]int{
-				"http://localhost/foo?foo=b=ar": http.StatusOK,
-				"http://localhost/foo?foo=bar":  http.StatusNotFound,
-			},
-		},
-		{
-			desc: "Rule with simple path",
-			rule: `Path("/a")`,
-			expected: map[string]int{
-				"http://plop/a": http.StatusOK,
-			},
-		},
-		{
-			desc: `Rule with a simple host`,
-			rule: `Host("plop")`,
-			expected: map[string]int{
-				"http://plop": http.StatusOK,
-			},
-		},
-		{
-			desc: "Rule with Path AND Host",
-			rule: `Path("/a") && Host("plop")`,
-			expected: map[string]int{
-				"http://plop/a":  http.StatusOK,
-				"http://plopi/a": http.StatusNotFound,
+				"https://localhost/css": http.StatusOK,
+				"https://localhost/js":  http.StatusNotFound,
 			},
 		},
 		{
 			desc: "Rule with Host OR Host",
-			rule: `Host("tchouk") || Host("pouet")`,
+			rule: "Host(`example.com`) || Host(`example.org`)",
 			expected: map[string]int{
-				"http://tchouk/toto": http.StatusOK,
-				"http://pouet/a":     http.StatusOK,
-				"http://plopi/a":     http.StatusNotFound,
+				"https://example.com/css": http.StatusOK,
+				"https://example.org/js":  http.StatusOK,
+				"https://example.eu/html": http.StatusNotFound,
 			},
 		},
 		{
 			desc: "Rule with host OR (host AND path)",
-			rule: `Host("tchouk") || (Host("pouet") && Path("/powpow"))`,
+			rule: `Host("example.com") || (Host("example.org") && Path("/css"))`,
 			expected: map[string]int{
-				"http://tchouk/toto":   http.StatusOK,
-				"http://tchouk/powpow": http.StatusOK,
-				"http://pouet/powpow":  http.StatusOK,
-				"http://pouet/toto":    http.StatusNotFound,
-				"http://plopi/a":       http.StatusNotFound,
+				"https://example.com/css": http.StatusOK,
+				"https://example.com/js":  http.StatusOK,
+				"https://example.org/css": http.StatusOK,
+				"https://example.org/js":  http.StatusNotFound,
+				"https://example.eu/css":  http.StatusNotFound,
 			},
 		},
 		{
 			desc: "Rule with host OR host AND path",
-			rule: `Host("tchouk") || Host("pouet") && Path("/powpow")`,
+			rule: `Host("example.com") || Host("example.org") && Path("/css")`,
 			expected: map[string]int{
-				"http://tchouk/toto":   http.StatusOK,
-				"http://tchouk/powpow": http.StatusOK,
-				"http://pouet/powpow":  http.StatusOK,
-				"http://pouet/toto":    http.StatusNotFound,
-				"http://plopi/a":       http.StatusNotFound,
+				"https://example.com/css": http.StatusOK,
+				"https://example.com/js":  http.StatusOK,
+				"https://example.org/css": http.StatusOK,
+				"https://example.org/js":  http.StatusNotFound,
+				"https://example.eu/css":  http.StatusNotFound,
 			},
 		},
 		{
 			desc: "Rule with (host OR host) AND path",
-			rule: `(Host("tchouk") || Host("pouet")) && Path("/powpow")`,
+			rule: `(Host("example.com") || Host("example.org")) && Path("/css")`,
 			expected: map[string]int{
-				"http://tchouk/toto":   http.StatusNotFound,
-				"http://tchouk/powpow": http.StatusOK,
-				"http://pouet/powpow":  http.StatusOK,
-				"http://pouet/toto":    http.StatusNotFound,
-				"http://plopi/a":       http.StatusNotFound,
-			},
-		},
-		{
-			desc: "Rule with multiple host AND path",
-			rule: `(Host("tchouk","pouet")) && Path("/powpow")`,
-			expected: map[string]int{
-				"http://tchouk/toto":   http.StatusNotFound,
-				"http://tchouk/powpow": http.StatusOK,
-				"http://pouet/powpow":  http.StatusOK,
-				"http://pouet/toto":    http.StatusNotFound,
-				"http://plopi/a":       http.StatusNotFound,
-			},
-		},
-		{
-			desc: "Rule with multiple host AND multiple path",
-			rule: `Host("tchouk","pouet") && Path("/powpow", "/titi")`,
-			expected: map[string]int{
-				"http://tchouk/toto":   http.StatusNotFound,
-				"http://tchouk/powpow": http.StatusOK,
-				"http://pouet/powpow":  http.StatusOK,
-				"http://tchouk/titi":   http.StatusOK,
-				"http://pouet/titi":    http.StatusOK,
-				"http://pouet/toto":    http.StatusNotFound,
-				"http://plopi/a":       http.StatusNotFound,
+				"https://example.com/css": http.StatusOK,
+				"https://example.com/js":  http.StatusNotFound,
+				"https://example.org/css": http.StatusOK,
+				"https://example.org/js":  http.StatusNotFound,
+				"https://example.eu/css":  http.StatusNotFound,
 			},
 		},
 		{
 			desc: "Rule with (host AND path) OR (host AND path)",
-			rule: `(Host("tchouk") && Path("/titi")) || ((Host("pouet")) && Path("/powpow"))`,
+			rule: `(Host("example.com") && Path("/js")) || ((Host("example.org")) && Path("/css"))`,
 			expected: map[string]int{
-				"http://tchouk/titi":   http.StatusOK,
-				"http://tchouk/powpow": http.StatusNotFound,
-				"http://pouet/powpow":  http.StatusOK,
-				"http://pouet/toto":    http.StatusNotFound,
-				"http://plopi/a":       http.StatusNotFound,
+				"https://example.com/css": http.StatusNotFound,
+				"https://example.com/js":  http.StatusOK,
+				"https://example.org/css": http.StatusOK,
+				"https://example.org/js":  http.StatusNotFound,
+				"https://example.eu/css":  http.StatusNotFound,
 			},
 		},
-		{
-			desc:          "Rule without quote",
-			rule:          `Host(tchouk)`,
-			expectedError: true,
-		},
 		{
 			desc: "Rule case UPPER",
-			rule: `(HOST("tchouk") && PATHPREFIX("/titi"))`,
+			rule: `PATHPREFIX("/css")`,
 			expected: map[string]int{
-				"http://tchouk/titi":   http.StatusOK,
-				"http://tchouk/powpow": http.StatusNotFound,
+				"https://example.com/css": http.StatusOK,
+				"https://example.com/js":  http.StatusNotFound,
 			},
 		},
 		{
 			desc: "Rule case lower",
-			rule: `(host("tchouk") && pathprefix("/titi"))`,
+			rule: `pathprefix("/css")`,
 			expected: map[string]int{
-				"http://tchouk/titi":   http.StatusOK,
-				"http://tchouk/powpow": http.StatusNotFound,
+				"https://example.com/css": http.StatusOK,
+				"https://example.com/js":  http.StatusNotFound,
 			},
 		},
 		{
 			desc: "Rule case CamelCase",
-			rule: `(Host("tchouk") && PathPrefix("/titi"))`,
+			rule: `PathPrefix("/css")`,
 			expected: map[string]int{
-				"http://tchouk/titi":   http.StatusOK,
-				"http://tchouk/powpow": http.StatusNotFound,
+				"https://example.com/css": http.StatusOK,
+				"https://example.com/js":  http.StatusNotFound,
 			},
 		},
 		{
 			desc: "Rule case Title",
-			rule: `(Host("tchouk") && Pathprefix("/titi"))`,
+			rule: `Pathprefix("/css")`,
 			expected: map[string]int{
-				"http://tchouk/titi":   http.StatusOK,
-				"http://tchouk/powpow": http.StatusNotFound,
+				"https://example.com/css": http.StatusOK,
+				"https://example.com/js":  http.StatusNotFound,
 			},
 		},
-		{
-			desc:          "Rule Path with error",
-			rule:          `Path("titi")`,
-			expectedError: true,
-		},
-		{
-			desc:          "Rule PathPrefix with error",
-			rule:          `PathPrefix("titi")`,
-			expectedError: true,
-		},
-		{
-			desc:          "Rule HostRegexp with error",
-			rule:          `HostRegexp("{test")`,
-			expectedError: true,
-		},
-		{
-			desc:          "Rule Headers with error",
-			rule:          `Headers("titi")`,
-			expectedError: true,
-		},
-		{
-			desc:          "Rule HeadersRegexp with error",
-			rule:          `HeadersRegexp("titi")`,
-			expectedError: true,
-		},
-		{
-			desc:          "Rule Query",
-			rule:          `Query("titi")`,
-			expectedError: true,
-		},
-		{
-			desc:          "Rule Query with bad syntax",
-			rule:          `Query("titi={test")`,
-			expectedError: true,
-		},
-		{
-			desc:          "Rule with Path without args",
-			rule:          `Host("tchouk") && Path()`,
-			expectedError: true,
-		},
-		{
-			desc:          "Rule with an empty path",
-			rule:          `Host("tchouk") && Path("")`,
-			expectedError: true,
-		},
-		{
-			desc:          "Rule with an empty path",
-			rule:          `Host("tchouk") && Path("", "/titi")`,
-			expectedError: true,
-		},
 		{
 			desc: "Rule with not",
-			rule: `!Host("tchouk")`,
-			expected: map[string]int{
-				"http://tchouk/titi": http.StatusNotFound,
-				"http://test/powpow": http.StatusOK,
-			},
-		},
-		{
-			desc: "Rule with not on Path",
-			rule: `!Path("/titi")`,
+			rule: `!Host("example.com")`,
 			expected: map[string]int{
-				"http://tchouk/titi":   http.StatusNotFound,
-				"http://tchouk/powpow": http.StatusOK,
+				"https://example.org": http.StatusOK,
+				"https://example.com": http.StatusNotFound,
 			},
 		},
 		{
 			desc: "Rule with not on multiple route with or",
-			rule: `!(Host("tchouk") || Host("toto"))`,
+			rule: `!(Host("example.com") || Host("example.org"))`,
 			expected: map[string]int{
-				"http://tchouk/titi": http.StatusNotFound,
-				"http://toto/powpow": http.StatusNotFound,
-				"http://test/powpow": http.StatusOK,
+				"https://example.eu/js":   http.StatusOK,
+				"https://example.com/css": http.StatusNotFound,
+				"https://example.org/js":  http.StatusNotFound,
 			},
 		},
 		{
 			desc: "Rule with not on multiple route with and",
-			rule: `!(Host("tchouk") && Path("/titi"))`,
+			rule: `!(Host("example.com") && Path("/css"))`,
 			expected: map[string]int{
-				"http://tchouk/titi": http.StatusNotFound,
-				"http://tchouk/toto": http.StatusOK,
-				"http://test/titi":   http.StatusOK,
+				"https://example.com/js":  http.StatusOK,
+				"https://example.eu/css":  http.StatusOK,
+				"https://example.com/css": http.StatusNotFound,
 			},
 		},
 		{
 			desc: "Rule with not on multiple route with and another not",
-			rule: `!(Host("tchouk") && !Path("/titi"))`,
+			rule: `!(Host("example.com") && !Path("/css"))`,
 			expected: map[string]int{
-				"http://tchouk/titi": http.StatusOK,
-				"http://toto/titi":   http.StatusOK,
-				"http://tchouk/toto": http.StatusNotFound,
+				"https://example.com/css": http.StatusOK,
+				"https://example.org/css": http.StatusOK,
+				"https://example.com/js":  http.StatusNotFound,
 			},
 		},
 		{
 			desc: "Rule with not on two rule",
-			rule: `!Host("tchouk") || !Path("/titi")`,
+			rule: `!Host("example.com") || !Path("/css")`,
 			expected: map[string]int{
-				"http://tchouk/titi": http.StatusNotFound,
-				"http://tchouk/toto": http.StatusOK,
-				"http://test/titi":   http.StatusOK,
+				"https://example.com/js":  http.StatusOK,
+				"https://example.org/css": http.StatusOK,
+				"https://example.com/css": http.StatusNotFound,
 			},
 		},
 		{
 			desc: "Rule case with double not",
-			rule: `!(!(Host("tchouk") && Pathprefix("/titi")))`,
+			rule: `!(!(Host("example.com") && Pathprefix("/css")))`,
 			expected: map[string]int{
-				"http://tchouk/titi":   http.StatusOK,
-				"http://tchouk/powpow": http.StatusNotFound,
-				"http://test/titi":     http.StatusNotFound,
+				"https://example.com/css": http.StatusOK,
+				"https://example.com/js":  http.StatusNotFound,
+				"https://example.org/css": http.StatusNotFound,
 			},
 		},
 		{
 			desc: "Rule case with not domain",
-			rule: `!Host("tchouk") && Pathprefix("/titi")`,
+			rule: `!Host("example.com") && Pathprefix("/css")`,
 			expected: map[string]int{
-				"http://tchouk/titi":   http.StatusNotFound,
-				"http://tchouk/powpow": http.StatusNotFound,
-				"http://toto/powpow":   http.StatusNotFound,
-				"http://toto/titi":     http.StatusOK,
+				"https://example.org/css": http.StatusOK,
+				"https://example.org/js":  http.StatusNotFound,
+				"https://example.com/css": http.StatusNotFound,
+				"https://example.com/js":  http.StatusNotFound,
 			},
 		},
 		{
 			desc: "Rule with multiple host AND multiple path AND not",
-			rule: `!(Host("tchouk","pouet") && Path("/powpow", "/titi"))`,
-			expected: map[string]int{
-				"http://tchouk/toto":   http.StatusOK,
-				"http://tchouk/powpow": http.StatusNotFound,
-				"http://pouet/powpow":  http.StatusNotFound,
-				"http://tchouk/titi":   http.StatusNotFound,
-				"http://pouet/titi":    http.StatusNotFound,
-				"http://pouet/toto":    http.StatusOK,
-				"http://plopi/a":       http.StatusOK,
-			},
-		},
-		{
-			desc:          "ClientIP empty",
-			rule:          "ClientIP(``)",
-			expectedError: true,
-		},
-		{
-			desc:          "Invalid ClientIP",
-			rule:          "ClientIP(`invalid`)",
-			expectedError: true,
-		},
-		{
-			desc:       "Non matching ClientIP",
-			rule:       "ClientIP(`10.10.1.1`)",
-			remoteAddr: "10.0.0.0",
-			expected: map[string]int{
-				"http://tchouk/toto": http.StatusNotFound,
-			},
-		},
-		{
-			desc:       "Non matching IPv6",
-			rule:       "ClientIP(`10::10`)",
-			remoteAddr: "::1",
-			expected: map[string]int{
-				"http://tchouk/toto": http.StatusNotFound,
-			},
-		},
-		{
-			desc:       "Matching IP",
-			rule:       "ClientIP(`10.0.0.0`)",
-			remoteAddr: "10.0.0.0:8456",
-			expected: map[string]int{
-				"http://tchouk/toto": http.StatusOK,
-			},
-		},
-		{
-			desc:       "Matching IPv6",
-			rule:       "ClientIP(`10::10`)",
-			remoteAddr: "10::10",
-			expected: map[string]int{
-				"http://tchouk/toto": http.StatusOK,
-			},
-		},
-		{
-			desc:       "Matching IP among several IP",
-			rule:       "ClientIP(`10.0.0.1`, `10.0.0.0`)",
-			remoteAddr: "10.0.0.0",
-			expected: map[string]int{
-				"http://tchouk/toto": http.StatusOK,
-			},
-		},
-		{
-			desc:       "Non Matching IP with CIDR",
-			rule:       "ClientIP(`11.0.0.0/24`)",
-			remoteAddr: "10.0.0.0",
-			expected: map[string]int{
-				"http://tchouk/toto": http.StatusNotFound,
-			},
-		},
-		{
-			desc:       "Non Matching IPv6 with CIDR",
-			rule:       "ClientIP(`11::/16`)",
-			remoteAddr: "10::",
-			expected: map[string]int{
-				"http://tchouk/toto": http.StatusNotFound,
-			},
-		},
-		{
-			desc:       "Matching IP with CIDR",
-			rule:       "ClientIP(`10.0.0.0/16`)",
-			remoteAddr: "10.0.0.0",
-			expected: map[string]int{
-				"http://tchouk/toto": http.StatusOK,
-			},
-		},
-		{
-			desc:       "Matching IPv6 with CIDR",
-			rule:       "ClientIP(`10::/16`)",
-			remoteAddr: "10::10",
-			expected: map[string]int{
-				"http://tchouk/toto": http.StatusOK,
-			},
-		},
-		{
-			desc:       "Matching IP among several CIDR",
-			rule:       "ClientIP(`11.0.0.0/16`, `10.0.0.0/16`)",
-			remoteAddr: "10.0.0.0",
-			expected: map[string]int{
-				"http://tchouk/toto": http.StatusOK,
-			},
-		},
-		{
-			desc:       "Matching IP among non matching CIDR and matching IP",
-			rule:       "ClientIP(`11.0.0.0/16`, `10.0.0.0`)",
-			remoteAddr: "10.0.0.0",
-			expected: map[string]int{
-				"http://tchouk/toto": http.StatusOK,
-			},
-		},
-		{
-			desc:       "Matching IP among matching CIDR and non matching IP",
-			rule:       "ClientIP(`11.0.0.0`, `10.0.0.0/16`)",
-			remoteAddr: "10.0.0.0",
+			rule: `!(Host("example.com") && Path("/js"))`,
 			expected: map[string]int{
-				"http://tchouk/toto": http.StatusOK,
+				"https://example.com/js":    http.StatusNotFound,
+				"https://example.com/html":  http.StatusOK,
+				"https://example.org/js":    http.StatusOK,
+				"https://example.com/css":   http.StatusOK,
+				"https://example.org/css":   http.StatusOK,
+				"https://example.org/html":  http.StatusOK,
+				"https://example.eu/images": http.StatusOK,
 			},
 		},
 	}
@@ -644,36 +213,37 @@ func Test_addRoute(t *testing.T) {
 		t.Run(test.desc, func(t *testing.T) {
 			t.Parallel()
 
-			handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})
 			muxer, err := NewMuxer()
 			require.NoError(t, err)
 
+			handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})
 			err = muxer.AddRoute(test.rule, 0, handler)
 			if test.expectedError {
 				require.Error(t, err)
-			} else {
-				require.NoError(t, err)
-
-				// RequestDecorator is necessary for the host rule
-				reqHost := requestdecorator.New(nil)
+				return
+			}
+			require.NoError(t, err)
 
-				results := make(map[string]int)
-				for calledURL := range test.expected {
-					w := httptest.NewRecorder()
+			// RequestDecorator is necessary for the host rule
+			reqHost := requestdecorator.New(nil)
 
-					req := testhelpers.MustNewRequest(http.MethodGet, calledURL, nil)
+			results := make(map[string]int)
+			for calledURL := range test.expected {
+				req := testhelpers.MustNewRequest(http.MethodGet, calledURL, http.NoBody)
 
-					// Useful for the ClientIP matcher
-					req.RemoteAddr = test.remoteAddr
+				// Useful for the ClientIP matcher
+				req.RemoteAddr = test.remoteAddr
 
-					for key, value := range test.headers {
-						req.Header.Set(key, value)
-					}
-					reqHost.ServeHTTP(w, req, muxer.ServeHTTP)
-					results[calledURL] = w.Code
+				for key, value := range test.headers {
+					req.Header.Set(key, value)
 				}
-				assert.Equal(t, test.expected, results)
+
+				w := httptest.NewRecorder()
+				reqHost.ServeHTTP(w, req, muxer.ServeHTTP)
+				results[calledURL] = w.Code
 			}
+
+			assert.Equal(t, test.expected, results)
 		})
 	}
 }
@@ -813,7 +383,7 @@ func Test_addRoutePriority(t *testing.T) {
 			muxer.SortRoutes()
 
 			w := httptest.NewRecorder()
-			req := testhelpers.MustNewRequest(http.MethodGet, test.path, nil)
+			req := testhelpers.MustNewRequest(http.MethodGet, test.path, http.NoBody)
 
 			muxer.ServeHTTP(w, req)
 
@@ -822,86 +392,6 @@ func Test_addRoutePriority(t *testing.T) {
 	}
 }
 
-func TestHostRegexp(t *testing.T) {
-	testCases := []struct {
-		desc    string
-		hostExp string
-		urls    map[string]bool
-	}{
-		{
-			desc:    "capturing group",
-			hostExp: "{subdomain:(foo\\.)?bar\\.com}",
-			urls: map[string]bool{
-				"http://foo.bar.com": true,
-				"http://bar.com":     true,
-				"http://fooubar.com": false,
-				"http://barucom":     false,
-				"http://barcom":      false,
-			},
-		},
-		{
-			desc:    "non capturing group",
-			hostExp: "{subdomain:(?:foo\\.)?bar\\.com}",
-			urls: map[string]bool{
-				"http://foo.bar.com": true,
-				"http://bar.com":     true,
-				"http://fooubar.com": false,
-				"http://barucom":     false,
-				"http://barcom":      false,
-			},
-		},
-		{
-			desc:    "regex insensitive",
-			hostExp: "{dummy:[A-Za-z-]+\\.bar\\.com}",
-			urls: map[string]bool{
-				"http://FOO.bar.com": true,
-				"http://foo.bar.com": true,
-				"http://fooubar.com": false,
-				"http://barucom":     false,
-				"http://barcom":      false,
-			},
-		},
-		{
-			desc:    "insensitive host",
-			hostExp: "{dummy:[a-z-]+\\.bar\\.com}",
-			urls: map[string]bool{
-				"http://FOO.bar.com": true,
-				"http://foo.bar.com": true,
-				"http://fooubar.com": false,
-				"http://barucom":     false,
-				"http://barcom":      false,
-			},
-		},
-		{
-			desc:    "insensitive host simple",
-			hostExp: "foo.bar.com",
-			urls: map[string]bool{
-				"http://FOO.bar.com": true,
-				"http://foo.bar.com": true,
-				"http://fooubar.com": false,
-				"http://barucom":     false,
-				"http://barcom":      false,
-			},
-		},
-	}
-
-	for _, test := range testCases {
-		test := test
-		t.Run(test.desc, func(t *testing.T) {
-			t.Parallel()
-
-			rt := &mux.Route{}
-			err := hostRegexp(rt, test.hostExp)
-			require.NoError(t, err)
-
-			for testURL, match := range test.urls {
-				req := testhelpers.MustNewRequest(http.MethodGet, testURL, nil)
-				assert.Equal(t, match, rt.Match(req, &mux.RouteMatch{}), testURL)
-			}
-		})
-	}
-}
-
 func TestParseDomains(t *testing.T) {
 	testCases := []struct {
 		description   string
@@ -914,21 +404,6 @@ func TestParseDomains(t *testing.T) {
 			expression:    "Foobar(`foo.bar`,`test.bar`)",
 			errorExpected: true,
 		},
-		{
-			description: "Several host rules",
-			expression:  "Host(`foo.bar`,`test.bar`)",
-			domain:      []string{"foo.bar", "test.bar"},
-		},
-		{
-			description: "Several host rules upper",
-			expression:  "HOST(`foo.bar`,`test.bar`)",
-			domain:      []string{"foo.bar", "test.bar"},
-		},
-		{
-			description: "Several host rules lower",
-			expression:  "host(`foo.bar`,`test.bar`)",
-			domain:      []string{"foo.bar", "test.bar"},
-		},
 		{
 			description: "No host rule",
 			expression:  "Path(`/test`)",
@@ -938,6 +413,11 @@ func TestParseDomains(t *testing.T) {
 			expression:  "Host(`foo.bar`) && Path(`/test`)",
 			domain:      []string{"foo.bar"},
 		},
+		{
+			description: "Host rule to trim and another rule",
+			expression:  "Host(`Foo.Bar`) || Host(`bar.buz`) && Path(`/test`)",
+			domain:      []string{"foo.bar", "bar.buz"},
+		},
 		{
 			description: "Host rule to trim and another rule",
 			expression:  "Host(`Foo.Bar`) && Path(`/test`)",
@@ -967,7 +447,9 @@ func TestParseDomains(t *testing.T) {
 	}
 }
 
-func TestAbsoluteFormURL(t *testing.T) {
+// TestEmptyHost is a non regression test for
+// https://github.com/traefik/traefik/pull/9131
+func TestEmptyHost(t *testing.T) {
 	testCases := []struct {
 		desc     string
 		request  string
@@ -975,40 +457,40 @@ func TestAbsoluteFormURL(t *testing.T) {
 		expected int
 	}{
 		{
-			desc:     "!HostRegexp with absolute-form URL with empty host with non-matching host header",
-			request:  "GET http://@/ HTTP/1.1\r\nHost: test.localhost\r\n\r\n",
-			rule:     "!HostRegexp(`test.localhost`)",
-			expected: http.StatusNotFound,
+			desc:     "HostRegexp with absolute-form URL with empty host with non-matching host header",
+			request:  "GET http://@/ HTTP/1.1\r\nHost: example.com\r\n\r\n",
+			rule:     "HostRegexp(`example.com`)",
+			expected: http.StatusOK,
 		},
 		{
-			desc:     "!Host with absolute-form URL with empty host with non-matching host header",
-			request:  "GET http://@/ HTTP/1.1\r\nHost: test.localhost\r\n\r\n",
-			rule:     "!Host(`test.localhost`)",
-			expected: http.StatusNotFound,
+			desc:     "Host with absolute-form URL with empty host with non-matching host header",
+			request:  "GET http://@/ HTTP/1.1\r\nHost: example.com\r\n\r\n",
+			rule:     "Host(`example.com`)",
+			expected: http.StatusOK,
 		},
 		{
-			desc:     "!HostRegexp with absolute-form URL with matching host header",
-			request:  "GET http://test.localhost/ HTTP/1.1\r\nHost: toto.localhost\r\n\r\n",
-			rule:     "!HostRegexp(`test.localhost`)",
-			expected: http.StatusNotFound,
+			desc:     "HostRegexp with absolute-form URL with matching host header",
+			request:  "GET http://example.com/ HTTP/1.1\r\nHost: example.org\r\n\r\n",
+			rule:     "HostRegexp(`example.com`)",
+			expected: http.StatusOK,
 		},
 		{
-			desc:     "!Host with absolute-form URL with matching host header",
-			request:  "GET http://test.localhost/ HTTP/1.1\r\nHost: toto.localhost\r\n\r\n",
-			rule:     "!Host(`test.localhost`)",
-			expected: http.StatusNotFound,
+			desc:     "Host with absolute-form URL with matching host header",
+			request:  "GET http://example.com/ HTTP/1.1\r\nHost: example.org\r\n\r\n",
+			rule:     "Host(`example.com`)",
+			expected: http.StatusOK,
 		},
 		{
-			desc:     "!HostRegexp with absolute-form URL with non-matching host header",
-			request:  "GET http://test.localhost/ HTTP/1.1\r\nHost: toto.localhost\r\n\r\n",
-			rule:     "!HostRegexp(`toto.localhost`)",
-			expected: http.StatusOK,
+			desc:     "HostRegexp with absolute-form URL with non-matching host header",
+			request:  "GET http://example.com/ HTTP/1.1\r\nHost: example.org\r\n\r\n",
+			rule:     "HostRegexp(`example.org`)",
+			expected: http.StatusNotFound,
 		},
 		{
-			desc:     "!Host with absolute-form URL with non-matching host header",
-			request:  "GET http://test.localhost/ HTTP/1.1\r\nHost: toto.localhost\r\n\r\n",
-			rule:     "!Host(`toto.localhost`)",
-			expected: http.StatusOK,
+			desc:     "Host with absolute-form URL with non-matching host header",
+			request:  "GET http://example.com/ HTTP/1.1\r\nHost: example.org\r\n\r\n",
+			rule:     "Host(`example.org`)",
+			expected: http.StatusNotFound,
 		},
 	}
 
diff --git a/pkg/muxer/tcp/matcher_test.go b/pkg/muxer/tcp/matcher_test.go
new file mode 100644
index 0000000000..06b45e1866
--- /dev/null
+++ b/pkg/muxer/tcp/matcher_test.go
@@ -0,0 +1,383 @@
+package tcp
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+	"github.com/traefik/traefik/v2/pkg/tcp"
+)
+
+func Test_HostSNICatchAll(t *testing.T) {
+	testCases := []struct {
+		desc       string
+		rule       string
+		isCatchAll bool
+	}{
+		{
+			desc: "HostSNI(`example.com`) is not catchAll",
+			rule: "HostSNI(`example.com`)",
+		},
+		{
+			desc:       "HostSNI(`*`) is catchAll",
+			rule:       "HostSNI(`*`)",
+			isCatchAll: true,
+		},
+		{
+			desc:       "HostSNIRegexp(`^.*$`) is not catchAll",
+			rule:       "HostSNIRegexp(`.*`)",
+			isCatchAll: false,
+		},
+	}
+
+	for _, test := range testCases {
+		test := test
+		t.Run(test.desc, func(t *testing.T) {
+			t.Parallel()
+
+			muxer, err := NewMuxer()
+			require.NoError(t, err)
+
+			err = muxer.AddRoute(test.rule, 0, tcp.HandlerFunc(func(conn tcp.WriteCloser) {}))
+			require.NoError(t, err)
+
+			handler, catchAll := muxer.Match(ConnData{
+				serverName: "example.com",
+			})
+			require.NotNil(t, handler)
+			assert.Equal(t, test.isCatchAll, catchAll)
+		})
+	}
+}
+
+func Test_HostSNI(t *testing.T) {
+	testCases := []struct {
+		desc       string
+		rule       string
+		serverName string
+		buildErr   bool
+		match      bool
+	}{
+		{
+			desc:     "Empty",
+			buildErr: true,
+		},
+		{
+			desc:     "Invalid HostSNI matcher (empty host)",
+			rule:     "HostSNI(``)",
+			buildErr: true,
+		},
+		{
+			desc:     "Invalid HostSNI matcher (too many parameters)",
+			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`)",
+			buildErr: true,
+		},
+		{
+			desc:       "Valid HostSNI matcher - puny-coded emoji",
+			rule:       "HostSNI(`xn--9t9h.com`)",
+			serverName: "xn--9t9h.com",
+			match:      true,
+		},
+		{
+			desc:       "Valid HostSNI matcher - puny-coded emoji but emoji in server name",
+			rule:       "HostSNI(`xn--9t9h.com`)",
+			serverName: "🦭.com",
+		},
+		{
+			desc:       "Matching hosts",
+			rule:       "HostSNI(`example.com`)",
+			serverName: "example.com",
+			match:      true,
+		},
+		{
+			desc:       "No matching hosts",
+			rule:       "HostSNI(`example.com`)",
+			serverName: "example.org",
+		},
+		{
+			desc:       "Matching globing host `*`",
+			rule:       "HostSNI(`*`)",
+			serverName: "example.com",
+			match:      true,
+		},
+		{
+			desc:       "Matching globing host `*` and empty server name",
+			rule:       "HostSNI(`*`)",
+			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.`)",
+			serverName: "example.com",
+			match:      true,
+		},
+		{
+			desc:       "Matching hosts with subdomains",
+			rule:       "HostSNI(`foo.example.com`)",
+			serverName: "foo.example.com",
+			match:      true,
+		},
+	}
+
+	for _, test := range testCases {
+		test := test
+		t.Run(test.desc, func(t *testing.T) {
+			t.Parallel()
+
+			muxer, err := NewMuxer()
+			require.NoError(t, err)
+
+			err = muxer.AddRoute(test.rule, 0, tcp.HandlerFunc(func(conn tcp.WriteCloser) {}))
+			if test.buildErr {
+				require.Error(t, err)
+				return
+			}
+			require.NoError(t, err)
+
+			meta := ConnData{
+				serverName: test.serverName,
+			}
+
+			handler, _ := muxer.Match(meta)
+			require.Equal(t, test.match, handler != nil)
+		})
+	}
+}
+
+func Test_HostSNIRegexp(t *testing.T) {
+	testCases := []struct {
+		desc     string
+		rule     string
+		expected map[string]bool
+		buildErr bool
+		match    bool
+	}{
+		{
+			desc:     "Empty",
+			buildErr: true,
+		},
+		{
+			desc:     "Invalid HostSNIRegexp matcher (empty host)",
+			rule:     "HostSNIRegexp(``)",
+			buildErr: true,
+		},
+		{
+			desc:     "Invalid HostSNIRegexp matcher (non ASCII host)",
+			rule:     "HostSNIRegexp(`🦭.com`)",
+			buildErr: true,
+		},
+		{
+			desc:     "Invalid HostSNIRegexp matcher (invalid regexp)",
+			rule:     "HostSNIRegexp(`(example.com`)",
+			buildErr: true,
+		},
+		{
+			desc:     "Invalid HostSNIRegexp matcher (too many parameters)",
+			rule:     "HostSNIRegexp(`example.com`, `example.org`)",
+			buildErr: true,
+		},
+		{
+			desc: "valid HostSNIRegexp matcher",
+			rule: "HostSNIRegexp(`^example\\.(com|org)$`)",
+			expected: map[string]bool{
+				"example.com":  true,
+				"example.com.": false,
+				"EXAMPLE.com":  false,
+				"example.org":  true,
+				"exampleuorg":  false,
+				"":             false,
+			},
+		},
+		{
+			desc: "valid HostSNIRegexp matcher with Traefik v2 syntax",
+			rule: "HostSNIRegexp(`example.{tld:(com|org)}`)",
+			expected: map[string]bool{
+				"example.com":  false,
+				"example.com.": false,
+				"EXAMPLE.com":  false,
+				"example.org":  false,
+				"exampleuorg":  false,
+				"":             false,
+			},
+		},
+	}
+
+	for _, test := range testCases {
+		test := test
+		t.Run(test.desc, func(t *testing.T) {
+			t.Parallel()
+
+			muxer, err := NewMuxer()
+			require.NoError(t, err)
+
+			err = muxer.AddRoute(test.rule, 0, tcp.HandlerFunc(func(conn tcp.WriteCloser) {}))
+			if test.buildErr {
+				require.Error(t, err)
+				return
+			}
+			require.NoError(t, err)
+
+			for serverName, match := range test.expected {
+				meta := ConnData{
+					serverName: serverName,
+				}
+
+				handler, _ := muxer.Match(meta)
+				assert.Equal(t, match, handler != nil, serverName)
+			}
+		})
+	}
+}
+
+func Test_ClientIP(t *testing.T) {
+	testCases := []struct {
+		desc     string
+		rule     string
+		expected map[string]bool
+		buildErr bool
+	}{
+		{
+			desc:     "Empty",
+			buildErr: true,
+		},
+		{
+			desc:     "Invalid ClientIP matcher (empty host)",
+			rule:     "ClientIP(``)",
+			buildErr: true,
+		},
+		{
+			desc:     "Invalid ClientIP matcher (non ASCII host)",
+			rule:     "ClientIP(`🦭/32`)",
+			buildErr: true,
+		},
+		{
+			desc:     "Invalid ClientIP matcher (too many parameters)",
+			rule:     "ClientIP(`127.0.0.1`, `127.0.0.2`)",
+			buildErr: true,
+		},
+		{
+			desc: "valid ClientIP matcher",
+			rule: "ClientIP(`20.20.20.20`)",
+			expected: map[string]bool{
+				"20.20.20.20": true,
+				"10.10.10.10": false,
+			},
+		},
+		{
+			desc: "valid ClientIP matcher with CIDR",
+			rule: "ClientIP(`20.20.20.20/24`)",
+			expected: map[string]bool{
+				"20.20.20.20": true,
+				"20.20.20.40": true,
+				"10.10.10.10": false,
+			},
+		},
+	}
+
+	for _, test := range testCases {
+		test := test
+		t.Run(test.desc, func(t *testing.T) {
+			t.Parallel()
+
+			muxer, err := NewMuxer()
+			require.NoError(t, err)
+
+			err = muxer.AddRoute(test.rule, 0, tcp.HandlerFunc(func(conn tcp.WriteCloser) {}))
+			if test.buildErr {
+				require.Error(t, err)
+				return
+			}
+			require.NoError(t, err)
+
+			for remoteIP, match := range test.expected {
+				meta := ConnData{
+					remoteIP: remoteIP,
+				}
+
+				handler, _ := muxer.Match(meta)
+				assert.Equal(t, match, handler != nil, remoteIP)
+			}
+		})
+	}
+}
+
+func Test_ALPN(t *testing.T) {
+	testCases := []struct {
+		desc     string
+		rule     string
+		expected map[string]bool
+		buildErr bool
+	}{
+		{
+			desc:     "Empty",
+			buildErr: true,
+		},
+		{
+			desc:     "Invalid ALPN matcher (TLS proto)",
+			rule:     "ALPN(`acme-tls/1`)",
+			buildErr: true,
+		},
+		{
+			desc:     "Invalid ALPN matcher (empty parameters)",
+			rule:     "ALPN(``)",
+			buildErr: true,
+		},
+		{
+			desc:     "Invalid ALPN matcher (too many parameters)",
+			rule:     "ALPN(`h2`, `mqtt`)",
+			buildErr: true,
+		},
+		{
+			desc: "Valid ALPN matcher",
+			rule: "ALPN(`h2`)",
+			expected: map[string]bool{
+				"h2":   true,
+				"mqtt": false,
+				"":     false,
+			},
+		},
+	}
+
+	for _, test := range testCases {
+		test := test
+		t.Run(test.desc, func(t *testing.T) {
+			t.Parallel()
+
+			muxer, err := NewMuxer()
+			require.NoError(t, err)
+
+			err = muxer.AddRoute(test.rule, 0, tcp.HandlerFunc(func(conn tcp.WriteCloser) {}))
+			if test.buildErr {
+				require.Error(t, err)
+				return
+			}
+			require.NoError(t, err)
+
+			for proto, match := range test.expected {
+				meta := ConnData{
+					alpnProtos: []string{proto},
+				}
+
+				handler, _ := muxer.Match(meta)
+				assert.Equal(t, match, handler != nil, proto)
+			}
+		})
+	}
+}
diff --git a/pkg/muxer/tcp/mux_test.go b/pkg/muxer/tcp/mux_test.go
index 50b8938cf7..95e84a2458 100644
--- a/pkg/muxer/tcp/mux_test.go
+++ b/pkg/muxer/tcp/mux_test.go
@@ -1,59 +1,15 @@
 package tcp
 
 import (
-	"fmt"
 	"net"
 	"testing"
 	"time"
 
-	"github.com/go-acme/lego/v4/challenge/tlsalpn01"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 	"github.com/traefik/traefik/v2/pkg/tcp"
 )
 
-type fakeConn struct {
-	call       map[string]int
-	remoteAddr net.Addr
-}
-
-func (f *fakeConn) Read(b []byte) (n int, err error) {
-	panic("implement me")
-}
-
-func (f *fakeConn) Write(b []byte) (n int, err error) {
-	f.call[string(b)]++
-	return len(b), nil
-}
-
-func (f *fakeConn) Close() error {
-	panic("implement me")
-}
-
-func (f *fakeConn) LocalAddr() net.Addr {
-	panic("implement me")
-}
-
-func (f *fakeConn) RemoteAddr() net.Addr {
-	return f.remoteAddr
-}
-
-func (f *fakeConn) SetDeadline(t time.Time) error {
-	panic("implement me")
-}
-
-func (f *fakeConn) SetReadDeadline(t time.Time) error {
-	panic("implement me")
-}
-
-func (f *fakeConn) SetWriteDeadline(t time.Time) error {
-	panic("implement me")
-}
-
-func (f *fakeConn) CloseWrite() error {
-	panic("implement me")
-}
-
 func Test_addTCPRoute(t *testing.T) {
 	testCases := []struct {
 		desc       string
@@ -73,430 +29,225 @@ func Test_addTCPRoute(t *testing.T) {
 			rule:     "rulewithnotmatcher",
 			routeErr: true,
 		},
-		{
-			desc:       "Empty HostSNI rule",
-			rule:       "HostSNI()",
-			serverName: "foobar",
-			routeErr:   true,
-		},
 		{
 			desc:       "Empty HostSNI rule",
 			rule:       "HostSNI(``)",
-			serverName: "foobar",
+			serverName: "example.org",
 			routeErr:   true,
 		},
 		{
 			desc:       "Valid HostSNI rule matching",
-			rule:       "HostSNI(`foobar`)",
-			serverName: "foobar",
+			rule:       "HostSNI(`example.org`)",
+			serverName: "example.org",
 		},
 		{
 			desc:       "Valid negative HostSNI rule matching",
-			rule:       "!HostSNI(`bar`)",
-			serverName: "foobar",
+			rule:       "!HostSNI(`example.com`)",
+			serverName: "example.org",
 		},
 		{
 			desc:       "Valid HostSNI rule matching with alternative case",
-			rule:       "hostsni(`foobar`)",
-			serverName: "foobar",
+			rule:       "hostsni(`example.org`)",
+			serverName: "example.org",
 		},
 		{
 			desc:       "Valid HostSNI rule matching with alternative case",
-			rule:       "HOSTSNI(`foobar`)",
-			serverName: "foobar",
+			rule:       "HOSTSNI(`example.org`)",
+			serverName: "example.org",
 		},
 		{
 			desc:       "Valid HostSNI rule not matching",
-			rule:       "HostSNI(`foobar`)",
-			serverName: "bar",
-			matchErr:   true,
-		},
-		{
-			desc:       "Empty HostSNIRegexp rule",
-			rule:       "HostSNIRegexp()",
-			serverName: "foobar",
-			routeErr:   true,
-		},
-		{
-			desc:       "Empty HostSNIRegexp rule",
-			rule:       "HostSNIRegexp(``)",
-			serverName: "foobar",
-			routeErr:   true,
-		},
-		{
-			desc:       "Valid HostSNIRegexp rule matching",
-			rule:       "HostSNIRegexp(`{subdomain:[a-z]+}.foobar`)",
-			serverName: "sub.foobar",
-		},
-		{
-			desc:       "Valid negative HostSNIRegexp rule matching",
-			rule:       "!HostSNIRegexp(`bar`)",
-			serverName: "foobar",
-		},
-		{
-			desc:       "Valid HostSNIRegexp rule matching with alternative case",
-			rule:       "hostsniregexp(`foobar`)",
-			serverName: "foobar",
-		},
-		{
-			desc:       "Valid HostSNIRegexp rule matching with alternative case",
-			rule:       "HOSTSNIREGEXP(`foobar`)",
-			serverName: "foobar",
-		},
-		{
-			desc:       "Valid HostSNIRegexp rule not matching",
-			rule:       "HostSNIRegexp(`foobar`)",
-			serverName: "bar",
+			rule:       "HostSNI(`example.org`)",
+			serverName: "example.com",
 			matchErr:   true,
 		},
 		{
 			desc:       "Valid negative HostSNI rule not matching",
-			rule:       "!HostSNI(`bar`)",
-			serverName: "bar",
-			matchErr:   true,
-		},
-		{
-			desc:       "Valid HostSNIRegexp rule matching empty servername",
-			rule:       "HostSNIRegexp(`{subdomain:[a-z]*}`)",
-			serverName: "",
-		},
-		{
-			desc:       "Valid HostSNIRegexp rule with one name",
-			rule:       "HostSNIRegexp(`{dummy}`)",
-			serverName: "toto",
-		},
-		{
-			desc:       "Valid HostSNIRegexp rule with one name 2",
-			rule:       "HostSNIRegexp(`{dummy}`)",
-			serverName: "toto.com",
-			matchErr:   true,
-		},
-		{
-			desc:     "Empty ClientIP rule",
-			rule:     "ClientIP()",
-			routeErr: true,
-		},
-		{
-			desc:     "Empty ClientIP rule",
-			rule:     "ClientIP(``)",
-			routeErr: true,
-		},
-		{
-			desc:     "Invalid ClientIP",
-			rule:     "ClientIP(`invalid`)",
-			routeErr: true,
-		},
-		{
-			desc:       "Invalid remoteAddr",
-			rule:       "ClientIP(`10.0.0.1`)",
-			remoteAddr: "not.an.IP:80",
-			matchErr:   true,
-		},
-		{
-			desc:       "Valid ClientIP rule matching",
-			rule:       "ClientIP(`10.0.0.1`)",
-			remoteAddr: "10.0.0.1:80",
-		},
-		{
-			desc:       "Valid negative ClientIP rule matching",
-			rule:       "!ClientIP(`20.0.0.1`)",
-			remoteAddr: "10.0.0.1:80",
-		},
-		{
-			desc:       "Valid ClientIP rule matching with alternative case",
-			rule:       "clientip(`10.0.0.1`)",
-			remoteAddr: "10.0.0.1:80",
-		},
-		{
-			desc:       "Valid ClientIP rule matching with alternative case",
-			rule:       "CLIENTIP(`10.0.0.1`)",
-			remoteAddr: "10.0.0.1:80",
-		},
-		{
-			desc:       "Valid ClientIP rule not matching",
-			rule:       "ClientIP(`10.0.0.1`)",
-			remoteAddr: "10.0.0.2:80",
+			rule:       "!HostSNI(`example.com`)",
+			serverName: "example.com",
 			matchErr:   true,
 		},
-		{
-			desc:       "Valid negative ClientIP rule not matching",
-			rule:       "!ClientIP(`10.0.0.2`)",
-			remoteAddr: "10.0.0.2:80",
-			matchErr:   true,
-		},
-		{
-			desc:       "Valid ClientIP rule matching IPv6",
-			rule:       "ClientIP(`10::10`)",
-			remoteAddr: "[10::10]:80",
-		},
-		{
-			desc:       "Valid negative ClientIP rule matching IPv6",
-			rule:       "!ClientIP(`10::10`)",
-			remoteAddr: "[::1]:80",
-		},
-		{
-			desc:       "Valid ClientIP rule not matching IPv6",
-			rule:       "ClientIP(`10::10`)",
-			remoteAddr: "[::1]:80",
-			matchErr:   true,
-		},
-		{
-			desc:       "Valid ClientIP rule matching multiple IPs",
-			rule:       "ClientIP(`10.0.0.1`, `10.0.0.0`)",
-			remoteAddr: "10.0.0.0:80",
-		},
-		{
-			desc:       "Valid ClientIP rule matching CIDR",
-			rule:       "ClientIP(`11.0.0.0/24`)",
-			remoteAddr: "11.0.0.0:80",
-		},
-		{
-			desc:       "Valid ClientIP rule not matching CIDR",
-			rule:       "ClientIP(`11.0.0.0/24`)",
-			remoteAddr: "10.0.0.0:80",
-			matchErr:   true,
-		},
-		{
-			desc:       "Valid ClientIP rule matching CIDR IPv6",
-			rule:       "ClientIP(`11::/16`)",
-			remoteAddr: "[11::]:80",
-		},
-		{
-			desc:       "Valid ClientIP rule not matching CIDR IPv6",
-			rule:       "ClientIP(`11::/16`)",
-			remoteAddr: "[10::]:80",
-			matchErr:   true,
-		},
-		{
-			desc:       "Valid ClientIP rule matching multiple CIDR",
-			rule:       "ClientIP(`11.0.0.0/16`, `10.0.0.0/16`)",
-			remoteAddr: "10.0.0.0:80",
-		},
-		{
-			desc:       "Valid ClientIP rule not matching CIDR and matching IP",
-			rule:       "ClientIP(`11.0.0.0/16`, `10.0.0.0`)",
-			remoteAddr: "10.0.0.0:80",
-		},
-		{
-			desc:       "Valid ClientIP rule matching CIDR and not matching IP",
-			rule:       "ClientIP(`11.0.0.0`, `10.0.0.0/16`)",
-			remoteAddr: "10.0.0.0:80",
-		},
 		{
 			desc:       "Valid HostSNI and ClientIP rule matching",
-			rule:       "HostSNI(`foobar`) && ClientIP(`10.0.0.1`)",
-			serverName: "foobar",
+			rule:       "HostSNI(`example.org`) && ClientIP(`10.0.0.1`)",
+			serverName: "example.org",
 			remoteAddr: "10.0.0.1:80",
 		},
 		{
 			desc:       "Valid negative HostSNI and ClientIP rule matching",
-			rule:       "!HostSNI(`bar`) && ClientIP(`10.0.0.1`)",
-			serverName: "foobar",
+			rule:       "!HostSNI(`example.com`) && ClientIP(`10.0.0.1`)",
+			serverName: "example.org",
 			remoteAddr: "10.0.0.1:80",
 		},
 		{
 			desc:       "Valid HostSNI and negative ClientIP rule matching",
-			rule:       "HostSNI(`foobar`) && !ClientIP(`10.0.0.2`)",
-			serverName: "foobar",
+			rule:       "HostSNI(`example.org`) && !ClientIP(`10.0.0.2`)",
+			serverName: "example.org",
 			remoteAddr: "10.0.0.1:80",
 		},
 		{
 			desc:       "Valid negative HostSNI and negative ClientIP rule matching",
-			rule:       "!HostSNI(`bar`) && !ClientIP(`10.0.0.2`)",
-			serverName: "foobar",
+			rule:       "!HostSNI(`example.com`) && !ClientIP(`10.0.0.2`)",
+			serverName: "example.org",
 			remoteAddr: "10.0.0.1:80",
 		},
 		{
 			desc:       "Valid negative HostSNI or negative ClientIP rule matching",
-			rule:       "!(HostSNI(`bar`) || ClientIP(`10.0.0.2`))",
-			serverName: "foobar",
+			rule:       "!(HostSNI(`example.com`) || ClientIP(`10.0.0.2`))",
+			serverName: "example.org",
 			remoteAddr: "10.0.0.1:80",
 		},
 		{
 			desc:       "Valid negative HostSNI and negative ClientIP rule matching",
-			rule:       "!(HostSNI(`bar`) && ClientIP(`10.0.0.2`))",
-			serverName: "foobar",
+			rule:       "!(HostSNI(`example.com`) && ClientIP(`10.0.0.2`))",
+			serverName: "example.org",
 			remoteAddr: "10.0.0.2:80",
 		},
 		{
 			desc:       "Valid negative HostSNI and negative ClientIP rule matching",
-			rule:       "!(HostSNI(`bar`) && ClientIP(`10.0.0.2`))",
-			serverName: "bar",
+			rule:       "!(HostSNI(`example.com`) && ClientIP(`10.0.0.2`))",
+			serverName: "example.com",
 			remoteAddr: "10.0.0.1:80",
 		},
 		{
 			desc:       "Valid negative HostSNI and negative ClientIP rule matching",
-			rule:       "!(HostSNI(`bar`) && ClientIP(`10.0.0.2`))",
-			serverName: "bar",
+			rule:       "!(HostSNI(`example.com`) && ClientIP(`10.0.0.2`))",
+			serverName: "example.com",
 			remoteAddr: "10.0.0.2:80",
 			matchErr:   true,
 		},
 		{
 			desc:       "Valid negative HostSNI and negative ClientIP rule matching",
-			rule:       "!(HostSNI(`bar`) && ClientIP(`10.0.0.2`))",
-			serverName: "foobar",
+			rule:       "!(HostSNI(`example.com`) && ClientIP(`10.0.0.2`))",
+			serverName: "example.org",
 			remoteAddr: "10.0.0.1:80",
 		},
 		{
 			desc:       "Valid HostSNI and ClientIP rule not matching",
-			rule:       "HostSNI(`foobar`) && ClientIP(`10.0.0.1`)",
-			serverName: "bar",
+			rule:       "HostSNI(`example.org`) && ClientIP(`10.0.0.1`)",
+			serverName: "example.com",
 			remoteAddr: "10.0.0.1:80",
 			matchErr:   true,
 		},
 		{
 			desc:       "Valid HostSNI and ClientIP rule not matching",
-			rule:       "HostSNI(`foobar`) && ClientIP(`10.0.0.1`)",
-			serverName: "foobar",
+			rule:       "HostSNI(`example.org`) && ClientIP(`10.0.0.1`)",
+			serverName: "example.org",
 			remoteAddr: "10.0.0.2:80",
 			matchErr:   true,
 		},
 		{
 			desc:       "Valid HostSNI or ClientIP rule matching",
-			rule:       "HostSNI(`foobar`) || ClientIP(`10.0.0.1`)",
-			serverName: "foobar",
+			rule:       "HostSNI(`example.org`) || ClientIP(`10.0.0.1`)",
+			serverName: "example.org",
 			remoteAddr: "10.0.0.1:80",
 		},
 		{
 			desc:       "Valid HostSNI or ClientIP rule matching",
-			rule:       "HostSNI(`foobar`) || ClientIP(`10.0.0.1`)",
-			serverName: "bar",
+			rule:       "HostSNI(`example.org`) || ClientIP(`10.0.0.1`)",
+			serverName: "example.com",
 			remoteAddr: "10.0.0.1:80",
 		},
 		{
 			desc:       "Valid HostSNI or ClientIP rule matching",
-			rule:       "HostSNI(`foobar`) || ClientIP(`10.0.0.1`)",
-			serverName: "foobar",
+			rule:       "HostSNI(`example.org`) || ClientIP(`10.0.0.1`)",
+			serverName: "example.org",
 			remoteAddr: "10.0.0.2:80",
 		},
 		{
 			desc:       "Valid HostSNI or ClientIP rule not matching",
-			rule:       "HostSNI(`foobar`) || ClientIP(`10.0.0.1`)",
-			serverName: "bar",
+			rule:       "HostSNI(`example.org`) || ClientIP(`10.0.0.1`)",
+			serverName: "example.com",
 			remoteAddr: "10.0.0.2:80",
 			matchErr:   true,
 		},
 		{
 			desc:       "Valid HostSNI x 3 OR rule matching",
-			rule:       "HostSNI(`foobar`) || HostSNI(`foo`) || HostSNI(`bar`)",
-			serverName: "foobar",
+			rule:       "HostSNI(`example.org`) || HostSNI(`example.eu`) || HostSNI(`example.com`)",
+			serverName: "example.org",
 		},
 		{
 			desc:       "Valid HostSNI x 3 OR rule not matching",
-			rule:       "HostSNI(`foobar`) || HostSNI(`foo`) || HostSNI(`bar`)",
+			rule:       "HostSNI(`example.org`) || HostSNI(`example.eu`) || HostSNI(`example.com`)",
 			serverName: "baz",
 			matchErr:   true,
 		},
 		{
 			desc:       "Valid HostSNI and ClientIP Combined rule matching",
-			rule:       "HostSNI(`foobar`) || HostSNI(`bar`) && ClientIP(`10.0.0.1`)",
-			serverName: "foobar",
+			rule:       "HostSNI(`example.org`) || HostSNI(`example.com`) && ClientIP(`10.0.0.1`)",
+			serverName: "example.org",
 			remoteAddr: "10.0.0.2:80",
 		},
 		{
 			desc:       "Valid HostSNI and ClientIP Combined rule matching",
-			rule:       "HostSNI(`foobar`) || HostSNI(`bar`) && ClientIP(`10.0.0.1`)",
-			serverName: "bar",
+			rule:       "HostSNI(`example.org`) || HostSNI(`example.com`) && ClientIP(`10.0.0.1`)",
+			serverName: "example.com",
 			remoteAddr: "10.0.0.1:80",
 		},
 		{
 			desc:       "Valid HostSNI and ClientIP Combined rule not matching",
-			rule:       "HostSNI(`foobar`) || HostSNI(`bar`) && ClientIP(`10.0.0.1`)",
-			serverName: "bar",
+			rule:       "HostSNI(`example.org`) || HostSNI(`example.com`) && ClientIP(`10.0.0.1`)",
+			serverName: "example.com",
 			remoteAddr: "10.0.0.2:80",
 			matchErr:   true,
 		},
 		{
 			desc:       "Valid HostSNI and ClientIP Combined rule not matching",
-			rule:       "HostSNI(`foobar`) || HostSNI(`bar`) && ClientIP(`10.0.0.1`)",
+			rule:       "HostSNI(`example.org`) || HostSNI(`example.com`) && ClientIP(`10.0.0.1`)",
 			serverName: "baz",
 			remoteAddr: "10.0.0.1:80",
 			matchErr:   true,
 		},
 		{
 			desc:       "Valid HostSNI and ClientIP complex combined rule matching",
-			rule:       "(HostSNI(`foobar`) || HostSNI(`bar`)) && (ClientIP(`10.0.0.1`) || ClientIP(`10.0.0.2`))",
-			serverName: "bar",
+			rule:       "(HostSNI(`example.org`) || HostSNI(`example.com`)) && (ClientIP(`10.0.0.1`) || ClientIP(`10.0.0.2`))",
+			serverName: "example.com",
 			remoteAddr: "10.0.0.1:80",
 		},
 		{
 			desc:       "Valid HostSNI and ClientIP complex combined rule not matching",
-			rule:       "(HostSNI(`foobar`) || HostSNI(`bar`)) && (ClientIP(`10.0.0.1`) || ClientIP(`10.0.0.2`))",
+			rule:       "(HostSNI(`example.org`) || HostSNI(`example.com`)) && (ClientIP(`10.0.0.1`) || ClientIP(`10.0.0.2`))",
 			serverName: "baz",
 			remoteAddr: "10.0.0.1:80",
 			matchErr:   true,
 		},
 		{
 			desc:       "Valid HostSNI and ClientIP complex combined rule not matching",
-			rule:       "(HostSNI(`foobar`) || HostSNI(`bar`)) && (ClientIP(`10.0.0.1`) || ClientIP(`10.0.0.2`))",
-			serverName: "bar",
+			rule:       "(HostSNI(`example.org`) || HostSNI(`example.com`)) && (ClientIP(`10.0.0.1`) || ClientIP(`10.0.0.2`))",
+			serverName: "example.com",
 			remoteAddr: "10.0.0.3:80",
 			matchErr:   true,
 		},
 		{
 			desc:       "Valid HostSNI and ClientIP more complex (but absurd) combined rule matching",
-			rule:       "(HostSNI(`foobar`) || (HostSNI(`bar`) && !HostSNI(`foobar`))) && ((ClientIP(`10.0.0.1`) && !ClientIP(`10.0.0.2`)) || ClientIP(`10.0.0.2`)) ",
-			serverName: "bar",
+			rule:       "(HostSNI(`example.org`) || (HostSNI(`example.com`) && !HostSNI(`example.org`))) && ((ClientIP(`10.0.0.1`) && !ClientIP(`10.0.0.2`)) || ClientIP(`10.0.0.2`)) ",
+			serverName: "example.com",
 			remoteAddr: "10.0.0.1:80",
 		},
-		{
-			desc:     "Invalid ALPN rule matching ACME-TLS/1",
-			rule:     fmt.Sprintf("ALPN(`%s`)", tlsalpn01.ACMETLS1Protocol),
-			protos:   []string{"foo"},
-			routeErr: true,
-		},
-		{
-			desc:   "Valid ALPN rule matching single protocol",
-			rule:   "ALPN(`foo`)",
-			protos: []string{"foo"},
-		},
-		{
-			desc:     "Valid ALPN rule matching ACME-TLS/1 protocol",
-			rule:     "ALPN(`foo`)",
-			protos:   []string{tlsalpn01.ACMETLS1Protocol},
-			matchErr: true,
-		},
-		{
-			desc:     "Valid ALPN rule not matching single protocol",
-			rule:     "ALPN(`foo`)",
-			protos:   []string{"bar"},
-			matchErr: true,
-		},
-		{
-			desc:   "Valid alternative case ALPN rule matching single protocol without another being supported",
-			rule:   "ALPN(`foo`) && !alpn(`h2`)",
-			protos: []string{"foo", "bar"},
-		},
-		{
-			desc:     "Valid alternative case ALPN rule not matching single protocol because of another being supported",
-			rule:     "ALPN(`foo`) && !alpn(`h2`)",
-			protos:   []string{"foo", "h2", "bar"},
-			matchErr: true,
-		},
 		{
 			desc:       "Valid complex alternative case ALPN and HostSNI rule",
-			rule:       "ALPN(`foo`) && (!alpn(`h2`) || hostsni(`foo`))",
-			protos:     []string{"foo", "bar"},
-			serverName: "foo",
+			rule:       "ALPN(`h2c`) && (!ALPN(`h2`) || HostSNI(`example.eu`))",
+			protos:     []string{"h2c", "mqtt"},
+			serverName: "example.eu",
 		},
 		{
 			desc:       "Valid complex alternative case ALPN and HostSNI rule not matching by SNI",
-			rule:       "ALPN(`foo`) && (!alpn(`h2`) || hostsni(`foo`))",
-			protos:     []string{"foo", "bar", "h2"},
-			serverName: "bar",
+			rule:       "ALPN(`h2c`) && (!ALPN(`h2`) || HostSNI(`example.eu`))",
+			protos:     []string{"h2c", "http/1.1", "h2"},
+			serverName: "example.com",
 			matchErr:   true,
 		},
 		{
 			desc:       "Valid complex alternative case ALPN and HostSNI rule matching by ALPN",
-			rule:       "ALPN(`foo`) && (!alpn(`h2`) || hostsni(`foo`))",
-			protos:     []string{"foo", "bar"},
-			serverName: "bar",
+			rule:       "ALPN(`h2c`) && (!ALPN(`h2`) || HostSNI(`example.eu`))",
+			protos:     []string{"h2c", "http/1.1"},
+			serverName: "example.com",
 		},
 		{
 			desc:       "Valid complex alternative case ALPN and HostSNI rule not matching by protos",
-			rule:       "ALPN(`foo`) && (!alpn(`h2`) || hostsni(`foo`))",
-			protos:     []string{"h2", "bar"},
-			serverName: "bar",
+			rule:       "ALPN(`h2c`) && (!ALPN(`h2`) || HostSNI(`example.eu`))",
+			protos:     []string{"http/1.1", "mqtt"},
+			serverName: "example.com",
 			matchErr:   true,
 		},
 	}
@@ -554,68 +305,56 @@ func Test_addTCPRoute(t *testing.T) {
 	}
 }
 
-type fakeAddr struct {
-	addr string
-}
-
-func (f fakeAddr) String() string {
-	return f.addr
-}
-
-func (f fakeAddr) Network() string {
-	panic("Implement me")
-}
-
 func TestParseHostSNI(t *testing.T) {
 	testCases := []struct {
-		description   string
+		desc          string
 		expression    string
 		domain        []string
 		errorExpected bool
 	}{
 		{
-			description:   "Unknown rule",
-			expression:    "Foobar(`foo.bar`,`test.bar`)",
+			desc:          "Unknown rule",
+			expression:    "Unknown(`example.com`)",
 			errorExpected: true,
 		},
 		{
-			description: "Many hostSNI rules",
-			expression:  "HostSNI(`foo.bar`,`test.bar`)",
-			domain:      []string{"foo.bar", "test.bar"},
+			desc:       "HostSNI rule",
+			expression: "HostSNI(`example.com`)",
+			domain:     []string{"example.com"},
 		},
 		{
-			description: "Many hostSNI rules upper",
-			expression:  "HOSTSNI(`foo.bar`,`test.bar`)",
-			domain:      []string{"foo.bar", "test.bar"},
+			desc:       "HostSNI rule upper",
+			expression: "HOSTSNI(`example.com`)",
+			domain:     []string{"example.com"},
 		},
 		{
-			description: "Many hostSNI rules lower",
-			expression:  "hostsni(`foo.bar`,`test.bar`)",
-			domain:      []string{"foo.bar", "test.bar"},
+			desc:       "HostSNI rule lower",
+			expression: "hostsni(`example.com`)",
+			domain:     []string{"example.com"},
 		},
 		{
-			description: "No hostSNI rule",
-			expression:  "ClientIP(`10.1`)",
+			desc:       "No hostSNI rule",
+			expression: "ClientIP(`10.1`)",
 		},
 		{
-			description: "HostSNI rule and another rule",
-			expression:  "HostSNI(`foo.bar`) && ClientIP(`10.1`)",
-			domain:      []string{"foo.bar"},
+			desc:       "HostSNI rule and another rule",
+			expression: "HostSNI(`example.com`) && ClientIP(`10.1`)",
+			domain:     []string{"example.com"},
 		},
 		{
-			description: "HostSNI rule to lower and another rule",
-			expression:  "HostSNI(`Foo.Bar`) && ClientIP(`10.1`)",
-			domain:      []string{"foo.bar"},
+			desc:       "HostSNI rule to lower and another rule",
+			expression: "HostSNI(`example.com`) && ClientIP(`10.1`)",
+			domain:     []string{"example.com"},
 		},
 		{
-			description: "HostSNI rule with no domain",
-			expression:  "HostSNI() && ClientIP(`10.1`)",
+			desc:       "HostSNI rule with no domain",
+			expression: "HostSNI() && ClientIP(`10.1`)",
 		},
 	}
 
 	for _, test := range testCases {
 		test := test
-		t.Run(test.expression, func(t *testing.T) {
+		t.Run(test.desc, func(t *testing.T) {
 			t.Parallel()
 
 			domains, err := ParseHostSNI(test.expression)
@@ -631,468 +370,48 @@ func TestParseHostSNI(t *testing.T) {
 	}
 }
 
-func Test_HostSNICatchAll(t *testing.T) {
-	testCases := []struct {
-		desc       string
-		rule       string
-		isCatchAll bool
-	}{
-		{
-			desc: "HostSNI(`foobar`) is not catchAll",
-			rule: "HostSNI(`foobar`)",
-		},
-		{
-			desc:       "HostSNI(`*`) is catchAll",
-			rule:       "HostSNI(`*`)",
-			isCatchAll: true,
-		},
-		{
-			desc:       "HOSTSNI(`*`) is catchAll",
-			rule:       "HOSTSNI(`*`)",
-			isCatchAll: true,
-		},
-		{
-			desc:       `HostSNI("*") is catchAll`,
-			rule:       `HostSNI("*")`,
-			isCatchAll: true,
-		},
-	}
-
-	for _, test := range testCases {
-		test := test
-
-		t.Run(test.desc, func(t *testing.T) {
-			t.Parallel()
-
-			muxer, err := NewMuxer()
-			require.NoError(t, err)
-
-			err = muxer.AddRoute(test.rule, 0, tcp.HandlerFunc(func(conn tcp.WriteCloser) {}))
-			require.NoError(t, err)
-
-			handler, catchAll := muxer.Match(ConnData{
-				serverName: "foobar",
-			})
-			require.NotNil(t, handler)
-			assert.Equal(t, test.isCatchAll, catchAll)
-		})
-	}
-}
-
-func Test_HostSNI(t *testing.T) {
-	testCases := []struct {
-		desc       string
-		ruleHosts  []string
-		serverName string
-		buildErr   bool
-		matchErr   bool
-	}{
-		{
-			desc:     "Empty",
-			buildErr: true,
-		},
-		{
-			desc:      "Non ASCII host",
-			ruleHosts: []string{"héhé"},
-			buildErr:  true,
-		},
-		{
-			desc:       "Not Matching hosts",
-			ruleHosts:  []string{"foobar"},
-			serverName: "bar",
-			matchErr:   true,
-		},
-		{
-			desc:       "Matching globing host `*`",
-			ruleHosts:  []string{"*"},
-			serverName: "foobar",
-		},
-		{
-			desc:       "Matching globing host `*` and empty serverName",
-			ruleHosts:  []string{"*"},
-			serverName: "",
-		},
-		{
-			desc:       "Matching globing host `*` and another non matching host",
-			ruleHosts:  []string{"foo", "*"},
-			serverName: "bar",
-		},
-		{
-			desc:       "Matching globing host `*` and another non matching host, and empty servername",
-			ruleHosts:  []string{"foo", "*"},
-			serverName: "",
-			matchErr:   true,
-		},
-		{
-			desc:      "Not Matching globing host with subdomain",
-			ruleHosts: []string{"*.bar"},
-			buildErr:  true,
-		},
-		{
-			desc:       "Not Matching host with trailing dot with ",
-			ruleHosts:  []string{"foobar."},
-			serverName: "foobar.",
-		},
-		{
-			desc:       "Matching host with trailing dot",
-			ruleHosts:  []string{"foobar."},
-			serverName: "foobar",
-		},
-		{
-			desc:       "Matching hosts",
-			ruleHosts:  []string{"foobar"},
-			serverName: "foobar",
-		},
-		{
-			desc:       "Matching hosts with subdomains",
-			ruleHosts:  []string{"foo.bar"},
-			serverName: "foo.bar",
-		},
-	}
-
-	for _, test := range testCases {
-		test := test
-
-		t.Run(test.desc, func(t *testing.T) {
-			t.Parallel()
-
-			matcherTree := &matchersTree{}
-			err := hostSNI(matcherTree, test.ruleHosts...)
-			if test.buildErr {
-				require.Error(t, err)
-				return
-			}
-			require.NoError(t, err)
-
-			meta := ConnData{
-				serverName: test.serverName,
-			}
-
-			assert.Equal(t, test.matchErr, !matcherTree.match(meta))
-		})
-	}
-}
-
-func Test_HostSNIRegexp(t *testing.T) {
-	testCases := []struct {
-		desc        string
-		pattern     string
-		serverNames map[string]bool
-		buildErr    bool
-	}{
-		{
-			desc:     "unbalanced braces",
-			pattern:  "subdomain:(foo\\.)?bar\\.com}",
-			buildErr: true,
-		},
-		{
-			desc:     "empty group name",
-			pattern:  "{:(foo\\.)?bar\\.com}",
-			buildErr: true,
-		},
-		{
-			desc:     "empty capturing group",
-			pattern:  "{subdomain:}",
-			buildErr: true,
-		},
-		{
-			desc:     "malformed capturing group",
-			pattern:  "{subdomain:(foo\\.?bar\\.com}",
-			buildErr: true,
-		},
-		{
-			desc:    "not interpreted as a regexp",
-			pattern: "bar.com",
-			serverNames: map[string]bool{
-				"bar.com": true,
-				"barucom": false,
-			},
-		},
-		{
-			desc:    "capturing group",
-			pattern: "{subdomain:(foo\\.)?bar\\.com}",
-			serverNames: map[string]bool{
-				"foo.bar.com": true,
-				"bar.com":     true,
-				"fooubar.com": false,
-				"barucom":     false,
-				"barcom":      false,
-			},
-		},
-		{
-			desc:    "non capturing group",
-			pattern: "{subdomain:(?:foo\\.)?bar\\.com}",
-			serverNames: map[string]bool{
-				"foo.bar.com": true,
-				"bar.com":     true,
-				"fooubar.com": false,
-				"barucom":     false,
-				"barcom":      false,
-			},
-		},
-		{
-			desc:    "regex insensitive",
-			pattern: "{dummy:[A-Za-z-]+\\.bar\\.com}",
-			serverNames: map[string]bool{
-				"FOO.bar.com": true,
-				"foo.bar.com": true,
-				"fooubar.com": false,
-				"barucom":     false,
-				"barcom":      false,
-			},
-		},
-		{
-			desc:    "insensitive host",
-			pattern: "{dummy:[a-z-]+\\.bar\\.com}",
-			serverNames: map[string]bool{
-				"FOO.bar.com": true,
-				"foo.bar.com": true,
-				"fooubar.com": false,
-				"barucom":     false,
-				"barcom":      false,
-			},
-		},
-		{
-			desc:    "insensitive host simple",
-			pattern: "foo.bar.com",
-			serverNames: map[string]bool{
-				"FOO.bar.com": true,
-				"foo.bar.com": true,
-				"fooubar.com": false,
-				"barucom":     false,
-				"barcom":      false,
-			},
-		},
-	}
-
-	for _, test := range testCases {
-		test := test
-
-		t.Run(test.desc, func(t *testing.T) {
-			t.Parallel()
-
-			matchersTree := &matchersTree{}
-			err := hostSNIRegexp(matchersTree, test.pattern)
-			if test.buildErr {
-				require.Error(t, err)
-				return
-			}
-			require.NoError(t, err)
-
-			for serverName, match := range test.serverNames {
-				meta := ConnData{
-					serverName: serverName,
-				}
-
-				assert.Equal(t, match, matchersTree.match(meta))
-			}
-		})
-	}
-}
-
-func Test_ClientIP(t *testing.T) {
-	testCases := []struct {
-		desc      string
-		ruleCIDRs []string
-		remoteIP  string
-		buildErr  bool
-		matchErr  bool
-	}{
-		{
-			desc:     "Empty",
-			buildErr: true,
-		},
-		{
-			desc:      "Malformed CIDR",
-			ruleCIDRs: []string{"héhé"},
-			buildErr:  true,
-		},
-		{
-			desc:      "Not matching empty remote IP",
-			ruleCIDRs: []string{"20.20.20.20"},
-			matchErr:  true,
-		},
-		{
-			desc:      "Not matching IP",
-			ruleCIDRs: []string{"20.20.20.20"},
-			remoteIP:  "10.10.10.10",
-			matchErr:  true,
-		},
-		{
-			desc:      "Matching IP",
-			ruleCIDRs: []string{"10.10.10.10"},
-			remoteIP:  "10.10.10.10",
-		},
-		{
-			desc:      "Not matching multiple IPs",
-			ruleCIDRs: []string{"20.20.20.20", "30.30.30.30"},
-			remoteIP:  "10.10.10.10",
-			matchErr:  true,
-		},
-		{
-			desc:      "Matching multiple IPs",
-			ruleCIDRs: []string{"10.10.10.10", "20.20.20.20", "30.30.30.30"},
-			remoteIP:  "20.20.20.20",
-		},
-		{
-			desc:      "Not matching CIDR",
-			ruleCIDRs: []string{"20.0.0.0/24"},
-			remoteIP:  "10.10.10.10",
-			matchErr:  true,
-		},
-		{
-			desc:      "Matching CIDR",
-			ruleCIDRs: []string{"20.0.0.0/8"},
-			remoteIP:  "20.10.10.10",
-		},
-		{
-			desc:      "Not matching multiple CIDRs",
-			ruleCIDRs: []string{"10.0.0.0/24", "20.0.0.0/24"},
-			remoteIP:  "10.10.10.10",
-			matchErr:  true,
-		},
-		{
-			desc:      "Matching multiple CIDRs",
-			ruleCIDRs: []string{"10.0.0.0/8", "20.0.0.0/8"},
-			remoteIP:  "20.10.10.10",
-		},
-	}
-
-	for _, test := range testCases {
-		test := test
-
-		t.Run(test.desc, func(t *testing.T) {
-			t.Parallel()
-
-			matchersTree := &matchersTree{}
-			err := clientIP(matchersTree, test.ruleCIDRs...)
-			if test.buildErr {
-				require.Error(t, err)
-				return
-			}
-			require.NoError(t, err)
-
-			meta := ConnData{
-				remoteIP: test.remoteIP,
-			}
-
-			assert.Equal(t, test.matchErr, !matchersTree.match(meta))
-		})
-	}
-}
-
-func Test_ALPN(t *testing.T) {
-	testCases := []struct {
-		desc           string
-		ruleALPNProtos []string
-		connProto      string
-		buildErr       bool
-		matchErr       bool
-	}{
-		{
-			desc:     "Empty",
-			buildErr: true,
-		},
-		{
-			desc:           "ACME TLS proto",
-			ruleALPNProtos: []string{tlsalpn01.ACMETLS1Protocol},
-			buildErr:       true,
-		},
-		{
-			desc:           "Not matching empty proto",
-			ruleALPNProtos: []string{"h2"},
-			matchErr:       true,
-		},
-		{
-			desc:           "Not matching ALPN",
-			ruleALPNProtos: []string{"h2"},
-			connProto:      "mqtt",
-			matchErr:       true,
-		},
-		{
-			desc:           "Matching ALPN",
-			ruleALPNProtos: []string{"h2"},
-			connProto:      "h2",
-		},
-		{
-			desc:           "Not matching multiple ALPNs",
-			ruleALPNProtos: []string{"h2", "mqtt"},
-			connProto:      "h2c",
-			matchErr:       true,
-		},
-		{
-			desc:           "Matching multiple ALPNs",
-			ruleALPNProtos: []string{"h2", "h2c", "mqtt"},
-			connProto:      "h2c",
-		},
-	}
-
-	for _, test := range testCases {
-		test := test
-
-		t.Run(test.desc, func(t *testing.T) {
-			t.Parallel()
-
-			matchersTree := &matchersTree{}
-			err := alpn(matchersTree, test.ruleALPNProtos...)
-			if test.buildErr {
-				require.Error(t, err)
-				return
-			}
-			require.NoError(t, err)
-
-			meta := ConnData{
-				alpnProtos: []string{test.connProto},
-			}
-
-			assert.Equal(t, test.matchErr, !matchersTree.match(meta))
-		})
-	}
-}
-
 func Test_Priority(t *testing.T) {
 	testCases := []struct {
 		desc         string
 		rules        map[string]int
 		serverName   string
-		remoteIP     string
 		expectedRule string
 	}{
 		{
 			desc: "One matching rule, calculated priority",
 			rules: map[string]int{
-				"HostSNI(`bar`)":    0,
-				"HostSNI(`foobar`)": 0,
+				"HostSNI(`example.com`)": 0,
+				"HostSNI(`example.org`)": 0,
 			},
-			expectedRule: "HostSNI(`bar`)",
-			serverName:   "bar",
+			expectedRule: "HostSNI(`example.com`)",
+			serverName:   "example.com",
 		},
 		{
 			desc: "One matching rule, custom priority",
 			rules: map[string]int{
-				"HostSNI(`foobar`)": 0,
-				"HostSNI(`bar`)":    10000,
+				"HostSNI(`example.org`)": 0,
+				"HostSNI(`example.com`)": 10000,
 			},
-			expectedRule: "HostSNI(`foobar`)",
-			serverName:   "foobar",
+			expectedRule: "HostSNI(`example.org`)",
+			serverName:   "example.org",
 		},
 		{
 			desc: "Two matching rules, calculated priority",
 			rules: map[string]int{
-				"HostSNI(`foobar`)":        0,
-				"HostSNI(`foobar`, `bar`)": 0,
+				"HostSNI(`example.org`)": 0,
+				"HostSNI(`example.com`)": 0,
 			},
-			expectedRule: "HostSNI(`foobar`, `bar`)",
-			serverName:   "foobar",
+			expectedRule: "HostSNI(`example.org`)",
+			serverName:   "example.org",
 		},
 		{
 			desc: "Two matching rules, custom priority",
 			rules: map[string]int{
-				"HostSNI(`foobar`)":        10000,
-				"HostSNI(`foobar`, `bar`)": 0,
+				"HostSNI(`example.com`)": 10000,
+				"HostSNI(`example.org`)": 0,
 			},
-			expectedRule: "HostSNI(`foobar`)",
-			serverName:   "foobar",
+			expectedRule: "HostSNI(`example.com`)",
+			serverName:   "example.com",
 		},
 	}
 
@@ -1116,7 +435,6 @@ func Test_Priority(t *testing.T) {
 
 			handler, _ := muxer.Match(ConnData{
 				serverName: test.serverName,
-				remoteIP:   test.remoteIP,
 			})
 			require.NotNil(t, handler)
 
@@ -1125,3 +443,57 @@ func Test_Priority(t *testing.T) {
 		})
 	}
 }
+
+type fakeConn struct {
+	call       map[string]int
+	remoteAddr net.Addr
+}
+
+func (f *fakeConn) Read(b []byte) (n int, err error) {
+	panic("implement me")
+}
+
+func (f *fakeConn) Write(b []byte) (n int, err error) {
+	f.call[string(b)]++
+	return len(b), nil
+}
+
+func (f *fakeConn) Close() error {
+	panic("implement me")
+}
+
+func (f *fakeConn) LocalAddr() net.Addr {
+	panic("implement me")
+}
+
+func (f *fakeConn) RemoteAddr() net.Addr {
+	return f.remoteAddr
+}
+
+func (f *fakeConn) SetDeadline(t time.Time) error {
+	panic("implement me")
+}
+
+func (f *fakeConn) SetReadDeadline(t time.Time) error {
+	panic("implement me")
+}
+
+func (f *fakeConn) SetWriteDeadline(t time.Time) error {
+	panic("implement me")
+}
+
+func (f *fakeConn) CloseWrite() error {
+	panic("implement me")
+}
+
+type fakeAddr struct {
+	addr string
+}
+
+func (f fakeAddr) String() string {
+	return f.addr
+}
+
+func (f fakeAddr) Network() string {
+	panic("Implement me")
+}
diff --git a/pkg/provider/kubernetes/gateway/kubernetes_test.go b/pkg/provider/kubernetes/gateway/kubernetes_test.go
index 8037014769..44b3ce2492 100644
--- a/pkg/provider/kubernetes/gateway/kubernetes_test.go
+++ b/pkg/provider/kubernetes/gateway/kubernetes_test.go
@@ -744,15 +744,15 @@ func TestLoadHTTPRoutes(t *testing.T) {
 				},
 				HTTP: &dynamic.HTTPConfiguration{
 					Routers: map[string]*dynamic.Router{
-						"default-http-app-1-my-gateway-web-75dd1ad561e42725558a": {
+						"default-http-app-1-my-gateway-web-66e726cd8903b49727ae": {
 							EntryPoints: []string{"web"},
-							Service:     "default-http-app-1-my-gateway-web-75dd1ad561e42725558a-wrr",
-							Rule:        "Host(`foo.com`, `bar.com`) && PathPrefix(`/`)",
+							Service:     "default-http-app-1-my-gateway-web-66e726cd8903b49727ae-wrr",
+							Rule:        "(Host(`foo.com`) || Host(`bar.com`)) && PathPrefix(`/`)",
 						},
 					},
 					Middlewares: map[string]*dynamic.Middleware{},
 					Services: map[string]*dynamic.Service{
-						"default-http-app-1-my-gateway-web-75dd1ad561e42725558a-wrr": {
+						"default-http-app-1-my-gateway-web-66e726cd8903b49727ae-wrr": {
 							Weighted: &dynamic.WeightedRoundRobin{
 								Services: []dynamic.WRRService{
 									{
@@ -802,15 +802,15 @@ func TestLoadHTTPRoutes(t *testing.T) {
 				},
 				HTTP: &dynamic.HTTPConfiguration{
 					Routers: map[string]*dynamic.Router{
-						"default-http-app-1-my-gateway-web-2dbd7883f5537db39bca": {
+						"default-http-app-1-my-gateway-web-3b78e2feb3295ddd87f0": {
 							EntryPoints: []string{"web"},
-							Service:     "default-http-app-1-my-gateway-web-2dbd7883f5537db39bca-wrr",
-							Rule:        "(Host(`foo.com`) || HostRegexp(`{subdomain:[a-zA-Z0-9-]+}.bar.com`)) && PathPrefix(`/`)",
+							Service:     "default-http-app-1-my-gateway-web-3b78e2feb3295ddd87f0-wrr",
+							Rule:        "(Host(`foo.com`) || HostRegexp(`^[a-zA-Z0-9-]+\\.bar\\.com$`)) && PathPrefix(`/`)",
 						},
 					},
 					Middlewares: map[string]*dynamic.Middleware{},
 					Services: map[string]*dynamic.Service{
-						"default-http-app-1-my-gateway-web-2dbd7883f5537db39bca-wrr": {
+						"default-http-app-1-my-gateway-web-3b78e2feb3295ddd87f0-wrr": {
 							Weighted: &dynamic.WeightedRoundRobin{
 								Services: []dynamic.WRRService{
 									{
@@ -860,15 +860,15 @@ func TestLoadHTTPRoutes(t *testing.T) {
 				},
 				HTTP: &dynamic.HTTPConfiguration{
 					Routers: map[string]*dynamic.Router{
-						"default-http-app-1-my-gateway-web-eb1490f180299bf5ed29": {
+						"default-http-app-1-my-gateway-web-b0521a61fb43068694b4": {
 							EntryPoints: []string{"web"},
-							Service:     "default-http-app-1-my-gateway-web-eb1490f180299bf5ed29-wrr",
-							Rule:        "(Host(`foo.com`) || HostRegexp(`{subdomain:[a-zA-Z0-9-]+}.foo.com`)) && PathPrefix(`/`)",
+							Service:     "default-http-app-1-my-gateway-web-b0521a61fb43068694b4-wrr",
+							Rule:        "(Host(`foo.com`) || HostRegexp(`^[a-zA-Z0-9-]+\\.foo\\.com$`)) && PathPrefix(`/`)",
 						},
 					},
 					Middlewares: map[string]*dynamic.Middleware{},
 					Services: map[string]*dynamic.Service{
-						"default-http-app-1-my-gateway-web-eb1490f180299bf5ed29-wrr": {
+						"default-http-app-1-my-gateway-web-b0521a61fb43068694b4-wrr": {
 							Weighted: &dynamic.WeightedRoundRobin{
 								Services: []dynamic.WRRService{
 									{
@@ -3011,10 +3011,10 @@ func TestLoadTLSRoutes(t *testing.T) {
 				},
 				TCP: &dynamic.TCPConfiguration{
 					Routers: map[string]*dynamic.TCPRouter{
-						"default-tls-app-1-my-gateway-tls-339184c3296a9c2c39fa": {
+						"default-tls-app-1-my-gateway-tls-dfc5c7506ac1b172c8b7": {
 							EntryPoints: []string{"tls"},
-							Service:     "default-tls-app-1-my-gateway-tls-339184c3296a9c2c39fa-wrr-0",
-							Rule:        "HostSNI(`foo.example.com`,`bar.example.com`)",
+							Service:     "default-tls-app-1-my-gateway-tls-dfc5c7506ac1b172c8b7-wrr-0",
+							Rule:        "(HostSNI(`foo.example.com`) || HostSNI(`bar.example.com`))",
 							TLS: &dynamic.RouterTCPTLSConfig{
 								Passthrough: true,
 							},
@@ -3022,7 +3022,7 @@ func TestLoadTLSRoutes(t *testing.T) {
 					},
 					Middlewares: map[string]*dynamic.TCPMiddleware{},
 					Services: map[string]*dynamic.TCPService{
-						"default-tls-app-1-my-gateway-tls-339184c3296a9c2c39fa-wrr-0": {
+						"default-tls-app-1-my-gateway-tls-dfc5c7506ac1b172c8b7-wrr-0": {
 							Weighted: &dynamic.TCPWeightedRoundRobin{
 								Services: []dynamic.TCPWRRService{
 									{
@@ -4362,7 +4362,7 @@ func Test_hostRule(t *testing.T) {
 				"Bar",
 				"Bir",
 			},
-			expectedRule: "Host(`Foo`, `Bar`, `Bir`)",
+			expectedRule: "(Host(`Foo`) || Host(`Bar`) || Host(`Bir`))",
 		},
 		{
 			desc: "Multiple Hosts with empty one",
@@ -4389,14 +4389,14 @@ func Test_hostRule(t *testing.T) {
 				"bar.foo",
 				"foo.foo",
 			},
-			expectedRule: "(Host(`bar.foo`, `foo.foo`) || HostRegexp(`{subdomain:[a-zA-Z0-9-]+}.bar.foo`))",
+			expectedRule: "(HostRegexp(`^[a-zA-Z0-9-]+\\.bar\\.foo$`) || Host(`bar.foo`) || Host(`foo.foo`))",
 		},
 		{
 			desc: "Host with wildcard",
 			hostnames: []v1alpha2.Hostname{
 				"*.bar.foo",
 			},
-			expectedRule: "HostRegexp(`{subdomain:[a-zA-Z0-9-]+}.bar.foo`)",
+			expectedRule: "HostRegexp(`^[a-zA-Z0-9-]+\\.bar\\.foo$`)",
 		},
 		{
 			desc: "Alone wildcard",
@@ -4708,7 +4708,7 @@ func Test_hostSNIRule(t *testing.T) {
 		{
 			desc:         "Some empty hostnames",
 			hostnames:    []v1alpha2.Hostname{"foo", "", "bar"},
-			expectedRule: "HostSNI(`foo`,`bar`)",
+			expectedRule: "(HostSNI(`foo`) || HostSNI(`bar`))",
 		},
 		{
 			desc:         "Valid hostname",
@@ -4718,12 +4718,12 @@ func Test_hostSNIRule(t *testing.T) {
 		{
 			desc:         "Multiple valid hostnames",
 			hostnames:    []v1alpha2.Hostname{"foo", "bar"},
-			expectedRule: "HostSNI(`foo`,`bar`)",
+			expectedRule: "(HostSNI(`foo`) || HostSNI(`bar`))",
 		},
 		{
 			desc:         "Multiple overlapping hostnames",
 			hostnames:    []v1alpha2.Hostname{"foo", "bar", "foo", "baz"},
-			expectedRule: "HostSNI(`foo`,`bar`,`baz`)",
+			expectedRule: "(HostSNI(`foo`) || HostSNI(`bar`) || HostSNI(`baz`))",
 		},
 	}
 
diff --git a/pkg/provider/kubernetes/ingress/kubernetes_test.go b/pkg/provider/kubernetes/ingress/kubernetes_test.go
index d1b76ec8a2..3b032f9961 100644
--- a/pkg/provider/kubernetes/ingress/kubernetes_test.go
+++ b/pkg/provider/kubernetes/ingress/kubernetes_test.go
@@ -189,8 +189,8 @@ func TestLoadConfigurationFromIngresses(t *testing.T) {
 				HTTP: &dynamic.HTTPConfiguration{
 					Middlewares: map[string]*dynamic.Middleware{},
 					Routers: map[string]*dynamic.Router{
-						"testing-bar-bar-3be6cfd7daba66cf2fdd": {
-							Rule:    "HostRegexp(`{subdomain:[a-zA-Z0-9-]+}.bar`) && PathPrefix(`/bar`)",
+						"testing-bar-bar-aba9a7d00e9b06a78e16": {
+							Rule:    "HostRegexp(`^[a-zA-Z0-9-]+\\.bar$`) && PathPrefix(`/bar`)",
 							Service: "testing-service1-80",
 						},
 						"testing-bar-bar-636bf36c00fedaab3d44": {
@@ -1104,7 +1104,7 @@ func TestLoadConfigurationFromIngresses(t *testing.T) {
 					Middlewares: map[string]*dynamic.Middleware{},
 					Routers: map[string]*dynamic.Router{
 						"testing-foobar-com-bar": {
-							Rule:    "HostRegexp(`{subdomain:[a-zA-Z0-9-]+}.foobar.com`) && PathPrefix(`/bar`)",
+							Rule:    "HostRegexp(`^[a-zA-Z0-9-]+\\.foobar\\.com$`) && PathPrefix(`/bar`)",
 							Service: "testing-service1-80",
 						},
 					},