Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 39 additions & 1 deletion api/v1alpha1/httproutefilter_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,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 {
Expand All @@ -139,6 +143,26 @@ 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.
//
// 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
Comment thread
zhaohuabing marked this conversation as resolved.
// +kubebuilder:validation:Pattern=`^[^\r\n\x00]*$`
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
Expand Down Expand Up @@ -169,14 +193,28 @@ 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
Comment thread
zhaohuabing marked this conversation as resolved.
// +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, 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"`
}

// HTTPCredentialInjectionFilter defines the configuration to inject credentials into the request.
Expand Down
20 changes: 20 additions & 0 deletions api/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -424,12 +424,48 @@ 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, 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: |-
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.

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
- substitution
type: object
type:
description: HTTPPathModifierType defines the type of Hostname
rewrite.
enum:
- Header
- Backend
- PathRegex
type: string
required:
- type
Expand All @@ -439,6 +475,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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -423,12 +423,48 @@ 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, 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: |-
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.

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
- substitution
type: object
type:
description: HTTPPathModifierType defines the type of Hostname
rewrite.
enum:
- Header
- Backend
- PathRegex
type: string
required:
- type
Expand All @@ -438,6 +474,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:
Expand Down
27 changes: 25 additions & 2 deletions internal/gatewayapi/filters.go
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,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)
}
Expand All @@ -259,7 +260,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)
}

Expand Down Expand Up @@ -902,6 +903,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.
// 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{
PathRegex: &ir.RegexMatchReplace{
Pattern: hrf.Spec.URLRewrite.Hostname.PathRegex.Pattern,
Comment thread
zhaohuabing marked this conversation as resolved.
Substitution: hrf.Spec.URLRewrite.Hostname.PathRegex.Substitution,
},
}
}

if filterContext.URLRewrite != nil {
Expand Down
38 changes: 38 additions & 0 deletions internal/gatewayapi/route.go
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,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:
Expand Down Expand Up @@ -455,6 +482,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
Expand Down
Loading