From 18faedcdd829860a06ea86e5296561567c1aa48b Mon Sep 17 00:00:00 2001 From: "Huabing (Robin) Zhao" Date: Tue, 12 May 2026 15:31:52 +0800 Subject: [PATCH 01/18] API: host rewrite from path Signed-off-by: Huabing (Robin) Zhao --- api/v1alpha1/httproutefilter_types.go | 34 ++++++++++++++++++- api/v1alpha1/zz_generated.deepcopy.go | 20 +++++++++++ ...ateway.envoyproxy.io_httproutefilters.yaml | 33 ++++++++++++++++++ ...ateway.envoyproxy.io_httproutefilters.yaml | 33 ++++++++++++++++++ site/content/en/latest/api/extension_types.md | 16 +++++++++ test/helm/gateway-crds-helm/all.out.yaml | 33 ++++++++++++++++++ test/helm/gateway-crds-helm/e2e.out.yaml | 33 ++++++++++++++++++ .../envoy-gateway-crds.out.yaml | 33 ++++++++++++++++++ 8 files changed, 234 insertions(+), 1 deletion(-) diff --git a/api/v1alpha1/httproutefilter_types.go b/api/v1alpha1/httproutefilter_types.go index b14d273933..4f550bd043 100644 --- a/api/v1alpha1/httproutefilter_types.go +++ b/api/v1alpha1/httproutefilter_types.go @@ -116,6 +116,10 @@ const ( // BackendHTTPHostnameModifier indicates that the Host header value would be replaced by the DNS name of the backend if it exists. // https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/route/v3/route_components.proto#envoy-v3-api-field-config-route-v3-routeaction-auto-host-rewrite BackendHTTPHostnameModifier HTTPHostnameModifierType = "Backend" + // PathRegexHTTPHostnameModifier indicates that the Host header value would be rewritten by applying a regex + // match and substitution to the request path. + // https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/route/v3/route_components.proto#envoy-v3-api-field-config-route-v3-routeaction-host-rewrite-path-regex + PathRegexHTTPHostnameModifier HTTPHostnameModifierType = "PathRegex" ) type ReplaceRegexMatch struct { @@ -128,6 +132,21 @@ type ReplaceRegexMatch struct { Substitution string `json:"substitution"` } +// HostnamePathRegexRewrite defines a hostname rewrite computed from the request path using regex. +type HostnamePathRegexRewrite struct { + // Pattern matches a regular expression against the value of the HTTP Path. The regex string must + // adhere to the syntax documented in https://github.com/google/re2/wiki/Syntax. + // +kubebuilder:validation:MinLength=1 + Pattern string `json:"pattern"` + // Substitution is an expression that replaces the matched portion. The expression may include numbered + // capture groups that adhere to syntax documented in https://github.com/google/re2/wiki/Syntax. + // + // The resulting value is used as the upstream Host header and should be constrained to a valid + // DNS hostname by using explicit regex capture groups in Pattern. + // +kubebuilder:validation:MinLength=1 + Substitution string `json:"substitution"` +} + // +kubebuilder:validation:XValidation:rule="self.type == 'ReplaceRegexMatch' ? has(self.replaceRegexMatch) : !has(self.replaceRegexMatch)",message="If HTTPPathModifier type is ReplaceRegexMatch, replaceRegexMatch field needs to be set." type HTTPPathModifier struct { // +kubebuilder:validation:Enum=ReplaceRegexMatch @@ -158,14 +177,27 @@ type HTTPPathModifier struct { // +kubebuilder:validation:XValidation:message="header must be nil if the type is not Header",rule="!(has(self.header) && self.type != 'Header')" // +kubebuilder:validation:XValidation:message="header must be specified for Header type",rule="!(!has(self.header) && self.type == 'Header')" +// +kubebuilder:validation:XValidation:message="pathRegex must be nil if the type is not PathRegex",rule="!(has(self.pathRegex) && self.type != 'PathRegex')" +// +kubebuilder:validation:XValidation:message="pathRegex must be specified for PathRegex type",rule="!(!has(self.pathRegex) && self.type == 'PathRegex')" type HTTPHostnameModifier struct { - // +kubebuilder:validation:Enum=Header;Backend + // +kubebuilder:validation:Enum=Header;Backend;PathRegex // +kubebuilder:validation:Required Type HTTPHostnameModifierType `json:"type"` // Header is the name of the header whose value would be used to rewrite the Host header // +optional Header *string `json:"header,omitempty"` + + // PathRegex defines a regex match and substitution applied to the request path to compute + // the rewritten Host header. + // For example: + // pathRegex: + // pattern: "^/tenant/([a-z0-9-]+)/.*" + // substitution: "\\1.example.internal" + // + // +optional + // +notImplementedHide + PathRegex *HostnamePathRegexRewrite `json:"pathRegex,omitempty"` } // HTTPCredentialInjectionFilter defines the configuration to inject credentials into the request. diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 091e03c578..426008bd6b 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -4483,6 +4483,11 @@ func (in *HTTPHostnameModifier) DeepCopyInto(out *HTTPHostnameModifier) { *out = new(string) **out = **in } + if in.PathRegex != nil { + in, out := &in.PathRegex, &out.PathRegex + *out = new(HostnamePathRegexRewrite) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HTTPHostnameModifier. @@ -4907,6 +4912,21 @@ func (in *HealthCheckSettings) DeepCopy() *HealthCheckSettings { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *HostnamePathRegexRewrite) DeepCopyInto(out *HostnamePathRegexRewrite) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HostnamePathRegexRewrite. +func (in *HostnamePathRegexRewrite) DeepCopy() *HostnamePathRegexRewrite { + if in == nil { + return nil + } + out := new(HostnamePathRegexRewrite) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *IPEndpoint) DeepCopyInto(out *IPEndpoint) { *out = *in diff --git a/charts/gateway-crds-helm/templates/generated/gateway.envoyproxy.io_httproutefilters.yaml b/charts/gateway-crds-helm/templates/generated/gateway.envoyproxy.io_httproutefilters.yaml index 6d16615bdb..212a0e1b2b 100644 --- a/charts/gateway-crds-helm/templates/generated/gateway.envoyproxy.io_httproutefilters.yaml +++ b/charts/gateway-crds-helm/templates/generated/gateway.envoyproxy.io_httproutefilters.yaml @@ -415,12 +415,41 @@ spec: description: Header is the name of the header whose value would be used to rewrite the Host header type: string + pathRegex: + description: |- + PathRegex defines a regex match and substitution applied to the request path to compute + the rewritten Host header. + For example: + pathRegex: + pattern: "^/tenant/([a-z0-9-]+)/.*" + substitution: "\\1.example.internal" + properties: + pattern: + description: |- + Pattern matches a regular expression against the value of the HTTP Path. The regex string must + adhere to the syntax documented in https://github.com/google/re2/wiki/Syntax. + minLength: 1 + type: string + substitution: + description: |- + Substitution is an expression that replaces the matched portion. The expression may include numbered + capture groups that adhere to syntax documented in https://github.com/google/re2/wiki/Syntax. + + The resulting value is used as the upstream Host header and should be constrained to a valid + DNS hostname by using explicit regex capture groups in Pattern. + minLength: 1 + type: string + required: + - pattern + - substitution + type: object type: description: HTTPPathModifierType defines the type of Hostname rewrite. enum: - Header - Backend + - PathRegex type: string required: - type @@ -430,6 +459,10 @@ spec: rule: '!(has(self.header) && self.type != ''Header'')' - message: header must be specified for Header type rule: '!(!has(self.header) && self.type == ''Header'')' + - message: pathRegex must be nil if the type is not PathRegex + rule: '!(has(self.pathRegex) && self.type != ''PathRegex'')' + - message: pathRegex must be specified for PathRegex type + rule: '!(!has(self.pathRegex) && self.type == ''PathRegex'')' path: description: Path defines a path rewrite. properties: diff --git a/charts/gateway-helm/charts/crds/crds/generated/gateway.envoyproxy.io_httproutefilters.yaml b/charts/gateway-helm/charts/crds/crds/generated/gateway.envoyproxy.io_httproutefilters.yaml index 7d4f8c7f6d..d7155ad5c7 100644 --- a/charts/gateway-helm/charts/crds/crds/generated/gateway.envoyproxy.io_httproutefilters.yaml +++ b/charts/gateway-helm/charts/crds/crds/generated/gateway.envoyproxy.io_httproutefilters.yaml @@ -414,12 +414,41 @@ spec: description: Header is the name of the header whose value would be used to rewrite the Host header type: string + pathRegex: + description: |- + PathRegex defines a regex match and substitution applied to the request path to compute + the rewritten Host header. + For example: + pathRegex: + pattern: "^/tenant/([a-z0-9-]+)/.*" + substitution: "\\1.example.internal" + properties: + pattern: + description: |- + Pattern matches a regular expression against the value of the HTTP Path. The regex string must + adhere to the syntax documented in https://github.com/google/re2/wiki/Syntax. + minLength: 1 + type: string + substitution: + description: |- + Substitution is an expression that replaces the matched portion. The expression may include numbered + capture groups that adhere to syntax documented in https://github.com/google/re2/wiki/Syntax. + + The resulting value is used as the upstream Host header and should be constrained to a valid + DNS hostname by using explicit regex capture groups in Pattern. + minLength: 1 + type: string + required: + - pattern + - substitution + type: object type: description: HTTPPathModifierType defines the type of Hostname rewrite. enum: - Header - Backend + - PathRegex type: string required: - type @@ -429,6 +458,10 @@ spec: rule: '!(has(self.header) && self.type != ''Header'')' - message: header must be specified for Header type rule: '!(!has(self.header) && self.type == ''Header'')' + - message: pathRegex must be nil if the type is not PathRegex + rule: '!(has(self.pathRegex) && self.type != ''PathRegex'')' + - message: pathRegex must be specified for PathRegex type + rule: '!(!has(self.pathRegex) && self.type == ''PathRegex'')' path: description: Path defines a path rewrite. properties: diff --git a/site/content/en/latest/api/extension_types.md b/site/content/en/latest/api/extension_types.md index 5fd964201a..c7118e9abd 100644 --- a/site/content/en/latest/api/extension_types.md +++ b/site/content/en/latest/api/extension_types.md @@ -3090,6 +3090,7 @@ _Appears in:_ | ----- | ----------- | | `Header` | HeaderHTTPHostnameModifier indicates that the Host header value would be replaced with the value of the header specified in header.
https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/route/v3/route_components.proto#envoy-v3-api-field-config-route-v3-routeaction-host-rewrite-header
| | `Backend` | BackendHTTPHostnameModifier indicates that the Host header value would be replaced by the DNS name of the backend if it exists.
https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/route/v3/route_components.proto#envoy-v3-api-field-config-route-v3-routeaction-auto-host-rewrite
| +| `PathRegex` | PathRegexHTTPHostnameModifier indicates that the Host header value would be rewritten by applying a regex
match and substitution to the request path.
https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/route/v3/route_components.proto#envoy-v3-api-field-config-route-v3-routeaction-host-rewrite-path-regex
| #### HTTPPathModifier @@ -3363,6 +3364,21 @@ _Appears in:_ | `path` | _string_ | true | | Path specifies the HTTP path to match on for health check requests. | +#### HostnamePathRegexRewrite + + + +HostnamePathRegexRewrite defines a hostname rewrite computed from the request path using regex. + +_Appears in:_ +- [HTTPHostnameModifier](#httphostnamemodifier) + +| Field | Type | Required | Default | Description | +| --- | --- | --- | --- | --- | +| `pattern` | _string_ | true | | Pattern matches a regular expression against the value of the HTTP Path. The regex string must
adhere to the syntax documented in https://github.com/google/re2/wiki/Syntax. | +| `substitution` | _string_ | true | | Substitution is an expression that replaces the matched portion. The expression may include numbered
capture groups that adhere to syntax documented in https://github.com/google/re2/wiki/Syntax.
The resulting value is used as the upstream Host header and should be constrained to a valid
DNS hostname by using explicit regex capture groups in Pattern. | + + #### IPEndpoint diff --git a/test/helm/gateway-crds-helm/all.out.yaml b/test/helm/gateway-crds-helm/all.out.yaml index 7b513e0e3e..d2ffcb035d 100644 --- a/test/helm/gateway-crds-helm/all.out.yaml +++ b/test/helm/gateway-crds-helm/all.out.yaml @@ -50185,12 +50185,41 @@ spec: description: Header is the name of the header whose value would be used to rewrite the Host header type: string + pathRegex: + description: |- + PathRegex defines a regex match and substitution applied to the request path to compute + the rewritten Host header. + For example: + pathRegex: + pattern: "^/tenant/([a-z0-9-]+)/.*" + substitution: "\\1.example.internal" + properties: + pattern: + description: |- + Pattern matches a regular expression against the value of the HTTP Path. The regex string must + adhere to the syntax documented in https://github.com/google/re2/wiki/Syntax. + minLength: 1 + type: string + substitution: + description: |- + Substitution is an expression that replaces the matched portion. The expression may include numbered + capture groups that adhere to syntax documented in https://github.com/google/re2/wiki/Syntax. + + The resulting value is used as the upstream Host header and should be constrained to a valid + DNS hostname by using explicit regex capture groups in Pattern. + minLength: 1 + type: string + required: + - pattern + - substitution + type: object type: description: HTTPPathModifierType defines the type of Hostname rewrite. enum: - Header - Backend + - PathRegex type: string required: - type @@ -50200,6 +50229,10 @@ spec: rule: '!(has(self.header) && self.type != ''Header'')' - message: header must be specified for Header type rule: '!(!has(self.header) && self.type == ''Header'')' + - message: pathRegex must be nil if the type is not PathRegex + rule: '!(has(self.pathRegex) && self.type != ''PathRegex'')' + - message: pathRegex must be specified for PathRegex type + rule: '!(!has(self.pathRegex) && self.type == ''PathRegex'')' path: description: Path defines a path rewrite. properties: diff --git a/test/helm/gateway-crds-helm/e2e.out.yaml b/test/helm/gateway-crds-helm/e2e.out.yaml index d46b9ba2ca..7d132746c7 100644 --- a/test/helm/gateway-crds-helm/e2e.out.yaml +++ b/test/helm/gateway-crds-helm/e2e.out.yaml @@ -28158,12 +28158,41 @@ spec: description: Header is the name of the header whose value would be used to rewrite the Host header type: string + pathRegex: + description: |- + PathRegex defines a regex match and substitution applied to the request path to compute + the rewritten Host header. + For example: + pathRegex: + pattern: "^/tenant/([a-z0-9-]+)/.*" + substitution: "\\1.example.internal" + properties: + pattern: + description: |- + Pattern matches a regular expression against the value of the HTTP Path. The regex string must + adhere to the syntax documented in https://github.com/google/re2/wiki/Syntax. + minLength: 1 + type: string + substitution: + description: |- + Substitution is an expression that replaces the matched portion. The expression may include numbered + capture groups that adhere to syntax documented in https://github.com/google/re2/wiki/Syntax. + + The resulting value is used as the upstream Host header and should be constrained to a valid + DNS hostname by using explicit regex capture groups in Pattern. + minLength: 1 + type: string + required: + - pattern + - substitution + type: object type: description: HTTPPathModifierType defines the type of Hostname rewrite. enum: - Header - Backend + - PathRegex type: string required: - type @@ -28173,6 +28202,10 @@ spec: rule: '!(has(self.header) && self.type != ''Header'')' - message: header must be specified for Header type rule: '!(!has(self.header) && self.type == ''Header'')' + - message: pathRegex must be nil if the type is not PathRegex + rule: '!(has(self.pathRegex) && self.type != ''PathRegex'')' + - message: pathRegex must be specified for PathRegex type + rule: '!(!has(self.pathRegex) && self.type == ''PathRegex'')' path: description: Path defines a path rewrite. properties: diff --git a/test/helm/gateway-crds-helm/envoy-gateway-crds.out.yaml b/test/helm/gateway-crds-helm/envoy-gateway-crds.out.yaml index 7a250dc54c..d906358afb 100644 --- a/test/helm/gateway-crds-helm/envoy-gateway-crds.out.yaml +++ b/test/helm/gateway-crds-helm/envoy-gateway-crds.out.yaml @@ -28158,12 +28158,41 @@ spec: description: Header is the name of the header whose value would be used to rewrite the Host header type: string + pathRegex: + description: |- + PathRegex defines a regex match and substitution applied to the request path to compute + the rewritten Host header. + For example: + pathRegex: + pattern: "^/tenant/([a-z0-9-]+)/.*" + substitution: "\\1.example.internal" + properties: + pattern: + description: |- + Pattern matches a regular expression against the value of the HTTP Path. The regex string must + adhere to the syntax documented in https://github.com/google/re2/wiki/Syntax. + minLength: 1 + type: string + substitution: + description: |- + Substitution is an expression that replaces the matched portion. The expression may include numbered + capture groups that adhere to syntax documented in https://github.com/google/re2/wiki/Syntax. + + The resulting value is used as the upstream Host header and should be constrained to a valid + DNS hostname by using explicit regex capture groups in Pattern. + minLength: 1 + type: string + required: + - pattern + - substitution + type: object type: description: HTTPPathModifierType defines the type of Hostname rewrite. enum: - Header - Backend + - PathRegex type: string required: - type @@ -28173,6 +28202,10 @@ spec: rule: '!(has(self.header) && self.type != ''Header'')' - message: header must be specified for Header type rule: '!(!has(self.header) && self.type == ''Header'')' + - message: pathRegex must be nil if the type is not PathRegex + rule: '!(has(self.pathRegex) && self.type != ''PathRegex'')' + - message: pathRegex must be specified for PathRegex type + rule: '!(!has(self.pathRegex) && self.type == ''PathRegex'')' path: description: Path defines a path rewrite. properties: From b274ae3ed43c57760747dda76b713f530ff2d999 Mon Sep 17 00:00:00 2001 From: Huabing Zhao Date: Mon, 8 Jun 2026 14:20:59 +0800 Subject: [PATCH 02/18] implementation Signed-off-by: Huabing Zhao --- internal/gatewayapi/filters.go | 22 +++ ...urlrewrite-hostname-filter-invalid.in.yaml | 71 ++++++++++ ...rlrewrite-hostname-filter-invalid.out.yaml | 130 ++++++++++++++++-- ...te-with-urlrewrite-hostname-filter.in.yaml | 37 +++++ ...e-with-urlrewrite-hostname-filter.out.yaml | 80 ++++++++++- internal/ir/xds.go | 16 ++- internal/ir/zz_generated.deepcopy.go | 5 + internal/xds/translator/route.go | 9 ++ internal/xds/translator/route_test.go | 57 ++++++++ .../xds-ir/http-route-rewrite-url-host.yaml | 21 +++ .../http-route-rewrite-url-host.routes.yaml | 17 +++ release-notes/current.yaml | 1 + test/cel-validation/httproutefilter_test.go | 114 +++++++++++++++ 13 files changed, 565 insertions(+), 15 deletions(-) diff --git a/internal/gatewayapi/filters.go b/internal/gatewayapi/filters.go index 6fe4d2b77c..116273952e 100644 --- a/internal/gatewayapi/filters.go +++ b/internal/gatewayapi/filters.go @@ -886,6 +886,28 @@ func (t *Translator) processExtensionRefHTTPFilter(extFilter *gwapiv1.LocalObjec hm = &ir.HTTPHostModifier{ Backend: new(true), } + case egv1a1.PathRegexHTTPHostnameModifier: + if hrf.Spec.URLRewrite.Hostname.PathRegex == nil || + hrf.Spec.URLRewrite.Hostname.PathRegex.Pattern == "" || + hrf.Spec.URLRewrite.Hostname.PathRegex.Substitution == "" { + return status.NewRouteStatusError( + errors.New("PathRegex Pattern and Substitution must be set when rewrite hostname type is \"PathRegex\""), + gwapiv1.RouteReasonUnsupportedValue, + ).WithType(gwapiv1.RouteConditionAccepted) + } else if _, err := regexp.Compile(hrf.Spec.URLRewrite.Hostname.PathRegex.Pattern); err != nil { + // Avoid envoy NACKs due to invalid regex. + // Golang's regexp is almost identical to RE2: https://pkg.go.dev/regexp/syntax + return status.NewRouteStatusError( + errors.New("PathRegex must be a valid RE2 regular expression"), + gwapiv1.RouteReasonUnsupportedValue, + ).WithType(gwapiv1.RouteConditionAccepted) + } + hm = &ir.HTTPHostModifier{ + PathRegex: &ir.RegexMatchReplace{ + Pattern: hrf.Spec.URLRewrite.Hostname.PathRegex.Pattern, + Substitution: hrf.Spec.URLRewrite.Hostname.PathRegex.Substitution, + }, + } } if filterContext.URLRewrite != nil { diff --git a/internal/gatewayapi/testdata/httproute-with-urlrewrite-hostname-filter-invalid.in.yaml b/internal/gatewayapi/testdata/httproute-with-urlrewrite-hostname-filter-invalid.in.yaml index 5a7f449904..84fc643990 100644 --- a/internal/gatewayapi/testdata/httproute-with-urlrewrite-hostname-filter-invalid.in.yaml +++ b/internal/gatewayapi/testdata/httproute-with-urlrewrite-hostname-filter-invalid.in.yaml @@ -186,6 +186,56 @@ httpRoutes: group: gateway.envoyproxy.io kind: HTTPRouteFilter name: valid-header + - apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + namespace: default + name: httproute-missing-path-regex + spec: + hostnames: + - gateway.envoyproxy.io + parentRefs: + - namespace: envoy-gateway + name: gateway-1 + sectionName: http + rules: + - matches: + - path: + value: "/missing-path-regex" + backendRefs: + - name: service-1 + port: 8080 + filters: + - type: ExtensionRef + extensionRef: + group: gateway.envoyproxy.io + kind: HTTPRouteFilter + name: missing-path-regex + - apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + namespace: default + name: httproute-invalid-path-regex + spec: + hostnames: + - gateway.envoyproxy.io + parentRefs: + - namespace: envoy-gateway + name: gateway-1 + sectionName: http + rules: + - matches: + - path: + value: "/invalid-path-regex" + backendRefs: + - name: service-1 + port: 8080 + filters: + - type: ExtensionRef + extensionRef: + group: gateway.envoyproxy.io + kind: HTTPRouteFilter + name: invalid-path-regex httpFilters: - apiVersion: gateway.envoyproxy.io/v1alpha1 kind: HTTPRouteFilter @@ -196,6 +246,27 @@ httpFilters: urlRewrite: hostname: type: Header + - apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: HTTPRouteFilter + metadata: + name: missing-path-regex + namespace: default + spec: + urlRewrite: + hostname: + type: PathRegex + - apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: HTTPRouteFilter + metadata: + name: invalid-path-regex + namespace: default + spec: + urlRewrite: + hostname: + type: PathRegex + pathRegex: + pattern: '"([a-z]+)"*+?' + substitution: "backend-\\1.service.namespace.svc.cluster.local" header: my-host - apiVersion: gateway.envoyproxy.io/v1alpha1 kind: HTTPRouteFilter diff --git a/internal/gatewayapi/testdata/httproute-with-urlrewrite-hostname-filter-invalid.out.yaml b/internal/gatewayapi/testdata/httproute-with-urlrewrite-hostname-filter-invalid.out.yaml index cad290b2e0..53b0234a16 100644 --- a/internal/gatewayapi/testdata/httproute-with-urlrewrite-hostname-filter-invalid.out.yaml +++ b/internal/gatewayapi/testdata/httproute-with-urlrewrite-hostname-filter-invalid.out.yaml @@ -16,7 +16,7 @@ gateways: protocol: HTTP status: listeners: - - attachedRoutes: 6 + - attachedRoutes: 8 conditions: - lastTransitionTime: null message: Sending translated listener configuration to the data plane @@ -115,7 +115,7 @@ httpRoutes: parents: - conditions: - lastTransitionTime: null - message: Cannot configure multiple urlRewrite filters for a single HTTPRouteRule. + message: Header must be set when rewrite path type is "Header". reason: UnsupportedValue status: "False" type: Accepted @@ -209,7 +209,7 @@ httpRoutes: parents: - conditions: - lastTransitionTime: null - message: Cannot configure multiple urlRewrite filters for a single HTTPRouteRule. + message: Header must be set when rewrite path type is "Header". reason: UnsupportedValue status: "False" type: Accepted @@ -305,7 +305,96 @@ httpRoutes: parents: - conditions: - lastTransitionTime: null - message: Cannot configure multiple urlRewrite filters for a single HTTPRouteRule. + message: |- + Header must be set when rewrite path type is "Header". + Header must be set when rewrite path type is "Header". + reason: UnsupportedValue + status: "False" + type: Accepted + - lastTransitionTime: null + message: Resolved all the Object references for the Route + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + controllerName: gateway.envoyproxy.io/gatewayclass-controller + parentRef: + name: gateway-1 + namespace: envoy-gateway + sectionName: http +- apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + name: httproute-missing-path-regex + namespace: default + spec: + hostnames: + - gateway.envoyproxy.io + parentRefs: + - name: gateway-1 + namespace: envoy-gateway + sectionName: http + rules: + - backendRefs: + - name: service-1 + port: 8080 + filters: + - extensionRef: + group: gateway.envoyproxy.io + kind: HTTPRouteFilter + name: missing-path-regex + type: ExtensionRef + matches: + - path: + value: /missing-path-regex + status: + parents: + - conditions: + - lastTransitionTime: null + message: PathRegex Pattern and Substitution must be set when rewrite hostname + type is "PathRegex". + reason: UnsupportedValue + status: "False" + type: Accepted + - lastTransitionTime: null + message: Resolved all the Object references for the Route + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + controllerName: gateway.envoyproxy.io/gatewayclass-controller + parentRef: + name: gateway-1 + namespace: envoy-gateway + sectionName: http +- apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + name: httproute-invalid-path-regex + namespace: default + spec: + hostnames: + - gateway.envoyproxy.io + parentRefs: + - name: gateway-1 + namespace: envoy-gateway + sectionName: http + rules: + - backendRefs: + - name: service-1 + port: 8080 + filters: + - extensionRef: + group: gateway.envoyproxy.io + kind: HTTPRouteFilter + name: invalid-path-regex + type: ExtensionRef + matches: + - path: + value: /invalid-path-regex + status: + parents: + - conditions: + - lastTransitionTime: null + message: PathRegex must be a valid RE2 regular expression. reason: UnsupportedValue status: "False" type: Accepted @@ -393,9 +482,32 @@ xdsIR: distinct: false name: "" prefix: /header-and-backend - urlRewrite: - host: - header: my-host + - directResponse: + statusCode: 500 + hostname: gateway.envoyproxy.io + isHTTP2: false + metadata: + kind: HTTPRoute + name: httproute-missing-path-regex + namespace: default + name: httproute/default/httproute-missing-path-regex/rule/0/match/0/gateway_envoyproxy_io + pathMatch: + distinct: false + name: "" + prefix: /missing-path-regex + - directResponse: + statusCode: 500 + hostname: gateway.envoyproxy.io + isHTTP2: false + metadata: + kind: HTTPRoute + name: httproute-invalid-path-regex + namespace: default + name: httproute/default/httproute-invalid-path-regex/rule/0/match/0/gateway_envoyproxy_io + pathMatch: + distinct: false + name: "" + prefix: /invalid-path-regex - directResponse: statusCode: 500 hostname: gateway.envoyproxy.io @@ -456,7 +568,7 @@ xdsIR: prefix: /two-headers urlRewrite: host: - header: my-host + header: my-host2 - directResponse: statusCode: 500 hostname: gateway.envoyproxy.io @@ -472,7 +584,7 @@ xdsIR: prefix: /ext-first urlRewrite: host: - header: my-host + name: rewrite.com readyListener: address: 0.0.0.0 ipFamily: IPv4 diff --git a/internal/gatewayapi/testdata/httproute-with-urlrewrite-hostname-filter.in.yaml b/internal/gatewayapi/testdata/httproute-with-urlrewrite-hostname-filter.in.yaml index 09b7115694..d0a0484b70 100644 --- a/internal/gatewayapi/testdata/httproute-with-urlrewrite-hostname-filter.in.yaml +++ b/internal/gatewayapi/testdata/httproute-with-urlrewrite-hostname-filter.in.yaml @@ -161,6 +161,31 @@ httpRoutes: group: gateway.envoyproxy.io kind: HTTPRouteFilter name: not-found-filter +- apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + namespace: default + name: httproute-6 + spec: + hostnames: + - gateway.envoyproxy.io + parentRefs: + - namespace: envoy-gateway + name: gateway-1 + sectionName: http + rules: + - matches: + - path: + value: "/node" + backendRefs: + - name: service-1 + port: 8080 + filters: + - type: ExtensionRef + extensionRef: + group: gateway.envoyproxy.io + kind: HTTPRouteFilter + name: valid-path-regex httpFilters: - apiVersion: gateway.envoyproxy.io/v1alpha1 kind: HTTPRouteFilter @@ -181,3 +206,15 @@ httpFilters: urlRewrite: hostname: type: Backend +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: HTTPRouteFilter + metadata: + name: valid-path-regex + namespace: default + spec: + urlRewrite: + hostname: + type: PathRegex + pathRegex: + pattern: "^/node/([0-9]+)/api.*" + substitution: "backend-\\1.service.namespace.svc.cluster.local" diff --git a/internal/gatewayapi/testdata/httproute-with-urlrewrite-hostname-filter.out.yaml b/internal/gatewayapi/testdata/httproute-with-urlrewrite-hostname-filter.out.yaml index 0dca8c386c..d08cd97665 100644 --- a/internal/gatewayapi/testdata/httproute-with-urlrewrite-hostname-filter.out.yaml +++ b/internal/gatewayapi/testdata/httproute-with-urlrewrite-hostname-filter.out.yaml @@ -16,7 +16,7 @@ gateways: protocol: HTTP status: listeners: - - attachedRoutes: 5 + - attachedRoutes: 6 conditions: - lastTransitionTime: null message: Sending translated listener configuration to the data plane @@ -278,6 +278,49 @@ httpRoutes: name: gateway-1 namespace: envoy-gateway sectionName: http +- apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + name: httproute-6 + namespace: default + spec: + hostnames: + - gateway.envoyproxy.io + parentRefs: + - name: gateway-1 + namespace: envoy-gateway + sectionName: http + rules: + - backendRefs: + - name: service-1 + port: 8080 + filters: + - extensionRef: + group: gateway.envoyproxy.io + kind: HTTPRouteFilter + name: valid-path-regex + type: ExtensionRef + matches: + - path: + value: /node + status: + parents: + - conditions: + - lastTransitionTime: null + message: Route is accepted + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: Resolved all the Object references for the Route + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + controllerName: gateway.envoyproxy.io/gatewayclass-controller + parentRef: + name: gateway-1 + namespace: envoy-gateway + sectionName: http infraIR: envoy-gateway/gateway-1: proxy: @@ -494,6 +537,41 @@ xdsIR: urlRewrite: host: header: my-host + - destination: + metadata: + kind: HTTPRoute + name: httproute-6 + namespace: default + name: httproute/default/httproute-6/rule/0 + settings: + - addressType: IP + endpoints: + - host: 7.7.7.7 + port: 8080 + metadata: + kind: Service + name: service-1 + namespace: default + sectionName: "8080" + name: httproute/default/httproute-6/rule/0/backend/0 + protocol: HTTP + weight: 1 + hostname: gateway.envoyproxy.io + isHTTP2: false + metadata: + kind: HTTPRoute + name: httproute-6 + namespace: default + name: httproute/default/httproute-6/rule/0/match/0/gateway_envoyproxy_io + pathMatch: + distinct: false + name: "" + prefix: /node + urlRewrite: + host: + pathRegex: + pattern: ^/node/([0-9]+)/api.* + substitution: backend-\1.service.namespace.svc.cluster.local readyListener: address: 0.0.0.0 ipFamily: IPv4 diff --git a/internal/ir/xds.go b/internal/ir/xds.go index ba58014fce..6a5923f6f7 100644 --- a/internal/ir/xds.go +++ b/internal/ir/xds.go @@ -58,7 +58,8 @@ var ( ErrHTTPPathModifierDoubleReplace = errors.New("redirect filter cannot have a path modifier that supplies more than one of fullPathReplace, prefixMatchReplace and regexMatchReplace") ErrHTTPPathModifierNoReplace = errors.New("redirect filter cannot have a path modifier that does not supply either fullPathReplace, prefixMatchReplace or regexMatchReplace") ErrHTTPPathRegexModifierNoSetting = errors.New("redirect filter cannot have a path modifier that does not supply either fullPathReplace, prefixMatchReplace or regexMatchReplace") - ErrHTTPHostModifierDoubleReplace = errors.New("redirect filter cannot have a host modifier that supplies more than one of Hostname, Header and Backend") + ErrHTTPHostModifierDoubleReplace = errors.New("redirect filter cannot have a host modifier that supplies more than one of Hostname, Header, Backend and PathRegex") + ErrHTTPHostModifierNoReplace = errors.New("redirect filter cannot have a host modifier that does not supply either Hostname, Header, Backend or PathRegex") ErrAddHeaderEmptyName = errors.New("header modifier filter cannot configure a header without a name to be added") ErrAddHeaderDuplicate = errors.New("header modifier filter attempts to add the same header more than once (case insensitive)") ErrRemoveHeaderDuplicate = errors.New("header modifier filter attempts to remove the same header more than once (case insensitive)") @@ -2277,16 +2278,17 @@ func (r ExtendedHTTPPathModifier) Validate() error { // +k8s:deepcopy-gen=true type HTTPHostModifier struct { // Name provides a string to replace the host of the request. - Name *string `json:"name,omitempty" yaml:"name,omitempty"` - Header *string `json:"header,omitempty" yaml:"header,omitempty"` - Backend *bool `json:"backend,omitempty" yaml:"backend,omitempty"` + Name *string `json:"name,omitempty" yaml:"name,omitempty"` + Header *string `json:"header,omitempty" yaml:"header,omitempty"` + Backend *bool `json:"backend,omitempty" yaml:"backend,omitempty"` + PathRegex *RegexMatchReplace `json:"pathRegex,omitempty" yaml:"pathRegex,omitempty"` } // Validate the fields within the HTTPPathModifier structure func (r HTTPHostModifier) Validate() error { var errs error - rewrites := []bool{r.Name != nil, r.Header != nil, r.Backend != nil} + rewrites := []bool{r.Name != nil, r.Header != nil, r.Backend != nil, r.PathRegex != nil} rwc := 0 for _, rw := range rewrites { if rw { @@ -2298,6 +2300,10 @@ func (r HTTPHostModifier) Validate() error { errs = errors.Join(errs, ErrHTTPHostModifierDoubleReplace) } + if r.PathRegex != nil && (r.PathRegex.Pattern == "" || r.PathRegex.Substitution == "") { + errs = errors.Join(errs, ErrHTTPHostModifierNoReplace) + } + return errs } diff --git a/internal/ir/zz_generated.deepcopy.go b/internal/ir/zz_generated.deepcopy.go index 889ca17250..d3658db6fc 100644 --- a/internal/ir/zz_generated.deepcopy.go +++ b/internal/ir/zz_generated.deepcopy.go @@ -2249,6 +2249,11 @@ func (in *HTTPHostModifier) DeepCopyInto(out *HTTPHostModifier) { *out = new(bool) **out = **in } + if in.PathRegex != nil { + in, out := &in.PathRegex, &out.PathRegex + *out = new(RegexMatchReplace) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HTTPHostModifier. diff --git a/internal/xds/translator/route.go b/internal/xds/translator/route.go index 4426dc2efe..5304d1da77 100644 --- a/internal/xds/translator/route.go +++ b/internal/xds/translator/route.go @@ -592,6 +592,15 @@ func buildXdsURLRewriteAction(route *ir.HTTPRoute, urlRewrite *ir.URLRewrite, pa routeAction.HostRewriteSpecifier = &routev3.RouteAction_AutoHostRewrite{ AutoHostRewrite: wrapperspb.Bool(true), } + case urlRewrite.Host.PathRegex != nil: + routeAction.HostRewriteSpecifier = &routev3.RouteAction_HostRewritePathRegex{ + HostRewritePathRegex: &matcherv3.RegexMatchAndSubstitute{ + Pattern: &matcherv3.RegexMatcher{ + Regex: urlRewrite.Host.PathRegex.Pattern, + }, + Substitution: urlRewrite.Host.PathRegex.Substitution, + }, + } } if urlRewrite.AppendXForwardedHost == nil || *urlRewrite.AppendXForwardedHost { diff --git a/internal/xds/translator/route_test.go b/internal/xds/translator/route_test.go index ab3a0bdcc3..d23cf722f0 100644 --- a/internal/xds/translator/route_test.go +++ b/internal/xds/translator/route_test.go @@ -308,3 +308,60 @@ func TestBuildXdsURLRewriteAction_AppendXForwardedHost(t *testing.T) { }) } } + +func TestBuildXdsURLRewriteAction_PathRegexHostRewrite(t *testing.T) { + baseRoute := &ir.HTTPRoute{ + Name: "test-route", + Destination: &ir.RouteDestination{ + Name: "test-dest", + Settings: []*ir.DestinationSetting{ + { + Endpoints: []*ir.DestinationEndpoint{ + {Host: "1.2.3.4", Port: 8080}, + }, + }, + }, + }, + } + + tests := []struct { + name string + appendXForwardedHost *bool + wantAppendXForwardedHost bool + }{ + { + name: "nil defaults to true", + appendXForwardedHost: nil, + wantAppendXForwardedHost: true, + }, + { + name: "explicit false", + appendXForwardedHost: new(false), + wantAppendXForwardedHost: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + urlRewrite := &ir.URLRewrite{ + Host: &ir.HTTPHostModifier{ + PathRegex: &ir.RegexMatchReplace{ + Pattern: "^/node/([0-9]+)/api.*", + Substitution: "backend-\\1.service.namespace.svc.cluster.local", + }, + }, + AppendXForwardedHost: tt.appendXForwardedHost, + } + got := buildXdsURLRewriteAction(baseRoute, urlRewrite, nil) + if got.AppendXForwardedHost != tt.wantAppendXForwardedHost { + t.Errorf("AppendXForwardedHost = %v, want %v", got.AppendXForwardedHost, tt.wantAppendXForwardedHost) + } + if got.GetHostRewritePathRegex().GetPattern().GetRegex() != "^/node/([0-9]+)/api.*" { + t.Errorf("HostRewritePathRegex pattern = %v, want %v", got.GetHostRewritePathRegex().GetPattern().GetRegex(), "^/node/([0-9]+)/api.*") + } + if got.GetHostRewritePathRegex().GetSubstitution() != "backend-\\1.service.namespace.svc.cluster.local" { + t.Errorf("HostRewritePathRegex substitution = %v, want %v", got.GetHostRewritePathRegex().GetSubstitution(), "backend-\\1.service.namespace.svc.cluster.local") + } + }) + } +} diff --git a/internal/xds/translator/testdata/in/xds-ir/http-route-rewrite-url-host.yaml b/internal/xds/translator/testdata/in/xds-ir/http-route-rewrite-url-host.yaml index b1ab28b1ef..d7d4fcd6cd 100644 --- a/internal/xds/translator/testdata/in/xds-ir/http-route-rewrite-url-host.yaml +++ b/internal/xds/translator/testdata/in/xds-ir/http-route-rewrite-url-host.yaml @@ -65,6 +65,27 @@ http: backend: true path: prefixMatchReplace: /rewrite + - name: "rewrite-host-path-regex" + pathMatch: + prefix: "/node" + hostname: gateway.envoyproxy.io + headerMatches: + - name: ":authority" + exact: gateway.envoyproxy.io + destination: + name: "rewrite-route-dest" + settings: + - endpoints: + - host: "1.2.3.4" + port: 50000 + name: "rewrite-route-dest/backend/0" + urlRewrite: + host: + pathRegex: + pattern: "^/node/([0-9]+)/api.*" + substitution: "backend-\\1.service.namespace.svc.cluster.local" + path: + prefixMatchReplace: /rewrite - name: "rewrite-route-timeout" pathMatch: prefix: "/origin" diff --git a/internal/xds/translator/testdata/out/xds-ir/http-route-rewrite-url-host.routes.yaml b/internal/xds/translator/testdata/out/xds-ir/http-route-rewrite-url-host.routes.yaml index 69ea825367..4d8d0185c5 100644 --- a/internal/xds/translator/testdata/out/xds-ir/http-route-rewrite-url-host.routes.yaml +++ b/internal/xds/translator/testdata/out/xds-ir/http-route-rewrite-url-host.routes.yaml @@ -47,6 +47,23 @@ prefixRewrite: /rewrite upgradeConfigs: - upgradeType: websocket + - match: + headers: + - name: :authority + stringMatch: + exact: gateway.envoyproxy.io + pathSeparatedPrefix: /node + name: rewrite-host-path-regex + route: + appendXForwardedHost: true + cluster: rewrite-route-dest + hostRewritePathRegex: + pattern: + regex: ^/node/([0-9]+)/api.* + substitution: backend-\1.service.namespace.svc.cluster.local + prefixRewrite: /rewrite + upgradeConfigs: + - upgradeType: websocket - match: headers: - name: :authority diff --git a/release-notes/current.yaml b/release-notes/current.yaml index 79792717c3..4d643b3eff 100644 --- a/release-notes/current.yaml +++ b/release-notes/current.yaml @@ -13,6 +13,7 @@ security updates: | new features: | Added support for authorization path match. Added a `requestBody` field to the HTTP active health checker in `BackendTrafficPolicy`, allowing a request body payload to be sent during HTTP health checking. The field requires the health check `method` to be `POST` or `PUT`. + Added support for `HTTPRouteFilter` PathRegex hostname rewrites, allowing the upstream Host header to be derived from regex captures on the request path. bug fixes: | Fixed Backend TLS `alpnProtocols: []` to disable upstream ALPN instead of inheriting EnvoyProxy BackendTLS defaults. diff --git a/test/cel-validation/httproutefilter_test.go b/test/cel-validation/httproutefilter_test.go index 693c28672a..b5ab8fda18 100644 --- a/test/cel-validation/httproutefilter_test.go +++ b/test/cel-validation/httproutefilter_test.go @@ -115,6 +115,23 @@ func TestHTTPRouteFilter(t *testing.T) { }, wantErrors: []string{}, }, + { + desc: "Valid PathRegex", + mutate: func(httproutefilter *egv1a1.HTTPRouteFilter) { + httproutefilter.Spec = egv1a1.HTTPRouteFilterSpec{ + URLRewrite: &egv1a1.HTTPURLRewriteFilter{ + Hostname: &egv1a1.HTTPHostnameModifier{ + Type: egv1a1.PathRegexHTTPHostnameModifier, + PathRegex: &egv1a1.HostnamePathRegexRewrite{ + Pattern: "^/node/([0-9]+)/api.*", + Substitution: "backend-\\1.service.namespace.svc.cluster.local", + }, + }, + }, + } + }, + wantErrors: []string{}, + }, { desc: "Valid appendXForwardedHost false", mutate: func(httproutefilter *egv1a1.HTTPRouteFilter) { @@ -178,6 +195,103 @@ func TestHTTPRouteFilter(t *testing.T) { ": header must be nil if the type is not Header", }, }, + { + desc: "invalid PathRegex missing settings", + mutate: func(httproutefilter *egv1a1.HTTPRouteFilter) { + httproutefilter.Spec = egv1a1.HTTPRouteFilterSpec{ + URLRewrite: &egv1a1.HTTPURLRewriteFilter{ + Hostname: &egv1a1.HTTPHostnameModifier{ + Type: egv1a1.PathRegexHTTPHostnameModifier, + }, + }, + } + }, + wantErrors: []string{ + "spec.urlRewrite.hostname: Invalid value:", + ": pathRegex must be specified for PathRegex type", + }, + }, + { + desc: "invalid PathRegex with header", + mutate: func(httproutefilter *egv1a1.HTTPRouteFilter) { + httproutefilter.Spec = egv1a1.HTTPRouteFilterSpec{ + URLRewrite: &egv1a1.HTTPURLRewriteFilter{ + Hostname: &egv1a1.HTTPHostnameModifier{ + Type: egv1a1.PathRegexHTTPHostnameModifier, + Header: new("foo"), + PathRegex: &egv1a1.HostnamePathRegexRewrite{ + Pattern: "^/node/([0-9]+)/api.*", + Substitution: "backend-\\1.service.namespace.svc.cluster.local", + }, + }, + }, + } + }, + wantErrors: []string{ + "spec.urlRewrite.hostname: Invalid value:", + ": header must be nil if the type is not Header", + }, + }, + { + desc: "invalid Header with pathRegex", + mutate: func(httproutefilter *egv1a1.HTTPRouteFilter) { + httproutefilter.Spec = egv1a1.HTTPRouteFilterSpec{ + URLRewrite: &egv1a1.HTTPURLRewriteFilter{ + Hostname: &egv1a1.HTTPHostnameModifier{ + Type: egv1a1.HeaderHTTPHostnameModifier, + Header: new("foo"), + PathRegex: &egv1a1.HostnamePathRegexRewrite{ + Pattern: "^/node/([0-9]+)/api.*", + Substitution: "backend-\\1.service.namespace.svc.cluster.local", + }, + }, + }, + } + }, + wantErrors: []string{ + "spec.urlRewrite.hostname: Invalid value:", + ": pathRegex must be nil if the type is not PathRegex", + }, + }, + { + desc: "invalid Backend with pathRegex", + mutate: func(httproutefilter *egv1a1.HTTPRouteFilter) { + httproutefilter.Spec = egv1a1.HTTPRouteFilterSpec{ + URLRewrite: &egv1a1.HTTPURLRewriteFilter{ + Hostname: &egv1a1.HTTPHostnameModifier{ + Type: egv1a1.BackendHTTPHostnameModifier, + PathRegex: &egv1a1.HostnamePathRegexRewrite{ + Pattern: "^/node/([0-9]+)/api.*", + Substitution: "backend-\\1.service.namespace.svc.cluster.local", + }, + }, + }, + } + }, + wantErrors: []string{ + "spec.urlRewrite.hostname: Invalid value:", + ": pathRegex must be nil if the type is not PathRegex", + }, + }, + { + desc: "invalid PathRegex missing pattern and substitution", + mutate: func(httproutefilter *egv1a1.HTTPRouteFilter) { + httproutefilter.Spec = egv1a1.HTTPRouteFilterSpec{ + URLRewrite: &egv1a1.HTTPURLRewriteFilter{ + Hostname: &egv1a1.HTTPHostnameModifier{ + Type: egv1a1.PathRegexHTTPHostnameModifier, + PathRegex: &egv1a1.HostnamePathRegexRewrite{ + Pattern: "", + Substitution: "", + }, + }, + }, + } + }, + wantErrors: []string{ + "spec.urlRewrite.hostname.pathRegex.pattern: Invalid value: \"\": spec.urlRewrite.hostname.pathRegex.pattern in body should be at least 1 chars long", + }, + }, { desc: "Valid DirectResponse with header add", mutate: func(httproutefilter *egv1a1.HTTPRouteFilter) { From 4b0aebdb08b0e3943159275d9f22e24b52ec331c Mon Sep 17 00:00:00 2001 From: Huabing Zhao Date: Mon, 8 Jun 2026 14:25:19 +0800 Subject: [PATCH 03/18] e2e tests Signed-off-by: Huabing Zhao --- test/e2e/testdata/httproute-rewrite-host.yaml | 26 +++++++++++++++++++ test/e2e/tests/httproute_rewrite_host.go | 13 ++++++++++ 2 files changed, 39 insertions(+) diff --git a/test/e2e/testdata/httproute-rewrite-host.yaml b/test/e2e/testdata/httproute-rewrite-host.yaml index 871b2008b3..741d8ede1a 100644 --- a/test/e2e/testdata/httproute-rewrite-host.yaml +++ b/test/e2e/testdata/httproute-rewrite-host.yaml @@ -34,6 +34,19 @@ spec: - group: gateway.envoyproxy.io kind: Backend name: backend-fqdn + - matches: + - path: + type: PathPrefix + value: /path-regex + filters: + - type: ExtensionRef + extensionRef: + group: gateway.envoyproxy.io + kind: HTTPRouteFilter + name: path-regex-host-rewrite + backendRefs: + - name: infra-backend-v1 + port: 8080 --- apiVersion: gateway.envoyproxy.io/v1alpha1 kind: HTTPRouteFilter @@ -57,6 +70,19 @@ spec: type: Backend --- apiVersion: gateway.envoyproxy.io/v1alpha1 +kind: HTTPRouteFilter +metadata: + name: path-regex-host-rewrite + namespace: gateway-conformance-infra +spec: + urlRewrite: + hostname: + type: PathRegex + pathRegex: + pattern: "^/path-regex/([^/]+).*" + substitution: "\\1.gateway-conformance-infra.svc.cluster.local" +--- +apiVersion: gateway.envoyproxy.io/v1alpha1 kind: Backend metadata: name: backend-fqdn diff --git a/test/e2e/tests/httproute_rewrite_host.go b/test/e2e/tests/httproute_rewrite_host.go index 49353e4afe..fec1f0e5e1 100644 --- a/test/e2e/tests/httproute_rewrite_host.go +++ b/test/e2e/tests/httproute_rewrite_host.go @@ -62,6 +62,19 @@ var HTTPRouteRewriteHostHeader = suite.ConformanceTest{ Backend: "infra-backend-v1", Namespace: ns, }, + { + Request: http.Request{ + Path: "/path-regex/infra-backend-v1", + }, + ExpectedRequest: &http.ExpectedRequest{ + Request: http.Request{ + Path: "/path-regex/infra-backend-v1", + Host: "infra-backend-v1.gateway-conformance-infra.svc.cluster.local", + }, + }, + Backend: "infra-backend-v1", + Namespace: ns, + }, } for i := range testCases { // Declare tc here to avoid loop variable From 6c1f21224e80152e4a1397ada5bae07c4d2598d8 Mon Sep 17 00:00:00 2001 From: Huabing Zhao Date: Mon, 8 Jun 2026 15:47:39 +0800 Subject: [PATCH 04/18] update e2e test Signed-off-by: Huabing Zhao --- test/e2e/testdata/httproute-rewrite-host.yaml | 2 +- test/e2e/tests/httproute_rewrite_host.go | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/test/e2e/testdata/httproute-rewrite-host.yaml b/test/e2e/testdata/httproute-rewrite-host.yaml index 741d8ede1a..9832e3e76f 100644 --- a/test/e2e/testdata/httproute-rewrite-host.yaml +++ b/test/e2e/testdata/httproute-rewrite-host.yaml @@ -80,7 +80,7 @@ spec: type: PathRegex pathRegex: pattern: "^/path-regex/([^/]+).*" - substitution: "\\1.gateway-conformance-infra.svc.cluster.local" + substitution: "\\1.example.internal" --- apiVersion: gateway.envoyproxy.io/v1alpha1 kind: Backend diff --git a/test/e2e/tests/httproute_rewrite_host.go b/test/e2e/tests/httproute_rewrite_host.go index fec1f0e5e1..f6dbfc1532 100644 --- a/test/e2e/tests/httproute_rewrite_host.go +++ b/test/e2e/tests/httproute_rewrite_host.go @@ -64,12 +64,12 @@ var HTTPRouteRewriteHostHeader = suite.ConformanceTest{ }, { Request: http.Request{ - Path: "/path-regex/infra-backend-v1", + Path: "/path-regex/custom-host", }, ExpectedRequest: &http.ExpectedRequest{ Request: http.Request{ - Path: "/path-regex/infra-backend-v1", - Host: "infra-backend-v1.gateway-conformance-infra.svc.cluster.local", + Path: "/path-regex/custom-host", + Host: "custom-host.example.internal", }, }, Backend: "infra-backend-v1", From 5d9bbb81f7d3f2a4a714f9e4a9fc97dfcddc7c97 Mon Sep 17 00:00:00 2001 From: Huabing Zhao Date: Tue, 9 Jun 2026 10:38:29 +0800 Subject: [PATCH 05/18] reject PathRegex host rewrite with DFP backend Signed-off-by: Huabing Zhao --- internal/gatewayapi/route.go | 38 +++++++++ ...urlrewrite-hostname-filter-invalid.in.yaml | 48 +++++++++++ ...rlrewrite-hostname-filter-invalid.out.yaml | 80 ++++++++++++++++++- 3 files changed, 165 insertions(+), 1 deletion(-) diff --git a/internal/gatewayapi/route.go b/internal/gatewayapi/route.go index 19ceee6fb0..1cc8fd0698 100644 --- a/internal/gatewayapi/route.go +++ b/internal/gatewayapi/route.go @@ -395,6 +395,33 @@ func (t *Translator) processHTTPRouteRules(httpRoute *HTTPRouteContext, parentRe t.Logger.Info("setting 500 direct response in routes due to all valid destinations having 0 weight", "routes", sets.List(routesWithDirectResponse)) } + // Host rewrite from path (PathRegex) is rejected for dynamic resolver routes: the upstream host is + // derived from request-controlled path text, which is not validated by the dynamic forward proxy + // loopback protection (that guard only inspects the rewrite header or :authority). Allowing it would + // let a crafted path resolve to a loopback address and bypass the SSRF protection. + case hasDynamicResolver && hasPathRegexHostRewrite(ruleRoutes): + routesWithDirectResponse := sets.New[string]() + for _, irRoute := range ruleRoutes { + // If the route already has a direct response or redirect configured, then it was from a filter so skip + // the direct response from errors. + if irRoute.DirectResponse != nil || irRoute.Redirect != nil { + continue + } + irRoute.DirectResponse = &ir.CustomResponse{ + StatusCode: new(uint32(500)), + } + routesWithDirectResponse.Insert(irRoute.Name) + } + errorCollector.Add(status.NewRouteStatusError( + fmt.Errorf( + "failed to process route rule %d: host rewrite from path (PathRegex) is not supported with a dynamic resolver backend", + ruleIdx), + gwapiv1.RouteReasonUnsupportedValue, + )) + if len(routesWithDirectResponse) > 0 { + t.Logger.Info("setting 500 direct response in routes due to dynamic resolver with host rewrite from path", + "routes", sets.List(routesWithDirectResponse)) + } // A route can only have one destination if this destination is a dynamic resolver, because the behavior of // multiple destinations with one being a dynamic resolver just doesn't make sense. case hasDynamicResolver && len(rule.BackendRefs) > 1: @@ -458,6 +485,17 @@ func (t *Translator) processHTTPRouteRules(httpRoute *HTTPRouteContext, parentRe return irRoutes, errorCollector.GetAllErrors(), unacceptedRules.List() } +// hasPathRegexHostRewrite reports whether any of the given IR routes rewrites the upstream host +// from the request path via a regex substitution (urlRewrite.hostname.type: PathRegex). +func hasPathRegexHostRewrite(routes []*ir.HTTPRoute) bool { + for _, irRoute := range routes { + if irRoute.URLRewrite != nil && irRoute.URLRewrite.Host != nil && irRoute.URLRewrite.Host.PathRegex != nil { + return true + } + } + return false +} + type routeMatchCombination struct { gwapiv1.HTTPRouteMatch cookies []egv1a1.HTTPCookieMatch diff --git a/internal/gatewayapi/testdata/httproute-with-urlrewrite-hostname-filter-invalid.in.yaml b/internal/gatewayapi/testdata/httproute-with-urlrewrite-hostname-filter-invalid.in.yaml index 84fc643990..aead86bfac 100644 --- a/internal/gatewayapi/testdata/httproute-with-urlrewrite-hostname-filter-invalid.in.yaml +++ b/internal/gatewayapi/testdata/httproute-with-urlrewrite-hostname-filter-invalid.in.yaml @@ -236,6 +236,42 @@ httpRoutes: group: gateway.envoyproxy.io kind: HTTPRouteFilter name: invalid-path-regex + # PathRegex host rewrite is invalid on a dynamic resolver backend: the upstream host would be + # derived from request-controlled path text, bypassing the dynamic forward proxy loopback protection. + - apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + namespace: default + name: httproute-dynamic-resolver-path-regex + spec: + hostnames: + - gateway.envoyproxy.io + parentRefs: + - namespace: envoy-gateway + name: gateway-1 + sectionName: http + rules: + - matches: + - path: + value: "/dynamic-resolver-path-regex" + backendRefs: + - group: gateway.envoyproxy.io + kind: Backend + name: backend-dynamic-resolver + filters: + - type: ExtensionRef + extensionRef: + group: gateway.envoyproxy.io + kind: HTTPRouteFilter + name: valid-path-regex +backends: + - apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: Backend + metadata: + name: backend-dynamic-resolver + namespace: default + spec: + type: DynamicResolver httpFilters: - apiVersion: gateway.envoyproxy.io/v1alpha1 kind: HTTPRouteFilter @@ -305,3 +341,15 @@ httpFilters: urlRewrite: hostname: type: Header + - apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: HTTPRouteFilter + metadata: + name: valid-path-regex + namespace: default + spec: + urlRewrite: + hostname: + type: PathRegex + pathRegex: + pattern: "^/[^/]+/([^/]+).*" + substitution: "\\1" diff --git a/internal/gatewayapi/testdata/httproute-with-urlrewrite-hostname-filter-invalid.out.yaml b/internal/gatewayapi/testdata/httproute-with-urlrewrite-hostname-filter-invalid.out.yaml index 53b0234a16..2fe2892c68 100644 --- a/internal/gatewayapi/testdata/httproute-with-urlrewrite-hostname-filter-invalid.out.yaml +++ b/internal/gatewayapi/testdata/httproute-with-urlrewrite-hostname-filter-invalid.out.yaml @@ -1,3 +1,18 @@ +backends: +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: Backend + metadata: + name: backend-dynamic-resolver + namespace: default + spec: + type: DynamicResolver + status: + conditions: + - lastTransitionTime: null + message: The Backend was accepted + reason: Accepted + status: "True" + type: Accepted gateways: - apiVersion: gateway.networking.k8s.io/v1 kind: Gateway @@ -16,7 +31,7 @@ gateways: protocol: HTTP status: listeners: - - attachedRoutes: 8 + - attachedRoutes: 9 conditions: - lastTransitionTime: null message: Sending translated listener configuration to the data plane @@ -408,6 +423,51 @@ httpRoutes: name: gateway-1 namespace: envoy-gateway sectionName: http +- apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + name: httproute-dynamic-resolver-path-regex + namespace: default + spec: + hostnames: + - gateway.envoyproxy.io + parentRefs: + - name: gateway-1 + namespace: envoy-gateway + sectionName: http + rules: + - backendRefs: + - group: gateway.envoyproxy.io + kind: Backend + name: backend-dynamic-resolver + filters: + - extensionRef: + group: gateway.envoyproxy.io + kind: HTTPRouteFilter + name: valid-path-regex + type: ExtensionRef + matches: + - path: + value: /dynamic-resolver-path-regex + status: + parents: + - conditions: + - lastTransitionTime: null + message: Route is accepted + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: 'Failed to process route rule 0: host rewrite from path (PathRegex) + is not supported with a dynamic resolver backend.' + reason: UnsupportedValue + status: "False" + type: ResolvedRefs + controllerName: gateway.envoyproxy.io/gatewayclass-controller + parentRef: + name: gateway-1 + namespace: envoy-gateway + sectionName: http infraIR: envoy-gateway/gateway-1: proxy: @@ -469,6 +529,24 @@ xdsIR: mergeSlashes: true port: 10080 routes: + - directResponse: + statusCode: 500 + hostname: gateway.envoyproxy.io + isHTTP2: false + metadata: + kind: HTTPRoute + name: httproute-dynamic-resolver-path-regex + namespace: default + name: httproute/default/httproute-dynamic-resolver-path-regex/rule/0/match/0/gateway_envoyproxy_io + pathMatch: + distinct: false + name: "" + prefix: /dynamic-resolver-path-regex + urlRewrite: + host: + pathRegex: + pattern: ^/[^/]+/([^/]+).* + substitution: \1 - directResponse: statusCode: 500 hostname: gateway.envoyproxy.io From ddad677fdb9325d8fcb603a45ccb323bbd14f36c Mon Sep 17 00:00:00 2001 From: Huabing Zhao Date: Tue, 9 Jun 2026 10:53:28 +0800 Subject: [PATCH 06/18] address comment Signed-off-by: Huabing Zhao --- internal/gatewayapi/filters.go | 5 +- ...urlrewrite-hostname-filter-invalid.in.yaml | 60 ++++++++ ...rlrewrite-hostname-filter-invalid.out.yaml | 132 +++++++++++++++++- 3 files changed, 194 insertions(+), 3 deletions(-) diff --git a/internal/gatewayapi/filters.go b/internal/gatewayapi/filters.go index 116273952e..a02deef655 100644 --- a/internal/gatewayapi/filters.go +++ b/internal/gatewayapi/filters.go @@ -234,7 +234,8 @@ func hasMultipleCoreRewrites(rewrite *gwapiv1.HTTPURLRewriteFilter, contextRewri // Checks if the context and the rewrite both contain a envoy-gateway extended HTTP URL rewrite func hasMultipleExtensionRewrites(rewrite *egv1a1.HTTPURLRewriteFilter, contextRewrite *ir.URLRewrite) bool { contextHasExtensionRewrites := (contextRewrite.Path != nil && contextRewrite.Path.RegexMatchReplace != nil) || - (contextRewrite.Host != nil && (contextRewrite.Host.Header != nil || contextRewrite.Host.Backend != nil)) + (contextRewrite.Host != nil && (contextRewrite.Host.Header != nil || contextRewrite.Host.Backend != nil || + contextRewrite.Host.PathRegex != nil)) return contextHasExtensionRewrites && (rewrite.Hostname != nil || rewrite.Path != nil) } @@ -243,7 +244,7 @@ func hasMultipleExtensionRewrites(rewrite *egv1a1.HTTPURLRewriteFilter, contextR func hasConflictingCoreAndExtensionRewrites(rewrite *gwapiv1.HTTPURLRewriteFilter, contextRewrite *ir.URLRewrite) bool { contextHasExtensionPathRewrites := contextRewrite.Path != nil && contextRewrite.Path.RegexMatchReplace != nil contextHasExtensionHostRewrites := contextRewrite.Host != nil && (contextRewrite.Host.Header != nil || - contextRewrite.Host.Backend != nil) + contextRewrite.Host.Backend != nil || contextRewrite.Host.PathRegex != nil) return (rewrite.Hostname != nil && contextHasExtensionHostRewrites) || (rewrite.Path != nil && contextHasExtensionPathRewrites) } diff --git a/internal/gatewayapi/testdata/httproute-with-urlrewrite-hostname-filter-invalid.in.yaml b/internal/gatewayapi/testdata/httproute-with-urlrewrite-hostname-filter-invalid.in.yaml index aead86bfac..7e757d6dd6 100644 --- a/internal/gatewayapi/testdata/httproute-with-urlrewrite-hostname-filter-invalid.in.yaml +++ b/internal/gatewayapi/testdata/httproute-with-urlrewrite-hostname-filter-invalid.in.yaml @@ -236,6 +236,66 @@ httpRoutes: group: gateway.envoyproxy.io kind: HTTPRouteFilter name: invalid-path-regex + # A PathRegex host rewrite conflicting with a second extension PathRegex host rewrite must be rejected. + - apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + namespace: default + name: httproute-multiple-path-regex-host-rewrites + spec: + hostnames: + - gateway.envoyproxy.io + parentRefs: + - namespace: envoy-gateway + name: gateway-1 + sectionName: http + rules: + - matches: + - path: + value: "/two-path-regex" + backendRefs: + - name: service-1 + port: 8080 + filters: + - type: ExtensionRef + extensionRef: + group: gateway.envoyproxy.io + kind: HTTPRouteFilter + name: valid-path-regex + - type: ExtensionRef + extensionRef: + group: gateway.envoyproxy.io + kind: HTTPRouteFilter + name: valid-path-regex + # A PathRegex host rewrite conflicting with a core URLRewrite hostname rewrite must be rejected. + - apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + namespace: default + name: httproute-path-regex-and-core-host-rewrite + spec: + hostnames: + - gateway.envoyproxy.io + parentRefs: + - namespace: envoy-gateway + name: gateway-1 + sectionName: http + rules: + - matches: + - path: + value: "/path-regex-and-core" + backendRefs: + - name: service-1 + port: 8080 + filters: + - type: ExtensionRef + extensionRef: + group: gateway.envoyproxy.io + kind: HTTPRouteFilter + name: valid-path-regex + - type: URLRewrite + urlRewrite: + hostname: "rewrite.com" # PathRegex host rewrite is invalid on a dynamic resolver backend: the upstream host would be # derived from request-controlled path text, bypassing the dynamic forward proxy loopback protection. - apiVersion: gateway.networking.k8s.io/v1 diff --git a/internal/gatewayapi/testdata/httproute-with-urlrewrite-hostname-filter-invalid.out.yaml b/internal/gatewayapi/testdata/httproute-with-urlrewrite-hostname-filter-invalid.out.yaml index 2fe2892c68..67ea8dcefe 100644 --- a/internal/gatewayapi/testdata/httproute-with-urlrewrite-hostname-filter-invalid.out.yaml +++ b/internal/gatewayapi/testdata/httproute-with-urlrewrite-hostname-filter-invalid.out.yaml @@ -31,7 +31,7 @@ gateways: protocol: HTTP status: listeners: - - attachedRoutes: 9 + - attachedRoutes: 11 conditions: - lastTransitionTime: null message: Sending translated listener configuration to the data plane @@ -423,6 +423,100 @@ httpRoutes: name: gateway-1 namespace: envoy-gateway sectionName: http +- apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + name: httproute-multiple-path-regex-host-rewrites + namespace: default + spec: + hostnames: + - gateway.envoyproxy.io + parentRefs: + - name: gateway-1 + namespace: envoy-gateway + sectionName: http + rules: + - backendRefs: + - name: service-1 + port: 8080 + filters: + - extensionRef: + group: gateway.envoyproxy.io + kind: HTTPRouteFilter + name: valid-path-regex + type: ExtensionRef + - extensionRef: + group: gateway.envoyproxy.io + kind: HTTPRouteFilter + name: valid-path-regex + type: ExtensionRef + matches: + - path: + value: /two-path-regex + status: + parents: + - conditions: + - lastTransitionTime: null + message: Cannot configure multiple urlRewrite filters for a single HTTPRouteRule. + reason: UnsupportedValue + status: "False" + type: Accepted + - lastTransitionTime: null + message: Resolved all the Object references for the Route + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + controllerName: gateway.envoyproxy.io/gatewayclass-controller + parentRef: + name: gateway-1 + namespace: envoy-gateway + sectionName: http +- apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + name: httproute-path-regex-and-core-host-rewrite + namespace: default + spec: + hostnames: + - gateway.envoyproxy.io + parentRefs: + - name: gateway-1 + namespace: envoy-gateway + sectionName: http + rules: + - backendRefs: + - name: service-1 + port: 8080 + filters: + - extensionRef: + group: gateway.envoyproxy.io + kind: HTTPRouteFilter + name: valid-path-regex + type: ExtensionRef + - type: URLRewrite + urlRewrite: + hostname: rewrite.com + matches: + - path: + value: /path-regex-and-core + status: + parents: + - conditions: + - lastTransitionTime: null + message: Cannot configure multiple urlRewrite filters for a single HTTPRouteRule. + reason: UnsupportedValue + status: "False" + type: Accepted + - lastTransitionTime: null + message: Resolved all the Object references for the Route + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + controllerName: gateway.envoyproxy.io/gatewayclass-controller + parentRef: + name: gateway-1 + namespace: envoy-gateway + sectionName: http - apiVersion: gateway.networking.k8s.io/v1 kind: HTTPRoute metadata: @@ -547,6 +641,24 @@ xdsIR: pathRegex: pattern: ^/[^/]+/([^/]+).* substitution: \1 + - directResponse: + statusCode: 500 + hostname: gateway.envoyproxy.io + isHTTP2: false + metadata: + kind: HTTPRoute + name: httproute-path-regex-and-core-host-rewrite + namespace: default + name: httproute/default/httproute-path-regex-and-core-host-rewrite/rule/0/match/0/gateway_envoyproxy_io + pathMatch: + distinct: false + name: "" + prefix: /path-regex-and-core + urlRewrite: + host: + pathRegex: + pattern: ^/[^/]+/([^/]+).* + substitution: \1 - directResponse: statusCode: 500 hostname: gateway.envoyproxy.io @@ -599,6 +711,24 @@ xdsIR: distinct: false name: "" prefix: /invalid-header + - directResponse: + statusCode: 500 + hostname: gateway.envoyproxy.io + isHTTP2: false + metadata: + kind: HTTPRoute + name: httproute-multiple-path-regex-host-rewrites + namespace: default + name: httproute/default/httproute-multiple-path-regex-host-rewrites/rule/0/match/0/gateway_envoyproxy_io + pathMatch: + distinct: false + name: "" + prefix: /two-path-regex + urlRewrite: + host: + pathRegex: + pattern: ^/[^/]+/([^/]+).* + substitution: \1 - directResponse: statusCode: 500 hostname: gateway.envoyproxy.io From 79982b7f8f3356f13736b6b036b30fd608d2b13b Mon Sep 17 00:00:00 2001 From: Huabing Zhao Date: Tue, 9 Jun 2026 11:03:22 +0800 Subject: [PATCH 07/18] update test files Signed-off-by: Huabing Zhao --- ...urlrewrite-hostname-filter-invalid.in.yaml | 179 ---------- ...rlrewrite-hostname-filter-invalid.out.yaml | 336 +----------------- 2 files changed, 8 insertions(+), 507 deletions(-) diff --git a/internal/gatewayapi/testdata/httproute-with-urlrewrite-hostname-filter-invalid.in.yaml b/internal/gatewayapi/testdata/httproute-with-urlrewrite-hostname-filter-invalid.in.yaml index 7e757d6dd6..5a7f449904 100644 --- a/internal/gatewayapi/testdata/httproute-with-urlrewrite-hostname-filter-invalid.in.yaml +++ b/internal/gatewayapi/testdata/httproute-with-urlrewrite-hostname-filter-invalid.in.yaml @@ -186,152 +186,6 @@ httpRoutes: group: gateway.envoyproxy.io kind: HTTPRouteFilter name: valid-header - - apiVersion: gateway.networking.k8s.io/v1 - kind: HTTPRoute - metadata: - namespace: default - name: httproute-missing-path-regex - spec: - hostnames: - - gateway.envoyproxy.io - parentRefs: - - namespace: envoy-gateway - name: gateway-1 - sectionName: http - rules: - - matches: - - path: - value: "/missing-path-regex" - backendRefs: - - name: service-1 - port: 8080 - filters: - - type: ExtensionRef - extensionRef: - group: gateway.envoyproxy.io - kind: HTTPRouteFilter - name: missing-path-regex - - apiVersion: gateway.networking.k8s.io/v1 - kind: HTTPRoute - metadata: - namespace: default - name: httproute-invalid-path-regex - spec: - hostnames: - - gateway.envoyproxy.io - parentRefs: - - namespace: envoy-gateway - name: gateway-1 - sectionName: http - rules: - - matches: - - path: - value: "/invalid-path-regex" - backendRefs: - - name: service-1 - port: 8080 - filters: - - type: ExtensionRef - extensionRef: - group: gateway.envoyproxy.io - kind: HTTPRouteFilter - name: invalid-path-regex - # A PathRegex host rewrite conflicting with a second extension PathRegex host rewrite must be rejected. - - apiVersion: gateway.networking.k8s.io/v1 - kind: HTTPRoute - metadata: - namespace: default - name: httproute-multiple-path-regex-host-rewrites - spec: - hostnames: - - gateway.envoyproxy.io - parentRefs: - - namespace: envoy-gateway - name: gateway-1 - sectionName: http - rules: - - matches: - - path: - value: "/two-path-regex" - backendRefs: - - name: service-1 - port: 8080 - filters: - - type: ExtensionRef - extensionRef: - group: gateway.envoyproxy.io - kind: HTTPRouteFilter - name: valid-path-regex - - type: ExtensionRef - extensionRef: - group: gateway.envoyproxy.io - kind: HTTPRouteFilter - name: valid-path-regex - # A PathRegex host rewrite conflicting with a core URLRewrite hostname rewrite must be rejected. - - apiVersion: gateway.networking.k8s.io/v1 - kind: HTTPRoute - metadata: - namespace: default - name: httproute-path-regex-and-core-host-rewrite - spec: - hostnames: - - gateway.envoyproxy.io - parentRefs: - - namespace: envoy-gateway - name: gateway-1 - sectionName: http - rules: - - matches: - - path: - value: "/path-regex-and-core" - backendRefs: - - name: service-1 - port: 8080 - filters: - - type: ExtensionRef - extensionRef: - group: gateway.envoyproxy.io - kind: HTTPRouteFilter - name: valid-path-regex - - type: URLRewrite - urlRewrite: - hostname: "rewrite.com" - # PathRegex host rewrite is invalid on a dynamic resolver backend: the upstream host would be - # derived from request-controlled path text, bypassing the dynamic forward proxy loopback protection. - - apiVersion: gateway.networking.k8s.io/v1 - kind: HTTPRoute - metadata: - namespace: default - name: httproute-dynamic-resolver-path-regex - spec: - hostnames: - - gateway.envoyproxy.io - parentRefs: - - namespace: envoy-gateway - name: gateway-1 - sectionName: http - rules: - - matches: - - path: - value: "/dynamic-resolver-path-regex" - backendRefs: - - group: gateway.envoyproxy.io - kind: Backend - name: backend-dynamic-resolver - filters: - - type: ExtensionRef - extensionRef: - group: gateway.envoyproxy.io - kind: HTTPRouteFilter - name: valid-path-regex -backends: - - apiVersion: gateway.envoyproxy.io/v1alpha1 - kind: Backend - metadata: - name: backend-dynamic-resolver - namespace: default - spec: - type: DynamicResolver httpFilters: - apiVersion: gateway.envoyproxy.io/v1alpha1 kind: HTTPRouteFilter @@ -342,27 +196,6 @@ httpFilters: urlRewrite: hostname: type: Header - - apiVersion: gateway.envoyproxy.io/v1alpha1 - kind: HTTPRouteFilter - metadata: - name: missing-path-regex - namespace: default - spec: - urlRewrite: - hostname: - type: PathRegex - - apiVersion: gateway.envoyproxy.io/v1alpha1 - kind: HTTPRouteFilter - metadata: - name: invalid-path-regex - namespace: default - spec: - urlRewrite: - hostname: - type: PathRegex - pathRegex: - pattern: '"([a-z]+)"*+?' - substitution: "backend-\\1.service.namespace.svc.cluster.local" header: my-host - apiVersion: gateway.envoyproxy.io/v1alpha1 kind: HTTPRouteFilter @@ -401,15 +234,3 @@ httpFilters: urlRewrite: hostname: type: Header - - apiVersion: gateway.envoyproxy.io/v1alpha1 - kind: HTTPRouteFilter - metadata: - name: valid-path-regex - namespace: default - spec: - urlRewrite: - hostname: - type: PathRegex - pathRegex: - pattern: "^/[^/]+/([^/]+).*" - substitution: "\\1" diff --git a/internal/gatewayapi/testdata/httproute-with-urlrewrite-hostname-filter-invalid.out.yaml b/internal/gatewayapi/testdata/httproute-with-urlrewrite-hostname-filter-invalid.out.yaml index 67ea8dcefe..cad290b2e0 100644 --- a/internal/gatewayapi/testdata/httproute-with-urlrewrite-hostname-filter-invalid.out.yaml +++ b/internal/gatewayapi/testdata/httproute-with-urlrewrite-hostname-filter-invalid.out.yaml @@ -1,18 +1,3 @@ -backends: -- apiVersion: gateway.envoyproxy.io/v1alpha1 - kind: Backend - metadata: - name: backend-dynamic-resolver - namespace: default - spec: - type: DynamicResolver - status: - conditions: - - lastTransitionTime: null - message: The Backend was accepted - reason: Accepted - status: "True" - type: Accepted gateways: - apiVersion: gateway.networking.k8s.io/v1 kind: Gateway @@ -31,7 +16,7 @@ gateways: protocol: HTTP status: listeners: - - attachedRoutes: 11 + - attachedRoutes: 6 conditions: - lastTransitionTime: null message: Sending translated listener configuration to the data plane @@ -130,7 +115,7 @@ httpRoutes: parents: - conditions: - lastTransitionTime: null - message: Header must be set when rewrite path type is "Header". + message: Cannot configure multiple urlRewrite filters for a single HTTPRouteRule. reason: UnsupportedValue status: "False" type: Accepted @@ -224,7 +209,7 @@ httpRoutes: parents: - conditions: - lastTransitionTime: null - message: Header must be set when rewrite path type is "Header". + message: Cannot configure multiple urlRewrite filters for a single HTTPRouteRule. reason: UnsupportedValue status: "False" type: Accepted @@ -316,143 +301,6 @@ httpRoutes: matches: - path: value: /header-and-backend - status: - parents: - - conditions: - - lastTransitionTime: null - message: |- - Header must be set when rewrite path type is "Header". - Header must be set when rewrite path type is "Header". - reason: UnsupportedValue - status: "False" - type: Accepted - - lastTransitionTime: null - message: Resolved all the Object references for the Route - reason: ResolvedRefs - status: "True" - type: ResolvedRefs - controllerName: gateway.envoyproxy.io/gatewayclass-controller - parentRef: - name: gateway-1 - namespace: envoy-gateway - sectionName: http -- apiVersion: gateway.networking.k8s.io/v1 - kind: HTTPRoute - metadata: - name: httproute-missing-path-regex - namespace: default - spec: - hostnames: - - gateway.envoyproxy.io - parentRefs: - - name: gateway-1 - namespace: envoy-gateway - sectionName: http - rules: - - backendRefs: - - name: service-1 - port: 8080 - filters: - - extensionRef: - group: gateway.envoyproxy.io - kind: HTTPRouteFilter - name: missing-path-regex - type: ExtensionRef - matches: - - path: - value: /missing-path-regex - status: - parents: - - conditions: - - lastTransitionTime: null - message: PathRegex Pattern and Substitution must be set when rewrite hostname - type is "PathRegex". - reason: UnsupportedValue - status: "False" - type: Accepted - - lastTransitionTime: null - message: Resolved all the Object references for the Route - reason: ResolvedRefs - status: "True" - type: ResolvedRefs - controllerName: gateway.envoyproxy.io/gatewayclass-controller - parentRef: - name: gateway-1 - namespace: envoy-gateway - sectionName: http -- apiVersion: gateway.networking.k8s.io/v1 - kind: HTTPRoute - metadata: - name: httproute-invalid-path-regex - namespace: default - spec: - hostnames: - - gateway.envoyproxy.io - parentRefs: - - name: gateway-1 - namespace: envoy-gateway - sectionName: http - rules: - - backendRefs: - - name: service-1 - port: 8080 - filters: - - extensionRef: - group: gateway.envoyproxy.io - kind: HTTPRouteFilter - name: invalid-path-regex - type: ExtensionRef - matches: - - path: - value: /invalid-path-regex - status: - parents: - - conditions: - - lastTransitionTime: null - message: PathRegex must be a valid RE2 regular expression. - reason: UnsupportedValue - status: "False" - type: Accepted - - lastTransitionTime: null - message: Resolved all the Object references for the Route - reason: ResolvedRefs - status: "True" - type: ResolvedRefs - controllerName: gateway.envoyproxy.io/gatewayclass-controller - parentRef: - name: gateway-1 - namespace: envoy-gateway - sectionName: http -- apiVersion: gateway.networking.k8s.io/v1 - kind: HTTPRoute - metadata: - name: httproute-multiple-path-regex-host-rewrites - namespace: default - spec: - hostnames: - - gateway.envoyproxy.io - parentRefs: - - name: gateway-1 - namespace: envoy-gateway - sectionName: http - rules: - - backendRefs: - - name: service-1 - port: 8080 - filters: - - extensionRef: - group: gateway.envoyproxy.io - kind: HTTPRouteFilter - name: valid-path-regex - type: ExtensionRef - - extensionRef: - group: gateway.envoyproxy.io - kind: HTTPRouteFilter - name: valid-path-regex - type: ExtensionRef - matches: - - path: - value: /two-path-regex status: parents: - conditions: @@ -471,97 +319,6 @@ httpRoutes: name: gateway-1 namespace: envoy-gateway sectionName: http -- apiVersion: gateway.networking.k8s.io/v1 - kind: HTTPRoute - metadata: - name: httproute-path-regex-and-core-host-rewrite - namespace: default - spec: - hostnames: - - gateway.envoyproxy.io - parentRefs: - - name: gateway-1 - namespace: envoy-gateway - sectionName: http - rules: - - backendRefs: - - name: service-1 - port: 8080 - filters: - - extensionRef: - group: gateway.envoyproxy.io - kind: HTTPRouteFilter - name: valid-path-regex - type: ExtensionRef - - type: URLRewrite - urlRewrite: - hostname: rewrite.com - matches: - - path: - value: /path-regex-and-core - status: - parents: - - conditions: - - lastTransitionTime: null - message: Cannot configure multiple urlRewrite filters for a single HTTPRouteRule. - reason: UnsupportedValue - status: "False" - type: Accepted - - lastTransitionTime: null - message: Resolved all the Object references for the Route - reason: ResolvedRefs - status: "True" - type: ResolvedRefs - controllerName: gateway.envoyproxy.io/gatewayclass-controller - parentRef: - name: gateway-1 - namespace: envoy-gateway - sectionName: http -- apiVersion: gateway.networking.k8s.io/v1 - kind: HTTPRoute - metadata: - name: httproute-dynamic-resolver-path-regex - namespace: default - spec: - hostnames: - - gateway.envoyproxy.io - parentRefs: - - name: gateway-1 - namespace: envoy-gateway - sectionName: http - rules: - - backendRefs: - - group: gateway.envoyproxy.io - kind: Backend - name: backend-dynamic-resolver - filters: - - extensionRef: - group: gateway.envoyproxy.io - kind: HTTPRouteFilter - name: valid-path-regex - type: ExtensionRef - matches: - - path: - value: /dynamic-resolver-path-regex - status: - parents: - - conditions: - - lastTransitionTime: null - message: Route is accepted - reason: Accepted - status: "True" - type: Accepted - - lastTransitionTime: null - message: 'Failed to process route rule 0: host rewrite from path (PathRegex) - is not supported with a dynamic resolver backend.' - reason: UnsupportedValue - status: "False" - type: ResolvedRefs - controllerName: gateway.envoyproxy.io/gatewayclass-controller - parentRef: - name: gateway-1 - namespace: envoy-gateway - sectionName: http infraIR: envoy-gateway/gateway-1: proxy: @@ -623,42 +380,6 @@ xdsIR: mergeSlashes: true port: 10080 routes: - - directResponse: - statusCode: 500 - hostname: gateway.envoyproxy.io - isHTTP2: false - metadata: - kind: HTTPRoute - name: httproute-dynamic-resolver-path-regex - namespace: default - name: httproute/default/httproute-dynamic-resolver-path-regex/rule/0/match/0/gateway_envoyproxy_io - pathMatch: - distinct: false - name: "" - prefix: /dynamic-resolver-path-regex - urlRewrite: - host: - pathRegex: - pattern: ^/[^/]+/([^/]+).* - substitution: \1 - - directResponse: - statusCode: 500 - hostname: gateway.envoyproxy.io - isHTTP2: false - metadata: - kind: HTTPRoute - name: httproute-path-regex-and-core-host-rewrite - namespace: default - name: httproute/default/httproute-path-regex-and-core-host-rewrite/rule/0/match/0/gateway_envoyproxy_io - pathMatch: - distinct: false - name: "" - prefix: /path-regex-and-core - urlRewrite: - host: - pathRegex: - pattern: ^/[^/]+/([^/]+).* - substitution: \1 - directResponse: statusCode: 500 hostname: gateway.envoyproxy.io @@ -672,32 +393,9 @@ xdsIR: distinct: false name: "" prefix: /header-and-backend - - directResponse: - statusCode: 500 - hostname: gateway.envoyproxy.io - isHTTP2: false - metadata: - kind: HTTPRoute - name: httproute-missing-path-regex - namespace: default - name: httproute/default/httproute-missing-path-regex/rule/0/match/0/gateway_envoyproxy_io - pathMatch: - distinct: false - name: "" - prefix: /missing-path-regex - - directResponse: - statusCode: 500 - hostname: gateway.envoyproxy.io - isHTTP2: false - metadata: - kind: HTTPRoute - name: httproute-invalid-path-regex - namespace: default - name: httproute/default/httproute-invalid-path-regex/rule/0/match/0/gateway_envoyproxy_io - pathMatch: - distinct: false - name: "" - prefix: /invalid-path-regex + urlRewrite: + host: + header: my-host - directResponse: statusCode: 500 hostname: gateway.envoyproxy.io @@ -711,24 +409,6 @@ xdsIR: distinct: false name: "" prefix: /invalid-header - - directResponse: - statusCode: 500 - hostname: gateway.envoyproxy.io - isHTTP2: false - metadata: - kind: HTTPRoute - name: httproute-multiple-path-regex-host-rewrites - namespace: default - name: httproute/default/httproute-multiple-path-regex-host-rewrites/rule/0/match/0/gateway_envoyproxy_io - pathMatch: - distinct: false - name: "" - prefix: /two-path-regex - urlRewrite: - host: - pathRegex: - pattern: ^/[^/]+/([^/]+).* - substitution: \1 - directResponse: statusCode: 500 hostname: gateway.envoyproxy.io @@ -776,7 +456,7 @@ xdsIR: prefix: /two-headers urlRewrite: host: - header: my-host2 + header: my-host - directResponse: statusCode: 500 hostname: gateway.envoyproxy.io @@ -792,7 +472,7 @@ xdsIR: prefix: /ext-first urlRewrite: host: - name: rewrite.com + header: my-host readyListener: address: 0.0.0.0 ipFamily: IPv4 From 030d9d9e335d821a6b69f7ac7b4612ef26027654 Mon Sep 17 00:00:00 2001 From: Huabing Zhao Date: Tue, 9 Jun 2026 11:08:30 +0800 Subject: [PATCH 08/18] update test files Signed-off-by: Huabing Zhao --- ...urlrewrite-hostname-filter-invalid.in.yaml | 49 ++++++++++++ ...rlrewrite-hostname-filter-invalid.out.yaml | 74 ++++++++++++++++++- 2 files changed, 122 insertions(+), 1 deletion(-) diff --git a/internal/gatewayapi/testdata/httproute-with-urlrewrite-hostname-filter-invalid.in.yaml b/internal/gatewayapi/testdata/httproute-with-urlrewrite-hostname-filter-invalid.in.yaml index 5a7f449904..99eb9b9015 100644 --- a/internal/gatewayapi/testdata/httproute-with-urlrewrite-hostname-filter-invalid.in.yaml +++ b/internal/gatewayapi/testdata/httproute-with-urlrewrite-hostname-filter-invalid.in.yaml @@ -186,6 +186,42 @@ httpRoutes: group: gateway.envoyproxy.io kind: HTTPRouteFilter name: valid-header + # PathRegex host rewrite is invalid on a dynamic resolver backend: the upstream host would be + # derived from request-controlled path text, bypassing the dynamic forward proxy loopback protection. + - apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + namespace: default + name: httproute-dynamic-resolver-path-regex + spec: + hostnames: + - gateway.envoyproxy.io + parentRefs: + - namespace: envoy-gateway + name: gateway-1 + sectionName: http + rules: + - matches: + - path: + value: "/dynamic-resolver-path-regex" + backendRefs: + - group: gateway.envoyproxy.io + kind: Backend + name: backend-dynamic-resolver + filters: + - type: ExtensionRef + extensionRef: + group: gateway.envoyproxy.io + kind: HTTPRouteFilter + name: valid-path-regex +backends: + - apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: Backend + metadata: + name: backend-dynamic-resolver + namespace: default + spec: + type: DynamicResolver httpFilters: - apiVersion: gateway.envoyproxy.io/v1alpha1 kind: HTTPRouteFilter @@ -234,3 +270,16 @@ httpFilters: urlRewrite: hostname: type: Header + - apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: HTTPRouteFilter + metadata: + name: invalid-path-regex + namespace: default + spec: + urlRewrite: + hostname: + type: PathRegex + pathRegex: + pattern: '"([a-z]+)"*+?' + substitution: "backend-\\1.service.namespace.svc.cluster.local" + header: my-host diff --git a/internal/gatewayapi/testdata/httproute-with-urlrewrite-hostname-filter-invalid.out.yaml b/internal/gatewayapi/testdata/httproute-with-urlrewrite-hostname-filter-invalid.out.yaml index cad290b2e0..10c26bf7b3 100644 --- a/internal/gatewayapi/testdata/httproute-with-urlrewrite-hostname-filter-invalid.out.yaml +++ b/internal/gatewayapi/testdata/httproute-with-urlrewrite-hostname-filter-invalid.out.yaml @@ -1,3 +1,18 @@ +backends: +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: Backend + metadata: + name: backend-dynamic-resolver + namespace: default + spec: + type: DynamicResolver + status: + conditions: + - lastTransitionTime: null + message: The Backend was accepted + reason: Accepted + status: "True" + type: Accepted gateways: - apiVersion: gateway.networking.k8s.io/v1 kind: Gateway @@ -16,7 +31,7 @@ gateways: protocol: HTTP status: listeners: - - attachedRoutes: 6 + - attachedRoutes: 7 conditions: - lastTransitionTime: null message: Sending translated listener configuration to the data plane @@ -319,6 +334,50 @@ httpRoutes: name: gateway-1 namespace: envoy-gateway sectionName: http +- apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + name: httproute-dynamic-resolver-path-regex + namespace: default + spec: + hostnames: + - gateway.envoyproxy.io + parentRefs: + - name: gateway-1 + namespace: envoy-gateway + sectionName: http + rules: + - backendRefs: + - group: gateway.envoyproxy.io + kind: Backend + name: backend-dynamic-resolver + filters: + - extensionRef: + group: gateway.envoyproxy.io + kind: HTTPRouteFilter + name: valid-path-regex + type: ExtensionRef + matches: + - path: + value: /dynamic-resolver-path-regex + status: + parents: + - conditions: + - lastTransitionTime: null + message: Route is accepted + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: 'Unable to translate HTTPRouteFilter: default/valid-path-regex.' + reason: UnsupportedValue + status: "False" + type: ResolvedRefs + controllerName: gateway.envoyproxy.io/gatewayclass-controller + parentRef: + name: gateway-1 + namespace: envoy-gateway + sectionName: http infraIR: envoy-gateway/gateway-1: proxy: @@ -380,6 +439,19 @@ xdsIR: mergeSlashes: true port: 10080 routes: + - directResponse: + statusCode: 500 + hostname: gateway.envoyproxy.io + isHTTP2: false + metadata: + kind: HTTPRoute + name: httproute-dynamic-resolver-path-regex + namespace: default + name: httproute/default/httproute-dynamic-resolver-path-regex/rule/0/match/0/gateway_envoyproxy_io + pathMatch: + distinct: false + name: "" + prefix: /dynamic-resolver-path-regex - directResponse: statusCode: 500 hostname: gateway.envoyproxy.io From 27a62f7fefabac52dd49b693fa6f5bd12dc28cf4 Mon Sep 17 00:00:00 2001 From: Huabing Zhao Date: Tue, 9 Jun 2026 11:18:03 +0800 Subject: [PATCH 09/18] update test files Signed-off-by: Huabing Zhao --- ...urlrewrite-hostname-filter-invalid.in.yaml | 37 +++++++++- ...rlrewrite-hostname-filter-invalid.out.yaml | 74 ++++++++++++++++++- 2 files changed, 105 insertions(+), 6 deletions(-) diff --git a/internal/gatewayapi/testdata/httproute-with-urlrewrite-hostname-filter-invalid.in.yaml b/internal/gatewayapi/testdata/httproute-with-urlrewrite-hostname-filter-invalid.in.yaml index 99eb9b9015..9a53ea1f56 100644 --- a/internal/gatewayapi/testdata/httproute-with-urlrewrite-hostname-filter-invalid.in.yaml +++ b/internal/gatewayapi/testdata/httproute-with-urlrewrite-hostname-filter-invalid.in.yaml @@ -186,6 +186,36 @@ httpRoutes: group: gateway.envoyproxy.io kind: HTTPRouteFilter name: valid-header + - apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + namespace: default + name: httproute-header-and-pathregex-host-rewrites + spec: + hostnames: + - gateway.envoyproxy.io + parentRefs: + - namespace: envoy-gateway + name: gateway-1 + sectionName: http + rules: + - matches: + - path: + value: "/header-and-pathregex" + backendRefs: + - name: service-1 + port: 8080 + filters: + - type: ExtensionRef + extensionRef: + group: gateway.envoyproxy.io + kind: HTTPRouteFilter + name: valid-header + - type: ExtensionRef + extensionRef: + group: gateway.envoyproxy.io + kind: HTTPRouteFilter + name: valid-path-regex # PathRegex host rewrite is invalid on a dynamic resolver backend: the upstream host would be # derived from request-controlled path text, bypassing the dynamic forward proxy loopback protection. - apiVersion: gateway.networking.k8s.io/v1 @@ -273,13 +303,12 @@ httpFilters: - apiVersion: gateway.envoyproxy.io/v1alpha1 kind: HTTPRouteFilter metadata: - name: invalid-path-regex + name: valid-path-regex namespace: default spec: urlRewrite: hostname: type: PathRegex pathRegex: - pattern: '"([a-z]+)"*+?' - substitution: "backend-\\1.service.namespace.svc.cluster.local" - header: my-host + pattern: "^/[^/]+/([^/]+).*" + substitution: "\\1" diff --git a/internal/gatewayapi/testdata/httproute-with-urlrewrite-hostname-filter-invalid.out.yaml b/internal/gatewayapi/testdata/httproute-with-urlrewrite-hostname-filter-invalid.out.yaml index 10c26bf7b3..ca16a3d596 100644 --- a/internal/gatewayapi/testdata/httproute-with-urlrewrite-hostname-filter-invalid.out.yaml +++ b/internal/gatewayapi/testdata/httproute-with-urlrewrite-hostname-filter-invalid.out.yaml @@ -31,7 +31,7 @@ gateways: protocol: HTTP status: listeners: - - attachedRoutes: 7 + - attachedRoutes: 8 conditions: - lastTransitionTime: null message: Sending translated listener configuration to the data plane @@ -334,6 +334,54 @@ httpRoutes: name: gateway-1 namespace: envoy-gateway sectionName: http +- apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + name: httproute-header-and-pathregex-host-rewrites + namespace: default + spec: + hostnames: + - gateway.envoyproxy.io + parentRefs: + - name: gateway-1 + namespace: envoy-gateway + sectionName: http + rules: + - backendRefs: + - name: service-1 + port: 8080 + filters: + - extensionRef: + group: gateway.envoyproxy.io + kind: HTTPRouteFilter + name: valid-header + type: ExtensionRef + - extensionRef: + group: gateway.envoyproxy.io + kind: HTTPRouteFilter + name: valid-path-regex + type: ExtensionRef + matches: + - path: + value: /header-and-pathregex + status: + parents: + - conditions: + - lastTransitionTime: null + message: Cannot configure multiple urlRewrite filters for a single HTTPRouteRule. + reason: UnsupportedValue + status: "False" + type: Accepted + - lastTransitionTime: null + message: Resolved all the Object references for the Route + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + controllerName: gateway.envoyproxy.io/gatewayclass-controller + parentRef: + name: gateway-1 + namespace: envoy-gateway + sectionName: http - apiVersion: gateway.networking.k8s.io/v1 kind: HTTPRoute metadata: @@ -369,7 +417,8 @@ httpRoutes: status: "True" type: Accepted - lastTransitionTime: null - message: 'Unable to translate HTTPRouteFilter: default/valid-path-regex.' + message: 'Failed to process route rule 0: host rewrite from path (PathRegex) + is not supported with a dynamic resolver backend.' reason: UnsupportedValue status: "False" type: ResolvedRefs @@ -452,6 +501,27 @@ xdsIR: distinct: false name: "" prefix: /dynamic-resolver-path-regex + urlRewrite: + host: + pathRegex: + pattern: ^/[^/]+/([^/]+).* + substitution: \1 + - directResponse: + statusCode: 500 + hostname: gateway.envoyproxy.io + isHTTP2: false + metadata: + kind: HTTPRoute + name: httproute-header-and-pathregex-host-rewrites + namespace: default + name: httproute/default/httproute-header-and-pathregex-host-rewrites/rule/0/match/0/gateway_envoyproxy_io + pathMatch: + distinct: false + name: "" + prefix: /header-and-pathregex + urlRewrite: + host: + header: my-host - directResponse: statusCode: 500 hostname: gateway.envoyproxy.io From 8fc1d63562fbf5a8419f735cb8b8e6659b378b60 Mon Sep 17 00:00:00 2001 From: Huabing Zhao Date: Tue, 9 Jun 2026 11:22:57 +0800 Subject: [PATCH 10/18] update test files Signed-off-by: Huabing Zhao --- ...urlrewrite-hostname-filter-invalid.in.yaml | 72 +++++++++++ ...rlrewrite-hostname-filter-invalid.out.yaml | 115 +++++++++++++++++- 2 files changed, 186 insertions(+), 1 deletion(-) diff --git a/internal/gatewayapi/testdata/httproute-with-urlrewrite-hostname-filter-invalid.in.yaml b/internal/gatewayapi/testdata/httproute-with-urlrewrite-hostname-filter-invalid.in.yaml index 9a53ea1f56..1293aa5d33 100644 --- a/internal/gatewayapi/testdata/httproute-with-urlrewrite-hostname-filter-invalid.in.yaml +++ b/internal/gatewayapi/testdata/httproute-with-urlrewrite-hostname-filter-invalid.in.yaml @@ -216,6 +216,56 @@ httpRoutes: group: gateway.envoyproxy.io kind: HTTPRouteFilter name: valid-path-regex + - apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + namespace: default + name: httproute-invalid-path-regex + spec: + hostnames: + - gateway.envoyproxy.io + parentRefs: + - namespace: envoy-gateway + name: gateway-1 + sectionName: http + rules: + - matches: + - path: + value: "/invalid-path-regex" + backendRefs: + - name: service-1 + port: 8080 + filters: + - type: ExtensionRef + extensionRef: + group: gateway.envoyproxy.io + kind: HTTPRouteFilter + name: invalid-path-regex + - apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + namespace: default + name: httproute-missing-path-regex + spec: + hostnames: + - gateway.envoyproxy.io + parentRefs: + - namespace: envoy-gateway + name: gateway-1 + sectionName: http + rules: + - matches: + - path: + value: "/missing-path-regex" + backendRefs: + - name: service-1 + port: 8080 + filters: + - type: ExtensionRef + extensionRef: + group: gateway.envoyproxy.io + kind: HTTPRouteFilter + name: missing-path-regex # PathRegex host rewrite is invalid on a dynamic resolver backend: the upstream host would be # derived from request-controlled path text, bypassing the dynamic forward proxy loopback protection. - apiVersion: gateway.networking.k8s.io/v1 @@ -312,3 +362,25 @@ httpFilters: pathRegex: pattern: "^/[^/]+/([^/]+).*" substitution: "\\1" + - apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: HTTPRouteFilter + metadata: + name: invalid-path-regex + namespace: default + spec: + urlRewrite: + hostname: + type: PathRegex + pathRegex: + pattern: '"([a-z]+)"*+?' + substitution: "backend-\\1.service.namespace.svc.cluster.local" + header: my-host + - apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: HTTPRouteFilter + metadata: + name: missing-path-regex + namespace: default + spec: + urlRewrite: + hostname: + type: PathRegex diff --git a/internal/gatewayapi/testdata/httproute-with-urlrewrite-hostname-filter-invalid.out.yaml b/internal/gatewayapi/testdata/httproute-with-urlrewrite-hostname-filter-invalid.out.yaml index ca16a3d596..417ae07eb4 100644 --- a/internal/gatewayapi/testdata/httproute-with-urlrewrite-hostname-filter-invalid.out.yaml +++ b/internal/gatewayapi/testdata/httproute-with-urlrewrite-hostname-filter-invalid.out.yaml @@ -31,7 +31,7 @@ gateways: protocol: HTTP status: listeners: - - attachedRoutes: 8 + - attachedRoutes: 10 conditions: - lastTransitionTime: null message: Sending translated listener configuration to the data plane @@ -382,6 +382,93 @@ httpRoutes: name: gateway-1 namespace: envoy-gateway sectionName: http +- apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + name: httproute-invalid-path-regex + namespace: default + spec: + hostnames: + - gateway.envoyproxy.io + parentRefs: + - name: gateway-1 + namespace: envoy-gateway + sectionName: http + rules: + - backendRefs: + - name: service-1 + port: 8080 + filters: + - extensionRef: + group: gateway.envoyproxy.io + kind: HTTPRouteFilter + name: invalid-path-regex + type: ExtensionRef + matches: + - path: + value: /invalid-path-regex + status: + parents: + - conditions: + - lastTransitionTime: null + message: PathRegex must be a valid RE2 regular expression. + reason: UnsupportedValue + status: "False" + type: Accepted + - lastTransitionTime: null + message: Resolved all the Object references for the Route + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + controllerName: gateway.envoyproxy.io/gatewayclass-controller + parentRef: + name: gateway-1 + namespace: envoy-gateway + sectionName: http +- apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + name: httproute-missing-path-regex + namespace: default + spec: + hostnames: + - gateway.envoyproxy.io + parentRefs: + - name: gateway-1 + namespace: envoy-gateway + sectionName: http + rules: + - backendRefs: + - name: service-1 + port: 8080 + filters: + - extensionRef: + group: gateway.envoyproxy.io + kind: HTTPRouteFilter + name: missing-path-regex + type: ExtensionRef + matches: + - path: + value: /missing-path-regex + status: + parents: + - conditions: + - lastTransitionTime: null + message: PathRegex Pattern and Substitution must be set when rewrite hostname + type is "PathRegex". + reason: UnsupportedValue + status: "False" + type: Accepted + - lastTransitionTime: null + message: Resolved all the Object references for the Route + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + controllerName: gateway.envoyproxy.io/gatewayclass-controller + parentRef: + name: gateway-1 + namespace: envoy-gateway + sectionName: http - apiVersion: gateway.networking.k8s.io/v1 kind: HTTPRoute metadata: @@ -538,6 +625,32 @@ xdsIR: urlRewrite: host: header: my-host + - directResponse: + statusCode: 500 + hostname: gateway.envoyproxy.io + isHTTP2: false + metadata: + kind: HTTPRoute + name: httproute-invalid-path-regex + namespace: default + name: httproute/default/httproute-invalid-path-regex/rule/0/match/0/gateway_envoyproxy_io + pathMatch: + distinct: false + name: "" + prefix: /invalid-path-regex + - directResponse: + statusCode: 500 + hostname: gateway.envoyproxy.io + isHTTP2: false + metadata: + kind: HTTPRoute + name: httproute-missing-path-regex + namespace: default + name: httproute/default/httproute-missing-path-regex/rule/0/match/0/gateway_envoyproxy_io + pathMatch: + distinct: false + name: "" + prefix: /missing-path-regex - directResponse: statusCode: 500 hostname: gateway.envoyproxy.io From a497ca39ddcd23d22ad1b09e4d7394ace9e1181b Mon Sep 17 00:00:00 2001 From: Huabing Zhao Date: Tue, 9 Jun 2026 11:35:32 +0800 Subject: [PATCH 11/18] address comment Signed-off-by: Huabing Zhao --- api/v1alpha1/httproutefilter_types.go | 1 - site/content/en/latest/api/extension_types.md | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/api/v1alpha1/httproutefilter_types.go b/api/v1alpha1/httproutefilter_types.go index 4f550bd043..7cbf4da584 100644 --- a/api/v1alpha1/httproutefilter_types.go +++ b/api/v1alpha1/httproutefilter_types.go @@ -196,7 +196,6 @@ type HTTPHostnameModifier struct { // substitution: "\\1.example.internal" // // +optional - // +notImplementedHide PathRegex *HostnamePathRegexRewrite `json:"pathRegex,omitempty"` } diff --git a/site/content/en/latest/api/extension_types.md b/site/content/en/latest/api/extension_types.md index c7118e9abd..a6744672f5 100644 --- a/site/content/en/latest/api/extension_types.md +++ b/site/content/en/latest/api/extension_types.md @@ -3075,6 +3075,7 @@ _Appears in:_ | --- | --- | --- | --- | --- | | `type` | _[HTTPHostnameModifierType](#httphostnamemodifiertype)_ | true | | | | `header` | _string_ | false | | Header is the name of the header whose value would be used to rewrite the Host header | +| `pathRegex` | _[HostnamePathRegexRewrite](#hostnamepathregexrewrite)_ | false | | PathRegex defines a regex match and substitution applied to the request path to compute
the rewritten Host header.
For example:
pathRegex:
pattern: "^/tenant/([a-z0-9-]+)/.*"
substitution: "\\1.example.internal" | #### HTTPHostnameModifierType From 40c156b42ff43e17a03cda708ec98f0701a61df6 Mon Sep 17 00:00:00 2001 From: Huabing Zhao Date: Tue, 9 Jun 2026 11:55:43 +0800 Subject: [PATCH 12/18] update Signed-off-by: Huabing Zhao --- api/v1alpha1/httproutefilter_types.go | 4 +++- .../generated/gateway.envoyproxy.io_httproutefilters.yaml | 4 +++- .../generated/gateway.envoyproxy.io_httproutefilters.yaml | 4 +++- internal/ir/xds.go | 4 ++-- site/content/en/latest/api/extension_types.md | 2 +- test/helm/gateway-crds-helm/all.out.yaml | 4 +++- test/helm/gateway-crds-helm/e2e.out.yaml | 4 +++- test/helm/gateway-crds-helm/envoy-gateway-crds.out.yaml | 4 +++- 8 files changed, 21 insertions(+), 9 deletions(-) diff --git a/api/v1alpha1/httproutefilter_types.go b/api/v1alpha1/httproutefilter_types.go index 7cbf4da584..92a2c88a1c 100644 --- a/api/v1alpha1/httproutefilter_types.go +++ b/api/v1alpha1/httproutefilter_types.go @@ -190,10 +190,12 @@ type HTTPHostnameModifier struct { // PathRegex defines a regex match and substitution applied to the request path to compute // the rewritten Host header. - // For example: + // For example, with: // pathRegex: // pattern: "^/tenant/([a-z0-9-]+)/.*" // substitution: "\\1.example.internal" + // a request to "http://foo.bar.com/tenant/tenant1/api/v1" has its upstream Host header rewritten + // to "tenant1.example.internal" (the request path "/tenant/tenant1/api/v1" is preserved). // // +optional PathRegex *HostnamePathRegexRewrite `json:"pathRegex,omitempty"` diff --git a/charts/gateway-crds-helm/templates/generated/gateway.envoyproxy.io_httproutefilters.yaml b/charts/gateway-crds-helm/templates/generated/gateway.envoyproxy.io_httproutefilters.yaml index 212a0e1b2b..4e09cd5a49 100644 --- a/charts/gateway-crds-helm/templates/generated/gateway.envoyproxy.io_httproutefilters.yaml +++ b/charts/gateway-crds-helm/templates/generated/gateway.envoyproxy.io_httproutefilters.yaml @@ -419,10 +419,12 @@ spec: description: |- PathRegex defines a regex match and substitution applied to the request path to compute the rewritten Host header. - For example: + For example, with: pathRegex: pattern: "^/tenant/([a-z0-9-]+)/.*" substitution: "\\1.example.internal" + a request to "http://foo.bar.com/tenant/tenant1/api/v1" has its upstream Host header rewritten + to "tenant1.example.internal" (the request path "/tenant/tenant1/api/v1" is preserved). properties: pattern: description: |- diff --git a/charts/gateway-helm/charts/crds/crds/generated/gateway.envoyproxy.io_httproutefilters.yaml b/charts/gateway-helm/charts/crds/crds/generated/gateway.envoyproxy.io_httproutefilters.yaml index d7155ad5c7..c3b3b0fdca 100644 --- a/charts/gateway-helm/charts/crds/crds/generated/gateway.envoyproxy.io_httproutefilters.yaml +++ b/charts/gateway-helm/charts/crds/crds/generated/gateway.envoyproxy.io_httproutefilters.yaml @@ -418,10 +418,12 @@ spec: description: |- PathRegex defines a regex match and substitution applied to the request path to compute the rewritten Host header. - For example: + For example, with: pathRegex: pattern: "^/tenant/([a-z0-9-]+)/.*" substitution: "\\1.example.internal" + a request to "http://foo.bar.com/tenant/tenant1/api/v1" has its upstream Host header rewritten + to "tenant1.example.internal" (the request path "/tenant/tenant1/api/v1" is preserved). properties: pattern: description: |- diff --git a/internal/ir/xds.go b/internal/ir/xds.go index 6a5923f6f7..7532a03033 100644 --- a/internal/ir/xds.go +++ b/internal/ir/xds.go @@ -59,7 +59,7 @@ var ( ErrHTTPPathModifierNoReplace = errors.New("redirect filter cannot have a path modifier that does not supply either fullPathReplace, prefixMatchReplace or regexMatchReplace") ErrHTTPPathRegexModifierNoSetting = errors.New("redirect filter cannot have a path modifier that does not supply either fullPathReplace, prefixMatchReplace or regexMatchReplace") ErrHTTPHostModifierDoubleReplace = errors.New("redirect filter cannot have a host modifier that supplies more than one of Hostname, Header, Backend and PathRegex") - ErrHTTPHostModifierNoReplace = errors.New("redirect filter cannot have a host modifier that does not supply either Hostname, Header, Backend or PathRegex") + ErrHTTPHostModifierEmptyPathRegex = errors.New("host modifier with a PathRegex must supply both a Pattern and a Substitution") ErrAddHeaderEmptyName = errors.New("header modifier filter cannot configure a header without a name to be added") ErrAddHeaderDuplicate = errors.New("header modifier filter attempts to add the same header more than once (case insensitive)") ErrRemoveHeaderDuplicate = errors.New("header modifier filter attempts to remove the same header more than once (case insensitive)") @@ -2301,7 +2301,7 @@ func (r HTTPHostModifier) Validate() error { } if r.PathRegex != nil && (r.PathRegex.Pattern == "" || r.PathRegex.Substitution == "") { - errs = errors.Join(errs, ErrHTTPHostModifierNoReplace) + errs = errors.Join(errs, ErrHTTPHostModifierEmptyPathRegex) } return errs diff --git a/site/content/en/latest/api/extension_types.md b/site/content/en/latest/api/extension_types.md index a6744672f5..72f0944531 100644 --- a/site/content/en/latest/api/extension_types.md +++ b/site/content/en/latest/api/extension_types.md @@ -3075,7 +3075,7 @@ _Appears in:_ | --- | --- | --- | --- | --- | | `type` | _[HTTPHostnameModifierType](#httphostnamemodifiertype)_ | true | | | | `header` | _string_ | false | | Header is the name of the header whose value would be used to rewrite the Host header | -| `pathRegex` | _[HostnamePathRegexRewrite](#hostnamepathregexrewrite)_ | false | | PathRegex defines a regex match and substitution applied to the request path to compute
the rewritten Host header.
For example:
pathRegex:
pattern: "^/tenant/([a-z0-9-]+)/.*"
substitution: "\\1.example.internal" | +| `pathRegex` | _[HostnamePathRegexRewrite](#hostnamepathregexrewrite)_ | false | | PathRegex defines a regex match and substitution applied to the request path to compute
the rewritten Host header.
For example, with:
pathRegex:
pattern: "^/tenant/([a-z0-9-]+)/.*"
substitution: "\\1.example.internal"
a request to "http://foo.bar.com/tenant/tenant1/api/v1" has its upstream Host header rewritten
to "tenant1.example.internal" (the request path "/tenant/tenant1/api/v1" is preserved). | #### HTTPHostnameModifierType diff --git a/test/helm/gateway-crds-helm/all.out.yaml b/test/helm/gateway-crds-helm/all.out.yaml index d2ffcb035d..6fd9ca7808 100644 --- a/test/helm/gateway-crds-helm/all.out.yaml +++ b/test/helm/gateway-crds-helm/all.out.yaml @@ -50189,10 +50189,12 @@ spec: description: |- PathRegex defines a regex match and substitution applied to the request path to compute the rewritten Host header. - For example: + For example, with: pathRegex: pattern: "^/tenant/([a-z0-9-]+)/.*" substitution: "\\1.example.internal" + a request to "http://foo.bar.com/tenant/tenant1/api/v1" has its upstream Host header rewritten + to "tenant1.example.internal" (the request path "/tenant/tenant1/api/v1" is preserved). properties: pattern: description: |- diff --git a/test/helm/gateway-crds-helm/e2e.out.yaml b/test/helm/gateway-crds-helm/e2e.out.yaml index 7d132746c7..39e50c7d63 100644 --- a/test/helm/gateway-crds-helm/e2e.out.yaml +++ b/test/helm/gateway-crds-helm/e2e.out.yaml @@ -28162,10 +28162,12 @@ spec: description: |- PathRegex defines a regex match and substitution applied to the request path to compute the rewritten Host header. - For example: + For example, with: pathRegex: pattern: "^/tenant/([a-z0-9-]+)/.*" substitution: "\\1.example.internal" + a request to "http://foo.bar.com/tenant/tenant1/api/v1" has its upstream Host header rewritten + to "tenant1.example.internal" (the request path "/tenant/tenant1/api/v1" is preserved). properties: pattern: description: |- diff --git a/test/helm/gateway-crds-helm/envoy-gateway-crds.out.yaml b/test/helm/gateway-crds-helm/envoy-gateway-crds.out.yaml index d906358afb..dc19552805 100644 --- a/test/helm/gateway-crds-helm/envoy-gateway-crds.out.yaml +++ b/test/helm/gateway-crds-helm/envoy-gateway-crds.out.yaml @@ -28162,10 +28162,12 @@ spec: description: |- PathRegex defines a regex match and substitution applied to the request path to compute the rewritten Host header. - For example: + For example, with: pathRegex: pattern: "^/tenant/([a-z0-9-]+)/.*" substitution: "\\1.example.internal" + a request to "http://foo.bar.com/tenant/tenant1/api/v1" has its upstream Host header rewritten + to "tenant1.example.internal" (the request path "/tenant/tenant1/api/v1" is preserved). properties: pattern: description: |- From c8e3c6c28baab0ab1f5e44618546b01543a19ae8 Mon Sep 17 00:00:00 2001 From: "Huabing (Robin) Zhao" Date: Tue, 9 Jun 2026 15:28:44 +0800 Subject: [PATCH 13/18] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Signed-off-by: Huabing (Robin) Zhao --- internal/gatewayapi/filters.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/internal/gatewayapi/filters.go b/internal/gatewayapi/filters.go index a02deef655..3d116cf565 100644 --- a/internal/gatewayapi/filters.go +++ b/internal/gatewayapi/filters.go @@ -897,10 +897,9 @@ func (t *Translator) processExtensionRefHTTPFilter(extFilter *gwapiv1.LocalObjec ).WithType(gwapiv1.RouteConditionAccepted) } else if _, err := regexp.Compile(hrf.Spec.URLRewrite.Hostname.PathRegex.Pattern); err != nil { // Avoid envoy NACKs due to invalid regex. - // Golang's regexp is almost identical to RE2: https://pkg.go.dev/regexp/syntax + // Go's regexp syntax is RE2: https://pkg.go.dev/regexp/syntax return status.NewRouteStatusError( errors.New("PathRegex must be a valid RE2 regular expression"), - gwapiv1.RouteReasonUnsupportedValue, ).WithType(gwapiv1.RouteConditionAccepted) } hm = &ir.HTTPHostModifier{ From 5a9ceb98e4628a2afd386ed4f4d1731d37e782fc Mon Sep 17 00:00:00 2001 From: "Huabing (Robin) Zhao" Date: Tue, 9 Jun 2026 15:45:33 +0800 Subject: [PATCH 14/18] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Signed-off-by: Huabing (Robin) Zhao --- .../httproute-with-urlrewrite-hostname-filter-invalid.in.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/gatewayapi/testdata/httproute-with-urlrewrite-hostname-filter-invalid.in.yaml b/internal/gatewayapi/testdata/httproute-with-urlrewrite-hostname-filter-invalid.in.yaml index 1293aa5d33..bd856f8c6e 100644 --- a/internal/gatewayapi/testdata/httproute-with-urlrewrite-hostname-filter-invalid.in.yaml +++ b/internal/gatewayapi/testdata/httproute-with-urlrewrite-hostname-filter-invalid.in.yaml @@ -265,7 +265,7 @@ httpRoutes: extensionRef: group: gateway.envoyproxy.io kind: HTTPRouteFilter - name: missing-path-regex + name: missing-path-regex # PathRegex host rewrite is invalid on a dynamic resolver backend: the upstream host would be # derived from request-controlled path text, bypassing the dynamic forward proxy loopback protection. - apiVersion: gateway.networking.k8s.io/v1 From 8c1f054eb100f5985d75a962eadc40705b6517b0 Mon Sep 17 00:00:00 2001 From: Huabing Zhao Date: Tue, 9 Jun 2026 17:18:24 +0800 Subject: [PATCH 15/18] address comment Signed-off-by: Huabing Zhao --- internal/ir/xds.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/ir/xds.go b/internal/ir/xds.go index 7532a03033..7b56f4d649 100644 --- a/internal/ir/xds.go +++ b/internal/ir/xds.go @@ -58,7 +58,7 @@ var ( ErrHTTPPathModifierDoubleReplace = errors.New("redirect filter cannot have a path modifier that supplies more than one of fullPathReplace, prefixMatchReplace and regexMatchReplace") ErrHTTPPathModifierNoReplace = errors.New("redirect filter cannot have a path modifier that does not supply either fullPathReplace, prefixMatchReplace or regexMatchReplace") ErrHTTPPathRegexModifierNoSetting = errors.New("redirect filter cannot have a path modifier that does not supply either fullPathReplace, prefixMatchReplace or regexMatchReplace") - ErrHTTPHostModifierDoubleReplace = errors.New("redirect filter cannot have a host modifier that supplies more than one of Hostname, Header, Backend and PathRegex") + ErrHTTPHostModifierDoubleReplace = errors.New("url rewrite filter cannot have a host modifier that supplies more than one of Name, Header, Backend and PathRegex") ErrHTTPHostModifierEmptyPathRegex = errors.New("host modifier with a PathRegex must supply both a Pattern and a Substitution") ErrAddHeaderEmptyName = errors.New("header modifier filter cannot configure a header without a name to be added") ErrAddHeaderDuplicate = errors.New("header modifier filter attempts to add the same header more than once (case insensitive)") From fe7118a10e67e7714113197ffe9b4a01e4b1b172 Mon Sep 17 00:00:00 2001 From: Huabing Zhao Date: Tue, 9 Jun 2026 17:28:36 +0800 Subject: [PATCH 16/18] update Signed-off-by: Huabing Zhao --- internal/gatewayapi/filters.go | 1 + .../httproute-with-urlrewrite-hostname-filter-invalid.in.yaml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/internal/gatewayapi/filters.go b/internal/gatewayapi/filters.go index 3d116cf565..acd6c18ff5 100644 --- a/internal/gatewayapi/filters.go +++ b/internal/gatewayapi/filters.go @@ -900,6 +900,7 @@ func (t *Translator) processExtensionRefHTTPFilter(extFilter *gwapiv1.LocalObjec // Go's regexp syntax is RE2: https://pkg.go.dev/regexp/syntax return status.NewRouteStatusError( errors.New("PathRegex must be a valid RE2 regular expression"), + gwapiv1.RouteReasonUnsupportedValue, ).WithType(gwapiv1.RouteConditionAccepted) } hm = &ir.HTTPHostModifier{ diff --git a/internal/gatewayapi/testdata/httproute-with-urlrewrite-hostname-filter-invalid.in.yaml b/internal/gatewayapi/testdata/httproute-with-urlrewrite-hostname-filter-invalid.in.yaml index bd856f8c6e..12fb8895ee 100644 --- a/internal/gatewayapi/testdata/httproute-with-urlrewrite-hostname-filter-invalid.in.yaml +++ b/internal/gatewayapi/testdata/httproute-with-urlrewrite-hostname-filter-invalid.in.yaml @@ -301,7 +301,7 @@ backends: name: backend-dynamic-resolver namespace: default spec: - type: DynamicResolver + type: DynamicResolver httpFilters: - apiVersion: gateway.envoyproxy.io/v1alpha1 kind: HTTPRouteFilter From 8496b1980166039ec239a575a6db3ee16bdf57d9 Mon Sep 17 00:00:00 2001 From: "Huabing (Robin) Zhao" Date: Thu, 23 Jul 2026 18:37:41 -0700 Subject: [PATCH 17/18] api,test: reject CR/LF/NUL in PathRegex hostname rewrite substitution The substitution of a PathRegex hostname rewrite becomes the value of the upstream Host header, which Envoy's proto constrains via well_known_regex HTTP_HEADER_VALUE (^[^\x00\n\r]*$). Sending NUL/CR/LF characters causes an xDS NACK at config load instead of a clear admission-time error. Add a CRD pattern constraint (^[^\r\n\x00]*$) to the substitution field of HostnamePathRegexRewrite so the value is rejected at kubectl apply, mirroring the fix in #8862, and regenerate the CRDs and API reference docs. Add a CEL-suite case covering control characters in the substitution. Signed-off-by: Huabing (Robin) Zhao --- api/v1alpha1/httproutefilter_types.go | 5 +++++ ...ateway.envoyproxy.io_httproutefilters.yaml | 5 +++++ ...ateway.envoyproxy.io_httproutefilters.yaml | 5 +++++ site/content/en/latest/api/extension_types.md | 2 +- test/cel-validation/httproutefilter_test.go | 19 +++++++++++++++++++ 5 files changed, 35 insertions(+), 1 deletion(-) diff --git a/api/v1alpha1/httproutefilter_types.go b/api/v1alpha1/httproutefilter_types.go index 02179da64f..9d8c5df305 100644 --- a/api/v1alpha1/httproutefilter_types.go +++ b/api/v1alpha1/httproutefilter_types.go @@ -154,7 +154,12 @@ type HostnamePathRegexRewrite struct { // // The resulting value is used as the upstream Host header and should be constrained to a valid // DNS hostname by using explicit regex capture groups in Pattern. + // + // The NUL, CR, and LF characters are not allowed: they are invalid in an HTTP header value and are + // rejected by the Envoy proto (well_known_regex HTTP_HEADER_VALUE), which would otherwise cause the + // generated configuration to be rejected by the data plane. // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:Pattern=`^[^\r\n\x00]*$` Substitution string `json:"substitution"` } diff --git a/charts/gateway-crds-helm/templates/generated/gateway.envoyproxy.io_httproutefilters.yaml b/charts/gateway-crds-helm/templates/generated/gateway.envoyproxy.io_httproutefilters.yaml index 45a603d55a..57e3053b85 100644 --- a/charts/gateway-crds-helm/templates/generated/gateway.envoyproxy.io_httproutefilters.yaml +++ b/charts/gateway-crds-helm/templates/generated/gateway.envoyproxy.io_httproutefilters.yaml @@ -448,7 +448,12 @@ spec: The resulting value is used as the upstream Host header and should be constrained to a valid DNS hostname by using explicit regex capture groups in Pattern. + + The NUL, CR, and LF characters are not allowed: they are invalid in an HTTP header value and are + rejected by the Envoy proto (well_known_regex HTTP_HEADER_VALUE), which would otherwise cause the + generated configuration to be rejected by the data plane. minLength: 1 + pattern: ^[^\r\n\x00]*$ type: string required: - pattern diff --git a/charts/gateway-helm/charts/crds/crds/generated/gateway.envoyproxy.io_httproutefilters.yaml b/charts/gateway-helm/charts/crds/crds/generated/gateway.envoyproxy.io_httproutefilters.yaml index cb11c3b65a..1ca1eb7156 100644 --- a/charts/gateway-helm/charts/crds/crds/generated/gateway.envoyproxy.io_httproutefilters.yaml +++ b/charts/gateway-helm/charts/crds/crds/generated/gateway.envoyproxy.io_httproutefilters.yaml @@ -447,7 +447,12 @@ spec: The resulting value is used as the upstream Host header and should be constrained to a valid DNS hostname by using explicit regex capture groups in Pattern. + + The NUL, CR, and LF characters are not allowed: they are invalid in an HTTP header value and are + rejected by the Envoy proto (well_known_regex HTTP_HEADER_VALUE), which would otherwise cause the + generated configuration to be rejected by the data plane. minLength: 1 + pattern: ^[^\r\n\x00]*$ type: string required: - pattern diff --git a/site/content/en/latest/api/extension_types.md b/site/content/en/latest/api/extension_types.md index c5b81927e7..f9228ac372 100644 --- a/site/content/en/latest/api/extension_types.md +++ b/site/content/en/latest/api/extension_types.md @@ -3425,7 +3425,7 @@ _Appears in:_ | Field | Type | Required | Default | Description | | --- | --- | --- | --- | --- | | `pattern` | _string_ | true | | Pattern matches a regular expression against the value of the HTTP Path. The regex string must
adhere to the syntax documented in https://github.com/google/re2/wiki/Syntax. | -| `substitution` | _string_ | true | | Substitution is an expression that replaces the matched portion. The expression may include numbered
capture groups that adhere to syntax documented in https://github.com/google/re2/wiki/Syntax.
The resulting value is used as the upstream Host header and should be constrained to a valid
DNS hostname by using explicit regex capture groups in Pattern. | +| `substitution` | _string_ | true | | Substitution is an expression that replaces the matched portion. The expression may include numbered
capture groups that adhere to syntax documented in https://github.com/google/re2/wiki/Syntax.
The resulting value is used as the upstream Host header and should be constrained to a valid
DNS hostname by using explicit regex capture groups in Pattern.
The NUL, CR, and LF characters are not allowed: they are invalid in an HTTP header value and are
rejected by the Envoy proto (well_known_regex HTTP_HEADER_VALUE), which would otherwise cause the
generated configuration to be rejected by the data plane. | #### IPEndpoint diff --git a/test/cel-validation/httproutefilter_test.go b/test/cel-validation/httproutefilter_test.go index b5ab8fda18..8024d6a940 100644 --- a/test/cel-validation/httproutefilter_test.go +++ b/test/cel-validation/httproutefilter_test.go @@ -132,6 +132,25 @@ func TestHTTPRouteFilter(t *testing.T) { }, wantErrors: []string{}, }, + { + desc: "invalid PathRegex substitution with control characters", + mutate: func(httproutefilter *egv1a1.HTTPRouteFilter) { + httproutefilter.Spec = egv1a1.HTTPRouteFilterSpec{ + URLRewrite: &egv1a1.HTTPURLRewriteFilter{ + Hostname: &egv1a1.HTTPHostnameModifier{ + Type: egv1a1.PathRegexHTTPHostnameModifier, + PathRegex: &egv1a1.HostnamePathRegexRewrite{ + Pattern: "^/node/([0-9]+)/api.*", + Substitution: "backend-\\1.service\r\n.local", + }, + }, + }, + } + }, + wantErrors: []string{ + "spec.urlRewrite.hostname.pathRegex.substitution in body should match '^[^\\r\\n\\x00]*$'", + }, + }, { desc: "Valid appendXForwardedHost false", mutate: func(httproutefilter *egv1a1.HTTPRouteFilter) { From 021b5c67d4846efed273e10d20a46684affe7af3 Mon Sep 17 00:00:00 2001 From: "Huabing (Robin) Zhao" Date: Thu, 23 Jul 2026 19:28:12 -0700 Subject: [PATCH 18/18] fix gen Signed-off-by: Huabing (Robin) Zhao --- test/helm/gateway-crds-helm/all.out.yaml | 5 +++++ test/helm/gateway-crds-helm/e2e.out.yaml | 5 +++++ test/helm/gateway-crds-helm/envoy-gateway-crds.out.yaml | 5 +++++ 3 files changed, 15 insertions(+) diff --git a/test/helm/gateway-crds-helm/all.out.yaml b/test/helm/gateway-crds-helm/all.out.yaml index b65b05e957..a407ed1343 100644 --- a/test/helm/gateway-crds-helm/all.out.yaml +++ b/test/helm/gateway-crds-helm/all.out.yaml @@ -52538,7 +52538,12 @@ spec: The resulting value is used as the upstream Host header and should be constrained to a valid DNS hostname by using explicit regex capture groups in Pattern. + + The NUL, CR, and LF characters are not allowed: they are invalid in an HTTP header value and are + rejected by the Envoy proto (well_known_regex HTTP_HEADER_VALUE), which would otherwise cause the + generated configuration to be rejected by the data plane. minLength: 1 + pattern: ^[^\r\n\x00]*$ type: string required: - pattern diff --git a/test/helm/gateway-crds-helm/e2e.out.yaml b/test/helm/gateway-crds-helm/e2e.out.yaml index 86690cd7af..446d11508e 100644 --- a/test/helm/gateway-crds-helm/e2e.out.yaml +++ b/test/helm/gateway-crds-helm/e2e.out.yaml @@ -28476,7 +28476,12 @@ spec: The resulting value is used as the upstream Host header and should be constrained to a valid DNS hostname by using explicit regex capture groups in Pattern. + + The NUL, CR, and LF characters are not allowed: they are invalid in an HTTP header value and are + rejected by the Envoy proto (well_known_regex HTTP_HEADER_VALUE), which would otherwise cause the + generated configuration to be rejected by the data plane. minLength: 1 + pattern: ^[^\r\n\x00]*$ type: string required: - pattern diff --git a/test/helm/gateway-crds-helm/envoy-gateway-crds.out.yaml b/test/helm/gateway-crds-helm/envoy-gateway-crds.out.yaml index f9109531fb..7d026c9ae9 100644 --- a/test/helm/gateway-crds-helm/envoy-gateway-crds.out.yaml +++ b/test/helm/gateway-crds-helm/envoy-gateway-crds.out.yaml @@ -28476,7 +28476,12 @@ spec: The resulting value is used as the upstream Host header and should be constrained to a valid DNS hostname by using explicit regex capture groups in Pattern. + + The NUL, CR, and LF characters are not allowed: they are invalid in an HTTP header value and are + rejected by the Envoy proto (well_known_regex HTTP_HEADER_VALUE), which would otherwise cause the + generated configuration to be rejected by the data plane. minLength: 1 + pattern: ^[^\r\n\x00]*$ type: string required: - pattern