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)) }