main_test.go 27 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034
  1. // Copyright (c) Tailscale Inc & AUTHORS
  2. // SPDX-License-Identifier: BSD-3-Clause
  3. //go:build linux
  4. package main
  5. import (
  6. "bytes"
  7. _ "embed"
  8. "encoding/base64"
  9. "encoding/json"
  10. "encoding/pem"
  11. "errors"
  12. "fmt"
  13. "io"
  14. "io/fs"
  15. "net"
  16. "net/http"
  17. "net/http/httptest"
  18. "net/netip"
  19. "os"
  20. "os/exec"
  21. "path/filepath"
  22. "strconv"
  23. "strings"
  24. "sync"
  25. "testing"
  26. "time"
  27. "github.com/google/go-cmp/cmp"
  28. "golang.org/x/sys/unix"
  29. "tailscale.com/ipn"
  30. "tailscale.com/tailcfg"
  31. "tailscale.com/tstest"
  32. "tailscale.com/types/netmap"
  33. "tailscale.com/types/ptr"
  34. )
  35. func TestContainerBoot(t *testing.T) {
  36. d := t.TempDir()
  37. lapi := localAPI{FSRoot: d}
  38. if err := lapi.Start(); err != nil {
  39. t.Fatal(err)
  40. }
  41. defer lapi.Close()
  42. kube := kubeServer{FSRoot: d}
  43. if err := kube.Start(); err != nil {
  44. t.Fatal(err)
  45. }
  46. defer kube.Close()
  47. dirs := []string{
  48. "var/lib",
  49. "usr/bin",
  50. "tmp",
  51. "dev/net",
  52. "proc/sys/net/ipv4",
  53. "proc/sys/net/ipv6/conf/all",
  54. }
  55. for _, path := range dirs {
  56. if err := os.MkdirAll(filepath.Join(d, path), 0700); err != nil {
  57. t.Fatal(err)
  58. }
  59. }
  60. files := map[string][]byte{
  61. "usr/bin/tailscaled": fakeTailscaled,
  62. "usr/bin/tailscale": fakeTailscale,
  63. "usr/bin/iptables": fakeTailscale,
  64. "usr/bin/ip6tables": fakeTailscale,
  65. "dev/net/tun": []byte(""),
  66. "proc/sys/net/ipv4/ip_forward": []byte("0"),
  67. "proc/sys/net/ipv6/conf/all/forwarding": []byte("0"),
  68. }
  69. resetFiles := func() {
  70. for path, content := range files {
  71. // Making everything executable is a little weird, but the
  72. // stuff that doesn't need to be executable doesn't care if we
  73. // do make it executable.
  74. if err := os.WriteFile(filepath.Join(d, path), content, 0700); err != nil {
  75. t.Fatal(err)
  76. }
  77. }
  78. }
  79. resetFiles()
  80. boot := filepath.Join(d, "containerboot")
  81. if err := exec.Command("go", "build", "-o", boot, "tailscale.com/cmd/containerboot").Run(); err != nil {
  82. t.Fatalf("Building containerboot: %v", err)
  83. }
  84. argFile := filepath.Join(d, "args")
  85. runningSockPath := filepath.Join(d, "tmp/tailscaled.sock")
  86. type phase struct {
  87. // If non-nil, send this IPN bus notification (and remember it as the
  88. // initial update for any future new watchers, then wait for all the
  89. // Waits below to be true before proceeding to the next phase.
  90. Notify *ipn.Notify
  91. // WantCmds is the commands that containerboot should run in this phase.
  92. WantCmds []string
  93. // WantKubeSecret is the secret keys/values that should exist in the
  94. // kube secret.
  95. WantKubeSecret map[string]string
  96. // WantFiles files that should exist in the container and their
  97. // contents.
  98. WantFiles map[string]string
  99. }
  100. runningNotify := &ipn.Notify{
  101. State: ptr.To(ipn.Running),
  102. NetMap: &netmap.NetworkMap{
  103. SelfNode: (&tailcfg.Node{
  104. StableID: tailcfg.StableNodeID("myID"),
  105. Name: "test-node.test.ts.net",
  106. Addresses: []netip.Prefix{netip.MustParsePrefix("100.64.0.1/32")},
  107. }).View(),
  108. },
  109. }
  110. tests := []struct {
  111. Name string
  112. Env map[string]string
  113. KubeSecret map[string]string
  114. KubeDenyPatch bool
  115. Phases []phase
  116. }{
  117. {
  118. // Out of the box default: runs in userspace mode, ephemeral storage, interactive login.
  119. Name: "no_args",
  120. Env: nil,
  121. Phases: []phase{
  122. {
  123. WantCmds: []string{
  124. "/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=mem: --statedir=/tmp --tun=userspace-networking",
  125. "/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false",
  126. },
  127. },
  128. {
  129. Notify: runningNotify,
  130. },
  131. },
  132. },
  133. {
  134. // Userspace mode, ephemeral storage, authkey provided on every run.
  135. Name: "authkey",
  136. Env: map[string]string{
  137. "TS_AUTHKEY": "tskey-key",
  138. },
  139. Phases: []phase{
  140. {
  141. WantCmds: []string{
  142. "/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=mem: --statedir=/tmp --tun=userspace-networking",
  143. "/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --authkey=tskey-key",
  144. },
  145. },
  146. {
  147. Notify: runningNotify,
  148. },
  149. },
  150. },
  151. {
  152. // Userspace mode, ephemeral storage, authkey provided on every run.
  153. Name: "authkey-old-flag",
  154. Env: map[string]string{
  155. "TS_AUTH_KEY": "tskey-key",
  156. },
  157. Phases: []phase{
  158. {
  159. WantCmds: []string{
  160. "/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=mem: --statedir=/tmp --tun=userspace-networking",
  161. "/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --authkey=tskey-key",
  162. },
  163. },
  164. {
  165. Notify: runningNotify,
  166. },
  167. },
  168. },
  169. {
  170. Name: "authkey_disk_state",
  171. Env: map[string]string{
  172. "TS_AUTHKEY": "tskey-key",
  173. "TS_STATE_DIR": filepath.Join(d, "tmp"),
  174. },
  175. Phases: []phase{
  176. {
  177. WantCmds: []string{
  178. "/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --statedir=/tmp --tun=userspace-networking",
  179. "/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --authkey=tskey-key",
  180. },
  181. },
  182. {
  183. Notify: runningNotify,
  184. },
  185. },
  186. },
  187. {
  188. Name: "routes",
  189. Env: map[string]string{
  190. "TS_AUTHKEY": "tskey-key",
  191. "TS_ROUTES": "1.2.3.0/24,10.20.30.0/24",
  192. },
  193. Phases: []phase{
  194. {
  195. WantCmds: []string{
  196. "/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=mem: --statedir=/tmp --tun=userspace-networking",
  197. "/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --authkey=tskey-key --advertise-routes=1.2.3.0/24,10.20.30.0/24",
  198. },
  199. },
  200. {
  201. Notify: runningNotify,
  202. WantFiles: map[string]string{
  203. "proc/sys/net/ipv4/ip_forward": "0",
  204. "proc/sys/net/ipv6/conf/all/forwarding": "0",
  205. },
  206. },
  207. },
  208. },
  209. {
  210. Name: "routes_kernel_ipv4",
  211. Env: map[string]string{
  212. "TS_AUTHKEY": "tskey-key",
  213. "TS_ROUTES": "1.2.3.0/24,10.20.30.0/24",
  214. "TS_USERSPACE": "false",
  215. },
  216. Phases: []phase{
  217. {
  218. WantCmds: []string{
  219. "/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=mem: --statedir=/tmp",
  220. "/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --authkey=tskey-key --advertise-routes=1.2.3.0/24,10.20.30.0/24",
  221. },
  222. },
  223. {
  224. Notify: runningNotify,
  225. WantFiles: map[string]string{
  226. "proc/sys/net/ipv4/ip_forward": "1",
  227. "proc/sys/net/ipv6/conf/all/forwarding": "0",
  228. },
  229. },
  230. },
  231. },
  232. {
  233. Name: "routes_kernel_ipv6",
  234. Env: map[string]string{
  235. "TS_AUTHKEY": "tskey-key",
  236. "TS_ROUTES": "::/64,1::/64",
  237. "TS_USERSPACE": "false",
  238. },
  239. Phases: []phase{
  240. {
  241. WantCmds: []string{
  242. "/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=mem: --statedir=/tmp",
  243. "/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --authkey=tskey-key --advertise-routes=::/64,1::/64",
  244. },
  245. },
  246. {
  247. Notify: runningNotify,
  248. WantFiles: map[string]string{
  249. "proc/sys/net/ipv4/ip_forward": "0",
  250. "proc/sys/net/ipv6/conf/all/forwarding": "1",
  251. },
  252. },
  253. },
  254. },
  255. {
  256. Name: "routes_kernel_all_families",
  257. Env: map[string]string{
  258. "TS_AUTHKEY": "tskey-key",
  259. "TS_ROUTES": "::/64,1.2.3.0/24",
  260. "TS_USERSPACE": "false",
  261. },
  262. Phases: []phase{
  263. {
  264. WantCmds: []string{
  265. "/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=mem: --statedir=/tmp",
  266. "/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --authkey=tskey-key --advertise-routes=::/64,1.2.3.0/24",
  267. },
  268. },
  269. {
  270. Notify: runningNotify,
  271. WantFiles: map[string]string{
  272. "proc/sys/net/ipv4/ip_forward": "1",
  273. "proc/sys/net/ipv6/conf/all/forwarding": "1",
  274. },
  275. },
  276. },
  277. },
  278. {
  279. Name: "ingres proxy",
  280. Env: map[string]string{
  281. "TS_AUTHKEY": "tskey-key",
  282. "TS_DEST_IP": "1.2.3.4",
  283. "TS_USERSPACE": "false",
  284. },
  285. Phases: []phase{
  286. {
  287. WantCmds: []string{
  288. "/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=mem: --statedir=/tmp",
  289. "/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --authkey=tskey-key",
  290. },
  291. },
  292. {
  293. Notify: runningNotify,
  294. },
  295. },
  296. },
  297. {
  298. Name: "egress proxy",
  299. Env: map[string]string{
  300. "TS_AUTHKEY": "tskey-key",
  301. "TS_TAILNET_TARGET_IP": "100.99.99.99",
  302. "TS_USERSPACE": "false",
  303. },
  304. Phases: []phase{
  305. {
  306. WantCmds: []string{
  307. "/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=mem: --statedir=/tmp",
  308. "/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --authkey=tskey-key",
  309. },
  310. },
  311. {
  312. Notify: runningNotify,
  313. },
  314. },
  315. },
  316. {
  317. Name: "authkey_once",
  318. Env: map[string]string{
  319. "TS_AUTHKEY": "tskey-key",
  320. "TS_AUTH_ONCE": "true",
  321. },
  322. Phases: []phase{
  323. {
  324. WantCmds: []string{
  325. "/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=mem: --statedir=/tmp --tun=userspace-networking",
  326. },
  327. },
  328. {
  329. Notify: &ipn.Notify{
  330. State: ptr.To(ipn.NeedsLogin),
  331. },
  332. WantCmds: []string{
  333. "/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --authkey=tskey-key",
  334. },
  335. },
  336. {
  337. Notify: runningNotify,
  338. WantCmds: []string{
  339. "/usr/bin/tailscale --socket=/tmp/tailscaled.sock set --accept-dns=false",
  340. },
  341. },
  342. },
  343. },
  344. {
  345. Name: "kube_storage",
  346. Env: map[string]string{
  347. "KUBERNETES_SERVICE_HOST": kube.Host,
  348. "KUBERNETES_SERVICE_PORT_HTTPS": kube.Port,
  349. },
  350. KubeSecret: map[string]string{
  351. "authkey": "tskey-key",
  352. },
  353. Phases: []phase{
  354. {
  355. WantCmds: []string{
  356. "/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=kube:tailscale --statedir=/tmp --tun=userspace-networking",
  357. "/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --authkey=tskey-key",
  358. },
  359. WantKubeSecret: map[string]string{
  360. "authkey": "tskey-key",
  361. },
  362. },
  363. {
  364. Notify: runningNotify,
  365. WantKubeSecret: map[string]string{
  366. "authkey": "tskey-key",
  367. "device_fqdn": "test-node.test.ts.net",
  368. "device_id": "myID",
  369. "device_ips": `["100.64.0.1"]`,
  370. },
  371. },
  372. },
  373. },
  374. {
  375. Name: "kube_disk_storage",
  376. Env: map[string]string{
  377. "KUBERNETES_SERVICE_HOST": kube.Host,
  378. "KUBERNETES_SERVICE_PORT_HTTPS": kube.Port,
  379. // Explicitly set to an empty value, to override the default of "tailscale".
  380. "TS_KUBE_SECRET": "",
  381. "TS_STATE_DIR": filepath.Join(d, "tmp"),
  382. "TS_AUTHKEY": "tskey-key",
  383. },
  384. KubeSecret: map[string]string{},
  385. Phases: []phase{
  386. {
  387. WantCmds: []string{
  388. "/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --statedir=/tmp --tun=userspace-networking",
  389. "/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --authkey=tskey-key",
  390. },
  391. WantKubeSecret: map[string]string{},
  392. },
  393. {
  394. Notify: runningNotify,
  395. WantKubeSecret: map[string]string{},
  396. },
  397. },
  398. },
  399. {
  400. Name: "kube_storage_no_patch",
  401. Env: map[string]string{
  402. "KUBERNETES_SERVICE_HOST": kube.Host,
  403. "KUBERNETES_SERVICE_PORT_HTTPS": kube.Port,
  404. "TS_AUTHKEY": "tskey-key",
  405. },
  406. KubeSecret: map[string]string{},
  407. KubeDenyPatch: true,
  408. Phases: []phase{
  409. {
  410. WantCmds: []string{
  411. "/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=kube:tailscale --statedir=/tmp --tun=userspace-networking",
  412. "/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --authkey=tskey-key",
  413. },
  414. WantKubeSecret: map[string]string{},
  415. },
  416. {
  417. Notify: runningNotify,
  418. WantKubeSecret: map[string]string{},
  419. },
  420. },
  421. },
  422. {
  423. // Same as previous, but deletes the authkey from the kube secret.
  424. Name: "kube_storage_auth_once",
  425. Env: map[string]string{
  426. "KUBERNETES_SERVICE_HOST": kube.Host,
  427. "KUBERNETES_SERVICE_PORT_HTTPS": kube.Port,
  428. "TS_AUTH_ONCE": "true",
  429. },
  430. KubeSecret: map[string]string{
  431. "authkey": "tskey-key",
  432. },
  433. Phases: []phase{
  434. {
  435. WantCmds: []string{
  436. "/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=kube:tailscale --statedir=/tmp --tun=userspace-networking",
  437. },
  438. WantKubeSecret: map[string]string{
  439. "authkey": "tskey-key",
  440. },
  441. },
  442. {
  443. Notify: &ipn.Notify{
  444. State: ptr.To(ipn.NeedsLogin),
  445. },
  446. WantCmds: []string{
  447. "/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --authkey=tskey-key",
  448. },
  449. WantKubeSecret: map[string]string{
  450. "authkey": "tskey-key",
  451. },
  452. },
  453. {
  454. Notify: runningNotify,
  455. WantCmds: []string{
  456. "/usr/bin/tailscale --socket=/tmp/tailscaled.sock set --accept-dns=false",
  457. },
  458. WantKubeSecret: map[string]string{
  459. "device_fqdn": "test-node.test.ts.net",
  460. "device_id": "myID",
  461. "device_ips": `["100.64.0.1"]`,
  462. },
  463. },
  464. },
  465. },
  466. {
  467. Name: "kube_storage_updates",
  468. Env: map[string]string{
  469. "KUBERNETES_SERVICE_HOST": kube.Host,
  470. "KUBERNETES_SERVICE_PORT_HTTPS": kube.Port,
  471. },
  472. KubeSecret: map[string]string{
  473. "authkey": "tskey-key",
  474. },
  475. Phases: []phase{
  476. {
  477. WantCmds: []string{
  478. "/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=kube:tailscale --statedir=/tmp --tun=userspace-networking",
  479. "/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --authkey=tskey-key",
  480. },
  481. WantKubeSecret: map[string]string{
  482. "authkey": "tskey-key",
  483. },
  484. },
  485. {
  486. Notify: runningNotify,
  487. WantKubeSecret: map[string]string{
  488. "authkey": "tskey-key",
  489. "device_fqdn": "test-node.test.ts.net",
  490. "device_id": "myID",
  491. "device_ips": `["100.64.0.1"]`,
  492. },
  493. },
  494. {
  495. Notify: &ipn.Notify{
  496. State: ptr.To(ipn.Running),
  497. NetMap: &netmap.NetworkMap{
  498. SelfNode: (&tailcfg.Node{
  499. StableID: tailcfg.StableNodeID("newID"),
  500. Name: "new-name.test.ts.net",
  501. Addresses: []netip.Prefix{netip.MustParsePrefix("100.64.0.1/32")},
  502. }).View(),
  503. },
  504. },
  505. WantKubeSecret: map[string]string{
  506. "authkey": "tskey-key",
  507. "device_fqdn": "new-name.test.ts.net",
  508. "device_id": "newID",
  509. "device_ips": `["100.64.0.1"]`,
  510. },
  511. },
  512. },
  513. },
  514. {
  515. Name: "proxies",
  516. Env: map[string]string{
  517. "TS_SOCKS5_SERVER": "localhost:1080",
  518. "TS_OUTBOUND_HTTP_PROXY_LISTEN": "localhost:8080",
  519. },
  520. Phases: []phase{
  521. {
  522. WantCmds: []string{
  523. "/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=mem: --statedir=/tmp --tun=userspace-networking --socks5-server=localhost:1080 --outbound-http-proxy-listen=localhost:8080",
  524. "/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false",
  525. },
  526. },
  527. {
  528. Notify: runningNotify,
  529. },
  530. },
  531. },
  532. {
  533. Name: "dns",
  534. Env: map[string]string{
  535. "TS_ACCEPT_DNS": "true",
  536. },
  537. Phases: []phase{
  538. {
  539. WantCmds: []string{
  540. "/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=mem: --statedir=/tmp --tun=userspace-networking",
  541. "/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=true",
  542. },
  543. },
  544. {
  545. Notify: runningNotify,
  546. },
  547. },
  548. },
  549. {
  550. Name: "extra_args",
  551. Env: map[string]string{
  552. "TS_EXTRA_ARGS": "--widget=rotated",
  553. "TS_TAILSCALED_EXTRA_ARGS": "--experiments=widgets",
  554. },
  555. Phases: []phase{
  556. {
  557. WantCmds: []string{
  558. "/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=mem: --statedir=/tmp --tun=userspace-networking --experiments=widgets",
  559. "/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --widget=rotated",
  560. },
  561. }, {
  562. Notify: runningNotify,
  563. },
  564. },
  565. },
  566. {
  567. Name: "hostname",
  568. Env: map[string]string{
  569. "TS_HOSTNAME": "my-server",
  570. },
  571. Phases: []phase{
  572. {
  573. WantCmds: []string{
  574. "/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=mem: --statedir=/tmp --tun=userspace-networking",
  575. "/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --hostname=my-server",
  576. },
  577. }, {
  578. Notify: runningNotify,
  579. },
  580. },
  581. },
  582. }
  583. for _, test := range tests {
  584. t.Run(test.Name, func(t *testing.T) {
  585. lapi.Reset()
  586. kube.Reset()
  587. os.Remove(argFile)
  588. os.Remove(runningSockPath)
  589. resetFiles()
  590. for k, v := range test.KubeSecret {
  591. kube.SetSecret(k, v)
  592. }
  593. kube.SetPatching(!test.KubeDenyPatch)
  594. cmd := exec.Command(boot)
  595. cmd.Env = []string{
  596. fmt.Sprintf("PATH=%s/usr/bin:%s", d, os.Getenv("PATH")),
  597. fmt.Sprintf("TS_TEST_RECORD_ARGS=%s", argFile),
  598. fmt.Sprintf("TS_TEST_SOCKET=%s", lapi.Path),
  599. fmt.Sprintf("TS_SOCKET=%s", runningSockPath),
  600. fmt.Sprintf("TS_TEST_ONLY_ROOT=%s", d),
  601. fmt.Sprint("TS_TEST_FAKE_NETFILTER=true"),
  602. }
  603. for k, v := range test.Env {
  604. cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%s", k, v))
  605. }
  606. cbOut := &lockingBuffer{}
  607. defer func() {
  608. if t.Failed() {
  609. t.Logf("containerboot output:\n%s", cbOut.String())
  610. }
  611. }()
  612. cmd.Stderr = cbOut
  613. if err := cmd.Start(); err != nil {
  614. t.Fatalf("starting containerboot: %v", err)
  615. }
  616. defer func() {
  617. cmd.Process.Signal(unix.SIGTERM)
  618. cmd.Process.Wait()
  619. }()
  620. var wantCmds []string
  621. for i, p := range test.Phases {
  622. lapi.Notify(p.Notify)
  623. wantCmds = append(wantCmds, p.WantCmds...)
  624. waitArgs(t, 2*time.Second, d, argFile, strings.Join(wantCmds, "\n"))
  625. err := tstest.WaitFor(2*time.Second, func() error {
  626. if p.WantKubeSecret != nil {
  627. got := kube.Secret()
  628. if diff := cmp.Diff(got, p.WantKubeSecret); diff != "" {
  629. return fmt.Errorf("unexpected kube secret data (-got+want):\n%s", diff)
  630. }
  631. } else {
  632. got := kube.Secret()
  633. if len(got) > 0 {
  634. return fmt.Errorf("kube secret unexpectedly not empty, got %#v", got)
  635. }
  636. }
  637. return nil
  638. })
  639. if err != nil {
  640. t.Fatalf("phase %d: %v", i, err)
  641. }
  642. err = tstest.WaitFor(2*time.Second, func() error {
  643. for path, want := range p.WantFiles {
  644. gotBs, err := os.ReadFile(filepath.Join(d, path))
  645. if err != nil {
  646. return fmt.Errorf("reading wanted file %q: %v", path, err)
  647. }
  648. if got := strings.TrimSpace(string(gotBs)); got != want {
  649. return fmt.Errorf("wrong file contents for %q, got %q want %q", path, got, want)
  650. }
  651. }
  652. return nil
  653. })
  654. if err != nil {
  655. t.Fatal(err)
  656. }
  657. }
  658. waitLogLine(t, 2*time.Second, cbOut, "Startup complete, waiting for shutdown signal")
  659. })
  660. }
  661. }
  662. type lockingBuffer struct {
  663. sync.Mutex
  664. b bytes.Buffer
  665. }
  666. func (b *lockingBuffer) Write(bs []byte) (int, error) {
  667. b.Lock()
  668. defer b.Unlock()
  669. return b.b.Write(bs)
  670. }
  671. func (b *lockingBuffer) String() string {
  672. b.Lock()
  673. defer b.Unlock()
  674. return b.b.String()
  675. }
  676. // waitLogLine looks for want in the contents of b.
  677. //
  678. // Only lines starting with 'boot: ' (the output of containerboot
  679. // itself) are considered, and the logged timestamp is ignored.
  680. //
  681. // waitLogLine fails the entire test if path doesn't contain want
  682. // before the timeout.
  683. func waitLogLine(t *testing.T, timeout time.Duration, b *lockingBuffer, want string) {
  684. deadline := time.Now().Add(timeout)
  685. for time.Now().Before(deadline) {
  686. for _, line := range strings.Split(b.String(), "\n") {
  687. if !strings.HasPrefix(line, "boot: ") {
  688. continue
  689. }
  690. if strings.HasSuffix(line, " "+want) {
  691. return
  692. }
  693. }
  694. time.Sleep(100 * time.Millisecond)
  695. }
  696. t.Fatalf("timed out waiting for wanted output line %q. Output:\n%s", want, b.String())
  697. }
  698. // waitArgs waits until the contents of path matches wantArgs, a set
  699. // of command lines recorded by test_tailscale.sh and
  700. // test_tailscaled.sh.
  701. //
  702. // All occurrences of removeStr are removed from the file prior to
  703. // comparison. This is used to remove the varying temporary root
  704. // directory name from recorded commandlines, so that wantArgs can be
  705. // a constant value.
  706. //
  707. // waitArgs fails the entire test if path doesn't contain wantArgs
  708. // before the timeout.
  709. func waitArgs(t *testing.T, timeout time.Duration, removeStr, path, wantArgs string) {
  710. t.Helper()
  711. wantArgs = strings.TrimSpace(wantArgs)
  712. deadline := time.Now().Add(timeout)
  713. var got string
  714. for time.Now().Before(deadline) {
  715. bs, err := os.ReadFile(path)
  716. if errors.Is(err, fs.ErrNotExist) {
  717. // Don't bother logging that the file doesn't exist, it
  718. // should start existing soon.
  719. goto loop
  720. } else if err != nil {
  721. t.Logf("reading %q: %v", path, err)
  722. goto loop
  723. }
  724. got = strings.TrimSpace(string(bs))
  725. got = strings.ReplaceAll(got, removeStr, "")
  726. if got == wantArgs {
  727. return
  728. }
  729. loop:
  730. time.Sleep(100 * time.Millisecond)
  731. }
  732. t.Fatalf("waiting for args file %q to have expected output, got:\n%s\n\nWant: %s", path, got, wantArgs)
  733. }
  734. //go:embed test_tailscaled.sh
  735. var fakeTailscaled []byte
  736. //go:embed test_tailscale.sh
  737. var fakeTailscale []byte
  738. // localAPI is a minimal fake tailscaled LocalAPI server that presents
  739. // just enough functionality for containerboot to function
  740. // correctly. In practice this means it only supports querying
  741. // tailscaled status, and panics on all other uses to make it very
  742. // obvious that something unexpected happened.
  743. type localAPI struct {
  744. FSRoot string
  745. Path string // populated by Start
  746. srv *http.Server
  747. sync.Mutex
  748. cond *sync.Cond
  749. notify *ipn.Notify
  750. }
  751. func (l *localAPI) Start() error {
  752. path := filepath.Join(l.FSRoot, "tmp/tailscaled.sock.fake")
  753. if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil {
  754. return err
  755. }
  756. ln, err := net.Listen("unix", path)
  757. if err != nil {
  758. return err
  759. }
  760. l.srv = &http.Server{
  761. Handler: l,
  762. }
  763. l.Path = path
  764. l.cond = sync.NewCond(&l.Mutex)
  765. go l.srv.Serve(ln)
  766. return nil
  767. }
  768. func (l *localAPI) Close() {
  769. l.srv.Close()
  770. }
  771. func (l *localAPI) Reset() {
  772. l.Lock()
  773. defer l.Unlock()
  774. l.notify = nil
  775. l.cond.Broadcast()
  776. }
  777. func (l *localAPI) Notify(n *ipn.Notify) {
  778. if n == nil {
  779. return
  780. }
  781. l.Lock()
  782. defer l.Unlock()
  783. l.notify = n
  784. l.cond.Broadcast()
  785. }
  786. func (l *localAPI) ServeHTTP(w http.ResponseWriter, r *http.Request) {
  787. switch r.URL.Path {
  788. case "/localapi/v0/serve-config":
  789. if r.Method != "POST" {
  790. panic(fmt.Sprintf("unsupported method %q", r.Method))
  791. }
  792. return
  793. case "/localapi/v0/watch-ipn-bus":
  794. if r.Method != "GET" {
  795. panic(fmt.Sprintf("unsupported method %q", r.Method))
  796. }
  797. default:
  798. panic(fmt.Sprintf("unsupported path %q", r.URL.Path))
  799. }
  800. w.Header().Set("Content-Type", "application/json")
  801. w.WriteHeader(http.StatusOK)
  802. if f, ok := w.(http.Flusher); ok {
  803. f.Flush()
  804. }
  805. enc := json.NewEncoder(w)
  806. l.Lock()
  807. defer l.Unlock()
  808. for {
  809. if l.notify != nil {
  810. if err := enc.Encode(l.notify); err != nil {
  811. // Usually broken pipe as the test client disconnects.
  812. return
  813. }
  814. if f, ok := w.(http.Flusher); ok {
  815. f.Flush()
  816. }
  817. }
  818. l.cond.Wait()
  819. }
  820. }
  821. // kubeServer is a minimal fake Kubernetes server that presents just
  822. // enough functionality for containerboot to function correctly. In
  823. // practice this means it only supports reading and modifying a single
  824. // kube secret, and panics on all other uses to make it very obvious
  825. // that something unexpected happened.
  826. type kubeServer struct {
  827. FSRoot string
  828. Host, Port string // populated by Start
  829. srv *httptest.Server
  830. sync.Mutex
  831. secret map[string]string
  832. canPatch bool
  833. }
  834. func (k *kubeServer) Secret() map[string]string {
  835. k.Lock()
  836. defer k.Unlock()
  837. ret := map[string]string{}
  838. for k, v := range k.secret {
  839. ret[k] = v
  840. }
  841. return ret
  842. }
  843. func (k *kubeServer) SetSecret(key, val string) {
  844. k.Lock()
  845. defer k.Unlock()
  846. k.secret[key] = val
  847. }
  848. func (k *kubeServer) SetPatching(canPatch bool) {
  849. k.Lock()
  850. defer k.Unlock()
  851. k.canPatch = canPatch
  852. }
  853. func (k *kubeServer) Reset() {
  854. k.Lock()
  855. defer k.Unlock()
  856. k.secret = map[string]string{}
  857. }
  858. func (k *kubeServer) Start() error {
  859. root := filepath.Join(k.FSRoot, "var/run/secrets/kubernetes.io/serviceaccount")
  860. if err := os.MkdirAll(root, 0700); err != nil {
  861. return err
  862. }
  863. if err := os.WriteFile(filepath.Join(root, "namespace"), []byte("default"), 0600); err != nil {
  864. return err
  865. }
  866. if err := os.WriteFile(filepath.Join(root, "token"), []byte("bearer_token"), 0600); err != nil {
  867. return err
  868. }
  869. k.srv = httptest.NewTLSServer(k)
  870. k.Host = k.srv.Listener.Addr().(*net.TCPAddr).IP.String()
  871. k.Port = strconv.Itoa(k.srv.Listener.Addr().(*net.TCPAddr).Port)
  872. var cert bytes.Buffer
  873. if err := pem.Encode(&cert, &pem.Block{Type: "CERTIFICATE", Bytes: k.srv.Certificate().Raw}); err != nil {
  874. return err
  875. }
  876. if err := os.WriteFile(filepath.Join(root, "ca.crt"), cert.Bytes(), 0600); err != nil {
  877. return err
  878. }
  879. return nil
  880. }
  881. func (k *kubeServer) Close() {
  882. k.srv.Close()
  883. }
  884. func (k *kubeServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
  885. if r.Header.Get("Authorization") != "Bearer bearer_token" {
  886. panic("client didn't provide bearer token in request")
  887. }
  888. switch r.URL.Path {
  889. case "/api/v1/namespaces/default/secrets/tailscale":
  890. k.serveSecret(w, r)
  891. case "/apis/authorization.k8s.io/v1/selfsubjectaccessreviews":
  892. k.serveSSAR(w, r)
  893. default:
  894. panic(fmt.Sprintf("unhandled fake kube api path %q", r.URL.Path))
  895. }
  896. }
  897. func (k *kubeServer) serveSSAR(w http.ResponseWriter, r *http.Request) {
  898. var req struct {
  899. Spec struct {
  900. ResourceAttributes struct {
  901. Verb string `json:"verb"`
  902. } `json:"resourceAttributes"`
  903. } `json:"spec"`
  904. }
  905. if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
  906. panic(fmt.Sprintf("decoding SSAR request: %v", err))
  907. }
  908. ok := true
  909. if req.Spec.ResourceAttributes.Verb == "patch" {
  910. k.Lock()
  911. defer k.Unlock()
  912. ok = k.canPatch
  913. }
  914. // Just say yes to all SARs, we don't enforce RBAC.
  915. w.Header().Set("Content-Type", "application/json")
  916. fmt.Fprintf(w, `{"status":{"allowed":%v}}`, ok)
  917. }
  918. func (k *kubeServer) serveSecret(w http.ResponseWriter, r *http.Request) {
  919. bs, err := io.ReadAll(r.Body)
  920. if err != nil {
  921. http.Error(w, fmt.Sprintf("reading request body: %v", err), http.StatusInternalServerError)
  922. return
  923. }
  924. switch r.Method {
  925. case "GET":
  926. w.Header().Set("Content-Type", "application/json")
  927. ret := map[string]map[string]string{
  928. "data": {},
  929. }
  930. k.Lock()
  931. defer k.Unlock()
  932. for k, v := range k.secret {
  933. v := base64.StdEncoding.EncodeToString([]byte(v))
  934. if err != nil {
  935. panic("encode failed")
  936. }
  937. ret["data"][k] = v
  938. }
  939. if err := json.NewEncoder(w).Encode(ret); err != nil {
  940. panic("encode failed")
  941. }
  942. case "PATCH":
  943. k.Lock()
  944. defer k.Unlock()
  945. if !k.canPatch {
  946. panic("containerboot tried to patch despite not being allowed")
  947. }
  948. switch r.Header.Get("Content-Type") {
  949. case "application/json-patch+json":
  950. req := []struct {
  951. Op string `json:"op"`
  952. Path string `json:"path"`
  953. }{}
  954. if err := json.Unmarshal(bs, &req); err != nil {
  955. panic(fmt.Sprintf("json decode failed: %v. Body:\n\n%s", err, string(bs)))
  956. }
  957. for _, op := range req {
  958. if op.Op != "remove" {
  959. panic(fmt.Sprintf("unsupported json-patch op %q", op.Op))
  960. }
  961. if !strings.HasPrefix(op.Path, "/data/") {
  962. panic(fmt.Sprintf("unsupported json-patch path %q", op.Path))
  963. }
  964. delete(k.secret, strings.TrimPrefix(op.Path, "/data/"))
  965. }
  966. case "application/strategic-merge-patch+json":
  967. req := struct {
  968. Data map[string][]byte `json:"data"`
  969. }{}
  970. if err := json.Unmarshal(bs, &req); err != nil {
  971. panic(fmt.Sprintf("json decode failed: %v. Body:\n\n%s", err, string(bs)))
  972. }
  973. for key, val := range req.Data {
  974. k.secret[key] = string(val)
  975. }
  976. default:
  977. panic(fmt.Sprintf("unknown content type %q", r.Header.Get("Content-Type")))
  978. }
  979. default:
  980. panic(fmt.Sprintf("unhandled HTTP method %q", r.Method))
  981. }
  982. }