TUN-3441: Multiple-origin routing via ingress rules

This commit is contained in:
Adam Chalmers 2020-10-12 12:54:15 -05:00
parent 0eebc7cef9
commit 4a4a1bb6b1
5 changed files with 114 additions and 23 deletions

View File

@ -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),

View File

@ -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)
}
}
}

View File

@ -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
}

View File

@ -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)
}
}

View File

@ -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
}