From 4a4a1bb6b179655ab1b78f2ce270fb8a2b98e57d Mon Sep 17 00:00:00 2001 From: Adam Chalmers Date: Mon, 12 Oct 2020 12:54:15 -0500 Subject: [PATCH] TUN-3441: Multiple-origin routing via ingress rules --- cmd/cloudflared/tunnel/cmd.go | 2 +- cmd/cloudflared/tunnel/configuration.go | 25 +++++++++--- ingress/ingress.go | 38 +++++++++++------- ingress/ingress_test.go | 51 ++++++++++++++++++++++++- origin/tunnel.go | 21 ++++++++-- 5 files changed, 114 insertions(+), 23 deletions(-) diff --git a/cmd/cloudflared/tunnel/cmd.go b/cmd/cloudflared/tunnel/cmd.go index 46f11fd0..a54fd686 100644 --- a/cmd/cloudflared/tunnel/cmd.go +++ b/cmd/cloudflared/tunnel/cmd.go @@ -128,7 +128,7 @@ func Commands() []*cli.Command { dbConnectCmd(), } - return []*cli.Command { + return []*cli.Command{ buildTunnelCommand(subcommands), // for compatibility, allow following as top-level subcommands buildLoginSubcommand(true), diff --git a/cmd/cloudflared/tunnel/configuration.go b/cmd/cloudflared/tunnel/configuration.go index 0490269d..c91d1008 100644 --- a/cmd/cloudflared/tunnel/configuration.go +++ b/cmd/cloudflared/tunnel/configuration.go @@ -232,16 +232,17 @@ func prepareTunnelConfig( Arch: fmt.Sprintf("%s_%s", buildInfo.GoOS, buildInfo.GoArch), } ingressRules, err = config.ReadRules(c) - if err != nil { + if err != nil && err != ingress.ErrNoIngressRules { return nil, err } - if c.IsSet("url") { + if len(ingressRules) > 0 && c.IsSet("url") { return nil, ingress.ErrURLIncompatibleWithIngress } } var originURL string - if len(ingressRules) == 0 { + isUsingMultipleOrigins := len(ingressRules) > 0 + if !isUsingMultipleOrigins { originURL, err = config.ValidateUrl(c, compatibilityMode) if err != nil { logger.Errorf("Error validating origin URL: %s", err) @@ -271,8 +272,22 @@ func prepareTunnelConfig( } // If tunnel running in bastion mode, a connection to origin will not exist until initiated by the client. if !c.IsSet(bastionFlag) { - if err = validation.ValidateHTTPService(originURL, hostname, httpTransport); err != nil { - logger.Errorf("unable to connect to the origin: %s", err) + + // List all origin URLs that require validation + var originURLs []string + if !isUsingMultipleOrigins { + originURLs = append(originURLs, originURL) + } else { + for _, rule := range ingressRules { + originURLs = append(originURLs, rule.Service.String()) + } + } + + // Validate each origin URL + for _, u := range originURLs { + if err = validation.ValidateHTTPService(u, hostname, httpTransport); err != nil { + logger.Errorf("unable to connect to the origin: %s", err) + } } } diff --git a/ingress/ingress.go b/ingress/ingress.go index fcfab77d..3489bbcb 100644 --- a/ingress/ingress.go +++ b/ingress/ingress.go @@ -11,7 +11,7 @@ import ( ) var ( - errNoIngressRules = errors.New("No ingress rules were specified in the config file") + ErrNoIngressRules = errors.New("No ingress rules were specified in the config file") errLastRuleNotCatchAll = errors.New("The last ingress rule must match all hostnames (i.e. it must be missing, or must be \"*\")") errBadWildcard = errors.New("Hostname patterns can have at most one wildcard character (\"*\") and it can only be used for subdomains, e.g. \"*.example.com\"") errNoIngressRulesMatch = errors.New("The URL didn't match any ingress rules") @@ -50,12 +50,24 @@ func (r Rule) String() string { return out.String() } -func (r Rule) matches(requestURL *url.URL) bool { - hostMatch := r.Hostname == "" || r.Hostname == "*" || matchHost(r.Hostname, requestURL.Hostname()) - pathMatch := r.Path == nil || r.Path.MatchString(requestURL.Path) +func (r *Rule) Matches(hostname, path string) bool { + hostMatch := r.Hostname == "" || r.Hostname == "*" || matchHost(r.Hostname, hostname) + pathMatch := r.Path == nil || r.Path.MatchString(path) return hostMatch && pathMatch } +// FindMatchingRule returns the index of the Ingress Rule which matches the given +// hostname and path. This function assumes the last rule matches everything, +// which is the case if the rules were instantiated via the ingress#Validate method +func FindMatchingRule(hostname, path string, rules []Rule) int { + for i, rule := range rules { + if rule.Matches(hostname, path) { + return i + } + } + return len(rules) - 1 +} + func matchHost(ruleHost, reqHost string) bool { if ruleHost == reqHost { return true @@ -94,6 +106,10 @@ func (ing ingress) validate() ([]Rule, error) { return nil, fmt.Errorf("The service %s must have a scheme and a hostname", r.Service) } + if service.Path != "" { + return nil, fmt.Errorf("%s is an invalid address, ingress rules don't support proxying to a different path on the origin service. The path will be the same as the eyeball request's path.", r.Service) + } + // Ensure that there are no wildcards anywhere except the first character // of the hostname. if strings.LastIndex(r.Hostname, "*") > 0 { @@ -145,7 +161,7 @@ func ParseIngress(rawYAML []byte) ([]Rule, error) { return nil, err } if len(ing.Ingress) == 0 { - return nil, errNoIngressRules + return nil, ErrNoIngressRules } return ing.validate() } @@ -155,12 +171,8 @@ func RuleCommand(rules []Rule, requestURL *url.URL) error { if requestURL.Hostname() == "" { return fmt.Errorf("%s is malformed and doesn't have a hostname", requestURL) } - for i, r := range rules { - if r.matches(requestURL) { - fmt.Printf("Matched rule #%d\n", i+1) - fmt.Println(r.String()) - return nil - } - } - return errNoIngressRulesMatch + i := FindMatchingRule(requestURL.Hostname(), requestURL.Path, rules) + fmt.Printf("Matched rule #%d\n", i+1) + fmt.Println(rules[i].String()) + return nil } diff --git a/ingress/ingress_test.go b/ingress/ingress_test.go index 1d5cd379..99450a0c 100644 --- a/ingress/ingress_test.go +++ b/ingress/ingress_test.go @@ -135,6 +135,33 @@ ingress: args: args{rawYAML: ` ingress: - service: localhost:8000 +`}, + wantErr: true, + }, + { + name: "Wildcard not at start", + args: args{rawYAML: ` +ingress: + - hostname: "test.*.example.com" + service: https://localhost:8000 +`}, + wantErr: true, + }, + { + name: "Can't use --url", + args: args{rawYAML: ` +url: localhost:8080 +ingress: + - hostname: "*.example.com" + service: https://localhost:8000 +`}, + wantErr: true, + }, + { + name: "Service can't have a path", + args: args{rawYAML: ` +ingress: + - service: https://localhost:8000/static/ `}, wantErr: true, }, @@ -263,9 +290,31 @@ func Test_rule_matches(t *testing.T) { Path: tt.fields.Path, Service: tt.fields.Service, } - if got := r.matches(tt.args.requestURL); got != tt.want { + u := tt.args.requestURL + if got := r.Matches(u.Hostname(), u.Path); got != tt.want { t.Errorf("rule.matches() = %v, want %v", got, tt.want) } }) } } + +func BenchmarkFindMatch(b *testing.B) { + rulesYAML := ` +ingress: + - hostname: tunnel1.example.com + service: https://localhost:8000 + - hostname: tunnel2.example.com + service: https://localhost:8001 + - hostname: "*" + service: https://localhost:8002 +` + rules, err := ParseIngress([]byte(rulesYAML)) + if err != nil { + b.Error(err) + } + for n := 0; n < b.N; n++ { + FindMatchingRule("tunnel1.example.com", "", rules) + FindMatchingRule("tunnel2.example.com", "", rules) + FindMatchingRule("tunnel3.example.com", "", rules) + } +} diff --git a/origin/tunnel.go b/origin/tunnel.go index b9ea8c1e..997ea779 100644 --- a/origin/tunnel.go +++ b/origin/tunnel.go @@ -619,6 +619,7 @@ func LogServerInfo( type TunnelHandler struct { originUrl string + ingressRules []ingress.Rule httpHostHeader string muxer *h2mux.Muxer httpClient http.RoundTripper @@ -640,12 +641,20 @@ func NewTunnelHandler(ctx context.Context, connectionID uint8, bufferPool *buffer.Pool, ) (*TunnelHandler, string, error) { - originURL, err := validation.ValidateUrl(config.OriginUrl) - if err != nil { - return nil, "", fmt.Errorf("unable to parse origin URL %#v", originURL) + + // Check single-origin config + var originURL string + var err error + if len(config.IngressRules) == 0 { + originURL, err = validation.ValidateUrl(config.OriginUrl) + if err != nil { + return nil, "", fmt.Errorf("unable to parse origin URL %#v", originURL) + } } + h := &TunnelHandler{ originUrl: originURL, + ingressRules: config.IngressRules, httpHostHeader: config.HTTPHostHeader, httpClient: config.HTTPTransport, tlsConfig: config.ClientTlsConfig, @@ -718,6 +727,12 @@ func (h *TunnelHandler) createRequest(stream *h2mux.MuxedStream) (*http.Request, return nil, errors.Wrap(err, "invalid request received") } h.AppendTagHeaders(req) + if len(h.ingressRules) > 0 { + ruleNumber := ingress.FindMatchingRule(req.Host, req.URL.Path, h.ingressRules) + destination := h.ingressRules[ruleNumber].Service + req.URL.Host = destination.Host + req.URL.Scheme = destination.Scheme + } return req, nil }