使用Go为网格代理编写Wasm插件

WebAssembly For Proxies是一种新插件规范,它允许开发者使用WebAssembly编写可移植插件,并且在各种代理服务器中运行。ASM支持WebAssembly For Proxies规范。本文介绍如何在ASM中使用Golang为网格代理编写Wasm插件。

前提条件

背景信息

WebAssembly(Wasm)是一种新兴的可移植的二进制可执行文件格式。这些代码将会在一个内存安全(对于主机)的沙箱中以接近本机的运行速度执行,使用的资源严格受限,且通过明确定义的API与主机通信。

使用Wasm插件有以下好处:

  • 敏捷性:无需重启Envoy就可以完成插件二进制更新,不会影响当前请求处理。

  • 可靠性和隔离性:插件运行在沙箱内,即使插件崩溃,Envoy本身不会受到影响。

  • 安全性:Proxy沙箱提供的API很明确,插件行为可控。

  • 多样性:支持使用多种语言编写插件(C++、Go、Rust等)。

关于Wasm插件的更多信息,请参见Envoy中Wasm Filter相关概念解释WebAssembly-in-Envoy.mdOVERVIEW.md

示例介绍

本文将编写一个基于Go语言的Wasm插件,编写完成后生成Wasm二进制文件,然后打包到镜像中。该镜像需要上传至镜像服务的OCI镜像仓库。上传之后,在ASM中配置WasmPlugin资源,将该插件应用至指定的网格代理上。

本文将开发一个插件用于判断请求中是否存在allow: true Header。如果不存在,返回403和指定的body;如果存在,则正常访问httpbin应用。

步骤一:开发环境准备

使用Go语言开发Envoy的Wasm插件需要提前安装以下工具:

  • Go:编写Go语言项目依赖Go编译器及相关工具,详情请参见The Go Programming Language

  • Docker:本文主要使用Docker构建和推送OCI镜像。

  • TinyGo:编写本项目依赖Go语言工具,但是Go代码编译成Wasm并不能使用Go官方提供的编译器,还需要使用TinyGo,详情请参见Quick install guide

关于Wasm插件依赖的SDK,请参见proxy-wasm-go-sdk。本文将提供完整的代码,如果您需要使用其他SDK能力,请自行参考该项目。

步骤二:编写插件代码

  1. 新建一个文件夹,使用以下内容,创建main.go文件。

    展开查看main.go文件

    package main
    
    import (
    	"github.com/tetratelabs/proxy-wasm-go-sdk/proxywasm"
    	"github.com/tetratelabs/proxy-wasm-go-sdk/proxywasm/types"
    )
    
    func main() {
    	proxywasm.SetVMContext(&vmContext{})
    }
    
    type vmContext struct {
    	// Embed the default VM context here,
    	// so that we don't need to reimplement all the methods.
    	types.DefaultVMContext
    }
    
    // Override types.DefaultVMContext.
    func (*vmContext) NewPluginContext(contextID uint32) types.PluginContext {
    	return &pluginContext{}
    }
    
    type pluginContext struct {
    	// Embed the default plugin context here,
    	// so that we don't need to reimplement all the methods.
    	types.DefaultPluginContext
    }
    
    // Override types.DefaultPluginContext.
    func (ctx *pluginContext) OnPluginStart(pluginConfigurationSize int) types.OnPluginStartStatus {
    	return types.OnPluginStartStatusOK
    }
    
    // Override types.DefaultPluginContext.
    func (ctx *pluginContext) NewHttpContext(contextID uint32) types.HttpContext {
    	return &HeaderAuthorizationHandler{}
    }
    
    type HeaderAuthorizationHandler struct {
    	// Embed the default http context here,
    	// so that we don't need to reimplement all the methods.
    	types.DefaultHttpContext
    }
    
    // Override types.DefaultHttpContext.
    func (ctx *HeaderAuthorizationHandler) OnHttpRequestHeaders(numHeaders int, endOfStream bool) types.Action {
    	// Randomly routing to the canary cluster.
    
    	const AuthorizationKey = "allow"
    	value, err := proxywasm.GetHttpRequestHeader(AuthorizationKey)
    	if err != nil || value != "true" {
    		proxywasm.LogDebugf("request header: 'allow' is %v, only true can passthrough", value)
    		return ctx.DenyRequest()
    	}
    	return types.ActionContinue
    }
    
    func (ctx *HeaderAuthorizationHandler) DenyRequest() types.Action {
    	proxywasm.SendHttpResponse(403, [][2]string{{"Content-Type", "text/plain"}}, []byte("Forbidden by ASM Wasm Plugin"), -1)
    	return types.ActionPause
    }
    
  2. 在新建的文件夹下执行以下命令,获取SDK相关依赖。

    go mod init
    go mod tidy
  3. 执行以下命令,编译代码得到Wasm二进制文件。

    tinygo build -o plugin.wasm -scheduler=none -target=wasi main.go

    您可以看到生成一个plugin.wasm文件。该文件即为Wasm的二进制可执行文件。

