istio.io/istio@v0.0.0-20240520182934-d79c90f27776/pilot/test/xdstest/validate.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 xdstest
    16  
    17  import (
    18  	"strings"
    19  	"testing"
    20  
    21  	cluster "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3"
    22  	core "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
    23  	endpoint "github.com/envoyproxy/go-control-plane/envoy/config/endpoint/v3"
    24  	listener "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3"
    25  	route "github.com/envoyproxy/go-control-plane/envoy/config/route/v3"
    26  
    27  	istio_route "istio.io/istio/pilot/pkg/networking/core/route"
    28  	xdsfilters "istio.io/istio/pilot/pkg/xds/filters"
    29  	"istio.io/istio/pkg/util/sets"
    30  )
    31  
    32  func ValidateListeners(t testing.TB, ls []*listener.Listener) {
    33  	t.Helper()
    34  	found := sets.New[string]()
    35  	for _, l := range ls {
    36  		if found.InsertContains(l.Name) {
    37  			t.Errorf("duplicate listener name %v", l.Name)
    38  		}
    39  		ValidateListener(t, l)
    40  	}
    41  }
    42  
    43  func ValidateListener(t testing.TB, l *listener.Listener) {
    44  	t.Helper()
    45  	if err := l.Validate(); err != nil {
    46  		t.Errorf("listener %v is invalid: %v", l.Name, err)
    47  	}
    48  	validateInspector(t, l)
    49  	validateListenerTLS(t, l)
    50  	validateFilterChainMatch(t, l)
    51  	validateInboundListener(t, l)
    52  	validateListenerFilters(t, l)
    53  }
    54  
    55  func validateListenerFilters(t testing.TB, l *listener.Listener) {
    56  	found := sets.New[string]()
    57  	for _, lf := range l.GetListenerFilters() {
    58  		if found.InsertContains(lf.GetName()) {
    59  			// Technically legal in Envoy but should always be a bug when done in Istio based on our usage
    60  			t.Errorf("listener contains duplicate listener filter: %v", lf.GetName())
    61  		}
    62  	}
    63  }
    64  
    65  func validateInboundListener(t testing.TB, l *listener.Listener) {
    66  	if l.GetAddress().GetSocketAddress().GetPortValue() != 15006 {
    67  		// Not an inbound port
    68  		return
    69  	}
    70  	if l.GetTrafficDirection() != core.TrafficDirection_INBOUND {
    71  		// Not an inbound listener
    72  		return
    73  	}
    74  	for i, fc := range l.GetFilterChains() {
    75  		if fc.FilterChainMatch == nil {
    76  			t.Errorf("nil filter chain %d", i)
    77  			continue
    78  		}
    79  		if fc.FilterChainMatch.TransportProtocol == "" && fc.FilterChainMatch.GetDestinationPort().GetValue() != 15006 {
    80  			// Not setting transport protocol may lead to unexpected matching behavior due to https://github.com/istio/istio/issues/26079
    81  			// This is not *always* a bug, just a guideline - the 15006 blocker filter chain doesn't follow this rule and is exluced.
    82  			t.Errorf("filter chain %d had no transport protocol set", i)
    83  		}
    84  	}
    85  }
    86  
    87  func validateFilterChainMatch(t testing.TB, l *listener.Listener) {
    88  	t.Helper()
    89  
    90  	// Check for duplicate filter chains, to avoid "multiple filter chains with the same matching rules are defined" error
    91  	check := map[string]int{}
    92  	for i1, l1 := range l.FilterChains {
    93  		// We still create virtual inbound listeners before merging into single inbound
    94  		// This hack skips these ones, as they will be processed later
    95  		if hcm := ExtractHTTPConnectionManager(t, l1); strings.HasPrefix(hcm.GetStatPrefix(), "inbound_") && l.Name != "virtualInbound" {
    96  			continue
    97  		}
    98  
    99  		s := Dump(t, l1.FilterChainMatch)
   100  		if i2, ok := check[s]; ok {
   101  			var fcms []string
   102  			for _, fc := range l.FilterChains {
   103  				fcms = append(fcms, Dump(t, fc.GetFilterChainMatch()))
   104  			}
   105  			t.Errorf("overlapping filter chains %d and %d:\n%v\n Full listener: %v", i1, i2, strings.Join(fcms, ",\n"), Dump(t, l))
   106  		} else {
   107  			check[s] = i1
   108  		}
   109  	}
   110  
   111  	// Due to the trie based logic of FCM, an unset field is only a wildcard if no
   112  	// other FCM sets it. Therefore, we should ensure we explicitly set the FCM on
   113  	// all match clauses if its set on any other match clause See
   114  	// https://github.com/envoyproxy/envoy/issues/12572 for details
   115  	destPorts := sets.New[uint32]()
   116  	for _, fc := range l.FilterChains {
   117  		if fc.GetFilterChainMatch().GetDestinationPort() != nil {
   118  			destPorts.Insert(fc.GetFilterChainMatch().GetDestinationPort().GetValue())
   119  		}
   120  	}
   121  	for p := range destPorts {
   122  		hasTLSInspector := false
   123  		for _, fc := range l.FilterChains {
   124  			if p == fc.GetFilterChainMatch().GetDestinationPort().GetValue() && fc.GetFilterChainMatch().GetTransportProtocol() != "" {
   125  				hasTLSInspector = true
   126  			}
   127  		}
   128  		if hasTLSInspector {
   129  			for _, fc := range l.FilterChains {
   130  				if p == fc.GetFilterChainMatch().GetDestinationPort().GetValue() && fc.GetFilterChainMatch().GetTransportProtocol() == "" {
   131  					// Note: matches [{transport=tls},{}] and [{transport=tls},{transport=buffer}]
   132  					// are equivalent, so technically this error is overly sensitive. However, for
   133  					// more complicated use cases its generally best to be explicit rather than
   134  					// assuming that {} will be treated as wildcard when in reality it may not be.
   135  					// Instead, we should explicitly double the filter chain (one for raw buffer, one
   136  					// for TLS)
   137  					t.Errorf("filter chain should have transport protocol set for port %v: %v", p, Dump(t, fc))
   138  				}
   139  			}
   140  		}
   141  	}
   142  }
   143  
   144  func validateListenerTLS(t testing.TB, l *listener.Listener) {
   145  	t.Helper()
   146  	for _, fc := range l.FilterChains {
   147  		m := fc.FilterChainMatch
   148  		if m == nil {
   149  			continue
   150  		}
   151  		// if we are matching TLS traffic and doing HTTP traffic, we must terminate the TLS
   152  		if m.TransportProtocol == xdsfilters.TLSTransportProtocol && fc.TransportSocket == nil && ExtractHTTPConnectionManager(t, fc) != nil {
   153  			t.Errorf("listener %v is invalid: tls traffic may not be terminated: %v", l.Name, Dump(t, fc))
   154  		}
   155  	}
   156  }
   157  
   158  // Validate a tls inspect filter is added whenever it is needed
   159  // matches logic in https://github.com/envoyproxy/envoy/blob/22683a0a24ffbb0cdeb4111eec5ec90246bec9cb/source/server/listener_impl.cc#L41
   160  func validateInspector(t testing.TB, l *listener.Listener) {
   161  	t.Helper()
   162  	for _, lf := range l.ListenerFilters {
   163  		if lf.Name == xdsfilters.TLSInspector.Name {
   164  			return
   165  		}
   166  	}
   167  	for _, fc := range l.FilterChains {
   168  		m := fc.FilterChainMatch
   169  		if fc.FilterChainMatch == nil {
   170  			continue
   171  		}
   172  		if m.TransportProtocol == xdsfilters.TLSTransportProtocol {
   173  			t.Errorf("transport protocol set, but missing tls inspector: %v", Dump(t, l))
   174  		}
   175  		if m.TransportProtocol == "" && len(m.ServerNames) > 0 {
   176  			t.Errorf("server names set, but missing tls inspector: %v", Dump(t, l))
   177  		}
   178  		// This is a bit suspect; I suspect this could be done with just http inspector without tls inspector,
   179  		// but this mirrors Envoy validation logic
   180  		if m.TransportProtocol == "" && len(m.ApplicationProtocols) > 0 {
   181  			t.Errorf("application protocol set, but missing tls inspector: %v", Dump(t, l))
   182  		}
   183  	}
   184  }
   185  
   186  func ValidateClusters(t testing.TB, ls []*cluster.Cluster) {
   187  	found := sets.New[string]()
   188  	for _, l := range ls {
   189  		if found.Contains(l.Name) {
   190  			t.Errorf("duplicate cluster name %v", l.Name)
   191  		}
   192  		found.Insert(l.Name)
   193  		ValidateCluster(t, l)
   194  	}
   195  }
   196  
   197  func ValidateCluster(t testing.TB, c *cluster.Cluster) {
   198  	if err := c.Validate(); err != nil {
   199  		t.Errorf("cluster %v is invalid: %v", c.Name, err)
   200  	}
   201  	validateClusterTLS(t, c)
   202  }
   203  
   204  func validateClusterTLS(t testing.TB, c *cluster.Cluster) {
   205  	if c.TransportSocket != nil && c.TransportSocketMatches != nil {
   206  		t.Errorf("both transport_socket and transport_socket_matches set for %v", c)
   207  	}
   208  }
   209  
   210  func ValidateRoutes(t testing.TB, ls []*route.Route) {
   211  	for _, l := range ls {
   212  		ValidateRoute(t, l)
   213  	}
   214  }
   215  
   216  func ValidateRoute(t testing.TB, r *route.Route) {
   217  	if err := r.Validate(); err != nil {
   218  		t.Errorf("route %v is invalid: %v", r.Name, err)
   219  	}
   220  }
   221  
   222  func ValidateRouteConfigurations(t testing.TB, ls []*route.RouteConfiguration) {
   223  	found := sets.New[string]()
   224  	for _, l := range ls {
   225  		if found.InsertContains(l.Name) {
   226  			t.Errorf("duplicate route config name %v", l.Name)
   227  		}
   228  		ValidateRouteConfiguration(t, l)
   229  	}
   230  }
   231  
   232  func ValidateRouteConfiguration(t testing.TB, l *route.RouteConfiguration) {
   233  	t.Helper()
   234  	if err := l.Validate(); err != nil {
   235  		t.Errorf("route configuration %v is invalid: %v", l.Name, err)
   236  	}
   237  
   238  	if l.MaxDirectResponseBodySizeBytes.Value != istio_route.DefaultMaxDirectResponseBodySizeBytes.Value {
   239  		t.Errorf("expected MaxDirectResponseBodySizeBytes %v, got %v",
   240  			istio_route.DefaultMaxDirectResponseBodySizeBytes, l.MaxDirectResponseBodySizeBytes)
   241  	}
   242  	validateRouteConfigurationDomains(t, l)
   243  }
   244  
   245  func validateRouteConfigurationDomains(t testing.TB, l *route.RouteConfiguration) {
   246  	t.Helper()
   247  
   248  	vhosts := sets.New[string]()
   249  	domains := sets.New[string]()
   250  	for _, vhost := range l.VirtualHosts {
   251  		if vhosts.InsertContains(vhost.Name) {
   252  			t.Errorf("duplicate virtual host found %s", vhost.Name)
   253  		}
   254  		for _, domain := range vhost.Domains {
   255  			if domains.InsertContains(domain) {
   256  				t.Errorf("duplicate virtual host domain found %s", domain)
   257  			}
   258  		}
   259  	}
   260  }
   261  
   262  func ValidateClusterLoadAssignments(t testing.TB, ls []*endpoint.ClusterLoadAssignment) {
   263  	for _, l := range ls {
   264  		ValidateClusterLoadAssignment(t, l)
   265  	}
   266  }
   267  
   268  func ValidateClusterLoadAssignment(t testing.TB, l *endpoint.ClusterLoadAssignment) {
   269  	if err := l.Validate(); err != nil {
   270  		t.Errorf("cluster load assignment %v is invalid: %v", l.ClusterName, err)
   271  	}
   272  }