Repository: envoyproxy/gateway
Pull request #2733 (2024-02-29 23:00:45)

fix(translator): Panic when translating routes with empty backends
This PR fixes a panic which occurs when adding either an `HTTPRoute` or `GRPCRoute` with no filters or backendRefs. For example:

```yaml
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: empty-backends
spec:
  parentRefs:
  - group: gateway.networking.k8s.io
    kind: Gateway
    name: eg
  rules:
  - matches:
    - path:
        type: PathPrefix
        value: /
```

```
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x8 pc=0x190dadc]

goroutine 59 [running]:
github.com/envoyproxy/gateway/internal/xds/translator.buildXdsRouteAction(...)
    /Users/davidalger/work/envoyproxy/gateway/internal/xds/translator/route.go:214
github.com/envoyproxy/gateway/internal/xds/translator.buildXdsRoute(0x40007c2680)
    /Users/davidalger/work/envoyproxy/gateway/internal/xds/translator/route.go:66 +0x46c
github.com/envoyproxy/gateway/internal/xds/translator.(*Translator).processHTTPListenerXdsTranslation(0x4000b15348, 0x4000708ee0, {0x4000691d40, 0x1, 0x1ffa23a?}, 0x12?, 0x1?, 0x0)
    /Users/davidalger/work/envoyproxy/gateway/internal/xds/translator/translator.go:244 +0xab4
github.com/envoyproxy/gateway/internal/xds/translator.(*Translator).Translate(0x4000b15348, 0x40003f9700)
    /Users/davidalger/work/envoyproxy/gateway/internal/xds/translator/translator.go:79 +0x68
github.com/envoyproxy/gateway/internal/xds/translator/runner.(*Runner).subscribeAndTranslate.func1({{0x4000b74940?, 0x0?}, 0x0?, 0x40003f9700?}, 0x0?)
    /Users/davidalger/work/envoyproxy/gateway/internal/xds/translator/runner/runner.go:80 +0x1d0
github.com/envoyproxy/gateway/internal/message.HandleSubscription[...]({{0x1ff221a, 0xe?}, {0x1fe523f?, 0x6?}}, 0x4000141740?, 0x4000b15f88)
    /Users/davidalger/work/envoyproxy/gateway/internal/message/watchutil.go:76 +0xd74
github.com/envoyproxy/gateway/internal/xds/translator/runner.(*Runner).subscribeAndTranslate(0x40002c6100, {0x235bdf0?, 0x400079bd60?})
    /Users/davidalger/work/envoyproxy/gateway/internal/xds/translator/runner/runner.go:52 +0x88
created by github.com/envoyproxy/gateway/internal/xds/translator/runner.(*Runner).Start in goroutine 1
    /Users/davidalger/work/envoyproxy/gateway/internal/xds/translator/runner/runner.go:45 +0x1f0
Stream closed EOF for envoy-gateway-system/envoy-gateway-c589d95bf-mw66t (envoy-gateway)
```


codecov[bot]: ## [Codecov](https://app.codecov.io/gh/envoyproxy/gateway/pull/2733?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=envoyproxy) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 63.31%. Comparing base [(`c9b3f19`)](https://app.codecov.io/gh/envoyproxy/gateway/commit/c9b3f1940711e319d0f77a1cefbdae33d61a14af?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=envoyproxy) to head [(`212c5af`)](https://app.codecov.io/gh/envoyproxy/gateway/pull/2733?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=envoyproxy).


<details><summary>Additional details and impacted files</summary>


```diff
@@           Coverage Diff           @@
##             main    #2733   +/-   ##
=======================================
  Coverage   63.31%   63.31%           
=======================================
  Files         123      123           
  Lines       19988    19988           
=======================================
  Hits        12655    12655           
  Misses       6519     6519           
  Partials      814      814           
```



</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/envoyproxy/gateway/pull/2733?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=envoyproxy).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=envoyproxy).

davidalger: /retest
davidalger: /retest

