├── .bazelrc ├── .bazelversion ├── .clang-format ├── .github └── workflows │ ├── push.yaml │ ├── release.yaml │ └── test.yaml ├── .gitignore ├── BUILD ├── CODEOWNERS ├── LICENSE ├── README.md ├── WORKSPACE ├── bazel ├── BUILD └── wasm.bzl ├── doc ├── development-setup.md ├── how-to-build-oci-images.md ├── release-process.md ├── write-a-wasm-extension-with-cpp.md ├── write-cpp-unit-test.md └── write-integration-test.md ├── example ├── BUILD ├── README.md ├── WORKSPACE ├── config │ └── example-filter.yaml ├── plugin.cc ├── plugin.h └── test │ ├── example_test.go │ └── testdata │ └── server_filter.yaml.tmpl ├── extensions ├── attributegen │ ├── .gitignore │ ├── BUILD │ ├── config.pb.html │ ├── config.proto │ ├── plugin.cc │ ├── plugin.h │ ├── plugin_test.cc │ └── testdata │ │ ├── BUILD │ │ ├── operation.json │ │ ├── responseCode.json │ │ └── server.yaml ├── basic_auth │ ├── BUILD │ ├── README.md │ ├── config │ │ └── gateway-filter.yaml │ ├── plugin.cc │ ├── plugin.h │ └── plugin_test.cc ├── common │ ├── BUILD │ ├── context.cc │ ├── context.h │ ├── node_info.fbs │ ├── node_info_bfbs_generated.h │ ├── node_info_generated.h │ ├── util.cc │ ├── util.h │ └── wasm │ │ ├── BUILD │ │ ├── base64.h │ │ ├── json_util.cc │ │ └── json_util.h ├── grpc_logging │ ├── BUILD │ ├── README.md │ ├── config.proto │ ├── log.proto │ ├── plugin.cc │ └── plugin.h ├── local_rate_limit │ ├── BUILD │ ├── README.md │ ├── bucket.cc │ ├── bucket.h │ ├── config │ │ └── gateway-filter.yaml │ ├── plugin.cc │ └── plugin.h ├── open_policy_agent │ ├── BUILD │ ├── README.md │ ├── cache.cc │ ├── cache.h │ ├── config │ │ ├── example-filter.yaml │ │ └── opa-service.yaml │ ├── plugin.cc │ └── plugin.h ├── scaffold │ ├── BUILD │ ├── README.md │ ├── plugin.cc │ └── plugin.h ├── stats │ ├── BUILD │ ├── config.pb.html │ ├── config.proto │ ├── plugin.cc │ ├── plugin.h │ ├── plugin_test.cc │ ├── run_test.sh │ └── testdata │ │ ├── client.yaml │ │ ├── istio │ │ ├── metadata-exchange_filter.yaml │ │ └── stats_filter.yaml │ │ └── server.yaml └── zig_demo │ ├── README.md │ ├── build.zig │ └── module.zig ├── go.mod ├── go.sum ├── scripts ├── get-dep.sh ├── update-dep.sh └── update-guide.sh └── test ├── basicauth ├── basicauth_test.go └── testdata │ └── server_filter.yaml.tmpl ├── grpclogging ├── grpclogging_test.go ├── testdata │ ├── node_metadata.yaml.tmpl │ └── server_filter.yaml.tmpl └── testserver │ ├── proto │ ├── log.pb.go │ └── log_grpc.pb.go │ └── server.go ├── inventory.go ├── localratelimit ├── localratelimit_test.go └── testdata │ └── server_filter.yaml.tmpl └── opa ├── opa_test.go ├── server └── server.go └── testdata ├── certs ├── client-key.cert ├── client.cert ├── root.cert ├── server-key.cert └── server.cert ├── resource ├── opa_cluster.yaml.tmpl ├── opa_filter.yaml.tmpl └── server_node_metadata.yaml.tmpl ├── rule └── opa_rule.rego ├── stats ├── cache_hit.yaml.tmpl └── cache_miss.yaml.tmpl └── transport_socket ├── client_tls_context.yaml.tmpl └── server_tls_context.yaml.tmpl /.bazelrc: -------------------------------------------------------------------------------- 1 | build --cxxopt=-std=c++17 2 | 3 | build:clang --action_env=BAZEL_COMPILER=clang 4 | build:clang --linkopt=-fuse-ld=lld 5 | 6 | # Clang with libc++ 7 | build:libc++ --config=clang 8 | build:libc++ --action_env=CXXFLAGS=-stdlib=libc++ 9 | build:libc++ --action_env=LDFLAGS=-stdlib=libc++ 10 | build:libc++ --action_env=BAZEL_CXXOPTS=-stdlib=libc++ 11 | build:libc++ --action_env=BAZEL_LINKLIBS=-l%:libc++.a:-l%:libc++abi.a 12 | build:libc++ --action_env=BAZEL_LINKOPTS=-lm:-pthread 13 | build:libc++ --define force_libcpp=enabled 14 | 15 | # Have the default value for Wasm image tags. 16 | build --define=WASM_IMAGE_TAG=latest 17 | -------------------------------------------------------------------------------- /.bazelversion: -------------------------------------------------------------------------------- 1 | 6.3.2 2 | -------------------------------------------------------------------------------- /.clang-format: -------------------------------------------------------------------------------- 1 | BasedOnStyle: Google 2 | -------------------------------------------------------------------------------- /.github/workflows/push.yaml: -------------------------------------------------------------------------------- 1 | name: Postsubmit Push 2 | 3 | on: 4 | 5 | push: 6 | branches: 7 | - master 8 | - release-[0-9]+.[0-9]+ 9 | 10 | jobs: 11 | 12 | push: 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v1 17 | 18 | - name: Mount bazel cache 19 | uses: actions/cache@v1 20 | with: 21 | path: "/home/runner/.cache/bazel" 22 | key: bazel 23 | 24 | - name: Install bazelisk 25 | run: | 26 | curl -LO "https://github.com/bazelbuild/bazelisk/releases/download/v1.6.1/bazelisk-linux-amd64" 27 | mkdir -p "${GITHUB_WORKSPACE}/bin/" 28 | mv bazelisk-linux-amd64 "${GITHUB_WORKSPACE}/bin/bazel" 29 | chmod +x "${GITHUB_WORKSPACE}/bin/bazel" 30 | 31 | - name: Build 32 | run: | 33 | set -eux 34 | 35 | BRANCH_NAME=$(echo "${GITHUB_REF}" | cut -d "/" -f3) 36 | VERSION=${BRANCH_NAME#"release-"} 37 | for target in $(bazel query ...); do 38 | if [[ "{$target}" == *":_wasm_"* ]]; then 39 | # skip wasm transition target 40 | continue 41 | fi 42 | if [[ "${target}" != *".wasm" ]]; then 43 | # Build wasm modules only 44 | continue 45 | fi 46 | "${GITHUB_WORKSPACE}/bin/bazel" build "${target}" 47 | tmp=${target#"//"} 48 | WASM_PATH="${tmp//:/\/}" 49 | tmp=${WASM_PATH%%.wasm} 50 | EXTENSION_NAME=$(basename ${tmp}) 51 | mkdir -p wasm-extensions/${EXTENSION_NAME//_/-} 52 | cp bazel-bin/${WASM_PATH} wasm-extensions/${EXTENSION_NAME//_/-}/${GITHUB_SHA}.wasm 53 | 54 | # TODO: add precompiled module 55 | done 56 | 57 | - name: Login to GitHub Container Registry 58 | uses: docker/login-action@v1 59 | with: 60 | registry: ghcr.io 61 | username: ${{ github.repository_owner }} 62 | password: ${{ secrets.GHCR_TOKEN }} 63 | 64 | - name: Push Wasm OCI images 65 | run: | 66 | for target in $(bazel query //extensions/...); do 67 | if [[ "{$target}" == *":push_wasm_image"* ]]; then 68 | bazel run --define WASM_IMAGE_TAG=${GITHUB_SHA} "${target}" 69 | fi 70 | done 71 | 72 | - name: Upload Modules 73 | uses: google-github-actions/upload-cloud-storage@main 74 | with: 75 | path: wasm-extensions/ 76 | destination: istio-ecosystem 77 | credentials: ${{ secrets.WASM_UPLOAD_CRED }} 78 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | jobs: 9 | release: 10 | name: Release 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout code 14 | uses: actions/checkout@v2 15 | - name: Download artifacts 16 | run: | 17 | COMMIT=$(git rev-list -n 1 ${{ github.ref }}) 18 | gsutil cp gs://istio-ecosystem/wasm-extensions/basic-auth/${COMMIT}.wasm basic-auth.wasm 19 | gsutil cp gs://istio-ecosystem/wasm-extensions/local-rate-limit/${COMMIT}.wasm local-rate-limit.wasm 20 | gsutil cp gs://istio-ecosystem/wasm-extensions/open-policy-agent/${COMMIT}.wasm open-policy-agent.wasm 21 | # Also prepare an Wasm extension dir for GCS uploading. 22 | mkdir -p wasm-extensions/basic-auth 23 | mkdir -p wasm-extensions/local-rate-limit 24 | mkdir -p wasm-extensions/open-policy-agent 25 | GITHUB_TAG_REF=${{ github.ref }} 26 | cp basic-auth.wasm wasm-extensions/basic-auth/${GITHUB_TAG_REF##*/}.wasm 27 | cp local-rate-limit.wasm wasm-extensions/local-rate-limit/${GITHUB_TAG_REF##*/}.wasm 28 | cp open-policy-agent.wasm wasm-extensions/open-policy-agent/${GITHUB_TAG_REF##*/}.wasm 29 | - name: Create Release 30 | id: create_release 31 | uses: actions/create-release@v1 32 | env: 33 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 34 | with: 35 | tag_name: ${{ github.ref }} 36 | release_name: Release ${{ github.ref }} 37 | draft: false 38 | prerelease: false 39 | - name: Upload Basic Auth Release Asset 40 | id: upload-basic-auth-release-asset 41 | uses: actions/upload-release-asset@v1 42 | env: 43 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 44 | with: 45 | upload_url: ${{ steps.create_release.outputs.upload_url }} 46 | asset_path: ./basic-auth.wasm 47 | asset_name: basic-auth.wasm 48 | asset_content_type: application/wasm 49 | - name: Upload Local Rate Limit Release Asset 50 | id: upload-local-rate-limit-release-asset 51 | uses: actions/upload-release-asset@v1 52 | env: 53 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 54 | with: 55 | upload_url: ${{ steps.create_release.outputs.upload_url }} 56 | asset_path: ./local-rate-limit.wasm 57 | asset_name: local-rate-limit.wasm 58 | asset_content_type: application/wasm 59 | - name: Upload Open Policy Agent Release Asset 60 | id: upload-open-policy-agent-release-asset 61 | uses: actions/upload-release-asset@v1 62 | env: 63 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 64 | with: 65 | upload_url: ${{ steps.create_release.outputs.upload_url }} 66 | asset_path: ./open-policy-agent.wasm 67 | asset_name: open-policy-agent.wasm 68 | asset_content_type: application/wasm 69 | - name: Upload Modules 70 | uses: google-github-actions/upload-cloud-storage@main 71 | with: 72 | path: wasm-extensions/ 73 | destination: istio-ecosystem 74 | credentials: ${{ secrets.WASM_UPLOAD_CRED }} 75 | - name: Login to GitHub Container Registry 76 | uses: docker/login-action@v1 77 | with: 78 | registry: ghcr.io 79 | username: ${{ github.repository_owner }} 80 | password: ${{ secrets.GHCR_TOKEN }} 81 | - name: Push Wasm OCI images 82 | run: | 83 | GITHUB_TAG_REF=${{ github.ref }} 84 | for target in $(bazel query //extensions/...); do 85 | if [[ "{$target}" == *":push_wasm_image"* ]]; then 86 | bazel run --define WASM_IMAGE_TAG=${GITHUB_TAG_REF##*/} "${target}" 87 | fi 88 | done 89 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | 5 | pull_request: 6 | branches: 7 | - master 8 | - release-[0-9]+.[0-9]+ 9 | 10 | jobs: 11 | 12 | test: 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v1 17 | 18 | - name: Mount bazel cache 19 | uses: actions/cache@v1 20 | with: 21 | path: "/home/runner/.cache/bazel" 22 | key: bazel 23 | 24 | - name: Install bazelisk 25 | run: | 26 | curl -LO "https://github.com/bazelbuild/bazelisk/releases/download/v1.6.1/bazelisk-linux-amd64" 27 | mkdir -p "${GITHUB_WORKSPACE}/bin/" 28 | mv bazelisk-linux-amd64 "${GITHUB_WORKSPACE}/bin/bazel" 29 | chmod +x "${GITHUB_WORKSPACE}/bin/bazel" 30 | 31 | - uses: actions/setup-go@v2 32 | with: 33 | go-version: '^1.17.0' 34 | 35 | - name: Build 36 | run: | 37 | set -eux 38 | 39 | for target in $(bazel query ...); do 40 | if [[ "{$target}" == *":_wasm_"* ]]; then 41 | # skip wasm transition target 42 | continue 43 | fi 44 | if [[ "${target}" != *".wasm" ]]; then 45 | # Build wasm modules only 46 | continue 47 | fi 48 | "${GITHUB_WORKSPACE}/bin/bazel" build "${target}" 49 | done 50 | 51 | - name: Unit Test 52 | run: | 53 | "${GITHUB_WORKSPACE}/bin/bazel" test --test_output=errors //... 54 | 55 | - name: Integration Test 56 | run: | 57 | set -eux 58 | 59 | BRANCH_NAME=$(echo "${GITHUB_BASE_REF}" | cut -d "/" -f3) 60 | export ISTIO_TEST_VERSION=${BRANCH_NAME#"release-"} 61 | go test ./test/... 62 | 63 | - name: Build Example 64 | run: | 65 | set -eux 66 | cd example 67 | bazel build //:example.wasm 68 | 69 | - name: Example Integration Test 70 | run: | 71 | set -eux 72 | 73 | BRANCH_NAME=$(echo "${GITHUB_BASE_REF}" | cut -d "/" -f3) 74 | export ISTIO_TEST_VERSION=${BRANCH_NAME#"release-"} 75 | go test ./example/... 76 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /bazel-* 2 | example/bazel-* 3 | zig-cache 4 | .vscode 5 | 6 | user.bazelrc -------------------------------------------------------------------------------- /BUILD: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/istio-ecosystem/wasm-extensions/1ebcbdde1c9aba6e3eb963c52ebfe449f447dae5/BUILD -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @bianpengyuan @kyessenov @mandarjog @richardwxn 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Istio Ecosystem Wasm Extensions 2 | 3 | [![Test Status][test-badge]][test-link] 4 | 5 | This repository contains several canonical Wasm extensions, which intend to demonstrate: 6 | 7 | * Wasm extension development patterns. 8 | * Best practice to test, build, and release a Wasm extension. 9 | 10 | ## Extensions 11 | 12 | * *[Basic auth](/extensions/basic_auth/)* enforces basic auth based on request host, path, and methods. In this extension, you can find how to perform local auth decision based on headers and local reply, as well as JSON configuration string parsing and base64 decoding. 13 | 14 | * *[C++ scaffold](/extensions/scaffold/)* provides an empty C++ extension, which can be used as a starting point to write a C++ Wasm extension. 15 | 16 | * *[gRPC access logging](./extensions/grpc_logging)* makes a logging request to a gRPC service with various kinds of request and workload attributes. In this extension, you can find how to perform asynchronous telemetry reporting, fetch various request attributes and proxy properties, use protobuf and make gRPC callout. 17 | 18 | * *JWT based routing ([WIP](https://github.com/istio-ecosystem/wasm-extensions/issues/16))* reads JWT token information from Envoy dynamic metadata written by JWT auth filter, update host header accordingly, and trigger routing recomputation. In this extension, you can find how to read dynamic metadata, manipulate headers, and affect request routing. 19 | 20 | * *[Local rate limit](/extensions/local_rate_limit/)* applies a token bucket rate limit to incoming requests. Each request processed by the filter utilizes a single token, and if no tokens are available, the request is denied. In this extension you can find how to share data across all plugin VMs and deny request with local reply. 21 | 22 | * *[wasm-oidc-plugin](https://github.com/antonengelhardt/wasm-oidc-plugin)* performs the OIDC Authorization Code Flow when session cookies are not presented in a request. These cookies are encrypted with AES-256-GCM and contain the authorization state of the client, such as `id_token` and `access_token`. The plugin can be configured to exclude certain hosts, paths, and URLs. ID Token validation is optional, and both the config reload interval and the cookie duration can be configured as desired. 23 | 24 | * *[Open Policy Agent client](/extensions/open_policy_agent)* makes HTTP callout to an Open Policy Agent (OPA) server and based on OPA server response decides whether to allow or deny an incoming request. A result cache is also included to avoid expensive callout on every request. In this extension, you can find how to perform HTTP callout, and asynchronously continue or stop an incoming request based on the response of HTTP call. You will also find how to record stats, which can be scraped in the same way as Istio standard metrics. 25 | 26 | * *[Zig scaffold](/extensions/zig_demo/)* provides an empty [Zig](https://ziglang.org/) extension, which can be used as a starting point to write a Zig Wasm extension. 27 | 28 | ## Development Guides 29 | 30 | ### Write a Wasm Extension with C++ 31 | 32 | * [Development set up](doc/development-setup.md) 33 | * [Write, test, deploy, and maintain a C++ Wasm extension](./doc/write-a-wasm-extension-with-cpp.md) 34 | * [Write unit test with C++ Wasm extension](./doc/write-cpp-unit-test.md) 35 | 36 | ### Integration Test 37 | 38 | * [Write integration test with Istio proxy for Wasm extension](./doc/write-integration-test.md) 39 | 40 | ### Tips & Tricks 41 | 42 | * [Wasm development tips and tricks](https://github.com/istio-ecosystem/wasm-extensions/wiki/Wasm-Development-Tips&Tricks) 43 | 44 | [test-badge]: https://github.com/istio-ecosystem/wasm-extensions/workflows/Test/badge.svg 45 | [test-link]: https://github.com/istio-ecosystem/wasm-extensions/actions?query=workflow%3ATest 46 | -------------------------------------------------------------------------------- /WORKSPACE: -------------------------------------------------------------------------------- 1 | workspace(name = "istio_ecosystem_wasm_extensions") 2 | 3 | load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") 4 | 5 | # Need to push Wasm OCI images 6 | http_archive( 7 | name = "rules_oci", 8 | sha256 = "176e601d21d1151efd88b6b027a24e782493c5d623d8c6211c7767f306d655c8", 9 | strip_prefix = "rules_oci-1.2.0", 10 | url = "https://github.com/bazel-contrib/rules_oci/releases/download/v1.2.0/rules_oci-v1.2.0.tar.gz", 11 | ) 12 | 13 | load("@rules_oci//oci:dependencies.bzl", "rules_oci_dependencies") 14 | 15 | rules_oci_dependencies() 16 | 17 | load("@rules_oci//oci:repositories.bzl", "LATEST_CRANE_VERSION", "oci_register_toolchains") 18 | 19 | oci_register_toolchains( 20 | name = "oci", 21 | crane_version = LATEST_CRANE_VERSION, 22 | ) 23 | 24 | # rules_pkg 25 | http_archive( 26 | name = "rules_pkg", 27 | sha256 = "451e08a4d78988c06fa3f9306ec813b836b1d076d0f055595444ba4ff22b867f", 28 | urls = [ 29 | "https://mirror.bazel.build/github.com/bazelbuild/rules_pkg/releases/download/0.7.1/rules_pkg-0.7.1.tar.gz", 30 | "https://github.com/bazelbuild/rules_pkg/releases/download/0.7.1/rules_pkg-0.7.1.tar.gz", 31 | ], 32 | ) 33 | 34 | load("@rules_pkg//:deps.bzl", "rules_pkg_dependencies") 35 | 36 | rules_pkg_dependencies() 37 | 38 | # proxy wasm cpp sdk 39 | PROXY_WASM_CPP_SDK_SHA = "95bb82ce45c41d9100fd1ec15d2ffc67f7f3ceee" 40 | 41 | PROXY_WASM_CPP_SDK_SHA256 = "89792fc1abca331f29f99870476a04146de5e82ff903bdffca90e6729c1f2470" 42 | 43 | http_archive( 44 | name = "proxy_wasm_cpp_sdk", 45 | sha256 = PROXY_WASM_CPP_SDK_SHA256, 46 | strip_prefix = "proxy-wasm-cpp-sdk-" + PROXY_WASM_CPP_SDK_SHA, 47 | url = "https://github.com/proxy-wasm/proxy-wasm-cpp-sdk/archive/" + PROXY_WASM_CPP_SDK_SHA + ".tar.gz", 48 | ) 49 | 50 | load("@proxy_wasm_cpp_sdk//bazel:repositories.bzl", "proxy_wasm_cpp_sdk_repositories") 51 | 52 | proxy_wasm_cpp_sdk_repositories() 53 | 54 | load("@proxy_wasm_cpp_sdk//bazel:dependencies.bzl", "proxy_wasm_cpp_sdk_dependencies") 55 | 56 | proxy_wasm_cpp_sdk_dependencies() 57 | 58 | load("@proxy_wasm_cpp_sdk//bazel:dependencies_extra.bzl", "proxy_wasm_cpp_sdk_dependencies_extra") 59 | 60 | proxy_wasm_cpp_sdk_dependencies_extra() 61 | 62 | 63 | load("@istio_ecosystem_wasm_extensions//bazel:wasm.bzl", "wasm_libraries") 64 | 65 | wasm_libraries() 66 | 67 | # To import proxy wasm cpp host, which will be used in unit testing. 68 | load("@proxy_wasm_cpp_host//bazel:repositories.bzl", "proxy_wasm_cpp_host_repositories") 69 | 70 | proxy_wasm_cpp_host_repositories() 71 | 72 | load("@proxy_wasm_cpp_host//bazel:dependencies.bzl", "proxy_wasm_cpp_host_dependencies") 73 | 74 | proxy_wasm_cpp_host_dependencies() 75 | -------------------------------------------------------------------------------- /bazel/BUILD: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/istio-ecosystem/wasm-extensions/1ebcbdde1c9aba6e3eb963c52ebfe449f447dae5/bazel/BUILD -------------------------------------------------------------------------------- /bazel/wasm.bzl: -------------------------------------------------------------------------------- 1 | load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive", "http_file") 2 | load("@rules_pkg//pkg:tar.bzl", "pkg_tar") 3 | load("@rules_oci//oci:defs.bzl", "oci_image", "oci_push") 4 | 5 | def wasm_dependencies(): 6 | FLAT_BUFFERS_VERSION = "23.3.3" 7 | 8 | http_archive( 9 | name = "com_github_google_flatbuffers", 10 | sha256 = "8aff985da30aaab37edf8e5b02fda33ed4cbdd962699a8e2af98fdef306f4e4d", 11 | strip_prefix = "flatbuffers-" + FLAT_BUFFERS_VERSION, 12 | url = "https://github.com/google/flatbuffers/archive/v" + FLAT_BUFFERS_VERSION + ".tar.gz", 13 | ) 14 | 15 | http_file( 16 | name = "com_github_nlohmann_json_single_header", 17 | sha256 = "3b5d2b8f8282b80557091514d8ab97e27f9574336c804ee666fda673a9b59926", 18 | urls = [ 19 | "https://github.com/nlohmann/json/releases/download/v3.7.3/json.hpp", 20 | ], 21 | ) 22 | 23 | def wasm_libraries(): 24 | """ 25 | Loads the necessary libraries for WebAssembly modules. 26 | """ 27 | ABSL_VERSION = "c8b33b0191a2db8364cacf94b267ea8a3f20ad83" 28 | http_archive( 29 | name = "com_google_absl", 30 | sha256 = "a7803eac00bf68eae1a84ee3b9fcf0c1173e8d9b89b2cee92c7b487ea65be2a9", 31 | strip_prefix = "abseil-cpp-" + ABSL_VERSION, 32 | url = "https://github.com/abseil/abseil-cpp/archive/" + ABSL_VERSION + ".tar.gz", 33 | ) 34 | 35 | # import json, base64, and flatbuffer library from istio proxy repo 36 | wasm_dependencies() 37 | 38 | # import google test and cpp host for unit testing 39 | GOOGLE_TEST_VERSION = "f8d7d77c06936315286eb55f8de22cd23c188571" 40 | http_archive( 41 | name = "com_google_googletest", 42 | sha256 = "7ff5db23de232a39cbb5c9f5143c355885e30ac596161a6b9fc50c4538bfbf01", 43 | strip_prefix = "googletest-" + GOOGLE_TEST_VERSION, 44 | urls = ["https://github.com/google/googletest/archive/" + GOOGLE_TEST_VERSION + ".tar.gz"], 45 | ) 46 | 47 | PROXY_WASM_CPP_HOST_SHA = "5d76116c449d6892b298b7ae79a84ef1cf5752bf" 48 | PROXY_WASM_CPP_HOST_SHA256 = "a5825a1a5bbd5b0178c6189b227d5cf4370ac713a883b41f6a54edd768a03cb7" 49 | 50 | http_archive( 51 | name = "proxy_wasm_cpp_host", 52 | sha256 = PROXY_WASM_CPP_HOST_SHA256, 53 | strip_prefix = "proxy-wasm-cpp-host-" + PROXY_WASM_CPP_HOST_SHA, 54 | url = "https://github.com/proxy-wasm/proxy-wasm-cpp-host/archive/" + PROXY_WASM_CPP_HOST_SHA +".tar.gz", 55 | ) 56 | 57 | def declare_wasm_image_targets(name, wasm_file): 58 | pkg_tar( 59 | name = "wasm_tar", 60 | srcs = [wasm_file], 61 | package_dir = "./plugin.wasm", 62 | ) 63 | oci_image( 64 | name = "wasm_image", 65 | architecture = "amd64", 66 | os = "linux", 67 | tars = [":wasm_tar"], 68 | ) 69 | oci_push( 70 | name = "push_wasm_image", 71 | image = ":wasm_image", 72 | repository = "ghcr.io/istio-ecosystem/wasm-extensions/"+name, 73 | remote_tags = ["$(WASM_IMAGE_TAG)"], 74 | ) 75 | -------------------------------------------------------------------------------- /doc/development-setup.md: -------------------------------------------------------------------------------- 1 | # Set up Develop Environment 2 | 3 | ## C++ Extension Set Up 4 | 5 | ### Install Bazelisk as Bazel 6 | 7 | It is recommended to use Bazelisk installed as bazel: 8 | 9 | On Linux, run the following commands: 10 | 11 | ``` 12 | sudo wget -O /usr/local/bin/bazel https://github.com/bazelbuild/bazelisk/releases/latest/download/bazelisk-linux-amd64 13 | sudo chmod +x /usr/local/bin/bazel 14 | ``` 15 | 16 | On MacOS, you can `brew install bazelisk`. This adds both `bazelisk` and `bazel` to the `PATH`. 17 | 18 | For additional installation methods such as using `npm` and advanced configuration see the [official Bazelisk Installation Guide](https://github.com/bazelbuild/bazelisk#installation). 19 | 20 | ### Installing Minimum Dependencies 21 | 22 | Several dependencies are needs in order to build a C++ WebAssembly extensions with Bazel. 23 | 24 | on Unbuntu, run the following command: 25 | 26 | ``` 27 | sudo apt-get install gcc curl python3 28 | ``` 29 | -------------------------------------------------------------------------------- /doc/how-to-build-oci-images.md: -------------------------------------------------------------------------------- 1 | # How to build Istio Wasm Plugin compatible OCI images 2 | 3 | As of 1.12, Istio has supports for OCI images in its Wasm Plugin mechanism, and users are able to specify any OCI registry location in Wasm Plugin API resources where they store Wasm plugins as containers. 4 | 5 | This document describes how to build OCI images which are consumable by Istio. 6 | 7 | ## Overview 8 | 9 | There are two types of OCI images that are supported by Istio. One is in the Docker format, and another is the standard OCI specification compliant format. Please note that both of them are supported by any OCI registries, and you can operate on standard CLI tools e.g. Docker CLI, [buildah](https://buildah.io/), etc. You can choose either format depending on your preference, and both types of containers are consumable by Istio Wasm Plugin API. 10 | 11 | For the formal specification, please refer to [the link here](https://github.com/solo-io/wasm/blob/master/spec/spec-compat.md). 12 | 13 | ## Build Istio compatible Docker image 14 | 15 | We assume that you have a valid Wasm binary named `plugin.wasm`. 16 | 17 | 1. First, we prepare the following Dockerfile: 18 | 19 | ``` 20 | $ cat Dockerfile 21 | FROM scratch 22 | 23 | COPY plugin.wasm ./ 24 | ``` 25 | 26 | **Note: you must have exactly one `COPY` instruction in the Dockerfile in order to end up having only one layer in produced images.** 27 | 28 | 2. Then, build your image via `docker build` command 29 | 30 | ``` 31 | $ docker build . -t my-registry/mywasm:0.1.0 32 | ``` 33 | 34 | 3. Finally, push the image to your registry via `docker push` command 35 | 36 | ``` 37 | $ docker push my-registry/mywasm:0.1.0 38 | ``` 39 | 40 | ## Build Istio compatible OCI image 41 | 42 | We assume that you have a valid Wasm binary named `plugin.wasm` that you want to package as an image. 43 | 44 | 1. First, we create a working container from `scratch` base image with `buildah from` command. 45 | 46 | ``` 47 | $ buildah --name mywasm from scratch 48 | mywasm 49 | ``` 50 | 51 | 2. Then copy the Wasm binary into that base image by `buildah copy` command to create the layer. 52 | 53 | ``` 54 | $ buildah copy mywasm plugin.wasm ./ 55 | af82a227630327c24026d7c6d3057c3d5478b14426b74c547df011ca5f23d271 56 | ``` 57 | 58 | **Note: you must execute `buildah copy` exactly once in order to end up having only one layer in produced images** 59 | 60 | 4. Now, you can build a *compat* image and push it to your registry via `buildah commit` command 61 | 62 | ``` 63 | $ buildah commit mywasm docker://my-remote-registry/mywasm:0.1.0 64 | ``` 65 | -------------------------------------------------------------------------------- /doc/release-process.md: -------------------------------------------------------------------------------- 1 | # Wasm Extensions Release Process 2 | 3 | The release of Wasm extensions shipped from this repo should follow Istio releases. When Istio makes a minor release, this repo should also cut a new release with the following steps. Assume we are release extensions for 1.x: 4 | 5 | 1. Create a new release branch (release-1.x): `git checkout -b release-1.x upstream/master && git push upstream release-1.x`. 6 | 2. At the new release branch, run `update-dep.sh` to update various dep's SHA to match Istio release. For example `./scripts/update-dep.sh -r 1.x` for `release-1.x` branch. Create a PR to commit the dep update. 7 | 3. After dep is updated, which also means the extension has passed integration test with the 1.x Istio proxy, create and push a release tag. For example, `git tag -a 1.x.0 -m "istio ecosystem wasm extensions release 1.x.0"` and `git push upstream 1.x.0`. After the tag is pushed, a [release workflow](https://github.com/istio-ecosystem/wasm-extensions/actions?query=workflow%3ARelease) should be triggered, which create a github release and upload Wasm binaries. If any more patch releases are needed, repeat step 2 and 3. 8 | 4. With new release published, various user guide also needs to be updated. Specifically, at `master` and `release-1.x` branch, run `update-guide.sh -r 1.x` to update guide and example extension to be based on 1.x. We always want guide and example to be based on the latest release. 9 | 10 | TODO: Automate step 1, 2, and 4 (maybe 3 as well) with prow bot. -------------------------------------------------------------------------------- /doc/write-a-wasm-extension-with-cpp.md: -------------------------------------------------------------------------------- 1 | # Develop a Wasm extension with C++ 2 | --- 3 | 4 | This guide will walk you through how to write, test, deploy, and maintain a HTTP Wasm extension with C++. The code for this guide can be found under [`example` folder](../example). Before walking through this guide, please go through this [guide](./development-setup.md) for development environment set up. 5 | 6 | ## Step 1: Initialize a Bazel workspace 7 | --- 8 | 9 | Create a folder for the extension code. Under the folder, craete a `WORKSPACE` file , which pulls in proxy wasm cpp SDK and necessary toolchain dependencies to build a Wasm filter. The follow is a minimum `WORKSPACE` file used by the example Wasm extension, which pulls the C++ proxy Wasm SDK and invoke several rules to import tool chain: 10 | 11 | ```python 12 | # Pulls proxy wasm cpp SDK with a specific SHA 13 | PROXY_WASM_CPP_SDK_SHA = "fd0be8405db25de0264bdb78fae3a82668c03782" 14 | PROXY_WASM_CPP_SDK_SHA256 = "c57de2425b5c61d7f630c5061e319b4557ae1f1c7526e5a51c33dc1299471b08" 15 | 16 | http_archive( 17 | name = "proxy_wasm_cpp_sdk", 18 | sha256 = PROXY_WASM_CPP_SDK_SHA256, 19 | strip_prefix = "proxy-wasm-cpp-sdk-" + PROXY_WASM_CPP_SDK_SHA, 20 | url = "https://github.com/proxy-wasm/proxy-wasm-cpp-sdk/archive/" + PROXY_WASM_CPP_SDK_SHA + ".tar.gz", 21 | ) 22 | ``` 23 | 24 | Currently, it is recommanded to update SHA of Wasm C++ SDK and build your extension following Istio releases. See [step 7](#step-7-maintain-your-extension-along-with-istio-releases) for more details on extension maintainance. To get SHA and checksum of various dependencies, you can run [`get-dep.sh`](../scripts/get-dep.sh). For example, to get SHA of 1.8 dependencies, run `./scripts/get-dep.sh -r 1.8`. You can rename the workspace to anything relevent to your extension. Other dependencies could also be imported as needed, such as JSON library, base64 library, etc. See the [top level `WORKSPACE` file](../WORKSPACE) for examples. 25 | 26 | ## Step 2: Set up extension scaffold 27 | --- 28 | 29 | Copy the [scaffold plugin code](../extensions/scaffold), which includes a plugin header file `plugin.h`, a source file `plugin.cc`, and a Bazel BUILD file `BUILD`. Give the Wasm binary a meaninful name. In the example folder, we rename it to `example.wasm`. 30 | 31 | The plugin source code implements the follows: 32 | * A root context, which has the same lifetime as the WebAssembly VM, handles plugin configuration, and acts as the context for any interactions with Envoy host which outlives individual streams, e.g. timer tick, HTTP or gRPC call out. 33 | * A stream context, which has the same lifetime as each request stream and acts as the target for callbacks from each stream. 34 | 35 | After these files are copied, try build your Wasm extension with: 36 | ``` 37 | bazel build //:example.wasm 38 | ``` 39 | 40 | After the build finishes, under the generated `bazel-bin/` folder, you should be able to find a Wasm file named as `example.wasm`. Congratulations! you just successfully built an Envoy Wasm extension! 41 | 42 | ## Step 3: Fill in extension logic 43 | --- 44 | 45 | The current Wasm extension is a no-op extension, callbacks need to be filled in order to make it actually function. (TODO: add doc into proxy wasm cpp sdk about available callbacks). In this example plugin, `OnResponseHeaders` is implemented to inject a response header. Specifically, followings are added into header and source file: 46 | 47 | `plugin.h` 48 | ```cpp 49 | class PluginContext : public Context { 50 | public: 51 | ... 52 | FilterHeadersStatus onResponseHeaders(uint32_t, bool) override; 53 | ... 54 | }; 55 | ``` 56 | 57 | `plugin.cc` 58 | ```cpp 59 | FilterHeadersStatus PluginContext::onResponseHeaders(uint32_t, bool) { 60 | addResponseHeader("X-Wasm-custom", "foo"); 61 | return FilterHeadersStatus::Continue; 62 | } 63 | ``` 64 | 65 | After adding the method, build the extension again with the bazel command mentioned in step 2 and the `example.wasm` file should be updated now with the new logic. 66 | 67 | ## Step 4: Write integration test 68 | --- 69 | 70 | To enable quick iteration and deploy your extension with confidence, it is **highly** recommended to write integration tests with your extension and the same Envoy binary Istio proxy runs. To achieve this, the same [Golang integration test framework](https://godoc.org/github.com/istio/proxy/test/envoye2e) for Telemetry v2 filter could be used, which at high level does the follows: 71 | * Downloads Envoy binary, which is built by Istio proxy postsubmit and used in istio proxy docker container. 72 | * Spawns up Envoy processes locally with customizable bootstrap template. 73 | * Spawns up a xDS server locally, which serves customizable xDS resources. In the test logic, the xDS resource will reference the local WebAssembly extension files built at former steps. 74 | * Executes test logic which sends request through Envoy, then examines extension logic accordingly. 75 | 76 | An [integration test](../example/test) is also added to the example extension, which starts up a Envoy process, configures it with a Listener which points to local example Wasm file, sends a HTTP GET request, and verifies that the response header has the desired header. To learn more about how to write an integration test, please refer to this [guide](./write-integration-test.md). 77 | 78 | ## Step 5: Write unit test 79 | --- 80 | 81 | To further harden your extension, it is also **highly** recommended to write unit tests for it. In the unit test, a mock host will be created, which could implement any relevant Wasm callbacks with desired simulation. Specifically, the unit test set up will utilize `null plugin` mode provided by [C++ Wasm host](https://github.com/proxy-wasm/proxy-wasm-cpp-host), under which the extension will be built to a native binary with common C++ toolchain, instead WebAssembly binary with Wasm toolchain. 82 | 83 | An unit test example could be found under the [basic auth plugin](../extensions/basic_auth/plugin_test.cc), where several callbacks are mocked, such as `getBuffer` is mocked to return desired configuration, `getHeaderMapValue` is mocked to return wanted header pair. The whole test needs to be wrapped with `null_plugin` namespace. To learn more about how to write unit tests for the extension, please refer to this [guide](./write-cpp-unit-test.md). 84 | 85 | ## Step 6: Push and deploy the extension 86 | --- 87 | 88 | After the extension has been verified and tested, it is time to deploy the extension with Istio! An example `EnvoyFilter` configuration can be found [here](../example/config/example-filter.yaml), which applies the filter at inbound sidecar. After applying the config, `x-custom-foo` should be added into the response. 89 | 90 | ```console 91 | $ curl -I {GATEWAY_URL} 92 | HTTP/1.1 200 OK 93 | server: istio-envoy 94 | ... 95 | x-wasm-custom: foo 96 | ``` 97 | 98 | ## Step 7: Maintain your extension along with Istio releases 99 | --- 100 | 101 | After writing, testing, and deploying the extension successfully, it is time to make sure that your extension could evolve along with Istio. Currently it is recommended to build and deploy your extension following Istio releases. Specifically: 102 | 103 | * Import all toolchain and deps to import with the same SHA as Istio releases. 104 | * Cut release branch as Istio, and run integration test with proxy of the same version before deploying it. 105 | * Version your Wasm extension module files as well as the `EnvoyFilter` configuration according to Istio version. During Istio upgrade, it will be necessary to have two `EnvoyFilters` simultaneously availble in the cluster. 106 | -------------------------------------------------------------------------------- /example/BUILD: -------------------------------------------------------------------------------- 1 | load("@proxy_wasm_cpp_sdk//bazel:defs.bzl", "proxy_wasm_cc_binary") 2 | 3 | proxy_wasm_cc_binary( 4 | name = "example.wasm", 5 | srcs = [ 6 | "plugin.cc", 7 | "plugin.h", 8 | ], 9 | ) 10 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # Example Wasm Extension 2 | 3 | This folder includes code for [C++ Wasm extension development walkthrough](../doc/write-a-wasm-extension-with-cpp.md). 4 | -------------------------------------------------------------------------------- /example/WORKSPACE: -------------------------------------------------------------------------------- 1 | workspace(name = "example_extension") 2 | 3 | load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") 4 | 5 | PROXY_WASM_CPP_SDK_SHA = "95bb82ce45c41d9100fd1ec15d2ffc67f7f3ceee" 6 | 7 | PROXY_WASM_CPP_SDK_SHA256 = "89792fc1abca331f29f99870476a04146de5e82ff903bdffca90e6729c1f2470" 8 | 9 | http_archive( 10 | name = "proxy_wasm_cpp_sdk", 11 | sha256 = PROXY_WASM_CPP_SDK_SHA256, 12 | strip_prefix = "proxy-wasm-cpp-sdk-" + PROXY_WASM_CPP_SDK_SHA, 13 | url = "https://github.com/proxy-wasm/proxy-wasm-cpp-sdk/archive/" + PROXY_WASM_CPP_SDK_SHA + ".tar.gz", 14 | ) 15 | 16 | load("@proxy_wasm_cpp_sdk//bazel:repositories.bzl", "proxy_wasm_cpp_sdk_repositories") 17 | 18 | proxy_wasm_cpp_sdk_repositories() 19 | 20 | load("@proxy_wasm_cpp_sdk//bazel:dependencies.bzl", "proxy_wasm_cpp_sdk_dependencies") 21 | 22 | proxy_wasm_cpp_sdk_dependencies() 23 | 24 | load("@proxy_wasm_cpp_sdk//bazel:dependencies_extra.bzl", "proxy_wasm_cpp_sdk_dependencies_extra") 25 | 26 | proxy_wasm_cpp_sdk_dependencies_extra() 27 | -------------------------------------------------------------------------------- /example/config/example-filter.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: networking.istio.io/v1alpha3 2 | kind: EnvoyFilter 3 | metadata: 4 | name: example-filter 5 | spec: 6 | configPatches: 7 | - applyTo: HTTP_FILTER 8 | match: 9 | context: SIDECAR_INBOUND 10 | listener: 11 | filterChain: 12 | filter: 13 | name: envoy.http_connection_manager 14 | subFilter: 15 | name: envoy.filters.http.router 16 | patch: 17 | operation: INSERT_BEFORE 18 | value: 19 | name: example-filter-config 20 | config_discovery: 21 | config_source: 22 | ads: {} 23 | initial_fetch_timeout: 0s 24 | type_urls: [ "type.googleapis.com/envoy.extensions.filters.http.wasm.v3.Wasm"] 25 | --- 26 | apiVersion: networking.istio.io/v1alpha3 27 | kind: EnvoyFilter 28 | metadata: 29 | name: example-filter-config 30 | spec: 31 | configPatches: 32 | - applyTo: EXTENSION_CONFIG 33 | match: 34 | context: SIDECAR_INBOUND 35 | patch: 36 | operation: ADD 37 | value: 38 | name: example-filter-config 39 | typed_config: 40 | '@type': type.googleapis.com/udpa.type.v1.TypedStruct 41 | type_url: type.googleapis.com/envoy.extensions.filters.http.wasm.v3.Wasm 42 | value: 43 | config: 44 | vm_config: 45 | code: 46 | remote: 47 | http_uri: 48 | uri: https://storage.googleapis.com/istio-ecosystem/wasm-extensions/example/1.9.0.wasm 49 | # Optional: specifying checksum will let istio agent 50 | # verify the checksum of download artifacts. Missing 51 | # checksum will cause the Wasm module to be downloaded 52 | # repeatedly 53 | sha256: d53a7f59658b0e154338fc50a4a1545e763ca01b5cc5e013f09c99865ee57aad 54 | runtime: envoy.wasm.runtime.v8 55 | -------------------------------------------------------------------------------- /example/plugin.cc: -------------------------------------------------------------------------------- 1 | #include "plugin.h" 2 | 3 | // Boilderplate code to register the extension implementation. 4 | static RegisterContextFactory register_Example(CONTEXT_FACTORY(PluginContext), 5 | ROOT_FACTORY(PluginRootContext)); 6 | 7 | bool PluginRootContext::onConfigure(size_t) { return true; } 8 | 9 | FilterHeadersStatus PluginContext::onResponseHeaders(uint32_t, bool) { 10 | addResponseHeader("X-Wasm-custom", "foo"); 11 | return FilterHeadersStatus::Continue; 12 | } 13 | -------------------------------------------------------------------------------- /example/plugin.h: -------------------------------------------------------------------------------- 1 | #include "proxy_wasm_intrinsics.h" 2 | 3 | class PluginRootContext : public RootContext { 4 | public: 5 | explicit PluginRootContext(uint32_t id, std::string_view root_id) 6 | : RootContext(id, root_id) {} 7 | 8 | bool onConfigure(size_t) override; 9 | }; 10 | 11 | class PluginContext : public Context { 12 | public: 13 | explicit PluginContext(uint32_t id, RootContext* root) : Context(id, root) {} 14 | 15 | FilterHeadersStatus onResponseHeaders(uint32_t, bool) override; 16 | 17 | private: 18 | inline PluginRootContext* rootContext() { 19 | return dynamic_cast(this->root()); 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /example/test/example_test.go: -------------------------------------------------------------------------------- 1 | package basicauth 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "testing" 7 | "time" 8 | 9 | "istio.io/proxy/test/envoye2e/driver" 10 | "istio.io/proxy/test/envoye2e/env" 11 | "istio.io/proxy/testdata" 12 | 13 | "github.com/istio-ecosystem/wasm-extensions/test" 14 | ) 15 | 16 | func TestExamplePlugin(t *testing.T) { 17 | params := driver.NewTestParams(t, map[string]string{ 18 | "ExampleWasmFile": filepath.Join(env.GetBazelBinOrDie(), "example.wasm"), 19 | }, test.ExtensionE2ETests) 20 | params.Vars["ServerHTTPFilters"] = params.LoadTestData("test/testdata/server_filter.yaml.tmpl") 21 | if err := (&driver.Scenario{ 22 | Steps: []driver.Step{ 23 | &driver.XDS{}, 24 | &driver.Update{ 25 | Node: "server", Version: "0", Listeners: []string{string(testdata.MustAsset("listener/server.yaml.tmpl"))}, 26 | }, 27 | &driver.Envoy{ 28 | Bootstrap: params.FillTestData(string(testdata.MustAsset("bootstrap/server.yaml.tmpl"))), 29 | DownloadVersion: os.Getenv("ISTIO_TEST_VERSION"), 30 | }, 31 | &driver.Sleep{Duration: 1 * time.Second}, 32 | &driver.HTTPCall{ 33 | Port: params.Ports.ServerPort, 34 | Method: "GET", 35 | ResponseHeaders: map[string]string{"x-wasm-custom": "foo"}, 36 | ResponseCode: 200, 37 | }, 38 | }, 39 | }).Run(params); err != nil { 40 | t.Fatal(err) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /example/test/testdata/server_filter.yaml.tmpl: -------------------------------------------------------------------------------- 1 | - name: example_plugin 2 | typed_config: 3 | "@type": type.googleapis.com/udpa.type.v1.TypedStruct 4 | type_url: type.googleapis.com/envoy.extensions.filters.http.wasm.v3.Wasm 5 | value: 6 | config: 7 | vm_config: 8 | runtime: "envoy.wasm.runtime.v8" 9 | code: 10 | local: { filename: "{{ .Vars.ExampleWasmFile }}" } 11 | configuration: 12 | "@type": "type.googleapis.com/google.protobuf.StringValue" 13 | value: | 14 | {} 15 | -------------------------------------------------------------------------------- /extensions/attributegen/.gitignore: -------------------------------------------------------------------------------- 1 | config.pb.cc 2 | config.pb.h 3 | -------------------------------------------------------------------------------- /extensions/attributegen/BUILD: -------------------------------------------------------------------------------- 1 | # Copyright Istio Authors. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | ################################################################################ 16 | # 17 | 18 | licenses(["notice"]) # Apache 2 19 | 20 | load("@proxy_wasm_cpp_sdk//bazel:defs.bzl", "proxy_wasm_cc_binary") 21 | 22 | proxy_wasm_cc_binary( 23 | name = "attributegen.wasm", 24 | srcs = [ 25 | "plugin.cc", 26 | "plugin.h", 27 | "//extensions/common:context.cc", 28 | "//extensions/common:context.h", 29 | "//extensions/common:util.cc", 30 | "//extensions/common:util.h", 31 | "//extensions/common:node_info_bfbs_generated.h", 32 | "//extensions/common:node_info_generated.h", 33 | ], 34 | copts = ["-UNULL_PLUGIN"], 35 | deps = [ 36 | ":config_cc_proto", 37 | "//extensions/common/wasm:json_util", 38 | "@com_google_absl//absl/strings", 39 | "@com_google_absl//absl/time", 40 | "@proxy_wasm_cpp_sdk//contrib:contrib_lib", 41 | "@com_github_google_flatbuffers//:flatbuffers", 42 | "@com_github_google_flatbuffers//:runtime_cc", 43 | ], 44 | ) 45 | 46 | cc_proto_library( 47 | name = "config_cc_proto", 48 | visibility = ["//visibility:public"], 49 | deps = ["config_proto"], 50 | ) 51 | 52 | proto_library( 53 | name = "config_proto", 54 | srcs = ["config.proto"], 55 | ) 56 | 57 | 58 | -------------------------------------------------------------------------------- /extensions/attributegen/plugin.cc: -------------------------------------------------------------------------------- 1 | /* Copyright 2020 Istio Authors. All Rights Reserved. 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | #include "extensions/attributegen/plugin.h" 17 | 18 | // WASM_PROLOG 19 | #ifndef NULL_PLUGIN 20 | 21 | #else // NULL_PLUGIN 22 | 23 | #include "include/proxy-wasm/null_plugin.h" 24 | 25 | namespace proxy_wasm { 26 | namespace null_plugin { 27 | 28 | #endif // NULL_PLUGIN 29 | 30 | // END WASM_PROLOG 31 | 32 | namespace AttributeGen { 33 | 34 | // class Match 35 | // Returns the result of evaluation or nothing in case of an error. 36 | std::optional Match::evaluate() const { 37 | if (condition_.empty()) { 38 | return true; 39 | } 40 | 41 | std::optional ret = {}; 42 | 43 | const std::string function = "expr_evaluate"; 44 | char* out = nullptr; 45 | size_t out_size = 0; 46 | auto result = proxy_call_foreign_function(function.data(), function.size(), 47 | reinterpret_cast(&condition_token_), 48 | sizeof(uint32_t), &out, &out_size); 49 | 50 | if (result != WasmResult::Ok) { 51 | LOG_TRACE(absl::StrCat("Failed to evaluate expression:[", condition_token_, "] ", condition_, 52 | " result: ", toString(result))); 53 | } else if (out_size != sizeof(bool)) { 54 | LOG_TRACE(absl::StrCat("Expression:[", condition_token_, "] ", condition_, 55 | " did not return a bool, size:", out_size)); 56 | } else { 57 | // we have a bool. 58 | bool matched = *reinterpret_cast(out); 59 | ret = std::optional{matched}; 60 | } 61 | 62 | if (out != nullptr) { 63 | free(out); 64 | } 65 | 66 | return ret; 67 | } 68 | 69 | // end class Match 70 | 71 | // class AttributeGenerator 72 | 73 | // If evaluation is successful returns true and sets result. 74 | std::optional AttributeGenerator::evaluate(std::string* val) const { 75 | for (const auto& match : matches_) { 76 | auto eval_status = match.evaluate(); 77 | if (!eval_status) { 78 | return {}; 79 | } 80 | if (eval_status.value()) { 81 | *val = match.value(); 82 | return true; 83 | } 84 | } 85 | return false; 86 | } 87 | 88 | // end class AttributeGenerator 89 | 90 | // onConfigure validates configuration. 91 | // If it returns `false` the Proxy will crash. 92 | // It is the responsibility of the control plane to send valid configuration. 93 | // AttributeGen plugin will not return `false`. 94 | bool PluginRootContext::onConfigure(size_t configuration_size) { 95 | auto configuration_data = 96 | getBufferBytes(WasmBufferType::PluginConfiguration, 0, configuration_size); 97 | auto configuration = configuration_data->toString(); 98 | // Parse configuration JSON string. 99 | JsonParseOptions json_options; 100 | json_options.ignore_unknown_fields = true; 101 | istio::attributegen::PluginConfig config; 102 | const auto status = JsonStringToMessage(configuration, &config, json_options); 103 | if (!status.ok()) { 104 | LOG_WARN(absl::StrCat("Config Error: cannot parse 'attributegen' plugin " 105 | "configuration JSON string [YAML is " 106 | "not supported]: ", 107 | configuration)); 108 | incrementMetric(config_errors_, 1); 109 | return true; 110 | } 111 | 112 | debug_ = config.debug(); 113 | 114 | cleanupAttributeGen(); 115 | auto init_status = initAttributeGen(config); 116 | if (!init_status) { 117 | incrementMetric(config_errors_, 1); 118 | cleanupAttributeGen(); 119 | LOG_WARN("Config Error: attributegen plugin rejected invalid configuration"); 120 | } 121 | return true; 122 | } 123 | 124 | bool PluginRootContext::initAttributeGen(const istio::attributegen::PluginConfig& config) { 125 | for (const auto& attribute_gen_config : config.attributes()) { 126 | EvalPhase phase = OnLog; 127 | if (attribute_gen_config.phase() == istio::attributegen::ON_REQUEST) { 128 | phase = OnRequest; 129 | } 130 | std::vector matches; 131 | 132 | for (const auto& matchconfig : attribute_gen_config.match()) { 133 | uint32_t token = 0; 134 | if (matchconfig.condition().empty()) { 135 | matches.push_back(Match("", 0, matchconfig.value())); 136 | continue; 137 | } 138 | auto create_status = createExpression(matchconfig.condition(), &token); 139 | 140 | if (create_status != WasmResult::Ok) { 141 | LOG_WARN(absl::StrCat("Cannot create expression: <", matchconfig.condition(), "> for ", 142 | attribute_gen_config.output_attribute(), 143 | " result:", toString(create_status))); 144 | return false; 145 | } 146 | if (debug_) { 147 | LOG_DEBUG(absl::StrCat("Added [", token, "] ", attribute_gen_config.output_attribute(), 148 | " if (", matchconfig.condition(), ") -> ", matchconfig.value())); 149 | } 150 | 151 | tokens_.push_back(token); 152 | matches.push_back(Match(matchconfig.condition(), token, matchconfig.value())); 153 | } 154 | gen_.push_back( 155 | AttributeGenerator(phase, attribute_gen_config.output_attribute(), std::move(matches))); 156 | matches.clear(); 157 | } 158 | return true; 159 | } 160 | 161 | void PluginRootContext::cleanupAttributeGen() { 162 | gen_.clear(); 163 | for (const auto& token : tokens_) { 164 | exprDelete(token); 165 | } 166 | tokens_.clear(); 167 | } 168 | 169 | bool PluginRootContext::onDone() { 170 | cleanupAttributeGen(); 171 | return true; 172 | } 173 | 174 | // attributeGen is called on the data path. 175 | void PluginRootContext::attributeGen(EvalPhase phase) { 176 | for (const auto& attribute_generator : gen_) { 177 | if (phase != attribute_generator.phase()) { 178 | continue; 179 | } 180 | 181 | std::string val; 182 | auto eval_status = attribute_generator.evaluate(&val); 183 | if (!eval_status) { 184 | incrementMetric(runtime_errors_, 1); 185 | continue; 186 | } 187 | 188 | if (!eval_status.value()) { 189 | continue; 190 | } 191 | 192 | if (debug_) { 193 | LOG_DEBUG(absl::StrCat("Setting ", attribute_generator.outputAttribute(), " --> ", val)); 194 | } 195 | setFilterState(attribute_generator.outputAttribute(), val); 196 | } 197 | } 198 | 199 | #ifdef NULL_PLUGIN 200 | NullPluginRegistry* context_registry_{}; 201 | 202 | RegisterNullVmPluginFactory register_attribute_gen_filter("envoy.wasm.attributegen", []() { 203 | return std::make_unique(context_registry_); 204 | }); 205 | #endif 206 | 207 | } // namespace AttributeGen 208 | 209 | #ifdef NULL_PLUGIN 210 | // WASM_EPILOG 211 | } // namespace null_plugin 212 | } // namespace proxy_wasm 213 | #endif 214 | -------------------------------------------------------------------------------- /extensions/attributegen/plugin.h: -------------------------------------------------------------------------------- 1 | /* Copyright 2020 Istio Authors. All Rights Reserved. 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | #pragma once 17 | 18 | #include 19 | 20 | #include "absl/strings/str_join.h" 21 | #include "extensions/attributegen/config.pb.h" 22 | #include "google/protobuf/util/json_util.h" 23 | 24 | // WASM_PROLOG 25 | #ifndef NULL_PLUGIN 26 | 27 | #include "proxy_wasm_intrinsics.h" 28 | // Do not reorder. 29 | #include "contrib/proxy_expr.h" 30 | 31 | #else // NULL_PLUGIN 32 | 33 | #include "include/proxy-wasm/null_plugin.h" 34 | 35 | namespace proxy_wasm { 36 | namespace null_plugin { 37 | 38 | #include "contrib/proxy_expr.h" 39 | 40 | #endif // NULL_PLUGIN 41 | 42 | // END WASM_PROLOG 43 | 44 | namespace AttributeGen { 45 | 46 | using google::protobuf::util::JsonParseOptions; 47 | using google::protobuf::util::Status; 48 | 49 | class Match { 50 | public: 51 | explicit Match(const std::string& condition, uint32_t condition_token, const std::string& value) 52 | : condition_(condition), condition_token_(condition_token), value_(value){}; 53 | 54 | std::optional evaluate() const; 55 | const std::string& value() const { return value_; }; 56 | 57 | private: 58 | const std::string condition_; 59 | // Expression token associated with the condition. 60 | const uint32_t condition_token_; 61 | const std::string value_; 62 | }; 63 | 64 | enum EvalPhase { OnLog, OnRequest }; 65 | 66 | class AttributeGenerator { 67 | public: 68 | explicit AttributeGenerator(EvalPhase phase, const std::string& output_attribute, 69 | const std::vector& matches) 70 | : phase_(phase), output_attribute_(output_attribute), matches_(std::move(matches)) {} 71 | 72 | // If evaluation is successful returns true and sets result. 73 | std::optional evaluate(std::string* val) const; 74 | EvalPhase phase() const { return phase_; } 75 | const std::string& outputAttribute() const { return output_attribute_; } 76 | 77 | private: 78 | EvalPhase phase_; 79 | const std::string output_attribute_; 80 | const std::vector matches_; 81 | }; 82 | 83 | // PluginRootContext is the root context for all streams processed by the 84 | // thread. It has the same lifetime as the worker thread and acts as target 85 | // for interactions that outlives individual stream, e.g. timer, async calls. 86 | class PluginRootContext : public RootContext { 87 | public: 88 | PluginRootContext(uint32_t id, std::string_view root_id) : RootContext(id, root_id) { 89 | Metric error_count(MetricType::Counter, "error_count", 90 | {MetricTag{"wasm_filter", MetricTag::TagType::String}, 91 | MetricTag{"type", MetricTag::TagType::String}}); 92 | config_errors_ = error_count.resolve("attributegen", "config"); 93 | runtime_errors_ = error_count.resolve("attributegen", "runtime"); 94 | } 95 | 96 | bool onConfigure(size_t) override; 97 | bool onDone() override; 98 | void attributeGen(EvalPhase); 99 | 100 | private: 101 | // Destroy host resources for the allocated expressions. 102 | void cleanupAttributeGen(); 103 | bool initAttributeGen(const istio::attributegen::PluginConfig& config); 104 | 105 | // list of generators. 106 | std::vector gen_; 107 | // Token are created and destroyed by PluginContext. 108 | std::vector tokens_; 109 | 110 | bool debug_; 111 | 112 | // error counter metrics. 113 | uint32_t config_errors_; 114 | uint32_t runtime_errors_; 115 | }; 116 | 117 | // Per-stream context. 118 | class PluginContext : public Context { 119 | public: 120 | explicit PluginContext(uint32_t id, RootContext* root) : Context(id, root) {} 121 | 122 | void onLog() override { rootContext()->attributeGen(OnLog); }; 123 | 124 | FilterHeadersStatus onRequestHeaders(uint32_t, bool) override { 125 | rootContext()->attributeGen(OnRequest); 126 | return FilterHeadersStatus::Continue; 127 | } 128 | 129 | private: 130 | inline PluginRootContext* rootContext() { 131 | return dynamic_cast(this->root()); 132 | }; 133 | }; 134 | 135 | #ifdef NULL_PLUGIN 136 | PROXY_WASM_NULL_PLUGIN_REGISTRY; 137 | #endif 138 | 139 | static RegisterContextFactory register_AttributeGen(CONTEXT_FACTORY(AttributeGen::PluginContext), 140 | ROOT_FACTORY(AttributeGen::PluginRootContext)); 141 | 142 | } // namespace AttributeGen 143 | 144 | // WASM_EPILOG 145 | #ifdef NULL_PLUGIN 146 | } // namespace null_plugin 147 | } // namespace proxy_wasm 148 | #endif 149 | -------------------------------------------------------------------------------- /extensions/attributegen/testdata/BUILD: -------------------------------------------------------------------------------- 1 | licenses(["notice"]) # Apache 2 2 | 3 | filegroup( 4 | name = "testdata", 5 | srcs = glob([ 6 | "*.json", 7 | "*.wasm", 8 | "*.yaml", 9 | ]), 10 | ) 11 | -------------------------------------------------------------------------------- /extensions/attributegen/testdata/operation.json: -------------------------------------------------------------------------------- 1 | { 2 | "attributes": [ 3 | { 4 | "phase": "AFTER_RESPONSE", 5 | "output_attribute": "istio.operationId", 6 | "match": [ 7 | { 8 | "value": "ListBooks", 9 | "condition": "request.url_path == '/books' && request.method == 'GET'" 10 | }, 11 | { 12 | "value": "GetBook", 13 | "condition": "request.url_path.matches('^/shelves/[[:alnum:]]*/books/[[:alnum:]]*$') && request.method == 'GET'" 14 | }, 15 | { 16 | "value": "CreateBook1", 17 | "condition": "request.url_path == '/books/' && request.method == 'POST'" 18 | } 19 | ] 20 | } 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /extensions/attributegen/testdata/responseCode.json: -------------------------------------------------------------------------------- 1 | { 2 | "attributes": [ 3 | { 4 | "phase": "AFTER_RESPONSE", 5 | "output_attribute": "istio.responseClass", 6 | "match": [ 7 | { 8 | "value": "2xx", 9 | "condition": "response.code >= 200 && response.code <= 299" 10 | }, 11 | { 12 | "value": "3xx", 13 | "condition": "response.code >= 300 && response.code <= 399" 14 | }, 15 | { 16 | "value": "404", 17 | "condition": "response.code == 404" 18 | }, 19 | { 20 | "value": "401", 21 | "condition": "response.code == 401" 22 | }, 23 | { 24 | "value": "403", 25 | "condition": "response.code == 403" 26 | }, 27 | { 28 | "value": "429", 29 | "condition": "response.code == 429" 30 | }, 31 | { 32 | "value": "503", 33 | "condition": "response.code == 503" 34 | }, 35 | { 36 | "value": "5xx", 37 | "condition": "response.code >= 500 && response.code <= 599" 38 | }, 39 | { 40 | "value": "4xx", 41 | "condition": "response.code >= 400 && response.code <= 499" 42 | } 43 | ] 44 | } 45 | ] 46 | } 47 | -------------------------------------------------------------------------------- /extensions/basic_auth/BUILD: -------------------------------------------------------------------------------- 1 | load("@proxy_wasm_cpp_sdk//bazel:defs.bzl", "proxy_wasm_cc_binary") 2 | load("//bazel:wasm.bzl", "declare_wasm_image_targets") 3 | 4 | proxy_wasm_cc_binary( 5 | name = "basic_auth.wasm", 6 | srcs = [ 7 | "plugin.cc", 8 | "plugin.h", 9 | "//extensions/common/wasm:base64.h", 10 | ], 11 | deps = [ 12 | "@com_google_absl//absl/strings", 13 | "@com_google_absl//absl/time", 14 | "//extensions/common/wasm:json_util", 15 | ], 16 | ) 17 | 18 | cc_library( 19 | name = "basic_auth_lib", 20 | srcs = [ 21 | "plugin.cc", 22 | "//extensions/common/wasm:base64.h", 23 | ], 24 | hdrs = [ 25 | "plugin.h", 26 | ], 27 | copts = ["-DNULL_PLUGIN"], 28 | deps = [ 29 | "@com_google_absl//absl/strings", 30 | "@com_google_absl//absl/time", 31 | "//extensions/common/wasm:json_util", 32 | "@proxy_wasm_cpp_host//:null_lib", 33 | ], 34 | ) 35 | 36 | cc_test( 37 | name = "basic_auth_test", 38 | srcs = [ 39 | "plugin_test.cc", 40 | ], 41 | copts = ["-DNULL_PLUGIN"], 42 | deps = [ 43 | ":basic_auth_lib", 44 | "@com_google_googletest//:gtest", 45 | "@com_google_googletest//:gtest_main", 46 | "@proxy_wasm_cpp_host//:lib", 47 | ], 48 | ) 49 | 50 | declare_wasm_image_targets( 51 | name = "basic_auth", 52 | wasm_file = ":basic_auth.wasm", 53 | ) 54 | -------------------------------------------------------------------------------- /extensions/basic_auth/README.md: -------------------------------------------------------------------------------- 1 | # Basic Auth Filter User Guide 2 | 3 | > **Note**: This is an experimental feature. 4 | > **Note**: Basic Auth is not recommended for production usage since it is not secure enough. Please consider using Istio mTLS instead in production. 5 | 6 | Basic Auth filter is shipped as a WebAssembly module from this repo. 7 | It is versioned following Istio minor release (e.g. basic auth Wasm module with version 1.9.x should work with any Istio 1.9 patch versions). 8 | All released versions could be found [here](https://github.com/istio-ecosystem/wasm-extensions/releases). 9 | 10 | Before going through this guide, please read official Istio document about [Wasm module remote load](https://istio.io/latest/docs/tasks/extensibility/wasm-module-distribution/). 11 | 12 | ## Deploy basic auth filter 13 | 14 | --- 15 | 16 | In the following guide we will configure Istio proxy to download and apply Basic Auth filter. 17 | 18 | Two `EnvoyFilter` resources will be applied, to inject basic auth filter into HTTP filter chain. 19 | For example, [this configuration](./config/gateway-filter.yaml) injects the basic auth filter to `gateway`. 20 | 21 | The first `EnvoyFilter` will inject an HTTP filter into gateway proxies. The second `EnvoyFilter` resource provides configuration for the filter. 22 | 23 | After applying the filter, gateway should start enforce the basic auth rule. 24 | Use `productpage` app as an example, to test that the rule works, you can curl with and without the authorization header. 25 | For example 26 | 27 | ```console 28 | foo@bar:~$ curl -i /productpage 29 | HTTP/1.1 401 Unauthorized 30 | date: Wed, 09 Dec 2020 18:06:21 GMT 31 | server: istio-envoy 32 | content-length: 0 33 | foo@bar:~$ curl -i -H "authorization: Basic YWRtaW4yOmFkbWluMg==" /productpage 34 | HTTP/1.1 200 OK 35 | content-type: text/html; charset=utf-8 36 | content-length: 4063 37 | server: istio-envoy 38 | date: Wed, 09 Dec 2020 18:07:19 GMT 39 | x-envoy-upstream-service-time: 85 40 | ``` 41 | 42 | ## Configuration Reference 43 | 44 | --- 45 | 46 | The following proto message describes the schema of basic auth filter configuration. 47 | 48 | ```protobuf 49 | message PluginConfig { 50 | // Specifies a list of basic auth rules 51 | repeated BasicAuth basic_auth_rules = 1; 52 | 53 | // Protection space of basic auth: https://tools.ietf.org/html/rfc7617#section-2. 54 | // If not provided, the default value is `istio`. 55 | string realm = 2; 56 | } 57 | 58 | // BasicAuth defines restriction rules based on three elements. 59 | message BasicAuth { 60 | // A list of hosts that this basic auth rule applies on. 61 | // Wildcard hosts are supported in the suffix or prefix form. 62 | // For example: 63 | // Suffix domain wildcards: *.foo.com or *-bar.foo.com. 64 | // Prefix domain wildcards: foo.* or foo-*. 65 | // 66 | // Rule matches when at least one host in the list matches or the 67 | // host list is empty. Port will be stripped from the request host 68 | // when comparing with the host configured here. 69 | repeated string hosts = 1; 70 | 71 | // HTTP path to restrict access according to match pattern specification. 72 | oneof match_pattern { 73 | // match exact pattern in request_path 74 | string exact = 2; 75 | 76 | // match prefix pattern in request_path 77 | string prefix = 3; 78 | 79 | // match suffix pattern in request_path 80 | string suffix = 4; 81 | } 82 | 83 | // HTTP request method operations such as GET, POST, HEAD, PUT, and DELETE. 84 | repeated string request_methods = 5; 85 | 86 | // Credentials provided in the form username:password that have access. 87 | // Credential could be provided in two formats: `USERNAME:PASSWD` and base64 encoded credentials. 88 | repeated string credentials = 6; 89 | } 90 | ``` 91 | 92 | ## Feature Request and Customization 93 | 94 | --- 95 | 96 | If you have any feature request or bug report, please open an issue in this repo. Currently it is on the roadmap to: 97 | 98 | * [ ] Read secret from local file. This is pending on [proxy wasm host implementation to support file read](https://github.com/proxy-wasm/proxy-wasm-cpp-host/issues/127). 99 | * [ ] Add regex to path matching 100 | 101 | It is recommended to customize the extension according to your needs. 102 | Please take a look at [Wasm extension C++ development guide](../../doc/write-a-wasm-extension-with-cpp.md) for more information about how to write your own extension. 103 | -------------------------------------------------------------------------------- /extensions/basic_auth/config/gateway-filter.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: networking.istio.io/v1alpha3 2 | kind: EnvoyFilter 3 | metadata: 4 | name: basic-auth 5 | namespace: istio-system 6 | spec: 7 | configPatches: 8 | - applyTo: HTTP_FILTER 9 | match: 10 | context: GATEWAY 11 | listener: 12 | filterChain: 13 | filter: 14 | name: envoy.http_connection_manager 15 | proxy: 16 | proxyVersion: ^1\.11.* 17 | patch: 18 | operation: INSERT_BEFORE 19 | value: 20 | name: istio.basic_auth 21 | config_discovery: 22 | config_source: 23 | ads: {} 24 | initial_fetch_timeout: 0s # wait indefinitely to prevent bad Wasm fetch 25 | type_urls: [ "type.googleapis.com/envoy.extensions.filters.http.wasm.v3.Wasm"] 26 | --- 27 | apiVersion: networking.istio.io/v1alpha3 28 | kind: EnvoyFilter 29 | metadata: 30 | name: basic-auth-config 31 | namespace: istio-system 32 | spec: 33 | configPatches: 34 | - applyTo: EXTENSION_CONFIG 35 | match: 36 | context: GATEWAY 37 | patch: 38 | operation: ADD 39 | value: 40 | name: istio.basic_auth 41 | typed_config: 42 | '@type': type.googleapis.com/udpa.type.v1.TypedStruct 43 | type_url: type.googleapis.com/envoy.extensions.filters.http.wasm.v3.Wasm 44 | value: 45 | config: 46 | configuration: 47 | '@type': type.googleapis.com/google.protobuf.StringValue 48 | value: | 49 | { 50 | "basic_auth_rules": [ 51 | { 52 | "prefix": "/productpage", 53 | "request_methods":[ "GET", "POST" ], 54 | "credentials":[ "ok:test", "YWRtaW4zOmFkbWluMw==" ] 55 | } 56 | ] 57 | } 58 | vm_config: 59 | vm_id: basic_auth 60 | code: 61 | remote: 62 | http_uri: 63 | uri: https://github.com/istio-ecosystem/wasm-extensions/releases/download/1.11.0/basic-auth.wasm 64 | # Optional: specifying checksum will let istio agent 65 | # verify the checksum of download artifacts. Missing 66 | # checksum will cause the Wasm module to be downloaded 67 | # repeatedly 68 | sha256: 1a9aec2e3356a5ce3b69e99822cb4a1aada8eebeea1c3f73b19672796b3bcc16 69 | runtime: envoy.wasm.runtime.v8 70 | -------------------------------------------------------------------------------- /extensions/basic_auth/plugin.h: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | #include 5 | #define ASSERT(_X) assert(_X) 6 | 7 | static const std::string EMPTY_STRING; 8 | 9 | #ifndef NULL_PLUGIN 10 | 11 | #include "proxy_wasm_intrinsics.h" 12 | 13 | #else 14 | 15 | #include "include/proxy-wasm/null_plugin.h" 16 | 17 | namespace proxy_wasm { 18 | namespace null_plugin { 19 | namespace basic_auth { 20 | 21 | #endif 22 | 23 | // PluginRootContext is the root context for all streams processed by the 24 | // thread. It has the same lifetime as the worker thread and acts as target for 25 | // interactions that outlives individual stream, e.g. timer, async calls. 26 | class PluginRootContext : public RootContext { 27 | public: 28 | PluginRootContext(uint32_t id, std::string_view root_id) 29 | : RootContext(id, root_id) {} 30 | ~PluginRootContext() {} 31 | bool onConfigure(size_t) override; 32 | 33 | // check() handles the retrieval of certain headers (path, 34 | // method and authorization) from the HTTP Request Header in order to compare 35 | // it against the plugin's configuration data and deny or grant access to that 36 | // requested path. 37 | FilterHeadersStatus check(); 38 | 39 | enum MATCH_TYPE { Prefix, Exact, Suffix }; 40 | struct BasicAuthConfigRule { 41 | std::string request_path; 42 | MATCH_TYPE path_pattern; 43 | std::vector> hosts; 44 | std::unordered_set encoded_credentials; 45 | }; 46 | 47 | private: 48 | bool configure(size_t); 49 | 50 | // The following map holds information regarding the plugin's configuration 51 | // data. The key will hold the request_method (GET, POST, DELETE for example) 52 | // The value is a vector of structs holding request_path, match_pattern and 53 | // encoded_credentials container at each position of the vector for a given 54 | // request_method. Here is an example layout of the container: 55 | //{ 56 | // "GET":{ 57 | // { "/products", 58 | // "prefix", 59 | // ["YWRtaW46YWRtaW4="] 60 | // }, 61 | // }, 62 | // "POST":{ 63 | // { "/wiki", 64 | // "prefix", 65 | // ["YWRtaW46YWRtaW4=", "AWRtaW46YWRtaW4="] 66 | // } 67 | // }, 68 | //} 69 | std::unordered_map> 71 | basic_auth_configuration_; 72 | std::string realm_ = "istio"; 73 | FilterHeadersStatus credentialsCheck( 74 | const PluginRootContext::BasicAuthConfigRule&, std::string_view); 75 | }; 76 | 77 | // Per-stream context. 78 | class PluginContext : public Context { 79 | public: 80 | explicit PluginContext(uint32_t id, RootContext* root) : Context(id, root) {} 81 | FilterHeadersStatus onRequestHeaders(uint32_t, bool) override; 82 | 83 | private: 84 | inline PluginRootContext* rootContext() { 85 | return dynamic_cast(this->root()); 86 | } 87 | }; 88 | 89 | #ifdef NULL_PLUGIN 90 | 91 | } // namespace basic_auth 92 | } // namespace null_plugin 93 | } // namespace proxy_wasm 94 | 95 | #endif 96 | -------------------------------------------------------------------------------- /extensions/common/BUILD: -------------------------------------------------------------------------------- 1 | exports_files([ 2 | "context.cc", 3 | "context.h", 4 | "node_info_generated.h", 5 | "node_info_bfbs_generated.h", 6 | "util.cc", 7 | "util.h", 8 | ]) 9 | -------------------------------------------------------------------------------- /extensions/common/node_info.fbs: -------------------------------------------------------------------------------- 1 | /* Copyright 2020 Istio Authors. All Rights Reserved. 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | namespace Wasm.Common; 17 | 18 | table KeyVal { 19 | key:string (key); 20 | value:string; 21 | } 22 | 23 | // NodeInfo represents the information extracted from proxy node metadata. 24 | table FlatNode { 25 | // Name of the node. e.g. in k8s, name is the pod name. 26 | name:string; 27 | // Namespace that the node runs in. 28 | namespace:string; 29 | // K8s or vm workload attributes. 30 | labels:[KeyVal]; 31 | owner:string; 32 | workload_name:string; 33 | // Platform metadata uses prefixed keys 34 | // GCP uses gcp_* keys 35 | platform_metadata:[KeyVal]; 36 | // Version identifier for the proxy. 37 | istio_version:string; 38 | // Unique identifier for the mesh. Taken from global mesh id parameter (or 39 | // the configured trust domain when not specified). 40 | mesh_id:string; 41 | // List of short names for application containers that are using this proxy. 42 | // This is only used for kubernetes, and is populated by the sidecar injector. 43 | app_containers:[string]; 44 | // Identifier for the cluster to which this workload belongs (for k8s workloads). 45 | cluster_id:string; 46 | // instance ip addresses 47 | instance_ips:[string]; 48 | } 49 | 50 | root_type FlatNode; 51 | -------------------------------------------------------------------------------- /extensions/common/node_info_bfbs_generated.h: -------------------------------------------------------------------------------- 1 | // automatically generated by the FlatBuffers compiler, do not modify 2 | 3 | 4 | #ifndef FLATBUFFERS_GENERATED_NODEINFO_WASM_COMMON_BFBS_H_ 5 | #define FLATBUFFERS_GENERATED_NODEINFO_WASM_COMMON_BFBS_H_ 6 | 7 | namespace Wasm { 8 | namespace Common { 9 | 10 | struct FlatNodeBinarySchema { 11 | static const uint8_t *data() { 12 | // Buffer containing the binary schema. 13 | static const uint8_t bfbsData[852] = { 14 | 0x18,0x00,0x00,0x00,0x42,0x46,0x42,0x53,0x10,0x00,0x1C,0x00,0x04,0x00,0x08,0x00,0x0C,0x00,0x10,0x00, 15 | 0x14,0x00,0x18,0x00,0x10,0x00,0x00,0x00,0x30,0x00,0x00,0x00,0x28,0x00,0x00,0x00,0x1C,0x00,0x00,0x00, 16 | 0x10,0x00,0x00,0x00,0x2C,0x00,0x00,0x00,0x04,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 17 | 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x02,0x00,0x00,0x00, 18 | 0x08,0x00,0x00,0x00,0x60,0x02,0x00,0x00,0xB0,0xFD,0xFF,0xFF,0x3C,0x00,0x00,0x00,0x08,0x00,0x00,0x00, 19 | 0x01,0x00,0x00,0x00,0x0B,0x00,0x00,0x00,0x9C,0x00,0x00,0x00,0x70,0x00,0x00,0x00,0x40,0x00,0x00,0x00, 20 | 0xE8,0x00,0x00,0x00,0xA0,0x01,0x00,0x00,0xBC,0x00,0x00,0x00,0x00,0x02,0x00,0x00,0xC8,0x01,0x00,0x00, 21 | 0x60,0x01,0x00,0x00,0xFC,0x00,0x00,0x00,0x2C,0x01,0x00,0x00,0x14,0x00,0x00,0x00,0x57,0x61,0x73,0x6D, 22 | 0x2E,0x43,0x6F,0x6D,0x6D,0x6F,0x6E,0x2E,0x46,0x6C,0x61,0x74,0x4E,0x6F,0x64,0x65,0x00,0x00,0x00,0x00, 23 | 0xCC,0xFD,0xFF,0xFF,0x0A,0x00,0x18,0x00,0x10,0x00,0x00,0x00,0x04,0x00,0x00,0x00,0xAC,0xFF,0xFF,0xFF, 24 | 0x00,0x00,0x0E,0x0D,0x0C,0x00,0x00,0x00,0x69,0x6E,0x73,0x74,0x61,0x6E,0x63,0x65,0x5F,0x69,0x70,0x73, 25 | 0x00,0x00,0x00,0x00,0xF8,0xFD,0xFF,0xFF,0x09,0x00,0x16,0x00,0x10,0x00,0x00,0x00,0x04,0x00,0x00,0x00, 26 | 0xB2,0xFD,0xFF,0xFF,0x00,0x00,0x00,0x0D,0x0A,0x00,0x00,0x00,0x63,0x6C,0x75,0x73,0x74,0x65,0x72,0x5F, 27 | 0x69,0x64,0x00,0x00,0x20,0xFE,0xFF,0xFF,0x08,0x00,0x14,0x00,0x18,0x00,0x00,0x00,0x0C,0x00,0x00,0x00, 28 | 0x08,0x00,0x08,0x00,0x06,0x00,0x07,0x00,0x08,0x00,0x00,0x00,0x00,0x00,0x0E,0x0D,0x0E,0x00,0x00,0x00, 29 | 0x61,0x70,0x70,0x5F,0x63,0x6F,0x6E,0x74,0x61,0x69,0x6E,0x65,0x72,0x73,0x00,0x00,0x54,0xFE,0xFF,0xFF, 30 | 0x07,0x00,0x12,0x00,0x10,0x00,0x00,0x00,0x04,0x00,0x00,0x00,0x0E,0xFE,0xFF,0xFF,0x00,0x00,0x00,0x0D, 31 | 0x07,0x00,0x00,0x00,0x6D,0x65,0x73,0x68,0x5F,0x69,0x64,0x00,0x78,0xFE,0xFF,0xFF,0x06,0x00,0x10,0x00, 32 | 0x10,0x00,0x00,0x00,0x04,0x00,0x00,0x00,0x32,0xFE,0xFF,0xFF,0x00,0x00,0x00,0x0D,0x0D,0x00,0x00,0x00, 33 | 0x69,0x73,0x74,0x69,0x6F,0x5F,0x76,0x65,0x72,0x73,0x69,0x6F,0x6E,0x00,0x00,0x00,0xA4,0xFE,0xFF,0xFF, 34 | 0x05,0x00,0x0E,0x00,0x14,0x00,0x00,0x00,0x04,0x00,0x00,0x00,0x6E,0xFF,0xFF,0xFF,0x00,0x00,0x0E,0x0F, 35 | 0x01,0x00,0x00,0x00,0x11,0x00,0x00,0x00,0x70,0x6C,0x61,0x74,0x66,0x6F,0x72,0x6D,0x5F,0x6D,0x65,0x74, 36 | 0x61,0x64,0x61,0x74,0x61,0x00,0x00,0x00,0xD8,0xFE,0xFF,0xFF,0x04,0x00,0x0C,0x00,0x10,0x00,0x00,0x00, 37 | 0x04,0x00,0x00,0x00,0x92,0xFE,0xFF,0xFF,0x00,0x00,0x00,0x0D,0x0D,0x00,0x00,0x00,0x77,0x6F,0x72,0x6B, 38 | 0x6C,0x6F,0x61,0x64,0x5F,0x6E,0x61,0x6D,0x65,0x00,0x00,0x00,0x04,0xFF,0xFF,0xFF,0x03,0x00,0x0A,0x00, 39 | 0x10,0x00,0x00,0x00,0x04,0x00,0x00,0x00,0xBE,0xFE,0xFF,0xFF,0x00,0x00,0x00,0x0D,0x05,0x00,0x00,0x00, 40 | 0x6F,0x77,0x6E,0x65,0x72,0x00,0x00,0x00,0x0C,0x00,0x12,0x00,0x08,0x00,0x0C,0x00,0x04,0x00,0x06,0x00, 41 | 0x0C,0x00,0x00,0x00,0x02,0x00,0x08,0x00,0x20,0x00,0x00,0x00,0x10,0x00,0x00,0x00,0x00,0x00,0x0A,0x00, 42 | 0x0C,0x00,0x06,0x00,0x07,0x00,0x08,0x00,0x0A,0x00,0x00,0x00,0x00,0x00,0x0E,0x0F,0x01,0x00,0x00,0x00, 43 | 0x06,0x00,0x00,0x00,0x6C,0x61,0x62,0x65,0x6C,0x73,0x00,0x00,0x68,0xFF,0xFF,0xFF,0x01,0x00,0x06,0x00, 44 | 0x10,0x00,0x00,0x00,0x04,0x00,0x00,0x00,0x22,0xFF,0xFF,0xFF,0x00,0x00,0x00,0x0D,0x09,0x00,0x00,0x00, 45 | 0x6E,0x61,0x6D,0x65,0x73,0x70,0x61,0x63,0x65,0x00,0x00,0x00,0x0C,0x00,0x10,0x00,0x08,0x00,0x0C,0x00, 46 | 0x00,0x00,0x06,0x00,0x0C,0x00,0x00,0x00,0x00,0x00,0x04,0x00,0x10,0x00,0x00,0x00,0x04,0x00,0x00,0x00, 47 | 0x56,0xFF,0xFF,0xFF,0x00,0x00,0x00,0x0D,0x04,0x00,0x00,0x00,0x6E,0x61,0x6D,0x65,0x00,0x00,0x00,0x00, 48 | 0x0C,0x00,0x10,0x00,0x04,0x00,0x08,0x00,0x00,0x00,0x0C,0x00,0x0C,0x00,0x00,0x00,0x18,0x00,0x00,0x00, 49 | 0x08,0x00,0x00,0x00,0x01,0x00,0x00,0x00,0x02,0x00,0x00,0x00,0x64,0x00,0x00,0x00,0x28,0x00,0x00,0x00, 50 | 0x12,0x00,0x00,0x00,0x57,0x61,0x73,0x6D,0x2E,0x43,0x6F,0x6D,0x6D,0x6F,0x6E,0x2E,0x4B,0x65,0x79,0x56, 51 | 0x61,0x6C,0x00,0x00,0x0C,0x00,0x10,0x00,0x08,0x00,0x0C,0x00,0x04,0x00,0x06,0x00,0x0C,0x00,0x00,0x00, 52 | 0x01,0x00,0x06,0x00,0x10,0x00,0x00,0x00,0x04,0x00,0x00,0x00,0xC6,0xFF,0xFF,0xFF,0x00,0x00,0x00,0x0D, 53 | 0x05,0x00,0x00,0x00,0x76,0x61,0x6C,0x75,0x65,0x00,0x16,0x00,0x12,0x00,0x08,0x00,0x0C,0x00,0x00,0x00, 54 | 0x06,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x04,0x00,0x05,0x00,0x16,0x00,0x00,0x00,0x01,0x01,0x04,0x00, 55 | 0x18,0x00,0x00,0x00,0x0C,0x00,0x00,0x00,0x00,0x00,0x06,0x00,0x08,0x00,0x07,0x00,0x06,0x00,0x00,0x00, 56 | 0x00,0x00,0x00,0x0D,0x03,0x00,0x00,0x00,0x6B,0x65,0x79,0x00 57 | }; 58 | return bfbsData; 59 | } 60 | static size_t size() { 61 | return 852; 62 | } 63 | const uint8_t *begin() { 64 | return data(); 65 | } 66 | const uint8_t *end() { 67 | return data() + size(); 68 | } 69 | }; 70 | 71 | } // namespace Common 72 | } // namespace Wasm 73 | 74 | #endif // FLATBUFFERS_GENERATED_NODEINFO_WASM_COMMON_BFBS_H_ 75 | -------------------------------------------------------------------------------- /extensions/common/util.h: -------------------------------------------------------------------------------- 1 | /* Copyright 2019 Istio Authors. All Rights Reserved. 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | #pragma once 17 | 18 | #include 19 | 20 | #include "absl/strings/string_view.h" 21 | 22 | namespace Wasm { 23 | namespace Common { 24 | 25 | // None response flag. 26 | const char NONE[] = "-"; 27 | 28 | // Parses an integer response flag into a readable short string. 29 | const std::string parseResponseFlag(uint64_t response_flag); 30 | 31 | // Used for converting sanctioned uses of std string_view (e.g. extensions) to 32 | // absl::string_view for internal use. 33 | inline absl::string_view toAbslStringView(std::string_view view) { 34 | return absl::string_view(view.data(), view.size()); 35 | } 36 | 37 | // Used for converting internal absl::string_view to sanctioned uses of std 38 | // string_view (e.g. extensions). 39 | inline std::string_view toStdStringView(absl::string_view view) { 40 | return std::string_view(view.data(), view.size()); 41 | } 42 | 43 | } // namespace Common 44 | } // namespace Wasm 45 | -------------------------------------------------------------------------------- /extensions/common/wasm/BUILD: -------------------------------------------------------------------------------- 1 | licenses(["notice"]) 2 | 3 | genrule( 4 | name = "nlohmann_json_hpp", 5 | srcs = ["@com_github_nlohmann_json_single_header//file"], 6 | outs = ["nlohmann_json.hpp"], 7 | cmd = "cp $< $@", 8 | visibility = ["//visibility:public"], 9 | ) 10 | 11 | cc_library( 12 | name = "json_util", 13 | srcs = ["json_util.cc"], 14 | hdrs = [ 15 | "json_util.h", 16 | ":nlohmann_json_hpp", 17 | ], 18 | copts = ["-UNULL_PLUGIN"], 19 | visibility = ["//visibility:public"], 20 | deps = [ 21 | "@com_google_absl//absl/strings", 22 | "@com_google_absl//absl/types:optional", 23 | ], 24 | ) 25 | 26 | exports_files([ 27 | "base64.h", 28 | "json_util.cc", 29 | "json_util.h", 30 | ]) 31 | -------------------------------------------------------------------------------- /extensions/common/wasm/base64.h: -------------------------------------------------------------------------------- 1 | /* Copyright 2019 Istio Authors. All Rights Reserved. 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | * 15 | * From 16 | * https://github.com/envoyproxy/envoy/blob/master/source/common/common/base64.{h,cc} 17 | */ 18 | 19 | #pragma once 20 | 21 | #include 22 | 23 | class Base64 { 24 | public: 25 | static std::string encode(const char* input, uint64_t length, bool add_padding); 26 | static std::string encode(const char* input, uint64_t length) { 27 | return encode(input, length, true); 28 | } 29 | static std::string decodeWithoutPadding(std::string_view input); 30 | }; 31 | 32 | // clang-format off 33 | inline constexpr char CHAR_TABLE[] = 34 | "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; 35 | 36 | inline constexpr unsigned char REVERSE_LOOKUP_TABLE[256] = { 37 | 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 38 | 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 62, 64, 64, 64, 63, 39 | 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 64, 64, 64, 64, 64, 64, 64, 0, 1, 2, 3, 4, 5, 6, 40 | 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 64, 64, 64, 64, 64, 41 | 64, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 42 | 49, 50, 51, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 43 | 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 44 | 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 45 | 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 46 | 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 47 | 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64}; 48 | // clang-format on 49 | 50 | inline bool decodeBase(const uint8_t cur_char, uint64_t pos, std::string& ret, 51 | const unsigned char* const reverse_lookup_table) { 52 | const unsigned char c = reverse_lookup_table[static_cast(cur_char)]; 53 | if (c == 64) { 54 | // Invalid character 55 | return false; 56 | } 57 | 58 | switch (pos % 4) { 59 | case 0: 60 | ret.push_back(c << 2); 61 | break; 62 | case 1: 63 | ret.back() |= c >> 4; 64 | ret.push_back(c << 4); 65 | break; 66 | case 2: 67 | ret.back() |= c >> 2; 68 | ret.push_back(c << 6); 69 | break; 70 | case 3: 71 | ret.back() |= c; 72 | break; 73 | } 74 | return true; 75 | } 76 | 77 | inline bool decodeLast(const uint8_t cur_char, uint64_t pos, std::string& ret, 78 | const unsigned char* const reverse_lookup_table) { 79 | const unsigned char c = reverse_lookup_table[static_cast(cur_char)]; 80 | if (c == 64) { 81 | // Invalid character 82 | return false; 83 | } 84 | 85 | switch (pos % 4) { 86 | case 0: 87 | return false; 88 | case 1: 89 | ret.back() |= c >> 4; 90 | return (c & 0b1111) == 0; 91 | case 2: 92 | ret.back() |= c >> 2; 93 | return (c & 0b11) == 0; 94 | case 3: 95 | ret.back() |= c; 96 | break; 97 | } 98 | return true; 99 | } 100 | 101 | inline void encodeBase(const uint8_t cur_char, uint64_t pos, uint8_t& next_c, std::string& ret, 102 | const char* const char_table) { 103 | switch (pos % 3) { 104 | case 0: 105 | ret.push_back(char_table[cur_char >> 2]); 106 | next_c = (cur_char & 0x03) << 4; 107 | break; 108 | case 1: 109 | ret.push_back(char_table[next_c | (cur_char >> 4)]); 110 | next_c = (cur_char & 0x0f) << 2; 111 | break; 112 | case 2: 113 | ret.push_back(char_table[next_c | (cur_char >> 6)]); 114 | ret.push_back(char_table[cur_char & 0x3f]); 115 | next_c = 0; 116 | break; 117 | } 118 | } 119 | 120 | inline void encodeLast(uint64_t pos, uint8_t last_char, std::string& ret, 121 | const char* const char_table, bool add_padding) { 122 | switch (pos % 3) { 123 | case 1: 124 | ret.push_back(char_table[last_char]); 125 | if (add_padding) { 126 | ret.push_back('='); 127 | ret.push_back('='); 128 | } 129 | break; 130 | case 2: 131 | ret.push_back(char_table[last_char]); 132 | if (add_padding) { 133 | ret.push_back('='); 134 | } 135 | break; 136 | default: 137 | break; 138 | } 139 | } 140 | 141 | inline std::string Base64::encode(const char* input, uint64_t length, bool add_padding) { 142 | uint64_t output_length = (length + 2) / 3 * 4; 143 | std::string ret; 144 | ret.reserve(output_length); 145 | 146 | uint64_t pos = 0; 147 | uint8_t next_c = 0; 148 | 149 | for (uint64_t i = 0; i < length; ++i) { 150 | encodeBase(input[i], pos++, next_c, ret, CHAR_TABLE); 151 | } 152 | 153 | encodeLast(pos, next_c, ret, CHAR_TABLE, add_padding); 154 | 155 | return ret; 156 | } 157 | 158 | inline std::string Base64::decodeWithoutPadding(std::string_view input) { 159 | if (input.empty()) { 160 | return EMPTY_STRING; 161 | } 162 | 163 | // At most last two chars can be '='. 164 | size_t n = input.length(); 165 | if (input[n - 1] == '=') { 166 | n--; 167 | if (n > 0 && input[n - 1] == '=') { 168 | n--; 169 | } 170 | } 171 | // Last position before "valid" padding character. 172 | uint64_t last = n - 1; 173 | // Determine output length. 174 | size_t max_length = (n + 3) / 4 * 3; 175 | if (n % 4 == 3) { 176 | max_length -= 1; 177 | } 178 | if (n % 4 == 2) { 179 | max_length -= 2; 180 | } 181 | 182 | std::string ret; 183 | ret.reserve(max_length); 184 | for (uint64_t i = 0; i < last; ++i) { 185 | if (!decodeBase(input[i], i, ret, REVERSE_LOOKUP_TABLE)) { 186 | return EMPTY_STRING; 187 | } 188 | } 189 | 190 | if (!decodeLast(input[last], last, ret, REVERSE_LOOKUP_TABLE)) { 191 | return EMPTY_STRING; 192 | } 193 | 194 | ASSERT(ret.size() == max_length); 195 | return ret; 196 | } 197 | -------------------------------------------------------------------------------- /extensions/common/wasm/json_util.cc: -------------------------------------------------------------------------------- 1 | /* Copyright 2020 Istio Authors. All Rights Reserved. 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | #include "extensions/common/wasm/json_util.h" 17 | 18 | #include "absl/strings/numbers.h" 19 | 20 | namespace Wasm { 21 | namespace Common { 22 | 23 | std::optional JsonParse(std::string_view str) { 24 | const auto result = JsonObject::parse(str, nullptr, false); 25 | if (result.is_discarded() || !result.is_object()) { 26 | return std::nullopt; 27 | } 28 | return result; 29 | } 30 | 31 | template <> 32 | std::pair, JsonParserResultDetail> 33 | JsonValueAs(const JsonObject& j) { 34 | if (j.is_number()) { 35 | return std::make_pair(j.get(), JsonParserResultDetail::OK); 36 | } else if (j.is_string()) { 37 | int64_t result = 0; 38 | if (absl::SimpleAtoi(j.get_ref(), &result)) { 39 | return std::make_pair(result, JsonParserResultDetail::OK); 40 | } else { 41 | return std::make_pair(std::nullopt, JsonParserResultDetail::INVALID_VALUE); 42 | } 43 | } 44 | return std::make_pair(std::nullopt, JsonParserResultDetail::TYPE_ERROR); 45 | } 46 | 47 | template <> 48 | std::pair, JsonParserResultDetail> 49 | JsonValueAs(const JsonObject& j) { 50 | if (j.is_number()) { 51 | return std::make_pair(j.get(), JsonParserResultDetail::OK); 52 | } else if (j.is_string()) { 53 | uint64_t result = 0; 54 | if (absl::SimpleAtoi(j.get_ref(), &result)) { 55 | return std::make_pair(result, JsonParserResultDetail::OK); 56 | } else { 57 | return std::make_pair(std::nullopt, JsonParserResultDetail::INVALID_VALUE); 58 | } 59 | } 60 | return std::make_pair(std::nullopt, JsonParserResultDetail::TYPE_ERROR); 61 | } 62 | 63 | template <> 64 | std::pair, JsonParserResultDetail> 65 | JsonValueAs(const JsonObject& j) { 66 | if (j.is_string()) { 67 | return std::make_pair(std::string_view(j.get_ref()), 68 | JsonParserResultDetail::OK); 69 | } 70 | return std::make_pair(std::nullopt, JsonParserResultDetail::TYPE_ERROR); 71 | } 72 | 73 | template <> 74 | std::pair, JsonParserResultDetail> 75 | JsonValueAs(const JsonObject& j) { 76 | if (j.is_string()) { 77 | return std::make_pair(j.get_ref(), JsonParserResultDetail::OK); 78 | } 79 | return std::make_pair(std::nullopt, JsonParserResultDetail::TYPE_ERROR); 80 | } 81 | 82 | template <> 83 | std::pair, JsonParserResultDetail> JsonValueAs(const JsonObject& j) { 84 | if (j.is_boolean()) { 85 | return std::make_pair(j.get(), JsonParserResultDetail::OK); 86 | } 87 | if (j.is_string()) { 88 | const std::string& v = j.get_ref(); 89 | if (v == "true") { 90 | return std::make_pair(true, JsonParserResultDetail::OK); 91 | } else if (v == "false") { 92 | return std::make_pair(false, JsonParserResultDetail::OK); 93 | } else { 94 | return std::make_pair(std::nullopt, JsonParserResultDetail::INVALID_VALUE); 95 | } 96 | } 97 | return std::make_pair(std::nullopt, JsonParserResultDetail::TYPE_ERROR); 98 | } 99 | 100 | template <> 101 | std::pair>, JsonParserResultDetail> 102 | JsonValueAs>(const JsonObject& j) { 103 | std::pair>, JsonParserResultDetail> values = 104 | std::make_pair(std::nullopt, JsonParserResultDetail::OK); 105 | if (j.is_array()) { 106 | for (const auto& elt : j) { 107 | if (!elt.is_string()) { 108 | values.first = std::nullopt; 109 | values.second = JsonParserResultDetail::TYPE_ERROR; 110 | return values; 111 | } 112 | if (!values.first.has_value()) { 113 | values.first = std::vector(); 114 | } 115 | values.first->emplace_back(elt.get_ref()); 116 | } 117 | return values; 118 | } 119 | values.second = JsonParserResultDetail::TYPE_ERROR; 120 | return values; 121 | } 122 | 123 | template <> 124 | std::pair, JsonParserResultDetail> 125 | JsonValueAs(const JsonObject& j) { 126 | if (j.is_object()) { 127 | return std::make_pair(j.get(), JsonParserResultDetail::OK); 128 | } 129 | return std::make_pair(std::nullopt, JsonParserResultDetail::TYPE_ERROR); 130 | } 131 | 132 | bool JsonArrayIterate(const JsonObject& j, std::string_view field, 133 | const std::function& visitor) { 134 | auto it = j.find(field); 135 | if (it == j.end()) { 136 | return true; 137 | } 138 | if (!it.value().is_array()) { 139 | return false; 140 | } 141 | for (const auto& elt : it.value().items()) { 142 | if (!visitor(elt.value())) { 143 | return false; 144 | } 145 | } 146 | return true; 147 | } 148 | 149 | bool JsonObjectIterate(const JsonObject& j, std::string_view field, 150 | const std::function& visitor) { 151 | auto it = j.find(field); 152 | if (it == j.end()) { 153 | return true; 154 | } 155 | if (!it.value().is_object()) { 156 | return false; 157 | } 158 | for (const auto& elt : it.value().items()) { 159 | auto json_value = JsonValueAs(elt.key()); 160 | if (json_value.second != JsonParserResultDetail::OK) { 161 | return false; 162 | } 163 | if (!visitor(json_value.first.value())) { 164 | return false; 165 | } 166 | } 167 | return true; 168 | } 169 | 170 | bool JsonObjectIterate(const JsonObject& j, const std::function& visitor) { 171 | for (const auto& elt : j.items()) { 172 | auto json_value = JsonValueAs(elt.key()); 173 | if (json_value.second != JsonParserResultDetail::OK) { 174 | return false; 175 | } 176 | if (!visitor(json_value.first.value())) { 177 | return false; 178 | } 179 | } 180 | return true; 181 | } 182 | 183 | } // namespace Common 184 | } // namespace Wasm 185 | -------------------------------------------------------------------------------- /extensions/common/wasm/json_util.h: -------------------------------------------------------------------------------- 1 | /* Copyright 2020 Istio Authors. All Rights Reserved. 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | #pragma once 17 | 18 | #include 19 | 20 | #include "extensions/common/wasm/nlohmann_json.hpp" 21 | 22 | /** 23 | * Utilities for working with JSON without exceptions. 24 | */ 25 | namespace Wasm { 26 | namespace Common { 27 | 28 | using JsonObject = ::nlohmann::json; 29 | 30 | enum JsonParserResultDetail { 31 | UNKNOWN, 32 | OK, 33 | OUT_OF_RANGE, 34 | TYPE_ERROR, 35 | INVALID_VALUE, 36 | }; 37 | 38 | std::optional JsonParse(std::string_view str); 39 | 40 | template 41 | std::pair, JsonParserResultDetail> JsonValueAs(const JsonObject&) { 42 | static_assert(true, "Unsupported Type"); 43 | } 44 | 45 | template <> 46 | std::pair, JsonParserResultDetail> 47 | JsonValueAs(const JsonObject& j); 48 | 49 | template <> 50 | std::pair, JsonParserResultDetail> 51 | JsonValueAs(const JsonObject& j); 52 | 53 | template <> 54 | std::pair, JsonParserResultDetail> JsonValueAs(const JsonObject& j); 55 | 56 | template <> 57 | std::pair, JsonParserResultDetail> 58 | JsonValueAs(const JsonObject& j); 59 | 60 | template <> 61 | std::pair, JsonParserResultDetail> JsonValueAs(const JsonObject& j); 62 | 63 | template <> 64 | std::pair, JsonParserResultDetail> 65 | JsonValueAs(const JsonObject& j); 66 | 67 | template <> 68 | std::pair>, JsonParserResultDetail> 69 | JsonValueAs>(const JsonObject& j); 70 | 71 | template class JsonGetField { 72 | public: 73 | JsonGetField(const JsonObject& j, std::string_view field); 74 | const JsonParserResultDetail& detail() { return detail_; } 75 | T value() { return object_; } 76 | T value_or(T v) { 77 | if (detail_ != JsonParserResultDetail::OK) 78 | return v; 79 | else 80 | return object_; 81 | }; 82 | 83 | private: 84 | JsonParserResultDetail detail_; 85 | T object_; 86 | }; 87 | 88 | template JsonGetField::JsonGetField(const JsonObject& j, std::string_view field) { 89 | auto it = j.find(field); 90 | if (it == j.end()) { 91 | detail_ = JsonParserResultDetail::OUT_OF_RANGE; 92 | return; 93 | } 94 | auto value = JsonValueAs(it.value()); 95 | detail_ = value.second; 96 | if (value.first.has_value()) { 97 | object_ = value.first.value(); 98 | } 99 | } 100 | 101 | // Iterate over an optional array field. 102 | // Returns false if set and not an array, or any of the visitor calls returns 103 | // false. 104 | bool JsonArrayIterate(const JsonObject& j, std::string_view field, 105 | const std::function& visitor); 106 | 107 | // Iterate over an optional object field key set. 108 | // Returns false if set and not an object, or any of the visitor calls returns 109 | // false. 110 | bool JsonObjectIterate(const JsonObject& j, std::string_view field, 111 | const std::function& visitor); 112 | bool JsonObjectIterate(const JsonObject& j, const std::function& visitor); 113 | 114 | } // namespace Common 115 | } // namespace Wasm 116 | -------------------------------------------------------------------------------- /extensions/grpc_logging/BUILD: -------------------------------------------------------------------------------- 1 | load("@proxy_wasm_cpp_sdk//bazel:defs.bzl", "proxy_wasm_cc_binary") 2 | load("//bazel:wasm.bzl", "declare_wasm_image_targets") 3 | 4 | proxy_wasm_cc_binary( 5 | name = "grpc_logging.wasm", 6 | srcs = [ 7 | "plugin.cc", 8 | "plugin.h", 9 | ], 10 | deps = [ 11 | ":config_cc_proto", 12 | ":log_cc_proto", 13 | "@com_google_absl//absl/strings", 14 | "@proxy_wasm_cpp_sdk//:proxy_wasm_intrinsics_lite", 15 | ], 16 | ) 17 | 18 | cc_proto_library( 19 | name = "config_cc_proto", 20 | deps = [":config_proto"], 21 | ) 22 | 23 | proto_library( 24 | name = "config_proto", 25 | srcs = ["config.proto"], 26 | ) 27 | 28 | cc_proto_library( 29 | name = "log_cc_proto", 30 | deps = [":log_proto"], 31 | ) 32 | 33 | proto_library( 34 | name = "log_proto", 35 | srcs = ["log.proto"], 36 | deps = [ 37 | "@com_google_protobuf//:duration_proto", 38 | "@com_google_protobuf//:timestamp_proto", 39 | ], 40 | ) 41 | 42 | declare_wasm_image_targets( 43 | name = "grpc_logging", 44 | wasm_file = ":grpc_logging.wasm", 45 | ) 46 | -------------------------------------------------------------------------------- /extensions/grpc_logging/README.md: -------------------------------------------------------------------------------- 1 | # gRPC Logging Wasm Extension 2 | 3 | This is a sample gRPC logging Wasm extension, which batches and sends [HTTP request access log](./log.proto) to a gRPC logging service. 4 | -------------------------------------------------------------------------------- /extensions/grpc_logging/config.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package istio_ecosystem.wasm_extensions.grpc_logging; 4 | 5 | message PluginConfig { 6 | // logging service address. 7 | string logging_service = 1; 8 | } 9 | -------------------------------------------------------------------------------- /extensions/grpc_logging/log.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package istio_ecosystem.wasm_extensions.grpc_logging; 4 | 5 | import "google/protobuf/timestamp.proto"; 6 | import "google/protobuf/duration.proto"; 7 | 8 | service LoggingService { 9 | rpc WriteLog(WriteLogRequest) returns (WriteLogResponse); 10 | } 11 | 12 | message WriteLogRequest { 13 | message LogEntry { 14 | string source_address = 1; 15 | string destination_address = 2; 16 | string destination_workload = 3; 17 | string destination_namespace = 4; 18 | string host = 5; 19 | string path = 6; 20 | string user_agent = 7; 21 | string referer = 8; 22 | string request_id = 9; 23 | uint32 response_code = 12; 24 | google.protobuf.Timestamp timestamp = 10; 25 | google.protobuf.Duration latency = 11; 26 | } 27 | 28 | repeated LogEntry log_entries = 1; 29 | } 30 | 31 | message WriteLogResponse {} 32 | -------------------------------------------------------------------------------- /extensions/grpc_logging/plugin.cc: -------------------------------------------------------------------------------- 1 | #include "extensions/grpc_logging/plugin.h" 2 | 3 | #include "absl/strings/str_cat.h" 4 | #include "google/protobuf/util/json_util.h" 5 | #include "google/protobuf/util/time_util.h" 6 | 7 | using google::protobuf::util::JsonParseOptions; 8 | using google::protobuf::util::Status; 9 | using istio_ecosystem::wasm_extensions::grpc_logging::PluginConfig; 10 | using istio_ecosystem::wasm_extensions::grpc_logging::WriteLogRequest; 11 | 12 | static RegisterContextFactory register_gRPCLogging( 13 | CONTEXT_FACTORY(PluginContext), ROOT_FACTORY(PluginRootContext)); 14 | 15 | PluginRootContext::PluginRootContext(uint32_t id, std::string_view root_id) 16 | : RootContext(id, root_id) { 17 | cur_log_req_ = std::make_unique(); 18 | log_entry_count_ = 0; 19 | } 20 | 21 | bool PluginRootContext::onConfigure(size_t configuration_size) { 22 | // Parse configuration JSON string. 23 | std::string configuration = "{}"; 24 | if (configuration_size > 0) { 25 | auto configuration_data = getBufferBytes( 26 | WasmBufferType::PluginConfiguration, 0, configuration_size); 27 | configuration = configuration_data->toString(); 28 | } 29 | 30 | JsonParseOptions json_options; 31 | json_options.ignore_unknown_fields = true; 32 | PluginConfig config; 33 | Status status = JsonStringToMessage(configuration, &config, json_options); 34 | if (!status.ok()) { 35 | LOG_WARN( 36 | absl::StrCat("cannot parse logging plugin configuration JSON string ", 37 | configuration, ", ", status.message().ToString())); 38 | return false; 39 | } 40 | 41 | if (config.logging_service().empty()) { 42 | LOG_WARN(absl::StrCat( 43 | "logging service has to be provided in the logging plugin config ", 44 | configuration)); 45 | return false; 46 | } 47 | 48 | // Init gRPC call configuration. 49 | logging_service_address_ = config.logging_service(); 50 | GrpcService grpc_service; 51 | grpc_service.mutable_google_grpc()->set_target_uri(logging_service_address_); 52 | grpc_service.SerializeToString(&grpc_service_); 53 | 54 | // Init gRPC callbacks. 55 | success_callback_ = [this](size_t) { 56 | LOG_DEBUG("successfully sent loggin request"); 57 | in_flight_export_call_ -= 1; 58 | if (in_flight_export_call_ < 0) { 59 | LOG_WARN("in flight report call should not be negative"); 60 | } 61 | if (in_flight_export_call_ <= 0 && is_on_done_) { 62 | // All works have been finished. The plugin is safe to be destroyed. 63 | proxy_done(); 64 | } 65 | }; 66 | 67 | failure_callback_ = [this](GrpcStatus status) { 68 | LOG_WARN(absl::StrCat( 69 | "Logging call error: ", std::to_string(static_cast(status)), 70 | getStatus().second->toString())); 71 | in_flight_export_call_ -= 1; 72 | if (in_flight_export_call_ < 0) { 73 | LOG_WARN("in flight report call should not be negative"); 74 | } 75 | if (in_flight_export_call_ <= 0 && is_on_done_) { 76 | proxy_done(); 77 | } 78 | }; 79 | 80 | // Start timer, which will trigger log report every 10s. 81 | proxy_set_tick_period_milliseconds(10000 /* milliseconds */); 82 | 83 | return true; 84 | } 85 | 86 | bool PluginRootContext::onDone() { 87 | if (req_buffer_.empty() && in_flight_export_call_ == 0) { 88 | // returning true to signal that the plugin still has pending work to be 89 | // done. 90 | return true; 91 | } 92 | // Flush out all log entries 93 | flushLogBuffer(); 94 | sendLogRequest(/* ondone */ true); 95 | // returning false to signal that the plugin has finished all the works and is 96 | // safe to be destroyed. 97 | return false; 98 | } 99 | 100 | void PluginRootContext::onTick() { 101 | // Flush out all log entries 102 | flushLogBuffer(); 103 | if (req_buffer_.empty()) { 104 | return; 105 | } 106 | sendLogRequest(/* ondone */ false); 107 | } 108 | 109 | void PluginRootContext::addLogEntry(PluginContext* stream) { 110 | auto* log_entries = cur_log_req_->mutable_log_entries(); 111 | auto* new_entry = log_entries->Add(); 112 | 113 | // Add log labels. Note the following logic assumes this extension 114 | // is running at a server sidecar. 115 | 116 | // Workload attributes. 117 | getValue({"source", "address"}, new_entry->mutable_source_address()); 118 | getValue({"destination", "address"}, 119 | new_entry->mutable_destination_address()); 120 | getValue({"node", "metadata", "WORKLOAD_NAME"}, 121 | new_entry->mutable_destination_workload()); 122 | getValue({"node", "metadata", "NAMESPACE"}, 123 | new_entry->mutable_destination_namespace()); 124 | 125 | // Request attributes. 126 | int64_t response_code, timestamp, duration; 127 | getValue({"request", "time"}, ×tamp); 128 | getValue({"request", "duration"}, &duration); 129 | getValue({"request", "id"}, new_entry->mutable_request_id()); 130 | getValue({"request", "host"}, new_entry->mutable_host()); 131 | getValue({"request", "url_path"}, new_entry->mutable_path()); 132 | getValue({"response", "code"}, &response_code); 133 | getValue({"request", "referer"}, new_entry->mutable_referer()); 134 | getValue({"request", "user_agent"}, new_entry->mutable_user_agent()); 135 | 136 | *new_entry->mutable_timestamp() = 137 | google::protobuf::util::TimeUtil::NanosecondsToTimestamp(timestamp); 138 | *new_entry->mutable_latency() = 139 | google::protobuf::util::TimeUtil::NanosecondsToDuration(duration); 140 | new_entry->set_response_code(response_code); 141 | 142 | log_entry_count_ += 1; 143 | if (log_entry_count_ >= 500) { 144 | flushLogBuffer(); 145 | } 146 | } 147 | 148 | void PluginRootContext::flushLogBuffer() { 149 | if (log_entry_count_ <= 0) { 150 | return; 151 | } 152 | auto new_log_req = std::make_unique(); 153 | cur_log_req_.swap(new_log_req); 154 | req_buffer_.emplace_back(std::move(new_log_req)); 155 | log_entry_count_ = 0; 156 | } 157 | 158 | void PluginRootContext::sendLogRequest(bool ondone) { 159 | is_on_done_ = ondone; 160 | HeaderStringPairs initial_metadata; 161 | 162 | std::vector>::iterator itr = 163 | req_buffer_.begin(); 164 | while (itr != req_buffer_.end()) { 165 | const auto& req = *itr; 166 | auto result = grpcSimpleCall( 167 | grpc_service_, 168 | /* service name */ 169 | "istio_ecosystem.wasm_extensions.grpc_logging.LoggingService", 170 | /* method name */ "WriteLog", initial_metadata, *req, 171 | /* time out in milliseconds */ 5000, success_callback_, 172 | failure_callback_); 173 | if (result != WasmResult::Ok) { 174 | LOG_WARN("failed to make stackdriver logging export call"); 175 | break; 176 | } 177 | in_flight_export_call_ += 1; 178 | // delete req_buffer_ req data; 179 | itr = req_buffer_.erase(itr); 180 | } 181 | } 182 | 183 | void PluginContext::onLog() { rootContext()->addLogEntry(this); } 184 | -------------------------------------------------------------------------------- /extensions/grpc_logging/plugin.h: -------------------------------------------------------------------------------- 1 | #include "extensions/grpc_logging/config.pb.h" 2 | #include "extensions/grpc_logging/log.pb.h" 3 | #include "proxy_wasm_intrinsics_lite.h" 4 | 5 | class PluginContext; 6 | 7 | // gRPC logging plugin root context. 8 | class PluginRootContext : public RootContext { 9 | public: 10 | PluginRootContext(uint32_t id, std::string_view root_id); 11 | ~PluginRootContext() override = default; 12 | 13 | bool onConfigure(size_t) override; 14 | void onTick() override; 15 | bool onDone() override; 16 | 17 | void addLogEntry(PluginContext *stream_context); 18 | void flushLogBuffer(); 19 | void sendLogRequest(bool ondone); 20 | 21 | private: 22 | std::string logging_service_address_; 23 | 24 | // Log request that is being written currently. 25 | std::unique_ptr< 26 | istio_ecosystem::wasm_extensions::grpc_logging::WriteLogRequest> 27 | cur_log_req_; 28 | 29 | // Count of buffered log entries. 30 | int log_entry_count_; 31 | 32 | // Log request that are buffered to be sent. 33 | std::vector> 35 | req_buffer_; 36 | 37 | // gRPC service string contains gRPC call configuration for Wasm gRPC call. 38 | std::string grpc_service_; 39 | 40 | // gRPC callback handler functions. 41 | std::function success_callback_; 42 | std::function failure_callback_; 43 | 44 | // Record in flight export calls. When ondone is triggered, export call needs 45 | // to be zero before calling proxy_done. 46 | int in_flight_export_call_ = 0; 47 | 48 | // Indicates if the current exporting is triggered by root context onDone. If 49 | // this is true, gRPC callback needs to call proxy_done to indicate that async 50 | // call finishes. 51 | bool is_on_done_ = false; 52 | }; 53 | 54 | // Per-stream context. 55 | class PluginContext : public Context { 56 | public: 57 | explicit PluginContext(uint32_t id, ::RootContext *root) 58 | : Context(id, root) {} 59 | 60 | void onLog() override; 61 | 62 | private: 63 | inline PluginRootContext *rootContext() { 64 | return dynamic_cast(this->root()); 65 | } 66 | }; 67 | -------------------------------------------------------------------------------- /extensions/local_rate_limit/BUILD: -------------------------------------------------------------------------------- 1 | load("@proxy_wasm_cpp_sdk//bazel:defs.bzl", "proxy_wasm_cc_binary") 2 | load("//bazel:wasm.bzl", "declare_wasm_image_targets") 3 | 4 | proxy_wasm_cc_binary( 5 | name = "local_rate_limit.wasm", 6 | srcs = [ 7 | "bucket.cc", 8 | "bucket.h", 9 | "plugin.cc", 10 | "plugin.h", 11 | ], 12 | deps = [ 13 | "@com_google_absl//absl/strings", 14 | "//extensions/common/wasm:json_util", 15 | ], 16 | ) 17 | 18 | declare_wasm_image_targets( 19 | name = "local_rate_limit", 20 | wasm_file = ":local_rate_limit.wasm", 21 | ) 22 | -------------------------------------------------------------------------------- /extensions/local_rate_limit/README.md: -------------------------------------------------------------------------------- 1 | # Local Rate Limit Wasm Extensions 2 | 3 | > **Note**: This is a very basic local rate limit Wasm filter. For sophisticated production use case, please consider using [Envoy local rate limit filter](https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_filters/local_rate_limit_filter). 4 | 5 | Before going through this guide, please read official Istio document about [Wasm module remote load](https://istio.io/latest/docs/ops/configuration/extensibility/wasm-module-distribution/). 6 | 7 | ## Deploy Local Rate Limit Wasm Extension 8 | 9 | --- 10 | 11 | In the following guide we will configure Istio proxy to download and apply a local rate limit filter. 12 | 13 | Two `EnvoyFilter` resources will be applied, to inject local rate limit filter into HTTP filter chain. 14 | For example, [this configuration](./config/gateway-filter.yaml) injects the rate limit filter to `gateway`. 15 | 16 | The first `EnvoyFilter` will inject an HTTP filter into gateway proxies. The second `EnvoyFilter` resource provides configuration for the filter. 17 | 18 | After applying the filter, gateway should start enforce rate limiting. 19 | Use `productpage` app as an example, to test that the filter works, 20 | 21 | ```console 22 | foo@bar:~$ for i in {1..50}; do curl -I http://x.x.x.x/productpage; done 23 | ... 24 | HTTP/1.1 200 OK 25 | content-type: text/html; charset=utf-8 26 | content-length: 5183 27 | server: istio-envoy 28 | date: Sat, 27 Feb 2021 22:12:46 GMT 29 | x-envoy-upstream-service-time: 34 30 | ... 31 | HTTP/1.1 429 Too Many Requests 32 | date: Sat, 27 Feb 2021 22:12:47 GMT 33 | server: istio-envoy 34 | transfer-encoding: chunked 35 | ... 36 | ``` 37 | 38 | ## Configuration Reference 39 | 40 | --- 41 | 42 | The following proto message describes the schema of auth filter configuration. 43 | 44 | ```protobuf 45 | 46 | // PluginConfig defines local rate limit configuration. 47 | // Specifically it defines a token bucket for shared by all Envoy workers. 48 | message PluginConfig { 49 | // max tokens of the toek bucket. 50 | uint64 max_tokens = 1; 51 | 52 | // number of tokens to add at each refill. 53 | uint64 tokens_per_refill = 2; 54 | 55 | // interval in seconds to refill the token bucket. 56 | uint64 refill_interval_sec = 3; 57 | } 58 | ``` 59 | 60 | ## Feature Request and Customization 61 | 62 | --- 63 | 64 | If you have any feature request or bug report, please open an issue in this repo. 65 | 66 | It is highly recommended to customize the extension according to your needs. 67 | Please take a look at [Wasm extension C++ development guide](../doc/write-a-wasm-extension-with-cpp.md) for more information about how to write your own extension. 68 | -------------------------------------------------------------------------------- /extensions/local_rate_limit/bucket.cc: -------------------------------------------------------------------------------- 1 | #include "extensions/local_rate_limit/bucket.h" 2 | 3 | #include "absl/strings/str_cat.h" 4 | 5 | namespace { 6 | 7 | const int maxGetTokenRetry = 20; 8 | 9 | // Key for token bucket shared data. 10 | constexpr char localRateLimitTokenBucket[] = 11 | "wasm_local_rate_limit.token_bucket"; 12 | 13 | // Key for token bucket last updated time. 14 | constexpr char localRateLimitLastRefilled[] = 15 | "wasm_local_rate_limit.last_refilled"; 16 | 17 | } // namespace 18 | 19 | bool getToken() { 20 | WasmDataPtr token_bucket_data; 21 | uint32_t cas; 22 | for (int i = 0; i < maxGetTokenRetry; i++) { 23 | // Get the current token left with cas (compare-and-swap), which will be 24 | // used in the set call below. 25 | if (WasmResult::Ok != 26 | getSharedData(localRateLimitTokenBucket, &token_bucket_data, &cas)) { 27 | return false; 28 | } 29 | uint64_t token_left = 30 | *reinterpret_cast(token_bucket_data->data()); 31 | 32 | // If there is no token left, returns false so that request gets 429. 33 | if (token_left == 0) { 34 | return false; 35 | } 36 | 37 | // If there is token left, subtract it by 1, and try set it with cas. 38 | // If token bucket set fails because of cas mismatch, which indicates the 39 | // bucket is updated by other VMs, retry the whole process. 40 | token_left -= 1; 41 | auto res = setSharedData( 42 | localRateLimitTokenBucket, 43 | {reinterpret_cast(&token_left), sizeof(token_left)}, cas); 44 | if (res == WasmResult::Ok) { 45 | // token bucket is updated successfully, returns true and let the request 46 | // go through. 47 | return true; 48 | } 49 | if (res == WasmResult::CasMismatch) { 50 | continue; 51 | } 52 | return false; 53 | } 54 | 55 | // We tried to get token for more than `maxGetTokenRetry` times. Return true 56 | // and let the request through. 57 | return true; 58 | } 59 | 60 | void refillToken(uint64_t tokens_per_refill, uint64_t refill_interval_nanosec, 61 | uint64_t max_tokens) { 62 | // Get last refill time, if it is less than refill interval, which indicates 63 | // the bucket is going to be refilled or has already been refilled by other 64 | // VMs. 65 | // TODO(bianpengyuan): simplify this by designating one VM to refill the 66 | // bucket when https://github.com/proxy-wasm/proxy-wasm-cpp-host/issues/135 is 67 | // done. 68 | uint32_t last_update_cas; 69 | WasmDataPtr last_update_data; 70 | auto result = getSharedData(localRateLimitLastRefilled, &last_update_data, 71 | &last_update_cas); 72 | if (result != WasmResult::Ok) { 73 | LOG_DEBUG(absl::StrCat( 74 | "failed to get last update time of the local rate limit token bucket ", 75 | toString(result))); 76 | return; 77 | } 78 | uint64_t last_update = 79 | *reinterpret_cast(last_update_data->data()); 80 | uint64_t now = getCurrentTimeNanoseconds(); 81 | if (now - last_update < refill_interval_nanosec) { 82 | return; 83 | } 84 | 85 | // Otherwise, try set last updated time. If updated failed because of cas 86 | // mismatch, the bucket is going to be refilled by other VMs. 87 | auto res = setSharedData(localRateLimitLastRefilled, 88 | {reinterpret_cast(&now), sizeof(now)}, 89 | last_update_cas); 90 | if (res == WasmResult::CasMismatch) { 91 | return; 92 | } 93 | if (res != WasmResult::Ok) { 94 | LOG_DEBUG("failed to set local rate limit token bucket last update time"); 95 | return; 96 | } 97 | 98 | // Refill token bucket. 99 | WasmDataPtr token_bucket_data; 100 | uint32_t cas; 101 | while (true) { 102 | // Get token left with cas. 103 | if (WasmResult::Ok != 104 | getSharedData(localRateLimitTokenBucket, &token_bucket_data, &cas)) { 105 | LOG_DEBUG("failed to get current local rate limit token bucket"); 106 | break; 107 | } 108 | uint64_t token_left = 109 | *reinterpret_cast(token_bucket_data->data()); 110 | 111 | // Refill tokens, and update bucket with cas. If update failed because of 112 | // cas mismatch, retry refilling. 113 | token_left += tokens_per_refill; 114 | if (token_left > max_tokens) { 115 | token_left = max_tokens; 116 | } 117 | auto res = setSharedData( 118 | localRateLimitTokenBucket, 119 | {reinterpret_cast(&token_left), sizeof(token_left)}, cas); 120 | if (res == WasmResult::CasMismatch) { 121 | continue; 122 | } 123 | if (res != WasmResult::Ok) { 124 | LOG_DEBUG("failed to refill local rate limit token bucket"); 125 | } 126 | break; 127 | } 128 | } 129 | 130 | bool initializeTokenBucket(uint64_t initial_tokens) { 131 | // Check if the bucket is already initialized. 132 | WasmDataPtr last_update_data; 133 | if (WasmResult::Ok == 134 | getSharedData(localRateLimitLastRefilled, &last_update_data)) { 135 | return true; 136 | } 137 | // If not yet initialized, set last update time to 0 and tokens left to 138 | // initial_tokens. 139 | uint64_t init_last_update = 0; 140 | auto res = setSharedData(localRateLimitLastRefilled, 141 | {reinterpret_cast(&init_last_update), 142 | sizeof(init_last_update)}); 143 | if (res == WasmResult::CasMismatch) { 144 | return true; 145 | } 146 | if (res != WasmResult::Ok) { 147 | LOG_DEBUG("failed to set local rate limit token bucket last update time"); 148 | return false; 149 | } 150 | 151 | res = setSharedData(localRateLimitTokenBucket, 152 | {reinterpret_cast(&initial_tokens), 153 | sizeof(initial_tokens)}); 154 | if (res != WasmResult::Ok) { 155 | LOG_DEBUG("failed to initialize token bucket"); 156 | return false; 157 | } 158 | return true; 159 | } 160 | -------------------------------------------------------------------------------- /extensions/local_rate_limit/bucket.h: -------------------------------------------------------------------------------- 1 | #include "proxy_wasm_intrinsics.h" 2 | 3 | // getToken try fetch a token from the local rate limit token buckets. 4 | // Returns false if no token left, or any error returns when accessing the token 5 | // bucket. 6 | bool getToken(); 7 | 8 | // Refill token bucket. 9 | void refillToken(uint64_t tokens_per_refill, uint64_t refill_interval_nanosec, 10 | uint64_t max_tokens); 11 | 12 | // Initialize token buckets. 13 | bool initializeTokenBucket(uint64_t initial_tokens); 14 | -------------------------------------------------------------------------------- /extensions/local_rate_limit/config/gateway-filter.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: networking.istio.io/v1alpha3 2 | kind: EnvoyFilter 3 | metadata: 4 | name: local-rate-limit 5 | namespace: istio-system 6 | spec: 7 | configPatches: 8 | - applyTo: HTTP_FILTER 9 | match: 10 | context: GATEWAY 11 | listener: 12 | filterChain: 13 | filter: 14 | name: envoy.http_connection_manager 15 | patch: 16 | operation: INSERT_BEFORE 17 | value: 18 | name: istio.local_rate_limit 19 | config_discovery: 20 | config_source: 21 | ads: {} 22 | initial_fetch_timeout: 0s # wait indefinitely to prevent bad Wasm fetch 23 | type_urls: [ "type.googleapis.com/envoy.extensions.filters.http.wasm.v3.Wasm"] 24 | --- 25 | apiVersion: networking.istio.io/v1alpha3 26 | kind: EnvoyFilter 27 | metadata: 28 | name: local-rate-limit-config 29 | namespace: istio-system 30 | spec: 31 | configPatches: 32 | - applyTo: EXTENSION_CONFIG 33 | match: 34 | context: GATEWAY 35 | patch: 36 | operation: ADD 37 | value: 38 | name: istio.local_rate_limit 39 | typed_config: 40 | '@type': type.googleapis.com/udpa.type.v1.TypedStruct 41 | type_url: type.googleapis.com/envoy.extensions.filters.http.wasm.v3.Wasm 42 | value: 43 | config: 44 | configuration: 45 | '@type': type.googleapis.com/google.protobuf.StringValue 46 | value: | 47 | { 48 | "max_tokens": 50, 49 | "tokens_per_refill": 20, 50 | "refill_interval_sec": 60 51 | } 52 | vm_config: 53 | vm_id: local_rate_limit 54 | code: 55 | remote: 56 | http_uri: 57 | uri: https://storage.googleapis.com/istio-ecosystem/wasm-extensions/local-rate-limit/test.wasm 58 | runtime: envoy.wasm.runtime.v8 59 | -------------------------------------------------------------------------------- /extensions/local_rate_limit/plugin.cc: -------------------------------------------------------------------------------- 1 | #include "extensions/local_rate_limit/plugin.h" 2 | 3 | #include "absl/strings/str_cat.h" 4 | #include "extensions/common/wasm/json_util.h" 5 | #include "extensions/local_rate_limit/bucket.h" 6 | 7 | using ::nlohmann::json; 8 | using ::Wasm::Common::JsonArrayIterate; 9 | using ::Wasm::Common::JsonGetField; 10 | using ::Wasm::Common::JsonObjectIterate; 11 | using ::Wasm::Common::JsonValueAs; 12 | 13 | namespace { 14 | 15 | // tooManyRequest returns a 429 response code. 16 | void tooManyRequest() { 17 | sendLocalResponse(429, "Too many requests", "rate_limited", {}); 18 | } 19 | 20 | bool shouldRateLimit() { 21 | /* 22 | Add any rate limit condition here, such as host, path, etc. 23 | */ 24 | return true; 25 | } 26 | 27 | } // namespace 28 | 29 | static RegisterContextFactory register_LocalRateLimit( 30 | CONTEXT_FACTORY(PluginContext), ROOT_FACTORY(PluginRootContext)); 31 | 32 | bool PluginRootContext::onConfigure(size_t configuration_size) { 33 | if (!parseConfiguration(configuration_size)) { 34 | return false; 35 | } 36 | 37 | // Initialize token bucket. 38 | if (!initializeTokenBucket(tokens_per_refill_)) { 39 | return false; 40 | } 41 | 42 | // Start ticker, which will trigger token bucket refill. 43 | proxy_set_tick_period_milliseconds(refill_interval_nanosec_ / 1000000); 44 | 45 | return true; 46 | } 47 | 48 | void PluginRootContext::onTick() { 49 | refillToken(tokens_per_refill_, refill_interval_nanosec_, max_tokens_); 50 | } 51 | 52 | FilterHeadersStatus PluginContext::onRequestHeaders(uint32_t, bool) { 53 | if (shouldRateLimit() && !getToken()) { 54 | tooManyRequest(); 55 | return FilterHeadersStatus::StopIteration; 56 | } 57 | return FilterHeadersStatus::Continue; 58 | } 59 | 60 | bool PluginRootContext::parseConfiguration(size_t configuration_size) { 61 | auto configuration_data = getBufferBytes(WasmBufferType::PluginConfiguration, 62 | 0, configuration_size); 63 | // Parse configuration JSON string. 64 | auto result = ::Wasm::Common::JsonParse(configuration_data->view()); 65 | if (!result.has_value()) { 66 | LOG_WARN(absl::StrCat("cannot parse plugin configuration JSON string: ", 67 | configuration_data->view())); 68 | return false; 69 | } 70 | 71 | // j is a JsonObject holds configuration data 72 | auto j = result.value(); 73 | 74 | // Get token bucket configuration 75 | // { 76 | // "max_tokens": 100, 77 | // "tokens_per_fill": 50, 78 | // "fill_interval_seconds": 10 79 | // } 80 | // Parse and get max tokens. 81 | auto it = j.find("max_tokens"); 82 | if (it != j.end()) { 83 | auto max_tokens_val = JsonValueAs(it.value()); 84 | if (max_tokens_val.second != Wasm::Common::JsonParserResultDetail::OK) { 85 | LOG_WARN(absl::StrCat( 86 | "cannot parse max token in plugin configuration JSON string: ", 87 | configuration_data->view())); 88 | return false; 89 | } 90 | max_tokens_ = max_tokens_val.first.value(); 91 | } else { 92 | LOG_WARN(absl::StrCat( 93 | "max token must be provided in plugin configuration JSON string: ", 94 | configuration_data->view())); 95 | return false; 96 | } 97 | 98 | // Parse and get tokens per refill. 99 | it = j.find("tokens_per_refill"); 100 | if (it != j.end()) { 101 | auto tokens_per_refill_val = JsonValueAs(it.value()); 102 | if (tokens_per_refill_val.second != 103 | Wasm::Common::JsonParserResultDetail::OK) { 104 | LOG_WARN( 105 | absl::StrCat("cannot parse tokens per refill in plugin configuration " 106 | "JSON string: ", 107 | configuration_data->view())); 108 | return false; 109 | } 110 | tokens_per_refill_ = tokens_per_refill_val.first.value(); 111 | } else { 112 | LOG_WARN( 113 | absl::StrCat("tokens per refill must be provided in plugin " 114 | "configuration JSON string: ", 115 | configuration_data->view())); 116 | return false; 117 | } 118 | 119 | // Parse and get refill interval. 120 | it = j.find("refill_interval_sec"); 121 | if (it != j.end()) { 122 | auto refill_interval_sec_val = JsonValueAs(it.value()); 123 | if (refill_interval_sec_val.second != 124 | Wasm::Common::JsonParserResultDetail::OK) { 125 | LOG_WARN(absl::StrCat( 126 | "cannot parse refill interval in plugin configuration JSON string: ", 127 | configuration_data->view())); 128 | return false; 129 | } 130 | refill_interval_nanosec_ = 131 | refill_interval_sec_val.first.value() * 1000000000; 132 | } else { 133 | LOG_WARN( 134 | absl::StrCat("refill interval must be provided in plugin configuration " 135 | "JSON string: ", 136 | configuration_data->view())); 137 | return false; 138 | } 139 | 140 | return true; 141 | } 142 | -------------------------------------------------------------------------------- /extensions/local_rate_limit/plugin.h: -------------------------------------------------------------------------------- 1 | #include "proxy_wasm_intrinsics.h" 2 | 3 | class PluginRootContext : public RootContext { 4 | public: 5 | explicit PluginRootContext(uint32_t id, std::string_view root_id) 6 | : RootContext(id, root_id) {} 7 | 8 | bool onConfigure(size_t) override; 9 | 10 | // onTick will trigger token bucket refill. 11 | void onTick() override; 12 | 13 | private: 14 | bool parseConfiguration(size_t); 15 | 16 | uint64_t max_tokens_; 17 | uint64_t tokens_per_refill_; 18 | uint64_t refill_interval_nanosec_; 19 | }; 20 | 21 | class PluginContext : public Context { 22 | public: 23 | explicit PluginContext(uint32_t id, RootContext* root) : Context(id, root) {} 24 | FilterHeadersStatus onRequestHeaders(uint32_t, bool) override; 25 | 26 | private: 27 | inline PluginRootContext* rootContext() { 28 | return dynamic_cast(this->root()); 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /extensions/open_policy_agent/BUILD: -------------------------------------------------------------------------------- 1 | load("@proxy_wasm_cpp_sdk//bazel:defs.bzl", "proxy_wasm_cc_binary") 2 | load("//bazel:wasm.bzl", "declare_wasm_image_targets") 3 | 4 | proxy_wasm_cc_binary( 5 | name = "open_policy_agent.wasm", 6 | srcs = [ 7 | "cache.cc", 8 | "cache.h", 9 | "plugin.cc", 10 | "plugin.h", 11 | ], 12 | deps = [ 13 | "//extensions/common/wasm:json_util", 14 | ], 15 | ) 16 | 17 | declare_wasm_image_targets( 18 | name = "open_policy_agent", 19 | wasm_file = ":open_policy_agent.wasm", 20 | ) 21 | -------------------------------------------------------------------------------- /extensions/open_policy_agent/README.md: -------------------------------------------------------------------------------- 1 | # Open Policy Agent (OPA) Client Filter User Guide 2 | 3 | > **Note**: This open policy agent should only be used for demo purpose. You should use this extension by [customizing it](#feature-request-and-customization) according to your needs. 4 | 5 | Before going through this guide, please read official Istio document about [Wasm module distribution](https://istio.io/latest/docs/ops/configuration/extensibility/wasm-module-distribution/). 6 | 7 | ## Deploy Open Policy Agent Filter 8 | 9 | --- 10 | 11 | In the following guide we will configure Istio proxy to download and apply an OPA filter. 12 | 13 | Before applying the filter, you can deploy an OPA service in `istio-system` namespace with [this configuration](../config/opa-service.yaml), 14 | which consumes the following OPA rule: 15 | 16 | ``` 17 | default allow = false 18 | 19 | allow = true { 20 | input.request_method == "GET" 21 | input.destination_workload == "istio-ingressgateway" 22 | input.request_url_path == "/status/200" 23 | } 24 | 25 | allow = true { 26 | input.request_method == "GET" 27 | input.destination_workload == "istio-ingressgateway" 28 | input.request_url_path == "/headers" 29 | } 30 | ``` 31 | 32 | Two `EnvoyFilter` resources will be applied in order to inject OPA filter into HTTP filter chain. 33 | For example, [this configuration](./config/example-filter.yaml) injects the OPA filter to `gateway`. 34 | The OPA filter will extract information from a request stream, send a policy check request to the OPA server, 35 | and decide whether to allow the request based on the response from OPA server. 36 | It also has a LRU check cache built in, which will cache check result for configurable duration. 37 | In the check request, the following information will be included: 38 | 39 | ```json 40 | { 41 | "input": { 42 | "request_method": "GET", 43 | "input.source_principal": "spiffe://cluster.local/ns/default/sa/client", 44 | "input.destination_workload": "echo-server", 45 | "input.request_url_path": "/echo" 46 | } 47 | } 48 | ``` 49 | 50 | The first `EnvoyFilter` will inject an HTTP filter into gateway proxies. The second `EnvoyFilter` resource provides configuration for the filter. 51 | 52 | After applying the filter, gateway should start sending request to OPA server for policy check. 53 | Use `httpbin` app as an example, to test that the rule works, you can curl with and without the authorization header. 54 | For example 55 | 56 | ```console 57 | foo@bar:~$ curl -i /headers 58 | HTTP/1.1 200 OK 59 | ... 60 | foo@bar:~$ curl -i /status 61 | HTTP/1.1 403 Forbidden 62 | ... 63 | foo@bar:~$ curl -i 104.155.135.213/status/200 64 | HTTP/1.1 200 OK 65 | ... 66 | ... 67 | ``` 68 | 69 | ## Configuration Reference 70 | 71 | --- 72 | 73 | The following proto message describes the schema of OPA filter configuration. 74 | 75 | ```protobuf 76 | message PluginConfig { 77 | // Host for OPA check call. This will be used as host header. 78 | string opa_service_host = 1; 79 | 80 | // Envoy cluster for OPA HTTP call. 81 | string opa_cluster_name = 2; 82 | 83 | // Cache entry valid duration in seconds. 84 | string cache_valid_for_sec = 3; 85 | } 86 | ``` 87 | 88 | ## Feature Request and Customization 89 | 90 | --- 91 | 92 | It is recommended to customize the extension according to your needs. 93 | Please take a look at [Wasm extension C++ development guide](../doc/write-a-wasm-extension-with-cpp.md) for more information about how to write your own extension. 94 | -------------------------------------------------------------------------------- /extensions/open_policy_agent/cache.cc: -------------------------------------------------------------------------------- 1 | #include "extensions/open_policy_agent/cache.h" 2 | 3 | const uint64_t MAX_NUM_ENTRY = 1000; 4 | 5 | namespace { 6 | 7 | uint64_t computeHash(const Payload &payload) { 8 | const uint64_t kMul = static_cast(0x9ddfea08eb382d69); 9 | uint64_t h = 0; 10 | h += std::hash()(payload.source_principal) * kMul; 11 | h += std::hash()(payload.destination_workload) * kMul; 12 | h += std::hash()(payload.request_method) * kMul; 13 | h += std::hash()(payload.request_url_path) * kMul; 14 | return h; 15 | } 16 | 17 | } // namespace 18 | 19 | bool ResultCache::check(const Payload ¶m, uint64_t &hash, bool &allowed, 20 | uint64_t timestamp) { 21 | hash = computeHash(param); 22 | auto iter = result_cache_.find(hash); 23 | if (iter == result_cache_.end()) { 24 | return false; 25 | } 26 | const auto &entry = iter->second; 27 | if (entry.second + valid_for_nanosec_ > timestamp) { 28 | use(hash); 29 | allowed = entry.first; 30 | return true; 31 | } 32 | auto recent_iter = pos_.find(hash); 33 | recent_.erase(recent_iter->second); 34 | pos_.erase(hash); 35 | result_cache_.erase(hash); 36 | return false; 37 | } 38 | 39 | void ResultCache::add(const uint64_t hash, bool result, uint64_t timestamp) { 40 | use(hash); 41 | result_cache_.emplace(hash, std::make_pair(result, timestamp)); 42 | } 43 | 44 | void ResultCache::use(const uint64_t hash) { 45 | if (pos_.find(hash) != pos_.end()) { 46 | recent_.erase(pos_[hash]); 47 | } else if (recent_.size() >= MAX_NUM_ENTRY) { 48 | int old = recent_.back(); 49 | recent_.pop_back(); 50 | result_cache_.erase(old); 51 | pos_.erase(old); 52 | } 53 | recent_.push_front(hash); 54 | pos_[hash] = recent_.begin(); 55 | } 56 | -------------------------------------------------------------------------------- /extensions/open_policy_agent/cache.h: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | struct Payload { 6 | // Principal of source workload. 7 | std::string source_principal; 8 | 9 | // destination workload name. 10 | std::string destination_workload; 11 | 12 | // Request method. 13 | std::string request_method; 14 | 15 | // URL path of the request. 16 | std::string request_url_path; 17 | }; 18 | 19 | // LRU cache for OPA policy check result. 20 | class ResultCache { 21 | public: 22 | void setValidDuration(uint64_t valid_for_sec) { 23 | valid_for_nanosec_ = valid_for_sec * 1000000000; 24 | } 25 | 26 | // Check if a payload is in the cache. This will update last touched time. 27 | bool check(const Payload &payload, uint64_t &hash, bool &allowed, 28 | uint64_t timestamp); 29 | 30 | // Add an entry to check cache. 31 | void add(const uint64_t hash, bool result, uint64_t timestamp); 32 | 33 | private: 34 | void use(const uint64_t hash); 35 | 36 | uint64_t valid_for_nanosec_ = 10000000000; 37 | 38 | // LRU cache for OPA check result. 39 | std::unordered_map< 40 | uint64_t /* payload hash */, 41 | std::pair> 42 | result_cache_; 43 | std::list recent_; 44 | std::unordered_map::iterator> pos_; 45 | }; 46 | -------------------------------------------------------------------------------- /extensions/open_policy_agent/config/example-filter.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: networking.istio.io/v1alpha3 2 | kind: EnvoyFilter 3 | metadata: 4 | name: opa 5 | namespace: istio-system 6 | spec: 7 | configPatches: 8 | - applyTo: HTTP_FILTER 9 | match: 10 | context: GATEWAY 11 | listener: 12 | filterChain: 13 | filter: 14 | name: envoy.http_connection_manager 15 | patch: 16 | operation: INSERT_BEFORE 17 | value: 18 | name: istio.opa 19 | config_discovery: 20 | config_source: 21 | ads: {} 22 | initial_fetch_timeout: 0s # wait indefinitely to prevent bad Wasm fetch 23 | type_urls: [ "type.googleapis.com/envoy.extensions.filters.http.wasm.v3.Wasm"] 24 | --- 25 | apiVersion: networking.istio.io/v1alpha3 26 | kind: EnvoyFilter 27 | metadata: 28 | name: opa-config 29 | namespace: istio-system 30 | spec: 31 | configPatches: 32 | - applyTo: EXTENSION_CONFIG 33 | match: 34 | context: GATEWAY 35 | patch: 36 | operation: ADD 37 | value: 38 | name: istio.opa 39 | typed_config: 40 | '@type': type.googleapis.com/udpa.type.v1.TypedStruct 41 | type_url: type.googleapis.com/envoy.extensions.filters.http.wasm.v3.Wasm 42 | value: 43 | config: 44 | configuration: 45 | '@type': type.googleapis.com/google.protobuf.StringValue 46 | value: | 47 | { 48 | "opa_cluster_name": "outbound|8181||opa.istio-system.svc.cluster.local", 49 | "opa_service_host": "opa.istio-system.svc.cluster.local", 50 | "check_result_cache_valid_sec": 10 51 | } 52 | vm_config: 53 | vm_id: opa 54 | code: 55 | remote: 56 | http_uri: 57 | uri: https://storage.googleapis.com/istio-ecosystem/wasm-extensions/opa/test.wasm 58 | runtime: envoy.wasm.runtime.v8 59 | -------------------------------------------------------------------------------- /extensions/open_policy_agent/config/opa-service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: example-policy 5 | namespace: istio-system 6 | data: 7 | opa_rule.rego: | 8 | package test 9 | 10 | default allow = false 11 | 12 | allow = true { 13 | input.request_method == "GET" 14 | input.destination_workload == "istio-ingressgateway" 15 | input.request_url_path == "/status/200" 16 | } 17 | 18 | allow = true { 19 | input.request_method == "GET" 20 | input.destination_workload == "istio-ingressgateway" 21 | input.request_url_path == "/headers" 22 | } 23 | --- 24 | apiVersion: apps/v1 25 | kind: Deployment 26 | metadata: 27 | name: opa 28 | namespace: istio-system 29 | labels: 30 | app: opa 31 | spec: 32 | replicas: 1 33 | selector: 34 | matchLabels: 35 | app: opa 36 | template: 37 | metadata: 38 | labels: 39 | app: opa 40 | name: opa 41 | spec: 42 | containers: 43 | - name: opa 44 | image: openpolicyagent/opa:latest 45 | ports: 46 | - name: http 47 | containerPort: 8181 48 | args: 49 | - "run" 50 | - "--ignore=.*" # exclude hidden dirs created by Kubernetes 51 | - "--server" 52 | - "/policies" 53 | volumeMounts: 54 | - readOnly: true 55 | mountPath: /policies 56 | name: example-policy 57 | volumes: 58 | - name: example-policy 59 | configMap: 60 | name: example-policy 61 | --- 62 | kind: Service 63 | apiVersion: v1 64 | metadata: 65 | name: opa 66 | namespace: istio-system 67 | labels: 68 | app: opa 69 | spec: 70 | type: NodePort 71 | selector: 72 | app: opa 73 | ports: 74 | - name: http 75 | protocol: TCP 76 | port: 8181 77 | targetPort: 8181 78 | -------------------------------------------------------------------------------- /extensions/open_policy_agent/plugin.h: -------------------------------------------------------------------------------- 1 | #include "extensions/open_policy_agent/cache.h" 2 | #include "proxy_wasm_intrinsics.h" 3 | 4 | // OPA filter root context. 5 | class PluginRootContext : public RootContext { 6 | public: 7 | explicit PluginRootContext(uint32_t id, std::string_view root_id) 8 | : RootContext(id, root_id) {} 9 | 10 | bool onConfigure(size_t) override; 11 | 12 | // Check sends out a HTTP check call to OPA server for the given stream. 13 | FilterHeadersStatus check(uint32_t stream_context_id); 14 | 15 | private: 16 | bool parseConfiguration(size_t); 17 | 18 | // Cache operations. 19 | bool checkCache(const Payload &payload, uint64_t &hash, bool &allowed) { 20 | bool hit = 21 | cache_.check(payload, hash, allowed, getCurrentTimeNanoseconds()); 22 | incrementMetric((hit ? cache_hits_ : cache_misses_), 1); 23 | return hit; 24 | } 25 | void addCache(const uint64_t hash, bool result) { 26 | cache_.add(hash, result, getCurrentTimeNanoseconds()); 27 | } 28 | 29 | // LRU cache for OPA check results. 30 | ResultCache cache_; 31 | // Duration that cache entry is valid for. 32 | uint64_t cache_valid_for_sec_ = 0; 33 | 34 | // Host for OPA check call. This will be used as host header. 35 | std::string opa_host_; 36 | // Envoy cluster for OPA HTTP call. 37 | std::string opa_cluster_; 38 | 39 | // Handler for cache stats. 40 | uint32_t cache_hits_; 41 | uint32_t cache_misses_; 42 | }; 43 | 44 | // OPA filter stream context. 45 | class PluginContext : public Context { 46 | public: 47 | explicit PluginContext(uint32_t id, RootContext *root) : Context(id, root) {} 48 | 49 | FilterHeadersStatus onRequestHeaders(uint32_t, bool) override; 50 | 51 | private: 52 | inline PluginRootContext *rootContext() { 53 | return dynamic_cast(this->root()); 54 | } 55 | }; 56 | -------------------------------------------------------------------------------- /extensions/scaffold/BUILD: -------------------------------------------------------------------------------- 1 | load("@proxy_wasm_cpp_sdk//bazel:defs.bzl", "proxy_wasm_cc_binary") 2 | load("//bazel:wasm.bzl", "declare_wasm_image_targets") 3 | 4 | proxy_wasm_cc_binary( 5 | name = "scaffold.wasm", 6 | srcs = [ 7 | "plugin.cc", 8 | "plugin.h", 9 | ], 10 | ) 11 | 12 | declare_wasm_image_targets( 13 | name = "scaffold", 14 | wasm_file = ":scaffold.wasm", 15 | ) 16 | -------------------------------------------------------------------------------- /extensions/scaffold/README.md: -------------------------------------------------------------------------------- 1 | # Scaffold extension 2 | 3 | This extension provides a starting point for a new extension. 4 | 5 | -------------------------------------------------------------------------------- /extensions/scaffold/plugin.cc: -------------------------------------------------------------------------------- 1 | #include "extensions/scaffold/plugin.h" 2 | 3 | static RegisterContextFactory register_Scaffold( 4 | CONTEXT_FACTORY(PluginContext), ROOT_FACTORY(PluginRootContext)); 5 | 6 | bool PluginRootContext::onConfigure(size_t) { return true; } 7 | -------------------------------------------------------------------------------- /extensions/scaffold/plugin.h: -------------------------------------------------------------------------------- 1 | #include "proxy_wasm_intrinsics.h" 2 | 3 | // PluginRootContext is the root context for all streams processed by the 4 | // thread. It has the same lifetime as the VM and acts as target for 5 | // interactions that outlives individual stream, e.g. timer, async calls. 6 | class PluginRootContext : public RootContext { 7 | public: 8 | explicit PluginRootContext(uint32_t id, std::string_view root_id) 9 | : RootContext(id, root_id) {} 10 | 11 | bool onConfigure(size_t) override; 12 | }; 13 | 14 | // Per-stream context. 15 | class PluginContext : public Context { 16 | public: 17 | explicit PluginContext(uint32_t id, RootContext* root) : Context(id, root) {} 18 | 19 | private: 20 | inline PluginRootContext* rootContext() { 21 | return dynamic_cast(this->root()); 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /extensions/stats/BUILD: -------------------------------------------------------------------------------- 1 | # Copyright Istio Authors. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | ################################################################################ 16 | # 17 | 18 | licenses(["notice"]) # Apache 2 19 | 20 | load("@proxy_wasm_cpp_sdk//bazel:defs.bzl", "proxy_wasm_cc_binary") 21 | 22 | proxy_wasm_cc_binary( 23 | name = "stats.wasm", 24 | srcs = [ 25 | "plugin.cc", 26 | "plugin.h", 27 | "//extensions/common:context.cc", 28 | "//extensions/common:context.h", 29 | "//extensions/common:util.cc", 30 | "//extensions/common:util.h", 31 | "//extensions/common:node_info_bfbs_generated.h", 32 | "//extensions/common:node_info_generated.h", 33 | ], 34 | copts = ["-UNULL_PLUGIN"], 35 | deps = [ 36 | "//extensions/common/wasm:json_util", 37 | "@com_google_absl//absl/strings", 38 | "@com_google_absl//absl/time", 39 | "@proxy_wasm_cpp_sdk//contrib:contrib_lib", 40 | "@com_github_google_flatbuffers//:flatbuffers", 41 | "@com_github_google_flatbuffers//:runtime_cc", 42 | ], 43 | ) 44 | -------------------------------------------------------------------------------- /extensions/stats/config.pb.html: -------------------------------------------------------------------------------- 1 | --- 2 | title: Stats Config 3 | description: Configuration for Stats Filter. 4 | location: https://istio.io/docs/reference/config/proxy_extensions/stats.html 5 | layout: protoc-gen-docs 6 | generator: protoc-gen-docs 7 | weight: 20 8 | number_of_entries: 5 9 | --- 10 |

