cli_test.go 39 KB

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