Contact Us 1-800-596-4880

Implement a Flex Gateway Custom Policy in Rust

New Feature Available: This product feature is outdated and is only provided for existing custom policies. To create new custom policies, use the Flex Gateway Policy Development Kit (PDK).

Summary

You can implement custom policies via WebAssembly (WASM) extensions that run on Envoy as custom filters. You implement these policies using the WebAssembly for Proxies (Rust SDK). The following examples assume knowledge about Rust and related tools.

Lifecycle Events

Custom policies are based on proxy-wasm ABI, an event-driven, Envoy-agnostic, low-level interface for L4/L7 proxies. This interface specifies how a WASM extension and its host interact, and it includes listenable lifecycle events: HTTP request lifecycle events and also WASM filter lifecycle events.

The SDK exposes a method to retrieve information related to each lifecycle event. For example, data retrieved from the on_http_request_headers event can be used to perform related custom policy logic, which can then inform the runtime if the request should be processed.

HTTP Request Lifecycle Events

  • on_http_request_headers

    Triggered when the endpoint receives the complete set of HTTP request headers.

  • on_http_request_body

    Triggered when the endpoint receives the first bytes of the HTTP request.

  • on_http_response_headers

    Triggered when the endpoint receives the complete set of HTTP response headers.

  • on_http_response_body

    Triggered when the endpoint receives the first bytes of the HTTP response body.

Filter Lifecycle Events

  • on_configure

    Triggered when the WASM filter starts with an available configuration.

    Flex Gateway serializes policy configuration into JSON, which is used to configure the Envoy WASM filter. You can deserialize and parse this JSON into data that will be used by your custom policy.

Example Lifecycle Events Implementation

The following Envoy filter template demonstrates how to implement lifecycle events:

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(HttpConfigHeaderRoot {
            header_content: String::new(),
        })
    });
}}

struct HttpConfigHeader {
    header_content: String,
}

impl Context for HttpConfigHeader {}

impl HttpContext for HttpConfigHeader {
    fn on_http_request_headers(&mut self, _num_headers: usize, _end_of_stream: bool) -> Action {
        Action::Continue
    }

    fn on_http_request_body(&mut self, _body_size: usize, _end_of_stream: bool) -> Action {
        Action::Continue
    }

    fn on_http_response_headers(&mut self, _num_headers: usize, _end_of_stream: bool) -> Action {
        Action::Continue
    }

    fn on_http_response_body(&mut self, _body_size: usize, _end_of_stream: bool) -> Action {
        Action::Continue
    }
}

struct HttpConfigHeaderRoot {
    header_content: String,
}

impl Context for HttpConfigHeaderRoot {}

impl RootContext for HttpConfigHeaderRoot {
    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, _: u32) -> Option<Box<dyn HttpContext>> {
        Some(Box::new(HttpConfigHeader {
            header_content: self.header_content.clone(),
        }))
    }

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

Custom Authentication Header Policy Example

The following tutorial describes how to create an example policy implementation for a Policy Definition that has already been published in Exchange. The example policy blocks requests whose x-custom-auth header does not match a user-configured value.

During the policy development process, you complete the following steps:

  1. Create the policy definition JSON file.

  2. Set up the project.

  3. Develop the custom policy.

Create the Policy Definition JSON File

Example policy implementations require a policy definition JSON file. Flex Gateway passes policy configuration as JSON with that policy definition structure to the Envoy WASM filter.

The example authentication header policy has only one parameter: the value that needs to be passed in the header. The policy definition JSON file should match the following:

{
  "title": "Custom Auth Header",
  "type": "object",
  "description": "Enforces HTTP authentication matching x-custom-auth value to what is configured in the policy.",
  "properties": {
    "secret-value": {
      "title": "Custom Auth Header Password",
      "type": "string",
      "@context": {
        "@characteristics": [
          "security:sensitive"
        ]
      }
    }
  },
  "required": [
    "secret-value"
  ],
  "unevaluatedProperties": false,
  "@context": {
    "@vocab": "anypoint://vocabulary/policy.yaml#",
    "security": "anypoint://vocabulary/policy.yaml#"
  },
  "$id": "custom-auth-header-simple",
  "$schema": "https://json-schema.org/draft/2019-09/schema"
}

Set Up the Project

  1. Create the Rust project via the following command:

    cargo new flex_custom_policy_example --lib

    This creates a flex_custom_policy_example directory.

  2. Copy the following into Cargo.toml, located in the flex_custom_policy_example directory:

    [package]
    name = "flex_custom_policy_example"
    version = "0.1.0"
    edition = "2021"
    
    # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
    
    [lib]
    crate-type = ["cdylib"]
    name="flex_custom_policy_example"
    path="src/lib.rs"
    
    [dependencies]
    proxy-wasm = { git = "https://github.com/proxy-wasm/proxy-wasm-rust-sdk.git", tag = "v0.2.0" }
    serde = { version = "1.0", features = ["derive"] }
    serde_json = "1.0"
    
    [profile.release]
    opt-level = "z"
    lto = "fat"