MetricConfig

11 |
12 |

Metric instance configuration overrides. 13 | The metric value and the metric type are optional and permit changing the 14 | reported value for an existing metric. 15 | The standard metrics are optimized and reported through a “fast-path”. 16 | The customizations allow full configurability, at the cost of a “slower” 17 | path.

18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 38 | 41 | 42 | 43 | 44 | 45 | 50 | 53 | 54 | 55 | 56 | 57 | 61 | 64 | 65 | 66 | 67 | 68 | 72 | 75 | 76 | 77 | 78 | 79 | 84 | 87 | 88 | 89 |
FieldTypeDescriptionRequired
dimensionsmap<string, string> 33 |

(Optional) Collection of tag names and tag expressions to include in the 34 | metric. Conflicts are resolved by the tag name by overriding previously 35 | supplied values.

36 | 37 |
39 | No 40 |
namestring 46 |

(Optional) Metric name to restrict the override to a metric. If not 47 | specified, applies to all.

48 | 49 |
51 | No 52 |
tags_to_removestring[] 58 |

(Optional) A list of tags to remove.

59 | 60 |
62 | No 63 |
matchstring 69 |

NOT IMPLEMENTED. (Optional) Conditional enabling the override.

70 | 71 |
73 | No 74 |
dropbool 80 |

