TUN-3438: move ingress into own package, read into TunnelConfig

This commit is contained in:
Adam Chalmers 2020-10-08 19:12:29 -05:00
parent 53a1fa46a8
commit 0eebc7cef9
6 changed files with 141 additions and 111 deletions

View File

@ -3,6 +3,7 @@ package config
import ( import (
"errors" "errors"
"fmt" "fmt"
"io/ioutil"
"os" "os"
"path/filepath" "path/filepath"
"runtime" "runtime"
@ -11,6 +12,7 @@ import (
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
"gopkg.in/yaml.v2" "gopkg.in/yaml.v2"
"github.com/cloudflare/cloudflared/ingress"
"github.com/cloudflare/cloudflared/validation" "github.com/cloudflare/cloudflared/validation"
) )
@ -193,3 +195,17 @@ func ValidateUrl(c *cli.Context, allowFromArgs bool) (string, error) {
validUrl, err := validation.ValidateUrl(url) validUrl, err := validation.ValidateUrl(url)
return validUrl, err return validUrl, err
} }
func ReadRules(c *cli.Context) ([]ingress.Rule, error) {
configFilePath := c.String("config")
if configFilePath == "" {
return nil, ErrNoConfigFile
}
fmt.Printf("Reading from config file %s\n", configFilePath)
configBytes, err := ioutil.ReadFile(configFilePath)
if err != nil {
return nil, err
}
rules, err := ingress.ParseIngress(configBytes)
return rules, err
}

View File

@ -26,6 +26,7 @@ import (
"github.com/cloudflare/cloudflared/dbconnect" "github.com/cloudflare/cloudflared/dbconnect"
"github.com/cloudflare/cloudflared/h2mux" "github.com/cloudflare/cloudflared/h2mux"
"github.com/cloudflare/cloudflared/hello" "github.com/cloudflare/cloudflared/hello"
"github.com/cloudflare/cloudflared/ingress"
"github.com/cloudflare/cloudflared/logger" "github.com/cloudflare/cloudflared/logger"
"github.com/cloudflare/cloudflared/metrics" "github.com/cloudflare/cloudflared/metrics"
"github.com/cloudflare/cloudflared/origin" "github.com/cloudflare/cloudflared/origin"
@ -1248,3 +1249,64 @@ reconnect [delay]
} }
} }
} }
func buildValidateCommand() *cli.Command {
return &cli.Command{
Name: "validate",
Action: cliutil.ErrorHandler(ValidateCommand),
Usage: "Validate the ingress configuration ",
UsageText: "cloudflared tunnel [--config FILEPATH] ingress validate",
Description: "Validates the configuration file, ensuring your ingress rules are OK.",
}
}
func buildRuleCommand() *cli.Command {
return &cli.Command{
Name: "rule",
Action: cliutil.ErrorHandler(RuleCommand),
Usage: "Check which ingress rule matches a given request URL",
UsageText: "cloudflared tunnel [--config FILEPATH] ingress rule URL",
ArgsUsage: "URL",
Description: "Check which ingress rule matches a given request URL. " +
"Ingress rules match a request's hostname and path. Hostname is " +
"optional and is either a full hostname like `www.example.com` or a " +
"hostname with a `*` for its subdomains, e.g. `*.example.com`. Path " +
"is optional and matches a regular expression, like `/[a-zA-Z0-9_]+.html`",
}
}
// Validates the ingress rules in the cloudflared config file
func ValidateCommand(c *cli.Context) error {
_, err := config.ReadRules(c)
if err != nil {
return errors.Wrap(err, "Validation failed")
}
if c.IsSet("url") {
return ingress.ErrURLIncompatibleWithIngress
}
fmt.Println("OK")
return nil
}
// Checks which ingress rule matches the given URL.
func RuleCommand(c *cli.Context) error {
rules, err := config.ReadRules(c)
if err != nil {
return err
}
requestArg := c.Args().First()
if requestArg == "" {
return errors.New("cloudflared tunnel rule expects a single argument, the URL to test")
}
requestURL, err := url.Parse(requestArg)
if err != nil {
return fmt.Errorf("%s is not a valid URL", requestArg)
}
if requestURL.Hostname() == "" && requestURL.Scheme == "" {
return fmt.Errorf("%s doesn't have a hostname, consider adding a scheme", requestArg)
}
if requestURL.Hostname() == "" {
return fmt.Errorf("%s doesn't have a hostname", requestArg)
}
return ingress.RuleCommand(rules, requestURL)
}

View File

