Higress WASM 插件入门(二)
-
Http Filter 是一种处理 Http 协议的插件,例如操作 Http 请求头、正文等 -
Network Filter 是一种处理 Tcp 协议的插件,例如操作 Tcp 数据帧、连接建立等 -
Wasm Service 是在单例 VM 中运行的插件类型(即在 Envoy 主线程中只有一个实例)。它主要用于执行与 Network Filter 或 Http Filter 并行的一些额外工作,如聚合指标、日志等。这样的单例 VM 本身也被称为 Wasm Service

-
envoy.wasm.runtime.null:这表示一个空的沙盒(null sandbox)环境,Wasm 模块必须被编译并链接到 Envoy 的二进制文件中。这种方式适用于那些需要将 Wasm 模块与 Envoy 二进制文件一起分发的部署场景。 -
envoy.wasm.runtime.v8: 基于 V8 JavaScript 引擎的运行时。 -
envoy.wasm.runtime.wamr: WAMR (WebAssembly Micro Runtime) 运行时。 -
envoy.wasm.runtime.wasmtime: Wasmtime 运行时。
-
UNSPECIFIED_PHASE:在插件过滤器链的末端,在路由器之前插入插件,如果没有指定插件的 phase,则默认设置为 UNSPECIFIED_PHASE -
AUTHN:在 Istio 认证过滤器之前插入插件 -
AUTHZ:在 Istio 授权过滤器之前且在 Istio 认证过滤器之后插入插件 -
STATS:在 Istio 统计过滤器之前且在 Istio 授权过滤器之后插入插件

vm_config:vm_id: "foo"runtime: "envoy.wasm.runtime.v8"configuration:"@type": type.googleapis.com/google.protobuf.StringValuevalue: '{"my-vm-env": "dev"}'code:local:filename: "example.wasm"configuration:"@type": type.googleapis.com/google.protobuf.StringValuevalue: '{"my-plugin-config": "bar"}'
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: configuration:"@type": type.googleapis.com/google.protobuf.StringValue value: | {"header": "x-wasm-header","value": "demo-wasm" } vm_config:runtime: "envoy.wasm.runtime.v8" code: local: filename: "./examples/http_headers/main.wasm" - name: envoy.filters.http.router typed_config:"@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
filter_chains:- filters: - name: envoy.filters.network.wasm typed_config:"@type": type.googleapis.com/envoy.extensions.filters.network.wasm.v3.Wasm config: vm_config: { ... } # ... plugin config follows - name: envoy.tcp_proxy
bootstrap_extensions:- name: envoy.bootstrap.wasm typed_config:"@type": type.googleapis.com/envoy.extensions.wasm.v3.WasmService singleton: true config: vm_config: { ... } # ... plugin config follows

-
共享 VM 的插件:如果多个 Wasm 插件配置了相同的 vm_id,它们将共享同一个 VM 实例。此时,该 VM 的故障会影响所有使用此 vm_id 的插件。 -
独立 VM 的插件:如果 Wasm 插件配置了不同的 vm_id,它们会运行在独立的 VM 中。此时,一个 VM 的故障通常不会影响其他 VM 中的插件。
-
fail_open 设置为 true:这是默认的宽容策略。当 VM 出现致命故障时,Envoy 会跳过该 Wasm 插件的处理,继续执行过滤器链中的后续操作,并将请求转发给上游服务。这保证了请求的连通性,但可能意味着会跳过一些重要的安全或处理逻辑。 -
fail_open 设置为 false:这是严格策略。当 VM 出现致命故障时,Envoy 会拒绝请求,并通常返回 503(Service Unavailable)错误 给客户端。这可以防止在插件功能不全时处理请求,但会牺牲部分可用性。
-
运行时故障:指在处理请求的过程中发生的故障,其行为由上述 fail_open 规则决定。 -
启动或配置阶段故障:如果故障发生在 VM 初始化、插件加载或配置解析阶段(例如 onStart 或 onConfigure 回调函数返回 false),影响可能更严重,可能导致 xDS 配置被拒绝或 Envoy 进程无法启动3。
apiVersion: extensions.higress.io/v1alpha1kind: WasmPluginmetadata: name: request-block namespace: higress-systemspec: defaultConfig: block_urls: - swagger.html - foo=bar case_sensitive: false url: oci://higress-registry.cn-hangzhou.cr.aliyuncs.com/plugins/request-block:1.0.0

