istio.io/istio@v0.0.0-20240520182934-d79c90f27776/istioctl/pkg/writer/envoy/configdump/listener.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 configdump 16 17 import ( 18 "encoding/json" 19 "fmt" 20 "reflect" 21 "sort" 22 "strings" 23 "text/tabwriter" 24 25 matcher "github.com/cncf/xds/go/xds/type/matcher/v3" 26 listener "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3" 27 route "github.com/envoyproxy/go-control-plane/envoy/config/route/v3" 28 hcm "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3" 29 tcp "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/tcp_proxy/v3" 30 "sigs.k8s.io/yaml" 31 32 "istio.io/istio/istioctl/pkg/util/proto" 33 "istio.io/istio/pilot/pkg/networking/util" 34 "istio.io/istio/pilot/pkg/util/protoconv" 35 v3 "istio.io/istio/pilot/pkg/xds/v3" 36 "istio.io/istio/pkg/wellknown" 37 ) 38 39 const ( 40 // HTTPListener identifies a listener as being of HTTP type by the presence of an HTTP connection manager filter 41 HTTPListener = wellknown.HTTPConnectionManager 42 43 // TCPListener identifies a listener as being of TCP type by the presence of TCP proxy filter 44 TCPListener = wellknown.TCPProxy 45 46 IPMatcher = "type.googleapis.com/xds.type.matcher.v3.IPMatcher" 47 ) 48 49 // ListenerFilter is used to pass filter information into listener based config writer print functions 50 type ListenerFilter struct { 51 Address string 52 Port uint32 53 Type string 54 Verbose bool 55 } 56 57 // Verify returns true if the passed listener matches the filter fields 58 func (l *ListenerFilter) Verify(listener *listener.Listener) bool { 59 if l.Address == "" && l.Port == 0 && l.Type == "" { 60 return true 61 } 62 if l.Address != "" { 63 addresses := retrieveListenerAdditionalAddresses(listener) 64 addresses = append(addresses, retrieveListenerAddress(listener)) 65 found := false 66 for _, address := range addresses { 67 if strings.EqualFold(address, l.Address) { 68 found = true 69 } 70 } 71 if !found { 72 return false 73 } 74 } 75 if l.Port != 0 && retrieveListenerPort(listener) != l.Port { 76 return false 77 } 78 if l.Type != "" && !strings.EqualFold(retrieveListenerType(listener), l.Type) { 79 return false 80 } 81 return true 82 } 83 84 func getFilterChains(l *listener.Listener) []*listener.FilterChain { 85 res := l.FilterChains 86 if l.DefaultFilterChain != nil { 87 res = append(res, l.DefaultFilterChain) 88 } 89 return res 90 } 91 92 // retrieveListenerType classifies a Listener as HTTP|TCP|HTTP+TCP|UNKNOWN 93 func retrieveListenerType(l *listener.Listener) string { 94 nHTTP := 0 95 nTCP := 0 96 for _, filterChain := range getFilterChains(l) { 97 for _, filter := range filterChain.GetFilters() { 98 if filter.Name == HTTPListener { 99 nHTTP++ 100 } else if filter.Name == TCPListener { 101 if !strings.Contains(string(filter.GetTypedConfig().GetValue()), util.BlackHoleCluster) { 102 nTCP++ 103 } 104 } 105 } 106 } 107 108 if nHTTP > 0 { 109 if nTCP == 0 { 110 return "HTTP" 111 } 112 return "HTTP+TCP" 113 } else if nTCP > 0 { 114 return "TCP" 115 } 116 117 return "UNKNOWN" 118 } 119 120 func retrieveListenerAddress(l *listener.Listener) string { 121 sockAddr := l.Address.GetSocketAddress() 122 if sockAddr != nil { 123 return sockAddr.Address 124 } 125 126 pipe := l.Address.GetPipe() 127 if pipe != nil { 128 return pipe.Path 129 } 130 131 return "" 132 } 133 134 func retrieveListenerAdditionalAddresses(l *listener.Listener) []string { 135 var addrs []string 136 socketAddresses := l.GetAdditionalAddresses() 137 for _, socketAddr := range socketAddresses { 138 addr := socketAddr.Address 139 addrs = append(addrs, addr.GetSocketAddress().Address) 140 } 141 142 return addrs 143 } 144 145 func retrieveListenerPort(l *listener.Listener) uint32 { 146 return l.Address.GetSocketAddress().GetPortValue() 147 } 148 149 func (c *ConfigWriter) PrintRemoteListenerSummary() error { 150 w, listeners, err := c.setupListenerConfigWriter() 151 if err != nil { 152 return err 153 } 154 // Sort by port, addr, type 155 sort.Slice(listeners, func(i, j int) bool { 156 if listeners[i].GetInternalListener() != nil && listeners[j].GetInternalListener() != nil { 157 return listeners[i].GetName() < listeners[j].GetName() 158 } 159 iPort := retrieveListenerPort(listeners[i]) 160 jPort := retrieveListenerPort(listeners[j]) 161 if iPort != jPort { 162 return iPort < jPort 163 } 164 iAddr := retrieveListenerAddress(listeners[i]) 165 jAddr := retrieveListenerAddress(listeners[j]) 166 if iAddr != jAddr { 167 return iAddr < jAddr 168 } 169 iType := retrieveListenerType(listeners[i]) 170 jType := retrieveListenerType(listeners[j]) 171 return iType < jType 172 }) 173 174 fmt.Fprintln(w, "LISTENER\tCHAIN\tMATCH\tDESTINATION") 175 for _, l := range listeners { 176 chains := getFilterChains(l) 177 lname := "envoy://" + l.GetName() 178 // Avoid duplicating the listener and filter name 179 if l.GetInternalListener() != nil && len(chains) == 1 && chains[0].GetName() == lname { 180 lname = "internal" 181 } 182 for _, fc := range chains { 183 184 name := fc.GetName() 185 matches := newMatcher(fc, l) 186 destination := getFilterType(fc.GetFilters()) 187 for _, match := range matches { 188 fmt.Fprintf(w, "%v\t%v\t%v\t%v\n", lname, name, match, destination) 189 } 190 } 191 } 192 return w.Flush() 193 } 194 195 func newMatcher(fc *listener.FilterChain, l *listener.Listener) []string { 196 if l.FilterChainMatcher == nil { 197 return []string{getMatches(fc.GetFilterChainMatch())} 198 } 199 switch v := l.GetFilterChainMatcher().GetOnNoMatch().GetOnMatch().(type) { 200 case *matcher.Matcher_OnMatch_Action: 201 if v.Action.GetName() == fc.GetName() { 202 return []string{"UNMATCHED"} 203 } 204 case *matcher.Matcher_OnMatch_Matcher: 205 ms, f := recurse(fc.GetName(), v.Matcher) 206 if !f { 207 return []string{"NONE"} 208 } 209 return ms 210 } 211 ms, f := recurse(fc.GetName(), l.GetFilterChainMatcher()) 212 if !f { 213 return []string{"NONE"} 214 } 215 return ms 216 } 217 218 func recurse(name string, match *matcher.Matcher) ([]string, bool) { 219 switch v := match.GetOnNoMatch().GetOnMatch().(type) { 220 case *matcher.Matcher_OnMatch_Action: 221 if v.Action.GetName() == name { 222 // TODO this only makes sense in context of a chain... do we need a way to give it context 223 return []string{"ANY"}, true 224 } 225 case *matcher.Matcher_OnMatch_Matcher: 226 ms, f := recurse(name, v.Matcher) 227 if !f { 228 return []string{"NONE"}, true 229 } 230 return ms, true 231 } 232 // TODO support list 233 n := match.GetMatcherTree().GetInput().GetName() 234 235 var m map[string]*matcher.Matcher_OnMatch 236 equality := "=" 237 switch v := match.GetMatcherTree().GetTreeType().(type) { 238 case *matcher.Matcher_MatcherTree_ExactMatchMap: 239 m = v.ExactMatchMap.Map 240 case *matcher.Matcher_MatcherTree_PrefixMatchMap: 241 m = v.PrefixMatchMap.Map 242 equality = "^" 243 case *matcher.Matcher_MatcherTree_CustomMatch: 244 tc := v.CustomMatch.GetTypedConfig() 245 switch tc.TypeUrl { 246 case IPMatcher: 247 ip := protoconv.SilentlyUnmarshalAny[matcher.IPMatcher](tc) 248 m = map[string]*matcher.Matcher_OnMatch{} 249 for _, rm := range ip.GetRangeMatchers() { 250 for _, r := range rm.Ranges { 251 cidr := r.AddressPrefix 252 pl := r.PrefixLen.GetValue() 253 if pl != 32 && pl != 128 { 254 cidr += fmt.Sprintf("/%d", pl) 255 } 256 m[cidr] = rm.OnMatch 257 } 258 } 259 default: 260 panic("unhandled") 261 } 262 } 263 outputs := []string{} 264 for k, v := range m { 265 switch v := v.GetOnMatch().(type) { 266 case *matcher.Matcher_OnMatch_Action: 267 if v.Action.GetName() == name { 268 outputs = append(outputs, fmt.Sprintf("%v%v%v", n, equality, k)) 269 } 270 continue 271 case *matcher.Matcher_OnMatch_Matcher: 272 children, match := recurse(name, v.Matcher) 273 if !match { 274 continue 275 } 276 for _, child := range children { 277 outputs = append(outputs, fmt.Sprintf("%v%v%v -> %v", n, equality, k, child)) 278 } 279 } 280 } 281 return outputs, len(outputs) > 0 282 } 283 284 // PrintListenerSummary prints a summary of the relevant listeners in the config dump to the ConfigWriter stdout 285 func (c *ConfigWriter) PrintListenerSummary(filter ListenerFilter) error { 286 w, listeners, err := c.setupListenerConfigWriter() 287 if err != nil { 288 return err 289 } 290 291 verifiedListeners := make([]*listener.Listener, 0, len(listeners)) 292 for _, l := range listeners { 293 if filter.Verify(l) { 294 verifiedListeners = append(verifiedListeners, l) 295 } 296 } 297 298 // Sort by port, addr, type 299 sort.Slice(verifiedListeners, func(i, j int) bool { 300 iPort := retrieveListenerPort(verifiedListeners[i]) 301 jPort := retrieveListenerPort(verifiedListeners[j]) 302 if iPort != jPort { 303 return iPort < jPort 304 } 305 iAddr := retrieveListenerAddress(verifiedListeners[i]) 306 jAddr := retrieveListenerAddress(verifiedListeners[j]) 307 if iAddr != jAddr { 308 return iAddr < jAddr 309 } 310 iType := retrieveListenerType(verifiedListeners[i]) 311 jType := retrieveListenerType(verifiedListeners[j]) 312 return iType < jType 313 }) 314 315 printStr := "ADDRESSES\tPORT" 316 if includeConfigType { 317 printStr = "NAME\t" + printStr 318 } 319 if filter.Verbose { 320 printStr += "\tMATCH\tDESTINATION" 321 } else { 322 printStr += "\tTYPE" 323 } 324 fmt.Fprintln(w, printStr) 325 for _, l := range verifiedListeners { 326 addresses := []string{retrieveListenerAddress(l)} 327 addresses = append(addresses, retrieveListenerAdditionalAddresses(l)...) 328 port := retrieveListenerPort(l) 329 if filter.Verbose { 330 331 matches := retrieveListenerMatches(l) 332 sort.Slice(matches, func(i, j int) bool { 333 return matches[i].destination > matches[j].destination 334 }) 335 for _, match := range matches { 336 if includeConfigType { 337 name := fmt.Sprintf("listener/%s", l.Name) 338 fmt.Fprintf(w, "%v\t%v\t%v\t%v\t%v\n", name, strings.Join(addresses, ","), port, match.match, match.destination) 339 } else { 340 fmt.Fprintf(w, "%v\t%v\t%v\t%v\n", strings.Join(addresses, ","), port, match.match, match.destination) 341 } 342 } 343 } else { 344 listenerType := retrieveListenerType(l) 345 if includeConfigType { 346 name := fmt.Sprintf("listener/%s", l.Name) 347 fmt.Fprintf(w, "%v\t%v\t%v\t%v\n", name, strings.Join(addresses, ","), port, listenerType) 348 } else { 349 fmt.Fprintf(w, "%v\t%v\t%v\n", strings.Join(addresses, ","), port, listenerType) 350 } 351 } 352 } 353 return w.Flush() 354 } 355 356 type filterchain struct { 357 match string 358 destination string 359 } 360 361 var ( 362 plaintextHTTPALPNs = []string{"http/1.0", "http/1.1", "h2c"} 363 istioHTTPPlaintext = []string{"istio", "istio-http/1.0", "istio-http/1.1", "istio-h2"} 364 httpTLS = []string{"http/1.0", "http/1.1", "h2c", "istio-http/1.0", "istio-http/1.1", "istio-h2"} 365 tcpTLS = []string{"istio-peer-exchange", "istio"} 366 367 protDescrs = map[string][]string{ 368 "App: HTTP TLS": httpTLS, 369 "App: Istio HTTP Plain": istioHTTPPlaintext, 370 "App: TCP TLS": tcpTLS, 371 "App: HTTP": plaintextHTTPALPNs, 372 } 373 ) 374 375 func retrieveListenerMatches(l *listener.Listener) []filterchain { 376 fChains := getFilterChains(l) 377 resp := make([]filterchain, 0, len(fChains)) 378 for _, filterChain := range fChains { 379 fc := filterchain{ 380 destination: getFilterType(filterChain.GetFilters()), 381 match: getMatches(filterChain.FilterChainMatch), 382 } 383 resp = append(resp, fc) 384 } 385 return resp 386 } 387 388 func getMatches(f *listener.FilterChainMatch) string { 389 match := f 390 if match == nil { 391 match = &listener.FilterChainMatch{} 392 } 393 // filterChaince also has SuffixLen, SourceType, SourcePrefixRanges which are not rendered. 394 395 descrs := []string{} 396 if len(match.ServerNames) > 0 { 397 descrs = append(descrs, fmt.Sprintf("SNI: %s", strings.Join(match.ServerNames, ","))) 398 } 399 if len(match.TransportProtocol) > 0 { 400 descrs = append(descrs, fmt.Sprintf("Trans: %s", match.TransportProtocol)) 401 } 402 403 if len(match.ApplicationProtocols) > 0 { 404 found := false 405 for protDescr, protocols := range protDescrs { 406 if reflect.DeepEqual(match.ApplicationProtocols, protocols) { 407 found = true 408 descrs = append(descrs, protDescr) 409 break 410 } 411 } 412 if !found { 413 descrs = append(descrs, fmt.Sprintf("App: %s", strings.Join(match.ApplicationProtocols, ","))) 414 } 415 } 416 417 port := "" 418 if match.DestinationPort != nil { 419 port = fmt.Sprintf(":%d", match.DestinationPort.GetValue()) 420 } 421 if len(match.PrefixRanges) > 0 { 422 pf := []string{} 423 for _, p := range match.PrefixRanges { 424 pf = append(pf, fmt.Sprintf("%s/%d", p.AddressPrefix, p.GetPrefixLen().GetValue())) 425 } 426 descrs = append(descrs, fmt.Sprintf("Addr: %s%s", strings.Join(pf, ","), port)) 427 } else if port != "" { 428 descrs = append(descrs, fmt.Sprintf("Addr: *%s", port)) 429 } 430 if len(descrs) == 0 { 431 descrs = []string{"ALL"} 432 } 433 return strings.Join(descrs, "; ") 434 } 435 436 func getFilterType(filters []*listener.Filter) string { 437 for _, filter := range filters { 438 if filter.Name == HTTPListener { 439 httpProxy := &hcm.HttpConnectionManager{} 440 // Allow Unmarshal to work even if Envoy and istioctl are different 441 filter.GetTypedConfig().TypeUrl = "type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager" 442 err := filter.GetTypedConfig().UnmarshalTo(httpProxy) 443 if err != nil { 444 return err.Error() 445 } 446 if httpProxy.GetRouteConfig() != nil { 447 return describeRouteConfig(httpProxy.GetRouteConfig()) 448 } 449 if httpProxy.GetRds().GetRouteConfigName() != "" { 450 return fmt.Sprintf("Route: %s", httpProxy.GetRds().GetRouteConfigName()) 451 } 452 return "HTTP" 453 } else if filter.Name == TCPListener { 454 if !strings.Contains(string(filter.GetTypedConfig().GetValue()), util.BlackHoleCluster) { 455 tcpProxy := &tcp.TcpProxy{} 456 // Allow Unmarshal to work even if Envoy and istioctl are different 457 filter.GetTypedConfig().TypeUrl = "type.googleapis.com/envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy" 458 err := filter.GetTypedConfig().UnmarshalTo(tcpProxy) 459 if err != nil { 460 return err.Error() 461 } 462 if strings.Contains(tcpProxy.GetCluster(), "Cluster") { 463 return tcpProxy.GetCluster() 464 } 465 return fmt.Sprintf("Cluster: %s", tcpProxy.GetCluster()) 466 } 467 } 468 } 469 return "Non-HTTP/Non-TCP" 470 } 471 472 func describeRouteConfig(route *route.RouteConfiguration) string { 473 if cluster := getMatchAllCluster(route); cluster != "" { 474 return cluster 475 } 476 vhosts := []string{} 477 for _, vh := range route.GetVirtualHosts() { 478 if describeDomains(vh) == "" { 479 vhosts = append(vhosts, describeRoutes(vh)) 480 } else { 481 vhosts = append(vhosts, fmt.Sprintf("%s %s", describeDomains(vh), describeRoutes(vh))) 482 } 483 } 484 return fmt.Sprintf("Inline Route: %s", strings.Join(vhosts, "; ")) 485 } 486 487 // If this is a route that matches everything and forwards to a cluster, just report the cluster. 488 func getMatchAllCluster(er *route.RouteConfiguration) string { 489 if len(er.GetVirtualHosts()) != 1 { 490 return "" 491 } 492 vh := er.GetVirtualHosts()[0] 493 if !reflect.DeepEqual(vh.Domains, []string{"*"}) { 494 return "" 495 } 496 if len(vh.GetRoutes()) != 1 { 497 return "" 498 } 499 r := vh.GetRoutes()[0] 500 if r.GetMatch().GetPrefix() != "/" { 501 return "" 502 } 503 a, ok := r.GetAction().(*route.Route_Route) 504 if !ok { 505 return "" 506 } 507 cl, ok := a.Route.ClusterSpecifier.(*route.RouteAction_Cluster) 508 if !ok { 509 return "" 510 } 511 if strings.Contains(cl.Cluster, "Cluster") { 512 return cl.Cluster 513 } 514 return fmt.Sprintf("Cluster: %s", cl.Cluster) 515 } 516 517 func describeDomains(vh *route.VirtualHost) string { 518 if len(vh.GetDomains()) == 1 && vh.GetDomains()[0] == "*" { 519 return "" 520 } 521 return strings.Join(vh.GetDomains(), "/") 522 } 523 524 func describeRoutes(vh *route.VirtualHost) string { 525 routes := make([]string, 0, len(vh.GetRoutes())) 526 for _, route := range vh.GetRoutes() { 527 routes = append(routes, describeMatch(route.GetMatch())) 528 } 529 return strings.Join(routes, ", ") 530 } 531 532 func describeMatch(match *route.RouteMatch) string { 533 conds := []string{} 534 if match.GetPrefix() != "" { 535 conds = append(conds, fmt.Sprintf("%s*", match.GetPrefix())) 536 } 537 if match.GetPathSeparatedPrefix() != "" { 538 conds = append(conds, fmt.Sprintf("PathPrefix:%s", match.GetPathSeparatedPrefix())) 539 } 540 if match.GetPath() != "" { 541 conds = append(conds, match.GetPath()) 542 } 543 if match.GetSafeRegex() != nil { 544 conds = append(conds, fmt.Sprintf("regex %s", match.GetSafeRegex().Regex)) 545 } 546 // Ignore headers 547 return strings.Join(conds, " ") 548 } 549 550 // PrintListenerDump prints the relevant listeners in the config dump to the ConfigWriter stdout 551 func (c *ConfigWriter) PrintListenerDump(filter ListenerFilter, outputFormat string) error { 552 _, listeners, err := c.setupListenerConfigWriter() 553 if err != nil { 554 return err 555 } 556 filteredListeners := proto.MessageSlice{} 557 for _, listener := range listeners { 558 if filter.Verify(listener) { 559 filteredListeners = append(filteredListeners, listener) 560 } 561 } 562 out, err := json.MarshalIndent(filteredListeners, "", " ") 563 if err != nil { 564 return fmt.Errorf("failed to marshal listeners: %v", err) 565 } 566 if outputFormat == "yaml" { 567 if out, err = yaml.JSONToYAML(out); err != nil { 568 return err 569 } 570 } 571 fmt.Fprintln(c.Stdout, string(out)) 572 return nil 573 } 574 575 func (c *ConfigWriter) setupListenerConfigWriter() (*tabwriter.Writer, []*listener.Listener, error) { 576 listeners, err := c.retrieveSortedListenerSlice() 577 if err != nil { 578 return nil, nil, err 579 } 580 w := new(tabwriter.Writer).Init(c.Stdout, 0, 8, 1, ' ', 0) 581 return w, listeners, nil 582 } 583 584 func (c *ConfigWriter) retrieveSortedListenerSlice() ([]*listener.Listener, error) { 585 if c.configDump == nil { 586 return nil, fmt.Errorf("config writer has not been primed") 587 } 588 listenerDump, err := c.configDump.GetListenerConfigDump() 589 if err != nil { 590 return nil, fmt.Errorf("listener dump: %v", err) 591 } 592 listeners := make([]*listener.Listener, 0) 593 for _, l := range listenerDump.DynamicListeners { 594 if l.ActiveState != nil && l.ActiveState.Listener != nil { 595 listenerTyped := &listener.Listener{} 596 // Support v2 or v3 in config dump. See ads.go:RequestedTypes for more info. 597 l.ActiveState.Listener.TypeUrl = v3.ListenerType 598 err = l.ActiveState.Listener.UnmarshalTo(listenerTyped) 599 if err != nil { 600 return nil, fmt.Errorf("unmarshal listener: %v", err) 601 } 602 listeners = append(listeners, listenerTyped) 603 } 604 } 605 606 for _, l := range listenerDump.StaticListeners { 607 if l.Listener != nil { 608 listenerTyped := &listener.Listener{} 609 // Support v2 or v3 in config dump. See ads.go:RequestedTypes for more info. 610 l.Listener.TypeUrl = v3.ListenerType 611 err = l.Listener.UnmarshalTo(listenerTyped) 612 if err != nil { 613 return nil, fmt.Errorf("unmarshal listener: %v", err) 614 } 615 listeners = append(listeners, listenerTyped) 616 } 617 } 618 if len(listeners) == 0 { 619 return nil, fmt.Errorf("no listeners found") 620 } 621 return listeners, nil 622 }