Develop the Custom Policy

  1. Add the following policy bootstrapping code to a new Rust source file:

    use proxy_wasm::traits::*;
    use proxy_wasm::types::*;
    use serde::Deserialize;
    
    proxy_wasm::main! {{
        proxy_wasm::set_log_level(LogLevel::Trace);
        proxy_wasm::set_root_context(|_| -> Box<dyn RootContext> {
            Box::new(CustomAuthRootContext {
                config: CustomAuthConfig::default(),
            })
        });
    }}

    The code is required to deploy your WASM filter to Envoy. The main block sets the root context, which is an Envoy entity used to generate the child context for each incoming HTTP request.

  2. Add the following root context implementation code:

    struct CustomAuthRootContext {
        config: CustomAuthConfig,
    }
    
    impl Context for CustomAuthRootContext {}
    
    impl RootContext for CustomAuthRootContext {
    
        fn create_http_context(&self, _: u32) -> Option<Box<dyn HttpContext>> {
            Some(Box::new(CustomAuthHttpContext {
                config: self.config.clone(),
            }))
        }
    
        fn get_type(&self) -> Option<ContextType> {
            Some(ContextType::HttpContext)
        }
    }

    Each Envoy filter is required to provide a root context implementation. The RootContext trait contains useful methods you can implement. In this example, HTTP filters implement create_http_context and get_type so that Envoy can generate the child contexts.

  3. Add the following Struct to enable parsing this JSON:

    #[derive(Default, Clone, Deserialize)]
    struct CustomAuthConfig {
    
        #[serde(alias = "secret-value")]
        secret_value: String,
    }

    Flex Gateway configures your policy with JSON defined in the policy definition. The JSON that needs to be parsed contains a single field called secret-value.

  4. Deserialize the configuration:

    After creating the basic policy configuration structure, you must implement the RootContext on_configure method in order to deserialize it.

    Within the implementation of RootContext for CustomAuthRootContext, add the on_configure method, like the following snippet.

    impl RootContext for CustomAuthRootContext {
    
        fn on_configure(&mut self, _: usize) -> bool {
            if let Some(config_bytes) = self.get_plugin_configuration() {
                self.config = serde_json::from_slice(config_bytes.as_slice()).unwrap();
            }
    
            true
        }
    
        // Other implemented methods
        // ...
    }
  5. Add the following HTTP context code:

    struct CustomAuthHttpContext {
        pub config: CustomAuthConfig,
    }
    
    impl Context for CustomAuthHttpContext {}
    
    impl HttpContext for CustomAuthHttpContext {}

    Each incoming request creates a new CustomAuthHttpContext, and this CustomAuthHttpContext lives as long as the HTTP request lives. This enables you to store request-related state data (but not inter-request state data.)

  6. Add the following core policy functionality logic:

    impl HttpContext for CustomAuthHttpContext {
        fn on_http_request_headers(&mut self, _num_headers: usize, _end_of_stream: bool) -> Action {
    
            if let Some(value) = self.get_http_request_header("x-custom-auth") {
                if self.config.secret_value == value {
                    return Action::Continue;
                }
            }
    
            self.send_http_response(401, Vec::new(), None);
    
            Action::Pause
        }
    }

    The proxy-wasm ABI retrieves the header value of the incoming request and then compares the value against secret-value. If secret-value matches the header, the implementation returns an Action::Continue. Otherwise, it returns Action::Pause, and the filter itself emits a response to the calling client using send_http_response.

    Note: The Rust SDK contains an issue reading non-UTF-8 values with get_http_request_header. To work around the issue, use the get_http_request_headers_bytes method and read the bytes using the from_utf8 method. For example:

    let header = self.get_http_request_header_bytes("x-custom-auth").map(String::from_utf8).and_then(Result::ok);
  7. Enable compilation by adding wasm32 as a target:

    rustup target add wasm32-wasi

    All third-party libraries included in your policy must be compatible with the wasm32-wasi Rust compilation target.

    Some libraries might compile properly to the wasm32-wasi target but might not work properly when deployed to Flex Gateway. Example deployment errors when this happens are:

    • Failed to load Wasm module due to a missing import

    • Wasm VM failed to initialize Wasm code

    • Plugin configured to fail closed failed to load

    To resolve these issues, contact the owners of the third-party library or use a different library.

  8. Compile your custom policy via the following command:

    cargo build --target wasm32-wasi --release

    Compilation outputs a binary .wasm file into the ./target/wasm32-wasi/release directory.

    For more information on pushing custom policies to Exchange, refer to Publish a Flex Gateway or Mule 4 Custom Policy.