istio.io/istio@v0.0.0-20240520182934-d79c90f27776/pilot/pkg/networking/grpcgen/lds.go (about) 1 // Copyright Istio Authors 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package grpcgen 16 17 import ( 18 "fmt" 19 "net" 20 "strconv" 21 "strings" 22 23 core "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" 24 listener "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3" 25 rbacpb "github.com/envoyproxy/go-control-plane/envoy/config/rbac/v3" 26 route "github.com/envoyproxy/go-control-plane/envoy/config/route/v3" 27 rbachttp "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/rbac/v3" 28 hcm "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3" 29 tls "github.com/envoyproxy/go-control-plane/envoy/extensions/transport_sockets/tls/v3" 30 discovery "github.com/envoyproxy/go-control-plane/envoy/service/discovery/v3" 31 wrappers "google.golang.org/protobuf/types/known/wrapperspb" 32 33 "istio.io/api/label" 34 "istio.io/istio/pilot/pkg/model" 35 "istio.io/istio/pilot/pkg/networking/util" 36 "istio.io/istio/pilot/pkg/security/authn" 37 authzmodel "istio.io/istio/pilot/pkg/security/authz/model" 38 "istio.io/istio/pilot/pkg/util/protoconv" 39 xdsfilters "istio.io/istio/pilot/pkg/xds/filters" 40 "istio.io/istio/pkg/istio-agent/grpcxds" 41 "istio.io/istio/pkg/util/sets" 42 ) 43 44 var supportedFilters = []*hcm.HttpFilter{ 45 xdsfilters.Fault, 46 xdsfilters.BuildRouterFilter(xdsfilters.RouterFilterContext{ 47 StartChildSpan: false, 48 SuppressDebugHeaders: false, // No need to set this to true, gRPC doesn't respect it anyways 49 }), 50 } 51 52 const ( 53 RBACHTTPFilterName = "envoy.filters.http.rbac" 54 RBACHTTPFilterNameDeny = "envoy.filters.http.rbac.DENY" 55 ) 56 57 // BuildListeners handles a LDS request, returning listeners of ApiListener type. 58 // The request may include a list of resource names, using the full_hostname[:port] format to select only 59 // specific services. 60 func (g *GrpcConfigGenerator) BuildListeners(node *model.Proxy, push *model.PushContext, names []string) model.Resources { 61 filter := newListenerNameFilter(names, node) 62 63 log.Debugf("building lds for %s with filter:\n%v", node.ID, filter) 64 65 resp := make(model.Resources, 0, len(filter)) 66 resp = append(resp, buildOutboundListeners(node, push, filter)...) 67 resp = append(resp, buildInboundListeners(node, push, filter.inboundNames())...) 68 69 return resp 70 } 71 72 func buildInboundListeners(node *model.Proxy, push *model.PushContext, names []string) model.Resources { 73 if len(names) == 0 { 74 return nil 75 } 76 var out model.Resources 77 mtlsPolicy := authn.NewMtlsPolicy(push, node.Metadata.Namespace, node.Labels, node.IsWaypointProxy()) 78 serviceInstancesByPort := map[uint32]model.ServiceTarget{} 79 for _, si := range node.ServiceTargets { 80 serviceInstancesByPort[si.Port.TargetPort] = si 81 } 82 83 for _, name := range names { 84 listenAddress := strings.TrimPrefix(name, grpcxds.ServerListenerNamePrefix) 85 listenHost, listenPortStr, err := net.SplitHostPort(listenAddress) 86 if err != nil { 87 log.Errorf("failed parsing address from gRPC listener name %s: %v", name, err) 88 continue 89 } 90 listenPort, err := strconv.Atoi(listenPortStr) 91 if err != nil { 92 log.Errorf("failed parsing port from gRPC listener name %s: %v", name, err) 93 continue 94 } 95 si, ok := serviceInstancesByPort[uint32(listenPort)] 96 if !ok { 97 log.Warnf("%s has no service instance for port %s", node.ID, listenPortStr) 98 continue 99 } 100 101 ll := &listener.Listener{ 102 Name: name, 103 Address: &core.Address{Address: &core.Address_SocketAddress{ 104 SocketAddress: &core.SocketAddress{ 105 Address: listenHost, 106 PortSpecifier: &core.SocketAddress_PortValue{ 107 PortValue: uint32(listenPort), 108 }, 109 }, 110 }}, 111 FilterChains: buildInboundFilterChains(node, push, si, mtlsPolicy), 112 // the following must not be set or the client will NACK 113 ListenerFilters: nil, 114 UseOriginalDst: nil, 115 } 116 // add extra addresses for the listener 117 extrAddresses := si.Service.GetExtraAddressesForProxy(node) 118 if len(extrAddresses) > 0 { 119 ll.AdditionalAddresses = util.BuildAdditionalAddresses(extrAddresses, uint32(listenPort)) 120 } 121 122 out = append(out, &discovery.Resource{ 123 Name: ll.Name, 124 Resource: protoconv.MessageToAny(ll), 125 }) 126 } 127 return out 128 } 129 130 // nolint: unparam 131 func buildInboundFilterChains(node *model.Proxy, push *model.PushContext, si model.ServiceTarget, checker authn.MtlsPolicy) []*listener.FilterChain { 132 mode := checker.GetMutualTLSModeForPort(si.Port.TargetPort) 133 134 // auto-mtls label is set - clients will attempt to connect using mtls, and 135 // gRPC doesn't support permissive. 136 if node.Labels[label.SecurityTlsMode.Name] == "istio" && mode == model.MTLSPermissive { 137 mode = model.MTLSStrict 138 } 139 140 var tlsContext *tls.DownstreamTlsContext 141 if mode != model.MTLSDisable && mode != model.MTLSUnknown { 142 tlsContext = &tls.DownstreamTlsContext{ 143 CommonTlsContext: buildCommonTLSContext(nil), 144 // TODO match_subject_alt_names field in validation context is not supported on the server 145 // CommonTlsContext: buildCommonTLSContext(authnplugin.TrustDomainsForValidation(push.Mesh)), 146 // TODO plain TLS support 147 RequireClientCertificate: &wrappers.BoolValue{Value: true}, 148 } 149 } 150 151 if mode == model.MTLSUnknown { 152 log.Warnf("could not find mTLS mode for %s on %s; defaulting to DISABLE", si.Service.Hostname, node.ID) 153 mode = model.MTLSDisable 154 } 155 if mode == model.MTLSPermissive { 156 // TODO gRPC's filter chain match is super limited - only effective transport_protocol match is "raw_buffer" 157 // see https://github.com/grpc/proposal/blob/master/A36-xds-for-servers.md for detail 158 // No need to warn on each push - the behavior is still consistent with auto-mtls, which is the 159 // replacement for permissive. 160 mode = model.MTLSDisable 161 } 162 163 var out []*listener.FilterChain 164 switch mode { 165 case model.MTLSDisable: 166 out = append(out, buildInboundFilterChain(node, push, "plaintext", nil)) 167 case model.MTLSStrict: 168 out = append(out, buildInboundFilterChain(node, push, "mtls", tlsContext)) 169 // TODO permissive builts both plaintext and mtls; when tlsContext is present add a match for protocol 170 } 171 172 return out 173 } 174 175 func buildInboundFilterChain(node *model.Proxy, push *model.PushContext, nameSuffix string, tlsContext *tls.DownstreamTlsContext) *listener.FilterChain { 176 fc := []*hcm.HttpFilter{} 177 // See security/authz/builder and grpc internal/xds/rbac 178 // grpc supports ALLOW and DENY actions (fail if it is not one of them), so we can't use the normal generator 179 selectionOpts := model.PolicyMatcherForProxy(node) 180 policies := push.AuthzPolicies.ListAuthorizationPolicies(selectionOpts) 181 if len(policies.Deny)+len(policies.Allow) > 0 { 182 rules := buildRBAC(node, push, nameSuffix, tlsContext, rbacpb.RBAC_DENY, policies.Deny) 183 if rules != nil && len(rules.Policies) > 0 { 184 rbac := &rbachttp.RBAC{ 185 Rules: rules, 186 } 187 fc = append(fc, 188 &hcm.HttpFilter{ 189 Name: RBACHTTPFilterNameDeny, 190 ConfigType: &hcm.HttpFilter_TypedConfig{TypedConfig: protoconv.MessageToAny(rbac)}, 191 }) 192 } 193 arules := buildRBAC(node, push, nameSuffix, tlsContext, rbacpb.RBAC_ALLOW, policies.Allow) 194 if arules != nil && len(arules.Policies) > 0 { 195 rbac := &rbachttp.RBAC{ 196 Rules: arules, 197 } 198 fc = append(fc, 199 &hcm.HttpFilter{ 200 Name: RBACHTTPFilterName, 201 ConfigType: &hcm.HttpFilter_TypedConfig{TypedConfig: protoconv.MessageToAny(rbac)}, 202 }) 203 } 204 } 205 206 // Must be last 207 fc = append(fc, xdsfilters.BuildRouterFilter(xdsfilters.RouterFilterContext{ 208 StartChildSpan: false, 209 SuppressDebugHeaders: false, // No need to set this to true, gRPC doesn't respect it anyways 210 })) 211 212 out := &listener.FilterChain{ 213 Name: "inbound-" + nameSuffix, 214 FilterChainMatch: nil, 215 Filters: []*listener.Filter{{ 216 Name: "inbound-hcm" + nameSuffix, 217 ConfigType: &listener.Filter_TypedConfig{ 218 TypedConfig: protoconv.MessageToAny(&hcm.HttpConnectionManager{ 219 RouteSpecifier: &hcm.HttpConnectionManager_RouteConfig{ 220 // https://github.com/grpc/grpc-go/issues/4924 221 RouteConfig: &route.RouteConfiguration{ 222 Name: "inbound", 223 VirtualHosts: []*route.VirtualHost{{ 224 Domains: []string{"*"}, 225 Routes: []*route.Route{{ 226 Match: &route.RouteMatch{ 227 PathSpecifier: &route.RouteMatch_Prefix{Prefix: "/"}, 228 }, 229 Action: &route.Route_NonForwardingAction{}, 230 }}, 231 }}, 232 }, 233 }, 234 HttpFilters: fc, 235 }), 236 }, 237 }}, 238 } 239 if tlsContext != nil { 240 out.TransportSocket = &core.TransportSocket{ 241 Name: transportSocketName, 242 ConfigType: &core.TransportSocket_TypedConfig{TypedConfig: protoconv.MessageToAny(tlsContext)}, 243 } 244 } 245 return out 246 } 247 248 // buildRBAC builds the RBAC config expected by gRPC. 249 // 250 // See: xds/internal/httpfilter/rbac 251 // 252 // TODO: gRPC also supports 'per route override' - not yet clear how to use it, Istio uses path expressions instead and we don't generate 253 // vhosts or routes for the inbound listener. 254 // 255 // For gateways it would make a lot of sense to use this concept, same for moving path prefix at top level ( more scalable, easier for users) 256 // This should probably be done for the v2 API. 257 // 258 // nolint: unparam 259 func buildRBAC(node *model.Proxy, push *model.PushContext, suffix string, context *tls.DownstreamTlsContext, 260 a rbacpb.RBAC_Action, policies []model.AuthorizationPolicy, 261 ) *rbacpb.RBAC { 262 rules := &rbacpb.RBAC{ 263 Action: a, 264 Policies: map[string]*rbacpb.Policy{}, 265 } 266 for _, policy := range policies { 267 for i, rule := range policy.Spec.Rules { 268 name := fmt.Sprintf("%s-%s-%d", policy.Namespace, policy.Name, i) 269 m, err := authzmodel.New(rule, true) 270 if err != nil { 271 log.Warnf("Invalid rule %v: %v", rule, err) 272 continue 273 } 274 generated, _ := m.Generate(false, true, a) 275 rules.Policies[name] = generated 276 } 277 } 278 279 return rules 280 } 281 282 // nolint: unparam 283 func buildOutboundListeners(node *model.Proxy, push *model.PushContext, filter listenerNames) model.Resources { 284 out := make(model.Resources, 0, len(filter)) 285 for _, sv := range node.SidecarScope.Services() { 286 serviceHost := string(sv.Hostname) 287 match, ok := filter.includes(serviceHost) 288 if !ok { 289 continue 290 } 291 // we must duplicate the listener for every requested host - grpc may have watches for both foo and foo.ns 292 for _, matchedHost := range sets.SortedList(match.RequestedNames) { 293 for _, p := range sv.Ports { 294 sPort := strconv.Itoa(p.Port) 295 if !match.includesPort(sPort) { 296 continue 297 } 298 filters := supportedFilters 299 if sessionFilter := util.BuildStatefulSessionFilter(sv); sessionFilter != nil { 300 filters = append([]*hcm.HttpFilter{sessionFilter}, filters...) 301 } 302 ll := &listener.Listener{ 303 Name: net.JoinHostPort(matchedHost, sPort), 304 Address: &core.Address{ 305 Address: &core.Address_SocketAddress{ 306 SocketAddress: &core.SocketAddress{ 307 Address: sv.GetAddressForProxy(node), 308 PortSpecifier: &core.SocketAddress_PortValue{ 309 PortValue: uint32(p.Port), 310 }, 311 }, 312 }, 313 }, 314 ApiListener: &listener.ApiListener{ 315 ApiListener: protoconv.MessageToAny(&hcm.HttpConnectionManager{ 316 HttpFilters: filters, 317 RouteSpecifier: &hcm.HttpConnectionManager_Rds{ 318 // TODO: for TCP listeners don't generate RDS, but some indication of cluster name. 319 Rds: &hcm.Rds{ 320 ConfigSource: &core.ConfigSource{ 321 ConfigSourceSpecifier: &core.ConfigSource_Ads{ 322 Ads: &core.AggregatedConfigSource{}, 323 }, 324 }, 325 RouteConfigName: clusterKey(serviceHost, p.Port), 326 }, 327 }, 328 }), 329 }, 330 } 331 // add extra addresses for the listener 332 extrAddresses := sv.GetExtraAddressesForProxy(node) 333 if len(extrAddresses) > 0 { 334 ll.AdditionalAddresses = util.BuildAdditionalAddresses(extrAddresses, uint32(p.Port)) 335 } 336 337 out = append(out, &discovery.Resource{ 338 Name: ll.Name, 339 Resource: protoconv.MessageToAny(ll), 340 }) 341 } 342 } 343 } 344 return out 345 } 346 347 // map[host] -> map[port] -> exists 348 // if the map[port] is empty, an exact listener name was provided (non-hostport) 349 type listenerNames map[string]listenerName 350 351 type listenerName struct { 352 RequestedNames sets.String 353 Ports sets.String 354 } 355 356 func (ln *listenerName) includesPort(port string) bool { 357 if len(ln.Ports) == 0 { 358 return true 359 } 360 _, ok := ln.Ports[port] 361 return ok 362 } 363 364 func (f listenerNames) includes(s string) (listenerName, bool) { 365 if len(f) == 0 { 366 // filter is empty, include everything 367 return listenerName{RequestedNames: sets.New(s)}, true 368 } 369 n, ok := f[s] 370 return n, ok 371 } 372 373 func (f listenerNames) inboundNames() []string { 374 var out []string 375 for key := range f { 376 if strings.HasPrefix(key, grpcxds.ServerListenerNamePrefix) { 377 out = append(out, key) 378 } 379 } 380 return out 381 } 382 383 func newListenerNameFilter(names []string, node *model.Proxy) listenerNames { 384 filter := make(listenerNames, len(names)) 385 for _, name := range names { 386 // inbound, create a simple entry and move on 387 if strings.HasPrefix(name, grpcxds.ServerListenerNamePrefix) { 388 filter[name] = listenerName{RequestedNames: sets.New(name)} 389 continue 390 } 391 392 host, port, err := net.SplitHostPort(name) 393 hasPort := err == nil 394 395 // attempt to expand shortname to FQDN 396 requestedName := name 397 if hasPort { 398 requestedName = host 399 } 400 allNames := []string{requestedName} 401 if fqdn := tryFindFQDN(requestedName, node); fqdn != "" { 402 allNames = append(allNames, fqdn) 403 } 404 405 for _, name := range allNames { 406 ln, ok := filter[name] 407 if !ok { 408 ln = listenerName{RequestedNames: sets.New[string]()} 409 } 410 ln.RequestedNames.Insert(requestedName) 411 412 // only build the portmap if we aren't filtering this name yet, or if the existing filter is non-empty 413 if hasPort && (!ok || len(ln.Ports) != 0) { 414 if ln.Ports == nil { 415 ln.Ports = map[string]struct{}{} 416 } 417 ln.Ports.Insert(port) 418 } else if !hasPort { 419 // if we didn't have a port, we should clear the portmap 420 ln.Ports = nil 421 } 422 filter[name] = ln 423 } 424 } 425 return filter 426 } 427 428 func tryFindFQDN(name string, node *model.Proxy) string { 429 // no "." - assuming this is a shortname "foo" -> "foo.ns.svc.cluster.local" 430 if !strings.Contains(name, ".") { 431 return fmt.Sprintf("%s.%s", name, node.DNSDomain) 432 } 433 for _, suffix := range []string{ 434 node.Metadata.Namespace, 435 node.Metadata.Namespace + ".svc", 436 } { 437 shortname := strings.TrimSuffix(name, "."+suffix) 438 if shortname != name && strings.HasPrefix(node.DNSDomain, suffix) { 439 return fmt.Sprintf("%s.%s", shortname, node.DNSDomain) 440 } 441 } 442 return "" 443 }