ingress_test.go 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620
  1. package ingress
  2. import (
  3. "flag"
  4. "fmt"
  5. "net/http"
  6. "net/url"
  7. "regexp"
  8. "testing"
  9. "time"
  10. "github.com/stretchr/testify/assert"
  11. "github.com/stretchr/testify/require"
  12. "github.com/urfave/cli/v2"
  13. yaml "gopkg.in/yaml.v2"
  14. "github.com/cloudflare/cloudflared/config"
  15. "github.com/cloudflare/cloudflared/ipaccess"
  16. "github.com/cloudflare/cloudflared/tlsconfig"
  17. )
  18. func TestParseUnixSocket(t *testing.T) {
  19. rawYAML := `
  20. ingress:
  21. - service: unix:/tmp/echo.sock
  22. `
  23. ing, err := ParseIngress(MustReadIngress(rawYAML))
  24. require.NoError(t, err)
  25. _, ok := ing.Rules[0].Service.(*unixSocketPath)
  26. require.True(t, ok)
  27. }
  28. func Test_parseIngress(t *testing.T) {
  29. localhost8000 := MustParseURL(t, "https://localhost:8000")
  30. localhost8001 := MustParseURL(t, "https://localhost:8001")
  31. fourOhFour := newStatusCode(404)
  32. defaultConfig := setConfig(originRequestFromYAML(config.OriginRequestConfig{}), config.OriginRequestConfig{})
  33. require.Equal(t, defaultKeepAliveConnections, defaultConfig.KeepAliveConnections)
  34. tr := true
  35. type args struct {
  36. rawYAML string
  37. }
  38. tests := []struct {
  39. name string
  40. args args
  41. want []Rule
  42. wantErr bool
  43. }{
  44. {
  45. name: "Empty file",
  46. args: args{rawYAML: ""},
  47. wantErr: true,
  48. },
  49. {
  50. name: "Multiple rules",
  51. args: args{rawYAML: `
  52. ingress:
  53. - hostname: tunnel1.example.com
  54. service: https://localhost:8000
  55. - hostname: "*"
  56. service: https://localhost:8001
  57. `},
  58. want: []Rule{
  59. {
  60. Hostname: "tunnel1.example.com",
  61. Service: &httpService{url: localhost8000},
  62. Config: defaultConfig,
  63. },
  64. {
  65. Hostname: "*",
  66. Service: &httpService{url: localhost8001},
  67. Config: defaultConfig,
  68. },
  69. },
  70. },
  71. {
  72. name: "Extra keys",
  73. args: args{rawYAML: `
  74. ingress:
  75. - hostname: "*"
  76. service: https://localhost:8000
  77. extraKey: extraValue
  78. `},
  79. want: []Rule{
  80. {
  81. Hostname: "*",
  82. Service: &httpService{url: localhost8000},
  83. Config: defaultConfig,
  84. },
  85. },
  86. },
  87. {
  88. name: "ws service",
  89. args: args{rawYAML: `
  90. ingress:
  91. - hostname: "*"
  92. service: wss://localhost:8000
  93. `},
  94. want: []Rule{
  95. {
  96. Hostname: "*",
  97. Service: &httpService{url: MustParseURL(t, "wss://localhost:8000")},
  98. Config: defaultConfig,
  99. },
  100. },
  101. },
  102. {
  103. name: "Hostname can be omitted",
  104. args: args{rawYAML: `
  105. ingress:
  106. - service: https://localhost:8000
  107. `},
  108. want: []Rule{
  109. {
  110. Service: &httpService{url: localhost8000},
  111. Config: defaultConfig,
  112. },
  113. },
  114. },
  115. {
  116. name: "Invalid service",
  117. args: args{rawYAML: `
  118. ingress:
  119. - hostname: "*"
  120. service: https://local host:8000
  121. `},
  122. wantErr: true,
  123. },
  124. {
  125. name: "Last rule isn't catchall",
  126. args: args{rawYAML: `
  127. ingress:
  128. - hostname: example.com
  129. service: https://localhost:8000
  130. `},
  131. wantErr: true,
  132. },
  133. {
  134. name: "First rule is catchall",
  135. args: args{rawYAML: `
  136. ingress:
  137. - service: https://localhost:8000
  138. - hostname: example.com
  139. service: https://localhost:8000
  140. `},
  141. wantErr: true,
  142. },
  143. {
  144. name: "Catch-all rule can't have a path",
  145. args: args{rawYAML: `
  146. ingress:
  147. - service: https://localhost:8001
  148. path: /subpath1/(.*)/subpath2
  149. `},
  150. wantErr: true,
  151. },
  152. {
  153. name: "Invalid regex",
  154. args: args{rawYAML: `
  155. ingress:
  156. - hostname: example.com
  157. service: https://localhost:8000
  158. path: "*/subpath2"
  159. - service: https://localhost:8001
  160. `},
  161. wantErr: true,
  162. },
  163. {
  164. name: "Service must have a scheme",
  165. args: args{rawYAML: `
  166. ingress:
  167. - service: localhost:8000
  168. `},
  169. wantErr: true,
  170. },
  171. {
  172. name: "Wildcard not at start",
  173. args: args{rawYAML: `
  174. ingress:
  175. - hostname: "test.*.example.com"
  176. service: https://localhost:8000
  177. `},
  178. wantErr: true,
  179. },
  180. {
  181. name: "Service can't have a path",
  182. args: args{rawYAML: `
  183. ingress:
  184. - service: https://localhost:8000/static/
  185. `},
  186. wantErr: true,
  187. },
  188. {
  189. name: "Invalid HTTP status",
  190. args: args{rawYAML: `
  191. ingress:
  192. - service: http_status:asdf
  193. `},
  194. wantErr: true,
  195. },
  196. {
  197. name: "Valid HTTP status",
  198. args: args{rawYAML: `
  199. ingress:
  200. - service: http_status:404
  201. `},
  202. want: []Rule{
  203. {
  204. Hostname: "",
  205. Service: &fourOhFour,
  206. Config: defaultConfig,
  207. },
  208. },
  209. },
  210. {
  211. name: "Valid hello world service",
  212. args: args{rawYAML: `
  213. ingress:
  214. - service: hello_world
  215. `},
  216. want: []Rule{
  217. {
  218. Hostname: "",
  219. Service: new(helloWorld),
  220. Config: defaultConfig,
  221. },
  222. },
  223. },
  224. {
  225. name: "TCP services",
  226. args: args{rawYAML: `
  227. ingress:
  228. - hostname: tcp.foo.com
  229. service: tcp://127.0.0.1
  230. - hostname: tcp2.foo.com
  231. service: tcp://localhost:8000
  232. - service: http_status:404
  233. `},
  234. want: []Rule{
  235. {
  236. Hostname: "tcp.foo.com",
  237. Service: newTCPOverWSService(MustParseURL(t, "tcp://127.0.0.1:7864")),
  238. Config: defaultConfig,
  239. },
  240. {
  241. Hostname: "tcp2.foo.com",
  242. Service: newTCPOverWSService(MustParseURL(t, "tcp://localhost:8000")),
  243. Config: defaultConfig,
  244. },
  245. {
  246. Service: &fourOhFour,
  247. Config: defaultConfig,
  248. },
  249. },
  250. },
  251. {
  252. name: "SSH services",
  253. args: args{rawYAML: `
  254. ingress:
  255. - service: ssh://127.0.0.1
  256. `},
  257. want: []Rule{
  258. {
  259. Service: newTCPOverWSService(MustParseURL(t, "ssh://127.0.0.1:22")),
  260. Config: defaultConfig,
  261. },
  262. },
  263. },
  264. {
  265. name: "RDP services",
  266. args: args{rawYAML: `
  267. ingress:
  268. - service: rdp://127.0.0.1
  269. `},
  270. want: []Rule{
  271. {
  272. Service: newTCPOverWSService(MustParseURL(t, "rdp://127.0.0.1:3389")),
  273. Config: defaultConfig,
  274. },
  275. },
  276. },
  277. {
  278. name: "SMB services",
  279. args: args{rawYAML: `
  280. ingress:
  281. - service: smb://127.0.0.1
  282. `},
  283. want: []Rule{
  284. {
  285. Service: newTCPOverWSService(MustParseURL(t, "smb://127.0.0.1:445")),
  286. Config: defaultConfig,
  287. },
  288. },
  289. },
  290. {
  291. name: "Other TCP services",
  292. args: args{rawYAML: `
  293. ingress:
  294. - service: ftp://127.0.0.1
  295. `},
  296. want: []Rule{
  297. {
  298. Service: newTCPOverWSService(MustParseURL(t, "ftp://127.0.0.1")),
  299. Config: defaultConfig,
  300. },
  301. },
  302. },
  303. {
  304. name: "SOCKS services",
  305. args: args{rawYAML: `
  306. ingress:
  307. - hostname: socks.foo.com
  308. service: socks-proxy
  309. originRequest:
  310. ipRules:
  311. - prefix: 1.1.1.0/24
  312. ports: [80, 443]
  313. allow: true
  314. - prefix: 0.0.0.0/0
  315. allow: false
  316. - service: http_status:404
  317. `},
  318. want: []Rule{
  319. {
  320. Hostname: "socks.foo.com",
  321. Service: newSocksProxyOverWSService(accessPolicy()),
  322. Config: defaultConfig,
  323. },
  324. {
  325. Service: &fourOhFour,
  326. Config: defaultConfig,
  327. },
  328. },
  329. },
  330. {
  331. name: "URL isn't necessary if using bastion",
  332. args: args{rawYAML: `
  333. ingress:
  334. - hostname: bastion.foo.com
  335. originRequest:
  336. bastionMode: true
  337. - service: http_status:404
  338. `},
  339. want: []Rule{
  340. {
  341. Hostname: "bastion.foo.com",
  342. Service: newBastionService(),
  343. Config: setConfig(originRequestFromYAML(config.OriginRequestConfig{}), config.OriginRequestConfig{BastionMode: &tr}),
  344. },
  345. {
  346. Service: &fourOhFour,
  347. Config: defaultConfig,
  348. },
  349. },
  350. },
  351. {
  352. name: "Bastion service",
  353. args: args{rawYAML: `
  354. ingress:
  355. - hostname: bastion.foo.com
  356. service: bastion
  357. - service: http_status:404
  358. `},
  359. want: []Rule{
  360. {
  361. Hostname: "bastion.foo.com",
  362. Service: newBastionService(),
  363. Config: setConfig(originRequestFromYAML(config.OriginRequestConfig{}), config.OriginRequestConfig{BastionMode: &tr}),
  364. },
  365. {
  366. Service: &fourOhFour,
  367. Config: defaultConfig,
  368. },
  369. },
  370. },
  371. {
  372. name: "Hostname contains port",
  373. args: args{rawYAML: `
  374. ingress:
  375. - hostname: "test.example.com:443"
  376. service: https://localhost:8000
  377. - hostname: "*"
  378. service: https://localhost:8001
  379. `},
  380. wantErr: true,
  381. },
  382. }
  383. for _, tt := range tests {
  384. t.Run(tt.name, func(t *testing.T) {
  385. got, err := ParseIngress(MustReadIngress(tt.args.rawYAML))
  386. if (err != nil) != tt.wantErr {
  387. t.Errorf("ParseIngress() error = %v, wantErr %v", err, tt.wantErr)
  388. return
  389. }
  390. require.Equal(t, tt.want, got.Rules)
  391. })
  392. }
  393. }
  394. func TestSingleOriginSetsConfig(t *testing.T) {
  395. flagSet := flag.NewFlagSet(t.Name(), flag.PanicOnError)
  396. flagSet.Bool("hello-world", true, "")
  397. flagSet.Duration(ProxyConnectTimeoutFlag, time.Second, "")
  398. flagSet.Duration(ProxyTLSTimeoutFlag, time.Second, "")
  399. flagSet.Duration(ProxyTCPKeepAliveFlag, time.Second, "")
  400. flagSet.Bool(ProxyNoHappyEyeballsFlag, true, "")
  401. flagSet.Int(ProxyKeepAliveConnectionsFlag, 10, "")
  402. flagSet.Duration(ProxyKeepAliveTimeoutFlag, time.Second, "")
  403. flagSet.String(HTTPHostHeaderFlag, "example.com:8080", "")
  404. flagSet.String(OriginServerNameFlag, "example.com", "")
  405. flagSet.String(tlsconfig.OriginCAPoolFlag, "/etc/certs/ca.pem", "")
  406. flagSet.Bool(NoTLSVerifyFlag, true, "")
  407. flagSet.Bool(NoChunkedEncodingFlag, true, "")
  408. flagSet.Bool(config.BastionFlag, true, "")
  409. flagSet.String(ProxyAddressFlag, "localhost:8080", "")
  410. flagSet.Uint(ProxyPortFlag, 8080, "")
  411. flagSet.Bool(Socks5Flag, true, "")
  412. cliCtx := cli.NewContext(cli.NewApp(), flagSet, nil)
  413. err := cliCtx.Set("hello-world", "true")
  414. require.NoError(t, err)
  415. err = cliCtx.Set(ProxyConnectTimeoutFlag, "1s")
  416. require.NoError(t, err)
  417. err = cliCtx.Set(ProxyTLSTimeoutFlag, "1s")
  418. require.NoError(t, err)
  419. err = cliCtx.Set(ProxyTCPKeepAliveFlag, "1s")
  420. require.NoError(t, err)
  421. err = cliCtx.Set(ProxyNoHappyEyeballsFlag, "true")
  422. require.NoError(t, err)
  423. err = cliCtx.Set(ProxyKeepAliveConnectionsFlag, "10")
  424. require.NoError(t, err)
  425. err = cliCtx.Set(ProxyKeepAliveTimeoutFlag, "1s")
  426. require.NoError(t, err)
  427. err = cliCtx.Set(HTTPHostHeaderFlag, "example.com:8080")
  428. require.NoError(t, err)
  429. err = cliCtx.Set(OriginServerNameFlag, "example.com")
  430. require.NoError(t, err)
  431. err = cliCtx.Set(tlsconfig.OriginCAPoolFlag, "/etc/certs/ca.pem")
  432. require.NoError(t, err)
  433. err = cliCtx.Set(NoTLSVerifyFlag, "true")
  434. require.NoError(t, err)
  435. err = cliCtx.Set(NoChunkedEncodingFlag, "true")
  436. require.NoError(t, err)
  437. err = cliCtx.Set(config.BastionFlag, "true")
  438. require.NoError(t, err)
  439. err = cliCtx.Set(ProxyAddressFlag, "localhost:8080")
  440. require.NoError(t, err)
  441. err = cliCtx.Set(ProxyPortFlag, "8080")
  442. require.NoError(t, err)
  443. err = cliCtx.Set(Socks5Flag, "true")
  444. require.NoError(t, err)
  445. allowURLFromArgs := false
  446. require.NoError(t, err)
  447. ingress, err := NewSingleOrigin(cliCtx, allowURLFromArgs)
  448. require.NoError(t, err)
  449. assert.Equal(t, time.Second, ingress.Rules[0].Config.ConnectTimeout)
  450. assert.Equal(t, time.Second, ingress.Rules[0].Config.TLSTimeout)
  451. assert.Equal(t, time.Second, ingress.Rules[0].Config.TCPKeepAlive)
  452. assert.True(t, ingress.Rules[0].Config.NoHappyEyeballs)
  453. assert.Equal(t, 10, ingress.Rules[0].Config.KeepAliveConnections)
  454. assert.Equal(t, time.Second, ingress.Rules[0].Config.KeepAliveTimeout)
  455. assert.Equal(t, "example.com:8080", ingress.Rules[0].Config.HTTPHostHeader)
  456. assert.Equal(t, "example.com", ingress.Rules[0].Config.OriginServerName)
  457. assert.Equal(t, "/etc/certs/ca.pem", ingress.Rules[0].Config.CAPool)
  458. assert.True(t, ingress.Rules[0].Config.NoTLSVerify)
  459. assert.True(t, ingress.Rules[0].Config.DisableChunkedEncoding)
  460. assert.True(t, ingress.Rules[0].Config.BastionMode)
  461. assert.Equal(t, "localhost:8080", ingress.Rules[0].Config.ProxyAddress)
  462. assert.Equal(t, uint(8080), ingress.Rules[0].Config.ProxyPort)
  463. assert.Equal(t, socksProxy, ingress.Rules[0].Config.ProxyType)
  464. }
  465. func TestFindMatchingRule(t *testing.T) {
  466. ingress := Ingress{
  467. Rules: []Rule{
  468. {
  469. Hostname: "tunnel-a.example.com",
  470. Path: nil,
  471. },
  472. {
  473. Hostname: "tunnel-b.example.com",
  474. Path: mustParsePath(t, "/health"),
  475. },
  476. {
  477. Hostname: "*",
  478. },
  479. },
  480. }
  481. tests := []struct {
  482. host string
  483. path string
  484. req *http.Request
  485. wantRuleIndex int
  486. }{
  487. {
  488. host: "tunnel-a.example.com",
  489. path: "/",
  490. wantRuleIndex: 0,
  491. },
  492. {
  493. host: "tunnel-a.example.com",
  494. path: "/pages/about",
  495. wantRuleIndex: 0,
  496. },
  497. {
  498. host: "tunnel-a.example.com:443",
  499. path: "/pages/about",
  500. wantRuleIndex: 0,
  501. },
  502. {
  503. host: "tunnel-b.example.com",
  504. path: "/health",
  505. wantRuleIndex: 1,
  506. },
  507. {
  508. host: "tunnel-b.example.com",
  509. path: "/index.html",
  510. wantRuleIndex: 2,
  511. },
  512. {
  513. host: "tunnel-c.example.com",
  514. path: "/",
  515. wantRuleIndex: 2,
  516. },
  517. }
  518. for _, test := range tests {
  519. _, ruleIndex := ingress.FindMatchingRule(test.host, test.path)
  520. assert.Equal(t, test.wantRuleIndex, ruleIndex, fmt.Sprintf("Expect host=%s, path=%s to match rule %d, got %d", test.host, test.path, test.wantRuleIndex, ruleIndex))
  521. }
  522. }
  523. func TestIsHTTPService(t *testing.T) {
  524. tests := []struct {
  525. url *url.URL
  526. isHTTP bool
  527. }{
  528. {
  529. url: MustParseURL(t, "http://localhost"),
  530. isHTTP: true,
  531. },
  532. {
  533. url: MustParseURL(t, "https://127.0.0.1:8000"),
  534. isHTTP: true,
  535. },
  536. {
  537. url: MustParseURL(t, "ws://localhost"),
  538. isHTTP: true,
  539. },
  540. {
  541. url: MustParseURL(t, "wss://localhost:8000"),
  542. isHTTP: true,
  543. },
  544. {
  545. url: MustParseURL(t, "tcp://localhost:9000"),
  546. isHTTP: false,
  547. },
  548. }
  549. for _, test := range tests {
  550. assert.Equal(t, test.isHTTP, isHTTPService(test.url))
  551. }
  552. }
  553. func mustParsePath(t *testing.T, path string) *regexp.Regexp {
  554. regexp, err := regexp.Compile(path)
  555. assert.NoError(t, err)
  556. return regexp
  557. }
  558. func MustParseURL(t *testing.T, rawURL string) *url.URL {
  559. u, err := url.Parse(rawURL)
  560. require.NoError(t, err)
  561. return u
  562. }
  563. func accessPolicy() *ipaccess.Policy {
  564. cidr1 := "1.1.1.0/24"
  565. cidr2 := "0.0.0.0/0"
  566. rule1, _ := ipaccess.NewRuleByCIDR(&cidr1, []int{80, 443}, true)
  567. rule2, _ := ipaccess.NewRuleByCIDR(&cidr2, nil, false)
  568. rules := []ipaccess.Rule{rule1, rule2}
  569. accessPolicy, _ := ipaccess.NewPolicy(false, rules)
  570. return accessPolicy
  571. }
  572. func BenchmarkFindMatch(b *testing.B) {
  573. rulesYAML := `
  574. ingress:
  575. - hostname: tunnel1.example.com
  576. service: https://localhost:8000
  577. - hostname: tunnel2.example.com
  578. service: https://localhost:8001
  579. - hostname: "*"
  580. service: https://localhost:8002
  581. `
  582. ing, err := ParseIngress(MustReadIngress(rulesYAML))
  583. if err != nil {
  584. b.Error(err)
  585. }
  586. for n := 0; n < b.N; n++ {
  587. ing.FindMatchingRule("tunnel1.example.com", "")
  588. ing.FindMatchingRule("tunnel2.example.com", "")
  589. ing.FindMatchingRule("tunnel3.example.com", "")
  590. }
  591. }
  592. func MustReadIngress(s string) *config.Configuration {
  593. var conf config.Configuration
  594. err := yaml.Unmarshal([]byte(s), &conf)
  595. if err != nil {
  596. panic(err)
  597. }
  598. return &conf
  599. }