Repository: traefik/traefik (language: go)

Issue:
K8s Gateway API rule priority
### 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?

Hi,
I'm working on a system where TCP services are dynamically spawned via a controller and routed via TLS SNI using a TLSRoute like the following
```yaml
apiVersion: gateway.networking.k8s.io/v1alpha2
kind: TLSRoute
spec:
  hostnames:
  - whoami-tcp.apps.local
  parentRefs:
  - group: gateway.networking.k8s.io
    kind: Gateway
    name: my-gateway
    sectionName: tls
  rules:
  - backendRefs:
    - kind: Service
      name: my-backend
      namespace: my-ns
      port: 8080
```
Everything worked fine until I added a catch-all service that should handle connections to non-registered SNIs, the service has the following config
```yaml
apiVersion: gateway.networking.k8s.io/v1alpha2
kind: TLSRoute
spec:
  hostnames:
  - "*.apps.local"
  parentRefs:
  - group: gateway.networking.k8s.io
    kind: Gateway
    name: my-gateway
    sectionName: tls
  rules:
    - backendRefs:
        - name: catchall-svc
          kind: Service
          port: 1337
```
After adding that all the requests were routed to the catch-all service, even the ones to `whoami-tcp.apps.local`.
I looked into Traefik dashboard and figured out it was caused by the catch-all rule having higher priority over the specific SNI rule.
Would it be possible to have some way of overriding the rule priority?
Gateway API doesn't offer that possibility by itself but I guess it could be implemented using a label on the various *Route objects.
Please let me hear your thought about this, if there's enough consensus I might work myself on a PR addressing this


Discussion / hints:
Hello @augustozanellato,

Thanks for reporting this and opening https://github.com/kubernetes-sigs/gateway-api/issues/3541!

> Would it be possible to have some way of overriding the rule priority?
Gateway API doesn't offer that possibility by itself but I guess it could be implemented using a label on the various *Route objects.
Please let me hear your thought about this, if there's enough consensus I might work myself on a PR addressing this

We do not want to introduce a label for this.

Without more specific details from the Gateway API spec on how priority should be evaluated between `TLSRoutes`, we think we could replicate what is already specified for HTTPRoute and hostnames.

If you would like to contribute to this, we would welcome a pull request.
>  If you would like to contribute to this, we would welcome a pull request.

Just made a PR!

Fix patch:
diff --git a/pkg/provider/kubernetes/gateway/tlsroute.go b/pkg/provider/kubernetes/gateway/tlsroute.go
index 8c8e1cb76a..d481ccacde 100644
--- a/pkg/provider/kubernetes/gateway/tlsroute.go
+++ b/pkg/provider/kubernetes/gateway/tlsroute.go
@@ -122,9 +122,11 @@ func (p *Provider) loadTLSRoute(listener gatewayListener, route *gatev1alpha2.TL
 			continue
 		}
 
