乐于分享
好东西不私藏

Higress WASM 插件入门(二)

本文最后更新于2026-03-11,某些文章具有时效性,若有错误或已失效,请在下方留言或联系老夜

Higress WASM 插件入门(二)

核心机制
Higress 的核心思路是基于 Wasm 插件机制进行扩展,提供了AI、认证、安全、流量控制等类型的 Wasm 插件(可以参考 Higress 插件文档 了解各个 Wasm 插件的使用方式)。这里详细来看下 Wasm 插件如何在 Higress 中使用、分发及最终在 Envoy 中加载的整个流程。
Wasm VM
插件类型
Wasm 虚拟机(Wasm VM 或简称 VM)指的是加载 Wasm 程序的实例。 在 Envoy 中,VM 通常在每个线程中创建并相互隔离。因此 Wasm 程序将复制到 Envoy 所创建的线程里,并在这些虚拟机上加载并执行。 插件提供了一种灵活的方式来扩展和自定义 Envoy 的行为。Proxy-Wasm 规范允许在每个 VM 中配置多个插件。因此一个 VM 可以被多个插件共同使用。Envoy 中有三种类型插件: 
  • Http Filter 是一种处理 Http 协议的插件,例如操作 Http 请求头、正文等
  • Network Filter 是一种处理 Tcp 协议的插件,例如操作 Tcp 数据帧、连接建立等
  • Wasm Service 是在单例 VM 中运行的插件类型(即在 Envoy 主线程中只有一个实例)。它主要用于执行与 Network Filter 或 Http Filter 并行的一些额外工作,如聚合指标、日志等。这样的单例 VM 本身也被称为 Wasm Service
Wasm 运行时
Envoy Wasm 运行时目前有以下几种选择:
  • 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 运行时。
不同的运行时有各自的优缺点,比如:V8 性能较好(❓)但容器体积较大,WAMR 和 wasmtime 则相对轻量。
❗上面有关不同运行时的性能说明出处来自官方的原理入门介绍文章:《Wasm 插件原理》https://higress.cn/docs/ebook/wasm15/?spm=36971b57.d48b2c5.0.0.638c456a7vbu0V但根据官方技术博客实际给出的测试结果:《Higress全新Wasm运行时,性能大幅提升》https://higress.cn/en/blog/higress-gvr7dx_awbbpb_rggp2v9saa6gehsg/WAMR 实际测试结果更优,具体内容也可以参照 附录三
插件的 phase 和 priority
Wasm 插件 phase 有4个值,分别为:
  • UNSPECIFIED_PHASE:在插件过滤器链的末端,在路由器之前插入插件,如果没有指定插件的 phase,则默认设置为 UNSPECIFIED_PHASE
  • AUTHN:在 Istio 认证过滤器之前插入插件
  • AUTHZ:在 Istio 授权过滤器之前且在 Istio 认证过滤器之后插入插件
  • STATS:在 Istio 统计过滤器之前且在 Istio 授权过滤器之后插入插件
同时在相同 phase 情况下,priority 值越大,插件在插件链位置越靠前。 关于认证和授权相关内容可以参考 Istio 安全官方文档 。其插件链结构如下图:
 Envoy 配置
