main_test.go 31 KB

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