+		rule, priority := hostSNIRule(hostnames)
 		router := dynamic.TCPRouter{
 			RuleSyntax:  "v3",
-			Rule:        hostSNIRule(hostnames),
+			Rule:        rule,
+			Priority:    priority,
 			EntryPoints: []string{listener.EPName},
 			TLS: &dynamic.RouterTCPTLSConfig{
 				Passthrough: listener.TLS != nil && listener.TLS.Mode != nil && *listener.TLS.Mode == gatev1.TLSModePassthrough,
@@ -298,7 +300,9 @@ func (p *Provider) loadTLSServers(namespace string, route *gatev1alpha2.TLSRoute
 	return lb, nil
 }
 
-func hostSNIRule(hostnames []gatev1.Hostname) string {
+func hostSNIRule(hostnames []gatev1.Hostname) (string, int) {
+	var priority int
+
 	rules := make([]string, 0, len(hostnames))
 	uniqHostnames := map[gatev1.Hostname]struct{}{}
 
@@ -307,14 +311,21 @@ func hostSNIRule(hostnames []gatev1.Hostname) string {
 			continue
 		}
 
+		host := string(hostname)
+		wildcard := strings.Count(host, "*")
+
+		thisPriority := len(hostname) - wildcard
+
+		if priority < thisPriority {
+			priority = thisPriority
+		}
+
 		if _, exists := uniqHostnames[hostname]; exists {
 			continue
 		}
 
-		host := string(hostname)
 		uniqHostnames[hostname] = struct{}{}
 
-		wildcard := strings.Count(host, "*")
 		if wildcard == 0 {
 			rules = append(rules, fmt.Sprintf("HostSNI(`%s`)", host))
 			continue
@@ -325,8 +336,8 @@ func hostSNIRule(hostnames []gatev1.Hostname) string {
 	}
 
 	if len(hostnames) == 0 || len(rules) == 0 {
-		return "HostSNI(`*`)"
+		return "HostSNI(`*`)", 0
 	}
 
-	return strings.Join(rules, " || ")
+	return strings.Join(rules, " || "), priority
 }


Test patch:
diff --git a/integration/testdata/rawdata-gateway.json b/integration/testdata/rawdata-gateway.json
index 6ed9494404..c51f00a395 100644
--- a/integration/testdata/rawdata-gateway.json
+++ b/integration/testdata/rawdata-gateway.json
@@ -186,7 +186,7 @@
 			"service": "tlsroute-default-tls-app-1-gw-default-my-tls-gateway-ep-footlspassthrough-0-e3b0c44298fc1c149afb-wrr",
 			"rule": "HostSNI(`foo.bar`)",
 			"ruleSyntax": "v3",
-			"priority": 18,
+			"priority": 7,
 			"tls": {
 				"passthrough": true
 			},
diff --git a/pkg/provider/kubernetes/gateway/kubernetes_test.go b/pkg/provider/kubernetes/gateway/kubernetes_test.go
index c25a4b4c05..183fe4dae2 100644
--- a/pkg/provider/kubernetes/gateway/kubernetes_test.go
+++ b/pkg/provider/kubernetes/gateway/kubernetes_test.go
@@ -4582,6 +4582,7 @@ func TestLoadTLSRoutes(t *testing.T) {
 					Routers: map[string]*dynamic.TCPRouter{
 						"tlsroute-default-tls-app-1-gw-default-my-gateway-ep-TCP-0-e3b0c44298fc1c149afb": {
 							EntryPoints: []string{"TCP"},
+							Priority:    0,
 							Rule:        "HostSNI(`*`)",
 							RuleSyntax:  "v3",
 							Service:     "tlsroute-default-tls-app-1-gw-default-my-gateway-ep-TCP-0-e3b0c44298fc1c149afb-wrr",
@@ -4799,6 +4800,7 @@ func TestLoadTLSRoutes(t *testing.T) {
 						"tlsroute-default-tls-app-1-gw-default-my-tls-gateway-ep-tcp-0-e3b0c44298fc1c149afb": {
 							EntryPoints: []string{"tcp"},
 							Service:     "tlsroute-default-tls-app-1-gw-default-my-tls-gateway-ep-tcp-0-e3b0c44298fc1c149afb-wrr",
+							Priority:    15,
 							Rule:        "HostSNI(`foo.example.com`)",
 							RuleSyntax:  "v3",
 							TLS: &dynamic.RouterTCPTLSConfig{
@@ -4866,6 +4868,7 @@ func TestLoadTLSRoutes(t *testing.T) {
 						"tlsroute-default-tls-app-1-gw-default-my-tls-gateway-ep-tcp-0-e3b0c44298fc1c149afb": {
 							EntryPoints: []string{"tcp"},
 							Service:     "tlsroute-default-tls-app-1-gw-default-my-tls-gateway-ep-tcp-0-e3b0c44298fc1c149afb-wrr",
+							Priority:    0,
 							Rule:        "HostSNI(`*`)",
 							RuleSyntax:  "v3",
 							TLS: &dynamic.RouterTCPTLSConfig{
@@ -5026,6 +5029,7 @@ func TestLoadTLSRoutes(t *testing.T) {
 						"tlsroute-default-tls-app-1-gw-default-my-gateway-ep-tls-0-e3b0c44298fc1c149afb": {
 							EntryPoints: []string{"tls"},
 							Service:     "tlsroute-default-tls-app-1-gw-default-my-gateway-ep-tls-0-e3b0c44298fc1c149afb-wrr",
+							Priority:    15,
 							Rule:        "HostSNI(`foo.example.com`)",
 							RuleSyntax:  "v3",
 							TLS: &dynamic.RouterTCPTLSConfig{
@@ -5085,6 +5089,7 @@ func TestLoadTLSRoutes(t *testing.T) {
 						"tlsroute-default-tls-app-1-gw-default-my-gateway-ep-tls-0-e3b0c44298fc1c149afb": {
 							EntryPoints: []string{"tls"},
 							Service:     "tlsroute-default-tls-app-1-gw-default-my-gateway-ep-tls-0-e3b0c44298fc1c149afb-wrr",
+							Priority:    15,
 							Rule:        "HostSNI(`foo.example.com`)",
 							RuleSyntax:  "v3",
 							TLS: &dynamic.RouterTCPTLSConfig{
@@ -5144,6 +5149,7 @@ func TestLoadTLSRoutes(t *testing.T) {
 						"tlsroute-default-tls-app-1-gw-default-my-gateway-ep-tls-0-e3b0c44298fc1c149afb": {
 							EntryPoints: []string{"tls"},
 							Service:     "tlsroute-default-tls-app-1-gw-default-my-gateway-ep-tls-0-e3b0c44298fc1c149afb-wrr",
+							Priority:    15,
 							Rule:        "HostSNI(`foo.example.com`)",
 							RuleSyntax:  "v3",
 							TLS: &dynamic.RouterTCPTLSConfig{
@@ -5203,6 +5209,7 @@ func TestLoadTLSRoutes(t *testing.T) {
 						"tlsroute-default-tls-app-1-gw-default-my-gateway-ep-tls-0-e3b0c44298fc1c149afb": {
 							EntryPoints: []string{"tls"},
 							Service:     "tlsroute-default-tls-app-1-gw-default-my-gateway-ep-tls-0-e3b0c44298fc1c149afb-wrr",
+							Priority:    15,
 							Rule:        "HostSNI(`foo.example.com`) || HostSNI(`bar.example.com`)",
 							RuleSyntax:  "v3",
 							TLS: &dynamic.RouterTCPTLSConfig{
@@ -5262,6 +5269,7 @@ func TestLoadTLSRoutes(t *testing.T) {
 						"tlsroute-default-tls-app-default-gw-default-my-gateway-ep-tls-0-e3b0c44298fc1c149afb": {
 							EntryPoints: []string{"tls"},
 							Service:     "tlsroute-default-tls-app-default-gw-default-my-gateway-ep-tls-0-e3b0c44298fc1c149afb-wrr",
+							Priority:    11,
 							Rule:        "HostSNI(`foo.default`)",
 							RuleSyntax:  "v3",
 							TLS: &dynamic.RouterTCPTLSConfig{
@@ -5321,6 +5329,7 @@ func TestLoadTLSRoutes(t *testing.T) {
 						"tlsroute-default-tls-app-default-gw-default-my-gateway-ep-tls-0-e3b0c44298fc1c149afb": {
 							EntryPoints: []string{"tls"},
 							Service:     "tlsroute-default-tls-app-default-gw-default-my-gateway-ep-tls-0-e3b0c44298fc1c149afb-wrr",
+							Priority:    11,
 							Rule:        "HostSNI(`foo.default`)",
 							RuleSyntax:  "v3",
 							TLS: &dynamic.RouterTCPTLSConfig{
@@ -5330,6 +5339,7 @@ func TestLoadTLSRoutes(t *testing.T) {
 						"tlsroute-bar-tls-app-bar-gw-default-my-gateway-ep-tls-0-e3b0c44298fc1c149afb": {
 							EntryPoints: []string{"tls"},
 							Service:     "tlsroute-bar-tls-app-bar-gw-default-my-gateway-ep-tls-0-e3b0c44298fc1c149afb-wrr",
+							Priority:    7,
 							Rule:        "HostSNI(`foo.bar`)",
 							RuleSyntax:  "v3",
 							TLS: &dynamic.RouterTCPTLSConfig{
@@ -5411,6 +5421,7 @@ func TestLoadTLSRoutes(t *testing.T) {
 						"tlsroute-bar-tls-app-bar-gw-default-my-gateway-ep-tls-0-e3b0c44298fc1c149afb": {
 							EntryPoints: []string{"tls"},
 							Service:     "tlsroute-bar-tls-app-bar-gw-default-my-gateway-ep-tls-0-e3b0c44298fc1c149afb-wrr",
+							Priority:    7,
 							Rule:        "HostSNI(`foo.bar`)",
 							RuleSyntax:  "v3",
 							TLS: &dynamic.RouterTCPTLSConfig{
@@ -5470,6 +5481,7 @@ func TestLoadTLSRoutes(t *testing.T) {
 						"tlsroute-default-tls-app-gw-default-my-gateway-ep-tcp-1-0-e3b0c44298fc1c149afb": {
 							EntryPoints: []string{"tcp-1"},
 							Service:     "tlsroute-default-tls-app-gw-default-my-gateway-ep-tcp-1-0-e3b0c44298fc1c149afb-wrr",
+							Priority:    0,
 							Rule:        "HostSNI(`*`)",
 							RuleSyntax:  "v3",
 							TLS: &dynamic.RouterTCPTLSConfig{
@@ -5479,6 +5491,7 @@ func TestLoadTLSRoutes(t *testing.T) {
 						"tlsroute-default-tls-app-gw-default-my-gateway-ep-tcp-1-1-e3b0c44298fc1c149afb": {
 							EntryPoints: []string{"tcp-1"},
 							Service:     "tlsroute-default-tls-app-gw-default-my-gateway-ep-tcp-1-1-e3b0c44298fc1c149afb-wrr",
+							Priority:    0,
 							Rule:        "HostSNI(`*`)",
 							RuleSyntax:  "v3",
 							TLS: &dynamic.RouterTCPTLSConfig{
@@ -5561,6 +5574,7 @@ func TestLoadTLSRoutes(t *testing.T) {
 						"tlsroute-default-tls-app-1-gw-default-my-tls-gateway-ep-tcp-0-e3b0c44298fc1c149afb": {
 							EntryPoints: []string{"tcp"},
 							Service:     "tlsroute-default-tls-app-1-gw-default-my-tls-gateway-ep-tcp-0-e3b0c44298fc1c149afb-wrr",
+							Priority:    15,
 							Rule:        "HostSNI(`foo.example.com`)",
 							RuleSyntax:  "v3",
 							TLS: &dynamic.RouterTCPTLSConfig{
@@ -5617,6 +5631,7 @@ func TestLoadTLSRoutes(t *testing.T) {
 						"tlsroute-default-tls-app-1-gw-default-my-tls-gateway-ep-tcp-0-e3b0c44298fc1c149afb": {
 							EntryPoints: []string{"tcp"},
 							Service:     "tlsroute-default-tls-app-1-gw-default-my-tls-gateway-ep-tcp-0-e3b0c44298fc1c149afb-wrr",
+							Priority:    15,
 							Rule:        "HostSNI(`foo.example.com`)",
 							RuleSyntax:  "v3",
 							TLS: &dynamic.RouterTCPTLSConfig{
@@ -5839,6 +5854,7 @@ func TestLoadMixedRoutes(t *testing.T) {
 						"tlsroute-default-tls-app-1-gw-default-my-gateway-ep-tls-2-0-e3b0c44298fc1c149afb": {
 							EntryPoints: []string{"tls-2"},
 							Service:     "tlsroute-default-tls-app-1-gw-default-my-gateway-ep-tls-2-0-e3b0c44298fc1c149afb-wrr",
+							Priority:    24,
 							Rule:        "HostSNI(`pass.tls.foo.example.com`)",
 							RuleSyntax:  "v3",
 							TLS: &dynamic.RouterTCPTLSConfig{
@@ -6026,6 +6042,7 @@ func TestLoadMixedRoutes(t *testing.T) {
 						"tlsroute-default-tls-app-default-gw-default-my-gateway-ep-tls-2-0-e3b0c44298fc1c149afb": {
 							EntryPoints: []string{"tls-2"},
 							Service:     "tlsroute-default-tls-app-default-gw-default-my-gateway-ep-tls-2-0-e3b0c44298fc1c149afb-wrr",
+							Priority:    24,
 							Rule:        "HostSNI(`pass.tls.foo.example.com`)",
 							RuleSyntax:  "v3",
 							TLS: &dynamic.RouterTCPTLSConfig{
@@ -6185,6 +6202,7 @@ func TestLoadMixedRoutes(t *testing.T) {
 						"tlsroute-default-tls-app-default-gw-default-my-gateway-ep-tls-2-0-e3b0c44298fc1c149afb": {
 							EntryPoints: []string{"tls-2"},
 							Service:     "tlsroute-default-tls-app-default-gw-default-my-gateway-ep-tls-2-0-e3b0c44298fc1c149afb-wrr",
+							Priority:    24,
 							Rule:        "HostSNI(`pass.tls.foo.example.com`)",
 							RuleSyntax:  "v3",
 							TLS: &dynamic.RouterTCPTLSConfig{
@@ -6440,6 +6458,7 @@ func TestLoadMixedRoutes(t *testing.T) {
 						"tlsroute-bar-tls-app-bar-gw-default-my-gateway-ep-tls-2-0-e3b0c44298fc1c149afb": {
 							EntryPoints: []string{"tls-2"},
 							Service:     "tlsroute-bar-tls-app-bar-gw-default-my-gateway-ep-tls-2-0-e3b0c44298fc1c149afb-wrr",
+							Priority:    24,
 							Rule:        "HostSNI(`pass.tls.foo.example.com`)",
 							RuleSyntax:  "v3",
 							TLS: &dynamic.RouterTCPTLSConfig{
diff --git a/pkg/provider/kubernetes/gateway/tlsroute_test.go b/pkg/provider/kubernetes/gateway/tlsroute_test.go
index 89a688e5e9..64858b09b6 100644
--- a/pkg/provider/kubernetes/gateway/tlsroute_test.go
+++ b/pkg/provider/kubernetes/gateway/tlsroute_test.go
@@ -9,49 +9,58 @@ import (
 
 func Test_hostSNIRule(t *testing.T) {
 	testCases := []struct {
-		desc         string
-		hostnames    []gatev1.Hostname
-		expectedRule string
-		expectError  bool
+		desc             string
+		hostnames        []gatev1.Hostname
+		expectedRule     string
+		expectedPriority int
+		expectError      bool
 	}{
 		{
-			desc:         "Empty",
-			expectedRule: "HostSNI(`*`)",
+			desc:             "Empty",
+			expectedRule:     "HostSNI(`*`)",
+			expectedPriority: 0,
 		},
 		{
-			desc:         "Empty hostname",
-			hostnames:    []gatev1.Hostname{""},
-			expectedRule: "HostSNI(`*`)",
+			desc:             "Empty hostname",
+			hostnames:        []gatev1.Hostname{""},
+			expectedRule:     "HostSNI(`*`)",
+			expectedPriority: 0,
 		},
 		{
-			desc:         "Supported wildcard",
-			hostnames:    []gatev1.Hostname{"*.foo"},
-			expectedRule: "HostSNIRegexp(`^[a-z0-9-\\.]+\\.foo$`)",
+			desc:             "Supported wildcard",
+			hostnames:        []gatev1.Hostname{"*.foo"},
+			expectedRule:     "HostSNIRegexp(`^[a-z0-9-\\.]+\\.foo$`)",
+			expectedPriority: 4,
 		},
 		{
-			desc:         "Some empty hostnames",
-			hostnames:    []gatev1.Hostname{"foo", "", "bar"},
-			expectedRule: "HostSNI(`foo`) || HostSNI(`bar`)",
+			desc:             "Some empty hostnames",
+			hostnames:        []gatev1.Hostname{"foo", "", "bar"},
+			expectedRule:     "HostSNI(`foo`) || HostSNI(`bar`)",
+			expectedPriority: 3,
 		},
 		{
-			desc:         "Valid hostname",
-			hostnames:    []gatev1.Hostname{"foo"},
-			expectedRule: "HostSNI(`foo`)",
+			desc:             "Valid hostname",
+			hostnames:        []gatev1.Hostname{"foo"},
+			expectedRule:     "HostSNI(`foo`)",
+			expectedPriority: 3,
 		},
 		{
-			desc:         "Multiple valid hostnames",
-			hostnames:    []gatev1.Hostname{"foo", "bar"},
-			expectedRule: "HostSNI(`foo`) || HostSNI(`bar`)",
+			desc:             "Multiple valid hostnames",
+			hostnames:        []gatev1.Hostname{"foo", "bar"},
+			expectedRule:     "HostSNI(`foo`) || HostSNI(`bar`)",
+			expectedPriority: 3,
 		},
 		{
-			desc:         "Multiple valid hostnames with wildcard",
-			hostnames:    []gatev1.Hostname{"bar.foo", "foo.foo", "*.foo"},
-			expectedRule: "HostSNI(`bar.foo`) || HostSNI(`foo.foo`) || HostSNIRegexp(`^[a-z0-9-\\.]+\\.foo$`)",
+			desc:             "Multiple valid hostnames with wildcard",
+			hostnames:        []gatev1.Hostname{"bar.foo", "foo.foo", "*.foo"},
+			expectedRule:     "HostSNI(`bar.foo`) || HostSNI(`foo.foo`) || HostSNIRegexp(`^[a-z0-9-\\.]+\\.foo$`)",
+			expectedPriority: 7,
 		},
 		{
-			desc:         "Multiple overlapping hostnames",
-			hostnames:    []gatev1.Hostname{"foo", "bar", "foo", "baz"},
-			expectedRule: "HostSNI(`foo`) || HostSNI(`bar`) || HostSNI(`baz`)",
+			desc:             "Multiple overlapping hostnames",
+			hostnames:        []gatev1.Hostname{"foo", "bar", "foo", "baz"},
+			expectedRule:     "HostSNI(`foo`) || HostSNI(`bar`) || HostSNI(`baz`)",
+			expectedPriority: 3,
 		},
 	}
 
@@ -59,8 +68,9 @@ func Test_hostSNIRule(t *testing.T) {
 		t.Run(test.desc, func(t *testing.T) {
 			t.Parallel()
 
-			rule := hostSNIRule(test.hostnames)
+			rule, priority := hostSNIRule(test.hostnames)
 			assert.Equal(t, test.expectedRule, rule)
+			assert.Equal(t, test.expectedPriority, priority)
 		})
 	}
 }