cli_test.go 45 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482
  1. // Copyright (c) Tailscale Inc & AUTHORS
  2. // SPDX-License-Identifier: BSD-3-Clause
  3. package cli
  4. import (
  5. "bytes"
  6. stdcmp "cmp"
  7. "encoding/json"
  8. "flag"
  9. "fmt"
  10. "net/netip"
  11. "reflect"
  12. "strings"
  13. "testing"
  14. qt "github.com/frankban/quicktest"
  15. "github.com/google/go-cmp/cmp"
  16. "tailscale.com/envknob"
  17. "tailscale.com/health/healthmsg"
  18. "tailscale.com/ipn"
  19. "tailscale.com/ipn/ipnstate"
  20. "tailscale.com/tailcfg"
  21. "tailscale.com/tka"
  22. "tailscale.com/tstest"
  23. "tailscale.com/types/logger"
  24. "tailscale.com/types/opt"
  25. "tailscale.com/types/persist"
  26. "tailscale.com/types/preftype"
  27. "tailscale.com/version/distro"
  28. )
  29. func TestPanicIfAnyEnvCheckedInInit(t *testing.T) {
  30. envknob.PanicIfAnyEnvCheckedInInit()
  31. }
  32. func TestShortUsage(t *testing.T) {
  33. t.Setenv("TAILSCALE_USE_WIP_CODE", "1")
  34. if !envknob.UseWIPCode() {
  35. t.Fatal("expected envknob.UseWIPCode() to be true")
  36. }
  37. walkCommands(newRootCmd(), func(w cmdWalk) bool {
  38. c, parents := w.Command, w.parents
  39. // Words that we expect to be in the usage.
  40. words := make([]string, len(parents)+1)
  41. for i, parent := range parents {
  42. words[i] = parent.Name
  43. }
  44. words[len(parents)] = c.Name
  45. // Check the ShortHelp starts with a capital letter.
  46. if prefix, help := trimPrefixes(c.ShortHelp, "HIDDEN: ", "[ALPHA] ", "[BETA] "); help != "" {
  47. if 'a' <= help[0] && help[0] <= 'z' {
  48. if len(help) > 20 {
  49. help = help[:20] + "…"
  50. }
  51. caphelp := string(help[0]-'a'+'A') + help[1:]
  52. t.Errorf("command: %s: ShortHelp %q should start with a capital letter %q", strings.Join(words, " "), prefix+help, prefix+caphelp)
  53. }
  54. }
  55. // Check all words appear in the usage.
  56. usage := c.ShortUsage
  57. for _, word := range words {
  58. var ok bool
  59. usage, ok = cutWord(usage, word)
  60. if !ok {
  61. full := strings.Join(words, " ")
  62. t.Errorf("command: %s: usage %q should contain the full path %q", full, c.ShortUsage, full)
  63. return true
  64. }
  65. }
  66. return true
  67. })
  68. }
  69. func trimPrefixes(full string, prefixes ...string) (trimmed, remaining string) {
  70. s := full
  71. start:
  72. for _, p := range prefixes {
  73. var ok bool
  74. s, ok = strings.CutPrefix(s, p)
  75. if ok {
  76. goto start
  77. }
  78. }
  79. return full[:len(full)-len(s)], s
  80. }
  81. // cutWord("tailscale debug scale 123", "scale") returns (" 123", true).
  82. func cutWord(s, w string) (after string, ok bool) {
  83. var p string
  84. for {
  85. p, s, ok = strings.Cut(s, w)
  86. if !ok {
  87. return "", false
  88. }
  89. if p != "" && isWordChar(p[len(p)-1]) {
  90. continue
  91. }
  92. if s != "" && isWordChar(s[0]) {
  93. continue
  94. }
  95. return s, true
  96. }
  97. }
  98. func isWordChar(r byte) bool {
  99. return r == '_' ||
  100. ('0' <= r && r <= '9') ||
  101. ('A' <= r && r <= 'Z') ||
  102. ('a' <= r && r <= 'z')
  103. }
  104. func TestCutWord(t *testing.T) {
  105. tests := []struct {
  106. in string
  107. word string
  108. out string
  109. ok bool
  110. }{
  111. {"tailscale debug", "debug", "", true},
  112. {"tailscale debug", "bug", "", false},
  113. {"tailscale debug", "tail", "", false},
  114. {"tailscale debug scaley scale 123", "scale", " 123", true},
  115. }
  116. for _, test := range tests {
  117. out, ok := cutWord(test.in, test.word)
  118. if out != test.out || ok != test.ok {
  119. t.Errorf("cutWord(%q, %q) = (%q, %t), wanted (%q, %t)", test.in, test.word, out, ok, test.out, test.ok)
  120. }
  121. }
  122. }
  123. // geese is a collection of gooses. It need not be complete.
  124. // But it should include anything handled specially (e.g. linux, windows)
  125. // and at least one thing that's not (darwin, freebsd).
  126. var geese = []string{"linux", "darwin", "windows", "freebsd"}
  127. // Test that checkForAccidentalSettingReverts's updateMaskedPrefsFromUpFlag can handle
  128. // all flags. This will panic if a new flag creeps in that's unhandled.
  129. //
  130. // Also, issue 1880: advertise-exit-node was being ignored. Verify that all flags cause an edit.
  131. func TestUpdateMaskedPrefsFromUpFlag(t *testing.T) {
  132. for _, goos := range geese {
  133. var upArgs upArgsT
  134. fs := newUpFlagSet(goos, &upArgs, "up")
  135. fs.VisitAll(func(f *flag.Flag) {
  136. mp := new(ipn.MaskedPrefs)
  137. updateMaskedPrefsFromUpOrSetFlag(mp, f.Name)
  138. got := mp.Pretty()
  139. wantEmpty := preflessFlag(f.Name)
  140. isEmpty := got == "MaskedPrefs{}"
  141. if isEmpty != wantEmpty {
  142. t.Errorf("flag %q created MaskedPrefs %s; want empty=%v", f.Name, got, wantEmpty)
  143. }
  144. })
  145. }
  146. }
  147. func TestCheckForAccidentalSettingReverts(t *testing.T) {
  148. tests := []struct {
  149. name string
  150. flags []string // argv to be parsed by FlagSet
  151. curPrefs *ipn.Prefs
  152. curExitNodeIP netip.Addr
  153. curUser string // os.Getenv("USER") on the client side
  154. goos string // empty means "linux"
  155. distro distro.Distro
  156. want string
  157. }{
  158. {
  159. name: "bare_up_means_up",
  160. flags: []string{},
  161. curPrefs: &ipn.Prefs{
  162. ControlURL: ipn.DefaultControlURL,
  163. WantRunning: false,
  164. Hostname: "foo",
  165. NoStatefulFiltering: opt.NewBool(true),
  166. },
  167. want: "",
  168. },
  169. {
  170. name: "losing_hostname",
  171. flags: []string{"--accept-dns"},
  172. curPrefs: &ipn.Prefs{
  173. ControlURL: ipn.DefaultControlURL,
  174. WantRunning: false,
  175. Hostname: "foo",
  176. CorpDNS: true,
  177. NetfilterMode: preftype.NetfilterOn,
  178. NoStatefulFiltering: opt.NewBool(true),
  179. },
  180. want: accidentalUpPrefix + " --accept-dns --hostname=foo",
  181. },
  182. {
  183. name: "hostname_changing_explicitly",
  184. flags: []string{"--hostname=bar"},
  185. curPrefs: &ipn.Prefs{
  186. ControlURL: ipn.DefaultControlURL,
  187. CorpDNS: true,
  188. NetfilterMode: preftype.NetfilterOn,
  189. Hostname: "foo",
  190. NoStatefulFiltering: opt.NewBool(true),
  191. },
  192. want: "",
  193. },
  194. {
  195. name: "hostname_changing_empty_explicitly",
  196. flags: []string{"--hostname="},
  197. curPrefs: &ipn.Prefs{
  198. ControlURL: ipn.DefaultControlURL,
  199. CorpDNS: true,
  200. NetfilterMode: preftype.NetfilterOn,
  201. Hostname: "foo",
  202. NoStatefulFiltering: opt.NewBool(true),
  203. },
  204. want: "",
  205. },
  206. {
  207. // Issue 1725: "tailscale up --authkey=..." (or other non-empty flags) works from
  208. // a fresh server's initial prefs.
  209. name: "up_with_default_prefs",
  210. flags: []string{"--authkey=foosdlkfjskdljf"},
  211. curPrefs: ipn.NewPrefs(),
  212. want: "",
  213. },
  214. {
  215. name: "implicit_operator_change",
  216. flags: []string{"--hostname=foo"},
  217. curPrefs: &ipn.Prefs{
  218. ControlURL: ipn.DefaultControlURL,
  219. OperatorUser: "alice",
  220. CorpDNS: true,
  221. NetfilterMode: preftype.NetfilterOn,
  222. NoStatefulFiltering: opt.NewBool(true),
  223. },
  224. curUser: "eve",
  225. want: accidentalUpPrefix + " --hostname=foo --operator=alice",
  226. },
  227. {
  228. name: "implicit_operator_matches_shell_user",
  229. flags: []string{"--hostname=foo"},
  230. curPrefs: &ipn.Prefs{
  231. ControlURL: ipn.DefaultControlURL,
  232. CorpDNS: true,
  233. NetfilterMode: preftype.NetfilterOn,
  234. OperatorUser: "alice",
  235. NoStatefulFiltering: opt.NewBool(true),
  236. },
  237. curUser: "alice",
  238. want: "",
  239. },
  240. {
  241. name: "error_advertised_routes_exit_node_removed",
  242. flags: []string{"--advertise-routes=10.0.42.0/24"},
  243. curPrefs: &ipn.Prefs{
  244. ControlURL: ipn.DefaultControlURL,
  245. CorpDNS: true,
  246. NetfilterMode: preftype.NetfilterOn,
  247. AdvertiseRoutes: []netip.Prefix{
  248. netip.MustParsePrefix("10.0.42.0/24"),
  249. netip.MustParsePrefix("0.0.0.0/0"),
  250. netip.MustParsePrefix("::/0"),
  251. },
  252. NoStatefulFiltering: opt.NewBool(true),
  253. },
  254. want: accidentalUpPrefix + " --advertise-routes=10.0.42.0/24 --advertise-exit-node",
  255. },
  256. {
  257. name: "advertised_routes_exit_node_removed_explicit",
  258. flags: []string{"--advertise-routes=10.0.42.0/24", "--advertise-exit-node=false"},
  259. curPrefs: &ipn.Prefs{
  260. ControlURL: ipn.DefaultControlURL,
  261. CorpDNS: true,
  262. NetfilterMode: preftype.NetfilterOn,
  263. AdvertiseRoutes: []netip.Prefix{
  264. netip.MustParsePrefix("10.0.42.0/24"),
  265. netip.MustParsePrefix("0.0.0.0/0"),
  266. netip.MustParsePrefix("::/0"),
  267. },
  268. NoStatefulFiltering: opt.NewBool(true),
  269. },
  270. want: "",
  271. },
  272. {
  273. name: "advertised_routes_includes_the_0_routes", // but no --advertise-exit-node
  274. flags: []string{"--advertise-routes=11.1.43.0/24,0.0.0.0/0,::/0"},
  275. curPrefs: &ipn.Prefs{
  276. ControlURL: ipn.DefaultControlURL,
  277. CorpDNS: true,
  278. NetfilterMode: preftype.NetfilterOn,
  279. AdvertiseRoutes: []netip.Prefix{
  280. netip.MustParsePrefix("10.0.42.0/24"),
  281. netip.MustParsePrefix("0.0.0.0/0"),
  282. netip.MustParsePrefix("::/0"),
  283. },
  284. NoStatefulFiltering: opt.NewBool(true),
  285. },
  286. want: "",
  287. },
  288. {
  289. name: "advertise_exit_node", // Issue 1859
  290. flags: []string{"--advertise-exit-node"},
  291. curPrefs: &ipn.Prefs{
  292. ControlURL: ipn.DefaultControlURL,
  293. CorpDNS: true,
  294. NetfilterMode: preftype.NetfilterOn,
  295. NoStatefulFiltering: opt.NewBool(true),
  296. },
  297. want: "",
  298. },
  299. {
  300. name: "advertise_exit_node_over_existing_routes",
  301. flags: []string{"--advertise-exit-node"},
  302. curPrefs: &ipn.Prefs{
  303. ControlURL: ipn.DefaultControlURL,
  304. CorpDNS: true,
  305. NetfilterMode: preftype.NetfilterOn,
  306. AdvertiseRoutes: []netip.Prefix{
  307. netip.MustParsePrefix("1.2.0.0/16"),
  308. },
  309. NoStatefulFiltering: opt.NewBool(true),
  310. },
  311. want: accidentalUpPrefix + " --advertise-exit-node --advertise-routes=1.2.0.0/16",
  312. },
  313. {
  314. name: "advertise_exit_node_over_existing_routes_and_exit_node",
  315. flags: []string{"--advertise-exit-node"},
  316. curPrefs: &ipn.Prefs{
  317. ControlURL: ipn.DefaultControlURL,
  318. CorpDNS: true,
  319. NetfilterMode: preftype.NetfilterOn,
  320. AdvertiseRoutes: []netip.Prefix{
  321. netip.MustParsePrefix("0.0.0.0/0"),
  322. netip.MustParsePrefix("::/0"),
  323. netip.MustParsePrefix("1.2.0.0/16"),
  324. },
  325. NoStatefulFiltering: opt.NewBool(true),
  326. },
  327. want: accidentalUpPrefix + " --advertise-exit-node --advertise-routes=1.2.0.0/16",
  328. },
  329. {
  330. name: "exit_node_clearing", // Issue 1777
  331. flags: []string{"--exit-node="},
  332. curPrefs: &ipn.Prefs{
  333. ControlURL: ipn.DefaultControlURL,
  334. CorpDNS: true,
  335. NetfilterMode: preftype.NetfilterOn,
  336. ExitNodeID: "fooID",
  337. NoStatefulFiltering: opt.NewBool(true),
  338. },
  339. want: "",
  340. },
  341. {
  342. name: "remove_all_implicit",
  343. flags: []string{"--force-reauth"},
  344. curPrefs: &ipn.Prefs{
  345. WantRunning: true,
  346. ControlURL: ipn.DefaultControlURL,
  347. RouteAll: true,
  348. ExitNodeIP: netip.MustParseAddr("100.64.5.6"),
  349. CorpDNS: false,
  350. ShieldsUp: true,
  351. AdvertiseTags: []string{"tag:foo", "tag:bar"},
  352. Hostname: "myhostname",
  353. ForceDaemon: true,
  354. AdvertiseRoutes: []netip.Prefix{
  355. netip.MustParsePrefix("10.0.0.0/16"),
  356. netip.MustParsePrefix("0.0.0.0/0"),
  357. netip.MustParsePrefix("::/0"),
  358. },
  359. NetfilterMode: preftype.NetfilterNoDivert,
  360. OperatorUser: "alice",
  361. NoStatefulFiltering: opt.NewBool(true),
  362. },
  363. curUser: "eve",
  364. want: accidentalUpPrefix + " --force-reauth --accept-dns=false --accept-routes --advertise-exit-node --advertise-routes=10.0.0.0/16 --advertise-tags=tag:foo,tag:bar --exit-node=100.64.5.6 --hostname=myhostname --netfilter-mode=nodivert --operator=alice --shields-up",
  365. },
  366. {
  367. name: "remove_all_implicit_except_hostname",
  368. flags: []string{"--hostname=newhostname"},
  369. curPrefs: &ipn.Prefs{
  370. WantRunning: true,
  371. ControlURL: ipn.DefaultControlURL,
  372. RouteAll: true,
  373. ExitNodeIP: netip.MustParseAddr("100.64.5.6"),
  374. CorpDNS: false,
  375. ShieldsUp: true,
  376. AdvertiseTags: []string{"tag:foo", "tag:bar"},
  377. Hostname: "myhostname",
  378. ForceDaemon: true,
  379. AdvertiseRoutes: []netip.Prefix{
  380. netip.MustParsePrefix("10.0.0.0/16"),
  381. },
  382. NetfilterMode: preftype.NetfilterNoDivert,
  383. OperatorUser: "alice",
  384. NoStatefulFiltering: opt.NewBool(true),
  385. },
  386. curUser: "eve",
  387. want: accidentalUpPrefix + " --hostname=newhostname --accept-dns=false --accept-routes --advertise-routes=10.0.0.0/16 --advertise-tags=tag:foo,tag:bar --exit-node=100.64.5.6 --netfilter-mode=nodivert --operator=alice --shields-up",
  388. },
  389. {
  390. name: "loggedout_is_implicit",
  391. flags: []string{"--hostname=foo"},
  392. curPrefs: &ipn.Prefs{
  393. ControlURL: ipn.DefaultControlURL,
  394. LoggedOut: true,
  395. CorpDNS: true,
  396. NetfilterMode: preftype.NetfilterOn,
  397. NoStatefulFiltering: opt.NewBool(true),
  398. },
  399. want: "", // not an error. LoggedOut is implicit.
  400. },
  401. {
  402. // Test that a pre-1.8 version of Tailscale with bogus NoSNAT pref
  403. // values is able to enable exit nodes without warnings.
  404. name: "make_windows_exit_node",
  405. flags: []string{"--advertise-exit-node"},
  406. curPrefs: &ipn.Prefs{
  407. ControlURL: ipn.DefaultControlURL,
  408. CorpDNS: true,
  409. RouteAll: true,
  410. // And assume this no-op accidental pre-1.8 value:
  411. NoSNAT: true,
  412. },
  413. goos: "windows",
  414. want: "", // not an error
  415. },
  416. {
  417. name: "ignore_netfilter_change_non_linux",
  418. flags: []string{"--accept-dns"},
  419. curPrefs: &ipn.Prefs{
  420. ControlURL: ipn.DefaultControlURL,
  421. NetfilterMode: preftype.NetfilterNoDivert, // we never had this bug, but pretend it got set non-zero on Windows somehow
  422. },
  423. goos: "openbsd",
  424. want: "", // not an error
  425. },
  426. {
  427. name: "operator_losing_routes_step1", // https://twitter.com/EXPbits/status/1390418145047887877
  428. flags: []string{"--operator=expbits"},
  429. curPrefs: &ipn.Prefs{
  430. ControlURL: ipn.DefaultControlURL,
  431. CorpDNS: true,
  432. NetfilterMode: preftype.NetfilterOn,
  433. AdvertiseRoutes: []netip.Prefix{
  434. netip.MustParsePrefix("0.0.0.0/0"),
  435. netip.MustParsePrefix("::/0"),
  436. netip.MustParsePrefix("1.2.0.0/16"),
  437. },
  438. NoStatefulFiltering: opt.NewBool(true),
  439. },
  440. want: accidentalUpPrefix + " --operator=expbits --advertise-exit-node --advertise-routes=1.2.0.0/16",
  441. },
  442. {
  443. name: "operator_losing_routes_step2", // https://twitter.com/EXPbits/status/1390418145047887877
  444. flags: []string{"--operator=expbits", "--advertise-routes=1.2.0.0/16"},
  445. curPrefs: &ipn.Prefs{
  446. ControlURL: ipn.DefaultControlURL,
  447. CorpDNS: true,
  448. NetfilterMode: preftype.NetfilterOn,
  449. AdvertiseRoutes: []netip.Prefix{
  450. netip.MustParsePrefix("0.0.0.0/0"),
  451. netip.MustParsePrefix("::/0"),
  452. netip.MustParsePrefix("1.2.0.0/16"),
  453. },
  454. NoStatefulFiltering: opt.NewBool(true),
  455. },
  456. want: accidentalUpPrefix + " --advertise-routes=1.2.0.0/16 --operator=expbits --advertise-exit-node",
  457. },
  458. {
  459. name: "errors_preserve_explicit_flags",
  460. flags: []string{"--reset", "--force-reauth=false", "--authkey=secretrand"},
  461. curPrefs: &ipn.Prefs{
  462. ControlURL: ipn.DefaultControlURL,
  463. WantRunning: false,
  464. CorpDNS: true,
  465. NetfilterMode: preftype.NetfilterOn,
  466. Hostname: "foo",
  467. NoStatefulFiltering: opt.NewBool(true),
  468. },
  469. want: accidentalUpPrefix + " --auth-key=secretrand --force-reauth=false --reset --hostname=foo",
  470. },
  471. {
  472. name: "error_exit_node_omit_with_ip_pref",
  473. flags: []string{"--hostname=foo"},
  474. curPrefs: &ipn.Prefs{
  475. ControlURL: ipn.DefaultControlURL,
  476. CorpDNS: true,
  477. NetfilterMode: preftype.NetfilterOn,
  478. ExitNodeIP: netip.MustParseAddr("100.64.5.4"),
  479. NoStatefulFiltering: opt.NewBool(true),
  480. },
  481. want: accidentalUpPrefix + " --hostname=foo --exit-node=100.64.5.4",
  482. },
  483. {
  484. name: "error_exit_node_omit_with_id_pref",
  485. flags: []string{"--hostname=foo"},
  486. curExitNodeIP: netip.MustParseAddr("100.64.5.7"),
  487. curPrefs: &ipn.Prefs{
  488. ControlURL: ipn.DefaultControlURL,
  489. CorpDNS: true,
  490. NetfilterMode: preftype.NetfilterOn,
  491. ExitNodeID: "some_stable_id",
  492. NoStatefulFiltering: opt.NewBool(true),
  493. },
  494. want: accidentalUpPrefix + " --hostname=foo --exit-node=100.64.5.7",
  495. },
  496. {
  497. name: "error_exit_node_and_allow_lan_omit_with_id_pref", // Issue 3480
  498. flags: []string{"--hostname=foo"},
  499. curExitNodeIP: netip.MustParseAddr("100.2.3.4"),
  500. curPrefs: &ipn.Prefs{
  501. ControlURL: ipn.DefaultControlURL,
  502. CorpDNS: true,
  503. NetfilterMode: preftype.NetfilterOn,
  504. ExitNodeAllowLANAccess: true,
  505. ExitNodeID: "some_stable_id",
  506. NoStatefulFiltering: opt.NewBool(true),
  507. },
  508. want: accidentalUpPrefix + " --hostname=foo --exit-node-allow-lan-access --exit-node=100.2.3.4",
  509. },
  510. {
  511. name: "ignore_login_server_synonym",
  512. flags: []string{"--login-server=https://controlplane.tailscale.com"},
  513. curPrefs: &ipn.Prefs{
  514. ControlURL: "https://login.tailscale.com",
  515. CorpDNS: true,
  516. NetfilterMode: preftype.NetfilterOn,
  517. NoStatefulFiltering: opt.NewBool(true),
  518. },
  519. want: "", // not an error
  520. },
  521. {
  522. name: "ignore_login_server_synonym_on_other_change",
  523. flags: []string{"--netfilter-mode=off"},
  524. curPrefs: &ipn.Prefs{
  525. ControlURL: "https://login.tailscale.com",
  526. CorpDNS: false,
  527. NetfilterMode: preftype.NetfilterOn,
  528. NoStatefulFiltering: opt.NewBool(true),
  529. },
  530. want: accidentalUpPrefix + " --netfilter-mode=off --accept-dns=false",
  531. },
  532. {
  533. // Issue 3176: on Synology, don't require --accept-routes=false because user
  534. // might've had an old install, and we don't support --accept-routes anyway.
  535. name: "synology_permit_omit_accept_routes",
  536. flags: []string{"--hostname=foo"},
  537. curPrefs: &ipn.Prefs{
  538. ControlURL: "https://login.tailscale.com",
  539. CorpDNS: true,
  540. RouteAll: true,
  541. NetfilterMode: preftype.NetfilterOn,
  542. NoStatefulFiltering: opt.NewBool(true),
  543. },
  544. goos: "linux",
  545. distro: distro.Synology,
  546. want: "",
  547. },
  548. {
  549. // Same test case as "synology_permit_omit_accept_routes" above, but
  550. // on non-Synology distro.
  551. name: "not_synology_dont_permit_omit_accept_routes",
  552. flags: []string{"--hostname=foo"},
  553. curPrefs: &ipn.Prefs{
  554. ControlURL: "https://login.tailscale.com",
  555. CorpDNS: true,
  556. RouteAll: true,
  557. NetfilterMode: preftype.NetfilterOn,
  558. NoStatefulFiltering: opt.NewBool(true),
  559. },
  560. goos: "linux",
  561. distro: "", // not Synology
  562. want: accidentalUpPrefix + " --hostname=foo --accept-routes",
  563. },
  564. {
  565. name: "profile_name_ignored_in_up",
  566. flags: []string{"--hostname=foo"},
  567. curPrefs: &ipn.Prefs{
  568. ControlURL: "https://login.tailscale.com",
  569. CorpDNS: true,
  570. NetfilterMode: preftype.NetfilterOn,
  571. ProfileName: "foo",
  572. NoStatefulFiltering: opt.NewBool(true),
  573. },
  574. goos: "linux",
  575. want: "",
  576. },
  577. }
  578. for _, tt := range tests {
  579. t.Run(tt.name, func(t *testing.T) {
  580. goos := "linux"
  581. if tt.goos != "" {
  582. goos = tt.goos
  583. }
  584. var upArgs upArgsT
  585. flagSet := newUpFlagSet(goos, &upArgs, "up")
  586. flags := CleanUpArgs(tt.flags)
  587. flagSet.Parse(flags)
  588. newPrefs, err := prefsFromUpArgs(upArgs, t.Logf, new(ipnstate.Status), goos)
  589. if err != nil {
  590. t.Fatal(err)
  591. }
  592. upEnv := upCheckEnv{
  593. goos: goos,
  594. flagSet: flagSet,
  595. curExitNodeIP: tt.curExitNodeIP,
  596. distro: tt.distro,
  597. user: tt.curUser,
  598. }
  599. applyImplicitPrefs(newPrefs, tt.curPrefs, upEnv)
  600. var got string
  601. if err := checkForAccidentalSettingReverts(newPrefs, tt.curPrefs, upEnv); err != nil {
  602. got = err.Error()
  603. }
  604. if strings.TrimSpace(got) != tt.want {
  605. t.Errorf("unexpected result\n got: %s\nwant: %s\n", got, tt.want)
  606. }
  607. })
  608. }
  609. }
  610. func upArgsFromOSArgs(goos string, flagArgs ...string) (args upArgsT) {
  611. fs := newUpFlagSet(goos, &args, "up")
  612. fs.Parse(flagArgs) // populates args
  613. return
  614. }
  615. func TestPrefsFromUpArgs(t *testing.T) {
  616. tests := []struct {
  617. name string
  618. args upArgsT
  619. goos string // runtime.GOOS; empty means linux
  620. st *ipnstate.Status // or nil
  621. want *ipn.Prefs
  622. wantErr string
  623. wantWarn string
  624. }{
  625. {
  626. name: "default_linux",
  627. goos: "linux",
  628. args: upArgsFromOSArgs("linux"),
  629. want: &ipn.Prefs{
  630. ControlURL: ipn.DefaultControlURL,
  631. WantRunning: true,
  632. NoSNAT: false,
  633. NoStatefulFiltering: "true",
  634. NetfilterMode: preftype.NetfilterOn,
  635. CorpDNS: true,
  636. AutoUpdate: ipn.AutoUpdatePrefs{
  637. Check: true,
  638. },
  639. },
  640. },
  641. {
  642. name: "default_windows",
  643. goos: "windows",
  644. args: upArgsFromOSArgs("windows"),
  645. want: &ipn.Prefs{
  646. ControlURL: ipn.DefaultControlURL,
  647. WantRunning: true,
  648. CorpDNS: true,
  649. RouteAll: true,
  650. NoSNAT: false,
  651. NoStatefulFiltering: "true",
  652. NetfilterMode: preftype.NetfilterOn,
  653. AutoUpdate: ipn.AutoUpdatePrefs{
  654. Check: true,
  655. },
  656. },
  657. },
  658. {
  659. name: "advertise_default_route",
  660. args: upArgsFromOSArgs("linux", "--advertise-exit-node"),
  661. want: &ipn.Prefs{
  662. ControlURL: ipn.DefaultControlURL,
  663. WantRunning: true,
  664. CorpDNS: true,
  665. AdvertiseRoutes: []netip.Prefix{
  666. netip.MustParsePrefix("0.0.0.0/0"),
  667. netip.MustParsePrefix("::/0"),
  668. },
  669. NoStatefulFiltering: "true",
  670. NetfilterMode: preftype.NetfilterOn,
  671. AutoUpdate: ipn.AutoUpdatePrefs{
  672. Check: true,
  673. },
  674. },
  675. },
  676. {
  677. name: "error_advertise_route_invalid_ip",
  678. args: upArgsT{
  679. advertiseRoutes: "foo",
  680. },
  681. wantErr: `"foo" is not a valid IP address or CIDR prefix`,
  682. },
  683. {
  684. name: "error_advertise_route_unmasked_bits",
  685. args: upArgsT{
  686. advertiseRoutes: "1.2.3.4/16",
  687. },
  688. wantErr: `1.2.3.4/16 has non-address bits set; expected 1.2.0.0/16`,
  689. },
  690. {
  691. name: "error_exit_node_bad_ip",
  692. args: upArgsT{
  693. exitNodeIP: "foo",
  694. },
  695. wantErr: `invalid value "foo" for --exit-node; must be IP or unique node name`,
  696. },
  697. {
  698. name: "error_exit_node_allow_lan_without_exit_node",
  699. args: upArgsT{
  700. exitNodeAllowLANAccess: true,
  701. },
  702. wantErr: `--exit-node-allow-lan-access can only be used with --exit-node`,
  703. },
  704. {
  705. name: "error_tag_prefix",
  706. args: upArgsT{
  707. advertiseTags: "foo",
  708. },
  709. wantErr: `tag: "foo": tags must start with 'tag:'`,
  710. },
  711. {
  712. name: "error_long_hostname",
  713. args: upArgsT{
  714. hostname: strings.Repeat(strings.Repeat("a", 63)+".", 4),
  715. },
  716. wantErr: `"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" is too long to be a DNS name`,
  717. },
  718. {
  719. name: "error_long_label",
  720. args: upArgsT{
  721. hostname: strings.Repeat("a", 64) + ".example.com",
  722. },
  723. wantErr: `"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" is not a valid DNS label`,
  724. },
  725. {
  726. name: "error_linux_netfilter_empty",
  727. args: upArgsT{
  728. netfilterMode: "",
  729. },
  730. wantErr: `invalid value --netfilter-mode=""`,
  731. },
  732. {
  733. name: "error_linux_netfilter_bogus",
  734. args: upArgsT{
  735. netfilterMode: "bogus",
  736. },
  737. wantErr: `invalid value --netfilter-mode="bogus"`,
  738. },
  739. {
  740. name: "error_exit_node_ip_is_self_ip",
  741. args: upArgsT{
  742. exitNodeIP: "100.105.106.107",
  743. },
  744. st: &ipnstate.Status{
  745. TailscaleIPs: []netip.Addr{netip.MustParseAddr("100.105.106.107")},
  746. },
  747. wantErr: `cannot use 100.105.106.107 as an exit node as it is a local IP address to this machine; did you mean --advertise-exit-node?`,
  748. },
  749. {
  750. name: "warn_linux_netfilter_nodivert",
  751. goos: "linux",
  752. args: upArgsT{
  753. netfilterMode: "nodivert",
  754. },
  755. wantWarn: "netfilter=nodivert; add iptables calls to ts-* chains manually.",
  756. want: &ipn.Prefs{
  757. WantRunning: true,
  758. NetfilterMode: preftype.NetfilterNoDivert,
  759. NoSNAT: true,
  760. NoStatefulFiltering: "true",
  761. AutoUpdate: ipn.AutoUpdatePrefs{
  762. Check: true,
  763. },
  764. },
  765. },
  766. {
  767. name: "warn_linux_netfilter_off",
  768. goos: "linux",
  769. args: upArgsT{
  770. netfilterMode: "off",
  771. },
  772. wantWarn: "netfilter=off; configure iptables yourself.",
  773. want: &ipn.Prefs{
  774. WantRunning: true,
  775. NetfilterMode: preftype.NetfilterOff,
  776. NoSNAT: true,
  777. NoStatefulFiltering: "true",
  778. AutoUpdate: ipn.AutoUpdatePrefs{
  779. Check: true,
  780. },
  781. },
  782. },
  783. {
  784. name: "via_route_good",
  785. goos: "linux",
  786. args: upArgsT{
  787. advertiseRoutes: "fd7a:115c:a1e0:b1a::bb:10.0.0.0/112",
  788. netfilterMode: "off",
  789. },
  790. want: &ipn.Prefs{
  791. WantRunning: true,
  792. NoSNAT: true,
  793. NoStatefulFiltering: "true",
  794. AdvertiseRoutes: []netip.Prefix{
  795. netip.MustParsePrefix("fd7a:115c:a1e0:b1a::bb:10.0.0.0/112"),
  796. },
  797. AutoUpdate: ipn.AutoUpdatePrefs{
  798. Check: true,
  799. },
  800. },
  801. },
  802. {
  803. name: "via_route_good_16_bit",
  804. goos: "linux",
  805. args: upArgsT{
  806. advertiseRoutes: "fd7a:115c:a1e0:b1a::aabb:10.0.0.0/112",
  807. netfilterMode: "off",
  808. },
  809. want: &ipn.Prefs{
  810. WantRunning: true,
  811. NoSNAT: true,
  812. NoStatefulFiltering: "true",
  813. AdvertiseRoutes: []netip.Prefix{
  814. netip.MustParsePrefix("fd7a:115c:a1e0:b1a::aabb:10.0.0.0/112"),
  815. },
  816. AutoUpdate: ipn.AutoUpdatePrefs{
  817. Check: true,
  818. },
  819. },
  820. },
  821. {
  822. name: "via_route_short_prefix",
  823. goos: "linux",
  824. args: upArgsT{
  825. advertiseRoutes: "fd7a:115c:a1e0:b1a::/64",
  826. netfilterMode: "off",
  827. },
  828. wantErr: "fd7a:115c:a1e0:b1a::/64 4-in-6 prefix must be at least a /96",
  829. },
  830. {
  831. name: "via_route_short_reserved_siteid",
  832. goos: "linux",
  833. args: upArgsT{
  834. advertiseRoutes: "fd7a:115c:a1e0:b1a:1234:5678::/112",
  835. netfilterMode: "off",
  836. },
  837. wantErr: "route fd7a:115c:a1e0:b1a:1234:5678::/112 contains invalid site ID 12345678; must be 0xffff or less",
  838. },
  839. }
  840. for _, tt := range tests {
  841. t.Run(tt.name, func(t *testing.T) {
  842. var warnBuf tstest.MemLogger
  843. goos := stdcmp.Or(tt.goos, "linux")
  844. st := tt.st
  845. if st == nil {
  846. st = new(ipnstate.Status)
  847. }
  848. got, err := prefsFromUpArgs(tt.args, warnBuf.Logf, st, goos)
  849. gotErr := fmt.Sprint(err)
  850. if tt.wantErr != "" {
  851. if tt.wantErr != gotErr {
  852. t.Errorf("wrong error.\n got error: %v\nwant error: %v\n", gotErr, tt.wantErr)
  853. }
  854. return
  855. }
  856. if err != nil {
  857. t.Fatal(err)
  858. }
  859. if tt.want == nil {
  860. t.Fatal("tt.want is nil")
  861. }
  862. if !got.Equals(tt.want) {
  863. jgot, _ := json.MarshalIndent(got, "", "\t")
  864. jwant, _ := json.MarshalIndent(tt.want, "", "\t")
  865. if bytes.Equal(jgot, jwant) {
  866. t.Logf("prefs differ only in non-JSON-visible ways (nil/non-nil zero-length arrays)")
  867. }
  868. t.Errorf("wrong prefs\n got: %s\nwant: %s\n\ngot: %s\nwant: %s\n",
  869. got.Pretty(), tt.want.Pretty(),
  870. jgot, jwant,
  871. )
  872. }
  873. })
  874. }
  875. }
  876. func TestPrefFlagMapping(t *testing.T) {
  877. prefHasFlag := map[string]bool{}
  878. for _, pv := range prefsOfFlag {
  879. for _, pref := range pv {
  880. prefHasFlag[strings.Split(pref, ".")[0]] = true
  881. }
  882. }
  883. prefType := reflect.TypeFor[ipn.Prefs]()
  884. for i := range prefType.NumField() {
  885. prefName := prefType.Field(i).Name
  886. if prefHasFlag[prefName] {
  887. continue
  888. }
  889. switch prefName {
  890. case "AllowSingleHosts":
  891. // Fake pref for downgrade compat. See #12058.
  892. continue
  893. case "WantRunning", "Persist", "LoggedOut":
  894. // All explicitly handled (ignored) by checkForAccidentalSettingReverts.
  895. continue
  896. case "OSVersion", "DeviceModel":
  897. // Only used by Android, which doesn't have a CLI mode anyway, so
  898. // fine to not map.
  899. continue
  900. case "NotepadURLs":
  901. // TODO(bradfitz): https://github.com/tailscale/tailscale/issues/1830
  902. continue
  903. case "Egg":
  904. // Not applicable.
  905. continue
  906. case "RunWebClient":
  907. // TODO(tailscale/corp#14335): Currently behind a feature flag.
  908. continue
  909. case "NetfilterKind":
  910. // Handled by TS_DEBUG_FIREWALL_MODE env var, we don't want to have
  911. // a CLI flag for this. The Pref is used by c2n.
  912. continue
  913. case "DriveShares":
  914. // Handled by the tailscale share subcommand, we don't want a CLI
  915. // flag for this.
  916. continue
  917. case "AdvertiseServices":
  918. // Handled by the tailscale advertise subcommand, we don't want a
  919. // CLI flag for this.
  920. continue
  921. case "InternalExitNodePrior":
  922. // Used internally by LocalBackend as part of exit node usage toggling.
  923. // No CLI flag for this.
  924. continue
  925. }
  926. t.Errorf("unexpected new ipn.Pref field %q is not handled by up.go (see addPrefFlagMapping and checkForAccidentalSettingReverts)", prefName)
  927. }
  928. }
  929. func TestFlagAppliesToOS(t *testing.T) {
  930. for _, goos := range geese {
  931. var upArgs upArgsT
  932. fs := newUpFlagSet(goos, &upArgs, "up")
  933. fs.VisitAll(func(f *flag.Flag) {
  934. if !flagAppliesToOS(f.Name, goos) {
  935. t.Errorf("flagAppliesToOS(%q, %q) = false but found in %s set", f.Name, goos, goos)
  936. }
  937. })
  938. }
  939. }
  940. func TestUpdatePrefs(t *testing.T) {
  941. tests := []struct {
  942. name string
  943. flags []string // argv to be parsed into env.flagSet and env.upArgs
  944. curPrefs *ipn.Prefs
  945. env upCheckEnv // empty goos means "linux"
  946. // sshOverTailscale specifies if the cmd being run over SSH over Tailscale.
  947. // It is used to test the --accept-risks flag.
  948. sshOverTailscale bool
  949. // checkUpdatePrefsMutations, if non-nil, is run with the new prefs after
  950. // updatePrefs might've mutated them (from applyImplicitPrefs).
  951. checkUpdatePrefsMutations func(t *testing.T, newPrefs *ipn.Prefs)
  952. wantSimpleUp bool
  953. wantJustEditMP *ipn.MaskedPrefs
  954. wantErrSubtr string
  955. }{
  956. {
  957. name: "bare_up_means_up",
  958. flags: []string{},
  959. curPrefs: &ipn.Prefs{
  960. ControlURL: ipn.DefaultControlURL,
  961. WantRunning: false,
  962. Hostname: "foo",
  963. },
  964. },
  965. {
  966. name: "just_up",
  967. flags: []string{},
  968. curPrefs: &ipn.Prefs{
  969. ControlURL: ipn.DefaultControlURL,
  970. Persist: &persist.Persist{UserProfile: tailcfg.UserProfile{LoginName: "crawshaw.github"}},
  971. },
  972. env: upCheckEnv{
  973. backendState: "Stopped",
  974. },
  975. wantSimpleUp: true,
  976. },
  977. {
  978. name: "just_edit",
  979. flags: []string{},
  980. curPrefs: &ipn.Prefs{
  981. ControlURL: ipn.DefaultControlURL,
  982. Persist: &persist.Persist{UserProfile: tailcfg.UserProfile{LoginName: "crawshaw.github"}},
  983. },
  984. env: upCheckEnv{backendState: "Running"},
  985. wantSimpleUp: true,
  986. wantJustEditMP: &ipn.MaskedPrefs{WantRunningSet: true},
  987. },
  988. {
  989. name: "just_edit_reset",
  990. flags: []string{"--reset"},
  991. curPrefs: &ipn.Prefs{
  992. ControlURL: ipn.DefaultControlURL,
  993. Persist: &persist.Persist{UserProfile: tailcfg.UserProfile{LoginName: "crawshaw.github"}},
  994. },
  995. env: upCheckEnv{backendState: "Running"},
  996. wantJustEditMP: &ipn.MaskedPrefs{
  997. AdvertiseRoutesSet: true,
  998. AdvertiseTagsSet: true,
  999. AppConnectorSet: true,
  1000. ControlURLSet: true,
  1001. CorpDNSSet: true,
  1002. ExitNodeAllowLANAccessSet: true,
  1003. ExitNodeIDSet: true,
  1004. ExitNodeIPSet: true,
  1005. HostnameSet: true,
  1006. NetfilterModeSet: true,
  1007. NoSNATSet: true,
  1008. NoStatefulFilteringSet: true,
  1009. OperatorUserSet: true,
  1010. RouteAllSet: true,
  1011. RunSSHSet: true,
  1012. ShieldsUpSet: true,
  1013. WantRunningSet: true,
  1014. },
  1015. },
  1016. {
  1017. name: "control_synonym",
  1018. flags: []string{},
  1019. curPrefs: &ipn.Prefs{
  1020. ControlURL: "https://login.tailscale.com",
  1021. Persist: &persist.Persist{UserProfile: tailcfg.UserProfile{LoginName: "crawshaw.github"}},
  1022. },
  1023. env: upCheckEnv{backendState: "Running"},
  1024. wantSimpleUp: true,
  1025. wantJustEditMP: &ipn.MaskedPrefs{WantRunningSet: true},
  1026. },
  1027. {
  1028. name: "change_login_server",
  1029. flags: []string{"--login-server=https://localhost:1000"},
  1030. curPrefs: &ipn.Prefs{
  1031. ControlURL: "https://login.tailscale.com",
  1032. Persist: &persist.Persist{UserProfile: tailcfg.UserProfile{LoginName: "crawshaw.github"}},
  1033. CorpDNS: true,
  1034. NetfilterMode: preftype.NetfilterOn,
  1035. NoStatefulFiltering: opt.NewBool(true),
  1036. },
  1037. env: upCheckEnv{backendState: "Running"},
  1038. wantSimpleUp: true,
  1039. wantJustEditMP: &ipn.MaskedPrefs{WantRunningSet: true},
  1040. wantErrSubtr: "can't change --login-server without --force-reauth",
  1041. },
  1042. {
  1043. name: "change_tags",
  1044. flags: []string{"--advertise-tags=tag:foo"},
  1045. curPrefs: &ipn.Prefs{
  1046. ControlURL: "https://login.tailscale.com",
  1047. Persist: &persist.Persist{UserProfile: tailcfg.UserProfile{LoginName: "crawshaw.github"}},
  1048. CorpDNS: true,
  1049. NetfilterMode: preftype.NetfilterOn,
  1050. NoStatefulFiltering: opt.NewBool(true),
  1051. },
  1052. env: upCheckEnv{backendState: "Running"},
  1053. },
  1054. {
  1055. // Issue 3808: explicitly empty --operator= should clear value.
  1056. name: "explicit_empty_operator",
  1057. flags: []string{"--operator="},
  1058. curPrefs: &ipn.Prefs{
  1059. ControlURL: "https://login.tailscale.com",
  1060. CorpDNS: true,
  1061. NetfilterMode: preftype.NetfilterOn,
  1062. OperatorUser: "somebody",
  1063. NoStatefulFiltering: opt.NewBool(true),
  1064. },
  1065. env: upCheckEnv{user: "somebody", backendState: "Running"},
  1066. wantJustEditMP: &ipn.MaskedPrefs{
  1067. OperatorUserSet: true,
  1068. WantRunningSet: true,
  1069. },
  1070. checkUpdatePrefsMutations: func(t *testing.T, prefs *ipn.Prefs) {
  1071. if prefs.OperatorUser != "" {
  1072. t.Errorf("operator sent to backend should be empty; got %q", prefs.OperatorUser)
  1073. }
  1074. },
  1075. },
  1076. {
  1077. name: "enable_ssh",
  1078. flags: []string{"--ssh"},
  1079. curPrefs: &ipn.Prefs{
  1080. ControlURL: "https://login.tailscale.com",
  1081. Persist: &persist.Persist{UserProfile: tailcfg.UserProfile{LoginName: "crawshaw.github"}},
  1082. CorpDNS: true,
  1083. NetfilterMode: preftype.NetfilterOn,
  1084. NoStatefulFiltering: opt.NewBool(true),
  1085. },
  1086. wantJustEditMP: &ipn.MaskedPrefs{
  1087. RunSSHSet: true,
  1088. WantRunningSet: true,
  1089. },
  1090. checkUpdatePrefsMutations: func(t *testing.T, newPrefs *ipn.Prefs) {
  1091. if !newPrefs.RunSSH {
  1092. t.Errorf("RunSSH not set to true")
  1093. }
  1094. },
  1095. env: upCheckEnv{backendState: "Running"},
  1096. },
  1097. {
  1098. name: "disable_ssh",
  1099. flags: []string{"--ssh=false"},
  1100. curPrefs: &ipn.Prefs{
  1101. ControlURL: "https://login.tailscale.com",
  1102. Persist: &persist.Persist{UserProfile: tailcfg.UserProfile{LoginName: "crawshaw.github"}},
  1103. CorpDNS: true,
  1104. RunSSH: true,
  1105. NetfilterMode: preftype.NetfilterOn,
  1106. NoStatefulFiltering: opt.NewBool(true),
  1107. },
  1108. wantJustEditMP: &ipn.MaskedPrefs{
  1109. RunSSHSet: true,
  1110. WantRunningSet: true,
  1111. },
  1112. checkUpdatePrefsMutations: func(t *testing.T, newPrefs *ipn.Prefs) {
  1113. if newPrefs.RunSSH {
  1114. t.Errorf("RunSSH not set to false")
  1115. }
  1116. },
  1117. env: upCheckEnv{backendState: "Running", upArgs: upArgsT{
  1118. runSSH: true,
  1119. }},
  1120. },
  1121. {
  1122. name: "disable_ssh_over_ssh_no_risk",
  1123. flags: []string{"--ssh=false"},
  1124. sshOverTailscale: true,
  1125. curPrefs: &ipn.Prefs{
  1126. ControlURL: "https://login.tailscale.com",
  1127. Persist: &persist.Persist{UserProfile: tailcfg.UserProfile{LoginName: "crawshaw.github"}},
  1128. CorpDNS: true,
  1129. NetfilterMode: preftype.NetfilterOn,
  1130. RunSSH: true,
  1131. NoStatefulFiltering: opt.NewBool(true),
  1132. },
  1133. wantJustEditMP: &ipn.MaskedPrefs{
  1134. RunSSHSet: true,
  1135. WantRunningSet: true,
  1136. },
  1137. checkUpdatePrefsMutations: func(t *testing.T, newPrefs *ipn.Prefs) {
  1138. if !newPrefs.RunSSH {
  1139. t.Errorf("RunSSH not set to true")
  1140. }
  1141. },
  1142. env: upCheckEnv{backendState: "Running"},
  1143. wantErrSubtr: "aborted, no changes made",
  1144. },
  1145. {
  1146. name: "enable_ssh_over_ssh_no_risk",
  1147. flags: []string{"--ssh=true"},
  1148. sshOverTailscale: true,
  1149. curPrefs: &ipn.Prefs{
  1150. ControlURL: "https://login.tailscale.com",
  1151. Persist: &persist.Persist{UserProfile: tailcfg.UserProfile{LoginName: "crawshaw.github"}},
  1152. CorpDNS: true,
  1153. NetfilterMode: preftype.NetfilterOn,
  1154. NoStatefulFiltering: opt.NewBool(true),
  1155. },
  1156. wantJustEditMP: &ipn.MaskedPrefs{
  1157. RunSSHSet: true,
  1158. WantRunningSet: true,
  1159. },
  1160. checkUpdatePrefsMutations: func(t *testing.T, newPrefs *ipn.Prefs) {
  1161. if !newPrefs.RunSSH {
  1162. t.Errorf("RunSSH not set to true")
  1163. }
  1164. },
  1165. env: upCheckEnv{backendState: "Running"},
  1166. wantErrSubtr: "aborted, no changes made",
  1167. },
  1168. {
  1169. name: "enable_ssh_over_ssh",
  1170. flags: []string{"--ssh=true", "--accept-risk=lose-ssh"},
  1171. sshOverTailscale: true,
  1172. curPrefs: &ipn.Prefs{
  1173. ControlURL: "https://login.tailscale.com",
  1174. Persist: &persist.Persist{UserProfile: tailcfg.UserProfile{LoginName: "crawshaw.github"}},
  1175. CorpDNS: true,
  1176. NetfilterMode: preftype.NetfilterOn,
  1177. NoStatefulFiltering: opt.NewBool(true),
  1178. },
  1179. wantJustEditMP: &ipn.MaskedPrefs{
  1180. RunSSHSet: true,
  1181. WantRunningSet: true,
  1182. },
  1183. checkUpdatePrefsMutations: func(t *testing.T, newPrefs *ipn.Prefs) {
  1184. if !newPrefs.RunSSH {
  1185. t.Errorf("RunSSH not set to true")
  1186. }
  1187. },
  1188. env: upCheckEnv{backendState: "Running"},
  1189. },
  1190. {
  1191. name: "disable_ssh_over_ssh",
  1192. flags: []string{"--ssh=false", "--accept-risk=lose-ssh"},
  1193. sshOverTailscale: true,
  1194. curPrefs: &ipn.Prefs{
  1195. ControlURL: "https://login.tailscale.com",
  1196. Persist: &persist.Persist{UserProfile: tailcfg.UserProfile{LoginName: "crawshaw.github"}},
  1197. CorpDNS: true,
  1198. RunSSH: true,
  1199. NetfilterMode: preftype.NetfilterOn,
  1200. NoStatefulFiltering: opt.NewBool(true),
  1201. },
  1202. wantJustEditMP: &ipn.MaskedPrefs{
  1203. RunSSHSet: true,
  1204. WantRunningSet: true,
  1205. },
  1206. checkUpdatePrefsMutations: func(t *testing.T, newPrefs *ipn.Prefs) {
  1207. if newPrefs.RunSSH {
  1208. t.Errorf("RunSSH not set to false")
  1209. }
  1210. },
  1211. env: upCheckEnv{backendState: "Running"},
  1212. },
  1213. {
  1214. name: "force_reauth_over_ssh_no_risk",
  1215. flags: []string{"--force-reauth"},
  1216. sshOverTailscale: true,
  1217. curPrefs: &ipn.Prefs{
  1218. ControlURL: "https://login.tailscale.com",
  1219. CorpDNS: true,
  1220. NetfilterMode: preftype.NetfilterOn,
  1221. NoStatefulFiltering: opt.NewBool(true),
  1222. },
  1223. env: upCheckEnv{backendState: "Running"},
  1224. wantErrSubtr: "aborted, no changes made",
  1225. },
  1226. {
  1227. name: "force_reauth_over_ssh",
  1228. flags: []string{"--force-reauth", "--accept-risk=lose-ssh"},
  1229. sshOverTailscale: true,
  1230. curPrefs: &ipn.Prefs{
  1231. ControlURL: "https://login.tailscale.com",
  1232. CorpDNS: true,
  1233. NetfilterMode: preftype.NetfilterOn,
  1234. NoStatefulFiltering: opt.NewBool(true),
  1235. },
  1236. wantJustEditMP: nil,
  1237. env: upCheckEnv{backendState: "Running"},
  1238. },
  1239. {
  1240. name: "advertise_connector",
  1241. flags: []string{"--advertise-connector"},
  1242. curPrefs: &ipn.Prefs{
  1243. ControlURL: ipn.DefaultControlURL,
  1244. CorpDNS: true,
  1245. NetfilterMode: preftype.NetfilterOn,
  1246. NoStatefulFiltering: opt.NewBool(true),
  1247. },
  1248. wantJustEditMP: &ipn.MaskedPrefs{
  1249. AppConnectorSet: true,
  1250. WantRunningSet: true,
  1251. },
  1252. env: upCheckEnv{backendState: "Running"},
  1253. checkUpdatePrefsMutations: func(t *testing.T, newPrefs *ipn.Prefs) {
  1254. if !newPrefs.AppConnector.Advertise {
  1255. t.Errorf("prefs.AppConnector.Advertise not set")
  1256. }
  1257. },
  1258. },
  1259. {
  1260. name: "no_advertise_connector",
  1261. flags: []string{"--advertise-connector=false"},
  1262. curPrefs: &ipn.Prefs{
  1263. ControlURL: ipn.DefaultControlURL,
  1264. CorpDNS: true,
  1265. NetfilterMode: preftype.NetfilterOn,
  1266. AppConnector: ipn.AppConnectorPrefs{
  1267. Advertise: true,
  1268. },
  1269. NoStatefulFiltering: opt.NewBool(true),
  1270. },
  1271. wantJustEditMP: &ipn.MaskedPrefs{
  1272. AppConnectorSet: true,
  1273. WantRunningSet: true,
  1274. },
  1275. env: upCheckEnv{backendState: "Running"},
  1276. checkUpdatePrefsMutations: func(t *testing.T, newPrefs *ipn.Prefs) {
  1277. if newPrefs.AppConnector.Advertise {
  1278. t.Errorf("prefs.AppConnector.Advertise not unset")
  1279. }
  1280. },
  1281. },
  1282. }
  1283. for _, tt := range tests {
  1284. t.Run(tt.name, func(t *testing.T) {
  1285. if tt.sshOverTailscale {
  1286. tstest.Replace(t, &getSSHClientEnvVar, func() string { return "100.100.100.100 1 1" })
  1287. } else if isSSHOverTailscale() {
  1288. // The test is being executed over a "real" tailscale SSH
  1289. // session, but sshOverTailscale is unset. Make the test appear
  1290. // as if it's not over tailscale SSH.
  1291. tstest.Replace(t, &getSSHClientEnvVar, func() string { return "" })
  1292. }
  1293. if tt.env.goos == "" {
  1294. tt.env.goos = "linux"
  1295. }
  1296. tt.env.flagSet = newUpFlagSet(tt.env.goos, &tt.env.upArgs, "up")
  1297. flags := CleanUpArgs(tt.flags)
  1298. if err := tt.env.flagSet.Parse(flags); err != nil {
  1299. t.Fatal(err)
  1300. }
  1301. newPrefs, err := prefsFromUpArgs(tt.env.upArgs, t.Logf, new(ipnstate.Status), tt.env.goos)
  1302. if err != nil {
  1303. t.Fatal(err)
  1304. }
  1305. simpleUp, justEditMP, err := updatePrefs(newPrefs, tt.curPrefs, tt.env)
  1306. if err != nil {
  1307. if tt.wantErrSubtr != "" {
  1308. if !strings.Contains(err.Error(), tt.wantErrSubtr) {
  1309. t.Fatalf("want error %q, got: %v", tt.wantErrSubtr, err)
  1310. }
  1311. return
  1312. }
  1313. t.Fatal(err)
  1314. } else if tt.wantErrSubtr != "" {
  1315. t.Fatalf("want error %q, got nil", tt.wantErrSubtr)
  1316. }
  1317. if tt.checkUpdatePrefsMutations != nil {
  1318. tt.checkUpdatePrefsMutations(t, newPrefs)
  1319. }
  1320. if simpleUp != tt.wantSimpleUp {
  1321. t.Fatalf("simpleUp=%v, want %v", simpleUp, tt.wantSimpleUp)
  1322. }
  1323. var oldEditPrefs ipn.Prefs
  1324. if justEditMP != nil {
  1325. oldEditPrefs = justEditMP.Prefs
  1326. justEditMP.Prefs = ipn.Prefs{} // uninteresting
  1327. }
  1328. if !reflect.DeepEqual(justEditMP, tt.wantJustEditMP) {
  1329. t.Logf("justEditMP != wantJustEditMP; following diff omits the Prefs field, which was \n%v", logger.AsJSON(oldEditPrefs))
  1330. t.Fatalf("justEditMP: %v\n\n: ", cmp.Diff(justEditMP, tt.wantJustEditMP, cmpIP))
  1331. }
  1332. })
  1333. }
  1334. }
  1335. var cmpIP = cmp.Comparer(func(a, b netip.Addr) bool {
  1336. return a == b
  1337. })
  1338. func TestCleanUpArgs(t *testing.T) {
  1339. c := qt.New(t)
  1340. tests := []struct {
  1341. in []string
  1342. want []string
  1343. }{
  1344. {in: []string{"something"}, want: []string{"something"}},
  1345. {in: []string{}, want: []string{}},
  1346. {in: []string{"--authkey=0"}, want: []string{"--auth-key=0"}},
  1347. {in: []string{"a", "--authkey=1", "b"}, want: []string{"a", "--auth-key=1", "b"}},
  1348. {in: []string{"a", "--auth-key=2", "b"}, want: []string{"a", "--auth-key=2", "b"}},
  1349. {in: []string{"a", "-authkey=3", "b"}, want: []string{"a", "--auth-key=3", "b"}},
  1350. {in: []string{"a", "-auth-key=4", "b"}, want: []string{"a", "-auth-key=4", "b"}},
  1351. {in: []string{"a", "--authkey", "5", "b"}, want: []string{"a", "--auth-key", "5", "b"}},
  1352. {in: []string{"a", "-authkey", "6", "b"}, want: []string{"a", "--auth-key", "6", "b"}},
  1353. {in: []string{"a", "authkey", "7", "b"}, want: []string{"a", "authkey", "7", "b"}},
  1354. {in: []string{"--authkeyexpiry", "8"}, want: []string{"--authkeyexpiry", "8"}},
  1355. {in: []string{"--auth-key-expiry", "9"}, want: []string{"--auth-key-expiry", "9"}},
  1356. }
  1357. for _, tt := range tests {
  1358. got := CleanUpArgs(tt.in)
  1359. c.Assert(got, qt.DeepEquals, tt.want)
  1360. }
  1361. }
  1362. func TestUpWorthWarning(t *testing.T) {
  1363. if !upWorthyWarning(healthmsg.WarnAcceptRoutesOff) {
  1364. t.Errorf("WarnAcceptRoutesOff of %q should be worth warning", healthmsg.WarnAcceptRoutesOff)
  1365. }
  1366. if !upWorthyWarning(healthmsg.TailscaleSSHOnBut + "some problem") {
  1367. t.Errorf("want true for SSH problems")
  1368. }
  1369. if upWorthyWarning("not in map poll") {
  1370. t.Errorf("want false for other misc errors")
  1371. }
  1372. }
  1373. func TestParseNLArgs(t *testing.T) {
  1374. tcs := []struct {
  1375. name string
  1376. input []string
  1377. parseKeys bool
  1378. parseDisablements bool
  1379. wantErr error
  1380. wantKeys []tka.Key
  1381. wantDisablements [][]byte
  1382. }{
  1383. {
  1384. name: "empty",
  1385. input: nil,
  1386. parseKeys: true,
  1387. parseDisablements: true,
  1388. },
  1389. {
  1390. name: "key no votes",
  1391. input: []string{"nlpub:" + strings.Repeat("00", 32)},
  1392. parseKeys: true,
  1393. wantKeys: []tka.Key{{Kind: tka.Key25519, Votes: 1, Public: bytes.Repeat([]byte{0}, 32)}},
  1394. },
  1395. {
  1396. name: "key with votes",
  1397. input: []string{"nlpub:" + strings.Repeat("01", 32) + "?5"},
  1398. parseKeys: true,
  1399. wantKeys: []tka.Key{{Kind: tka.Key25519, Votes: 5, Public: bytes.Repeat([]byte{1}, 32)}},
  1400. },
  1401. {
  1402. name: "disablements",
  1403. input: []string{"disablement:" + strings.Repeat("02", 32), "disablement-secret:" + strings.Repeat("03", 32)},
  1404. parseDisablements: true,
  1405. wantDisablements: [][]byte{bytes.Repeat([]byte{2}, 32), bytes.Repeat([]byte{3}, 32)},
  1406. },
  1407. {
  1408. name: "disablements not allowed",
  1409. input: []string{"disablement:" + strings.Repeat("02", 32)},
  1410. parseKeys: true,
  1411. wantErr: fmt.Errorf("parsing key 1: key hex string doesn't have expected type prefix tlpub:"),
  1412. },
  1413. {
  1414. name: "keys not allowed",
  1415. input: []string{"nlpub:" + strings.Repeat("02", 32)},
  1416. parseDisablements: true,
  1417. wantErr: fmt.Errorf("parsing argument 1: expected value with \"disablement:\" or \"disablement-secret:\" prefix, got %q", "nlpub:0202020202020202020202020202020202020202020202020202020202020202"),
  1418. },
  1419. }
  1420. for _, tc := range tcs {
  1421. t.Run(tc.name, func(t *testing.T) {
  1422. keys, disablements, err := parseNLArgs(tc.input, tc.parseKeys, tc.parseDisablements)
  1423. if (tc.wantErr == nil && err != nil) ||
  1424. (tc.wantErr != nil && err == nil) ||
  1425. (tc.wantErr != nil && err != nil && tc.wantErr.Error() != err.Error()) {
  1426. t.Fatalf("parseNLArgs(%v).err = %v, want %v", tc.input, err, tc.wantErr)
  1427. }
  1428. if !reflect.DeepEqual(keys, tc.wantKeys) {
  1429. t.Errorf("keys = %v, want %v", keys, tc.wantKeys)
  1430. }
  1431. if !reflect.DeepEqual(disablements, tc.wantDisablements) {
  1432. t.Errorf("disablements = %v, want %v", disablements, tc.wantDisablements)
  1433. }
  1434. })
  1435. }
  1436. }