peerapi_test.go 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589
  1. // Copyright (c) Tailscale Inc & AUTHORS
  2. // SPDX-License-Identifier: BSD-3-Clause
  3. package taildrop
  4. import (
  5. "bytes"
  6. "fmt"
  7. "io"
  8. "io/fs"
  9. "math/rand"
  10. "net/http"
  11. "net/http/httptest"
  12. "net/netip"
  13. "os"
  14. "path/filepath"
  15. "strings"
  16. "testing"
  17. "github.com/google/go-cmp/cmp"
  18. "tailscale.com/client/tailscale/apitype"
  19. "tailscale.com/ipn/ipnlocal"
  20. "tailscale.com/tailcfg"
  21. "tailscale.com/tstest"
  22. "tailscale.com/tstime"
  23. "tailscale.com/types/logger"
  24. "tailscale.com/util/must"
  25. )
  26. // peerAPIHandler serves the PeerAPI for a source specific client.
  27. type peerAPIHandler struct {
  28. remoteAddr netip.AddrPort
  29. isSelf bool // whether peerNode is owned by same user as this node
  30. selfNode tailcfg.NodeView // this node; always non-nil
  31. peerNode tailcfg.NodeView // peerNode is who's making the request
  32. canDebug bool // whether peerNode can debug this node (goroutines, metrics, magicsock internal state, etc)
  33. }
  34. func (h *peerAPIHandler) IsSelfUntagged() bool {
  35. return !h.selfNode.IsTagged() && !h.peerNode.IsTagged() && h.isSelf
  36. }
  37. func (h *peerAPIHandler) CanDebug() bool { return h.canDebug }
  38. func (h *peerAPIHandler) Peer() tailcfg.NodeView { return h.peerNode }
  39. func (h *peerAPIHandler) Self() tailcfg.NodeView { return h.selfNode }
  40. func (h *peerAPIHandler) RemoteAddr() netip.AddrPort { return h.remoteAddr }
  41. func (h *peerAPIHandler) LocalBackend() *ipnlocal.LocalBackend { panic("unexpected") }
  42. func (h *peerAPIHandler) Logf(format string, a ...any) {
  43. //h.logf(format, a...)
  44. }
  45. func (h *peerAPIHandler) PeerCaps() tailcfg.PeerCapMap {
  46. return nil
  47. }
  48. type fakeExtension struct {
  49. logf logger.Logf
  50. capFileSharing bool
  51. clock tstime.Clock
  52. taildrop *manager
  53. }
  54. func (lb *fakeExtension) manager() *manager {
  55. return lb.taildrop
  56. }
  57. func (lb *fakeExtension) Clock() tstime.Clock { return lb.clock }
  58. func (lb *fakeExtension) hasCapFileSharing() bool {
  59. return lb.capFileSharing
  60. }
  61. type peerAPITestEnv struct {
  62. taildrop *manager
  63. ph *peerAPIHandler
  64. rr *httptest.ResponseRecorder
  65. logBuf tstest.MemLogger
  66. }
  67. type check func(*testing.T, *peerAPITestEnv)
  68. func checks(vv ...check) []check { return vv }
  69. func httpStatus(wantStatus int) check {
  70. return func(t *testing.T, e *peerAPITestEnv) {
  71. if res := e.rr.Result(); res.StatusCode != wantStatus {
  72. t.Errorf("HTTP response code = %v; want %v", res.Status, wantStatus)
  73. }
  74. }
  75. }
  76. func bodyContains(sub string) check {
  77. return func(t *testing.T, e *peerAPITestEnv) {
  78. if body := e.rr.Body.String(); !strings.Contains(body, sub) {
  79. t.Errorf("HTTP response body does not contain %q; got: %s", sub, body)
  80. }
  81. }
  82. }
  83. func fileHasSize(name string, size int) check {
  84. return func(t *testing.T, e *peerAPITestEnv) {
  85. fsImpl, ok := e.taildrop.opts.fileOps.(*fsFileOps)
  86. if !ok {
  87. t.Skip("fileHasSize only supported on fsFileOps backend")
  88. return
  89. }
  90. root := fsImpl.rootDir
  91. if root == "" {
  92. t.Errorf("no rootdir; can't check whether %q has size %v", name, size)
  93. return
  94. }
  95. if root == "" {
  96. t.Errorf("no rootdir; can't check whether %q has size %v", name, size)
  97. return
  98. }
  99. path := filepath.Join(root, name)
  100. if fi, err := os.Stat(path); err != nil {
  101. t.Errorf("fileHasSize(%q, %v): %v", name, size, err)
  102. } else if fi.Size() != int64(size) {
  103. t.Errorf("file %q has size %v; want %v", name, fi.Size(), size)
  104. }
  105. }
  106. }
  107. func fileHasContents(name string, want string) check {
  108. return func(t *testing.T, e *peerAPITestEnv) {
  109. fsImpl, ok := e.taildrop.opts.fileOps.(*fsFileOps)
  110. if !ok {
  111. t.Skip("fileHasContents only supported on fsFileOps backend")
  112. return
  113. }
  114. path := filepath.Join(fsImpl.rootDir, name)
  115. got, err := os.ReadFile(path)
  116. if err != nil {
  117. t.Errorf("fileHasContents: %v", err)
  118. return
  119. }
  120. if string(got) != want {
  121. t.Errorf("file contents = %q; want %q", got, want)
  122. }
  123. }
  124. }
  125. func hexAll(v string) string {
  126. var sb strings.Builder
  127. for i := range len(v) {
  128. fmt.Fprintf(&sb, "%%%02x", v[i])
  129. }
  130. return sb.String()
  131. }
  132. func TestHandlePeerAPI(t *testing.T) {
  133. tests := []struct {
  134. name string
  135. isSelf bool // the peer sending the request is owned by us
  136. capSharing bool // self node has file sharing capability
  137. debugCap bool // self node has debug capability
  138. omitRoot bool // don't configure
  139. reqs []*http.Request
  140. checks []check
  141. }{
  142. {
  143. name: "reject_non_owner_put",
  144. isSelf: false,
  145. capSharing: true,
  146. reqs: []*http.Request{httptest.NewRequest("PUT", "/v0/put/foo", nil)},
  147. checks: checks(
  148. httpStatus(http.StatusForbidden),
  149. bodyContains("Taildrop disabled"),
  150. ),
  151. },
  152. {
  153. name: "owner_without_cap",
  154. isSelf: true,
  155. capSharing: false,
  156. reqs: []*http.Request{httptest.NewRequest("PUT", "/v0/put/foo", nil)},
  157. checks: checks(
  158. httpStatus(http.StatusForbidden),
  159. bodyContains("Taildrop disabled"),
  160. ),
  161. },
  162. {
  163. name: "owner_with_cap_no_rootdir",
  164. omitRoot: true,
  165. isSelf: true,
  166. capSharing: true,
  167. reqs: []*http.Request{httptest.NewRequest("PUT", "/v0/put/foo", nil)},
  168. checks: checks(
  169. httpStatus(http.StatusForbidden),
  170. bodyContains("Taildrop disabled"),
  171. ),
  172. },
  173. {
  174. name: "bad_method",
  175. isSelf: true,
  176. capSharing: true,
  177. reqs: []*http.Request{httptest.NewRequest("POST", "/v0/put/foo", nil)},
  178. checks: checks(
  179. httpStatus(405),
  180. bodyContains("expected method GET or PUT"),
  181. ),
  182. },
  183. {
  184. name: "put_zero_length",
  185. isSelf: true,
  186. capSharing: true,
  187. reqs: []*http.Request{httptest.NewRequest("PUT", "/v0/put/foo", nil)},
  188. checks: checks(
  189. httpStatus(200),
  190. bodyContains("{}"),
  191. fileHasSize("foo", 0),
  192. fileHasContents("foo", ""),
  193. ),
  194. },
  195. {
  196. name: "put_non_zero_length_content_length",
  197. isSelf: true,
  198. capSharing: true,
  199. reqs: []*http.Request{httptest.NewRequest("PUT", "/v0/put/foo", strings.NewReader("contents"))},
  200. checks: checks(
  201. httpStatus(200),
  202. bodyContains("{}"),
  203. fileHasSize("foo", len("contents")),
  204. fileHasContents("foo", "contents"),
  205. ),
  206. },
  207. {
  208. name: "put_non_zero_length_chunked",
  209. isSelf: true,
  210. capSharing: true,
  211. reqs: []*http.Request{httptest.NewRequest("PUT", "/v0/put/foo", struct{ io.Reader }{strings.NewReader("contents")})},
  212. checks: checks(
  213. httpStatus(200),
  214. bodyContains("{}"),
  215. fileHasSize("foo", len("contents")),
  216. fileHasContents("foo", "contents"),
  217. ),
  218. },
  219. {
  220. name: "bad_filename_partial",
  221. isSelf: true,
  222. capSharing: true,
  223. reqs: []*http.Request{httptest.NewRequest("PUT", "/v0/put/foo.partial", nil)},
  224. checks: checks(
  225. httpStatus(400),
  226. bodyContains("invalid filename"),
  227. ),
  228. },
  229. {
  230. name: "bad_filename_deleted",
  231. isSelf: true,
  232. capSharing: true,
  233. reqs: []*http.Request{httptest.NewRequest("PUT", "/v0/put/foo.deleted", nil)},
  234. checks: checks(
  235. httpStatus(400),
  236. bodyContains("invalid filename"),
  237. ),
  238. },
  239. {
  240. name: "bad_filename_dot",
  241. isSelf: true,
  242. capSharing: true,
  243. reqs: []*http.Request{httptest.NewRequest("PUT", "/v0/put/.", nil)},
  244. checks: checks(
  245. httpStatus(400),
  246. bodyContains("invalid filename"),
  247. ),
  248. },
  249. {
  250. name: "bad_filename_empty",
  251. isSelf: true,
  252. capSharing: true,
  253. reqs: []*http.Request{httptest.NewRequest("PUT", "/v0/put/", nil)},
  254. checks: checks(
  255. httpStatus(400),
  256. bodyContains("invalid filename"),
  257. ),
  258. },
  259. {
  260. name: "bad_filename_slash",
  261. isSelf: true,
  262. capSharing: true,
  263. reqs: []*http.Request{httptest.NewRequest("PUT", "/v0/put/foo/bar", nil)},
  264. checks: checks(
  265. httpStatus(400),
  266. bodyContains("invalid filename"),
  267. ),
  268. },
  269. {
  270. name: "bad_filename_encoded_dot",
  271. isSelf: true,
  272. capSharing: true,
  273. reqs: []*http.Request{httptest.NewRequest("PUT", "/v0/put/"+hexAll("."), nil)},
  274. checks: checks(
  275. httpStatus(400),
  276. bodyContains("invalid filename"),
  277. ),
  278. },
  279. {
  280. name: "bad_filename_encoded_slash",
  281. isSelf: true,
  282. capSharing: true,
  283. reqs: []*http.Request{httptest.NewRequest("PUT", "/v0/put/"+hexAll("/"), nil)},
  284. checks: checks(
  285. httpStatus(400),
  286. bodyContains("invalid filename"),
  287. ),
  288. },
  289. {
  290. name: "bad_filename_encoded_backslash",
  291. isSelf: true,
  292. capSharing: true,
  293. reqs: []*http.Request{httptest.NewRequest("PUT", "/v0/put/"+hexAll("\\"), nil)},
  294. checks: checks(
  295. httpStatus(400),
  296. bodyContains("invalid filename"),
  297. ),
  298. },
  299. {
  300. name: "bad_filename_encoded_dotdot",
  301. isSelf: true,
  302. capSharing: true,
  303. reqs: []*http.Request{httptest.NewRequest("PUT", "/v0/put/"+hexAll(".."), nil)},
  304. checks: checks(
  305. httpStatus(400),
  306. bodyContains("invalid filename"),
  307. ),
  308. },
  309. {
  310. name: "bad_filename_encoded_dotdot_out",
  311. isSelf: true,
  312. capSharing: true,
  313. reqs: []*http.Request{httptest.NewRequest("PUT", "/v0/put/"+hexAll("foo/../../../../../etc/passwd"), nil)},
  314. checks: checks(
  315. httpStatus(400),
  316. bodyContains("invalid filename"),
  317. ),
  318. },
  319. {
  320. name: "put_spaces_and_caps",
  321. isSelf: true,
  322. capSharing: true,
  323. reqs: []*http.Request{httptest.NewRequest("PUT", "/v0/put/"+hexAll("Foo Bar.dat"), strings.NewReader("baz"))},
  324. checks: checks(
  325. httpStatus(200),
  326. bodyContains("{}"),
  327. fileHasContents("Foo Bar.dat", "baz"),
  328. ),
  329. },
  330. {
  331. name: "put_unicode",
  332. isSelf: true,
  333. capSharing: true,
  334. reqs: []*http.Request{httptest.NewRequest("PUT", "/v0/put/"+hexAll("Томас и его друзья.mp3"), strings.NewReader("главный озорник"))},
  335. checks: checks(
  336. httpStatus(200),
  337. bodyContains("{}"),
  338. fileHasContents("Томас и его друзья.mp3", "главный озорник"),
  339. ),
  340. },
  341. {
  342. name: "put_invalid_utf8",
  343. isSelf: true,
  344. capSharing: true,
  345. reqs: []*http.Request{httptest.NewRequest("PUT", "/v0/put/"+(hexAll("😜")[:3]), nil)},
  346. checks: checks(
  347. httpStatus(400),
  348. bodyContains("invalid filename"),
  349. ),
  350. },
  351. {
  352. name: "put_invalid_null",
  353. isSelf: true,
  354. capSharing: true,
  355. reqs: []*http.Request{httptest.NewRequest("PUT", "/v0/put/%00", nil)},
  356. checks: checks(
  357. httpStatus(400),
  358. bodyContains("invalid filename"),
  359. ),
  360. },
  361. {
  362. name: "put_invalid_non_printable",
  363. isSelf: true,
  364. capSharing: true,
  365. reqs: []*http.Request{httptest.NewRequest("PUT", "/v0/put/%01", nil)},
  366. checks: checks(
  367. httpStatus(400),
  368. bodyContains("invalid filename"),
  369. ),
  370. },
  371. {
  372. name: "put_invalid_colon",
  373. isSelf: true,
  374. capSharing: true,
  375. reqs: []*http.Request{httptest.NewRequest("PUT", "/v0/put/"+hexAll("nul:"), nil)},
  376. checks: checks(
  377. httpStatus(400),
  378. bodyContains("invalid filename"),
  379. ),
  380. },
  381. {
  382. name: "put_invalid_surrounding_whitespace",
  383. isSelf: true,
  384. capSharing: true,
  385. reqs: []*http.Request{httptest.NewRequest("PUT", "/v0/put/"+hexAll(" foo "), nil)},
  386. checks: checks(
  387. httpStatus(400),
  388. bodyContains("invalid filename"),
  389. ),
  390. },
  391. {
  392. name: "duplicate_zero_length",
  393. isSelf: true,
  394. capSharing: true,
  395. reqs: []*http.Request{
  396. httptest.NewRequest("PUT", "/v0/put/foo", nil),
  397. httptest.NewRequest("PUT", "/v0/put/foo", nil),
  398. },
  399. checks: checks(
  400. httpStatus(200),
  401. func(t *testing.T, env *peerAPITestEnv) {
  402. got, err := env.taildrop.WaitingFiles()
  403. if err != nil {
  404. t.Fatalf("WaitingFiles error: %v", err)
  405. }
  406. want := []apitype.WaitingFile{{Name: "foo", Size: 0}}
  407. if diff := cmp.Diff(got, want); diff != "" {
  408. t.Fatalf("WaitingFile mismatch (-got +want):\n%s", diff)
  409. }
  410. },
  411. ),
  412. },
  413. {
  414. name: "duplicate_non_zero_length_content_length",
  415. isSelf: true,
  416. capSharing: true,
  417. reqs: []*http.Request{
  418. httptest.NewRequest("PUT", "/v0/put/foo", strings.NewReader("contents")),
  419. httptest.NewRequest("PUT", "/v0/put/foo", strings.NewReader("contents")),
  420. },
  421. checks: checks(
  422. httpStatus(200),
  423. func(t *testing.T, env *peerAPITestEnv) {
  424. got, err := env.taildrop.WaitingFiles()
  425. if err != nil {
  426. t.Fatalf("WaitingFiles error: %v", err)
  427. }
  428. want := []apitype.WaitingFile{{Name: "foo", Size: 8}}
  429. if diff := cmp.Diff(got, want); diff != "" {
  430. t.Fatalf("WaitingFile mismatch (-got +want):\n%s", diff)
  431. }
  432. },
  433. ),
  434. },
  435. {
  436. name: "duplicate_different_files",
  437. isSelf: true,
  438. capSharing: true,
  439. reqs: []*http.Request{
  440. httptest.NewRequest("PUT", "/v0/put/foo", strings.NewReader("fizz")),
  441. httptest.NewRequest("PUT", "/v0/put/foo", strings.NewReader("buzz")),
  442. },
  443. checks: checks(
  444. httpStatus(200),
  445. func(t *testing.T, env *peerAPITestEnv) {
  446. got, err := env.taildrop.WaitingFiles()
  447. if err != nil {
  448. t.Fatalf("WaitingFiles error: %v", err)
  449. }
  450. want := []apitype.WaitingFile{{Name: "foo", Size: 4}, {Name: "foo (1)", Size: 4}}
  451. if diff := cmp.Diff(got, want); diff != "" {
  452. t.Fatalf("WaitingFile mismatch (-got +want):\n%s", diff)
  453. }
  454. },
  455. ),
  456. },
  457. }
  458. for _, tt := range tests {
  459. t.Run(tt.name, func(t *testing.T) {
  460. selfNode := &tailcfg.Node{
  461. Addresses: []netip.Prefix{
  462. netip.MustParsePrefix("100.100.100.101/32"),
  463. },
  464. }
  465. if tt.debugCap {
  466. selfNode.CapMap = tailcfg.NodeCapMap{tailcfg.CapabilityDebug: nil}
  467. }
  468. var rootDir string
  469. var fo FileOps
  470. if !tt.omitRoot {
  471. var err error
  472. if fo, err = newFileOps(t.TempDir()); err != nil {
  473. t.Fatalf("newFileOps: %v", err)
  474. }
  475. }
  476. var e peerAPITestEnv
  477. e.taildrop = managerOptions{
  478. Logf: e.logBuf.Logf,
  479. fileOps: fo,
  480. }.New()
  481. ext := &fakeExtension{
  482. logf: e.logBuf.Logf,
  483. capFileSharing: tt.capSharing,
  484. clock: &tstest.Clock{},
  485. taildrop: e.taildrop,
  486. }
  487. e.ph = &peerAPIHandler{
  488. isSelf: tt.isSelf,
  489. selfNode: selfNode.View(),
  490. peerNode: (&tailcfg.Node{ComputedName: "some-peer-name"}).View(),
  491. }
  492. for _, req := range tt.reqs {
  493. e.rr = httptest.NewRecorder()
  494. if req.Host == "example.com" {
  495. req.Host = "100.100.100.101:12345"
  496. }
  497. handlePeerPutWithBackend(e.ph, ext, e.rr, req)
  498. }
  499. for _, f := range tt.checks {
  500. f(t, &e)
  501. }
  502. if t.Failed() && rootDir != "" {
  503. t.Logf("Contents of %s:", rootDir)
  504. des, _ := fs.ReadDir(os.DirFS(rootDir), ".")
  505. for _, de := range des {
  506. fi, err := de.Info()
  507. if err != nil {
  508. t.Log(err)
  509. } else {
  510. t.Logf(" %v %5d %s", fi.Mode(), fi.Size(), de.Name())
  511. }
  512. }
  513. }
  514. })
  515. }
  516. }
  517. // Windows likes to hold on to file descriptors for some indeterminate
  518. // amount of time after you close them and not let you delete them for
  519. // a bit. So test that we work around that sufficiently.
  520. func TestFileDeleteRace(t *testing.T) {
  521. dir := t.TempDir()
  522. taildropMgr := managerOptions{
  523. Logf: t.Logf,
  524. fileOps: must.Get(newFileOps(dir)),
  525. }.New()
  526. ph := &peerAPIHandler{
  527. isSelf: true,
  528. peerNode: (&tailcfg.Node{
  529. ComputedName: "some-peer-name",
  530. }).View(),
  531. selfNode: (&tailcfg.Node{
  532. Addresses: []netip.Prefix{netip.MustParsePrefix("100.100.100.101/32")},
  533. }).View(),
  534. }
  535. fakeLB := &fakeExtension{
  536. logf: t.Logf,
  537. capFileSharing: true,
  538. clock: &tstest.Clock{},
  539. taildrop: taildropMgr,
  540. }
  541. buf := make([]byte, 2<<20)
  542. for range 30 {
  543. rr := httptest.NewRecorder()
  544. handlePeerPutWithBackend(ph, fakeLB, rr, httptest.NewRequest("PUT", "http://100.100.100.101:123/v0/put/foo.txt", bytes.NewReader(buf[:rand.Intn(len(buf))])))
  545. if res := rr.Result(); res.StatusCode != 200 {
  546. t.Fatal(res.Status)
  547. }
  548. wfs, err := taildropMgr.WaitingFiles()
  549. if err != nil {
  550. t.Fatal(err)
  551. }
  552. if len(wfs) != 1 {
  553. t.Fatalf("waiting files = %d; want 1", len(wfs))
  554. }
  555. if err := taildropMgr.DeleteFile("foo.txt"); err != nil {
  556. t.Fatal(err)
  557. }
  558. wfs, err = taildropMgr.WaitingFiles()
  559. if err != nil {
  560. t.Fatal(err)
  561. }
  562. if len(wfs) != 0 {
  563. t.Fatalf("waiting files = %d; want 0", len(wfs))
  564. }
  565. }
  566. }