From 5c570fe194ebfc72fe66617769b005cce2db99d4 Mon Sep 17 00:00:00 2001 From: Piotr Sikora Date: Sun, 21 Jul 2024 12:10:18 -0700 Subject: [PATCH] Add support for sending error responses with gRPC status codes. (#248) Note that some hosts (e.g. Envoy v1.31+) already map HTTP status codes from send_http_response() to gRPC status codes, when talking with gRPC clients, so this API is needed only when more control is needed. Fixes #148. Signed-off-by: Piotr Sikora --- .github/workflows/rust.yml | 2 + README.md | 1 + examples/grpc_auth_random/Cargo.toml | 21 +++++ examples/grpc_auth_random/README.md | 52 ++++++++++++ examples/grpc_auth_random/docker-compose.yaml | 36 ++++++++ examples/grpc_auth_random/envoy.yaml | 69 +++++++++++++++ examples/grpc_auth_random/src/lib.rs | 83 +++++++++++++++++++ src/hostcalls.rs | 23 +++++ src/traits.rs | 9 ++ src/types.rs | 23 +++++ 10 files changed, 319 insertions(+) create mode 100644 examples/grpc_auth_random/Cargo.toml create mode 100644 examples/grpc_auth_random/README.md create mode 100644 examples/grpc_auth_random/docker-compose.yaml create mode 100644 examples/grpc_auth_random/envoy.yaml create mode 100644 examples/grpc_auth_random/src/lib.rs diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 49d5e0bd..24131ee7 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -238,6 +238,7 @@ jobs: - 'http_body' - 'http_config' - 'http_headers' + - 'grpc_auth_random' defaults: run: @@ -301,6 +302,7 @@ jobs: - 'http_body' - 'http_config' - 'http_headers' + - 'grpc_auth_random' defaults: run: diff --git a/README.md b/README.md index d43816d8..53f3de94 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/examples/grpc_auth_random/Cargo.toml b/examples/grpc_auth_random/Cargo.toml new file mode 100644 index 00000000..c3e6ec01 --- /dev/null +++ b/examples/grpc_auth_random/Cargo.toml @@ -0,0 +1,21 @@ +[package] +publish = false +name = "proxy-wasm-example-grpc-auth-random" +version = "0.0.1" +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" diff --git a/examples/grpc_auth_random/README.md b/examples/grpc_auth_random/README.md new file mode 100644 index 00000000..a153bb57 --- /dev/null +++ b/examples/grpc_auth_random/README.md @@ -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. +``` diff --git a/examples/grpc_auth_random/docker-compose.yaml b/examples/grpc_auth_random/docker-compose.yaml new file mode 100644 index 00000000..aa43ed9b --- /dev/null +++ b/examples/grpc_auth_random/docker-compose.yaml @@ -0,0 +1,36 @@ +# 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" + networks: + - envoymesh +networks: + envoymesh: {} diff --git a/examples/grpc_auth_random/envoy.yaml b/examples/grpc_auth_random/envoy.yaml new file mode 100644 index 00000000..19a67d4a --- /dev/null +++ b/examples/grpc_auth_random/envoy.yaml @@ -0,0 +1,69 @@ +# 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: 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 diff --git a/examples/grpc_auth_random/src/lib.rs b/examples/grpc_auth_random/src/lib.rs new file mode 100644 index 00000000..d1c589e9 --- /dev/null +++ b/examples/grpc_auth_random/src/lib.rs @@ -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 { 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")], + ); + } + } +} diff --git a/src/hostcalls.rs b/src/hostcalls.rs index a72ef290..15687888 100644 --- a/src/hostcalls.rs +++ b/src/hostcalls.rs @@ -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, diff --git a/src/traits.rs b/src/traits.rs index 034f87ea..bd54bcbe 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -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) {} } diff --git a/src/types.rs b/src/types.rs index 444bdfef..7407d3ca 100644 --- a/src/types.rs +++ b/src/types.rs @@ -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, + Unimplemented = 12, + Internal = 13, + Unavailable = 14, + DataLoss = 15, + Unauthenticated = 16, +} + pub type Bytes = Vec;