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 }