/* * * Copyright 2019 gRPC 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 client import ( "fmt" "strings" xdspb "github.com/envoyproxy/go-control-plane/envoy/api/v2" routepb "github.com/envoyproxy/go-control-plane/envoy/api/v2/route" "github.com/golang/protobuf/ptypes" ) // handleRDSResponse processes an RDS response received from the xDS server. On // receipt of a good response, it caches validated resources and also invokes // the registered watcher callback. func (v2c *v2Client) handleRDSResponse(resp *xdspb.DiscoveryResponse) error { v2c.mu.Lock() hostname := v2c.hostname v2c.mu.Unlock() returnUpdate := make(map[string]rdsUpdate) for _, r := range resp.GetResources() { var resource ptypes.DynamicAny if err := ptypes.UnmarshalAny(r, &resource); err != nil { return fmt.Errorf("xds: failed to unmarshal resource in RDS response: %v", err) } rc, ok := resource.Message.(*xdspb.RouteConfiguration) if !ok { return fmt.Errorf("xds: unexpected resource type: %T in RDS response", resource.Message) } v2c.logger.Infof("Resource with name: %v, type: %T, contains: %v. Picking routes for current watching hostname %v", rc.GetName(), rc, rc, v2c.hostname) // Use the hostname (resourceName for LDS) to find the routes. cluster, err := getClusterFromRouteConfiguration(rc, hostname) if cluster == "" { return fmt.Errorf("xds: received invalid RouteConfiguration in RDS response: %+v with err: %v", rc, err) } // If we get here, it means that this resource was a good one. returnUpdate[rc.GetName()] = rdsUpdate{clusterName: cluster} } v2c.parent.newRDSUpdate(returnUpdate) return nil } // getClusterFromRouteConfiguration checks if the provided RouteConfiguration // meets the expected criteria. If so, it returns a non-empty clusterName with // nil error. // // A RouteConfiguration resource is considered valid when only if it contains a // VirtualHost whose domain field matches the server name from the URI passed // to the gRPC channel, and it contains a clusterName. // // The RouteConfiguration includes a list of VirtualHosts, which may have zero // or more elements. We are interested in the element whose domains field // matches the server name specified in the "xds:" URI. The only field in the // VirtualHost proto that the we are interested in is the list of routes. We // only look at the last route in the list (the default route), whose match // field must be empty and whose route field must be set. Inside that route // message, the cluster field will contain the clusterName we are looking for. func getClusterFromRouteConfiguration(rc *xdspb.RouteConfiguration, host string) (string, error) { // // Currently this returns "" on error, and the caller will return an error. // But the error doesn't contain details of why the response is invalid // (mismatch domain or empty route). // // For logging purposes, we can log in line. But if we want to populate // error details for nack, a detailed error needs to be returned. vh := findBestMatchingVirtualHost(host, rc.GetVirtualHosts()) if vh == nil { // No matching virtual host found. return "", fmt.Errorf("no matching virtual host found") } if len(vh.Routes) == 0 { // The matched virtual host has no routes, this is invalid because there // should be at least one default route. return "", fmt.Errorf("matched virtual host has no routes") } dr := vh.Routes[len(vh.Routes)-1] if match := dr.GetMatch(); match == nil || (match.GetPrefix() != "" && match.GetPrefix() != "/") { // The matched virtual host is invalid. Match is not "" or "/". return "", fmt.Errorf("matched virtual host is invalid") } if route := dr.GetRoute(); route != nil { return route.GetCluster(), nil } return "", fmt.Errorf("matched route is nil") } type domainMatchType int const ( domainMatchTypeInvalid domainMatchType = iota domainMatchTypeUniversal domainMatchTypePrefix domainMatchTypeSuffix domainMatchTypeExact ) // Exact > Suffix > Prefix > Universal > Invalid. func (t domainMatchType) betterThan(b domainMatchType) bool { return t > b } func matchTypeForDomain(d string) domainMatchType { if d == "" { return domainMatchTypeInvalid } if d == "*" { return domainMatchTypeUniversal } if strings.HasPrefix(d, "*") { return domainMatchTypeSuffix } if strings.HasSuffix(d, "*") { return domainMatchTypePrefix } if strings.Contains(d, "*") { return domainMatchTypeInvalid } return domainMatchTypeExact } func match(domain, host string) (domainMatchType, bool) { switch typ := matchTypeForDomain(domain); typ { case domainMatchTypeInvalid: return typ, false case domainMatchTypeUniversal: return typ, true case domainMatchTypePrefix: // abc.* return typ, strings.HasPrefix(host, strings.TrimSuffix(domain, "*")) case domainMatchTypeSuffix: // *.123 return typ, strings.HasSuffix(host, strings.TrimPrefix(domain, "*")) case domainMatchTypeExact: return typ, domain == host default: return domainMatchTypeInvalid, false } } // findBestMatchingVirtualHost returns the virtual host whose domains field best // matches host // // The domains field support 4 different matching pattern types: // - Exact match // - Suffix match (e.g. “*ABC”) // - Prefix match (e.g. “ABC*) // - Universal match (e.g. “*”) // // The best match is defined as: // - A match is better if it’s matching pattern type is better // - Exact match > suffix match > prefix match > universal match // - If two matches are of the same pattern type, the longer match is better // - This is to compare the length of the matching pattern, e.g. “*ABCDE” > // “*ABC” func findBestMatchingVirtualHost(host string, vHosts []*routepb.VirtualHost) *routepb.VirtualHost { var ( matchVh *routepb.VirtualHost matchType = domainMatchTypeInvalid matchLen int ) for _, vh := range vHosts { for _, domain := range vh.GetDomains() { typ, matched := match(domain, host) if typ == domainMatchTypeInvalid { // The rds response is invalid. return nil } if matchType.betterThan(typ) || matchType == typ && matchLen >= len(domain) || !matched { // The previous match has better type, or the previous match has // better length, or this domain isn't a match. continue } matchVh = vh matchType = typ matchLen = len(domain) } } return matchVh }