所有类型插件的配置都包含 vm_config 用于配置 Wasm VM, 和 configuration 用于配置插件实例。
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"}'
配置说明如下:
完全相同的 vm_config 配置的多个插件它们之间共享一个 Wasm VM,单个 Wasm VM 用于多个 Http Filter 或 Network Filter,可以提升内存/CPU 资源效率、降低启动延迟。 完整的 Envoy API 配置可以 参考 Envoy 文档
Http Filter 配置
Http Filter 插件配置设置为 envoy.filter.http.wasm,Http Filter 插件可以处理 HTTP 请求和响应。 其主要配置如下:
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
这时 Envoy 会在每个工作线程中实例化一个 Wasm 虚拟机,该虚拟机将专门用于处理该线程上的 HTTP 请求和响应。每个虚拟机都会加载和执行 WebAssembly 代码,允许对 HTTP 流量进行自定义处理,如修改头信息、处理请求和响应体等。 完整的配置可以参考 envoy.yaml 。
Network Filter 配置
Network Filter 插件配置设置为 envoy.filters.network.wasm,Network Filter 插件可以处理 TCP 请求和响应。 其主要配置如下:
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
这时 Envoy 会在每个工作线程中实例化一个 Wasm 虚拟机,该虚拟机将专门用于处理该线程上的 TCP 请求和响应。每个虚拟机都会加载和执行 WebAssembly 代码,允许对 TCP 流量进行自定义处理等。 完整的配置可以参考 envoy.yaml 。
Wasm Service 配置
Wasm Service 插件配置设置为 envoy.bootstrap.wasm。插件在 Envoy 启动时加载的,其主要配置如下:
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
singleton 设置为 true 时,生成虚拟机(VM)是单例,并且运行在 Envoy 的主线程上,因此它不会阻塞任何工作线程。
插件共享VM
为了重用相同的 VM,所有的 vm_config.vm_id、vm_config.runtime、vm_config.configuration 和 vm_config.code 必须相同。
通过这种配置方式允许为不同的过滤器重用同一个 Wasm 虚拟机,通过为每个 PluginContext 提供了一个隔离的环境,使得插件能够独立运行,同时共享同一个虚拟机的执行环境,虚拟机只需要加载和初始化一次即可为多个插件服务,这不仅可以减少内存占用,还可以降低启动时的延迟。
故障域隔离
在 Envoy 中,当其中一个 Wasm 虚拟机(VM)出现故障时,其影响范围和行为主要取决于你的配置策略,特别是 fail_open 设置和 VM 的共享方式。下图概括了其主要影响因素和可能的行为结果:
影响因素与表现
Wasm 插件运行在沙箱中,这提供了一个安全隔离的环境。故障的影响范围首先取决于你的 vm_id 配置策略
  • 共享 VM 的插件:如果多个 Wasm 插件配置了相同的 vm_id,它们将共享同一个 VM 实例。此时,该 VM 的故障会影响所有使用此 vm_id 的插件
  • 独立 VM 的插件:如果 Wasm 插件配置了不同的 vm_id,它们会运行在独立的 VM 中。此时,一个 VM 的故障通常不会影响其他 VM 中的插件
VM 故障对 Envoy 行为的影响,主要通过 fail_open 这个配置项来控制:
  • fail_open 设置为 true:这是默认的宽容策略。当 VM 出现致命故障时,Envoy 会跳过该 Wasm 插件的处理,继续执行过滤器链中的后续操作,并将请求转发给上游服务。这保证了请求的连通性,但可能意味着会跳过一些重要的安全或处理逻辑。
  • fail_open 设置为 false:这是严格策略。当 VM 出现致命故障时,Envoy 会拒绝请求,并通常返回 503(Service Unavailable)错误 给客户端。这可以防止在插件功能不全时处理请求,但会牺牲部分可用性。
插件故障发生的时机也不同:
  • 运行时故障:指在处理请求的过程中发生的故障,其行为由上述 fail_open 规则决定。
  • 启动或配置阶段故障:如果故障发生在 VM 初始化、插件加载或配置解析阶段(例如 onStart 或 onConfigure 回调函数返回 false),影响可能更严重,可能导致 xDS 配置被拒绝或 Envoy 进程无法启动3。
WasmPlugin CRD
开发构建完的 wasm 插件是通过 WasmPlugin CRD 资源配置文件部署到 k8s 集群中生效;当 Higress WasmPlugin 资源更新时,Higress Controller 监听到这个变化,通过 LDS、ECDS 协议最终下发到 Higress Gateway 中的 Envoy 服务,从而加载 Wasm 文件。
Higress WasmPlugin CRD 在 Istio WasmPlugin CRD 的基础上进行了扩展,新增 defaultConfig、defaultConfigDisable 和 matchRules 字段,用于配置插件的默认配置和路由级、域名级、服务级配置,常见的配置文件格式如下:
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
 插件分发的原理
