snapshot of https://github.com/grpc/grpc-go.git (master, 2026-09-15; blobs > 131072B omitted)
Some checks failed
codecov / upload (push) Failing after 13m59s
CodeQL / Analyze (push) Failing after 1s
Testing / vet-proto (push) Failing after 1s
Testing / tests (arm, latest) (push) Waiting to run
Testing / extras (latest) (push) Failing after 1s
Testing / static checks (latest-1) (push) Failing after 1s
Testing / tests (-race, latest) (push) Failing after 1s
Testing / tests (i386, latest) (push) Failing after 1s
Testing / tests (latest) (push) Failing after 1s
Testing / tests (latest-1) (push) Failing after 1s
Lock Threads / lock (push) Failing after 1s
govulncheck / govulncheck (.) (push) Failing after 1s
govulncheck / govulncheck (./cmd/protoc-gen-go-grpc) (push) Failing after 1s
govulncheck / govulncheck (./gcp/observability) (push) Failing after 1s
govulncheck / govulncheck (./security/advancedtls) (push) Failing after 1s
govulncheck / govulncheck (./stats/opencensus) (push) Failing after 1s
Stale bot / stale (push) Failing after 1s
Some checks failed
codecov / upload (push) Failing after 13m59s
CodeQL / Analyze (push) Failing after 1s
Testing / vet-proto (push) Failing after 1s
Testing / tests (arm, latest) (push) Waiting to run
Testing / extras (latest) (push) Failing after 1s
Testing / static checks (latest-1) (push) Failing after 1s
Testing / tests (-race, latest) (push) Failing after 1s
Testing / tests (i386, latest) (push) Failing after 1s
Testing / tests (latest) (push) Failing after 1s
Testing / tests (latest-1) (push) Failing after 1s
Lock Threads / lock (push) Failing after 1s
govulncheck / govulncheck (.) (push) Failing after 1s
govulncheck / govulncheck (./cmd/protoc-gen-go-grpc) (push) Failing after 1s
govulncheck / govulncheck (./gcp/observability) (push) Failing after 1s
govulncheck / govulncheck (./security/advancedtls) (push) Failing after 1s
govulncheck / govulncheck (./stats/opencensus) (push) Failing after 1s
Stale bot / stale (push) Failing after 1s
This commit is contained in:
203
internal/xds/server/conn_wrapper.go
Normal file
203
internal/xds/server/conn_wrapper.go
Normal file
@@ -0,0 +1,203 @@
|
||||
/*
|
||||
*
|
||||
* Copyright 2021 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 server
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"google.golang.org/grpc/credentials/tls/certprovider"
|
||||
"google.golang.org/grpc/internal/credentials/xds"
|
||||
"google.golang.org/grpc/internal/grpcsync"
|
||||
"google.golang.org/grpc/internal/transport"
|
||||
)
|
||||
|
||||
// connWrapper is a thin wrapper around a net.Conn returned by Accept(). It
|
||||
// provides the following additional functionality:
|
||||
// 1. A way to retrieve the configured deadline. This is required by the
|
||||
// ServerHandshake() method of the xdsCredentials when it attempts to read
|
||||
// key material from the certificate providers.
|
||||
// 2. Implements the XDSHandshakeInfo() method used by the xdsCredentials to
|
||||
// retrieve the configured certificate providers.
|
||||
// 3. xDS filter_chain configuration determines security configuration.
|
||||
// 4. Dynamically reads routing configuration in UsableRouteConfiguration(), called
|
||||
// to process incoming RPC's. (LDS + RDS configuration).
|
||||
type connWrapper struct {
|
||||
net.Conn
|
||||
|
||||
// The specific filter chain picked for handling this connection.
|
||||
filterChain *filterChain
|
||||
|
||||
// A reference to the listenerWrapper on which this connection was accepted.
|
||||
parent *listenerWrapper
|
||||
|
||||
// The connection deadline as configured by the grpc.Server on the rawConn
|
||||
// that is returned by a call to Accept(). This is set to the connection
|
||||
// timeout value configured by the user (or to a default value) before
|
||||
// initiating the transport credential handshake, and set to zero after
|
||||
// completing the HTTP2 handshake.
|
||||
deadlineMu sync.Mutex
|
||||
deadline time.Time
|
||||
|
||||
mu sync.Mutex
|
||||
st transport.ServerTransport
|
||||
draining bool
|
||||
closed bool
|
||||
hi *grpcsync.RefCounted[*xds.HandshakeInfo]
|
||||
|
||||
// The virtual hosts with matchable routes and instantiated HTTP Filters per
|
||||
// route, or an error.
|
||||
urc *atomic.Pointer[usableRouteConfiguration]
|
||||
}
|
||||
|
||||
// SetDeadline makes a copy of the passed in deadline and forwards the call to
|
||||
// the underlying rawConn.
|
||||
func (c *connWrapper) SetDeadline(t time.Time) error {
|
||||
c.deadlineMu.Lock()
|
||||
c.deadline = t
|
||||
c.deadlineMu.Unlock()
|
||||
return c.Conn.SetDeadline(t)
|
||||
}
|
||||
|
||||
// GetDeadline returns the configured deadline. This will be invoked by the
|
||||
// ServerHandshake() method of the XdsCredentials, which needs a deadline to
|
||||
// pass to the certificate provider.
|
||||
func (c *connWrapper) GetDeadline() time.Time {
|
||||
c.deadlineMu.Lock()
|
||||
t := c.deadline
|
||||
c.deadlineMu.Unlock()
|
||||
return t
|
||||
}
|
||||
|
||||
// XDSHandshakeInfo returns a HandshakeInfo with appropriate security
|
||||
// configuration for this connection. This method is invoked by the
|
||||
// ServerHandshake() method of the XdsCredentials.
|
||||
func (c *connWrapper) XDSHandshakeInfo() (*grpcsync.RefCounted[*xds.HandshakeInfo], error) {
|
||||
if c.filterChain.securityCfg == nil {
|
||||
// If the security config is empty, this means that the control plane
|
||||
// did not provide any security configuration and therefore we should
|
||||
// return nil here so that the xdsCreds can use the configured fallback
|
||||
// credentials.
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
cpc := c.parent.xdsC.BootstrapConfig().CertProviderConfigs()
|
||||
// Identity provider name is mandatory on the server-side, and this is
|
||||
// enforced when the resource is received at the XDSClient layer.
|
||||
secCfg := c.filterChain.securityCfg
|
||||
ip, err := buildProviderFunc(cpc, secCfg.IdentityInstanceName, secCfg.IdentityCertName, true, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Root provider name is optional and required only when doing mTLS.
|
||||
var rp certprovider.Provider
|
||||
if instance, cert := secCfg.RootInstanceName, secCfg.RootCertName; instance != "" {
|
||||
rp, err = buildProviderFunc(cpc, instance, cert, false, true)
|
||||
if err != nil {
|
||||
ip.Close()
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Note that this method is invoked when a connection is accepted, and the
|
||||
// xdsCredentials are doing a handshake on it. This can only ever be called
|
||||
// once per connection. So, we do not need to worry about decrementing the
|
||||
// reference count for the existing HandshakeInfo, which will always be nil.
|
||||
// The reference count will be decremented when the connection is closed.
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
if c.closed {
|
||||
if rp != nil {
|
||||
rp.Close()
|
||||
}
|
||||
ip.Close()
|
||||
return nil, fmt.Errorf("xds: connection closed")
|
||||
}
|
||||
c.hi = xds.NewHandshakeInfo(rp, ip, nil, "", secCfg.RequireClientCert, false, false)
|
||||
return c.hi, nil
|
||||
}
|
||||
|
||||
// PassServerTransport drains the passed in ServerTransport if draining is set,
|
||||
// or persists it to be drained once drained is called.
|
||||
func (c *connWrapper) PassServerTransport(st transport.ServerTransport) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
if c.draining {
|
||||
st.Drain("draining")
|
||||
} else {
|
||||
c.st = st
|
||||
}
|
||||
}
|
||||
|
||||
// Drain drains the associated ServerTransport, or sets draining to true so it
|
||||
// will be drained after it is created.
|
||||
func (c *connWrapper) Drain() {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
if c.st == nil {
|
||||
c.draining = true
|
||||
} else {
|
||||
c.st.Drain("draining")
|
||||
}
|
||||
}
|
||||
|
||||
// Close closes the providers and the underlying connection.
|
||||
func (c *connWrapper) Close() error {
|
||||
c.mu.Lock()
|
||||
if c.closed {
|
||||
c.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
c.closed = true
|
||||
hi := c.hi
|
||||
c.mu.Unlock()
|
||||
|
||||
if hi != nil {
|
||||
hi.Decrement()
|
||||
}
|
||||
|
||||
c.parent.removeConn(c)
|
||||
return c.Conn.Close()
|
||||
}
|
||||
|
||||
func buildProviderFunc(configs map[string]*certprovider.BuildableConfig, instanceName, certName string, wantIdentity, wantRoot bool) (certprovider.Provider, error) {
|
||||
cfg, ok := configs[instanceName]
|
||||
if !ok {
|
||||
// Defensive programming. If a resource received from the management
|
||||
// server contains a certificate provider instance name that is not
|
||||
// found in the bootstrap, the resource is NACKed by the xDS client.
|
||||
return nil, fmt.Errorf("certificate provider instance %q not found in bootstrap file", instanceName)
|
||||
}
|
||||
provider, err := cfg.Build(certprovider.BuildOptions{
|
||||
CertName: certName,
|
||||
WantIdentity: wantIdentity,
|
||||
WantRoot: wantRoot,
|
||||
})
|
||||
if err != nil {
|
||||
// This error is not expected since the bootstrap process parses the
|
||||
// config and makes sure that it is acceptable to the plugin. Still, it
|
||||
// is possible that the plugin parses the config successfully, but its
|
||||
// Build() method errors out.
|
||||
return nil, fmt.Errorf("failed to get security plugin instance (%+v): %v", cfg, err)
|
||||
}
|
||||
return provider, nil
|
||||
}
|
||||
607
internal/xds/server/filter_chain_manager.go
Normal file
607
internal/xds/server/filter_chain_manager.go
Normal file
@@ -0,0 +1,607 @@
|
||||
/*
|
||||
*
|
||||
* 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 server
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/internal/xds/httpfilter"
|
||||
"google.golang.org/grpc/internal/xds/xdsclient/xdsresource"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
const (
|
||||
// An unspecified destination or source prefix should be considered a less
|
||||
// specific match than a wildcard prefix, `0.0.0.0/0` or `::/0`. Also, an
|
||||
// unspecified prefix should match most v4 and v6 addresses compared to the
|
||||
// wildcard prefixes which match only a specific network (v4 or v6).
|
||||
//
|
||||
// We use these constants when looking up the most specific prefix match. A
|
||||
// wildcard prefix will match 0 bits, and to make sure that a wildcard
|
||||
// prefix is considered a more specific match than an unspecified prefix, we
|
||||
// use a value of -1 for the latter.
|
||||
noPrefixMatch = -2
|
||||
unspecifiedPrefixMatch = -1
|
||||
)
|
||||
|
||||
// filterChainManager contains the match criteria specified through filter
|
||||
// chains in a single Listener resource.
|
||||
//
|
||||
// For the match criteria and the filter chains, we need to use package local
|
||||
// structs that are very similar to the xdsresource structs. This is because the
|
||||
// xdsresource structs are meant to contain only configuration and not runtime
|
||||
// state. Here, we need to store runtime state such as the usable route
|
||||
// configuration.
|
||||
type filterChainManager struct {
|
||||
dstPrefixes []*destPrefixEntry // Linear lookup list.
|
||||
defaultFilterChain *filterChain // Default filter chain, if specified.
|
||||
filterChains []*filterChain // Filter chains managed by this filter chain manager.
|
||||
routeConfigNames map[string]bool // Route configuration names which need to be dynamically queried.
|
||||
}
|
||||
|
||||
func newFilterChainManager(filterChainConfigs *xdsresource.NetworkFilterChainMap, defFilterChainConfig *xdsresource.NetworkFilterChainConfig) *filterChainManager {
|
||||
fcMgr := &filterChainManager{routeConfigNames: make(map[string]bool)}
|
||||
|
||||
if filterChainConfigs != nil {
|
||||
for _, entry := range filterChainConfigs.DstPrefixes {
|
||||
dstEntry := &destPrefixEntry{prefix: entry.Prefix}
|
||||
|
||||
for i, srcPrefixes := range entry.SourceTypeArr {
|
||||
if len(srcPrefixes.Entries) == 0 {
|
||||
continue
|
||||
}
|
||||
stDest := &sourcePrefixes{}
|
||||
dstEntry.srcTypeArr[i] = stDest
|
||||
for _, srcEntryConfig := range srcPrefixes.Entries {
|
||||
srcEntry := &sourcePrefixEntry{
|
||||
prefix: srcEntryConfig.Prefix,
|
||||
srcPortMap: make(map[int]*filterChain, len(srcEntryConfig.PortMap)),
|
||||
}
|
||||
stDest.srcPrefixes = append(stDest.srcPrefixes, srcEntry)
|
||||
|
||||
for port, fcConfig := range srcEntryConfig.PortMap {
|
||||
fc := fcMgr.filterChainFromConfig(&fcConfig)
|
||||
if fc.routeConfigName != "" {
|
||||
fcMgr.routeConfigNames[fc.routeConfigName] = true
|
||||
}
|
||||
srcEntry.srcPortMap[port] = fc
|
||||
fcMgr.filterChains = append(fcMgr.filterChains, fc)
|
||||
}
|
||||
}
|
||||
}
|
||||
fcMgr.dstPrefixes = append(fcMgr.dstPrefixes, dstEntry)
|
||||
}
|
||||
}
|
||||
|
||||
if defFilterChainConfig != nil && !defFilterChainConfig.IsEmpty() {
|
||||
fc := fcMgr.filterChainFromConfig(defFilterChainConfig)
|
||||
if fc.routeConfigName != "" {
|
||||
fcMgr.routeConfigNames[fc.routeConfigName] = true
|
||||
}
|
||||
fcMgr.defaultFilterChain = fc
|
||||
fcMgr.filterChains = append(fcMgr.filterChains, fc)
|
||||
}
|
||||
|
||||
return fcMgr
|
||||
}
|
||||
|
||||
func (fcm *filterChainManager) filterChainFromConfig(config *xdsresource.NetworkFilterChainConfig) *filterChain {
|
||||
fc := &filterChain{
|
||||
securityCfg: config.SecurityCfg,
|
||||
routeConfigName: config.HTTPConnMgr.RouteConfigName,
|
||||
inlineRouteConfig: config.HTTPConnMgr.InlineRouteConfig,
|
||||
httpFilters: config.HTTPConnMgr.HTTPFilters,
|
||||
usableRouteConfiguration: &atomic.Pointer[usableRouteConfiguration]{}, // Active state
|
||||
}
|
||||
fc.usableRouteConfiguration.Store(&usableRouteConfiguration{})
|
||||
return fc
|
||||
}
|
||||
|
||||
func (fcm *filterChainManager) stop() {
|
||||
for _, fc := range fcm.filterChains {
|
||||
fc.usableRouteConfiguration.Load().stop()
|
||||
fc.stop()
|
||||
}
|
||||
}
|
||||
|
||||
// destPrefixEntry contains a destination prefix entry and associated source
|
||||
// type matchers.
|
||||
type destPrefixEntry struct {
|
||||
prefix netip.Prefix
|
||||
srcTypeArr sourceTypesArray
|
||||
}
|
||||
|
||||
// An array for the fixed number of source types that we have.
|
||||
type sourceTypesArray [3]*sourcePrefixes
|
||||
|
||||
// sourceType specifies the connection source IP match type.
|
||||
type sourceType int
|
||||
|
||||
const (
|
||||
sourceTypeAny sourceType = iota // matches connection attempts from any source
|
||||
sourceTypeSameOrLoopback // matches connection attempts from the same host
|
||||
sourceTypeExternal // matches connection attempts from a different host
|
||||
)
|
||||
|
||||
// sourcePrefixes contains a list of source prefix entries.
|
||||
type sourcePrefixes struct {
|
||||
srcPrefixes []*sourcePrefixEntry
|
||||
}
|
||||
|
||||
// sourcePrefixEntry contains a source prefix entry and associated source port
|
||||
// matchers.
|
||||
type sourcePrefixEntry struct {
|
||||
prefix netip.Prefix
|
||||
srcPortMap map[int]*filterChain
|
||||
}
|
||||
|
||||
// filterChain captures information from within a FilterChain message in a
|
||||
// Listener resource. This struct contains the active state of a filter chain,
|
||||
// which includes the usable route configuration.
|
||||
type filterChain struct {
|
||||
// The following fields are set at initialization time, and are not
|
||||
// updated afterwards.
|
||||
securityCfg *xdsresource.SecurityConfig
|
||||
httpFilters []xdsresource.HTTPFilter
|
||||
routeConfigName string
|
||||
inlineRouteConfig *xdsresource.RouteConfigUpdate
|
||||
|
||||
// Server filters with reference counts. Is updated when a usable route
|
||||
// configuration is set, and when the filter chain is stopped. Both these
|
||||
// operations always happen with the listener wrapper's lock held.
|
||||
serverFilters []httpfilter.ServerFilter
|
||||
|
||||
// The usable route configuration, is passed to the connWrapper, and is used
|
||||
// to route incoming RPCs. Therefore, this needs to be accessed atomically.
|
||||
usableRouteConfiguration *atomic.Pointer[usableRouteConfiguration]
|
||||
}
|
||||
|
||||
// usableRouteConfiguration contains a matchable route configuration, with
|
||||
// instantiated HTTP Filters per route.
|
||||
type usableRouteConfiguration struct {
|
||||
vhs []virtualHostWithInterceptors
|
||||
err error
|
||||
nodeID string // For logging purposes. Populated by the listener wrapper.
|
||||
}
|
||||
|
||||
func (rc *usableRouteConfiguration) stop() {
|
||||
for _, vh := range rc.vhs {
|
||||
for _, r := range vh.routes {
|
||||
if r.interceptor != nil {
|
||||
r.interceptor.Close()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// virtualHostWithInterceptors captures information present in a VirtualHost
|
||||
// update, and also contains routes with instantiated HTTP Filters.
|
||||
type virtualHostWithInterceptors struct {
|
||||
domains []string
|
||||
routes []routeWithInterceptors
|
||||
}
|
||||
|
||||
// routeWithInterceptors captures information in a Route, and contains
|
||||
// a usable matcher and also instantiated HTTP Filters.
|
||||
type routeWithInterceptors struct {
|
||||
matcher *xdsresource.CompositeMatcher
|
||||
actionType xdsresource.RouteActionType
|
||||
interceptor httpfilter.ServerInterceptor
|
||||
}
|
||||
|
||||
type lookupParams struct {
|
||||
isUnspecifiedListener bool // Whether the server is listening on a wildcard address.
|
||||
dstAddr netip.Addr // dstAddr is the local address of an incoming connection.
|
||||
srcAddr netip.Addr // srcAddr is the remote address of an incoming connection.
|
||||
srcPort int // srcPort is the remote port of an incoming connection.
|
||||
}
|
||||
|
||||
// lookup returns the most specific matching filter chain to be used for an
|
||||
// incoming connection on the server side. Returns a non-nil error if no
|
||||
// matching filter chain could be found.
|
||||
func (fcm *filterChainManager) lookup(params lookupParams) (*filterChain, error) {
|
||||
dstPrefixes := filterByDestinationPrefixes(fcm.dstPrefixes, params.isUnspecifiedListener, params.dstAddr)
|
||||
if len(dstPrefixes) == 0 {
|
||||
if fcm.defaultFilterChain != nil {
|
||||
return fcm.defaultFilterChain, nil
|
||||
}
|
||||
return nil, fmt.Errorf("no matching filter chain based on destination prefix match for %+v", params)
|
||||
}
|
||||
|
||||
srcType := sourceTypeExternal
|
||||
if params.srcAddr == params.dstAddr || params.srcAddr.IsLoopback() {
|
||||
srcType = sourceTypeSameOrLoopback
|
||||
}
|
||||
srcPrefixes := filterBySourceType(dstPrefixes, srcType)
|
||||
if len(srcPrefixes) == 0 {
|
||||
if fcm.defaultFilterChain != nil {
|
||||
return fcm.defaultFilterChain, nil
|
||||
}
|
||||
return nil, fmt.Errorf("no matching filter chain based on source type match for %+v", params)
|
||||
}
|
||||
srcPrefixEntry, err := filterBySourcePrefixes(srcPrefixes, params.srcAddr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if fc := filterBySourcePorts(srcPrefixEntry, params.srcPort); fc != nil {
|
||||
return fc, nil
|
||||
}
|
||||
if fcm.defaultFilterChain != nil {
|
||||
return fcm.defaultFilterChain, nil
|
||||
}
|
||||
return nil, fmt.Errorf("no matching filter chain after all match criteria for %+v", params)
|
||||
}
|
||||
|
||||
// filterByDestinationPrefixes is the first stage of the filter chain
|
||||
// matching algorithm. It takes the complete set of configured filter chain
|
||||
// matchers and returns the most specific matchers based on the destination
|
||||
// prefix match criteria (the prefixes which match the most number of bits).
|
||||
func filterByDestinationPrefixes(dstPrefixes []*destPrefixEntry, isUnspecified bool, dstAddr netip.Addr) []*destPrefixEntry {
|
||||
if !isUnspecified {
|
||||
// Destination prefix matchers are considered only when the listener is
|
||||
// bound to the wildcard address.
|
||||
return dstPrefixes
|
||||
}
|
||||
|
||||
var matchingDstPrefixes []*destPrefixEntry
|
||||
maxSubnetMatch := noPrefixMatch
|
||||
for _, entry := range dstPrefixes {
|
||||
if entry.prefix.IsValid() && !entry.prefix.Contains(dstAddr) {
|
||||
// Skip prefixes which don't match.
|
||||
continue
|
||||
}
|
||||
// For unspecified prefixes, since we do not store a real netip.Prefix
|
||||
// inside prefix, we do not perform a match. Instead we simply set
|
||||
// the matchSize to -1, which is less than the matchSize (0) for a
|
||||
// wildcard prefix.
|
||||
matchSize := unspecifiedPrefixMatch
|
||||
if entry.prefix.IsValid() {
|
||||
matchSize = entry.prefix.Bits()
|
||||
}
|
||||
if matchSize < maxSubnetMatch {
|
||||
continue
|
||||
}
|
||||
if matchSize > maxSubnetMatch {
|
||||
maxSubnetMatch = matchSize
|
||||
matchingDstPrefixes = make([]*destPrefixEntry, 0, 1)
|
||||
}
|
||||
matchingDstPrefixes = append(matchingDstPrefixes, entry)
|
||||
}
|
||||
return matchingDstPrefixes
|
||||
}
|
||||
|
||||
// filterBySourceType is the second stage of the matching algorithm. It
|
||||
// trims the filter chains based on the most specific source type match.
|
||||
//
|
||||
// srcType is one of sourceTypeSameOrLoopback or sourceTypeExternal.
|
||||
func filterBySourceType(dstPrefixes []*destPrefixEntry, srcType sourceType) []*sourcePrefixes {
|
||||
var (
|
||||
srcPrefixes []*sourcePrefixes
|
||||
bestSrcTypeMatch sourceType
|
||||
)
|
||||
for _, entry := range dstPrefixes {
|
||||
match := srcType
|
||||
srcPrefix := entry.srcTypeArr[srcType]
|
||||
if srcPrefix == nil {
|
||||
match = sourceTypeAny
|
||||
srcPrefix = entry.srcTypeArr[sourceTypeAny]
|
||||
}
|
||||
if match < bestSrcTypeMatch {
|
||||
continue
|
||||
}
|
||||
if match > bestSrcTypeMatch {
|
||||
bestSrcTypeMatch = match
|
||||
srcPrefixes = make([]*sourcePrefixes, 0)
|
||||
}
|
||||
if srcPrefix != nil {
|
||||
// The source type array always has 3 entries, but these could be
|
||||
// nil if the appropriate source type match was not specified.
|
||||
srcPrefixes = append(srcPrefixes, srcPrefix)
|
||||
}
|
||||
}
|
||||
return srcPrefixes
|
||||
}
|
||||
|
||||
// filterBySourcePrefixes is the third stage of the filter chain matching
|
||||
// algorithm. It trims the filter chains based on the source prefix. At most one
|
||||
// filter chain with the most specific match progress to the next stage.
|
||||
func filterBySourcePrefixes(srcPrefixes []*sourcePrefixes, srcAddr netip.Addr) (*sourcePrefixEntry, error) {
|
||||
var matchingSrcPrefixes []*sourcePrefixEntry
|
||||
maxSubnetMatch := noPrefixMatch
|
||||
for _, sp := range srcPrefixes {
|
||||
for _, entry := range sp.srcPrefixes {
|
||||
if entry.prefix.IsValid() && !entry.prefix.Contains(srcAddr) {
|
||||
// Skip prefixes which don't match.
|
||||
continue
|
||||
}
|
||||
// For unspecified prefixes, since we do not store a real netip.Prefix
|
||||
// inside prefix, we do not perform a match. Instead we simply set
|
||||
// the matchSize to -1, which is less than the matchSize (0) for a
|
||||
// wildcard prefix.
|
||||
matchSize := unspecifiedPrefixMatch
|
||||
if entry.prefix.IsValid() {
|
||||
matchSize = entry.prefix.Bits()
|
||||
}
|
||||
if matchSize < maxSubnetMatch {
|
||||
continue
|
||||
}
|
||||
if matchSize > maxSubnetMatch {
|
||||
maxSubnetMatch = matchSize
|
||||
matchingSrcPrefixes = make([]*sourcePrefixEntry, 0, 1)
|
||||
}
|
||||
matchingSrcPrefixes = append(matchingSrcPrefixes, entry)
|
||||
}
|
||||
}
|
||||
if len(matchingSrcPrefixes) == 0 {
|
||||
// Finding no match is not an error condition. The caller will end up
|
||||
// using the default filter chain if one was configured.
|
||||
return nil, nil
|
||||
}
|
||||
if len(matchingSrcPrefixes) != 1 {
|
||||
// We expect at most a single matching source prefix entry at this
|
||||
// point. If we have multiple entries here, and some of their source
|
||||
// port matchers had wildcard entries, we could be left with more than
|
||||
// one matching filter chain and hence would have been flagged as an
|
||||
// invalid configuration at config validation time.
|
||||
return nil, errors.New("multiple matching filter chains")
|
||||
}
|
||||
return matchingSrcPrefixes[0], nil
|
||||
}
|
||||
|
||||
// filterBySourcePorts is the last stage of the filter chain matching
|
||||
// algorithm. It trims the filter chains based on the source ports.
|
||||
func filterBySourcePorts(spe *sourcePrefixEntry, srcPort int) *filterChain {
|
||||
if spe == nil {
|
||||
return nil
|
||||
}
|
||||
// A match could be a wildcard match (this happens when the match
|
||||
// criteria does not specify source ports) or a specific port match (this
|
||||
// happens when the match criteria specifies a set of ports and the source
|
||||
// port of the incoming connection matches one of the specified ports). The
|
||||
// latter is considered to be a more specific match.
|
||||
if fc := spe.srcPortMap[srcPort]; fc != nil {
|
||||
return fc
|
||||
}
|
||||
if fc := spe.srcPortMap[0]; fc != nil {
|
||||
return fc
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (fc *filterChain) stop() {
|
||||
for _, sf := range fc.serverFilters {
|
||||
sf.Close()
|
||||
}
|
||||
}
|
||||
|
||||
// serverFilterProvider is used to get a ServerFilter.
|
||||
//
|
||||
// This functionality is provided by the listener wrapper, which maintains a map
|
||||
// of reference counted ServerFilters keyed by {filter_name + type_urls} for all
|
||||
// filters across all filter chains, and ensures that the same filter instance
|
||||
// is reused across resource updates. This allows filter instances to retain
|
||||
// state across resource updates.
|
||||
type serverFilterProvider func(filter xdsresource.HTTPFilter) (httpfilter.ServerFilter, error)
|
||||
|
||||
// updateUsableRouteConfiguration takes Route Configuration and converts it
|
||||
// into matchable route configuration, with instantiated HTTP Filters per route.
|
||||
func (fc *filterChain) updateUsableRouteConfiguration(config *xdsresource.RouteConfigUpdate, updateErr error, provider serverFilterProvider, nodeID string) {
|
||||
if updateErr != nil {
|
||||
urc := &usableRouteConfiguration{err: updateErr, nodeID: nodeID}
|
||||
fc.applyConfiguration(urc, nil)
|
||||
return
|
||||
}
|
||||
|
||||
var serverFilters []httpfilter.ServerFilter
|
||||
vhs := make([]virtualHostWithInterceptors, 0, len(config.VirtualHosts))
|
||||
for _, vh := range config.VirtualHosts {
|
||||
vhwi, sfs, err := fc.convertVirtualHost(vh, provider)
|
||||
if err != nil {
|
||||
for _, sf := range serverFilters {
|
||||
sf.Close()
|
||||
}
|
||||
// Close interceptors from successfully converted virtual hosts.
|
||||
for _, v := range vhs {
|
||||
for _, r := range v.routes {
|
||||
if r.interceptor != nil {
|
||||
r.interceptor.Close()
|
||||
r.interceptor = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
// Non nil if (lds + rds) fails, shouldn't happen since validated by
|
||||
// xDS Client, treat as L7 error but shouldn't happen.
|
||||
urc := &usableRouteConfiguration{err: fmt.Errorf("virtual host construction: %v", err), nodeID: nodeID}
|
||||
fc.applyConfiguration(urc, nil)
|
||||
return
|
||||
}
|
||||
vhs = append(vhs, vhwi)
|
||||
serverFilters = append(serverFilters, sfs...)
|
||||
}
|
||||
|
||||
urc := &usableRouteConfiguration{vhs: vhs, nodeID: nodeID}
|
||||
fc.applyConfiguration(urc, serverFilters)
|
||||
}
|
||||
|
||||
func (fc *filterChain) applyConfiguration(urc *usableRouteConfiguration, serverFilters []httpfilter.ServerFilter) {
|
||||
// Swap in the new configuration first so new RPCs use it immediately.
|
||||
oldURC := fc.usableRouteConfiguration.Swap(urc)
|
||||
oldFilters := fc.serverFilters
|
||||
fc.serverFilters = serverFilters
|
||||
|
||||
// Stop the old interceptors before releasing the filters they might depend on.
|
||||
if oldURC != nil {
|
||||
oldURC.stop()
|
||||
}
|
||||
|
||||
// Release references to old server filters.
|
||||
for _, sf := range oldFilters {
|
||||
sf.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func (fc *filterChain) convertVirtualHost(virtualHost *xdsresource.VirtualHost, provider serverFilterProvider) (_ virtualHostWithInterceptors, _ []httpfilter.ServerFilter, err error) {
|
||||
var serverFilters []httpfilter.ServerFilter
|
||||
defer func() {
|
||||
if err != nil {
|
||||
for _, sf := range serverFilters {
|
||||
sf.Close()
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
rs := make([]routeWithInterceptors, len(virtualHost.Routes))
|
||||
for i, r := range virtualHost.Routes {
|
||||
rs[i].actionType = r.ActionType
|
||||
rs[i].matcher = xdsresource.RouteToMatcher(r)
|
||||
interceptor, sfs, err := fc.newInterceptor(r.HTTPFilterConfigOverride, virtualHost.HTTPFilterConfigOverride, provider)
|
||||
if err != nil {
|
||||
// Close interceptors from successfully converted routes.
|
||||
for _, route := range rs {
|
||||
if route.interceptor != nil {
|
||||
route.interceptor.Close()
|
||||
route.interceptor = nil
|
||||
}
|
||||
}
|
||||
return virtualHostWithInterceptors{}, nil, err
|
||||
}
|
||||
serverFilters = append(serverFilters, sfs...)
|
||||
rs[i].interceptor = interceptor
|
||||
}
|
||||
return virtualHostWithInterceptors{domains: virtualHost.Domains, routes: rs}, serverFilters, nil
|
||||
}
|
||||
|
||||
// statusErrWithNodeID returns an error produced by the status package with the
|
||||
// specified code and message, and includes the xDS node ID.
|
||||
func (rc *usableRouteConfiguration) statusErrWithNodeID(c codes.Code, msg string, args ...any) error {
|
||||
return status.Error(c, fmt.Sprintf("[xDS node id: %v]: %s", rc.nodeID, fmt.Sprintf(msg, args...)))
|
||||
}
|
||||
|
||||
func (fc *filterChain) newInterceptor(routeOverride, virtualHostOverride map[string]httpfilter.FilterConfig, provider serverFilterProvider) (_ httpfilter.ServerInterceptor, _ []httpfilter.ServerFilter, err error) {
|
||||
serverFilters := []httpfilter.ServerFilter{}
|
||||
interceptors := make([]httpfilter.ServerInterceptor, 0, len(fc.httpFilters))
|
||||
defer func() {
|
||||
if err != nil {
|
||||
for _, sf := range serverFilters {
|
||||
sf.Close()
|
||||
}
|
||||
for _, i := range interceptors {
|
||||
i.Close()
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
for _, filter := range fc.httpFilters {
|
||||
// Route is highest priority on server side, as there is no concept
|
||||
// of an upstream cluster on server side.
|
||||
override := routeOverride[filter.Name]
|
||||
if override == nil {
|
||||
// Virtual Host is second priority.
|
||||
override = virtualHostOverride[filter.Name]
|
||||
}
|
||||
|
||||
disabled := filter.Disabled
|
||||
if override != nil {
|
||||
_, disabled = override.(httpfilter.DisabledFilterConfig)
|
||||
}
|
||||
if disabled {
|
||||
continue
|
||||
}
|
||||
|
||||
serverFilter, err := provider(filter)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
serverFilters = append(serverFilters, serverFilter)
|
||||
i, err := serverFilter.BuildServerInterceptor(filter.Config, override)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("error constructing filter: %v", err)
|
||||
}
|
||||
if i != nil {
|
||||
interceptors = append(interceptors, i)
|
||||
}
|
||||
}
|
||||
|
||||
return &interceptorList{interceptors: interceptors}, serverFilters, nil
|
||||
}
|
||||
|
||||
type interceptorList struct {
|
||||
interceptors []httpfilter.ServerInterceptor
|
||||
}
|
||||
|
||||
func (il *interceptorList) InterceptRPC(ss grpc.ServerStream) (grpc.ServerStream, error) {
|
||||
var err error
|
||||
for _, i := range il.interceptors {
|
||||
ss, err = i.InterceptRPC(ss)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return ss, nil
|
||||
}
|
||||
|
||||
func (il *interceptorList) Close() {
|
||||
for _, i := range il.interceptors {
|
||||
i.Close()
|
||||
}
|
||||
}
|
||||
|
||||
// refCountedServerFilter wraps a ServerFilter along with a reference count.
|
||||
// This is used to manage server filters that are shared across filter chains
|
||||
// within a filter chain manager.
|
||||
type refCountedServerFilter struct {
|
||||
httpfilter.ServerFilter
|
||||
refCnt atomic.Int32
|
||||
}
|
||||
|
||||
func (rsf *refCountedServerFilter) incRef() {
|
||||
rsf.refCnt.Add(1)
|
||||
}
|
||||
|
||||
func (rsf *refCountedServerFilter) Close() {
|
||||
if rsf.refCnt.Add(-1) == 0 {
|
||||
rsf.ServerFilter.Close()
|
||||
}
|
||||
}
|
||||
|
||||
// newServerFilterKey generates a key for the given filter using the filter name
|
||||
// and type URLs. This is used for storing ServerFilters in a map.
|
||||
func newServerFilterKey(f *xdsresource.HTTPFilter) serverFilterKey {
|
||||
typeURLs := slices.Clone(f.Filter.TypeURLs())
|
||||
slices.Sort(typeURLs)
|
||||
return serverFilterKey{
|
||||
name: f.Name,
|
||||
typeURLs: strings.Join(typeURLs, ":"),
|
||||
}
|
||||
}
|
||||
|
||||
type serverFilterKey struct {
|
||||
name string
|
||||
typeURLs string
|
||||
}
|
||||
|
||||
func (f *serverFilterKey) String() string {
|
||||
return f.name + ":" + f.typeURLs
|
||||
}
|
||||
796
internal/xds/server/filter_chain_manager_test.go
Normal file
796
internal/xds/server/filter_chain_manager_test.go
Normal file
@@ -0,0 +1,796 @@
|
||||
/*
|
||||
*
|
||||
* 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 server
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/google/go-cmp/cmp/cmpopts"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials/tls/certprovider"
|
||||
"google.golang.org/grpc/internal/testutils"
|
||||
"google.golang.org/grpc/internal/testutils/xds/e2e"
|
||||
"google.golang.org/grpc/internal/xds/bootstrap"
|
||||
"google.golang.org/grpc/internal/xds/clients/xdsclient"
|
||||
"google.golang.org/grpc/internal/xds/httpfilter"
|
||||
"google.golang.org/grpc/internal/xds/httpfilter/router"
|
||||
"google.golang.org/grpc/internal/xds/xdsclient/xdsresource"
|
||||
"google.golang.org/protobuf/types/known/anypb"
|
||||
"google.golang.org/protobuf/types/known/wrapperspb"
|
||||
|
||||
v3corepb "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
|
||||
v3listenerpb "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3"
|
||||
v3routepb "github.com/envoyproxy/go-control-plane/envoy/config/route/v3"
|
||||
v3routerpb "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/router/v3"
|
||||
v3httppb "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3"
|
||||
v3tlspb "github.com/envoyproxy/go-control-plane/envoy/extensions/transport_sockets/tls/v3"
|
||||
)
|
||||
|
||||
const (
|
||||
topLevel = "top level"
|
||||
vhLevel = "virtual host level"
|
||||
rLevel = "route level"
|
||||
)
|
||||
|
||||
func init() {
|
||||
certprovider.Register(&testCertProviderBuilder{})
|
||||
}
|
||||
|
||||
type testCertProviderBuilder struct{}
|
||||
|
||||
func (b *testCertProviderBuilder) ParseConfig(any) (*certprovider.BuildableConfig, error) {
|
||||
return certprovider.NewBuildableConfig("test_cert_provider", nil, func(certprovider.BuildOptions) certprovider.Provider {
|
||||
return nil
|
||||
}), nil
|
||||
}
|
||||
|
||||
func (b *testCertProviderBuilder) Name() string {
|
||||
return "test_cert_provider"
|
||||
}
|
||||
|
||||
// newFilterChainManagerForTesting creates a filterChainManager using the
|
||||
// newFilterChainManager() function. It parses the provided listener resource
|
||||
// using the xdsresource package.
|
||||
func newFilterChainManagerForTesting(t *testing.T, lis *v3listenerpb.Listener) *filterChainManager {
|
||||
t.Helper()
|
||||
if lis.Address == nil {
|
||||
lis.Address = &v3corepb.Address{
|
||||
Address: &v3corepb.Address_SocketAddress{
|
||||
SocketAddress: &v3corepb.SocketAddress{
|
||||
Address: "0.0.0.0",
|
||||
PortSpecifier: &v3corepb.SocketAddress_PortValue{PortValue: 80},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
if lis.Name == "" {
|
||||
lis.Name = "default_listener"
|
||||
}
|
||||
lAny, err := anypb.New(lis)
|
||||
if err != nil {
|
||||
t.Fatalf("anypb.New() failed: %v", err)
|
||||
}
|
||||
bc, err := bootstrap.NewConfigFromContents([]byte(`{
|
||||
"xds_servers": [
|
||||
{
|
||||
"server_uri": "ipv4:///127.0.0.1:443",
|
||||
"channel_creds": [
|
||||
{
|
||||
"type": "insecure"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"certificate_providers": {
|
||||
"default": {
|
||||
"plugin_name": "test_cert_provider"
|
||||
},
|
||||
"instance1": {
|
||||
"plugin_name": "test_cert_provider"
|
||||
},
|
||||
"unspecified-dest-and-source-prefix": {
|
||||
"plugin_name": "test_cert_provider"
|
||||
},
|
||||
"wildcard-prefixes-v4": {
|
||||
"plugin_name": "test_cert_provider"
|
||||
},
|
||||
"wildcard-source-prefix-v6": {
|
||||
"plugin_name": "test_cert_provider"
|
||||
},
|
||||
"specific-destination-prefix-unspecified-source-type": {
|
||||
"plugin_name": "test_cert_provider"
|
||||
},
|
||||
"specific-destination-prefix-specific-source-type": {
|
||||
"plugin_name": "test_cert_provider"
|
||||
},
|
||||
"specific-destination-prefix-specific-source-type-specific-source-prefix": {
|
||||
"plugin_name": "test_cert_provider"
|
||||
},
|
||||
"specific-destination-prefix-specific-source-type-specific-source-prefix-specific-source-port": {
|
||||
"plugin_name": "test_cert_provider"
|
||||
}
|
||||
}
|
||||
}`))
|
||||
if err != nil {
|
||||
t.Fatalf("bootstrap.NewConfigFromContents() failed: %v", err)
|
||||
}
|
||||
decoder := xdsresource.NewListenerResourceTypeDecoder(bc, nil)
|
||||
res, err := decoder.Decode(xdsclient.NewAnyProto(lAny), xdsclient.DecodeOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("decoder.Decode() failed: %v", err)
|
||||
}
|
||||
lrd, ok := res.Resource.(*xdsresource.ListenerResourceData)
|
||||
if !ok {
|
||||
t.Fatalf("Resource type mismatch: %T", res.Resource)
|
||||
}
|
||||
upd := lrd.Resource
|
||||
if upd.TCPListener == nil {
|
||||
t.Fatalf("Decoded listener is not TCP or failed validation")
|
||||
}
|
||||
return newFilterChainManager(&upd.TCPListener.FilterChains, &upd.TCPListener.DefaultFilterChain)
|
||||
}
|
||||
|
||||
func emptyValidNetworkFilters(t *testing.T) []*v3listenerpb.Filter {
|
||||
return []*v3listenerpb.Filter{
|
||||
{
|
||||
Name: "filter-1",
|
||||
ConfigType: &v3listenerpb.Filter_TypedConfig{
|
||||
TypedConfig: testutils.MarshalAny(t, &v3httppb.HttpConnectionManager{
|
||||
RouteSpecifier: &v3httppb.HttpConnectionManager_RouteConfig{
|
||||
RouteConfig: &v3routepb.RouteConfiguration{
|
||||
Name: "routeName",
|
||||
VirtualHosts: []*v3routepb.VirtualHost{{
|
||||
Domains: []string{"lds.target.good:3333"},
|
||||
Routes: []*v3routepb.Route{{
|
||||
Match: &v3routepb.RouteMatch{
|
||||
PathSpecifier: &v3routepb.RouteMatch_Prefix{Prefix: "/"},
|
||||
},
|
||||
Action: &v3routepb.Route_NonForwardingAction{},
|
||||
}},
|
||||
}},
|
||||
},
|
||||
},
|
||||
HttpFilters: []*v3httppb.HttpFilter{e2e.RouterHTTPFilter},
|
||||
}),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
var inlineRouteConfig = &xdsresource.RouteConfigUpdate{
|
||||
VirtualHosts: []*xdsresource.VirtualHost{{
|
||||
Domains: []string{"lds.target.good:3333"},
|
||||
Routes: []*xdsresource.Route{{Prefix: newStringP("/"), ActionType: xdsresource.RouteActionNonForwardingAction}},
|
||||
}},
|
||||
}
|
||||
|
||||
func newStringP(s string) *string {
|
||||
return &s
|
||||
}
|
||||
|
||||
func makeRouterFilterList(t *testing.T) []xdsresource.HTTPFilter {
|
||||
routerBuilder := httpfilter.Get(router.TypeURL)
|
||||
routerConfig, _ := routerBuilder.ParseFilterConfig(testutils.MarshalAny(t, &v3routerpb.Router{}), httpfilter.ParseOptions{})
|
||||
return []xdsresource.HTTPFilter{{
|
||||
Name: "router",
|
||||
Filter: routerBuilder,
|
||||
Config: routerConfig,
|
||||
}}
|
||||
}
|
||||
|
||||
func (s) TestLookup_Failures(t *testing.T) {
|
||||
tests := []struct {
|
||||
desc string
|
||||
lis *v3listenerpb.Listener
|
||||
params lookupParams
|
||||
wantErr string
|
||||
}{
|
||||
{
|
||||
desc: "no_destination_prefix_match",
|
||||
lis: &v3listenerpb.Listener{
|
||||
FilterChains: []*v3listenerpb.FilterChain{
|
||||
{
|
||||
FilterChainMatch: &v3listenerpb.FilterChainMatch{PrefixRanges: []*v3corepb.CidrRange{cidrRangeFromAddressAndPrefixLen("192.168.1.1", 16)}},
|
||||
Filters: emptyValidNetworkFilters(t),
|
||||
},
|
||||
},
|
||||
},
|
||||
params: lookupParams{
|
||||
isUnspecifiedListener: true,
|
||||
dstAddr: netip.AddrFrom4([4]byte([]byte{10, 1, 1, 1})),
|
||||
},
|
||||
wantErr: "no matching filter chain based on destination prefix match",
|
||||
},
|
||||
{
|
||||
desc: "no_source_type_match",
|
||||
lis: &v3listenerpb.Listener{
|
||||
FilterChains: []*v3listenerpb.FilterChain{
|
||||
{
|
||||
FilterChainMatch: &v3listenerpb.FilterChainMatch{
|
||||
PrefixRanges: []*v3corepb.CidrRange{cidrRangeFromAddressAndPrefixLen("192.168.1.1", 16)},
|
||||
SourceType: v3listenerpb.FilterChainMatch_SAME_IP_OR_LOOPBACK,
|
||||
},
|
||||
Filters: emptyValidNetworkFilters(t),
|
||||
},
|
||||
},
|
||||
},
|
||||
params: lookupParams{
|
||||
isUnspecifiedListener: true,
|
||||
dstAddr: netip.AddrFrom4([4]byte([]byte{192, 168, 100, 1})),
|
||||
srcAddr: netip.AddrFrom4([4]byte([]byte{192, 168, 100, 2})),
|
||||
},
|
||||
wantErr: "no matching filter chain based on source type match",
|
||||
},
|
||||
{
|
||||
desc: "no_source_prefix_match",
|
||||
lis: &v3listenerpb.Listener{
|
||||
FilterChains: []*v3listenerpb.FilterChain{
|
||||
{
|
||||
FilterChainMatch: &v3listenerpb.FilterChainMatch{
|
||||
SourcePrefixRanges: []*v3corepb.CidrRange{cidrRangeFromAddressAndPrefixLen("192.168.1.1", 24)},
|
||||
SourceType: v3listenerpb.FilterChainMatch_SAME_IP_OR_LOOPBACK,
|
||||
},
|
||||
Filters: emptyValidNetworkFilters(t),
|
||||
},
|
||||
},
|
||||
},
|
||||
params: lookupParams{
|
||||
isUnspecifiedListener: true,
|
||||
dstAddr: netip.AddrFrom4([4]byte([]byte{192, 168, 100, 1})),
|
||||
srcAddr: netip.AddrFrom4([4]byte([]byte{192, 168, 100, 1})),
|
||||
},
|
||||
wantErr: "no matching filter chain after all match criteria",
|
||||
},
|
||||
{
|
||||
desc: "multiple_matching_filter_chains",
|
||||
lis: &v3listenerpb.Listener{
|
||||
FilterChains: []*v3listenerpb.FilterChain{
|
||||
{
|
||||
FilterChainMatch: &v3listenerpb.FilterChainMatch{SourcePorts: []uint32{1, 2, 3}},
|
||||
Filters: emptyValidNetworkFilters(t),
|
||||
},
|
||||
{
|
||||
FilterChainMatch: &v3listenerpb.FilterChainMatch{
|
||||
PrefixRanges: []*v3corepb.CidrRange{cidrRangeFromAddressAndPrefixLen("192.168.1.1", 16)},
|
||||
SourcePorts: []uint32{1},
|
||||
},
|
||||
Filters: emptyValidNetworkFilters(t),
|
||||
},
|
||||
},
|
||||
},
|
||||
params: lookupParams{
|
||||
// IsUnspecified is not set. This means that the destination
|
||||
// prefix matchers will be ignored.
|
||||
dstAddr: netip.AddrFrom4([4]byte([]byte{192, 168, 100, 1})),
|
||||
srcAddr: netip.AddrFrom4([4]byte([]byte{192, 168, 100, 1})),
|
||||
srcPort: 1,
|
||||
},
|
||||
wantErr: "multiple matching filter chains",
|
||||
},
|
||||
{
|
||||
desc: "no_default_filter_chain",
|
||||
lis: &v3listenerpb.Listener{
|
||||
FilterChains: []*v3listenerpb.FilterChain{
|
||||
{
|
||||
FilterChainMatch: &v3listenerpb.FilterChainMatch{SourcePorts: []uint32{1, 2, 3}},
|
||||
Filters: emptyValidNetworkFilters(t),
|
||||
},
|
||||
},
|
||||
},
|
||||
params: lookupParams{
|
||||
isUnspecifiedListener: true,
|
||||
dstAddr: netip.AddrFrom4([4]byte([]byte{192, 168, 100, 1})),
|
||||
srcAddr: netip.AddrFrom4([4]byte([]byte{192, 168, 100, 1})),
|
||||
srcPort: 80,
|
||||
},
|
||||
wantErr: "no matching filter chain after all match criteria",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
fci := newFilterChainManagerForTesting(t, test.lis)
|
||||
if _, err := fci.lookup(test.params); err == nil || !strings.Contains(err.Error(), test.wantErr) {
|
||||
t.Fatalf("filterChainManager.lookup() failed with %q want %q", err, test.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (s) TestLookup_Successes(t *testing.T) {
|
||||
lisWithDefaultChain := &v3listenerpb.Listener{
|
||||
FilterChains: []*v3listenerpb.FilterChain{
|
||||
{
|
||||
FilterChainMatch: &v3listenerpb.FilterChainMatch{PrefixRanges: []*v3corepb.CidrRange{cidrRangeFromAddressAndPrefixLen("192.168.1.1", 16)}},
|
||||
TransportSocket: &v3corepb.TransportSocket{
|
||||
Name: "envoy.transport_sockets.tls",
|
||||
ConfigType: &v3corepb.TransportSocket_TypedConfig{
|
||||
TypedConfig: testutils.MarshalAny(t, &v3tlspb.DownstreamTlsContext{
|
||||
CommonTlsContext: &v3tlspb.CommonTlsContext{
|
||||
TlsCertificateCertificateProviderInstance: &v3tlspb.CommonTlsContext_CertificateProviderInstance{InstanceName: "instance1"},
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
Filters: emptyValidNetworkFilters(t),
|
||||
},
|
||||
},
|
||||
// A default filter chain with an empty transport socket.
|
||||
DefaultFilterChain: &v3listenerpb.FilterChain{
|
||||
TransportSocket: &v3corepb.TransportSocket{
|
||||
Name: "envoy.transport_sockets.tls",
|
||||
ConfigType: &v3corepb.TransportSocket_TypedConfig{
|
||||
TypedConfig: testutils.MarshalAny(t, &v3tlspb.DownstreamTlsContext{
|
||||
CommonTlsContext: &v3tlspb.CommonTlsContext{
|
||||
TlsCertificateCertificateProviderInstance: &v3tlspb.CommonTlsContext_CertificateProviderInstance{InstanceName: "default"},
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
Filters: emptyValidNetworkFilters(t),
|
||||
},
|
||||
}
|
||||
lisWithoutDefaultChain := &v3listenerpb.Listener{
|
||||
FilterChains: []*v3listenerpb.FilterChain{
|
||||
{
|
||||
TransportSocket: transportSocketWithInstanceName(t, "unspecified-dest-and-source-prefix"),
|
||||
Filters: emptyValidNetworkFilters(t),
|
||||
},
|
||||
{
|
||||
FilterChainMatch: &v3listenerpb.FilterChainMatch{
|
||||
PrefixRanges: []*v3corepb.CidrRange{cidrRangeFromAddressAndPrefixLen("0.0.0.0", 0)},
|
||||
SourcePrefixRanges: []*v3corepb.CidrRange{cidrRangeFromAddressAndPrefixLen("0.0.0.0", 0)},
|
||||
},
|
||||
TransportSocket: transportSocketWithInstanceName(t, "wildcard-prefixes-v4"),
|
||||
Filters: emptyValidNetworkFilters(t),
|
||||
},
|
||||
{
|
||||
FilterChainMatch: &v3listenerpb.FilterChainMatch{
|
||||
SourcePrefixRanges: []*v3corepb.CidrRange{cidrRangeFromAddressAndPrefixLen("::", 0)},
|
||||
},
|
||||
TransportSocket: transportSocketWithInstanceName(t, "wildcard-source-prefix-v6"),
|
||||
Filters: emptyValidNetworkFilters(t),
|
||||
},
|
||||
{
|
||||
FilterChainMatch: &v3listenerpb.FilterChainMatch{PrefixRanges: []*v3corepb.CidrRange{cidrRangeFromAddressAndPrefixLen("192.168.1.1", 16)}},
|
||||
TransportSocket: transportSocketWithInstanceName(t, "specific-destination-prefix-unspecified-source-type"),
|
||||
Filters: emptyValidNetworkFilters(t),
|
||||
},
|
||||
{
|
||||
FilterChainMatch: &v3listenerpb.FilterChainMatch{
|
||||
PrefixRanges: []*v3corepb.CidrRange{cidrRangeFromAddressAndPrefixLen("192.168.1.1", 24)},
|
||||
SourceType: v3listenerpb.FilterChainMatch_EXTERNAL,
|
||||
},
|
||||
TransportSocket: transportSocketWithInstanceName(t, "specific-destination-prefix-specific-source-type"),
|
||||
Filters: emptyValidNetworkFilters(t),
|
||||
},
|
||||
{
|
||||
FilterChainMatch: &v3listenerpb.FilterChainMatch{
|
||||
PrefixRanges: []*v3corepb.CidrRange{cidrRangeFromAddressAndPrefixLen("192.168.1.1", 24)},
|
||||
SourcePrefixRanges: []*v3corepb.CidrRange{cidrRangeFromAddressAndPrefixLen("192.168.92.1", 24)},
|
||||
SourceType: v3listenerpb.FilterChainMatch_EXTERNAL,
|
||||
},
|
||||
TransportSocket: transportSocketWithInstanceName(t, "specific-destination-prefix-specific-source-type-specific-source-prefix"),
|
||||
Filters: emptyValidNetworkFilters(t),
|
||||
},
|
||||
{
|
||||
FilterChainMatch: &v3listenerpb.FilterChainMatch{
|
||||
PrefixRanges: []*v3corepb.CidrRange{cidrRangeFromAddressAndPrefixLen("192.168.1.1", 24)},
|
||||
SourcePrefixRanges: []*v3corepb.CidrRange{cidrRangeFromAddressAndPrefixLen("192.168.92.1", 24)},
|
||||
SourceType: v3listenerpb.FilterChainMatch_EXTERNAL,
|
||||
SourcePorts: []uint32{80},
|
||||
},
|
||||
TransportSocket: transportSocketWithInstanceName(t, "specific-destination-prefix-specific-source-type-specific-source-prefix-specific-source-port"),
|
||||
Filters: emptyValidNetworkFilters(t),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
desc string
|
||||
lis *v3listenerpb.Listener
|
||||
params lookupParams
|
||||
wantFC *filterChain
|
||||
}{
|
||||
{
|
||||
desc: "default_filter_chain",
|
||||
lis: lisWithDefaultChain,
|
||||
params: lookupParams{
|
||||
isUnspecifiedListener: true,
|
||||
dstAddr: netip.AddrFrom4([4]byte([]byte{10, 1, 1, 1})),
|
||||
},
|
||||
wantFC: &filterChain{
|
||||
securityCfg: &xdsresource.SecurityConfig{IdentityInstanceName: "default"},
|
||||
inlineRouteConfig: inlineRouteConfig,
|
||||
httpFilters: makeRouterFilterList(t),
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "unspecified_destination_match",
|
||||
lis: lisWithoutDefaultChain,
|
||||
params: lookupParams{
|
||||
isUnspecifiedListener: true,
|
||||
dstAddr: netip.MustParseAddr("2001:68::db8"),
|
||||
srcAddr: netip.AddrFrom4([4]byte([]byte{10, 1, 1, 1})),
|
||||
srcPort: 1,
|
||||
},
|
||||
wantFC: &filterChain{
|
||||
securityCfg: &xdsresource.SecurityConfig{IdentityInstanceName: "unspecified-dest-and-source-prefix"},
|
||||
inlineRouteConfig: inlineRouteConfig,
|
||||
httpFilters: makeRouterFilterList(t),
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "wildcard_destination_match_v4",
|
||||
lis: lisWithoutDefaultChain,
|
||||
params: lookupParams{
|
||||
isUnspecifiedListener: true,
|
||||
dstAddr: netip.AddrFrom4([4]byte([]byte{10, 1, 1, 1})),
|
||||
srcAddr: netip.AddrFrom4([4]byte([]byte{10, 1, 1, 1})),
|
||||
srcPort: 1,
|
||||
},
|
||||
wantFC: &filterChain{
|
||||
securityCfg: &xdsresource.SecurityConfig{IdentityInstanceName: "wildcard-prefixes-v4"},
|
||||
inlineRouteConfig: inlineRouteConfig,
|
||||
httpFilters: makeRouterFilterList(t),
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "wildcard_source_match_v6",
|
||||
lis: lisWithoutDefaultChain,
|
||||
params: lookupParams{
|
||||
isUnspecifiedListener: true,
|
||||
dstAddr: netip.MustParseAddr("2001:68::1"),
|
||||
srcAddr: netip.MustParseAddr("2001:68::2"),
|
||||
srcPort: 1,
|
||||
},
|
||||
wantFC: &filterChain{
|
||||
securityCfg: &xdsresource.SecurityConfig{IdentityInstanceName: "wildcard-source-prefix-v6"},
|
||||
inlineRouteConfig: inlineRouteConfig,
|
||||
httpFilters: makeRouterFilterList(t),
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "specific_destination_and_wildcard_source_type_match",
|
||||
lis: lisWithoutDefaultChain,
|
||||
params: lookupParams{
|
||||
isUnspecifiedListener: true,
|
||||
dstAddr: netip.AddrFrom4([4]byte([]byte{192, 168, 100, 1})),
|
||||
srcAddr: netip.AddrFrom4([4]byte([]byte{192, 168, 100, 1})),
|
||||
srcPort: 80,
|
||||
},
|
||||
wantFC: &filterChain{
|
||||
securityCfg: &xdsresource.SecurityConfig{IdentityInstanceName: "specific-destination-prefix-unspecified-source-type"},
|
||||
inlineRouteConfig: inlineRouteConfig,
|
||||
httpFilters: makeRouterFilterList(t),
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "specific_destination_and_source_type_match",
|
||||
lis: lisWithoutDefaultChain,
|
||||
params: lookupParams{
|
||||
isUnspecifiedListener: true,
|
||||
dstAddr: netip.AddrFrom4([4]byte([]byte{192, 168, 1, 1})),
|
||||
srcAddr: netip.AddrFrom4([4]byte([]byte{10, 1, 1, 1})),
|
||||
srcPort: 80,
|
||||
},
|
||||
wantFC: &filterChain{
|
||||
securityCfg: &xdsresource.SecurityConfig{IdentityInstanceName: "specific-destination-prefix-specific-source-type"},
|
||||
inlineRouteConfig: inlineRouteConfig,
|
||||
httpFilters: makeRouterFilterList(t),
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "specific_destination_source_type_and_source_prefix",
|
||||
lis: lisWithoutDefaultChain,
|
||||
params: lookupParams{
|
||||
isUnspecifiedListener: true,
|
||||
dstAddr: netip.AddrFrom4([4]byte([]byte{192, 168, 1, 1})),
|
||||
srcAddr: netip.AddrFrom4([4]byte([]byte{192, 168, 92, 100})),
|
||||
srcPort: 70,
|
||||
},
|
||||
wantFC: &filterChain{
|
||||
securityCfg: &xdsresource.SecurityConfig{IdentityInstanceName: "specific-destination-prefix-specific-source-type-specific-source-prefix"},
|
||||
inlineRouteConfig: inlineRouteConfig,
|
||||
httpFilters: makeRouterFilterList(t),
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "specific_destination_source_type_source_prefix_and_source_port",
|
||||
lis: lisWithoutDefaultChain,
|
||||
params: lookupParams{
|
||||
isUnspecifiedListener: true,
|
||||
dstAddr: netip.AddrFrom4([4]byte([]byte{192, 168, 1, 1})),
|
||||
srcAddr: netip.AddrFrom4([4]byte([]byte{192, 168, 92, 100})),
|
||||
srcPort: 80,
|
||||
},
|
||||
wantFC: &filterChain{
|
||||
securityCfg: &xdsresource.SecurityConfig{IdentityInstanceName: "specific-destination-prefix-specific-source-type-specific-source-prefix-specific-source-port"},
|
||||
inlineRouteConfig: inlineRouteConfig,
|
||||
httpFilters: makeRouterFilterList(t),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
fci := newFilterChainManagerForTesting(t, test.lis)
|
||||
gotFC, err := fci.lookup(test.params)
|
||||
if err != nil {
|
||||
t.Fatalf("FilterChainManager.Lookup(%v) failed: %v", test.params, err)
|
||||
}
|
||||
if diff := cmp.Diff(gotFC, test.wantFC, cmpopts.EquateEmpty(), cmpopts.IgnoreFields(filterChain{}, "usableRouteConfiguration"), cmp.AllowUnexported(filterChainManager{}, destPrefixEntry{}, sourcePrefixes{}, sourcePrefixEntry{}, filterChain{}, usableRouteConfiguration{})); diff != "" {
|
||||
t.Fatalf("FilterChainManager.Lookup(%v) mismatch (-got +want):\n%s", test.params, diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type filterCfg struct {
|
||||
httpfilter.FilterConfig
|
||||
// Level is what differentiates top level filters ("top level") vs. second
|
||||
// level ("virtual host level"), and third level ("route level").
|
||||
level string
|
||||
}
|
||||
|
||||
type filterBuilder struct {
|
||||
httpfilter.Builder
|
||||
}
|
||||
|
||||
func (fb *filterBuilder) TypeURLs() []string { return []string{"custom.server.filter"} }
|
||||
|
||||
func (fb *filterBuilder) BuildServerFilter() httpfilter.ServerFilter {
|
||||
return fb
|
||||
}
|
||||
|
||||
func (fb *filterBuilder) Close() {}
|
||||
|
||||
var _ httpfilter.ServerFilterBuilder = &filterBuilder{}
|
||||
|
||||
func (fb *filterBuilder) BuildServerInterceptor(config httpfilter.FilterConfig, override httpfilter.FilterConfig) (httpfilter.ServerInterceptor, error) {
|
||||
var level string
|
||||
level = config.(filterCfg).level
|
||||
|
||||
if override != nil {
|
||||
level = override.(filterCfg).level
|
||||
}
|
||||
return &serverInterceptor{level: level}, nil
|
||||
}
|
||||
|
||||
type serverInterceptor struct {
|
||||
level string
|
||||
}
|
||||
|
||||
func (si *serverInterceptor) InterceptRPC(grpc.ServerStream) (grpc.ServerStream, error) {
|
||||
return nil, errors.New(si.level)
|
||||
}
|
||||
|
||||
func (si *serverInterceptor) Close() {}
|
||||
|
||||
func (s) TestHTTPFilterInstantiation(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
filters []xdsresource.HTTPFilter
|
||||
routeConfig xdsresource.RouteConfigUpdate
|
||||
// A list of strings which will be built from iterating through the
|
||||
// filters ["top level", "vh level", "route level", "route level"...]
|
||||
// wantErrs is the list of error strings that will be constructed from
|
||||
// the deterministic iteration through the vh list and route list. The
|
||||
// error string will be determined by the level of config that the
|
||||
// filter builder receives (i.e. top level, vs. virtual host level vs.
|
||||
// route level).
|
||||
wantErrs []string
|
||||
}{
|
||||
{
|
||||
name: "one http filter no overrides",
|
||||
filters: []xdsresource.HTTPFilter{
|
||||
{Name: "server-interceptor", Filter: &filterBuilder{}, Config: filterCfg{level: topLevel}},
|
||||
},
|
||||
routeConfig: xdsresource.RouteConfigUpdate{
|
||||
VirtualHosts: []*xdsresource.VirtualHost{
|
||||
{
|
||||
Domains: []string{"target"},
|
||||
Routes: []*xdsresource.Route{{
|
||||
Prefix: newStringP("1"),
|
||||
},
|
||||
},
|
||||
},
|
||||
}},
|
||||
wantErrs: []string{topLevel},
|
||||
},
|
||||
{
|
||||
name: "one http filter vh override",
|
||||
filters: []xdsresource.HTTPFilter{
|
||||
{Name: "server-interceptor", Filter: &filterBuilder{}, Config: filterCfg{level: topLevel}},
|
||||
},
|
||||
routeConfig: xdsresource.RouteConfigUpdate{
|
||||
VirtualHosts: []*xdsresource.VirtualHost{
|
||||
{
|
||||
Domains: []string{"target"},
|
||||
Routes: []*xdsresource.Route{{
|
||||
Prefix: newStringP("1"),
|
||||
},
|
||||
},
|
||||
HTTPFilterConfigOverride: map[string]httpfilter.FilterConfig{
|
||||
"server-interceptor": filterCfg{level: vhLevel},
|
||||
},
|
||||
},
|
||||
}},
|
||||
wantErrs: []string{vhLevel},
|
||||
},
|
||||
{
|
||||
name: "one http filter route override",
|
||||
filters: []xdsresource.HTTPFilter{
|
||||
{Name: "server-interceptor", Filter: &filterBuilder{}, Config: filterCfg{level: topLevel}},
|
||||
},
|
||||
routeConfig: xdsresource.RouteConfigUpdate{
|
||||
VirtualHosts: []*xdsresource.VirtualHost{
|
||||
{
|
||||
Domains: []string{"target"},
|
||||
Routes: []*xdsresource.Route{{
|
||||
Prefix: newStringP("1"),
|
||||
HTTPFilterConfigOverride: map[string]httpfilter.FilterConfig{
|
||||
"server-interceptor": filterCfg{level: rLevel},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}},
|
||||
wantErrs: []string{rLevel},
|
||||
},
|
||||
{
|
||||
name: "three routes with different overrides",
|
||||
filters: []xdsresource.HTTPFilter{
|
||||
{Name: "server-interceptor", Filter: &filterBuilder{}, Config: filterCfg{level: topLevel}},
|
||||
},
|
||||
routeConfig: xdsresource.RouteConfigUpdate{
|
||||
VirtualHosts: []*xdsresource.VirtualHost{
|
||||
{
|
||||
Domains: []string{"no overrides"},
|
||||
Routes: []*xdsresource.Route{{
|
||||
Prefix: newStringP("1"),
|
||||
}},
|
||||
},
|
||||
{
|
||||
Domains: []string{"virtual host override"},
|
||||
Routes: []*xdsresource.Route{{
|
||||
Prefix: newStringP("1"),
|
||||
}},
|
||||
HTTPFilterConfigOverride: map[string]httpfilter.FilterConfig{
|
||||
"server-interceptor": filterCfg{level: vhLevel},
|
||||
},
|
||||
},
|
||||
{
|
||||
Domains: []string{"route and virtual host override"},
|
||||
Routes: []*xdsresource.Route{{
|
||||
Prefix: newStringP("1"),
|
||||
HTTPFilterConfigOverride: map[string]httpfilter.FilterConfig{
|
||||
"server-interceptor": filterCfg{level: rLevel},
|
||||
},
|
||||
}},
|
||||
HTTPFilterConfigOverride: map[string]httpfilter.FilterConfig{
|
||||
"server-interceptor": filterCfg{level: vhLevel},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErrs: []string{topLevel, vhLevel, rLevel},
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
fc := filterChain{
|
||||
httpFilters: test.filters,
|
||||
usableRouteConfiguration: &atomic.Pointer[usableRouteConfiguration]{},
|
||||
}
|
||||
fc.usableRouteConfiguration.Store(&usableRouteConfiguration{})
|
||||
|
||||
filters := make(map[serverFilterKey]*refCountedServerFilter)
|
||||
provider := func(filter xdsresource.HTTPFilter) (httpfilter.ServerFilter, error) {
|
||||
builder, ok := filter.Filter.(httpfilter.ServerFilterBuilder)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("filter %q does not support use in server", filter.Name)
|
||||
}
|
||||
return getOrCreateServerFilterWithMap(filters, builder, newServerFilterKey(&filter)), nil
|
||||
}
|
||||
fc.updateUsableRouteConfiguration(&test.routeConfig, nil, provider, "node-id")
|
||||
urc := fc.usableRouteConfiguration.Load()
|
||||
if urc.err != nil {
|
||||
t.Fatalf("Error constructing usable route configuration: %v", urc.err)
|
||||
}
|
||||
// Build out list of errors by iterating through the virtual hosts and routes,
|
||||
// and running the filters in route configurations.
|
||||
var errs []string
|
||||
for _, vh := range urc.vhs {
|
||||
for _, r := range vh.routes {
|
||||
_, err := r.interceptor.InterceptRPC(nil)
|
||||
errs = append(errs, err.Error())
|
||||
}
|
||||
}
|
||||
if !cmp.Equal(errs, test.wantErrs) {
|
||||
t.Fatalf("List of errors %v, want %v", errs, test.wantErrs)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func transportSocketWithInstanceName(t *testing.T, name string) *v3corepb.TransportSocket {
|
||||
return &v3corepb.TransportSocket{
|
||||
Name: "envoy.transport_sockets.tls",
|
||||
ConfigType: &v3corepb.TransportSocket_TypedConfig{
|
||||
TypedConfig: testutils.MarshalAny(t, &v3tlspb.DownstreamTlsContext{
|
||||
CommonTlsContext: &v3tlspb.CommonTlsContext{
|
||||
TlsCertificateCertificateProviderInstance: &v3tlspb.CommonTlsContext_CertificateProviderInstance{InstanceName: name},
|
||||
},
|
||||
}),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func cidrRangeFromAddressAndPrefixLen(address string, prefixLen int) *v3corepb.CidrRange {
|
||||
return &v3corepb.CidrRange{
|
||||
AddressPrefix: address,
|
||||
PrefixLen: &wrapperspb.UInt32Value{
|
||||
Value: uint32(prefixLen),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (s) TestLookup_DroppedChainFallback(t *testing.T) {
|
||||
lis := &v3listenerpb.Listener{
|
||||
FilterChains: []*v3listenerpb.FilterChain{
|
||||
{
|
||||
// This chain has server names, so it should be dropped.
|
||||
Name: "test-filter-chain",
|
||||
FilterChainMatch: &v3listenerpb.FilterChainMatch{
|
||||
PrefixRanges: []*v3corepb.CidrRange{cidrRangeFromAddressAndPrefixLen("192.168.100.1", 32)},
|
||||
ServerNames: []string{"foo"},
|
||||
},
|
||||
Filters: emptyValidNetworkFilters(t),
|
||||
},
|
||||
{
|
||||
// This chain matches destination and has no unsupported fields.
|
||||
FilterChainMatch: &v3listenerpb.FilterChainMatch{
|
||||
PrefixRanges: []*v3corepb.CidrRange{cidrRangeFromAddressAndPrefixLen("192.168.100.0", 16)},
|
||||
},
|
||||
Filters: emptyValidNetworkFilters(t),
|
||||
},
|
||||
},
|
||||
}
|
||||
params := lookupParams{
|
||||
isUnspecifiedListener: true,
|
||||
dstAddr: netip.AddrFrom4([4]byte([]byte{192, 168, 100, 1})),
|
||||
srcAddr: netip.AddrFrom4([4]byte([]byte{192, 168, 100, 1})),
|
||||
srcPort: 80,
|
||||
}
|
||||
|
||||
fci := newFilterChainManagerForTesting(t, lis)
|
||||
fc, err := fci.lookup(params)
|
||||
if err != nil {
|
||||
t.Fatalf("filterChainManager.lookup(%v) failed: %v", params, err)
|
||||
}
|
||||
if fc == nil {
|
||||
t.Fatalf("filterChainManager.lookup(%v) returned nil", params)
|
||||
}
|
||||
}
|
||||
521
internal/xds/server/listener_wrapper.go
Normal file
521
internal/xds/server/listener_wrapper.go
Normal file
@@ -0,0 +1,521 @@
|
||||
/*
|
||||
*
|
||||
* Copyright 2021 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 server contains internal server-side functionality used by the public
|
||||
// facing xds package.
|
||||
package server
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/netip"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"google.golang.org/grpc/backoff"
|
||||
"google.golang.org/grpc/connectivity"
|
||||
"google.golang.org/grpc/grpclog"
|
||||
internalbackoff "google.golang.org/grpc/internal/backoff"
|
||||
internalgrpclog "google.golang.org/grpc/internal/grpclog"
|
||||
"google.golang.org/grpc/internal/grpcsync"
|
||||
"google.golang.org/grpc/internal/xds/bootstrap"
|
||||
"google.golang.org/grpc/internal/xds/clients/xdsclient"
|
||||
"google.golang.org/grpc/internal/xds/httpfilter"
|
||||
"google.golang.org/grpc/internal/xds/xdsclient/xdsresource"
|
||||
)
|
||||
|
||||
var (
|
||||
logger = grpclog.Component("xds")
|
||||
|
||||
// Backoff strategy for temporary errors received from Accept(). If this
|
||||
// needs to be configurable, we can inject it through ListenerWrapperParams.
|
||||
bs = internalbackoff.Exponential{Config: backoff.Config{
|
||||
BaseDelay: 5 * time.Millisecond,
|
||||
Multiplier: 2.0,
|
||||
MaxDelay: 1 * time.Second,
|
||||
}}
|
||||
backoffFunc = bs.Backoff
|
||||
)
|
||||
|
||||
// ServingModeCallback is the callback that users can register to get notified
|
||||
// about the server's serving mode changes. The callback is invoked with the
|
||||
// address of the listener and its new mode. The err parameter is set to a
|
||||
// non-nil error if the server has transitioned into not-serving mode.
|
||||
type ServingModeCallback func(addr net.Addr, mode connectivity.ServingMode, err error)
|
||||
|
||||
// XDSClient wraps the methods on the XDSClient which are required by
|
||||
// the listenerWrapper.
|
||||
type XDSClient interface {
|
||||
WatchResource(typeURL, resourceName string, watcher xdsclient.ResourceWatcher) (cancel func())
|
||||
BootstrapConfig() *bootstrap.Config
|
||||
}
|
||||
|
||||
// ListenerWrapperParams wraps parameters required to create a listenerWrapper.
|
||||
type ListenerWrapperParams struct {
|
||||
// Listener is the net.Listener passed by the user that is to be wrapped.
|
||||
Listener net.Listener
|
||||
// ListenerResourceName is the xDS Listener resource to request.
|
||||
ListenerResourceName string
|
||||
// XDSClient provides the functionality from the XDSClient required here.
|
||||
XDSClient XDSClient
|
||||
// ModeCallback is the callback to invoke when the serving mode changes.
|
||||
ModeCallback ServingModeCallback
|
||||
}
|
||||
|
||||
// NewListenerWrapper creates a new listenerWrapper with params. It returns a
|
||||
// net.Listener. It starts in state not serving, which triggers Accept() +
|
||||
// Close() on any incoming connections.
|
||||
//
|
||||
// Only TCP listeners are supported.
|
||||
func NewListenerWrapper(params ListenerWrapperParams) net.Listener {
|
||||
lw := &listenerWrapper{
|
||||
Listener: params.Listener,
|
||||
name: params.ListenerResourceName,
|
||||
xdsC: params.XDSClient,
|
||||
xdsNodeID: params.XDSClient.BootstrapConfig().Node().GetId(),
|
||||
modeCallback: params.ModeCallback,
|
||||
isUnspecifiedAddr: params.Listener.Addr().(*net.TCPAddr).IP.IsUnspecified(),
|
||||
conns: make(map[*connWrapper]bool),
|
||||
mode: connectivity.ServingModeNotServing,
|
||||
closed: grpcsync.NewEvent(),
|
||||
httpFilters: make(map[serverFilterKey]*refCountedServerFilter),
|
||||
}
|
||||
lw.logger = internalgrpclog.NewPrefixLogger(logger, fmt.Sprintf("[xds-server-listener %p] ", lw))
|
||||
|
||||
// Serve() verifies that Addr() returns a valid TCPAddr. So, it is safe to
|
||||
// ignore the error from SplitHostPort().
|
||||
lisAddr := lw.Listener.Addr().String()
|
||||
lw.addr, lw.port, _ = net.SplitHostPort(lisAddr)
|
||||
|
||||
lw.rdsHandler = newRDSHandler(lw.handleRDSUpdate, lw.xdsC, lw.logger)
|
||||
lw.cancelWatch = xdsresource.WatchListener(lw.xdsC, lw.name, &ldsWatcher{
|
||||
parent: lw,
|
||||
logger: lw.logger,
|
||||
name: lw.name,
|
||||
})
|
||||
return lw
|
||||
}
|
||||
|
||||
// listenerWrapper wraps the net.Listener associated with the listening address
|
||||
// passed to Serve(). It also contains all other state associated with this
|
||||
// particular invocation of Serve().
|
||||
type listenerWrapper struct {
|
||||
// The following fields are initialized at creation and are immutable
|
||||
// afterwards, so do not need synchronization.
|
||||
net.Listener
|
||||
logger *internalgrpclog.PrefixLogger
|
||||
name string
|
||||
xdsC XDSClient
|
||||
xdsNodeID string
|
||||
cancelWatch func()
|
||||
modeCallback ServingModeCallback
|
||||
isUnspecifiedAddr bool // True if listener is bound to IP_ANY address.
|
||||
addr, port string // Listening address and port, validates socket address in LDS responses.
|
||||
closed *grpcsync.Event
|
||||
|
||||
// mu guards access to the below fields.
|
||||
mu sync.Mutex
|
||||
mode connectivity.ServingMode // Current serving mode.
|
||||
activeFilterChainManager *filterChainManager // Filter chain manager currently serving.
|
||||
httpFilters map[serverFilterKey]*refCountedServerFilter // HTTP filter instances keyed by {filter_name + type_urls}.
|
||||
conns map[*connWrapper]bool // conns accepted with configuration from activeFilterChainManager.
|
||||
|
||||
// These fields are read/written to in the context of xDS updates, which are
|
||||
// guaranteed to be emitted synchronously from the xDS Client. Thus, they do
|
||||
// not need further synchronization.
|
||||
pendingFilterChainManager *filterChainManager // Will become active when all route configurations are ready.
|
||||
rdsHandler *rdsHandler // Handles RDS watches and updates for this listener.
|
||||
}
|
||||
|
||||
// maybeUpdateFilterChains swaps in the pending filter chain manager to the
|
||||
// active one if the pending filter chain manager is present. If a swap occurs,
|
||||
// it also drains (gracefully stops) any connections that were accepted on the
|
||||
// old active filter chain manager, and puts this listener in state SERVING.
|
||||
// Must be called within an xDS Client Callback.
|
||||
func (l *listenerWrapper) maybeUpdateFilterChains() {
|
||||
if l.pendingFilterChainManager == nil {
|
||||
// Nothing to update, return early.
|
||||
return
|
||||
}
|
||||
|
||||
l.mu.Lock()
|
||||
// "Updates to a Listener cause all older connections on that Listener to be
|
||||
// gracefully shut down with a grace period of 10 minutes for long-lived
|
||||
// RPC's, such that clients will reconnect and have the updated
|
||||
// configuration apply." - A36
|
||||
connsToClose := l.conns
|
||||
l.conns = make(map[*connWrapper]bool)
|
||||
|
||||
// Swap in the new filter chain manager before draining connections.
|
||||
prevActive := l.activeFilterChainManager
|
||||
l.activeFilterChainManager = l.pendingFilterChainManager
|
||||
l.pendingFilterChainManager = nil
|
||||
l.instantiateFilterChainRoutingConfigurationsLocked()
|
||||
if prevActive != nil {
|
||||
prevActive.stop()
|
||||
}
|
||||
|
||||
// Build a set of all keys currently in use.
|
||||
activeKeys := make(map[serverFilterKey]struct{})
|
||||
for _, fc := range l.activeFilterChainManager.filterChains {
|
||||
for _, f := range fc.httpFilters {
|
||||
activeKeys[newServerFilterKey(&f)] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
// Iterate through the existing filters and delete those not in the active
|
||||
// set.
|
||||
for key, filter := range l.httpFilters {
|
||||
if _, active := activeKeys[key]; !active {
|
||||
if filter.refCnt.Load() != 0 {
|
||||
l.logger.Errorf("HTTP filter with key %s is not used by any active filter chain but has non-zero reference count: %d. This indicates a bug in the filter reference counting logic.", key, filter.refCnt.Load())
|
||||
}
|
||||
delete(l.httpFilters, key)
|
||||
}
|
||||
}
|
||||
l.switchModeLocked(connectivity.ServingModeServing, nil)
|
||||
l.mu.Unlock()
|
||||
|
||||
go func() {
|
||||
for conn := range connsToClose {
|
||||
conn.Drain()
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// handleRDSUpdate rebuilds any routing configuration server side for any filter
|
||||
// chains that point to this RDS, and potentially makes pending lds
|
||||
// configuration to swap to be active.
|
||||
func (l *listenerWrapper) handleRDSUpdate(routeName string, rcu rdsWatcherUpdate) {
|
||||
// Update any filter chains that point to this route configuration.
|
||||
l.mu.Lock()
|
||||
if l.activeFilterChainManager != nil {
|
||||
for _, fc := range l.activeFilterChainManager.filterChains {
|
||||
if fc.routeConfigName != routeName {
|
||||
continue
|
||||
}
|
||||
if rcu.err != nil && rcu.data == nil { // Either NACK before update, or resource not found triggers this conditional.
|
||||
fc.updateUsableRouteConfiguration(nil, rcu.err, l.getOrCreateServerFilterLocked, l.xdsNodeID)
|
||||
continue
|
||||
}
|
||||
fc.updateUsableRouteConfiguration(rcu.data, nil, l.getOrCreateServerFilterLocked, l.xdsNodeID)
|
||||
}
|
||||
}
|
||||
l.mu.Unlock()
|
||||
|
||||
if l.rdsHandler.determineRouteConfigurationReady() {
|
||||
l.maybeUpdateFilterChains()
|
||||
}
|
||||
}
|
||||
|
||||
// instantiateFilterChainRoutingConfigurationsLocked instantiates all of the
|
||||
// routing configuration for the newly active filter chains. For any inline
|
||||
// route configurations, uses that, otherwise uses cached rdsHandler updates.
|
||||
//
|
||||
// Must be called with l.mu held.
|
||||
func (l *listenerWrapper) instantiateFilterChainRoutingConfigurationsLocked() {
|
||||
for _, fc := range l.activeFilterChainManager.filterChains {
|
||||
if fc.inlineRouteConfig != nil {
|
||||
fc.updateUsableRouteConfiguration(fc.inlineRouteConfig, nil, l.getOrCreateServerFilterLocked, l.xdsNodeID)
|
||||
continue
|
||||
} // Inline configuration constructed once here, will remain for lifetime of filter chain.
|
||||
rcu := l.rdsHandler.updates[fc.routeConfigName]
|
||||
if rcu.err != nil && rcu.data == nil {
|
||||
fc.updateUsableRouteConfiguration(nil, rcu.err, l.getOrCreateServerFilterLocked, l.xdsNodeID)
|
||||
continue
|
||||
}
|
||||
fc.updateUsableRouteConfiguration(rcu.data, nil, l.getOrCreateServerFilterLocked, l.xdsNodeID)
|
||||
}
|
||||
}
|
||||
|
||||
// Accept blocks on an Accept() on the underlying listener, and wraps the
|
||||
// returned net.connWrapper with the configured certificate providers.
|
||||
func (l *listenerWrapper) Accept() (net.Conn, error) {
|
||||
var retries int
|
||||
for {
|
||||
conn, err := l.Listener.Accept()
|
||||
if err != nil {
|
||||
// Temporary() method is implemented by certain error types returned
|
||||
// from the net package, and it is useful for us to not shutdown the
|
||||
// server in these conditions. The listen queue being full is one
|
||||
// such case.
|
||||
if ne, ok := err.(interface{ Temporary() bool }); !ok || !ne.Temporary() {
|
||||
return nil, err
|
||||
}
|
||||
retries++
|
||||
timer := time.NewTimer(backoffFunc(retries))
|
||||
select {
|
||||
case <-timer.C:
|
||||
case <-l.closed.Done():
|
||||
timer.Stop()
|
||||
// Continuing here will cause us to call Accept() again
|
||||
// which will return a non-temporary error.
|
||||
continue
|
||||
}
|
||||
continue
|
||||
}
|
||||
// Reset retries after a successful Accept().
|
||||
retries = 0
|
||||
|
||||
// Since the net.Conn represents an incoming connection, the source and
|
||||
// destination address can be retrieved from the local address and
|
||||
// remote address of the net.Conn respectively.
|
||||
destAddr, ok1 := conn.LocalAddr().(*net.TCPAddr)
|
||||
srcAddr, ok2 := conn.RemoteAddr().(*net.TCPAddr)
|
||||
if !ok1 || !ok2 {
|
||||
// If the incoming connection is not a TCP connection, which is
|
||||
// really unexpected since we check whether the provided listener is
|
||||
// a TCP listener in Serve(), we return an error which would cause
|
||||
// us to stop serving.
|
||||
return nil, fmt.Errorf("received connection with non-TCP address (local: %T, remote %T)", conn.LocalAddr(), conn.RemoteAddr())
|
||||
}
|
||||
|
||||
l.mu.Lock()
|
||||
if l.mode == connectivity.ServingModeNotServing {
|
||||
// Close connections as soon as we accept them when we are in
|
||||
// "not-serving" mode. Since we accept a net.Listener from the user
|
||||
// in Serve(), we cannot close the listener when we move to
|
||||
// "not-serving". Closing the connection immediately upon accepting
|
||||
// is one of the other ways to implement the "not-serving" mode as
|
||||
// outlined in gRFC A36.
|
||||
l.mu.Unlock()
|
||||
conn.Close()
|
||||
continue
|
||||
}
|
||||
|
||||
destIP, _ := netip.AddrFromSlice(destAddr.IP)
|
||||
srcIP, _ := netip.AddrFromSlice(srcAddr.IP)
|
||||
fc, err := l.activeFilterChainManager.lookup(lookupParams{
|
||||
isUnspecifiedListener: l.isUnspecifiedAddr,
|
||||
dstAddr: destIP.Unmap(),
|
||||
srcAddr: srcIP.Unmap(),
|
||||
srcPort: srcAddr.Port,
|
||||
})
|
||||
if err != nil {
|
||||
l.mu.Unlock()
|
||||
// When a matching filter chain is not found, we close the
|
||||
// connection right away, but do not return an error back to
|
||||
// `grpc.Serve()` from where this Accept() was invoked. Returning an
|
||||
// error to `grpc.Serve()` causes the server to shutdown. If we want
|
||||
// to avoid the server from shutting down, we would need to return
|
||||
// an error type which implements the `Temporary() bool` method,
|
||||
// which is invoked by `grpc.Serve()` to see if the returned error
|
||||
// represents a temporary condition. In the case of a temporary
|
||||
// error, `grpc.Serve()` method sleeps for a small duration and
|
||||
// therefore ends up blocking all connection attempts during that
|
||||
// time frame, which is also not ideal for an error like this.
|
||||
l.logger.Warningf("Connection from %s to %s failed to find any matching filter chain", conn.RemoteAddr(), conn.LocalAddr())
|
||||
conn.Close()
|
||||
continue
|
||||
}
|
||||
cw := &connWrapper{Conn: conn, filterChain: fc, parent: l, urc: fc.usableRouteConfiguration}
|
||||
l.conns[cw] = true
|
||||
l.mu.Unlock()
|
||||
return cw, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (l *listenerWrapper) removeConn(conn *connWrapper) {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
delete(l.conns, conn)
|
||||
}
|
||||
|
||||
// Close closes the underlying listener. It also cancels the xDS watch
|
||||
// registered in Serve() and closes any certificate provider instances created
|
||||
// based on security configuration received in the LDS response.
|
||||
func (l *listenerWrapper) Close() error {
|
||||
l.closed.Fire()
|
||||
l.Listener.Close()
|
||||
if l.cancelWatch != nil {
|
||||
l.cancelWatch()
|
||||
}
|
||||
l.rdsHandler.close()
|
||||
l.mu.Lock()
|
||||
if l.activeFilterChainManager != nil {
|
||||
l.activeFilterChainManager.stop()
|
||||
}
|
||||
if l.pendingFilterChainManager != nil {
|
||||
l.pendingFilterChainManager.stop()
|
||||
}
|
||||
l.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
// switchModeLocked switches the current mode of the listener wrapper. It also
|
||||
// gracefully closes any connections if the listener wrapper transitions into
|
||||
// not serving. If the serving mode has changed, it invokes the registered mode
|
||||
// change callback.
|
||||
func (l *listenerWrapper) switchModeLocked(newMode connectivity.ServingMode, err error) {
|
||||
if l.mode == newMode && l.mode == connectivity.ServingModeServing {
|
||||
// Redundant updates are suppressed only when we are SERVING and the new
|
||||
// mode is also SERVING. In the other case (where we are NOT_SERVING and the
|
||||
// new mode is also NOT_SERVING), the update is not suppressed as:
|
||||
// 1. the error may have change
|
||||
// 2. it provides a timestamp of the last backoff attempt
|
||||
return
|
||||
}
|
||||
l.mode = newMode
|
||||
if l.mode == connectivity.ServingModeNotServing {
|
||||
connsToClose := l.conns
|
||||
l.conns = make(map[*connWrapper]bool)
|
||||
go func() {
|
||||
for conn := range connsToClose {
|
||||
conn.Drain()
|
||||
}
|
||||
}()
|
||||
}
|
||||
// The XdsServer API will allow applications to register a "serving state"
|
||||
// callback to be invoked when the server begins serving and when the
|
||||
// server encounters errors that force it to be "not serving". If "not
|
||||
// serving", the callback must be provided error information, for
|
||||
// debugging use by developers - A36.
|
||||
if l.modeCallback != nil {
|
||||
l.modeCallback(l.Listener.Addr(), newMode, err)
|
||||
}
|
||||
}
|
||||
|
||||
// getOrCreateServerFilterLocked retrieves an existing server filter from the
|
||||
// httpFilters map or creates a new one if it doesn't exist. It uses the filter
|
||||
// builder to create a new server filter and stores it in the httpFilters map
|
||||
// for future use.
|
||||
//
|
||||
// Must be called with l.mu held.
|
||||
func (l *listenerWrapper) getOrCreateServerFilterLocked(filter xdsresource.HTTPFilter) (httpfilter.ServerFilter, error) {
|
||||
builder, ok := filter.Filter.(httpfilter.ServerFilterBuilder)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("filter %q does not support use in server", filter.Name)
|
||||
}
|
||||
return getOrCreateServerFilterWithMap(l.httpFilters, builder, newServerFilterKey(&filter)), nil
|
||||
}
|
||||
|
||||
// This functionality is put in a separate function to allow for testing with a
|
||||
// custom map.
|
||||
func getOrCreateServerFilterWithMap(httpFilters map[serverFilterKey]*refCountedServerFilter, builder httpfilter.ServerFilterBuilder, key serverFilterKey) httpfilter.ServerFilter {
|
||||
serverFilter, ok := httpFilters[key]
|
||||
if ok {
|
||||
serverFilter.incRef()
|
||||
return serverFilter
|
||||
}
|
||||
|
||||
sf := builder.BuildServerFilter()
|
||||
serverFilter = &refCountedServerFilter{ServerFilter: sf}
|
||||
httpFilters[key] = serverFilter
|
||||
serverFilter.incRef()
|
||||
return serverFilter
|
||||
}
|
||||
|
||||
// ldsWatcher implements the xdsresource.ListenerWatcher interface and is
|
||||
// passed to the WatchListener API.
|
||||
type ldsWatcher struct {
|
||||
parent *listenerWrapper
|
||||
logger *internalgrpclog.PrefixLogger
|
||||
name string
|
||||
}
|
||||
|
||||
func (lw *ldsWatcher) ResourceChanged(update *xdsresource.ListenerUpdate, onDone func()) {
|
||||
defer onDone()
|
||||
if lw.parent.closed.HasFired() {
|
||||
lw.logger.Warningf("Resource %q received update: %#v after listener was closed", lw.name, update)
|
||||
return
|
||||
}
|
||||
if lw.logger.V(2) {
|
||||
lw.logger.Infof("LDS watch for resource %q received update: %#v", lw.name, update)
|
||||
}
|
||||
l := lw.parent
|
||||
ilc := update.TCPListener
|
||||
|
||||
// A server-side listener update must contain inbound listener configuration
|
||||
// (TCPListener). If it is nil, it indicates that a client-side listener (API
|
||||
// Listener) resource was received instead. In this case, we transition the
|
||||
// server to non-serving mode.
|
||||
//
|
||||
// Also, make sure that the socket address on the received Listener resource
|
||||
// matches the address of the net.Listener passed to us by the user. This
|
||||
// check is done here instead of at the XDSClient layer because of the
|
||||
// following couple of reasons:
|
||||
// - XDSClient cannot know the listening address of every listener in the
|
||||
// system, and hence cannot perform this check.
|
||||
// - this is a very context-dependent check and only the server has the
|
||||
// appropriate context to perform this check.
|
||||
//
|
||||
// What this means is that the XDSClient has ACKed a resource which can push
|
||||
// the server into a "not serving" mode. This is not ideal, but this is
|
||||
// what we have decided to do.
|
||||
//
|
||||
// A port value of 0 in the xDS Listener matches any listening port, but the
|
||||
// listener address itself must still match exactly.
|
||||
if ilc == nil || ilc.Address != l.addr || (ilc.Port != "0" && ilc.Port != l.port) {
|
||||
// TODO(purnesh42h): Are there any other cases where this can be
|
||||
// treated as an ambient error?
|
||||
l.mu.Lock()
|
||||
var err error
|
||||
if ilc == nil {
|
||||
err = fmt.Errorf("[xDS node id: %v]: %w", l.xdsNodeID, fmt.Errorf("received client-side listener resource %q on server-side", lw.name))
|
||||
} else {
|
||||
err = fmt.Errorf("[xDS node id: %v]: %w", l.xdsNodeID, fmt.Errorf("address (%s:%s) in Listener update does not match listening address: (%s:%s)", ilc.Address, ilc.Port, l.addr, l.port))
|
||||
}
|
||||
l.switchModeLocked(connectivity.ServingModeNotServing, err)
|
||||
l.mu.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
fcm := newFilterChainManager(&ilc.FilterChains, &ilc.DefaultFilterChain)
|
||||
l.pendingFilterChainManager = fcm
|
||||
l.rdsHandler.updateRouteNamesToWatch(fcm.routeConfigNames)
|
||||
|
||||
if l.rdsHandler.determineRouteConfigurationReady() {
|
||||
l.maybeUpdateFilterChains()
|
||||
}
|
||||
}
|
||||
|
||||
func (lw *ldsWatcher) ResourceError(err error, onDone func()) {
|
||||
defer onDone()
|
||||
if lw.parent.closed.HasFired() {
|
||||
lw.logger.Warningf("Resource %q received resource error: %v after listener was closed", lw.name, err)
|
||||
return
|
||||
}
|
||||
if lw.logger.V(2) {
|
||||
lw.logger.Infof("LDS watch for resource %q reported resource error: %v", lw.name, err)
|
||||
}
|
||||
|
||||
l := lw.parent
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
l.switchModeLocked(connectivity.ServingModeNotServing, err)
|
||||
if l.activeFilterChainManager != nil {
|
||||
l.activeFilterChainManager.stop()
|
||||
l.activeFilterChainManager = nil
|
||||
}
|
||||
if l.pendingFilterChainManager != nil {
|
||||
l.pendingFilterChainManager.stop()
|
||||
l.pendingFilterChainManager = nil
|
||||
}
|
||||
l.rdsHandler.updateRouteNamesToWatch(make(map[string]bool))
|
||||
}
|
||||
|
||||
func (lw *ldsWatcher) AmbientError(err error, onDone func()) {
|
||||
defer onDone()
|
||||
if lw.parent.closed.HasFired() {
|
||||
lw.logger.Warningf("Resource %q received ambient error: %v after listener was closed", lw.name, err)
|
||||
return
|
||||
}
|
||||
if lw.logger.V(2) {
|
||||
lw.logger.Infof("LDS watch for resource %q reported ambient error: %v", lw.name, err)
|
||||
}
|
||||
}
|
||||
148
internal/xds/server/listener_wrapper_test.go
Normal file
148
internal/xds/server/listener_wrapper_test.go
Normal file
@@ -0,0 +1,148 @@
|
||||
/*
|
||||
*
|
||||
* 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 server
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
)
|
||||
|
||||
// Test verifies that an error received from RDS before any valid update is
|
||||
// received is correctly propagated to the filter chains.
|
||||
func (s) TestHandleRDSUpdate_ErrorBeforeUpdate(t *testing.T) {
|
||||
const routeName = "route-name"
|
||||
nodeID := "node-id"
|
||||
rdsErr := errors.New("rds error")
|
||||
|
||||
// Create a filter chain that points to routeName.
|
||||
fc := &filterChain{
|
||||
routeConfigName: routeName,
|
||||
usableRouteConfiguration: &atomic.Pointer[usableRouteConfiguration]{},
|
||||
}
|
||||
fc.usableRouteConfiguration.Store(&usableRouteConfiguration{}) // Initial state is an empty configuration with no error.
|
||||
|
||||
// Create a listener wrapper with the above filter chain. This simulates the
|
||||
// state of the server after LDS has been processed, but before RDS has
|
||||
// returned any response for the route configuration.
|
||||
lw := &listenerWrapper{
|
||||
xdsNodeID: nodeID,
|
||||
activeFilterChainManager: &filterChainManager{
|
||||
filterChains: []*filterChain{fc},
|
||||
},
|
||||
rdsHandler: &rdsHandler{
|
||||
updates: make(map[string]rdsWatcherUpdate),
|
||||
cancels: make(map[string]func()),
|
||||
},
|
||||
}
|
||||
|
||||
// Simulate an RDS update with an error for the route configuration.
|
||||
lw.handleRDSUpdate(routeName, rdsWatcherUpdate{err: rdsErr})
|
||||
|
||||
// Verify that the error is propagated to the filter chain.
|
||||
gotURC := fc.usableRouteConfiguration.Load()
|
||||
wantURC := &usableRouteConfiguration{err: rdsErr, nodeID: nodeID}
|
||||
if diff := cmp.Diff(gotURC, wantURC, cmp.AllowUnexported(usableRouteConfiguration{}), cmp.Comparer(func(x, y error) bool {
|
||||
if x == nil || y == nil {
|
||||
return x == y
|
||||
}
|
||||
return x.Error() == y.Error()
|
||||
})); diff != "" {
|
||||
t.Fatalf("Unexpected usableRouteConfiguration (-got, +want):\n%s", diff)
|
||||
}
|
||||
}
|
||||
|
||||
// Test verifies that a successful RDS update correctly populates the nodeID.
|
||||
func (s) TestHandleRDSUpdate_Success(t *testing.T) {
|
||||
const routeName = "route-name"
|
||||
nodeID := "node-id"
|
||||
|
||||
// Create a filter chain that points to routeName.
|
||||
fc := &filterChain{
|
||||
routeConfigName: routeName,
|
||||
usableRouteConfiguration: &atomic.Pointer[usableRouteConfiguration]{},
|
||||
}
|
||||
fc.usableRouteConfiguration.Store(&usableRouteConfiguration{})
|
||||
|
||||
lw := &listenerWrapper{
|
||||
xdsNodeID: nodeID,
|
||||
activeFilterChainManager: &filterChainManager{
|
||||
filterChains: []*filterChain{fc},
|
||||
},
|
||||
rdsHandler: &rdsHandler{
|
||||
updates: make(map[string]rdsWatcherUpdate),
|
||||
cancels: make(map[string]func()),
|
||||
},
|
||||
}
|
||||
|
||||
// Simulate an RDS update with a valid route configuration for the routeName.
|
||||
lw.handleRDSUpdate(routeName, rdsWatcherUpdate{data: inlineRouteConfig})
|
||||
|
||||
// Verify that the nodeID is populated in the resulting configuration.
|
||||
gotURC := fc.usableRouteConfiguration.Load()
|
||||
if gotURC.nodeID != nodeID {
|
||||
t.Fatalf("gotURC.nodeID = %q, want %q", gotURC.nodeID, nodeID)
|
||||
}
|
||||
}
|
||||
|
||||
// Test verifies that an error received from RDS *after* a valid update is
|
||||
// received is correctly propagated, and does not result in the filter chain
|
||||
// keeping the "stale" successful configuration.
|
||||
func (s) TestHandleRDSUpdate_ErrorAfterSuccess(t *testing.T) {
|
||||
const routeName = "route-name"
|
||||
nodeID := "node-id"
|
||||
rdsErr := errors.New("subsequent rds error")
|
||||
|
||||
fc := &filterChain{
|
||||
routeConfigName: routeName,
|
||||
usableRouteConfiguration: &atomic.Pointer[usableRouteConfiguration]{},
|
||||
}
|
||||
fc.usableRouteConfiguration.Store(&usableRouteConfiguration{})
|
||||
|
||||
// Create a listener wrapper with the above filter chain. This simulates the
|
||||
// state of the server after LDS has been processed, but before RDS has
|
||||
// returned any response for the route configuration.
|
||||
lw := &listenerWrapper{
|
||||
xdsNodeID: nodeID,
|
||||
activeFilterChainManager: &filterChainManager{
|
||||
filterChains: []*filterChain{fc},
|
||||
},
|
||||
rdsHandler: &rdsHandler{
|
||||
updates: make(map[string]rdsWatcherUpdate),
|
||||
cancels: make(map[string]func()),
|
||||
},
|
||||
}
|
||||
|
||||
// Simulate an RDS update with a valid route configuration for the routeName.
|
||||
lw.handleRDSUpdate(routeName, rdsWatcherUpdate{data: inlineRouteConfig})
|
||||
if err := fc.usableRouteConfiguration.Load().err; err != nil {
|
||||
t.Fatalf("Initial update failed: %v", err)
|
||||
}
|
||||
|
||||
// Simulate an RDS update with an error for the route configuration.
|
||||
lw.handleRDSUpdate(routeName, rdsWatcherUpdate{err: rdsErr})
|
||||
|
||||
// Verify that the error is propagated.
|
||||
gotURC := fc.usableRouteConfiguration.Load()
|
||||
if !errors.Is(gotURC.err, rdsErr) {
|
||||
t.Fatalf("urc.err = %v, want %v", gotURC.err, rdsErr)
|
||||
}
|
||||
}
|
||||
188
internal/xds/server/rds_handler.go
Normal file
188
internal/xds/server/rds_handler.go
Normal file
@@ -0,0 +1,188 @@
|
||||
/*
|
||||
*
|
||||
* Copyright 2021 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 server
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
igrpclog "google.golang.org/grpc/internal/grpclog"
|
||||
"google.golang.org/grpc/internal/xds/xdsclient/xdsresource"
|
||||
)
|
||||
|
||||
// rdsHandler handles any RDS queries that need to be started for a given server
|
||||
// side listeners Filter Chains (i.e. not inline). It persists rdsWatcher
|
||||
// updates for later use and also determines whether all the rdsWatcher updates
|
||||
// needed have been received or not.
|
||||
type rdsHandler struct {
|
||||
xdsC XDSClient
|
||||
xdsNodeID string
|
||||
logger *igrpclog.PrefixLogger
|
||||
|
||||
callback func(string, rdsWatcherUpdate)
|
||||
|
||||
// updates is a map from routeName to rdsWatcher update, including
|
||||
// RouteConfiguration resources and any errors received. If not written in
|
||||
// this map, no RouteConfiguration or error for that route name yet. If
|
||||
// update set in value, use that as valid route configuration, otherwise
|
||||
// treat as an error case and fail at L7 level.
|
||||
updates map[string]rdsWatcherUpdate
|
||||
|
||||
mu sync.Mutex
|
||||
cancels map[string]func()
|
||||
}
|
||||
|
||||
// newRDSHandler creates a new rdsHandler to watch for RouteConfiguration
|
||||
// resources. listenerWrapper updates the list of route names to watch by
|
||||
// calling updateRouteNamesToWatch() upon receipt of new Listener configuration.
|
||||
func newRDSHandler(cb func(string, rdsWatcherUpdate), xdsC XDSClient, logger *igrpclog.PrefixLogger) *rdsHandler {
|
||||
r := &rdsHandler{
|
||||
xdsC: xdsC,
|
||||
logger: logger,
|
||||
callback: cb,
|
||||
updates: make(map[string]rdsWatcherUpdate),
|
||||
cancels: make(map[string]func()),
|
||||
}
|
||||
r.xdsNodeID = xdsC.BootstrapConfig().Node().GetId()
|
||||
return r
|
||||
}
|
||||
|
||||
// updateRouteNamesToWatch handles a list of route names to watch for a given
|
||||
// server side listener (if a filter chain specifies dynamic
|
||||
// RouteConfiguration). This function handles all the logic with respect to any
|
||||
// routes that may have been added or deleted as compared to what was previously
|
||||
// present. Must be called within an xDS Client callback.
|
||||
func (rh *rdsHandler) updateRouteNamesToWatch(routeNamesToWatch map[string]bool) {
|
||||
rh.mu.Lock()
|
||||
defer rh.mu.Unlock()
|
||||
// Add and start watches for any new routes in routeNamesToWatch.
|
||||
for routeName := range routeNamesToWatch {
|
||||
if _, ok := rh.cancels[routeName]; !ok {
|
||||
// The xDS client keeps a reference to the watcher until the cancel
|
||||
// func is invoked. So, we don't need to keep a reference for fear
|
||||
// of it being garbage collected.
|
||||
w := &rdsWatcher{parent: rh, routeName: routeName}
|
||||
cancel := xdsresource.WatchRouteConfig(rh.xdsC, routeName, w)
|
||||
// Set bit on cancel function to eat any RouteConfiguration calls
|
||||
// for this watcher after it has been canceled.
|
||||
rh.cancels[routeName] = func() {
|
||||
w.mu.Lock()
|
||||
w.canceled = true
|
||||
w.mu.Unlock()
|
||||
cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Delete and cancel watches for any routes from persisted routeNamesToWatch
|
||||
// that are no longer present.
|
||||
for routeName := range rh.cancels {
|
||||
if _, ok := routeNamesToWatch[routeName]; !ok {
|
||||
rh.cancels[routeName]()
|
||||
delete(rh.cancels, routeName)
|
||||
delete(rh.updates, routeName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// determines if all dynamic RouteConfiguration needed has received
|
||||
// configuration or update. Must be called from an xDS Client Callback.
|
||||
func (rh *rdsHandler) determineRouteConfigurationReady() bool {
|
||||
// Safe to read cancels because only written to in other parts of xDS Client
|
||||
// Callbacks, which are sync.
|
||||
return len(rh.updates) == len(rh.cancels)
|
||||
}
|
||||
|
||||
// close() is meant to be called by wrapped listener when the wrapped listener
|
||||
// is closed, and it cleans up resources by canceling all the active RDS
|
||||
// watches.
|
||||
func (rh *rdsHandler) close() {
|
||||
rh.mu.Lock()
|
||||
defer rh.mu.Unlock()
|
||||
for _, cancel := range rh.cancels {
|
||||
cancel()
|
||||
}
|
||||
}
|
||||
|
||||
type rdsWatcherUpdate struct {
|
||||
data *xdsresource.RouteConfigUpdate
|
||||
err error
|
||||
}
|
||||
|
||||
// rdsWatcher implements the xdsresource.RouteConfigWatcher interface and is
|
||||
// passed to the WatchRouteConfig API.
|
||||
type rdsWatcher struct {
|
||||
parent *rdsHandler
|
||||
logger *igrpclog.PrefixLogger
|
||||
routeName string
|
||||
|
||||
mu sync.Mutex
|
||||
canceled bool // eats callbacks if true
|
||||
}
|
||||
|
||||
func (rw *rdsWatcher) ResourceChanged(update *xdsresource.RouteConfigUpdate, onDone func()) {
|
||||
defer onDone()
|
||||
rw.mu.Lock()
|
||||
if rw.canceled {
|
||||
rw.mu.Unlock()
|
||||
return
|
||||
}
|
||||
rw.mu.Unlock()
|
||||
if rw.logger.V(2) {
|
||||
rw.logger.Infof("RDS watch for resource %q received update: %#v", rw.routeName, update)
|
||||
}
|
||||
|
||||
routeName := rw.routeName
|
||||
rwu := rdsWatcherUpdate{data: update}
|
||||
rw.parent.updates[routeName] = rwu
|
||||
rw.parent.callback(routeName, rwu)
|
||||
}
|
||||
|
||||
func (rw *rdsWatcher) ResourceError(err error, onDone func()) {
|
||||
defer onDone()
|
||||
rw.mu.Lock()
|
||||
if rw.canceled {
|
||||
rw.mu.Unlock()
|
||||
return
|
||||
}
|
||||
rw.mu.Unlock()
|
||||
if rw.logger.V(2) {
|
||||
rw.logger.Infof("RDS watch for resource %q reported resource error", rw.routeName)
|
||||
}
|
||||
|
||||
routeName := rw.routeName
|
||||
rwu := rdsWatcherUpdate{err: err}
|
||||
rw.parent.updates[routeName] = rwu
|
||||
rw.parent.callback(routeName, rwu)
|
||||
}
|
||||
|
||||
func (rw *rdsWatcher) AmbientError(err error, onDone func()) {
|
||||
defer onDone()
|
||||
rw.mu.Lock()
|
||||
if rw.canceled {
|
||||
rw.mu.Unlock()
|
||||
return
|
||||
}
|
||||
rw.mu.Unlock()
|
||||
if rw.logger.V(2) {
|
||||
rw.logger.Infof("RDS watch for resource %q reported ambient error: %v", rw.routeName, err)
|
||||
}
|
||||
routeName := rw.routeName
|
||||
rwu := rw.parent.updates[routeName]
|
||||
rw.parent.callback(routeName, rwu)
|
||||
}
|
||||
272
internal/xds/server/rds_handler_test.go
Normal file
272
internal/xds/server/rds_handler_test.go
Normal file
@@ -0,0 +1,272 @@
|
||||
/*
|
||||
*
|
||||
* Copyright 2021 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 server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/google/go-cmp/cmp/cmpopts"
|
||||
"github.com/google/uuid"
|
||||
"google.golang.org/grpc/internal/grpctest"
|
||||
"google.golang.org/grpc/internal/testutils/xds/e2e"
|
||||
"google.golang.org/grpc/internal/xds/bootstrap"
|
||||
"google.golang.org/grpc/internal/xds/xdsclient"
|
||||
"google.golang.org/grpc/internal/xds/xdsclient/xdsresource/version"
|
||||
|
||||
v3routepb "github.com/envoyproxy/go-control-plane/envoy/config/route/v3"
|
||||
v3discoverypb "github.com/envoyproxy/go-control-plane/envoy/service/discovery/v3"
|
||||
)
|
||||
|
||||
type s struct {
|
||||
grpctest.Tester
|
||||
}
|
||||
|
||||
func Test(t *testing.T) {
|
||||
grpctest.RunSubTests(t, s{})
|
||||
}
|
||||
|
||||
const (
|
||||
defaultTestTimeout = 10 * time.Second
|
||||
defaultTestShortTimeout = 10 * time.Millisecond // For events expected to *not* happen.
|
||||
)
|
||||
|
||||
const (
|
||||
listenerName = "listener"
|
||||
clusterName = "cluster"
|
||||
|
||||
route1 = "route1"
|
||||
route2 = "route2"
|
||||
route3 = "route3"
|
||||
route4 = "route4"
|
||||
)
|
||||
|
||||
// xdsSetupForTests performs the following setup actions:
|
||||
// - spins up an xDS management server
|
||||
// - creates an xDS client with a bootstrap configuration pointing to the above
|
||||
// management server
|
||||
//
|
||||
// Returns the following:
|
||||
// - a reference to the management server
|
||||
// - nodeID to use when pushing resources to the management server
|
||||
// - a channel to read lds resource names received by the management server
|
||||
// - a channel to read rds resource names received by the management server
|
||||
// - an xDS client to pass to the rdsHandler under test
|
||||
func xdsSetupForTests(t *testing.T) (*e2e.ManagementServer, string, chan []string, chan []string, xdsclient.XDSClient) {
|
||||
t.Helper()
|
||||
|
||||
ldsNamesCh := make(chan []string, 1)
|
||||
rdsNamesCh := make(chan []string, 1)
|
||||
|
||||
// Setup the management server to push the requested route configuration
|
||||
// resource names on to a channel for the test to inspect.
|
||||
mgmtServer := e2e.StartManagementServer(t, e2e.ManagementServerOptions{
|
||||
OnStreamRequest: func(_ int64, req *v3discoverypb.DiscoveryRequest) error {
|
||||
switch req.GetTypeUrl() {
|
||||
case version.V3ListenerURL: // Waits on the listener, and route config below...
|
||||
select {
|
||||
case <-ldsNamesCh:
|
||||
default:
|
||||
}
|
||||
select {
|
||||
case ldsNamesCh <- req.GetResourceNames():
|
||||
default:
|
||||
}
|
||||
case version.V3RouteConfigURL: // waits on route config names here...
|
||||
select {
|
||||
case <-rdsNamesCh:
|
||||
default:
|
||||
}
|
||||
select {
|
||||
case rdsNamesCh <- req.GetResourceNames():
|
||||
default:
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("unexpected resources %v of type %q requested", req.GetResourceNames(), req.GetTypeUrl())
|
||||
}
|
||||
return nil
|
||||
},
|
||||
AllowResourceSubset: true,
|
||||
})
|
||||
|
||||
// Create bootstrap configuration pointing to the above management server.
|
||||
nodeID := uuid.New().String()
|
||||
bootstrapContents := e2e.DefaultBootstrapContents(t, nodeID, mgmtServer.Address)
|
||||
|
||||
config, err := bootstrap.NewConfigFromContents(bootstrapContents)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to parse bootstrap contents: %s, %v", string(bootstrapContents), err)
|
||||
}
|
||||
pool := xdsclient.NewPool(config)
|
||||
xdsC, cancel, err := pool.NewClientForTesting(xdsclient.OptionsForTesting{
|
||||
Name: t.Name(),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(cancel)
|
||||
|
||||
return mgmtServer, nodeID, ldsNamesCh, rdsNamesCh, xdsC
|
||||
}
|
||||
|
||||
// Waits for the wantNames to be pushed on to namesCh. Fails the test by calling
|
||||
// t.Fatal if the context expires before that.
|
||||
func waitForResourceNames(ctx context.Context, t *testing.T, namesCh chan []string, wantNames []string) {
|
||||
t.Helper()
|
||||
|
||||
for ; ctx.Err() == nil; <-time.After(defaultTestShortTimeout) {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
case gotNames := <-namesCh:
|
||||
if cmp.Equal(gotNames, wantNames, cmpopts.EquateEmpty(), cmpopts.SortSlices(func(s1, s2 string) bool { return s1 < s2 })) {
|
||||
return
|
||||
}
|
||||
t.Logf("Received resource names %v, want %v", gotNames, wantNames)
|
||||
}
|
||||
}
|
||||
t.Fatalf("Timeout waiting for resource to be requested from the management server")
|
||||
}
|
||||
|
||||
func routeConfigResourceForName(name string) *v3routepb.RouteConfiguration {
|
||||
return e2e.RouteConfigResourceWithOptions(e2e.RouteConfigOptions{
|
||||
RouteConfigName: name,
|
||||
ListenerName: listenerName,
|
||||
ClusterSpecifierType: e2e.RouteConfigClusterSpecifierTypeCluster,
|
||||
ClusterName: clusterName,
|
||||
})
|
||||
}
|
||||
|
||||
type testCallbackVerify struct {
|
||||
ch chan callbackStruct
|
||||
}
|
||||
|
||||
type callbackStruct struct {
|
||||
routeName string
|
||||
rwu rdsWatcherUpdate
|
||||
}
|
||||
|
||||
func (tcv *testCallbackVerify) testCallback(routeName string, rwu rdsWatcherUpdate) {
|
||||
tcv.ch <- callbackStruct{routeName: routeName, rwu: rwu}
|
||||
}
|
||||
|
||||
func verifyRouteName(ctx context.Context, t *testing.T, ch chan callbackStruct, want callbackStruct) {
|
||||
t.Helper()
|
||||
select {
|
||||
case got := <-ch:
|
||||
if diff := cmp.Diff(got.routeName, want.routeName); diff != "" {
|
||||
t.Fatalf("unexpected update received (-got, +want):%v, want: %v", got, want)
|
||||
}
|
||||
case <-ctx.Done():
|
||||
t.Fatalf("timeout waiting for callback")
|
||||
}
|
||||
}
|
||||
|
||||
// TestRDSHandler tests the RDS Handler. It first configures the rds handler to
|
||||
// watch route 1 and 2. Before receiving both RDS updates, it should not be
|
||||
// ready, but after receiving both, it should be ready. It then tells the rds
|
||||
// handler to watch route 1 2 and 3. It should not be ready until it receives
|
||||
// route3 from the management server. It then configures the rds handler to
|
||||
// watch route 1 and 3. It should immediately be ready. It then configures the
|
||||
// rds handler to watch route 1 and 4. It should not be ready until it receives
|
||||
// an rds update for route 4.
|
||||
func (s) TestRDSHandler(t *testing.T) {
|
||||
mgmtServer, nodeID, _, rdsNamesCh, xdsC := xdsSetupForTests(t)
|
||||
|
||||
ch := make(chan callbackStruct, 1)
|
||||
tcv := &testCallbackVerify{ch: ch}
|
||||
rh := newRDSHandler(tcv.testCallback, xdsC, nil)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
|
||||
defer cancel()
|
||||
|
||||
// Configure the management server with a single route config resource.
|
||||
routeResource1 := routeConfigResourceForName(route1)
|
||||
resources := e2e.UpdateOptions{
|
||||
NodeID: nodeID,
|
||||
Routes: []*v3routepb.RouteConfiguration{routeResource1},
|
||||
SkipValidation: true,
|
||||
}
|
||||
if err := mgmtServer.Update(ctx, resources); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
rh.updateRouteNamesToWatch(map[string]bool{route1: true, route2: true})
|
||||
waitForResourceNames(ctx, t, rdsNamesCh, []string{route1, route2})
|
||||
verifyRouteName(ctx, t, ch, callbackStruct{routeName: route1})
|
||||
|
||||
// The rds handler update should not be ready.
|
||||
if got := rh.determineRouteConfigurationReady(); got != false {
|
||||
t.Fatalf("rh.determineRouteConfigurationReady: %v, want: false", false)
|
||||
}
|
||||
|
||||
// Configure the management server both route config resources.
|
||||
routeResource2 := routeConfigResourceForName(route2)
|
||||
resources.Routes = []*v3routepb.RouteConfiguration{routeResource1, routeResource2}
|
||||
if err := mgmtServer.Update(ctx, resources); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
verifyRouteName(ctx, t, ch, callbackStruct{routeName: route2})
|
||||
|
||||
if got := rh.determineRouteConfigurationReady(); got != true {
|
||||
t.Fatalf("rh.determineRouteConfigurationReady: %v, want: true", got)
|
||||
}
|
||||
|
||||
rh.updateRouteNamesToWatch(map[string]bool{route1: true, route2: true, route3: true})
|
||||
waitForResourceNames(ctx, t, rdsNamesCh, []string{route1, route2, route3})
|
||||
if got := rh.determineRouteConfigurationReady(); got != false {
|
||||
t.Fatalf("rh.determineRouteConfigurationReady: %v, want: false", got)
|
||||
}
|
||||
|
||||
// Configure the management server with route config resources.
|
||||
routeResource3 := routeConfigResourceForName(route3)
|
||||
resources.Routes = []*v3routepb.RouteConfiguration{routeResource1, routeResource2, routeResource3}
|
||||
if err := mgmtServer.Update(ctx, resources); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
verifyRouteName(ctx, t, ch, callbackStruct{routeName: route3})
|
||||
|
||||
if got := rh.determineRouteConfigurationReady(); got != true {
|
||||
t.Fatalf("rh.determineRouteConfigurationReady: %v, want: true", got)
|
||||
}
|
||||
// Update to route 1 and route 2. Should immediately go ready.
|
||||
rh.updateRouteNamesToWatch(map[string]bool{route1: true, route3: true})
|
||||
if got := rh.determineRouteConfigurationReady(); got != true {
|
||||
t.Fatalf("rh.determineRouteConfigurationReady: %v, want: true", got)
|
||||
}
|
||||
|
||||
// Update to route 1 and route 4. No route 4, so should not be ready.
|
||||
rh.updateRouteNamesToWatch(map[string]bool{route1: true, route4: true})
|
||||
waitForResourceNames(ctx, t, rdsNamesCh, []string{route1, route4})
|
||||
if got := rh.determineRouteConfigurationReady(); got != false {
|
||||
t.Fatalf("rh.determineRouteConfigurationReady: %v, want: false", got)
|
||||
}
|
||||
routeResource4 := routeConfigResourceForName(route4)
|
||||
resources.Routes = []*v3routepb.RouteConfiguration{routeResource1, routeResource2, routeResource3, routeResource4}
|
||||
if err := mgmtServer.Update(ctx, resources); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
verifyRouteName(ctx, t, ch, callbackStruct{routeName: route4})
|
||||
if got := rh.determineRouteConfigurationReady(); got != true {
|
||||
t.Fatalf("rh.determineRouteConfigurationReady: %v, want: true", got)
|
||||
}
|
||||
}
|
||||
192
internal/xds/server/routing.go
Normal file
192
internal/xds/server/routing.go
Normal file
@@ -0,0 +1,192 @@
|
||||
/*
|
||||
*
|
||||
* 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 server
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/internal/transport"
|
||||
"google.golang.org/grpc/internal/xds/xdsclient/xdsresource"
|
||||
"google.golang.org/grpc/metadata"
|
||||
)
|
||||
|
||||
// RouteAndProcess routes the incoming RPC to a configured route in the route
|
||||
// table and also processes the RPC by running the incoming RPC through any HTTP
|
||||
// Filters configured.
|
||||
func RouteAndProcess(ss grpc.ServerStream) (grpc.ServerStream, error) {
|
||||
ctx := ss.Context()
|
||||
conn := transport.GetConnection(ctx)
|
||||
cw, ok := conn.(*connWrapper)
|
||||
if !ok {
|
||||
return nil, errors.New("missing virtual hosts in incoming context")
|
||||
}
|
||||
|
||||
rc := cw.urc.Load()
|
||||
// Error out at routing l7 level with a status code UNAVAILABLE, represents
|
||||
// an nack before usable route configuration or resource not found for RDS
|
||||
// or error combining LDS + RDS (Shouldn't happen).
|
||||
if rc.err != nil {
|
||||
if logger.V(2) {
|
||||
logger.Infof("RPC on connection with xDS Configuration error: %v", rc.err)
|
||||
}
|
||||
return nil, rc.statusErrWithNodeID(codes.Unavailable, "error from xDS configuration for matched route configuration: %v", rc.err)
|
||||
}
|
||||
|
||||
mn, ok := grpc.Method(ctx)
|
||||
if !ok {
|
||||
return nil, rc.statusErrWithNodeID(codes.Internal, "missing method name in incoming context")
|
||||
}
|
||||
md, ok := metadata.FromIncomingContext(ctx)
|
||||
if !ok {
|
||||
return nil, rc.statusErrWithNodeID(codes.Internal, "missing metadata in incoming context")
|
||||
}
|
||||
// A41 added logic to the core grpc implementation to guarantee that once the
|
||||
// RPC gets to this point, there will be a single, unambiguous authority
|
||||
// present in the header map. But add a defensive check to ensure authority
|
||||
// header is present.
|
||||
authority := md.Get(":authority")
|
||||
if len(authority) == 0 {
|
||||
return nil, rc.statusErrWithNodeID(codes.Internal, "no :authority header present")
|
||||
}
|
||||
vh := findBestMatchingVirtualHostServer(authority[0], rc.vhs)
|
||||
if vh == nil {
|
||||
return nil, rc.statusErrWithNodeID(codes.Unavailable, "the incoming RPC did not match a configured Virtual Host")
|
||||
}
|
||||
|
||||
var rwi *routeWithInterceptors
|
||||
for _, r := range vh.routes {
|
||||
if r.matcher.Match(mn, md) {
|
||||
// "NonForwardingAction is expected for all Routes used on
|
||||
// server-side; a route with an inappropriate action causes RPCs
|
||||
// matching that route to fail with UNAVAILABLE." - A36
|
||||
if r.actionType != xdsresource.RouteActionNonForwardingAction {
|
||||
return nil, rc.statusErrWithNodeID(codes.Unavailable, "the incoming RPC matched to a route that was not of action type non forwarding")
|
||||
}
|
||||
rwi = &r
|
||||
break
|
||||
}
|
||||
}
|
||||
if rwi == nil {
|
||||
return nil, rc.statusErrWithNodeID(codes.Unavailable, "the incoming RPC did not match a configured Route")
|
||||
}
|
||||
if rwi.interceptor != nil {
|
||||
return rwi.interceptor.InterceptRPC(ss)
|
||||
}
|
||||
return ss, nil
|
||||
}
|
||||
|
||||
// findBestMatchingVirtualHostServer returns the virtual host whose domains field best
|
||||
// matches host
|
||||
//
|
||||
// The domains field support 4 different matching pattern types:
|
||||
//
|
||||
// - Exact match
|
||||
// - Suffix match (e.g. “*ABC”)
|
||||
// - Prefix match (e.g. “ABC*)
|
||||
// - Universal match (e.g. “*”)
|
||||
//
|
||||
// The best match is defined as:
|
||||
// - A match is better if it’s matching pattern type is better.
|
||||
// * Exact match > suffix match > prefix match > universal match.
|
||||
//
|
||||
// - If two matches are of the same pattern type, the longer match is
|
||||
// better.
|
||||
// * This is to compare the length of the matching pattern, e.g. “*ABCDE” >
|
||||
// “*ABC”
|
||||
func findBestMatchingVirtualHostServer(authority string, vHosts []virtualHostWithInterceptors) *virtualHostWithInterceptors {
|
||||
var (
|
||||
matchVh *virtualHostWithInterceptors
|
||||
matchType = domainMatchTypeInvalid
|
||||
matchLen int
|
||||
)
|
||||
for _, vh := range vHosts {
|
||||
for _, domain := range vh.domains {
|
||||
typ, matched := match(domain, authority)
|
||||
if typ == domainMatchTypeInvalid {
|
||||
// The rds response is invalid.
|
||||
return nil
|
||||
}
|
||||
if matchType.betterThan(typ) || matchType == typ && matchLen >= len(domain) || !matched {
|
||||
// The previous match has better type, or the previous match has
|
||||
// better length, or this domain isn't a match.
|
||||
continue
|
||||
}
|
||||
matchVh = &vh
|
||||
matchType = typ
|
||||
matchLen = len(domain)
|
||||
}
|
||||
}
|
||||
return matchVh
|
||||
}
|
||||
|
||||
type domainMatchType int
|
||||
|
||||
const (
|
||||
domainMatchTypeInvalid domainMatchType = iota
|
||||
domainMatchTypeUniversal
|
||||
domainMatchTypePrefix
|
||||
domainMatchTypeSuffix
|
||||
domainMatchTypeExact
|
||||
)
|
||||
|
||||
// Exact > Suffix > Prefix > Universal > Invalid.
|
||||
func (t domainMatchType) betterThan(b domainMatchType) bool {
|
||||
return t > b
|
||||
}
|
||||
|
||||
func matchTypeForDomain(d string) domainMatchType {
|
||||
if d == "" {
|
||||
return domainMatchTypeInvalid
|
||||
}
|
||||
if d == "*" {
|
||||
return domainMatchTypeUniversal
|
||||
}
|
||||
if strings.HasPrefix(d, "*") {
|
||||
return domainMatchTypeSuffix
|
||||
}
|
||||
if strings.HasSuffix(d, "*") {
|
||||
return domainMatchTypePrefix
|
||||
}
|
||||
if strings.Contains(d, "*") {
|
||||
return domainMatchTypeInvalid
|
||||
}
|
||||
return domainMatchTypeExact
|
||||
}
|
||||
|
||||
func match(domain, host string) (domainMatchType, bool) {
|
||||
switch typ := matchTypeForDomain(domain); typ {
|
||||
case domainMatchTypeInvalid:
|
||||
return typ, false
|
||||
case domainMatchTypeUniversal:
|
||||
return typ, true
|
||||
case domainMatchTypePrefix:
|
||||
// abc.*
|
||||
return typ, strings.HasPrefix(host, strings.TrimSuffix(domain, "*"))
|
||||
case domainMatchTypeSuffix:
|
||||
// *.123
|
||||
return typ, strings.HasSuffix(host, strings.TrimPrefix(domain, "*"))
|
||||
case domainMatchTypeExact:
|
||||
return typ, domain == host
|
||||
default:
|
||||
return domainMatchTypeInvalid, false
|
||||
}
|
||||
}
|
||||
155
internal/xds/server/routing_test.go
Normal file
155
internal/xds/server/routing_test.go
Normal file
@@ -0,0 +1,155 @@
|
||||
/*
|
||||
*
|
||||
* 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 server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/internal/transport"
|
||||
"google.golang.org/grpc/metadata"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
func (s) TestMatchTypeForDomain(t *testing.T) {
|
||||
tests := []struct {
|
||||
d string
|
||||
want domainMatchType
|
||||
}{
|
||||
{d: "", want: domainMatchTypeInvalid},
|
||||
{d: "*", want: domainMatchTypeUniversal},
|
||||
{d: "bar.*", want: domainMatchTypePrefix},
|
||||
{d: "*.abc.com", want: domainMatchTypeSuffix},
|
||||
{d: "foo.bar.com", want: domainMatchTypeExact},
|
||||
{d: "foo.*.com", want: domainMatchTypeInvalid},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
if got := matchTypeForDomain(tt.d); got != tt.want {
|
||||
t.Errorf("matchTypeForDomain(%q) = %v, want %v", tt.d, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s) TestMatch(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
domain string
|
||||
host string
|
||||
wantTyp domainMatchType
|
||||
wantMatched bool
|
||||
}{
|
||||
{name: "invalid-empty", domain: "", host: "", wantTyp: domainMatchTypeInvalid, wantMatched: false},
|
||||
{name: "invalid", domain: "a.*.b", host: "", wantTyp: domainMatchTypeInvalid, wantMatched: false},
|
||||
{name: "universal", domain: "*", host: "abc.com", wantTyp: domainMatchTypeUniversal, wantMatched: true},
|
||||
{name: "prefix-match", domain: "abc.*", host: "abc.123", wantTyp: domainMatchTypePrefix, wantMatched: true},
|
||||
{name: "prefix-no-match", domain: "abc.*", host: "abcd.123", wantTyp: domainMatchTypePrefix, wantMatched: false},
|
||||
{name: "suffix-match", domain: "*.123", host: "abc.123", wantTyp: domainMatchTypeSuffix, wantMatched: true},
|
||||
{name: "suffix-no-match", domain: "*.123", host: "abc.1234", wantTyp: domainMatchTypeSuffix, wantMatched: false},
|
||||
{name: "exact-match", domain: "foo.bar", host: "foo.bar", wantTyp: domainMatchTypeExact, wantMatched: true},
|
||||
{name: "exact-no-match", domain: "foo.bar.com", host: "foo.bar", wantTyp: domainMatchTypeExact, wantMatched: false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if gotTyp, gotMatched := match(tt.domain, tt.host); gotTyp != tt.wantTyp || gotMatched != tt.wantMatched {
|
||||
t.Errorf("match() = %v, %v, want %v, %v", gotTyp, gotMatched, tt.wantTyp, tt.wantMatched)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (s) TestFindBestMatchingVirtualHost(t *testing.T) {
|
||||
var (
|
||||
oneExactMatch = virtualHostWithInterceptors{domains: []string{"foo.bar.com"}}
|
||||
oneSuffixMatch = virtualHostWithInterceptors{domains: []string{"*.bar.com"}}
|
||||
onePrefixMatch = virtualHostWithInterceptors{domains: []string{"foo.bar.*"}}
|
||||
oneUniversalMatch = virtualHostWithInterceptors{domains: []string{"*"}}
|
||||
longExactMatch = virtualHostWithInterceptors{domains: []string{"v2.foo.bar.com"}}
|
||||
multipleMatch = virtualHostWithInterceptors{domains: []string{"pi.foo.bar.com", "314.*", "*.159"}}
|
||||
vhs = []virtualHostWithInterceptors{oneExactMatch, oneSuffixMatch, onePrefixMatch, oneUniversalMatch, longExactMatch, multipleMatch}
|
||||
)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
host string
|
||||
vHosts []virtualHostWithInterceptors
|
||||
want *virtualHostWithInterceptors
|
||||
}{
|
||||
{name: "exact-match", host: "foo.bar.com", vHosts: vhs, want: &oneExactMatch},
|
||||
{name: "suffix-match", host: "123.bar.com", vHosts: vhs, want: &oneSuffixMatch},
|
||||
{name: "prefix-match", host: "foo.bar.org", vHosts: vhs, want: &onePrefixMatch},
|
||||
{name: "universal-match", host: "abc.123", vHosts: vhs, want: &oneUniversalMatch},
|
||||
{name: "long-exact-match", host: "v2.foo.bar.com", vHosts: vhs, want: &longExactMatch},
|
||||
// Matches suffix "*.bar.com" and exact "pi.foo.bar.com". Takes exact.
|
||||
{name: "multiple-match-exact", host: "pi.foo.bar.com", vHosts: vhs, want: &multipleMatch},
|
||||
// Matches suffix "*.159" and prefix "foo.bar.*". Takes suffix.
|
||||
{name: "multiple-match-suffix", host: "foo.bar.159", vHosts: vhs, want: &multipleMatch},
|
||||
// Matches suffix "*.bar.com" and prefix "314.*". Takes suffix.
|
||||
{name: "multiple-match-prefix", host: "314.bar.com", vHosts: vhs, want: &oneSuffixMatch},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := findBestMatchingVirtualHostServer(tt.host, tt.vHosts); !cmp.Equal(got, tt.want, cmp.AllowUnexported(virtualHostWithInterceptors{}, routeWithInterceptors{})) {
|
||||
t.Errorf("findBestMatchingVirtualHostServer() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type testServerTransportStream struct {
|
||||
method string
|
||||
}
|
||||
|
||||
func (s *testServerTransportStream) Method() string { return s.method }
|
||||
func (s *testServerTransportStream) SetHeader(metadata.MD) error { return nil }
|
||||
func (s *testServerTransportStream) SendHeader(metadata.MD) error { return nil }
|
||||
func (s *testServerTransportStream) SetTrailer(metadata.MD) error { return nil }
|
||||
|
||||
type testServerStream struct {
|
||||
grpc.ServerStream
|
||||
ctx context.Context
|
||||
}
|
||||
|
||||
func (s *testServerStream) Context() context.Context {
|
||||
return s.ctx
|
||||
}
|
||||
|
||||
func (s) TestRouteAndProcess_MissingAuthority(t *testing.T) {
|
||||
var ptr atomic.Pointer[usableRouteConfiguration]
|
||||
ptr.Store(&usableRouteConfiguration{})
|
||||
cw := &connWrapper{urc: &ptr}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
|
||||
defer cancel()
|
||||
ctx = transport.SetConnection(ctx, cw)
|
||||
ctx = grpc.NewContextWithServerTransportStream(ctx, &testServerTransportStream{method: "/test.Service/Method"})
|
||||
ctx = metadata.NewIncomingContext(ctx, metadata.MD{})
|
||||
|
||||
ss := &testServerStream{ctx: ctx}
|
||||
_, err := RouteAndProcess(ss)
|
||||
if status.Code(err) != codes.Internal {
|
||||
t.Fatalf("RouteAndProcess() returned error code %v, want %v", status.Code(err), codes.Internal)
|
||||
}
|
||||
if !strings.Contains(err.Error(), "no :authority header present") {
|
||||
t.Fatalf("RouteAndProcess() returned error message %q, want %q", err.Error(), "no :authority header present")
|
||||
}
|
||||
}
|
||||
69
internal/xds/server/server_options.go
Normal file
69
internal/xds/server/server_options.go
Normal file
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
*
|
||||
* 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 server
|
||||
|
||||
import (
|
||||
"net"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/internal/xds/xdsclient"
|
||||
)
|
||||
|
||||
// Options contains options used by an xDS-enabled gRPC server.
|
||||
//
|
||||
// This type is internal so that the public xds package can expose server
|
||||
// options without also owning their application and storage.
|
||||
type Options struct {
|
||||
ModeCallback ServingModeCallback
|
||||
ClientPoolForTesting *xdsclient.Pool
|
||||
OverrideListenerResourceName func(net.Addr) string
|
||||
}
|
||||
|
||||
type serverOption struct {
|
||||
grpc.EmptyServerOption
|
||||
apply func(*Options)
|
||||
}
|
||||
|
||||
// NewServerOption returns a grpc.ServerOption which applies f to the internal
|
||||
// xDS server options.
|
||||
func NewServerOption(f func(*Options)) grpc.ServerOption {
|
||||
return &serverOption{apply: f}
|
||||
}
|
||||
|
||||
// ApplyServerOptions applies all internal xDS server options in opts to so.
|
||||
func ApplyServerOptions(opts []grpc.ServerOption, so *Options) {
|
||||
for _, opt := range opts {
|
||||
if o, ok := opt.(*serverOption); ok {
|
||||
o.apply(so)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// OverrideListenerResourceName returns a server option that overrides the LDS
|
||||
// resource name selected for an xDS server listener. The supplied function is
|
||||
// called by Serve with the address returned by the listener's Addr method, and
|
||||
// its return value is used as-is as the LDS listener resource name.
|
||||
//
|
||||
// The function is called once for each Serve invocation that gets past listener
|
||||
// validation and the server-stopped check. If Serve is called concurrently, the
|
||||
// function may be called concurrently and must be safe for concurrent use.
|
||||
func OverrideListenerResourceName(f func(net.Addr) string) grpc.ServerOption {
|
||||
return NewServerOption(func(o *Options) {
|
||||
o.OverrideListenerResourceName = f
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user