proxy-wasm/proxy-wasm-rust-sdk を使って Envoy のフィルタを書いてみたので、そのメモです。
環境とセットアップ
https://github.com/mrtc0/sandbox/tree/master/proxy-wasm/my-wasm-filter にある。
$ rustc --version
rustc 1.54.0 (a178d0322 2021-07-26)
$ cargo --version
cargo 1.54.0 (5ae8d74b3 2021-06-22)
wasm32-unknown-unknown
をインストールしておく。
$ rustup toolchain install nightly
$ rustup target add wasm32-unknown-unknown
Working Direcotry を作る。
$ cargo new --lib my-wasm-filter
Cargo.toml はこんな感じ。Crate Type に cdylib を指定しておく。
[package]
name = "my-wasm-filter"
version = "0.1.0"
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
crate-type = ["cdylib"]
[dependencies]
proxy-wasm = "0.1.0"
log = "0.4"
Envoy は Docker で動かす。バックエンドのアプリケーションとして hashicorp/http-echo を用意しておく。
ビルドされた wasm をマウントするようにしておく。
version: "3.8"
services:
envoy:
image: envoyproxy/envoy:v1.17.0
command: envoy -c /etc/envoy.yaml
volumes:
- "./envoy/envoy.yaml:/etc/envoy.yaml:ro"
- "./target/wasm32-unknown-unknown/release/my_wasm_filter.wasm:/etc/my_wasm_filter.wasm"
ports:
- "9000:9000"
- "8000:8000"
echo:
image: hashicorp/http-echo
command:
- '-text="Hello, proxy-wasm"'
envoy の設定はこんな感じ。
admin:
access_log_path: /dev/null
profile_path: /dev/null
address:
socket_address: { address: 0.0.0.0, port_value: 9000 }
static_resources:
listeners:
- name: listener_0
address:
socket_address: { address: 0.0.0.0, port_value: 8000 }
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
stat_prefix: ingress_http
codec_type: AUTO
route_config:
name: local_route
virtual_hosts:
- name: backend
domains: ["*"]
routes:
- match:
prefix: "/"
route:
cluster: echo
http_filters:
- name: envoy.filters.http.wasm
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.wasm.v3.Wasm
config:
name: "my_wasm_filter"
root_id: "my_wasm_filter"
vm_config:
vm_id: my_wasm_filter
runtime: "envoy.wasm.runtime.v8"
code:
local:
filename: "/etc/my_wasm_filter.wasm"
- name: envoy.router
clusters:
- name: echo
connect_timeout: 0.25s
type: STRICT_DNS
lb_policy: ROUND_ROBIN
load_assignment:
cluster_name: echo
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address: { address: echo, port_value: 5678 }
モジュールがロードされるエントリポイントとして、 src/lib.rs
はこんな感じで用意。
mod filter;
use crate::filter::HelloWorldFilterRoot;
use proxy_wasm::traits::*;
use proxy_wasm::types::*;
#[no_mangle]
pub fn _start() {
proxy_wasm::set_log_level(LogLevel::Trace);
proxy_wasm::set_root_context(|_| -> Box<dyn RootContext> {
Box::new(HelloWorldFilterRoot)
});
}
これで OK.
Hello World
proxy-wasm/proxy-wasm-rust-sdk の README と example のコード、それから https://dorayakikun.medium.com/getting-started-with-proxy-rust-sdk-82f9a0e46db6 を見ると雰囲気が分かる。
また、ABI については https://github.com/proxy-wasm/spec/tree/0bde0487db5ea6a892e71f8dbb4b476c8e8f682a/abi-versions/vNEXT を見ると良い。
とりあえず HTTP リクエストを受け取ったときにメッセージを出してみる。
// src/filter.rs
use log::info;
use proxy_wasm::traits::*;
use proxy_wasm::types::*;
pub struct HelloWorldFilterRoot;
impl Context for HelloWorldFilterRoot {}
impl RootContext for HelloWorldFilterRoot {
fn get_type(&self) -> Option<ContextType> {
Some(ContextType::HttpContext)
}
fn create_http_context(&self, _: u32) -> Option<Box<dyn HttpContext>> {
Some(Box::new(HelloWorldFilter))
}
}
struct HelloWorldFilter;
impl<'a> HelloWorldFilter {
}
impl Context for HelloWorldFilter {}
impl HttpContext for HelloWorldFilter {
fn on_http_request_headers(&mut self, _: usize) -> Action {
info!("On http request.");
Action::Continue
}
}
ビルドする。
$ cargo build --target=wasm32-unknown-unknown --release
HTTP リクエストを送信すると、Envoy でログが出力される。
$ curl http://localhost:8000/
$ docker-compose up --build
...
envoy_1 | [2021-08-22 12:00:11.184][32][info][wasm] [source/extensions/common/wasm/context.cc:1171] wasm log: On http request.
HTTP リクエストヘッダを取得する
ABI にあるように get_http_request_headers
で取れる。
// src/filter.rs
use log::info;
use proxy_wasm::traits::*;
use proxy_wasm::types::*;
pub struct HelloWorldFilterRoot;
impl Context for HelloWorldFilterRoot {}
impl RootContext for HelloWorldFilterRoot {
fn get_type(&self) -> Option<ContextType> {
Some(ContextType::HttpContext)
}
fn create_http_context(&self, _: u32) -> Option<Box<dyn HttpContext>> {
Some(Box::new(HelloWorldFilter))
}
}
struct HelloWorldFilter;
impl<'a> HelloWorldFilter {
}
impl Context for HelloWorldFilter {}
impl HttpContext for HelloWorldFilter {
fn on_http_request_headers(&mut self, _: usize) -> Action {
info!("On http request.");
for (name, value) in &self.get_http_request_headers() {
info!("{}: {}", name, value);
}
Action::Continue
}
}
ログにはこう出る。
$ curl -H 'X-Test: hoge' http://localhost:8000/aaa
...
envoy_1 | [2021-08-22 12:27:13.006][32][info][wasm] [source/extensions/common/wasm/context.cc:1171] wasm log: On http request.
envoy_1 | [2021-08-22 12:27:13.006][32][info][wasm] [source/extensions/common/wasm/context.cc:1171] wasm log: :authority: localhost:8000
envoy_1 | [2021-08-22 12:27:13.006][32][info][wasm] [source/extensions/common/wasm/context.cc:1171] wasm log: :path: /aaa
envoy_1 | [2021-08-22 12:27:13.006][32][info][wasm] [source/extensions/common/wasm/context.cc:1171] wasm log: :method: GET
envoy_1 | [2021-08-22 12:27:13.006][32][info][wasm] [source/extensions/common/wasm/context.cc:1171] wasm log: user-agent: curl/7.68.0
envoy_1 | [2021-08-22 12:27:13.006][32][info][wasm] [source/extensions/common/wasm/context.cc:1171] wasm log: accept: */*
envoy_1 | [2021-08-22 12:27:13.006][32][info][wasm] [source/extensions/common/wasm/context.cc:1171] wasm log: x-test: hoge
envoy_1 | [2021-08-22 12:27:13.006][32][info][wasm] [source/extensions/common/wasm/context.cc:1171] wasm log: x-forwarded-proto: http
envoy_1 | [2021-08-22 12:27:13.006][32][info][wasm] [source/extensions/common/wasm/context.cc:1171] wasm log: x-request-id: b327d85c-774f-4e89-85b5-a474802a7cb5
echo_1 | 2021/08/22 12:27:13 localhost:8000 172.20.0.3:59388 "GET /aaa HTTP/1.1" 200 20 "curl/7.68.0" 24.3µs
特定のパスの場合にレスポンスを wasm で返す
/hello
にリクエストが来たら wasm 側でレスポンスを返す。Rust だとパターンマッチが使えて気持ちいい。
(以降関係ないコードは省略)
impl HttpContext for HelloWorldFilter {
fn on_http_request_headers(&mut self, _: usize) -> Action {
info!("On http request.");
match self.get_http_request_header(":path") {
Some(path) if path == "/hello" => {
self.send_http_response(
200,
// Response Headers
vec![("X-hoge", "piyo"), ("Powered-By", "proxy-wasm")],
// Response Body
Some(b"Hello, World!\n"),
);
Action::Pause
}
_ => Action::Continue,
}
}
}
$ curl -i http://localhost:8000/hello
HTTP/1.1 200 OK
x-hoge: piyo
powered-by: proxy-wasm
content-length: 14
content-type: text/plain
date: Sun, 22 Aug 2021 12:31:40 GMT
server: envoy
Hello, World!!
upstream で認証する
リクエストが来たら https://httpbin.org/bearer に送信して認証する。
upstream は Envoy の方で設定しておく必要がある。
- name: httpbin
connect_timeout: 5s
type: LOGICAL_DNS
dns_lookup_family: V4_ONLY
lb_policy: ROUND_ROBIN
load_assignment:
cluster_name: httpbin
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address: { address: httpbin.org, port_value: 443 }
受け取ったリクエストの Authorization ヘッダをそのまま httpbin へのリクエストに追加する。
httpbin では Bearer
トークンがあれば 200 を、なければ 403 を返すようになっているので、それで判断する。
impl Context for HelloWorldFilter {
fn on_http_call_response(&mut self, _: u32, _: usize, _: usize, _: usize) {
if let Some(status) = self.get_http_call_response_header(":status") {
if status == "200" {
info!("Access granted.");
self.resume_http_request();
return;
}
}
info!("Access forbidden.");
self.send_http_response(
403,
vec![("Powered-By", "proxy-wasm")],
Some(b"Access forbidden.\n"),
);
}
}
impl HttpContext for HelloWorldFilter {
fn on_http_request_headers(&mut self, _: usize) -> Action {
info!("On http request.");
match self.get_http_request_header("authorization") {
Some(authorization) => {
self.dispatch_http_call(
"httpbin",
vec![
(":method", "GET"),
(":path", "/bearer"),
(":authority", "httpbin.org"),
("authorization", &authorization)
],
None,
vec![],
Duration::from_secs(5),
).unwrap();
Action::Pause
}
_ => {
info!("Not found authorization header.");
self.send_http_response(
403,
vec![],
Some(b"Access forbidden.\n"),
);
Action::Pause
}
}
}
fn on_http_response_headers(&mut self, _: usize) -> Action {
self.set_http_response_header("Powered-By", Some("proxy-wasm"));
Action::Continue
}
}
$ curl -H 'Authorization: hoge' http://localhost:8000/
Access forbidden.
$ curl -H 'Authorization: Bearer hoge' http://localhost:8000/
"Hello, proxy-wasm"
プロキシの動作をプログラマブルに変更できるのハチャメチャ面白い。