Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for sending error responses with gRPC status codes. #248

Merged
merged 6 commits into from
Jul 21, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,7 @@ jobs:
- 'http_body'
- 'http_config'
- 'http_headers'
- 'grpc_auth_random'

defaults:
run:
Expand Down Expand Up @@ -301,6 +302,7 @@ jobs:
- 'http_body'
- 'http_config'
- 'http_headers'
- 'grpc_auth_random'

defaults:
run:
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
- [HTTP Headers](./examples/http_headers/)
- [HTTP Response body](./examples/http_body/)
- [HTTP Configuration](./examples/http_config/)
- [gRPC Auth (random)](./examples/grpc_auth_random/)

## Articles & blog posts from the community

Expand Down
22 changes: 22 additions & 0 deletions examples/grpc_auth_random/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
[package]
publish = false
name = "proxy-wasm-example-grpc-auth-random"
version = "0.0.1"
authors = ["Piotr Sikora <[email protected]>"]
PiotrSikora marked this conversation as resolved.
Show resolved Hide resolved
description = "Proxy-Wasm plugin example: gRPC auth (random)"
license = "Apache-2.0"
edition = "2018"

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

[dependencies]
log = "0.4"
proxy-wasm = { path = "../../" }

[profile.release]
lto = true
opt-level = 3
codegen-units = 1
panic = "abort"
strip = "debuginfo"
52 changes: 52 additions & 0 deletions examples/grpc_auth_random/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
## Proxy-Wasm plugin example: gRPC auth (random)

Proxy-Wasm plugin that grants access based on a result of gRPC callout.

