Wasm & Envoy for more functionality

Gokhan Karadas
4 min readFeb 7, 2021

--

Wasm with envoy

Envoy is a high performance, lb proxy that many service mesh implementations, such as Istio, Consul Connect … At the core of Envoy’s connection and traffic management handling with network filters. You can control all the logging, monitoring with these filters. If you want to add new filters to extend Envoy’s current feature set with new functionalities. There are two ways to do that.

The first option to extend Envoy’s feature set is envoy filters require code changes to the Envoy filters require changes to its code and new binary set. This approach hard to manage all envoy version sync situations…

The second option is dynamically loading new filters using a Web Assembly (WASM) plugin. Web assembly has great advantages.

  • Filters can load dynamically loaded into the running Envoy process.
  • Don’t need to know Envoy core language(C++).
  • When the WASM filter crashes it will not impact the Envoy process. (VM sandbox)
  • You can develop wasm filter more popular language Go, Rust…

Envoy Proxy WASM SDK

Envoy Proxy runs WASM filters inside a stack-based virtual machine. All interactions between the embedding host (Envoy Proxy) and the WASM filter are realized through functions and callbacks provided by the Envoy Proxy WASM SDK. The Envoy Proxy WASM SDK has implementations in various programming languages like:

In this post, we will build wasm using AssemblyScript and envoy.

First, you need to install Envoy:

brew install envoy

AssemblyScript

Create package.json for dependencies.

{
"scripts": {
"asbuild:untouched": "asc assembly/index.ts -b build/untouched.wasm -t build/untouched.wat --use abort=abort_proc_exit --sourceMap --debug",
"asbuild:optimized": "asc assembly/index.ts -b build/optimized.wasm -t build/optimized.wat --use abort=abort_proc_exit --sourceMap --optimize",
"asbuild": "npm run asbuild:untouched && npm run asbuild:optimized",
"test": "node tests"
},
"dependencies": {
"@assemblyscript/loader": "^0.14.8",
"@solo-io/proxy-runtime": "0.1.8"
},
"devDependencies": {
"assemblyscript": "^0.14.8"
}
}

Code

Copy this into assembly/index.ts:

// @ts-ignore
export * from "@solo-io/proxy-runtime/proxy"; // this exports the required functions for the proxy to interact with us.
import { RootContext, Context, registerRootContext, FilterHeadersStatusValues, stream_context } from "@solo-io/proxy-runtime";

class AddHeaderRoot extends RootContext {
createContext(context_id: u32): Context {
return new AddHeader(context_id, this);
}
}

class AddHeader extends Context {
constructor(context_id: u32, root_context: AddHeaderRoot) {
super(context_id, root_context);
}
onResponseHeaders(a: u32, end_of_stream: bool): FilterHeadersStatusValues {
stream_context.headers.response.add("hi", "trendyol!");

return FilterHeadersStatusValues.Continue;
}
}

registerRootContext((context_id: u32) => { return new AddHeaderRoot(context_id); }, "add_header");

We declare a header class it extends from Context. You can implement which functionality you use. we override is onResponseHeaders which will add response header new value.

onRequestHeaders(a: u32, end_of_stream: bool): FilterHeadersStatusValues { return FilterHeadersStatusValues.Continue }onRequestMetadata(a: u32): FilterMetadataStatusValues { return FilterMetadataStatusValues.Continue }

RootContentxt: A root context represents a class of instance specific contexts. This is usually used to hold configuration that is used in the individual contexts.

Context: Context class the base for a class for entities that are per-request or per-connection.

RegisterRootContext: Register a root context and make it available to the runtime.
Params: root_context_factory — A function that creates a new root context.
name — The name of the root context. This should match the name configured in the proxy.

Let's build our wasm filter. If you remember we created package.json that has some build cmd.

To build, simply run:

npm run asbuild

build results will be in the build folder. optimized.wasm are the compiled file that we will use it.

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

static_resources:
listeners:
- name: listener_0
address:
socket_address: { address: 127.0.0.1, port_value: 8099 }
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_trendyol }
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: "add_header"
root_id: "add_header"
vm_config:
vm_id: "my_vm_id"
runtime: "envoy.wasm.runtime.v8"
code:
local:
filename: "optimized.wasm"
allow_precompiled: false
- name: envoy.filters.http.router
typed_config: {}
clusters:
- name: svc_trendyol
connect_timeout: 1.00s
type: STATIC
lb_policy: ROUND_ROBIN
load_assignment:
cluster_name: svc_trendyol
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: 127.0.0.1
port_value: 8080

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

var http = require('http');//create a server object:
http.createServer(function (req, res) {
res.write('Hello World!');
res.end(); //end the response
}).listen(8080);
console.log("server started 8080");

After running the node service.

node demo-server.jsserver started 8080

Let’s run the Envoy proxy with our config file:

$ envoy -c envoycfg.yaml -l debug

Calling our service:

$ curl localhost:8089

should result with the following Envoy’s logs:

':status', '200'
'date', 'Sun, 07 Feb 2021 14:44:38 GMT'
'x-envoy-upstream-service-time', '0'
'hi', 'trendyol!'
'server', 'envoy'
[2021-02-07 17:44:38.959][1963853][debug][client] [source/common/http/codec_client.cc:112] [C9] response complete
[2021-02-07 17:44:38.960][1963853][debug][wasm] [source/extensions/common/wasm/context.cc:1168] wasm log: context id: 2: onLog()
[2021-02-07 17:44:38.960][1963853][debug][wasm] [source/extensions/common/wasm/context.cc:1168] wasm log: context id: 2: onDone()
[2021-02-07 17:44:38.960][1963853][debug][wasm] [source/extensions/common/wasm/context.cc:1168] wasm log: context id: 2: onDelete()

See full code in here.

In the next part, we will cover go based web assembly and building on WebAssembly in Istio.

References:

--

--