From a08a428787f2fafa5de1f5cabc8e5a2e84033e9b Mon Sep 17 00:00:00 2001 From: Douglas De Toni Machado Date: Mon, 12 Dec 2022 12:30:05 -0300 Subject: [PATCH] Support HostSNIRegexp in GatewayAPI TLS routes --- .../tlsroute/with_invalid_SNI_matching.yml | 2 +- pkg/provider/kubernetes/gateway/kubernetes.go | 43 ++++++++++++------- .../kubernetes/gateway/kubernetes_test.go | 24 ++++++++--- 3 files changed, 45 insertions(+), 24 deletions(-) diff --git a/pkg/provider/kubernetes/gateway/fixtures/tlsroute/with_invalid_SNI_matching.yml b/pkg/provider/kubernetes/gateway/fixtures/tlsroute/with_invalid_SNI_matching.yml index 004e1a4b9..512e39770 100644 --- a/pkg/provider/kubernetes/gateway/fixtures/tlsroute/with_invalid_SNI_matching.yml +++ b/pkg/provider/kubernetes/gateway/fixtures/tlsroute/with_invalid_SNI_matching.yml @@ -39,7 +39,7 @@ spec: kind: Gateway group: gateway.networking.k8s.io hostnames: - - "*.foo.bar" + - "*.foo.*.bar" rules: - backendRefs: - name: whoamitcp diff --git a/pkg/provider/kubernetes/gateway/kubernetes.go b/pkg/provider/kubernetes/gateway/kubernetes.go index fc620ae69..847d1c09b 100644 --- a/pkg/provider/kubernetes/gateway/kubernetes.go +++ b/pkg/provider/kubernetes/gateway/kubernetes.go @@ -839,7 +839,7 @@ func gatewayTCPRouteToTCPConf(ctx context.Context, ep string, listener v1alpha2. } router := dynamic.TCPRouter{ - Rule: "HostSNI(`*`)", // Gateway listener hostname not available in TCP + Rule: "HostSNI(`*`)", EntryPoints: []string{ep}, } @@ -970,8 +970,16 @@ func gatewayTLSRouteToTCPConf(ctx context.Context, ep string, listener v1alpha2. hostnames := matchingHostnames(listener, route.Spec.Hostnames) if len(hostnames) == 0 && listener.Hostname != nil && *listener.Hostname != "" && len(route.Spec.Hostnames) > 0 { - // TODO update the corresponding route parent status - // https://gateway-api.sigs.k8s.io/v1alpha2/references/spec/#gateway.networking.k8s.io/v1alpha2.TLSRoute + for _, parent := range route.Status.Parents { + parent.Conditions = append(parent.Conditions, metav1.Condition{ + Type: string(v1alpha2.GatewayClassConditionStatusAccepted), + Status: metav1.ConditionFalse, + Reason: string(v1alpha2.ListenerReasonRouteConflict), + Message: fmt.Sprintf("No hostname match between listener: %v and route: %v", listener.Hostname, route.Spec.Hostnames), + LastTransitionTime: metav1.Now(), + }) + } + continue } @@ -1207,7 +1215,7 @@ func hostRule(hostnames []v1alpha2.Hostname) (string, error) { } func hostSNIRule(hostnames []v1alpha2.Hostname) (string, error) { - var matchers []string + rules := make([]string, 0, len(hostnames)) uniqHostnames := map[v1alpha2.Hostname]struct{}{} for _, hostname := range hostnames { @@ -1219,25 +1227,28 @@ func hostSNIRule(hostnames []v1alpha2.Hostname) (string, error) { continue } - h := string(hostname) + host := string(hostname) + uniqHostnames[hostname] = struct{}{} - // TODO support wildcard hostnames with an HostSNI regexp matcher - if strings.Contains(h, "*") { - return "", fmt.Errorf("wildcard hostname is not supported: %q", h) + wildcard := strings.Count(host, "*") + if wildcard == 0 { + rules = append(rules, fmt.Sprintf("HostSNI(`%s`)", host)) + continue } - matchers = append(matchers, fmt.Sprintf("HostSNI(`%s`)", h)) - uniqHostnames[hostname] = struct{}{} + if !strings.HasPrefix(host, "*.") || wildcard > 1 { + return "", fmt.Errorf("invalid rule: %q", host) + } + + host = strings.Replace(regexp.QuoteMeta(host), `\*\.`, `[a-zA-Z0-9-]+\.`, 1) + rules = append(rules, fmt.Sprintf("HostSNIRegexp(`^%s$`)", host)) } - switch len(matchers) { - case 0: + if len(hostnames) == 0 || len(rules) == 0 { return "HostSNI(`*`)", nil - case 1: - return matchers[0], nil - default: - return fmt.Sprintf("(%s)", strings.Join(matchers, " || ")), nil } + + return strings.Join(rules, " || "), nil } func extractRule(routeRule v1alpha2.HTTPRouteRule, hostRule string) (string, error) { diff --git a/pkg/provider/kubernetes/gateway/kubernetes_test.go b/pkg/provider/kubernetes/gateway/kubernetes_test.go index f7b6b9cd9..28946ba7b 100644 --- a/pkg/provider/kubernetes/gateway/kubernetes_test.go +++ b/pkg/provider/kubernetes/gateway/kubernetes_test.go @@ -3076,10 +3076,10 @@ func TestLoadTLSRoutes(t *testing.T) { }, TCP: &dynamic.TCPConfiguration{ Routers: map[string]*dynamic.TCPRouter{ - "default-tls-app-1-my-gateway-tls-dfc5c7506ac1b172c8b7": { + "default-tls-app-1-my-gateway-tls-d5342d75658583f03593": { EntryPoints: []string{"tls"}, - Service: "default-tls-app-1-my-gateway-tls-dfc5c7506ac1b172c8b7-wrr-0", - Rule: "(HostSNI(`foo.example.com`) || HostSNI(`bar.example.com`))", + Service: "default-tls-app-1-my-gateway-tls-d5342d75658583f03593-wrr-0", + Rule: "HostSNI(`foo.example.com`) || HostSNI(`bar.example.com`)", TLS: &dynamic.RouterTCPTLSConfig{ Passthrough: true, }, @@ -3087,7 +3087,7 @@ func TestLoadTLSRoutes(t *testing.T) { }, Middlewares: map[string]*dynamic.TCPMiddleware{}, Services: map[string]*dynamic.TCPService{ - "default-tls-app-1-my-gateway-tls-dfc5c7506ac1b172c8b7-wrr-0": { + "default-tls-app-1-my-gateway-tls-d5342d75658583f03593-wrr-0": { Weighted: &dynamic.TCPWeightedRoundRobin{ Services: []dynamic.TCPWRRService{ { @@ -4780,6 +4780,11 @@ func Test_hostSNIRule(t *testing.T) { hostnames: []v1alpha2.Hostname{"*"}, expectError: true, }, + { + desc: "Supported wildcard", + hostnames: []v1alpha2.Hostname{"*.foo"}, + expectedRule: "HostSNIRegexp(`^[a-zA-Z0-9-]+\\.foo$`)", + }, { desc: "Multiple malformed wildcard", hostnames: []v1alpha2.Hostname{"*.foo.*"}, @@ -4788,7 +4793,7 @@ func Test_hostSNIRule(t *testing.T) { { desc: "Some empty hostnames", hostnames: []v1alpha2.Hostname{"foo", "", "bar"}, - expectedRule: "(HostSNI(`foo`) || HostSNI(`bar`))", + expectedRule: "HostSNI(`foo`) || HostSNI(`bar`)", }, { desc: "Valid hostname", @@ -4798,12 +4803,17 @@ func Test_hostSNIRule(t *testing.T) { { desc: "Multiple valid hostnames", hostnames: []v1alpha2.Hostname{"foo", "bar"}, - expectedRule: "(HostSNI(`foo`) || HostSNI(`bar`))", + expectedRule: "HostSNI(`foo`) || HostSNI(`bar`)", + }, + { + desc: "Multiple valid hostnames with wildcard", + hostnames: []v1alpha2.Hostname{"bar.foo", "foo.foo", "*.foo"}, + expectedRule: "HostSNI(`bar.foo`) || HostSNI(`foo.foo`) || HostSNIRegexp(`^[a-zA-Z0-9-]+\\.foo$`)", }, { desc: "Multiple overlapping hostnames", hostnames: []v1alpha2.Hostname{"foo", "bar", "foo", "baz"}, - expectedRule: "(HostSNI(`foo`) || HostSNI(`bar`) || HostSNI(`baz`))", + expectedRule: "HostSNI(`foo`) || HostSNI(`bar`) || HostSNI(`baz`)", }, }