2020-10-09 00:12:29 +00:00
package ingress
2020-10-06 17:12:52 +00:00
import (
"fmt"
2020-11-04 15:44:15 +00:00
"net"
2020-10-06 17:12:52 +00:00
"net/url"
"regexp"
2020-11-04 18:22:21 +00:00
"strconv"
2020-10-07 21:34:53 +00:00
"strings"
2020-10-06 17:12:52 +00:00
"github.com/pkg/errors"
2020-11-25 06:55:13 +00:00
"github.com/rs/zerolog"
2020-10-15 21:41:03 +00:00
"github.com/urfave/cli/v2"
2022-09-22 23:19:06 +00:00
"golang.org/x/net/idna"
2021-03-23 14:30:43 +00:00
"github.com/cloudflare/cloudflared/config"
2022-09-22 13:04:47 +00:00
"github.com/cloudflare/cloudflared/ingress/middleware"
2021-03-23 14:30:43 +00:00
"github.com/cloudflare/cloudflared/ipaccess"
2020-10-06 17:12:52 +00:00
)
var (
2020-11-12 23:57:19 +00:00
ErrNoIngressRules = errors . New ( "The config file doesn't contain any ingress rules" )
2023-03-13 09:24:20 +00:00
ErrNoIngressRulesCLI = errors . New ( "No ingress rules were defined in provided config (if any) nor from the cli, cloudflared will return 503 for all incoming HTTP requests" )
2020-11-12 23:57:19 +00:00
errLastRuleNotCatchAll = errors . New ( "The last ingress rule must match all URLs (i.e. it should not have a hostname or path filter)" )
2020-10-09 00:12:29 +00:00
errBadWildcard = errors . New ( "Hostname patterns can have at most one wildcard character (\"*\") and it can only be used for subdomains, e.g. \"*.example.com\"" )
2020-11-12 23:57:19 +00:00
errHostnameContainsPort = errors . New ( "Hostname cannot contain a port" )
2020-10-09 00:12:29 +00:00
ErrURLIncompatibleWithIngress = errors . New ( "You can't set the --url flag (or $TUNNEL_URL) when using multiple-origin ingress rules" )
2020-10-06 17:12:52 +00:00
)
2021-01-11 19:59:45 +00:00
const (
2021-01-17 20:22:53 +00:00
ServiceBastion = "bastion"
2021-03-01 22:26:37 +00:00
ServiceSocksProxy = "socks-proxy"
2021-01-17 20:22:53 +00:00
ServiceWarpRouting = "warp-routing"
2021-01-11 19:59:45 +00:00
)
2020-10-12 17:54:15 +00:00
// FindMatchingRule returns the index of the Ingress Rule which matches the given
// hostname and path. This function assumes the last rule matches everything,
2023-03-21 18:42:25 +00:00
// which is the case if the rules were instantiated via the ingress#Validate method.
//
// Negative index rule signifies local cloudflared rules (not-user defined).
2020-10-15 21:41:03 +00:00
func ( ing Ingress ) FindMatchingRule ( hostname , path string ) ( * Rule , int ) {
2020-11-04 15:44:15 +00:00
// The hostname might contain port. We only want to compare the host part with the rule
host , _ , err := net . SplitHostPort ( hostname )
if err == nil {
hostname = host
}
2023-06-09 17:17:04 +00:00
for i , rule := range ing . InternalRules {
2023-03-21 18:42:25 +00:00
if rule . Matches ( hostname , path ) {
// Local rule matches return a negative rule index to distiguish local rules from user-defined rules in logs
// Full range would be [-1 .. )
return & rule , - 1 - i
}
}
2020-10-15 17:41:50 +00:00
for i , rule := range ing . Rules {
2020-10-12 17:54:15 +00:00
if rule . Matches ( hostname , path ) {
2020-10-15 21:41:03 +00:00
return & rule , i
2020-10-12 17:54:15 +00:00
}
}
2021-01-17 20:22:53 +00:00
2020-10-15 21:41:03 +00:00
i := len ( ing . Rules ) - 1
return & ing . Rules [ i ] , i
2020-10-12 17:54:15 +00:00
}
2020-10-07 21:34:53 +00:00
func matchHost ( ruleHost , reqHost string ) bool {
if ruleHost == reqHost {
return true
}
// Validate hostnames that use wildcards at the start
if strings . HasPrefix ( ruleHost , "*." ) {
2022-11-25 16:29:34 +00:00
toMatch := strings . TrimPrefix ( ruleHost , "*" )
2020-10-07 21:34:53 +00:00
return strings . HasSuffix ( reqHost , toMatch )
}
return false
}
2020-10-15 17:41:50 +00:00
// Ingress maps eyeball requests to origins.
type Ingress struct {
2023-03-21 18:42:25 +00:00
// Set of ingress rules that are not added to remote config, e.g. management
2023-06-09 17:17:04 +00:00
InternalRules [ ] Rule
2023-03-21 18:42:25 +00:00
// Rules that are provided by the user from remote or local configuration
2022-03-14 17:51:10 +00:00
Rules [ ] Rule ` json:"ingress" `
Defaults OriginRequestConfig ` json:"originRequest" `
2020-10-15 21:41:03 +00:00
}
2023-03-09 23:23:11 +00:00
// ParseIngress parses ingress rules, but does not send HTTP requests to the origins.
func ParseIngress ( conf * config . Configuration ) ( Ingress , error ) {
2023-12-14 16:29:40 +00:00
if conf == nil || len ( conf . Ingress ) == 0 {
2023-03-09 23:23:11 +00:00
return Ingress { } , ErrNoIngressRules
}
return validateIngress ( conf . Ingress , originRequestFromConfig ( conf . OriginRequest ) )
}
// ParseIngressFromConfigAndCLI will parse the configuration rules from config files for ingress
// rules and then attempt to parse CLI for ingress rules.
// Will always return at least one valid ingress rule. If none are provided by the user, the default
2023-04-06 17:08:02 +00:00
// will be to return 503 status code for all incoming requests.
2023-03-09 23:23:11 +00:00
func ParseIngressFromConfigAndCLI ( conf * config . Configuration , c * cli . Context , log * zerolog . Logger ) ( Ingress , error ) {
// Attempt to parse ingress rules from configuration
ingressRules , err := ParseIngress ( conf )
if err == nil && ! ingressRules . IsEmpty ( ) {
return ingressRules , nil
}
if err != ErrNoIngressRules {
return Ingress { } , err
}
// Attempt to parse ingress rules from CLI:
// --url or --unix-socket flag for a tunnel HTTP ingress
// --hello-world for a basic HTTP ingress self-served
// --bastion for ssh bastion service
ingressRules , err = parseCLIIngress ( c , false )
if errors . Is ( err , ErrNoIngressRulesCLI ) {
2023-06-09 17:17:04 +00:00
// If no token is provided, the probability of NOT being a remotely managed tunnel is higher.
// So, we should warn the user that no ingress rules were found, because remote configuration will most likely not exist.
2023-04-06 17:08:02 +00:00
if ! c . IsSet ( "token" ) {
log . Warn ( ) . Msgf ( ErrNoIngressRulesCLI . Error ( ) )
}
2023-03-09 23:23:11 +00:00
return newDefaultOrigin ( c , log ) , nil
}
2023-06-09 17:17:04 +00:00
2023-03-09 23:23:11 +00:00
if err != nil {
return Ingress { } , err
}
2023-06-09 17:17:04 +00:00
2023-03-09 23:23:11 +00:00
return ingressRules , nil
}
// parseCLIIngress constructs an Ingress set with only one rule constructed from
// CLI parameters: --url, --hello-world, --bastion, or --unix-socket
func parseCLIIngress ( c * cli . Context , allowURLFromArgs bool ) ( Ingress , error ) {
2020-11-02 11:21:34 +00:00
service , err := parseSingleOriginService ( c , allowURLFromArgs )
2020-10-15 21:41:03 +00:00
if err != nil {
return Ingress { } , err
}
// Construct an Ingress with the single rule.
2023-03-09 23:23:11 +00:00
defaults := originRequestFromSingleRule ( c )
2020-10-15 21:41:03 +00:00
ing := Ingress {
Rules : [ ] Rule {
{
Service : service ,
2020-11-11 13:14:51 +00:00
Config : setConfig ( defaults , config . OriginRequestConfig { } ) ,
2020-10-15 21:41:03 +00:00
} ,
} ,
2022-03-14 17:51:10 +00:00
Defaults : defaults ,
2020-10-15 21:41:03 +00:00
}
return ing , err
}
2023-03-13 09:24:20 +00:00
// newDefaultOrigin always returns a 503 response code to help indicate that there are no ingress
2023-03-09 23:23:11 +00:00
// rules setup, but the tunnel is reachable.
func newDefaultOrigin ( c * cli . Context , log * zerolog . Logger ) Ingress {
2023-06-09 17:17:04 +00:00
defaultRule := GetDefaultIngressRules ( log )
2023-03-09 23:23:11 +00:00
defaults := originRequestFromSingleRule ( c )
ingress := Ingress {
2023-06-09 17:17:04 +00:00
Rules : defaultRule ,
2023-03-09 23:23:11 +00:00
Defaults : defaults ,
}
return ingress
}
2020-10-15 21:41:03 +00:00
// Get a single origin service from the CLI/config.
2021-07-01 18:30:26 +00:00
func parseSingleOriginService ( c * cli . Context , allowURLFromArgs bool ) ( OriginService , error ) {
2023-03-06 23:19:10 +00:00
if c . IsSet ( HelloWorldFlag ) {
2020-10-30 21:37:40 +00:00
return new ( helloWorld ) , nil
2020-10-15 21:41:03 +00:00
}
2020-12-09 21:46:53 +00:00
if c . IsSet ( config . BastionFlag ) {
2021-02-05 13:01:53 +00:00
return newBastionService ( ) , nil
2020-12-09 21:46:53 +00:00
}
if c . IsSet ( "url" ) {
2020-11-02 11:21:34 +00:00
originURL , err := config . ValidateUrl ( c , allowURLFromArgs )
2020-10-15 21:41:03 +00:00
if err != nil {
return nil , errors . Wrap ( err , "Error validating origin URL" )
}
2020-12-09 21:46:53 +00:00
if isHTTPService ( originURL ) {
return & httpService {
url : originURL ,
} , nil
}
2021-02-05 13:01:53 +00:00
return newTCPOverWSService ( originURL ) , nil
2020-10-15 21:41:03 +00:00
}
if c . IsSet ( "unix-socket" ) {
2020-10-30 21:37:40 +00:00
path , err := config . ValidateUnixSocket ( c )
2020-10-15 21:41:03 +00:00
if err != nil {
return nil , errors . Wrap ( err , "Error validating --unix-socket" )
}
2022-02-28 20:07:47 +00:00
return & unixSocketPath { path : path , scheme : "http" } , nil
2020-10-15 21:41:03 +00:00
}
2023-03-09 23:23:11 +00:00
return nil , ErrNoIngressRulesCLI
2020-10-15 17:41:50 +00:00
}
// IsEmpty checks if there are any ingress rules.
func ( ing Ingress ) IsEmpty ( ) bool {
return len ( ing . Rules ) == 0
}
2021-04-09 21:30:14 +00:00
// IsSingleRule checks if the user only specified a single ingress rule.
func ( ing Ingress ) IsSingleRule ( ) bool {
return len ( ing . Rules ) == 1
}
2020-10-15 21:41:03 +00:00
// StartOrigins will start any origin services managed by cloudflared, e.g. proxy servers or Hello World.
2020-12-30 19:48:19 +00:00
func ( ing Ingress ) StartOrigins (
log * zerolog . Logger ,
shutdownC <- chan struct { } ,
) error {
2020-10-15 21:41:03 +00:00
for _ , rule := range ing . Rules {
2022-02-11 10:49:06 +00:00
if err := rule . Service . start ( log , shutdownC , rule . Config ) ; err != nil {
2020-12-30 19:48:19 +00:00
return errors . Wrapf ( err , "Error starting local service %s" , rule . Service )
2020-10-15 21:41:03 +00:00
}
}
2020-12-30 19:48:19 +00:00
return nil
2020-10-15 21:41:03 +00:00
}
// CatchAll returns the catch-all rule (i.e. the last rule)
func ( ing Ingress ) CatchAll ( ) * Rule {
return & ing . Rules [ len ( ing . Rules ) - 1 ]
}
2023-06-09 17:17:04 +00:00
// Gets the default ingress rule that will be return 503 status
// code for all incoming requests.
func GetDefaultIngressRules ( log * zerolog . Logger ) [ ] Rule {
noRulesService := newDefaultStatusCode ( log )
return [ ] Rule {
{
Service : & noRulesService ,
} ,
}
}
2022-09-22 13:04:47 +00:00
func validateAccessConfiguration ( cfg * config . AccessConfig ) error {
if ! cfg . Required {
return nil
}
2022-11-09 12:12:37 +00:00
// we allow for an initial setup where user can force Access but not configure the rest of the keys.
// however, if the user specified audTags but forgot teamName, we should alert it.
if cfg . TeamName == "" && len ( cfg . AudTag ) > 0 {
return errors . New ( "access.TeamName cannot be blank when access.audTags are present" )
2022-09-22 13:04:47 +00:00
}
return nil
}
2022-01-28 14:37:17 +00:00
func validateIngress ( ingress [ ] config . UnvalidatedIngressRule , defaults OriginRequestConfig ) ( Ingress , error ) {
2020-10-20 17:00:34 +00:00
rules := make ( [ ] Rule , len ( ingress ) )
for i , r := range ingress {
2020-11-15 18:47:51 +00:00
cfg := setConfig ( defaults , r . OriginRequest )
2021-07-01 18:30:26 +00:00
var service OriginService
2020-10-15 21:41:03 +00:00
2020-10-30 21:37:40 +00:00
if prefix := "unix:" ; strings . HasPrefix ( r . Service , prefix ) {
2020-10-15 21:41:03 +00:00
// No validation necessary for unix socket filepath services
2020-10-30 21:37:40 +00:00
path := strings . TrimPrefix ( r . Service , prefix )
2022-02-28 20:07:47 +00:00
service = & unixSocketPath { path : path , scheme : "http" }
} else if prefix := "unix+tls:" ; strings . HasPrefix ( r . Service , prefix ) {
path := strings . TrimPrefix ( r . Service , prefix )
service = & unixSocketPath { path : path , scheme : "https" }
2020-11-04 18:22:21 +00:00
} else if prefix := "http_status:" ; strings . HasPrefix ( r . Service , prefix ) {
2022-09-01 21:20:22 +00:00
statusCode , err := strconv . Atoi ( strings . TrimPrefix ( r . Service , prefix ) )
2020-11-04 18:22:21 +00:00
if err != nil {
2022-09-01 21:20:22 +00:00
return Ingress { } , errors . Wrap ( err , "invalid HTTP status code" )
2020-11-04 18:22:21 +00:00
}
2022-09-01 21:20:22 +00:00
if statusCode < 100 || statusCode > 999 {
return Ingress { } , fmt . Errorf ( "invalid HTTP status code: %d" , statusCode )
}
srv := newStatusCode ( statusCode )
2020-11-04 18:22:21 +00:00
service = & srv
2023-03-06 23:19:10 +00:00
} else if r . Service == HelloWorldFlag || r . Service == HelloWorldService {
2020-10-30 21:37:40 +00:00
service = new ( helloWorld )
2021-03-01 22:26:37 +00:00
} else if r . Service == ServiceSocksProxy {
rules := make ( [ ] ipaccess . Rule , len ( r . OriginRequest . IPRules ) )
for i , ipRule := range r . OriginRequest . IPRules {
rule , err := ipaccess . NewRuleByCIDR ( ipRule . Prefix , ipRule . Ports , ipRule . Allow )
if err != nil {
return Ingress { } , fmt . Errorf ( "unable to create ip rule for %s: %s" , r . Service , err )
}
rules [ i ] = rule
}
accessPolicy , err := ipaccess . NewPolicy ( false , rules )
if err != nil {
return Ingress { } , fmt . Errorf ( "unable to create ip access policy for %s: %s" , r . Service , err )
}
service = newSocksProxyOverWSService ( accessPolicy )
2021-01-11 19:59:45 +00:00
} else if r . Service == ServiceBastion || cfg . BastionMode {
2020-11-15 18:47:51 +00:00
// Bastion mode will always start a Websocket proxy server, which will
// overwrite the localService.URL field when `start` is called. So,
// leave the URL field empty for now.
cfg . BastionMode = true
2021-02-05 13:01:53 +00:00
service = newBastionService ( )
2020-10-15 21:41:03 +00:00
} else {
// Validate URL services
u , err := url . Parse ( r . Service )
if err != nil {
return Ingress { } , err
}
if u . Scheme == "" || u . Hostname ( ) == "" {
2020-11-12 23:57:19 +00:00
return Ingress { } , fmt . Errorf ( "%s is an invalid address, please make sure it has a scheme and a hostname" , r . Service )
2020-10-15 21:41:03 +00:00
}
2020-10-06 17:12:52 +00:00
2020-10-15 21:41:03 +00:00
if u . Path != "" {
return Ingress { } , 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 )
}
2020-12-09 21:46:53 +00:00
if isHTTPService ( u ) {
service = & httpService { url : u }
} else {
2021-02-05 13:01:53 +00:00
service = newTCPOverWSService ( u )
2020-12-09 21:46:53 +00:00
}
2020-10-12 17:54:15 +00:00
}
2022-09-22 13:04:47 +00:00
var handlers [ ] middleware . Handler
if access := r . OriginRequest . Access ; access != nil {
if err := validateAccessConfiguration ( access ) ; err != nil {
return Ingress { } , err
}
if access . Required {
verifier := middleware . NewJWTValidator ( access . TeamName , "" , access . AudTag )
handlers = append ( handlers , verifier )
}
}
2020-11-04 15:44:15 +00:00
if err := validateHostname ( r , i , len ( ingress ) ) ; err != nil {
return Ingress { } , err
2020-10-06 17:12:52 +00:00
}
2022-09-22 23:19:06 +00:00
isCatchAllRule := ( r . Hostname == "" || r . Hostname == "*" ) && r . Path == ""
punycodeHostname := ""
if ! isCatchAllRule {
punycode , err := idna . Lookup . ToASCII ( r . Hostname )
// Don't provide the punycode hostname if it is the same as the original hostname
if err == nil && punycode != r . Hostname {
punycodeHostname = punycode
}
}
2022-03-14 17:51:10 +00:00
var pathRegexp * Regexp
2020-10-06 17:12:52 +00:00
if r . Path != "" {
2020-10-15 21:41:03 +00:00
var err error
2022-03-14 17:51:10 +00:00
regex , err := regexp . Compile ( r . Path )
2020-10-06 17:12:52 +00:00
if err != nil {
2020-10-15 17:41:50 +00:00
return Ingress { } , errors . Wrapf ( err , "Rule #%d has an invalid regex" , i + 1 )
2020-10-06 17:12:52 +00:00
}
2022-03-14 17:51:10 +00:00
pathRegexp = & Regexp { Regexp : regex }
2020-10-06 17:12:52 +00:00
}
2020-10-09 00:12:29 +00:00
rules [ i ] = Rule {
2022-09-22 23:19:06 +00:00
Hostname : r . Hostname ,
punycodeHostname : punycodeHostname ,
Service : service ,
Path : pathRegexp ,
Handlers : handlers ,
Config : cfg ,
2020-10-06 17:12:52 +00:00
}
}
2022-03-14 17:51:10 +00:00
return Ingress { Rules : rules , Defaults : defaults } , nil
2020-10-06 17:12:52 +00:00
}
2020-11-04 15:44:15 +00:00
func validateHostname ( r config . UnvalidatedIngressRule , ruleIndex , totalRules int ) error {
// Ensure that the hostname doesn't contain port
_ , _ , err := net . SplitHostPort ( r . Hostname )
if err == nil {
return errHostnameContainsPort
}
// Ensure that there are no wildcards anywhere except the first character
// of the hostname.
if strings . LastIndex ( r . Hostname , "*" ) > 0 {
return errBadWildcard
}
// The last rule should catch all hostnames.
isCatchAllRule := ( r . Hostname == "" || r . Hostname == "*" ) && r . Path == ""
isLastRule := ruleIndex == totalRules - 1
if isLastRule && ! isCatchAllRule {
return errLastRuleNotCatchAll
}
// ONLY the last rule should catch all hostnames.
if ! isLastRule && isCatchAllRule {
return errRuleShouldNotBeCatchAll { index : ruleIndex , hostname : r . Hostname }
}
return nil
}
2020-10-06 17:12:52 +00:00
type errRuleShouldNotBeCatchAll struct {
2020-11-04 15:44:15 +00:00
index int
2020-10-06 17:12:52 +00:00
hostname string
}
func ( e errRuleShouldNotBeCatchAll ) Error ( ) string {
return fmt . Sprintf ( "Rule #%d is matching the hostname '%s', but " +
"this will match every hostname, meaning the rules which follow it " +
2020-11-04 15:44:15 +00:00
"will never be triggered." , e . index + 1 , e . hostname )
2020-10-06 17:12:52 +00:00
}
2020-12-09 21:46:53 +00:00
func isHTTPService ( url * url . URL ) bool {
return url . Scheme == "http" || url . Scheme == "https" || url . Scheme == "ws" || url . Scheme == "wss"
}