ingress.go 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400
  1. package ingress
  2. import (
  3. "fmt"
  4. "net"
  5. "net/url"
  6. "regexp"
  7. "strconv"
  8. "strings"
  9. "github.com/pkg/errors"
  10. "github.com/rs/zerolog"
  11. "github.com/urfave/cli/v2"
  12. "golang.org/x/net/idna"
  13. "github.com/cloudflare/cloudflared/config"
  14. "github.com/cloudflare/cloudflared/ingress/middleware"
  15. "github.com/cloudflare/cloudflared/ipaccess"
  16. )
  17. var (
  18. ErrNoIngressRules = errors.New("The config file doesn't contain any ingress rules")
  19. 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")
  20. errLastRuleNotCatchAll = errors.New("The last ingress rule must match all URLs (i.e. it should not have a hostname or path filter)")
  21. errBadWildcard = errors.New("Hostname patterns can have at most one wildcard character (\"*\") and it can only be used for subdomains, e.g. \"*.example.com\"")
  22. errHostnameContainsPort = errors.New("Hostname cannot contain a port")
  23. ErrURLIncompatibleWithIngress = errors.New("You can't set the --url flag (or $TUNNEL_URL) when using multiple-origin ingress rules")
  24. )
  25. const (
  26. ServiceBastion = "bastion"
  27. ServiceSocksProxy = "socks-proxy"
  28. ServiceWarpRouting = "warp-routing"
  29. )
  30. // FindMatchingRule returns the index of the Ingress Rule which matches the given
  31. // hostname and path. This function assumes the last rule matches everything,
  32. // which is the case if the rules were instantiated via the ingress#Validate method.
  33. //
  34. // Negative index rule signifies local cloudflared rules (not-user defined).
  35. func (ing Ingress) FindMatchingRule(hostname, path string) (*Rule, int) {
  36. // The hostname might contain port. We only want to compare the host part with the rule
  37. host, _, err := net.SplitHostPort(hostname)
  38. if err == nil {
  39. hostname = host
  40. }
  41. for i, rule := range ing.InternalRules {
  42. if rule.Matches(hostname, path) {
  43. // Local rule matches return a negative rule index to distiguish local rules from user-defined rules in logs
  44. // Full range would be [-1 .. )
  45. return &rule, -1 - i
  46. }
  47. }
  48. for i, rule := range ing.Rules {
  49. if rule.Matches(hostname, path) {
  50. return &rule, i
  51. }
  52. }
  53. i := len(ing.Rules) - 1
  54. return &ing.Rules[i], i
  55. }
  56. func matchHost(ruleHost, reqHost string) bool {
  57. if ruleHost == reqHost {
  58. return true
  59. }
  60. // Validate hostnames that use wildcards at the start
  61. if strings.HasPrefix(ruleHost, "*.") {
  62. toMatch := strings.TrimPrefix(ruleHost, "*")
  63. return strings.HasSuffix(reqHost, toMatch)
  64. }
  65. return false
  66. }
  67. // Ingress maps eyeball requests to origins.
  68. type Ingress struct {
  69. // Set of ingress rules that are not added to remote config, e.g. management
  70. InternalRules []Rule
  71. // Rules that are provided by the user from remote or local configuration
  72. Rules []Rule `json:"ingress"`
  73. Defaults OriginRequestConfig `json:"originRequest"`
  74. }
  75. // ParseIngress parses ingress rules, but does not send HTTP requests to the origins.
  76. func ParseIngress(conf *config.Configuration) (Ingress, error) {
  77. if conf == nil || len(conf.Ingress) == 0 {
  78. return Ingress{}, ErrNoIngressRules
  79. }
  80. return validateIngress(conf.Ingress, originRequestFromConfig(conf.OriginRequest))
  81. }
  82. // ParseIngressFromConfigAndCLI will parse the configuration rules from config files for ingress
  83. // rules and then attempt to parse CLI for ingress rules.
  84. // Will always return at least one valid ingress rule. If none are provided by the user, the default
  85. // will be to return 503 status code for all incoming requests.
  86. func ParseIngressFromConfigAndCLI(conf *config.Configuration, c *cli.Context, log *zerolog.Logger) (Ingress, error) {
  87. // Attempt to parse ingress rules from configuration
  88. ingressRules, err := ParseIngress(conf)
  89. if err == nil && !ingressRules.IsEmpty() {
  90. return ingressRules, nil
  91. }
  92. if err != ErrNoIngressRules {
  93. return Ingress{}, err
  94. }
  95. // Attempt to parse ingress rules from CLI:
  96. // --url or --unix-socket flag for a tunnel HTTP ingress
  97. // --hello-world for a basic HTTP ingress self-served
  98. // --bastion for ssh bastion service
  99. ingressRules, err = parseCLIIngress(c, false)
  100. if errors.Is(err, ErrNoIngressRulesCLI) {
  101. // If no token is provided, the probability of NOT being a remotely managed tunnel is higher.
  102. // So, we should warn the user that no ingress rules were found, because remote configuration will most likely not exist.
  103. if !c.IsSet("token") {
  104. log.Warn().Msgf(ErrNoIngressRulesCLI.Error())
  105. }
  106. return newDefaultOrigin(c, log), nil
  107. }
  108. if err != nil {
  109. return Ingress{}, err
  110. }
  111. return ingressRules, nil
  112. }
  113. // parseCLIIngress constructs an Ingress set with only one rule constructed from
  114. // CLI parameters: --url, --hello-world, --bastion, or --unix-socket
  115. func parseCLIIngress(c *cli.Context, allowURLFromArgs bool) (Ingress, error) {
  116. service, err := parseSingleOriginService(c, allowURLFromArgs)
  117. if err != nil {
  118. return Ingress{}, err
  119. }
  120. // Construct an Ingress with the single rule.
  121. defaults := originRequestFromSingleRule(c)
  122. ing := Ingress{
  123. Rules: []Rule{
  124. {
  125. Service: service,
  126. Config: setConfig(defaults, config.OriginRequestConfig{}),
  127. },
  128. },
  129. Defaults: defaults,
  130. }
  131. return ing, err
  132. }
  133. // newDefaultOrigin always returns a 503 response code to help indicate that there are no ingress
  134. // rules setup, but the tunnel is reachable.
  135. func newDefaultOrigin(c *cli.Context, log *zerolog.Logger) Ingress {
  136. defaultRule := GetDefaultIngressRules(log)
  137. defaults := originRequestFromSingleRule(c)
  138. ingress := Ingress{
  139. Rules: defaultRule,
  140. Defaults: defaults,
  141. }
  142. return ingress
  143. }
  144. // Get a single origin service from the CLI/config.
  145. func parseSingleOriginService(c *cli.Context, allowURLFromArgs bool) (OriginService, error) {
  146. if c.IsSet(HelloWorldFlag) {
  147. return new(helloWorld), nil
  148. }
  149. if c.IsSet(config.BastionFlag) {
  150. return newBastionService(), nil
  151. }
  152. if c.IsSet("url") {
  153. originURL, err := config.ValidateUrl(c, allowURLFromArgs)
  154. if err != nil {
  155. return nil, errors.Wrap(err, "Error validating origin URL")
  156. }
  157. if isHTTPService(originURL) {
  158. return &httpService{
  159. url: originURL,
  160. }, nil
  161. }
  162. return newTCPOverWSService(originURL), nil
  163. }
  164. if c.IsSet("unix-socket") {
  165. path, err := config.ValidateUnixSocket(c)
  166. if err != nil {
  167. return nil, errors.Wrap(err, "Error validating --unix-socket")
  168. }
  169. return &unixSocketPath{path: path, scheme: "http"}, nil
  170. }
  171. return nil, ErrNoIngressRulesCLI
  172. }
  173. // IsEmpty checks if there are any ingress rules.
  174. func (ing Ingress) IsEmpty() bool {
  175. return len(ing.Rules) == 0
  176. }
  177. // IsSingleRule checks if the user only specified a single ingress rule.
  178. func (ing Ingress) IsSingleRule() bool {
  179. return len(ing.Rules) == 1
  180. }
  181. // StartOrigins will start any origin services managed by cloudflared, e.g. proxy servers or Hello World.
  182. func (ing Ingress) StartOrigins(
  183. log *zerolog.Logger,
  184. shutdownC <-chan struct{},
  185. ) error {
  186. for _, rule := range ing.Rules {
  187. if err := rule.Service.start(log, shutdownC, rule.Config); err != nil {
  188. return errors.Wrapf(err, "Error starting local service %s", rule.Service)
  189. }
  190. }
  191. return nil
  192. }
  193. // CatchAll returns the catch-all rule (i.e. the last rule)
  194. func (ing Ingress) CatchAll() *Rule {
  195. return &ing.Rules[len(ing.Rules)-1]
  196. }
  197. // Gets the default ingress rule that will be return 503 status
  198. // code for all incoming requests.
  199. func GetDefaultIngressRules(log *zerolog.Logger) []Rule {
  200. noRulesService := newDefaultStatusCode(log)
  201. return []Rule{
  202. {
  203. Service: &noRulesService,
  204. },
  205. }
  206. }
  207. func validateAccessConfiguration(cfg *config.AccessConfig) error {
  208. if !cfg.Required {
  209. return nil
  210. }
  211. // we allow for an initial setup where user can force Access but not configure the rest of the keys.
  212. // however, if the user specified audTags but forgot teamName, we should alert it.
  213. if cfg.TeamName == "" && len(cfg.AudTag) > 0 {
  214. return errors.New("access.TeamName cannot be blank when access.audTags are present")
  215. }
  216. return nil
  217. }
  218. func validateIngress(ingress []config.UnvalidatedIngressRule, defaults OriginRequestConfig) (Ingress, error) {
  219. rules := make([]Rule, len(ingress))
  220. for i, r := range ingress {
  221. cfg := setConfig(defaults, r.OriginRequest)
  222. var service OriginService
  223. if prefix := "unix:"; strings.HasPrefix(r.Service, prefix) {
  224. // No validation necessary for unix socket filepath services
  225. path := strings.TrimPrefix(r.Service, prefix)
  226. service = &unixSocketPath{path: path, scheme: "http"}
  227. } else if prefix := "unix+tls:"; strings.HasPrefix(r.Service, prefix) {
  228. path := strings.TrimPrefix(r.Service, prefix)
  229. service = &unixSocketPath{path: path, scheme: "https"}
  230. } else if prefix := "http_status:"; strings.HasPrefix(r.Service, prefix) {
  231. statusCode, err := strconv.Atoi(strings.TrimPrefix(r.Service, prefix))
  232. if err != nil {
  233. return Ingress{}, errors.Wrap(err, "invalid HTTP status code")
  234. }
  235. if statusCode < 100 || statusCode > 999 {
  236. return Ingress{}, fmt.Errorf("invalid HTTP status code: %d", statusCode)
  237. }
  238. srv := newStatusCode(statusCode)
  239. service = &srv
  240. } else if r.Service == HelloWorldFlag || r.Service == HelloWorldService {
  241. service = new(helloWorld)
  242. } else if r.Service == ServiceSocksProxy {
  243. rules := make([]ipaccess.Rule, len(r.OriginRequest.IPRules))
  244. for i, ipRule := range r.OriginRequest.IPRules {
  245. rule, err := ipaccess.NewRuleByCIDR(ipRule.Prefix, ipRule.Ports, ipRule.Allow)
  246. if err != nil {
  247. return Ingress{}, fmt.Errorf("unable to create ip rule for %s: %s", r.Service, err)
  248. }
  249. rules[i] = rule
  250. }
  251. accessPolicy, err := ipaccess.NewPolicy(false, rules)
  252. if err != nil {
  253. return Ingress{}, fmt.Errorf("unable to create ip access policy for %s: %s", r.Service, err)
  254. }
  255. service = newSocksProxyOverWSService(accessPolicy)
  256. } else if r.Service == ServiceBastion || cfg.BastionMode {
  257. // Bastion mode will always start a Websocket proxy server, which will
  258. // overwrite the localService.URL field when `start` is called. So,
  259. // leave the URL field empty for now.
  260. cfg.BastionMode = true
  261. service = newBastionService()
  262. } else {
  263. // Validate URL services
  264. u, err := url.Parse(r.Service)
  265. if err != nil {
  266. return Ingress{}, err
  267. }
  268. if u.Scheme == "" || u.Hostname() == "" {
  269. return Ingress{}, fmt.Errorf("%s is an invalid address, please make sure it has a scheme and a hostname", r.Service)
  270. }
  271. if u.Path != "" {
  272. 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)
  273. }
  274. if isHTTPService(u) {
  275. service = &httpService{url: u}
  276. } else {
  277. service = newTCPOverWSService(u)
  278. }
  279. }
  280. var handlers []middleware.Handler
  281. if access := r.OriginRequest.Access; access != nil {
  282. if err := validateAccessConfiguration(access); err != nil {
  283. return Ingress{}, err
  284. }
  285. if access.Required {
  286. verifier := middleware.NewJWTValidator(access.TeamName, "", access.AudTag)
  287. handlers = append(handlers, verifier)
  288. }
  289. }
  290. if err := validateHostname(r, i, len(ingress)); err != nil {
  291. return Ingress{}, err
  292. }
  293. isCatchAllRule := (r.Hostname == "" || r.Hostname == "*") && r.Path == ""
  294. punycodeHostname := ""
  295. if !isCatchAllRule {
  296. punycode, err := idna.Lookup.ToASCII(r.Hostname)
  297. // Don't provide the punycode hostname if it is the same as the original hostname
  298. if err == nil && punycode != r.Hostname {
  299. punycodeHostname = punycode
  300. }
  301. }
  302. var pathRegexp *Regexp
  303. if r.Path != "" {
  304. var err error
  305. regex, err := regexp.Compile(r.Path)
  306. if err != nil {
  307. return Ingress{}, errors.Wrapf(err, "Rule #%d has an invalid regex", i+1)
  308. }
  309. pathRegexp = &Regexp{Regexp: regex}
  310. }
  311. rules[i] = Rule{
  312. Hostname: r.Hostname,
  313. punycodeHostname: punycodeHostname,
  314. Service: service,
  315. Path: pathRegexp,
  316. Handlers: handlers,
  317. Config: cfg,
  318. }
  319. }
  320. return Ingress{Rules: rules, Defaults: defaults}, nil
  321. }
  322. func validateHostname(r config.UnvalidatedIngressRule, ruleIndex, totalRules int) error {
  323. // Ensure that the hostname doesn't contain port
  324. _, _, err := net.SplitHostPort(r.Hostname)
  325. if err == nil {
  326. return errHostnameContainsPort
  327. }
  328. // Ensure that there are no wildcards anywhere except the first character
  329. // of the hostname.
  330. if strings.LastIndex(r.Hostname, "*") > 0 {
  331. return errBadWildcard
  332. }
  333. // The last rule should catch all hostnames.
  334. isCatchAllRule := (r.Hostname == "" || r.Hostname == "*") && r.Path == ""
  335. isLastRule := ruleIndex == totalRules-1
  336. if isLastRule && !isCatchAllRule {
  337. return errLastRuleNotCatchAll
  338. }
  339. // ONLY the last rule should catch all hostnames.
  340. if !isLastRule && isCatchAllRule {
  341. return errRuleShouldNotBeCatchAll{index: ruleIndex, hostname: r.Hostname}
  342. }
  343. return nil
  344. }
  345. type errRuleShouldNotBeCatchAll struct {
  346. index int
  347. hostname string
  348. }
  349. func (e errRuleShouldNotBeCatchAll) Error() string {
  350. return fmt.Sprintf("Rule #%d is matching the hostname '%s', but "+
  351. "this will match every hostname, meaning the rules which follow it "+
  352. "will never be triggered.", e.index+1, e.hostname)
  353. }
  354. func isHTTPService(url *url.URL) bool {
  355. return url.Scheme == "http" || url.Scheme == "https" || url.Scheme == "ws" || url.Scheme == "wss"
  356. }