prefs.go 38 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164
  1. // Copyright (c) Tailscale Inc & AUTHORS
  2. // SPDX-License-Identifier: BSD-3-Clause
  3. package ipn
  4. import (
  5. "bytes"
  6. "cmp"
  7. "encoding/json"
  8. "errors"
  9. "fmt"
  10. "log"
  11. "net/netip"
  12. "os"
  13. "path/filepath"
  14. "reflect"
  15. "runtime"
  16. "slices"
  17. "strings"
  18. "tailscale.com/atomicfile"
  19. "tailscale.com/drive"
  20. "tailscale.com/feature/buildfeatures"
  21. "tailscale.com/ipn/ipnstate"
  22. "tailscale.com/net/netaddr"
  23. "tailscale.com/net/tsaddr"
  24. "tailscale.com/tailcfg"
  25. "tailscale.com/types/opt"
  26. "tailscale.com/types/persist"
  27. "tailscale.com/types/preftype"
  28. "tailscale.com/types/views"
  29. "tailscale.com/util/dnsname"
  30. "tailscale.com/util/syspolicy/pkey"
  31. "tailscale.com/util/syspolicy/policyclient"
  32. "tailscale.com/version"
  33. )
  34. // DefaultControlURL is the URL base of the control plane
  35. // ("coordination server") for use when no explicit one is configured.
  36. // The default control plane is the hosted version run by Tailscale.com.
  37. const DefaultControlURL = "https://controlplane.tailscale.com"
  38. var (
  39. // ErrExitNodeIDAlreadySet is returned from (*Prefs).SetExitNodeIP when the
  40. // Prefs.ExitNodeID field is already set.
  41. ErrExitNodeIDAlreadySet = errors.New("cannot set ExitNodeIP when ExitNodeID is already set")
  42. )
  43. // IsLoginServerSynonym reports whether a URL is a drop-in replacement
  44. // for the primary Tailscale login server.
  45. func IsLoginServerSynonym(val any) bool {
  46. return val == "https://login.tailscale.com" || val == "https://controlplane.tailscale.com"
  47. }
  48. // Prefs are the user modifiable settings of the Tailscale node agent.
  49. // When you add a Pref to this struct, remember to add a corresponding
  50. // field in MaskedPrefs, and check your field for equality in Prefs.Equals().
  51. type Prefs struct {
  52. // ControlURL is the URL of the control server to use.
  53. //
  54. // If empty, the default for new installs, DefaultControlURL
  55. // is used. It's set non-empty once the daemon has been started
  56. // for the first time.
  57. //
  58. // TODO(apenwarr): Make it safe to update this with EditPrefs().
  59. // Right now, you have to pass it in the initial prefs in Start(),
  60. // which is the only code that actually uses the ControlURL value.
  61. // It would be more consistent to restart controlclient
  62. // automatically whenever this variable changes.
  63. //
  64. // Meanwhile, you have to provide this as part of
  65. // Options.LegacyMigrationPrefs or Options.UpdatePrefs when
  66. // calling Backend.Start().
  67. ControlURL string
  68. // RouteAll specifies whether to accept subnets advertised by
  69. // other nodes on the Tailscale network. Note that this does not
  70. // include default routes (0.0.0.0/0 and ::/0), those are
  71. // controlled by ExitNodeID/IP below.
  72. RouteAll bool
  73. // ExitNodeID and ExitNodeIP specify the node that should be used
  74. // as an exit node for internet traffic. At most one of these
  75. // should be non-zero.
  76. //
  77. // The preferred way to express the chosen node is ExitNodeID, but
  78. // in some cases it's not possible to use that ID (e.g. in the
  79. // linux CLI, before tailscaled has a netmap). For those
  80. // situations, we allow specifying the exit node by IP, and
  81. // ipnlocal.LocalBackend will translate the IP into an ID when the
  82. // node is found in the netmap.
  83. //
  84. // If the selected exit node doesn't exist (e.g. it's not part of
  85. // the current tailnet), or it doesn't offer exit node services, a
  86. // blackhole route will be installed on the local system to
  87. // prevent any traffic escaping to the local network.
  88. ExitNodeID tailcfg.StableNodeID
  89. ExitNodeIP netip.Addr
  90. // AutoExitNode is an optional expression that specifies whether and how
  91. // tailscaled should pick an exit node automatically.
  92. //
  93. // If specified, tailscaled will use an exit node based on the expression,
  94. // and will re-evaluate the selection periodically as network conditions,
  95. // available exit nodes, or policy settings change. A blackhole route will
  96. // be installed to prevent traffic from escaping to the local network until
  97. // an exit node is selected. It takes precedence over ExitNodeID and ExitNodeIP.
  98. //
  99. // If empty, tailscaled will not automatically select an exit node.
  100. //
  101. // If the specified expression is invalid or unsupported by the client,
  102. // it falls back to the behavior of [AnyExitNode].
  103. //
  104. // As of 2025-07-02, the only supported value is [AnyExitNode].
  105. // It's a string rather than a boolean to allow future extensibility
  106. // (e.g., AutoExitNode = "mullvad" or AutoExitNode = "geo:us").
  107. AutoExitNode ExitNodeExpression `json:",omitempty"`
  108. // InternalExitNodePrior is the most recently used ExitNodeID in string form. It is set by
  109. // the backend on transition from exit node on to off and used by the
  110. // backend.
  111. //
  112. // As an Internal field, it can't be set by LocalAPI clients, rather it is set indirectly
  113. // when the ExitNodeID value is zero'd and via the set-use-exit-node-enabled endpoint.
  114. InternalExitNodePrior tailcfg.StableNodeID
  115. // ExitNodeAllowLANAccess indicates whether locally accessible subnets should be
  116. // routed directly or via the exit node.
  117. ExitNodeAllowLANAccess bool
  118. // CorpDNS specifies whether to install the Tailscale network's
  119. // DNS configuration, if it exists.
  120. CorpDNS bool
  121. // RunSSH bool is whether this node should run an SSH
  122. // server, permitting access to peers according to the
  123. // policies as configured by the Tailnet's admin(s).
  124. RunSSH bool
  125. // RunWebClient bool is whether this node should expose
  126. // its web client over Tailscale at port 5252,
  127. // permitting access to peers according to the
  128. // policies as configured by the Tailnet's admin(s).
  129. RunWebClient bool
  130. // WantRunning indicates whether networking should be active on
  131. // this node.
  132. WantRunning bool
  133. // LoggedOut indicates whether the user intends to be logged out.
  134. // There are other reasons we may be logged out, including no valid
  135. // keys.
  136. // We need to remember this state so that, on next startup, we can
  137. // generate the "Login" vs "Connect" buttons correctly, without having
  138. // to contact the server to confirm our nodekey status first.
  139. LoggedOut bool
  140. // ShieldsUp indicates whether to block all incoming connections,
  141. // regardless of the control-provided packet filter. If false, we
  142. // use the packet filter as provided. If true, we block incoming
  143. // connections. This overrides tailcfg.Hostinfo's ShieldsUp.
  144. ShieldsUp bool
  145. // AdvertiseTags specifies tags that should be applied to this node, for
  146. // purposes of ACL enforcement. These can be referenced from the ACL policy
  147. // document. Note that advertising a tag on the client doesn't guarantee
  148. // that the control server will allow the node to adopt that tag.
  149. AdvertiseTags []string
  150. // Hostname is the hostname to use for identifying the node. If
  151. // not set, os.Hostname is used.
  152. Hostname string
  153. // NotepadURLs is a debugging setting that opens OAuth URLs in
  154. // notepad.exe on Windows, rather than loading them in a browser.
  155. //
  156. // apenwarr 2020-04-29: Unfortunately this is still needed sometimes.
  157. // Windows' default browser setting is sometimes screwy and this helps
  158. // users narrow it down a bit.
  159. NotepadURLs bool
  160. // ForceDaemon specifies whether a platform that normally
  161. // operates in "client mode" (that is, requires an active user
  162. // logged in with the GUI app running) should keep running after the
  163. // GUI ends and/or the user logs out.
  164. //
  165. // The only current applicable platform is Windows. This
  166. // forced Windows to go into "server mode" where Tailscale is
  167. // running even with no users logged in. This might also be
  168. // used for macOS in the future. This setting has no effect
  169. // for Linux/etc, which always operate in daemon mode.
  170. ForceDaemon bool `json:"ForceDaemon,omitempty"`
  171. // Egg is a optional debug flag.
  172. Egg bool `json:",omitempty"`
  173. // The following block of options only have an effect on Linux.
  174. // AdvertiseRoutes specifies CIDR prefixes to advertise into the
  175. // Tailscale network as reachable through the current
  176. // node.
  177. AdvertiseRoutes []netip.Prefix
  178. // AdvertiseServices specifies the list of services that this
  179. // node can serve as a destination for. Note that an advertised
  180. // service must still go through the approval process from the
  181. // control server.
  182. AdvertiseServices []string
  183. // Sync is whether this node should sync its configuration from
  184. // the control plane. If unset, this defaults to true.
  185. // This exists primarily for testing, to verify that netmap caching
  186. // and offline operation work correctly.
  187. Sync opt.Bool
  188. // NoSNAT specifies whether to source NAT traffic going to
  189. // destinations in AdvertiseRoutes. The default is to apply source
  190. // NAT, which makes the traffic appear to come from the router
  191. // machine rather than the peer's Tailscale IP.
  192. //
  193. // Disabling SNAT requires additional manual configuration in your
  194. // network to route Tailscale traffic back to the subnet relay
  195. // machine.
  196. //
  197. // Linux-only.
  198. NoSNAT bool
  199. // NoStatefulFiltering specifies whether to apply stateful filtering when
  200. // advertising routes in AdvertiseRoutes. The default is to not apply
  201. // stateful filtering.
  202. //
  203. // To allow inbound connections from advertised routes, both NoSNAT and
  204. // NoStatefulFiltering must be true.
  205. //
  206. // This is an opt.Bool because it was first added after NoSNAT, with a
  207. // backfill based on the value of that parameter. The backfill has been
  208. // removed since then, but the field remains an opt.Bool.
  209. //
  210. // Linux-only.
  211. NoStatefulFiltering opt.Bool `json:",omitempty"`
  212. // NetfilterMode specifies how much to manage netfilter rules for
  213. // Tailscale, if at all.
  214. NetfilterMode preftype.NetfilterMode
  215. // OperatorUser is the local machine user name who is allowed to
  216. // operate tailscaled without being root or using sudo.
  217. OperatorUser string `json:",omitempty"`
  218. // ProfileName is the desired name of the profile. If empty, then the user's
  219. // LoginName is used. It is only used for display purposes in the client UI
  220. // and CLI.
  221. ProfileName string `json:",omitempty"`
  222. // AutoUpdate sets the auto-update preferences for the node agent. See
  223. // AutoUpdatePrefs docs for more details.
  224. AutoUpdate AutoUpdatePrefs
  225. // AppConnector sets the app connector preferences for the node agent. See
  226. // AppConnectorPrefs docs for more details.
  227. AppConnector AppConnectorPrefs
  228. // PostureChecking enables the collection of information used for device
  229. // posture checks.
  230. //
  231. // Note: this should be named ReportPosture, but it was shipped as
  232. // PostureChecking in some early releases and this JSON field is written to
  233. // disk, so we just keep its old name. (akin to CorpDNS which is an internal
  234. // pref name that doesn't match the public interface)
  235. PostureChecking bool
  236. // NetfilterKind specifies what netfilter implementation to use.
  237. //
  238. // It can be "iptables", "nftables", or "" to auto-detect.
  239. //
  240. // Linux-only.
  241. NetfilterKind string
  242. // DriveShares are the configured DriveShares, stored in increasing order
  243. // by name.
  244. DriveShares []*drive.Share
  245. // RelayServerPort is the UDP port number for the relay server to bind to,
  246. // on all interfaces. A non-nil zero value signifies a random unused port
  247. // should be used. A nil value signifies relay server functionality
  248. // should be disabled.
  249. RelayServerPort *uint16 `json:",omitempty"`
  250. // RelayServerStaticEndpoints are static IP:port endpoints to advertise as
  251. // candidates for relay connections. Only relevant when RelayServerPort is
  252. // non-nil.
  253. RelayServerStaticEndpoints []netip.AddrPort `json:",omitempty"`
  254. // AllowSingleHosts was a legacy field that was always true
  255. // for the past 4.5 years. It controlled whether Tailscale
  256. // peers got /32 or /128 routes for each other.
  257. // As of 2024-05-17 we're starting to ignore it, but to let
  258. // people still downgrade Tailscale versions and not break
  259. // all peer-to-peer networking we still write it to disk (as JSON)
  260. // so it can be loaded back by old versions.
  261. // TODO(bradfitz): delete this in 2025 sometime. See #12058.
  262. AllowSingleHosts marshalAsTrueInJSON
  263. // The Persist field is named 'Config' in the file for backward
  264. // compatibility with earlier versions.
  265. // TODO(apenwarr): We should move this out of here, it's not a pref.
  266. // We can maybe do that once we're sure which module should persist
  267. // it (backend or frontend?)
  268. Persist *persist.Persist `json:"Config"`
  269. }
  270. // AutoUpdatePrefs are the auto update settings for the node agent.
  271. type AutoUpdatePrefs struct {
  272. // Check specifies whether background checks for updates are enabled. When
  273. // enabled, tailscaled will periodically check for available updates and
  274. // notify the user about them.
  275. Check bool
  276. // Apply specifies whether background auto-updates are enabled. When
  277. // enabled, tailscaled will apply available updates in the background.
  278. // Check must also be set when Apply is set.
  279. Apply opt.Bool
  280. }
  281. func (au1 AutoUpdatePrefs) Equals(au2 AutoUpdatePrefs) bool {
  282. // This could almost be as easy as `au1.Apply == au2.Apply`, except that
  283. // opt.Bool("") and opt.Bool("unset") should be treated as equal.
  284. apply1, ok1 := au1.Apply.Get()
  285. apply2, ok2 := au2.Apply.Get()
  286. return au1.Check == au2.Check &&
  287. apply1 == apply2 &&
  288. ok1 == ok2
  289. }
  290. type marshalAsTrueInJSON struct{}
  291. var trueJSON = []byte("true")
  292. func (marshalAsTrueInJSON) MarshalJSON() ([]byte, error) { return trueJSON, nil }
  293. func (*marshalAsTrueInJSON) UnmarshalJSON([]byte) error { return nil }
  294. // AppConnectorPrefs are the app connector settings for the node agent.
  295. type AppConnectorPrefs struct {
  296. // Advertise specifies whether the app connector subsystem is advertising
  297. // this node as a connector.
  298. Advertise bool
  299. }
  300. // MaskedPrefs is a Prefs with an associated bitmask of which fields are set.
  301. //
  302. // Each FooSet field maps to a corresponding Foo field in Prefs. FooSet can be
  303. // a struct, in which case inner fields of FooSet map to inner fields of Foo in
  304. // Prefs (see AutoUpdateSet for example).
  305. type MaskedPrefs struct {
  306. Prefs
  307. ControlURLSet bool `json:",omitempty"`
  308. RouteAllSet bool `json:",omitempty"`
  309. ExitNodeIDSet bool `json:",omitempty"`
  310. ExitNodeIPSet bool `json:",omitempty"`
  311. AutoExitNodeSet bool `json:",omitempty"`
  312. InternalExitNodePriorSet bool `json:",omitempty"` // Internal; can't be set by LocalAPI clients
  313. ExitNodeAllowLANAccessSet bool `json:",omitempty"`
  314. CorpDNSSet bool `json:",omitempty"`
  315. RunSSHSet bool `json:",omitempty"`
  316. RunWebClientSet bool `json:",omitempty"`
  317. WantRunningSet bool `json:",omitempty"`
  318. LoggedOutSet bool `json:",omitempty"`
  319. ShieldsUpSet bool `json:",omitempty"`
  320. AdvertiseTagsSet bool `json:",omitempty"`
  321. HostnameSet bool `json:",omitempty"`
  322. NotepadURLsSet bool `json:",omitempty"`
  323. ForceDaemonSet bool `json:",omitempty"`
  324. EggSet bool `json:",omitempty"`
  325. AdvertiseRoutesSet bool `json:",omitempty"`
  326. AdvertiseServicesSet bool `json:",omitempty"`
  327. SyncSet bool `json:",omitzero"`
  328. NoSNATSet bool `json:",omitempty"`
  329. NoStatefulFilteringSet bool `json:",omitempty"`
  330. NetfilterModeSet bool `json:",omitempty"`
  331. OperatorUserSet bool `json:",omitempty"`
  332. ProfileNameSet bool `json:",omitempty"`
  333. AutoUpdateSet AutoUpdatePrefsMask `json:",omitzero"`
  334. AppConnectorSet bool `json:",omitempty"`
  335. PostureCheckingSet bool `json:",omitempty"`
  336. NetfilterKindSet bool `json:",omitempty"`
  337. DriveSharesSet bool `json:",omitempty"`
  338. RelayServerPortSet bool `json:",omitempty"`
  339. RelayServerStaticEndpointsSet bool `json:",omitzero"`
  340. }
  341. // SetsInternal reports whether mp has any of the Internal*Set field bools set
  342. // to true.
  343. func (mp *MaskedPrefs) SetsInternal() bool {
  344. return mp.InternalExitNodePriorSet
  345. }
  346. type AutoUpdatePrefsMask struct {
  347. CheckSet bool `json:",omitempty"`
  348. ApplySet bool `json:",omitempty"`
  349. }
  350. func (m AutoUpdatePrefsMask) Pretty(au AutoUpdatePrefs) string {
  351. var fields []string
  352. if m.CheckSet {
  353. fields = append(fields, fmt.Sprintf("Check=%v", au.Check))
  354. }
  355. if m.ApplySet {
  356. fields = append(fields, fmt.Sprintf("Apply=%v", au.Apply))
  357. }
  358. return strings.Join(fields, " ")
  359. }
  360. // ApplyEdits mutates p, assigning fields from m.Prefs for each MaskedPrefs
  361. // Set field that's true.
  362. func (p *Prefs) ApplyEdits(m *MaskedPrefs) {
  363. if p == nil {
  364. panic("can't edit nil Prefs")
  365. }
  366. pv := reflect.ValueOf(p).Elem()
  367. mv := reflect.ValueOf(m).Elem()
  368. mpv := reflect.ValueOf(&m.Prefs).Elem()
  369. applyPrefsEdits(mpv, pv, maskFields(mv))
  370. }
  371. func applyPrefsEdits(src, dst reflect.Value, mask map[string]reflect.Value) {
  372. for n, m := range mask {
  373. switch m.Kind() {
  374. case reflect.Bool:
  375. if m.Bool() {
  376. dst.FieldByName(n).Set(src.FieldByName(n))
  377. }
  378. case reflect.Struct:
  379. applyPrefsEdits(src.FieldByName(n), dst.FieldByName(n), maskFields(m))
  380. default:
  381. panic(fmt.Sprintf("unsupported mask field kind %v", m.Kind()))
  382. }
  383. }
  384. }
  385. func maskFields(v reflect.Value) map[string]reflect.Value {
  386. mask := make(map[string]reflect.Value)
  387. for i := range v.NumField() {
  388. f := v.Type().Field(i).Name
  389. if !strings.HasSuffix(f, "Set") {
  390. continue
  391. }
  392. mask[strings.TrimSuffix(f, "Set")] = v.Field(i)
  393. }
  394. return mask
  395. }
  396. // IsEmpty reports whether there are no masks set or if m is nil.
  397. func (m *MaskedPrefs) IsEmpty() bool {
  398. if m == nil {
  399. return true
  400. }
  401. mv := reflect.ValueOf(m).Elem()
  402. fields := mv.NumField()
  403. for i := 1; i < fields; i++ {
  404. if !mv.Field(i).IsZero() {
  405. return false
  406. }
  407. }
  408. return true
  409. }
  410. func (m *MaskedPrefs) Pretty() string {
  411. if m == nil {
  412. return "MaskedPrefs{<nil>}"
  413. }
  414. var sb strings.Builder
  415. sb.WriteString("MaskedPrefs{")
  416. mv := reflect.ValueOf(m).Elem()
  417. mt := mv.Type()
  418. mpv := reflect.ValueOf(&m.Prefs).Elem()
  419. first := true
  420. format := func(v reflect.Value) string {
  421. switch v.Type().Kind() {
  422. case reflect.String:
  423. return "%s=%q"
  424. case reflect.Slice:
  425. // []string
  426. if v.Type().Elem().Kind() == reflect.String {
  427. return "%s=%q"
  428. }
  429. case reflect.Struct:
  430. return "%s=%+v"
  431. case reflect.Pointer:
  432. if v.Type().Elem().Kind() == reflect.Struct {
  433. return "%s=%+v"
  434. }
  435. }
  436. return "%s=%v"
  437. }
  438. for i := 1; i < mt.NumField(); i++ {
  439. name := mt.Field(i).Name
  440. mf := mv.Field(i)
  441. switch mf.Kind() {
  442. case reflect.Bool:
  443. if mf.Bool() {
  444. if !first {
  445. sb.WriteString(" ")
  446. }
  447. first = false
  448. f := mpv.Field(i - 1)
  449. fmt.Fprintf(&sb, format(f),
  450. strings.TrimSuffix(name, "Set"),
  451. f.Interface())
  452. }
  453. case reflect.Struct:
  454. if mf.IsZero() {
  455. continue
  456. }
  457. mpf := mpv.Field(i - 1)
  458. // This would be much simpler with reflect.MethodByName("Pretty"),
  459. // but using MethodByName disables some linker optimizations and
  460. // makes our binaries much larger. See
  461. // https://github.com/tailscale/tailscale/issues/10627#issuecomment-1861211945
  462. //
  463. // Instead, have this explicit switch by field name to do type
  464. // assertions.
  465. switch name {
  466. case "AutoUpdateSet":
  467. p := mf.Interface().(AutoUpdatePrefsMask).Pretty(mpf.Interface().(AutoUpdatePrefs))
  468. fmt.Fprintf(&sb, "%s={%s}", strings.TrimSuffix(name, "Set"), p)
  469. default:
  470. panic(fmt.Sprintf("unexpected MaskedPrefs field %q", name))
  471. }
  472. }
  473. }
  474. sb.WriteString("}")
  475. return sb.String()
  476. }
  477. // IsEmpty reports whether p is nil or pointing to a Prefs zero value.
  478. func (p *Prefs) IsEmpty() bool { return p == nil || p.Equals(&Prefs{}) }
  479. func (p PrefsView) Pretty() string { return p.ж.Pretty() }
  480. func (p *Prefs) Pretty() string { return p.pretty(runtime.GOOS) }
  481. func (p *Prefs) pretty(goos string) string {
  482. var sb strings.Builder
  483. sb.WriteString("Prefs{")
  484. if buildfeatures.HasUseRoutes {
  485. fmt.Fprintf(&sb, "ra=%v ", p.RouteAll)
  486. }
  487. if buildfeatures.HasDNS {
  488. fmt.Fprintf(&sb, "dns=%v want=%v ", p.CorpDNS, p.WantRunning)
  489. }
  490. if buildfeatures.HasSSH && p.RunSSH {
  491. sb.WriteString("ssh=true ")
  492. }
  493. if buildfeatures.HasWebClient && p.RunWebClient {
  494. sb.WriteString("webclient=true ")
  495. }
  496. if p.LoggedOut {
  497. sb.WriteString("loggedout=true ")
  498. }
  499. if p.Sync.EqualBool(false) {
  500. sb.WriteString("sync=false ")
  501. }
  502. if p.ForceDaemon {
  503. sb.WriteString("server=true ")
  504. }
  505. if p.NotepadURLs {
  506. sb.WriteString("notepad=true ")
  507. }
  508. if p.ShieldsUp {
  509. sb.WriteString("shields=true ")
  510. }
  511. if buildfeatures.HasUseExitNode {
  512. if p.ExitNodeIP.IsValid() {
  513. fmt.Fprintf(&sb, "exit=%v lan=%t ", p.ExitNodeIP, p.ExitNodeAllowLANAccess)
  514. } else if !p.ExitNodeID.IsZero() {
  515. fmt.Fprintf(&sb, "exit=%v lan=%t ", p.ExitNodeID, p.ExitNodeAllowLANAccess)
  516. }
  517. if p.AutoExitNode.IsSet() {
  518. fmt.Fprintf(&sb, "auto=%v ", p.AutoExitNode)
  519. }
  520. }
  521. if buildfeatures.HasAdvertiseRoutes {
  522. if len(p.AdvertiseRoutes) > 0 || goos == "linux" {
  523. fmt.Fprintf(&sb, "routes=%v ", p.AdvertiseRoutes)
  524. }
  525. if len(p.AdvertiseRoutes) > 0 || p.NoSNAT {
  526. fmt.Fprintf(&sb, "snat=%v ", !p.NoSNAT)
  527. }
  528. if len(p.AdvertiseRoutes) > 0 || p.NoStatefulFiltering.EqualBool(true) {
  529. // Only print if we're advertising any routes, or the user has
  530. // turned off stateful filtering (NoStatefulFiltering=true ⇒
  531. // StatefulFiltering=false).
  532. bb, _ := p.NoStatefulFiltering.Get()
  533. fmt.Fprintf(&sb, "statefulFiltering=%v ", !bb)
  534. }
  535. }
  536. if len(p.AdvertiseTags) > 0 {
  537. fmt.Fprintf(&sb, "tags=%s ", strings.Join(p.AdvertiseTags, ","))
  538. }
  539. if len(p.AdvertiseServices) > 0 {
  540. fmt.Fprintf(&sb, "services=%s ", strings.Join(p.AdvertiseServices, ","))
  541. }
  542. if goos == "linux" {
  543. fmt.Fprintf(&sb, "nf=%v ", p.NetfilterMode)
  544. }
  545. if p.ControlURL != "" && p.ControlURL != DefaultControlURL {
  546. fmt.Fprintf(&sb, "url=%q ", p.ControlURL)
  547. }
  548. if p.Hostname != "" {
  549. fmt.Fprintf(&sb, "host=%q ", p.Hostname)
  550. }
  551. if p.OperatorUser != "" {
  552. fmt.Fprintf(&sb, "op=%q ", p.OperatorUser)
  553. }
  554. if p.NetfilterKind != "" {
  555. fmt.Fprintf(&sb, "netfilterKind=%s ", p.NetfilterKind)
  556. }
  557. if buildfeatures.HasClientUpdate {
  558. sb.WriteString(p.AutoUpdate.Pretty())
  559. }
  560. if buildfeatures.HasAppConnectors {
  561. sb.WriteString(p.AppConnector.Pretty())
  562. }
  563. if buildfeatures.HasRelayServer && p.RelayServerPort != nil {
  564. fmt.Fprintf(&sb, "relayServerPort=%d ", *p.RelayServerPort)
  565. }
  566. if buildfeatures.HasRelayServer && len(p.RelayServerStaticEndpoints) > 0 {
  567. fmt.Fprintf(&sb, "relayServerStaticEndpoints=%v ", p.RelayServerStaticEndpoints)
  568. }
  569. if p.Persist != nil {
  570. sb.WriteString(p.Persist.Pretty())
  571. } else {
  572. sb.WriteString("Persist=nil")
  573. }
  574. sb.WriteString("}")
  575. return sb.String()
  576. }
  577. func (p PrefsView) ToBytes() []byte {
  578. return p.ж.ToBytes()
  579. }
  580. func (p *Prefs) ToBytes() []byte {
  581. data, err := json.MarshalIndent(p, "", "\t")
  582. if err != nil {
  583. log.Fatalf("Prefs marshal: %v\n", err)
  584. }
  585. return data
  586. }
  587. func (p PrefsView) Equals(p2 PrefsView) bool {
  588. return p.ж.Equals(p2.ж)
  589. }
  590. func (p *Prefs) Equals(p2 *Prefs) bool {
  591. if p == p2 {
  592. return true
  593. }
  594. if p == nil || p2 == nil {
  595. return false
  596. }
  597. return p.ControlURL == p2.ControlURL &&
  598. p.RouteAll == p2.RouteAll &&
  599. p.ExitNodeID == p2.ExitNodeID &&
  600. p.ExitNodeIP == p2.ExitNodeIP &&
  601. p.AutoExitNode == p2.AutoExitNode &&
  602. p.InternalExitNodePrior == p2.InternalExitNodePrior &&
  603. p.ExitNodeAllowLANAccess == p2.ExitNodeAllowLANAccess &&
  604. p.CorpDNS == p2.CorpDNS &&
  605. p.RunSSH == p2.RunSSH &&
  606. p.Sync.Normalized() == p2.Sync.Normalized() &&
  607. p.RunWebClient == p2.RunWebClient &&
  608. p.WantRunning == p2.WantRunning &&
  609. p.LoggedOut == p2.LoggedOut &&
  610. p.NotepadURLs == p2.NotepadURLs &&
  611. p.ShieldsUp == p2.ShieldsUp &&
  612. p.NoSNAT == p2.NoSNAT &&
  613. p.NoStatefulFiltering == p2.NoStatefulFiltering &&
  614. p.NetfilterMode == p2.NetfilterMode &&
  615. p.OperatorUser == p2.OperatorUser &&
  616. p.Hostname == p2.Hostname &&
  617. p.ForceDaemon == p2.ForceDaemon &&
  618. slices.Equal(p.AdvertiseRoutes, p2.AdvertiseRoutes) &&
  619. slices.Equal(p.AdvertiseTags, p2.AdvertiseTags) &&
  620. slices.Equal(p.AdvertiseServices, p2.AdvertiseServices) &&
  621. p.Persist.Equals(p2.Persist) &&
  622. p.ProfileName == p2.ProfileName &&
  623. p.AutoUpdate.Equals(p2.AutoUpdate) &&
  624. p.AppConnector == p2.AppConnector &&
  625. p.PostureChecking == p2.PostureChecking &&
  626. slices.EqualFunc(p.DriveShares, p2.DriveShares, drive.SharesEqual) &&
  627. p.NetfilterKind == p2.NetfilterKind &&
  628. compareUint16Ptrs(p.RelayServerPort, p2.RelayServerPort) &&
  629. slices.Equal(p.RelayServerStaticEndpoints, p2.RelayServerStaticEndpoints)
  630. }
  631. func (au AutoUpdatePrefs) Pretty() string {
  632. if au.Apply.EqualBool(true) {
  633. return "update=on "
  634. }
  635. if au.Check {
  636. return "update=check "
  637. }
  638. return "update=off "
  639. }
  640. func (ap AppConnectorPrefs) Pretty() string {
  641. if ap.Advertise {
  642. return "appconnector=advertise "
  643. }
  644. return ""
  645. }
  646. func compareUint16Ptrs(a, b *uint16) bool {
  647. if (a == nil) != (b == nil) {
  648. return false
  649. }
  650. if a == nil {
  651. return true
  652. }
  653. return *a == *b
  654. }
  655. // NewPrefs returns the default preferences to use.
  656. func NewPrefs() *Prefs {
  657. // Provide default values for options which might be missing
  658. // from the json data for any reason. The json can still
  659. // override them to false.
  660. p := &Prefs{
  661. // ControlURL is explicitly not set to signal that
  662. // it's not yet configured, which relaxes the CLI "up"
  663. // safety net features. It will get set to DefaultControlURL
  664. // on first up. Or, if not, DefaultControlURL will be used
  665. // later anyway.
  666. ControlURL: "",
  667. CorpDNS: true,
  668. WantRunning: false,
  669. NetfilterMode: preftype.NetfilterOn,
  670. NoStatefulFiltering: opt.NewBool(true),
  671. AutoUpdate: AutoUpdatePrefs{
  672. Check: true,
  673. Apply: opt.Bool("unset"),
  674. },
  675. }
  676. p.RouteAll = p.DefaultRouteAll(runtime.GOOS)
  677. return p
  678. }
  679. // ControlURLOrDefault returns the coordination server's URL base.
  680. //
  681. // If not configured, or if the configured value is a legacy name equivalent to
  682. // the default, then DefaultControlURL is returned instead.
  683. func (p PrefsView) ControlURLOrDefault(polc policyclient.Client) string {
  684. return p.ж.ControlURLOrDefault(polc)
  685. }
  686. // ControlURLOrDefault returns the coordination server's URL base.
  687. //
  688. // If not configured, or if the configured value is a legacy name equivalent to
  689. // the default, then DefaultControlURL is returned instead.
  690. func (p *Prefs) ControlURLOrDefault(polc policyclient.Client) string {
  691. controlURL, err := polc.GetString(pkey.ControlURL, p.ControlURL)
  692. if err != nil {
  693. controlURL = p.ControlURL
  694. }
  695. if controlURL != "" {
  696. if controlURL != DefaultControlURL && IsLoginServerSynonym(controlURL) {
  697. return DefaultControlURL
  698. }
  699. return controlURL
  700. }
  701. return DefaultControlURL
  702. }
  703. // DefaultRouteAll returns the default value of [Prefs.RouteAll] as a function
  704. // of the platform it's running on.
  705. func (p *Prefs) DefaultRouteAll(goos string) bool {
  706. switch goos {
  707. case "windows", "android", "ios":
  708. return true
  709. case "darwin":
  710. // Only true for macAppStore and macsys, false for darwin tailscaled.
  711. return version.IsSandboxedMacOS()
  712. default:
  713. return false
  714. }
  715. }
  716. // AdminPageURL returns the admin web site URL for the current ControlURL.
  717. func (p PrefsView) AdminPageURL(polc policyclient.Client) string { return p.ж.AdminPageURL(polc) }
  718. // AdminPageURL returns the admin web site URL for the current ControlURL.
  719. func (p *Prefs) AdminPageURL(polc policyclient.Client) string {
  720. url := p.ControlURLOrDefault(polc)
  721. if IsLoginServerSynonym(url) {
  722. // TODO(crawshaw): In future release, make this https://console.tailscale.com
  723. url = "https://login.tailscale.com"
  724. }
  725. return url + "/admin"
  726. }
  727. // AdvertisesExitNode reports whether p is advertising both the v4 and
  728. // v6 /0 exit node routes.
  729. func (p PrefsView) AdvertisesExitNode() bool { return p.ж.AdvertisesExitNode() }
  730. // AdvertisesExitNode reports whether p is advertising both the v4 and
  731. // v6 /0 exit node routes.
  732. func (p *Prefs) AdvertisesExitNode() bool {
  733. if p == nil {
  734. return false
  735. }
  736. return tsaddr.ContainsExitRoutes(views.SliceOf(p.AdvertiseRoutes))
  737. }
  738. // SetAdvertiseExitNode mutates p (if non-nil) to add or remove the two
  739. // /0 exit node routes.
  740. func (p *Prefs) SetAdvertiseExitNode(runExit bool) {
  741. if !buildfeatures.HasAdvertiseExitNode {
  742. return
  743. }
  744. if p == nil {
  745. return
  746. }
  747. all := p.AdvertiseRoutes
  748. p.AdvertiseRoutes = p.AdvertiseRoutes[:0]
  749. for _, r := range all {
  750. if r.Bits() != 0 {
  751. p.AdvertiseRoutes = append(p.AdvertiseRoutes, r)
  752. }
  753. }
  754. if !runExit {
  755. return
  756. }
  757. p.AdvertiseRoutes = append(p.AdvertiseRoutes,
  758. netip.PrefixFrom(netaddr.IPv4(0, 0, 0, 0), 0),
  759. netip.PrefixFrom(netip.IPv6Unspecified(), 0))
  760. }
  761. // peerWithTailscaleIP returns the peer in st with the provided
  762. // Tailscale IP.
  763. func peerWithTailscaleIP(st *ipnstate.Status, ip netip.Addr) (ps *ipnstate.PeerStatus, ok bool) {
  764. for _, ps := range st.Peer {
  765. for _, ip2 := range ps.TailscaleIPs {
  766. if ip == ip2 {
  767. return ps, true
  768. }
  769. }
  770. }
  771. return nil, false
  772. }
  773. func isRemoteIP(st *ipnstate.Status, ip netip.Addr) bool {
  774. for _, selfIP := range st.TailscaleIPs {
  775. if ip == selfIP {
  776. return false
  777. }
  778. }
  779. return true
  780. }
  781. // ClearExitNode sets the ExitNodeID and ExitNodeIP to their zero values.
  782. func (p *Prefs) ClearExitNode() {
  783. p.ExitNodeID = ""
  784. p.ExitNodeIP = netip.Addr{}
  785. p.AutoExitNode = ""
  786. }
  787. // ExitNodeLocalIPError is returned when the requested IP address for an exit
  788. // node belongs to the local machine.
  789. type ExitNodeLocalIPError struct {
  790. hostOrIP string
  791. }
  792. func (e ExitNodeLocalIPError) Error() string {
  793. return fmt.Sprintf("cannot use %s as an exit node as it is a local IP address to this machine", e.hostOrIP)
  794. }
  795. func exitNodeIPOfArg(s string, st *ipnstate.Status) (ip netip.Addr, err error) {
  796. if s == "" {
  797. return ip, os.ErrInvalid
  798. }
  799. ip, err = netip.ParseAddr(s)
  800. if err == nil {
  801. if !isRemoteIP(st, ip) {
  802. return ip, ExitNodeLocalIPError{s}
  803. }
  804. // If we're online already and have a netmap, double check that the IP
  805. // address specified is valid.
  806. if st.BackendState == "Running" {
  807. ps, ok := peerWithTailscaleIP(st, ip)
  808. if !ok {
  809. return ip, fmt.Errorf("no node found in netmap with IP %v", ip)
  810. }
  811. if !ps.ExitNodeOption {
  812. return ip, fmt.Errorf("node %v is not advertising an exit node", ip)
  813. }
  814. }
  815. return ip, nil
  816. }
  817. match := 0
  818. for _, ps := range st.Peer {
  819. baseName := dnsname.TrimSuffix(ps.DNSName, st.MagicDNSSuffix)
  820. if !strings.EqualFold(s, baseName) && !strings.EqualFold(s, ps.DNSName) {
  821. continue
  822. }
  823. match++
  824. if len(ps.TailscaleIPs) == 0 {
  825. return ip, fmt.Errorf("node %q has no Tailscale IP?", s)
  826. }
  827. if !ps.ExitNodeOption {
  828. return ip, fmt.Errorf("node %q is not advertising an exit node", s)
  829. }
  830. ip = ps.TailscaleIPs[0]
  831. }
  832. switch match {
  833. case 0:
  834. return ip, fmt.Errorf("invalid value %q for --exit-node; must be IP or unique node name", s)
  835. case 1:
  836. if !isRemoteIP(st, ip) {
  837. return ip, ExitNodeLocalIPError{s}
  838. }
  839. return ip, nil
  840. default:
  841. return ip, fmt.Errorf("ambiguous exit node name %q", s)
  842. }
  843. }
  844. // SetExitNodeIP validates and sets the ExitNodeIP from a user-provided string
  845. // specifying either an IP address or a MagicDNS base name ("foo", as opposed to
  846. // "foo.bar.beta.tailscale.net"). This method does not mutate ExitNodeID and
  847. // will fail if ExitNodeID is already set.
  848. func (p *Prefs) SetExitNodeIP(s string, st *ipnstate.Status) error {
  849. if !p.ExitNodeID.IsZero() {
  850. return ErrExitNodeIDAlreadySet
  851. }
  852. ip, err := exitNodeIPOfArg(s, st)
  853. if err == nil {
  854. p.ExitNodeIP = ip
  855. }
  856. return err
  857. }
  858. // ShouldSSHBeRunning reports whether the SSH server should be running based on
  859. // the prefs.
  860. func (p PrefsView) ShouldSSHBeRunning() bool {
  861. return p.Valid() && p.ж.ShouldSSHBeRunning()
  862. }
  863. // ShouldSSHBeRunning reports whether the SSH server should be running based on
  864. // the prefs.
  865. func (p *Prefs) ShouldSSHBeRunning() bool {
  866. return p.WantRunning && p.RunSSH
  867. }
  868. // ShouldWebClientBeRunning reports whether the web client server should be running based on
  869. // the prefs.
  870. func (p PrefsView) ShouldWebClientBeRunning() bool {
  871. return p.Valid() && p.ж.ShouldWebClientBeRunning()
  872. }
  873. // ShouldWebClientBeRunning reports whether the web client server should be running based on
  874. // the prefs.
  875. func (p *Prefs) ShouldWebClientBeRunning() bool {
  876. return p.WantRunning && p.RunWebClient
  877. }
  878. // PrefsFromBytes deserializes Prefs from a JSON blob b into base. Values in
  879. // base are preserved, unless they are populated in the JSON blob.
  880. func PrefsFromBytes(b []byte, base *Prefs) error {
  881. if len(b) == 0 {
  882. return nil
  883. }
  884. return json.Unmarshal(b, base)
  885. }
  886. func (p *Prefs) normalizeOptBools() {
  887. if p.Sync == opt.ExplicitlyUnset {
  888. p.Sync = ""
  889. }
  890. }
  891. var jsonEscapedZero = []byte(`\u0000`)
  892. // LoadPrefsWindows loads a legacy relaynode config file into Prefs with
  893. // sensible migration defaults set. Windows-only.
  894. func LoadPrefsWindows(filename string) (*Prefs, error) {
  895. data, err := os.ReadFile(filename)
  896. if err != nil {
  897. return nil, fmt.Errorf("LoadPrefs open: %w", err) // err includes path
  898. }
  899. if bytes.Contains(data, jsonEscapedZero) {
  900. // Tailscale 1.2.0 - 1.2.8 on Windows had a memory corruption bug
  901. // in the backend process that ended up sending NULL bytes over JSON
  902. // to the frontend which wrote them out to JSON files on disk.
  903. // So if we see one, treat is as corrupt and the user will need
  904. // to log in again. (better than crashing)
  905. return nil, os.ErrNotExist
  906. }
  907. p := NewPrefs()
  908. if err := PrefsFromBytes(data, p); err != nil {
  909. return nil, fmt.Errorf("LoadPrefs(%q) decode: %w", filename, err)
  910. }
  911. return p, nil
  912. }
  913. func SavePrefs(filename string, p *Prefs) {
  914. log.Printf("Saving prefs %v %v\n", filename, p.Pretty())
  915. data := p.ToBytes()
  916. os.MkdirAll(filepath.Dir(filename), 0700)
  917. if err := atomicfile.WriteFile(filename, data, 0600); err != nil {
  918. log.Printf("SavePrefs: %v\n", err)
  919. }
  920. }
  921. // ProfileID is an auto-generated system-wide unique identifier for a login
  922. // profile. It is a 4 character hex string like "1ab3".
  923. type ProfileID string
  924. // WindowsUserID is a userid (suitable for passing to ipnauth.LookupUserFromID
  925. // or os/user.LookupId) but only set on Windows. It's empty on all other
  926. // platforms, unless envknob.GOOS is in used, making Linux act like Windows for
  927. // tests.
  928. type WindowsUserID string
  929. // NetworkProfile is a subset of netmap.NetworkMap
  930. // that should be saved with each user profile.
  931. type NetworkProfile struct {
  932. MagicDNSName string
  933. DomainName string
  934. DisplayName string
  935. }
  936. // RequiresBackfill returns whether this object does not have all the data
  937. // expected. This is because this struct is a later addition to LoginProfile and
  938. // this method can be checked to see if it's been backfilled to the current
  939. // expectation or not. Note that for now, it just checks if the struct is empty.
  940. // In the future, if we have new optional fields, this method can be changed to
  941. // do more explicit checks to return whether it's apt for a backfill or not.
  942. func (n NetworkProfile) RequiresBackfill() bool {
  943. return n == NetworkProfile{}
  944. }
  945. // DisplayNameOrDefault will always return a non-empty string.
  946. // If there is a defined display name, it will return that.
  947. // If they did not it will default to their domain name.
  948. func (n NetworkProfile) DisplayNameOrDefault() string {
  949. return cmp.Or(n.DisplayName, n.DomainName)
  950. }
  951. // LoginProfile represents a single login profile as managed
  952. // by the ProfileManager.
  953. type LoginProfile struct {
  954. // ID is a unique identifier for this profile.
  955. // It is assigned on creation and never changes.
  956. // It may seem redundant to have both ID and UserProfile.ID
  957. // but they are different things. UserProfile.ID may change
  958. // over time (e.g. if a device is tagged).
  959. ID ProfileID
  960. // Name is the user-visible name of this profile.
  961. // It is filled in from the UserProfile.LoginName field.
  962. Name string
  963. // NetworkProfile is a subset of netmap.NetworkMap that we
  964. // store to remember information about the tailnet that this
  965. // profile was logged in with.
  966. //
  967. // This field was added on 2023-11-17.
  968. NetworkProfile NetworkProfile
  969. // Key is the StateKey under which the profile is stored.
  970. // It is assigned once at profile creation time and never changes.
  971. Key StateKey
  972. // UserProfile is the server provided UserProfile for this profile.
  973. // This is updated whenever the server provides a new UserProfile.
  974. UserProfile tailcfg.UserProfile
  975. // NodeID is the NodeID of the node that this profile is logged into.
  976. // This should be stable across tagging and untagging nodes.
  977. // It may seem redundant to check against both the UserProfile.UserID
  978. // and the NodeID. However the NodeID can change if the node is deleted
  979. // from the admin panel.
  980. NodeID tailcfg.StableNodeID
  981. // LocalUserID is the user ID of the user who created this profile.
  982. // It is only relevant on Windows where we have a multi-user system.
  983. // It is assigned once at profile creation time and never changes.
  984. LocalUserID WindowsUserID
  985. // ControlURL is the URL of the control server that this profile is logged
  986. // into.
  987. ControlURL string
  988. }
  989. // Equals reports whether p and p2 are equal.
  990. func (p LoginProfileView) Equals(p2 LoginProfileView) bool {
  991. return p.ж.Equals(p2.ж)
  992. }
  993. // Equals reports whether p and p2 are equal.
  994. func (p *LoginProfile) Equals(p2 *LoginProfile) bool {
  995. if p == p2 {
  996. return true
  997. }
  998. if p == nil || p2 == nil {
  999. return false
  1000. }
  1001. return p.ID == p2.ID &&
  1002. p.Name == p2.Name &&
  1003. p.NetworkProfile == p2.NetworkProfile &&
  1004. p.Key == p2.Key &&
  1005. p.UserProfile.Equal(&p2.UserProfile) &&
  1006. p.NodeID == p2.NodeID &&
  1007. p.LocalUserID == p2.LocalUserID &&
  1008. p.ControlURL == p2.ControlURL
  1009. }
  1010. // ExitNodeExpression is a string that specifies how an exit node
  1011. // should be selected. An empty string means that no exit node
  1012. // should be selected.
  1013. //
  1014. // As of 2025-07-02, the only supported value is [AnyExitNode].
  1015. type ExitNodeExpression string
  1016. // AnyExitNode indicates that the exit node should be automatically
  1017. // selected from the pool of available exit nodes, excluding any
  1018. // disallowed by policy (e.g., [syspolicy.AllowedSuggestedExitNodes]).
  1019. // The exact implementation is subject to change, but exit nodes
  1020. // offering the best performance will be preferred.
  1021. const AnyExitNode ExitNodeExpression = "any"
  1022. // IsSet reports whether the expression is non-empty and can be used
  1023. // to select an exit node.
  1024. func (e ExitNodeExpression) IsSet() bool {
  1025. return e != ""
  1026. }
  1027. const (
  1028. // AutoExitNodePrefix is the prefix used in [syspolicy.ExitNodeID] values and CLI
  1029. // to indicate that the string following the prefix is an [ipn.ExitNodeExpression].
  1030. AutoExitNodePrefix = "auto:"
  1031. )
  1032. // ParseAutoExitNodeString attempts to parse the given string
  1033. // as an [ExitNodeExpression].
  1034. //
  1035. // It returns the parsed expression and true on success,
  1036. // or an empty string and false if the input does not appear to be
  1037. // an [ExitNodeExpression] (i.e., it doesn't start with "auto:").
  1038. //
  1039. // It is mainly used to parse the [syspolicy.ExitNodeID] value
  1040. // when it is set to "auto:<expression>" (e.g., auto:any).
  1041. func ParseAutoExitNodeString[T ~string](s T) (_ ExitNodeExpression, ok bool) {
  1042. if expr, ok := strings.CutPrefix(string(s), AutoExitNodePrefix); ok && expr != "" {
  1043. return ExitNodeExpression(expr), true
  1044. }
  1045. return "", false
  1046. }