Wasm 插件是通过 OCI 实现了 wasm 文件更新,直接热加载,不会导致任何连接中断,业务流量完全无损。其插件分发流程如下图:
OCI 分发流程如下:
  1. 当 Higress WasmPlugin 资源更新时,Higress Core 监听到这个变化,同时把 Higress WasmPlugin 转成 Istio WasmPlugin。
  2. Higress Core 将转成 Istio WasmPlugin 通过 MCP Over Xds 推送给 Discovery。
  3. Discovery 通过 Pilot Agent 的 ADS 连接,通过 LDS 协议下发给 Plot Agent。
  4. Pilot Agent 将 LDS 响应直接代理转发给 Envoy。
  5. Envoy 根据 Listener 里插件配置,通过 ECDS (Extension Config Discovery Service) 订阅插件配置。
  6. Pilot Agent 代理 ECDS 协议请求到 Discovery, 同时拦截 ECDS 协议响应。
  7. Pilot Agent 根据 ECDS 响应里插件 OCI 配置,从 Registry Hub 下载镜像。
  8. Pilot Agent 把镜像里插件 Wasm 文件解压到本地,同时修改 ECDS 响应里插件地址到本地 Wasm 文件路径,然后把 ECDS 协议响应返回给 Envoy。
  9. Envoy 根据 ECDS 协议响应,加载本地 Wasm 文件。
Proxy-Wasm Go API
Higress 插件 Go SDK 在 proxy-wasm-go-sdk 上封装了一层,简化插件开发和增强功能。其代码位置:https://github.com/alibaba/higress/tree/main/plugins/wasm-go/pkg ,下面主要介绍 proxy-wasm-go-sdk 的一些通用概念。
Contexts
上下文(Contexts) 是 Proxy-Wasm Go SDK 中的接口集合,它们在 types 包中定义。 有四种类型的上下文:VMContext、PluginContext、TcpContext 和 HttpContext。它们的关系如下图:
WasmVirtualMachine                      (.vm_config.code)┌────────────────────────────────────────────────────────────────┐│  Yourprogram (.vm_config.code)                TcpContext      ││          │                                  ╱ (Tcp stream)     ││          │ 11                            ╱                   ││          │         1N                   ╱ 1N               ││      VMContext  ──────────  PluginContext                      ││                                (Plugin)   ╲ 1N               ││                                            ╲                   ││                                             ╲  HttpContext     ││                                               (Http stream)    │└────────────────────────────────────────────────────────────────┘
  1. VMContext 对应于每个 .vm_config.code,每个 VM 中只存在一个 VMContext。
  2. VMContext 是 PluginContexts 的父上下文,负责创建 PluginContext。
  3. PluginContext 对应于一个 Plugin 实例。一个 PluginContext 对应于 Http Filter、Network Filter、Wasm Service 的 configuration 字段配置。
  4. PluginContext 是 TcpContext 和 HttpContext 的父上下文,并且负责为 处理 Http 流的Http Filter 或 处理 Tcp 流的 Network Filter 创建上下文。
  5. TcpContext 负责处理每个 Tcp 流。
  6. HttpContext 负责处理每个 Http 流。
