Skip to content

Commit 2a76dcf

Browse files
committed
Add TCP rerouting example with dynamic cluster selection
Introduce a new Proxy-Wasm TCP filter example that demonstrates dynamic routing of TCP connections based on source IP addresses. This example showcases TCP-level filtering, filling a gap in the existing HTTP-focused examples. Key features: - Dynamic cluster selection based on source IP last octet (even/odd) - Uses Envoy's set_envoy_filter_state foreign function with protobuf encoding - Includes comprehensive Docker Compose setup for testing - Full documentation with usage examples and expected output Implementation details: - Protobuf definitions for Envoy's filter state API - Build script for code generation from proto files - Unit tests for IP parsing and routing logic - Envoy configuration with dual upstream clusters
1 parent ba23ab4 commit 2a76dcf

File tree

8 files changed

+438
-0
lines changed

8 files changed

+438
-0
lines changed

examples/tcp_rerouting/Cargo.toml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
[package]
2+
publish = false
3+
name = "proxy-wasm-example-tcp-rerouting"
4+
version = "0.0.1"
5+
authors = ["Proxy-Wasm contributors"]
6+
description = "Proxy-Wasm plugin example: TCP Rerouting based on source IP"
7+
license = "Apache-2.0"
8+
edition = "2021"
9+
10+
[lib]
11+
crate-type = ["cdylib"]
12+
13+
[dependencies]
14+
log = "0.4"
15+
proxy-wasm = { path = "../../" }
16+
prost = "0.12"
17+
18+
[build-dependencies]
19+
prost-build = "0.12"

