istio.io/istio@v0.0.0-20240520182934-d79c90f27776/pilot/pkg/simulation/traffic.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 simulation 16 17 import ( 18 "errors" 19 "fmt" 20 "net" 21 "net/http" 22 "regexp" 23 "strings" 24 "testing" 25 26 cluster "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3" 27 envoycore "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" 28 listener "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3" 29 route "github.com/envoyproxy/go-control-plane/envoy/config/route/v3" 30 tls "github.com/envoyproxy/go-control-plane/envoy/extensions/transport_sockets/tls/v3" 31 "github.com/google/go-cmp/cmp" 32 "github.com/google/go-cmp/cmp/cmpopts" 33 "github.com/yl2chen/cidranger" 34 35 "istio.io/istio/pilot/pkg/model" 36 "istio.io/istio/pilot/pkg/networking/core" 37 xdsfilters "istio.io/istio/pilot/pkg/xds/filters" 38 "istio.io/istio/pilot/test/xds" 39 "istio.io/istio/pilot/test/xdstest" 40 "istio.io/istio/pkg/config/host" 41 istiolog "istio.io/istio/pkg/log" 42 "istio.io/istio/pkg/test" 43 "istio.io/istio/pkg/util/sets" 44 ) 45 46 var log = istiolog.RegisterScope("simulation", "") 47 48 type Protocol string 49 50 const ( 51 HTTP Protocol = "http" 52 HTTP2 Protocol = "http2" 53 TCP Protocol = "tcp" 54 ) 55 56 type TLSMode string 57 58 const ( 59 Plaintext TLSMode = "plaintext" 60 TLS TLSMode = "tls" 61 MTLS TLSMode = "mtls" 62 ) 63 64 func (c Call) IsHTTP() bool { 65 return httpProtocols.Contains(string(c.Protocol)) && (c.TLS == Plaintext || c.TLS == "") 66 } 67 68 var httpProtocols = sets.New(string(HTTP), string(HTTP2)) 69 70 var ( 71 ErrNoListener = errors.New("no listener matched") 72 ErrNoFilterChain = errors.New("no filter chains matched") 73 ErrNoRoute = errors.New("no route matched") 74 ErrTLSRedirect = errors.New("tls required, sending 301") 75 ErrNoVirtualHost = errors.New("no virtual host matched") 76 ErrMultipleFilterChain = errors.New("multiple filter chains matched") 77 // ErrProtocolError happens when sending TLS/TCP request to HCM, for example 78 ErrProtocolError = errors.New("protocol error") 79 ErrTLSError = errors.New("invalid TLS") 80 ErrMTLSError = errors.New("invalid mTLS") 81 ) 82 83 type Expect struct { 84 Name string 85 Call Call 86 Result Result 87 } 88 89 type CallMode string 90 91 type CustomFilterChainValidation func(filterChain *listener.FilterChain) error 92 93 var ( 94 // CallModeGateway simulate no iptables 95 CallModeGateway CallMode = "gateway" 96 // CallModeOutbound simulate iptables redirect to 15001 97 CallModeOutbound CallMode = "outbound" 98 // CallModeInbound simulate iptables redirect to 15006 99 CallModeInbound CallMode = "inbound" 100 ) 101 102 type Call struct { 103 Address string 104 Port int 105 Path string 106 107 // Protocol describes the protocol type. TLS encapsulation is separate 108 Protocol Protocol 109 // TLS describes the connection tls parameters 110 // TODO: currently this does not verify TLS vs mTLS 111 TLS TLSMode 112 Alpn string 113 114 // HostHeader is a convenience field for Headers 115 HostHeader string 116 Headers http.Header 117 118 Sni string 119 120 // CallMode describes the type of call to make. 121 CallMode CallMode 122 123 CustomListenerValidations []CustomFilterChainValidation 124 125 MtlsSecretConfigName string 126 } 127 128 func (c Call) FillDefaults() Call { 129 if c.Headers == nil { 130 c.Headers = http.Header{} 131 } 132 if c.HostHeader != "" { 133 c.Headers["Host"] = []string{c.HostHeader} 134 } 135 // For simplicity, set SNI automatically for TLS traffic. 136 if c.Sni == "" && (c.TLS == TLS) { 137 c.Sni = c.HostHeader 138 } 139 if c.Path == "" { 140 c.Path = "/" 141 } 142 if c.TLS == "" { 143 c.TLS = Plaintext 144 } 145 if c.Address == "" { 146 // pick a random address, assumption is the test does not care 147 c.Address = "1.3.3.7" 148 } 149 if c.TLS == MTLS && c.Alpn == "" { 150 c.Alpn = protocolToMTLSAlpn(c.Protocol) 151 } 152 if c.TLS == TLS && c.Alpn == "" { 153 c.Alpn = protocolToTLSAlpn(c.Protocol) 154 } 155 return c 156 } 157 158 type Result struct { 159 Error error 160 ListenerMatched string 161 FilterChainMatched string 162 RouteMatched string 163 RouteConfigMatched string 164 VirtualHostMatched string 165 ClusterMatched string 166 // StrictMatch controls whether we will strictly match the result. If unset, empty fields will 167 // be ignored, allowing testing only fields we care about This allows asserting that the result 168 // is *exactly* equal, allowing asserting a field is empty 169 StrictMatch bool 170 // If set, this will mark a test as skipped. Note the result is still checked first - we skip only 171 // if we pass the test. This is to ensure that if the behavior changes, we still capture it; the skip 172 // just ensures we notice a test is wrong 173 Skip string 174 t test.Failer 175 } 176 177 func (r Result) Matches(t *testing.T, want Result) { 178 t.Helper() 179 r.StrictMatch = want.StrictMatch // to make diff pass 180 r.Skip = want.Skip // to make diff pass 181 diff := cmp.Diff(want, r, cmpopts.IgnoreUnexported(Result{}), cmpopts.EquateErrors()) 182 if want.StrictMatch && diff != "" { 183 t.Errorf("Diff: %v", diff) 184 return 185 } 186 if want.Error != r.Error { 187 t.Errorf("want error %v got %v", want.Error, r.Error) 188 } 189 if want.ListenerMatched != "" && want.ListenerMatched != r.ListenerMatched { 190 t.Errorf("want listener matched %q got %q", want.ListenerMatched, r.ListenerMatched) 191 } else { 192 // Populate each field in case we did not care about it. This avoids confusing errors when we have fields 193 // we don't care about in the test that are present in the result. 194 want.ListenerMatched = r.ListenerMatched 195 } 196 if want.FilterChainMatched != "" && want.FilterChainMatched != r.FilterChainMatched { 197 t.Errorf("want filter chain matched %q got %q", want.FilterChainMatched, r.FilterChainMatched) 198 } else { 199 want.FilterChainMatched = r.FilterChainMatched 200 } 201 if want.RouteMatched != "" && want.RouteMatched != r.RouteMatched { 202 t.Errorf("want route matched %q got %q", want.RouteMatched, r.RouteMatched) 203 } else { 204 want.RouteMatched = r.RouteMatched 205 } 206 if want.RouteConfigMatched != "" && want.RouteConfigMatched != r.RouteConfigMatched { 207 t.Errorf("want route config matched %q got %q", want.RouteConfigMatched, r.RouteConfigMatched) 208 } else { 209 want.RouteConfigMatched = r.RouteConfigMatched 210 } 211 if want.VirtualHostMatched != "" && want.VirtualHostMatched != r.VirtualHostMatched { 212 t.Errorf("want virtual host matched %q got %q", want.VirtualHostMatched, r.VirtualHostMatched) 213 } else { 214 want.VirtualHostMatched = r.VirtualHostMatched 215 } 216 if want.ClusterMatched != "" && want.ClusterMatched != r.ClusterMatched { 217 t.Errorf("want cluster matched %q got %q", want.ClusterMatched, r.ClusterMatched) 218 } else { 219 want.ClusterMatched = r.ClusterMatched 220 } 221 if t.Failed() { 222 t.Logf("Diff: %+v", diff) 223 t.Logf("Full Diff: %+v", cmp.Diff(want, r, cmpopts.IgnoreUnexported(Result{}), cmpopts.EquateErrors())) 224 } else if want.Skip != "" { 225 t.Skipf("Known bug: %v", r.Skip) 226 } 227 } 228 229 type Simulation struct { 230 t *testing.T 231 Listeners []*listener.Listener 232 Clusters []*cluster.Cluster 233 Routes []*route.RouteConfiguration 234 } 235 236 func NewSimulationFromConfigGen(t *testing.T, s *core.ConfigGenTest, proxy *model.Proxy) *Simulation { 237 l := s.Listeners(proxy) 238 sim := &Simulation{ 239 t: t, 240 Listeners: l, 241 Clusters: s.Clusters(proxy), 242 Routes: s.RoutesFromListeners(proxy, l), 243 } 244 return sim 245 } 246 247 func NewSimulation(t *testing.T, s *xds.FakeDiscoveryServer, proxy *model.Proxy) *Simulation { 248 return NewSimulationFromConfigGen(t, s.ConfigGenTest, proxy) 249 } 250 251 // withT swaps out the testing struct. This allows executing sub tests. 252 func (sim *Simulation) withT(t *testing.T) *Simulation { 253 cpy := *sim 254 cpy.t = t 255 return &cpy 256 } 257 258 func (sim *Simulation) RunExpectations(es []Expect) { 259 for _, e := range es { 260 sim.t.Run(e.Name, func(t *testing.T) { 261 sim.withT(t).Run(e.Call).Matches(t, e.Result) 262 }) 263 } 264 } 265 266 func hasFilterOnPort(l *listener.Listener, filter string, port int) bool { 267 got, f := xdstest.ExtractListenerFilters(l)[filter] 268 if !f { 269 return false 270 } 271 if got.FilterDisabled == nil { 272 return true 273 } 274 return !xdstest.EvaluateListenerFilterPredicates(got.FilterDisabled, port) 275 } 276 277 func (sim *Simulation) Run(input Call) (result Result) { 278 result = Result{t: sim.t} 279 input = input.FillDefaults() 280 if input.Alpn != "" && input.TLS == Plaintext { 281 result.Error = fmt.Errorf("invalid call, ALPN can only be sent in TLS requests") 282 return result 283 } 284 285 // First we will match a listener 286 l := matchListener(sim.Listeners, input) 287 if l == nil { 288 result.Error = ErrNoListener 289 return 290 } 291 result.ListenerMatched = l.Name 292 293 hasTLSInspector := hasFilterOnPort(l, xdsfilters.TLSInspector.Name, input.Port) 294 if !hasTLSInspector { 295 // Without tls inspector, Envoy would not read the ALPN in the TLS handshake 296 // HTTP inspector still may set it though 297 input.Alpn = "" 298 } 299 300 // Apply listener filters 301 if hasFilterOnPort(l, xdsfilters.HTTPInspector.Name, input.Port) { 302 if alpn := protocolToAlpn(input.Protocol); alpn != "" && input.TLS == Plaintext { 303 input.Alpn = alpn 304 } 305 } 306 307 fc, err := sim.matchFilterChain(l.FilterChains, l.DefaultFilterChain, input, hasTLSInspector) 308 if err != nil { 309 result.Error = err 310 return 311 } 312 result.FilterChainMatched = fc.Name 313 // Plaintext to TLS is an error 314 if fc.TransportSocket != nil && input.TLS == Plaintext { 315 result.Error = ErrTLSError 316 return 317 } 318 319 mTLSSecretConfigName := "default" 320 if input.MtlsSecretConfigName != "" { 321 mTLSSecretConfigName = input.MtlsSecretConfigName 322 } 323 324 // mTLS listener will only accept mTLS traffic 325 if fc.TransportSocket != nil && sim.requiresMTLS(fc, mTLSSecretConfigName) != (input.TLS == MTLS) { 326 // If there is no tls inspector, then 327 result.Error = ErrMTLSError 328 return 329 } 330 331 if len(input.CustomListenerValidations) > 0 { 332 for _, validation := range input.CustomListenerValidations { 333 if err := validation(fc); err != nil { 334 result.Error = err 335 } 336 } 337 } 338 339 if hcm := xdstest.ExtractHTTPConnectionManager(sim.t, fc); hcm != nil { 340 // We matched HCM and didn't terminate TLS, but we are sending TLS traffic - decoding will fail 341 if input.TLS != Plaintext && fc.TransportSocket == nil { 342 result.Error = ErrProtocolError 343 return 344 } 345 // TCP to HCM is invalid 346 if input.Protocol != HTTP && input.Protocol != HTTP2 { 347 result.Error = ErrProtocolError 348 return 349 } 350 351 // Fetch inline route 352 rc := hcm.GetRouteConfig() 353 if rc == nil { 354 // If not set, fallback to RDS 355 routeName := hcm.GetRds().RouteConfigName 356 result.RouteConfigMatched = routeName 357 rc = xdstest.ExtractRouteConfigurations(sim.Routes)[routeName] 358 } 359 hostHeader := "" 360 if len(input.Headers["Host"]) > 0 { 361 hostHeader = input.Headers["Host"][0] 362 } 363 vh := sim.matchVirtualHost(rc, hostHeader) 364 if vh == nil { 365 result.Error = ErrNoVirtualHost 366 return 367 } 368 result.VirtualHostMatched = vh.Name 369 if vh.RequireTls == route.VirtualHost_ALL && input.TLS == Plaintext { 370 result.Error = ErrTLSRedirect 371 return 372 } 373 374 r := sim.matchRoute(vh, input) 375 if r == nil { 376 result.Error = ErrNoRoute 377 return 378 } 379 result.RouteMatched = r.Name 380 switch t := r.GetAction().(type) { 381 case *route.Route_Route: 382 result.ClusterMatched = t.Route.GetCluster() 383 } 384 } else if tcp := xdstest.ExtractTCPProxy(sim.t, fc); tcp != nil { 385 result.ClusterMatched = tcp.GetCluster() 386 } 387 return 388 } 389 390 func (sim *Simulation) requiresMTLS(fc *listener.FilterChain, mTLSSecretConfigName string) bool { 391 if fc.TransportSocket == nil { 392 return false 393 } 394 t := &tls.DownstreamTlsContext{} 395 if err := fc.GetTransportSocket().GetTypedConfig().UnmarshalTo(t); err != nil { 396 sim.t.Fatal(err) 397 } 398 399 if len(t.GetCommonTlsContext().GetTlsCertificateSdsSecretConfigs()) == 0 { 400 return false 401 } 402 // This is a lazy heuristic, we could check for explicit default resource or spiffe if it becomes necessary 403 if t.GetCommonTlsContext().GetTlsCertificateSdsSecretConfigs()[0].Name != mTLSSecretConfigName { 404 return false 405 } 406 if !t.RequireClientCertificate.Value { 407 return false 408 } 409 return true 410 } 411 412 func (sim *Simulation) matchRoute(vh *route.VirtualHost, input Call) *route.Route { 413 for _, r := range vh.Routes { 414 // check path 415 switch pt := r.Match.GetPathSpecifier().(type) { 416 case *route.RouteMatch_Prefix: 417 if !strings.HasPrefix(input.Path, pt.Prefix) { 418 continue 419 } 420 case *route.RouteMatch_PathSeparatedPrefix: 421 if !strings.HasPrefix(input.Path, pt.PathSeparatedPrefix) { 422 continue 423 } 424 case *route.RouteMatch_Path: 425 if input.Path != pt.Path { 426 continue 427 } 428 case *route.RouteMatch_SafeRegex: 429 r, err := regexp.Compile(pt.SafeRegex.GetRegex()) 430 if err != nil { 431 sim.t.Fatalf("invalid regex %v: %v", pt.SafeRegex.GetRegex(), err) 432 } 433 if !r.MatchString(input.Path) { 434 continue 435 } 436 default: 437 sim.t.Fatalf("unknown route path type %T", pt) 438 } 439 440 // TODO this only handles path - we need to add headers, query params, etc to be complete. 441 442 return r 443 } 444 return nil 445 } 446 447 func (sim *Simulation) matchVirtualHost(rc *route.RouteConfiguration, host string) *route.VirtualHost { 448 if rc.GetIgnorePortInHostMatching() { 449 if h, _, err := net.SplitHostPort(host); err == nil { 450 host = h 451 } 452 } 453 // Exact match 454 for _, vh := range rc.VirtualHosts { 455 for _, d := range vh.Domains { 456 if d == host { 457 return vh 458 } 459 } 460 } 461 // prefix match 462 var bestMatch *route.VirtualHost 463 longest := 0 464 for _, vh := range rc.VirtualHosts { 465 for _, d := range vh.Domains { 466 if d[0] != '*' { 467 continue 468 } 469 if len(host) >= len(d) && strings.HasSuffix(host, d[1:]) && len(d) > longest { 470 bestMatch = vh 471 longest = len(d) 472 } 473 } 474 } 475 if bestMatch != nil { 476 return bestMatch 477 } 478 // Suffix match 479 longest = 0 480 for _, vh := range rc.VirtualHosts { 481 for _, d := range vh.Domains { 482 if d[len(d)-1] != '*' { 483 continue 484 } 485 if len(host) >= len(d) && strings.HasPrefix(host, d[:len(d)-1]) && len(d) > longest { 486 bestMatch = vh 487 longest = len(d) 488 } 489 } 490 } 491 if bestMatch != nil { 492 return bestMatch 493 } 494 // wildcard match 495 for _, vh := range rc.VirtualHosts { 496 for _, d := range vh.Domains { 497 if d == "*" { 498 return vh 499 } 500 } 501 } 502 return nil 503 } 504 505 // Follow the 8 step Sieve as in 506 // https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/listener/v3/listener_components.proto.html#config-listener-v3-filterchainmatch 507 // The implementation may initially be confusing because of a property of the 508 // Envoy algorithm - at each level we will filter out all FilterChains that do 509 // not match. This means an empty match (`{}`) may not match if another chain 510 // matches one criteria but not another. 511 func (sim *Simulation) matchFilterChain(chains []*listener.FilterChain, defaultChain *listener.FilterChain, 512 input Call, hasTLSInspector bool, 513 ) (*listener.FilterChain, error) { 514 chains = filter("DestinationPort", chains, func(fc *listener.FilterChainMatch) bool { 515 return fc.GetDestinationPort() == nil 516 }, func(fc *listener.FilterChainMatch) bool { 517 return int(fc.GetDestinationPort().GetValue()) == input.Port 518 }) 519 chains = filter("PrefixRanges", chains, func(fc *listener.FilterChainMatch) bool { 520 return fc.GetPrefixRanges() == nil 521 }, func(fc *listener.FilterChainMatch) bool { 522 ranger := cidranger.NewPCTrieRanger() 523 for _, a := range fc.GetPrefixRanges() { 524 s := fmt.Sprintf("%s/%d", a.AddressPrefix, a.GetPrefixLen().GetValue()) 525 _, cidr, err := net.ParseCIDR(s) 526 if err != nil { 527 sim.t.Fatalf("failed to parse cidr %v: %v", s, err) 528 } 529 if err := ranger.Insert(cidranger.NewBasicRangerEntry(*cidr)); err != nil { 530 sim.t.Fatalf("failed to insert cidr %v: %v", cidr, err) 531 } 532 } 533 f, err := ranger.Contains(net.ParseIP(input.Address)) 534 if err != nil { 535 sim.t.Fatalf("cidr containers %v failed: %v", input.Address, err) 536 } 537 return f 538 }) 539 chains = filter("ServerNames", chains, func(fc *listener.FilterChainMatch) bool { 540 return fc.GetServerNames() == nil 541 }, func(fc *listener.FilterChainMatch) bool { 542 sni := host.Name(input.Sni) 543 for _, s := range fc.GetServerNames() { 544 if sni.SubsetOf(host.Name(s)) { 545 return true 546 } 547 } 548 return false 549 }) 550 chains = filter("TransportProtocol", chains, func(fc *listener.FilterChainMatch) bool { 551 return fc.GetTransportProtocol() == "" 552 }, func(fc *listener.FilterChainMatch) bool { 553 if !hasTLSInspector { 554 // Without tls inspector, transport protocol will always be raw buffer 555 return fc.GetTransportProtocol() == xdsfilters.RawBufferTransportProtocol 556 } 557 switch fc.GetTransportProtocol() { 558 case xdsfilters.TLSTransportProtocol: 559 return input.TLS == TLS || input.TLS == MTLS 560 case xdsfilters.RawBufferTransportProtocol: 561 return input.TLS == Plaintext 562 } 563 return false 564 }) 565 chains = filter("ApplicationProtocols", chains, func(fc *listener.FilterChainMatch) bool { 566 return fc.GetApplicationProtocols() == nil 567 }, func(fc *listener.FilterChainMatch) bool { 568 return sets.New(fc.GetApplicationProtocols()...).Contains(input.Alpn) 569 }) 570 // We do not implement the "source" based filters as we do not use them 571 if len(chains) > 1 { 572 for _, c := range chains { 573 log.Warnf("Matched chain %v", c.Name) 574 } 575 return nil, ErrMultipleFilterChain 576 } 577 if len(chains) == 0 { 578 if defaultChain != nil { 579 return defaultChain, nil 580 } 581 return nil, ErrNoFilterChain 582 } 583 return chains[0], nil 584 } 585 586 func filter(desc string, chains []*listener.FilterChain, 587 empty func(fc *listener.FilterChainMatch) bool, 588 match func(fc *listener.FilterChainMatch) bool, 589 ) []*listener.FilterChain { 590 res := []*listener.FilterChain{} 591 anySet := false 592 for _, c := range chains { 593 if !empty(c.GetFilterChainMatch()) { 594 anySet = true 595 break 596 } 597 } 598 if !anySet { 599 log.Debugf("%v: none set, skipping", desc) 600 return chains 601 } 602 for i, c := range chains { 603 if match(c.GetFilterChainMatch()) { 604 log.Debugf("%v: matched chain %v/%v", desc, i, c.GetName()) 605 res = append(res, c) 606 } 607 } 608 // Return all matching filter chains 609 if len(res) > 0 { 610 return res 611 } 612 // Unless there were no matches - in which case we return all filter chains that did not have a 613 // match set 614 for i, c := range chains { 615 if empty(c.GetFilterChainMatch()) { 616 log.Debugf("%v: no matches, found empty chain match %v/%v", desc, i, c.GetName()) 617 res = append(res, c) 618 } 619 } 620 return res 621 } 622 623 func protocolToMTLSAlpn(s Protocol) string { 624 switch s { 625 case HTTP: 626 return "istio-http/1.1" 627 case HTTP2: 628 return "istio-h2" 629 default: 630 return "istio" 631 } 632 } 633 634 func protocolToTLSAlpn(s Protocol) string { 635 switch s { 636 case HTTP: 637 return "http/1.1" 638 case HTTP2: 639 return "h2" 640 default: 641 return "" 642 } 643 } 644 645 func protocolToAlpn(s Protocol) string { 646 switch s { 647 case HTTP: 648 return "http/1.1" 649 case HTTP2: 650 return "h2c" 651 default: 652 return "" 653 } 654 } 655 656 func matchListener(listeners []*listener.Listener, input Call) *listener.Listener { 657 if input.CallMode == CallModeInbound { 658 return xdstest.ExtractListener(model.VirtualInboundListenerName, listeners) 659 } 660 // First find exact match for the IP/Port, then fallback to wildcard IP/Port 661 // There is no wildcard port 662 for _, l := range listeners { 663 if matchAddress(l.GetAddress(), input.Address, input.Port) { 664 return l 665 } 666 } 667 for _, l := range listeners { 668 if matchAddress(l.GetAddress(), "0.0.0.0", input.Port) { 669 return l 670 } 671 } 672 673 // Fallback to the outbound listener 674 // TODO - support inbound 675 for _, l := range listeners { 676 if l.Name == model.VirtualOutboundListenerName { 677 return l 678 } 679 } 680 return nil 681 } 682 683 func matchAddress(a *envoycore.Address, address string, port int) bool { 684 if a.GetSocketAddress().GetAddress() != address { 685 return false 686 } 687 if int(a.GetSocketAddress().GetPortValue()) != port { 688 return false 689 } 690 return true 691 }