You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Fix a real regression from my own isolation fix: dropped wildcard black-holed exact routes
Round 8 (codex + gemini + grok + harper-domain) reviewed the prior commit's
route-isolation fix and found I'd introduced exactly the class of bug it
was meant to prevent, just inverted: RouteTable::resolve() checked
dropped_wildcard before the exact-match lookup, so any SNI matching a
dropped wildcard's suffix returned None immediately — including a
perfectly healthy, fully-built exact route at that same SNI. One tenant's
wildcard misconfiguration (e.g. `*.acme.com` missing `protocol: 'http'`)
would black-hole every co-tenant with an exact route under that suffix
(`api.acme.com`, ACME challenge routes, etc.), which is worse than the
fall-through bug being fixed.
Root cause: I checked both dropped sets before any live lookup, on the
theory that "a poison entry should always win." That's wrong — a poison
entry must only suppress a fallback *broader* than itself, never something
*more specific*. Fixed by checking exact (live, then dropped) before moving
to wildcard (live, then dropped) before default — each dropped set
immediately follows its same-specificity live counterpart, so an exact
match always wins regardless of wildcard state. Added a Rust unit test
reproducing the exact scenario (healthy exact route + dropped sibling
wildcard) that fails against the previous ordering and passes against this
one — verified by reverting and reconfirming the failure before restoring.
Also from round 8: last-good carry-forward (designed for a transient
cert-rotation race that heals on the next reconcile) was reaching the new
protocol/no-carrier rejections too, which are permanent config errors that
never heal — so a route could silently keep serving a stale, previous
version indefinitely across every later reconcile, with the operator's
`updateConfig` reporting success while their edit quietly never took
effect. Extracted the two protocol/carrier checks into a standalone
validate_route_protocol_declaration(), called before build_route in the
build_route_table loop, and given its own never-carry-forward path — a
permanent rejection now always drops the route on a hot-swap, same as on
initial build. Added a test for this too.
Also narrowed the http2 warning in parse_route_spec: it fired for both
xForwardedFor and forwardFingerprint, but xForwardedFor+http2 is always a
hard rejection (build_route), so that branch was dead and misleadingly
implied the route degrades gracefully when it's actually dropped outright.
cargo build/clippy clean, cargo test 116/116, npm test 115/115.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
// xForwardedFor + http2 is a hard error (build_route, router.rs) since it's unconditionally
818
818
// unsafe — an h2 client's XFF would be neither injected nor stripped, forwarding an arbitrary
819
-
// client-supplied value as if authoritative. A header-carried forwardFingerprint in the same
820
-
// spot is intentionally only a warning here, not a hard error, even though the risk is the
821
-
// same in kind (an h2 client can forge X-JA3/X-JA4, not merely go unforwarded) — see
822
-
// README.md's "Forwarding the fingerprint downstream" section for the tradeoff this leaves
823
-
// open and why it isn't a hard error like XFF. Name whichever mode actually triggered this so
824
-
// the message doesn't blame X-Forwarded-For when the route never configured it.
825
-
if requires_http && spec.http2{
826
-
let mode_desc = if source_address_mode == SourceAddressMode::XForwardedFor{
827
-
"xForwardedFor"
828
-
}else{
829
-
"a header-carried forwardFingerprint (X-JA3/X-JA4)"
830
-
};
819
+
// client-supplied value as if authoritative — so it's excluded here: this warning would always
820
+
// be immediately followed by that hard rejection, misleadingly implying the route still works
821
+
// (minus the header) when the whole route is in fact dropped. A header-carried
822
+
// forwardFingerprint in the same spot is intentionally only a warning, not a hard error, even
823
+
// though the risk is the same in kind (an h2 client can forge X-JA3/X-JA4, not merely go
824
+
// unforwarded) — see README.md's "Forwarding the fingerprint downstream" section for the
825
+
// tradeoff this leaves open and why it isn't a hard error like XFF.
826
+
if requires_http && spec.http2 && source_address_mode != SourceAddressMode::XForwardedFor{
827
+
let mode_desc = "a header-carried forwardFingerprint (X-JA3/X-JA4)";
831
828
eprintln!(
832
829
"symphony: route '{}': {mode_desc} has no effect for any client that negotiates h2 (http2=true) — injection and client-supplied-header stripping are both skipped, so an h2 client's own value reaches the upstream unmodified; consider sourceAddressHeader='proxyProtocolV2'",
"route '{}': sourceAddressHeader 'xForwardedFor', or forwardFingerprint via an HTTP header (any mode other than 'proxyProtocolV2'), has no carrier when terminateTls=false (passthrough never decrypts the stream, so no header can be injected) — use sourceAddressHeader='proxyProtocolV2' (carries both source address and fingerprint), or remove forwardFingerprint",
@@ -456,6 +483,14 @@ fn build_route(
456
483
)));
457
484
}
458
485
486
+
Ok(())
487
+
}
488
+
489
+
fnbuild_route(
490
+
spec:&RouteSpec,
491
+
listener_tls:&ListenerTlsSpec,
492
+
cache:&mutTlsConfigCache,
493
+
) -> crate::error::Result<Route>{
459
494
let tls_config = if spec.terminate_tls{
460
495
let cert_pem = spec
461
496
.cert_pem
@@ -811,6 +846,30 @@ UlqL1DcgX6Szi9w/p7B4BZO9iA==
811
846
);
812
847
}
813
848
849
+
// The inverse and more dangerous case: a dropped wildcard must NOT shadow a healthy, more
850
+
// specific exact route at the same suffix. A prior version of the fail-closed check tested
851
+
// `dropped_wildcard` before `exact`, which black-holed every exact route under any wildcard
852
+
// that happened to fail validation — turning one tenant's wildcard typo into an outage for
0 commit comments