使用rust 编写PROXY_WASM插件

  1. 1. 开发环境
  2. 2. 初始化项目
  3. 3. 代码部分
    1. 3.1. 初始化配置
    2. 3.2. 逻辑
  4. 4. 编译
  5. 5. 测试
    1. 5.1. 编写docker-compose文件
  6. 6. 附录
    1. 6.1. Http请求阶段挂载表
    2. 6.2. 方法汇总(仅列出部分)

proxy_wasm 是一套相对通用的标准,他允许实现了这套标准的wasm插件在envoy或者apisix网关中对请求进行一些操作。

proxy_wasm目前支持多个语言的sdk,支持的sdk参考此处Proxy-Wasm · GitHub,这边使用rust来写,实际上各个语言的写法大同小异。

开发环境

安装rust

1
2
3
4
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# 安装rust
cargo install wasm-gc
# 裁剪wasm包,也可不安装

初始化项目

1
cargo new hello-wold --lib 

Cargo.toml中添加动态库编译配置和sdk依赖

1
2
3
4
5
6
7
[lib]
path = "src/lib.rs"
crate-type = ["cdylib"]

[dependencies]
proxy-wasm = "0.2"
# proxy-wasm依赖

代码部分

初始化配置

其实比较简单,基本都是固定写法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#[no_mangle]
pub fn _start() {
//启动函数,使用no_mango禁止内存管理,交给网关处理
proxy_wasm::set_log_level(LogLevel::Trace);
proxy_wasm::set_root_context(|_| -> Box<dyn RootContext> {
Box::new(MydRoot{
config: FilterConfig{
host: "".to_string(),
}
})
});
}

struct MyRoot {
// 初始化结构体
config: FilterConfig
}


struct MyFilter{
// 实际请求的结构体
context_id: u32,
config: FilterConfig,
}

struct FilterConfig {
// 配置存放相关的结构体
host: String,
}

以上定义了三个结构体,实际上起作用的部分为MyFilter,MyRoot结构体用于初始化配置的读取以及参数传递

wasm启动时,将初始化一些配置,我们实际的配置则通过配置文件读取。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
impl RootContext for MyRoot {
// 为MyRoot 结构体实现RootContext的trait
fn on_configure(&mut self, _plugin_configuration_size: usize) -> bool {
// 读取配置阶段
if self.config.host == "" {
match self.get_plugin_configuration {
Some(config_bytes) => {
// 获取配置参数 此处使用了serde_json包,使用时需先在cargo.toml中添加依赖
let cfg:Value = serde_json::from_slice(config_bytes.as_slice()).unwrap();
self.config.host = cfg.get("host")
.unwrap()
.as_str()
.unwrap()
.to_string();
}
None => {
warn!("can not get config");
}
}
}
true
}
fn create_http_context(&self, context_id: u32) -> Option<Box<dyn HttpContext>> {
info!("::::create_http_context,context_id: {} ,host: {}",context_id,self.config.host);
Some(Box::new(MyFilter{
// 为myfilter结构体传入context_id以及其他参数
context_id,
config: FilterConfig{
host: self.config.host.clone(),
}
}))
}
fn get_type(&self) -> Option<ContextType> {
Some(ContextType::HttpContext)
}
}

impl Context for MyRoot {}

逻辑

到这边开始就可以写具体逻辑了,一般性来说我们只需要在HttpContext的trait中实现逻辑即可。这边的操作是在返回的请求中添加了两个请求头。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
impl HttpContext for Filter {
fn on_http_request_headers(&mut self, _num_headers: usize, _end_of_stream: bool) -> Action {

for (name, value) in &self.get_http_request_headers() {
info!("::::H[{}] -> {}: {}", self.context_id, name, value);
}
Action::Pause
}

fn on_http_response_headers(&mut self, _num_headers: usize, _end_of_stream: bool) -> Action {
// 设置一些header
self.set_http_response_header("X-Test-Hosts", Some(self.config.host.as_str()));
self.set_http_response_header("Powered-By", Some("proxy-wasm"));
Action::Continue
}
}

编译

1
2
3
4
cargo build --target=wasm32-unknown-unknown  --release 

wasm-gc target/wasm32-unknown-unknown/release/$WASMFILE.wasm
# 裁剪wasm包

测试

编写docker-compose文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
version: '3.7'
services:
envoy:
image: envoyproxy/envoy:v1.21-latest
depends_on:
- httpbin
networks:
- wasmtest
ports:
- "10000:10000"
volumes:
- ./envoy.yaml:/etc/envoy/envoy.yaml
- ./$WASMFILE.wasm:/etc/envoy/main.wasm
httpbin:
image: kennethreitz/httpbin:latest
networks:
- wasmtest
ports:
- "12345:80"
# 存在多个upstream的时候,在此处添加
networks:
wasmtest: {}

修改envoy.yaml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
admin:
address:
socket_address:
protocol: TCP
address: 0.0.0.0
port_value: 9901
static_resources:
listeners:
- name: listener_0
address:
socket_address:
protocol: TCP
address: 0.0.0.0
port_value: 10000
filter_chains:
- filters:
- name: envoy.filters.network.http_connection_manager
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
scheme_header_transformation:
scheme_to_overwrite: https
stat_prefix: ingress_http
route_config:
name: local_route
virtual_hosts:
- name: local_service
domains: ["*"]
routes:
- match:
prefix: "/"
route:
cluster: httpbin
http_filters:
- name: wasmdemo
typed_config:
"@type": type.googleapis.com/udpa.type.v1.TypedStruct
type_url: type.googleapis.com/envoy.extensions.filters.http.wasm.v3.Wasm
value:
config:
name: wasmdemo
vm_config:
runtime: envoy.wasm.runtime.v8
code:
local:
filename: /etc/envoy/main.wasm
configuration:
"@type": "type.googleapis.com/google.protobuf.StringValue"
value: |
{
"host":"http://auth",
}
# 此处填写自定义的配置,json格式
- name: envoy.filters.http.router
clusters:
- name: httpbin
connect_timeout: 30s
type: LOGICAL_DNS
# Comment out the following line to test on v6 networks
dns_lookup_family: V4_ONLY
lb_policy: ROUND_ROBIN
load_assignment:
cluster_name: httpbin
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: httpbin
port_value: 80

