| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164 |
- // Copyright (c) Tailscale Inc & AUTHORS
- // SPDX-License-Identifier: BSD-3-Clause
- package ipn
- import (
- "bytes"
- "cmp"
- "encoding/json"
- "errors"
- "fmt"
- "log"
- "net/netip"
- "os"
- "path/filepath"
- "reflect"
- "runtime"
- "slices"
- "strings"
- "tailscale.com/atomicfile"
- "tailscale.com/drive"
- "tailscale.com/feature/buildfeatures"
- "tailscale.com/ipn/ipnstate"
- "tailscale.com/net/netaddr"
- "tailscale.com/net/tsaddr"
- "tailscale.com/tailcfg"
- "tailscale.com/types/opt"
- "tailscale.com/types/persist"
- "tailscale.com/types/preftype"
- "tailscale.com/types/views"
- "tailscale.com/util/dnsname"
- "tailscale.com/util/syspolicy/pkey"
- "tailscale.com/util/syspolicy/policyclient"
- "tailscale.com/version"
- )
- // DefaultControlURL is the URL base of the control plane
- // ("coordination server") for use when no explicit one is configured.
- // The default control plane is the hosted version run by Tailscale.com.
- const DefaultControlURL = "https://controlplane.tailscale.com"
- var (
- // ErrExitNodeIDAlreadySet is returned from (*Prefs).SetExitNodeIP when the
- // Prefs.ExitNodeID field is already set.
- ErrExitNodeIDAlreadySet = errors.New("cannot set ExitNodeIP when ExitNodeID is already set")
- )
- // IsLoginServerSynonym reports whether a URL is a drop-in replacement
- // for the primary Tailscale login server.
- func IsLoginServerSynonym(val any) bool {
- return val == "https://login.tailscale.com" || val == "https://controlplane.tailscale.com"
- }
- // Prefs are the user modifiable settings of the Tailscale node agent.
- // When you add a Pref to this struct, remember to add a corresponding
- // field in MaskedPrefs, and check your field for equality in Prefs.Equals().
- type Prefs struct {
- // ControlURL is the URL of the control server to use.
- //
- // If empty, the default for new installs, DefaultControlURL
- // is used. It's set non-empty once the daemon has been started
- // for the first time.
- //
- // TODO(apenwarr): Make it safe to update this with EditPrefs().
- // Right now, you have to pass it in the initial prefs in Start(),
- // which is the only code that actually uses the ControlURL value.
- // It would be more consistent to restart controlclient
- // automatically whenever this variable changes.
- //
- // Meanwhile, you have to provide this as part of
- // Options.LegacyMigrationPrefs or Options.UpdatePrefs when
- // calling Backend.Start().
- ControlURL string
- // RouteAll specifies whether to accept subnets advertised by
- // other nodes on the Tailscale network. Note that this does not
- // include default routes (0.0.0.0/0 and ::/0), those are
- // controlled by ExitNodeID/IP below.
- RouteAll bool
- // ExitNodeID and ExitNodeIP specify the node that should be used
- // as an exit node for internet traffic. At most one of these
- // should be non-zero.
- //
- // The preferred way to express the chosen node is ExitNodeID, but
- // in some cases it's not possible to use that ID (e.g. in the
- // linux CLI, before tailscaled has a netmap). For those
- // situations, we allow specifying the exit node by IP, and
- // ipnlocal.LocalBackend will translate the IP into an ID when the
- // node is found in the netmap.
- //
- // If the selected exit node doesn't exist (e.g. it's not part of
- // the current tailnet), or it doesn't offer exit node services, a
- // blackhole route will be installed on the local system to
- // prevent any traffic escaping to the local network.
- ExitNodeID tailcfg.StableNodeID
- ExitNodeIP netip.Addr
- // AutoExitNode is an optional expression that specifies whether and how
- // tailscaled should pick an exit node automatically.
- //
- // If specified, tailscaled will use an exit node based on the expression,
- // and will re-evaluate the selection periodically as network conditions,
- // available exit nodes, or policy settings change. A blackhole route will
- // be installed to prevent traffic from escaping to the local network until
- // an exit node is selected. It takes precedence over ExitNodeID and ExitNodeIP.
- //
- // If empty, tailscaled will not automatically select an exit node.
- //
- // If the specified expression is invalid or unsupported by the client,
- // it falls back to the behavior of [AnyExitNode].
- //
- // As of 2025-07-02, the only supported value is [AnyExitNode].
- // It's a string rather than a boolean to allow future extensibility
- // (e.g., AutoExitNode = "mullvad" or AutoExitNode = "geo:us").
- AutoExitNode ExitNodeExpression `json:",omitempty"`
- // InternalExitNodePrior is the most recently used ExitNodeID in string form. It is set by
- // the backend on transition from exit node on to off and used by the
- // backend.
- //
- // As an Internal field, it can't be set by LocalAPI clients, rather it is set indirectly
- // when the ExitNodeID value is zero'd and via the set-use-exit-node-enabled endpoint.
- InternalExitNodePrior tailcfg.StableNodeID
- // ExitNodeAllowLANAccess indicates whether locally accessible subnets should be
- // routed directly or via the exit node.
- ExitNodeAllowLANAccess bool
- // CorpDNS specifies whether to install the Tailscale network's
- // DNS configuration, if it exists.
- CorpDNS bool
- // RunSSH bool is whether this node should run an SSH
- // server, permitting access to peers according to the
- // policies as configured by the Tailnet's admin(s).
- RunSSH bool
- // RunWebClient bool is whether this node should expose
- // its web client over Tailscale at port 5252,
- // permitting access to peers according to the
- // policies as configured by the Tailnet's admin(s).
- RunWebClient bool
- // WantRunning indicates whether networking should be active on
- // this node.
- WantRunning bool
- // LoggedOut indicates whether the user intends to be logged out.
- // There are other reasons we may be logged out, including no valid
- // keys.
- // We need to remember this state so that, on next startup, we can
- // generate the "Login" vs "Connect" buttons correctly, without having
- // to contact the server to confirm our nodekey status first.
- LoggedOut bool
- // ShieldsUp indicates whether to block all incoming connections,
- // regardless of the control-provided packet filter. If false, we
- // use the packet filter as provided. If true, we block incoming
- // connections. This overrides tailcfg.Hostinfo's ShieldsUp.
- ShieldsUp bool
- // AdvertiseTags specifies tags that should be applied to this node, for
- // purposes of ACL enforcement. These can be referenced from the ACL policy
- // document. Note that advertising a tag on the client doesn't guarantee
- // that the control server will allow the node to adopt that tag.
- AdvertiseTags []string
- // Hostname is the hostname to use for identifying the node. If
- // not set, os.Hostname is used.
- Hostname string
- // NotepadURLs is a debugging setting that opens OAuth URLs in
- // notepad.exe on Windows, rather than loading them in a browser.
- //
- // apenwarr 2020-04-29: Unfortunately this is still needed sometimes.
- // Windows' default browser setting is sometimes screwy and this helps
- // users narrow it down a bit.
- NotepadURLs bool
- // ForceDaemon specifies whether a platform that normally
- // operates in "client mode" (that is, requires an active user
- // logged in with the GUI app running) should keep running after the
- // GUI ends and/or the user logs out.
- //
- // The only current applicable platform is Windows. This
- // forced Windows to go into "server mode" where Tailscale is
- // running even with no users logged in. This might also be
- // used for macOS in the future. This setting has no effect
- // for Linux/etc, which always operate in daemon mode.
- ForceDaemon bool `json:"ForceDaemon,omitempty"`
- // Egg is a optional debug flag.
- Egg bool `json:",omitempty"`
- // The following block of options only have an effect on Linux.
- // AdvertiseRoutes specifies CIDR prefixes to advertise into the
- // Tailscale network as reachable through the current
- // node.
- AdvertiseRoutes []netip.Prefix
- // AdvertiseServices specifies the list of services that this
- // node can serve as a destination for. Note that an advertised
- // service must still go through the approval process from the
- // control server.
- AdvertiseServices []string
- // Sync is whether this node should sync its configuration from
- // the control plane. If unset, this defaults to true.
- // This exists primarily for testing, to verify that netmap caching
- // and offline operation work correctly.
- Sync opt.Bool
- // NoSNAT specifies whether to source NAT traffic going to
- // destinations in AdvertiseRoutes. The default is to apply source
- // NAT, which makes the traffic appear to come from the router
- // machine rather than the peer's Tailscale IP.
- //
- // Disabling SNAT requires additional manual configuration in your
- // network to route Tailscale traffic back to the subnet relay
- // machine.
- //
- // Linux-only.
- NoSNAT bool
- // NoStatefulFiltering specifies whether to apply stateful filtering when
- // advertising routes in AdvertiseRoutes. The default is to not apply
- // stateful filtering.
- //
- // To allow inbound connections from advertised routes, both NoSNAT and
- // NoStatefulFiltering must be true.
- //
- // This is an opt.Bool because it was first added after NoSNAT, with a
- // backfill based on the value of that parameter. The backfill has been
- // removed since then, but the field remains an opt.Bool.
- //
- // Linux-only.
- NoStatefulFiltering opt.Bool `json:",omitempty"`
- // NetfilterMode specifies how much to manage netfilter rules for
- // Tailscale, if at all.
- NetfilterMode preftype.NetfilterMode
- // OperatorUser is the local machine user name who is allowed to
- // operate tailscaled without being root or using sudo.
- OperatorUser string `json:",omitempty"`
- // ProfileName is the desired name of the profile. If empty, then the user's
- // LoginName is used. It is only used for display purposes in the client UI
- // and CLI.
- ProfileName string `json:",omitempty"`
- // AutoUpdate sets the auto-update preferences for the node agent. See
- // AutoUpdatePrefs docs for more details.
- AutoUpdate AutoUpdatePrefs
- // AppConnector sets the app connector preferences for the node agent. See
- // AppConnectorPrefs docs for more details.
- AppConnector AppConnectorPrefs
- // PostureChecking enables the collection of information used for device
- // posture checks.
- //
- // Note: this should be named ReportPosture, but it was shipped as
- // PostureChecking in some early releases and this JSON field is written to
- // disk, so we just keep its old name. (akin to CorpDNS which is an internal
- // pref name that doesn't match the public interface)
- PostureChecking bool
- // NetfilterKind specifies what netfilter implementation to use.
- //
- // It can be "iptables", "nftables", or "" to auto-detect.
- //
- // Linux-only.
- NetfilterKind string
- // DriveShares are the configured DriveShares, stored in increasing order
- // by name.
- DriveShares []*drive.Share
- // RelayServerPort is the UDP port number for the relay server to bind to,
- // on all interfaces. A non-nil zero value signifies a random unused port
- // should be used. A nil value signifies relay server functionality
- // should be disabled.
- RelayServerPort *uint16 `json:",omitempty"`
- // RelayServerStaticEndpoints are static IP:port endpoints to advertise as
- // candidates for relay connections. Only relevant when RelayServerPort is
- // non-nil.
- RelayServerStaticEndpoints []netip.AddrPort `json:",omitempty"`
- // AllowSingleHosts was a legacy field that was always true
- // for the past 4.5 years. It controlled whether Tailscale
- // peers got /32 or /128 routes for each other.
- // As of 2024-05-17 we're starting to ignore it, but to let
- // people still downgrade Tailscale versions and not break
- // all peer-to-peer networking we still write it to disk (as JSON)
- // so it can be loaded back by old versions.
- // TODO(bradfitz): delete this in 2025 sometime. See #12058.
- AllowSingleHosts marshalAsTrueInJSON
- // The Persist field is named 'Config' in the file for backward
- // compatibility with earlier versions.
- // TODO(apenwarr): We should move this out of here, it's not a pref.
- // We can maybe do that once we're sure which module should persist
- // it (backend or frontend?)
- Persist *persist.Persist `json:"Config"`
- }
- // AutoUpdatePrefs are the auto update settings for the node agent.
- type AutoUpdatePrefs struct {
- // Check specifies whether background checks for updates are enabled. When
- // enabled, tailscaled will periodically check for available updates and
- // notify the user about them.
- Check bool
- // Apply specifies whether background auto-updates are enabled. When
- // enabled, tailscaled will apply available updates in the background.
- // Check must also be set when Apply is set.
- Apply opt.Bool
- }
- func (au1 AutoUpdatePrefs) Equals(au2 AutoUpdatePrefs) bool {
- // This could almost be as easy as `au1.Apply == au2.Apply`, except that
- // opt.Bool("") and opt.Bool("unset") should be treated as equal.
- apply1, ok1 := au1.Apply.Get()
- apply2, ok2 := au2.Apply.Get()
- return au1.Check == au2.Check &&
- apply1 == apply2 &&
- ok1 == ok2
- }
- type marshalAsTrueInJSON struct{}
- var trueJSON = []byte("true")
- func (marshalAsTrueInJSON) MarshalJSON() ([]byte, error) { return trueJSON, nil }
- func (*marshalAsTrueInJSON) UnmarshalJSON([]byte) error { return nil }
- // AppConnectorPrefs are the app connector settings for the node agent.
- type AppConnectorPrefs struct {
- // Advertise specifies whether the app connector subsystem is advertising
- // this node as a connector.
- Advertise bool
- }
- // MaskedPrefs is a Prefs with an associated bitmask of which fields are set.
- //
- // Each FooSet field maps to a corresponding Foo field in Prefs. FooSet can be
- // a struct, in which case inner fields of FooSet map to inner fields of Foo in
- // Prefs (see AutoUpdateSet for example).
- type MaskedPrefs struct {
- Prefs
- ControlURLSet bool `json:",omitempty"`
- RouteAllSet bool `json:",omitempty"`
- ExitNodeIDSet bool `json:",omitempty"`
- ExitNodeIPSet bool `json:",omitempty"`
- AutoExitNodeSet bool `json:",omitempty"`
- InternalExitNodePriorSet bool `json:",omitempty"` // Internal; can't be set by LocalAPI clients
- ExitNodeAllowLANAccessSet bool `json:",omitempty"`
- CorpDNSSet bool `json:",omitempty"`
- RunSSHSet bool `json:",omitempty"`
- RunWebClientSet bool `json:",omitempty"`
- WantRunningSet bool `json:",omitempty"`
- LoggedOutSet bool `json:",omitempty"`
- ShieldsUpSet bool `json:",omitempty"`
- AdvertiseTagsSet bool `json:",omitempty"`
- HostnameSet bool `json:",omitempty"`
- NotepadURLsSet bool `json:",omitempty"`
- ForceDaemonSet bool `json:",omitempty"`
- EggSet bool `json:",omitempty"`
- AdvertiseRoutesSet bool `json:",omitempty"`
- AdvertiseServicesSet bool `json:",omitempty"`
- SyncSet bool `json:",omitzero"`
- NoSNATSet bool `json:",omitempty"`
- NoStatefulFilteringSet bool `json:",omitempty"`
- NetfilterModeSet bool `json:",omitempty"`
- OperatorUserSet bool `json:",omitempty"`
- ProfileNameSet bool `json:",omitempty"`
- AutoUpdateSet AutoUpdatePrefsMask `json:",omitzero"`
- AppConnectorSet bool `json:",omitempty"`
- PostureCheckingSet bool `json:",omitempty"`
- NetfilterKindSet bool `json:",omitempty"`
- DriveSharesSet bool `json:",omitempty"`
- RelayServerPortSet bool `json:",omitempty"`
- RelayServerStaticEndpointsSet bool `json:",omitzero"`
- }
- // SetsInternal reports whether mp has any of the Internal*Set field bools set
- // to true.
- func (mp *MaskedPrefs) SetsInternal() bool {
- return mp.InternalExitNodePriorSet
- }
- type AutoUpdatePrefsMask struct {
- CheckSet bool `json:",omitempty"`
- ApplySet bool `json:",omitempty"`
- }
- func (m AutoUpdatePrefsMask) Pretty(au AutoUpdatePrefs) string {
- var fields []string
- if m.CheckSet {
- fields = append(fields, fmt.Sprintf("Check=%v", au.Check))
- }
- if m.ApplySet {
- fields = append(fields, fmt.Sprintf("Apply=%v", au.Apply))
- }
- return strings.Join(fields, " ")
- }
- // ApplyEdits mutates p, assigning fields from m.Prefs for each MaskedPrefs
- // Set field that's true.
- func (p *Prefs) ApplyEdits(m *MaskedPrefs) {
- if p == nil {
- panic("can't edit nil Prefs")
- }
- pv := reflect.ValueOf(p).Elem()
- mv := reflect.ValueOf(m).Elem()
- mpv := reflect.ValueOf(&m.Prefs).Elem()
- applyPrefsEdits(mpv, pv, maskFields(mv))
- }
- func applyPrefsEdits(src, dst reflect.Value, mask map[string]reflect.Value) {
- for n, m := range mask {
- switch m.Kind() {
- case reflect.Bool:
- if m.Bool() {
- dst.FieldByName(n).Set(src.FieldByName(n))
- }
- case reflect.Struct:
- applyPrefsEdits(src.FieldByName(n), dst.FieldByName(n), maskFields(m))
- default:
- panic(fmt.Sprintf("unsupported mask field kind %v", m.Kind()))
- }
- }
- }
- func maskFields(v reflect.Value) map[string]reflect.Value {
- mask := make(map[string]reflect.Value)
- for i := range v.NumField() {
- f := v.Type().Field(i).Name
- if !strings.HasSuffix(f, "Set") {
- continue
- }
- mask[strings.TrimSuffix(f, "Set")] = v.Field(i)
- }
- return mask
- }
- // IsEmpty reports whether there are no masks set or if m is nil.
- func (m *MaskedPrefs) IsEmpty() bool {
- if m == nil {
- return true
- }
- mv := reflect.ValueOf(m).Elem()
- fields := mv.NumField()
- for i := 1; i < fields; i++ {
- if !mv.Field(i).IsZero() {
- return false
- }
- }
- return true
- }
- func (m *MaskedPrefs) Pretty() string {
- if m == nil {
- return "MaskedPrefs{<nil>}"
- }
- var sb strings.Builder
- sb.WriteString("MaskedPrefs{")
- mv := reflect.ValueOf(m).Elem()
- mt := mv.Type()
- mpv := reflect.ValueOf(&m.Prefs).Elem()
- first := true
- format := func(v reflect.Value) string {
- switch v.Type().Kind() {
- case reflect.String:
- return "%s=%q"
- case reflect.Slice:
- // []string
- if v.Type().Elem().Kind() == reflect.String {
- return "%s=%q"
- }
- case reflect.Struct:
- return "%s=%+v"
- case reflect.Pointer:
- if v.Type().Elem().Kind() == reflect.Struct {
- return "%s=%+v"
- }
- }
- return "%s=%v"
- }
- for i := 1; i < mt.NumField(); i++ {
- name := mt.Field(i).Name
- mf := mv.Field(i)
- switch mf.Kind() {
- case reflect.Bool:
- if mf.Bool() {
- if !first {
- sb.WriteString(" ")
- }
- first = false
- f := mpv.Field(i - 1)
- fmt.Fprintf(&sb, format(f),
- strings.TrimSuffix(name, "Set"),
- f.Interface())
- }
- case reflect.Struct:
- if mf.IsZero() {
- continue
- }
- mpf := mpv.Field(i - 1)
- // This would be much simpler with reflect.MethodByName("Pretty"),
- // but using MethodByName disables some linker optimizations and
- // makes our binaries much larger. See
- // https://github.com/tailscale/tailscale/issues/10627#issuecomment-1861211945
- //
- // Instead, have this explicit switch by field name to do type
- // assertions.
- switch name {
- case "AutoUpdateSet":
- p := mf.Interface().(AutoUpdatePrefsMask).Pretty(mpf.Interface().(AutoUpdatePrefs))
- fmt.Fprintf(&sb, "%s={%s}", strings.TrimSuffix(name, "Set"), p)
- default:
- panic(fmt.Sprintf("unexpected MaskedPrefs field %q", name))
- }
- }
- }
- sb.WriteString("}")
- return sb.String()
- }
- // IsEmpty reports whether p is nil or pointing to a Prefs zero value.
- func (p *Prefs) IsEmpty() bool { return p == nil || p.Equals(&Prefs{}) }
- func (p PrefsView) Pretty() string { return p.ж.Pretty() }
- func (p *Prefs) Pretty() string { return p.pretty(runtime.GOOS) }
- func (p *Prefs) pretty(goos string) string {
- var sb strings.Builder
- sb.WriteString("Prefs{")
- if buildfeatures.HasUseRoutes {
- fmt.Fprintf(&sb, "ra=%v ", p.RouteAll)
- }
- if buildfeatures.HasDNS {
- fmt.Fprintf(&sb, "dns=%v want=%v ", p.CorpDNS, p.WantRunning)
- }
- if buildfeatures.HasSSH && p.RunSSH {
- sb.WriteString("ssh=true ")
- }
- if buildfeatures.HasWebClient && p.RunWebClient {
- sb.WriteString("webclient=true ")
- }
- if p.LoggedOut {
- sb.WriteString("loggedout=true ")
- }
- if p.Sync.EqualBool(false) {
- sb.WriteString("sync=false ")
- }
- if p.ForceDaemon {
- sb.WriteString("server=true ")
- }
- if p.NotepadURLs {
- sb.WriteString("notepad=true ")
- }
- if p.ShieldsUp {
- sb.WriteString("shields=true ")
- }
- if buildfeatures.HasUseExitNode {
- if p.ExitNodeIP.IsValid() {
- fmt.Fprintf(&sb, "exit=%v lan=%t ", p.ExitNodeIP, p.ExitNodeAllowLANAccess)
- } else if !p.ExitNodeID.IsZero() {
- fmt.Fprintf(&sb, "exit=%v lan=%t ", p.ExitNodeID, p.ExitNodeAllowLANAccess)
- }
- if p.AutoExitNode.IsSet() {
- fmt.Fprintf(&sb, "auto=%v ", p.AutoExitNode)
- }
- }
- if buildfeatures.HasAdvertiseRoutes {
- if len(p.AdvertiseRoutes) > 0 || goos == "linux" {
- fmt.Fprintf(&sb, "routes=%v ", p.AdvertiseRoutes)
- }
- if len(p.AdvertiseRoutes) > 0 || p.NoSNAT {
- fmt.Fprintf(&sb, "snat=%v ", !p.NoSNAT)
- }
- if len(p.AdvertiseRoutes) > 0 || p.NoStatefulFiltering.EqualBool(true) {
- // Only print if we're advertising any routes, or the user has
- // turned off stateful filtering (NoStatefulFiltering=true ⇒
- // StatefulFiltering=false).
- bb, _ := p.NoStatefulFiltering.Get()
- fmt.Fprintf(&sb, "statefulFiltering=%v ", !bb)
- }
- }
- if len(p.AdvertiseTags) > 0 {
- fmt.Fprintf(&sb, "tags=%s ", strings.Join(p.AdvertiseTags, ","))
- }
- if len(p.AdvertiseServices) > 0 {
- fmt.Fprintf(&sb, "services=%s ", strings.Join(p.AdvertiseServices, ","))
- }
- if goos == "linux" {
- fmt.Fprintf(&sb, "nf=%v ", p.NetfilterMode)
- }
- if p.ControlURL != "" && p.ControlURL != DefaultControlURL {
- fmt.Fprintf(&sb, "url=%q ", p.ControlURL)
- }
- if p.Hostname != "" {
- fmt.Fprintf(&sb, "host=%q ", p.Hostname)
- }
- if p.OperatorUser != "" {
- fmt.Fprintf(&sb, "op=%q ", p.OperatorUser)
- }
- if p.NetfilterKind != "" {
- fmt.Fprintf(&sb, "netfilterKind=%s ", p.NetfilterKind)
- }
- if buildfeatures.HasClientUpdate {
- sb.WriteString(p.AutoUpdate.Pretty())
- }
- if buildfeatures.HasAppConnectors {
- sb.WriteString(p.AppConnector.Pretty())
- }
- if buildfeatures.HasRelayServer && p.RelayServerPort != nil {
- fmt.Fprintf(&sb, "relayServerPort=%d ", *p.RelayServerPort)
- }
- if buildfeatures.HasRelayServer && len(p.RelayServerStaticEndpoints) > 0 {
- fmt.Fprintf(&sb, "relayServerStaticEndpoints=%v ", p.RelayServerStaticEndpoints)
- }
- if p.Persist != nil {
- sb.WriteString(p.Persist.Pretty())
- } else {
- sb.WriteString("Persist=nil")
- }
- sb.WriteString("}")
- return sb.String()
- }
- func (p PrefsView) ToBytes() []byte {
- return p.ж.ToBytes()
- }
- func (p *Prefs) ToBytes() []byte {
- data, err := json.MarshalIndent(p, "", "\t")
- if err != nil {
- log.Fatalf("Prefs marshal: %v\n", err)
- }
- return data
- }
- func (p PrefsView) Equals(p2 PrefsView) bool {
- return p.ж.Equals(p2.ж)
- }
- func (p *Prefs) Equals(p2 *Prefs) bool {
- if p == p2 {
- return true
- }
- if p == nil || p2 == nil {
- return false
- }
- return p.ControlURL == p2.ControlURL &&
- p.RouteAll == p2.RouteAll &&
- p.ExitNodeID == p2.ExitNodeID &&
- p.ExitNodeIP == p2.ExitNodeIP &&
- p.AutoExitNode == p2.AutoExitNode &&
- p.InternalExitNodePrior == p2.InternalExitNodePrior &&
- p.ExitNodeAllowLANAccess == p2.ExitNodeAllowLANAccess &&
- p.CorpDNS == p2.CorpDNS &&
- p.RunSSH == p2.RunSSH &&
- p.Sync.Normalized() == p2.Sync.Normalized() &&
- p.RunWebClient == p2.RunWebClient &&
- p.WantRunning == p2.WantRunning &&
- p.LoggedOut == p2.LoggedOut &&
- p.NotepadURLs == p2.NotepadURLs &&
- p.ShieldsUp == p2.ShieldsUp &&
- p.NoSNAT == p2.NoSNAT &&
- p.NoStatefulFiltering == p2.NoStatefulFiltering &&
- p.NetfilterMode == p2.NetfilterMode &&
- p.OperatorUser == p2.OperatorUser &&
- p.Hostname == p2.Hostname &&
- p.ForceDaemon == p2.ForceDaemon &&
- slices.Equal(p.AdvertiseRoutes, p2.AdvertiseRoutes) &&
- slices.Equal(p.AdvertiseTags, p2.AdvertiseTags) &&
- slices.Equal(p.AdvertiseServices, p2.AdvertiseServices) &&
- p.Persist.Equals(p2.Persist) &&
- p.ProfileName == p2.ProfileName &&
- p.AutoUpdate.Equals(p2.AutoUpdate) &&
- p.AppConnector == p2.AppConnector &&
- p.PostureChecking == p2.PostureChecking &&
- slices.EqualFunc(p.DriveShares, p2.DriveShares, drive.SharesEqual) &&
- p.NetfilterKind == p2.NetfilterKind &&
- compareUint16Ptrs(p.RelayServerPort, p2.RelayServerPort) &&
- slices.Equal(p.RelayServerStaticEndpoints, p2.RelayServerStaticEndpoints)
- }
- func (au AutoUpdatePrefs) Pretty() string {
- if au.Apply.EqualBool(true) {
- return "update=on "
- }
- if au.Check {
- return "update=check "
- }
- return "update=off "
- }
- func (ap AppConnectorPrefs) Pretty() string {
- if ap.Advertise {
- return "appconnector=advertise "
- }
- return ""
- }
- func compareUint16Ptrs(a, b *uint16) bool {
- if (a == nil) != (b == nil) {
- return false
- }
- if a == nil {
- return true
- }
- return *a == *b
- }
- // NewPrefs returns the default preferences to use.
- func NewPrefs() *Prefs {
- // Provide default values for options which might be missing
- // from the json data for any reason. The json can still
- // override them to false.
- p := &Prefs{
- // ControlURL is explicitly not set to signal that
- // it's not yet configured, which relaxes the CLI "up"
- // safety net features. It will get set to DefaultControlURL
- // on first up. Or, if not, DefaultControlURL will be used
- // later anyway.
- ControlURL: "",
- CorpDNS: true,
- WantRunning: false,
- NetfilterMode: preftype.NetfilterOn,
- NoStatefulFiltering: opt.NewBool(true),
- AutoUpdate: AutoUpdatePrefs{
- Check: true,
- Apply: opt.Bool("unset"),
- },
- }
- p.RouteAll = p.DefaultRouteAll(runtime.GOOS)
- return p
- }
- // ControlURLOrDefault returns the coordination server's URL base.
- //
- // If not configured, or if the configured value is a legacy name equivalent to
- // the default, then DefaultControlURL is returned instead.
- func (p PrefsView) ControlURLOrDefault(polc policyclient.Client) string {
- return p.ж.ControlURLOrDefault(polc)
- }
- // ControlURLOrDefault returns the coordination server's URL base.
- //
- // If not configured, or if the configured value is a legacy name equivalent to
- // the default, then DefaultControlURL is returned instead.
- func (p *Prefs) ControlURLOrDefault(polc policyclient.Client) string {
- controlURL, err := polc.GetString(pkey.ControlURL, p.ControlURL)
- if err != nil {
- controlURL = p.ControlURL
- }
- if controlURL != "" {
- if controlURL != DefaultControlURL && IsLoginServerSynonym(controlURL) {
- return DefaultControlURL
- }
- return controlURL
- }
- return DefaultControlURL
- }
- // DefaultRouteAll returns the default value of [Prefs.RouteAll] as a function
- // of the platform it's running on.
- func (p *Prefs) DefaultRouteAll(goos string) bool {
- switch goos {
- case "windows", "android", "ios":
- return true
- case "darwin":
- // Only true for macAppStore and macsys, false for darwin tailscaled.
- return version.IsSandboxedMacOS()
- default:
- return false
- }
- }
- // AdminPageURL returns the admin web site URL for the current ControlURL.
- func (p PrefsView) AdminPageURL(polc policyclient.Client) string { return p.ж.AdminPageURL(polc) }
- // AdminPageURL returns the admin web site URL for the current ControlURL.
- func (p *Prefs) AdminPageURL(polc policyclient.Client) string {
- url := p.ControlURLOrDefault(polc)
- if IsLoginServerSynonym(url) {
- // TODO(crawshaw): In future release, make this https://console.tailscale.com
- url = "https://login.tailscale.com"
- }
- return url + "/admin"
- }
- // AdvertisesExitNode reports whether p is advertising both the v4 and
- // v6 /0 exit node routes.
- func (p PrefsView) AdvertisesExitNode() bool { return p.ж.AdvertisesExitNode() }
- // AdvertisesExitNode reports whether p is advertising both the v4 and
- // v6 /0 exit node routes.
- func (p *Prefs) AdvertisesExitNode() bool {
- if p == nil {
- return false
- }
- return tsaddr.ContainsExitRoutes(views.SliceOf(p.AdvertiseRoutes))
- }
- // SetAdvertiseExitNode mutates p (if non-nil) to add or remove the two
- // /0 exit node routes.
- func (p *Prefs) SetAdvertiseExitNode(runExit bool) {
- if !buildfeatures.HasAdvertiseExitNode {
- return
- }
- if p == nil {
- return
- }
- all := p.AdvertiseRoutes
- p.AdvertiseRoutes = p.AdvertiseRoutes[:0]
- for _, r := range all {
- if r.Bits() != 0 {
- p.AdvertiseRoutes = append(p.AdvertiseRoutes, r)
- }
- }
- if !runExit {
- return
- }
- p.AdvertiseRoutes = append(p.AdvertiseRoutes,
- netip.PrefixFrom(netaddr.IPv4(0, 0, 0, 0), 0),
- netip.PrefixFrom(netip.IPv6Unspecified(), 0))
- }
- // peerWithTailscaleIP returns the peer in st with the provided
- // Tailscale IP.
- func peerWithTailscaleIP(st *ipnstate.Status, ip netip.Addr) (ps *ipnstate.PeerStatus, ok bool) {
- for _, ps := range st.Peer {
- for _, ip2 := range ps.TailscaleIPs {
- if ip == ip2 {
- return ps, true
- }
- }
- }
- return nil, false
- }
- func isRemoteIP(st *ipnstate.Status, ip netip.Addr) bool {
- for _, selfIP := range st.TailscaleIPs {
- if ip == selfIP {
- return false
- }
- }
- return true
- }
- // ClearExitNode sets the ExitNodeID and ExitNodeIP to their zero values.
- func (p *Prefs) ClearExitNode() {
- p.ExitNodeID = ""
- p.ExitNodeIP = netip.Addr{}
- p.AutoExitNode = ""
- }
- // ExitNodeLocalIPError is returned when the requested IP address for an exit
- // node belongs to the local machine.
- type ExitNodeLocalIPError struct {
- hostOrIP string
- }
- func (e ExitNodeLocalIPError) Error() string {
- return fmt.Sprintf("cannot use %s as an exit node as it is a local IP address to this machine", e.hostOrIP)
- }
- func exitNodeIPOfArg(s string, st *ipnstate.Status) (ip netip.Addr, err error) {
- if s == "" {
- return ip, os.ErrInvalid
- }
- ip, err = netip.ParseAddr(s)
- if err == nil {
- if !isRemoteIP(st, ip) {
- return ip, ExitNodeLocalIPError{s}
- }
- // If we're online already and have a netmap, double check that the IP
- // address specified is valid.
- if st.BackendState == "Running" {
- ps, ok := peerWithTailscaleIP(st, ip)
- if !ok {
- return ip, fmt.Errorf("no node found in netmap with IP %v", ip)
- }
- if !ps.ExitNodeOption {
- return ip, fmt.Errorf("node %v is not advertising an exit node", ip)
- }
- }
- return ip, nil
- }
- match := 0
- for _, ps := range st.Peer {
- baseName := dnsname.TrimSuffix(ps.DNSName, st.MagicDNSSuffix)
- if !strings.EqualFold(s, baseName) && !strings.EqualFold(s, ps.DNSName) {
- continue
- }
- match++
- if len(ps.TailscaleIPs) == 0 {
- return ip, fmt.Errorf("node %q has no Tailscale IP?", s)
- }
- if !ps.ExitNodeOption {
- return ip, fmt.Errorf("node %q is not advertising an exit node", s)
- }
- ip = ps.TailscaleIPs[0]
- }
- switch match {
- case 0:
- return ip, fmt.Errorf("invalid value %q for --exit-node; must be IP or unique node name", s)
- case 1:
- if !isRemoteIP(st, ip) {
- return ip, ExitNodeLocalIPError{s}
- }
- return ip, nil
- default:
- return ip, fmt.Errorf("ambiguous exit node name %q", s)
- }
- }
- // SetExitNodeIP validates and sets the ExitNodeIP from a user-provided string
- // specifying either an IP address or a MagicDNS base name ("foo", as opposed to
- // "foo.bar.beta.tailscale.net"). This method does not mutate ExitNodeID and
- // will fail if ExitNodeID is already set.
- func (p *Prefs) SetExitNodeIP(s string, st *ipnstate.Status) error {
- if !p.ExitNodeID.IsZero() {
- return ErrExitNodeIDAlreadySet
- }
- ip, err := exitNodeIPOfArg(s, st)
- if err == nil {
- p.ExitNodeIP = ip
- }
- return err
- }
- // ShouldSSHBeRunning reports whether the SSH server should be running based on
- // the prefs.
- func (p PrefsView) ShouldSSHBeRunning() bool {
- return p.Valid() && p.ж.ShouldSSHBeRunning()
- }
- // ShouldSSHBeRunning reports whether the SSH server should be running based on
- // the prefs.
- func (p *Prefs) ShouldSSHBeRunning() bool {
- return p.WantRunning && p.RunSSH
- }
- // ShouldWebClientBeRunning reports whether the web client server should be running based on
- // the prefs.
- func (p PrefsView) ShouldWebClientBeRunning() bool {
- return p.Valid() && p.ж.ShouldWebClientBeRunning()
- }
- // ShouldWebClientBeRunning reports whether the web client server should be running based on
- // the prefs.
- func (p *Prefs) ShouldWebClientBeRunning() bool {
- return p.WantRunning && p.RunWebClient
- }
- // PrefsFromBytes deserializes Prefs from a JSON blob b into base. Values in
- // base are preserved, unless they are populated in the JSON blob.
- func PrefsFromBytes(b []byte, base *Prefs) error {
- if len(b) == 0 {
- return nil
- }
- return json.Unmarshal(b, base)
- }
- func (p *Prefs) normalizeOptBools() {
- if p.Sync == opt.ExplicitlyUnset {
- p.Sync = ""
- }
- }
- var jsonEscapedZero = []byte(`\u0000`)
- // LoadPrefsWindows loads a legacy relaynode config file into Prefs with
- // sensible migration defaults set. Windows-only.
- func LoadPrefsWindows(filename string) (*Prefs, error) {
- data, err := os.ReadFile(filename)
- if err != nil {
- return nil, fmt.Errorf("LoadPrefs open: %w", err) // err includes path
- }
- if bytes.Contains(data, jsonEscapedZero) {
- // Tailscale 1.2.0 - 1.2.8 on Windows had a memory corruption bug
- // in the backend process that ended up sending NULL bytes over JSON
- // to the frontend which wrote them out to JSON files on disk.
- // So if we see one, treat is as corrupt and the user will need
- // to log in again. (better than crashing)
- return nil, os.ErrNotExist
- }
- p := NewPrefs()
- if err := PrefsFromBytes(data, p); err != nil {
- return nil, fmt.Errorf("LoadPrefs(%q) decode: %w", filename, err)
- }
- return p, nil
- }
- func SavePrefs(filename string, p *Prefs) {
- log.Printf("Saving prefs %v %v\n", filename, p.Pretty())
- data := p.ToBytes()
- os.MkdirAll(filepath.Dir(filename), 0700)
- if err := atomicfile.WriteFile(filename, data, 0600); err != nil {
- log.Printf("SavePrefs: %v\n", err)
- }
- }
- // ProfileID is an auto-generated system-wide unique identifier for a login
- // profile. It is a 4 character hex string like "1ab3".
- type ProfileID string
- // WindowsUserID is a userid (suitable for passing to ipnauth.LookupUserFromID
- // or os/user.LookupId) but only set on Windows. It's empty on all other
- // platforms, unless envknob.GOOS is in used, making Linux act like Windows for
- // tests.
- type WindowsUserID string
- // NetworkProfile is a subset of netmap.NetworkMap
- // that should be saved with each user profile.
- type NetworkProfile struct {
- MagicDNSName string
- DomainName string
- DisplayName string
- }
- // RequiresBackfill returns whether this object does not have all the data
- // expected. This is because this struct is a later addition to LoginProfile and
- // this method can be checked to see if it's been backfilled to the current
- // expectation or not. Note that for now, it just checks if the struct is empty.
- // In the future, if we have new optional fields, this method can be changed to
- // do more explicit checks to return whether it's apt for a backfill or not.
- func (n NetworkProfile) RequiresBackfill() bool {
- return n == NetworkProfile{}
- }
- // DisplayNameOrDefault will always return a non-empty string.
- // If there is a defined display name, it will return that.
- // If they did not it will default to their domain name.
- func (n NetworkProfile) DisplayNameOrDefault() string {
- return cmp.Or(n.DisplayName, n.DomainName)
- }
- // LoginProfile represents a single login profile as managed
- // by the ProfileManager.
- type LoginProfile struct {
- // ID is a unique identifier for this profile.
- // It is assigned on creation and never changes.
- // It may seem redundant to have both ID and UserProfile.ID
- // but they are different things. UserProfile.ID may change
- // over time (e.g. if a device is tagged).
- ID ProfileID
- // Name is the user-visible name of this profile.
- // It is filled in from the UserProfile.LoginName field.
- Name string
- // NetworkProfile is a subset of netmap.NetworkMap that we
- // store to remember information about the tailnet that this
- // profile was logged in with.
- //
- // This field was added on 2023-11-17.
- NetworkProfile NetworkProfile
- // Key is the StateKey under which the profile is stored.
- // It is assigned once at profile creation time and never changes.
- Key StateKey
- // UserProfile is the server provided UserProfile for this profile.
- // This is updated whenever the server provides a new UserProfile.
- UserProfile tailcfg.UserProfile
- // NodeID is the NodeID of the node that this profile is logged into.
- // This should be stable across tagging and untagging nodes.
- // It may seem redundant to check against both the UserProfile.UserID
- // and the NodeID. However the NodeID can change if the node is deleted
- // from the admin panel.
- NodeID tailcfg.StableNodeID
- // LocalUserID is the user ID of the user who created this profile.
- // It is only relevant on Windows where we have a multi-user system.
- // It is assigned once at profile creation time and never changes.
- LocalUserID WindowsUserID
- // ControlURL is the URL of the control server that this profile is logged
- // into.
- ControlURL string
- }
- // Equals reports whether p and p2 are equal.
- func (p LoginProfileView) Equals(p2 LoginProfileView) bool {
- return p.ж.Equals(p2.ж)
- }
- // Equals reports whether p and p2 are equal.
- func (p *LoginProfile) Equals(p2 *LoginProfile) bool {
- if p == p2 {
- return true
- }
- if p == nil || p2 == nil {
- return false
- }
- return p.ID == p2.ID &&
- p.Name == p2.Name &&
- p.NetworkProfile == p2.NetworkProfile &&
- p.Key == p2.Key &&
- p.UserProfile.Equal(&p2.UserProfile) &&
- p.NodeID == p2.NodeID &&
- p.LocalUserID == p2.LocalUserID &&
- p.ControlURL == p2.ControlURL
- }
- // ExitNodeExpression is a string that specifies how an exit node
- // should be selected. An empty string means that no exit node
- // should be selected.
- //
- // As of 2025-07-02, the only supported value is [AnyExitNode].
- type ExitNodeExpression string
- // AnyExitNode indicates that the exit node should be automatically
- // selected from the pool of available exit nodes, excluding any
- // disallowed by policy (e.g., [syspolicy.AllowedSuggestedExitNodes]).
- // The exact implementation is subject to change, but exit nodes
- // offering the best performance will be preferred.
- const AnyExitNode ExitNodeExpression = "any"
- // IsSet reports whether the expression is non-empty and can be used
- // to select an exit node.
- func (e ExitNodeExpression) IsSet() bool {
- return e != ""
- }
- const (
- // AutoExitNodePrefix is the prefix used in [syspolicy.ExitNodeID] values and CLI
- // to indicate that the string following the prefix is an [ipn.ExitNodeExpression].
- AutoExitNodePrefix = "auto:"
- )
- // ParseAutoExitNodeString attempts to parse the given string
- // as an [ExitNodeExpression].
- //
- // It returns the parsed expression and true on success,
- // or an empty string and false if the input does not appear to be
- // an [ExitNodeExpression] (i.e., it doesn't start with "auto:").
- //
- // It is mainly used to parse the [syspolicy.ExitNodeID] value
- // when it is set to "auto:<expression>" (e.g., auto:any).
- func ParseAutoExitNodeString[T ~string](s T) (_ ExitNodeExpression, ok bool) {
- if expr, ok := strings.CutPrefix(string(s), AutoExitNodePrefix); ok && expr != "" {
- return ExitNodeExpression(expr), true
- }
- return "", false
- }
|