@ -14,6 +14,7 @@ import (
"github.com/cloudflare/cloudflared/cmd/cloudflared/buildinfo" "github.com/cloudflare/cloudflared/cmd/cloudflared/buildinfo"
"github.com/cloudflare/cloudflared/cmd/cloudflared/config" "github.com/cloudflare/cloudflared/cmd/cloudflared/config"
"github.com/cloudflare/cloudflared/ingress"
"github.com/cloudflare/cloudflared/logger" "github.com/cloudflare/cloudflared/logger"
"github.com/cloudflare/cloudflared/origin" "github.com/cloudflare/cloudflared/origin"
"github.com/cloudflare/cloudflared/tlsconfig" "github.com/cloudflare/cloudflared/tlsconfig"
@ -184,12 +185,6 @@ func prepareTunnelConfig(
tags = append(tags, tunnelpogs.Tag{Name: "ID", Value: clientID}) tags = append(tags, tunnelpogs.Tag{Name: "ID", Value: clientID})
originURL, err := config.ValidateUrl(c, compatibilityMode)
if err != nil {
logger.Errorf("Error validating origin URL: %s", err)
return nil, errors.Wrap(err, "Error validating origin URL")
}
var originCert []byte var originCert []byte
if !isFreeTunnel { if !isFreeTunnel {
originCert, err = getOriginCert(c, logger) originCert, err = getOriginCert(c, logger)
@ -224,6 +219,36 @@ func prepareTunnelConfig(
} }
dialContext := dialer.DialContext dialContext := dialer.DialContext
var ingressRules []ingress.Rule
if namedTunnel != nil {
clientUUID, err := uuid.NewRandom()
if err != nil {
return nil, errors.Wrap(err, "can't generate clientUUID")
}
namedTunnel.Client = tunnelpogs.ClientInfo{
ClientID: clientUUID[:],
Features: []string{origin.FeatureSerializedHeaders},
Version: version,
Arch: fmt.Sprintf("%s_%s", buildInfo.GoOS, buildInfo.GoArch),
}
ingressRules, err = config.ReadRules(c)
if err != nil {
return nil, err
}
if c.IsSet("url") {
return nil, ingress.ErrURLIncompatibleWithIngress
}
}
var originURL string
if len(ingressRules) == 0 {
originURL, err = config.ValidateUrl(c, compatibilityMode)
if err != nil {
logger.Errorf("Error validating origin URL: %s", err)
return nil, errors.Wrap(err, "Error validating origin URL")
}
}
if c.IsSet("unix-socket") { if c.IsSet("unix-socket") {
unixSocket, err := config.ValidateUnixSocket(c) unixSocket, err := config.ValidateUnixSocket(c)
if err != nil { if err != nil {
@ -256,20 +281,6 @@ func prepareTunnelConfig(
logger.Errorf("unable to create TLS config to connect with edge: %s", err) logger.Errorf("unable to create TLS config to connect with edge: %s", err)
return nil, errors.Wrap(err, "unable to create TLS config to connect with edge") return nil, errors.Wrap(err, "unable to create TLS config to connect with edge")
} }
if namedTunnel != nil {
clientUUID, err := uuid.NewRandom()
if err != nil {
return nil, errors.Wrap(err, "can't generate clientUUID")
}
namedTunnel.Client = tunnelpogs.ClientInfo{
ClientID: clientUUID[:],
Features: []string{origin.FeatureSerializedHeaders},
Version: version,
Arch: fmt.Sprintf("%s_%s", buildInfo.GoOS, buildInfo.GoArch),
}
}
return &origin.TunnelConfig{ return &origin.TunnelConfig{
BuildInfo: buildInfo, BuildInfo: buildInfo,
ClientID: clientID, ClientID: clientID,
@ -301,6 +312,7 @@ func prepareTunnelConfig(
TlsConfig: toEdgeTLSConfig, TlsConfig: toEdgeTLSConfig,
NamedTunnel: namedTunnel, NamedTunnel: namedTunnel,
ReplaceExisting: c.Bool("force"), ReplaceExisting: c.Bool("force"),
IngressRules: ingressRules,
// turn off use of reconnect token and auth refresh when using named tunnels // turn off use of reconnect token and auth refresh when using named tunnels
UseReconnectToken: compatibilityMode && c.Bool("use-reconnect-token"), UseReconnectToken: compatibilityMode && c.Bool("use-reconnect-token"),
}, nil }, nil

View File

@ -1,17 +1,12 @@
package tunnel package ingress
import ( import (
"fmt" "fmt"
"io/ioutil"
"net/url" "net/url"
"regexp" "regexp"
"strings" "strings"
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/urfave/cli/v2"
"gopkg.in/yaml.v2" "gopkg.in/yaml.v2"
) )
@ -20,11 +15,12 @@ var (
errLastRuleNotCatchAll = errors.New("The last ingress rule must match all hostnames (i.e. it must be missing, or must be \"*\")") 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\"") 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") errNoIngressRulesMatch = errors.New("The URL didn't match any ingress rules")
ErrURLIncompatibleWithIngress = errors.New("You can't set the --url flag (or $TUNNEL_URL) when using multiple-origin ingress rules")
) )
// Each rule route traffic from a hostname/path on the public // Each rule route traffic from a hostname/path on the public
// internet to the service running on the given URL. // internet to the service running on the given URL.
type rule struct { type Rule struct {
// Requests for this hostname will be proxied to this rule's service. // Requests for this hostname will be proxied to this rule's service.
Hostname string Hostname string
@ -37,7 +33,7 @@ type rule struct {
Service *url.URL Service *url.URL
} }
func (r rule) String() string { func (r Rule) String() string {
var out strings.Builder var out strings.Builder
if r.Hostname != "" { if r.Hostname != "" {
out.WriteString("\thostname: ") out.WriteString("\thostname: ")
@ -54,7 +50,7 @@ func (r rule) String() string {
return out.String() return out.String()
} }
func (r rule) matches(requestURL *url.URL) bool { func (r Rule) matches(requestURL *url.URL) bool {
hostMatch := r.Hostname == "" || r.Hostname == "*" || matchHost(r.Hostname, requestURL.Hostname()) hostMatch := r.Hostname == "" || r.Hostname == "*" || matchHost(r.Hostname, requestURL.Hostname())
pathMatch := r.Path == nil || r.Path.MatchString(requestURL.Path) pathMatch := r.Path == nil || r.Path.MatchString(requestURL.Path)
return hostMatch && pathMatch return hostMatch && pathMatch
@ -81,10 +77,14 @@ type unvalidatedRule struct {
type ingress struct { type ingress struct {
Ingress []unvalidatedRule Ingress []unvalidatedRule
Url string
} }
func (ing ingress) validate() ([]rule, error) { func (ing ingress) validate() ([]Rule, error) {
rules := make([]rule, len(ing.Ingress)) if ing.Url != "" {
return nil, ErrURLIncompatibleWithIngress
}
rules := make([]Rule, len(ing.Ingress))
for i, r := range ing.Ingress { for i, r := range ing.Ingress {
service, err := url.Parse(r.Service) service, err := url.Parse(r.Service)
if err != nil { if err != nil {
@ -119,7 +119,7 @@ func (ing ingress) validate() ([]rule, error) {
} }
} }
rules[i] = rule{ rules[i] = Rule{
Hostname: r.Hostname, Hostname: r.Hostname,
Service: service, Service: service,
Path: pathRegex, Path: pathRegex,
@ -139,7 +139,7 @@ func (e errRuleShouldNotBeCatchAll) Error() string {
"will never be triggered.", e.i+1, e.hostname) "will never be triggered.", e.i+1, e.hostname)
} }
func parseIngress(rawYAML []byte) ([]rule, error) { func ParseIngress(rawYAML []byte) ([]Rule, error) {
var ing ingress var ing ingress
if err := yaml.Unmarshal(rawYAML, &ing); err != nil { if err := yaml.Unmarshal(rawYAML, &ing); err != nil {
return nil, err return nil, err
@ -150,57 +150,10 @@ func parseIngress(rawYAML []byte) ([]rule, error) {
return ing.validate() return ing.validate()
} }
func ingressContext(c *cli.Context) ([]rule, error) { // RuleCommand checks which ingress rule matches the given request URL.
configFilePath := c.String("config") func RuleCommand(rules []Rule, requestURL *url.URL) error {
if configFilePath == "" {
return nil, config.ErrNoConfigFile
}
fmt.Printf("Reading from config file %s\n", configFilePath)
configBytes, err := ioutil.ReadFile(configFilePath)
if err != nil {
return nil, err
}
rules, err := parseIngress(configBytes)
return rules, err
}
// Validates the ingress rules in the cloudflared config file
func validateCommand(c *cli.Context) error {
_, err := ingressContext(c)
if err != nil {
fmt.Println(err.Error())
return errors.New("Validation failed")
}
fmt.Println("OK")
return nil
}
func buildValidateCommand() *cli.Command {
return &cli.Command{
Name: "validate",
Action: cliutil.ErrorHandler(validateCommand),
Usage: "Validate the ingress configuration ",
UsageText: "cloudflared tunnel [--config FILEPATH] ingress validate",
Description: "Validates the configuration file, ensuring your ingress rules are OK.",
}
}
// Checks which ingress rule matches the given URL.
func ruleCommand(c *cli.Context) error {
rules, err := ingressContext(c)
if err != nil {
return err
}
requestArg := c.Args().First()
if requestArg == "" {
return errors.New("cloudflared tunnel rule expects a single argument, the URL to test")
}
requestURL, err := url.Parse(requestArg)
if err != nil {
return fmt.Errorf("%s is not a valid URL", requestArg)
}
if requestURL.Hostname() == "" { if requestURL.Hostname() == "" {
return fmt.Errorf("%s is malformed and doesn't have a hostname", requestArg) return fmt.Errorf("%s is malformed and doesn't have a hostname", requestURL)
} }
for i, r := range rules { for i, r := range rules {
if r.matches(requestURL) { if r.matches(requestURL) {
@ -211,18 +164,3 @@ func ruleCommand(c *cli.Context) error {
} }
return errNoIngressRulesMatch return errNoIngressRulesMatch
} }
func buildRuleCommand() *cli.Command {
return &cli.Command{
Name: "rule",
Action: cliutil.ErrorHandler(ruleCommand),
Usage: "Check which ingress rule matches a given request URL",
UsageText: "cloudflared [--config CONFIGFILE] tunnel ingress rule URL",
ArgsUsage: "URL",
Description: "Check which ingress rule matches a given request URL. " +
"Ingress rules match a request's hostname and path. Hostname is " +
"optional and is either a full hostname like `www.example.com` or a " +
"hostname with a `*` for its subdomains, e.g. `*.example.com`. Path " +
"is optional and matches a regular expression, like `/[a-zA-Z0-9_]+.html`",
}
}

View File

@ -1,4 +1,4 @@
package tunnel package ingress
import ( import (
"net/url" "net/url"
@ -20,7 +20,7 @@ func Test_parseIngress(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
args args args args
want []rule want []Rule
wantErr bool wantErr bool
}{ }{
{ {
@ -37,7 +37,7 @@ ingress:
- hostname: "*" - hostname: "*"
service: https://localhost:8001 service: https://localhost:8001
`}, `},
want: []rule{ want: []Rule{
{ {
Hostname: "tunnel1.example.com", Hostname: "tunnel1.example.com",
Service: localhost8000, Service: localhost8000,
@ -56,7 +56,7 @@ ingress:
service: https://localhost:8000 service: https://localhost:8000
extraKey: extraValue extraKey: extraValue
`}, `},
want: []rule{ want: []Rule{
{ {
Hostname: "*", Hostname: "*",
Service: localhost8000, Service: localhost8000,
@ -69,7 +69,7 @@ extraKey: extraValue
ingress: ingress:
- service: https://localhost:8000 - service: https://localhost:8000
`}, `},
want: []rule{ want: []Rule{
{ {
Service: localhost8000, Service: localhost8000,
}, },
@ -141,13 +141,13 @@ ingress:
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
got, err := parseIngress([]byte(tt.args.rawYAML)) got, err := ParseIngress([]byte(tt.args.rawYAML))
if (err != nil) != tt.wantErr { if (err != nil) != tt.wantErr {
t.Errorf("parseIngress() error = %v, wantErr %v", err, tt.wantErr) t.Errorf("ParseIngress() error = %v, wantErr %v", err, tt.wantErr)
return return
} }
if !reflect.DeepEqual(got, tt.want) { if !reflect.DeepEqual(got, tt.want) {
t.Errorf("parseIngress() = %v, want %v", got, tt.want) t.Errorf("ParseIngress() = %v, want %v", got, tt.want)
} }
}) })
} }
@ -258,7 +258,7 @@ func Test_rule_matches(t *testing.T) {
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
r := rule{ r := Rule{
Hostname: tt.fields.Hostname, Hostname: tt.fields.Hostname,
Path: tt.fields.Path, Path: tt.fields.Path,
Service: tt.fields.Service, Service: tt.fields.Service,

View File

@ -24,6 +24,7 @@ import (
"github.com/cloudflare/cloudflared/cmd/cloudflared/ui" "github.com/cloudflare/cloudflared/cmd/cloudflared/ui"
"github.com/cloudflare/cloudflared/connection" "github.com/cloudflare/cloudflared/connection"
"github.com/cloudflare/cloudflared/h2mux" "github.com/cloudflare/cloudflared/h2mux"
"github.com/cloudflare/cloudflared/ingress"
"github.com/cloudflare/cloudflared/logger" "github.com/cloudflare/cloudflared/logger"
"github.com/cloudflare/cloudflared/signal" "github.com/cloudflare/cloudflared/signal"
"github.com/cloudflare/cloudflared/tunnelrpc" "github.com/cloudflare/cloudflared/tunnelrpc"
@ -92,6 +93,7 @@ type TunnelConfig struct {
NamedTunnel *NamedTunnelConfig NamedTunnel *NamedTunnelConfig
ReplaceExisting bool ReplaceExisting bool
TunnelEventChan chan<- ui.TunnelEvent TunnelEventChan chan<- ui.TunnelEvent
IngressRules []ingress.Rule
} }
type dupConnRegisterTunnelError struct{} type dupConnRegisterTunnelError struct{}