Categorygithub.com/picatz/hook
repository
0.0.1
Repository: https://github.com/picatz/hook.git
Documentation: pkg.go.dev

# Packages

No description provided by the author
No description provided by the author

# README

šŸ“ā€ā˜ ļø Hook

Proxy WASM filter SDK with a bit of fairy dust āœØšŸ§šā€ā™€ļø

Examples

To help you get started, here are a few examples to use as reference:

  • Header is a filter that sets the wasm: enabled HTTP header on all requests/responses.
  • Security Headers is a filter that adds multiple security headers on all HTTP responses.
  • Replace Response is a filter that replaces the HTTP response body (and content-length header) for all HTTP responses using a custom http.Context object.
  • Sniff is a filter that logs all HTTP request/response headers and bodies.

Why

There is already a similar SDK framework for Go maintained by @mathetake, and I totally suggest using that SDK. They are doing fantastic work across the Envoy, WASM, and TinyGo community. This is currently a personal project to experiment with different SDK patterns focused on breaking up the required pieces into logical packages, and enabling a potentially more idiomatic SDK. It's another option for the community.

I've also started playing with enabling WASM filters in HashiCorp Consul using expierimental changes on the wasm-filters branch. Having a ton of fun learning how exactly this all works at a lower-level, and exposing clean APIs at a higher-level in Consul and Go.

The Consul service config to enable WASM filters for an Envoy sidecar proxy looks like this:

protocol = "http"
wasm_filters = [
    {
        name       = "security_headers"
        local_file = "path/to/filters/security_headers.wasm"
    },
    {
        name       = "replace_response"
        local_file = "path/to/filters/replace_response.wasm"
    }
]
ā„¹ļø Full configuration example (click to expand)

Sample configuration using wasm_filters for Consul to configure Envoy sidecar proxies with the /tmp/filters/replace_response.wasm WASM filter, assuming this binary file is appropriatley compiled and is avaiable on the local filesystem of the sidecar host.

