Page MenuHomePhabricator

Evaluate Envoy proxy for API gateway (and rate-limiter)
Open, MediumPublic

Description

T246379 resulted in a list of candidates for an API router, among them Envoy. Since Envoy is already in use at the foundation, we should be particularly thorough in determining its feasibility. Where gaps exist, we should establish what sort of extensions would be required to satisfy requirements.


Evaluation

Routing requests

The routes we require will generally take the form api.wm.o/{something}/v{version}/{project}/{lang}/{path}{lang}.{project}.org/{...}; We need to parse language and project from the source URL, and use it to construct a destination hostname. Envoy has no native means of supporting this.

Our test configuration uses the Lua HTTP filter to parse the URL, string format the destination hostname, and inject a new HTTP header, X-Internal-Host. During routing, auto_host_rewrite_header is used to substitute the destination hostname with the value of X-Internal-Host (see snipet below).

- name: envoy.filters.network.http_connection_manager
  typed_config:
    "@type": type.googleapis.com/envoy.config.filter.network.http_connection_manager.v2.HttpConnectionManager
    stat_prefix: ingress_http
    route_config:
      name: local_route
      virtual_hosts:
      - name: local_service
        domains: ["*"]
        routes:
        - match:
            safe_regex:
              google_re2: {}
              regex: "^/core/v\\d{1}/wikipedia/en/.*$"
          route:
            regex_rewrite:
              pattern:
                google_re2: {}
                regex: ".*\/([^\/]+)\/wikipedia\/([^\/]+)(\/.*)$"
              substitution: /w/rest.php/\1\3
            auto_host_rewrite_header: "x-internal-host"
            cluster: service_echoapi
        - match:
            prefix: "/"
          route:
            host_rewrite: www.google.com
            cluster: service_echoapi
    http_filters:
    - name: envoy.filters.http.lua
      typed_config:
        "@type": type.googleapis.com/envoy.config.filter.http.lua.v2.Lua
        inline_code: |
          function envoy_on_request(request_handle)
            local path = request_handle:headers():get(":path")
            project, lang = string.match(path, "^/%a+/v%d/(%a+)/(%a+)/.*$")

            request_handle:headers():add("x-internal-host", lang .. "." ..project .. ".org")
          end
    - name: envoy.filters.http.router

This works, and despite seeming hacky, seems to be endorsed upstream.

It is not clear what the performance impact of using Lua to pattern match like this would be.

Any improvements would require coding (e.g. forking the filter if the changes cannot be pushed upstream, and/or creating a custom filter to perform the match).

Parsing rate limit values from JWT

In the context of an API router w/ rate-limiting, the JWT is a means of delivering a rate limit as an additional attribute of the payload. There is currently no requirement to perform authorization here; Authorization will be handled upstream. Envoy does have a JWT filter, which unsurprisingly, is scoped as a means of performing authorization. It's not clear that this will be a problem, as long as the filter can be configured to validate the signature, and otherwise allow requests to pass, it may suffice. The option does exist (ala forward_payload_header) to inject a header with the base64 encoded payload. Such a mechanism could be used to pass the payload on to subsequent filters, including one tasked with rate limiting.

Rate limiting
What we want

Ideally, we parse the limit from an attribute of the JWT payload, and pass that to the rate-limiter.

What we have

Envoy has an HTTP filter for global rate limiting, it utilizes a rate limit service with a generic gRPC interface (as defined by rls.proto). The RPC for this is something like:

ShouldRateLimit(RateLimitRequest) → RateLimitResponse

A RateLimitRequest has attributes domain (application namespacing), descriptors, and hits_addend (number of requests to count toward the limit; defaults to 1). The descriptors attribute is used to specify a list of identifiers, if any is over the limit, the request should be rejected.

Obviously, the assumption that is made here is that limit configuration is encapsulated in the rate limiter service, (and this is in fact how the reference implementation works).

Logging
TODO: Do.

See also: https://github.com/eevans/wmf-api-gateway — a test/dev environment using Docker Compose.

Event Timeline

Eevans created this task.Thu, Mar 26, 12:51 AM
Restricted Application added a subscriber: Aklapper. · View Herald TranscriptThu, Mar 26, 12:51 AM
Eevans triaged this task as Medium priority.Thu, Mar 26, 12:53 AM
Eevans updated the task description. (Show Details)
Eevans updated the task description. (Show Details)Thu, Mar 26, 6:38 PM
Eevans updated the task description. (Show Details)Mon, Mar 30, 7:41 PM
Eevans updated the task description. (Show Details)
Eevans updated the task description. (Show Details)Mon, Mar 30, 7:45 PM
Eevans updated the task description. (Show Details)
Eevans updated the task description. (Show Details)Mon, Mar 30, 7:57 PM
Eevans updated the task description. (Show Details)Mon, Mar 30, 8:00 PM
Eevans updated the task description. (Show Details)Mon, Mar 30, 8:39 PM
Eevans updated the task description. (Show Details)Mon, Mar 30, 9:35 PM
Eevans updated the task description. (Show Details)Mon, Mar 30, 9:45 PM
Eevans updated the task description. (Show Details)Mon, Mar 30, 9:54 PM