| // Copyright Istio Authors |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| package configdump |
| |
| import ( |
| "encoding/json" |
| "fmt" |
| "reflect" |
| "sort" |
| "strings" |
| "text/tabwriter" |
| ) |
| |
| import ( |
| listener "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3" |
| route "github.com/envoyproxy/go-control-plane/envoy/config/route/v3" |
| httpConn "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3" |
| tcp "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/tcp_proxy/v3" |
| "github.com/envoyproxy/go-control-plane/pkg/wellknown" |
| "sigs.k8s.io/yaml" |
| ) |
| |
| import ( |
| protio "github.com/apache/dubbo-go-pixiu/istioctl/pkg/util/proto" |
| "github.com/apache/dubbo-go-pixiu/pilot/pkg/networking/util" |
| v3 "github.com/apache/dubbo-go-pixiu/pilot/pkg/xds/v3" |
| ) |
| |
| const ( |
| // HTTPListener identifies a listener as being of HTTP type by the presence of an HTTP connection manager filter |
| HTTPListener = wellknown.HTTPConnectionManager |
| |
| // TCPListener identifies a listener as being of TCP type by the presence of TCP proxy filter |
| TCPListener = wellknown.TCPProxy |
| ) |
| |
| // ListenerFilter is used to pass filter information into listener based config writer print functions |
| type ListenerFilter struct { |
| Address string |
| Port uint32 |
| Type string |
| Verbose bool |
| } |
| |
| // Verify returns true if the passed listener matches the filter fields |
| func (l *ListenerFilter) Verify(listener *listener.Listener) bool { |
| if l.Address == "" && l.Port == 0 && l.Type == "" { |
| return true |
| } |
| if l.Address != "" && !strings.EqualFold(retrieveListenerAddress(listener), l.Address) { |
| return false |
| } |
| if l.Port != 0 && retrieveListenerPort(listener) != l.Port { |
| return false |
| } |
| if l.Type != "" && !strings.EqualFold(retrieveListenerType(listener), l.Type) { |
| return false |
| } |
| return true |
| } |
| |
| func getFilterChains(l *listener.Listener) []*listener.FilterChain { |
| res := l.FilterChains |
| if l.DefaultFilterChain != nil { |
| res = append(res, l.DefaultFilterChain) |
| } |
| return res |
| } |
| |
| // retrieveListenerType classifies a Listener as HTTP|TCP|HTTP+TCP|UNKNOWN |
| func retrieveListenerType(l *listener.Listener) string { |
| nHTTP := 0 |
| nTCP := 0 |
| for _, filterChain := range getFilterChains(l) { |
| for _, filter := range filterChain.GetFilters() { |
| if filter.Name == HTTPListener { |
| nHTTP++ |
| } else if filter.Name == TCPListener { |
| if !strings.Contains(string(filter.GetTypedConfig().GetValue()), util.BlackHoleCluster) { |
| nTCP++ |
| } |
| } |
| } |
| } |
| |
| if nHTTP > 0 { |
| if nTCP == 0 { |
| return "HTTP" |
| } |
| return "HTTP+TCP" |
| } else if nTCP > 0 { |
| return "TCP" |
| } |
| |
| return "UNKNOWN" |
| } |
| |
| func retrieveListenerAddress(l *listener.Listener) string { |
| sockAddr := l.Address.GetSocketAddress() |
| if sockAddr != nil { |
| return sockAddr.Address |
| } |
| |
| pipe := l.Address.GetPipe() |
| if pipe != nil { |
| return pipe.Path |
| } |
| |
| return "" |
| } |
| |
| func retrieveListenerPort(l *listener.Listener) uint32 { |
| return l.Address.GetSocketAddress().GetPortValue() |
| } |
| |
| // PrintListenerSummary prints a summary of the relevant listeners in the config dump to the ConfigWriter stdout |
| func (c *ConfigWriter) PrintListenerSummary(filter ListenerFilter) error { |
| w, listeners, err := c.setupListenerConfigWriter() |
| if err != nil { |
| return err |
| } |
| |
| verifiedListeners := make([]*listener.Listener, 0, len(listeners)) |
| for _, l := range listeners { |
| if filter.Verify(l) { |
| verifiedListeners = append(verifiedListeners, l) |
| } |
| } |
| |
| // Sort by port, addr, type |
| sort.Slice(verifiedListeners, func(i, j int) bool { |
| iPort := retrieveListenerPort(verifiedListeners[i]) |
| jPort := retrieveListenerPort(verifiedListeners[j]) |
| if iPort != jPort { |
| return iPort < jPort |
| } |
| iAddr := retrieveListenerAddress(verifiedListeners[i]) |
| jAddr := retrieveListenerAddress(verifiedListeners[j]) |
| if iAddr != jAddr { |
| return iAddr < jAddr |
| } |
| iType := retrieveListenerType(verifiedListeners[i]) |
| jType := retrieveListenerType(verifiedListeners[j]) |
| return iType < jType |
| }) |
| |
| if filter.Verbose { |
| fmt.Fprintln(w, "ADDRESS\tPORT\tMATCH\tDESTINATION") |
| } else { |
| fmt.Fprintln(w, "ADDRESS\tPORT\tTYPE") |
| } |
| for _, l := range verifiedListeners { |
| address := retrieveListenerAddress(l) |
| port := retrieveListenerPort(l) |
| if filter.Verbose { |
| |
| matches := retrieveListenerMatches(l) |
| sort.Slice(matches, func(i, j int) bool { |
| return matches[i].destination > matches[j].destination |
| }) |
| for _, match := range matches { |
| fmt.Fprintf(w, "%v\t%v\t%v\t%v\n", address, port, match.match, match.destination) |
| } |
| } else { |
| listenerType := retrieveListenerType(l) |
| fmt.Fprintf(w, "%v\t%v\t%v\n", address, port, listenerType) |
| } |
| } |
| return w.Flush() |
| } |
| |
| type filterchain struct { |
| match string |
| destination string |
| } |
| |
| var ( |
| plaintextHTTPALPNs = []string{"http/1.0", "http/1.1", "h2c"} |
| istioHTTPPlaintext = []string{"istio", "istio-http/1.0", "istio-http/1.1", "istio-h2"} |
| httpTLS = []string{"http/1.0", "http/1.1", "h2c", "istio-http/1.0", "istio-http/1.1", "istio-h2"} |
| tcpTLS = []string{"istio-peer-exchange", "istio"} |
| |
| protDescrs = map[string][]string{ |
| "App: HTTP TLS": httpTLS, |
| "App: Istio HTTP Plain": istioHTTPPlaintext, |
| "App: TCP TLS": tcpTLS, |
| "App: HTTP": plaintextHTTPALPNs, |
| } |
| ) |
| |
| func retrieveListenerMatches(l *listener.Listener) []filterchain { |
| fChains := getFilterChains(l) |
| resp := make([]filterchain, 0, len(fChains)) |
| for _, filterChain := range fChains { |
| match := filterChain.FilterChainMatch |
| if match == nil { |
| match = &listener.FilterChainMatch{} |
| } |
| // filterChaince also has SuffixLen, SourceType, SourcePrefixRanges which are not rendered. |
| |
| descrs := []string{} |
| if len(match.ServerNames) > 0 { |
| descrs = append(descrs, fmt.Sprintf("SNI: %s", strings.Join(match.ServerNames, ","))) |
| } |
| if len(match.TransportProtocol) > 0 { |
| descrs = append(descrs, fmt.Sprintf("Trans: %s", match.TransportProtocol)) |
| } |
| |
| if len(match.ApplicationProtocols) > 0 { |
| found := false |
| for protDescr, protocols := range protDescrs { |
| if reflect.DeepEqual(match.ApplicationProtocols, protocols) { |
| found = true |
| descrs = append(descrs, protDescr) |
| break |
| } |
| } |
| if !found { |
| descrs = append(descrs, fmt.Sprintf("App: %s", strings.Join(match.ApplicationProtocols, ","))) |
| } |
| } |
| |
| port := "" |
| if match.DestinationPort != nil { |
| port = fmt.Sprintf(":%d", match.DestinationPort.GetValue()) |
| } |
| if len(match.PrefixRanges) > 0 { |
| pf := []string{} |
| for _, p := range match.PrefixRanges { |
| pf = append(pf, fmt.Sprintf("%s/%d", p.AddressPrefix, p.GetPrefixLen().GetValue())) |
| } |
| descrs = append(descrs, fmt.Sprintf("Addr: %s%s", strings.Join(pf, ","), port)) |
| } else if port != "" { |
| descrs = append(descrs, fmt.Sprintf("Addr: *%s", port)) |
| } |
| if len(descrs) == 0 { |
| descrs = []string{"ALL"} |
| } |
| fc := filterchain{ |
| destination: getFilterType(filterChain.GetFilters()), |
| match: strings.Join(descrs, "; "), |
| } |
| resp = append(resp, fc) |
| } |
| return resp |
| } |
| |
| func getFilterType(filters []*listener.Filter) string { |
| for _, filter := range filters { |
| if filter.Name == HTTPListener { |
| httpProxy := &httpConn.HttpConnectionManager{} |
| // Allow Unmarshal to work even if Envoy and istioctl are different |
| filter.GetTypedConfig().TypeUrl = "type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager" |
| err := filter.GetTypedConfig().UnmarshalTo(httpProxy) |
| if err != nil { |
| return err.Error() |
| } |
| if httpProxy.GetRouteConfig() != nil { |
| return describeRouteConfig(httpProxy.GetRouteConfig()) |
| } |
| if httpProxy.GetRds().GetRouteConfigName() != "" { |
| return fmt.Sprintf("Route: %s", httpProxy.GetRds().GetRouteConfigName()) |
| } |
| return "HTTP" |
| } else if filter.Name == TCPListener { |
| if !strings.Contains(string(filter.GetTypedConfig().GetValue()), util.BlackHoleCluster) { |
| tcpProxy := &tcp.TcpProxy{} |
| // Allow Unmarshal to work even if Envoy and istioctl are different |
| filter.GetTypedConfig().TypeUrl = "type.googleapis.com/envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy" |
| err := filter.GetTypedConfig().UnmarshalTo(tcpProxy) |
| if err != nil { |
| return err.Error() |
| } |
| if strings.Contains(tcpProxy.GetCluster(), "Cluster") { |
| return tcpProxy.GetCluster() |
| } |
| return fmt.Sprintf("Cluster: %s", tcpProxy.GetCluster()) |
| } |
| } |
| } |
| return "Non-HTTP/Non-TCP" |
| } |
| |
| func describeRouteConfig(route *route.RouteConfiguration) string { |
| if cluster := getMatchAllCluster(route); cluster != "" { |
| return cluster |
| } |
| vhosts := []string{} |
| for _, vh := range route.GetVirtualHosts() { |
| if describeDomains(vh) == "" { |
| vhosts = append(vhosts, describeRoutes(vh)) |
| } else { |
| vhosts = append(vhosts, fmt.Sprintf("%s %s", describeDomains(vh), describeRoutes(vh))) |
| } |
| } |
| return fmt.Sprintf("Inline Route: %s", strings.Join(vhosts, "; ")) |
| } |
| |
| // If this is a route that matches everything and forwards to a cluster, just report the cluster. |
| func getMatchAllCluster(er *route.RouteConfiguration) string { |
| if len(er.GetVirtualHosts()) != 1 { |
| return "" |
| } |
| vh := er.GetVirtualHosts()[0] |
| if !reflect.DeepEqual(vh.Domains, []string{"*"}) { |
| return "" |
| } |
| if len(vh.GetRoutes()) != 1 { |
| return "" |
| } |
| r := vh.GetRoutes()[0] |
| if r.GetMatch().GetPrefix() != "/" { |
| return "" |
| } |
| a, ok := r.GetAction().(*route.Route_Route) |
| if !ok { |
| return "" |
| } |
| cl, ok := a.Route.ClusterSpecifier.(*route.RouteAction_Cluster) |
| if !ok { |
| return "" |
| } |
| if strings.Contains(cl.Cluster, "Cluster") { |
| return cl.Cluster |
| } |
| return fmt.Sprintf("Cluster: %s", cl.Cluster) |
| } |
| |
| func describeDomains(vh *route.VirtualHost) string { |
| if len(vh.GetDomains()) == 1 && vh.GetDomains()[0] == "*" { |
| return "" |
| } |
| return strings.Join(vh.GetDomains(), "/") |
| } |
| |
| func describeRoutes(vh *route.VirtualHost) string { |
| routes := make([]string, 0, len(vh.GetRoutes())) |
| for _, route := range vh.GetRoutes() { |
| routes = append(routes, describeMatch(route.GetMatch())) |
| } |
| return strings.Join(routes, ", ") |
| } |
| |
| func describeMatch(match *route.RouteMatch) string { |
| conds := []string{} |
| if match.GetPrefix() != "" { |
| conds = append(conds, fmt.Sprintf("%s*", match.GetPrefix())) |
| } |
| if match.GetPath() != "" { |
| conds = append(conds, match.GetPath()) |
| } |
| if match.GetSafeRegex() != nil { |
| conds = append(conds, fmt.Sprintf("regex %s", match.GetSafeRegex().Regex)) |
| } |
| // Ignore headers |
| return strings.Join(conds, " ") |
| } |
| |
| // PrintListenerDump prints the relevant listeners in the config dump to the ConfigWriter stdout |
| func (c *ConfigWriter) PrintListenerDump(filter ListenerFilter, outputFormat string) error { |
| _, listeners, err := c.setupListenerConfigWriter() |
| if err != nil { |
| return err |
| } |
| filteredListeners := protio.MessageSlice{} |
| for _, listener := range listeners { |
| if filter.Verify(listener) { |
| filteredListeners = append(filteredListeners, listener) |
| } |
| } |
| out, err := json.MarshalIndent(filteredListeners, "", " ") |
| if err != nil { |
| return fmt.Errorf("failed to marshal listeners: %v", err) |
| } |
| if outputFormat == "yaml" { |
| if out, err = yaml.JSONToYAML(out); err != nil { |
| return err |
| } |
| } |
| fmt.Fprintln(c.Stdout, string(out)) |
| return nil |
| } |
| |
| func (c *ConfigWriter) setupListenerConfigWriter() (*tabwriter.Writer, []*listener.Listener, error) { |
| listeners, err := c.retrieveSortedListenerSlice() |
| if err != nil { |
| return nil, nil, err |
| } |
| w := new(tabwriter.Writer).Init(c.Stdout, 0, 8, 1, ' ', 0) |
| return w, listeners, nil |
| } |
| |
| func (c *ConfigWriter) retrieveSortedListenerSlice() ([]*listener.Listener, error) { |
| if c.configDump == nil { |
| return nil, fmt.Errorf("config writer has not been primed") |
| } |
| listenerDump, err := c.configDump.GetListenerConfigDump() |
| if err != nil { |
| return nil, fmt.Errorf("listener dump: %v", err) |
| } |
| listeners := make([]*listener.Listener, 0) |
| for _, l := range listenerDump.DynamicListeners { |
| if l.ActiveState != nil && l.ActiveState.Listener != nil { |
| listenerTyped := &listener.Listener{} |
| // Support v2 or v3 in config dump. See ads.go:RequestedTypes for more info. |
| l.ActiveState.Listener.TypeUrl = v3.ListenerType |
| err = l.ActiveState.Listener.UnmarshalTo(listenerTyped) |
| if err != nil { |
| return nil, fmt.Errorf("unmarshal listener: %v", err) |
| } |
| listeners = append(listeners, listenerTyped) |
| } |
| } |
| |
| for _, l := range listenerDump.StaticListeners { |
| if l.Listener != nil { |
| listenerTyped := &listener.Listener{} |
| // Support v2 or v3 in config dump. See ads.go:RequestedTypes for more info. |
| l.Listener.TypeUrl = v3.ListenerType |
| err = l.Listener.UnmarshalTo(listenerTyped) |
| if err != nil { |
| return nil, fmt.Errorf("unmarshal listener: %v", err) |
| } |
| listeners = append(listeners, listenerTyped) |
| } |
| } |
| if len(listeners) == 0 { |
| return nil, fmt.Errorf("no listeners found") |
| } |
| return listeners, nil |
| } |