From 945cf836ee25f412490b39f49669360b66727863 Mon Sep 17 00:00:00 2001 From: Andrew Lytvynov Date: Mon, 18 Dec 2023 16:57:03 -0600 Subject: [PATCH] ipn: apply tailnet-wide default for auto-updates (#10508) When auto-update setting in local Prefs is unset, apply the tailnet default value from control. This only happens once, when we apply the default (or when the user manually overrides it), tailnet default no longer affects the node. Updates #16244 Signed-off-by: Andrew Lytvynov --- cmd/tailscale/cli/cli_test.go | 6 -- cmd/tailscale/cli/set.go | 5 +- control/controlclient/direct.go | 118 +++++++++++++++++--------------- ipn/ipnlocal/c2n.go | 2 +- ipn/ipnlocal/local.go | 72 +++++++++++++------ ipn/ipnlocal/local_test.go | 70 ++++++++++++++++--- ipn/prefs.go | 19 +++-- ipn/prefs_test.go | 25 +++---- tailcfg/tailcfg.go | 10 ++- types/netmap/nodemut.go | 3 +- types/netmap/nodemut_test.go | 4 ++ types/opt/bool.go | 6 ++ 12 files changed, 228 insertions(+), 112 deletions(-) diff --git a/cmd/tailscale/cli/cli_test.go b/cmd/tailscale/cli/cli_test.go index c2e092aa5..e0bba450d 100644 --- a/cmd/tailscale/cli/cli_test.go +++ b/cmd/tailscale/cli/cli_test.go @@ -558,7 +558,6 @@ func TestPrefsFromUpArgs(t *testing.T) { AllowSingleHosts: true, AutoUpdate: ipn.AutoUpdatePrefs{ Check: true, - Apply: false, }, }, }, @@ -575,7 +574,6 @@ func TestPrefsFromUpArgs(t *testing.T) { NetfilterMode: preftype.NetfilterOn, AutoUpdate: ipn.AutoUpdatePrefs{ Check: true, - Apply: false, }, }, }, @@ -594,7 +592,6 @@ func TestPrefsFromUpArgs(t *testing.T) { NetfilterMode: preftype.NetfilterOn, AutoUpdate: ipn.AutoUpdatePrefs{ Check: true, - Apply: false, }, }, }, @@ -684,7 +681,6 @@ func TestPrefsFromUpArgs(t *testing.T) { NoSNAT: true, AutoUpdate: ipn.AutoUpdatePrefs{ Check: true, - Apply: false, }, }, }, @@ -701,7 +697,6 @@ func TestPrefsFromUpArgs(t *testing.T) { NoSNAT: true, AutoUpdate: ipn.AutoUpdatePrefs{ Check: true, - Apply: false, }, }, }, @@ -720,7 +715,6 @@ func TestPrefsFromUpArgs(t *testing.T) { }, AutoUpdate: ipn.AutoUpdatePrefs{ Check: true, - Apply: false, }, }, }, diff --git a/cmd/tailscale/cli/set.go b/cmd/tailscale/cli/set.go index ffa0d6e6f..a8d7a26af 100644 --- a/cmd/tailscale/cli/set.go +++ b/cmd/tailscale/cli/set.go @@ -17,6 +17,7 @@ import ( "tailscale.com/net/netutil" "tailscale.com/net/tsaddr" "tailscale.com/safesocket" + "tailscale.com/types/opt" "tailscale.com/types/views" "tailscale.com/version" ) @@ -116,7 +117,7 @@ func runSet(ctx context.Context, args []string) (retErr error) { ForceDaemon: setArgs.forceDaemon, AutoUpdate: ipn.AutoUpdatePrefs{ Check: setArgs.updateCheck, - Apply: setArgs.updateApply, + Apply: opt.NewBool(setArgs.updateApply), }, AppConnector: ipn.AppConnectorPrefs{ Advertise: setArgs.advertiseConnector, @@ -172,7 +173,7 @@ func runSet(ctx context.Context, args []string) (retErr error) { // does not use clientupdate. if version.IsMacSysExt() { apply := "0" - if maskedPrefs.AutoUpdate.Apply { + if maskedPrefs.AutoUpdate.Apply.EqualBool(true) { apply = "1" } out, err := exec.Command("defaults", "write", "io.tailscale.ipn.macsys", "SUAutomaticallyUpdate", apply).CombinedOutput() diff --git a/control/controlclient/direct.go b/control/controlclient/direct.go index 67ca800af..743ab70b6 100644 --- a/control/controlclient/direct.go +++ b/control/controlclient/direct.go @@ -61,23 +61,24 @@ import ( // Direct is the client that connects to a tailcontrol server for a node. type Direct struct { - httpc *http.Client // HTTP client used to talk to tailcontrol - dialer *tsdial.Dialer - dnsCache *dnscache.Resolver - controlKnobs *controlknobs.Knobs // always non-nil - serverURL string // URL of the tailcontrol server - clock tstime.Clock - logf logger.Logf - netMon *netmon.Monitor // or nil - discoPubKey key.DiscoPublic - getMachinePrivKey func() (key.MachinePrivate, error) - debugFlags []string - skipIPForwardingCheck bool - pinger Pinger - popBrowser func(url string) // or nil - c2nHandler http.Handler // or nil - onClientVersion func(*tailcfg.ClientVersion) // or nil - onControlTime func(time.Time) // or nil + httpc *http.Client // HTTP client used to talk to tailcontrol + dialer *tsdial.Dialer + dnsCache *dnscache.Resolver + controlKnobs *controlknobs.Knobs // always non-nil + serverURL string // URL of the tailcontrol server + clock tstime.Clock + logf logger.Logf + netMon *netmon.Monitor // or nil + discoPubKey key.DiscoPublic + getMachinePrivKey func() (key.MachinePrivate, error) + debugFlags []string + skipIPForwardingCheck bool + pinger Pinger + popBrowser func(url string) // or nil + c2nHandler http.Handler // or nil + onClientVersion func(*tailcfg.ClientVersion) // or nil + onControlTime func(time.Time) // or nil + onTailnetDefaultAutoUpdate func(bool) // or nil dialPlan ControlDialPlanner // can be nil @@ -110,24 +111,25 @@ type Observer interface { } type Options struct { - Persist persist.Persist // initial persistent data - GetMachinePrivateKey func() (key.MachinePrivate, error) // returns the machine key to use - ServerURL string // URL of the tailcontrol server - AuthKey string // optional node auth key for auto registration - Clock tstime.Clock - Hostinfo *tailcfg.Hostinfo // non-nil passes ownership, nil means to use default using os.Hostname, etc - DiscoPublicKey key.DiscoPublic - Logf logger.Logf - HTTPTestClient *http.Client // optional HTTP client to use (for tests only) - NoiseTestClient *http.Client // optional HTTP client to use for noise RPCs (tests only) - DebugFlags []string // debug settings to send to control - NetMon *netmon.Monitor // optional network monitor - PopBrowserURL func(url string) // optional func to open browser - OnClientVersion func(*tailcfg.ClientVersion) // optional func to inform GUI of client version status - OnControlTime func(time.Time) // optional func to notify callers of new time from control - Dialer *tsdial.Dialer // non-nil - C2NHandler http.Handler // or nil - ControlKnobs *controlknobs.Knobs // or nil to ignore + Persist persist.Persist // initial persistent data + GetMachinePrivateKey func() (key.MachinePrivate, error) // returns the machine key to use + ServerURL string // URL of the tailcontrol server + AuthKey string // optional node auth key for auto registration + Clock tstime.Clock + Hostinfo *tailcfg.Hostinfo // non-nil passes ownership, nil means to use default using os.Hostname, etc + DiscoPublicKey key.DiscoPublic + Logf logger.Logf + HTTPTestClient *http.Client // optional HTTP client to use (for tests only) + NoiseTestClient *http.Client // optional HTTP client to use for noise RPCs (tests only) + DebugFlags []string // debug settings to send to control + NetMon *netmon.Monitor // optional network monitor + PopBrowserURL func(url string) // optional func to open browser + OnClientVersion func(*tailcfg.ClientVersion) // optional func to inform GUI of client version status + OnControlTime func(time.Time) // optional func to notify callers of new time from control + OnTailnetDefaultAutoUpdate func(bool) // optional func to inform GUI of default auto-update setting for the tailnet + Dialer *tsdial.Dialer // non-nil + C2NHandler http.Handler // or nil + ControlKnobs *controlknobs.Knobs // or nil to ignore // Observer is called when there's a change in status to report // from the control client. @@ -263,26 +265,27 @@ func NewDirect(opts Options) (*Direct, error) { } c := &Direct{ - httpc: httpc, - controlKnobs: opts.ControlKnobs, - getMachinePrivKey: opts.GetMachinePrivateKey, - serverURL: opts.ServerURL, - clock: opts.Clock, - logf: opts.Logf, - persist: opts.Persist.View(), - authKey: opts.AuthKey, - discoPubKey: opts.DiscoPublicKey, - debugFlags: opts.DebugFlags, - netMon: opts.NetMon, - skipIPForwardingCheck: opts.SkipIPForwardingCheck, - pinger: opts.Pinger, - popBrowser: opts.PopBrowserURL, - onClientVersion: opts.OnClientVersion, - onControlTime: opts.OnControlTime, - c2nHandler: opts.C2NHandler, - dialer: opts.Dialer, - dnsCache: dnsCache, - dialPlan: opts.DialPlan, + httpc: httpc, + controlKnobs: opts.ControlKnobs, + getMachinePrivKey: opts.GetMachinePrivateKey, + serverURL: opts.ServerURL, + clock: opts.Clock, + logf: opts.Logf, + persist: opts.Persist.View(), + authKey: opts.AuthKey, + discoPubKey: opts.DiscoPublicKey, + debugFlags: opts.DebugFlags, + netMon: opts.NetMon, + skipIPForwardingCheck: opts.SkipIPForwardingCheck, + pinger: opts.Pinger, + popBrowser: opts.PopBrowserURL, + onClientVersion: opts.OnClientVersion, + onTailnetDefaultAutoUpdate: opts.OnTailnetDefaultAutoUpdate, + onControlTime: opts.OnControlTime, + c2nHandler: opts.C2NHandler, + dialer: opts.Dialer, + dnsCache: dnsCache, + dialPlan: opts.DialPlan, } if opts.Hostinfo == nil { c.SetHostinfo(hostinfo.New()) @@ -1091,6 +1094,11 @@ func (c *Direct) sendMapRequest(ctx context.Context, isStreaming bool, nu Netmap metricMapResponseKeepAlives.Add(1) continue } + if au, ok := resp.DefaultAutoUpdate.Get(); ok { + if c.onTailnetDefaultAutoUpdate != nil { + c.onTailnetDefaultAutoUpdate(au) + } + } metricMapResponseMap.Add(1) if gotNonKeepAliveMessage { diff --git a/ipn/ipnlocal/c2n.go b/ipn/ipnlocal/c2n.go index 4668fe85d..9e6af14de 100644 --- a/ipn/ipnlocal/c2n.go +++ b/ipn/ipnlocal/c2n.go @@ -378,7 +378,7 @@ func (b *LocalBackend) newC2NUpdateResponse() tailcfg.C2NUpdateResponse { // invoke it here. For this purpose, it is ok to pass it a zero Arguments. prefs := b.Prefs().AutoUpdate() return tailcfg.C2NUpdateResponse{ - Enabled: envknob.AllowsRemoteUpdate() || prefs.Apply, + Enabled: envknob.AllowsRemoteUpdate() || prefs.Apply.EqualBool(true), Supported: clientupdate.CanAutoUpdate(), } } diff --git a/ipn/ipnlocal/local.go b/ipn/ipnlocal/local.go index 2dceede5a..5e1fd42b4 100644 --- a/ipn/ipnlocal/local.go +++ b/ipn/ipnlocal/local.go @@ -76,6 +76,7 @@ import ( "tailscale.com/types/logger" "tailscale.com/types/logid" "tailscale.com/types/netmap" + "tailscale.com/types/opt" "tailscale.com/types/persist" "tailscale.com/types/preftype" "tailscale.com/types/ptr" @@ -1271,8 +1272,8 @@ var preferencePolicies = []preferencePolicyInfo{ }, { key: syspolicy.ApplyUpdates, - get: func(p ipn.PrefsView) bool { return p.AutoUpdate().Apply }, - set: func(p *ipn.Prefs, v bool) { p.AutoUpdate.Apply = v }, + get: func(p ipn.PrefsView) bool { v, _ := p.AutoUpdate().Apply.Get(); return v }, + set: func(p *ipn.Prefs, v bool) { p.AutoUpdate.Apply.Set(v) }, }, { key: syspolicy.EnableRunExitNode, @@ -1767,25 +1768,26 @@ func (b *LocalBackend) Start(opts ipn.Options) error { // new controlclient. SetPrefs() allows you to overwrite ServerURL, // but it won't take effect until the next Start(). cc, err := b.getNewControlClientFunc()(controlclient.Options{ - GetMachinePrivateKey: b.createGetMachinePrivateKeyFunc(), - Logf: logger.WithPrefix(b.logf, "control: "), - Persist: *persistv, - ServerURL: serverURL, - AuthKey: opts.AuthKey, - Hostinfo: hostinfo, - HTTPTestClient: httpTestClient, - DiscoPublicKey: discoPublic, - DebugFlags: debugFlags, - NetMon: b.sys.NetMon.Get(), - Pinger: b, - PopBrowserURL: b.tellClientToBrowseToURL, - OnClientVersion: b.onClientVersion, - OnControlTime: b.em.onControlTime, - Dialer: b.Dialer(), - Observer: b, - C2NHandler: http.HandlerFunc(b.handleC2N), - DialPlan: &b.dialPlan, // pointer because it can't be copied - ControlKnobs: b.sys.ControlKnobs(), + GetMachinePrivateKey: b.createGetMachinePrivateKeyFunc(), + Logf: logger.WithPrefix(b.logf, "control: "), + Persist: *persistv, + ServerURL: serverURL, + AuthKey: opts.AuthKey, + Hostinfo: hostinfo, + HTTPTestClient: httpTestClient, + DiscoPublicKey: discoPublic, + DebugFlags: debugFlags, + NetMon: b.sys.NetMon.Get(), + Pinger: b, + PopBrowserURL: b.tellClientToBrowseToURL, + OnClientVersion: b.onClientVersion, + OnTailnetDefaultAutoUpdate: b.onTailnetDefaultAutoUpdate, + OnControlTime: b.em.onControlTime, + Dialer: b.Dialer(), + Observer: b, + C2NHandler: http.HandlerFunc(b.handleC2N), + DialPlan: &b.dialPlan, // pointer because it can't be copied + ControlKnobs: b.sys.ControlKnobs(), // Don't warn about broken Linux IP forwarding when // netstack is being used. @@ -2500,6 +2502,32 @@ func (b *LocalBackend) onClientVersion(v *tailcfg.ClientVersion) { b.send(ipn.Notify{ClientVersion: v}) } +func (b *LocalBackend) onTailnetDefaultAutoUpdate(au bool) { + prefs := b.pm.CurrentPrefs() + if !prefs.Valid() { + b.logf("[unexpected]: received tailnet default auto-update callback but current prefs are nil") + return + } + if _, ok := prefs.AutoUpdate().Apply.Get(); ok { + // Apply was already set from a previous default or manually by the + // user. Tailnet default should not affect us, even if it changes. + return + } + b.logf("using tailnet default auto-update setting: %v", au) + prefsClone := prefs.AsStruct() + prefsClone.AutoUpdate.Apply = opt.NewBool(au) + _, err := b.EditPrefs(&ipn.MaskedPrefs{ + Prefs: *prefsClone, + AutoUpdateSet: ipn.AutoUpdatePrefsMask{ + ApplySet: true, + }, + }) + if err != nil { + b.logf("failed to apply tailnet-wide default for auto-updates (%v): %v", au, err) + return + } +} + // For testing lazy machine key generation. var panicOnMachineKeyGeneration = envknob.RegisterBool("TS_DEBUG_PANIC_MACHINE_KEY") @@ -4079,7 +4107,7 @@ func (b *LocalBackend) applyPrefsToHostinfoLocked(hi *tailcfg.Hostinfo, prefs ip hi.RoutableIPs = prefs.AdvertiseRoutes().AsSlice() hi.RequestTags = prefs.AdvertiseTags().AsSlice() hi.ShieldsUp = prefs.ShieldsUp() - hi.AllowsUpdate = envknob.AllowsRemoteUpdate() || prefs.AutoUpdate().Apply + hi.AllowsUpdate = envknob.AllowsRemoteUpdate() || prefs.AutoUpdate().Apply.EqualBool(true) var sshHostKeys []string if prefs.RunSSH() && envknob.CanSSHD() { diff --git a/ipn/ipnlocal/local_test.go b/ipn/ipnlocal/local_test.go index 6fe87f821..71d08822b 100644 --- a/ipn/ipnlocal/local_test.go +++ b/ipn/ipnlocal/local_test.go @@ -31,6 +31,7 @@ import ( "tailscale.com/types/logger" "tailscale.com/types/logid" "tailscale.com/types/netmap" + "tailscale.com/types/opt" "tailscale.com/types/ptr" "tailscale.com/util/dnsname" "tailscale.com/util/mak" @@ -1780,13 +1781,13 @@ func TestApplySysPolicy(t *testing.T) { prefs: ipn.Prefs{ AutoUpdate: ipn.AutoUpdatePrefs{ Check: true, - Apply: false, + Apply: opt.NewBool(false), }, }, wantPrefs: ipn.Prefs{ AutoUpdate: ipn.AutoUpdatePrefs{ Check: true, - Apply: true, + Apply: opt.NewBool(true), }, }, wantAnyChange: true, @@ -1799,13 +1800,13 @@ func TestApplySysPolicy(t *testing.T) { prefs: ipn.Prefs{ AutoUpdate: ipn.AutoUpdatePrefs{ Check: true, - Apply: true, + Apply: opt.NewBool(true), }, }, wantPrefs: ipn.Prefs{ AutoUpdate: ipn.AutoUpdatePrefs{ Check: true, - Apply: false, + Apply: opt.NewBool(false), }, }, wantAnyChange: true, @@ -1818,13 +1819,13 @@ func TestApplySysPolicy(t *testing.T) { prefs: ipn.Prefs{ AutoUpdate: ipn.AutoUpdatePrefs{ Check: false, - Apply: true, + Apply: opt.NewBool(true), }, }, wantPrefs: ipn.Prefs{ AutoUpdate: ipn.AutoUpdatePrefs{ Check: true, - Apply: true, + Apply: opt.NewBool(true), }, }, wantAnyChange: true, @@ -1837,13 +1838,13 @@ func TestApplySysPolicy(t *testing.T) { prefs: ipn.Prefs{ AutoUpdate: ipn.AutoUpdatePrefs{ Check: true, - Apply: true, + Apply: opt.NewBool(true), }, }, wantPrefs: ipn.Prefs{ AutoUpdate: ipn.AutoUpdatePrefs{ Check: false, - Apply: true, + Apply: opt.NewBool(true), }, }, wantAnyChange: true, @@ -2055,3 +2056,56 @@ func TestPreferencePolicyInfo(t *testing.T) { }) } } + +func TestOnTailnetDefaultAutoUpdate(t *testing.T) { + tests := []struct { + desc string + before, after opt.Bool + tailnetDefault bool + }{ + { + before: opt.Bool(""), + tailnetDefault: true, + after: opt.NewBool(true), + }, + { + before: opt.Bool(""), + tailnetDefault: false, + after: opt.NewBool(false), + }, + { + before: opt.Bool("unset"), + tailnetDefault: true, + after: opt.NewBool(true), + }, + { + before: opt.Bool("unset"), + tailnetDefault: false, + after: opt.NewBool(false), + }, + { + before: opt.NewBool(false), + tailnetDefault: true, + after: opt.NewBool(false), + }, + { + before: opt.NewBool(true), + tailnetDefault: false, + after: opt.NewBool(true), + }, + } + for _, tt := range tests { + t.Run(fmt.Sprintf("before=%s after=%s", tt.before, tt.after), func(t *testing.T) { + b := newTestBackend(t) + p := ipn.NewPrefs() + p.AutoUpdate.Apply = tt.before + if err := b.pm.setPrefsLocked(p.View()); err != nil { + t.Fatal(err) + } + b.onTailnetDefaultAutoUpdate(tt.tailnetDefault) + if want, got := tt.after, b.pm.CurrentPrefs().AutoUpdate().Apply; got != want { + t.Errorf("got: %q, want %q", got, want) + } + }) + } +} diff --git a/ipn/prefs.go b/ipn/prefs.go index 3cf0dd3f9..afa8c1cd0 100644 --- a/ipn/prefs.go +++ b/ipn/prefs.go @@ -21,6 +21,7 @@ import ( "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" @@ -237,7 +238,17 @@ type AutoUpdatePrefs struct { // 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 bool + 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 } // AppConnectorPrefs are the app connector settings for the node agent. @@ -533,14 +544,14 @@ func (p *Prefs) Equals(p2 *Prefs) bool { compareStrings(p.AdvertiseTags, p2.AdvertiseTags) && p.Persist.Equals(p2.Persist) && p.ProfileName == p2.ProfileName && - p.AutoUpdate == p2.AutoUpdate && + p.AutoUpdate.Equals(p2.AutoUpdate) && p.AppConnector == p2.AppConnector && p.PostureChecking == p2.PostureChecking && p.NetfilterKind == p2.NetfilterKind } func (au AutoUpdatePrefs) Pretty() string { - if au.Apply { + if au.Apply.EqualBool(true) { return "update=on " } if au.Check { @@ -600,7 +611,7 @@ func NewPrefs() *Prefs { NetfilterMode: preftype.NetfilterOn, AutoUpdate: AutoUpdatePrefs{ Check: true, - Apply: false, + Apply: opt.Bool("unset"), }, } } diff --git a/ipn/prefs_test.go b/ipn/prefs_test.go index 1a09a94a9..787b75d51 100644 --- a/ipn/prefs_test.go +++ b/ipn/prefs_test.go @@ -20,6 +20,7 @@ import ( "tailscale.com/tailcfg" "tailscale.com/tstest" "tailscale.com/types/key" + "tailscale.com/types/opt" "tailscale.com/types/persist" "tailscale.com/types/preftype" ) @@ -294,18 +295,18 @@ func TestPrefsEqual(t *testing.T) { false, }, { - &Prefs{AutoUpdate: AutoUpdatePrefs{Check: true, Apply: false}}, - &Prefs{AutoUpdate: AutoUpdatePrefs{Check: false, Apply: false}}, + &Prefs{AutoUpdate: AutoUpdatePrefs{Check: true, Apply: opt.NewBool(false)}}, + &Prefs{AutoUpdate: AutoUpdatePrefs{Check: false, Apply: opt.NewBool(false)}}, false, }, { - &Prefs{AutoUpdate: AutoUpdatePrefs{Check: true, Apply: true}}, - &Prefs{AutoUpdate: AutoUpdatePrefs{Check: true, Apply: false}}, + &Prefs{AutoUpdate: AutoUpdatePrefs{Check: true, Apply: opt.NewBool(true)}}, + &Prefs{AutoUpdate: AutoUpdatePrefs{Check: true, Apply: opt.NewBool(false)}}, false, }, { - &Prefs{AutoUpdate: AutoUpdatePrefs{Check: true, Apply: false}}, - &Prefs{AutoUpdate: AutoUpdatePrefs{Check: true, Apply: false}}, + &Prefs{AutoUpdate: AutoUpdatePrefs{Check: true, Apply: opt.NewBool(false)}}, + &Prefs{AutoUpdate: AutoUpdatePrefs{Check: true, Apply: opt.NewBool(false)}}, true, }, { @@ -522,7 +523,7 @@ func TestPrefsPretty(t *testing.T) { Prefs{ AutoUpdate: AutoUpdatePrefs{ Check: true, - Apply: false, + Apply: opt.NewBool(false), }, }, "linux", @@ -532,7 +533,7 @@ func TestPrefsPretty(t *testing.T) { Prefs{ AutoUpdate: AutoUpdatePrefs{ Check: true, - Apply: true, + Apply: opt.NewBool(true), }, }, "linux", @@ -764,7 +765,7 @@ func TestMaskedPrefsPretty(t *testing.T) { { m: &MaskedPrefs{ Prefs: Prefs{ - AutoUpdate: AutoUpdatePrefs{Check: true, Apply: false}, + AutoUpdate: AutoUpdatePrefs{Check: true, Apply: opt.NewBool(false)}, }, AutoUpdateSet: AutoUpdatePrefsMask{CheckSet: true, ApplySet: false}, }, @@ -773,7 +774,7 @@ func TestMaskedPrefsPretty(t *testing.T) { { m: &MaskedPrefs{ Prefs: Prefs{ - AutoUpdate: AutoUpdatePrefs{Check: true, Apply: true}, + AutoUpdate: AutoUpdatePrefs{Check: true, Apply: opt.NewBool(true)}, }, AutoUpdateSet: AutoUpdatePrefsMask{CheckSet: true, ApplySet: true}, }, @@ -782,7 +783,7 @@ func TestMaskedPrefsPretty(t *testing.T) { { m: &MaskedPrefs{ Prefs: Prefs{ - AutoUpdate: AutoUpdatePrefs{Check: true, Apply: false}, + AutoUpdate: AutoUpdatePrefs{Check: true, Apply: opt.NewBool(false)}, }, AutoUpdateSet: AutoUpdatePrefsMask{CheckSet: false, ApplySet: true}, }, @@ -791,7 +792,7 @@ func TestMaskedPrefsPretty(t *testing.T) { { m: &MaskedPrefs{ Prefs: Prefs{ - AutoUpdate: AutoUpdatePrefs{Check: true, Apply: true}, + AutoUpdate: AutoUpdatePrefs{Check: true, Apply: opt.NewBool(true)}, }, AutoUpdateSet: AutoUpdatePrefsMask{CheckSet: false, ApplySet: false}, }, diff --git a/tailcfg/tailcfg.go b/tailcfg/tailcfg.go index 4e8faa8c4..dae9cf1b1 100644 --- a/tailcfg/tailcfg.go +++ b/tailcfg/tailcfg.go @@ -123,7 +123,8 @@ type CapabilityVersion int // - 80: 2023-11-16: can handle c2n GET /tls-cert-status // - 81: 2023-11-17: MapResponse.PacketFilters (incremental packet filter updates) // - 82: 2023-12-01: Client understands NodeAttrLinuxMustUseIPTables, NodeAttrLinuxMustUseNfTables, c2n /netfilter-kind -const CurrentCapabilityVersion CapabilityVersion = 82 +// - 83: 2023-12-18: Client understands DefaultAutoUpdate +const CurrentCapabilityVersion CapabilityVersion = 83 type StableID string @@ -1877,6 +1878,13 @@ type MapResponse struct { // download and whether the client is using it. A nil value means no change // or nothing to report. ClientVersion *ClientVersion `json:",omitempty"` + + // DefaultAutoUpdate is the default node auto-update setting for this + // tailnet. The node is free to opt-in or out locally regardless of this + // value. This value is only used on first MapResponse from control, the + // auto-update setting doesn't change if the tailnet admin flips the + // default after the node registered. + DefaultAutoUpdate opt.Bool `json:",omitempty"` } // ClientVersion is information about the latest client version that's available diff --git a/types/netmap/nodemut.go b/types/netmap/nodemut.go index 932e0c186..e6d414f4f 100644 --- a/types/netmap/nodemut.go +++ b/types/netmap/nodemut.go @@ -176,5 +176,6 @@ func mapResponseContainsNonPatchFields(res *tailcfg.MapResponse) bool { // PeersChanged to PeersChangedPatch in patchifyPeersChanged before this // function is called, so it should never be set anyway. But for // completedness, and for tests, check it too: - res.PeersChanged != nil + res.PeersChanged != nil || + res.DefaultAutoUpdate != "" } diff --git a/types/netmap/nodemut_test.go b/types/netmap/nodemut_test.go index f11a303af..f691588f2 100644 --- a/types/netmap/nodemut_test.go +++ b/types/netmap/nodemut_test.go @@ -13,6 +13,7 @@ import ( "github.com/google/go-cmp/cmp" "tailscale.com/tailcfg" "tailscale.com/types/logger" + "tailscale.com/types/opt" "tailscale.com/types/ptr" ) @@ -26,6 +27,9 @@ func TestMapResponseContainsNonPatchFields(t *testing.T) { case reflect.Bool: return reflect.ValueOf(true) case reflect.String: + if reflect.TypeOf(opt.Bool("")) == t { + return reflect.ValueOf("true").Convert(t) + } return reflect.ValueOf("foo").Convert(t) case reflect.Int64: return reflect.ValueOf(int64(1)) diff --git a/types/opt/bool.go b/types/opt/bool.go index ca9c048d5..2a9efe31b 100644 --- a/types/opt/bool.go +++ b/types/opt/bool.go @@ -18,6 +18,12 @@ import ( // field without it being dropped. type Bool string +// NewBool constructs a new Bool value equal to b. The returned Bool is set, +// unless Set("") or Clear() methods are called. +func NewBool(b bool) Bool { + return Bool(strconv.FormatBool(b)) +} + func (b *Bool) Set(v bool) { *b = Bool(strconv.FormatBool(v)) }