### 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
```

#### Access granted.

Send gRPC request to `localhost:10000` service `hello.HelloService`:

```sh
$ grpcurl -d '{"greeting": "Rust"}' -plaintext localhost:10000 hello.HelloService/SayHello
{
"reply": "hello Rust"
}
```

Expected Envoy logs:

```console
[...] wasm log grpc_auth_random: Access granted.
```

#### Access forbidden.

Send gRPC request to `localhost:10000` service `hello.HelloService`:

```sh
$ grpcurl -d '{"greeting": "Rust"}' -plaintext localhost:10000 hello.HelloService/SayHello
ERROR:
Code: Aborted
Message: Aborted by Proxy-Wasm!
```

Expected Envoy logs:

```console
[...] wasm log grpc_auth_random: Access forbidden.
```
37 changes: 37 additions & 0 deletions examples/grpc_auth_random/docker-compose.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Copyright 2022 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

services:
envoy:
image: envoyproxy/envoy:v1.24-latest
hostname: envoy
ports:
- "10000:10000"
volumes:
- ./envoy.yaml:/etc/envoy/envoy.yaml
- ./target/wasm32-wasi/release:/etc/envoy/proxy-wasm-plugins
networks:
- envoymesh
depends_on:
- grpcbin
grpcbin:
image: kong/grpcbin
hostname: grpcbin
ports:
- "9000:9000"
- "9001:9001"
PiotrSikora marked this conversation as resolved.
Show resolved Hide resolved
networks:
- envoymesh
networks:
envoymesh: {}
82 changes: 82 additions & 0 deletions examples/grpc_auth_random/envoy.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# Copyright 2022 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

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: "/"
route:
cluster: grpcbin
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: "grpc_auth_random"
vm_config:
runtime: "envoy.wasm.runtime.v8"
code:
local:
filename: "/etc/envoy/proxy-wasm-plugins/proxy_wasm_example_grpc_auth_random.wasm"
- name: envoy.filters.http.router
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
clusters:
- name: httpbin
PiotrSikora marked this conversation as resolved.
Show resolved Hide resolved
connect_timeout: 5s
type: STRICT_DNS
lb_policy: ROUND_ROBIN
load_assignment:
cluster_name: httpbin
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: httpbin
port_value: 8080
- name: grpcbin
connect_timeout: 5s
type: STRICT_DNS
lb_policy: ROUND_ROBIN
http2_protocol_options: {}
load_assignment:
cluster_name: grpcbin
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: grpcbin
port_value: 9000
83 changes: 83 additions & 0 deletions examples/grpc_auth_random/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
// Copyright 2020 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

use log::info;
use proxy_wasm::traits::*;
use proxy_wasm::types::*;
use std::time::Duration;

proxy_wasm::main! {{
proxy_wasm::set_log_level(LogLevel::Trace);
proxy_wasm::set_http_context(|_, _| -> Box<dyn HttpContext> { Box::new(GrpcAuthRandom) });
}}

struct GrpcAuthRandom;

impl HttpContext for GrpcAuthRandom {
fn on_http_request_headers(&mut self, _: usize, _: bool) -> Action {
match self.get_http_request_header("content-type") {
Some(value) if value.starts_with("application/grpc") => {}
_ => {
// Reject non-gRPC clients.
self.send_http_response(
503,
vec![("Powered-By", "proxy-wasm")],
Some(b"Service accessible only to gRPC clients.\n"),
);
return Action::Pause;
}
}

match self.get_http_request_header(":path") {
Some(value) if value.starts_with("/grpc.reflection") => {
// Always allow gRPC calls to the reflection API.
Action::Continue
}
_ => {
// Allow other gRPC calls based on the result of grpcbin.GRPCBin/RandomError.
self.dispatch_grpc_call(
"grpcbin",
"grpcbin.GRPCBin",
"RandomError",
vec![],
None,
Duration::from_secs(1),
)
.unwrap();
Action::Pause
}
}
}

fn on_http_response_headers(&mut self, _: usize, _: bool) -> Action {
self.set_http_response_header("Powered-By", Some("proxy-wasm"));
Action::Continue
}
}

impl Context for GrpcAuthRandom {
fn on_grpc_call_response(&mut self, _: u32, status_code: u32, _: usize) {
if status_code % 2 == 0 {
info!("Access granted.");
self.resume_http_request();
} else {
info!("Access forbidden.");
self.send_grpc_response(
GrpcStatusCode::Aborted,
Some("Aborted by Proxy-Wasm!"),
vec![("Powered-By", b"proxy-wasm")],
);
}
}
}
23 changes: 23 additions & 0 deletions src/hostcalls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -728,6 +728,29 @@ pub fn send_http_response(
}
}

pub fn send_grpc_response(
grpc_status: GrpcStatusCode,
grpc_status_message: Option<&str>,
custom_metadata: Vec<(&str, &[u8])>,
) -> Result<(), Status> {
let serialized_custom_metadata = utils::serialize_map_bytes(custom_metadata);
unsafe {
match proxy_send_local_response(
200,
null(),
0,
grpc_status_message.map_or(null(), |grpc_status_message| grpc_status_message.as_ptr()),
grpc_status_message.map_or(0, |grpc_status_message| grpc_status_message.len()),
serialized_custom_metadata.as_ptr(),
serialized_custom_metadata.len(),
grpc_status as i32,
) {
Status::Ok => Ok(()),
status => panic!("unexpected status: {}", status as u32),
}
}
}

extern "C" {
fn proxy_http_call(
upstream_data: *const u8,
Expand Down
9 changes: 9 additions & 0 deletions src/traits.rs
Original file line number Diff line number Diff line change
Expand Up @@ -532,5 +532,14 @@ pub trait HttpContext: Context {
hostcalls::send_http_response(status_code, headers, body).unwrap()
}

fn send_grpc_response(
&self,
grpc_status: GrpcStatusCode,
grpc_status_message: Option<&str>,
custom_metadata: Vec<(&str, &[u8])>,
) {
hostcalls::send_grpc_response(grpc_status, grpc_status_message, custom_metadata).unwrap()
}

fn on_log(&mut self) {}
}
23 changes: 23 additions & 0 deletions src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -115,4 +115,27 @@ pub enum MetricType {
Histogram = 2,
}

#[repr(u32)]
#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)]
#[non_exhaustive]
pub enum GrpcStatusCode {
Ok = 0,
Cancelled = 1,
Unknown = 2,
InvalidArgument = 3,
DeadlineExceeded = 4,
NotFound = 5,
AlreadyExists = 6,
PermissionDenied = 7,
ResourceExhausted = 8,
FailedPrecondition = 9,
Aborted = 10,
OutOfRange = 11,
Uninmplemented = 12,
PiotrSikora marked this conversation as resolved.
Show resolved Hide resolved
Internal = 13,
Unavailable = 14,
DataLoss = 15,
Unauthenticated = 16,
}

pub type Bytes = Vec<u8>;
Loading