{"id":20467627,"url":"https://github.com/derekargueta/envoy-rot-cipher-filter","last_synced_at":"2026-05-18T06:40:08.529Z","repository":{"id":81514482,"uuid":"115075969","full_name":"derekargueta/envoy-rot-cipher-filter","owner":"derekargueta","description":"An example of an isolated third-party Envoy filter","archived":false,"fork":false,"pushed_at":"2021-04-29T01:30:20.000Z","size":37,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-07-28T09:47:07.158Z","etag":null,"topics":["bazel","cpp","cpp11","envoy","envoy-development","envoy-filter","filter-development","http","protobuf","proxy"],"latest_commit_sha":null,"homepage":"","language":"Starlark","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/derekargueta.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2017-12-22T04:18:40.000Z","updated_at":"2021-04-29T01:30:23.000Z","dependencies_parsed_at":null,"dependency_job_id":"79d0e341-8968-4b86-831f-fed00e382100","html_url":"https://github.com/derekargueta/envoy-rot-cipher-filter","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/derekargueta/envoy-rot-cipher-filter","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/derekargueta%2Fenvoy-rot-cipher-filter","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/derekargueta%2Fenvoy-rot-cipher-filter/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/derekargueta%2Fenvoy-rot-cipher-filter/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/derekargueta%2Fenvoy-rot-cipher-filter/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/derekargueta","download_url":"https://codeload.github.com/derekargueta/envoy-rot-cipher-filter/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/derekargueta%2Fenvoy-rot-cipher-filter/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33167953,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-18T05:43:36.989Z","status":"ssl_error","status_checked_at":"2026-05-18T05:43:19.133Z","response_time":71,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["bazel","cpp","cpp11","envoy","envoy-development","envoy-filter","filter-development","http","protobuf","proxy"],"created_at":"2024-11-15T13:29:22.252Z","updated_at":"2026-05-18T06:40:08.513Z","avatar_url":"https://github.com/derekargueta.png","language":"Starlark","funding_links":[],"categories":[],"sub_categories":[],"readme":"Envoy Rot Cipher Filter\n===========================\n\nA simple filter that probably (hopefully) isn't of much production value, but demonstrates how to write a third-party [Envoy](https://github.com/envoyproxy/envoy) filter that is compatible with both the v1 and v2 API. I hope that this repository serves as a nice guide for people who maybe don't want to dive into the internals of Envoy too deeply, but want to be able to build custom Envoy filters _outside_ of the main source code.\n\nI wanted to write this because the [current defacto example](https://github.com/envoyproxy/envoy-filter-example) doesn't use any configuration data, which is one of the parts I wrestled with the most in building this filter. As a young whippersnapper new-grad, I thought that this \"outside perspective\" might be helpful for others getting started with C++/Envoy development.\n\nFYI The code here is not the greatest and isn't meant to be, but I do plan on coming back here and cleaning up somewhat.\n\nIf you don't understand the explanations here, please open an issue/PR.\nAlso, the filter's source code should be well-documented (actually over-documented) to provide further explanations.\n\n\n## Compiling \u0026 Running\n```bash\ngit submodule update --init  # we need the envoy source to compile/test\nmake compile\n```\n\nThe `compile` target should call the `compile/proto` target to generate the protobuf classes in the `gen/` directory.\n\nI've included a basic `app.py` web server that just prints the headers it receives. To run the whole thing:\n\nterminal pane 1:\n```bash\n./bazel-bin/envoy -c examples/example.v2.yaml --v2-config-only\n```\n\nterminal pane 2:\n```\npython examples/app.py\n```\n\nterminal pane 3:\n```\ncurl -H \"x-rot: asdf\" localhost:8000\n```\n\nand in pane 2 you should see something like\n```\nuser-agent: curl/7.54.0\naccept: */*\nx-forwarded-proto: http\nx-request-id: 5a16b07d-d811-4fdf-b9bc-790efdd1d97b\nx-rot: nopq\ncontent-length: 0\n```\nwhere you can see the letters of `x-rot` have been rotated.\n\n\n## Usage\nThis filter accepts two configuration options:\n- `rot_value`: an integer describing how much to rotate each letter by. For example, 13 would be the classic Caesar Cipher\n- `rot_header`: a string that names which header to apply the cipher to. If the header is not found, no action is taken and the filter continues.\n\nv2 Example\n```\n  http_filters:\n  - name: rot_cipher\n    config: { rot_value: 13, rot_header: x-rot }\n  - name: envoy.router\n```\n\nFull example configurations are in the `examples/` directory.\n\n\n## Filter Development\n\nEvery filter has 2 main components:\n- The config, which for HTTP filters should be a subclass of [`NamedHttpFilterConfigFactory`](https://github.com/envoyproxy/envoy/blob/b01b13865ac86c6b5716e128137afd29ef3147d0/include/envoy/server/filter_config.h#L196)\n- The filter implementation, which for HTTP filters would be a subclass of either [`StreamDecoderFilter`](https://github.com/envoyproxy/envoy/blob/3599784052e75ac1b18b5342aa711d987a852bf0/include/envoy/http/filter.h#L289), [`StreamEncoderFilter`](https://github.com/envoyproxy/envoy/blob/3599784052e75ac1b18b5342aa711d987a852bf0/include/envoy/http/filter.h#L402), or [`StreamFilter`](https://github.com/envoyproxy/envoy/blob/3599784052e75ac1b18b5342aa711d987a852bf0/include/envoy/http/filter.h#L438) for filters that do encoding and decoding.\n\nTo support the [v2 API](https://www.envoyproxy.io/docs/envoy/latest/api-v2/api), you'll additionally need a `.proto` file (if your filter uses configuration data. If it doesn't then there's no need for a `.proto` file).\nSimply put, if your config is v1, then Envoy will call `createFilterFactory` with a [`Json::Object`](https://github.com/envoyproxy/envoy/blob/master/include/envoy/json/json_object.h) which you can then pick apart and validate.\nIf your config is v2, then it will call `createFilterFactoryFromProto` with a [`Protobuf::Message`](https://developers.google.com/protocol-buffers/docs/reference/cpp/google.protobuf.message), which you can just cast to your protobuf-generated class. \nThe details of how/why this procedure works is worthy of a separate write-up, but I believe it has a lot to do with how the v2 API can use [gRPC](https://grpc.io/) streaming.\n(More on Envoy's use of gRPC in the [Envoy docs](https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/grpc.html))\n\nIf you look at filters in the main Envoy source code they have a lot more pieces due to the complexity, for example they have intermediate `config` classes that the Json and protobuf data get converted to so that the filters only need 1 constructor. But if you strip it all down, these are the bare minimums.\n\nI started with `rot_cipher.proto` to define what the configuration would look like.\nThat file then produces `rot_cipher.pb.(cc|h)` in `gen/`.\nThis is a pretty ad-hoc procedure that I constructed, and quite frankly the bazel `BUILD` file could be improved to use the native Bazel support for protobufs instead of calling the protobuf compiler in the `Makefile`.\n\n\n_But is using a `.proto` necessary?_\n\n_For the v2 API, yes. You might be able to hack something weird using [Protobuf Reflection](https://developers.google.com/protocol-buffers/docs/reference/cpp/google.protobuf.message#Reflection) but that's a scary path._\n\n\nAfter wrapping my head around how the protobufs integrate into Envoy filter configs (first-time protobuf user here), the rest was pretty straight forward to achieve based on the [envoy-filter-example](https://github.com/envoyproxy/envoy-filter-example). Some important classes to look at are [HeaderMap](https://github.com/envoyproxy/envoy/blob/master/include/envoy/http/header_map.h) for manipulating headers and [Object](https://github.com/envoyproxy/envoy/blob/master/include/envoy/json/json_object.h) for manipulating Json Objects. Additionally, [json_loader.cc](https://github.com/envoyproxy/envoy/blob/master/source/common/json/json_loader.cc), [config/utility.h](https://github.com/envoyproxy/envoy/blob/master/source/common/config/utility.h), and [protobuf/utility.h](https://github.com/envoyproxy/envoy/blob/master/source/common/protobuf/utility.h) were pretty helpful.\n\nAs an aside, before starting this filter I went through the [Protobuf C++ Tutorial](https://developers.google.com/protocol-buffers/docs/cpptutorial), substituting [proto3](https://developers.google.com/protocol-buffers/docs/proto3) syntax for [proto2](https://developers.google.com/protocol-buffers/docs/proto).\n\n## Problems I ran Into\n(this part is still kind of mumble-y and under revision)\n\nI had an issue with the v2 API config where each time I tried to start Envoy I got the error below:\n```\n[2017-12-22 13:55:03.124][4915417][debug][config] external/envoy/source/server/listener_manager_impl.cc:29]   filter #0:\n[2017-12-22 13:55:03.124][4915417][debug][config] external/envoy/source/server/listener_manager_impl.cc:30]     name: envoy.http_connection_manager\n[2017-12-22 13:55:03.195][4915417][debug][config] external/envoy/source/server/config/network/http_connection_manager.cc:226]     filter #0\n[2017-12-22 13:55:03.195][4915417][debug][config] external/envoy/source/server/config/network/http_connection_manager.cc:227]       name: rot_cipher\n[2017-12-22 13:55:03.195][4915417][critical][main] external/envoy/source/server/server.cc:72] error initializing configuration 'examples/example.v2.yaml': Unable to parse JSON as proto (INVALID_ARGUMENT:: Cannot find field.): {\"rot_value\":13,\"rot_header\":\"x-rot\"}\n[2017-12-22 13:55:03.195][4915417][debug][upstream] external/envoy/source/common/upstream/cluster_manager_impl.cc:523] shutting down thread local cluster manager\n```\n\nI had managed to narrow it down to this line in my code:\n\n```cpp\nconst auto\u0026 typed_config = dynamic_cast\u003cconst example::RotCipher\u0026\u003e(proto_config);\n```\n\nAll generated protobuf messages subclass `Protobuf::Message` (which is the type of `proto_config`) so this dynamic cast shouldn't be a problem. I actually found this snippet in the [Envoy source](https://github.com/envoyproxy/envoy/blob/bbe6618b90d87500e7ed4304996881c4be4abb09/source/common/protobuf/utility.h#L149).\n\nI had gone over the configuration multiple times and knew it was correct based on playing with other filters.\nAt the end my wits, I started throwing print statements into the Envoy source code, thinking maybe there was a bug somewhere in the JSON parsing?\nI started with the `HttpConnectionManagerConfig` right after it logs what filter it's processing, since the debug log kindly outputs what line it is at :)\nThis is at the [end of the constructor](https://github.com/envoyproxy/envoy/blob/9ed62923a8ff6745407046c4451ce757348d966f/source/server/config/network/http_connection_manager.cc#L221) which has the following code.\n\n```cpp\nconst auto\u0026 filters = config.http_filters();\nfor (int32_t i = 0; i \u003c filters.size(); i++) {\n  const ProtobufTypes::String\u0026 string_name = filters[i].name();\n  const auto\u0026 proto_config = filters[i];\n\n  ENVOY_LOG(debug, \"    filter #{}\", i);\n  ENVOY_LOG(debug, \"      name: {}\", string_name);\n\n  const Json::ObjectSharedPtr filter_config =\n      MessageUtil::getJsonObjectFromMessage(proto_config.config());\n\n  // Now see if there is a factory that will accept the config.\n  auto\u0026 factory = Config::Utility::getAndCheckFactory\u003cNamedHttpFilterConfigFactory\u003e(string_name);\n  HttpFilterFactoryCb callback;\n  if (filter_config-\u003egetBoolean(\"deprecated_v1\", false)) {\n    callback = factory.createFilterFactory(*filter_config-\u003egetObject(\"value\", true),\n                                           stats_prefix_, context);\n  } else {\n    ProtobufTypes::MessagePtr message =\n        Config::Utility::translateToFactoryConfig(proto_config, factory);\n    callback = factory.createFilterFactoryFromProto(*message, stats_prefix_, context);\n  }\n  filter_factories_.push_back(callback);\n}\n\n```\n\nI inspected the `filter_config` JSON object and it had the `rot_value` and `rot_header` as expected, so then I looked inside the call to [`Config::Utility::translateToFactoryConfig(...)`](https://github.com/envoyproxy/envoy/blob/6b2823da5006e92bc4b365e9e8804a4f6a2eba37/source/common/config/utility.h#L190) which contains the following code.\n\n```cpp\ntemplate \u003cclass ProtoMessage, class Factory\u003e\nstatic ProtobufTypes::MessagePtr translateToFactoryConfig(const ProtoMessage\u0026 enclosing_message,\n                                                          Factory\u0026 factory) {\n  ProtobufTypes::MessagePtr config = factory.createEmptyConfigProto();\n\n  if (config == nullptr) {\n    throw EnvoyException(fmt::format(\n        \"{} factory returned nullptr instead of empty config message.\", factory.name()));\n  }\n\n  if (enclosing_message.has_config()) {\n    MessageUtil::jsonConvert(enclosing_message.config(), *config);\n  }\n\n  return config;\n}\n```\n\nI wasn't getting the nullptr error message so the config wasn't coming back null... WAIT. `createEmptyConfigProto`? That function is required by the abstract class and I remember pasting in some code for it... I hadn't thought much of that function and just pasted what the `envoy-example-filter` code had, which is `return ProtobufTypes::MessagePtr{new Envoy::ProtobufWkt::Empty()}`. If the empty message is _not_ of type `example::RotCipher`, then yeah there would be issues in casting it to a `Protobuf::Message` and then to an `example::RotCipher`. I checked some built-in Envoy filters that had non-empty configurations and sure enough, they return an empty instance of the generated protobuf config.\n\nRateLimit config\n```cpp\nProtobufTypes::MessagePtr createEmptyConfigProto() override {\n  return ProtobufTypes::MessagePtr{new envoy::api::v2::filter::http::RateLimit()};\n}\n```\n\nSo once I changed the RotCipher config's `createEmtpyConfigProto` to \n```cpp\nProtobufTypes::MessagePtr createEmptyConfigProto() override {\n  return ProtobufTypes::MessagePtr{new example::RotCipher()};\n}\n```\n\nall was well! The error message was a little cryptic (yet correct), but sure enough it was a user-error.\n\n## Thoughts/Reflections\n\nThis was mostly an exercise in gaining familiarity with Envoy filter development so here's some thoughts on how the whole thing went.\n- There is very little internal documentation of how the mechanics work. This is no secret, but the documentation from Envoy could use some love and help from the community (low-hanging fruit for new Envoy contributors!)\n- The organization of the Envoy code made it a little difficult to piece this project together. For example, _all_ the Envoy filter configs are in one directoy ([`server/config/http/`](https://github.com/envoyproxy/envoy/tree/master/source/server/config/http)), and _all_ the Envoy filter implementations are in a different directory ([`source/common/`](https://github.com/envoyproxy/envoy/tree/master/source/common)), and there's multiple `config` directories for [non-filter config stuff](https://github.com/envoyproxy/envoy/tree/master/source/common/config), etc. so it felt like a scavenger hunt to get the complete image of what all 1 filter needs. This organization makes sense for the size of Envoy's project, but again makes it difficult to piece together everything that, for example, the buffer filter does.\n- Despite the above 2 \"complaints\", Envoy does provide a very clean API to work with. I found this to be a much easier exercise than implementing an Nginx module, which took a day-long workshop and I'm still not confident in my Nginx module development.\n- I noticed there's some scripts in `envoy/tools/` such as `stack_decode.py` that may have been helpful in debugging but I couldn't figure out how to get them to work.\n- The bazel build procedure is pretty slick. Props to the Envoy team!\n- If you want to write Envoy filters, you can't shy away from reading a lot of C++ :)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fderekargueta%2Fenvoy-rot-cipher-filter","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fderekargueta%2Fenvoy-rot-cipher-filter","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fderekargueta%2Fenvoy-rot-cipher-filter/lists"}