From 1f6c4e2a859b63afda792f1ad8eb96fac1dbacf2 Mon Sep 17 00:00:00 2001 From: Eric Curtin Date: Wed, 14 Oct 2026 22:10:47 +0100 Subject: [PATCH] e2e: accept agentgateway's WebSocket egress denial body Signed-off-by: Eric Curtin --- internal/e2e/atenet_dataplane.go | 17 ++++++++++ internal/e2e/atenet_dataplane_test.go | 29 +++++++++++++++++ .../suites/networking/websocketegress_test.go | 31 +++++++------------ 3 files changed, 58 insertions(+), 19 deletions(-) diff --git a/internal/e2e/atenet_dataplane.go b/internal/e2e/atenet_dataplane.go index dbae9f4133..6882db4c3a 100644 --- a/internal/e2e/atenet_dataplane.go +++ b/internal/e2e/atenet_dataplane.go @@ -35,12 +35,17 @@ type AtenetDataplane interface { IsRetryableParkingBudgetExhaustion(status int, body string) bool ParkingBudgetStatus() int IsEgressPolicyDenied(status int, body string) bool + IsWebSocketEgressDenied(status int, body string) bool PlatformMetricPrefixes([]string) []string RouteDurationSeen(context.Context, string) (bool, error) ScrapeMetrics(context.Context) (string, error) EgressConnectStats(before, after string) EgressConnectStats } +// webSocketEgressDeniedBody is the body both dataplanes are configured to +// answer a WebSocket upgrade from an actor with. +const webSocketEgressDeniedBody = "WebSocket egress is not supported" + // EgressConnectStats holds the delta of egress CONNECT connections and requests // observed between two metric scrapes. A value of -1 indicates that the // dataplane does not report the counter. @@ -81,6 +86,10 @@ func (envoyAtenetDataplane) IsEgressPolicyDenied(status int, body string) bool { (status == http.StatusBadGateway && strings.Contains(body, "request failed")) } +func (envoyAtenetDataplane) IsWebSocketEgressDenied(status int, body string) bool { + return status == http.StatusForbidden && body == webSocketEgressDeniedBody +} + func (envoyAtenetDataplane) PlatformMetricPrefixes(prefixes []string) []string { return prefixes } func (envoyAtenetDataplane) RouteDurationSeen(_ context.Context, collectorScrape string) (bool, error) { @@ -145,6 +154,14 @@ func (agentGatewayAtenetDataplane) IsEgressPolicyDenied(status int, body string) return status == http.StatusForbidden && strings.Contains(body, "actor egress policy denied") } +// IsWebSocketEgressDenied also accepts agentgateway's own denial: it applies +// its request policies before the route's direct response, so an upgrade +// without a substrate egress policy is refused with this body instead. +func (agentGatewayAtenetDataplane) IsWebSocketEgressDenied(status int, body string) bool { + return status == http.StatusForbidden && + (body == webSocketEgressDeniedBody || strings.Contains(body, "missing substrate egress request policy")) +} + func (agentGatewayAtenetDataplane) PlatformMetricPrefixes(prefixes []string) []string { filtered := make([]string, 0, len(prefixes)) for _, prefix := range prefixes { diff --git a/internal/e2e/atenet_dataplane_test.go b/internal/e2e/atenet_dataplane_test.go index 9d3b7c6d45..c47a156726 100644 --- a/internal/e2e/atenet_dataplane_test.go +++ b/internal/e2e/atenet_dataplane_test.go @@ -15,6 +15,7 @@ package e2e import ( + "fmt" "net/http" "testing" ) @@ -34,6 +35,34 @@ func TestAtenetDataplaneEgressPolicyDenial(t *testing.T) { }) } +func TestAtenetDataplaneWebSocketEgressDenial(t *testing.T) { + const ( + standard = "WebSocket egress is not supported" + agw = "missing substrate egress request policy" + ) + for _, tt := range []struct { + dataplane string + status int + body string + want bool + }{ + {"", http.StatusForbidden, standard, true}, + {"", http.StatusForbidden, agw, false}, + {"", http.StatusOK, standard, false}, + {"agentgateway", http.StatusForbidden, standard, true}, + {"agentgateway", http.StatusForbidden, agw, true}, + {"agentgateway", http.StatusForbidden, "other", false}, + {"agentgateway", http.StatusBadGateway, agw, false}, + } { + t.Run(fmt.Sprintf("%q/%d/%s", tt.dataplane, tt.status, tt.body), func(t *testing.T) { + t.Setenv(AtenetDataplaneEnv, tt.dataplane) + if got := CurrentAtenetDataplane().IsWebSocketEgressDenied(tt.status, tt.body); got != tt.want { + t.Errorf("IsWebSocketEgressDenied(%d, %q) = %v, want %v", tt.status, tt.body, got, tt.want) + } + }) + } +} + func TestAtenetDataplaneEgressConnectStats(t *testing.T) { const before = `# TYPE envoy_http_downstream_cx_http2_total counter envoy_http_downstream_cx_http2_total{envoy_http_conn_manager_prefix="egress_connect"} 1 diff --git a/internal/e2e/suites/networking/websocketegress_test.go b/internal/e2e/suites/networking/websocketegress_test.go index f70198e2c6..4602dc99d3 100644 --- a/internal/e2e/suites/networking/websocketegress_test.go +++ b/internal/e2e/suites/networking/websocketegress_test.go @@ -52,18 +52,7 @@ func TestActorEgressWebSocket(t *testing.T) { if err := json.Unmarshal(raw, &got); err != nil { t.Fatalf("decoding WebSocket observation: %v", err) } - if got.StatusCode != http.StatusForbidden { - t.Errorf("WebSocket handshake status = %d, want %d", got.StatusCode, http.StatusForbidden) - } - if got.HandshakeBody != "WebSocket egress is not supported" { - t.Errorf("WebSocket handshake body = %q, want %q", got.HandshakeBody, "WebSocket egress is not supported") - } - if got.Error == "" { - t.Error("WebSocket handshake error is empty") - } - if len(got.Messages) != 0 { - t.Errorf("WebSocket messages = %+v, want no echoes", got.Messages) - } + requireEgressWebSocketDenied(t, "WebSocket", got) } func TestActorEgressSecureWebSocket(t *testing.T) { @@ -80,16 +69,20 @@ func TestActorEgressSecureWebSocket(t *testing.T) { if err := json.Unmarshal(raw, &got); err != nil { t.Fatalf("decoding secure WebSocket observation: %v", err) } - if got.StatusCode != http.StatusForbidden { - t.Errorf("secure WebSocket handshake status = %d, want %d", got.StatusCode, http.StatusForbidden) - } - if got.HandshakeBody != "WebSocket egress is not supported" { - t.Errorf("secure WebSocket handshake body = %q, want %q", got.HandshakeBody, "WebSocket egress is not supported") + requireEgressWebSocketDenied(t, "secure WebSocket", got) +} + +// requireEgressWebSocketDenied checks that the dataplane refused the handshake +// and that nothing was echoed. +func requireEgressWebSocketDenied(t *testing.T, name string, got egressWebSocketResponse) { + t.Helper() + if !e2e.CurrentAtenetDataplane().IsWebSocketEgressDenied(got.StatusCode, got.HandshakeBody) { + t.Errorf("%s handshake = %d %q, want a WebSocket egress denial", name, got.StatusCode, got.HandshakeBody) } if got.Error == "" { - t.Error("secure WebSocket handshake error is empty") + t.Errorf("%s handshake error is empty", name) } if len(got.Messages) != 0 { - t.Errorf("secure WebSocket messages = %+v, want no echoes", got.Messages) + t.Errorf("%s messages = %+v, want no echoes", name, got.Messages) } }