google.golang.org/grpc@v1.72.2/xds/internal/clients/xdsclient/helpers_test.go (about) 1 /* 2 * 3 * Copyright 2025 gRPC authors. 4 * 5 * Licensed under the Apache License, Version 2.0 (the "License"); 6 * you may not use this file except in compliance with the License. 7 * You may obtain a copy of the License at 8 * 9 * http://www.apache.org/licenses/LICENSE-2.0 10 * 11 * Unless required by applicable law or agreed to in writing, software 12 * distributed under the License is distributed on an "AS IS" BASIS, 13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 * See the License for the specific language governing permissions and 15 * limitations under the License. 16 * 17 */ 18 19 package xdsclient 20 21 import ( 22 "bytes" 23 "errors" 24 "fmt" 25 "strconv" 26 "testing" 27 "time" 28 29 "google.golang.org/grpc/internal/grpctest" 30 "google.golang.org/grpc/xds/internal/clients/internal/pretty" 31 "google.golang.org/grpc/xds/internal/clients/xdsclient/internal/xdsresource" 32 "google.golang.org/protobuf/proto" 33 34 v3listenerpb "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3" 35 v3httppb "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3" 36 ) 37 38 type s struct { 39 grpctest.Tester 40 } 41 42 func Test(t *testing.T) { 43 grpctest.RunSubTests(t, s{}) 44 } 45 46 const ( 47 defaultTestWatchExpiryTimeout = 100 * time.Millisecond 48 defaultTestTimeout = 5 * time.Second 49 defaultTestShortTimeout = 10 * time.Millisecond // For events expected to *not* happen. 50 // listenerResourceTypeName represents the transport agnostic name for the 51 // listener resource. 52 listenerResourceTypeName = "ListenerResource" 53 ) 54 55 var ( 56 // Singleton instantiation of the resource type implementation. 57 listenerType = ResourceType{ 58 TypeURL: xdsresource.V3ListenerURL, 59 TypeName: listenerResourceTypeName, 60 AllResourcesRequiredInSotW: true, 61 Decoder: listenerDecoder{}, 62 } 63 ) 64 65 func unmarshalListenerResource(r []byte) (string, listenerUpdate, error) { 66 lis := &v3listenerpb.Listener{} 67 if err := proto.Unmarshal(r, lis); err != nil { 68 return "", listenerUpdate{}, fmt.Errorf("failed to unmarshal resource: %v", err) 69 } 70 71 lu, err := processListener(lis) 72 if err != nil { 73 return lis.GetName(), listenerUpdate{}, err 74 } 75 lu.Raw = r 76 return lis.GetName(), *lu, nil 77 } 78 79 func processListener(lis *v3listenerpb.Listener) (*listenerUpdate, error) { 80 if lis.GetApiListener() != nil { 81 return processClientSideListener(lis) 82 } 83 return processServerSideListener(lis) 84 } 85 86 // processClientSideListener checks if the provided Listener proto meets 87 // the expected criteria. If so, it returns a non-empty routeConfigName. 88 func processClientSideListener(lis *v3listenerpb.Listener) (*listenerUpdate, error) { 89 update := &listenerUpdate{} 90 91 apiLisAny := lis.GetApiListener().GetApiListener() 92 if !xdsresource.IsHTTPConnManagerResource(apiLisAny.GetTypeUrl()) { 93 return nil, fmt.Errorf("unexpected http connection manager resource type: %q", apiLisAny.GetTypeUrl()) 94 } 95 apiLis := &v3httppb.HttpConnectionManager{} 96 if err := proto.Unmarshal(apiLisAny.GetValue(), apiLis); err != nil { 97 return nil, fmt.Errorf("failed to unmarshal api_listener: %v", err) 98 } 99 // "HttpConnectionManager.xff_num_trusted_hops must be unset or zero and 100 // HttpConnectionManager.original_ip_detection_extensions must be empty. If 101 // either field has an incorrect value, the Listener must be NACKed." - A41 102 if apiLis.XffNumTrustedHops != 0 { 103 return nil, fmt.Errorf("xff_num_trusted_hops must be unset or zero %+v", apiLis) 104 } 105 if len(apiLis.OriginalIpDetectionExtensions) != 0 { 106 return nil, fmt.Errorf("original_ip_detection_extensions must be empty %+v", apiLis) 107 } 108 109 switch apiLis.RouteSpecifier.(type) { 110 case *v3httppb.HttpConnectionManager_Rds: 111 if configsource := apiLis.GetRds().GetConfigSource(); configsource.GetAds() == nil && configsource.GetSelf() == nil { 112 return nil, fmt.Errorf("LDS's RDS configSource is not ADS or Self: %+v", lis) 113 } 114 name := apiLis.GetRds().GetRouteConfigName() 115 if name == "" { 116 return nil, fmt.Errorf("empty route_config_name: %+v", lis) 117 } 118 update.RouteConfigName = name 119 case *v3httppb.HttpConnectionManager_RouteConfig: 120 routeU := apiLis.GetRouteConfig() 121 if routeU == nil { 122 return nil, fmt.Errorf("empty inline RDS resp:: %+v", lis) 123 } 124 if routeU.Name == "" { 125 return nil, fmt.Errorf("empty route_config_name in inline RDS resp: %+v", lis) 126 } 127 update.RouteConfigName = routeU.Name 128 case nil: 129 return nil, fmt.Errorf("no RouteSpecifier: %+v", apiLis) 130 default: 131 return nil, fmt.Errorf("unsupported type %T for RouteSpecifier", apiLis.RouteSpecifier) 132 } 133 134 return update, nil 135 } 136 137 func processServerSideListener(lis *v3listenerpb.Listener) (*listenerUpdate, error) { 138 if n := len(lis.ListenerFilters); n != 0 { 139 return nil, fmt.Errorf("unsupported field 'listener_filters' contains %d entries", n) 140 } 141 if useOrigDst := lis.GetUseOriginalDst(); useOrigDst != nil && useOrigDst.GetValue() { 142 return nil, errors.New("unsupported field 'use_original_dst' is present and set to true") 143 } 144 addr := lis.GetAddress() 145 if addr == nil { 146 return nil, fmt.Errorf("no address field in LDS response: %+v", lis) 147 } 148 sockAddr := addr.GetSocketAddress() 149 if sockAddr == nil { 150 return nil, fmt.Errorf("no socket_address field in LDS response: %+v", lis) 151 } 152 lu := &listenerUpdate{ 153 InboundListenerCfg: &inboundListenerConfig{ 154 Address: sockAddr.GetAddress(), 155 Port: strconv.Itoa(int(sockAddr.GetPortValue())), 156 }, 157 } 158 159 return lu, nil 160 } 161 162 type listenerDecoder struct{} 163 164 // Decode deserializes and validates an xDS resource serialized inside the 165 // provided `Any` proto, as received from the xDS management server. 166 func (listenerDecoder) Decode(resource []byte, _ DecodeOptions) (*DecodeResult, error) { 167 name, listener, err := unmarshalListenerResource(resource) 168 switch { 169 case name == "": 170 // Name is unset only when protobuf deserialization fails. 171 return nil, err 172 case err != nil: 173 // Protobuf deserialization succeeded, but resource validation failed. 174 return &DecodeResult{Name: name, Resource: &listenerResourceData{Resource: listenerUpdate{}}}, err 175 } 176 177 return &DecodeResult{Name: name, Resource: &listenerResourceData{Resource: listener}}, nil 178 179 } 180 181 // listenerResourceData wraps the configuration of a Listener resource as 182 // received from the management server. 183 // 184 // Implements the ResourceData interface. 185 type listenerResourceData struct { 186 ResourceData 187 188 Resource listenerUpdate 189 } 190 191 // Equal returns true if other is equal to l. 192 func (l *listenerResourceData) Equal(other ResourceData) bool { 193 if l == nil && other == nil { 194 return true 195 } 196 if (l == nil) != (other == nil) { 197 return false 198 } 199 return bytes.Equal(l.Resource.Raw, other.Bytes()) 200 } 201 202 // ToJSON returns a JSON string representation of the resource data. 203 func (l *listenerResourceData) ToJSON() string { 204 return pretty.ToJSON(l.Resource) 205 } 206 207 // Bytes returns the underlying raw protobuf form of the listener resource. 208 func (l *listenerResourceData) Bytes() []byte { 209 return l.Resource.Raw 210 } 211 212 // ListenerUpdate contains information received in an LDS response, which is of 213 // interest to the registered LDS watcher. 214 type listenerUpdate struct { 215 // RouteConfigName is the route configuration name corresponding to the 216 // target which is being watched through LDS. 217 RouteConfigName string 218 219 // InboundListenerCfg contains inbound listener configuration. 220 InboundListenerCfg *inboundListenerConfig 221 222 // Raw is the resource from the xds response. 223 Raw []byte 224 } 225 226 // InboundListenerConfig contains information about the inbound listener, i.e 227 // the server-side listener. 228 type inboundListenerConfig struct { 229 // Address is the local address on which the inbound listener is expected to 230 // accept incoming connections. 231 Address string 232 // Port is the local port on which the inbound listener is expected to 233 // accept incoming connections. 234 Port string 235 }