Day 02 / 共 20 天 · 第 1 周 入门与生命周期

第一个插件:request-block

最好的入门是读一个完整的小例子。今天逐行拆 examples/request-block/main.go——一个"拦截含某关键词请求"的插件。它虽小,却五脏俱全:注册、配置、请求头处理、请求体处理全都有。

📍 你在 wasm-go 前 10 天的位置(W1 入门与生命周期 · W2 配置与匹配;W3-4 见 D11-20)
D01 全景· D02 request-block· D03 ABI· D04 三级Context· D05 SetCtx/选项 D06 配置解析· D07 RuleMatcher· D08 运行时匹配· D09 全局vs规则· D10 生命周期 W3-4 请求处理/高级
💡 延续「机场安检」类比看今天 昨天(D01)认识了 wasm-go 这台"外挂芯片";今天拆开一台真芯片看内部——最简单的 request-block(关键词拦截)。把它对到安检线上:init()=芯片开机自检、向中控登记"我负责哪几道关卡"parseConfig=读今天的"黑名单清单"(哪些 URL/词要拦);onHttpRequestHeaders=查旅客证件那一关onHttpRequestBody=开箱查行李那一关。五脏俱全,麻雀虽小。
L01

main 与 init

examples/request-block/main.go:31-40

func main() {}   // :31 空的!

func init() {    // :33 真正的入口
    wrapper.SetCtx(
        "request-block",
        wrapper.ParseConfig(parseConfig),
        wrapper.ProcessRequestHeaders(onHttpRequestHeaders),
        wrapper.ProcessRequestBody(onHttpRequestBody),
    )
}
为什么 main 是空的? 普通 Go 程序从 main() 开始跑。但 Wasm 插件不是"程序",而是"被网关加载的模块"——网关不会调用你的 main,而是在加载模块时触发 init()Go 规定 init() 在包加载时自动执行。所以插件把"注册回调"写在 init() 里,main() 留空只为满足编译要求。记住:Wasm 插件的入口是 init(),不是 main()
⚠️ 常见误解:以为"main 是空的 = 这段代码没用"。恰恰相反——真正干活的是 init(),它在芯片开机(VM 加载)时自动跑一次,把回调登记好。删了 main() 反而编译不过(Go 要求可执行包必须有 main),所以它是"占位但必需"。
L02

SetCtx 注册

SetCtx("request-block", 选项...) 做了两件事:给插件起名、注册一组"回调函数"。这里注册了三个:

  • ParseConfig(parseConfig):怎么解析配置。
  • ProcessRequestHeaders(onHttpRequestHeaders):请求头到达时干什么。
  • ProcessRequestBody(onHttpRequestBody):请求体到达时干什么。
"注册回调"是什么意思? 你不主动调用这些函数——你把它们"登记"给 SDK,SDK 在合适的时机(配置加载时、请求头到达时…)替你调用。这叫"回调"(callback)或"钩子"(hook)。好比你在快递柜登记手机号,包裹到了它自动通知你,而不是你一直去查。写插件的本质就是"填这些回调"——你只关心"发生 X 时做 Y",何时发生由网关决定。
L03

配置结构体

:42-51 定义这个插件能接受哪些配置:

type RequestBlockConfig struct {
    blockedCode      uint32          // 拦截时返回的状态码
    blockedMessage   string          // 拦截时返回的消息
    caseSensitive    bool            // 是否大小写敏感
    blockUrls        []string        // 命中即拦的 URL 关键词
    blockExactUrls   []string        // 精确匹配的 URL
    blockHeaders     []string        // 命中即拦的请求头
    blockBodies      []string        // 命中即拦的请求体关键词
    blockRegExpArray []*regexp.Regexp // 正则规则
}
读法:这个结构体就是"这个插件的配置形状"。泛型参数 PluginConfig(明天讲)就是它——SDK 用 Go 泛型把"配置类型"贯穿始终,parseConfig 填它、各钩子读它。
L04

parseConfig

:53-124 把 JSON 配置读进结构体(用 gjson 按路径取值):

func parseConfig(json gjson.Result, config *RequestBlockConfig) error {
    code := json.Get("blocked_code").Int()
    if code != 0 && code > 100 && code < 600 {
        config.blockedCode = uint32(code)
    } else {
        config.blockedCode = 403     // 默认 403
    }
    config.caseSensitive = json.Get("case_sensitive").Bool()
    for _, item := range json.Get("block_urls").Array() {  // 遍历数组
        url := item.String()
        if url == "" { continue }
        config.blockUrls = append(config.blockUrls,
            ternaryLower(url, config.caseSensitive))
    }
    // ... block_exact_urls / block_regexp_urls / block_headers / block_bodies 同理
    if len(config.blockUrls)==0 && len(config.blockHeaders)==0 && len(config.blockBodies)==0 {
        return errors.New("there is no block rules")   // :121 配置校验
    }
    return nil
}
读法:json.Get("block_urls").Array() 直接从 JSON 取数组,不用定义中间结构体——这就是 gjson 的爽点。大小写不敏感时提前转小写存好,匹配时快。最后校验"至少有一条规则",没有就返回 error(插件启动会失败)。
L05

onHttpRequestHeaders

:126-185:请求头到达时,检查 URL/请求头是否命中拦截规则:

