1package xds
2
3import (
4	"errors"
5	"fmt"
6	"net"
7	"strings"
8
9	envoy "github.com/envoyproxy/go-control-plane/envoy/api/v2"
10	envoyroute "github.com/envoyproxy/go-control-plane/envoy/api/v2/route"
11	envoymatcher "github.com/envoyproxy/go-control-plane/envoy/type/matcher"
12	"github.com/golang/protobuf/proto"
13	"github.com/golang/protobuf/ptypes"
14	"github.com/hashicorp/consul/agent/proxycfg"
15	"github.com/hashicorp/consul/agent/structs"
16)
17
18// routesFromSnapshot returns the xDS API representation of the "routes" in the
19// snapshot.
20func routesFromSnapshot(cInfo connectionInfo, cfgSnap *proxycfg.ConfigSnapshot) ([]proto.Message, error) {
21	if cfgSnap == nil {
22		return nil, errors.New("nil config given")
23	}
24
25	switch cfgSnap.Kind {
26	case structs.ServiceKindConnectProxy:
27		return routesFromSnapshotConnectProxy(cInfo, cfgSnap)
28	case structs.ServiceKindIngressGateway:
29		return routesFromSnapshotIngressGateway(cInfo, cfgSnap)
30	default:
31		return nil, fmt.Errorf("Invalid service kind: %v", cfgSnap.Kind)
32	}
33}
34
35// routesFromSnapshotConnectProxy returns the xDS API representation of the
36// "routes" in the snapshot.
37func routesFromSnapshotConnectProxy(cInfo connectionInfo, cfgSnap *proxycfg.ConfigSnapshot) ([]proto.Message, error) {
38	if cfgSnap == nil {
39		return nil, errors.New("nil config given")
40	}
41
42	var resources []proto.Message
43	for _, u := range cfgSnap.Proxy.Upstreams {
44		upstreamID := u.Identifier()
45
46		var chain *structs.CompiledDiscoveryChain
47		if u.DestinationType != structs.UpstreamDestTypePreparedQuery {
48			chain = cfgSnap.ConnectProxy.DiscoveryChain[upstreamID]
49		}
50
51		if chain == nil || chain.IsDefault() {
52			// TODO(rb): make this do the old school stuff too
53		} else {
54			virtualHost, err := makeUpstreamRouteForDiscoveryChain(cInfo, upstreamID, chain, []string{"*"})
55			if err != nil {
56				return nil, err
57			}
58
59			route := &envoy.RouteConfiguration{
60				Name:         upstreamID,
61				VirtualHosts: []*envoyroute.VirtualHost{virtualHost},
62				// ValidateClusters defaults to true when defined statically and false
63				// when done via RDS. Re-set the sane value of true to prevent
64				// null-routing traffic.
65				ValidateClusters: makeBoolValue(true),
66			}
67			resources = append(resources, route)
68		}
69	}
70
71	// TODO(rb): make sure we don't generate an empty result
72	return resources, nil
73}
74
75// routesFromSnapshotIngressGateway returns the xDS API representation of the
76// "routes" in the snapshot.
77func routesFromSnapshotIngressGateway(cInfo connectionInfo, cfgSnap *proxycfg.ConfigSnapshot) ([]proto.Message, error) {
78	if cfgSnap == nil {
79		return nil, errors.New("nil config given")
80	}
81
82	var result []proto.Message
83	for listenerKey, upstreams := range cfgSnap.IngressGateway.Upstreams {
84		// Do not create any route configuration for TCP listeners
85		if listenerKey.Protocol == "tcp" {
86			continue
87		}
88
89		upstreamRoute := &envoy.RouteConfiguration{
90			Name: listenerKey.RouteName(),
91			// ValidateClusters defaults to true when defined statically and false
92			// when done via RDS. Re-set the sane value of true to prevent
93			// null-routing traffic.
94			ValidateClusters: makeBoolValue(true),
95		}
96		for _, u := range upstreams {
97			upstreamID := u.Identifier()
98			chain := cfgSnap.IngressGateway.DiscoveryChain[upstreamID]
99			if chain == nil {
100				continue
101			}
102
103			domains := generateUpstreamIngressDomains(listenerKey, u)
104			virtualHost, err := makeUpstreamRouteForDiscoveryChain(cInfo, upstreamID, chain, domains)
105			if err != nil {
106				return nil, err
107			}
108			upstreamRoute.VirtualHosts = append(upstreamRoute.VirtualHosts, virtualHost)
109		}
110
111		result = append(result, upstreamRoute)
112	}
113
114	return result, nil
115}
116
117func generateUpstreamIngressDomains(listenerKey proxycfg.IngressListenerKey, u structs.Upstream) []string {
118	var domains []string
119	domainsSet := make(map[string]bool)
120
121	namespace := u.GetEnterpriseMeta().NamespaceOrDefault()
122	switch {
123	case len(u.IngressHosts) > 0:
124		// If a user has specified hosts, do not add the default
125		// "<service-name>.ingress.*" prefixes
126		domains = u.IngressHosts
127	case namespace != structs.IntentionDefaultNamespace:
128		domains = []string{fmt.Sprintf("%s.ingress.%s.*", u.DestinationName, namespace)}
129	default:
130		domains = []string{fmt.Sprintf("%s.ingress.*", u.DestinationName)}
131	}
132
133	for _, h := range domains {
134		domainsSet[h] = true
135	}
136
137	// Host headers may contain port numbers in them, so we need to make sure
138	// we match on the host with and without the port number. Well-known
139	// ports like HTTP/HTTPS are stripped from Host headers, but other ports
140	// will be in the header.
141	for _, h := range domains {
142		_, _, err := net.SplitHostPort(h)
143		// Error message from Go's net/ipsock.go
144		// We check to see if a port is not missing, and ignore the
145		// error from SplitHostPort otherwise, since we have previously
146		// validated the Host values and should trust the user's input.
147		if err == nil || !strings.Contains(err.Error(), "missing port in address") {
148			continue
149		}
150
151		domainWithPort := fmt.Sprintf("%s:%d", h, listenerKey.Port)
152
153		// Do not add a duplicate domain if a hostname with port is already in the
154		// set
155		if !domainsSet[domainWithPort] {
156			domains = append(domains, domainWithPort)
157		}
158	}
159
160	return domains
161}
162
163func makeUpstreamRouteForDiscoveryChain(
164	cInfo connectionInfo,
165	routeName string,
166	chain *structs.CompiledDiscoveryChain,
167	serviceDomains []string,
168) (*envoyroute.VirtualHost, error) {
169	var routes []*envoyroute.Route
170
171	startNode := chain.Nodes[chain.StartNode]
172	if startNode == nil {
173		panic("missing first node in compiled discovery chain for: " + chain.ServiceName)
174	}
175
176	switch startNode.Type {
177	case structs.DiscoveryGraphNodeTypeRouter:
178		routes = make([]*envoyroute.Route, 0, len(startNode.Routes))
179
180		for _, discoveryRoute := range startNode.Routes {
181			routeMatch := makeRouteMatchForDiscoveryRoute(cInfo, discoveryRoute, chain.Protocol)
182
183			var (
184				routeAction *envoyroute.Route_Route
185				err         error
186			)
187
188			nextNode := chain.Nodes[discoveryRoute.NextNode]
189			switch nextNode.Type {
190			case structs.DiscoveryGraphNodeTypeSplitter:
191				routeAction, err = makeRouteActionForSplitter(nextNode.Splits, chain)
192				if err != nil {
193					return nil, err
194				}
195
196			case structs.DiscoveryGraphNodeTypeResolver:
197				routeAction = makeRouteActionForSingleCluster(nextNode.Resolver.Target, chain)
198
199			default:
200				return nil, fmt.Errorf("unexpected graph node after route %q", nextNode.Type)
201			}
202
203			// TODO(rb): Better help handle the envoy case where you need (prefix=/foo/,rewrite=/) and (exact=/foo,rewrite=/) to do a full rewrite
204
205			destination := discoveryRoute.Definition.Destination
206			if destination != nil {
207				if destination.PrefixRewrite != "" {
208					routeAction.Route.PrefixRewrite = destination.PrefixRewrite
209				}
210
211				if destination.RequestTimeout > 0 {
212					routeAction.Route.Timeout = ptypes.DurationProto(destination.RequestTimeout)
213				}
214
215				if destination.HasRetryFeatures() {
216					retryPolicy := &envoyroute.RetryPolicy{}
217					if destination.NumRetries > 0 {
218						retryPolicy.NumRetries = makeUint32Value(int(destination.NumRetries))
219					}
220
221					// The RetryOn magic values come from: https://www.envoyproxy.io/docs/envoy/v1.10.0/configuration/http_filters/router_filter#config-http-filters-router-x-envoy-retry-on
222					if destination.RetryOnConnectFailure {
223						retryPolicy.RetryOn = "connect-failure"
224					}
225					if len(destination.RetryOnStatusCodes) > 0 {
226						if retryPolicy.RetryOn != "" {
227							retryPolicy.RetryOn = retryPolicy.RetryOn + ",retriable-status-codes"
228						} else {
229							retryPolicy.RetryOn = "retriable-status-codes"
230						}
231						retryPolicy.RetriableStatusCodes = destination.RetryOnStatusCodes
232					}
233
234					routeAction.Route.RetryPolicy = retryPolicy
235				}
236			}
237
238			routes = append(routes, &envoyroute.Route{
239				Match:  routeMatch,
240				Action: routeAction,
241			})
242		}
243
244	case structs.DiscoveryGraphNodeTypeSplitter:
245		routeAction, err := makeRouteActionForSplitter(startNode.Splits, chain)
246		if err != nil {
247			return nil, err
248		}
249
250		defaultRoute := &envoyroute.Route{
251			Match:  makeDefaultRouteMatch(),
252			Action: routeAction,
253		}
254
255		routes = []*envoyroute.Route{defaultRoute}
256
257	case structs.DiscoveryGraphNodeTypeResolver:
258		routeAction := makeRouteActionForSingleCluster(startNode.Resolver.Target, chain)
259
260		defaultRoute := &envoyroute.Route{
261			Match:  makeDefaultRouteMatch(),
262			Action: routeAction,
263		}
264
265		routes = []*envoyroute.Route{defaultRoute}
266
267	default:
268		panic("unknown first node in discovery chain of type: " + startNode.Type)
269	}
270
271	host := &envoyroute.VirtualHost{
272		Name:    routeName,
273		Domains: serviceDomains,
274		Routes:  routes,
275	}
276
277	return host, nil
278}
279
280func makeRouteMatchForDiscoveryRoute(cInfo connectionInfo, discoveryRoute *structs.DiscoveryRoute, protocol string) *envoyroute.RouteMatch {
281	match := discoveryRoute.Definition.Match
282	if match == nil || match.IsEmpty() {
283		return makeDefaultRouteMatch()
284	}
285
286	em := &envoyroute.RouteMatch{}
287
288	switch {
289	case match.HTTP.PathExact != "":
290		em.PathSpecifier = &envoyroute.RouteMatch_Path{
291			Path: match.HTTP.PathExact,
292		}
293	case match.HTTP.PathPrefix != "":
294		em.PathSpecifier = &envoyroute.RouteMatch_Prefix{
295			Prefix: match.HTTP.PathPrefix,
296		}
297	case match.HTTP.PathRegex != "":
298		if cInfo.ProxyFeatures.RouterMatchSafeRegex {
299			em.PathSpecifier = &envoyroute.RouteMatch_SafeRegex{
300				SafeRegex: makeEnvoyRegexMatch(match.HTTP.PathRegex),
301			}
302		} else {
303			em.PathSpecifier = &envoyroute.RouteMatch_Regex{
304				Regex: match.HTTP.PathRegex,
305			}
306		}
307	default:
308		em.PathSpecifier = &envoyroute.RouteMatch_Prefix{
309			Prefix: "/",
310		}
311	}
312
313	if len(match.HTTP.Header) > 0 {
314		em.Headers = make([]*envoyroute.HeaderMatcher, 0, len(match.HTTP.Header))
315		for _, hdr := range match.HTTP.Header {
316			eh := &envoyroute.HeaderMatcher{
317				Name: hdr.Name,
318			}
319
320			switch {
321			case hdr.Exact != "":
322				eh.HeaderMatchSpecifier = &envoyroute.HeaderMatcher_ExactMatch{
323					ExactMatch: hdr.Exact,
324				}
325			case hdr.Regex != "":
326				if cInfo.ProxyFeatures.RouterMatchSafeRegex {
327					eh.HeaderMatchSpecifier = &envoyroute.HeaderMatcher_SafeRegexMatch{
328						SafeRegexMatch: makeEnvoyRegexMatch(hdr.Regex),
329					}
330				} else {
331					eh.HeaderMatchSpecifier = &envoyroute.HeaderMatcher_RegexMatch{
332						RegexMatch: hdr.Regex,
333					}
334				}
335			case hdr.Prefix != "":
336				eh.HeaderMatchSpecifier = &envoyroute.HeaderMatcher_PrefixMatch{
337					PrefixMatch: hdr.Prefix,
338				}
339			case hdr.Suffix != "":
340				eh.HeaderMatchSpecifier = &envoyroute.HeaderMatcher_SuffixMatch{
341					SuffixMatch: hdr.Suffix,
342				}
343			case hdr.Present:
344				eh.HeaderMatchSpecifier = &envoyroute.HeaderMatcher_PresentMatch{
345					PresentMatch: true,
346				}
347			default:
348				continue // skip this impossible situation
349			}
350
351			if hdr.Invert {
352				eh.InvertMatch = true
353			}
354
355			em.Headers = append(em.Headers, eh)
356		}
357	}
358
359	if len(match.HTTP.Methods) > 0 {
360		methodHeaderRegex := strings.Join(match.HTTP.Methods, "|")
361
362		eh := &envoyroute.HeaderMatcher{
363			Name: ":method",
364		}
365		if cInfo.ProxyFeatures.RouterMatchSafeRegex {
366			eh.HeaderMatchSpecifier = &envoyroute.HeaderMatcher_SafeRegexMatch{
367				SafeRegexMatch: makeEnvoyRegexMatch(methodHeaderRegex),
368			}
369		} else {
370			eh.HeaderMatchSpecifier = &envoyroute.HeaderMatcher_RegexMatch{
371				RegexMatch: methodHeaderRegex,
372			}
373		}
374
375		em.Headers = append(em.Headers, eh)
376	}
377
378	if len(match.HTTP.QueryParam) > 0 {
379		em.QueryParameters = make([]*envoyroute.QueryParameterMatcher, 0, len(match.HTTP.QueryParam))
380		for _, qm := range match.HTTP.QueryParam {
381			eq := &envoyroute.QueryParameterMatcher{
382				Name: qm.Name,
383			}
384
385			switch {
386			case qm.Exact != "":
387				if cInfo.ProxyFeatures.RouterMatchSafeRegex {
388					eq.QueryParameterMatchSpecifier = &envoyroute.QueryParameterMatcher_StringMatch{
389						StringMatch: &envoymatcher.StringMatcher{
390							MatchPattern: &envoymatcher.StringMatcher_Exact{
391								Exact: qm.Exact,
392							},
393						},
394					}
395				} else {
396					eq.Value = qm.Exact
397				}
398			case qm.Regex != "":
399				if cInfo.ProxyFeatures.RouterMatchSafeRegex {
400					eq.QueryParameterMatchSpecifier = &envoyroute.QueryParameterMatcher_StringMatch{
401						StringMatch: &envoymatcher.StringMatcher{
402							MatchPattern: &envoymatcher.StringMatcher_SafeRegex{
403								SafeRegex: makeEnvoyRegexMatch(qm.Regex),
404							},
405						},
406					}
407				} else {
408					eq.Value = qm.Regex
409					eq.Regex = makeBoolValue(true)
410				}
411			case qm.Present:
412				if cInfo.ProxyFeatures.RouterMatchSafeRegex {
413					eq.QueryParameterMatchSpecifier = &envoyroute.QueryParameterMatcher_PresentMatch{
414						PresentMatch: true,
415					}
416				} else {
417					eq.Value = ""
418				}
419			default:
420				continue // skip this impossible situation
421			}
422
423			em.QueryParameters = append(em.QueryParameters, eq)
424		}
425	}
426
427	return em
428}
429
430func makeDefaultRouteMatch() *envoyroute.RouteMatch {
431	return &envoyroute.RouteMatch{
432		PathSpecifier: &envoyroute.RouteMatch_Prefix{
433			Prefix: "/",
434		},
435		// TODO(banks) Envoy supports matching only valid GRPC
436		// requests which might be nice to add here for gRPC services
437		// but it's not supported in our current envoy SDK version
438		// although docs say it was supported by 1.8.0. Going to defer
439		// that until we've updated the deps.
440	}
441}
442
443func makeRouteActionForSingleCluster(targetID string, chain *structs.CompiledDiscoveryChain) *envoyroute.Route_Route {
444	target := chain.Targets[targetID]
445
446	clusterName := CustomizeClusterName(target.Name, chain)
447
448	return &envoyroute.Route_Route{
449		Route: &envoyroute.RouteAction{
450			ClusterSpecifier: &envoyroute.RouteAction_Cluster{
451				Cluster: clusterName,
452			},
453		},
454	}
455}
456
457func makeRouteActionForSplitter(splits []*structs.DiscoverySplit, chain *structs.CompiledDiscoveryChain) (*envoyroute.Route_Route, error) {
458	clusters := make([]*envoyroute.WeightedCluster_ClusterWeight, 0, len(splits))
459	for _, split := range splits {
460		nextNode := chain.Nodes[split.NextNode]
461
462		if nextNode.Type != structs.DiscoveryGraphNodeTypeResolver {
463			return nil, fmt.Errorf("unexpected splitter destination node type: %s", nextNode.Type)
464		}
465		targetID := nextNode.Resolver.Target
466
467		target := chain.Targets[targetID]
468
469		clusterName := CustomizeClusterName(target.Name, chain)
470
471		// The smallest representable weight is 1/10000 or .01% but envoy
472		// deals with integers so scale everything up by 100x.
473		cw := &envoyroute.WeightedCluster_ClusterWeight{
474			Weight: makeUint32Value(int(split.Weight * 100)),
475			Name:   clusterName,
476		}
477
478		clusters = append(clusters, cw)
479	}
480
481	return &envoyroute.Route_Route{
482		Route: &envoyroute.RouteAction{
483			ClusterSpecifier: &envoyroute.RouteAction_WeightedClusters{
484				WeightedClusters: &envoyroute.WeightedCluster{
485					Clusters:    clusters,
486					TotalWeight: makeUint32Value(10000), // scaled up 100%
487				},
488			},
489		},
490	}, nil
491}
492
493func makeEnvoyRegexMatch(patt string) *envoymatcher.RegexMatcher {
494	return &envoymatcher.RegexMatcher{
495		EngineType: &envoymatcher.RegexMatcher_GoogleRe2{
496			GoogleRe2: &envoymatcher.RegexMatcher_GoogleRE2{},
497		},
498		Regex: patt,
499	}
500}
501