main_test.go 49 KB

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