From 04be552ab950fe8329df75e1de24ed89c7b9ebd2 Mon Sep 17 00:00:00 2001 From: "Toader, Sebastian" Date: Mon, 27 Mar 2023 13:41:07 +0200 Subject: [PATCH 1/6] Support returning the filter chain mathcing inbound/outbound connections --- pkg/ads/api.go | 63 ++++ pkg/ads/api_http_client.go | 32 +- pkg/ads/api_listener_properties.go | 174 +++++----- pkg/ads/api_tcp_client.go | 159 +++++++-- pkg/ads/common.go | 13 + pkg/ads/e2etest/suite_test.go | 23 ++ pkg/ads/internal/filterchain/filterchain.go | 349 ++++++++++++++++++++ pkg/ads/internal/listener/listener.go | 4 +- 8 files changed, 686 insertions(+), 131 deletions(-) create mode 100644 pkg/ads/internal/filterchain/filterchain.go diff --git a/pkg/ads/api.go b/pkg/ads/api.go index 494cd7d1..5fee63c5 100644 --- a/pkg/ads/api.go +++ b/pkg/ads/api.go @@ -85,6 +85,61 @@ type Client interface { DecrementActiveRequestsCount(address string) } +// NetworkFilter represents a network filter configuration which a filter chain consists of +type NetworkFilter interface { + // Name returns the name of the network filter configuration + Name() string + + // Configuration returns filter specific configuration which depends on the filter being instantiated. + Configuration() map[string]interface{} +} + +// ConnectionOptions holds the inbound connections' options used to determine the +// filter chain to be instantiated for the stream +type ConnectionOptions struct { + // destinationPort is the destination port of the connection + destinationPort uint32 + + // serverName is the server name used with TLS connections + serverName string + + // transportProtocol is the transport protocol of the connection + transportProtocol string + + // applicationProtocols is the list of application protocols (e.g. ALPN for TLS protocol) of the connection + applicationProtocols []string +} + +type ConnectionOption func(*ConnectionOptions) + +// ConnectionWithDestinationPort specifies the given destination port as connection option +func ConnectionWithDestinationPort(destinationPort uint32) ConnectionOption { + return func(o *ConnectionOptions) { + o.destinationPort = destinationPort + } +} + +// TLSConnectionWithServerName specifies the given server name as TLS connection option +func TLSConnectionWithServerName(serverName string) ConnectionOption { + return func(o *ConnectionOptions) { + o.serverName = serverName + } +} + +// ConnectionWithTransportProtocol specifies the given transport protocol as connection option +func ConnectionWithTransportProtocol(transportProtocol string) ConnectionOption { + return func(o *ConnectionOptions) { + o.transportProtocol = transportProtocol + } +} + +// ConnectionWithApplicationProtocols specifies the given application protocols as connection option +func ConnectionWithApplicationProtocols(applicationProtocols []string) ConnectionOption { + return func(o *ConnectionOptions) { + o.applicationProtocols = applicationProtocols + } +} + // ListenerPropertiesResponse contains the result for the API call // to retrieve ListenerProperties for a given address. type ListenerPropertiesResponse interface { @@ -107,6 +162,10 @@ type ListenerProperties interface { // Metadata returns the metadata associated with this listener Metadata() map[string]interface{} + + // NetworkFilters returns the network filter chain that inbound traffic flows through + // when client workload connects with the given connection options. + NetworkFilters(connectionsOpts ...ConnectionOption) ([]NetworkFilter, error) } // ClientPropertiesResponse contains the result for the API call @@ -135,6 +194,10 @@ type ClientProperties interface { // Metadata returns the metadata associated with the target service Metadata() map[string]interface{} + + // NetworkFilters returns the network filter chain that outbound traffic flows through to the target service + // when client workload connects with the given connection options + NetworkFilters(connectionsOpts ...ConnectionOption) ([]NetworkFilter, error) } // HTTPClientPropertiesResponse contains the result for the API call diff --git a/pkg/ads/api_http_client.go b/pkg/ads/api_http_client.go index 7603b545..2f6f6383 100644 --- a/pkg/ads/api_http_client.go +++ b/pkg/ads/api_http_client.go @@ -152,35 +152,35 @@ func (c *client) getHTTPClientPropertiesByHost(input getHTTPClientPropertiesByHo return nil, nil } - cluster, route, err := c.getHttpClientTargetCluster(input.host, int(input.port)) + hostIPs, err := c.resolveHost(input.host) if err != nil { - return nil, errors.WrapIf(err, "couldn't get target cluster") + return nil, errors.WrapIff(err, "couldn't resolve host %q", input.host) } - clientProps, err := c.newClientProperties(cluster, route) + listener, err := c.getHTTPOutboundListener(hostIPs, int(input.port)) if err != nil { - return nil, errors.WrapIff(err, "couldn't create client properties for target service at %s:%d", input.host, input.port) + return nil, errors.WrapIff(err, "couldn't get HTTP outbound listener for address: %s:%d", input.host, input.port) } - return clientProps, nil -} - -// getHttpClientTargetCluster returns the upstream cluster which HTTP traffic is directed to when -// clients connect to host:port -func (c *client) getHttpClientTargetCluster(host string, port int) (*envoy_config_cluster_v3.Cluster, *envoy_config_route_v3.Route, error) { - hostIPs, err := c.resolveHost(host) + cluster, route, err := c.getHttpClientTargetCluster(input.host, int(input.port), hostIPs, listener) if err != nil { - return nil, nil, errors.WrapIff(err, "couldn't resolve host %q", host) + return nil, errors.WrapIff(err, "couldn't get target cluster for address: %s:%d", input.host, input.port) } - lstnr, err := c.getHTTPOutboundListener(hostIPs, port) + clientProps, err := c.newClientProperties(cluster, listener, route) if err != nil { - return nil, nil, errors.WrapIff(err, "couldn't get HTTP outbound listener for address: %s:%d", host, port) + return nil, errors.WrapIff(err, "couldn't create client properties for target service at %s:%d", input.host, input.port) } - routeConfigName := listener.GetRouteConfigName(lstnr) + return clientProps, nil +} + +// getHttpClientTargetCluster returns the upstream cluster which HTTP traffic is directed to when +// clients connect to host:port +func (c *client) getHttpClientTargetCluster(host string, port int, hostIPs []net.IP, targetListener *envoy_config_listener_v3.Listener) (*envoy_config_cluster_v3.Cluster, *envoy_config_route_v3.Route, error) { + routeConfigName := listener.GetRouteConfigName(targetListener) if routeConfigName == "" { - return nil, nil, errors.Errorf("no route config found for address: %s:%d", host, port) + return nil, nil, errors.New("couldn't determine route config") } routeConfig, err := c.getRouteConfig(routeConfigName) if err != nil { diff --git a/pkg/ads/api_listener_properties.go b/pkg/ads/api_listener_properties.go index f57d4477..9fb57a3c 100644 --- a/pkg/ads/api_listener_properties.go +++ b/pkg/ads/api_listener_properties.go @@ -16,12 +16,17 @@ package ads import ( "context" + "encoding/json" "fmt" "net" "net/url" "reflect" "strconv" + "google.golang.org/protobuf/encoding/protojson" + + "github.com/cisco-open/nasp/pkg/ads/internal/filterchain" + "github.com/cisco-open/nasp/pkg/ads/internal/listener" "github.com/cisco-open/nasp/pkg/ads/internal/util" @@ -36,6 +41,7 @@ type listenerProperties struct { permissive bool requireClientCertificate bool metadata map[string]interface{} + inboundListener *envoy_config_listener_v3.Listener } func (lp *listenerProperties) UseTLS() bool { @@ -54,6 +60,84 @@ func (lp *listenerProperties) Metadata() map[string]interface{} { return lp.metadata } +func (lp *listenerProperties) NetworkFilters(connectionsOpts ...ConnectionOption) ([]NetworkFilter, error) { + if lp.inboundListener == nil { + return nil, nil + } + + var connOpts ConnectionOptions + for _, opt := range connectionsOpts { + opt(&connOpts) + } + + var filterChainMatchOpts []filterchain.MatchOption + + if connOpts.destinationPort > 0 { + filterChainMatchOpts = append(filterChainMatchOpts, filterchain.WithDestinationPort(connOpts.destinationPort)) + } + if len(connOpts.transportProtocol) > 0 { + filterChainMatchOpts = append(filterChainMatchOpts, filterchain.WithTransportProtocol(connOpts.transportProtocol)) + } + if len(connOpts.applicationProtocols) > 0 { + filterChainMatchOpts = append(filterChainMatchOpts, filterchain.WithApplicationProtocols(connOpts.applicationProtocols)) + } + + filterChains, err := filterchain.Filter(lp.inboundListener, filterChainMatchOpts...) + if err != nil { + return nil, err + } + + if len(filterChains) == 0 && lp.inboundListener.GetDefaultFilterChain() != nil { + // if no filter chains found, use default filter chain of the listener + filterChains = append(filterChains, lp.inboundListener.GetDefaultFilterChain()) + } + + if len(filterChains) == 0 { + return nil, errors.Errorf("couldn't find a filter chain for listener %q, with matching fields:%s", + lp.inboundListener.GetName(), + filterChainMatchOpts) + } + if len(filterChains) > 1 { + fcNames := make([]string, 0, len(filterChains)) + for _, fc := range filterChains { + fcNames = append(fcNames, fc.GetName()) + } + return nil, errors.Errorf("multiple filter chains for listener %q, with matching fields:%s, filter chains:%s", + lp.inboundListener.GetName(), + filterChainMatchOpts, + fcNames) + } + + networkFilters := make([]NetworkFilter, 0, len(filterChains[0].GetFilters())) + for _, filter := range filterChains[0].GetFilters() { + if filter == nil { + continue + } + + configuration := make(map[string]interface{}) + proto, err := filter.GetTypedConfig().UnmarshalNew() + if err != nil { + return nil, err + } + + configurationJson, err := protojson.Marshal(proto) + if err != nil { + return nil, err + } + + if err = json.Unmarshal(configurationJson, &configuration); err != nil { + return nil, err + } + + networkFilters = append(networkFilters, &networkFilter{ + name: filter.GetName(), + configuration: configuration, + }) + } + + return networkFilters, nil +} + func (lp *listenerProperties) String() string { return fmt.Sprintf("{useTLS=%t, permissive=%t, isClientCertificateRequired=%t}", lp.useTLS, lp.permissive, lp.requireClientCertificate) } @@ -183,8 +267,13 @@ func (c *client) getListenerProperties(input getListenerPropertiesInput) (Listen return nil, errors.WrapIf(err, "couldn't list inbound listeners for address") } for _, lstnr := range listeners { - // matching rules https://github.com/envoyproxy/go-control-plane/blob/v0.9.9/envoy/config/listener/v3/listener_components.pb.go#L211 - filterChains, err := findFilterChain(lstnr.GetFilterChains(), input.port, net.ParseIP(input.host)) + // find listener's filter chains that are matching the first 2 steps of the rules described here + // https://github.com/envoyproxy/go-control-plane/blob/v0.9.9/envoy/config/listener/v3/listener_components.pb.go#L211 + // which is enough to determine the properties of a workload listener + filterChains, err := filterchain.Filter(lstnr, + filterchain.WithDestinationPort(input.port), + filterchain.WithDestinationIP(net.ParseIP(input.host))) + if err != nil { return nil, err } @@ -231,6 +320,7 @@ func (c *client) getListenerProperties(input getListenerPropertiesInput) (Listen // shows whether client certificate is required requireClientCertificate: requireClientCertificate, metadata: metadata, + inboundListener: matchedListener, } } } @@ -241,83 +331,3 @@ func (c *client) getListenerProperties(input getListenerPropertiesInput) (Listen return lp, nil } - -// findFilterChain returns filter chain items from the provided -// 'filterChains' that are matching the first 2 steps of the rules described here -// https://github.com/envoyproxy/go-control-plane/blob/v0.9.9/envoy/config/listener/v3/listener_components.pb.go#L211 -// which is enough to determine the properties of a workload listener -func findFilterChain(filterChains []*envoy_config_listener_v3.FilterChain, port uint32, ip net.IP) ([]*envoy_config_listener_v3.FilterChain, error) { - // 1. match by destination port - var fcsMatchedByPort []*envoy_config_listener_v3.FilterChain - - // match by exact destination port first as that is the most specific match - // if there are no matches by specific destination port then check the next most specific - // match which is the filter chain matches with no destination port - dstPortsToMatch := []uint32{port, 0} - for _, matchPort := range dstPortsToMatch { - for _, fc := range filterChains { - fcm := fc.GetFilterChainMatch() - - dstPort := uint32(0) - if fcm.GetDestinationPort() != nil { - dstPort = fcm.GetDestinationPort().GetValue() - } - - if matchPort == dstPort { - fcsMatchedByPort = append(fcsMatchedByPort, fc) - } - } - - if len(fcsMatchedByPort) > 0 { - break - } - } - - // 2. match destination IP address - var fcsMatchedByDstIP []*envoy_config_listener_v3.FilterChain - for _, fc := range fcsMatchedByPort { - cidrs := fc.GetFilterChainMatch().GetPrefixRanges() - if cidrs != nil { - // verify destination IP address matches any of the cidrs of the filter chain - ok, err := matchPrefixRanges(cidrs, ip) - if err != nil { - return nil, err - } - if ok { - fcsMatchedByDstIP = append(fcsMatchedByDstIP, fc) - } - } - } - - // if there are no filter chain matches by CIDR than the next most specific matches are those - // which don't have a CIDR defined - if len(fcsMatchedByDstIP) == 0 { - // if there is no CIDR specified for the filter chain main - for _, fc := range fcsMatchedByPort { - if fc.GetFilterChainMatch().GetPrefixRanges() == nil { - fcsMatchedByDstIP = append(fcsMatchedByDstIP, fc) - } - } - } - - return fcsMatchedByDstIP, nil -} - -func matchPrefixRanges(prefixRanges []*envoy_config_core_v3.CidrRange, ip net.IP) (bool, error) { - for _, cidr := range prefixRanges { - if cidr.GetAddressPrefix() == "" { - continue - } - - cidrStr := fmt.Sprintf("%s/%d", cidr.GetAddressPrefix(), cidr.GetPrefixLen().GetValue()) - _, ipnet, err := net.ParseCIDR(cidrStr) - if err != nil { - return false, errors.WrapIff(err, "couldn't parse address prefix: %q", cidrStr) - } - - if ipnet.Contains(ip) { - return true, nil - } - } - return false, nil -} diff --git a/pkg/ads/api_tcp_client.go b/pkg/ads/api_tcp_client.go index d7aa0338..a3de9c7d 100644 --- a/pkg/ads/api_tcp_client.go +++ b/pkg/ads/api_tcp_client.go @@ -16,12 +16,17 @@ package ads import ( "context" + "encoding/json" "fmt" "net" "net/url" "reflect" "strconv" + "google.golang.org/protobuf/encoding/protojson" + + "github.com/cisco-open/nasp/pkg/ads/internal/filterchain" + "github.com/cisco-open/nasp/pkg/ads/internal/listener" "github.com/cisco-open/nasp/pkg/ads/internal/loadbalancer" @@ -42,11 +47,12 @@ import ( ) type clientProperties struct { - useTLS bool - permissive bool - serverName string - address net.Addr - metadata map[string]interface{} + useTLS bool + permissive bool + serverName string + address net.Addr + metadata map[string]interface{} + outboundListener *envoy_config_listener_v3.Listener } func (p *clientProperties) UseTLS() bool { @@ -80,6 +86,94 @@ func (p *clientProperties) String() string { return fmt.Sprintf("{serverName=%s, useTLS=%t, permissive=%t, address=%s}", p.ServerName(), p.UseTLS(), p.Permissive(), addr) } +func (p *clientProperties) NetworkFilters(connectionsOpts ...ConnectionOption) ([]NetworkFilter, error) { + if p.outboundListener == nil { + return nil, nil + } + + var connOpts ConnectionOptions + for _, opt := range connectionsOpts { + opt(&connOpts) + } + + var filterChainMatchOpts []filterchain.MatchOption + if p.address != nil { + tcpAddress, ok := p.address.(*net.TCPAddr) + if !ok { + return nil, errors.Errorf("expected *TCPAddress but got: %T", p.address) + } + + filterChainMatchOpts = append(filterChainMatchOpts, + filterchain.WithDestinationPort(uint32(tcpAddress.Port)), + filterchain.WithDestinationIP(tcpAddress.IP)) + } + if len(connOpts.serverName) > 0 { + filterChainMatchOpts = append(filterChainMatchOpts, filterchain.WithServerName(connOpts.serverName)) + } + if len(connOpts.transportProtocol) > 0 { + filterChainMatchOpts = append(filterChainMatchOpts, filterchain.WithTransportProtocol(connOpts.transportProtocol)) + } + if len(connOpts.applicationProtocols) > 0 { + filterChainMatchOpts = append(filterChainMatchOpts, filterchain.WithApplicationProtocols(connOpts.applicationProtocols)) + } + + filterChains, err := filterchain.Filter(p.outboundListener, filterChainMatchOpts...) + if err != nil { + return nil, err + } + + if len(filterChains) == 0 && p.outboundListener.GetDefaultFilterChain() != nil { + // if no filter chains found, use default filter chain of the listener + filterChains = append(filterChains, p.outboundListener.GetDefaultFilterChain()) + } + + if len(filterChains) == 0 { + return nil, errors.Errorf("couldn't find a filter chain for listener %q, with matching fields:%s", + p.outboundListener.GetName(), + filterChainMatchOpts) + } + + if len(filterChains) > 1 { + fcNames := make([]string, 0, len(filterChains)) + for _, fc := range filterChains { + fcNames = append(fcNames, fc.GetName()) + } + return nil, errors.Errorf("multiple filter chains for listener %q, with matching fields:%s, filter chains:%s", + p.outboundListener.GetName(), + filterChainMatchOpts, + fcNames) + } + + networkFilters := make([]NetworkFilter, 0, len(filterChains[0].GetFilters())) + for _, filter := range filterChains[0].GetFilters() { + if filter == nil { + continue + } + + configuration := make(map[string]interface{}) + proto, err := filter.GetTypedConfig().UnmarshalNew() + if err != nil { + return nil, err + } + + configurationJson, err := protojson.Marshal(proto) + if err != nil { + return nil, err + } + + if err = json.Unmarshal(configurationJson, &configuration); err != nil { + return nil, err + } + + networkFilters = append(networkFilters, &networkFilter{ + name: filter.GetName(), + configuration: configuration, + }) + } + + return networkFilters, nil +} + type clientPropertiesResponse struct { result ClientProperties err error @@ -196,34 +290,34 @@ func (c *client) getTCPClientPropertiesByHost(input getTCPClientPropertiesByHost return nil, nil } - cluster, err := c.getTCPClientTargetCluster(input.host, int(input.port)) + hostIPs, err := c.resolveHost(input.host) if err != nil { - return nil, errors.WrapIf(err, "couldn't get target upstream cluster") + return nil, errors.WrapIff(err, "couldn't resolve host %q", input.host) } - clientProps, err := c.newClientProperties(cluster, nil) + listener, err := c.getTCPOutboundListener(hostIPs, int(input.port)) if err != nil { - return nil, errors.WrapIff(err, "couldn't create client properties for target service at %s:%d", input.host, input.port) + return nil, errors.WrapIff(err, "couldn't get TCP outbound listener for address: %s:%d", input.host, input.port) } - return clientProps, nil -} - -// getTCPClientTargetCluster returns the Envoy upstream cluster which TCP traffic is directed to when -// clients connect to host:port -func (c *client) getTCPClientTargetCluster(host string, port int) (*envoy_config_cluster_v3.Cluster, error) { - hostIPs, err := c.resolveHost(host) + cluster, err := c.getTCPClientTargetCluster(listener) if err != nil { - return nil, errors.WrapIff(err, "couldn't resolve host %q", host) + return nil, errors.WrapIff(err, "couldn't get target upstream cluster for address: %s:%d", input.host, input.port) } - listener, err := c.getTCPOutboundListener(hostIPs, port) + clientProps, err := c.newClientProperties(cluster, listener, nil) if err != nil { - return nil, errors.WrapIff(err, "couldn't get TCP outbound listener for address: %s:%d", host, port) + return nil, errors.WrapIff(err, "couldn't create client properties for target service at %s:%d", input.host, input.port) } + return clientProps, nil +} + +// getTCPClientTargetCluster returns the Envoy upstream cluster which TCP traffic is directed to when +// clients connect to tcp service listening on the address described by the provided listener +func (c *client) getTCPClientTargetCluster(targetListener *envoy_config_listener_v3.Listener) (*envoy_config_cluster_v3.Cluster, error) { var clusterName string - for _, fc := range listener.GetFilterChains() { + for _, fc := range targetListener.GetFilterChains() { for _, f := range fc.GetFilters() { if f.GetName() != wellknown.TCPProxy { continue @@ -235,11 +329,16 @@ func (c *client) getTCPClientTargetCluster(host string, port int) (*envoy_config } if tcpProxy.GetCluster() != "" { + if clusterName != "" { + return nil, errors.New("multiple clusters found for outbound traffic") + } clusterName = tcpProxy.GetCluster() // traffic is routed to single upstream cluster - break } if tcpProxy.GetWeightedClusters() != nil { + if clusterName != "" { + return nil, errors.New("multiple clusters found for outbound traffic") + } // traffic is routed to multiple upstream clusters according to cluster weights clustersWeightMap := make(map[string]uint32) for _, weightedCluster := range tcpProxy.GetWeightedClusters().GetClusters() { @@ -247,15 +346,12 @@ func (c *client) getTCPClientTargetCluster(host string, port int) (*envoy_config } clusterName = cluster.SelectCluster(clustersWeightMap, c.clustersStats) - if clusterName != "" { - break - } } } } if clusterName == "" { - return nil, errors.Errorf("no cluster found for outbound traffic for address: %s:%d", host, port) + return nil, errors.New("no cluster found for outbound traffic") } cluster, err := c.getCluster(clusterName) @@ -308,7 +404,7 @@ func (c *client) getTCPOutboundListener(hostIPs []net.IP, port int) (*envoy_conf // newClientProperties returns a new clientProperties instance populated with data from the given cluster and route // and selecting endpoint address according to the LB policy of the cluster -func (c *client) newClientProperties(cl *envoy_config_cluster_v3.Cluster, route *envoy_config_route_v3.Route) (*clientProperties, error) { +func (c *client) newClientProperties(cl *envoy_config_cluster_v3.Cluster, listener *envoy_config_listener_v3.Listener, route *envoy_config_route_v3.Route) (*clientProperties, error) { cla, err := c.getClusterLoadAssignmentForCluster(cl) if err != nil { return nil, errors.WrapIff(err, "couldn't list endpoints, cluster name=%q", cl.GetName()) @@ -405,11 +501,12 @@ func (c *client) newClientProperties(cl *envoy_config_cluster_v3.Cluster, route transports := cluster.GetMatchingTransportSockets(cl, endpointMatchMetadata) clientProps := &clientProperties{ - permissive: cluster.IsPermissive(transports), - serverName: cluster.GetTlsServerName(transports), - useTLS: cluster.UsesTls(transports), - address: endpointAddress, - metadata: metadata, + permissive: cluster.IsPermissive(transports), + serverName: cluster.GetTlsServerName(transports), + useTLS: cluster.UsesTls(transports), + address: endpointAddress, + metadata: metadata, + outboundListener: listener, } return clientProps, nil diff --git a/pkg/ads/common.go b/pkg/ads/common.go index 3f7cb701..d9cf267f 100644 --- a/pkg/ads/common.go +++ b/pkg/ads/common.go @@ -468,3 +468,16 @@ func newEmptyResource(resourceType resource_v3.Type) proto.Message { return nil } } + +type networkFilter struct { + name string + configuration map[string]interface{} +} + +func (n *networkFilter) Name() string { + return n.name +} + +func (n *networkFilter) Configuration() map[string]interface{} { + return n.configuration +} diff --git a/pkg/ads/e2etest/suite_test.go b/pkg/ads/e2etest/suite_test.go index a3be952d..92ff0272 100644 --- a/pkg/ads/e2etest/suite_test.go +++ b/pkg/ads/e2etest/suite_test.go @@ -396,6 +396,21 @@ var _ = Describe("The management server is running", func() { Expect(listenerProps.UseTLS()).To(BeTrue()) Expect(listenerProps.IsClientCertificateRequired()).To(BeTrue()) Expect(listenerProps.Permissive()).To(BeTrue()) + // network filter chain for incoming TLS connection + Expect(listenerProps.NetworkFilters( + ads.ConnectionWithDestinationPort(8080), + ads.ConnectionWithTransportProtocol("tls"), + ads.ConnectionWithApplicationProtocols([]string{"istio"})), + ).Should(HaveExactElements( + HaveField("Name()", "istio.metadata_exchange"), + HaveField("Name()", "envoy.filters.network.http_connection_manager"))) + // network filter chain for incoming non-TLS connection + Expect(listenerProps.NetworkFilters( + ads.ConnectionWithDestinationPort(8080), + ads.ConnectionWithTransportProtocol("raw_buffer")), + ).Should(HaveExactElements( + HaveField("Name()", "istio.metadata_exchange"), + HaveField("Name()", "envoy.filters.network.http_connection_manager"))) By("verify that correct tcp client properties are returned for 10.10.42.110:12050") tcpResp, err := adsClient.GetTCPClientPropertiesByHost(ctx, "10.10.42.110:12050") @@ -418,6 +433,10 @@ var _ = Describe("The management server is running", func() { Expect(tcpClientProps.UseTLS()).To(BeTrue()) Expect(tcpClientProps.ServerName()).To(Equal("outbound_.12050_._.echo.demo.svc.cluster.local")) Expect(tcpClientProps.Address()).To(Equal(&net.TCPAddr{IP: net.ParseIP("10.20.160.131"), Port: 8080})) + Expect(tcpClientProps.NetworkFilters()).Should(HaveExactElements( + HaveField("Name()", "istio.stats"), + HaveField("Name()", "envoy.filters.network.tcp_proxy"), + )) By("verify that correct http client properties are returned for 10.10.42.110:80") httpResp, err := adsClient.GetHTTPClientPropertiesByHost(ctx, "10.10.42.110:80") @@ -440,6 +459,10 @@ var _ = Describe("The management server is running", func() { Expect(httpClientProps.UseTLS()).To(BeTrue()) Expect(httpClientProps.ServerName()).To(Equal("outbound_.80_._.echo.demo.svc.cluster.local")) Expect(httpClientProps.Address()).To(Equal(&net.TCPAddr{IP: net.ParseIP("10.20.160.131"), Port: 8080})) + Expect(tcpClientProps.NetworkFilters()).To(HaveExactElements( + HaveField("Name()", "istio.stats"), + HaveField("Name()", "envoy.filters.network.tcp_proxy"), + )) // --- load config_v1.json which contains three echo endpoints in the demo namespace as the 'echo' deployment has been scaled up to three // replicas into Management server diff --git a/pkg/ads/internal/filterchain/filterchain.go b/pkg/ads/internal/filterchain/filterchain.go new file mode 100644 index 00000000..65a2314d --- /dev/null +++ b/pkg/ads/internal/filterchain/filterchain.go @@ -0,0 +1,349 @@ +// Copyright (c) 2023 Cisco and/or its affiliates. All rights reserved. +// +// 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 +// +// https://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 filterchain + +import ( + "fmt" + "net" + "strings" + + "emperror.dev/errors" + envoy_config_core_v3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" + envoy_config_listener_v3 "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3" +) + +type matchCriteria struct { + destinationPort uint32 + destinationIP net.IP + serverName string + transportProtocol string + applicationProtocols []string +} + +type MatchOption interface { + applyTo(*matchCriteria) + String() string +} + +type destinationPortMatchOption struct { + destinationPort uint32 +} + +func WithDestinationPort(port uint32) *destinationPortMatchOption { + return &destinationPortMatchOption{destinationPort: port} +} + +func (o *destinationPortMatchOption) applyTo(opts *matchCriteria) { + opts.destinationPort = o.destinationPort +} + +func (o *destinationPortMatchOption) String() string { + return fmt.Sprintf("destination port=%d", o.destinationPort) +} + +type destinationIPMatchOption struct { + destinationIP net.IP +} + +func WithDestinationIP(ip net.IP) *destinationIPMatchOption { + return &destinationIPMatchOption{destinationIP: ip} +} + +func (o *destinationIPMatchOption) applyTo(opts *matchCriteria) { + opts.destinationIP = o.destinationIP +} + +func (o *destinationIPMatchOption) String() string { + return fmt.Sprintf("destination IP=%s", o.destinationIP) +} + +type serverNameMatchOption struct { + serverName string +} + +func (o *serverNameMatchOption) applyTo(opts *matchCriteria) { + opts.serverName = o.serverName +} + +func (o *serverNameMatchOption) String() string { + return fmt.Sprintf("server name=%s", o.serverName) +} + +func WithServerName(serverName string) *serverNameMatchOption { + return &serverNameMatchOption{serverName: serverName} +} + +type transportProtocolMatchOption struct { + transportProtocol string +} + +func (o *transportProtocolMatchOption) applyTo(opts *matchCriteria) { + opts.transportProtocol = o.transportProtocol +} + +func (o *transportProtocolMatchOption) String() string { + return fmt.Sprintf("transport protocol=%s", o.transportProtocol) +} + +func WithTransportProtocol(transportProtocol string) *transportProtocolMatchOption { + return &transportProtocolMatchOption{transportProtocol: transportProtocol} +} + +type applicationProtocolMatchOption struct { + applicationProtocols []string +} + +func (o *applicationProtocolMatchOption) applyTo(opts *matchCriteria) { + opts.applicationProtocols = o.applicationProtocols +} + +func (o *applicationProtocolMatchOption) String() string { + return fmt.Sprintf("application protocols=%s", o.applicationProtocols) +} + +func WithApplicationProtocols(applicationProtocols []string) *applicationProtocolMatchOption { + return &applicationProtocolMatchOption{applicationProtocols: applicationProtocols} +} + +// Filter returns the filter chains from the provided listener that matches the provided +// matching opts according to the rules described at https://github.com/envoyproxy/go-control-plane/blob/v0.9.9/envoy/config/listener/v3/listener_components.pb.go#L211 +func Filter(listener *envoy_config_listener_v3.Listener, matchingOpts ...MatchOption) ([]*envoy_config_listener_v3.FilterChain, error) { + criteria := &matchCriteria{} + filterChains := listener.GetFilterChains() + + for _, opt := range matchingOpts { + opt.applyTo(criteria) + } + + if criteria.destinationPort <= 0 { + return filterChains, nil + } + + var err error + + // 1. match by destination port + filterChains = getFilterChainsMatchingDstPort(filterChains, criteria.destinationPort) + + // 2. match destination IP address + filterChains, err = getFilterChainsMatchingDstIP(filterChains, criteria.destinationIP) + if err != nil { + return nil, errors.WrapIf(err, "could match filter chains by destination address") + } + + // 3. Server name (e.g. SNI for TLS protocol) + filterChains = getFilterChainsMatchingServerName(filterChains, criteria.serverName) + + // 4. Transport protocol. + filterChains = getFilterChainsMatchingTransportProtocol(filterChains, criteria.transportProtocol) + + // 5. Application protocols (e.g. ALPN for TLS protocol). + filterChains = getFilterChainsMatchingApplicationProtocol(filterChains, criteria.applicationProtocols) + + return filterChains, nil +} + +func getFilterChainsMatchingDstPort(filterChains []*envoy_config_listener_v3.FilterChain, dstPort uint32) []*envoy_config_listener_v3.FilterChain { + var fcsMatchedByDstPort []*envoy_config_listener_v3.FilterChain + var fcsWithNoDstPort []*envoy_config_listener_v3.FilterChain + + // match by exact destination port first as that is the most specific match + // if there are no matches by specific destination port then check the next most specific + // match which is the filter chain matches with no destination port + for _, fc := range filterChains { + fcm := fc.GetFilterChainMatch() + + if fcm.GetDestinationPort() != nil { + if fcm.GetDestinationPort().GetValue() == dstPort { + fcsMatchedByDstPort = append(fcsMatchedByDstPort, fc) + } + } else { + fcsWithNoDstPort = append(fcsWithNoDstPort, fc) + } + } + + if len(fcsMatchedByDstPort) == 0 { + fcsMatchedByDstPort = fcsWithNoDstPort + } + + return fcsMatchedByDstPort +} + +func getFilterChainsMatchingDstIP(filterChains []*envoy_config_listener_v3.FilterChain, ip net.IP) ([]*envoy_config_listener_v3.FilterChain, error) { + // if there are no filter chain matches by CIDR than the next most specific matches are those + // which don't have a CIDR defined + var fcsMatchedByDstIP []*envoy_config_listener_v3.FilterChain + var fcsWithNoDstIP []*envoy_config_listener_v3.FilterChain + + for _, fc := range filterChains { + cidrs := fc.GetFilterChainMatch().GetPrefixRanges() + if cidrs != nil { + // verify destination IP address matches any of the cidrs of the filter chain + ok, err := matchPrefixRanges(cidrs, ip) + if err != nil { + return nil, err + } + if ok { + fcsMatchedByDstIP = append(fcsMatchedByDstIP, fc) + } + } else { + fcsWithNoDstIP = append(fcsWithNoDstIP, fc) + } + } + + if len(fcsMatchedByDstIP) == 0 { + fcsMatchedByDstIP = fcsWithNoDstIP + } + + return fcsMatchedByDstIP, nil +} + +func matchPrefixRanges(prefixRanges []*envoy_config_core_v3.CidrRange, ip net.IP) (bool, error) { + for _, cidr := range prefixRanges { + if cidr.GetAddressPrefix() == "" { + continue + } + + cidrStr := fmt.Sprintf("%s/%d", cidr.GetAddressPrefix(), cidr.GetPrefixLen().GetValue()) + _, ipnet, err := net.ParseCIDR(cidrStr) + if err != nil { + return false, errors.WrapIff(err, "couldn't parse address prefix: %q", cidrStr) + } + + if ipnet.Contains(ip) { + return true, nil + } + } + return false, nil +} + +func getFilterChainsMatchingServerName(filterChains []*envoy_config_listener_v3.FilterChain, serverName string) []*envoy_config_listener_v3.FilterChain { + if len(serverName) == 0 { + return filterChains + } + + var fcsMatched []*envoy_config_listener_v3.FilterChain + var fcsWithNoServerNames []*envoy_config_listener_v3.FilterChain + + for _, fc := range filterChains { + if len(fc.GetFilterChainMatch().GetServerNames()) == 0 { + fcsWithNoServerNames = append(fcsWithNoServerNames, fc) + continue + } + + for matchFound := false; !matchFound && len(serverName) > 0; { + matchServerNames := []string{ + serverName, + fmt.Sprintf(".%s", serverName), + fmt.Sprintf("*.%s", serverName), + } + + for _, matchServerName := range matchServerNames { + matchFound = false + for _, fcmServerName := range fc.GetFilterChainMatch().GetServerNames() { + if matchServerName == fcmServerName { + matchFound = true + break + } + } + + if matchFound { + fcsMatched = append(fcsMatched, fc) + break + } + } + + if !matchFound { + // e.g. if there is no match for a.b.com, .a.b.com, *.a.b.com + // than try b.com, .b.com, *.b.com + idx := strings.Index(serverName, ".") + if idx == -1 { + serverName = "" + } else { + serverName = serverName[idx+1:] + } + } + } + } + + if len(fcsMatched) == 0 { + fcsMatched = fcsWithNoServerNames + } + + return fcsMatched +} + +func getFilterChainsMatchingTransportProtocol(filterChains []*envoy_config_listener_v3.FilterChain, transportProtocol string) []*envoy_config_listener_v3.FilterChain { + if len(transportProtocol) == 0 { + return filterChains + } + + var fcsMatched []*envoy_config_listener_v3.FilterChain + var fcsWithNoTransportProtocol []*envoy_config_listener_v3.FilterChain + + for _, fc := range filterChains { + if len(fc.GetFilterChainMatch().GetTransportProtocol()) == 0 { + fcsWithNoTransportProtocol = append(fcsWithNoTransportProtocol, fc) + continue + } + + if transportProtocol == fc.GetFilterChainMatch().GetTransportProtocol() { + fcsMatched = append(fcsMatched, fc) + } + } + + if len(fcsMatched) == 0 { + fcsMatched = fcsWithNoTransportProtocol + } + + return fcsMatched +} + +func getFilterChainsMatchingApplicationProtocol(filterChains []*envoy_config_listener_v3.FilterChain, applicationProtocols []string) []*envoy_config_listener_v3.FilterChain { + if len(applicationProtocols) == 0 { + return filterChains + } + + var fcsMatched []*envoy_config_listener_v3.FilterChain + var fcsWithNoApplicationProtocol []*envoy_config_listener_v3.FilterChain + + for _, fc := range filterChains { + if len(fc.GetFilterChainMatch().GetApplicationProtocols()) == 0 { + fcsWithNoApplicationProtocol = append(fcsWithNoApplicationProtocol, fc) + continue + } + + for _, appProto := range applicationProtocols { + found := false + for _, fcmAppProto := range fc.GetFilterChainMatch().GetApplicationProtocols() { + if fcmAppProto == appProto { + found = true + break + } + } + + if found { + fcsMatched = append(fcsMatched, fc) + break + } + } + } + + if len(fcsMatched) == 0 { + fcsMatched = fcsWithNoApplicationProtocol + } + + return fcsMatched +} diff --git a/pkg/ads/internal/listener/listener.go b/pkg/ads/internal/listener/listener.go index 4ab82ac6..ad1c30d4 100644 --- a/pkg/ads/internal/listener/listener.go +++ b/pkg/ads/internal/listener/listener.go @@ -151,8 +151,8 @@ func GetRouteReferences(listeners []*envoy_config_listener_v3.Listener) []string // GetRouteConfigName returns the name of the route configuration in RDS that the specified http listener references func GetRouteConfigName(listener *envoy_config_listener_v3.Listener) string { - for _, chain := range listener.FilterChains { - for _, filter := range chain.Filters { + for _, chain := range listener.GetFilterChains() { + for _, filter := range chain.GetFilters() { if filter.Name != wellknown.HTTPConnectionManager { continue } From b288ffc49d8f2c5b037a33abefda64771bad5a15 Mon Sep 17 00:00:00 2001 From: "Toader, Sebastian" Date: Thu, 30 Mar 2023 15:59:37 +0200 Subject: [PATCH 2/6] Fix network filter matching on destination ip and port --- pkg/ads/api_tcp_client.go | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/pkg/ads/api_tcp_client.go b/pkg/ads/api_tcp_client.go index a3de9c7d..fcd3ca34 100644 --- a/pkg/ads/api_tcp_client.go +++ b/pkg/ads/api_tcp_client.go @@ -97,11 +97,8 @@ func (p *clientProperties) NetworkFilters(connectionsOpts ...ConnectionOption) ( } var filterChainMatchOpts []filterchain.MatchOption - if p.address != nil { - tcpAddress, ok := p.address.(*net.TCPAddr) - if !ok { - return nil, errors.Errorf("expected *TCPAddress but got: %T", p.address) - } + if p.outboundListener.GetAddress().GetSocketAddress().GetAddress() != "" && p.outboundListener.GetAddress().GetSocketAddress().GetPortValue() > 0 { + tcpAddress := net.TCPAddr{IP: net.ParseIP(p.outboundListener.GetAddress().GetSocketAddress().GetAddress()), Port: int(p.outboundListener.GetAddress().GetSocketAddress().GetPortValue())} filterChainMatchOpts = append(filterChainMatchOpts, filterchain.WithDestinationPort(uint32(tcpAddress.Port)), From b13ff16619244ea8573bae0f7afbd802bc218a8d Mon Sep 17 00:00:00 2001 From: "Toader, Sebastian" Date: Thu, 30 Mar 2023 16:11:17 +0200 Subject: [PATCH 3/6] Support destination IP for matching network filter chain --- pkg/ads/api.go | 10 ++++++++++ pkg/ads/api_listener_properties.go | 3 +++ 2 files changed, 13 insertions(+) diff --git a/pkg/ads/api.go b/pkg/ads/api.go index 5fee63c5..226a42ef 100644 --- a/pkg/ads/api.go +++ b/pkg/ads/api.go @@ -100,6 +100,9 @@ type ConnectionOptions struct { // destinationPort is the destination port of the connection destinationPort uint32 + // destinationIP is the destination IP of the connection + destinationIP net.IP + // serverName is the server name used with TLS connections serverName string @@ -119,6 +122,13 @@ func ConnectionWithDestinationPort(destinationPort uint32) ConnectionOption { } } +// ConnectionWithDestinationIP specifies the given destination IP as connection option +func ConnectionWithDestinationIP(destinationIP net.IP) ConnectionOption { + return func(o *ConnectionOptions) { + o.destinationIP = destinationIP + } +} + // TLSConnectionWithServerName specifies the given server name as TLS connection option func TLSConnectionWithServerName(serverName string) ConnectionOption { return func(o *ConnectionOptions) { diff --git a/pkg/ads/api_listener_properties.go b/pkg/ads/api_listener_properties.go index 9fb57a3c..b924f03b 100644 --- a/pkg/ads/api_listener_properties.go +++ b/pkg/ads/api_listener_properties.go @@ -75,6 +75,9 @@ func (lp *listenerProperties) NetworkFilters(connectionsOpts ...ConnectionOption if connOpts.destinationPort > 0 { filterChainMatchOpts = append(filterChainMatchOpts, filterchain.WithDestinationPort(connOpts.destinationPort)) } + if connOpts.destinationIP != nil { + filterChainMatchOpts = append(filterChainMatchOpts, filterchain.WithDestinationIP(connOpts.destinationIP)) + } if len(connOpts.transportProtocol) > 0 { filterChainMatchOpts = append(filterChainMatchOpts, filterchain.WithTransportProtocol(connOpts.transportProtocol)) } From e2889299a8d525eba65027d490694ae4664074e1 Mon Sep 17 00:00:00 2001 From: "Toader, Sebastian" Date: Fri, 31 Mar 2023 18:13:24 +0200 Subject: [PATCH 4/6] Support network filter filtering by type --- pkg/ads/api.go | 53 ++++++++------ pkg/ads/api_listener_properties.go | 84 +--------------------- pkg/ads/api_tcp_client.go | 91 +++--------------------- pkg/ads/common.go | 108 +++++++++++++++++++++++++++++ pkg/ads/e2etest/suite_test.go | 11 +++ 5 files changed, 161 insertions(+), 186 deletions(-) diff --git a/pkg/ads/api.go b/pkg/ads/api.go index 226a42ef..4e16e4be 100644 --- a/pkg/ads/api.go +++ b/pkg/ads/api.go @@ -94,9 +94,10 @@ type NetworkFilter interface { Configuration() map[string]interface{} } -// ConnectionOptions holds the inbound connections' options used to determine the -// filter chain to be instantiated for the stream -type ConnectionOptions struct { +// NetworkFilterSelectOptions holds the options used to determine the +// filter chain to be instantiated for inbound/outbound connections +// and the attributes to filter by the filters of the selected filter chain +type NetworkFilterSelectOptions struct { // destinationPort is the destination port of the connection destinationPort uint32 @@ -111,45 +112,55 @@ type ConnectionOptions struct { // applicationProtocols is the list of application protocols (e.g. ALPN for TLS protocol) of the connection applicationProtocols []string + + // filterType is the type of network filter + filterType string } -type ConnectionOption func(*ConnectionOptions) +type NetworkFilterSelectOption func(*NetworkFilterSelectOptions) -// ConnectionWithDestinationPort specifies the given destination port as connection option -func ConnectionWithDestinationPort(destinationPort uint32) ConnectionOption { - return func(o *ConnectionOptions) { +// ConnectionWithDestinationPort specifies the given destination port as connection option to select filter chain by +func ConnectionWithDestinationPort(destinationPort uint32) NetworkFilterSelectOption { + return func(o *NetworkFilterSelectOptions) { o.destinationPort = destinationPort } } -// ConnectionWithDestinationIP specifies the given destination IP as connection option -func ConnectionWithDestinationIP(destinationIP net.IP) ConnectionOption { - return func(o *ConnectionOptions) { +// ConnectionWithDestinationIP specifies the given destination IP as connection option to select filter chain by +func ConnectionWithDestinationIP(destinationIP net.IP) NetworkFilterSelectOption { + return func(o *NetworkFilterSelectOptions) { o.destinationIP = destinationIP } } -// TLSConnectionWithServerName specifies the given server name as TLS connection option -func TLSConnectionWithServerName(serverName string) ConnectionOption { - return func(o *ConnectionOptions) { +// TLSConnectionWithServerName specifies the given server name as TLS connection option to select filter chain by +func TLSConnectionWithServerName(serverName string) NetworkFilterSelectOption { + return func(o *NetworkFilterSelectOptions) { o.serverName = serverName } } -// ConnectionWithTransportProtocol specifies the given transport protocol as connection option -func ConnectionWithTransportProtocol(transportProtocol string) ConnectionOption { - return func(o *ConnectionOptions) { +// ConnectionWithTransportProtocol specifies the given transport protocol as connection option to select filter chain by +func ConnectionWithTransportProtocol(transportProtocol string) NetworkFilterSelectOption { + return func(o *NetworkFilterSelectOptions) { o.transportProtocol = transportProtocol } } -// ConnectionWithApplicationProtocols specifies the given application protocols as connection option -func ConnectionWithApplicationProtocols(applicationProtocols []string) ConnectionOption { - return func(o *ConnectionOptions) { +// ConnectionWithApplicationProtocols specifies the given application protocols as connection option select filter chain by +func ConnectionWithApplicationProtocols(applicationProtocols []string) NetworkFilterSelectOption { + return func(o *NetworkFilterSelectOptions) { o.applicationProtocols = applicationProtocols } } +// NetworkFiltersWithType specifies the given type as network filter type to filter by the network filters of the selected filter chain +func NetworkFiltersWithType(filterType string) NetworkFilterSelectOption { + return func(o *NetworkFilterSelectOptions) { + o.filterType = filterType + } +} + // ListenerPropertiesResponse contains the result for the API call // to retrieve ListenerProperties for a given address. type ListenerPropertiesResponse interface { @@ -175,7 +186,7 @@ type ListenerProperties interface { // NetworkFilters returns the network filter chain that inbound traffic flows through // when client workload connects with the given connection options. - NetworkFilters(connectionsOpts ...ConnectionOption) ([]NetworkFilter, error) + NetworkFilters(...NetworkFilterSelectOption) ([]NetworkFilter, error) } // ClientPropertiesResponse contains the result for the API call @@ -207,7 +218,7 @@ type ClientProperties interface { // NetworkFilters returns the network filter chain that outbound traffic flows through to the target service // when client workload connects with the given connection options - NetworkFilters(connectionsOpts ...ConnectionOption) ([]NetworkFilter, error) + NetworkFilters(connectionsOpts ...NetworkFilterSelectOption) ([]NetworkFilter, error) } // HTTPClientPropertiesResponse contains the result for the API call diff --git a/pkg/ads/api_listener_properties.go b/pkg/ads/api_listener_properties.go index b924f03b..360c875a 100644 --- a/pkg/ads/api_listener_properties.go +++ b/pkg/ads/api_listener_properties.go @@ -16,15 +16,12 @@ package ads import ( "context" - "encoding/json" "fmt" "net" "net/url" "reflect" "strconv" - "google.golang.org/protobuf/encoding/protojson" - "github.com/cisco-open/nasp/pkg/ads/internal/filterchain" "github.com/cisco-open/nasp/pkg/ads/internal/listener" @@ -60,85 +57,8 @@ func (lp *listenerProperties) Metadata() map[string]interface{} { return lp.metadata } -func (lp *listenerProperties) NetworkFilters(connectionsOpts ...ConnectionOption) ([]NetworkFilter, error) { - if lp.inboundListener == nil { - return nil, nil - } - - var connOpts ConnectionOptions - for _, opt := range connectionsOpts { - opt(&connOpts) - } - - var filterChainMatchOpts []filterchain.MatchOption - - if connOpts.destinationPort > 0 { - filterChainMatchOpts = append(filterChainMatchOpts, filterchain.WithDestinationPort(connOpts.destinationPort)) - } - if connOpts.destinationIP != nil { - filterChainMatchOpts = append(filterChainMatchOpts, filterchain.WithDestinationIP(connOpts.destinationIP)) - } - if len(connOpts.transportProtocol) > 0 { - filterChainMatchOpts = append(filterChainMatchOpts, filterchain.WithTransportProtocol(connOpts.transportProtocol)) - } - if len(connOpts.applicationProtocols) > 0 { - filterChainMatchOpts = append(filterChainMatchOpts, filterchain.WithApplicationProtocols(connOpts.applicationProtocols)) - } - - filterChains, err := filterchain.Filter(lp.inboundListener, filterChainMatchOpts...) - if err != nil { - return nil, err - } - - if len(filterChains) == 0 && lp.inboundListener.GetDefaultFilterChain() != nil { - // if no filter chains found, use default filter chain of the listener - filterChains = append(filterChains, lp.inboundListener.GetDefaultFilterChain()) - } - - if len(filterChains) == 0 { - return nil, errors.Errorf("couldn't find a filter chain for listener %q, with matching fields:%s", - lp.inboundListener.GetName(), - filterChainMatchOpts) - } - if len(filterChains) > 1 { - fcNames := make([]string, 0, len(filterChains)) - for _, fc := range filterChains { - fcNames = append(fcNames, fc.GetName()) - } - return nil, errors.Errorf("multiple filter chains for listener %q, with matching fields:%s, filter chains:%s", - lp.inboundListener.GetName(), - filterChainMatchOpts, - fcNames) - } - - networkFilters := make([]NetworkFilter, 0, len(filterChains[0].GetFilters())) - for _, filter := range filterChains[0].GetFilters() { - if filter == nil { - continue - } - - configuration := make(map[string]interface{}) - proto, err := filter.GetTypedConfig().UnmarshalNew() - if err != nil { - return nil, err - } - - configurationJson, err := protojson.Marshal(proto) - if err != nil { - return nil, err - } - - if err = json.Unmarshal(configurationJson, &configuration); err != nil { - return nil, err - } - - networkFilters = append(networkFilters, &networkFilter{ - name: filter.GetName(), - configuration: configuration, - }) - } - - return networkFilters, nil +func (lp *listenerProperties) NetworkFilters(networkFilterSelectOpts ...NetworkFilterSelectOption) ([]NetworkFilter, error) { + return listenerNetworkFilters(lp.inboundListener, networkFilterSelectOpts...) } func (lp *listenerProperties) String() string { diff --git a/pkg/ads/api_tcp_client.go b/pkg/ads/api_tcp_client.go index fcd3ca34..acbb0505 100644 --- a/pkg/ads/api_tcp_client.go +++ b/pkg/ads/api_tcp_client.go @@ -16,17 +16,12 @@ package ads import ( "context" - "encoding/json" "fmt" "net" "net/url" "reflect" "strconv" - "google.golang.org/protobuf/encoding/protojson" - - "github.com/cisco-open/nasp/pkg/ads/internal/filterchain" - "github.com/cisco-open/nasp/pkg/ads/internal/listener" "github.com/cisco-open/nasp/pkg/ads/internal/loadbalancer" @@ -86,89 +81,19 @@ func (p *clientProperties) String() string { return fmt.Sprintf("{serverName=%s, useTLS=%t, permissive=%t, address=%s}", p.ServerName(), p.UseTLS(), p.Permissive(), addr) } -func (p *clientProperties) NetworkFilters(connectionsOpts ...ConnectionOption) ([]NetworkFilter, error) { - if p.outboundListener == nil { - return nil, nil - } - - var connOpts ConnectionOptions - for _, opt := range connectionsOpts { - opt(&connOpts) - } - - var filterChainMatchOpts []filterchain.MatchOption +func (p *clientProperties) NetworkFilters(networkFilterSelectOpts ...NetworkFilterSelectOption) ([]NetworkFilter, error) { if p.outboundListener.GetAddress().GetSocketAddress().GetAddress() != "" && p.outboundListener.GetAddress().GetSocketAddress().GetPortValue() > 0 { - tcpAddress := net.TCPAddr{IP: net.ParseIP(p.outboundListener.GetAddress().GetSocketAddress().GetAddress()), Port: int(p.outboundListener.GetAddress().GetSocketAddress().GetPortValue())} - - filterChainMatchOpts = append(filterChainMatchOpts, - filterchain.WithDestinationPort(uint32(tcpAddress.Port)), - filterchain.WithDestinationIP(tcpAddress.IP)) - } - if len(connOpts.serverName) > 0 { - filterChainMatchOpts = append(filterChainMatchOpts, filterchain.WithServerName(connOpts.serverName)) - } - if len(connOpts.transportProtocol) > 0 { - filterChainMatchOpts = append(filterChainMatchOpts, filterchain.WithTransportProtocol(connOpts.transportProtocol)) - } - if len(connOpts.applicationProtocols) > 0 { - filterChainMatchOpts = append(filterChainMatchOpts, filterchain.WithApplicationProtocols(connOpts.applicationProtocols)) - } - - filterChains, err := filterchain.Filter(p.outboundListener, filterChainMatchOpts...) - if err != nil { - return nil, err - } - - if len(filterChains) == 0 && p.outboundListener.GetDefaultFilterChain() != nil { - // if no filter chains found, use default filter chain of the listener - filterChains = append(filterChains, p.outboundListener.GetDefaultFilterChain()) - } - - if len(filterChains) == 0 { - return nil, errors.Errorf("couldn't find a filter chain for listener %q, with matching fields:%s", - p.outboundListener.GetName(), - filterChainMatchOpts) - } - - if len(filterChains) > 1 { - fcNames := make([]string, 0, len(filterChains)) - for _, fc := range filterChains { - fcNames = append(fcNames, fc.GetName()) - } - return nil, errors.Errorf("multiple filter chains for listener %q, with matching fields:%s, filter chains:%s", - p.outboundListener.GetName(), - filterChainMatchOpts, - fcNames) - } - - networkFilters := make([]NetworkFilter, 0, len(filterChains[0].GetFilters())) - for _, filter := range filterChains[0].GetFilters() { - if filter == nil { - continue - } - - configuration := make(map[string]interface{}) - proto, err := filter.GetTypedConfig().UnmarshalNew() - if err != nil { - return nil, err - } - - configurationJson, err := protojson.Marshal(proto) - if err != nil { - return nil, err - } - - if err = json.Unmarshal(configurationJson, &configuration); err != nil { - return nil, err + tcpAddress := net.TCPAddr{ + IP: net.ParseIP(p.outboundListener.GetAddress().GetSocketAddress().GetAddress()), + Port: int(p.outboundListener.GetAddress().GetSocketAddress().GetPortValue()), } - networkFilters = append(networkFilters, &networkFilter{ - name: filter.GetName(), - configuration: configuration, - }) + networkFilterSelectOpts = append(networkFilterSelectOpts, + ConnectionWithDestinationPort(uint32(tcpAddress.Port)), + ConnectionWithDestinationIP(tcpAddress.IP)) } - return networkFilters, nil + return listenerNetworkFilters(p.outboundListener, networkFilterSelectOpts...) } type clientPropertiesResponse struct { diff --git a/pkg/ads/common.go b/pkg/ads/common.go index d9cf267f..1d967b63 100644 --- a/pkg/ads/common.go +++ b/pkg/ads/common.go @@ -15,9 +15,17 @@ package ads import ( + "encoding/json" "sort" + "strings" "sync" + udpa_type_v1 "github.com/cncf/xds/go/udpa/type/v1" + xds_type_v3 "github.com/cncf/xds/go/xds/type/v3" + "google.golang.org/protobuf/encoding/protojson" + + "github.com/cisco-open/nasp/pkg/ads/internal/filterchain" + "emperror.dev/errors" envoy_config_cluster_v3 "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3" envoy_config_endpoint_v3 "github.com/envoyproxy/go-control-plane/envoy/config/endpoint/v3" @@ -481,3 +489,103 @@ func (n *networkFilter) Name() string { func (n *networkFilter) Configuration() map[string]interface{} { return n.configuration } + +func listenerNetworkFilters(listener *envoy_config_listener_v3.Listener, networkFilterSelectOpts ...NetworkFilterSelectOption) ([]NetworkFilter, error) { + if listener == nil { + return nil, nil + } + + var opts NetworkFilterSelectOptions + for _, opt := range networkFilterSelectOpts { + opt(&opts) + } + + var filterChainMatchOpts []filterchain.MatchOption + if opts.destinationPort > 0 { + filterChainMatchOpts = append(filterChainMatchOpts, filterchain.WithDestinationPort(opts.destinationPort)) + } + if opts.destinationIP != nil { + filterChainMatchOpts = append(filterChainMatchOpts, filterchain.WithDestinationIP(opts.destinationIP)) + } + if len(opts.serverName) > 0 { + filterChainMatchOpts = append(filterChainMatchOpts, filterchain.WithServerName(opts.serverName)) + } + if len(opts.transportProtocol) > 0 { + filterChainMatchOpts = append(filterChainMatchOpts, filterchain.WithTransportProtocol(opts.transportProtocol)) + } + if len(opts.applicationProtocols) > 0 { + filterChainMatchOpts = append(filterChainMatchOpts, filterchain.WithApplicationProtocols(opts.applicationProtocols)) + } + + filterChains, err := filterchain.Filter(listener, filterChainMatchOpts...) + if err != nil { + return nil, err + } + + if len(filterChains) == 0 && listener.GetDefaultFilterChain() != nil { + // if no filter chains found, use default filter chain of the listener + filterChains = append(filterChains, listener.GetDefaultFilterChain()) + } + + if len(filterChains) == 0 { + return nil, errors.Errorf("couldn't find a filter chain for listener %q, with matching fields:%s", + listener.GetName(), + filterChainMatchOpts) + } + + if len(filterChains) > 1 { + fcNames := make([]string, 0, len(filterChains)) + for _, fc := range filterChains { + fcNames = append(fcNames, fc.GetName()) + } + return nil, errors.Errorf("multiple filter chains for listener %q, with matching fields:%s, filter chains:%s", + listener.GetName(), + filterChainMatchOpts, + fcNames) + } + + networkFilters := make([]NetworkFilter, 0, len(filterChains[0].GetFilters())) + for _, filter := range filterChains[0].GetFilters() { + if filter == nil { + continue + } + + proto, err := filter.GetTypedConfig().UnmarshalNew() + if err != nil { + return nil, err + } + + typeUrl := filter.GetTypedConfig().GetTypeUrl() + + if typedStruct, ok := proto.(*udpa_type_v1.TypedStruct); ok { + typeUrl = typedStruct.GetTypeUrl() + } + if typedStruct, ok := proto.(*xds_type_v3.TypedStruct); ok { + typeUrl = typedStruct.GetTypeUrl() + } + + if len(opts.filterType) > 0 { + // skip if provided filter type doesn't match filter's type url + if !(opts.filterType == typeUrl || strings.HasSuffix(typeUrl, "/"+opts.filterType)) { + continue + } + } + + configuration := make(map[string]interface{}) + configurationJson, err := protojson.Marshal(proto) + if err != nil { + return nil, err + } + + if err = json.Unmarshal(configurationJson, &configuration); err != nil { + return nil, err + } + + networkFilters = append(networkFilters, &networkFilter{ + name: filter.GetName(), + configuration: configuration, + }) + } + + return networkFilters, nil +} diff --git a/pkg/ads/e2etest/suite_test.go b/pkg/ads/e2etest/suite_test.go index 92ff0272..60445410 100644 --- a/pkg/ads/e2etest/suite_test.go +++ b/pkg/ads/e2etest/suite_test.go @@ -23,6 +23,8 @@ import ( "syscall" "time" + xds_type_v3 "github.com/cncf/xds/go/xds/type/v3" + "github.com/go-logr/logr" "github.com/go-logr/logr/testr" @@ -305,6 +307,7 @@ var _ = BeforeSuite(func() { _ = metadata_exchange.MetadataExchange{} _ = v2alpha1.FilterConfig{} _ = upda_type_v1.TypedStruct{} + _ = xds_type_v3.TypedStruct{} _ = grpcv3.TcpGrpcAccessLogConfig{} _ = httpwasmv3.Wasm{} _ = faultv3.HTTPFault{} @@ -437,6 +440,14 @@ var _ = Describe("The management server is running", func() { HaveField("Name()", "istio.stats"), HaveField("Name()", "envoy.filters.network.tcp_proxy"), )) + // filters by type + Expect(tcpClientProps.NetworkFilters(ads.NetworkFiltersWithType("envoy.extensions.filters.network.wasm.v3.Wasm"))). + Should(HaveExactElements(HaveField("Name()", "istio.stats"))) + Expect(tcpClientProps.NetworkFilters(ads.NetworkFiltersWithType("type.googleapis.com/envoy.extensions.filters.network.wasm.v3.Wasm"))). + Should(HaveExactElements(HaveField("Name()", "istio.stats"))) + Expect(tcpClientProps.NetworkFilters(ads.NetworkFiltersWithType("envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy"))). + Should(HaveExactElements(HaveField("Name()", "envoy.filters.network.tcp_proxy"))) + Expect(tcpClientProps.NetworkFilters(ads.NetworkFiltersWithType("unknown"))).Should(BeEmpty()) By("verify that correct http client properties are returned for 10.10.42.110:80") httpResp, err := adsClient.GetHTTPClientPropertiesByHost(ctx, "10.10.42.110:80") From 5fa43094993209fdd8482b1a375b40bfd411218d Mon Sep 17 00:00:00 2001 From: "Toader, Sebastian" Date: Mon, 3 Apr 2023 08:59:42 +0200 Subject: [PATCH 5/6] Fix typo --- pkg/ads/api.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/ads/api.go b/pkg/ads/api.go index 4e16e4be..f4a75208 100644 --- a/pkg/ads/api.go +++ b/pkg/ads/api.go @@ -218,7 +218,7 @@ type ClientProperties interface { // NetworkFilters returns the network filter chain that outbound traffic flows through to the target service // when client workload connects with the given connection options - NetworkFilters(connectionsOpts ...NetworkFilterSelectOption) ([]NetworkFilter, error) + NetworkFilters(...NetworkFilterSelectOption) ([]NetworkFilter, error) } // HTTPClientPropertiesResponse contains the result for the API call From c317101d7fa18fc3031e772525a45295d2d1cc36 Mon Sep 17 00:00:00 2001 From: "Toader, Sebastian" Date: Thu, 6 Apr 2023 12:26:22 +0200 Subject: [PATCH 6/6] Support filtering filters by multiple types --- pkg/ads/api.go | 10 +++++----- pkg/ads/common.go | 13 ++++++++++--- pkg/ads/e2etest/suite_test.go | 18 ++++++++++++++---- 3 files changed, 29 insertions(+), 12 deletions(-) diff --git a/pkg/ads/api.go b/pkg/ads/api.go index f4a75208..3472d26e 100644 --- a/pkg/ads/api.go +++ b/pkg/ads/api.go @@ -113,8 +113,8 @@ type NetworkFilterSelectOptions struct { // applicationProtocols is the list of application protocols (e.g. ALPN for TLS protocol) of the connection applicationProtocols []string - // filterType is the type of network filter - filterType string + // includeFiltersWithTypes include network filters with these types + includeFiltersWithTypes []string } type NetworkFilterSelectOption func(*NetworkFilterSelectOptions) @@ -154,10 +154,10 @@ func ConnectionWithApplicationProtocols(applicationProtocols []string) NetworkFi } } -// NetworkFiltersWithType specifies the given type as network filter type to filter by the network filters of the selected filter chain -func NetworkFiltersWithType(filterType string) NetworkFilterSelectOption { +// IncludeNetworkFiltersWithTypes specifies the network filter types to filter by the network filters of the selected filter chain +func IncludeNetworkFiltersWithTypes(filterTypes ...string) NetworkFilterSelectOption { return func(o *NetworkFilterSelectOptions) { - o.filterType = filterType + o.includeFiltersWithTypes = filterTypes } } diff --git a/pkg/ads/common.go b/pkg/ads/common.go index 1d967b63..57466ad8 100644 --- a/pkg/ads/common.go +++ b/pkg/ads/common.go @@ -564,9 +564,16 @@ func listenerNetworkFilters(listener *envoy_config_listener_v3.Listener, network typeUrl = typedStruct.GetTypeUrl() } - if len(opts.filterType) > 0 { - // skip if provided filter type doesn't match filter's type url - if !(opts.filterType == typeUrl || strings.HasSuffix(typeUrl, "/"+opts.filterType)) { + if len(opts.includeFiltersWithTypes) > 0 { + match := false + for _, filterType := range opts.includeFiltersWithTypes { + // skip if provided filter type doesn't match filter's type url + if filterType == typeUrl || strings.HasSuffix(typeUrl, "/"+filterType) { + match = true + break + } + } + if !match { continue } } diff --git a/pkg/ads/e2etest/suite_test.go b/pkg/ads/e2etest/suite_test.go index 60445410..00fe9a32 100644 --- a/pkg/ads/e2etest/suite_test.go +++ b/pkg/ads/e2etest/suite_test.go @@ -441,13 +441,23 @@ var _ = Describe("The management server is running", func() { HaveField("Name()", "envoy.filters.network.tcp_proxy"), )) // filters by type - Expect(tcpClientProps.NetworkFilters(ads.NetworkFiltersWithType("envoy.extensions.filters.network.wasm.v3.Wasm"))). + Expect(tcpClientProps.NetworkFilters(ads.IncludeNetworkFiltersWithTypes("envoy.extensions.filters.network.wasm.v3.Wasm"))). Should(HaveExactElements(HaveField("Name()", "istio.stats"))) - Expect(tcpClientProps.NetworkFilters(ads.NetworkFiltersWithType("type.googleapis.com/envoy.extensions.filters.network.wasm.v3.Wasm"))). + Expect(tcpClientProps.NetworkFilters(ads.IncludeNetworkFiltersWithTypes("type.googleapis.com/envoy.extensions.filters.network.wasm.v3.Wasm"))). Should(HaveExactElements(HaveField("Name()", "istio.stats"))) - Expect(tcpClientProps.NetworkFilters(ads.NetworkFiltersWithType("envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy"))). + Expect(tcpClientProps.NetworkFilters(ads.IncludeNetworkFiltersWithTypes("envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy"))). Should(HaveExactElements(HaveField("Name()", "envoy.filters.network.tcp_proxy"))) - Expect(tcpClientProps.NetworkFilters(ads.NetworkFiltersWithType("unknown"))).Should(BeEmpty()) + Expect(tcpClientProps.NetworkFilters(ads.IncludeNetworkFiltersWithTypes("unknown"))).Should(BeEmpty()) + Expect(tcpClientProps.NetworkFilters( + ads.IncludeNetworkFiltersWithTypes( + "envoy.extensions.filters.network.wasm.v3.Wasm", + "envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy"))). + Should( + HaveExactElements( + HaveField("Name()", "istio.stats"), + HaveField("Name()", "envoy.filters.network.tcp_proxy"), + ), + ) By("verify that correct http client properties are returned for 10.10.42.110:80") httpResp, err := adsClient.GetHTTPClientPropertiesByHost(ctx, "10.10.42.110:80")