Proposal: Port the aether_stats filter into the proxy build¶
Design record. This proposal is published as written, at its stated status. Later proposals may supersede parts of it, and implementation details drift. It documents the reasoning at a point in time, not the current behaviour of the system — for that, see the docs.
Status: Implemented — superseded in direction by proposal 012: the filter rides the custom proxy build as a native C++ extension rather than a bundled dynamic module (this proposal's envoy --mode validate harness lives on as //test/envoy_validate). (2026-06-15 design.)
Author: Bruno Palermo
Follows: proposal 010 (custom proxy workspace), proposal 007 (telemetry filter)
Problem statement¶
The aether_stats Envoy dynamic module (proposal 007 — source→destination
request metrics) is parked under proxy/filters/http/aether_stats/ (source
only, no BUILD.bazel). It was unwired when the proxy became a separate WORKSPACE
workspace (proposal 010) so we could first get a vanilla custom Envoy building.
This re-wires it into the proxy build. Since we already build a custom Envoy
from source, the recommended form is a native (statically-linked) module —
the filter compiled into the Envoy binary, not a runtime-loaded .so. The
dynamic-module .so is kept as a documented alternative.
Both use the exact same src/lib.rs and the same agent config — Envoy's
loader auto-selects static vs dynamic by probing for the symbols (below).
Key simplification: use the in-tree Envoy SDK¶
In the old aether bzlmod workspace, building the module was painful: the
envoy-proxy-dynamic-modules-rust-sdk was a git dependency resolved by
crate_universe, whose bindgen build script needed abi.h — which we fetched
separately (@envoy_abi_h) and fed in via a patch + a hermetic-LLVM libclang +
glibc sysroot dance (the whole crate.annotation block in MODULE.bazel).
In the proxy workspace @envoy is present, so the SDK is in-tree:
@envoy//source/extensions/dynamic_modules/sdk/rust:envoy_proxy_dynamic_modules_rust_sdk
(verified to exist at our pinned v1.38.0 — a rust_library). Depending on it
directly means no git dep, no crate_universe for the SDK, no fetched abi.h,
no bindgen/sysroot plumbing — the SDK's own build script runs bindgen against
the in-tree abi.h with Envoy's toolchain. This is exactly the model
bpalermo/istio-proxy uses for its rust_module, and the 010 "the bindgen
gymnastics disappear" prediction, realized.
It also makes the SDK inherently version-matched to the proxy's Envoy (same
@envoy source) — retiring the Cargo.toml git-tag lockstep (010 B1).
Design¶
Mirror istio-proxy's filters/http/rust_module exactly.
How native static modules work¶
Envoy's dynamic-modules loader auto-detects static vs dynamic by name
(source/extensions/dynamic_modules/dynamic_modules.cc):
newDynamicModuleByName("aether_stats") first probes
dlsym(RTLD_DEFAULT, "aether_stats_envoy_dynamic_module_on_program_init"). If
those prefixed symbols are statically linked into the Envoy binary, it builds
a newStaticModule — a native, compiled-in module. Otherwise it falls back to
loading libaether_stats.so from ENVOY_DYNAMIC_MODULES_SEARCH_PATH.
So the agent config (DynamicModuleConfig{name:"aether_stats"}) is identical
for both, and the loader picks whichever is present. v1.38.0 ships a first-class
build macro for the static path (envoy_dynamic_module_prefix_symbols, used by
Envoy's own test_data/rust/test_data.bzl).
proxy/filters/http/aether_stats/BUILD.bazel (new) — native¶
load("@rules_rust//rust:defs.bzl", "rust_static_library", "rust_test")
load("@envoy//source/extensions/dynamic_modules:dynamic_modules.bzl", "envoy_dynamic_module_prefix_symbols")
rust_static_library(
name = "aether_stats_lib",
srcs = glob(["src/**/*.rs"]),
edition = "2021",
rustc_flags = ["-C", "link-args=-Wl,-undefined,dynamic_lookup"],
deps = [
"@crate_index//:serde",
"@crate_index//:serde_json",
"@envoy//source/extensions/dynamic_modules/sdk/rust:envoy_proxy_dynamic_modules_rust_sdk",
],
)
# Renames the ABI symbols to the aether_stats_<symbol> prefix the static loader
# resolves via dlsym(RTLD_DEFAULT).
envoy_dynamic_module_prefix_symbols(
name = "aether_stats",
archive = ":aether_stats_lib",
module_name = "aether_stats",
visibility = ["//visibility:public"],
)
rust_test(
name = "aether_stats_test",
size = "small",
srcs = glob(["src/**/*.rs"]),
crate_root = "src/lib.rs",
edition = "2021",
deps = [
"@crate_index//:serde",
"@crate_index//:serde_json",
"@envoy//source/extensions/dynamic_modules/sdk/rust:envoy_proxy_dynamic_modules_rust_sdk",
],
)
Then link it into the custom Envoy in proxy/BUILD.bazel:
AETHER_EXTENSIONS = ["//filters/http/aether_stats:aether_stats"]
envoy_cc_binary(
name = "envoy",
repository = "@envoy",
deps = AETHER_EXTENSIONS + ["@envoy//source/exe:envoy_main_entry_lib"],
)
The prefixed symbols must remain in the binary's dynamic symbol table for
dlsym(RTLD_DEFAULT) — envoy_cc_binary exports dynamic symbols (Envoy already
relies on this for dynamic modules); verify on the first build (else add
-rdynamic/alwayslink).
src/lib.rs is unchanged — it already use envoy_proxy_dynamic_modules_rust_sdk::*,
use serde::Deserialize, calls declare_init_functions!, and its tests use the
SDK's MockEnvoyHttpFilter.
Alternative: dynamic .so¶
Swap rust_static_library + envoy_dynamic_module_prefix_symbols for a
rust_shared_library named aether_stats (same deps/flags), bake it into the
image at /modules via pkg_tar, and set
ENVOY_DYNAMIC_MODULES_SEARCH_PATH=/modules in the image. Pick this only if
decoupling the module from the Envoy binary (rebuild just the .so) is wanted —
but we rebuild Envoy anyway, so native is preferred.
Crate graph for serde — proxy/bazel/rust/¶
rules_rust + crate_universe are already available (Envoy uses them, e.g.
@envoy_rust_crate_index). Add a small crate_index for the two crates.io deps:
# proxy/bazel/rust/crates_repository.bzl
load("@rules_rust//crate_universe:defs.bzl", "crate", _crates_repository = "crates_repository")
def crates_repository():
_crates_repository(
name = "crate_index",
cargo_lockfile = "//bazel/rust:Cargo.lock",
lockfile = "//bazel/rust:Cargo.Bazel.lock",
packages = {
"serde": crate.spec(version = "1.0", features = ["derive"]),
"serde_json": crate.spec(version = "1.0"),
},
)
proxy/WORKSPACE (after the Envoy chain) re-adds:
load("//bazel/rust:crates_repository.bzl", "crates_repository")
crates_repository()
load("@crate_index//:defs.bzl", "crate_repositories")
crate_repositories()
Image — nothing to bake (native)¶
With the native module the filter is inside //:envoy, so the image is
unchanged: no /modules layer, no ENVOY_DYNAMIC_MODULES_SEARCH_PATH, no
pkg_tar. The module cross-compiles for each arch as part of the existing
multi-arch oci_image_index (it's linked into the binary the transition builds).
image.yaml stays as-is (the structure test already checks /usr/local/bin/envoy).
(Dynamic alternative only: pkg_tar the .so to /modules, set
ENVOY_DYNAMIC_MODULES_SEARCH_PATH, and assert /modules/libaether_stats.so in
image.yaml.)
Remove the now-vestigial bzlmod-era files¶
proxy/filters/http/aether_stats/Cargo.toml,Cargo.lock— the Bazel build uses the in-tree SDK +@crate_index; like istio'srust_module, the filter needs no Cargo manifest. (Keep one only if cargo-based local dev is wanted.)proxy/filters/http/aether_stats/patches/(sdk_abi_header.patch) — obsolete; the in-tree SDK readsabi.hitself.
What does NOT change¶
src/lib.rs— same SDK API and tests.- Agent wiring — the agent already attaches the filter to per-pod outbound
HCMs and passes the per-instance
filter_config(proposal 007, shipped), withDynamicModuleConfig{name:"aether_stats"}. That config is identical for native and dynamic — Envoy auto-detects. The proxy build just makes the module present (linked in). - Chart — references the proxy image, which now carries the module in the binary; no chart env / image volume.
Risks & validation¶
- arm64 cross-compile. Linked into Envoy, the module shares the (currently
broken) arm64 Envoy build — the LuaJIT
foreign_ccld/lldfailure from #189'sproxy-prmust be fixed first (v1.38'senvoy.bazelrclacks anarm64-clangconfig). Once Envoy builds arm64, the module links in for free. - Dynamic symbol visibility. The prefixed symbols must be in the binary's
dynamic symbol table for
dlsym(RTLD_DEFAULT). Envoy already exports dynamic symbols for dynamic modules; verify on the first build (else-rdynamic). MockEnvoyHttpFilterforrust_test. The test links the in-tree SDK; confirm the mock is exposed (it was via the git-dep SDK before; same source).- Build/iteration cost. Each filter change relinks Envoy. Marginal (small
crate); use the dynamic
.soif fast standalone iteration is needed.
Rollout¶
Single PR, additive: filter BUILD.bazel (rust_static_library +
envoy_dynamic_module_prefix_symbols) + bazel/rust/ crate graph + WORKSPACE
crate setup + AETHER_EXTENSIONS in proxy/BUILD.bazel; delete the vestigial
Cargo/patches. Gated on the arm64 Envoy fix. proxy-pr validates the module
builds (both arches, linked into //:image_index) and
the image structure test sees /modules/libaether_stats.so.
```