-
当 Higress WasmPlugin 资源更新时,Higress Core 监听到这个变化,同时把 Higress WasmPlugin 转成 Istio WasmPlugin。 -
Higress Core 将转成 Istio WasmPlugin 通过 MCP Over Xds 推送给 Discovery。 -
Discovery 通过 Pilot Agent 的 ADS 连接,通过 LDS 协议下发给 Plot Agent。 -
Pilot Agent 将 LDS 响应直接代理转发给 Envoy。 -
Envoy 根据 Listener 里插件配置,通过 ECDS (Extension Config Discovery Service) 订阅插件配置。 -
Pilot Agent 代理 ECDS 协议请求到 Discovery, 同时拦截 ECDS 协议响应。 -
Pilot Agent 根据 ECDS 响应里插件 OCI 配置,从 Registry Hub 下载镜像。 -
Pilot Agent 把镜像里插件 Wasm 文件解压到本地,同时修改 ECDS 响应里插件地址到本地 Wasm 文件路径,然后把 ECDS 协议响应返回给 Envoy。 -
Envoy 根据 ECDS 协议响应,加载本地 Wasm 文件。
WasmVirtualMachine (.vm_config.code)┌────────────────────────────────────────────────────────────────┐│ Yourprogram (.vm_config.code) TcpContext ││ │ ╱ (Tcp stream) ││ │ 1: 1 ╱ ││ │ 1: N ╱ 1: N ││ VMContext ────────── PluginContext ││ (Plugin) ╲ 1: N ││ ╲ ││ ╲ HttpContext ││ (Http stream) │└────────────────────────────────────────────────────────────────┘
-
VMContext 对应于每个 .vm_config.code,每个 VM 中只存在一个 VMContext。 -
VMContext 是 PluginContexts 的父上下文,负责创建 PluginContext。 -
PluginContext 对应于一个 Plugin 实例。一个 PluginContext 对应于 Http Filter、Network Filter、Wasm Service 的 configuration 字段配置。 -
PluginContext 是 TcpContext 和 HttpContext 的父上下文,并且负责为 处理 Http 流的Http Filter 或 处理 Tcp 流的 Network Filter 创建上下文。 -
TcpContext 负责处理每个 Tcp 流。 -
HttpContext 负责处理每个 Http 流。
type CommonHttpCtx[PluginConfig any] struct {// proxy-wasm-go-sdk DefaultHttpContext 默认实现 types.DefaultHttpContext// 引用 CommonPluginCtx plugin *CommonPluginCtx[PluginConfig]// 当前 Http 上下文下匹配插件配置,可能是路由、域名、服务级别配置或者全局配置 config *PluginConfig// 是否处理请求体 needRequestBody bool// 是否处理响应体 needResponseBody bool// 是否处理流式请求体 streamingRequestBody bool// 是否处理流式响应体 streamingResponseBody bool// 非流式处理缓存请求体大小 requestBodySize int// 非流式处理缓存响应体大小 responseBodySize int// Http 上下文 ID contextID uint32// 自定义插件设置自定义插件上下文 userContext map[string]interface{}}
funcmain() { wrapper.SetCtx(// 插件名称"hello-world",// 设置自定义函数解析插件配置,这个方法适合插件全局配置和路由、域名、服务级别配置内容规则是一样 wrapper.ParseConfigBy(parseConfig),// 设置自定义函数解析插件全局配置和路由、域名、服务级别配置,这个方法适合插件全局配置和路由、域名、服务级别配置内容规则不一样 wrapper.ParseOverrideConfigBy(parseConfig, parseRuleConfig)// 设置自定义函数处理请求头 wrapper.ProcessRequestHeadersBy(onHttpRequestHeaders),// 设置自定义函数处理请求体 wrapper.ProcessRequestBodyBy(onHttpRequestBody),// 设置自定义函数处理响应头 wrapper.ProcessResponseHeadersBy(onHttpResponseHeaders),// 设置自定义函数处理响应体 wrapper.ProcessResponseBodyBy(onHttpResponseBody),// 设置自定义函数处理流式请求体 wrapper.ProcessStreamingRequestBodyBy(onHttpStreamingRequestBody),// 设置自定义函数处理流式响应体 wrapper.ProcessStreamingResponseBodyBy(onHttpStreamingResponseBody),// 设置自定义函数处理流式请求完成 wrappper.ProcessStreamDoneBy(onHttpStreamDone) )}
wrapper.ProcessRequestHeadersBy将自定义函数 onHttpRequestHeaders用于HTTP 请求头处理阶段处理请求。除此之外,还可以通过下面方式,设置其他阶段的自定义处理函数

package mainimport ("fmt""strings" logs "github.com/higress-group/wasm-go/pkg/log""github.com/higress-group/wasm-go/pkg/wrapper""github.com/google/uuid""github.com/higress-group/proxy-wasm-go-sdk/proxywasm""github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types""github.com/tidwall/gjson")funcmain() {}funcinit() { wrapper.SetCtx(// 插件名称"easy-logger",// 设置自定义函数解析插件配置 wrapper.ParseConfigBy(parseConfig),// 设置自定义函数处理请求头 wrapper.ProcessRequestHeadersBy(onHttpRequestHeaders),// 设置自定义函数处理请求体 wrapper.ProcessRequestBodyBy(onHttpRequestBody),// 设置自定义函数处理响应头 wrapper.ProcessResponseHeadersBy(onHttpResponseHeaders),// 设置自定义函数处理响应体 wrapper.ProcessResponseBodyBy(onHttpResponseBody),// 设置自定义函数处理流式请求体//wrapper.ProcessStreamingRequestBodyBy(onHttpStreamingRequestBody),// 设置自定义函数处理流式响应体//wrapper.ProcessStreamingResponseBodyBy(onHttpStreamingResponseBody), )}// 自定义插件配置type LoggerConfig struct {// 是否打印请求 request bool// 是否打印响应 response bool// 打印响应状态码,* 表示打印所有状态响应,500,502,503 表示打印 HTTP 500、502、503 状态响应,默认是 * responseStatusCodes string}funcparseConfig(json gjson.Result, config *LoggerConfig, log logs.Log)error { log.Debugf("parseConfig()") config.request = json.Get("request").Bool() config.response = json.Get("response").Bool() config.responseStatusCodes = json.Get("responseStatusCodes").String()if config.responseStatusCodes == "" { config.responseStatusCodes = "*" } log.Debugf("parse config:%+v", config)returnnil}funconHttpRequestHeaders(ctx wrapper.HttpContext, config LoggerConfig, log logs.Log)types.Action { log.Debugf("onHttpRequestHeaders()") requestId := uuid.New().String() ctx.SetContext("requestId", requestId)if !config.request {return types.ActionContinue }// 获取并打印请求头 headers, _ := proxywasm.GetHttpRequestHeaders()var build strings.Builder build.WriteString("\n===========request headers===============\n") build.WriteString(fmt.Sprintf("requestId:%s\n", requestId))for _, values := range headers { build.WriteString(fmt.Sprintf("%s:%s\n", values[0], values[1])) } log.Infof(build.String())// 继续处理请求return types.ActionContinue}funconHttpRequestBody(ctx wrapper.HttpContext, config LoggerConfig, body []byte, log logs.Log)types.Action { log.Debugf("onHttpRequestBody()")// 打印请求体if config.request {var build strings.Builder build.WriteString("\n===========request body===============\n") requestId := ctx.GetContext("requestId").(string) build.WriteString(fmt.Sprintf("requestId:%s\n", requestId)) build.WriteString(fmt.Sprintf("body:%s\n", string(body))) log.Infof(build.String()) }return types.ActionContinue}funconHttpResponseHeaders(ctx wrapper.HttpContext, config LoggerConfig, log logs.Log)types.Action { log.Debugf("onHttpResponseHeaders()")// 添加自定义响应头 proxywasm.AddHttpResponseHeader("x-easy-logger", "1.0.0")if !config.response {return types.ActionContinue }// 获取响应状态码 statusCode, _ := proxywasm.GetHttpResponseHeader(":status") logResponseBody := false// 根据响应状态码决定是否打印响应体if config.responseStatusCodes == "*" || strings.Contains(config.responseStatusCodes, statusCode) { logResponseBody = true }// 将是否记录响应体的信息存储在上下文中,在 onHttpResponseBody 阶段获取上下文判断是否打印响应体 ctx.SetContext("logResponseBody", logResponseBody)// 获取响应头 headers, _ := proxywasm.GetHttpResponseHeaders()// 打印响应头var build strings.Builder build.WriteString("\n===========response headers===============\n") requestId := ctx.GetContext("requestId").(string) build.WriteString(fmt.Sprintf("requestId:%s\n", requestId))for _, values := range headers { build.WriteString(fmt.Sprintf("%s:%s\n", values[0], values[1])) } log.Infof(build.String())return types.ActionContinue}funconHttpResponseBody(ctx wrapper.HttpContext, config LoggerConfig, body []byte, log logs.Log)types.Action { log.Debugf("onHttpResponseBody()")// 获取在 onHttpRequestHeaders 阶段设置的上下文 logResponseBody, ok := ctx.GetContext("logResponseBody").(bool)if !ok {return types.ActionContinue }// 打印响应体if logResponseBody {var build strings.Builder build.WriteString("\n===========response body===============\n") requestId := ctx.GetContext("requestId").(string) build.WriteString(fmt.Sprintf("requestId:%s\n", requestId)) build.WriteString(fmt.Sprintf("body:%s\n", string(body))) log.Infof(build.String()) }return types.ActionContinue}
configuration:"@type": "type.googleapis.com/google.protobuf.StringValue"value: |- {"request": true,"response": true,"responseStatusCodes": "200,500,502,503" }


-
共享数据(Shared Data): -
共享数据是一种在所有 VMs 之间共享的键值存储,可以用于存储和检索简单的数据项。 -
它适用于存储小的、不经常变化的数据,例如配置参数或统计信息。 -
共享队列(Shared Queue): -
共享队列允许VMs之间进行更复杂的数据交换,支持发送和接收更丰富的数据结构。 -
队列可以用于实现任务调度、异步消息传递等模式。

-
RegisterSharedQueue 用于为调用者的 name 和 vm_id 创建共享队列。使用一个队列,那么必须先由一个 VM 调用这个函数。这可以由 PluginContext 调用,因此可以认为“消费者” = PluginContexts。 -
ResolveSharedQueue 用于获取 name 和 vm_id 的 queue id。这是为“生产者”准备的。

夜雨聆风