因此,自定义插件要实现 VMContext 和 PluginContext。 同时 Http Filter 或 Network Filter,要分别实现 HttpContext 或 TcpContext
HttpContext
   对于常见的 HttpFilter 类型插件,其必须需要实现 HttpContext 上下文对象,在 Higress 插件 Go SDK中,则将其包装成支持泛型的 CommonHttpCtx,其核心内容如下:
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{}}
因而在启动 VM 和设置上下文时可以使用如下代码简化:
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}
插件通过本地文件的方式加载到 Envoy 中,插件配置的如下:
configuration:"@type""type.googleapis.com/google.protobuf.StringValue"value: |-      {"request"true,"response"true,"responseStatusCodes""200,500,502,503"      }
Hostcall API
Hostcall API 是指在 Wasm 模块内调用 Envoy 提供的功能。这些功能通常用于获取外部数据或与 Envoy 交互。在开发 Wasm 插件时,需要访问网络请求的元数据、修改请求或响应头、记录日志等,这些都可以通过 Hostcall API 来实现。 Hostcall API 在 proxywasm 包的 hostcall.go 中定义。 Hostcall API 包括配置和初始化、定时器设置、上下文管理、插件完成、共享队列管理、Redis 操作、Http 调用、TCP 流操作、HTTP 请求/响应头和体操作、共享数据操作、日志操作、属性和元数据操作、指标操作等。
这里仅列一下最常用的 Redis 操作、http 调用的 API 
Redis 操作
HTTP 调用
跨虚拟机通信
Envoy 中的跨虚拟机通信(Cross-VM communications)允许在不同线程中运行 的Wasm 虚拟机(VMs)之间进行数据交换和通信。这在需要在多个VMs之间聚合数据、统计信息或缓存数据等场景中非常有用。 跨虚拟机通信主要有两种方式:
  • 共享数据(Shared Data):
    • 共享数据是一种在所有 VMs 之间共享的键值存储,可以用于存储和检索简单的数据项。
    • 它适用于存储小的、不经常变化的数据,例如配置参数或统计信息。
  • 共享队列(Shared Queue):
    • 共享队列允许VMs之间进行更复杂的数据交换,支持发送和接收更丰富的数据结构。
    • 队列可以用于实现任务调度、异步消息传递等模式。
共享数据(Shared Data)
如果想要在所有 Wasm 虚拟机(VMs)运行的多个工作线程间拥有全局请求计数器,或者想要缓存一些应被所有 Wasm VMs 使用的数据,那么共享数据(Shared Data)或等效的共享键值存储(Shared KVS)就会发挥作用。 共享数据本质上是一个跨所有VMs共享的键值存储(即跨 VM 或跨线程)。
共享数据 KVS 是根据 vm_config 中指定的创建的。可以在所有 Wasm VMs 之间共享一个键值存储,而它们不必具有相同的二进制文件 vm_config.code,唯一的要求是具有相同的 vm_id。
共享队列 Shared Queue
如果要在请求/响应处理的同时跨所有 Wasm VMs 聚合指标,或者将一些跨 VM 聚合的信息推送到远程服务器,可以通过 Shared Queue 来实现。Shared Queue 是为 vm_id 和队列名称的组合创建的 FIFO(先进先出)队列。并为该组合(vm_id,名称)分配了一个唯一的 queue id,该 ID 用于入队/出队操作。“入队”和“出队”等操作具有线程安全性和跨 VM 安全性。
RegisterSharedQueue 和 DequeueSharedQueue 由队列的“消费者”使用,而 ResolveSharedQueue 和 EnqueueSharedQueue 是为队列“生产者”准备的。请注意:
  • RegisterSharedQueue 用于为调用者的 name 和 vm_id 创建共享队列。使用一个队列,那么必须先由一个 VM 调用这个函数。这可以由 PluginContext 调用,因此可以认为“消费者” = PluginContexts。
  • ResolveSharedQueue 用于获取 name 和 vm_id 的 queue id。这是为“生产者”准备的。
这两个调用都返回一个队列 ID,该 ID 用于 DequeueSharedQueue 和 EnqueueSharedQueue。同时当队列中入队新数据时 消费者 PluginContext 中有 OnQueueReady(queueID uint32) 接口会收到通知。 还强烈建议由 Envoy 的主线程上的单例 Wasm Service 创建共享队列。否则 OnQueueReady 将在工作线程上调用,这会阻塞它们处理 Http 或 Tcp 流。
本站文章均为手工撰写未经允许谢绝转载:夜雨聆风 » Higress WASM 插件入门(二)

猜你喜欢

  • 暂无文章

评论 抢沙发

6 + 8 =