Skip to content

Commit

Permalink
Add initial WASM filter scaffolding
Browse files Browse the repository at this point in the history
  • Loading branch information
junr03 authored Jul 11, 2024
2 parents 71eccbe + 913a3f5 commit d3ddbd8
Show file tree
Hide file tree
Showing 8 changed files with 383 additions and 19 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
envoyfilter/target
118 changes: 118 additions & 0 deletions envoyfilter/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 6 additions & 1 deletion envoyfilter/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@
name = "intelligent-prompt-gateway"
version = "0.1.0"
authors = ["Katanemo Inc <info@katanemo.com>"]
edition = "2018"
edition = "2021"

[lib]
crate-type = ["cdylib"]

[dependencies]
proxy-wasm = "0.2.1"
log = "0.4"
67 changes: 66 additions & 1 deletion envoyfilter/README.md
Original file line number Diff line number Diff line change
@@ -1 +1,66 @@
Envoy filter code for gateway
# Envoy filter code for gateway

## Add toolchain

```sh
$ rustup target add wasm32-wasi
```

## Building

```sh
$ cargo build --target wasm32-wasi --release
```

## Using in Envoy

This example can be run with [`docker compose`](https://docs.docker.com/compose/install/)
and has a matching Envoy configuration.

```sh
$ docker compose up
```

## Examples

### Direct response.

Send HTTP request to `localhost:10000/hello`:

```sh
$ curl localhost:10000/hello
```

Expected response:

```console
HTTP/1.1 200 OK
content-length: 40
content-type: text/plain
custom-header: katanemo filter
date: Wed, 10 Jul 2024 16:59:43 GMT
server: envoy
```

### Inline call.

Send HTTP request to `localhost:10000/inline`:

```sh
$ curl localhost:10000/hello
{
"headers": {
"Accept": "*/*",
"Host": "localhost",
"User-Agent": "curl/7.81.0",
"X-Amzn-Trace-Id": "Root=1-637c4767-6e31776a0b407a0219b5b570",
"X-Envoy-Expected-Rq-Timeout-Ms": "15000"
}
}
```

Expected Envoy logs:

```console
[...] wasm log http_auth_random: Access granted.
```
13 changes: 13 additions & 0 deletions envoyfilter/docker-compose.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
services:
envoy:
image: envoyproxy/envoy:v1.30-latest
hostname: envoy
ports:
- "10000:10000"
volumes:
- ./envoy.yaml:/etc/envoy/envoy.yaml
- ./target/wasm32-wasi/release:/etc/envoy/proxy-wasm-plugins
networks:
- envoymesh
networks:
envoymesh: {}
65 changes: 65 additions & 0 deletions envoyfilter/envoy.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
static_resources:
listeners:
address:
socket_address:
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
stat_prefix: ingress_http
codec_type: AUTO
route_config:
name: local_routes
virtual_hosts:
- name: local_service
domains:
- "*"
routes:
- match:
prefix: "/inline"
route:
cluster: httpbin
- match:
prefix: "/"
direct_response:
status: 200
body:
inline_string: "Inspect the HTTP header: custom-header.\n"
http_filters:
- name: envoy.filters.http.wasm
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: "http_config"
configuration:
"@type": "type.googleapis.com/google.protobuf.StringValue"
value: katanemo filter
vm_config:
runtime: "envoy.wasm.runtime.v8"
code:
local:
filename: "/etc/envoy/proxy-wasm-plugins/intelligent_prompt_gateway.wasm"
- name: envoy.filters.http.router
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router

clusters:
- name: httpbin
connect_timeout: 5s
type: STRICT_DNS
lb_policy: ROUND_ROBIN
load_assignment:
cluster_name: httpbin
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: httpbin.org
port_value: 80
hostname: "httpbin.org"
114 changes: 114 additions & 0 deletions envoyfilter/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
use log::info;
use std::time::Duration;

use proxy_wasm::traits::*;
use proxy_wasm::types::*;

proxy_wasm::main! {{
proxy_wasm::set_log_level(LogLevel::Trace);
proxy_wasm::set_root_context(|_| -> Box<dyn RootContext> {
Box::new(HttpHeaderRoot {
header_content: String::new(),
})
});
}}

struct HttpHeader {
context_id: u32,
header_content: String,
}

// HttpContext is the trait that allows the Rust code to interact with HTTP objects.
impl HttpContext for HttpHeader {
// Envoy's HTTP model is event driven. The WASM ABI has given implementors events to hook onto
// the lifecycle of the http request and response.
fn on_http_request_headers(&mut self, _num_headers: usize, _end_of_stream: bool) -> Action {
// Example of reading the HTTP headers on the incoming request
for (name, value) in &self.get_http_request_headers() {
info!("#{} -> {}: {}", self.context_id, name, value);
}

// Example logic of branching based on a request header.
match self.get_http_request_header(":path") {
// If the path header is present and the path is /inline
Some(path) if path == "/inline" => {
// Dispatch an HTTP call inline. This is the model that we will use for the LLM routing host.
self.dispatch_http_call(
"httpbin",
vec![
(":method", "GET"),
(":path", "/bytes/1"),
(":authority", "httpbin.org"),
],
None,
vec![],
Duration::from_secs(5),
)
.unwrap();
// Pause the filter until the out of band HTTP response arrives.
Action::Pause
}

// Otherwise let the HTTP request continue.
_ => Action::Continue,
}
}

fn on_http_response_headers(&mut self, _: usize, _: bool) -> Action {
// Note that the filter can add custom headers. In this case the header is coming from a config value.
self.add_http_response_header("custom-header", self.header_content.as_str());
Action::Continue
}
}

impl Context for HttpHeader {
// Note that the event driven model continues here from the return of the on_http_request_headers above.
fn on_http_call_response(&mut self, _: u32, _: usize, body_size: usize, _: usize) {
if let Some(body) = self.get_http_call_response_body(0, body_size) {
if !body.is_empty() && body[0] % 2 == 0 {
info!("Access granted.");
// This call allows the filter to continue operating on the HTTP request sent by the user.
// In Katanemo's use case the call would continue after the LLM host has responded with routing
// decisions.
self.resume_http_request();
return;
}
}
info!("Access forbidden.");
// This is an example of short-circuiting the http request and sending back a response to the client.
// i.e there was never an external HTTP request made. This could be used for example if the user prompt requires
// more information before it can be sent out to a third party API.
self.send_http_response(
403,
vec![("Powered-By", "Katanemo")],
Some(b"Access forbidden.\n"),
);
}
}

struct HttpHeaderRoot {
header_content: String,
}

impl Context for HttpHeaderRoot {}

// RootContext allows the Rust code to reach into the Envoy Config
impl RootContext for HttpHeaderRoot {
fn on_configure(&mut self, _: usize) -> bool {
if let Some(config_bytes) = self.get_plugin_configuration() {
self.header_content = String::from_utf8(config_bytes).unwrap()
}
true
}

fn create_http_context(&self, context_id: u32) -> Option<Box<dyn HttpContext>> {
Some(Box::new(HttpHeader {
context_id,
header_content: self.header_content.clone(),
}))
}

fn get_type(&self) -> Option<ContextType> {
Some(ContextType::HttpContext)
}
}
Loading

0 comments on commit d3ddbd8

Please sign in to comment.