From fb8a091c92d2139927fa4ba510b5059aac0268b4 Mon Sep 17 00:00:00 2001 From: Kobi Levi Date: Tue, 13 Aug 2024 04:29:58 +0000 Subject: [PATCH] feat: gateway http listener isolation Signed-off-by: Kobi Levi --- internal/gatewayapi/conformance/suite.go | 1 - internal/gatewayapi/helpers.go | 45 +++- internal/gatewayapi/route.go | 4 +- ...istener-with-hostname-intersection.in.yaml | 65 +++++ ...stener-with-hostname-intersection.out.yaml | 238 ++++++++++++++++++ internal/gatewayapi/tls.go | 7 +- 6 files changed, 345 insertions(+), 15 deletions(-) create mode 100644 internal/gatewayapi/testdata/gateway-http-listener-with-hostname-intersection.in.yaml create mode 100644 internal/gatewayapi/testdata/gateway-http-listener-with-hostname-intersection.out.yaml diff --git a/internal/gatewayapi/conformance/suite.go b/internal/gatewayapi/conformance/suite.go index 4637e023779..4fafa008983 100644 --- a/internal/gatewayapi/conformance/suite.go +++ b/internal/gatewayapi/conformance/suite.go @@ -15,7 +15,6 @@ import ( // SkipTests is a list of tests that are skipped in the conformance suite. var SkipTests = []suite.ConformanceTest{ tests.GatewayStaticAddresses, - tests.GatewayHTTPListenerIsolation, // https://github.com/envoyproxy/gateway/issues/3352 } func skipTestsShortNames(skipTests []suite.ConformanceTest) []string { diff --git a/internal/gatewayapi/helpers.go b/internal/gatewayapi/helpers.go index a29736216bc..31428c5e13d 100644 --- a/internal/gatewayapi/helpers.go +++ b/internal/gatewayapi/helpers.go @@ -262,12 +262,12 @@ func servicePortToContainerPort(servicePort int32, envoyProxy *egv1a1.EnvoyProxy return servicePort } -// computeHosts returns a list of the intersecting hostnames between the route -// and the listener. -func computeHosts(routeHostnames []string, listenerHostname *gwapiv1.Hostname) []string { +// computeHosts returns a list of intersecting listener hostnames and route hostnames +// that don't intersect with other listener hostnames. +func computeHosts(routeHostnames []string, listenerContext *ListenerContext) []string { var listenerHostnameVal string - if listenerHostname != nil { - listenerHostnameVal = string(*listenerHostname) + if listenerContext != nil && listenerContext.Hostname != nil { + listenerHostnameVal = string(*listenerContext.Hostname) } // No route hostnames specified: use the listener hostname if specified, @@ -280,8 +280,9 @@ func computeHosts(routeHostnames []string, listenerHostname *gwapiv1.Hostname) [ return []string{"*"} } - var hostnames []string + hostnamesSet := map[string]struct{}{} + // Find intersecting hostnames for i := range routeHostnames { routeHostname := routeHostnames[i] @@ -290,27 +291,51 @@ func computeHosts(routeHostnames []string, listenerHostname *gwapiv1.Hostname) [ switch { // No listener hostname: use the route hostname. case len(listenerHostnameVal) == 0: - hostnames = append(hostnames, routeHostname) + hostnamesSet[routeHostname] = struct{}{} // Listener hostname matches the route hostname: use it. case listenerHostnameVal == routeHostname: - hostnames = append(hostnames, routeHostname) + hostnamesSet[routeHostname] = struct{}{} // Listener has a wildcard hostname: check if the route hostname matches. case strings.HasPrefix(listenerHostnameVal, "*"): if hostnameMatchesWildcardHostname(routeHostname, listenerHostnameVal) { - hostnames = append(hostnames, routeHostname) + hostnamesSet[routeHostname] = struct{}{} } // Route has a wildcard hostname: check if the listener hostname matches. case strings.HasPrefix(routeHostname, "*"): if hostnameMatchesWildcardHostname(listenerHostnameVal, routeHostname) { - hostnames = append(hostnames, listenerHostnameVal) + hostnamesSet[listenerHostnameVal] = struct{}{} } } } + // Filter out route hostnames that intersect with other listener hostnames + var listeners []*ListenerContext + if listenerContext != nil && listenerContext.gateway != nil { + listeners = listenerContext.gateway.listeners + } + + for _, listener := range listeners { + if listenerContext == listener { + continue + } + if listenerContext != nil && listenerContext.Port != listener.Port { + continue + } + if listener.Hostname == nil { + continue + } + delete(hostnamesSet, string(*listener.Hostname)) + } + + var hostnames []string + for host := range hostnamesSet { + hostnames = append(hostnames, host) + } + return hostnames } diff --git a/internal/gatewayapi/route.go b/internal/gatewayapi/route.go index 8a3c0272276..933a388c919 100644 --- a/internal/gatewayapi/route.go +++ b/internal/gatewayapi/route.go @@ -699,7 +699,7 @@ func (t *Translator) processHTTPRouteParentRefListener(route RouteContext, route var hasHostnameIntersection bool for _, listener := range parentRef.listeners { - hosts := computeHosts(GetHostnames(route), listener.Hostname) + hosts := computeHosts(GetHostnames(route), listener) if len(hosts) == 0 { continue } @@ -867,7 +867,7 @@ func (t *Translator) processTLSRouteParentRefs(tlsRoute *TLSRouteContext, resour var hasHostnameIntersection bool for _, listener := range parentRef.listeners { - hosts := computeHosts(GetHostnames(tlsRoute), listener.Hostname) + hosts := computeHosts(GetHostnames(tlsRoute), listener) if len(hosts) == 0 { continue } diff --git a/internal/gatewayapi/testdata/gateway-http-listener-with-hostname-intersection.in.yaml b/internal/gatewayapi/testdata/gateway-http-listener-with-hostname-intersection.in.yaml new file mode 100644 index 00000000000..267fcbba54b --- /dev/null +++ b/internal/gatewayapi/testdata/gateway-http-listener-with-hostname-intersection.in.yaml @@ -0,0 +1,65 @@ +gateways: + - apiVersion: gateway.networking.k8s.io/v1 + kind: Gateway + metadata: + name: gateway-1 + namespace: envoy-gateway + spec: + gatewayClassName: envoy-gateway-class + listeners: + - name: empty-hostname + port: 80 + protocol: HTTP + allowedRoutes: + namespaces: + from: All + - name: wildcard-example-com + port: 80 + protocol: HTTP + hostname: "*.example.com" + allowedRoutes: + namespaces: + from: All +httpRoutes: + - apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + name: httproute-1 + namespace: envoy-gateway + spec: + parentRefs: + - name: gateway-1 + namespace: envoy-gateway + sectionName: empty-hostname + hostnames: + - "bar.com" + - "*.example.com" # request matching is prevented by the isolation wildcard-example-com listener + rules: + - matches: + - path: + type: PathPrefix + value: /empty-hostname + backendRefs: + - name: service-1 + port: 8080 + - apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + name: httproute-2 + namespace: envoy-gateway + spec: + parentRefs: + - name: gateway-1 + namespace: envoy-gateway + sectionName: wildcard-example-com + hostnames: + - "bar.com" # doesn't match wildcard-example-com listener + - "*.example.com" + rules: + - matches: + - path: + type: PathPrefix + value: /wildcard-example-com + backendRefs: + - name: service-1 + port: 8080 diff --git a/internal/gatewayapi/testdata/gateway-http-listener-with-hostname-intersection.out.yaml b/internal/gatewayapi/testdata/gateway-http-listener-with-hostname-intersection.out.yaml new file mode 100644 index 00000000000..cb47542a1c7 --- /dev/null +++ b/internal/gatewayapi/testdata/gateway-http-listener-with-hostname-intersection.out.yaml @@ -0,0 +1,238 @@ +gateways: +- apiVersion: gateway.networking.k8s.io/v1 + kind: Gateway + metadata: + creationTimestamp: null + name: gateway-1 + namespace: envoy-gateway + spec: + gatewayClassName: envoy-gateway-class + listeners: + - allowedRoutes: + namespaces: + from: All + name: empty-hostname + port: 80 + protocol: HTTP + - allowedRoutes: + namespaces: + from: All + hostname: '*.example.com' + name: wildcard-example-com + port: 80 + protocol: HTTP + status: + listeners: + - attachedRoutes: 1 + conditions: + - lastTransitionTime: null + message: Sending translated listener configuration to the data plane + reason: Programmed + status: "True" + type: Programmed + - lastTransitionTime: null + message: Listener has been successfully translated + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: Listener references have been resolved + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + name: empty-hostname + supportedKinds: + - group: gateway.networking.k8s.io + kind: HTTPRoute + - group: gateway.networking.k8s.io + kind: GRPCRoute + - attachedRoutes: 1 + conditions: + - lastTransitionTime: null + message: Sending translated listener configuration to the data plane + reason: Programmed + status: "True" + type: Programmed + - lastTransitionTime: null + message: Listener has been successfully translated + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: Listener references have been resolved + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + name: wildcard-example-com + supportedKinds: + - group: gateway.networking.k8s.io + kind: HTTPRoute + - group: gateway.networking.k8s.io + kind: GRPCRoute +httpRoutes: +- apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + creationTimestamp: null + name: httproute-1 + namespace: envoy-gateway + spec: + hostnames: + - bar.com + - '*.example.com' + parentRefs: + - name: gateway-1 + namespace: envoy-gateway + sectionName: empty-hostname + rules: + - backendRefs: + - name: service-1 + port: 8080 + matches: + - path: + type: PathPrefix + value: /empty-hostname + status: + parents: + - conditions: + - lastTransitionTime: null + message: Route is accepted + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: Service envoy-gateway/service-1 not found + reason: BackendNotFound + status: "False" + type: ResolvedRefs + controllerName: gateway.envoyproxy.io/gatewayclass-controller + parentRef: + name: gateway-1 + namespace: envoy-gateway + sectionName: empty-hostname +- apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + creationTimestamp: null + name: httproute-2 + namespace: envoy-gateway + spec: + hostnames: + - bar.com + - '*.example.com' + parentRefs: + - name: gateway-1 + namespace: envoy-gateway + sectionName: wildcard-example-com + rules: + - backendRefs: + - name: service-1 + port: 8080 + matches: + - path: + type: PathPrefix + value: /wildcard-example-com + status: + parents: + - conditions: + - lastTransitionTime: null + message: Route is accepted + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: Service envoy-gateway/service-1 not found + reason: BackendNotFound + status: "False" + type: ResolvedRefs + controllerName: gateway.envoyproxy.io/gatewayclass-controller + parentRef: + name: gateway-1 + namespace: envoy-gateway + sectionName: wildcard-example-com +infraIR: + envoy-gateway/gateway-1: + proxy: + listeners: + - address: null + name: envoy-gateway/gateway-1/empty-hostname + ports: + - containerPort: 10080 + name: http-80 + protocol: HTTP + servicePort: 80 + metadata: + labels: + gateway.envoyproxy.io/owning-gateway-name: gateway-1 + gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway + name: envoy-gateway/gateway-1 +xdsIR: + envoy-gateway/gateway-1: + accessLog: + text: + - path: /dev/stdout + http: + - address: 0.0.0.0 + hostnames: + - '*' + isHTTP2: false + metadata: + kind: Gateway + name: gateway-1 + namespace: envoy-gateway + sectionName: empty-hostname + name: envoy-gateway/gateway-1/empty-hostname + path: + escapedSlashesAction: UnescapeAndRedirect + mergeSlashes: true + port: 10080 + routes: + - destination: + name: httproute/envoy-gateway/httproute-1/rule/0 + settings: + - weight: 1 + directResponse: + statusCode: 500 + hostname: bar.com + isHTTP2: false + metadata: + kind: HTTPRoute + name: httproute-1 + namespace: envoy-gateway + name: httproute/envoy-gateway/httproute-1/rule/0/match/0/bar_com + pathMatch: + distinct: false + name: "" + prefix: /empty-hostname + - address: 0.0.0.0 + hostnames: + - '*.example.com' + isHTTP2: false + metadata: + kind: Gateway + name: gateway-1 + namespace: envoy-gateway + sectionName: wildcard-example-com + name: envoy-gateway/gateway-1/wildcard-example-com + path: + escapedSlashesAction: UnescapeAndRedirect + mergeSlashes: true + port: 10080 + routes: + - destination: + name: httproute/envoy-gateway/httproute-2/rule/0 + settings: + - weight: 1 + directResponse: + statusCode: 500 + hostname: '*.example.com' + isHTTP2: false + metadata: + kind: HTTPRoute + name: httproute-2 + namespace: envoy-gateway + name: httproute/envoy-gateway/httproute-2/rule/0/match/0/*_example_com + pathMatch: + distinct: false + name: "" + prefix: /wildcard-example-com diff --git a/internal/gatewayapi/tls.go b/internal/gatewayapi/tls.go index 1d38897ed26..acde9bed339 100644 --- a/internal/gatewayapi/tls.go +++ b/internal/gatewayapi/tls.go @@ -88,10 +88,13 @@ func validateTLSSecretsData(secrets []*corev1.Secret, host *gwapiv1.Hostname) er func verifyHostname(cert *x509.Certificate, host *gwapiv1.Hostname) ([]string, error) { var matchedHosts []string + listenerContext := ListenerContext{ + Listener: &gwapiv1.Listener{Hostname: host}, + } if len(cert.DNSNames) > 0 { - matchedHosts = computeHosts(cert.DNSNames, host) + matchedHosts = computeHosts(cert.DNSNames, &listenerContext) } else { - matchedHosts = computeHosts([]string{cert.Subject.CommonName}, host) + matchedHosts = computeHosts([]string{cert.Subject.CommonName}, &listenerContext) } if len(matchedHosts) > 0 {