examples/tcp_rerouting/README.md

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
## Proxy-Wasm plugin example: TCP Rerouting
2+
3+
Proxy-Wasm TCP filter that dynamically routes connections to different upstream clusters based on the source IP address.
4+
5+
Most WASM filter examples focus on HTTP, but this example shows how to work at the TCP/IP level.
6+
7+
This example is inspired by the [wasmerang](https://github.com/SiiiTschiii/wasmerang) project, which demonstrates advanced TCP routing patterns in Envoy/Istio/K8s using WASM filters.
8+
9+
### Overview
10+
11+
This example demonstrates how to build a TCP filter that:
12+
13+
- Intercepts incoming TCP connections
14+
- Extracts the source IP address
15+
- Routes traffic to different upstream clusters based on whether the last octet is even or odd
16+
- **Even last octet** → routes to `egress-router1`
17+
- **Odd last octet** → routes to `egress-router2`
18+
19+
The filter uses Envoy's `set_envoy_filter_state` foreign function to dynamically override the TCP proxy cluster at runtime, requiring proper protobuf encoding via the included `set_envoy_filter_state.proto` file.
20+
21+
### Building
22+
23+
Build the WASM plugin from the example directory:
24+
25+
```sh
26+
$ cargo build --target wasm32-wasip1 --release
27+
```
28+
29+
### Running with Docker Compose
30+
31+
This example can be run with [`docker compose`](https://docs.docker.com/compose/install/) and has a matching Envoy configuration.
32+
33+
From the example directory:
34+
35+
```sh
36+
$ docker compose up
37+
```
38+
39+
### Test the Routing
40+
41+
In separate terminals, test the routing behavior with different source IP addresses:
42+
43+
```bash
44+
# Even IP (last octet 10) → routes to egress-router1
45+
docker run --rm -it --network tcp_rerouting_envoymesh --ip 172.22.0.10 curlimages/curl curl http://proxy:10000/ip -H "Host: httpbin.org"
46+
47+
# Odd IP (last octet 11) → routes to egress-router2
48+
docker run --rm -it --network tcp_rerouting_envoymesh --ip 172.22.0.11 curlimages/curl curl http://proxy:10000/ip -H "Host: httpbin.org"
49+
```
50+
51+
### Expected Output
52+
53+
Check the Docker Compose logs to see the WASM filter in action:
54+
55+
```console
56+
$ docker compose logs -f
57+
```
58+
59+
**For even IP (last octet 10) → routes to egress-router1:**
60+
61+
```
62+
proxy-1 | [TCP WASM] Source address: 172.22.0.10:39484
63+
proxy-1 | [TCP WASM] Source IP last octet: 10, intercepting ALL traffic
64+
proxy-1 | [TCP WASM] Routing to egress-router1
65+
proxy-1 | [TCP WASM] set_envoy_filter_state status (envoy.tcp_proxy.cluster): Ok(None)
66+
proxy-1 | [TCP WASM] Rerouting to egress-router1 via filter state
67+
proxy-1 | [2025-11-20T03:08:18.423Z] cluster=egress-router1 src=172.22.0.10:39484 dst=172.22.0.2:10000 -> 35.170.145.70:80
68+
```
69+
70+
**For odd IP (last octet 11) → routes to egress-router2:**
71+
72+
```
73+
proxy-1 | [TCP WASM] Source address: 172.22.0.11:55320
74+
proxy-1 | [TCP WASM] Source IP last octet: 11, intercepting ALL traffic
75+
proxy-1 | [TCP WASM] Routing to egress-router2
76+
proxy-1 | [TCP WASM] set_envoy_filter_state status (envoy.tcp_proxy.cluster): Ok(None)
77+
proxy-1 | [TCP WASM] Rerouting to egress-router2 via filter state
78+
proxy-1 | [2025-11-20T03:08:39.974Z] cluster=egress-router2 src=172.22.0.11:55320 dst=172.22.0.2:10000 -> 52.44.182.178:80
79+
```
80+
81+
The `Ok(None)` status confirms that the filter state was successfully set, and you can see in the access logs that traffic is being routed to the correct clusters (`egress-router1` for even IPs, `egress-router2` for odd IPs).

examples/tcp_rerouting/build.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
use std::fs;
2+
3+
fn main() {
4+
let out_dir = "src/generated";
5+
fs::create_dir_all(out_dir).unwrap();
6+
prost_build::Config::new()
7+
.out_dir(out_dir)
8+
.compile_protos(&["src/set_envoy_filter_state.proto"], &["src/"])
9+
.unwrap();
10+
println!("cargo:rerun-if-changed=src/set_envoy_filter_state.proto");
11+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
services:
2+
proxy:
3+
image: envoyproxy/envoy:v1.34.1
4+
entrypoint: /usr/local/bin/envoy -c /etc/envoy.yaml -l info --service-cluster proxy
5+
volumes:
6+
- ./envoy/envoy.yaml:/etc/envoy.yaml
7+
- ./target/wasm32-wasip1/release/proxy_wasm_example_tcp_rerouting.wasm:/etc/tcp_rerouting.wasm
8+
networks:
9+
- envoymesh
10+
ports:
11+
- "10000:10000"
12+
- "8001:8001"
13+
14+
networks:
15+
envoymesh:
16+
ipam:
17+
config:
18+
- subnet: 172.22.0.0/16
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
# Envoy configuration for TCP rerouting example
2+
static_resources:
3+
listeners:
4+
- name: main
5+
address:
6+
socket_address:
7+
address: 0.0.0.0
8+
port_value: 10000
9+
filter_chains:
10+
- filters:
11+
# WASM filter for TCP rerouting
12+
- name: envoy.filters.network.wasm
13+
typed_config:
14+
"@type": type.googleapis.com/envoy.extensions.filters.network.wasm.v3.Wasm
15+
config:
16+
name: tcp_rerouting_filter
17+
root_id: tcp_rerouting_filter
18+
configuration:
19+
"@type": type.googleapis.com/google.protobuf.StringValue
20+
value: "standalone"
21+
vm_config:
22+
vm_id: vm.tcp_rerouting
23+
runtime: envoy.wasm.runtime.v8
24+
code:
25+
local:
26+
filename: /etc/tcp_rerouting.wasm
27+
allow_precompiled: true
28+
# TCP proxy filter
29+
- name: envoy.filters.network.tcp_proxy
30+
typed_config:
31+
"@type": type.googleapis.com/envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy
32+
stat_prefix: destination
33+
cluster: egress-router1 # Default cluster, overridden by WASM filter
34+
access_log:
35+
- name: envoy.access_loggers.file
36+
typed_config:
37+
"@type": type.googleapis.com/envoy.extensions.access_loggers.file.v3.FileAccessLog
38+
path: /dev/stdout
39+
log_format:
40+
text_format: "[%START_TIME%] cluster=%UPSTREAM_CLUSTER% src=%DOWNSTREAM_REMOTE_ADDRESS% dst=%DOWNSTREAM_LOCAL_ADDRESS% -> %UPSTREAM_HOST%\n"
41+
42+
clusters:
43+
- name: egress-router1
44+
connect_timeout: 30s
45+
type: LOGICAL_DNS
46+
dns_lookup_family: V4_ONLY
47+
load_assignment:
48+
cluster_name: egress-router1
49+
endpoints:
50+
- lb_endpoints:
51+
- endpoint:
52+
address:
53+
socket_address:
54+
address: httpbin.org
55+
port_value: 80
56+
57+
- name: egress-router2
58+
connect_timeout: 30s
59+
type: LOGICAL_DNS
60+
dns_lookup_family: V4_ONLY
61+
load_assignment:
62+
cluster_name: egress-router2
63+
endpoints:
64+
- lb_endpoints:
65+
- endpoint:
66+
address:
67+
socket_address:
68+
address: httpbin.org
69+
port_value: 80
70+
71+
admin:
72+
access_log_path: "/dev/null"
73+
address:
74+
socket_address:
75+
address: 0.0.0.0
76+
port_value: 8001
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
// This file is @generated by prost-build.
2+
/// Argument expected by set_envoy_filter_state in envoy
3+
/// <https://github.com/envoyproxy/envoy/blob/d741713c376d1e024236519fb59406c05702ad77/source/extensions/common/wasm/foreign.cc#L116>
4+
#[allow(clippy::derive_partial_eq_without_eq)]
5+
#[derive(Clone, PartialEq, ::prost::Message)]
6+
pub struct SetEnvoyFilterStateArguments {
7+
#[prost(string, tag = "1")]
8+
pub path: ::prost::alloc::string::String,
9+
#[prost(string, tag = "2")]
10+
pub value: ::prost::alloc::string::String,
11+
#[prost(enumeration = "LifeSpan", tag = "3")]
12+
pub span: i32,
13+
}
14+
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)]
15+
#[repr(i32)]
16+
pub enum LifeSpan {
17+
FilterChain = 0,
18+
DownstreamRequest = 1,
19+
DownstreamConnection = 2,
20+
}
21+
impl LifeSpan {
22+
/// String value of the enum field names used in the ProtoBuf definition.
23+
///
24+
/// The values are not transformed in any way and thus are considered stable
25+
/// (if the ProtoBuf definition does not change) and safe for programmatic use.
26+
pub fn as_str_name(&self) -> &'static str {
27+
match self {
28+
LifeSpan::FilterChain => "FilterChain",
29+
LifeSpan::DownstreamRequest => "DownstreamRequest",
30+
LifeSpan::DownstreamConnection => "DownstreamConnection",
31+
}
32+
}
33+
/// Creates an enum from field names used in the ProtoBuf definition.
34+
pub fn from_str_name(value: &str) -> ::core::option::Option<Self> {
35+
match value {
36+
"FilterChain" => Some(Self::FilterChain),
37+
"DownstreamRequest" => Some(Self::DownstreamRequest),
38+
"DownstreamConnection" => Some(Self::DownstreamConnection),
39+
_ => None,
40+
}
41+
}
42+
}

0 commit comments

Comments
 (0)