步骤三:制作OCI镜像并推送至阿里云容器镜像服务

  1. 步骤二新建的文件夹下,使用以下内容,创建Dockerfile文件。

    FROM scratch
    ADD ./plugin.wasm ./plugin.wasm
  2. 执行以下命令,制作镜像。

    docker build -t header-authorization:v0.0.1 .
  3. 创建镜像仓库。具体操作,请参见使用Coraza Wasm插件在ASM网关上实现WAF能力的步骤一.2的a、b步骤。

    本文使用的命名空间为test-oci,仓库名称为header-authorization。创建完之后界面显示如下:

    image

    您可以参考图中的将镜像推送到Registry步骤完成镜像推送。

步骤四:将Wasm插件应用在网关上

  1. 配置镜像拉取权限。具体操作,请参见步骤二:配置镜像拉取权限

    本文使用的Secret名称为wasm-secret,具体命令如下:

    kubectl create secret docker-registry -n istio-system wasm-secret --docker-server=${镜像服务实例域名} --docker-username=${用户名} --docker-password=${密码}
  2. 使用以下内容,创建asm-plugin.yaml。

    apiVersion: extensions.istio.io/v1alpha1
    kind: WasmPlugin
    metadata:
      name: header-authorization
      namespace: istio-system
    spec:
      imagePullPolicy: IfNotPresent
      imagePullSecret: wasm-secret
      selector:
        matchLabels:
          istio: ingressgateway
      url: oci://${镜像服务实例域名}/test-oci/header-authorization:v0.0.1
      phase: AUTHN
  3. 在ASM实例对应的KubeConfig环境下,执行以下命令,将WasmPlugin应用到ASM实例。

    kubectl apply -f wasm-plugin.yaml

步骤五:验证插件是否生效

  1. 使用网关所在数据面集群的KubeConfig,执行以下命令,开启网关的Wasm组件debug日志。

    kubectl -n istio-system exec ${网关pod名称} -c istio-proxy -- curl -XPOST "localhost:15000/logging?wasm=debug"
  2. 执行以下命令,访问网关的httpbin应用。

    curl ${ASM网关IP}/status/418

    预期输出:

    Forbidden by ASM Wasm Plugin
  3. 查看网关Pod日志。

    日志示例如下:

    2024-03-08T08:16:46.747394Z	debug	envoy wasm external/envoy/source/extensions/common/wasm/context.cc:1168	wasm log istio-system.header-authorization: request header: 'allow' is , only true can passthrough	thread=24
    {"bytes_received":"0","bytes_sent":"28","downstream_local_address":"xxxxxxx","downstream_remote_address":"xxxxxxxx","duration":"0","istio_policy_status":"-","method":"GET","path":"/status/418","protocol":"HTTP/1.1","request_id":"780c8493-13e4-4f97-9771-486efe30347c","requested_server_name":"-","response_code":"403","response_flags":"-","route_name":"httpbin","start_time":"2024-03-08T08:16:46.747Z","trace_id":"-","upstream_cluster":"outbound|8000||httpbin.default.svc.cluster.local","upstream_host":"-","upstream_local_address":"-","upstream_service_time":"-","upstream_response_time":"-","upstream_transport_failure_reason":"-","user_agent":"curl/8.4.0","x_forwarded_for":"xxxxxx","authority_for":"xxxxxx"}
  4. 执行以下命令,访问网关的httpbin应用。

    curl ${ASM网关IP}/status/418 -H "allow: true"

    预期输出:

        -=[ teapot ]=-
    
           _...._
         .'  _ _ `.
        | ."` ^ `". _,
        \_;`"---"`|//
          |       ;/
          \_     _/
            `"""`

    预期输出表明可以正常访问httpbin应用。

TinyGo的内存泄漏问题说明

当前TinyGo编译出的Envoy Wasm插件存在内存泄露问题。proxy-wasm-go-sdk社区目前建议使用nottinygc进行编译优化。具体操作步骤如下:

  1. 在main.go文件开头添加如下import代码。

    import _ "github.com/wasilibs/nottinygc"

    如果提示没有相关依赖,可以运行go mod tidy命令自动下载。

  2. 执行以下命令,进行编译。

    tinygo build -o plugin.wasm -gc=custom -tags='custommalloc nottinygc_envoy'  -target=wasi -scheduler=none main.go

    以上命令主要设置了-gc-tags两个选项。更多信息,请参见nottinygc