Extending Envoy Proxy with Golang WebAssembly

By Effi Bar-She’an

Envoy is a L7 proxy and communication bus designed for large modern service-oriented architectures.

Envoy can be used to monitor and control HTTP connections. One way to do this is using the Lua scripting language, for example to intercept requests and responses. Another option, is using a Web Assembly (WASM) plugin.

As Golang developers, we can develop our WASM plugin using Go SDK.

Let’s write and run an Envoy proxy with a WASM extension written in Go :-)

First, you need to install Envoy:

$ brew update
$ brew install envoy

Note, that alternatively you can work with Docker, but currently on MacOS there is an issue that makes it harder.

WASM extensions can’t be developed with regular Go, instead you use TinyGo:

$ brew tap tinygo-org/tools
$ brew install tinygo

Now, let’s write our main.go to log request headers:

package main

import (
"github.com/tetratelabs/proxy-wasm-go-sdk/proxywasm"
"github.com/tetratelabs/proxy-wasm-go-sdk/proxywasm/types"
)

type helloHttpContext struct {
proxywasm.DefaultHttpContext
}

func main() {

proxywasm.SetNewHttpContext(newHttpContext)
}

func newHttpContext(uint32, uint32) proxywasm.HttpContext {

return &helloHttpContext{}
}

func (ctx *helloHttpContext) OnHttpRequestHeaders(numHeaders int, _ bool) types.Action {

if numHeaders > 0 {
headers, err := proxywasm.GetHttpRequestHeaders()
if err != nil {
proxywasm.LogErrorf("failed to get request headers with '%v'", err)
return types.ActionContinue
}
proxywasm.LogInfof("request headers: '%+v'", headers)
}

return types.ActionContinue
}

helloHttpContext embeds proxywasm.DefaultHttpContext so that we do not need to implement all the methods of HttpContext.

The only method that we override is OnHttpRequestHeaders which will log the request headers on all requests that contain headers.

In order to get the request headers we use proxywasm.GetHttpRequestHeaders. Note, that we can’t retrieve the request body as part of OnHttpRequestHeaders method. To do that we need to override OnHttpRequestBody.

Let’s build our WASM with TinyGo:

$ tinygo build -o ./hello.wasm -scheduler=none -target=wasi ./main.go

This should create the hello.wasm file in the current directory.

Before running envoy we need to to create a config file envoy.yaml. This configuration loads envoy with a WASM filter and then listens to port 8085 as a reverse-proxy to backend service — hello that listens on localhost:8080:

static_resources:
listeners:
- name: listener_0
address:
socket_address: { address: 127.0.0.1, port_value: 8085 }
filter_chains:
- filters:
- name: envoy.filters.network.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: svc_hello }
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: "my_plugin"
root_id: "my_root_id"
vm_config:
vm_id: "my_vm_id"
runtime: "envoy.wasm.runtime.v8"
code:
local:
filename: "hello.wasm"
allow_precompiled: true
- name: envoy.filters.http.router
typed_config: {}

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

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

Next, here’s our backend hello service which listens on port 8080 and responds to /hello which our Envoy proxy will redirect all the traffic to:

package main

import (
"net/http"

log "github.com/sirupsen/logrus"
)

func main() {

m := http.NewServeMux()
m.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) {
if _, err := w.Write([]byte("hola mundo :)")); err != nil {
log.Errorf("failed to stream response with '%v'", err)
}
})
const addr = ":8080"
server := &http.Server{
Addr: addr,
Handler: m,
}
log.Infof("listening on '%s'", addr)
log.Fatal(server.ListenAndServe())
}

After running the hello service, let’s run the Envoy proxy with our config file:

$ envoy -c envoy.yaml -l debug

Note, we’re running it with debug level debug, the default is info.

Calling our service:

$ curl localhost:8085/hello

should result with the follow Envoy’s logs:

wasm log: response headers: [[:status 200] [date Sun, 24 Jan 2021 00:42:00 GMT] [content-length 13] [content-type text/plain; charset=utf-8] [x-envoy-upstream-service-time 0]]

See full code in here.

References:

From the Security Policy Company. This blog is dedicated to cloud-native topics such as Kubernetes, cloud security and micro-services.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store