From add4592df4216832ddbe2fef7911028fb8158302 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 2 Sep 2026 19:41:57 +0000 Subject: [PATCH 1/3] Initial plan From f0fabc1909161cbcf5e28d940a7f0e21be37bb07 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 2 Sep 2026 19:57:10 +0000 Subject: [PATCH 2/3] Add filter-cc native filter build example using bzlmod Co-authored-by: phlax <454682+phlax@users.noreply.github.com> --- .bcr/config.yml | 1 + .bcr/filter-cc/metadata.template.json | 18 +++ .bcr/filter-cc/presubmit.yml | 16 +++ .bcr/filter-cc/source.template.json | 5 + .github/workflows/_verify_filter.yml | 67 ++++++++++ .github/workflows/verify.yml | 2 + BUILD | 15 ++- MODULE.bazel | 6 + filter-cc/.bazelrc | 47 ++++++++ filter-cc/.bazelversion | 1 + filter-cc/BUILD | 87 +++++++++++++ filter-cc/Dockerfile-echo | 1 + filter-cc/Dockerfile-proxy | 5 + filter-cc/MODULE.bazel | 99 +++++++++++++++ filter-cc/README.md | 2 + filter-cc/bin/.gitignore | 2 + filter-cc/docker-compose-build.yaml | 41 +++++++ filter-cc/docker-compose.yaml | 15 +++ filter-cc/envoy.yaml | 47 ++++++++ filter-cc/example.rst | 141 ++++++++++++++++++++++ filter-cc/http_filter.cc | 45 +++++++ filter-cc/http_filter.h | 48 ++++++++ filter-cc/http_filter.proto | 10 ++ filter-cc/http_filter_config.cc | 41 +++++++ filter-cc/http_filter_integration_test.cc | 47 ++++++++ filter-cc/verify.sh | 28 +++++ 26 files changed, 834 insertions(+), 3 deletions(-) create mode 100644 .bcr/filter-cc/metadata.template.json create mode 100644 .bcr/filter-cc/presubmit.yml create mode 100644 .bcr/filter-cc/source.template.json create mode 100644 .github/workflows/_verify_filter.yml create mode 100644 filter-cc/.bazelrc create mode 100644 filter-cc/.bazelversion create mode 100644 filter-cc/BUILD create mode 100644 filter-cc/Dockerfile-echo create mode 100644 filter-cc/Dockerfile-proxy create mode 100644 filter-cc/MODULE.bazel create mode 100644 filter-cc/README.md create mode 100644 filter-cc/bin/.gitignore create mode 100644 filter-cc/docker-compose-build.yaml create mode 100644 filter-cc/docker-compose.yaml create mode 100644 filter-cc/envoy.yaml create mode 100644 filter-cc/example.rst create mode 100644 filter-cc/http_filter.cc create mode 100644 filter-cc/http_filter.h create mode 100644 filter-cc/http_filter.proto create mode 100644 filter-cc/http_filter_config.cc create mode 100644 filter-cc/http_filter_integration_test.cc create mode 100755 filter-cc/verify.sh diff --git a/.bcr/config.yml b/.bcr/config.yml index ca41922a..95797edf 100644 --- a/.bcr/config.yml +++ b/.bcr/config.yml @@ -1,3 +1,4 @@ moduleRoots: - "." +- filter-cc - wasm-cc diff --git a/.bcr/filter-cc/metadata.template.json b/.bcr/filter-cc/metadata.template.json new file mode 100644 index 00000000..4bf3fa1c --- /dev/null +++ b/.bcr/filter-cc/metadata.template.json @@ -0,0 +1,18 @@ +{ + "homepage": "https://www.envoyproxy.io/", + "maintainers": [ + { + "github": "mmorel-35", + "github_user_id": 6032561 + }, + { + "github": "phlax", + "github_user_id": 454682 + } + ], + "repository": [ + "github:envoyproxy/examples" + ], + "versions": [], + "yanked_versions": {} +} diff --git a/.bcr/filter-cc/presubmit.yml b/.bcr/filter-cc/presubmit.yml new file mode 100644 index 00000000..9eb77fb9 --- /dev/null +++ b/.bcr/filter-cc/presubmit.yml @@ -0,0 +1,16 @@ +matrix: + unix_platform: + - debian11 + - ubuntu2404 + - macos_arm64 + bazel: + - 7.x + - 8.x + - 9.* +tasks: + verify_targets: + name: Verify build targets + platform: ${{ unix_platform }} + bazel: ${{ bazel }} + build_targets: + - "@envoy-example-filter-cc//..." diff --git a/.bcr/filter-cc/source.template.json b/.bcr/filter-cc/source.template.json new file mode 100644 index 00000000..686a9ee3 --- /dev/null +++ b/.bcr/filter-cc/source.template.json @@ -0,0 +1,5 @@ +{ + "integrity": "", + "strip_prefix": "{REPO}-{VERSION}/filter-cc", + "url": "https://github.com/{OWNER}/{REPO}/archive/refs/tags/v{TAG}.tar.gz" +} diff --git a/.github/workflows/_verify_filter.yml b/.github/workflows/_verify_filter.yml new file mode 100644 index 00000000..4d49b983 --- /dev/null +++ b/.github/workflows/_verify_filter.yml @@ -0,0 +1,67 @@ +name: Verify/Filter + +permissions: + contents: read + +on: + workflow_call: + + +jobs: + filter: + runs-on: ubuntu-24.04 + steps: + - uses: envoyproxy/toolshed/actions/bind-mounts@598eacce15ab5f208102a2fd5669292868002701 # v0.4.0 + with: + mounts: | + - src: /mnt/docker + target: /var/lib/docker + rm: true + command-pre: sudo systemctl stop docker + command-post: sudo systemctl start docker + - src: /mnt/runner-cache + target: /home/runner/.cache + chown: "runner:runner" + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + path: examples + - run: | + printf '%s\n' 'common --config=ci' > repo.bazelrc + working-directory: examples/filter-cc + - name: Inject bzlmod envoy pin into MODULE.bazel + run: | + # The envoy module is not (yet) published to a registry, so resolve + # the current envoy main HEAD and override the module to use it. + ENVOY_SHA=$(git ls-remote https://github.com/envoyproxy/envoy refs/heads/main | head -n1 | awk '{print $1}') + echo "Resolved envoy main HEAD: ${ENVOY_SHA}" + cat >> MODULE.bazel <` + Used to make HTTP requests. + +This sandbox demonstrates how to build a native C++ HTTP filter and statically link it +into the Envoy binary using `bzlmod `_. + +The example filter is a decoder filter which reads a ``key`` and ``val`` from its +configuration, and adds them as a header to proxied requests. + +The example provides all of the pieces required to build and use an Envoy extension: + +:download:`MODULE.bazel <_include/filter-cc/MODULE.bazel>` + Declares the module and its dependency on the ``envoy`` and ``envoy_api`` Bazel modules, + and sets up the compiler toolchain. This is the file to copy when starting your own extension. + +:download:`BUILD <_include/filter-cc/BUILD>` + Builds the filter's config proto, the filter itself and an Envoy binary with the + filter statically linked, using Envoy's build macros. + +:download:`http_filter.proto <_include/filter-cc/http_filter.proto>` + The configuration proto for the filter. + +:download:`http_filter.h <_include/filter-cc/http_filter.h>` / :download:`http_filter.cc <_include/filter-cc/http_filter.cc>` + The filter itself - an implementation of ``Envoy::Http::PassThroughDecoderFilter`` + which adds the configured header to requests. + +:download:`http_filter_config.cc <_include/filter-cc/http_filter_config.cc>` + The config factory which registers the filter with Envoy, making it available + as the ``sample`` filter in Envoy configuration. + +:download:`http_filter_integration_test.cc <_include/filter-cc/http_filter_integration_test.cc>` + An integration test which spins up the filter inside Envoy's HTTP integration test + framework and asserts the header is added. + +:download:`envoy.yaml <_include/filter-cc/envoy.yaml>` + An Envoy configuration using the ``sample`` filter. + +Step 1: Build the Envoy binary with the filter +********************************************** + +.. warning:: + + These instructions for building the binary use the + `envoyproxy/envoy-build-ubuntu `_ image. + You will need 4-5GB of disk space to accommodate this image, and building Envoy + itself requires significant time, cpu and memory. + +Export ``UID`` from your host system. This will ensure that the binary created inside the +build container has the same permissions as your host user: + +.. code-block:: console + + $ export UID + +Change to the ``filter-cc`` directory and build the Envoy binary with the ``sample`` +filter statically linked: + +.. code-block:: console + + $ pwd + examples/filter-cc + $ docker compose -f docker-compose-build.yaml run --remove-orphans filter_build + +The built binary should now be in the ``bin`` folder. + +.. code-block:: console + + $ ls -l bin + total 803408 + -r-xr-xr-x 1 user user 822683320 Oct 20 10:16 envoy + +Step 2: Start all of our containers +*********************************** + +Start the composition - an Envoy proxy which uses the binary built in Step 1, and a +backend which echos back our request: + +.. code-block:: console + + $ pwd + examples/filter-cc + $ docker compose up --build -d + $ docker compose ps + + NAME COMMAND SERVICE STATUS PORTS + filter-cc-proxy-1 "/usr/local/bin/envo…" proxy running 0.0.0.0:8000->8000/tcp + filter-cc-web_service-1 "/bin/echo-server" web_service running 8080/tcp + +Step 3: Check the filter has added its header +********************************************* + +The ``sample`` filter is configured in :download:`envoy.yaml <_include/filter-cc/envoy.yaml>` +to add a ``via: sample-filter`` header to proxied requests: + +.. literalinclude:: _include/filter-cc/envoy.yaml + :language: yaml + :lines: 26-31 + :lineno-start: 26 + :emphasize-lines: 2-6 + :linenos: + +As the backend service echos the request it receives, the header added by the filter +should be visible in the response body: + +.. code-block:: console + + $ curl -s http://localhost:8000 | grep "sample-filter" + Via: sample-filter + +Step 4: Run the integration test +******************************** + +The example also provides an integration test which exercises the filter inside +Envoy's HTTP integration test framework, without the need to build - or run - the +Envoy binary: + +.. code-block:: console + + $ pwd + examples/filter-cc + $ docker compose -f docker-compose-build.yaml run --remove-orphans filter_test + +.. seealso:: + + :ref:`HTTP filters ` + Further information about Envoy's HTTP filters. + + :ref:`Envoy Bazel build ` + Building Envoy with Bazel. + + `Envoy filter example `_ + The source files for this example. diff --git a/filter-cc/http_filter.cc b/filter-cc/http_filter.cc new file mode 100644 index 00000000..b906fbba --- /dev/null +++ b/filter-cc/http_filter.cc @@ -0,0 +1,45 @@ +#include + +#include "http_filter.h" + +#include "envoy/server/filter_config.h" + +namespace Envoy { +namespace Http { + +HttpSampleDecoderFilterConfig::HttpSampleDecoderFilterConfig( + const sample::Decoder& proto_config) + : key_(proto_config.key()), val_(proto_config.val()) {} + +HttpSampleDecoderFilter::HttpSampleDecoderFilter(HttpSampleDecoderFilterConfigSharedPtr config) + : config_(config) {} + +HttpSampleDecoderFilter::~HttpSampleDecoderFilter() {} + +void HttpSampleDecoderFilter::onDestroy() {} + +const LowerCaseString HttpSampleDecoderFilter::headerKey() const { + return LowerCaseString(config_->key()); +} + +const std::string HttpSampleDecoderFilter::headerValue() const { + return config_->val(); +} + +FilterHeadersStatus HttpSampleDecoderFilter::decodeHeaders(RequestHeaderMap& headers, bool) { + // add a header + headers.addCopy(headerKey(), headerValue()); + + return FilterHeadersStatus::Continue; +} + +FilterDataStatus HttpSampleDecoderFilter::decodeData(Buffer::Instance&, bool) { + return FilterDataStatus::Continue; +} + +void HttpSampleDecoderFilter::setDecoderFilterCallbacks(StreamDecoderFilterCallbacks& callbacks) { + decoder_callbacks_ = &callbacks; +} + +} // namespace Http +} // namespace Envoy diff --git a/filter-cc/http_filter.h b/filter-cc/http_filter.h new file mode 100644 index 00000000..aff0482b --- /dev/null +++ b/filter-cc/http_filter.h @@ -0,0 +1,48 @@ +#pragma once + +#include + +#include "source/extensions/filters/http/common/pass_through_filter.h" + +#include "http_filter.pb.h" + +namespace Envoy { +namespace Http { + +class HttpSampleDecoderFilterConfig { +public: + HttpSampleDecoderFilterConfig(const sample::Decoder& proto_config); + + const std::string& key() const { return key_; } + const std::string& val() const { return val_; } + +private: + const std::string key_; + const std::string val_; +}; + +using HttpSampleDecoderFilterConfigSharedPtr = std::shared_ptr; + +class HttpSampleDecoderFilter : public PassThroughDecoderFilter { +public: + HttpSampleDecoderFilter(HttpSampleDecoderFilterConfigSharedPtr); + ~HttpSampleDecoderFilter(); + + // Http::StreamFilterBase + void onDestroy() override; + + // Http::StreamDecoderFilter + FilterHeadersStatus decodeHeaders(RequestHeaderMap&, bool) override; + FilterDataStatus decodeData(Buffer::Instance&, bool) override; + void setDecoderFilterCallbacks(StreamDecoderFilterCallbacks&) override; + +private: + const HttpSampleDecoderFilterConfigSharedPtr config_; + StreamDecoderFilterCallbacks* decoder_callbacks_; + + const LowerCaseString headerKey() const; + const std::string headerValue() const; +}; + +} // namespace Http +} // namespace Envoy diff --git a/filter-cc/http_filter.proto b/filter-cc/http_filter.proto new file mode 100644 index 00000000..094103e1 --- /dev/null +++ b/filter-cc/http_filter.proto @@ -0,0 +1,10 @@ +syntax = "proto3"; + +package sample; + +import "validate/validate.proto"; + +message Decoder { + string key = 1 [(validate.rules).string.min_bytes = 1]; + string val = 2 [(validate.rules).string.min_bytes = 1]; +} diff --git a/filter-cc/http_filter_config.cc b/filter-cc/http_filter_config.cc new file mode 100644 index 00000000..366a7710 --- /dev/null +++ b/filter-cc/http_filter_config.cc @@ -0,0 +1,41 @@ +#include + +#include "envoy/registry/registry.h" + +#include "source/extensions/filters/http/common/factory_base.h" + +#include "http_filter.pb.h" +#include "http_filter.pb.validate.h" +#include "http_filter.h" + +namespace Envoy { +namespace Server { +namespace Configuration { + +class HttpSampleDecoderFilterConfigFactory + : public Extensions::HttpFilters::Common::UnifiedFactoryBase { +public: + HttpSampleDecoderFilterConfigFactory() : UnifiedFactoryBase("sample") {} + +private: + absl::StatusOr + createHttpFilterFactoryFromProtoTyped(const sample::Decoder& proto_config, + ServerFactoryContext&, ExtraFactoryContext&) override { + Http::HttpSampleDecoderFilterConfigSharedPtr config = + std::make_shared(proto_config); + + return [config](Http::FilterChainFactoryCallbacks& callbacks) -> void { + auto filter = new Http::HttpSampleDecoderFilter(config); + callbacks.addStreamDecoderFilter(Http::StreamDecoderFilterSharedPtr{filter}); + }; + } +}; + +/** + * Static registration for this sample filter. @see RegisterFactory. + */ +REGISTER_FACTORY(HttpSampleDecoderFilterConfigFactory, NamedHttpFilterConfigFactory); + +} // namespace Configuration +} // namespace Server +} // namespace Envoy diff --git a/filter-cc/http_filter_integration_test.cc b/filter-cc/http_filter_integration_test.cc new file mode 100644 index 00000000..0078c621 --- /dev/null +++ b/filter-cc/http_filter_integration_test.cc @@ -0,0 +1,47 @@ +#include "test/integration/http_integration.h" + +namespace Envoy { +class HttpFilterSampleIntegrationTest : public HttpIntegrationTest, + public testing::TestWithParam { +public: + HttpFilterSampleIntegrationTest() : HttpIntegrationTest(Http::CodecType::HTTP1, GetParam()) {} + /** + * Initializer for an individual integration test. + */ + void SetUp() override { initialize(); } + + void initialize() override { + config_helper_.prependFilter( + "{ name: sample, typed_config: { \"@type\": type.googleapis.com/sample.Decoder, key: via, " + "val: sample-filter } }"); + HttpIntegrationTest::initialize(); + } +}; + +INSTANTIATE_TEST_SUITE_P(IpVersions, HttpFilterSampleIntegrationTest, + testing::ValuesIn(TestEnvironment::getIpVersionsForTest())); + +TEST_P(HttpFilterSampleIntegrationTest, Test1) { + Http::TestRequestHeaderMapImpl headers{ + {":method", "GET"}, {":path", "/"}, {":authority", "host"}}; + Http::TestResponseHeaderMapImpl response_headers{{":status", "200"}}; + + IntegrationCodecClientPtr codec_client; + FakeHttpConnectionPtr fake_upstream_connection; + FakeStreamPtr request_stream; + + codec_client = makeHttpConnection(lookupPort("http")); + auto response = codec_client->makeHeaderOnlyRequest(headers); + ASSERT_TRUE(fake_upstreams_[0]->waitForHttpConnection(*dispatcher_, fake_upstream_connection)); + ASSERT_TRUE(fake_upstream_connection->waitForNewStream(*dispatcher_, request_stream)); + ASSERT_TRUE(request_stream->waitForEndStream(*dispatcher_)); + request_stream->encodeHeaders(response_headers, true); + ASSERT_TRUE(response->waitForEndStream()); + + EXPECT_EQ( + "sample-filter", + request_stream->headers().get(Http::LowerCaseString("via"))[0]->value().getStringView()); + + codec_client->close(); +} +} // namespace Envoy diff --git a/filter-cc/verify.sh b/filter-cc/verify.sh new file mode 100755 index 00000000..133f2062 --- /dev/null +++ b/filter-cc/verify.sh @@ -0,0 +1,28 @@ +#!/bin/bash -e + +export NAME=filter-cc +export MANUAL=true +export UID + + +# shellcheck source=verify-common.sh +. "$(dirname "${BASH_SOURCE[0]}")/../verify-common.sh" + +run_log "Build the Envoy binary with the sample filter statically linked" +"${DOCKER_COMPOSE[@]}" -f docker-compose-build.yaml run --quiet-pull --remove-orphans filter_build + +run_log "Check the compiled binary" +ls -l bin/envoy + +bring_up_example + +run_log "Test connection" +wait_for 10 bash -c "\ + responds_with \ + 'Request served by' \ + http://localhost:8000" + +run_log "Test the sample filter has added its header to the proxied request" +responds_with \ + "Via: sample-filter" \ + http://localhost:8000 From 50eabc5bfa89a7dd839d389655dbacdba222864c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 2 Sep 2026 19:58:27 +0000 Subject: [PATCH 3/3] Simplify filter to rely on PassThroughDecoderFilter base Co-authored-by: phlax <454682+phlax@users.noreply.github.com> --- filter-cc/http_filter.cc | 14 -------------- filter-cc/http_filter.h | 7 ------- 2 files changed, 21 deletions(-) diff --git a/filter-cc/http_filter.cc b/filter-cc/http_filter.cc index b906fbba..2984dc46 100644 --- a/filter-cc/http_filter.cc +++ b/filter-cc/http_filter.cc @@ -2,8 +2,6 @@ #include "http_filter.h" -#include "envoy/server/filter_config.h" - namespace Envoy { namespace Http { @@ -14,10 +12,6 @@ HttpSampleDecoderFilterConfig::HttpSampleDecoderFilterConfig( HttpSampleDecoderFilter::HttpSampleDecoderFilter(HttpSampleDecoderFilterConfigSharedPtr config) : config_(config) {} -HttpSampleDecoderFilter::~HttpSampleDecoderFilter() {} - -void HttpSampleDecoderFilter::onDestroy() {} - const LowerCaseString HttpSampleDecoderFilter::headerKey() const { return LowerCaseString(config_->key()); } @@ -33,13 +27,5 @@ FilterHeadersStatus HttpSampleDecoderFilter::decodeHeaders(RequestHeaderMap& hea return FilterHeadersStatus::Continue; } -FilterDataStatus HttpSampleDecoderFilter::decodeData(Buffer::Instance&, bool) { - return FilterDataStatus::Continue; -} - -void HttpSampleDecoderFilter::setDecoderFilterCallbacks(StreamDecoderFilterCallbacks& callbacks) { - decoder_callbacks_ = &callbacks; -} - } // namespace Http } // namespace Envoy diff --git a/filter-cc/http_filter.h b/filter-cc/http_filter.h index aff0482b..03f25d36 100644 --- a/filter-cc/http_filter.h +++ b/filter-cc/http_filter.h @@ -26,19 +26,12 @@ using HttpSampleDecoderFilterConfigSharedPtr = std::shared_ptr