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  }