Skip to content
Open
Show file tree
Hide file tree
Changes from 8 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
1 change: 1 addition & 0 deletions internal/envconfig/xds.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ var (
// the client side. For more details, see:
// https://github.com/grpc/proposal/blob/master/A93-xds-ext-proc.md
XDSClientExtProcEnabled = boolFromEnv("GRPC_EXPERIMENTAL_XDS_EXT_PROC_ON_CLIENT", false)

// GCPAuthenticationFilterEnabled enables the xDS GCP Authentication
// filter. For more details, see:
// https://github.com/grpc/proposal/blob/master/A83-xds-gcp-authn-filter.md
Expand Down
240 changes: 240 additions & 0 deletions internal/xds/httpfilter/extproc/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
/*
*
* Copyright 2026 gRPC authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/

package extproc

import (
"fmt"
"regexp"
"time"

"google.golang.org/grpc/credentials"
"google.golang.org/grpc/internal/xds/httpfilter"
"google.golang.org/grpc/internal/xds/matcher"
"google.golang.org/grpc/metadata"

mutationpb "github.com/envoyproxy/go-control-plane/envoy/config/common/mutation_rules/v3"
Comment thread
easwars marked this conversation as resolved.
Outdated
v3procfilterpb "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/ext_proc/v3"
v3matcherpb "github.com/envoyproxy/go-control-plane/envoy/type/matcher/v3"
)

type baseConfig struct {
httpfilter.FilterConfig
Comment thread
easwars marked this conversation as resolved.
config interceptorConfig
Comment thread
easwars marked this conversation as resolved.
Outdated
}

type overrideConfig struct {
httpfilter.FilterConfig
config interceptorConfig
}

// interceptorConfig contains the configuration for the external processing
// client interceptor.
type interceptorConfig struct {
// The following fields can be set either in the filter config or the override
// config. If both are set, the override config will be used.
//
// server is the configuration for the external processing server.
server *serverConfig
// failureModeAllow specifies the behavior when the RPC to the external
// processing server fails. If true, the dataplane RPC will be allowed to
// continue. If false, the data plane RPC will be failed with a grpc status
// code of UNAVAILABLE.
failureModeAllow *bool
// processingModes specifies the processing mode for each dataplane event.
processingModes *processingModes
Comment thread
easwars marked this conversation as resolved.
Outdated
// Attributes to be sent to the external processing server along with the
// request and response dataplane events.
requestAttributes []string
responseAttributes []string
Comment thread
easwars marked this conversation as resolved.

// The following fields can only be set in the base config.
//
Comment thread
easwars marked this conversation as resolved.
Outdated
// mutationRules specifies the rules for what modifications an external
// processing server may make to headers/trailers sent to it.
mutationRules headerMutationRules
Comment thread
easwars marked this conversation as resolved.
Outdated
// allowedHeaders specifies the headers that are allowed to be sent to the
// external processing server. If unset, all headers are allowed.
allowedHeaders []matcher.StringMatcher
// disallowedHeaders specifies the headers that will not be sent to the
// external processing server. This overrides the above allowedHeaders if a
// header matches both.
disallowedHeaders []matcher.StringMatcher
// disableImmediateResponse specifies whether to disable immediate response
// from the external processing server. When true, if the response from
// external processing server has the `immediate_response` field set, the
// dataplane RPC will be failed with `UNAVAILABLE` status code. When false,
// the `immediate_response` field in the response from external processing
// server will be ignored.
disableImmediateResponse bool
// observabilityMode determines if the filter waits for the external
// processing server. If true, events are sent to the server in
// "observation-only" mode; the filter does not wait for a response. If false,
// the filter waits for a response, allowing the server to modify events
// before they reach the dataplane.
observabilityMode bool
// deferredCloseTimeout is the duration the filter waits before closing the
// external processing stream after the dataplane RPC completes. This is only
// applicable when observabilityMode is true; otherwise, it is ignored. The
// default value is 5 seconds.
deferredCloseTimeout time.Duration
}

// processingMode defines how headers, trailers, and bodies are handled in
// relation to the external processing server.
type processingMode int

const (
// modeSkip indicates that the header/trailer/body should not be sent.
modeSkip processingMode = iota
// modeSend indicates that the header/trailer/body should be sent.
modeSend
)

type processingModes struct {
requestHeaderMode processingMode
responseHeaderMode processingMode
responseTrailerMode processingMode
requestBodyMode processingMode
responseBodyMode processingMode
Comment thread
easwars marked this conversation as resolved.
}

// headerMutationRules specifies the rules for what modifications an external
// processing server may make to headers sent on the data plane RPC.
type headerMutationRules struct {
// allowExpr specifies a regular expression that matches the headers that can
// be mutated.
allowExpr *regexp.Regexp
// disallowExpr specifies a regular expression that matches the headers that
// cannot be mutated. This overrides the above allowExpr if a header matches
// both.
disallowExpr *regexp.Regexp
// disallowAll specifies that no header mutations are allowed. This overrides
// all other settings.
disallowAll bool
// disallowIsError specifies whether to return an error if a header mutation
// is disallowed. If true, the data plane RPC will be failed with a grpc
// status code of Unknown.
disallowIsError bool
}

// serverConfig contains the configuration for an external server.
type serverConfig struct {
// targetURI is the name of the external server.
targetURI string
// channelCredentials specifies the transport credentials to use to connect to
// the external server. Must not be nil.
channelCredentials credentials.TransportCredentials
// callCredentials specifies the per-RPC credentials to use when making calls
// to the external server.
callCredentials []credentials.PerRPCCredentials
// timeout is the RPC timeout for the call to the external server. If unset,
// the timeout depends on the usage of this external server. For example,
// cases like ext_authz and ext_proc, where there is a 1:1 mapping between the
// data plane RPC and the external server call, the timeout will be capped by
// the timeout on the data plane RPC. For cases like RLQS where there is a
// side channel to the external server, an unset timeout will result in no
// timeout being applied to the external server call.
timeout time.Duration
// initialMetadata is the additional metadata to include in all RPCs sent to
// the external server.
initialMetadata metadata.MD
}
Comment thread
easwars marked this conversation as resolved.
Outdated

// convertStringMatchers converts a slice of protobuf StringMatcher messages to
// a slice of matcher.StringMatcher.
func convertStringMatchers(patterns []*v3matcherpb.StringMatcher) ([]matcher.StringMatcher, error) {
matchers := make([]matcher.StringMatcher, 0, len(patterns))
for _, p := range patterns {
sm, err := matcher.StringMatcherFromProto(p)
if err != nil {
return nil, err
}
matchers = append(matchers, sm)
}
return matchers, nil
}

// resolveHeaderMode resolves the processing mode for headers based on the
// protobuf enum value. If the mode is not set or set to Default, it returns the
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does or set to Default mean here? I don't see any such special processing.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So this is added because HeaderSendMode has a DEFAULT mode too other than send and skip. See here. And the behaviour of default mode is different for Header and trailer. And so to handle that , I have added this.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we say ProcessingMode_DEFAULT in that case to be clear?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.Because we also need a default case because we need a return value. We can make it return error on default mode but we have already verified that it is the acceptable mode when parsing the proto so I do not want to add the overhead of checking for error every time. WDYT?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If all cases are covered in the switch, the compiler will not require a default case and will not even require a return outside of the switch if the switch is the whole function.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From what I can understand, that is not true. As mentioned in the official GO documentation here , in point 6 , Switch is only terminating if it has a default case and all the cases including default has a return statement. So we need a default case. Let me know if I am missing something.

// provided defaultMode.
func resolveHeaderMode(mode v3procfilterpb.ProcessingMode_HeaderSendMode, defaultMode processingMode) processingMode {
switch mode {
case v3procfilterpb.ProcessingMode_SEND:
return modeSend
case v3procfilterpb.ProcessingMode_SKIP:
return modeSkip
default:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, it might be better here and down in resolveBodyMode to have a case for ProcessingMode_DEFAULT and not have a default case. That way, if a new enum value is added, we don't silently treat it like the default.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ping

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added a ProcessingMode_DEFAULT in the resolveHeaderMode but body mode does not have a ProcessingMode_DEFAULT and we need default statement because : As mentioned in the official GO documentation here , in point 6 , Switch is only terminating if it has a default case and all the cases including default has a return statement.

return defaultMode
}
}

// resolveBodyMode resolves the processing mode for body based on the protobuf
// enum value. If the mode is not set (i.e., default), it returns modeSkip, as
// the default for body is to skip.
func resolveBodyMode(mode v3procfilterpb.ProcessingMode_BodySendMode) processingMode {
switch mode {
case v3procfilterpb.ProcessingMode_GRPC:
return modeSend
case v3procfilterpb.ProcessingMode_NONE:
return modeSkip
default:
return modeSkip
}
}

// headerMutationRulesFromProto converts a protobuf HeaderMutationRules message
// to a headerMutationRules struct.
func headerMutationRulesFromProto(mr *mutationpb.HeaderMutationRules) (headerMutationRules, error) {
var rules headerMutationRules
if mr == nil {
return rules, nil
}
if allowExpr := mr.GetAllowExpression(); allowExpr != nil {
re, err := regexp.Compile(allowExpr.GetRegex())
if err != nil {
return rules, fmt.Errorf("extproc: %v", err)
}
rules.allowExpr = re
}
if disallowExpr := mr.GetDisallowExpression(); disallowExpr != nil {
re, err := regexp.Compile(disallowExpr.GetRegex())
if err != nil {
return rules, fmt.Errorf("extproc: %v", err)
}
rules.disallowExpr = re
}
rules.disallowAll = mr.GetDisallowAll().GetValue()
rules.disallowIsError = mr.GetDisallowIsError().GetValue()
return rules, nil
}
Comment thread
easwars marked this conversation as resolved.
Outdated

// processingModesFromProto converts a protobuf ProcessingMode message
// to a processingModes struct.
func processingModesFromProto(pm *v3procfilterpb.ProcessingMode) *processingModes {
if pm == nil {
return nil
}
Comment thread
easwars marked this conversation as resolved.
Outdated
return &processingModes{
requestHeaderMode: resolveHeaderMode(pm.GetRequestHeaderMode(), modeSend),
responseHeaderMode: resolveHeaderMode(pm.GetResponseHeaderMode(), modeSend),
responseTrailerMode: resolveHeaderMode(pm.GetResponseTrailerMode(), modeSkip),
requestBodyMode: resolveBodyMode(pm.GetRequestBodyMode()),
responseBodyMode: resolveBodyMode(pm.GetResponseBodyMode()),
Comment thread
easwars marked this conversation as resolved.
}
}
Loading
Loading