service {
  name = "web"
  port = 8080
  connect {
      sidecar_service {
          proxy {
              config {
                  protocol = "http"
                  wasm_filters = [
                      {
                          name       = "replace_response"
                          local_file = "/tmp/filters/replace_response.wasm"
                      }
                  ]
              }
          }
      }
}

šŸ‘©šŸ½ā€šŸ’» Learn more about Consul here.

SDK Usage

Simple example to set the wasm: enabled HTTP header on all requests/responses through the proxy. This is obviously a trivial example in order to demonstrate the how to start "hooking" into the the proxy.

package main

import (
    "github.com/picatz/hook/pkg/call/http"
    "github.com/picatz/hook/pkg/types/action"
)

func main() {
    http.OnRequestHeaders(func(int, bool) action.Type {
        http.SetRequestHeader("wasm", "enabled")
        return action.Continue
    })

    http.OnResponseHeaders(func(int, bool) action.Type {
        http.SetResponseHeader("wasm", "enabled")
        return action.Continue
    })
}
ā„¹ļø Example details (click to expand)
  • github.com/picatz/hook/pkg/call/http provides functions to interact with HTTP requests and responses.
    • http.OnRequestHeaders is a function to hook into the proxy. From here you can inspect, set, delete, read, and further handle header-based authn/authz tasks.
      • You can optionally use the two arguments provided to the function including the maxSize (int type) and endOfStream (bool type). You do not need to name these types at all if you do not plan to use them. That is a subtle, often unused, feature of the Go language.
      • To continue processing the request after your logic, return the action.Continue type to signal the HTTP stream is ready to be handled again by the proxy.
    • http.OnResponseHeaders is a function to hook into the the response headers, very similiar to http.OnRequestHeaders, but for the response side of the upstream service. You can do essentially the exact same things, but applying the logic to the "other side" of the proxy connection.
    • http.SetRequestHeader is a function to set an HTTP request header using a given key and value.
    • http.SetResponseHeader is a function to set an HTTP response header using a given key and value.
  • github.com/picatz/hook/pkg/types/action provides types to signal the proxy to continue/stop processing the next steps of the request/response stream.
    • action.Continue is a common type used to signal to the proxy to continue handling the connection.

Build with TinyGo

Compile using tinygo to bulild example.wasm:

$ tinygo build -o example.wasm -scheduler=none -target=wasi -wasm-abi=generic main.go

Configure Envoy Proxy

Add the filter config to the Envoy http_filters section using a version with WASM support:

# envoy.yaml
name: envoy.filters.http.wasm
typed_config:
  "@type": type.googleapis.com/udpa.type.v1.TypedStruct
  type_url: type.googleapis.com/envoy.extensions.filters.http.wasm.v3.Wasm
  value:
    config:
      name: "example"
      root_id: "example"
      vm_config:
        vm_id: "example"
        runtime: "envoy.wasm.runtime.v8"
        code:
          local:
            filename: "/full/path/to/example.wasm"
        allow_precompiled: true
ā„¹ļø Full configuration example (click to expand)
static_resources:
listeners:
  - name: main
    address:
      socket_address:
        address: 0.0.0.0
        port_value: 18000
    filter_chains:
      - filters:
          - name: envoy.http_connection_manager
            typed_config:
              "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
              stat_prefix: ingress_http
              codec_type: auto
              route_config:
                name: local_route
                virtual_hosts:
                  - name: local_service
                    domains:
                      - "*"
                    routes:
                      - match:
                          prefix: "/"
                        route:
                          cluster: web_service
              http_filters:
                - name: envoy.filters.http.wasm
                  typed_config:
                    "@type": type.googleapis.com/udpa.type.v1.TypedStruct
                    type_url: type.googleapis.com/envoy.extensions.filters.http.wasm.v3.Wasm
                    value:
                      config:
                        name: "req_body_replace"
                        root_id: "req_body_replace"
                        vm_config:
                          vm_id: "req_body_replace"
                          runtime: "envoy.wasm.runtime.v8"
                          code:
                            local:
                              filename: "./examples/header/header.wasm"
                          allow_precompiled: true
                - name: envoy.filters.http.router
                  typed_config: {}

  - name: staticreply
    address:
      socket_address:
        address: 127.0.0.1
        port_value: 8099
    filter_chains:
      - filters:
          - name: envoy.http_connection_manager
            typed_config:
              "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
              stat_prefix: ingress_http
              codec_type: auto
              route_config:
                name: local_route
                virtual_hosts:
                  - name: local_service
                    domains:
                      - "*"
                    routes:
                      - match:
                          prefix: "/"
                        direct_response:
                          status: 200
                          body:
                            inline_string: "example body\n"
              http_filters:
                - name: envoy.filters.http.router
                  typed_config: {}

clusters:
  - name: web_service
    connect_timeout: 0.25s
    type: STATIC
    lb_policy: ROUND_ROBIN
    load_assignment:
      cluster_name: mock_service
      endpoints:
        - lb_endpoints:
            - endpoint:
                address:
                  socket_address:
                    address: 127.0.0.1
                    port_value: 8099

admin:
  access_log_path: "/dev/null"
  address:
    socket_address:
      address: 0.0.0.0
      port_value: 8001

Run with Docker

Run an the envoyproxy/envoy-dev:latest container with the required config:

$ mkdir -p /tmp/envoy/filters        # setup temp dir to share with container
$ cp envoy.yaml  /tmp/envoy          # copy full envoy config into dir
$ cp example.wasm /tmp/envoy/filters # copy wasm binary into dir
$ docker run --entrypoint='/usr/local/bin/envoy' \
    -p 18000:18000 -p 8099:8099 \
    -w /tmp/envoy \
    -v /tmp/envoy:/tmp/envoy \
    envoyproxy/envoy-dev:latest \
    -c /tmp/envoy/envoy.yaml --concurrency 2 --log-level info --log-format '%v'
$ curl http://localhost:18000 -v
> GET / HTTP/1.1
> Host: localhost:18000
> User-Agent: curl/7.64.1
> Accept: */*
>
< HTTP/1.1 200 OK
< content-length: 13
< content-type: text/plain
< date: Sat, 05 Dec 2020 00:51:37 GMT
< server: envoy
< x-envoy-upstream-service-time: 0
< wasm: enabled
<
example body

ā˜ļø The wasm: enabled header was successfully applied to the response from the custom WASM filter! šŸŽ‰

Credits, Links, More Information, and Other SDK Options

Finding clear information about how this proxy WASM filter stuff works essentially requires you to navigate several SDK codebases, and finding extra information from GitHub comments, YouTube, or Twitter. This is still the early days of proxies generally supporting WASM filters, and really Envoy is the target proxy for most of this work. But, even Envoy doesn't yet have official documentation for the ABI (Application Binary Interface) to enable this feature. This should come in the near-ish future. Still looking for more information around this stuff myself.

With the lack of documentation, I really wouldn't have been able to make this SDK without the previous work from the community including the C++, Rust, AssemblyScript, and TinyGo code bases. There were also several talks avaialble on YouTube to help provide context. I am really looking forward to the growth of this WASM extension ecosystem, and how other proxies might implement this feature in the future.

šŸ”— Links to Helpful Information

šŸ›  Other SDKs