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"

プロキシの動作をプログラマブルに変更できるのハチャメチャ面白い。