func onHttpRequestHeaders(ctx wrapper.HttpContext, config RequestBlockConfig) types.Action {
    requestUrl, _ := proxywasm.GetHttpRequestHeader(":path")   // 取路径
    if !config.caseSensitive { requestUrl = strings.ToLower(requestUrl) }
    for _, blockUrl := range config.blockUrls {
        if strings.Contains(requestUrl, blockUrl) {
            proxywasm.SendHttpResponseWithDetail(config.blockedCode,
                "request-block.url_blocked.keyword", nil,
                []byte(config.blockedMessage), -1)   // 直接返回拦截响应
            return types.ActionContinue
        }
    }
    // ... 检查 block_headers ...
    if len(config.blockBodies) == 0 {
        ctx.DontReadRequestBody()   // :183 不需要读 body 就声明,省内存
    }
    return types.ActionContinue
}
SendHttpResponse 和 Action 是什么? SendHttpResponseWithDetail 直接生成一个响应发回客户端(比如 403 拦截页)——请求不再往后端转发。返回值 types.Action 告诉网关"接下来怎么办":ActionContinue = 继续处理,ActionPause = 暂停等待(Day 14 会用到)。关键细节 DontReadRequestBody():如果不用检查请求体,主动声明"别读 body",网关就不缓存请求体,省内存。
📝 举个例子:一个请求命中黑名单的完整过程 配置 block_urls: ["swagger.html"],来了请求 GET /api/swagger.html
① 取 :path = /api/swagger.html → ② 命中关键词 swagger.html → ③ SendHttpResponseWithDetail(403, …) 直接回 403 + blockedMessage → ④ 请求根本不转发给后端
换成 GET /api/users:不命中任何关键词 → 返回 ActionContinue → 正常放行到后端。就像安检员对黑名单里的旅客当场拦下、对普通旅客挥手放行。
一个请求穿过 request-block 的两道关卡 请求到达 旅客进站 ① 查证件(headers) onHttpRequestHeaders 看 :path / 请求头 ② 开箱(body) onHttpRequestBody 看请求体关键词 放行→后端 命中→403 拦截 ✋ 命中→403 拦截 ✋
图注:请求头、请求体是两道分开的关卡(两个回调);任何一关命中都能当场拦下,不再往后走。

👶 小白:拦截时明明结束了请求,为什么还 return types.ActionContinue 而不是 ActionPause

👨‍🏫 老师:因为 SendHttpResponseWithDetail 已经"接管"了这个请求、亲自把响应发回去了,后续的转发流程会因为响应已生成而自然终止。这里的 ActionContinue 只是把控制权交还给 SDK 收尾,不会再进后端。真正需要 ActionPause 的是"我要等一个异步结果(如外部 HTTP 调用)回来再决定"的场景——那是 Day 14 的戏。

L06

onHttpRequestBody

:187-201:请求体到达时,检查 body 是否含关键词:

func onHttpRequestBody(ctx wrapper.HttpContext, config RequestBlockConfig, body []byte) types.Action {
    bodyStr := string(body)
    if !config.caseSensitive { bodyStr = strings.ToLower(bodyStr) }
    for _, blockBody := range config.blockBodies {
        if strings.Contains(bodyStr, blockBody) {
            proxywasm.SendHttpResponseWithDetail(config.blockedCode,
                "request-block.body_blocked", nil, []byte(config.blockedMessage), -1)
            return types.ActionContinue
        }
    }
    return types.ActionContinue
}
读法:注意这个回调多了个 body []byte 参数——请求体作为参数传进来。只有注册了 ProcessRequestBody 且没调 DontReadRequestBody,网关才会读 body 并调这个回调。请求头阶段和请求体阶段是分开的两个钩子(Day 11 详讲各阶段)。
L07

WasmPlugin 配置(怎么用起来)

README.md 给出应用方式——一个 WasmPlugin CRD(上一站 Higress/Console 讲过):

apiVersion: extensions.higress.io/v1alpha1
kind: WasmPlugin
metadata:
  name: request-block
  namespace: higress-system
spec:
  defaultConfig:
    block_urls:
    - "swagger.html"     # ← 这就是 parseConfig 里读的 block_urls
  url: oci:///request-block:1.0.0
读法:defaultConfig 里的 JSON 就是 parseConfig 收到的 json 参数!回想上一站:Console 生成 WasmPlugin CR → Higress 下发 → Envoy 加载 url 指向的 wasm → 用 defaultConfig 调你的 parseConfig配好后,访问含 swagger.html 的 URL 就会被 403 拦截。
L08

今日小结 + 动手

🧠 今天你应该能回答

  • 为什么插件入口是 init() 不是 main()
  • SetCtx 注册了哪几类回调?"回调"是什么意思?
  • parseConfig 怎么用 gjson 读配置?为什么要校验?
  • ActionSendHttpResponseDontReadRequestBody 各干什么?
  • WasmPlugin CRD 的 defaultConfigparseConfig 什么关系?

✋ 动手

cd /Users/bitmart/work/codes/github/higress-group/wasm-go
cat examples/request-block/main.go
# 试着编译(需要 Go 1.24+)
cd examples/request-block && GOOS=wasip1 GOARCH=wasm go build -buildmode=c-shared -o main.wasm main.go && ls -la main.wasm
明天预告 · Day 03proxy-wasm 与 ABI——往下探一层,看 proxywasm.GetHttpRequestHeader 这类调用底层怎么和 Envoy 通信,Wasm 虚拟机怎么按线程克隆,下行/上行钩子分别是什么。
← Day 01 Day 03 · proxy-wasm →