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 → 正常放行到后端。就像安检员对黑名单里的旅客当场拦下、对普通旅客挥手放行。图注:请求头、请求体是两道分开的关卡(两个回调);任何一关命中都能当场拦下,不再往后走。
👶 小白:拦截时明明结束了请求,为什么还 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 读配置?为什么要校验?
Action和SendHttpResponse、DontReadRequestBody各干什么?- WasmPlugin CRD 的
defaultConfig和parseConfig什么关系?
✋ 动手
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 03:proxy-wasm 与 ABI——往下探一层,看
proxywasm.GetHttpRequestHeader 这类调用底层怎么和 Envoy 通信,Wasm 虚拟机怎么按线程克隆,下行/上行钩子分别是什么。