diff --git a/conformance/base/manifests.yaml b/conformance/base/manifests.yaml index 63efb5375a..95fbcf4cce 100644 --- a/conformance/base/manifests.yaml +++ b/conformance/base/manifests.yaml @@ -308,6 +308,68 @@ spec: path: key --- apiVersion: v1 +kind: Service +metadata: + name: tls-backend-2 + namespace: gateway-conformance-infra +spec: + selector: + app: tls-backend-2 + ports: + - protocol: TCP + port: 443 + targetPort: 8443 +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: tls-backend-2 + namespace: gateway-conformance-infra + labels: + app: tls-backend-2 +spec: + replicas: 1 + selector: + matchLabels: + app: tls-backend-2 + template: + metadata: + labels: + app: tls-backend-2 + spec: + containers: + - name: tls-backend-2 + image: gcr.io/k8s-staging-gateway-api/echo-basic:v20240412-v1.0.0-394-g40c666fd + volumeMounts: + - name: secret-volume + mountPath: /etc/secret-volume + env: + - name: POD_NAME + valueFrom: + fieldRef: + fieldPath: metadata.name + - name: NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + - name: TLS_SERVER_CERT + value: /etc/secret-volume/crt + - name: TLS_SERVER_PRIVKEY + value: /etc/secret-volume/key + resources: + requests: + cpu: 10m + volumes: + - name: secret-volume + secret: + secretName: tls-checks-certificate + items: + - key: tls.crt + path: crt + - key: tls.key + path: key +--- +apiVersion: v1 kind: Namespace metadata: name: gateway-conformance-app-backend diff --git a/conformance/tests/tlsroute-hostname-intersection.go b/conformance/tests/tlsroute-hostname-intersection.go new file mode 100644 index 0000000000..069ee4c5d6 --- /dev/null +++ b/conformance/tests/tlsroute-hostname-intersection.go @@ -0,0 +1,173 @@ +/* +Copyright 2026 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package tests + +import ( + "testing" + + "k8s.io/apimachinery/pkg/types" + + "sigs.k8s.io/gateway-api/conformance/utils/http" + "sigs.k8s.io/gateway-api/conformance/utils/kubernetes" + "sigs.k8s.io/gateway-api/conformance/utils/suite" + "sigs.k8s.io/gateway-api/conformance/utils/tls" + "sigs.k8s.io/gateway-api/pkg/features" +) + +func init() { + ConformanceTests = append(ConformanceTests, TLSRouteHostnameIntersection) +} + +var TLSRouteHostnameIntersection = suite.ConformanceTest{ + ShortName: "TLSRouteHostnameIntersection", + Description: "TLSRoutes should attach to listeners only if they have intersecting hostnames, and should accept requests only for the intersecting hostnames", + Features: []features.FeatureName{ + features.SupportGateway, + features.SupportTLSRoute, + }, + Manifests: []string{"tests/tlsroute-hostname-intersection.yaml"}, + Test: func(t *testing.T, suite *suite.ConformanceTestSuite) { + ns := "gateway-conformance-infra" + certNN := types.NamespacedName{Name: "tls-checks-certificate", Namespace: ns} + + // This test creates an additional Gateway in the gateway-conformance-infra + // namespace so we have to wait for it to be ready. + kubernetes.NamespacesMustBeReady(t, suite.Client, suite.TimeoutConfig, []string{ns}) + + serverCertPem, _, err := GetTLSSecret(suite.Client, certNN) + if err != nil { + t.Fatalf("unexpected error finding TLS secret: %v", err) + } + + t.Run("TLSRoutes intersect with exact listener hostname", func(t *testing.T) { + routeNN := types.NamespacedName{Namespace: ns, Name: "tlsroute-more-specific-wc-hostname-x-1"} + gwNN := types.NamespacedName{Name: "gw-tlsroute-exact-hostname-x-1", Namespace: ns} + gwAddr, _ := kubernetes.GatewayAndTLSRoutesMustBeAccepted(t, suite.Client, suite.TimeoutConfig, suite.ControllerName, kubernetes.NewGatewayRef(gwNN), routeNN) + + kubernetes.TLSRouteMustHaveResolvedRefsConditionsTrue(t, suite.Client, suite.TimeoutConfig, routeNN, gwNN) + + t.Run("TLS request matching exact hostnames intersection should reach backend", func(t *testing.T) { + t.Parallel() + tls.MakeTLSRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, gwAddr, serverCertPem, nil, nil, "abc.example.com", + http.ExpectedResponse{ + Request: http.Request{Host: "abc.example.com", Path: "/"}, + Backend: "tls-backend", + Namespace: ns, + }) + }) + }) + + t.Run("TLSRoutes intersect with more specific wildcard listener hostname", func(t *testing.T) { + routes := []types.NamespacedName{ + {Namespace: ns, Name: "tlsroute-exact-hostname-x-2"}, + {Namespace: ns, Name: "tlsroute-less-specific-wc-hostname-x-2"}, + } + gwNN := types.NamespacedName{Name: "gw-tlsroute-more-specific-wc-hostname-x-2", Namespace: ns} + gwAddr, _ := kubernetes.GatewayAndTLSRoutesMustBeAccepted(t, suite.Client, suite.TimeoutConfig, suite.ControllerName, kubernetes.NewGatewayRef(gwNN), routes...) + + for _, routeNN := range routes { + kubernetes.TLSRouteMustHaveResolvedRefsConditionsTrue(t, suite.Client, suite.TimeoutConfig, routeNN, gwNN) + } + + t.Run("TLS request matching exact hostnames intersection should reach backend", func(t *testing.T) { + t.Parallel() + tls.MakeTLSRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, gwAddr, serverCertPem, nil, nil, "abc.example.com", + http.ExpectedResponse{ + Request: http.Request{Host: "abc.example.com", Path: "/"}, + Backend: "tls-backend", + Namespace: ns, + }) + }) + + t.Run("TLS request matching wildcard hostnames intersection should reach backend 2", func(t *testing.T) { + t.Parallel() + tls.MakeTLSRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, gwAddr, serverCertPem, nil, nil, "other.example.com", + http.ExpectedResponse{ + Request: http.Request{Host: "other.example.com", Path: "/"}, + Backend: "tls-backend-2", + Namespace: ns, + }) + }) + }) + + t.Run("TLSRoutes intersect with less specific wildcard listener hostname", func(t *testing.T) { + routes := []types.NamespacedName{ + {Namespace: ns, Name: "tlsroute-exact-hostname-x-3"}, + {Namespace: ns, Name: "tlsroute-more-specific-wc-hostname-x-3"}, + } + gwNN := types.NamespacedName{Name: "gw-tlsroute-less-specific-wc-hostname-x-3", Namespace: ns} + gwAddr, _ := kubernetes.GatewayAndTLSRoutesMustBeAccepted(t, suite.Client, suite.TimeoutConfig, suite.ControllerName, kubernetes.NewGatewayRef(gwNN), routes...) + + for _, routeNN := range routes { + kubernetes.TLSRouteMustHaveResolvedRefsConditionsTrue(t, suite.Client, suite.TimeoutConfig, routeNN, gwNN) + } + + t.Run("TLS request matching exact hostnames intersection should reach backend", func(t *testing.T) { + t.Parallel() + tls.MakeTLSRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, gwAddr, serverCertPem, nil, nil, "abc.example.com", + http.ExpectedResponse{ + Request: http.Request{Host: "abc.example.com", Path: "/"}, + Backend: "tls-backend", + Namespace: ns, + }) + }) + + t.Run("TLS request matching wildcard hostnames intersection should reach backend 2", func(t *testing.T) { + t.Parallel() + tls.MakeTLSRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, gwAddr, serverCertPem, nil, nil, "other.example.com", + http.ExpectedResponse{ + Request: http.Request{Host: "other.example.com", Path: "/"}, + Backend: "tls-backend-2", + Namespace: ns, + }) + }) + }) + + t.Run("TLSRoutes intersect with empty listener hostname", func(t *testing.T) { + routes := []types.NamespacedName{ + {Namespace: ns, Name: "tlsroute-exact-hostname-x-4"}, + {Namespace: ns, Name: "tlsroute-less-specific-wc-hostname-x-4"}, + } + gwNN := types.NamespacedName{Name: "gw-tlsroute-empty-hostname-x-4", Namespace: ns} + gwAddr, _ := kubernetes.GatewayAndTLSRoutesMustBeAccepted(t, suite.Client, suite.TimeoutConfig, suite.ControllerName, kubernetes.NewGatewayRef(gwNN), routes...) + + for _, routeNN := range routes { + kubernetes.TLSRouteMustHaveResolvedRefsConditionsTrue(t, suite.Client, suite.TimeoutConfig, routeNN, gwNN) + } + + t.Run("TLS request matching exact hostnames intersection should reach backend", func(t *testing.T) { + t.Parallel() + tls.MakeTLSRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, gwAddr, serverCertPem, nil, nil, "abc.example.com", + http.ExpectedResponse{ + Request: http.Request{Host: "abc.example.com", Path: "/"}, + Backend: "tls-backend", + Namespace: ns, + }) + }) + + t.Run("TLS request matching wildcard hostnames intersection should reach backend 2", func(t *testing.T) { + t.Parallel() + tls.MakeTLSRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, gwAddr, serverCertPem, nil, nil, "other.example.com", + http.ExpectedResponse{ + Request: http.Request{Host: "other.example.com", Path: "/"}, + Backend: "tls-backend-2", + Namespace: ns, + }) + }) + }) + }, +} diff --git a/conformance/tests/tlsroute-hostname-intersection.yaml b/conformance/tests/tlsroute-hostname-intersection.yaml new file mode 100644 index 0000000000..73f7aa8a2f --- /dev/null +++ b/conformance/tests/tlsroute-hostname-intersection.yaml @@ -0,0 +1,190 @@ +apiVersion: gateway.networking.k8s.io/v1 +kind: Gateway +metadata: + name: gw-tlsroute-exact-hostname-x-1 + namespace: gateway-conformance-infra +spec: + gatewayClassName: "{GATEWAY_CLASS_NAME}" + listeners: + - name: listener-exact-hostname + protocol: TLS + port: 443 + hostname: abc.example.com + allowedRoutes: + namespaces: + from: Same + kinds: + - kind: TLSRoute + tls: + mode: Passthrough +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: Gateway +metadata: + name: gw-tlsroute-more-specific-wc-hostname-x-2 + namespace: gateway-conformance-infra +spec: + gatewayClassName: "{GATEWAY_CLASS_NAME}" + listeners: + - name: listener-more-specific-wc-hostname + protocol: TLS + port: 443 + hostname: "*.example.com" + allowedRoutes: + namespaces: + from: Same + kinds: + - kind: TLSRoute + tls: + mode: Passthrough +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: Gateway +metadata: + name: gw-tlsroute-less-specific-wc-hostname-x-3 + namespace: gateway-conformance-infra +spec: + gatewayClassName: "{GATEWAY_CLASS_NAME}" + listeners: + - name: listener-less-specific-wc-hostname + protocol: TLS + port: 443 + hostname: "*.com" + allowedRoutes: + namespaces: + from: Same + kinds: + - kind: TLSRoute + tls: + mode: Passthrough +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: Gateway +metadata: + name: gw-tlsroute-empty-hostname-x-4 + namespace: gateway-conformance-infra +spec: + gatewayClassName: "{GATEWAY_CLASS_NAME}" + listeners: + - name: listener-empty-hostname + protocol: TLS + port: 443 + allowedRoutes: + namespaces: + from: Same + kinds: + - kind: TLSRoute + tls: + mode: Passthrough +--- +apiVersion: gateway.networking.k8s.io/v1alpha3 +kind: TLSRoute +metadata: + name: tlsroute-more-specific-wc-hostname-x-1 + namespace: gateway-conformance-infra +spec: + parentRefs: + - name: gw-tlsroute-exact-hostname-x-1 + namespace: gateway-conformance-infra + hostnames: + - "*.example.com" + rules: + - backendRefs: + - name: tls-backend + port: 443 +--- +apiVersion: gateway.networking.k8s.io/v1alpha3 +kind: TLSRoute +metadata: + name: tlsroute-exact-hostname-x-2 + namespace: gateway-conformance-infra +spec: + parentRefs: + - name: gw-tlsroute-more-specific-wc-hostname-x-2 + namespace: gateway-conformance-infra + hostnames: + - abc.example.com + rules: + - backendRefs: + - name: tls-backend + port: 443 +--- +apiVersion: gateway.networking.k8s.io/v1alpha3 +kind: TLSRoute +metadata: + name: tlsroute-less-specific-wc-hostname-x-2 + namespace: gateway-conformance-infra +spec: + parentRefs: + - name: gw-tlsroute-more-specific-wc-hostname-x-2 + namespace: gateway-conformance-infra + hostnames: + - "*.com" + rules: + - backendRefs: + - name: tls-backend-2 + port: 443 +--- +apiVersion: gateway.networking.k8s.io/v1alpha3 +kind: TLSRoute +metadata: + name: tlsroute-exact-hostname-x-3 + namespace: gateway-conformance-infra +spec: + parentRefs: + - name: gw-tlsroute-less-specific-wc-hostname-x-3 + namespace: gateway-conformance-infra + hostnames: + - abc.example.com + rules: + - backendRefs: + - name: tls-backend + port: 443 +--- +apiVersion: gateway.networking.k8s.io/v1alpha3 +kind: TLSRoute +metadata: + name: tlsroute-more-specific-wc-hostname-x-3 + namespace: gateway-conformance-infra +spec: + parentRefs: + - name: gw-tlsroute-less-specific-wc-hostname-x-3 + namespace: gateway-conformance-infra + hostnames: + - "*.example.com" + rules: + - backendRefs: + - name: tls-backend-2 + port: 443 +--- +apiVersion: gateway.networking.k8s.io/v1alpha3 +kind: TLSRoute +metadata: + name: tlsroute-exact-hostname-x-4 + namespace: gateway-conformance-infra +spec: + parentRefs: + - name: gw-tlsroute-empty-hostname-x-4 + namespace: gateway-conformance-infra + hostnames: + - abc.example.com + rules: + - backendRefs: + - name: tls-backend + port: 443 +--- +apiVersion: gateway.networking.k8s.io/v1alpha3 +kind: TLSRoute +metadata: + name: tlsroute-less-specific-wc-hostname-x-4 + namespace: gateway-conformance-infra +spec: + parentRefs: + - name: gw-tlsroute-empty-hostname-x-4 + namespace: gateway-conformance-infra + hostnames: + - "*.com" + rules: + - backendRefs: + - name: tls-backend-2 + port: 443 diff --git a/conformance/utils/kubernetes/helpers.go b/conformance/utils/kubernetes/helpers.go index 6bc0b179c5..ae6a40a0a5 100644 --- a/conformance/utils/kubernetes/helpers.go +++ b/conformance/utils/kubernetes/helpers.go @@ -1097,6 +1097,16 @@ func ListenerSetListenersMustHaveConditions(t *testing.T, client client.Client, require.NoErrorf(t, waitErr, "error waiting for ListenerSet status to have conditions matching expectations on the listeners") } +// TLSRouteMustHaveResolvedRefsConditionsTrue checks that the supplied TLSRoute has the resolvedRefsCondition +// set to true. +func TLSRouteMustHaveResolvedRefsConditionsTrue(t *testing.T, client client.Client, timeoutConfig config.TimeoutConfig, routeNN types.NamespacedName, gwNN types.NamespacedName) { + TLSRouteMustHaveCondition(t, client, timeoutConfig, routeNN, gwNN, metav1.Condition{ + Type: string(gatewayv1.RouteConditionResolvedRefs), + Status: metav1.ConditionTrue, + Reason: string(gatewayv1.RouteReasonResolvedRefs), + }) +} + // TODO(mikemorris): this and parentsMatch could possibly be rewritten as a generic function? func listenersMatch(t *testing.T, expected, actual []gatewayv1.ListenerStatus) bool { t.Helper()