Skip to content

Commit b465854

Browse files
committed
Add runnable HAProxy-style routing policy example
1 parent 56311de commit b465854

11 files changed

Lines changed: 149 additions & 36 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ behavior when the change improves security or project direction.
1818
opt-in Wasm release gate.
1919
- Add the checked-in nginx Lua/OpenResty-style bounded request/response header
2020
policy and config example, compiled directly by its real listener tests.
21+
- Add the checked-in HAProxy Lua/SPOE-style symbolic route policy and config
22+
example, with live canary, load-balancer, persistence, mirror, and selected
23+
route-policy coverage.
2124

2225
### Security
2326

crates/fluxheim-config/src/config_tests_wasm.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,17 @@ fn openresty_header_policy_example_config_validates() {
5555
assert_eq!(config.validate(), Ok(()));
5656
}
5757

58+
#[cfg(feature = "wasm")]
59+
#[test]
60+
fn haproxy_spoe_routing_policy_example_config_validates() {
61+
let config: Config = toml::from_str(include_str!(
62+
"../../../examples/wasm/haproxy-spoe-routing-policy.toml"
63+
))
64+
.unwrap();
65+
66+
assert_eq!(config.validate(), Ok(()));
67+
}
68+
5869
#[cfg(feature = "wasm")]
5970
fn proxy_preview_wasm_config(plugin_fields: &str) -> Config {
6071
toml::from_str(&format!(

crates/fluxheim-server/src/native_http1_route_proxy_tests/wasm.rs

Lines changed: 22 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -536,7 +536,8 @@ async fn native_wasm_forbidden_header_mutation_fails_closed() {
536536

537537
#[tokio::test]
538538
async fn native_wasm_route_decision_selects_configured_canary_route() {
539-
let fixture = WasmRouteFixture::new(&[("router", WasmPluginBody::RouteDecision)]);
539+
let fixture =
540+
WasmRouteFixture::new(&[("router", WasmPluginBody::HaProxySpoeRoutingPolicyExample)]);
540541
let stable = upstream_expect_body("/lb/item", "stable").await;
541542
let canary = upstream_expect_body("/lb/item", "canary").await;
542543
let mut config = fixture.config_with_attachments(
@@ -567,7 +568,8 @@ async fn native_wasm_route_decision_selects_configured_canary_route() {
567568

568569
#[tokio::test]
569570
async fn native_wasm_route_decision_fails_closed_for_unconfigured_branch() {
570-
let fixture = WasmRouteFixture::new(&[("router", WasmPluginBody::RouteDecision)]);
571+
let fixture =
572+
WasmRouteFixture::new(&[("router", WasmPluginBody::HaProxySpoeRoutingPolicyExample)]);
571573
let stable = upstream_expect_body("/lb/item", "stable").await;
572574
let mut config = fixture.config_with_attachments(
573575
stable,
@@ -594,7 +596,8 @@ async fn native_wasm_route_decision_fails_closed_for_unconfigured_branch() {
594596

595597
#[tokio::test]
596598
async fn native_wasm_route_decision_does_not_run_before_route_acl() {
597-
let fixture = WasmRouteFixture::new(&[("router", WasmPluginBody::RouteDecision)]);
599+
let fixture =
600+
WasmRouteFixture::new(&[("router", WasmPluginBody::HaProxySpoeRoutingPolicyExample)]);
598601
let upstream = super::upstream_expect_path("/never", "unexpected").await;
599602
let mut config = fixture.config_with_attachments(
600603
upstream,
@@ -624,7 +627,8 @@ async fn native_wasm_route_decision_does_not_run_before_route_acl() {
624627

625628
#[tokio::test]
626629
async fn native_wasm_route_decision_enforces_selected_route_rate_limit() {
627-
let fixture = WasmRouteFixture::new(&[("router", WasmPluginBody::RouteDecision)]);
630+
let fixture =
631+
WasmRouteFixture::new(&[("router", WasmPluginBody::HaProxySpoeRoutingPolicyExample)]);
628632
let stable = upstream_expect_body("/limit/item", "stable").await;
629633
let canary = upstream_expect_body("/limit/item", "canary").await;
630634
let mut config = fixture.config_with_attachments(
@@ -671,7 +675,8 @@ async fn native_wasm_route_decision_enforces_selected_route_rate_limit() {
671675
#[cfg(feature = "load-balancer")]
672676
#[tokio::test]
673677
async fn native_wasm_route_decision_selects_configured_load_balanced_route() {
674-
let fixture = WasmRouteFixture::new(&[("router", WasmPluginBody::RouteDecision)]);
678+
let fixture =
679+
WasmRouteFixture::new(&[("router", WasmPluginBody::HaProxySpoeRoutingPolicyExample)]);
675680
let stable = upstream_expect_body("/lb/item", "stable").await;
676681
let canary_a = upstream_expect_body("/lb/item", "canary-a").await;
677682
let canary_b = upstream_expect_body("/lb/item", "canary-b").await;
@@ -707,7 +712,8 @@ async fn native_wasm_route_decision_selects_configured_load_balanced_route() {
707712
#[cfg(feature = "load-balancer")]
708713
#[tokio::test]
709714
async fn native_wasm_route_decision_selects_configured_persistent_route() {
710-
let fixture = WasmRouteFixture::new(&[("router", WasmPluginBody::RouteDecision)]);
715+
let fixture =
716+
WasmRouteFixture::new(&[("router", WasmPluginBody::HaProxySpoeRoutingPolicyExample)]);
711717
let stable = upstream_expect_body("/sticky/item", "stable").await;
712718
let sticky_a = upstream_body_loop("sticky-a", 2).await;
713719
let sticky_b = upstream_body_loop("sticky-b", 2).await;
@@ -757,7 +763,8 @@ async fn native_wasm_route_decision_selects_configured_persistent_route() {
757763
#[cfg(all(feature = "traffic-mirror", not(feature = "privacy-mode")))]
758764
#[tokio::test]
759765
async fn native_wasm_route_decision_selects_configured_mirror_route() {
760-
let fixture = WasmRouteFixture::new(&[("router", WasmPluginBody::RouteDecision)]);
766+
let fixture =
767+
WasmRouteFixture::new(&[("router", WasmPluginBody::HaProxySpoeRoutingPolicyExample)]);
761768
let origin = upstream_expect_body("/shadow/item", "origin").await;
762769
let (mirror, mirror_rx) = mirror_endpoint().await;
763770
let mut config = fixture.config_with_attachments(
@@ -1169,7 +1176,7 @@ async fn native_wasm_cross_family_chain_runs_in_order_with_cache_hit_metadata()
11691176
let fixture = WasmRouteFixture::new(&[
11701177
("access", WasmPluginBody::Decision(1)),
11711178
("headers", WasmPluginBody::OpenRestyHeaderPolicyExample),
1172-
("router", WasmPluginBody::RouteDecision),
1179+
("router", WasmPluginBody::HaProxySpoeRoutingPolicyExample),
11731180
("cache_key", WasmPluginBody::CacheLookupDeviceKey),
11741181
("cache_store", WasmPluginBody::CacheStoreHeader),
11751182
]);
@@ -1904,7 +1911,7 @@ enum WasmPluginBody {
19041911
IrulesAccessPolicyExample,
19051912
OpenRestyHeaderPolicyExample,
19061913
ForbiddenHeader,
1907-
RouteDecision,
1914+
HaProxySpoeRoutingPolicyExample,
19081915
CacheLookup,
19091916
CacheLookupDeviceKey,
19101917
CacheLookupForbiddenKey,
@@ -1960,32 +1967,9 @@ impl WasmPluginBody {
19601967
"#
19611968
.to_owned()
19621969
}
1963-
Self::RouteDecision => {
1964-
r#"
1965-
(module
1966-
(import "fluxheim_policy_v1" "context" (func $context (param i32 i32) (result i32)))
1967-
(func (export "fluxheim_route_decision") (result i32)
1968-
i32.const 3
1969-
i32.const 0
1970-
call $context
1971-
i32.const 1
1972-
i32.eq
1973-
if (result i32)
1974-
i32.const 3
1975-
else
1976-
i32.const 2
1977-
i32.const 0
1978-
call $context
1979-
i32.const 1
1980-
i32.eq
1981-
if (result i32)
1982-
i32.const 1
1983-
else
1984-
i32.const 0
1985-
end
1986-
end))
1987-
"#
1988-
.to_owned()
1970+
Self::HaProxySpoeRoutingPolicyExample => {
1971+
include_str!("../../../../examples/wasm/haproxy-spoe-routing-policy.wat")
1972+
.to_owned()
19891973
}
19901974
Self::CacheLookup => {
19911975
r#"
@@ -2332,7 +2316,9 @@ fn wasm_plugin_phases(body: WasmPluginBody) -> Vec<fluxheim_config::WasmPluginPh
23322316
WasmPluginBody::ForbiddenHeader => {
23332317
vec![fluxheim_config::WasmPluginPhase::RequestHeaders]
23342318
}
2335-
WasmPluginBody::RouteDecision => vec![fluxheim_config::WasmPluginPhase::RouteDecision],
2319+
WasmPluginBody::HaProxySpoeRoutingPolicyExample => {
2320+
vec![fluxheim_config::WasmPluginPhase::RouteDecision]
2321+
}
23362322
WasmPluginBody::CacheLookup
23372323
| WasmPluginBody::CacheLookupDeviceKey
23382324
| WasmPluginBody::CacheLookupForbiddenKey

docs/wasm-policy-example-parity.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,14 @@ The hook can continue, deny, or select a configured matching branch; arbitrary
132132
pool names, persistence keys, and mirror/shadow target decisions remain staged
133133
for later `1.7.x` slices.
134134

135+
The `v1.7.9` runnable mapping is checked in as
136+
`examples/wasm/haproxy-spoe-routing-policy.wat` with a complete config fixture.
137+
The shared live smoke compiles that source and proves configured canary route
138+
selection, unavailable-branch fail-closed behavior, selected-route policy
139+
enforcement, native load-balancer selection, managed-cookie persistence, and
140+
safe configured mirror routing. Backend addresses, arbitrary persistence keys,
141+
and mirror URLs remain outside the guest ABI.
142+
135143
### VCL-Like Cache Policy
136144

137145
Capability target:

examples/wasm/README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,18 @@ cannot create arbitrary names or values. Unknown IDs, duplicate/oversized
3131
mutations, traps, timeouts, and admission failures follow the configured fail
3232
mode; security-sensitive examples use `fail-closed`.
3333

34+
`haproxy-spoe-routing-policy.wat` and
35+
`haproxy-spoe-routing-policy.toml` demonstrate the HAProxy Lua/SPOE-style
36+
routing mapping. The guest receives only bounded `x-canary: 1` and
37+
`x-mirror: 1` signals. It may continue normal selection or choose an existing
38+
matching route named `canary` or `mirror`; it cannot name an arbitrary pool,
39+
backend, URL, persistence key, or TLS policy.
40+
41+
The selected route still passes its own ACL, rate-limit, concurrency,
42+
load-balancer, health, persistence, retry, and traffic-mirror controls. If the
43+
symbolic branch is not configured and still valid for the request, selection
44+
fails closed instead of falling back to an attacker-chosen destination.
45+
3446
`cache-lookup-policy.wat` and `cache-store-policy.wat` demonstrate the bounded
3547
1.7.5 cache-policy ABI:
3648

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# Compile haproxy-spoe-routing-policy.wat to the configured .wasm path and
2+
# replace the sha256 placeholder with the compiled module digest before use.
3+
4+
[wasm]
5+
enabled = true
6+
plugin_roots = ["/etc/fluxheim/plugins"]
7+
max_total_concurrent_executions = 256
8+
9+
[[wasm.plugins]]
10+
name = "traffic_branch_policy"
11+
path = "/etc/fluxheim/plugins/haproxy-spoe-routing-policy.wasm"
12+
sha256 = "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"
13+
abi = "fluxheim-policy-v1"
14+
host_call_namespace = "fluxheim-policy-v1"
15+
phases = ["route-decision"]
16+
fail_mode = "fail-closed"
17+
18+
[wasm.plugins.limits]
19+
max_module_bytes = "1MiB"
20+
max_memory_bytes = "16MiB"
21+
max_table_elements = 10000
22+
fuel = 5000000
23+
timeout_ms = 50
24+
compile_timeout_ms = 500
25+
26+
[[wasm.attachments]]
27+
plugin = "traffic_branch_policy"
28+
vhost = "example"
29+
priority = 100
30+
phases = ["route-decision"]
31+
32+
[[vhosts]]
33+
name = "example"
34+
hosts = ["example.com"]
35+
36+
[[vhosts.routes]]
37+
name = "standard"
38+
path_prefix = "/app/"
39+
40+
[vhosts.routes.proxy]
41+
upstreams = ["127.0.0.1:3000"]
42+
43+
[[vhosts.routes]]
44+
name = "canary"
45+
path_prefix = "/app/"
46+
47+
[vhosts.routes.proxy]
48+
upstreams = ["127.0.0.1:3001"]
49+
50+
[[vhosts.routes]]
51+
name = "mirror"
52+
path_prefix = "/app/"
53+
54+
[vhosts.routes.proxy]
55+
upstreams = ["127.0.0.1:3000"]
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
(module
2+
(import "fluxheim_policy_v1" "context"
3+
(func $context (param i32 i32) (result i32)))
4+
5+
(func (export "fluxheim_route_decision") (result i32)
6+
;; Context field 3 reports the bounded x-mirror: 1 signal. Decision 3
7+
;; selects only an already configured matching route named "mirror".
8+
i32.const 3
9+
i32.const 0
10+
call $context
11+
i32.const 1
12+
i32.eq
13+
if (result i32)
14+
i32.const 3
15+
else
16+
;; Context field 2 reports x-canary: 1. Decision 1 selects only an
17+
;; already configured matching route named "canary".
18+
i32.const 2
19+
i32.const 0
20+
call $context
21+
i32.const 1
22+
i32.eq
23+
if (result i32)
24+
i32.const 1
25+
else
26+
i32.const 0
27+
end
28+
end))

packaging/rpm/fluxheim.spec

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,7 @@ fi
158158
- Start documented and runnable Wasm migration-example parity work.
159159
- Add the F5 iRules-style route access example and real listener smoke.
160160
- Add the nginx Lua/OpenResty-style bounded header-policy example.
161+
- Add the HAProxy Lua/SPOE-style bounded route and load-balancer example.
161162
- Harden snapshot opens against pathname races and isolate corruption fixtures
162163
behind safe store primitives.
163164

release-notes/RELEASE_NOTES_1.7.9.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ products.
1919
fixture. Live coverage proves the allow-listed origin request mutation,
2020
client response mutation, upstream-header removal, and fail-closed rejection
2121
of unknown mutation IDs.
22+
- Add a checked-in HAProxy Lua/SPOE-style route policy and complete config
23+
fixture. Live coverage proves symbolic canary/mirror selection, unavailable
24+
branch rejection, selected-route policy enforcement, native load balancing,
25+
and managed-cookie persistence without exposing backend addresses.
2226

2327
## Security
2428

scripts/smoke_wasm_policy_examples.sh

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,8 @@ cargo test --locked -p fluxheim-server --features wasm \
99
native_wasm_openresty_header_policy_example_uses_bounded_host_calls
1010
cargo test --locked -p fluxheim-server --features wasm \
1111
native_wasm_forbidden_header_mutation_fails_closed
12+
cargo test --locked -p fluxheim-server \
13+
--features "wasm,load-balancer,traffic-mirror" \
14+
native_wasm_route_decision
1215

1316
echo "wasm policy examples smoke: ok"

0 commit comments

Comments
 (0)