(Optional) If this is set to true, the metric(s) selected by this 81 | configuration will not be generated or reported.

82 | 83 |
85 | No 86 |
90 |
91 |

MetricDefinition

92 |
93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 110 | 113 | 114 | 115 | 116 | 117 | 121 | 124 | 125 | 126 | 127 | 128 | 132 | 135 | 136 | 137 |
FieldTypeDescriptionRequired
namestring 107 |

Metric name.

108 | 109 |
111 | No 112 |
valuestring 118 |

Metric value expression.

119 | 120 |
122 | No 123 |
typeMetricType 129 |

NOT IMPLEMENTED (Optional) Metric type.

130 | 131 |
133 | No 134 |
138 |
139 |

PluginConfig

140 |
141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 160 | 163 | 164 | 165 | 166 | 167 | 172 | 175 | 176 | 177 | 178 | 179 | 183 | 186 | 187 | 188 | 189 | 190 | 194 | 197 | 198 | 199 | 200 | 201 | 205 | 208 | 209 | 210 |
FieldTypeDescriptionRequired
disable_host_header_fallbackbool 155 |

Optional: Disable using host header as a fallback if destination service is 156 | not available from the controlplane. Disable the fallback if the host 157 | header originates outsides the mesh, like at ingress.

158 | 159 |
161 | No 162 |
tcp_reporting_durationDuration 168 |

