diff --git a/internal/gatewayapi/helpers.go b/internal/gatewayapi/helpers.go index a6cde9133c..b30703b8f3 100644 --- a/internal/gatewayapi/helpers.go +++ b/internal/gatewayapi/helpers.go @@ -333,18 +333,30 @@ func computeHosts(routeHostnames []string, listenerContext *ListenerContext) []s case listenerHostnameVal == routeHostname: hostnamesSet.Insert(routeHostname) + // Both listener and route hostname have wildcards: + case strings.HasPrefix(listenerHostnameVal, "*") && strings.HasPrefix(routeHostname, "*"): + // the route hostname must be more wildcard than the listener hostname to match + // e.g. listener hostname *.example.com would match route hostname *.com. + // use regex to check this + if wildcardHostnameMatchesHostname(routeHostname, listenerHostnameVal) { + hostnamesSet.Insert(listenerHostnameVal) + } + + if wildcardHostnameMatchesHostname(listenerHostnameVal, routeHostname) { + hostnamesSet.Insert(routeHostname) + } + // Listener has a wildcard hostname: check if the route hostname matches. case strings.HasPrefix(listenerHostnameVal, "*"): - if hostnameMatchesWildcardHostname(routeHostname, listenerHostnameVal) { + if wildcardHostnameMatchesHostname(listenerHostnameVal, routeHostname) { hostnamesSet.Insert(routeHostname) } // Route has a wildcard hostname: check if the listener hostname matches. case strings.HasPrefix(routeHostname, "*"): - if hostnameMatchesWildcardHostname(listenerHostnameVal, routeHostname) { + if wildcardHostnameMatchesHostname(routeHostname, listenerHostnameVal) { hostnamesSet.Insert(listenerHostnameVal) } - } } @@ -370,16 +382,39 @@ func computeHosts(routeHostnames []string, listenerContext *ListenerContext) []s return hostnamesSet.List() } -// hostnameMatchesWildcardHostname returns true if hostname has the non-wildcard -// portion of wildcardHostname as a suffix, plus at least one DNS label matching the -// wildcard. -func hostnameMatchesWildcardHostname(hostname, wildcardHostname string) bool { - if !strings.HasSuffix(hostname, strings.TrimPrefix(wildcardHostname, "*")) { +// wildcardHostnameMatchesHostname returns true if wildcardHostname matches hostname. +// ref: https://github.com/kubernetes-sigs/gateway-api/pull/1173, this's different with RFC-2818 +// e.g. *.com matches *.example.com, *.example.com matches foo.example.com +func wildcardHostnameMatchesHostname(wildcardHostname, hostname string) bool { + // Strip the leading "*" from wildcardHostname + wildcardSuffix := strings.TrimPrefix(wildcardHostname, "*") + + // If hostname is not a wildcard, check if it matches the pattern + if !strings.HasPrefix(hostname, "*") { + // hostname must end with the wildcard suffix + if !strings.HasSuffix(hostname, wildcardSuffix) { + return false + } + // The part before the suffix should be non-empty (there's a label matching the wildcard) + wildcardMatch := strings.TrimSuffix(hostname, wildcardSuffix) + return len(wildcardMatch) > 0 + } + + // Both are wildcards - strip the leading "*" from hostname too + hostnameSuffix := strings.TrimPrefix(hostname, "*") + + // Check if the hostname suffix ends with the wildcard suffix + // This means wildcardHostname is a broader pattern + if !strings.HasSuffix(hostnameSuffix, wildcardSuffix) { return false } - wildcardMatch := strings.TrimSuffix(hostname, strings.TrimPrefix(wildcardHostname, "*")) - return len(wildcardMatch) > 0 + // Get the remaining part after removing the wildcard suffix from hostname + remaining := strings.TrimSuffix(hostnameSuffix, wildcardSuffix) + + // The remaining part should have content (can't be identical patterns) + // and should start with "." to be a valid subdomain + return len(remaining) > 0 && strings.HasPrefix(remaining, ".") } func containsPort(ports []*protocolPort, port *protocolPort) bool { diff --git a/internal/gatewayapi/helpers_test.go b/internal/gatewayapi/helpers_test.go index 9d24569f17..44940e634f 100644 --- a/internal/gatewayapi/helpers_test.go +++ b/internal/gatewayapi/helpers_test.go @@ -895,3 +895,56 @@ func TestIrStringMatch(t *testing.T) { }) } } + +func TestWildcardHostnameMatchesHostname(t *testing.T) { + testCases := []struct { + name string + wildcard string + hostname string + expected bool + }{ + { + name: "*.com matches *.example.com", + wildcard: "*.com", + hostname: "*.example.com", + expected: true, + }, + { + name: "*.example.com matches *.foo.example.com", + wildcard: "*.example.com", + hostname: "*.foo.example.com", + expected: true, + }, + { + name: "*.com does not match *.net", + wildcard: "*.com", + hostname: "*.net", + expected: false, + }, + { + name: "*.example.com does not match *.other.com", + wildcard: "*.example.com", + hostname: "*.other.com", + expected: false, + }, + { + name: "*.foo.example.com does not match *.example.com", + wildcard: "*.foo.example.com", + hostname: "*.example.com", + expected: false, + }, + { + name: "*.example.com match foo.example.com", + wildcard: "*.example.com", + hostname: "foo.example.com", + expected: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + result := wildcardHostnameMatchesHostname(tc.wildcard, tc.hostname) + require.Equal(t, tc.expected, result) + }) + } +} diff --git a/internal/gatewayapi/testdata/tlsroute-hostname-intersection.in.yaml b/internal/gatewayapi/testdata/tlsroute-hostname-intersection.in.yaml new file mode 100644 index 0000000000..45c81994bc --- /dev/null +++ b/internal/gatewayapi/testdata/tlsroute-hostname-intersection.in.yaml @@ -0,0 +1,66 @@ +gateways: + - apiVersion: gateway.networking.k8s.io/v1 + kind: Gateway + metadata: + namespace: envoy-gateway + name: gateway-1 + spec: + gatewayClassName: envoy-gateway-class + listeners: + - name: tls + protocol: TLS + hostname: "*.example.com" + port: 90 + tls: + mode: Passthrough + allowedRoutes: + namespaces: + from: All + - apiVersion: gateway.networking.k8s.io/v1 + kind: Gateway + metadata: + namespace: envoy-gateway + name: gateway-2 + spec: + gatewayClassName: envoy-gateway-class + listeners: + - name: tls + protocol: TLS + hostname: "*.com" + port: 90 + tls: + mode: Passthrough + allowedRoutes: + namespaces: + from: All +tlsRoutes: + - apiVersion: gateway.networking.k8s.io/v1alpha2 + kind: TLSRoute + metadata: + namespace: default + name: tlsroute-1 + spec: + parentRefs: + - namespace: envoy-gateway + name: gateway-1 + hostnames: + - "*.com" + rules: + - backendRefs: + - name: service-1 + port: 8080 + - apiVersion: gateway.networking.k8s.io/v1alpha2 + kind: TLSRoute + metadata: + namespace: default + name: tlsroute-2 + spec: + parentRefs: + - namespace: envoy-gateway + name: gateway-2 + hostnames: + - "*.example.com" + rules: + - backendRefs: + - name: service-1 + port: 8080 diff --git a/internal/gatewayapi/testdata/tlsroute-hostname-intersection.out.yaml b/internal/gatewayapi/testdata/tlsroute-hostname-intersection.out.yaml new file mode 100644 index 0000000000..2a093d78ed --- /dev/null +++ b/internal/gatewayapi/testdata/tlsroute-hostname-intersection.out.yaml @@ -0,0 +1,324 @@ +gateways: +- apiVersion: gateway.networking.k8s.io/v1 + kind: Gateway + metadata: + name: gateway-1 + namespace: envoy-gateway + spec: + gatewayClassName: envoy-gateway-class + listeners: + - allowedRoutes: + namespaces: + from: All + hostname: '*.example.com' + name: tls + port: 90 + protocol: TLS + tls: + mode: Passthrough + 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: tls + supportedKinds: + - group: gateway.networking.k8s.io + kind: TLSRoute +- apiVersion: gateway.networking.k8s.io/v1 + kind: Gateway + metadata: + name: gateway-2 + namespace: envoy-gateway + spec: + gatewayClassName: envoy-gateway-class + listeners: + - allowedRoutes: + namespaces: + from: All + hostname: '*.com' + name: tls + port: 90 + protocol: TLS + tls: + mode: Passthrough + 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: tls + supportedKinds: + - group: gateway.networking.k8s.io + kind: TLSRoute +infraIR: + envoy-gateway/gateway-1: + proxy: + listeners: + - address: null + name: envoy-gateway/gateway-1/tls + ports: + - containerPort: 10090 + name: tls-90 + protocol: TLS + servicePort: 90 + metadata: + labels: + gateway.envoyproxy.io/owning-gateway-name: gateway-1 + gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway + ownerReference: + kind: GatewayClass + name: envoy-gateway-class + name: envoy-gateway/gateway-1 + namespace: envoy-gateway-system + envoy-gateway/gateway-2: + proxy: + listeners: + - address: null + name: envoy-gateway/gateway-2/tls + ports: + - containerPort: 10090 + name: tls-90 + protocol: TLS + servicePort: 90 + metadata: + labels: + gateway.envoyproxy.io/owning-gateway-name: gateway-2 + gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway + ownerReference: + kind: GatewayClass + name: envoy-gateway-class + name: envoy-gateway/gateway-2 + namespace: envoy-gateway-system +tlsRoutes: +- apiVersion: gateway.networking.k8s.io/v1alpha2 + kind: TLSRoute + metadata: + name: tlsroute-1 + namespace: default + spec: + hostnames: + - '*.com' + parentRefs: + - name: gateway-1 + namespace: envoy-gateway + rules: + - backendRefs: + - name: service-1 + port: 8080 + status: + parents: + - conditions: + - lastTransitionTime: null + message: Route is accepted + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: Resolved all the Object references for the Route + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + controllerName: gateway.envoyproxy.io/gatewayclass-controller + parentRef: + name: gateway-1 + namespace: envoy-gateway +- apiVersion: gateway.networking.k8s.io/v1alpha2 + kind: TLSRoute + metadata: + name: tlsroute-2 + namespace: default + spec: + hostnames: + - '*.example.com' + parentRefs: + - name: gateway-2 + namespace: envoy-gateway + rules: + - backendRefs: + - name: service-1 + port: 8080 + status: + parents: + - conditions: + - lastTransitionTime: null + message: Route is accepted + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: Resolved all the Object references for the Route + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + controllerName: gateway.envoyproxy.io/gatewayclass-controller + parentRef: + name: gateway-2 + namespace: envoy-gateway +xdsIR: + envoy-gateway/gateway-1: + accessLog: + json: + - path: /dev/stdout + globalResources: + proxyServiceCluster: + metadata: + kind: Service + name: envoy-envoy-gateway-gateway-1-196ae069 + namespace: envoy-gateway-system + sectionName: "8080" + name: envoy-gateway/gateway-1 + settings: + - addressType: IP + endpoints: + - host: 7.6.5.4 + port: 8080 + zone: zone1 + metadata: + kind: Service + name: envoy-envoy-gateway-gateway-1-196ae069 + namespace: envoy-gateway-system + sectionName: "8080" + name: envoy-gateway/gateway-1 + protocol: TCP + readyListener: + address: 0.0.0.0 + ipFamily: IPv4 + path: /ready + port: 19003 + tcp: + - address: 0.0.0.0 + externalPort: 90 + metadata: + kind: Gateway + name: gateway-1 + namespace: envoy-gateway + sectionName: tls + name: envoy-gateway/gateway-1/tls + port: 10090 + routes: + - destination: + metadata: + kind: TLSRoute + name: tlsroute-1 + namespace: default + name: tlsroute/default/tlsroute-1/rule/-1 + settings: + - addressType: IP + endpoints: + - host: 7.7.7.7 + port: 8080 + metadata: + kind: Service + name: service-1 + namespace: default + sectionName: "8080" + name: tlsroute/default/tlsroute-1/rule/-1/backend/0 + protocol: HTTPS + weight: 1 + metadata: + kind: TLSRoute + name: tlsroute-1 + namespace: default + name: tlsroute/default/tlsroute-1 + tls: + inspector: + snis: + - '*.example.com' + envoy-gateway/gateway-2: + accessLog: + json: + - path: /dev/stdout + globalResources: + proxyServiceCluster: + metadata: + kind: Service + name: envoy-envoy-gateway-gateway-2-4a0e4eb9 + namespace: envoy-gateway-system + sectionName: "8080" + name: envoy-gateway/gateway-2 + settings: + - addressType: IP + endpoints: + - host: 7.6.5.4 + port: 8080 + zone: zone1 + metadata: + kind: Service + name: envoy-envoy-gateway-gateway-2-4a0e4eb9 + namespace: envoy-gateway-system + sectionName: "8080" + name: envoy-gateway/gateway-2 + protocol: TCP + readyListener: + address: 0.0.0.0 + ipFamily: IPv4 + path: /ready + port: 19003 + tcp: + - address: 0.0.0.0 + externalPort: 90 + metadata: + kind: Gateway + name: gateway-2 + namespace: envoy-gateway + sectionName: tls + name: envoy-gateway/gateway-2/tls + port: 10090 + routes: + - destination: + metadata: + kind: TLSRoute + name: tlsroute-2 + namespace: default + name: tlsroute/default/tlsroute-2/rule/-1 + settings: + - addressType: IP + endpoints: + - host: 7.7.7.7 + port: 8080 + metadata: + kind: Service + name: service-1 + namespace: default + sectionName: "8080" + name: tlsroute/default/tlsroute-2/rule/-1/backend/0 + protocol: HTTPS + weight: 1 + metadata: + kind: TLSRoute + name: tlsroute-2 + namespace: default + name: tlsroute/default/tlsroute-2 + tls: + inspector: + snis: + - '*.example.com'