diff --git a/.gitignore b/.gitignore index 2b2e639..1f11060 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,7 @@ bazel-* access_logs/ -*.so \ No newline at end of file +*.so + +*.jar +java/out \ No newline at end of file diff --git a/Makefile b/Makefile index 25627cb..c66b62e 100644 --- a/Makefile +++ b/Makefile @@ -82,7 +82,7 @@ test-rust: ## Run the unit tests for the Rust codebase. @$(call print_success,Rust unit tests completed) .PHONY: build -build: build-go build-rust ## Build all dynamic modules. +build: build-go build-rust build-java ## Build all dynamic modules. .PHONY: build-go build-go: ## Build the Go dynamic module. @@ -92,6 +92,14 @@ build-go: ## Build the Go dynamic module. @$(call print_task,Copying Go dynamic module for easier use with Envoy) @cp go/libgo_module.so integration/libgo_module.so +.PHONY: build-java +build-java: ## Build the Java filter JAR and copy it to the integration directory. + @$(call print_task,Building Java filter JAR) + @make -C java + @$(call print_success,Java filter JAR built at java/out/envoy-java-filter.jar) + @$(call print_task,Copying Java filter JAR for use with Envoy) + @cp java/out/envoy-java-filter.jar integration/envoy-java-filter.jar + .PHONY: build-rust build-rust: ## Build the Rust dynamic module. @$(call print_task,Building Rust dynamic module) @@ -102,7 +110,7 @@ build-rust: ## Build the Rust dynamic module. @cp rust/target/debug/librust_module.so integration/librust_module.so || true .PHONY: integration-test -integration-test: build-go build-rust ## Run the integration tests. +integration-test: build-go build-rust build-java ## Run the integration tests. @$(call print_task,Running integration tests) @cd integration && go test -v ./... @$(call print_success,Integration tests completed) diff --git a/integration/envoy.yaml b/integration/envoy.yaml index 0494abb..a44ffb0 100644 --- a/integration/envoy.yaml +++ b/integration/envoy.yaml @@ -207,6 +207,44 @@ static_resources: typed_config: "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router + - address: + socket_address: + address: 0.0.0.0 + port_value: 1065 + filter_chains: + - filters: + - name: envoy.filters.network.http_connection_manager + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager + stat_prefix: ingress_http + route_config: + virtual_hosts: + - name: local_route + domains: + - "*" + routes: + - match: + prefix: "/" + route: + cluster: httpbin + http_filters: + - name: dynamic_modules/java_filter + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.dynamic_modules.v3.DynamicModuleFilter + dynamic_module_config: + name: rust_module + filter_name: java_filter + filter_config: + "@type": "type.googleapis.com/google.protobuf.StringValue" + value: | + { + "jar_path": "./envoy-java-filter.jar", + "class_name": "io.envoyproxy.dynamicmodules.ExampleFilter" + } + - name: envoy.filters.http.router + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router + clusters: - name: httpbin # This demonstrates how to use the dynamic module HTTP filter as an upstream filter. diff --git a/integration/main_test.go b/integration/main_test.go index e896ee8..ae49b66 100644 --- a/integration/main_test.go +++ b/integration/main_test.go @@ -6,9 +6,11 @@ import ( "context" "encoding/json" "io" + "net" "net/http" "os" "os/exec" + "path/filepath" "strconv" "strings" "testing" @@ -26,12 +28,15 @@ func TestIntegration(t *testing.T) { // Setup the httpbin upstream local server. httpbinHandler := httpbin.New() - server := &http.Server{Addr: ":1234", Handler: httpbinHandler, + server := &http.Server{Handler: httpbinHandler, ReadHeaderTimeout: 5 * time.Second, IdleTimeout: 5 * time.Second, WriteTimeout: 5 * time.Second, } + // Use tcp4 to avoid conflicting with any IPv6-only service already on :1234. + httpbinListener, err := net.Listen("tcp4", ":1234") + require.NoError(t, err) go func() { - if err = server.ListenAndServe(); err != nil && err != http.ErrServerClosed { + if err := server.Serve(httpbinListener); err != nil && err != http.ErrServerClosed { t.Logf("HTTP server error: %v", err) } }() @@ -43,7 +48,7 @@ func TestIntegration(t *testing.T) { // Health check to ensure the server is up before starting tests. require.Eventually(t, func() bool { - resp, err := http.Get("http://localhost:1234/uuid") + resp, err := http.Get("http://127.0.0.1:1234/uuid") if err != nil { t.Logf("httpbin server not ready yet: %v", err) return false @@ -60,6 +65,11 @@ func TestIntegration(t *testing.T) { require.NoError(t, os.Mkdir(accessLogsDir, 0o700)) require.NoError(t, os.Chmod(accessLogsDir, 0o777)) + // Detect the JVM server library directory so librust_module.so can dlopen + // libjvm.so when the java_filter initialises the embedded JVM. + jvmLibPath := jvmServerLibPath(t) + t.Logf("JVM lib path: %s", jvmLibPath) + if envoyImage := cmp.Or(os.Getenv("ENVOY_IMAGE")); envoyImage != "" { cmd := exec.Command( "docker", @@ -92,9 +102,15 @@ func TestIntegration(t *testing.T) { cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr + existingLD := os.Getenv("LD_LIBRARY_PATH") + ldLibPath := jvmLibPath + if existingLD != "" { + ldLibPath = jvmLibPath + ":" + existingLD + } cmd.Env = append(os.Environ(), "ENVOY_DYNAMIC_MODULES_SEARCH_PATH="+cwd, "GODEBUG=cgocheck=0", + "LD_LIBRARY_PATH="+ldLibPath, ) require.NoError(t, cmd.Start()) defer func() { @@ -422,6 +438,47 @@ func TestIntegration(t *testing.T) { }, 30*time.Second, 200*time.Millisecond) }) + t.Run("java_filter", func(t *testing.T) { + require.Eventually(t, func() bool { + req, err := http.NewRequest("GET", "http://127.0.0.1:1065/headers", nil) + require.NoError(t, err) + + resp, err := http.DefaultClient.Do(req) + if err != nil { + t.Logf("Envoy not ready yet: %v", err) + return false + } + defer func() { + require.NoError(t, resp.Body.Close()) + }() + body, err := io.ReadAll(resp.Body) + if err != nil { + t.Logf("Envoy not ready yet: %v", err) + return false + } + + t.Logf("response: headers=%v, body=%s", resp.Header, string(body)) + require.Equal(t, 200, resp.StatusCode) + + // httpbin echoes request headers back as JSON. + type httpBinHeadersBody struct { + Headers map[string][]string `json:"headers"` + } + var headersBody httpBinHeadersBody + require.NoError(t, json.Unmarshal(body, &headersBody)) + + // ExampleFilter adds x-java-filter: active to every request. + require.Contains(t, headersBody.Headers["X-Java-Filter"], "active", + "x-java-filter request header should be set by the Java filter") + + // ExampleFilter mirrors the :path back as x-java-filter-path on the response. + require.Equal(t, "/headers", resp.Header.Get("x-java-filter-path"), + "x-java-filter-path response header should mirror the request :path") + + return true + }, 30*time.Second, 200*time.Millisecond) + }) + t.Run("http_metrics", func(t *testing.T) { // Send test request require.Eventually(t, func() bool { @@ -495,3 +552,30 @@ func TestIntegration(t *testing.T) { }, 5*time.Second, 200*time.Millisecond) }) } + +// jvmServerLibPath returns the directory containing libjvm.so, needed by the +// java_filter at runtime so the Rust module can dlopen the JVM. +// It first checks $JAVA_HOME, then falls back to running `java -XshowSettings:all -version`. +func jvmServerLibPath(t *testing.T) string { + t.Helper() + javaHome := os.Getenv("JAVA_HOME") + if javaHome == "" { + out, err := exec.Command("java", "-XshowSettings:all", "-version").CombinedOutput() + if err == nil { + for _, line := range strings.Split(string(out), "\n") { + if strings.Contains(line, "java.home") { + parts := strings.SplitN(line, "=", 2) + if len(parts) == 2 { + javaHome = strings.TrimSpace(parts[1]) + break + } + } + } + } + } + if javaHome == "" { + t.Log("JAVA_HOME not set and could not be detected; java_filter may fail to load libjvm.so") + return "" + } + return filepath.Join(javaHome, "lib", "server") +} diff --git a/java/Makefile b/java/Makefile new file mode 100644 index 0000000..c843425 --- /dev/null +++ b/java/Makefile @@ -0,0 +1,21 @@ +JAVA_SRC_DIR := src +JAVA_OUT_DIR := out +JAR := $(JAVA_OUT_DIR)/envoy-java-filter.jar + +JAVA_SOURCES := \ + $(JAVA_SRC_DIR)/io/envoyproxy/dynamicmodules/HeaderMutation.java \ + $(JAVA_SRC_DIR)/io/envoyproxy/dynamicmodules/EnvoyHttpFilter.java \ + $(JAVA_SRC_DIR)/io/envoyproxy/dynamicmodules/ExampleFilter.java + +.PHONY: all clean + +all: $(JAR) + +$(JAR): $(JAVA_SOURCES) + mkdir -p $(JAVA_OUT_DIR) + javac -source 11 -target 11 -d $(JAVA_OUT_DIR) $(JAVA_SOURCES) + jar cf $(JAR) -C $(JAVA_OUT_DIR) . + @echo "Built $(JAR)" + +clean: + rm -rf $(JAVA_OUT_DIR) diff --git a/java/src/io/envoyproxy/dynamicmodules/EnvoyHttpFilter.java b/java/src/io/envoyproxy/dynamicmodules/EnvoyHttpFilter.java new file mode 100644 index 0000000..6ffcdf6 --- /dev/null +++ b/java/src/io/envoyproxy/dynamicmodules/EnvoyHttpFilter.java @@ -0,0 +1,41 @@ +package io.envoyproxy.dynamicmodules; + +/** + * Interface for implementing Envoy HTTP filters in Java. + * + *
Implement this interface, package your class in a JAR (together with + * {@link HeaderMutation}), and configure the Rust dynamic module: + * + *
{@code
+ * {
+ * "jar_path": "/path/to/your-filter.jar",
+ * "class_name": "com.example.YourFilter"
+ * }
+ * }
+ *
+ * Your class must have a public no-arg constructor. A single instance is + * created per filter-chain config block and reused across all requests, so + * implementations must be thread-safe if Envoy dispatches requests concurrently. + */ +public interface EnvoyHttpFilter { + + /** + * Called when request headers arrive from the downstream client. + * + * @param names header names (parallel array) + * @param values header values (parallel array, same length as {@code names}) + * @return mutations to apply, or {@code null} to leave headers unchanged + * and continue the filter chain + */ + HeaderMutation onRequestHeaders(String[] names, String[] values); + + /** + * Called when response headers arrive from the upstream. + * + * @param names header names (parallel array) + * @param values header values (parallel array, same length as {@code names}) + * @return mutations to apply, or {@code null} to leave headers unchanged + * and continue the filter chain + */ + HeaderMutation onResponseHeaders(String[] names, String[] values); +} diff --git a/java/src/io/envoyproxy/dynamicmodules/ExampleFilter.java b/java/src/io/envoyproxy/dynamicmodules/ExampleFilter.java new file mode 100644 index 0000000..7cd9505 --- /dev/null +++ b/java/src/io/envoyproxy/dynamicmodules/ExampleFilter.java @@ -0,0 +1,55 @@ +package io.envoyproxy.dynamicmodules; + +/** + * Example Envoy HTTP filter implemented in Java. + * + *
This filter: + *
Build and package this example with: + *
{@code
+ * make -C java
+ * }
+ *
+ * Then reference it in your Envoy config: + *
{@code
+ * {
+ * "jar_path": "/path/to/envoy-java-filter.jar",
+ * "class_name": "io.envoyproxy.dynamicmodules.ExampleFilter"
+ * }
+ * }
+ */
+public class ExampleFilter implements EnvoyHttpFilter {
+
+ // Captured from request headers; written into the response.
+ // In a concurrent environment you would use ThreadLocal instead.
+ private volatile String lastPath = "";
+
+ @Override
+ public HeaderMutation onRequestHeaders(String[] names, String[] values) {
+ // Capture :path for use in onResponseHeaders.
+ for (int i = 0; i < names.length; i++) {
+ if (":path".equals(names[i])) {
+ lastPath = values[i];
+ break;
+ }
+ }
+
+ HeaderMutation m = new HeaderMutation();
+ m.addHeaders = new String[]{"x-java-filter", "active"};
+ return m;
+ }
+
+ @Override
+ public HeaderMutation onResponseHeaders(String[] names, String[] values) {
+ HeaderMutation m = new HeaderMutation();
+ m.addHeaders = new String[]{"x-java-filter-path", lastPath};
+ m.removeHeaders = new String[]{"x-powered-by"};
+ return m;
+ }
+}
diff --git a/java/src/io/envoyproxy/dynamicmodules/HeaderMutation.java b/java/src/io/envoyproxy/dynamicmodules/HeaderMutation.java
new file mode 100644
index 0000000..ed392f0
--- /dev/null
+++ b/java/src/io/envoyproxy/dynamicmodules/HeaderMutation.java
@@ -0,0 +1,34 @@
+package io.envoyproxy.dynamicmodules;
+
+/**
+ * Describes mutations the Java filter wants to apply to HTTP headers.
+ *
+ * Return an instance of this class (or {@code null} for no changes) from + * {@link EnvoyHttpFilter#onRequestHeaders} and + * {@link EnvoyHttpFilter#onResponseHeaders}. + * + *
All fields are optional; leave them {@code null} or {@code false} to + * skip the corresponding action. + */ +public class HeaderMutation { + + /** + * If {@code true} the filter chain is stopped (Envoy StopIteration). + * Use this to short-circuit a request, e.g. for authentication failures. + * The upstream will not receive the request. + */ + public boolean stopIteration = false; + + /** + * Headers to add or overwrite, expressed as alternating name/value pairs: + * {@code ["name0", "value0", "name1", "value1", …]}. + * + *
Must be an even-length array (or {@code null}).
+ */
+ public String[] addHeaders = null;
+
+ /**
+ * Names of headers to remove. {@code null} means no removals.
+ */
+ public String[] removeHeaders = null;
+}
diff --git a/rust/Cargo.lock b/rust/Cargo.lock
index cfabf0d..6f47ac3 100644
--- a/rust/Cargo.lock
+++ b/rust/Cargo.lock
@@ -60,6 +60,12 @@ version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
+[[package]]
+name = "cesu8"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c"
+
[[package]]
name = "cexpr"
version = "0.6.0"
@@ -83,7 +89,17 @@ checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4"
dependencies = [
"glob",
"libc",
- "libloading",
+ "libloading 0.8.8",
+]
+
+[[package]]
+name = "combine"
+version = "4.6.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd"
+dependencies = [
+ "bytes",
+ "memchr",
]
[[package]]
@@ -163,6 +179,7 @@ dependencies = [
"dashmap",
"envoy-proxy-dynamic-modules-rust-sdk",
"hickory-proto",
+ "jni",
"matchers",
"once_cell",
"parking_lot",
@@ -431,12 +448,55 @@ version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
+[[package]]
+name = "java-locator"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09c46c1fe465c59b1474e665e85e1256c3893dd00927b8d55f63b09044c1e64f"
+dependencies = [
+ "glob",
+]
+
+[[package]]
+name = "jni"
+version = "0.21.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97"
+dependencies = [
+ "cesu8",
+ "cfg-if",
+ "combine",
+ "java-locator",
+ "jni-sys",
+ "libloading 0.7.4",
+ "log",
+ "thiserror",
+ "walkdir",
+ "windows-sys 0.45.0",
+]
+
+[[package]]
+name = "jni-sys"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130"
+
[[package]]
name = "libc"
version = "0.2.174"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776"
+[[package]]
+name = "libloading"
+version = "0.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f"
+dependencies = [
+ "cfg-if",
+ "winapi",
+]
+
[[package]]
name = "libloading"
version = "0.8.8"
@@ -789,6 +849,15 @@ version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
+[[package]]
+name = "same-file"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
+dependencies = [
+ "winapi-util",
+]
+
[[package]]
name = "scopeguard"
version = "1.2.0"
@@ -1016,6 +1085,16 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
+[[package]]
+name = "walkdir"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
+dependencies = [
+ "same-file",
+ "winapi-util",
+]
+
[[package]]
name = "wasi"
version = "0.11.1+wasi-snapshot-preview1"
@@ -1031,12 +1110,52 @@ dependencies = [
"wit-bindgen-rt",
]
+[[package]]
+name = "winapi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
+dependencies = [
+ "winapi-i686-pc-windows-gnu",
+ "winapi-x86_64-pc-windows-gnu",
+]
+
+[[package]]
+name = "winapi-i686-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+
+[[package]]
+name = "winapi-util"
+version = "0.1.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
+dependencies = [
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "winapi-x86_64-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
+
[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
+[[package]]
+name = "windows-sys"
+version = "0.45.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0"
+dependencies = [
+ "windows-targets 0.42.2",
+]
+
[[package]]
name = "windows-sys"
version = "0.59.0"
@@ -1064,6 +1183,21 @@ dependencies = [
"windows-link",
]
+[[package]]
+name = "windows-targets"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071"
+dependencies = [
+ "windows_aarch64_gnullvm 0.42.2",
+ "windows_aarch64_msvc 0.42.2",
+ "windows_i686_gnu 0.42.2",
+ "windows_i686_msvc 0.42.2",
+ "windows_x86_64_gnu 0.42.2",
+ "windows_x86_64_gnullvm 0.42.2",
+ "windows_x86_64_msvc 0.42.2",
+]
+
[[package]]
name = "windows-targets"
version = "0.52.6"
@@ -1096,6 +1230,12 @@ dependencies = [
"windows_x86_64_msvc 0.53.0",
]
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8"
+
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.6"
@@ -1108,6 +1248,12 @@ version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764"
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43"
+
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.6"
@@ -1120,6 +1266,12 @@ version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c"
+[[package]]
+name = "windows_i686_gnu"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f"
+
[[package]]
name = "windows_i686_gnu"
version = "0.52.6"
@@ -1144,6 +1296,12 @@ version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11"
+[[package]]
+name = "windows_i686_msvc"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060"
+
[[package]]
name = "windows_i686_msvc"
version = "0.52.6"
@@ -1156,6 +1314,12 @@ version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d"
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36"
+
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.6"
@@ -1168,6 +1332,12 @@ version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba"
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3"
+
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.6"
@@ -1180,6 +1350,12 @@ version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57"
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0"
+
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.6"
diff --git a/rust/Cargo.toml b/rust/Cargo.toml
index 1324a41..0320053 100644
--- a/rust/Cargo.toml
+++ b/rust/Cargo.toml
@@ -16,6 +16,7 @@ dashmap = "6.1.0"
once_cell = "1.20.2"
hickory-proto = "0.24"
parking_lot = "0.12"
+jni = { version = "0.21", features = ["invocation"] }
[dev-dependencies]
tempfile = "3.16.0"
diff --git a/rust/src/http_java_filter.rs b/rust/src/http_java_filter.rs
new file mode 100644
index 0000000..f043fab
--- /dev/null
+++ b/rust/src/http_java_filter.rs
@@ -0,0 +1,406 @@
+//! Java HTTP filter — embeds a JVM to execute user-provided Java filter logic.
+//!
+//! Users write their filter in Java by implementing the
+//! `io.envoyproxy.dynamicmodules.EnvoyHttpFilter` interface, compile it to a JAR,
+//! and point to it from the Envoy config.
+//!
+//! # Envoy filter config (JSON)
+//!
+//! ```json
+//! {
+//! "jar_path": "/path/to/your-filter.jar",
+//! "class_name": "io.envoyproxy.dynamicmodules.ExampleFilter"
+//! }
+//! ```
+//!
+//! # Java interface contract
+//!
+//! ```java
+//! public class MyFilter implements EnvoyHttpFilter {
+//! public HeaderMutation onRequestHeaders(String[] names, String[] values) {
+//! HeaderMutation m = new HeaderMutation();
+//! m.addHeaders = new String[]{"x-java-filter", "active"};
+//! return m;
+//! }
+//! public HeaderMutation onResponseHeaders(String[] names, String[] values) {
+//! return null; // no changes
+//! }
+//! }
+//! ```
+//!
+//! # Build requirements
+//!
+//! Java (JDK 11+) must be installed both at compile time (for the `jni` invocation
+//! feature) and at runtime (Envoy needs `libjvm.so` in `LD_LIBRARY_PATH`).
+//!
+//! To compile the Java sources and build the example JAR:
+//!
+//! ```sh
+//! make -C java
+//! ```
+
+use envoy_proxy_dynamic_modules_rust_sdk::*;
+use jni::{
+ objects::{GlobalRef, JObject, JObjectArray, JString, JValue},
+ sys::jsize,
+ InitArgsBuilder, JNIVersion, JavaVM,
+};
+use serde::Deserialize;
+use std::sync::OnceLock;
+
+// ---------------------------------------------------------------------------
+// Global JVM — there can only be one per process.
+// ---------------------------------------------------------------------------
+
+static JVM: OnceLock