Optional. Allows configuration of the time between calls out to for TCP 169 | metrics reporting. The default duration is 15s.

170 | 171 |
173 | No 174 |
metricsMetricConfig[] 180 |

Metric overrides.

181 | 182 |
184 | No 185 |
definitionsMetricDefinition[] 191 |

Metric definitions.

192 | 193 |
195 | No 196 |
reporterReporter 202 |

Proxy deployment type.

203 | 204 |
206 | No 207 |
211 |
212 |

MetricType

213 |
214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 226 | 227 | 228 | 229 | 231 | 232 | 233 | 234 | 236 | 237 | 238 |
NameDescription
COUNTER 225 |
GAUGE 230 |
HISTOGRAM 235 |
239 |
240 |

Reporter

241 |
242 |

Specifies the proxy deployment type.

243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 259 | 260 | 261 | 262 | 266 | 267 | 268 |
NameDescription
UNSPECIFIED 255 |

Default value is inferred from the listener direction, as either client or 256 | server sidecar.

257 | 258 |
SERVER_GATEWAY 263 |

Shared server gateway, e.g. “waypoint”.

264 | 265 |
269 |
270 | -------------------------------------------------------------------------------- /extensions/stats/config.proto: -------------------------------------------------------------------------------- 1 | /* Copyright 2019 Istio Authors. All Rights Reserved. 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | syntax = "proto3"; 17 | 18 | // $title: Stats Config 19 | // $description: Configuration for Stats Filter. 20 | // $location: https://istio.io/docs/reference/config/proxy_extensions/stats.html 21 | // $weight: 20 22 | 23 | package stats; 24 | 25 | import "google/protobuf/duration.proto"; 26 | 27 | // Metric instance configuration overrides. 28 | // The metric value and the metric type are optional and permit changing the 29 | // reported value for an existing metric. 30 | // The standard metrics are optimized and reported through a "fast-path". 31 | // The customizations allow full configurability, at the cost of a "slower" 32 | // path. 33 | message MetricConfig { 34 | // (Optional) Collection of tag names and tag expressions to include in the 35 | // metric. Conflicts are resolved by the tag name by overriding previously 36 | // supplied values. 37 | map dimensions = 1; 38 | 39 | // (Optional) Metric name to restrict the override to a metric. If not 40 | // specified, applies to all. 41 | string name = 2; 42 | 43 | // (Optional) A list of tags to remove. 44 | repeated string tags_to_remove = 3; 45 | 46 | // NOT IMPLEMENTED. (Optional) Conditional enabling the override. 47 | string match = 4; 48 | 49 | // (Optional) If this is set to true, the metric(s) selected by this 50 | // configuration will not be generated or reported. 51 | bool drop = 5; 52 | } 53 | 54 | enum MetricType { 55 | COUNTER = 0; 56 | GAUGE = 1; 57 | HISTOGRAM = 2; 58 | } 59 | 60 | message MetricDefinition { 61 | // Metric name. 62 | string name = 1; 63 | 64 | // Metric value expression. 65 | string value = 2; 66 | 67 | // Metric type. 68 | MetricType type = 3; 69 | } 70 | 71 | // Specifies the proxy deployment type. 72 | enum Reporter { 73 | // Default value is inferred from the listener direction, as either client or 74 | // server sidecar. 75 | UNSPECIFIED = 0; 76 | 77 | // Shared server gateway, e.g. "waypoint". 78 | SERVER_GATEWAY = 1; 79 | } 80 | 81 | message PluginConfig { 82 | reserved 1; 83 | reserved "debug"; 84 | 85 | reserved 2; 86 | reserved "max_peer_cache_size"; 87 | 88 | reserved 3; 89 | reserved "stat_prefix"; 90 | 91 | reserved 4; 92 | reserved "field_separator"; 93 | 94 | reserved 5; 95 | reserved "value_separator"; 96 | 97 | // Optional: Disable using host header as a fallback if destination service is 98 | // not available from the controlplane. Disable the fallback if the host 99 | // header originates outsides the mesh, like at ingress. 100 | bool disable_host_header_fallback = 6; 101 | 102 | // Optional. Allows configuration of the time between calls out to for TCP 103 | // metrics reporting. The default duration is `15s`. 104 | google.protobuf.Duration tcp_reporting_duration = 7; 105 | 106 | // Metric overrides. 107 | repeated MetricConfig metrics = 8; 108 | 109 | // Metric definitions. 110 | repeated MetricDefinition definitions = 9; 111 | 112 | // Proxy deployment type. 113 | Reporter reporter = 10; 114 | } 115 | -------------------------------------------------------------------------------- /extensions/stats/plugin_test.cc: -------------------------------------------------------------------------------- 1 | /* Copyright 2019 Istio Authors. All Rights Reserved. 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | #include "extensions/stats/plugin.h" 17 | 18 | #include 19 | 20 | #include "absl/hash/hash_testing.h" 21 | #include "gtest/gtest.h" 22 | 23 | // WASM_PROLOG 24 | #ifndef NULL_PLUGIN 25 | #include "api/wasm/cpp/proxy_wasm_intrinsics.h" 26 | 27 | #else // NULL_PLUGIN 28 | 29 | #include "include/proxy-wasm/null_plugin.h" 30 | 31 | namespace proxy_wasm { 32 | namespace null_plugin { 33 | #endif // NULL_PLUGIN 34 | 35 | // END WASM_PROLOG 36 | 37 | namespace Stats { 38 | 39 | TEST(IstioDimensions, Hash) { 40 | IstioDimensions d1(count_standard_labels); 41 | IstioDimensions d2(count_standard_labels); 42 | d2[request_protocol] = "grpc"; 43 | IstioDimensions d3(count_standard_labels); 44 | d3[request_protocol] = "grpc"; 45 | d3[response_code] = "200"; 46 | IstioDimensions d4(count_standard_labels); 47 | d4[request_protocol] = "grpc"; 48 | d4[response_code] = "400"; 49 | IstioDimensions d5(count_standard_labels); 50 | d5[request_protocol] = "grpc"; 51 | d5[source_app] = "app_source"; 52 | IstioDimensions d6(count_standard_labels); 53 | d6[reporter] = source; 54 | d6[request_protocol] = "grpc"; 55 | d6[source_app] = "app_source"; 56 | d6[source_version] = "v2"; 57 | IstioDimensions d7(count_standard_labels); 58 | d7[request_protocol] = "grpc", d7[source_app] = "app_source", d7[source_version] = "v2"; 59 | IstioDimensions d7_duplicate(count_standard_labels); 60 | d7_duplicate[request_protocol] = "grpc"; 61 | d7_duplicate[source_app] = "app_source"; 62 | d7_duplicate[source_version] = "v2"; 63 | IstioDimensions d8(count_standard_labels); 64 | d8[request_protocol] = "grpc"; 65 | d8[source_app] = "app_source"; 66 | d8[source_version] = "v2"; 67 | d8[grpc_response_status] = "12"; 68 | 69 | // Must be unique except for d7 and d8. 70 | std::set hashes; 71 | hashes.insert(HashIstioDimensions()(d1)); 72 | hashes.insert(HashIstioDimensions()(d2)); 73 | hashes.insert(HashIstioDimensions()(d3)); 74 | hashes.insert(HashIstioDimensions()(d4)); 75 | hashes.insert(HashIstioDimensions()(d5)); 76 | hashes.insert(HashIstioDimensions()(d6)); 77 | hashes.insert(HashIstioDimensions()(d7)); 78 | hashes.insert(HashIstioDimensions()(d7_duplicate)); 79 | hashes.insert(HashIstioDimensions()(d8)); 80 | EXPECT_EQ(hashes.size(), 8); 81 | } 82 | 83 | } // namespace Stats 84 | 85 | // WASM_EPILOG 86 | #ifdef NULL_PLUGIN 87 | } // namespace null_plugin 88 | } // namespace proxy_wasm 89 | #endif 90 | -------------------------------------------------------------------------------- /extensions/stats/run_test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Copyright Istio Authors 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | WD=$(dirname "$0") 18 | WD=$(cd "$WD"; pwd) 19 | 20 | BAZEL_BIN="${WD}/../../bazel-bin" 21 | 22 | set -ex 23 | 24 | "${BAZEL_BIN}"/src/envoy/envoy -c "${WD}"/testdata/client.yaml --concurrency 2 --allow-unknown-fields 25 | "${BAZEL_BIN}"/src/envoy/envoy -c "${WD}"/testdata/server.yaml --concurrency 2 --allow-unknown-fields 26 | -------------------------------------------------------------------------------- /extensions/stats/testdata/client.yaml: -------------------------------------------------------------------------------- 1 | node: 2 | id: test-client-productpage 3 | metadata: 4 | ISTIO_VERSION: "1.4-dev" 5 | NAME: productpage-v11-84975bc778-pxz2w 6 | NAMESPACE: default 7 | WORKLOAD_NAME: productpage 8 | OWNER: /api/ns/deployment/product-deployment 9 | LABELS: { app: productpage, version: V11 } 10 | INSTANCE_IPS: "10.52.0.34,fe80::a075:11ff:fe5e:f1cd" 11 | istio: sidecar 12 | PLATFORM_METADATA: 13 | gcp_cluster_name: /redacted/ 14 | gcp_project: "/redacted/" 15 | gcp_cluster_location: "us-east4-b" 16 | stats_config: 17 | use_all_default_tags: true 18 | stats_tags: 19 | - tag_name: "reporter" 20 | regex: "(reporter=\\.=(.+?);\\.;)" 21 | - tag_name: "source_namespace" 22 | regex: "(source_namespace=\\.=(.+?);\\.;)" 23 | - tag_name: "source_workload" 24 | regex: "(source_workload=\\.=(.+?);\\.;)" 25 | - tag_name: "source_canonical_service" 26 | regex: "(source_canonical_service=\\.=(.+?);\\.;)" 27 | - tag_name: "source_canonical_revision" 28 | regex: "(source_canonical_revision=\\.=(.*?);\\.;)" 29 | - tag_name: "source_workload_namespace" 30 | regex: "(source_workload_namespace=\\.=(.+?);\\.;)" 31 | - tag_name: "source_principal" 32 | regex: "(source_principal=\\.=(.+?);\\.;)" 33 | - tag_name: "source_app" 34 | regex: "(source_app=\\.=(.+?);\\.;)" 35 | - tag_name: "source_version" 36 | regex: "(source_version=\\.=(.+?);\\.;)" 37 | - tag_name: "source_cluster" 38 | regex: "(source_cluster=\\.=(.+?);\\.;)" 39 | - tag_name: "destination_namespace" 40 | regex: "(destination_namespace=\\.=(.+?);\\.;)" 41 | - tag_name: "destination_workload" 42 | regex: "(destination_workload=\\.=(.+?);\\.;)" 43 | - tag_name: "destination_workload_namespace" 44 | regex: "(destination_workload_namespace=\\.=(.+?);\\.;)" 45 | - tag_name: "destination_principal" 46 | regex: "(destination_principal=\\.=(.+?);\\.;)" 47 | - tag_name: "destination_app" 48 | regex: "(destination_app=\\.=(.+?);\\.;)" 49 | - tag_name: "destination_version" 50 | regex: "(destination_version=\\.=(.+?);\\.;)" 51 | - tag_name: "destination_service" 52 | regex: "(destination_service=\\.=(.+?);\\.;)" 53 | - tag_name: "destination_canonical_service" 54 | regex: "(destination_canonical_service=\\.=(.+?);\\.;)" 55 | - tag_name: "destination_canonical_revision" 56 | regex: "(destination_canonical_revision=\\.=(.*?);\\.;)" 57 | - tag_name: "destination_service_name" 58 | regex: "(destination_service_name=\\.=(.+?);\\.;)" 59 | - tag_name: "destination_service_namespace" 60 | regex: "(destination_service_namespace=\\.=(.+?);\\.;)" 61 | - tag_name: "destination_cluster" 62 | regex: "(destination_cluster=\\.=(.+?);\\.;)" 63 | - tag_name: "request_protocol" 64 | regex: "(request_protocol=\\.=(.+?);\\.;)" 65 | - tag_name: "response_code" 66 | regex: "(response_code=\\.=(.+?);\\.;)|_rq(_(\\.d{3}))$" 67 | - tag_name: "grpc_response_status" 68 | regex: "(grpc_response_status=\\.=(.*?);\\.;)" 69 | - tag_name: "response_flags" 70 | regex: "(response_flags=\\.=(.+?);\\.;)" 71 | - tag_name: "connection_security_policy" 72 | regex: "(connection_security_policy=\\.=(.+?);\\.;)" 73 | - tag_name: "cache" 74 | regex: "(cache\\.(.+?)\\.)" 75 | - tag_name: "component" 76 | regex: "(component\\.(.+?)\\.)" 77 | - tag_name: "tag" 78 | regex: "(tag\\.(.+?);\\.)" 79 | static_resources: 80 | listeners: 81 | - name: client 82 | traffic_direction: OUTBOUND 83 | address: 84 | socket_address: 85 | address: 0.0.0.0 86 | port_value: 8080 87 | filter_chains: 88 | - filters: 89 | - name: envoy.http_connection_manager 90 | config: 91 | stat_prefix: ingress_http 92 | codec_type: auto 93 | route_config: 94 | name: local_route 95 | virtual_hosts: 96 | - name: local_service 97 | domains: 98 | - "*" 99 | routes: 100 | - match: 101 | prefix: "/" 102 | route: 103 | cluster: server 104 | http_filters: 105 | - name: envoy.filters.http.wasm 106 | config: 107 | config: 108 | vm_config: 109 | runtime: envoy.wasm.runtime.null 110 | code: 111 | local: { inline_string: "envoy.wasm.metadata_exchange" } 112 | configuration: "test" 113 | - name: envoy.filters.http.wasm 114 | config: 115 | config: 116 | root_id: "stats_outbound" 117 | vm_config: 118 | runtime: envoy.wasm.runtime.null 119 | code: 120 | local: { inline_string: "envoy.wasm.stats" } 121 | configuration: | 122 | { "debug": "false", max_peer_cache_size: 20, field_separator: ";.;" } 123 | - name: envoy.filters.http.router 124 | config: {} 125 | access_log: 126 | - name: envoy.file_access_log 127 | config: 128 | path: "/dev/null" 129 | format: "client %RESPONSE_CODE% downstream_id:%DYNAMIC_METADATA(envoy.wasm.metadata_exchange.downstream_id)% downstream:%DYNAMIC_METADATA(envoy.wasm.metadata_exchange.downstream)% upstream_id:%DYNAMIC_METADATA(envoy.wasm.metadata_exchange.upstream_id)% upstream:%DYNAMIC_METADATA(envoy.wasm.metadata_exchange.upstream)%\n" 130 | clusters: 131 | - name: server 132 | connect_timeout: 0.25s 133 | type: static 134 | lb_policy: round_robin 135 | hosts: 136 | - socket_address: 137 | address: 127.0.0.1 138 | port_value: 8081 139 | http2_protocol_options: {} 140 | admin: 141 | access_log_path: "/dev/null" 142 | address: 143 | socket_address: 144 | address: 0.0.0.0 145 | port_value: 8001 146 | -------------------------------------------------------------------------------- /extensions/stats/testdata/istio/metadata-exchange_filter.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: networking.istio.io/v1alpha3 2 | kind: EnvoyFilter 3 | metadata: 4 | name: metadata-exchange 5 | spec: 6 | configPatches: 7 | - applyTo: HTTP_FILTER 8 | match: 9 | context: ANY # inbound, outbound, and gateway 10 | listener: 11 | filterChain: 12 | filter: 13 | name: "envoy.http_connection_manager" 14 | patch: 15 | operation: INSERT_BEFORE 16 | value: 17 | name: envoy.filters.http.wasm 18 | config: 19 | config: 20 | configuration: envoy.wasm.metadata_exchange 21 | vm_config: 22 | runtime: envoy.wasm.runtime.null 23 | code: 24 | local: { inline_string: envoy.wasm.metadata_exchange } 25 | -------------------------------------------------------------------------------- /extensions/stats/testdata/istio/stats_filter.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: networking.istio.io/v1alpha3 2 | kind: EnvoyFilter 3 | metadata: 4 | name: stats-filter 5 | spec: 6 | configPatches: 7 | - applyTo: HTTP_FILTER 8 | match: 9 | context: SIDECAR_OUTBOUND 10 | listener: 11 | filterChain: 12 | filter: 13 | name: "envoy.http_connection_manager" 14 | subFilter: 15 | name: "envoy.filters.http.router" 16 | patch: 17 | operation: INSERT_BEFORE 18 | value: 19 | name: envoy.filters.http.wasm 20 | config: 21 | config: 22 | root_id: stats_outbound 23 | configuration: | 24 | { 25 | "debug": "false", 26 | "stat_prefix": "istio" 27 | } 28 | vm_config: 29 | runtime: envoy.wasm.runtime.null 30 | code: 31 | local: { inline_string: envoy.wasm.stats } 32 | - applyTo: HTTP_FILTER 33 | match: 34 | context: SIDECAR_INBOUND 35 | listener: 36 | filterChain: 37 | filter: 38 | name: "envoy.http_connection_manager" 39 | subFilter: 40 | name: "envoy.filters.http.router" 41 | patch: 42 | operation: INSERT_BEFORE 43 | value: 44 | name: envoy.filters.http.wasm 45 | config: 46 | config: 47 | root_id: stats_inbound 48 | configuration: | 49 | { 50 | "debug": "false", 51 | "stat_prefix": "istio" 52 | } 53 | vm_config: 54 | runtime: envoy.wasm.runtime.null 55 | code: 56 | local: { inline_string: envoy.wasm.stats } 57 | - applyTo: HTTP_FILTER 58 | match: 59 | context: GATEWAY 60 | listener: 61 | filterChain: 62 | filter: 63 | name: "envoy.http_connection_manager" 64 | subFilter: 65 | name: "envoy.filters.http.router" 66 | patch: 67 | operation: INSERT_BEFORE 68 | value: 69 | name: envoy.filters.http.wasm 70 | config: 71 | config: 72 | configuration: | 73 | { 74 | "debug": "false", 75 | "stat_prefix": "istio", 76 | "disable_host_header_fallback": true 77 | } 78 | vm_config: 79 | runtime: envoy.wasm.runtime.null 80 | code: 81 | local: { inline_string: envoy.wasm.stats } 82 | -------------------------------------------------------------------------------- /extensions/stats/testdata/server.yaml: -------------------------------------------------------------------------------- 1 | node: 2 | id: test-server-ratings 3 | metadata: 4 | ISTIO_VERSION: "1.4-dev" 5 | NAME: ratings-v22-84975bc778-pxz2w 6 | NAMESPACE: default 7 | WORKLOAD_NAME: ratings 8 | OWNER: /api/ns/deployment/ratings-deployment 9 | LABELS: { app: ratings, version: V22 } 10 | INSTANCE_IPS: "10.52.0.35,fe80::a075:11ff:fe5e:f1cd" 11 | istio: sidecar 12 | PLATFORM_METADATA: 13 | gcp_cluster_name: /redacted/ 14 | gcp_project: "/redacted/" 15 | gcp_cluster_location: "us-east4-b" 16 | stats_config: 17 | use_all_default_tags: false 18 | stats_tags: 19 | - tag_name: "reporter" 20 | regex: "(reporter=\\.=(.+?);\\.;)" 21 | - tag_name: "source_namespace" 22 | regex: "(source_namespace=\\.=(.+?);\\.;)" 23 | - tag_name: "source_workload" 24 | regex: "(source_workload=\\.=(.+?);\\.;)" 25 | - tag_name: "source_canonical_service" 26 | regex: "(source_canonical_service=\\.=(.+?);\\.;)" 27 | - tag_name: "source_canonical_revision" 28 | regex: "(source_canonical_revision=\\.=(.*?);\\.;)" 29 | - tag_name: "source_workload_namespace" 30 | regex: "(source_workload_namespace=\\.=(.+?);\\.;)" 31 | - tag_name: "source_principal" 32 | regex: "(source_principal=\\.=(.+?);\\.;)" 33 | - tag_name: "source_app" 34 | regex: "(source_app=\\.=(.+?);\\.;)" 35 | - tag_name: "source_version" 36 | regex: "(source_version=\\.=(.+?);\\.;)" 37 | - tag_name: "source_cluster" 38 | regex: "(source_cluster=\\.=(.+?);\\.;)" 39 | - tag_name: "destination_namespace" 40 | regex: "(destination_namespace=\\.=(.+?);\\.;)" 41 | - tag_name: "destination_workload" 42 | regex: "(destination_workload=\\.=(.+?);\\.;)" 43 | - tag_name: "destination_workload_namespace" 44 | regex: "(destination_workload_namespace=\\.=(.+?);\\.;)" 45 | - tag_name: "destination_principal" 46 | regex: "(destination_principal=\\.=(.+?);\\.;)" 47 | - tag_name: "destination_app" 48 | regex: "(destination_app=\\.=(.+?);\\.;)" 49 | - tag_name: "destination_version" 50 | regex: "(destination_version=\\.=(.+?);\\.;)" 51 | - tag_name: "destination_service" 52 | regex: "(destination_service=\\.=(.+?);\\.;)" 53 | - tag_name: "destination_canonical_service" 54 | regex: "(destination_canonical_service=\\.=(.+?);\\.;)" 55 | - tag_name: "destination_canonical_revision" 56 | regex: "(destination_canonical_revision=\\.=(.*?);\\.;)" 57 | - tag_name: "destination_service_name" 58 | regex: "(destination_service_name=\\.=(.+?);\\.;)" 59 | - tag_name: "destination_service_namespace" 60 | regex: "(destination_service_namespace=\\.=(.+?);\\.;)" 61 | - tag_name: "destination_cluster" 62 | regex: "(destination_cluster=\\.=(.+?);\\.;)" 63 | - tag_name: "request_protocol" 64 | regex: "(request_protocol=\\.=(.+?);\\.;)" 65 | - tag_name: "response_code" 66 | regex: "(response_code=\\.=(.+?);\\.;)|_rq(_(\\.d{3}))$" 67 | - tag_name: "grpc_response_status" 68 | regex: "(grpc_response_status=\\.=(.*?);\\.;)" 69 | - tag_name: "response_flags" 70 | regex: "(response_flags=\\.=(.+?);\\.;)" 71 | - tag_name: "connection_security_policy" 72 | regex: "(connection_security_policy=\\.=(.+?);\\.;)" 73 | - tag_name: "cache" 74 | regex: "(cache\\.(.+?)\\.)" 75 | - tag_name: "component" 76 | regex: "(component\\.(.+?)\\.)" 77 | - tag_name: "tag" 78 | regex: "(tag\\.(.+?);\\.)" 79 | static_resources: 80 | listeners: 81 | - name: server 82 | traffic_direction: INBOUND 83 | address: 84 | socket_address: 85 | address: 0.0.0.0 86 | port_value: 8081 87 | filter_chains: 88 | - filters: 89 | - name: envoy.http_connection_manager 90 | config: 91 | stat_prefix: ingress_http 92 | codec_type: auto 93 | route_config: 94 | name: local_route 95 | virtual_hosts: 96 | - name: local_service 97 | domains: 98 | - "*" 99 | routes: 100 | - match: 101 | prefix: "/" 102 | route: 103 | cluster: web_service 104 | http_filters: 105 | - name: envoy.filters.http.wasm 106 | config: 107 | config: 108 | vm_config: 109 | runtime: envoy.wasm.runtime.null 110 | code: 111 | local: { inline_string: "envoy.wasm.metadata_exchange" } 112 | configuration: "test" 113 | - name: envoy.filters.http.wasm 114 | config: 115 | config: 116 | root_id: "stats_inbound" 117 | vm_config: 118 | runtime: envoy.wasm.runtime.null 119 | code: 120 | local: { inline_string: "envoy.wasm.stats" } 121 | configuration: | 122 | { "debug": "false", max_peer_cache_size: 20 } 123 | - name: envoy.filters.http.router 124 | config: {} 125 | access_log: 126 | - name: envoy.file_access_log 127 | config: 128 | path: "/dev/null" 129 | format: "server %RESPONSE_CODE% downstream_id:%DYNAMIC_METADATA(envoy.wasm.metadata_exchange.downstream_id)% downstream:%DYNAMIC_METADATA(envoy.wasm.metadata_exchange.downstream)% upstream_id:%DYNAMIC_METADATA(envoy.wasm.metadata_exchange.upstream_id)% upstream:%DYNAMIC_METADATA(envoy.wasm.metadata_exchange.upstream)%\n" 130 | - name: staticreply 131 | address: 132 | socket_address: 133 | address: 127.0.0.1 134 | port_value: 8099 135 | filter_chains: 136 | - filters: 137 | - name: envoy.http_connection_manager 138 | config: 139 | stat_prefix: ingress_http 140 | codec_type: auto 141 | route_config: 142 | name: local_route 143 | virtual_hosts: 144 | - name: local_service 145 | domains: 146 | - "*" 147 | routes: 148 | - match: 149 | prefix: "/" 150 | direct_response: 151 | status: 200 152 | body: 153 | inline_string: "example body\n" 154 | http_filters: 155 | - name: envoy.lua 156 | typed_config: 157 | "@type": type.googleapis.com/envoy.config.filter.http.lua.v2.Lua 158 | inline_code: | 159 | function trivial_httpbin(request_handle) 160 | local headers = request_handle:headers() 161 | local path = headers:get(":path") 162 | local start, _ = path:find("/[^/]*$") 163 | local last_segment = path:sub(start+1) 164 | 165 | request_handle:respond( 166 | {[":status"] = last_segment}) 167 | end 168 | function envoy_on_request(request_handle) 169 | trivial_httpbin(request_handle) 170 | end 171 | - name: envoy.filters.http.router 172 | config: {} 173 | access_log: 174 | - name: envoy.file_access_log 175 | config: 176 | path: "/dev/null" 177 | format: "origin %RESPONSE_CODE% downstream:%REQ(x-envoy-peer-metadata-id)% downstream:%REQ(x-envoy-peer-metadata)%\n" 178 | clusters: 179 | - name: web_service 180 | connect_timeout: 0.25s 181 | type: static 182 | lb_policy: round_robin 183 | hosts: 184 | - socket_address: 185 | address: 127.0.0.1 186 | port_value: 8099 187 | - name: httpbin 188 | connect_timeout: 0.25s 189 | type: strict_dns 190 | lb_policy: round_robin 191 | hosts: 192 | - socket_address: 193 | address: httpbin.org 194 | port_value: 80 195 | admin: 196 | access_log_path: "/dev/null" 197 | address: 198 | socket_address: 199 | address: 0.0.0.0 200 | port_value: 8002 201 | -------------------------------------------------------------------------------- /extensions/zig_demo/README.md: -------------------------------------------------------------------------------- 1 | # Ziglang demo extension 2 | 3 | This is an example of an ABI-compatible module written in [Zig](https://ziglang.org/). 4 | 5 | To build it, zig 0.7.0 or latest is needed. Build with `zig build` and the module is placed into `zig-cache`. 6 | -------------------------------------------------------------------------------- /extensions/zig_demo/build.zig: -------------------------------------------------------------------------------- 1 | const Builder = @import("std").build.Builder; 2 | 3 | pub fn build(b: *Builder) void { 4 | const mode = b.standardReleaseOptions(); 5 | const lib = b.addStaticLibrary("zig-wasm", "module.zig"); 6 | lib.setBuildMode(mode); 7 | lib.setTarget(.{ .cpu_arch = .wasm32, .os_tag = .freestanding }); 8 | lib.setOutputDir("zig-cache"); 9 | b.default_step.dependOn(&lib.step); 10 | } 11 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/istio-ecosystem/wasm-extensions 2 | 3 | go 1.17 4 | 5 | require ( 6 | github.com/golang/protobuf v1.5.2 7 | google.golang.org/grpc v1.52.3 8 | google.golang.org/protobuf v1.28.1 9 | istio.io/proxy v0.0.0-20230307210546-1aed48c51a1d 10 | ) 11 | 12 | require ( 13 | github.com/census-instrumentation/opencensus-proto v0.4.1 // indirect 14 | github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4 // indirect 15 | github.com/cncf/xds/go v0.0.0-20221128185840-c261a164b73d // indirect 16 | github.com/d4l3k/messagediff v1.2.2-0.20180726183240-b9e99b2f9263 // indirect 17 | github.com/envoyproxy/go-control-plane v0.11.1-0.20230303230207-58bf12899b29 // indirect 18 | github.com/envoyproxy/protoc-gen-validate v0.9.1 // indirect 19 | github.com/fsnotify/fsnotify v1.4.9 // indirect 20 | github.com/matttproud/golang_protobuf_extensions v1.0.2 // indirect 21 | github.com/prometheus/client_model v0.3.0 // indirect 22 | github.com/prometheus/common v0.38.0 // indirect 23 | golang.org/x/net v0.7.0 // indirect 24 | golang.org/x/sys v0.5.0 // indirect 25 | golang.org/x/text v0.7.0 // indirect 26 | google.golang.org/genproto v0.0.0-20221207170731-23e4bf6bdc37 // indirect 27 | gopkg.in/yaml.v2 v2.4.0 // indirect 28 | sigs.k8s.io/yaml v1.3.0 // indirect 29 | ) 30 | -------------------------------------------------------------------------------- /scripts/get-dep.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | # Prints proxy-wasm-cpp-sdk, proxy-wasm-cpp-host, and istio-proxy SHA based on the given branch 6 | function usage() { 7 | echo "$0 8 | -r the release branch that this script should look at, e.g. 1.8, master." 9 | exit 1 10 | } 11 | 12 | RELEASE="" 13 | 14 | while getopts r: arg ; do 15 | case "${arg}" in 16 | r) RELEASE="${OPTARG}";; 17 | *) usage;; 18 | esac 19 | done 20 | 21 | if [[ ${RELEASE} == "" ]]; then 22 | echo "release branch has to be provided. e.g. get-dep.sh -r 1.8" 23 | exit 1 24 | fi 25 | 26 | echo "Fetching deps SHA..." 27 | 28 | ENVOY_TMP_DIR=$(mktemp -d -t envoy-XXXXXXXXXX) 29 | trap "rm -rf ${ENVOY_TMP_DIR}" EXIT 30 | 31 | pushd ${ENVOY_TMP_DIR} > /dev/null 32 | if [[ ${RELEASE} == "master" ]]; then 33 | git clone --depth=1 --branch master https://github.com/envoyproxy/envoy 2> /dev/null 34 | else 35 | git clone --depth=1 --branch release-${RELEASE} https://github.com/istio/envoy 2> /dev/null 36 | fi 37 | cd envoy 38 | 39 | NEW_SDK_SHA=$(bazel query //external:proxy_wasm_cpp_sdk --output=build 2> /dev/null | grep -Pom1 "https://github.com/proxy-wasm/proxy-wasm-cpp-sdk/archive/\K[a-zA-Z0-9]{40}") 40 | NEW_SDK_SHA256=$(bazel query //external:proxy_wasm_cpp_sdk --output=build 2> /dev/null | grep -Pom1 "sha256 = \"\K[a-zA-Z0-9]{64}") 41 | NEW_HOST_SHA=$(bazel query //external:proxy_wasm_cpp_host --output=build 2> /dev/null | grep -Pom1 "https://github.com/proxy-wasm/proxy-wasm-cpp-host/archive/\K[a-zA-Z0-9]{40}") 42 | NEW_HOST_SHA256=$(bazel query //external:proxy_wasm_cpp_host --output=build 2> /dev/null | grep -Pom1 "sha256 = \"\K[a-zA-Z0-9]{64}") 43 | 44 | popd > /dev/null 45 | 46 | # Istio proxy SHA update 47 | ISTIO_PROXY_TMP_DIR=$(mktemp -d -t proxy-XXXXXXXXXX) 48 | trap "rm -rf ${ISTIO_PROXY_TMP_DIR}" EXIT 49 | 50 | pushd ${ISTIO_PROXY_TMP_DIR} > /dev/null 51 | 52 | if [[ ${RELEASE} == "master" ]]; then 53 | git clone --depth=1 --branch master https://github.com/istio/proxy 2> /dev/null 54 | else 55 | git clone --depth=1 --branch release-${RELEASE} https://github.com/istio/proxy 2> /dev/null 56 | fi 57 | cd proxy 58 | 59 | NEW_PROXY_SHA=$(git rev-parse HEAD) 60 | NEW_PROXY_SHA256=$(wget https://github.com/istio/proxy/archive/${NEW_PROXY_SHA}.tar.gz 2> /dev/null && sha256sum ${NEW_PROXY_SHA}.tar.gz | grep -Pom1 "\K[a-zA-Z0-9]{64}") > /dev/null 61 | 62 | popd > /dev/null 63 | 64 | echo "At Istio ${RELEASE}:" 65 | echo "proxy Wasm cpp sdk SHA ${NEW_SDK_SHA} Checksum: ${NEW_SDK_SHA256}" 66 | echo "Proxy Wasm cpp host SHA ${NEW_HOST_SHA} Checksum: ${NEW_HOST_SHA256}" 67 | echo "Istio proxy SHA ${NEW_PROXY_SHA} Checksum: ${NEW_PROXY_SHA256}" 68 | -------------------------------------------------------------------------------- /scripts/update-dep.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -eu 4 | 5 | # update proxy-wasm-cpp-sdk, proxy-wasm-cpp-host, and istio-proxy SHA based on the given branch 6 | function usage() { 7 | echo "$0 8 | -r the release branch that this script should look at, e.g. 1.8, master." 9 | exit 1 10 | } 11 | 12 | RELEASE="" 13 | 14 | while getopts r: arg ; do 15 | case "${arg}" in 16 | r) RELEASE="${OPTARG}";; 17 | *) usage;; 18 | esac 19 | done 20 | 21 | if [[ ${RELEASE} == "" ]]; then 22 | echo "release branch has to be provided. e.g. update-dep.sh -r 1.8" 23 | exit 1 24 | fi 25 | 26 | WD=$(pwd) 27 | 28 | # Clone istio-envoy repo, get and update proxy Wasm sdk and host SHA 29 | ENVOY_TMP_DIR=$(mktemp -d -t envoy-XXXXXXXXXX) 30 | trap "rm -rf ${ENVOY_TMP_DIR}" EXIT 31 | 32 | cd ${ENVOY_TMP_DIR} 33 | git clone --depth=1 https://github.com/envoyproxy/envoy 34 | cd envoy 35 | git remote add istio-envoy https://github.com/istio/envoy 36 | git fetch istio-envoy 37 | cd envoy 38 | 39 | # SDK SHA update 40 | NEW_SDK_SHA=$(bazel query //external:proxy_wasm_cpp_sdk --output=build | grep -Pom1 "https://github.com/proxy-wasm/proxy-wasm-cpp-sdk/archive/\K[a-zA-Z0-9]{40}") 41 | NEW_SDK_SHA256=$(bazel query //external:proxy_wasm_cpp_sdk --output=build | grep -Pom1 "sha256 = \"\K[a-zA-Z0-9]{64}") 42 | pushd $WD 43 | CUR_SDK_SHA=$(bazel query //external:proxy_wasm_cpp_sdk --output=build | grep -Pom1 "https://github.com/proxy-wasm/proxy-wasm-cpp-sdk/archive/\K[a-zA-Z0-9]{40}") 44 | CUR_SDK_SHA256=$(bazel query //external:proxy_wasm_cpp_sdk --output=build | grep -Pom1 "sha256 = \"\K[a-zA-Z0-9]{64}") 45 | sed -i "s/${CUR_SDK_SHA}/${NEW_SDK_SHA}/g" WORKSPACE 46 | sed -i "s/${CUR_SDK_SHA256}/${NEW_SDK_SHA256}/g" WORKSPACE 47 | popd 48 | 49 | # Host SHA update 50 | NEW_HOST_SHA=$(bazel query //external:proxy_wasm_cpp_host --output=build | grep -Pom1 "https://github.com/proxy-wasm/proxy-wasm-cpp-host/archive/\K[a-zA-Z0-9]{40}") 51 | NEW_HOST_SHA256=$(bazel query //external:proxy_wasm_cpp_host --output=build | grep -Pom1 "sha256 = \"\K[a-zA-Z0-9]{64}") 52 | pushd $WD 53 | CUR_HOST_SHA=$(bazel query //external:proxy_wasm_cpp_host --output=build | grep -Pom1 "https://github.com/proxy-wasm/proxy-wasm-cpp-host/archive/\K[a-zA-Z0-9]{40}") 54 | CUR_HOST_SHA256=$(bazel query //external:proxy_wasm_cpp_host --output=build | grep -Pom1 "sha256 = \"\K[a-zA-Z0-9]{64}") 55 | sed -i "s/${CUR_HOST_SHA}/${NEW_HOST_SHA}/g" bazel/wasm.bzl 56 | sed -i "s/${CUR_HOST_SHA256}/${NEW_HOST_SHA256}/g" bazel/wasm.bzl 57 | popd 58 | 59 | # Istio proxy SHA update 60 | ISTIO_PROXY_TMP_DIR=$(mktemp -d -t proxy-XXXXXXXXXX) 61 | trap "rm -rf ${ISTIO_PROXY_TMP_DIR}" EXIT 62 | 63 | cd ${ISTIO_PROXY_TMP_DIR} 64 | 65 | if [[ ${RELEASE} == "master" ]]; then 66 | git clone --depth=1 --branch master https://github.com/istio/proxy 67 | else 68 | git clone --depth=1 --branch release-${RELEASE} https://github.com/istio/proxy 69 | fi 70 | cd proxy 71 | 72 | NEW_PROXY_SHA=$(git rev-parse HEAD) 73 | NEW_PROXY_SHA256=$(wget https://github.com/istio/proxy/archive/${NEW_PROXY_SHA}.tar.gz && sha256sum ${NEW_PROXY_SHA}.tar.gz | grep -Pom1 "\K[a-zA-Z0-9]{64}") 74 | trap "rm -rf ${NEW_PROXY_SHA}.tar.gz" EXIT 75 | pushd $WD 76 | CUR_PROXY_SHA=$(bazel query //external:io_istio_proxy --output=build | grep -Pom1 "https://github.com/istio/proxy/archive/\K[a-zA-Z0-9]{40}") 77 | CUR_PROXY_SHA256=$(bazel query //external:io_istio_proxy --output=build | grep -Pom1 "sha256 = \"\K[a-zA-Z0-9]{64}") 78 | sed -i "s/${CUR_PROXY_SHA}/${NEW_PROXY_SHA}/g" WORKSPACE 79 | sed -i "s/${CUR_PROXY_SHA256}/${NEW_PROXY_SHA256}/g" WORKSPACE 80 | popd 81 | -------------------------------------------------------------------------------- /scripts/update-guide.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -eu 4 | 5 | # Update example extension and various guide with SDK SHA the given release. 6 | # The files touched by this script are: 7 | # * Example extension config example file `example/config/example-filter.yaml`, which has the module download link and checksum. 8 | # * Example extension config integration test `example/test/example_test.go`, which has the proxy version to download for integration testing. 9 | # * Wasm extension C++ Walkthrough `doc/write-a-wasm-extension-with-cpp.md`, which has the proxy wasm cpp sdk SHA and checksum. 10 | # * Wasm extension integration test guide `doc/write-integration-test.md`, which has the proxy version to download for integration testing. 11 | # * Basic auth config example file `basic_auth/config/gateway-filter.yaml`, which has the module download link and checksum. 12 | function usage() { 13 | echo "$0 14 | -r the release branch that this script should look at, e.g. 1.8, master." 15 | exit 1 16 | } 17 | 18 | function semverParseInto() { 19 | local RE='[^0-9]*\([0-9]*\)[.]\([0-9]*\)[.]\([0-9]*\)\([0-9A-Za-z-]*\)' 20 | #MAJOR 21 | eval $2=`echo $1 | sed -e "s#$RE#\1#"` 22 | #MINOR 23 | eval $3=`echo $1 | sed -e "s#$RE#\2#"` 24 | } 25 | 26 | RELEASE="" 27 | 28 | while getopts r: arg ; do 29 | case "${arg}" in 30 | r) RELEASE="${OPTARG}";; 31 | *) usage;; 32 | esac 33 | done 34 | 35 | if [[ ${RELEASE} == "" ]]; then 36 | echo "release branch has to be provided. e.g. update-guide.sh -r 1.8" 37 | exit 1 38 | fi 39 | 40 | # Clone istio-envoy repo, get and update proxy Wasm sdk and host SHA 41 | ENVOY_TMP_DIR=$(mktemp -d -t envoy-XXXXXXXXXX) 42 | trap "rm -rf ${ENVOY_TMP_DIR}" EXIT 43 | 44 | pushd ${ENVOY_TMP_DIR} 45 | 46 | git clone --depth=1 https://github.com/envoyproxy/envoy 47 | cd envoy 48 | git remote add istio-envoy https://github.com/istio/envoy 49 | git fetch istio-envoy 50 | cd envoy 51 | 52 | NEW_SDK_SHA=$(bazel query //external:proxy_wasm_cpp_sdk --output=build | grep -Pom1 "https://github.com/proxy-wasm/proxy-wasm-cpp-sdk/archive/\K[a-zA-Z0-9]{40}") 53 | NEW_SDK_SHA256=$(bazel query //external:proxy_wasm_cpp_sdk --output=build | grep -Pom1 "sha256 = \"\K[a-zA-Z0-9]{64}") 54 | 55 | popd 56 | 57 | # Update example WORKSPACE, integration test proxy version 58 | sed -e "s|PROXY_WASM_CPP_SDK_SHA = .*|PROXY_WASM_CPP_SDK_SHA = \"${NEW_SDK_SHA}\"|" -i example/WORKSPACE 59 | sed -e "s|PROXY_WASM_CPP_SDK_SHA256 = .*|PROXY_WASM_CPP_SDK_SHA256 = \"${NEW_SDK_SHA256}\"|" -i example/WORKSPACE 60 | sed -e "s|DownloadVersion:.*|DownloadVersion: \"${RELEASE}\",|" -i example/test/example_test.go 61 | 62 | semverParseInto ${RELEASE}.0 MAJOR MINOR PATCH 63 | # Update basic auth config with wasm file of the new version. 64 | BASIC_AUTH_WASM_MODULE_URL="https://github.com/istio-ecosystem/wasm-extensions/releases/download/${RELEASE}.0/basic-auth.wasm" 65 | BASIC_AUTH_MODULE_CHECKSUM=$(wget ${BASIC_AUTH_WASM_MODULE_URL} && sha256sum basic-auth.wasm | cut -d' ' -f 1) 66 | trap "rm -rf basic-auth.wasm" EXIT 67 | sed -e "s|uri: .*|uri: ${BASIC_AUTH_WASM_MODULE_URL}|" -i extensions/basic_auth/config/gateway-filter.yaml 68 | sed -e "s|sha256: .*|sha256: ${BASIC_AUTH_MODULE_CHECKSUM}|" -i extensions/basic_auth/config/gateway-filter.yaml 69 | sed -e "s|proxyVersion: .*|proxyVersion: ^${MAJOR}\\\.${MINOR}.*|" -i extensions/basic_auth/config/gateway-filter.yaml 70 | rm -rf basic-auth.wasm 71 | 72 | # Update guide doc 73 | sed -e "s|PROXY_WASM_CPP_SDK_SHA = .*|PROXY_WASM_CPP_SDK_SHA = \"${NEW_SDK_SHA}\"|" -i doc/write-a-wasm-extension-with-cpp.md 74 | sed -e "s|PROXY_WASM_CPP_SDK_SHA256 = .*|PROXY_WASM_CPP_SDK_SHA256 = \"${NEW_SDK_SHA256}\"|" -i doc/write-a-wasm-extension-with-cpp.md 75 | sed -e "s|DownloadVersion:.*|DownloadVersion: \"${RELEASE}\",|" -i doc/write-integration-test.md 76 | -------------------------------------------------------------------------------- /test/basicauth/basicauth_test.go: -------------------------------------------------------------------------------- 1 | package basicauth 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "testing" 7 | "time" 8 | 9 | "istio.io/proxy/test/envoye2e/driver" 10 | "istio.io/proxy/test/envoye2e/env" 11 | "istio.io/proxy/testdata" 12 | 13 | "github.com/istio-ecosystem/wasm-extensions/test" 14 | ) 15 | 16 | var TestCases = []struct { 17 | Name string 18 | Method string 19 | Host string 20 | Path string 21 | Realm string 22 | RequestHeaders map[string]string 23 | ResponseHeaders map[string]string 24 | ResponseCode int 25 | }{ 26 | { 27 | Name: "CorrectCredentials", 28 | Method: "GET", 29 | Path: "/api", 30 | RequestHeaders: map[string]string{"Authorization": "Basic b2s6dGVzdA=="}, 31 | ResponseHeaders: map[string]string{}, 32 | ResponseCode: 200, 33 | }, 34 | { 35 | Name: "IncorrectCredentials", 36 | Method: "POST", 37 | Path: "/api/reviews/pay", 38 | RequestHeaders: map[string]string{"Authorization": "Basic AtRtaW46YWRtaW4="}, 39 | ResponseHeaders: map[string]string{"WWW-Authenticate": "Basic realm=istio"}, 40 | ResponseCode: 401, 41 | }, 42 | { 43 | Name: "Base64Credentials", 44 | Method: "POST", 45 | Path: "/api/reviews/pay", 46 | RequestHeaders: map[string]string{"Authorization": "Basic YWRtaW4zOmFkbWluMw=="}, 47 | ResponseHeaders: map[string]string{}, 48 | ResponseCode: 200, 49 | }, 50 | { 51 | Name: "MissingCredentials", 52 | Method: "GET", 53 | Path: "/api/reviews/pay", 54 | ResponseHeaders: map[string]string{"WWW-Authenticate": "Basic realm=istio"}, 55 | ResponseCode: 401, 56 | }, 57 | { 58 | Name: "NoPathMatch", 59 | Path: "/secret", 60 | ResponseHeaders: map[string]string{}, 61 | ResponseCode: 200, 62 | }, 63 | { 64 | Name: "NoMethodMatch", 65 | Method: "DELETE", 66 | Path: "/api/reviews/pay", 67 | ResponseHeaders: map[string]string{}, 68 | ResponseCode: 200, 69 | }, 70 | { 71 | Name: "NoConfigurationCredentialsProvided", 72 | Method: "POST", 73 | Path: "/api/reviews/pay", 74 | ResponseHeaders: map[string]string{"WWW-Authenticate": "Basic realm=istio"}, 75 | ResponseCode: 401, 76 | }, 77 | { 78 | Name: "Realm", 79 | Method: "POST", 80 | Path: "/api/reviews/pay", 81 | Realm: "test", 82 | ResponseHeaders: map[string]string{"WWW-Authenticate": "Basic realm=test"}, 83 | ResponseCode: 401, 84 | }, 85 | { 86 | Name: "HostMismatch", 87 | Method: "POST", 88 | Path: "/api/reviews/pay", 89 | Host: "\"127.0.0.2\", \"random\"", 90 | ResponseHeaders: map[string]string{}, 91 | ResponseCode: 200, 92 | }, 93 | { 94 | Name: "HostExactMatch", 95 | Method: "POST", 96 | Path: "/api/reviews/pay", 97 | Host: "\"random\", \"127.0.0.1\"", 98 | ResponseHeaders: map[string]string{}, 99 | ResponseCode: 401, 100 | }, 101 | { 102 | Name: "HostPrefixMatch", 103 | Method: "POST", 104 | Path: "/api/reviews/pay", 105 | Host: "\"random\", \"127.0.0*\"", 106 | ResponseHeaders: map[string]string{}, 107 | ResponseCode: 401, 108 | }, 109 | { 110 | Name: "HostSuffixMatch", 111 | Method: "POST", 112 | Path: "/api/reviews/pay", 113 | Host: "\"random\", \"*.0.0.1\"", 114 | ResponseHeaders: map[string]string{}, 115 | ResponseCode: 401, 116 | }, 117 | } 118 | 119 | func TestBasicAuth(t *testing.T) { 120 | for _, testCase := range TestCases { 121 | t.Run(testCase.Name, func(t *testing.T) { 122 | params := driver.NewTestParams(t, map[string]string{ 123 | "BasicAuthWasmFile": filepath.Join(env.GetBazelBinOrDie(), "extensions/basic_auth/basic_auth.wasm"), 124 | }, test.ExtensionE2ETests) 125 | if testCase.Realm != "" { 126 | params.Vars["Realm"] = testCase.Realm 127 | } 128 | if testCase.Host != "" { 129 | params.Vars["Host"] = testCase.Host 130 | } 131 | params.Vars["ServerHTTPFilters"] = params.LoadTestData("test/basicauth/testdata/server_filter.yaml.tmpl") 132 | if err := (&driver.Scenario{ 133 | Steps: []driver.Step{ 134 | &driver.XDS{}, 135 | &driver.Update{ 136 | Node: "server", Version: "0", Listeners: []string{string(testdata.MustAsset("listener/server.yaml.tmpl"))}, 137 | }, 138 | &driver.Envoy{ 139 | Bootstrap: params.FillTestData(string(testdata.MustAsset("bootstrap/server.yaml.tmpl"))), 140 | DownloadVersion: os.Getenv("ISTIO_TEST_VERSION"), 141 | }, 142 | &driver.Sleep{Duration: 1 * time.Second}, 143 | &driver.HTTPCall{ 144 | Port: params.Ports.ServerPort, 145 | Method: testCase.Method, 146 | Path: testCase.Path, 147 | RequestHeaders: testCase.RequestHeaders, 148 | ResponseHeaders: testCase.ResponseHeaders, 149 | ResponseCode: testCase.ResponseCode, 150 | }, 151 | }, 152 | }).Run(params); err != nil { 153 | t.Fatal(err) 154 | } 155 | }) 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /test/basicauth/testdata/server_filter.yaml.tmpl: -------------------------------------------------------------------------------- 1 | - name: istio.basic_auth 2 | typed_config: 3 | "@type": type.googleapis.com/udpa.type.v1.TypedStruct 4 | type_url: type.googleapis.com/envoy.extensions.filters.http.wasm.v3.Wasm 5 | value: 6 | config: 7 | vm_config: 8 | runtime: "envoy.wasm.runtime.v8" 9 | code: 10 | local: { filename: "{{ .Vars.BasicAuthWasmFile }}" } 11 | configuration: 12 | "@type": "type.googleapis.com/google.protobuf.StringValue" 13 | value: | 14 | { 15 | "basic_auth_rules": [ 16 | { 17 | {{- if .Vars.Host }} 18 | "hosts": [{{ .Vars.Host }}], 19 | {{- end }} 20 | "prefix": "/api", 21 | "request_methods":[ "GET", "POST" ], 22 | "credentials":[ "ok:test", "admin:admin", "admin2:admin2", "YWRtaW4zOmFkbWluMw==" ] 23 | }, 24 | { 25 | {{- if .Vars.Host }} 26 | "hosts": [{{ .Vars.Host }}], 27 | {{- end }} 28 | "exact": "/api", 29 | "request_methods":[ "GET", "POST" ], 30 | "credentials":[ "admin:admin", "admin2:admin2", "ok:test", "YWRtaW4zOmFkbWluMw==" ] 31 | } 32 | ], 33 | {{- if .Vars.Realm }} 34 | "realm": "{{ .Vars.Realm }}" 35 | {{- else }} 36 | "realm": "istio" 37 | {{- end }} 38 | } 39 | -------------------------------------------------------------------------------- /test/grpclogging/grpclogging_test.go: -------------------------------------------------------------------------------- 1 | package grpclogging 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | "testing" 8 | "time" 9 | 10 | "github.com/istio-ecosystem/wasm-extensions/test" 11 | "github.com/istio-ecosystem/wasm-extensions/test/grpclogging/testserver" 12 | pb "github.com/istio-ecosystem/wasm-extensions/test/grpclogging/testserver/proto" 13 | 14 | "istio.io/proxy/test/envoye2e/driver" 15 | "istio.io/proxy/test/envoye2e/env" 16 | "istio.io/proxy/testdata" 17 | ) 18 | 19 | func TestGrpcLogging(t *testing.T) { 20 | params := driver.NewTestParams(t, map[string]string{ 21 | "GrpcLoggingWasmFile": filepath.Join(env.GetBazelBinOrDie(), "extensions/grpc_logging/grpc_logging.wasm"), 22 | "ServerMetadata": driver.LoadTestData("test/grpclogging/testdata/node_metadata.yaml.tmpl"), 23 | }, test.ExtensionE2ETests) 24 | 25 | params.Vars["LoggingPort"] = fmt.Sprintf("%d", params.Ports.Max+1) 26 | params.Vars["ServerHTTPFilters"] = params.LoadTestData("test/grpclogging/testdata/server_filter.yaml.tmpl") 27 | 28 | srv := &testserver.Server{Port: params.Ports.Max + 1} 29 | if err := (&driver.Scenario{ 30 | Steps: []driver.Step{ 31 | &driver.XDS{}, 32 | srv, 33 | &driver.Update{ 34 | Node: "server", Version: "0", Listeners: []string{string(testdata.MustAsset("listener/server.yaml.tmpl"))}, 35 | }, 36 | &driver.Envoy{ 37 | Bootstrap: params.FillTestData(string(testdata.MustAsset("bootstrap/server.yaml.tmpl"))), 38 | DownloadVersion: os.Getenv("ISTIO_TEST_VERSION"), 39 | }, 40 | &driver.Sleep{1 * time.Second}, 41 | &driver.HTTPCall{ 42 | Port: params.Ports.ServerPort, 43 | Method: "GET", 44 | }, 45 | &testserver.VerifyLogs{ 46 | WantRequest: &pb.WriteLogRequest{ 47 | LogEntries: []*pb.WriteLogRequest_LogEntry{ 48 | { 49 | DestinationWorkload: "echo-server", 50 | DestinationNamespace: "test", 51 | DestinationAddress: "127.0.0.1:20243", 52 | Host: "127.0.0.1:20243", 53 | Path: "/", 54 | ResponseCode: 200, 55 | }, 56 | }, 57 | }, 58 | Server: srv, 59 | }, 60 | }}).Run(params); err != nil { 61 | t.Fatal(err) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /test/grpclogging/testdata/node_metadata.yaml.tmpl: -------------------------------------------------------------------------------- 1 | "WORKLOAD_NAME": "echo-server", 2 | "NAMESPACE": "test" -------------------------------------------------------------------------------- /test/grpclogging/testdata/server_filter.yaml.tmpl: -------------------------------------------------------------------------------- 1 | - name: istio.grpc_logging 2 | typed_config: 3 | "@type": type.googleapis.com/udpa.type.v1.TypedStruct 4 | type_url: type.googleapis.com/envoy.extensions.filters.http.wasm.v3.Wasm 5 | value: 6 | config: 7 | vm_config: 8 | runtime: "envoy.wasm.runtime.v8" 9 | code: 10 | local: { filename: "{{ .Vars.GrpcLoggingWasmFile }}" } 11 | configuration: 12 | "@type": "type.googleapis.com/google.protobuf.StringValue" 13 | value: | 14 | { "logging_service": "localhost:{{ .Vars.LoggingPort }}" } 15 | -------------------------------------------------------------------------------- /test/grpclogging/testserver/proto/log_grpc.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go-grpc. DO NOT EDIT. 2 | 3 | package istio_ecosystem_wasm_extensions_grpc_logging 4 | 5 | import ( 6 | context "context" 7 | grpc "google.golang.org/grpc" 8 | codes "google.golang.org/grpc/codes" 9 | status "google.golang.org/grpc/status" 10 | ) 11 | 12 | // This is a compile-time assertion to ensure that this generated file 13 | // is compatible with the grpc package it is being compiled against. 14 | // Requires gRPC-Go v1.32.0 or later. 15 | const _ = grpc.SupportPackageIsVersion7 16 | 17 | // LoggingServiceClient is the client API for LoggingService service. 18 | // 19 | // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. 20 | type LoggingServiceClient interface { 21 | WriteLog(ctx context.Context, in *WriteLogRequest, opts ...grpc.CallOption) (*WriteLogResponse, error) 22 | } 23 | 24 | type loggingServiceClient struct { 25 | cc grpc.ClientConnInterface 26 | } 27 | 28 | func NewLoggingServiceClient(cc grpc.ClientConnInterface) LoggingServiceClient { 29 | return &loggingServiceClient{cc} 30 | } 31 | 32 | func (c *loggingServiceClient) WriteLog(ctx context.Context, in *WriteLogRequest, opts ...grpc.CallOption) (*WriteLogResponse, error) { 33 | out := new(WriteLogResponse) 34 | err := c.cc.Invoke(ctx, "/istio_ecosystem.wasm_extensions.grpc_logging.LoggingService/WriteLog", in, out, opts...) 35 | if err != nil { 36 | return nil, err 37 | } 38 | return out, nil 39 | } 40 | 41 | // LoggingServiceServer is the server API for LoggingService service. 42 | // All implementations must embed UnimplementedLoggingServiceServer 43 | // for forward compatibility 44 | type LoggingServiceServer interface { 45 | WriteLog(context.Context, *WriteLogRequest) (*WriteLogResponse, error) 46 | mustEmbedUnimplementedLoggingServiceServer() 47 | } 48 | 49 | // UnimplementedLoggingServiceServer must be embedded to have forward compatible implementations. 50 | type UnimplementedLoggingServiceServer struct { 51 | } 52 | 53 | func (UnimplementedLoggingServiceServer) WriteLog(context.Context, *WriteLogRequest) (*WriteLogResponse, error) { 54 | return nil, status.Errorf(codes.Unimplemented, "method WriteLog not implemented") 55 | } 56 | func (UnimplementedLoggingServiceServer) mustEmbedUnimplementedLoggingServiceServer() {} 57 | 58 | // UnsafeLoggingServiceServer may be embedded to opt out of forward compatibility for this service. 59 | // Use of this interface is not recommended, as added methods to LoggingServiceServer will 60 | // result in compilation errors. 61 | type UnsafeLoggingServiceServer interface { 62 | mustEmbedUnimplementedLoggingServiceServer() 63 | } 64 | 65 | func RegisterLoggingServiceServer(s grpc.ServiceRegistrar, srv LoggingServiceServer) { 66 | s.RegisterService(&LoggingService_ServiceDesc, srv) 67 | } 68 | 69 | func _LoggingService_WriteLog_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 70 | in := new(WriteLogRequest) 71 | if err := dec(in); err != nil { 72 | return nil, err 73 | } 74 | if interceptor == nil { 75 | return srv.(LoggingServiceServer).WriteLog(ctx, in) 76 | } 77 | info := &grpc.UnaryServerInfo{ 78 | Server: srv, 79 | FullMethod: "/istio_ecosystem.wasm_extensions.grpc_logging.LoggingService/WriteLog", 80 | } 81 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 82 | return srv.(LoggingServiceServer).WriteLog(ctx, req.(*WriteLogRequest)) 83 | } 84 | return interceptor(ctx, in, info, handler) 85 | } 86 | 87 | // LoggingService_ServiceDesc is the grpc.ServiceDesc for LoggingService service. 88 | // It's only intended for direct use with grpc.RegisterService, 89 | // and not to be introspected or modified (even as a copy) 90 | var LoggingService_ServiceDesc = grpc.ServiceDesc{ 91 | ServiceName: "istio_ecosystem.wasm_extensions.grpc_logging.LoggingService", 92 | HandlerType: (*LoggingServiceServer)(nil), 93 | Methods: []grpc.MethodDesc{ 94 | { 95 | MethodName: "WriteLog", 96 | Handler: _LoggingService_WriteLog_Handler, 97 | }, 98 | }, 99 | Streams: []grpc.StreamDesc{}, 100 | Metadata: "extensions/grpc_logging/log.proto", 101 | } 102 | -------------------------------------------------------------------------------- /test/grpclogging/testserver/server.go: -------------------------------------------------------------------------------- 1 | package testserver 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "log" 8 | "net" 9 | "sync" 10 | "time" 11 | 12 | "github.com/golang/protobuf/proto" 13 | pb "github.com/istio-ecosystem/wasm-extensions/test/grpclogging/testserver/proto" 14 | 15 | "google.golang.org/grpc" 16 | "istio.io/proxy/test/envoye2e/driver" 17 | ) 18 | 19 | type Server struct { 20 | Port uint16 21 | s *grpc.Server 22 | Request *pb.WriteLogRequest 23 | Mux sync.Mutex 24 | pb.UnimplementedLoggingServiceServer 25 | } 26 | 27 | var _ driver.Step = &Server{} 28 | var _ pb.LoggingServiceServer = &Server{} 29 | 30 | func (srv *Server) runLoggingServer() { 31 | lis, err := net.Listen("tcp", fmt.Sprintf(":%v", srv.Port)) 32 | if err != nil { 33 | log.Fatalf("failed to listen: %v", err) 34 | } 35 | fmt.Printf("start listening on %v \n", srv.Port) 36 | srv.s = grpc.NewServer() 37 | pb.RegisterLoggingServiceServer(srv.s, srv) 38 | if err := srv.s.Serve(lis); err != nil { 39 | log.Fatalf("failed to serve: %v", err) 40 | } 41 | } 42 | 43 | func (srv *Server) Run(p *driver.Params) error { 44 | go srv.runLoggingServer() 45 | return nil 46 | } 47 | 48 | func (srv *Server) Cleanup() { 49 | srv.s.Stop() 50 | } 51 | 52 | func (srv *Server) WriteLog(ctx context.Context, in *pb.WriteLogRequest) (*pb.WriteLogResponse, error) { 53 | srv.Mux.Lock() 54 | defer srv.Mux.Unlock() 55 | srv.Request = in 56 | return &pb.WriteLogResponse{}, nil 57 | } 58 | 59 | type VerifyLogs struct { 60 | WantRequest *pb.WriteLogRequest 61 | Server *Server 62 | } 63 | 64 | func (v *VerifyLogs) Run(p *driver.Params) error { 65 | for i := 0; i < 20; i++ { 66 | // Expect only one log entry was received 67 | v.Server.Mux.Lock() 68 | if v.Server.Request != nil { 69 | v.Server.Request.LogEntries[0].Timestamp = nil 70 | v.Server.Request.LogEntries[0].Latency = nil 71 | v.Server.Request.LogEntries[0].SourceAddress = "" 72 | v.Server.Request.LogEntries[0].RequestId = "" 73 | if proto.Equal(v.Server.Request, v.WantRequest) { 74 | return nil 75 | } 76 | return fmt.Errorf("log request want %+v, got %+v", v.WantRequest, v.Server.Request) 77 | } 78 | v.Server.Mux.Unlock() 79 | fmt.Printf("no log entry received, retry after 1 sec") 80 | time.Sleep(1 * time.Second) 81 | } 82 | return errors.New("time out on waiting for log request") 83 | } 84 | 85 | func (v *VerifyLogs) Cleanup() {} 86 | -------------------------------------------------------------------------------- /test/inventory.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | env "istio.io/proxy/test/envoye2e/env" 5 | ) 6 | 7 | var ExtensionE2ETests *env.TestInventory 8 | 9 | func init() { 10 | ExtensionE2ETests = &env.TestInventory{ 11 | Tests: []string{ 12 | "TestBasicAuth/CorrectCredentials", 13 | "TestBasicAuth/IncorrectCredentials", 14 | "TestBasicAuth/MissingCredentials", 15 | "TestBasicAuth/NoPathMatch", 16 | "TestBasicAuth/NoMethodMatch", 17 | "TestBasicAuth/NoConfigurationCredentialsProvided", 18 | "TestBasicAuth/Realm", 19 | "TestBasicAuth/HostMismatch", 20 | "TestBasicAuth/HostExactMatch", 21 | "TestBasicAuth/HostPrefixMatch", 22 | "TestBasicAuth/HostSuffixMatch", 23 | "TestLocalRateLimit", 24 | "TestGrpcLogging", 25 | "TestOPA/allow", 26 | "TestOPA/deny", 27 | "TestOPA/cache_expire", 28 | "TestBasicAuth/Base64Credentials", 29 | "TestExamplePlugin", 30 | }, 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /test/localratelimit/localratelimit_test.go: -------------------------------------------------------------------------------- 1 | package localratelimit 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "testing" 7 | "time" 8 | 9 | "istio.io/proxy/test/envoye2e/driver" 10 | "istio.io/proxy/test/envoye2e/env" 11 | "istio.io/proxy/testdata" 12 | 13 | "github.com/istio-ecosystem/wasm-extensions/test" 14 | ) 15 | 16 | func TestLocalRateLimit(t *testing.T) { 17 | params := driver.NewTestParams(t, map[string]string{ 18 | "LocalRateLimitWasmFile": filepath.Join(env.GetBazelBinOrDie(), "extensions/local_rate_limit/local_rate_limit.wasm"), 19 | }, test.ExtensionE2ETests) 20 | params.Vars["ServerHTTPFilters"] = params.LoadTestData("test/localratelimit/testdata/server_filter.yaml.tmpl") 21 | if err := (&driver.Scenario{ 22 | Steps: []driver.Step{ 23 | &driver.XDS{}, 24 | &driver.Update{ 25 | Node: "server", Version: "0", Listeners: []string{string(testdata.MustAsset("listener/server.yaml.tmpl"))}, 26 | }, 27 | &driver.Envoy{ 28 | Bootstrap: params.FillTestData(string(testdata.MustAsset("bootstrap/server.yaml.tmpl"))), 29 | DownloadVersion: os.Getenv("ISTIO_TEST_VERSION"), 30 | }, 31 | &driver.Sleep{Duration: 3 * time.Second}, 32 | // Test with max token 20, per refill 10, and refill interval 1s. 33 | // The first 20 request will get 200, then next 10 request will get 429. 34 | &driver.Repeat{ 35 | N: 20, 36 | Step: &driver.HTTPCall{ 37 | Port: params.Ports.ServerPort, 38 | ResponseCode: 200, 39 | }, 40 | }, 41 | &driver.Repeat{ 42 | N: 10, 43 | Step: &driver.HTTPCall{ 44 | Port: params.Ports.ServerPort, 45 | ResponseCode: 429, 46 | Body: "rate_limited", 47 | }, 48 | }, 49 | // Sleep 1s + 500ms headroom, and token bucket should be refilled with 10 tokens. 50 | &driver.Sleep{Duration: 1500 * time.Millisecond}, 51 | &driver.Repeat{ 52 | N: 10, 53 | Step: &driver.HTTPCall{ 54 | Port: params.Ports.ServerPort, 55 | ResponseCode: 200, 56 | }, 57 | }, 58 | &driver.Repeat{ 59 | N: 10, 60 | Step: &driver.HTTPCall{ 61 | Port: params.Ports.ServerPort, 62 | ResponseCode: 429, 63 | Body: "rate_limited", 64 | }, 65 | }, 66 | }, 67 | }).Run(params); err != nil { 68 | t.Fatal(err) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /test/localratelimit/testdata/server_filter.yaml.tmpl: -------------------------------------------------------------------------------- 1 | - name: istio.local_rate_limit 2 | typed_config: 3 | "@type": type.googleapis.com/udpa.type.v1.TypedStruct 4 | type_url: type.googleapis.com/envoy.extensions.filters.http.wasm.v3.Wasm 5 | value: 6 | config: 7 | vm_config: 8 | runtime: "envoy.wasm.runtime.v8" 9 | code: 10 | local: { filename: "{{ .Vars.LocalRateLimitWasmFile }}" } 11 | configuration: 12 | "@type": "type.googleapis.com/google.protobuf.StringValue" 13 | value: | 14 | { 15 | "max_tokens": 20, 16 | "tokens_per_refill": 10, 17 | "refill_interval_sec": 1 18 | } 19 | -------------------------------------------------------------------------------- /test/opa/opa_test.go: -------------------------------------------------------------------------------- 1 | package opa 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "strconv" 7 | "testing" 8 | "time" 9 | 10 | "github.com/istio-ecosystem/wasm-extensions/test" 11 | opa "github.com/istio-ecosystem/wasm-extensions/test/opa/server" 12 | 13 | "istio.io/proxy/test/envoye2e/driver" 14 | "istio.io/proxy/test/envoye2e/env" 15 | "istio.io/proxy/testdata" 16 | ) 17 | 18 | func TestOPA(t *testing.T) { 19 | var tests = []struct { 20 | name string 21 | method string 22 | cacheHit int 23 | cacheMiss int 24 | requestCount int 25 | delay time.Duration 26 | wantRespCode int 27 | }{ 28 | {"allow", "GET", 9, 1, 10, 0, 200}, 29 | {"deny", "POST", 9, 1, 10, 0, 403}, 30 | {"cache_expire", "POST", 2, 2, 4, 4 * time.Second, 403}, 31 | } 32 | for _, tt := range tests { 33 | t.Run(tt.name, func(t *testing.T) { 34 | params := driver.NewTestParams(t, map[string]string{ 35 | "ClientTLSContext": driver.LoadTestData("test/opa/testdata/transport_socket/client_tls_context.yaml.tmpl"), 36 | "ServerTLSContext": driver.LoadTestData("test/opa/testdata/transport_socket/server_tls_context.yaml.tmpl"), 37 | "ServerStaticCluster": driver.LoadTestData("test/opa/testdata/resource/opa_cluster.yaml.tmpl"), 38 | "ServerMetadata": driver.LoadTestData("test/opa/testdata/resource/server_node_metadata.yaml.tmpl"), 39 | "OpaPluginFilePath": filepath.Join(env.GetBazelBinOrDie(), "extensions/open_policy_agent/open_policy_agent.wasm"), 40 | "CacheHit": strconv.Itoa(tt.cacheHit), 41 | "CacheMiss": strconv.Itoa(tt.cacheMiss), 42 | }, test.ExtensionE2ETests) 43 | params.Vars["ServerHTTPFilters"] = params.LoadTestData("test/opa/testdata/resource/opa_filter.yaml.tmpl") 44 | 45 | if err := (&driver.Scenario{ 46 | Steps: []driver.Step{ 47 | &driver.XDS{}, 48 | &driver.Update{ 49 | Node: "server", Version: "0", Listeners: []string{string(testdata.MustAsset("listener/server.yaml.tmpl"))}, 50 | }, 51 | &driver.Update{ 52 | Node: "client", Version: "0", Listeners: []string{string(testdata.MustAsset("listener/client.yaml.tmpl"))}, 53 | }, 54 | &opa.OpaServer{RuleFilePath: driver.TestPath("test/opa/testdata/rule/opa_rule.rego")}, 55 | &driver.Envoy{ 56 | Bootstrap: params.FillTestData(string(testdata.MustAsset("bootstrap/server.yaml.tmpl"))), 57 | DownloadVersion: os.Getenv("ISTIO_TEST_VERSION"), 58 | }, 59 | &driver.Envoy{ 60 | Bootstrap: params.FillTestData(string(testdata.MustAsset("bootstrap/client.yaml.tmpl"))), 61 | DownloadVersion: os.Getenv("ISTIO_TEST_VERSION"), 62 | }, 63 | &driver.Repeat{ 64 | N: tt.requestCount, 65 | Step: &driver.Scenario{ 66 | Steps: []driver.Step{ 67 | &driver.HTTPCall{ 68 | Port: params.Ports.ClientPort, 69 | Method: tt.method, 70 | Path: "/echo", 71 | ResponseCode: tt.wantRespCode, 72 | }, 73 | &driver.Sleep{Duration: tt.delay}, 74 | }, 75 | }, 76 | }, 77 | &driver.Stats{ 78 | AdminPort: params.Ports.ServerAdmin, 79 | Matchers: map[string]driver.StatMatcher{ 80 | "wasm_filter_opa_filter_cache_hit_policy_cache_count": &driver. 81 | ExactStat{Metric: "test/opa/testdata/stats/cache_hit.yaml.tmpl"}, 82 | "wasm_filter_opa_filter_cache_miss_policy_cache_count": &driver. 83 | ExactStat{Metric: "test/opa/testdata/stats/cache_miss.yaml.tmpl"}, 84 | }, 85 | }, 86 | }}).Run(params); err != nil { 87 | t.Fatal(err) 88 | } 89 | }) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /test/opa/server/server.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "io/ioutil" 7 | "net/http" 8 | "os" 9 | "os/exec" 10 | "path/filepath" 11 | 12 | framework "istio.io/proxy/test/envoye2e/driver" 13 | ) 14 | 15 | // OpaServer models an OPA server process. 16 | type OpaServer struct { 17 | opaProcess *os.Process 18 | tmpDir string 19 | RuleFilePath string 20 | } 21 | 22 | var _ framework.Step = &OpaServer{} 23 | 24 | // Run starts an OPA server 25 | func (o *OpaServer) Run(p *framework.Params) error { 26 | opaPath, err := downloadOpaServer() 27 | if err != nil { 28 | return err 29 | } 30 | o.tmpDir = filepath.Dir(opaPath) 31 | 32 | // Run Opa Server with given rule file 33 | opaServerCmd := fmt.Sprintf("%v run --server --log-level debug %v", opaPath, o.RuleFilePath) 34 | fmt.Printf("start opa server: %v\n", opaServerCmd) 35 | opaCmd := exec.Command("bash", "-c", opaServerCmd) 36 | opaCmd.Stderr = os.Stderr 37 | opaCmd.Stdout = os.Stdout 38 | err = opaCmd.Start() 39 | if err != nil { 40 | return err 41 | } 42 | o.opaProcess = opaCmd.Process 43 | return nil 44 | } 45 | 46 | // Cleanup closes an OPA server process. 47 | func (o *OpaServer) Cleanup() { 48 | os.Remove(o.tmpDir) 49 | o.opaProcess.Kill() 50 | } 51 | 52 | func downloadOpaServer() (string, error) { 53 | tmpdDir, err := ioutil.TempDir("", "opa-") 54 | dst := fmt.Sprintf("%s/%s", tmpdDir, "opa") 55 | 56 | opaURL := "https://openpolicyagent.org/downloads/latest/opa_linux_amd64" 57 | resp, err := http.Get(opaURL) 58 | if err != nil { 59 | return "", err 60 | } 61 | defer resp.Body.Close() 62 | if err != nil { 63 | return "", fmt.Errorf("fail to download opa: %v", err) 64 | } 65 | outFile, err := os.OpenFile(dst, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0755) 66 | if err != nil { 67 | return "", err 68 | } 69 | defer outFile.Close() 70 | 71 | // Write the body to file 72 | _, err = io.Copy(outFile, resp.Body) 73 | 74 | return dst, nil 75 | } 76 | -------------------------------------------------------------------------------- /test/opa/testdata/certs/client-key.cert: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEogIBAAKCAQEA07MQ4NQQrnDxl3gwHh5NNUyJzrYmK57GogtoJype0jrEjldw 3 | XZCYnvJEf9DJs1wtZ5p7Zij5wgM0vJsRB1BMM/uH8M8OikmlCyoajmA7wk5VVSRy 4 | 56h6ni14T93YOHEGlmnJOF7mkav1940ppuiNeT4V6+f8SDX+M5z1NplnkoOQAPqh 5 | 9s9191dpQC4lGinstioMFdnbXXvdtBFcgzDIsKxEL9/EHM/fCSQrcz0+SeJY0RSM 6 | 1GrqnnyWGfTQ/77R6pvhtMbW5ULUR4jKuQ5qvYGyLdn9Xh+k/8u+UXlF50Ndj3VC 7 | kxHGnzFEZFy8QtRCd1jiYsh6HZWgOAeAzUkqQQIDAQABAoIBAGhJtU3cil80+n7w 8 | 0Vt09/oCu3yelM02SYn4bpWktNOB6eRpRMyC9/yNQptoooR+K0v3eUTJeMhPxgIH 9 | rerZbsDI7538kqAjSW/njO+IjsfYyQbJjuV6RPV5VuSZV/PuEh20/VCMx68JdIFA 10 | BD3aIB+TKz9sqAZ2usR4VQBRsAknat5RhdcE7CGcbEiNGbSn1ASqTif+oiRZBnTZ 11 | hXik9gbjMi57nG2Mq8Ww0XZGAsFY+NxCldTVI//GwHT38uatjUvF8c/pfpKfDpam 12 | iD7U0EJsPUh/nzITCX+py7BDYcYDByhLWbgviH7/CoMT3wnggZQfpljepEG9PqMF 13 | 59FYAoUCgYEA9sv853zOR50msCs/66ohh5zbC/1DH1J9yWsk0VsmH096zfgUGCqP 14 | 0aTT7b25XnZYkvGiWslp8IEHc6ADEkwGsp88i0EvVO2pK/3xdAaCReVqF6jZs9Dn 15 | 0CuJHmJfgZaJsGT8ofQfUOSWhddLXGcLHMinjaPZOakn8XAizbtcoRsCgYEA25gG 16 | pdD1xwU07y8iVY6gxsDQbNRJbAkgtVju/8fIkqe/PxhvwUhxF8zdlL29+P/PYBjw 17 | P4L9zHVXQUKqV4clBECuhA31Yz9zhfivzz6y4NLzM7+6EzjQ4TOLlo2Vp7oPjN3y 18 | 29NHbPqG4JEwJ8aqXJqtWMUUdp4LuF+N5dkIM9MCgYAPXUOxZaOx8aam8QpZsY3E 19 | 048PgATdvlT2ZSU1o2cMK/aJPBiEKKIrewd2lYkkyFlbTI++9ysRPfcoy51lVjZU 20 | iHVMdhJsRx9xDa4qev1BPLcOIgTrnOXRn+Q5cAZiGu0XfjH8IyaP8qssSer3JbMb 21 | Z6KGvtyXKmDCNyjzheaOYQKBgAsBysuC9t7b9vRKQ4lQVeTAg3IBDhEZQAd3BrvR 22 | cs9PEzoBapCgpfKQdUbgX+ZcRDPH7DrywO//rbj6s3khsAxPha/e1z77TjoX5hAY 23 | T3UPfdtJL/WIsoenQsbwH+FBZUglU+gK5hijUiFthaFoxt9PbYL2lfkAIQxD1eQA 24 | hfW7AoGAGflnz6ea3u1j0hAFykKZ3D/82/qCjaB2H2jmdpl2xHodhUodrmYWkDBu 25 | vB28ez8aTx8QlIk0GIVVYolM7zlyCBs6FXrH34fL9P44najLqfQUbKdUUOqb6lfT 26 | BC1Kcvm8frJLtUUqFTkgtgrRlqihIja1uvF65VoHF1AfjUslhdc= 27 | -----END RSA PRIVATE KEY----- -------------------------------------------------------------------------------- /test/opa/testdata/certs/client.cert: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDXDCCAkSgAwIBAgIQUwQ9hAAm16Yf+PkWD1VM/jANBgkqhkiG9w0BAQsFADBD 3 | MQswCQYDVQQGEwJVUzELMAkGA1UECAwCQ0ExEjAQBgNVBAcMCVN1bm55dmFsZTET 4 | MBEGA1UECgwKZ29vZ2xlLmNvbTAeFw0xOTA4MTIxODU2MDhaFw0yNDA4MTAxODU2 5 | MDhaMBMxETAPBgNVBAoTCEp1anUgb3JnMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A 6 | MIIBCgKCAQEA07MQ4NQQrnDxl3gwHh5NNUyJzrYmK57GogtoJype0jrEjldwXZCY 7 | nvJEf9DJs1wtZ5p7Zij5wgM0vJsRB1BMM/uH8M8OikmlCyoajmA7wk5VVSRy56h6 8 | ni14T93YOHEGlmnJOF7mkav1940ppuiNeT4V6+f8SDX+M5z1NplnkoOQAPqh9s91 9 | 91dpQC4lGinstioMFdnbXXvdtBFcgzDIsKxEL9/EHM/fCSQrcz0+SeJY0RSM1Grq 10 | nnyWGfTQ/77R6pvhtMbW5ULUR4jKuQ5qvYGyLdn9Xh+k/8u+UXlF50Ndj3VCkxHG 11 | nzFEZFy8QtRCd1jiYsh6HZWgOAeAzUkqQQIDAQABo3wwejAOBgNVHQ8BAf8EBAMC 12 | BaAwDAYDVR0TAQH/BAIwADAfBgNVHSMEGDAWgBT1a7HehaEjoID50KbCqhryIRwh 13 | ETA5BgNVHREBAf8ELzAthitzcGlmZmU6Ly9jbHVzdGVyLmxvY2FsL25zL2RlZmF1 14 | bHQvc2EvY2xpZW50MA0GCSqGSIb3DQEBCwUAA4IBAQBW/xkRoVxuo+g9P6/mWuVI 15 | BSY7tsrdff8qkKzEmRLLSgMUFpDw5529wUSAsOwPjHK9xXeCT5lLxQMcbaGShf70 16 | 4r/lceFJXUpQ0NHU6uJx3DdTUXXhDc4Zhq6rX1GaxqYvKWVMAKCPmDEXVHd5Yh4u 17 | ZZIeq1uOTc7t3B6wXhQ68zY2GURjEMksafoCT65J/2CD5fBgBFOEeYxCl4iN5Vcv 18 | MM+xfi1ZiGTAakiCSSOUydaP5MBdbl04ZMKDDEZTRLJwEDmg0T1x6/T7zumtjrnX 19 | 5T4c/LV5cEMMb4vjty5MSNY/8t5dT6Bq8T4tAEN83W2OyABfSowyecXAItcMcZ66 20 | -----END CERTIFICATE----- -------------------------------------------------------------------------------- /test/opa/testdata/certs/root.cert: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDZzCCAk+gAwIBAgIUMzjfEUF3LQ/WfBiwIC9h+qndbGYwDQYJKoZIhvcNAQEL 3 | BQAwQzELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMRIwEAYDVQQHDAlTdW5ueXZh 4 | bGUxEzARBgNVBAoMCmdvb2dsZS5jb20wHhcNMTkwODEyMTgzMTAyWhcNMjQwODEw 5 | MTgzMTAyWjBDMQswCQYDVQQGEwJVUzELMAkGA1UECAwCQ0ExEjAQBgNVBAcMCVN1 6 | bm55dmFsZTETMBEGA1UECgwKZ29vZ2xlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQAD 7 | ggEPADCCAQoCggEBAL2O52MbZig1pfU1tut+QX/ISI3m2uMi079ZWy4ZE+Ccm4Ta 8 | XdR66T94T2x7uWbT2AtNIxZO+LPT75Suh1Zb/O1px3dKul7U1Fpl7gLVnKXQ35zL 9 | /fCh7MPa+aipZHH1KGG56ebdmoXrKM+S5k502Dm0Q0uyGxksBAiXHyixaiq00rYV 10 | XYrv9qw1wphYea2SLBRaQOpJrPI1CZu267LTMTq9a6gGTwMuz9tDveT/cM8Nh17C 11 | so+6PrLEbpXAJPqNUyuJBGsDG9AyqBh4ZKmgRDR+ZE03jNncaEx2vkjFenXLI+// 12 | YgZA1NJVAefCFfGRNGRZ+bR/01brUbnuGJCgJv0CAwEAAaNTMFEwHQYDVR0OBBYE 13 | FPVrsd6FoSOggPnQpsKqGvIhHCERMB8GA1UdIwQYMBaAFPVrsd6FoSOggPnQpsKq 14 | GvIhHCERMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBALKX8nmy 15 | SN8+MB5cSj/LymQpYlJVdvf0p2cBikWCVcAWL+CvBafYF0Y93ooKbv/jCZhWdGmz 16 | ItbJjwauaXDphHEGbAzyjsQXH1ZQti6+HigMIvTOYuqiOd+Lstdim9QHvgLCywT0 17 | PJ3k44/KyfEXN870heJmEDN4uv+hASmH+9zvhRqE/ABnb2An4auQT5j3/BXU0jjl 18 | sv3XDZ/Ke4PXqPptg4VGbhQi1+OUFoqAgvQFGbur0hnWFPsehC29kISMAJt/iTGJ 19 | HC0g4ZKkij56ohHIB6OLNJ1rGMS9OFwt+0ok0AI7kVI5K3KLdhPEY1k48t6ThFCn 20 | wPWDdGnjesEmztc= 21 | -----END CERTIFICATE----- -------------------------------------------------------------------------------- /test/opa/testdata/certs/server-key.cert: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEpAIBAAKCAQEA4vmrWwXSHIOLtRzMnPaAx2jB7kVzUd2PMIddSczm1JJFYusE 3 | GV0JoDGK3ZNh5bz0Ye7F/mKAOlv1a13sAJ8jcx7GC0UgbIbA2+0b/sOGXXu/9rhp 4 | ahLa7wC6mxdlXE85EwAk/EwCkgXqD5Zrbq6rinJ1Rw3S8AxPrIT422FzpHV+mjAa 5 | PW9qcC8w53/aMAxg80uBcge/4gYamD3bDnoRQp0bywrv3gtE+d20OusA8gN99x93 6 | 6yoEotVukZkq8Dbj7CQFhXvPhDkBTIm4cynA1h9V0GEgWCr9DDWV3A9nftL9bDPT 7 | r4oznZSJG41+VTx9/tLY1imjvRe6lqGgxRgdcwIDAQABAoIBAQDSZz9BkZPEeuz3 8 | Z0sF9jxKngGoLxlHumsSQWlpEFiqlS1dFR8no+dYaJSh8g2+OfsRDZbcydK0Rqqq 9 | bNZpfRwPi2dq6xmzgPcm6BYbhIT6A81fmHOfsPris3pIate7SnVN98RRXOTFGFZx 10 | PK86WxEJtjChPV9cxwzUkC9grmXU/Jbk1Dfdn2karEGnzwwhpZjsukUG/c1ug6Ig 11 | 6Wa1Ml5uxU0TAx44IFi3c6kMLf3hJVOc5wDtA196TGfhcAKBUYDW4DhOWt5gkg+C 12 | YYry1zLfTrMt019bnMp4AG8ximmAhkH+As9G/v4Qg7+oPJ4CdQ7uJUd7HSfbl8U5 13 | jgefqPuBAoGBAPb/alLTN3BiJAh2JnffTQYOpLQdQ8kamoNcA+WtxRKEOyLii9W2 14 | UOieDiyzZqFuvqRqNmPhWuC04Q55TIZKZHFW23KEiLspIZxp47Zghy7IV99xR/bo 15 | TcWNGVh8CuJpO5u8sc863+x2hO61oWe/S3d82sW+ffsswYMQs6Gg6NNFAoGBAOs/ 16 | b9SN6+WVZ1a+i0JsC8RDbtWvo1AyE6uT03IL2jJRAlGIWjejc+bPUbfwU/1IdrjG 17 | LJOVSVK63cep5Zsz/1dgfOWZ6nabvzTLhLaKxXiKgKjABeQhvRk0OfE/aZsVy2ul 18 | X9iXH/mNZj09A1KHB0TKswLXmbY2quUg2dUlu51XAoGAXGd1mYLXbL3qiRfakGID 19 | 6M41pASGxYekYpxcAOMfpSu/C/ABLHTGlB/9YY/ER4Ss4cmyi29VlldVExsiG+Nc 20 | 7GH4O0GF/a8HmgKrZCF8sW3WIgu5Ro/l+JAu+UF+uPFxkXPoeYSnHUnBtaRRvAR+ 21 | 8TbOicgYTY2S37ux2DfgopkCgYBwClWLqVA5lu+Ru8x9hRIRloA6G52vezotFIm3 22 | Hnf8UOLGzCcTqrBvtDvaXAbUcefBVvkyDP7P/RnVl1A4nAo3pke13plxhfoJ/ggm 23 | HG+yWlyugk4L+hmi4GHcSXRVnYq1qRy9/jQHWdXgwqdLbe4DUHrzlpWp192KpRu6 24 | TW9OnwKBgQDpM23NlpduTo0iCsKvTcjSZUrz7tQJ12T740ZjWe1s8vNvOcRoeO8A 25 | JQzVhxxOQx8mC3+NsbMWjkACAS5z6byC1rle88Gexnw5pT7MlaZU3xnxMukRIso/ 26 | Oo3EnpZzE6UlZ782oz1ibrpEGqn112marhzUIwoM/PnhNHKlTZLH/w== 27 | -----END RSA PRIVATE KEY----- -------------------------------------------------------------------------------- /test/opa/testdata/certs/server.cert: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDXDCCAkSgAwIBAgIQVTt6pYOM9fp3zF1NXUUJojANBgkqhkiG9w0BAQsFADBD 3 | MQswCQYDVQQGEwJVUzELMAkGA1UECAwCQ0ExEjAQBgNVBAcMCVN1bm55dmFsZTET 4 | MBEGA1UECgwKZ29vZ2xlLmNvbTAeFw0xOTA4MTIxODU1NDlaFw0yNDA4MTAxODU1 5 | NDlaMBMxETAPBgNVBAoTCEp1anUgb3JnMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A 6 | MIIBCgKCAQEA4vmrWwXSHIOLtRzMnPaAx2jB7kVzUd2PMIddSczm1JJFYusEGV0J 7 | oDGK3ZNh5bz0Ye7F/mKAOlv1a13sAJ8jcx7GC0UgbIbA2+0b/sOGXXu/9rhpahLa 8 | 7wC6mxdlXE85EwAk/EwCkgXqD5Zrbq6rinJ1Rw3S8AxPrIT422FzpHV+mjAaPW9q 9 | cC8w53/aMAxg80uBcge/4gYamD3bDnoRQp0bywrv3gtE+d20OusA8gN99x936yoE 10 | otVukZkq8Dbj7CQFhXvPhDkBTIm4cynA1h9V0GEgWCr9DDWV3A9nftL9bDPTr4oz 11 | nZSJG41+VTx9/tLY1imjvRe6lqGgxRgdcwIDAQABo3wwejAOBgNVHQ8BAf8EBAMC 12 | BaAwDAYDVR0TAQH/BAIwADAfBgNVHSMEGDAWgBT1a7HehaEjoID50KbCqhryIRwh 13 | ETA5BgNVHREBAf8ELzAthitzcGlmZmU6Ly9jbHVzdGVyLmxvY2FsL25zL2RlZmF1 14 | bHQvc2Evc2VydmVyMA0GCSqGSIb3DQEBCwUAA4IBAQCsBUDD33vlXI1FvwZuqSZ5 15 | zHQtH7N9jFtPu8qTkhHTlnA/Tt5S0IxuZDt2XfAhzYyQOgP6z8yVxdDP4FSlQuXq 16 | TrFr9tT4DGBOh44oV/SYUX5zn9RFJ+HJ22U5cEUo+WpqTx/vQzrm4kI3KMZ7Augt 17 | W915b1lkjrVlW+pnT7gGNYX4DD7cDX3vKfWDb78zb5hhdbyX/8jJx4BRfvdmO0E8 18 | qbpQgGZj5sbhmJ7a4bGhA3OFproEznmvGP85a+jT/pEO7V9fb3YBW5z7xr/fEnyu 19 | 50d3ydKKPzM6oQY6FjLIwKzqo7bVtQCYSzk2n49Sjs+GKphG/oCWhqW6JKbs8D0n 20 | -----END CERTIFICATE----- -------------------------------------------------------------------------------- /test/opa/testdata/resource/opa_cluster.yaml.tmpl: -------------------------------------------------------------------------------- 1 | - name: opa_policy_server 2 | connect_timeout: 5s 3 | type: STATIC 4 | load_assignment: 5 | cluster_name: opa_policy_server 6 | endpoints: 7 | - lb_endpoints: 8 | - endpoint: 9 | address: 10 | socket_address: 11 | address: 127.0.0.1 12 | port_value: 8181 13 | -------------------------------------------------------------------------------- /test/opa/testdata/resource/opa_filter.yaml.tmpl: -------------------------------------------------------------------------------- 1 | - name: envoy.filters.http.wasm 2 | typed_config: 3 | "@type": type.googleapis.com/udpa.type.v1.TypedStruct 4 | type_url: type.googleapis.com/envoy.extensions.filters.http.wasm.v3.Wasm 5 | value: 6 | config: 7 | vm_config: 8 | vm_id: "opa_vm" 9 | runtime: "envoy.wasm.runtime.v8" 10 | code: 11 | local: { filename: {{ .Vars.OpaPluginFilePath }} } 12 | configuration: 13 | "@type": "type.googleapis.com/google.protobuf.StringValue" 14 | value: | 15 | { 16 | "opa_cluster_name": "opa_policy_server", 17 | "opa_service_host": "localhost:8181", 18 | "check_result_cache_valid_sec": 10 19 | } 20 | -------------------------------------------------------------------------------- /test/opa/testdata/resource/server_node_metadata.yaml.tmpl: -------------------------------------------------------------------------------- 1 | "WORKLOAD_NAME": "echo-server", 2 | -------------------------------------------------------------------------------- /test/opa/testdata/rule/opa_rule.rego: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | default allow = false 4 | 5 | allow = true { 6 | input.request_method == "GET" 7 | input.source_principal == "spiffe://cluster.local/ns/default/sa/client" 8 | input.destination_workload == "echo-server" 9 | input.request_url_path == "/echo" 10 | } 11 | -------------------------------------------------------------------------------- /test/opa/testdata/stats/cache_hit.yaml.tmpl: -------------------------------------------------------------------------------- 1 | name: wasm_filter_opa_filter_cache_hit_policy_cache_count 2 | type: COUNTER 3 | metric: 4 | - counter: 5 | value: {{ .Vars.CacheHit }} 6 | -------------------------------------------------------------------------------- /test/opa/testdata/stats/cache_miss.yaml.tmpl: -------------------------------------------------------------------------------- 1 | name: wasm_filter_opa_filter_cache_miss_policy_cache_count 2 | type: COUNTER 3 | metric: 4 | - counter: 5 | value: {{ .Vars.CacheMiss }} 6 | -------------------------------------------------------------------------------- /test/opa/testdata/transport_socket/client_tls_context.yaml.tmpl: -------------------------------------------------------------------------------- 1 | transport_socket: 2 | name: tls 3 | typed_config: 4 | "@type": type.googleapis.com/udpa.type.v1.TypedStruct 5 | type_url: envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext 6 | value: 7 | common_tls_context: 8 | tls_certificates: 9 | - certificate_chain: { filename: "test/opa/testdata/certs/client.cert" } 10 | private_key: { filename: "test/opa/testdata/certs/client-key.cert" } 11 | validation_context: 12 | trusted_ca: { filename: "test/opa/testdata/certs/root.cert" } 13 | sni: server.com 14 | -------------------------------------------------------------------------------- /test/opa/testdata/transport_socket/server_tls_context.yaml.tmpl: -------------------------------------------------------------------------------- 1 | transport_socket: 2 | name: tls 3 | typed_config: 4 | "@type": type.googleapis.com/udpa.type.v1.TypedStruct 5 | type_url: envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext 6 | value: 7 | common_tls_context: 8 | tls_certificates: 9 | - certificate_chain: { filename: "test/opa/testdata/certs/server.cert" } 10 | private_key: { filename: "test/opa/testdata/certs/server-key.cert" } 11 | validation_context: 12 | trusted_ca: { filename: "test/opa/testdata/certs/root.cert" } 13 | require_client_certificate: true 14 | --------------------------------------------------------------------------------