main_test.go 43 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564
  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. "syscall"
  26. "testing"
  27. "time"
  28. "github.com/google/go-cmp/cmp"
  29. "golang.org/x/sys/unix"
  30. "tailscale.com/ipn"
  31. "tailscale.com/kube/egressservices"
  32. "tailscale.com/kube/kubeclient"
  33. "tailscale.com/kube/kubetypes"
  34. "tailscale.com/tailcfg"
  35. "tailscale.com/tstest"
  36. "tailscale.com/types/netmap"
  37. "tailscale.com/types/ptr"
  38. )
  39. func TestContainerBoot(t *testing.T) {
  40. d := t.TempDir()
  41. lapi := localAPI{FSRoot: d}
  42. if err := lapi.Start(); err != nil {
  43. t.Fatal(err)
  44. }
  45. defer lapi.Close()
  46. kube := kubeServer{FSRoot: d}
  47. kube.Start(t)
  48. defer kube.Close()
  49. tailscaledConf := &ipn.ConfigVAlpha{AuthKey: ptr.To("foo"), Version: "alpha0"}
  50. serveConf := ipn.ServeConfig{TCP: map[uint16]*ipn.TCPPortHandler{80: {HTTP: true}}}
  51. egressCfg := egressSvcConfig("foo", "foo.tailnetxyz.ts.net")
  52. egressStatus := egressSvcStatus("foo", "foo.tailnetxyz.ts.net")
  53. dirs := []string{
  54. "var/lib",
  55. "usr/bin",
  56. "tmp",
  57. "dev/net",
  58. "proc/sys/net/ipv4",
  59. "proc/sys/net/ipv6/conf/all",
  60. "etc/tailscaled",
  61. }
  62. for _, path := range dirs {
  63. if err := os.MkdirAll(filepath.Join(d, path), 0700); err != nil {
  64. t.Fatal(err)
  65. }
  66. }
  67. files := map[string][]byte{
  68. "usr/bin/tailscaled": fakeTailscaled,
  69. "usr/bin/tailscale": fakeTailscale,
  70. "usr/bin/iptables": fakeTailscale,
  71. "usr/bin/ip6tables": fakeTailscale,
  72. "dev/net/tun": []byte(""),
  73. "proc/sys/net/ipv4/ip_forward": []byte("0"),
  74. "proc/sys/net/ipv6/conf/all/forwarding": []byte("0"),
  75. "etc/tailscaled/cap-95.hujson": mustJSON(t, tailscaledConf),
  76. "etc/tailscaled/serve-config.json": mustJSON(t, serveConf),
  77. filepath.Join("etc/tailscaled/", egressservices.KeyEgressServices): mustJSON(t, egressCfg),
  78. filepath.Join("etc/tailscaled/", egressservices.KeyHEPPings): []byte("4"),
  79. }
  80. resetFiles := func() {
  81. for path, content := range files {
  82. // Making everything executable is a little weird, but the
  83. // stuff that doesn't need to be executable doesn't care if we
  84. // do make it executable.
  85. if err := os.WriteFile(filepath.Join(d, path), content, 0700); err != nil {
  86. t.Fatal(err)
  87. }
  88. }
  89. }
  90. resetFiles()
  91. boot := filepath.Join(d, "containerboot")
  92. if err := exec.Command("go", "build", "-o", boot, "tailscale.com/cmd/containerboot").Run(); err != nil {
  93. t.Fatalf("Building containerboot: %v", err)
  94. }
  95. argFile := filepath.Join(d, "args")
  96. runningSockPath := filepath.Join(d, "tmp/tailscaled.sock")
  97. var localAddrPort, healthAddrPort int
  98. for _, p := range []*int{&localAddrPort, &healthAddrPort} {
  99. ln, err := net.Listen("tcp", ":0")
  100. if err != nil {
  101. t.Fatalf("Failed to open listener: %v", err)
  102. }
  103. if err := ln.Close(); err != nil {
  104. t.Fatalf("Failed to close listener: %v", err)
  105. }
  106. port := ln.Addr().(*net.TCPAddr).Port
  107. *p = port
  108. }
  109. metricsURL := func(port int) string {
  110. return fmt.Sprintf("http://127.0.0.1:%d/metrics", port)
  111. }
  112. healthURL := func(port int) string {
  113. return fmt.Sprintf("http://127.0.0.1:%d/healthz", port)
  114. }
  115. egressSvcTerminateURL := func(port int) string {
  116. return fmt.Sprintf("http://127.0.0.1:%d%s", port, kubetypes.EgessServicesPreshutdownEP)
  117. }
  118. capver := fmt.Sprintf("%d", tailcfg.CurrentCapabilityVersion)
  119. type phase struct {
  120. // If non-nil, send this IPN bus notification (and remember it as the
  121. // initial update for any future new watchers, then wait for all the
  122. // Waits below to be true before proceeding to the next phase.
  123. Notify *ipn.Notify
  124. // WantCmds is the commands that containerboot should run in this phase.
  125. WantCmds []string
  126. // WantKubeSecret is the secret keys/values that should exist in the
  127. // kube secret.
  128. WantKubeSecret map[string]string
  129. // Update the kube secret with these keys/values at the beginning of the
  130. // phase (simulates our fake tailscaled doing it).
  131. UpdateKubeSecret map[string]string
  132. // WantFiles files that should exist in the container and their
  133. // contents.
  134. WantFiles map[string]string
  135. // WantLog is a log message we expect from containerboot.
  136. WantLog string
  137. // If set for a phase, the test will expect containerboot to exit with
  138. // this error code, and the test will finish on that phase without
  139. // waiting for the successful startup log message.
  140. WantExitCode *int
  141. // The signal to send to containerboot at the start of the phase.
  142. Signal *syscall.Signal
  143. EndpointStatuses map[string]int
  144. }
  145. runningNotify := &ipn.Notify{
  146. State: ptr.To(ipn.Running),
  147. NetMap: &netmap.NetworkMap{
  148. SelfNode: (&tailcfg.Node{
  149. StableID: tailcfg.StableNodeID("myID"),
  150. Name: "test-node.test.ts.net",
  151. Addresses: []netip.Prefix{netip.MustParsePrefix("100.64.0.1/32")},
  152. }).View(),
  153. },
  154. }
  155. tests := []struct {
  156. Name string
  157. Env map[string]string
  158. KubeSecret map[string]string
  159. KubeDenyPatch bool
  160. Phases []phase
  161. }{
  162. {
  163. // Out of the box default: runs in userspace mode, ephemeral storage, interactive login.
  164. Name: "no_args",
  165. Env: nil,
  166. Phases: []phase{
  167. {
  168. WantCmds: []string{
  169. "/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=mem: --statedir=/tmp --tun=userspace-networking",
  170. "/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false",
  171. },
  172. // No metrics or health by default.
  173. EndpointStatuses: map[string]int{
  174. metricsURL(9002): -1,
  175. healthURL(9002): -1,
  176. },
  177. },
  178. {
  179. Notify: runningNotify,
  180. },
  181. },
  182. },
  183. {
  184. // Userspace mode, ephemeral storage, authkey provided on every run.
  185. Name: "authkey",
  186. Env: map[string]string{
  187. "TS_AUTHKEY": "tskey-key",
  188. },
  189. Phases: []phase{
  190. {
  191. WantCmds: []string{
  192. "/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=mem: --statedir=/tmp --tun=userspace-networking",
  193. "/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --authkey=tskey-key",
  194. },
  195. },
  196. {
  197. Notify: runningNotify,
  198. },
  199. },
  200. },
  201. {
  202. // Userspace mode, ephemeral storage, authkey provided on every run.
  203. Name: "authkey-old-flag",
  204. Env: map[string]string{
  205. "TS_AUTH_KEY": "tskey-key",
  206. },
  207. Phases: []phase{
  208. {
  209. WantCmds: []string{
  210. "/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=mem: --statedir=/tmp --tun=userspace-networking",
  211. "/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --authkey=tskey-key",
  212. },
  213. },
  214. {
  215. Notify: runningNotify,
  216. },
  217. },
  218. },
  219. {
  220. Name: "authkey_disk_state",
  221. Env: map[string]string{
  222. "TS_AUTHKEY": "tskey-key",
  223. "TS_STATE_DIR": filepath.Join(d, "tmp"),
  224. },
  225. Phases: []phase{
  226. {
  227. WantCmds: []string{
  228. "/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --statedir=/tmp --tun=userspace-networking",
  229. "/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --authkey=tskey-key",
  230. },
  231. },
  232. {
  233. Notify: runningNotify,
  234. },
  235. },
  236. },
  237. {
  238. Name: "routes",
  239. Env: map[string]string{
  240. "TS_AUTHKEY": "tskey-key",
  241. "TS_ROUTES": "1.2.3.0/24,10.20.30.0/24",
  242. },
  243. Phases: []phase{
  244. {
  245. WantCmds: []string{
  246. "/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=mem: --statedir=/tmp --tun=userspace-networking",
  247. "/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",
  248. },
  249. },
  250. {
  251. Notify: runningNotify,
  252. WantFiles: map[string]string{
  253. "proc/sys/net/ipv4/ip_forward": "0",
  254. "proc/sys/net/ipv6/conf/all/forwarding": "0",
  255. },
  256. },
  257. },
  258. },
  259. {
  260. Name: "empty routes",
  261. Env: map[string]string{
  262. "TS_AUTHKEY": "tskey-key",
  263. "TS_ROUTES": "",
  264. },
  265. Phases: []phase{
  266. {
  267. WantCmds: []string{
  268. "/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=mem: --statedir=/tmp --tun=userspace-networking",
  269. "/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --authkey=tskey-key --advertise-routes=",
  270. },
  271. },
  272. {
  273. Notify: runningNotify,
  274. WantFiles: map[string]string{
  275. "proc/sys/net/ipv4/ip_forward": "0",
  276. "proc/sys/net/ipv6/conf/all/forwarding": "0",
  277. },
  278. },
  279. },
  280. },
  281. {
  282. Name: "routes_kernel_ipv4",
  283. Env: map[string]string{
  284. "TS_AUTHKEY": "tskey-key",
  285. "TS_ROUTES": "1.2.3.0/24,10.20.30.0/24",
  286. "TS_USERSPACE": "false",
  287. },
  288. Phases: []phase{
  289. {
  290. WantCmds: []string{
  291. "/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=mem: --statedir=/tmp",
  292. "/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",
  293. },
  294. },
  295. {
  296. Notify: runningNotify,
  297. WantFiles: map[string]string{
  298. "proc/sys/net/ipv4/ip_forward": "1",
  299. "proc/sys/net/ipv6/conf/all/forwarding": "0",
  300. },
  301. },
  302. },
  303. },
  304. {
  305. Name: "routes_kernel_ipv6",
  306. Env: map[string]string{
  307. "TS_AUTHKEY": "tskey-key",
  308. "TS_ROUTES": "::/64,1::/64",
  309. "TS_USERSPACE": "false",
  310. },
  311. Phases: []phase{
  312. {
  313. WantCmds: []string{
  314. "/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=mem: --statedir=/tmp",
  315. "/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --authkey=tskey-key --advertise-routes=::/64,1::/64",
  316. },
  317. },
  318. {
  319. Notify: runningNotify,
  320. WantFiles: map[string]string{
  321. "proc/sys/net/ipv4/ip_forward": "0",
  322. "proc/sys/net/ipv6/conf/all/forwarding": "1",
  323. },
  324. },
  325. },
  326. },
  327. {
  328. Name: "routes_kernel_all_families",
  329. Env: map[string]string{
  330. "TS_AUTHKEY": "tskey-key",
  331. "TS_ROUTES": "::/64,1.2.3.0/24",
  332. "TS_USERSPACE": "false",
  333. },
  334. Phases: []phase{
  335. {
  336. WantCmds: []string{
  337. "/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=mem: --statedir=/tmp",
  338. "/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --authkey=tskey-key --advertise-routes=::/64,1.2.3.0/24",
  339. },
  340. },
  341. {
  342. Notify: runningNotify,
  343. WantFiles: map[string]string{
  344. "proc/sys/net/ipv4/ip_forward": "1",
  345. "proc/sys/net/ipv6/conf/all/forwarding": "1",
  346. },
  347. },
  348. },
  349. },
  350. {
  351. Name: "ingress proxy",
  352. Env: map[string]string{
  353. "TS_AUTHKEY": "tskey-key",
  354. "TS_DEST_IP": "1.2.3.4",
  355. "TS_USERSPACE": "false",
  356. },
  357. Phases: []phase{
  358. {
  359. WantCmds: []string{
  360. "/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=mem: --statedir=/tmp",
  361. "/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --authkey=tskey-key",
  362. },
  363. },
  364. {
  365. Notify: runningNotify,
  366. },
  367. },
  368. },
  369. {
  370. Name: "egress proxy",
  371. Env: map[string]string{
  372. "TS_AUTHKEY": "tskey-key",
  373. "TS_TAILNET_TARGET_IP": "100.99.99.99",
  374. "TS_USERSPACE": "false",
  375. },
  376. Phases: []phase{
  377. {
  378. WantCmds: []string{
  379. "/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=mem: --statedir=/tmp",
  380. "/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --authkey=tskey-key",
  381. },
  382. WantFiles: map[string]string{
  383. "proc/sys/net/ipv4/ip_forward": "1",
  384. "proc/sys/net/ipv6/conf/all/forwarding": "0",
  385. },
  386. },
  387. {
  388. Notify: runningNotify,
  389. },
  390. },
  391. },
  392. {
  393. Name: "egress_proxy_fqdn_ipv6_target_on_ipv4_host",
  394. Env: map[string]string{
  395. "TS_AUTHKEY": "tskey-key",
  396. "TS_TAILNET_TARGET_FQDN": "ipv6-node.test.ts.net", // resolves to IPv6 address
  397. "TS_USERSPACE": "false",
  398. "TS_TEST_FAKE_NETFILTER_6": "false",
  399. },
  400. Phases: []phase{
  401. {
  402. WantCmds: []string{
  403. "/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=mem: --statedir=/tmp",
  404. "/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --authkey=tskey-key",
  405. },
  406. WantFiles: map[string]string{
  407. "proc/sys/net/ipv4/ip_forward": "1",
  408. "proc/sys/net/ipv6/conf/all/forwarding": "0",
  409. },
  410. },
  411. {
  412. Notify: &ipn.Notify{
  413. State: ptr.To(ipn.Running),
  414. NetMap: &netmap.NetworkMap{
  415. SelfNode: (&tailcfg.Node{
  416. StableID: tailcfg.StableNodeID("myID"),
  417. Name: "test-node.test.ts.net",
  418. Addresses: []netip.Prefix{netip.MustParsePrefix("100.64.0.1/32")},
  419. }).View(),
  420. Peers: []tailcfg.NodeView{
  421. (&tailcfg.Node{
  422. StableID: tailcfg.StableNodeID("ipv6ID"),
  423. Name: "ipv6-node.test.ts.net",
  424. Addresses: []netip.Prefix{netip.MustParsePrefix("::1/128")},
  425. }).View(),
  426. },
  427. },
  428. },
  429. WantLog: "no forwarding rules for egress addresses [::1/128], host supports IPv6: false",
  430. WantExitCode: ptr.To(1),
  431. },
  432. },
  433. },
  434. {
  435. Name: "authkey_once",
  436. Env: map[string]string{
  437. "TS_AUTHKEY": "tskey-key",
  438. "TS_AUTH_ONCE": "true",
  439. },
  440. Phases: []phase{
  441. {
  442. WantCmds: []string{
  443. "/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=mem: --statedir=/tmp --tun=userspace-networking",
  444. },
  445. },
  446. {
  447. Notify: &ipn.Notify{
  448. State: ptr.To(ipn.NeedsLogin),
  449. },
  450. WantCmds: []string{
  451. "/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --authkey=tskey-key",
  452. },
  453. },
  454. {
  455. Notify: runningNotify,
  456. WantCmds: []string{
  457. "/usr/bin/tailscale --socket=/tmp/tailscaled.sock set --accept-dns=false",
  458. },
  459. },
  460. },
  461. },
  462. {
  463. Name: "kube_storage",
  464. Env: map[string]string{
  465. "KUBERNETES_SERVICE_HOST": kube.Host,
  466. "KUBERNETES_SERVICE_PORT_HTTPS": kube.Port,
  467. },
  468. KubeSecret: map[string]string{
  469. "authkey": "tskey-key",
  470. },
  471. Phases: []phase{
  472. {
  473. WantCmds: []string{
  474. "/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=kube:tailscale --statedir=/tmp --tun=userspace-networking",
  475. "/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --authkey=tskey-key",
  476. },
  477. WantKubeSecret: map[string]string{
  478. "authkey": "tskey-key",
  479. },
  480. },
  481. {
  482. Notify: runningNotify,
  483. WantKubeSecret: map[string]string{
  484. "authkey": "tskey-key",
  485. "device_fqdn": "test-node.test.ts.net",
  486. "device_id": "myID",
  487. "device_ips": `["100.64.0.1"]`,
  488. "tailscale_capver": capver,
  489. },
  490. },
  491. },
  492. },
  493. {
  494. Name: "kube_disk_storage",
  495. Env: map[string]string{
  496. "KUBERNETES_SERVICE_HOST": kube.Host,
  497. "KUBERNETES_SERVICE_PORT_HTTPS": kube.Port,
  498. // Explicitly set to an empty value, to override the default of "tailscale".
  499. "TS_KUBE_SECRET": "",
  500. "TS_STATE_DIR": filepath.Join(d, "tmp"),
  501. "TS_AUTHKEY": "tskey-key",
  502. },
  503. KubeSecret: map[string]string{},
  504. Phases: []phase{
  505. {
  506. WantCmds: []string{
  507. "/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --statedir=/tmp --tun=userspace-networking",
  508. "/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --authkey=tskey-key",
  509. },
  510. WantKubeSecret: map[string]string{},
  511. },
  512. {
  513. Notify: runningNotify,
  514. WantKubeSecret: map[string]string{},
  515. },
  516. },
  517. },
  518. {
  519. Name: "kube_storage_no_patch",
  520. Env: map[string]string{
  521. "KUBERNETES_SERVICE_HOST": kube.Host,
  522. "KUBERNETES_SERVICE_PORT_HTTPS": kube.Port,
  523. "TS_AUTHKEY": "tskey-key",
  524. },
  525. KubeSecret: map[string]string{},
  526. KubeDenyPatch: true,
  527. Phases: []phase{
  528. {
  529. WantCmds: []string{
  530. "/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=kube:tailscale --statedir=/tmp --tun=userspace-networking",
  531. "/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --authkey=tskey-key",
  532. },
  533. WantKubeSecret: map[string]string{},
  534. },
  535. {
  536. Notify: runningNotify,
  537. WantKubeSecret: map[string]string{},
  538. },
  539. },
  540. },
  541. {
  542. // Same as previous, but deletes the authkey from the kube secret.
  543. Name: "kube_storage_auth_once",
  544. Env: map[string]string{
  545. "KUBERNETES_SERVICE_HOST": kube.Host,
  546. "KUBERNETES_SERVICE_PORT_HTTPS": kube.Port,
  547. "TS_AUTH_ONCE": "true",
  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. },
  557. WantKubeSecret: map[string]string{
  558. "authkey": "tskey-key",
  559. },
  560. },
  561. {
  562. Notify: &ipn.Notify{
  563. State: ptr.To(ipn.NeedsLogin),
  564. },
  565. WantCmds: []string{
  566. "/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --authkey=tskey-key",
  567. },
  568. WantKubeSecret: map[string]string{
  569. "authkey": "tskey-key",
  570. },
  571. },
  572. {
  573. Notify: runningNotify,
  574. WantCmds: []string{
  575. "/usr/bin/tailscale --socket=/tmp/tailscaled.sock set --accept-dns=false",
  576. },
  577. WantKubeSecret: map[string]string{
  578. "device_fqdn": "test-node.test.ts.net",
  579. "device_id": "myID",
  580. "device_ips": `["100.64.0.1"]`,
  581. "tailscale_capver": capver,
  582. },
  583. },
  584. },
  585. },
  586. {
  587. Name: "kube_storage_updates",
  588. Env: map[string]string{
  589. "KUBERNETES_SERVICE_HOST": kube.Host,
  590. "KUBERNETES_SERVICE_PORT_HTTPS": kube.Port,
  591. },
  592. KubeSecret: map[string]string{
  593. "authkey": "tskey-key",
  594. },
  595. Phases: []phase{
  596. {
  597. WantCmds: []string{
  598. "/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=kube:tailscale --statedir=/tmp --tun=userspace-networking",
  599. "/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --authkey=tskey-key",
  600. },
  601. WantKubeSecret: map[string]string{
  602. "authkey": "tskey-key",
  603. },
  604. },
  605. {
  606. Notify: runningNotify,
  607. WantKubeSecret: map[string]string{
  608. "authkey": "tskey-key",
  609. "device_fqdn": "test-node.test.ts.net",
  610. "device_id": "myID",
  611. "device_ips": `["100.64.0.1"]`,
  612. "tailscale_capver": capver,
  613. },
  614. },
  615. {
  616. Notify: &ipn.Notify{
  617. State: ptr.To(ipn.Running),
  618. NetMap: &netmap.NetworkMap{
  619. SelfNode: (&tailcfg.Node{
  620. StableID: tailcfg.StableNodeID("newID"),
  621. Name: "new-name.test.ts.net",
  622. Addresses: []netip.Prefix{netip.MustParsePrefix("100.64.0.1/32")},
  623. }).View(),
  624. },
  625. },
  626. WantKubeSecret: map[string]string{
  627. "authkey": "tskey-key",
  628. "device_fqdn": "new-name.test.ts.net",
  629. "device_id": "newID",
  630. "device_ips": `["100.64.0.1"]`,
  631. "tailscale_capver": capver,
  632. },
  633. },
  634. },
  635. },
  636. {
  637. Name: "proxies",
  638. Env: map[string]string{
  639. "TS_SOCKS5_SERVER": "localhost:1080",
  640. "TS_OUTBOUND_HTTP_PROXY_LISTEN": "localhost:8080",
  641. },
  642. Phases: []phase{
  643. {
  644. WantCmds: []string{
  645. "/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=mem: --statedir=/tmp --tun=userspace-networking --socks5-server=localhost:1080 --outbound-http-proxy-listen=localhost:8080",
  646. "/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false",
  647. },
  648. },
  649. {
  650. Notify: runningNotify,
  651. },
  652. },
  653. },
  654. {
  655. Name: "dns",
  656. Env: map[string]string{
  657. "TS_ACCEPT_DNS": "true",
  658. },
  659. Phases: []phase{
  660. {
  661. WantCmds: []string{
  662. "/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=mem: --statedir=/tmp --tun=userspace-networking",
  663. "/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=true",
  664. },
  665. },
  666. {
  667. Notify: runningNotify,
  668. },
  669. },
  670. },
  671. {
  672. Name: "extra_args",
  673. Env: map[string]string{
  674. "TS_EXTRA_ARGS": "--widget=rotated",
  675. "TS_TAILSCALED_EXTRA_ARGS": "--experiments=widgets",
  676. },
  677. Phases: []phase{
  678. {
  679. WantCmds: []string{
  680. "/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=mem: --statedir=/tmp --tun=userspace-networking --experiments=widgets",
  681. "/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --widget=rotated",
  682. },
  683. }, {
  684. Notify: runningNotify,
  685. },
  686. },
  687. },
  688. {
  689. Name: "extra_args_accept_routes",
  690. Env: map[string]string{
  691. "TS_EXTRA_ARGS": "--accept-routes",
  692. },
  693. Phases: []phase{
  694. {
  695. WantCmds: []string{
  696. "/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=mem: --statedir=/tmp --tun=userspace-networking",
  697. "/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --accept-routes",
  698. },
  699. }, {
  700. Notify: runningNotify,
  701. },
  702. },
  703. },
  704. {
  705. Name: "hostname",
  706. Env: map[string]string{
  707. "TS_HOSTNAME": "my-server",
  708. },
  709. Phases: []phase{
  710. {
  711. WantCmds: []string{
  712. "/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=mem: --statedir=/tmp --tun=userspace-networking",
  713. "/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --hostname=my-server",
  714. },
  715. }, {
  716. Notify: runningNotify,
  717. },
  718. },
  719. },
  720. {
  721. Name: "experimental tailscaled config path",
  722. Env: map[string]string{
  723. "TS_EXPERIMENTAL_VERSIONED_CONFIG_DIR": filepath.Join(d, "etc/tailscaled/"),
  724. },
  725. Phases: []phase{
  726. {
  727. WantCmds: []string{
  728. "/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=mem: --statedir=/tmp --tun=userspace-networking --config=/etc/tailscaled/cap-95.hujson",
  729. },
  730. }, {
  731. Notify: runningNotify,
  732. },
  733. },
  734. },
  735. {
  736. Name: "metrics_enabled",
  737. Env: map[string]string{
  738. "TS_LOCAL_ADDR_PORT": fmt.Sprintf("[::]:%d", localAddrPort),
  739. "TS_ENABLE_METRICS": "true",
  740. },
  741. Phases: []phase{
  742. {
  743. WantCmds: []string{
  744. "/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=mem: --statedir=/tmp --tun=userspace-networking",
  745. "/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false",
  746. },
  747. EndpointStatuses: map[string]int{
  748. metricsURL(localAddrPort): 200,
  749. healthURL(localAddrPort): -1,
  750. },
  751. }, {
  752. Notify: runningNotify,
  753. },
  754. },
  755. },
  756. {
  757. Name: "health_enabled",
  758. Env: map[string]string{
  759. "TS_LOCAL_ADDR_PORT": fmt.Sprintf("[::]:%d", localAddrPort),
  760. "TS_ENABLE_HEALTH_CHECK": "true",
  761. },
  762. Phases: []phase{
  763. {
  764. WantCmds: []string{
  765. "/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=mem: --statedir=/tmp --tun=userspace-networking",
  766. "/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false",
  767. },
  768. EndpointStatuses: map[string]int{
  769. metricsURL(localAddrPort): -1,
  770. healthURL(localAddrPort): 503, // Doesn't start passing until the next phase.
  771. },
  772. }, {
  773. Notify: runningNotify,
  774. EndpointStatuses: map[string]int{
  775. metricsURL(localAddrPort): -1,
  776. healthURL(localAddrPort): 200,
  777. },
  778. },
  779. },
  780. },
  781. {
  782. Name: "metrics_and_health_on_same_port",
  783. Env: map[string]string{
  784. "TS_LOCAL_ADDR_PORT": fmt.Sprintf("[::]:%d", localAddrPort),
  785. "TS_ENABLE_METRICS": "true",
  786. "TS_ENABLE_HEALTH_CHECK": "true",
  787. },
  788. Phases: []phase{
  789. {
  790. WantCmds: []string{
  791. "/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=mem: --statedir=/tmp --tun=userspace-networking",
  792. "/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false",
  793. },
  794. EndpointStatuses: map[string]int{
  795. metricsURL(localAddrPort): 200,
  796. healthURL(localAddrPort): 503, // Doesn't start passing until the next phase.
  797. },
  798. }, {
  799. Notify: runningNotify,
  800. EndpointStatuses: map[string]int{
  801. metricsURL(localAddrPort): 200,
  802. healthURL(localAddrPort): 200,
  803. },
  804. },
  805. },
  806. },
  807. {
  808. Name: "local_metrics_and_deprecated_health",
  809. Env: map[string]string{
  810. "TS_LOCAL_ADDR_PORT": fmt.Sprintf("[::]:%d", localAddrPort),
  811. "TS_ENABLE_METRICS": "true",
  812. "TS_HEALTHCHECK_ADDR_PORT": fmt.Sprintf("[::]:%d", healthAddrPort),
  813. },
  814. Phases: []phase{
  815. {
  816. WantCmds: []string{
  817. "/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=mem: --statedir=/tmp --tun=userspace-networking",
  818. "/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false",
  819. },
  820. EndpointStatuses: map[string]int{
  821. metricsURL(localAddrPort): 200,
  822. healthURL(healthAddrPort): 503, // Doesn't start passing until the next phase.
  823. },
  824. }, {
  825. Notify: runningNotify,
  826. EndpointStatuses: map[string]int{
  827. metricsURL(localAddrPort): 200,
  828. healthURL(healthAddrPort): 200,
  829. },
  830. },
  831. },
  832. },
  833. {
  834. Name: "serve_config_no_kube",
  835. Env: map[string]string{
  836. "TS_SERVE_CONFIG": filepath.Join(d, "etc/tailscaled/serve-config.json"),
  837. "TS_AUTHKEY": "tskey-key",
  838. },
  839. Phases: []phase{
  840. {
  841. WantCmds: []string{
  842. "/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=mem: --statedir=/tmp --tun=userspace-networking",
  843. "/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --authkey=tskey-key",
  844. },
  845. },
  846. {
  847. Notify: runningNotify,
  848. },
  849. },
  850. },
  851. {
  852. Name: "serve_config_kube",
  853. Env: map[string]string{
  854. "KUBERNETES_SERVICE_HOST": kube.Host,
  855. "KUBERNETES_SERVICE_PORT_HTTPS": kube.Port,
  856. "TS_SERVE_CONFIG": filepath.Join(d, "etc/tailscaled/serve-config.json"),
  857. },
  858. KubeSecret: map[string]string{
  859. "authkey": "tskey-key",
  860. },
  861. Phases: []phase{
  862. {
  863. WantCmds: []string{
  864. "/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=kube:tailscale --statedir=/tmp --tun=userspace-networking",
  865. "/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --authkey=tskey-key",
  866. },
  867. WantKubeSecret: map[string]string{
  868. "authkey": "tskey-key",
  869. },
  870. },
  871. {
  872. Notify: runningNotify,
  873. WantKubeSecret: map[string]string{
  874. "authkey": "tskey-key",
  875. "device_fqdn": "test-node.test.ts.net",
  876. "device_id": "myID",
  877. "device_ips": `["100.64.0.1"]`,
  878. "https_endpoint": "no-https",
  879. "tailscale_capver": capver,
  880. },
  881. },
  882. },
  883. },
  884. {
  885. Name: "egress_svcs_config_kube",
  886. Env: map[string]string{
  887. "KUBERNETES_SERVICE_HOST": kube.Host,
  888. "KUBERNETES_SERVICE_PORT_HTTPS": kube.Port,
  889. "TS_EGRESS_PROXIES_CONFIG_PATH": filepath.Join(d, "etc/tailscaled"),
  890. "TS_LOCAL_ADDR_PORT": fmt.Sprintf("[::]:%d", localAddrPort),
  891. },
  892. KubeSecret: map[string]string{
  893. "authkey": "tskey-key",
  894. },
  895. Phases: []phase{
  896. {
  897. WantCmds: []string{
  898. "/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=kube:tailscale --statedir=/tmp --tun=userspace-networking",
  899. "/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --authkey=tskey-key",
  900. },
  901. WantKubeSecret: map[string]string{
  902. "authkey": "tskey-key",
  903. },
  904. EndpointStatuses: map[string]int{
  905. egressSvcTerminateURL(localAddrPort): 200,
  906. },
  907. },
  908. {
  909. Notify: runningNotify,
  910. WantKubeSecret: map[string]string{
  911. "egress-services": mustBase64(t, egressStatus),
  912. "authkey": "tskey-key",
  913. "device_fqdn": "test-node.test.ts.net",
  914. "device_id": "myID",
  915. "device_ips": `["100.64.0.1"]`,
  916. "tailscale_capver": capver,
  917. },
  918. EndpointStatuses: map[string]int{
  919. egressSvcTerminateURL(localAddrPort): 200,
  920. },
  921. },
  922. },
  923. },
  924. {
  925. Name: "egress_svcs_config_no_kube",
  926. Env: map[string]string{
  927. "TS_EGRESS_PROXIES_CONFIG_PATH": filepath.Join(d, "etc/tailscaled"),
  928. "TS_AUTHKEY": "tskey-key",
  929. },
  930. Phases: []phase{
  931. {
  932. WantLog: "TS_EGRESS_PROXIES_CONFIG_PATH is only supported for Tailscale running on Kubernetes",
  933. WantExitCode: ptr.To(1),
  934. },
  935. },
  936. },
  937. {
  938. Name: "kube_shutdown_during_state_write",
  939. Env: map[string]string{
  940. "KUBERNETES_SERVICE_HOST": kube.Host,
  941. "KUBERNETES_SERVICE_PORT_HTTPS": kube.Port,
  942. "TS_ENABLE_HEALTH_CHECK": "true",
  943. },
  944. KubeSecret: map[string]string{
  945. "authkey": "tskey-key",
  946. },
  947. Phases: []phase{
  948. {
  949. // Normal startup.
  950. WantCmds: []string{
  951. "/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=kube:tailscale --statedir=/tmp --tun=userspace-networking",
  952. "/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --authkey=tskey-key",
  953. },
  954. WantKubeSecret: map[string]string{
  955. "authkey": "tskey-key",
  956. },
  957. },
  958. {
  959. // SIGTERM before state is finished writing, should wait for
  960. // consistent state before propagating SIGTERM to tailscaled.
  961. Signal: ptr.To(unix.SIGTERM),
  962. UpdateKubeSecret: map[string]string{
  963. "_machinekey": "foo",
  964. "_profiles": "foo",
  965. "profile-baff": "foo",
  966. // Missing "_current-profile" key.
  967. },
  968. WantKubeSecret: map[string]string{
  969. "authkey": "tskey-key",
  970. "_machinekey": "foo",
  971. "_profiles": "foo",
  972. "profile-baff": "foo",
  973. },
  974. WantLog: "Waiting for tailscaled to finish writing state to Secret \"tailscale\"",
  975. },
  976. {
  977. // tailscaled has finished writing state, should propagate SIGTERM.
  978. UpdateKubeSecret: map[string]string{
  979. "_current-profile": "foo",
  980. },
  981. WantKubeSecret: map[string]string{
  982. "authkey": "tskey-key",
  983. "_machinekey": "foo",
  984. "_profiles": "foo",
  985. "profile-baff": "foo",
  986. "_current-profile": "foo",
  987. },
  988. WantLog: "HTTP server at [::]:9002 closed",
  989. WantExitCode: ptr.To(0),
  990. },
  991. },
  992. },
  993. }
  994. for _, test := range tests {
  995. t.Run(test.Name, func(t *testing.T) {
  996. lapi.Reset()
  997. kube.Reset()
  998. os.Remove(argFile)
  999. os.Remove(runningSockPath)
  1000. resetFiles()
  1001. for k, v := range test.KubeSecret {
  1002. kube.SetSecret(k, v)
  1003. }
  1004. kube.SetPatching(!test.KubeDenyPatch)
  1005. cmd := exec.Command(boot)
  1006. cmd.Env = []string{
  1007. fmt.Sprintf("PATH=%s/usr/bin:%s", d, os.Getenv("PATH")),
  1008. fmt.Sprintf("TS_TEST_RECORD_ARGS=%s", argFile),
  1009. fmt.Sprintf("TS_TEST_SOCKET=%s", lapi.Path),
  1010. fmt.Sprintf("TS_SOCKET=%s", runningSockPath),
  1011. fmt.Sprintf("TS_TEST_ONLY_ROOT=%s", d),
  1012. fmt.Sprint("TS_TEST_FAKE_NETFILTER=true"),
  1013. }
  1014. for k, v := range test.Env {
  1015. cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%s", k, v))
  1016. }
  1017. cbOut := &lockingBuffer{}
  1018. defer func() {
  1019. if t.Failed() {
  1020. t.Logf("containerboot output:\n%s", cbOut.String())
  1021. }
  1022. }()
  1023. cmd.Stderr = cbOut
  1024. if err := cmd.Start(); err != nil {
  1025. t.Fatalf("starting containerboot: %v", err)
  1026. }
  1027. defer func() {
  1028. cmd.Process.Signal(unix.SIGTERM)
  1029. cmd.Process.Wait()
  1030. }()
  1031. var wantCmds []string
  1032. for i, p := range test.Phases {
  1033. for k, v := range p.UpdateKubeSecret {
  1034. kube.SetSecret(k, v)
  1035. }
  1036. lapi.Notify(p.Notify)
  1037. if p.Signal != nil {
  1038. cmd.Process.Signal(*p.Signal)
  1039. }
  1040. if p.WantLog != "" {
  1041. err := tstest.WaitFor(2*time.Second, func() error {
  1042. waitLogLine(t, time.Second, cbOut, p.WantLog)
  1043. return nil
  1044. })
  1045. if err != nil {
  1046. t.Fatal(err)
  1047. }
  1048. }
  1049. if p.WantExitCode != nil {
  1050. state, err := cmd.Process.Wait()
  1051. if err != nil {
  1052. t.Fatal(err)
  1053. }
  1054. if state.ExitCode() != *p.WantExitCode {
  1055. t.Fatalf("phase %d: want exit code %d, got %d", i, *p.WantExitCode, state.ExitCode())
  1056. }
  1057. // Early test return, we don't expect the successful startup log message.
  1058. return
  1059. }
  1060. wantCmds = append(wantCmds, p.WantCmds...)
  1061. waitArgs(t, 2*time.Second, d, argFile, strings.Join(wantCmds, "\n"))
  1062. err := tstest.WaitFor(2*time.Second, func() error {
  1063. if p.WantKubeSecret != nil {
  1064. got := kube.Secret()
  1065. if diff := cmp.Diff(got, p.WantKubeSecret); diff != "" {
  1066. return fmt.Errorf("unexpected kube secret data (-got+want):\n%s", diff)
  1067. }
  1068. } else {
  1069. got := kube.Secret()
  1070. if len(got) > 0 {
  1071. return fmt.Errorf("kube secret unexpectedly not empty, got %#v", got)
  1072. }
  1073. }
  1074. return nil
  1075. })
  1076. if err != nil {
  1077. t.Fatalf("phase %d: %v", i, err)
  1078. }
  1079. err = tstest.WaitFor(2*time.Second, func() error {
  1080. for path, want := range p.WantFiles {
  1081. gotBs, err := os.ReadFile(filepath.Join(d, path))
  1082. if err != nil {
  1083. return fmt.Errorf("reading wanted file %q: %v", path, err)
  1084. }
  1085. if got := strings.TrimSpace(string(gotBs)); got != want {
  1086. return fmt.Errorf("wrong file contents for %q, got %q want %q", path, got, want)
  1087. }
  1088. }
  1089. return nil
  1090. })
  1091. if err != nil {
  1092. t.Fatalf("phase %d: %v", i, err)
  1093. }
  1094. for url, want := range p.EndpointStatuses {
  1095. err := tstest.WaitFor(2*time.Second, func() error {
  1096. resp, err := http.Get(url)
  1097. if err != nil && want != -1 {
  1098. return fmt.Errorf("GET %s: %v", url, err)
  1099. }
  1100. if want > 0 && resp.StatusCode != want {
  1101. defer resp.Body.Close()
  1102. body, _ := io.ReadAll(resp.Body)
  1103. return fmt.Errorf("GET %s, want %d, got %d\n%s", url, want, resp.StatusCode, string(body))
  1104. }
  1105. return nil
  1106. })
  1107. if err != nil {
  1108. t.Fatalf("phase %d: %v", i, err)
  1109. }
  1110. }
  1111. }
  1112. waitLogLine(t, 2*time.Second, cbOut, "Startup complete, waiting for shutdown signal")
  1113. if cmd.ProcessState != nil {
  1114. t.Fatalf("containerboot should be running but exited with exit code %d", cmd.ProcessState.ExitCode())
  1115. }
  1116. })
  1117. }
  1118. }
  1119. type lockingBuffer struct {
  1120. sync.Mutex
  1121. b bytes.Buffer
  1122. }
  1123. func (b *lockingBuffer) Write(bs []byte) (int, error) {
  1124. b.Lock()
  1125. defer b.Unlock()
  1126. return b.b.Write(bs)
  1127. }
  1128. func (b *lockingBuffer) String() string {
  1129. b.Lock()
  1130. defer b.Unlock()
  1131. return b.b.String()
  1132. }
  1133. // waitLogLine looks for want in the contents of b.
  1134. //
  1135. // Only lines starting with 'boot: ' (the output of containerboot
  1136. // itself) are considered, and the logged timestamp is ignored.
  1137. //
  1138. // waitLogLine fails the entire test if path doesn't contain want
  1139. // before the timeout.
  1140. func waitLogLine(t *testing.T, timeout time.Duration, b *lockingBuffer, want string) {
  1141. deadline := time.Now().Add(timeout)
  1142. for time.Now().Before(deadline) {
  1143. for _, line := range strings.Split(b.String(), "\n") {
  1144. if !strings.HasPrefix(line, "boot: ") {
  1145. continue
  1146. }
  1147. if strings.HasSuffix(line, " "+want) {
  1148. return
  1149. }
  1150. }
  1151. time.Sleep(100 * time.Millisecond)
  1152. }
  1153. t.Fatalf("timed out waiting for wanted output line %q. Output:\n%s", want, b.String())
  1154. }
  1155. // waitArgs waits until the contents of path matches wantArgs, a set
  1156. // of command lines recorded by test_tailscale.sh and
  1157. // test_tailscaled.sh.
  1158. //
  1159. // All occurrences of removeStr are removed from the file prior to
  1160. // comparison. This is used to remove the varying temporary root
  1161. // directory name from recorded commandlines, so that wantArgs can be
  1162. // a constant value.
  1163. //
  1164. // waitArgs fails the entire test if path doesn't contain wantArgs
  1165. // before the timeout.
  1166. func waitArgs(t *testing.T, timeout time.Duration, removeStr, path, wantArgs string) {
  1167. t.Helper()
  1168. wantArgs = strings.TrimSpace(wantArgs)
  1169. deadline := time.Now().Add(timeout)
  1170. var got string
  1171. for time.Now().Before(deadline) {
  1172. bs, err := os.ReadFile(path)
  1173. if errors.Is(err, fs.ErrNotExist) {
  1174. // Don't bother logging that the file doesn't exist, it
  1175. // should start existing soon.
  1176. goto loop
  1177. } else if err != nil {
  1178. t.Logf("reading %q: %v", path, err)
  1179. goto loop
  1180. }
  1181. got = strings.TrimSpace(string(bs))
  1182. got = strings.ReplaceAll(got, removeStr, "")
  1183. if got == wantArgs {
  1184. return
  1185. }
  1186. loop:
  1187. time.Sleep(100 * time.Millisecond)
  1188. }
  1189. t.Fatalf("waiting for args file %q to have expected output, got:\n%s\n\nWant: %s", path, got, wantArgs)
  1190. }
  1191. //go:embed test_tailscaled.sh
  1192. var fakeTailscaled []byte
  1193. //go:embed test_tailscale.sh
  1194. var fakeTailscale []byte
  1195. // localAPI is a minimal fake tailscaled LocalAPI server that presents
  1196. // just enough functionality for containerboot to function
  1197. // correctly. In practice this means it only supports querying
  1198. // tailscaled status, and panics on all other uses to make it very
  1199. // obvious that something unexpected happened.
  1200. type localAPI struct {
  1201. FSRoot string
  1202. Path string // populated by Start
  1203. srv *http.Server
  1204. sync.Mutex
  1205. cond *sync.Cond
  1206. notify *ipn.Notify
  1207. }
  1208. func (l *localAPI) Start() error {
  1209. path := filepath.Join(l.FSRoot, "tmp/tailscaled.sock.fake")
  1210. if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil {
  1211. return err
  1212. }
  1213. ln, err := net.Listen("unix", path)
  1214. if err != nil {
  1215. return err
  1216. }
  1217. l.srv = &http.Server{
  1218. Handler: l,
  1219. }
  1220. l.Path = path
  1221. l.cond = sync.NewCond(&l.Mutex)
  1222. go l.srv.Serve(ln)
  1223. return nil
  1224. }
  1225. func (l *localAPI) Close() {
  1226. l.srv.Close()
  1227. }
  1228. func (l *localAPI) Reset() {
  1229. l.Lock()
  1230. defer l.Unlock()
  1231. l.notify = nil
  1232. l.cond.Broadcast()
  1233. }
  1234. func (l *localAPI) Notify(n *ipn.Notify) {
  1235. if n == nil {
  1236. return
  1237. }
  1238. l.Lock()
  1239. defer l.Unlock()
  1240. l.notify = n
  1241. l.cond.Broadcast()
  1242. }
  1243. func (l *localAPI) ServeHTTP(w http.ResponseWriter, r *http.Request) {
  1244. switch r.URL.Path {
  1245. case "/localapi/v0/serve-config":
  1246. if r.Method != "POST" {
  1247. panic(fmt.Sprintf("unsupported method %q", r.Method))
  1248. }
  1249. return
  1250. case "/localapi/v0/watch-ipn-bus":
  1251. if r.Method != "GET" {
  1252. panic(fmt.Sprintf("unsupported method %q", r.Method))
  1253. }
  1254. case "/localapi/v0/usermetrics":
  1255. if r.Method != "GET" {
  1256. panic(fmt.Sprintf("unsupported method %q", r.Method))
  1257. }
  1258. w.Write([]byte("fake metrics"))
  1259. return
  1260. default:
  1261. panic(fmt.Sprintf("unsupported path %q", r.URL.Path))
  1262. }
  1263. w.Header().Set("Content-Type", "application/json")
  1264. w.WriteHeader(http.StatusOK)
  1265. if f, ok := w.(http.Flusher); ok {
  1266. f.Flush()
  1267. }
  1268. enc := json.NewEncoder(w)
  1269. l.Lock()
  1270. defer l.Unlock()
  1271. for {
  1272. if l.notify != nil {
  1273. if err := enc.Encode(l.notify); err != nil {
  1274. // Usually broken pipe as the test client disconnects.
  1275. return
  1276. }
  1277. if f, ok := w.(http.Flusher); ok {
  1278. f.Flush()
  1279. }
  1280. }
  1281. l.cond.Wait()
  1282. }
  1283. }
  1284. // kubeServer is a minimal fake Kubernetes server that presents just
  1285. // enough functionality for containerboot to function correctly. In
  1286. // practice this means it only supports reading and modifying a single
  1287. // kube secret, and panics on all other uses to make it very obvious
  1288. // that something unexpected happened.
  1289. type kubeServer struct {
  1290. FSRoot string
  1291. Host, Port string // populated by Start
  1292. srv *httptest.Server
  1293. sync.Mutex
  1294. secret map[string]string
  1295. canPatch bool
  1296. }
  1297. func (k *kubeServer) Secret() map[string]string {
  1298. k.Lock()
  1299. defer k.Unlock()
  1300. ret := map[string]string{}
  1301. for k, v := range k.secret {
  1302. ret[k] = v
  1303. }
  1304. return ret
  1305. }
  1306. func (k *kubeServer) SetSecret(key, val string) {
  1307. k.Lock()
  1308. defer k.Unlock()
  1309. k.secret[key] = val
  1310. }
  1311. func (k *kubeServer) SetPatching(canPatch bool) {
  1312. k.Lock()
  1313. defer k.Unlock()
  1314. k.canPatch = canPatch
  1315. }
  1316. func (k *kubeServer) Reset() {
  1317. k.Lock()
  1318. defer k.Unlock()
  1319. k.secret = map[string]string{}
  1320. }
  1321. func (k *kubeServer) Start(t *testing.T) {
  1322. root := filepath.Join(k.FSRoot, "var/run/secrets/kubernetes.io/serviceaccount")
  1323. if err := os.MkdirAll(root, 0700); err != nil {
  1324. t.Fatal(err)
  1325. }
  1326. if err := os.WriteFile(filepath.Join(root, "namespace"), []byte("default"), 0600); err != nil {
  1327. t.Fatal(err)
  1328. }
  1329. if err := os.WriteFile(filepath.Join(root, "token"), []byte("bearer_token"), 0600); err != nil {
  1330. t.Fatal(err)
  1331. }
  1332. k.srv = httptest.NewTLSServer(k)
  1333. k.Host = k.srv.Listener.Addr().(*net.TCPAddr).IP.String()
  1334. k.Port = strconv.Itoa(k.srv.Listener.Addr().(*net.TCPAddr).Port)
  1335. var cert bytes.Buffer
  1336. if err := pem.Encode(&cert, &pem.Block{Type: "CERTIFICATE", Bytes: k.srv.Certificate().Raw}); err != nil {
  1337. t.Fatal(err)
  1338. }
  1339. if err := os.WriteFile(filepath.Join(root, "ca.crt"), cert.Bytes(), 0600); err != nil {
  1340. t.Fatal(err)
  1341. }
  1342. }
  1343. func (k *kubeServer) Close() {
  1344. k.srv.Close()
  1345. }
  1346. func (k *kubeServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
  1347. if r.Header.Get("Authorization") != "Bearer bearer_token" {
  1348. panic("client didn't provide bearer token in request")
  1349. }
  1350. switch r.URL.Path {
  1351. case "/api/v1/namespaces/default/secrets/tailscale":
  1352. k.serveSecret(w, r)
  1353. case "/apis/authorization.k8s.io/v1/selfsubjectaccessreviews":
  1354. k.serveSSAR(w, r)
  1355. default:
  1356. panic(fmt.Sprintf("unhandled fake kube api path %q", r.URL.Path))
  1357. }
  1358. }
  1359. func (k *kubeServer) serveSSAR(w http.ResponseWriter, r *http.Request) {
  1360. var req struct {
  1361. Spec struct {
  1362. ResourceAttributes struct {
  1363. Verb string `json:"verb"`
  1364. } `json:"resourceAttributes"`
  1365. } `json:"spec"`
  1366. }
  1367. if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
  1368. panic(fmt.Sprintf("decoding SSAR request: %v", err))
  1369. }
  1370. ok := true
  1371. if req.Spec.ResourceAttributes.Verb == "patch" {
  1372. k.Lock()
  1373. defer k.Unlock()
  1374. ok = k.canPatch
  1375. }
  1376. // Just say yes to all SARs, we don't enforce RBAC.
  1377. w.Header().Set("Content-Type", "application/json")
  1378. fmt.Fprintf(w, `{"status":{"allowed":%v}}`, ok)
  1379. }
  1380. func (k *kubeServer) serveSecret(w http.ResponseWriter, r *http.Request) {
  1381. bs, err := io.ReadAll(r.Body)
  1382. if err != nil {
  1383. http.Error(w, fmt.Sprintf("reading request body: %v", err), http.StatusInternalServerError)
  1384. return
  1385. }
  1386. defer r.Body.Close()
  1387. switch r.Method {
  1388. case "GET":
  1389. w.Header().Set("Content-Type", "application/json")
  1390. ret := map[string]map[string]string{
  1391. "data": {},
  1392. }
  1393. k.Lock()
  1394. defer k.Unlock()
  1395. for k, v := range k.secret {
  1396. v := base64.StdEncoding.EncodeToString([]byte(v))
  1397. ret["data"][k] = v
  1398. }
  1399. if err := json.NewEncoder(w).Encode(ret); err != nil {
  1400. panic("encode failed")
  1401. }
  1402. case "PATCH":
  1403. k.Lock()
  1404. defer k.Unlock()
  1405. if !k.canPatch {
  1406. panic("containerboot tried to patch despite not being allowed")
  1407. }
  1408. switch r.Header.Get("Content-Type") {
  1409. case "application/json-patch+json":
  1410. req := []struct {
  1411. Op string `json:"op"`
  1412. Path string `json:"path"`
  1413. }{}
  1414. if err := json.Unmarshal(bs, &req); err != nil {
  1415. panic(fmt.Sprintf("json decode failed: %v. Body:\n\n%s", err, string(bs)))
  1416. }
  1417. for _, op := range req {
  1418. switch op.Op {
  1419. case "remove":
  1420. if !strings.HasPrefix(op.Path, "/data/") {
  1421. panic(fmt.Sprintf("unsupported json-patch path %q", op.Path))
  1422. }
  1423. delete(k.secret, strings.TrimPrefix(op.Path, "/data/"))
  1424. case "replace":
  1425. path, ok := strings.CutPrefix(op.Path, "/data/")
  1426. if !ok {
  1427. panic(fmt.Sprintf("unsupported json-patch path %q", op.Path))
  1428. }
  1429. req := make([]kubeclient.JSONPatch, 0)
  1430. if err := json.Unmarshal(bs, &req); err != nil {
  1431. panic(fmt.Sprintf("json decode failed: %v. Body:\n\n%s", err, string(bs)))
  1432. }
  1433. for _, patch := range req {
  1434. val, ok := patch.Value.(string)
  1435. if !ok {
  1436. panic(fmt.Sprintf("unsupported json patch value %v: cannot be converted to string", patch.Value))
  1437. }
  1438. k.secret[path] = val
  1439. }
  1440. default:
  1441. panic(fmt.Sprintf("unsupported json-patch op %q", op.Op))
  1442. }
  1443. }
  1444. case "application/strategic-merge-patch+json":
  1445. req := struct {
  1446. Data map[string][]byte `json:"data"`
  1447. }{}
  1448. if err := json.Unmarshal(bs, &req); err != nil {
  1449. panic(fmt.Sprintf("json decode failed: %v. Body:\n\n%s", err, string(bs)))
  1450. }
  1451. for key, val := range req.Data {
  1452. k.secret[key] = string(val)
  1453. }
  1454. default:
  1455. panic(fmt.Sprintf("unknown content type %q", r.Header.Get("Content-Type")))
  1456. }
  1457. default:
  1458. panic(fmt.Sprintf("unhandled HTTP request %s %s", r.Method, r.URL))
  1459. }
  1460. }
  1461. func mustBase64(t *testing.T, v any) string {
  1462. b := mustJSON(t, v)
  1463. s := base64.StdEncoding.WithPadding('=').EncodeToString(b)
  1464. return s
  1465. }
  1466. func mustJSON(t *testing.T, v any) []byte {
  1467. b, err := json.Marshal(v)
  1468. if err != nil {
  1469. t.Fatalf("error converting %v to json: %v", v, err)
  1470. }
  1471. return b
  1472. }
  1473. // egress services status given one named tailnet target specified by FQDN. As written by the proxy to its state Secret.
  1474. func egressSvcStatus(name, fqdn string) egressservices.Status {
  1475. return egressservices.Status{
  1476. Services: map[string]*egressservices.ServiceStatus{
  1477. name: {
  1478. TailnetTarget: egressservices.TailnetTarget{
  1479. FQDN: fqdn,
  1480. },
  1481. },
  1482. },
  1483. }
  1484. }
  1485. // egress config given one named tailnet target specified by FQDN.
  1486. func egressSvcConfig(name, fqdn string) egressservices.Configs {
  1487. return egressservices.Configs{
  1488. name: egressservices.Config{
  1489. TailnetTarget: egressservices.TailnetTarget{
  1490. FQDN: fqdn,
  1491. },
  1492. },
  1493. }
  1494. }