main_test.go 26 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000
  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. },
  107. Addresses: []netip.Prefix{netip.MustParsePrefix("100.64.0.1/32")},
  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: "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. WantCmds: []string{
  295. "/usr/bin/iptables -t nat -I PREROUTING 1 -d 100.64.0.1 -j DNAT --to-destination 1.2.3.4",
  296. },
  297. },
  298. },
  299. },
  300. {
  301. Name: "authkey_once",
  302. Env: map[string]string{
  303. "TS_AUTHKEY": "tskey-key",
  304. "TS_AUTH_ONCE": "true",
  305. },
  306. Phases: []phase{
  307. {
  308. WantCmds: []string{
  309. "/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=mem: --statedir=/tmp --tun=userspace-networking",
  310. },
  311. },
  312. {
  313. Notify: &ipn.Notify{
  314. State: ptr.To(ipn.NeedsLogin),
  315. },
  316. WantCmds: []string{
  317. "/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --authkey=tskey-key",
  318. },
  319. },
  320. {
  321. Notify: runningNotify,
  322. },
  323. },
  324. },
  325. {
  326. Name: "kube_storage",
  327. Env: map[string]string{
  328. "KUBERNETES_SERVICE_HOST": kube.Host,
  329. "KUBERNETES_SERVICE_PORT_HTTPS": kube.Port,
  330. },
  331. KubeSecret: map[string]string{
  332. "authkey": "tskey-key",
  333. },
  334. Phases: []phase{
  335. {
  336. WantCmds: []string{
  337. "/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=kube:tailscale --statedir=/tmp --tun=userspace-networking",
  338. "/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --authkey=tskey-key",
  339. },
  340. WantKubeSecret: map[string]string{
  341. "authkey": "tskey-key",
  342. },
  343. },
  344. {
  345. Notify: runningNotify,
  346. WantKubeSecret: map[string]string{
  347. "authkey": "tskey-key",
  348. "device_fqdn": "test-node.test.ts.net",
  349. "device_id": "myID",
  350. },
  351. },
  352. },
  353. },
  354. {
  355. Name: "kube_disk_storage",
  356. Env: map[string]string{
  357. "KUBERNETES_SERVICE_HOST": kube.Host,
  358. "KUBERNETES_SERVICE_PORT_HTTPS": kube.Port,
  359. // Explicitly set to an empty value, to override the default of "tailscale".
  360. "TS_KUBE_SECRET": "",
  361. "TS_STATE_DIR": filepath.Join(d, "tmp"),
  362. "TS_AUTHKEY": "tskey-key",
  363. },
  364. KubeSecret: map[string]string{},
  365. Phases: []phase{
  366. {
  367. WantCmds: []string{
  368. "/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --statedir=/tmp --tun=userspace-networking",
  369. "/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --authkey=tskey-key",
  370. },
  371. WantKubeSecret: map[string]string{},
  372. },
  373. {
  374. Notify: runningNotify,
  375. WantKubeSecret: map[string]string{},
  376. },
  377. },
  378. },
  379. {
  380. Name: "kube_storage_no_patch",
  381. Env: map[string]string{
  382. "KUBERNETES_SERVICE_HOST": kube.Host,
  383. "KUBERNETES_SERVICE_PORT_HTTPS": kube.Port,
  384. "TS_AUTHKEY": "tskey-key",
  385. },
  386. KubeSecret: map[string]string{},
  387. KubeDenyPatch: true,
  388. Phases: []phase{
  389. {
  390. WantCmds: []string{
  391. "/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=kube:tailscale --statedir=/tmp --tun=userspace-networking",
  392. "/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --authkey=tskey-key",
  393. },
  394. WantKubeSecret: map[string]string{},
  395. },
  396. {
  397. Notify: runningNotify,
  398. WantKubeSecret: map[string]string{},
  399. },
  400. },
  401. },
  402. {
  403. // Same as previous, but deletes the authkey from the kube secret.
  404. Name: "kube_storage_auth_once",
  405. Env: map[string]string{
  406. "KUBERNETES_SERVICE_HOST": kube.Host,
  407. "KUBERNETES_SERVICE_PORT_HTTPS": kube.Port,
  408. "TS_AUTH_ONCE": "true",
  409. },
  410. KubeSecret: map[string]string{
  411. "authkey": "tskey-key",
  412. },
  413. Phases: []phase{
  414. {
  415. WantCmds: []string{
  416. "/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=kube:tailscale --statedir=/tmp --tun=userspace-networking",
  417. },
  418. WantKubeSecret: map[string]string{
  419. "authkey": "tskey-key",
  420. },
  421. },
  422. {
  423. Notify: &ipn.Notify{
  424. State: ptr.To(ipn.NeedsLogin),
  425. },
  426. WantCmds: []string{
  427. "/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --authkey=tskey-key",
  428. },
  429. WantKubeSecret: map[string]string{
  430. "authkey": "tskey-key",
  431. },
  432. },
  433. {
  434. Notify: runningNotify,
  435. WantKubeSecret: map[string]string{
  436. "device_fqdn": "test-node.test.ts.net",
  437. "device_id": "myID",
  438. },
  439. },
  440. },
  441. },
  442. {
  443. Name: "kube_storage_updates",
  444. Env: map[string]string{
  445. "KUBERNETES_SERVICE_HOST": kube.Host,
  446. "KUBERNETES_SERVICE_PORT_HTTPS": kube.Port,
  447. },
  448. KubeSecret: map[string]string{
  449. "authkey": "tskey-key",
  450. },
  451. Phases: []phase{
  452. {
  453. WantCmds: []string{
  454. "/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=kube:tailscale --statedir=/tmp --tun=userspace-networking",
  455. "/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --authkey=tskey-key",
  456. },
  457. WantKubeSecret: map[string]string{
  458. "authkey": "tskey-key",
  459. },
  460. },
  461. {
  462. Notify: runningNotify,
  463. WantKubeSecret: map[string]string{
  464. "authkey": "tskey-key",
  465. "device_fqdn": "test-node.test.ts.net",
  466. "device_id": "myID",
  467. },
  468. },
  469. {
  470. Notify: &ipn.Notify{
  471. State: ptr.To(ipn.Running),
  472. NetMap: &netmap.NetworkMap{
  473. SelfNode: &tailcfg.Node{
  474. StableID: tailcfg.StableNodeID("newID"),
  475. Name: "new-name.test.ts.net",
  476. },
  477. Addresses: []netip.Prefix{netip.MustParsePrefix("100.64.0.1/32")},
  478. },
  479. },
  480. WantKubeSecret: map[string]string{
  481. "authkey": "tskey-key",
  482. "device_fqdn": "new-name.test.ts.net",
  483. "device_id": "newID",
  484. },
  485. },
  486. },
  487. },
  488. {
  489. Name: "proxies",
  490. Env: map[string]string{
  491. "TS_SOCKS5_SERVER": "localhost:1080",
  492. "TS_OUTBOUND_HTTP_PROXY_LISTEN": "localhost:8080",
  493. },
  494. Phases: []phase{
  495. {
  496. WantCmds: []string{
  497. "/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=mem: --statedir=/tmp --tun=userspace-networking --socks5-server=localhost:1080 --outbound-http-proxy-listen=localhost:8080",
  498. "/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false",
  499. },
  500. },
  501. {
  502. Notify: runningNotify,
  503. },
  504. },
  505. },
  506. {
  507. Name: "dns",
  508. Env: map[string]string{
  509. "TS_ACCEPT_DNS": "true",
  510. },
  511. Phases: []phase{
  512. {
  513. WantCmds: []string{
  514. "/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=mem: --statedir=/tmp --tun=userspace-networking",
  515. "/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=true",
  516. },
  517. },
  518. {
  519. Notify: runningNotify,
  520. },
  521. },
  522. },
  523. {
  524. Name: "extra_args",
  525. Env: map[string]string{
  526. "TS_EXTRA_ARGS": "--widget=rotated",
  527. "TS_TAILSCALED_EXTRA_ARGS": "--experiments=widgets",
  528. },
  529. Phases: []phase{
  530. {
  531. WantCmds: []string{
  532. "/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=mem: --statedir=/tmp --tun=userspace-networking --experiments=widgets",
  533. "/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --widget=rotated",
  534. },
  535. }, {
  536. Notify: runningNotify,
  537. },
  538. },
  539. },
  540. {
  541. Name: "hostname",
  542. Env: map[string]string{
  543. "TS_HOSTNAME": "my-server",
  544. },
  545. Phases: []phase{
  546. {
  547. WantCmds: []string{
  548. "/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=mem: --statedir=/tmp --tun=userspace-networking",
  549. "/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --hostname=my-server",
  550. },
  551. }, {
  552. Notify: runningNotify,
  553. },
  554. },
  555. },
  556. }
  557. for _, test := range tests {
  558. t.Run(test.Name, func(t *testing.T) {
  559. lapi.Reset()
  560. kube.Reset()
  561. os.Remove(argFile)
  562. os.Remove(runningSockPath)
  563. resetFiles()
  564. for k, v := range test.KubeSecret {
  565. kube.SetSecret(k, v)
  566. }
  567. kube.SetPatching(!test.KubeDenyPatch)
  568. cmd := exec.Command(boot)
  569. cmd.Env = []string{
  570. fmt.Sprintf("PATH=%s/usr/bin:%s", d, os.Getenv("PATH")),
  571. fmt.Sprintf("TS_TEST_RECORD_ARGS=%s", argFile),
  572. fmt.Sprintf("TS_TEST_SOCKET=%s", lapi.Path),
  573. fmt.Sprintf("TS_SOCKET=%s", runningSockPath),
  574. fmt.Sprintf("TS_TEST_ONLY_ROOT=%s", d),
  575. }
  576. for k, v := range test.Env {
  577. cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%s", k, v))
  578. }
  579. cbOut := &lockingBuffer{}
  580. defer func() {
  581. if t.Failed() {
  582. t.Logf("containerboot output:\n%s", cbOut.String())
  583. }
  584. }()
  585. cmd.Stderr = cbOut
  586. if err := cmd.Start(); err != nil {
  587. t.Fatalf("starting containerboot: %v", err)
  588. }
  589. defer func() {
  590. cmd.Process.Signal(unix.SIGTERM)
  591. cmd.Process.Wait()
  592. }()
  593. var wantCmds []string
  594. for i, p := range test.Phases {
  595. lapi.Notify(p.Notify)
  596. wantCmds = append(wantCmds, p.WantCmds...)
  597. waitArgs(t, 2*time.Second, d, argFile, strings.Join(wantCmds, "\n"))
  598. err := tstest.WaitFor(2*time.Second, func() error {
  599. if p.WantKubeSecret != nil {
  600. got := kube.Secret()
  601. if diff := cmp.Diff(got, p.WantKubeSecret); diff != "" {
  602. return fmt.Errorf("unexpected kube secret data (-got+want):\n%s", diff)
  603. }
  604. } else {
  605. got := kube.Secret()
  606. if len(got) > 0 {
  607. return fmt.Errorf("kube secret unexpectedly not empty, got %#v", got)
  608. }
  609. }
  610. return nil
  611. })
  612. if err != nil {
  613. t.Fatalf("phase %d: %v", i, err)
  614. }
  615. err = tstest.WaitFor(2*time.Second, func() error {
  616. for path, want := range p.WantFiles {
  617. gotBs, err := os.ReadFile(filepath.Join(d, path))
  618. if err != nil {
  619. return fmt.Errorf("reading wanted file %q: %v", path, err)
  620. }
  621. if got := strings.TrimSpace(string(gotBs)); got != want {
  622. return fmt.Errorf("wrong file contents for %q, got %q want %q", path, got, want)
  623. }
  624. }
  625. return nil
  626. })
  627. if err != nil {
  628. t.Fatal(err)
  629. }
  630. }
  631. waitLogLine(t, 2*time.Second, cbOut, "Startup complete, waiting for shutdown signal")
  632. })
  633. }
  634. }
  635. type lockingBuffer struct {
  636. sync.Mutex
  637. b bytes.Buffer
  638. }
  639. func (b *lockingBuffer) Write(bs []byte) (int, error) {
  640. b.Lock()
  641. defer b.Unlock()
  642. return b.b.Write(bs)
  643. }
  644. func (b *lockingBuffer) String() string {
  645. b.Lock()
  646. defer b.Unlock()
  647. return b.b.String()
  648. }
  649. // waitLogLine looks for want in the contents of b.
  650. //
  651. // Only lines starting with 'boot: ' (the output of containerboot
  652. // itself) are considered, and the logged timestamp is ignored.
  653. //
  654. // waitLogLine fails the entire test if path doesn't contain want
  655. // before the timeout.
  656. func waitLogLine(t *testing.T, timeout time.Duration, b *lockingBuffer, want string) {
  657. deadline := time.Now().Add(timeout)
  658. for time.Now().Before(deadline) {
  659. for _, line := range strings.Split(b.String(), "\n") {
  660. if !strings.HasPrefix(line, "boot: ") {
  661. continue
  662. }
  663. if strings.HasSuffix(line, " "+want) {
  664. return
  665. }
  666. }
  667. time.Sleep(100 * time.Millisecond)
  668. }
  669. t.Fatalf("timed out waiting for wanted output line %q. Output:\n%s", want, b.String())
  670. }
  671. // waitArgs waits until the contents of path matches wantArgs, a set
  672. // of command lines recorded by test_tailscale.sh and
  673. // test_tailscaled.sh.
  674. //
  675. // All occurrences of removeStr are removed from the file prior to
  676. // comparison. This is used to remove the varying temporary root
  677. // directory name from recorded commandlines, so that wantArgs can be
  678. // a constant value.
  679. //
  680. // waitArgs fails the entire test if path doesn't contain wantArgs
  681. // before the timeout.
  682. func waitArgs(t *testing.T, timeout time.Duration, removeStr, path, wantArgs string) {
  683. t.Helper()
  684. wantArgs = strings.TrimSpace(wantArgs)
  685. deadline := time.Now().Add(timeout)
  686. var got string
  687. for time.Now().Before(deadline) {
  688. bs, err := os.ReadFile(path)
  689. if errors.Is(err, fs.ErrNotExist) {
  690. // Don't bother logging that the file doesn't exist, it
  691. // should start existing soon.
  692. goto loop
  693. } else if err != nil {
  694. t.Logf("reading %q: %v", path, err)
  695. goto loop
  696. }
  697. got = strings.TrimSpace(string(bs))
  698. got = strings.ReplaceAll(got, removeStr, "")
  699. if got == wantArgs {
  700. return
  701. }
  702. loop:
  703. time.Sleep(100 * time.Millisecond)
  704. }
  705. t.Fatalf("waiting for args file %q to have expected output, got:\n%s\n\nWant: %s", path, got, wantArgs)
  706. }
  707. //go:embed test_tailscaled.sh
  708. var fakeTailscaled []byte
  709. //go:embed test_tailscale.sh
  710. var fakeTailscale []byte
  711. // localAPI is a minimal fake tailscaled LocalAPI server that presents
  712. // just enough functionality for containerboot to function
  713. // correctly. In practice this means it only supports querying
  714. // tailscaled status, and panics on all other uses to make it very
  715. // obvious that something unexpected happened.
  716. type localAPI struct {
  717. FSRoot string
  718. Path string // populated by Start
  719. srv *http.Server
  720. sync.Mutex
  721. cond *sync.Cond
  722. notify *ipn.Notify
  723. }
  724. func (l *localAPI) Start() error {
  725. path := filepath.Join(l.FSRoot, "tmp/tailscaled.sock.fake")
  726. if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil {
  727. return err
  728. }
  729. ln, err := net.Listen("unix", path)
  730. if err != nil {
  731. return err
  732. }
  733. l.srv = &http.Server{
  734. Handler: l,
  735. }
  736. l.Path = path
  737. l.cond = sync.NewCond(&l.Mutex)
  738. go l.srv.Serve(ln)
  739. return nil
  740. }
  741. func (l *localAPI) Close() {
  742. l.srv.Close()
  743. }
  744. func (l *localAPI) Reset() {
  745. l.Lock()
  746. defer l.Unlock()
  747. l.notify = nil
  748. l.cond.Broadcast()
  749. }
  750. func (l *localAPI) Notify(n *ipn.Notify) {
  751. if n == nil {
  752. return
  753. }
  754. l.Lock()
  755. defer l.Unlock()
  756. l.notify = n
  757. l.cond.Broadcast()
  758. }
  759. func (l *localAPI) ServeHTTP(w http.ResponseWriter, r *http.Request) {
  760. if r.Method != "GET" {
  761. panic(fmt.Sprintf("unsupported method %q", r.Method))
  762. }
  763. if r.URL.Path != "/localapi/v0/watch-ipn-bus" {
  764. panic(fmt.Sprintf("unsupported path %q", r.URL.Path))
  765. }
  766. w.Header().Set("Content-Type", "application/json")
  767. w.WriteHeader(http.StatusOK)
  768. if f, ok := w.(http.Flusher); ok {
  769. f.Flush()
  770. }
  771. enc := json.NewEncoder(w)
  772. l.Lock()
  773. defer l.Unlock()
  774. for {
  775. if l.notify != nil {
  776. if err := enc.Encode(l.notify); err != nil {
  777. // Usually broken pipe as the test client disconnects.
  778. return
  779. }
  780. if f, ok := w.(http.Flusher); ok {
  781. f.Flush()
  782. }
  783. }
  784. l.cond.Wait()
  785. }
  786. }
  787. // kubeServer is a minimal fake Kubernetes server that presents just
  788. // enough functionality for containerboot to function correctly. In
  789. // practice this means it only supports reading and modifying a single
  790. // kube secret, and panics on all other uses to make it very obvious
  791. // that something unexpected happened.
  792. type kubeServer struct {
  793. FSRoot string
  794. Host, Port string // populated by Start
  795. srv *httptest.Server
  796. sync.Mutex
  797. secret map[string]string
  798. canPatch bool
  799. }
  800. func (k *kubeServer) Secret() map[string]string {
  801. k.Lock()
  802. defer k.Unlock()
  803. ret := map[string]string{}
  804. for k, v := range k.secret {
  805. ret[k] = v
  806. }
  807. return ret
  808. }
  809. func (k *kubeServer) SetSecret(key, val string) {
  810. k.Lock()
  811. defer k.Unlock()
  812. k.secret[key] = val
  813. }
  814. func (k *kubeServer) SetPatching(canPatch bool) {
  815. k.Lock()
  816. defer k.Unlock()
  817. k.canPatch = canPatch
  818. }
  819. func (k *kubeServer) Reset() {
  820. k.Lock()
  821. defer k.Unlock()
  822. k.secret = map[string]string{}
  823. }
  824. func (k *kubeServer) Start() error {
  825. root := filepath.Join(k.FSRoot, "var/run/secrets/kubernetes.io/serviceaccount")
  826. if err := os.MkdirAll(root, 0700); err != nil {
  827. return err
  828. }
  829. if err := os.WriteFile(filepath.Join(root, "namespace"), []byte("default"), 0600); err != nil {
  830. return err
  831. }
  832. if err := os.WriteFile(filepath.Join(root, "token"), []byte("bearer_token"), 0600); err != nil {
  833. return err
  834. }
  835. k.srv = httptest.NewTLSServer(k)
  836. k.Host = k.srv.Listener.Addr().(*net.TCPAddr).IP.String()
  837. k.Port = strconv.Itoa(k.srv.Listener.Addr().(*net.TCPAddr).Port)
  838. var cert bytes.Buffer
  839. if err := pem.Encode(&cert, &pem.Block{Type: "CERTIFICATE", Bytes: k.srv.Certificate().Raw}); err != nil {
  840. return err
  841. }
  842. if err := os.WriteFile(filepath.Join(root, "ca.crt"), cert.Bytes(), 0600); err != nil {
  843. return err
  844. }
  845. return nil
  846. }
  847. func (k *kubeServer) Close() {
  848. k.srv.Close()
  849. }
  850. func (k *kubeServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
  851. if r.Header.Get("Authorization") != "Bearer bearer_token" {
  852. panic("client didn't provide bearer token in request")
  853. }
  854. switch r.URL.Path {
  855. case "/api/v1/namespaces/default/secrets/tailscale":
  856. k.serveSecret(w, r)
  857. case "/apis/authorization.k8s.io/v1/selfsubjectaccessreviews":
  858. k.serveSSAR(w, r)
  859. default:
  860. panic(fmt.Sprintf("unhandled fake kube api path %q", r.URL.Path))
  861. }
  862. }
  863. func (k *kubeServer) serveSSAR(w http.ResponseWriter, r *http.Request) {
  864. var req struct {
  865. Spec struct {
  866. ResourceAttributes struct {
  867. Verb string `json:"verb"`
  868. } `json:"resourceAttributes"`
  869. } `json:"spec"`
  870. }
  871. if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
  872. panic(fmt.Sprintf("decoding SSAR request: %v", err))
  873. }
  874. ok := true
  875. if req.Spec.ResourceAttributes.Verb == "patch" {
  876. k.Lock()
  877. defer k.Unlock()
  878. ok = k.canPatch
  879. }
  880. // Just say yes to all SARs, we don't enforce RBAC.
  881. w.Header().Set("Content-Type", "application/json")
  882. fmt.Fprintf(w, `{"status":{"allowed":%v}}`, ok)
  883. }
  884. func (k *kubeServer) serveSecret(w http.ResponseWriter, r *http.Request) {
  885. bs, err := io.ReadAll(r.Body)
  886. if err != nil {
  887. http.Error(w, fmt.Sprintf("reading request body: %v", err), http.StatusInternalServerError)
  888. return
  889. }
  890. switch r.Method {
  891. case "GET":
  892. w.Header().Set("Content-Type", "application/json")
  893. ret := map[string]map[string]string{
  894. "data": {},
  895. }
  896. k.Lock()
  897. defer k.Unlock()
  898. for k, v := range k.secret {
  899. v := base64.StdEncoding.EncodeToString([]byte(v))
  900. if err != nil {
  901. panic("encode failed")
  902. }
  903. ret["data"][k] = v
  904. }
  905. if err := json.NewEncoder(w).Encode(ret); err != nil {
  906. panic("encode failed")
  907. }
  908. case "PATCH":
  909. k.Lock()
  910. defer k.Unlock()
  911. if !k.canPatch {
  912. panic("containerboot tried to patch despite not being allowed")
  913. }
  914. switch r.Header.Get("Content-Type") {
  915. case "application/json-patch+json":
  916. req := []struct {
  917. Op string `json:"op"`
  918. Path string `json:"path"`
  919. }{}
  920. if err := json.Unmarshal(bs, &req); err != nil {
  921. panic(fmt.Sprintf("json decode failed: %v. Body:\n\n%s", err, string(bs)))
  922. }
  923. for _, op := range req {
  924. if op.Op != "remove" {
  925. panic(fmt.Sprintf("unsupported json-patch op %q", op.Op))
  926. }
  927. if !strings.HasPrefix(op.Path, "/data/") {
  928. panic(fmt.Sprintf("unsupported json-patch path %q", op.Path))
  929. }
  930. delete(k.secret, strings.TrimPrefix(op.Path, "/data/"))
  931. }
  932. case "application/strategic-merge-patch+json":
  933. req := struct {
  934. Data map[string][]byte `json:"data"`
  935. }{}
  936. if err := json.Unmarshal(bs, &req); err != nil {
  937. panic(fmt.Sprintf("json decode failed: %v. Body:\n\n%s", err, string(bs)))
  938. }
  939. for key, val := range req.Data {
  940. k.secret[key] = string(val)
  941. }
  942. default:
  943. panic(fmt.Sprintf("unknown content type %q", r.Header.Get("Content-Type")))
  944. }
  945. default:
  946. panic(fmt.Sprintf("unhandled HTTP method %q", r.Method))
  947. }
  948. }