From e139b475f37d292b450ff62b2efb4312eec75113 Mon Sep 17 00:00:00 2001 From: Menghan Li Date: Fri, 28 Feb 2020 09:44:04 -0800 Subject: [PATCH] xds: support wildcard domain matching for RDS response (#3397) --- xds/internal/client/rds.go | 129 +++++++++++++++++++++++++++----- xds/internal/client/rds_test.go | 102 +++++++++++++++++++++++++ 2 files changed, 214 insertions(+), 17 deletions(-) diff --git a/xds/internal/client/rds.go b/xds/internal/client/rds.go index bf0f965334f0..5deeb4aaa347 100644 --- a/xds/internal/client/rds.go +++ b/xds/internal/client/rds.go @@ -24,6 +24,7 @@ import ( "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" ) @@ -116,27 +117,27 @@ func getClusterFromRouteConfiguration(rc *xdspb.RouteConfiguration, target strin // // 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. - host, err := hostFromTarget(target) if err != nil { return "" } - for _, vh := range rc.GetVirtualHosts() { - for _, domain := range vh.GetDomains() { - // TODO: Add support for wildcard matching here? - if domain != host || len(vh.GetRoutes()) == 0 { - continue - } - dr := vh.Routes[len(vh.Routes)-1] - if match := dr.GetMatch(); match == nil || match.GetPrefix() != "" { - continue - } - route := dr.GetRoute() - if route == nil { - continue - } - return route.GetCluster() - } + vh := findBestMatchingVirtualHost(host, rc.GetVirtualHosts()) + if vh == nil { + // No matching virtual host found. + return "" + } + 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 "" + } + dr := vh.Routes[len(vh.Routes)-1] + if match := dr.GetMatch(); match == nil || match.GetPrefix() != "" { + // The matched virtual host is invalid. + return "" + } + if route := dr.GetRoute(); route != nil { + return route.GetCluster() } return "" } @@ -155,3 +156,97 @@ func hostFromTarget(target string) (string, error) { } return h, 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 +} diff --git a/xds/internal/client/rds_test.go b/xds/internal/client/rds_test.go index d0d051bebe10..ba029fb4331a 100644 --- a/xds/internal/client/rds_test.go +++ b/xds/internal/client/rds_test.go @@ -500,3 +500,105 @@ func (s) TestHostFromTarget(t *testing.T) { }) } } + +func TestMatchTypeForDomain(t *testing.T) { + tests := []struct { + d string + want domainMatchType + }{ + {d: "", want: domainMatchTypeInvalid}, + {d: "*", want: domainMatchTypeUniversal}, + {d: "bar.*", want: domainMatchTypePrefix}, + {d: "*.abc.com", want: domainMatchTypeSuffix}, + {d: "foo.bar.com", want: domainMatchTypeExact}, + {d: "foo.*.com", want: domainMatchTypeInvalid}, + } + for _, tt := range tests { + if got := matchTypeForDomain(tt.d); got != tt.want { + t.Errorf("matchTypeForDomain(%q) = %v, want %v", tt.d, got, tt.want) + } + } +} + +func TestMatch(t *testing.T) { + tests := []struct { + name string + domain string + host string + wantTyp domainMatchType + wantMatched bool + }{ + {name: "invalid-empty", domain: "", host: "", wantTyp: domainMatchTypeInvalid, wantMatched: false}, + {name: "invalid", domain: "a.*.b", host: "", wantTyp: domainMatchTypeInvalid, wantMatched: false}, + {name: "universal", domain: "*", host: "abc.com", wantTyp: domainMatchTypeUniversal, wantMatched: true}, + {name: "prefix-match", domain: "abc.*", host: "abc.123", wantTyp: domainMatchTypePrefix, wantMatched: true}, + {name: "prefix-no-match", domain: "abc.*", host: "abcd.123", wantTyp: domainMatchTypePrefix, wantMatched: false}, + {name: "suffix-match", domain: "*.123", host: "abc.123", wantTyp: domainMatchTypeSuffix, wantMatched: true}, + {name: "suffix-no-match", domain: "*.123", host: "abc.1234", wantTyp: domainMatchTypeSuffix, wantMatched: false}, + {name: "exact-match", domain: "foo.bar", host: "foo.bar", wantTyp: domainMatchTypeExact, wantMatched: true}, + {name: "exact-no-match", domain: "foo.bar.com", host: "foo.bar", wantTyp: domainMatchTypeExact, wantMatched: false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if gotTyp, gotMatched := match(tt.domain, tt.host); gotTyp != tt.wantTyp || gotMatched != tt.wantMatched { + t.Errorf("match() = %v, %v, want %v, %v", gotTyp, gotMatched, tt.wantTyp, tt.wantMatched) + } + }) + } +} + +func TestFindBestMatchingVirtualHost(t *testing.T) { + var ( + oneExactMatch = &routepb.VirtualHost{ + Name: "one-exact-match", + Domains: []string{"foo.bar.com"}, + } + oneSuffixMatch = &routepb.VirtualHost{ + Name: "one-suffix-match", + Domains: []string{"*.bar.com"}, + } + onePrefixMatch = &routepb.VirtualHost{ + Name: "one-prefix-match", + Domains: []string{"foo.bar.*"}, + } + oneUniversalMatch = &routepb.VirtualHost{ + Name: "one-universal-match", + Domains: []string{"*"}, + } + longExactMatch = &routepb.VirtualHost{ + Name: "one-exact-match", + Domains: []string{"v2.foo.bar.com"}, + } + multipleMatch = &routepb.VirtualHost{ + Name: "multiple-match", + Domains: []string{"pi.foo.bar.com", "314.*", "*.159"}, + } + vhs = []*routepb.VirtualHost{oneExactMatch, oneSuffixMatch, onePrefixMatch, oneUniversalMatch, longExactMatch, multipleMatch} + ) + + tests := []struct { + name string + host string + vHosts []*routepb.VirtualHost + want *routepb.VirtualHost + }{ + {name: "exact-match", host: "foo.bar.com", vHosts: vhs, want: oneExactMatch}, + {name: "suffix-match", host: "123.bar.com", vHosts: vhs, want: oneSuffixMatch}, + {name: "prefix-match", host: "foo.bar.org", vHosts: vhs, want: onePrefixMatch}, + {name: "universal-match", host: "abc.123", vHosts: vhs, want: oneUniversalMatch}, + {name: "long-exact-match", host: "v2.foo.bar.com", vHosts: vhs, want: longExactMatch}, + // Matches suffix "*.bar.com" and exact "pi.foo.bar.com". Takes exact. + {name: "multiple-match-exact", host: "pi.foo.bar.com", vHosts: vhs, want: multipleMatch}, + // Matches suffix "*.159" and prefix "foo.bar.*". Takes suffix. + {name: "multiple-match-suffix", host: "foo.bar.159", vHosts: vhs, want: multipleMatch}, + // Matches suffix "*.bar.com" and prefix "314.*". Takes suffix. + {name: "multiple-match-prefix", host: "314.bar.com", vHosts: vhs, want: oneSuffixMatch}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := findBestMatchingVirtualHost(tt.host, tt.vHosts); !reflect.DeepEqual(got, tt.want) { + t.Errorf("findBestMatchingVirtualHost() = %v, want %v", got, tt.want) + } + }) + } +}