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