启动验证即可

附录

Http请求阶段挂载表

附注: grpc 也有类似方法,此处不多做讲解

HTTP 处 理 挂 载 点 触 发 时 机 挂 载 ⽅ 法
HTTP 请 求 头 处 理 阶 段 - 关 接 收 到 客 户 端 发 送 来 的 请 求 头 数 据 时 on_http_request_headers
HTTP 请 求 Body 处 理 阶 段 - 关 接 收 到 客 户 端 发 送 来 的 请 求 Body 数 据 时 on_http_request_body
HTTP 请 求 Trailer 处 理 阶 段 - 关 接 收 到 客 户 端 发 送 来 的 请 求 Trailer 数 据 时 on_http_request_trailers
HTTP 应 答 头 处 理 阶 段 - 关 接 收 到 后 端 服 务 响 应 的 应 答 头 数 据 时 on_http_response_headers
HTTP 应 答 Body 处 理 阶 段 - 关 接 收 到 后 端 服 务 响 应 的 应 答 Body 数 据 时 on_http_response_body
HTTP 应 答 Trailer 处 理 阶 段 - 关 接 收 到 后 端 服 务 响 应 的 应 答 Trailer 数 据 时 on_http_response_trailers

方法汇总(仅列出部分)

分 类 方法 名 称 用途 可 以 ⽣ 效 的 挂 载 ⽅ 法
请 求 头 处 理 get_http_request_headers 获 取 客 户 端 请 求 的 全 部 请 求 头 on_http_request_headers
set_http_request_headers 替 换 客 户 端 请 求 的 全 部 请 求 头 on_http_request_headers
get_http_request_header 获 取 客 户 端 请 求 的 指 定 请 求 头 on_http_request_headers
set_http_request_header 设置客 户 端 请 求 的 指 定 请 求 头 on_http_request_headers
add_http_request_header 新 增 ⼀ 个 客 户 端 请 求 头 on_http_request_headers
请 求 Body 处 理 get_http_request_body 获 取 客 户 端 请 求 Body on_http_request_body
set_http_request_body 替 换 客 户 端 请 求 Body on_http_request_body
请 求 Trailer 处 理 get_http_request_trailers 获 取 客 户 端 请 求 的 全 部 请 求 Trailer on_http_request_trailers
set_http_request_trailers 替 换 客 户 端 请 求 的 全 部 请 求 Trailer on_http_request_trailers
get_http_request_trailer 获 取 客 户 端 请 求 的 指 定 请 求 Trailer on_http_request_trailers
set_http_request_trailer 替 换 客 户 端 请 求 的 指 定 请 求 Trailer on_http_request_trailers
add_http_response_trailer 新 增 ⼀ 个 客 户 端 请 求 Trailer on_http_request_trailers
应 答 头 处 理 get_http_response_headers 获 取 后 端 响 应 的 全 部 应 答 头 on_http_response_headers
set_http_response_headers 替 换 后 端 响 应 的 全 部 应 答 头 on_http_response_headers
get_http_response_header 获 取 后 端 响 应 的 指 定 应 答 头 on_http_response_headers
set_http_response_header 替 换 后 端 响 应 的 指 定 应 答 头 on_http_response_headers
add_http_response_header 新 增 ⼀ 个 后 端 响 应 头 on_http_response_headers
应 答 Body 处 理 get_http_response_body 获 取 客 户 端 请 求 Body on_http_response_body
set_http_response_body 替 换 后 端 响 应 Body on_http_response_body
应 答 Trailer 处 理 get_http_response_trailers 获 取 后 端 响 应 的 全 部 应 答 Trailer on_http_response_trailers
set_http_response_trailers 替 换 后 端 响 应 的 全 部 应 答 on_http_response_trailers
get_http_response_trailer 获 取 后 端 响 应 的 指 定 应 答 Trailer on_http_response_trailers
set_http_response_trailer 替 换 后 端 响 应 的 指 定 应 答 Trailer on_http_response_trailers
add_http_response_trailer 新 增 ⼀ 个 后 端 响 应 on_http_response_trailers
HTTP 调 ⽤ dispatch_http_call 发 送 ⼀ 个 HTTP 请 求 -
get_http_call_response_headers 获 取 DispatchHttpCall 请 求 响 应 的 应 答 头 -
get_http_call_response_body 获 取 DispatchHttpCall 请 求 响 应 的 应 答 Body -
get_http_call_response_trailers 获 取 DispatchHttpCall 请 求 响 应 的 应 答 Trailer -
直 接 响 应 send_http_response 直 接 返 回 ⼀ 个 特 定 的 HTTP 应 答 -
上 下 ⽂ 切 换 resume_http_request 恢 复 先 前 被 暂 停 的 请 求 处 理 流 程 -
resume_http_response 恢 复 先 前 被 暂 停 的 应 答 处 理 流 程 -