Patch:
diff --git a/internal/gatewayapi/route.go b/internal/gatewayapi/route.go
index b09e08ba7..d913283e0 100644
--- a/internal/gatewayapi/route.go
+++ b/internal/gatewayapi/route.go
@@ -209,7 +209,7 @@ func (t *Translator) processHTTPRouteRules(httpRoute *HTTPRouteContext, parentRe
 
 		// If the route has no valid backends then just use a direct response and don't fuss with weighted responses
 		for _, ruleRoute := range ruleRoutes {
-			if ruleRoute.BackendWeights.Invalid > 0 && ruleRoute.Destination == nil {
+			if ruleRoute.Destination == nil && ruleRoute.Redirect == nil {
 				ruleRoute.DirectResponse = &ir.DirectResponse{
 					StatusCode: 500,
 				}
@@ -493,7 +493,7 @@ func (t *Translator) processGRPCRouteRules(grpcRoute *GRPCRouteContext, parentRe
 
 		// If the route has no valid backends then just use a direct response and don't fuss with weighted responses
 		for _, ruleRoute := range ruleRoutes {
-			if ruleRoute.BackendWeights.Invalid > 0 && ruleRoute.Destination == nil {
+			if ruleRoute.Destination == nil && ruleRoute.Redirect == nil {
 				ruleRoute.DirectResponse = &ir.DirectResponse{
 					StatusCode: 500,
 				}


Test patch:
diff --git a/internal/gatewayapi/testdata/grpcroute-with-empty-backends.in.yaml b/internal/gatewayapi/testdata/grpcroute-with-empty-backends.in.yaml
new file mode 100644
index 000000000..69bfd7a1f
--- /dev/null
+++ b/internal/gatewayapi/testdata/grpcroute-with-empty-backends.in.yaml
@@ -0,0 +1,31 @@
+gateways:
+- apiVersion: gateway.networking.k8s.io/v1
+  kind: Gateway
+  metadata:
+    namespace: envoy-gateway
+    name: gateway-1
+  spec:
+    gatewayClassName: envoy-gateway-class
+    listeners:
+    - name: http
+      protocol: HTTP
+      port: 80
+      allowedRoutes:
+        namespaces:
+          from: All
+grpcRoutes:
+- apiVersion: gateway.networking.k8s.io/v1alpha2
+  kind: GRPCRoute
+  metadata:
+    namespace: default
+    name: grpcroute-1
+  spec:
+    parentRefs:
+    - namespace: envoy-gateway
+      name: gateway-1
+      sectionName: http
+    rules:
+    - matches:
+      - method:
+          service: com.ExampleExact
+          type: Exact
diff --git a/internal/gatewayapi/testdata/grpcroute-with-empty-backends.out.yaml b/internal/gatewayapi/testdata/grpcroute-with-empty-backends.out.yaml
new file mode 100755
index 000000000..57d5ad03b
--- /dev/null
+++ b/internal/gatewayapi/testdata/grpcroute-with-empty-backends.out.yaml
@@ -0,0 +1,120 @@
+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: http
+      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: http
+      supportedKinds:
+      - group: gateway.networking.k8s.io
+        kind: HTTPRoute
+      - group: gateway.networking.k8s.io
+        kind: GRPCRoute
+grpcRoutes:
+- apiVersion: gateway.networking.k8s.io/v1alpha2
+  kind: GRPCRoute
+  metadata:
+    creationTimestamp: null
+    name: grpcroute-1
+    namespace: default
+  spec:
+    parentRefs:
+    - name: gateway-1
+      namespace: envoy-gateway
+      sectionName: http
+    rules:
+    - matches:
+      - method:
+          service: com.ExampleExact
+          type: Exact
+  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
+        sectionName: http
+infraIR:
+  envoy-gateway/gateway-1:
+    proxy:
+      listeners:
+      - address: null
+        name: envoy-gateway/gateway-1/http
+        ports:
+        - containerPort: 10080
+          name: http
+          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: true
+      name: envoy-gateway/gateway-1/http
+      path:
+        escapedSlashesAction: UnescapeAndRedirect
+        mergeSlashes: true
+      port: 10080
+      routes:
+      - backendWeights:
+          invalid: 0
+          valid: 0
+        directResponse:
+          statusCode: 500
+        hostname: '*'
+        isHTTP2: true
+        name: grpcroute/default/grpcroute-1/rule/0/match/0/*
+        pathMatch:
+          distinct: false
+          name: ""
+          prefix: /com.ExampleExact
diff --git a/internal/gatewayapi/testdata/httproute-rule-with-empty-backends-and-no-filters.in.yaml b/internal/gatewayapi/testdata/httproute-rule-with-empty-backends-and-no-filters.in.yaml
new file mode 100644
index 000000000..ba4acbbc7
--- /dev/null
+++ b/internal/gatewayapi/testdata/httproute-rule-with-empty-backends-and-no-filters.in.yaml
@@ -0,0 +1,29 @@
+gateways:
+  - apiVersion: gateway.networking.k8s.io/v1
+    kind: Gateway
+    metadata:
+      namespace: envoy-gateway
+      name: gateway-1
+    spec:
+      gatewayClassName: envoy-gateway-class
+      listeners:
+        - name: http
+          protocol: HTTP
+          port: 80
+          allowedRoutes:
+            namespaces:
+              from: All
+httpRoutes:
+  - apiVersion: gateway.networking.k8s.io/v1
+    kind: HTTPRoute
+    metadata:
+      namespace: default
+      name: httproute-1
+    spec:
+      parentRefs:
+        - namespace: envoy-gateway
+          name: gateway-1
+      rules:
+        - matches:
+            - path:
+                value: "/"
diff --git a/internal/gatewayapi/testdata/httproute-rule-with-empty-backends-and-no-filters.out.yaml b/internal/gatewayapi/testdata/httproute-rule-with-empty-backends-and-no-filters.out.yaml
new file mode 100755
index 000000000..d49bb265d
--- /dev/null
+++ b/internal/gatewayapi/testdata/httproute-rule-with-empty-backends-and-no-filters.out.yaml
@@ -0,0 +1,117 @@
+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: http
+      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: http
+      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: default
+  spec:
+    parentRefs:
+    - name: gateway-1
+      namespace: envoy-gateway
+    rules:
+    - matches:
+      - path:
+          value: /
+  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
+infraIR:
+  envoy-gateway/gateway-1:
+    proxy:
+      listeners:
+      - address: null
+        name: envoy-gateway/gateway-1/http
+        ports:
+        - containerPort: 10080
+          name: http
+          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
+      name: envoy-gateway/gateway-1/http
+      path:
+        escapedSlashesAction: UnescapeAndRedirect
+        mergeSlashes: true
+      port: 10080
+      routes:
+      - backendWeights:
+          invalid: 0
+          valid: 0
+        directResponse:
+          statusCode: 500
+        hostname: '*'
+        isHTTP2: false
+        name: httproute/default/httproute-1/rule/0/match/0/*
+        pathMatch:
+          distinct: false
+          name: ""
+          prefix: /
diff --git a/site/content/en/latest/user/http-routing.md b/site/content/en/latest/user/http-routing.md
index 4d710c8a0..1bcf9a0db 100644
--- a/site/content/en/latest/user/http-routing.md
+++ b/site/content/en/latest/user/http-routing.md
@@ -129,6 +129,107 @@ curl -vvv --header "Host: bar.example.com" --header "env: canary" "http://${GATE
 A `200` status code should be returned and the body should include `"pod": "bar-canary-backend-*"` indicating the
 traffic was routed to the foo backend service.
 
+### JWT Claims Based Routing
+
+Users can route to a specific backend by matching on JWT claims.
+This can be achieved, by defining a SecurityPolicy with a jwt configuration that does the following
+* Converts jwt claims to headers, which can be used for header based routing
+* Sets the recomputeRoute field to `true`. This is required so that the incoming request matches on a fallback/catch all route where the JWT can be authenticated, the claims from the JWT can be converted to headers, and then the route match can be recomputed to match based on the updated headers.
+
+For this feature to work please make sure
+* you have a fallback route rule defined, the backend for this route rule can be invalid.
+* The SecurityPolicy is applied to both the fallback route as well as the route with the claim header matches, to avoid spoofing.
+
+```shell
+cat <<EOF | kubectl apply -f -
+apiVersion: gateway.envoyproxy.io/v1alpha1
+kind: SecurityPolicy
+metadata:
+  name: jwt-example
+spec:
+  targetRef:
+    group: gateway.networking.k8s.io
+    kind: HTTPRoute
+    name: jwt-claim-routing
+  jwt:
+    providers:
+      - name: example
+        recomputeRoute: true
+        claimToHeaders:
+          - claim: sub
+            header: x-sub
+          - claim: admin
+            header: x-admin
+          - claim: name
+            header: x-name
+        remoteJWKS:
+          uri: https://raw.githubusercontent.com/envoyproxy/gateway/main/examples/kubernetes/jwt/jwks.json
+---
+apiVersion: gateway.networking.k8s.io/v1
+kind: HTTPRoute
+metadata:
+  name: jwt-claim-routing
+spec:
+  parentRefs:
+    - name: eg
+  rules:
+    - backendRefs:
+        - kind: Service
+          name: foo-svc
+          port: 8080
+          weight: 1
+      matches:
+        - headers:
+            - name: x-name
+              value: John Doe
+    - backendRefs:
+        - kind: Service
+          name: bar-svc
+          port: 8080
+          weight: 1
+      matches:
+        - headers:
+            - name: x-name
+              value: Tom
+    # catch all
+    - backendRefs:
+        - kind: Service
+          name: infra-backend-invalid
+          port: 8080
+          weight: 1
+      matches:
+        - path:
+            type: PathPrefix
+            value: /
+EOF
+```
+
+Get the JWT used for testing request authentication:
+
+```shell
+TOKEN=$(curl https://raw.githubusercontent.com/envoyproxy/gateway/main/examples/kubernetes/jwt/test.jwt -s) && echo "$TOKEN" | cut -d '.' -f2 - | base64 --decode -
+```
+
+Test routing to the `foo-svc` backend by specifying a JWT Token with a claim `name: John Doe`.
+
+```shell
+curl -sS -H "Authorization: Bearer $TOKEN" "http://${GATEWAY_HOST}/" | jq .pod
+"foo-backend-6df8cc6b9f-fmwcg"
+```
+
+Get another JWT used for testing request authentication:
+
+```shell
+TOKEN=$(curl https://raw.githubusercontent.com/envoyproxy/gateway/main/examples/kubernetes/jwt/with-different-claim.jwt -s) && echo "$TOKEN" | cut -d '.' -f2 - | base64 --decode -
+```
+
+Test HTTP routing to the `bar-svc` backenbackend by specifying a JWT Token with a claim `name: Tom`.
+
+```shell
+curl -sS -H "Authorization: Bearer $TOKEN" "http://${GATEWAY_HOST}/" | jq .pod
+"bar-backend-6688b8944c-s8htr"
+```
+
 [HTTPRoute]: https://gateway-api.sigs.k8s.io/api-types/httproute/
 [Gateway API documentation]: https://gateway-api.sigs.k8s.io/
 [GatewayClass]: https://gateway-api.sigs.k8s.io/api-types/gatewayclass/