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 <awly@tailscale.com>
pull/10644/head
Andrew Lytvynov 5 months ago committed by GitHub
parent d05a572db4
commit 945cf836ee
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -558,7 +558,6 @@ func TestPrefsFromUpArgs(t *testing.T) {
AllowSingleHosts: true, AllowSingleHosts: true,
AutoUpdate: ipn.AutoUpdatePrefs{ AutoUpdate: ipn.AutoUpdatePrefs{
Check: true, Check: true,
Apply: false,
}, },
}, },
}, },
@ -575,7 +574,6 @@ func TestPrefsFromUpArgs(t *testing.T) {
NetfilterMode: preftype.NetfilterOn, NetfilterMode: preftype.NetfilterOn,
AutoUpdate: ipn.AutoUpdatePrefs{ AutoUpdate: ipn.AutoUpdatePrefs{
Check: true, Check: true,
Apply: false,
}, },
}, },
}, },
@ -594,7 +592,6 @@ func TestPrefsFromUpArgs(t *testing.T) {
NetfilterMode: preftype.NetfilterOn, NetfilterMode: preftype.NetfilterOn,
AutoUpdate: ipn.AutoUpdatePrefs{ AutoUpdate: ipn.AutoUpdatePrefs{
Check: true, Check: true,
Apply: false,
}, },
}, },
}, },
@ -684,7 +681,6 @@ func TestPrefsFromUpArgs(t *testing.T) {
NoSNAT: true, NoSNAT: true,
AutoUpdate: ipn.AutoUpdatePrefs{ AutoUpdate: ipn.AutoUpdatePrefs{
Check: true, Check: true,
Apply: false,
}, },
}, },
}, },
@ -701,7 +697,6 @@ func TestPrefsFromUpArgs(t *testing.T) {
NoSNAT: true, NoSNAT: true,
AutoUpdate: ipn.AutoUpdatePrefs{ AutoUpdate: ipn.AutoUpdatePrefs{
Check: true, Check: true,
Apply: false,
}, },
}, },
}, },
@ -720,7 +715,6 @@ func TestPrefsFromUpArgs(t *testing.T) {
}, },
AutoUpdate: ipn.AutoUpdatePrefs{ AutoUpdate: ipn.AutoUpdatePrefs{
Check: true, Check: true,
Apply: false,
}, },
}, },
}, },

@ -17,6 +17,7 @@ import (
"tailscale.com/net/netutil" "tailscale.com/net/netutil"
"tailscale.com/net/tsaddr" "tailscale.com/net/tsaddr"
"tailscale.com/safesocket" "tailscale.com/safesocket"
"tailscale.com/types/opt"
"tailscale.com/types/views" "tailscale.com/types/views"
"tailscale.com/version" "tailscale.com/version"
) )
@ -116,7 +117,7 @@ func runSet(ctx context.Context, args []string) (retErr error) {
ForceDaemon: setArgs.forceDaemon, ForceDaemon: setArgs.forceDaemon,
AutoUpdate: ipn.AutoUpdatePrefs{ AutoUpdate: ipn.AutoUpdatePrefs{
Check: setArgs.updateCheck, Check: setArgs.updateCheck,
Apply: setArgs.updateApply, Apply: opt.NewBool(setArgs.updateApply),
}, },
AppConnector: ipn.AppConnectorPrefs{ AppConnector: ipn.AppConnectorPrefs{
Advertise: setArgs.advertiseConnector, Advertise: setArgs.advertiseConnector,
@ -172,7 +173,7 @@ func runSet(ctx context.Context, args []string) (retErr error) {
// does not use clientupdate. // does not use clientupdate.
if version.IsMacSysExt() { if version.IsMacSysExt() {
apply := "0" apply := "0"
if maskedPrefs.AutoUpdate.Apply { if maskedPrefs.AutoUpdate.Apply.EqualBool(true) {
apply = "1" apply = "1"
} }
out, err := exec.Command("defaults", "write", "io.tailscale.ipn.macsys", "SUAutomaticallyUpdate", apply).CombinedOutput() out, err := exec.Command("defaults", "write", "io.tailscale.ipn.macsys", "SUAutomaticallyUpdate", apply).CombinedOutput()

@ -61,23 +61,24 @@ import (
// Direct is the client that connects to a tailcontrol server for a node. // Direct is the client that connects to a tailcontrol server for a node.
type Direct struct { type Direct struct {
httpc *http.Client // HTTP client used to talk to tailcontrol httpc *http.Client // HTTP client used to talk to tailcontrol
dialer *tsdial.Dialer dialer *tsdial.Dialer
dnsCache *dnscache.Resolver dnsCache *dnscache.Resolver
controlKnobs *controlknobs.Knobs // always non-nil controlKnobs *controlknobs.Knobs // always non-nil
serverURL string // URL of the tailcontrol server serverURL string // URL of the tailcontrol server
clock tstime.Clock clock tstime.Clock
logf logger.Logf logf logger.Logf
netMon *netmon.Monitor // or nil netMon *netmon.Monitor // or nil
discoPubKey key.DiscoPublic discoPubKey key.DiscoPublic
getMachinePrivKey func() (key.MachinePrivate, error) getMachinePrivKey func() (key.MachinePrivate, error)
debugFlags []string debugFlags []string
skipIPForwardingCheck bool skipIPForwardingCheck bool
pinger Pinger pinger Pinger
popBrowser func(url string) // or nil popBrowser func(url string) // or nil
c2nHandler http.Handler // or nil c2nHandler http.Handler // or nil
onClientVersion func(*tailcfg.ClientVersion) // or nil onClientVersion func(*tailcfg.ClientVersion) // or nil
onControlTime func(time.Time) // or nil onControlTime func(time.Time) // or nil
onTailnetDefaultAutoUpdate func(bool) // or nil
dialPlan ControlDialPlanner // can be nil dialPlan ControlDialPlanner // can be nil
@ -110,24 +111,25 @@ type Observer interface {
} }
type Options struct { type Options struct {
Persist persist.Persist // initial persistent data Persist persist.Persist // initial persistent data
GetMachinePrivateKey func() (key.MachinePrivate, error) // returns the machine key to use GetMachinePrivateKey func() (key.MachinePrivate, error) // returns the machine key to use
ServerURL string // URL of the tailcontrol server ServerURL string // URL of the tailcontrol server
AuthKey string // optional node auth key for auto registration AuthKey string // optional node auth key for auto registration
Clock tstime.Clock Clock tstime.Clock
Hostinfo *tailcfg.Hostinfo // non-nil passes ownership, nil means to use default using os.Hostname, etc Hostinfo *tailcfg.Hostinfo // non-nil passes ownership, nil means to use default using os.Hostname, etc
DiscoPublicKey key.DiscoPublic DiscoPublicKey key.DiscoPublic
Logf logger.Logf Logf logger.Logf
HTTPTestClient *http.Client // optional HTTP client to use (for tests only) HTTPTestClient *http.Client // optional HTTP client to use (for tests only)
NoiseTestClient *http.Client // optional HTTP client to use for noise RPCs (tests only) NoiseTestClient *http.Client // optional HTTP client to use for noise RPCs (tests only)
DebugFlags []string // debug settings to send to control DebugFlags []string // debug settings to send to control
NetMon *netmon.Monitor // optional network monitor NetMon *netmon.Monitor // optional network monitor
PopBrowserURL func(url string) // optional func to open browser PopBrowserURL func(url string) // optional func to open browser
OnClientVersion func(*tailcfg.ClientVersion) // optional func to inform GUI of client version status 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 OnControlTime func(time.Time) // optional func to notify callers of new time from control
Dialer *tsdial.Dialer // non-nil OnTailnetDefaultAutoUpdate func(bool) // optional func to inform GUI of default auto-update setting for the tailnet
C2NHandler http.Handler // or nil Dialer *tsdial.Dialer // non-nil
ControlKnobs *controlknobs.Knobs // or nil to ignore C2NHandler http.Handler // or nil
ControlKnobs *controlknobs.Knobs // or nil to ignore
// Observer is called when there's a change in status to report // Observer is called when there's a change in status to report
// from the control client. // from the control client.
@ -263,26 +265,27 @@ func NewDirect(opts Options) (*Direct, error) {
} }
c := &Direct{ c := &Direct{
httpc: httpc, httpc: httpc,
controlKnobs: opts.ControlKnobs, controlKnobs: opts.ControlKnobs,
getMachinePrivKey: opts.GetMachinePrivateKey, getMachinePrivKey: opts.GetMachinePrivateKey,
serverURL: opts.ServerURL, serverURL: opts.ServerURL,
clock: opts.Clock, clock: opts.Clock,
logf: opts.Logf, logf: opts.Logf,
persist: opts.Persist.View(), persist: opts.Persist.View(),
authKey: opts.AuthKey, authKey: opts.AuthKey,
discoPubKey: opts.DiscoPublicKey, discoPubKey: opts.DiscoPublicKey,
debugFlags: opts.DebugFlags, debugFlags: opts.DebugFlags,
netMon: opts.NetMon, netMon: opts.NetMon,
skipIPForwardingCheck: opts.SkipIPForwardingCheck, skipIPForwardingCheck: opts.SkipIPForwardingCheck,
pinger: opts.Pinger, pinger: opts.Pinger,
popBrowser: opts.PopBrowserURL, popBrowser: opts.PopBrowserURL,
onClientVersion: opts.OnClientVersion, onClientVersion: opts.OnClientVersion,
onControlTime: opts.OnControlTime, onTailnetDefaultAutoUpdate: opts.OnTailnetDefaultAutoUpdate,
c2nHandler: opts.C2NHandler, onControlTime: opts.OnControlTime,
dialer: opts.Dialer, c2nHandler: opts.C2NHandler,
dnsCache: dnsCache, dialer: opts.Dialer,
dialPlan: opts.DialPlan, dnsCache: dnsCache,
dialPlan: opts.DialPlan,
} }
if opts.Hostinfo == nil { if opts.Hostinfo == nil {
c.SetHostinfo(hostinfo.New()) c.SetHostinfo(hostinfo.New())
@ -1091,6 +1094,11 @@ func (c *Direct) sendMapRequest(ctx context.Context, isStreaming bool, nu Netmap
metricMapResponseKeepAlives.Add(1) metricMapResponseKeepAlives.Add(1)
continue continue
} }
if au, ok := resp.DefaultAutoUpdate.Get(); ok {
if c.onTailnetDefaultAutoUpdate != nil {
c.onTailnetDefaultAutoUpdate(au)
}
}
metricMapResponseMap.Add(1) metricMapResponseMap.Add(1)
if gotNonKeepAliveMessage { if gotNonKeepAliveMessage {

@ -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. // invoke it here. For this purpose, it is ok to pass it a zero Arguments.
prefs := b.Prefs().AutoUpdate() prefs := b.Prefs().AutoUpdate()
return tailcfg.C2NUpdateResponse{ return tailcfg.C2NUpdateResponse{
Enabled: envknob.AllowsRemoteUpdate() || prefs.Apply, Enabled: envknob.AllowsRemoteUpdate() || prefs.Apply.EqualBool(true),
Supported: clientupdate.CanAutoUpdate(), Supported: clientupdate.CanAutoUpdate(),
} }
} }

@ -76,6 +76,7 @@ import (
"tailscale.com/types/logger" "tailscale.com/types/logger"
"tailscale.com/types/logid" "tailscale.com/types/logid"
"tailscale.com/types/netmap" "tailscale.com/types/netmap"
"tailscale.com/types/opt"
"tailscale.com/types/persist" "tailscale.com/types/persist"
"tailscale.com/types/preftype" "tailscale.com/types/preftype"
"tailscale.com/types/ptr" "tailscale.com/types/ptr"
@ -1271,8 +1272,8 @@ var preferencePolicies = []preferencePolicyInfo{
}, },
{ {
key: syspolicy.ApplyUpdates, key: syspolicy.ApplyUpdates,
get: func(p ipn.PrefsView) bool { return p.AutoUpdate().Apply }, get: func(p ipn.PrefsView) bool { v, _ := p.AutoUpdate().Apply.Get(); return v },
set: func(p *ipn.Prefs, v bool) { p.AutoUpdate.Apply = v }, set: func(p *ipn.Prefs, v bool) { p.AutoUpdate.Apply.Set(v) },
}, },
{ {
key: syspolicy.EnableRunExitNode, key: syspolicy.EnableRunExitNode,
@ -1767,25 +1768,26 @@ func (b *LocalBackend) Start(opts ipn.Options) error {
// new controlclient. SetPrefs() allows you to overwrite ServerURL, // new controlclient. SetPrefs() allows you to overwrite ServerURL,
// but it won't take effect until the next Start(). // but it won't take effect until the next Start().
cc, err := b.getNewControlClientFunc()(controlclient.Options{ cc, err := b.getNewControlClientFunc()(controlclient.Options{
GetMachinePrivateKey: b.createGetMachinePrivateKeyFunc(), GetMachinePrivateKey: b.createGetMachinePrivateKeyFunc(),
Logf: logger.WithPrefix(b.logf, "control: "), Logf: logger.WithPrefix(b.logf, "control: "),
Persist: *persistv, Persist: *persistv,
ServerURL: serverURL, ServerURL: serverURL,
AuthKey: opts.AuthKey, AuthKey: opts.AuthKey,
Hostinfo: hostinfo, Hostinfo: hostinfo,
HTTPTestClient: httpTestClient, HTTPTestClient: httpTestClient,
DiscoPublicKey: discoPublic, DiscoPublicKey: discoPublic,
DebugFlags: debugFlags, DebugFlags: debugFlags,
NetMon: b.sys.NetMon.Get(), NetMon: b.sys.NetMon.Get(),
Pinger: b, Pinger: b,
PopBrowserURL: b.tellClientToBrowseToURL, PopBrowserURL: b.tellClientToBrowseToURL,
OnClientVersion: b.onClientVersion, OnClientVersion: b.onClientVersion,
OnControlTime: b.em.onControlTime, OnTailnetDefaultAutoUpdate: b.onTailnetDefaultAutoUpdate,
Dialer: b.Dialer(), OnControlTime: b.em.onControlTime,
Observer: b, Dialer: b.Dialer(),
C2NHandler: http.HandlerFunc(b.handleC2N), Observer: b,
DialPlan: &b.dialPlan, // pointer because it can't be copied C2NHandler: http.HandlerFunc(b.handleC2N),
ControlKnobs: b.sys.ControlKnobs(), DialPlan: &b.dialPlan, // pointer because it can't be copied
ControlKnobs: b.sys.ControlKnobs(),
// Don't warn about broken Linux IP forwarding when // Don't warn about broken Linux IP forwarding when
// netstack is being used. // netstack is being used.
@ -2500,6 +2502,32 @@ func (b *LocalBackend) onClientVersion(v *tailcfg.ClientVersion) {
b.send(ipn.Notify{ClientVersion: v}) 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. // For testing lazy machine key generation.
var panicOnMachineKeyGeneration = envknob.RegisterBool("TS_DEBUG_PANIC_MACHINE_KEY") 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.RoutableIPs = prefs.AdvertiseRoutes().AsSlice()
hi.RequestTags = prefs.AdvertiseTags().AsSlice() hi.RequestTags = prefs.AdvertiseTags().AsSlice()
hi.ShieldsUp = prefs.ShieldsUp() hi.ShieldsUp = prefs.ShieldsUp()
hi.AllowsUpdate = envknob.AllowsRemoteUpdate() || prefs.AutoUpdate().Apply hi.AllowsUpdate = envknob.AllowsRemoteUpdate() || prefs.AutoUpdate().Apply.EqualBool(true)
var sshHostKeys []string var sshHostKeys []string
if prefs.RunSSH() && envknob.CanSSHD() { if prefs.RunSSH() && envknob.CanSSHD() {

@ -31,6 +31,7 @@ import (
"tailscale.com/types/logger" "tailscale.com/types/logger"
"tailscale.com/types/logid" "tailscale.com/types/logid"
"tailscale.com/types/netmap" "tailscale.com/types/netmap"
"tailscale.com/types/opt"
"tailscale.com/types/ptr" "tailscale.com/types/ptr"
"tailscale.com/util/dnsname" "tailscale.com/util/dnsname"
"tailscale.com/util/mak" "tailscale.com/util/mak"
@ -1780,13 +1781,13 @@ func TestApplySysPolicy(t *testing.T) {
prefs: ipn.Prefs{ prefs: ipn.Prefs{
AutoUpdate: ipn.AutoUpdatePrefs{ AutoUpdate: ipn.AutoUpdatePrefs{
Check: true, Check: true,
Apply: false, Apply: opt.NewBool(false),
}, },
}, },
wantPrefs: ipn.Prefs{ wantPrefs: ipn.Prefs{
AutoUpdate: ipn.AutoUpdatePrefs{ AutoUpdate: ipn.AutoUpdatePrefs{
Check: true, Check: true,
Apply: true, Apply: opt.NewBool(true),
}, },
}, },
wantAnyChange: true, wantAnyChange: true,
@ -1799,13 +1800,13 @@ func TestApplySysPolicy(t *testing.T) {
prefs: ipn.Prefs{ prefs: ipn.Prefs{
AutoUpdate: ipn.AutoUpdatePrefs{ AutoUpdate: ipn.AutoUpdatePrefs{
Check: true, Check: true,
Apply: true, Apply: opt.NewBool(true),
}, },
}, },
wantPrefs: ipn.Prefs{ wantPrefs: ipn.Prefs{
AutoUpdate: ipn.AutoUpdatePrefs{ AutoUpdate: ipn.AutoUpdatePrefs{
Check: true, Check: true,
Apply: false, Apply: opt.NewBool(false),
}, },
}, },
wantAnyChange: true, wantAnyChange: true,
@ -1818,13 +1819,13 @@ func TestApplySysPolicy(t *testing.T) {
prefs: ipn.Prefs{ prefs: ipn.Prefs{
AutoUpdate: ipn.AutoUpdatePrefs{ AutoUpdate: ipn.AutoUpdatePrefs{
Check: false, Check: false,
Apply: true, Apply: opt.NewBool(true),
}, },
}, },
wantPrefs: ipn.Prefs{ wantPrefs: ipn.Prefs{
AutoUpdate: ipn.AutoUpdatePrefs{ AutoUpdate: ipn.AutoUpdatePrefs{
Check: true, Check: true,
Apply: true, Apply: opt.NewBool(true),
}, },
}, },
wantAnyChange: true, wantAnyChange: true,
@ -1837,13 +1838,13 @@ func TestApplySysPolicy(t *testing.T) {
prefs: ipn.Prefs{ prefs: ipn.Prefs{
AutoUpdate: ipn.AutoUpdatePrefs{ AutoUpdate: ipn.AutoUpdatePrefs{
Check: true, Check: true,
Apply: true, Apply: opt.NewBool(true),
}, },
}, },
wantPrefs: ipn.Prefs{ wantPrefs: ipn.Prefs{
AutoUpdate: ipn.AutoUpdatePrefs{ AutoUpdate: ipn.AutoUpdatePrefs{
Check: false, Check: false,
Apply: true, Apply: opt.NewBool(true),
}, },
}, },
wantAnyChange: 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)
}
})
}
}

@ -21,6 +21,7 @@ import (
"tailscale.com/net/netaddr" "tailscale.com/net/netaddr"
"tailscale.com/net/tsaddr" "tailscale.com/net/tsaddr"
"tailscale.com/tailcfg" "tailscale.com/tailcfg"
"tailscale.com/types/opt"
"tailscale.com/types/persist" "tailscale.com/types/persist"
"tailscale.com/types/preftype" "tailscale.com/types/preftype"
"tailscale.com/types/views" "tailscale.com/types/views"
@ -237,7 +238,17 @@ type AutoUpdatePrefs struct {
// Apply specifies whether background auto-updates are enabled. When // Apply specifies whether background auto-updates are enabled. When
// enabled, tailscaled will apply available updates in the background. // enabled, tailscaled will apply available updates in the background.
// Check must also be set when Apply is set. // 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. // 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) && compareStrings(p.AdvertiseTags, p2.AdvertiseTags) &&
p.Persist.Equals(p2.Persist) && p.Persist.Equals(p2.Persist) &&
p.ProfileName == p2.ProfileName && p.ProfileName == p2.ProfileName &&
p.AutoUpdate == p2.AutoUpdate && p.AutoUpdate.Equals(p2.AutoUpdate) &&
p.AppConnector == p2.AppConnector && p.AppConnector == p2.AppConnector &&
p.PostureChecking == p2.PostureChecking && p.PostureChecking == p2.PostureChecking &&
p.NetfilterKind == p2.NetfilterKind p.NetfilterKind == p2.NetfilterKind
} }
func (au AutoUpdatePrefs) Pretty() string { func (au AutoUpdatePrefs) Pretty() string {
if au.Apply { if au.Apply.EqualBool(true) {
return "update=on " return "update=on "
} }
if au.Check { if au.Check {
@ -600,7 +611,7 @@ func NewPrefs() *Prefs {
NetfilterMode: preftype.NetfilterOn, NetfilterMode: preftype.NetfilterOn,
AutoUpdate: AutoUpdatePrefs{ AutoUpdate: AutoUpdatePrefs{
Check: true, Check: true,
Apply: false, Apply: opt.Bool("unset"),
}, },
} }
} }

@ -20,6 +20,7 @@ import (
"tailscale.com/tailcfg" "tailscale.com/tailcfg"
"tailscale.com/tstest" "tailscale.com/tstest"
"tailscale.com/types/key" "tailscale.com/types/key"
"tailscale.com/types/opt"
"tailscale.com/types/persist" "tailscale.com/types/persist"
"tailscale.com/types/preftype" "tailscale.com/types/preftype"
) )
@ -294,18 +295,18 @@ func TestPrefsEqual(t *testing.T) {
false, false,
}, },
{ {
&Prefs{AutoUpdate: AutoUpdatePrefs{Check: true, Apply: false}}, &Prefs{AutoUpdate: AutoUpdatePrefs{Check: true, Apply: opt.NewBool(false)}},
&Prefs{AutoUpdate: AutoUpdatePrefs{Check: false, Apply: false}}, &Prefs{AutoUpdate: AutoUpdatePrefs{Check: false, Apply: opt.NewBool(false)}},
false, false,
}, },
{ {
&Prefs{AutoUpdate: AutoUpdatePrefs{Check: true, Apply: true}}, &Prefs{AutoUpdate: AutoUpdatePrefs{Check: true, Apply: opt.NewBool(true)}},
&Prefs{AutoUpdate: AutoUpdatePrefs{Check: true, Apply: false}}, &Prefs{AutoUpdate: AutoUpdatePrefs{Check: true, Apply: opt.NewBool(false)}},
false, false,
}, },
{ {
&Prefs{AutoUpdate: AutoUpdatePrefs{Check: true, Apply: false}}, &Prefs{AutoUpdate: AutoUpdatePrefs{Check: true, Apply: opt.NewBool(false)}},
&Prefs{AutoUpdate: AutoUpdatePrefs{Check: true, Apply: false}}, &Prefs{AutoUpdate: AutoUpdatePrefs{Check: true, Apply: opt.NewBool(false)}},
true, true,
}, },
{ {
@ -522,7 +523,7 @@ func TestPrefsPretty(t *testing.T) {
Prefs{ Prefs{
AutoUpdate: AutoUpdatePrefs{ AutoUpdate: AutoUpdatePrefs{
Check: true, Check: true,
Apply: false, Apply: opt.NewBool(false),
}, },
}, },
"linux", "linux",
@ -532,7 +533,7 @@ func TestPrefsPretty(t *testing.T) {
Prefs{ Prefs{
AutoUpdate: AutoUpdatePrefs{ AutoUpdate: AutoUpdatePrefs{
Check: true, Check: true,
Apply: true, Apply: opt.NewBool(true),
}, },
}, },
"linux", "linux",
@ -764,7 +765,7 @@ func TestMaskedPrefsPretty(t *testing.T) {
{ {
m: &MaskedPrefs{ m: &MaskedPrefs{
Prefs: Prefs{ Prefs: Prefs{
AutoUpdate: AutoUpdatePrefs{Check: true, Apply: false}, AutoUpdate: AutoUpdatePrefs{Check: true, Apply: opt.NewBool(false)},
}, },
AutoUpdateSet: AutoUpdatePrefsMask{CheckSet: true, ApplySet: false}, AutoUpdateSet: AutoUpdatePrefsMask{CheckSet: true, ApplySet: false},
}, },
@ -773,7 +774,7 @@ func TestMaskedPrefsPretty(t *testing.T) {
{ {
m: &MaskedPrefs{ m: &MaskedPrefs{
Prefs: Prefs{ Prefs: Prefs{
AutoUpdate: AutoUpdatePrefs{Check: true, Apply: true}, AutoUpdate: AutoUpdatePrefs{Check: true, Apply: opt.NewBool(true)},
}, },
AutoUpdateSet: AutoUpdatePrefsMask{CheckSet: true, ApplySet: true}, AutoUpdateSet: AutoUpdatePrefsMask{CheckSet: true, ApplySet: true},
}, },
@ -782,7 +783,7 @@ func TestMaskedPrefsPretty(t *testing.T) {
{ {
m: &MaskedPrefs{ m: &MaskedPrefs{
Prefs: Prefs{ Prefs: Prefs{
AutoUpdate: AutoUpdatePrefs{Check: true, Apply: false}, AutoUpdate: AutoUpdatePrefs{Check: true, Apply: opt.NewBool(false)},
}, },
AutoUpdateSet: AutoUpdatePrefsMask{CheckSet: false, ApplySet: true}, AutoUpdateSet: AutoUpdatePrefsMask{CheckSet: false, ApplySet: true},
}, },
@ -791,7 +792,7 @@ func TestMaskedPrefsPretty(t *testing.T) {
{ {
m: &MaskedPrefs{ m: &MaskedPrefs{
Prefs: Prefs{ Prefs: Prefs{
AutoUpdate: AutoUpdatePrefs{Check: true, Apply: true}, AutoUpdate: AutoUpdatePrefs{Check: true, Apply: opt.NewBool(true)},
}, },
AutoUpdateSet: AutoUpdatePrefsMask{CheckSet: false, ApplySet: false}, AutoUpdateSet: AutoUpdatePrefsMask{CheckSet: false, ApplySet: false},
}, },

@ -123,7 +123,8 @@ type CapabilityVersion int
// - 80: 2023-11-16: can handle c2n GET /tls-cert-status // - 80: 2023-11-16: can handle c2n GET /tls-cert-status
// - 81: 2023-11-17: MapResponse.PacketFilters (incremental packet filter updates) // - 81: 2023-11-17: MapResponse.PacketFilters (incremental packet filter updates)
// - 82: 2023-12-01: Client understands NodeAttrLinuxMustUseIPTables, NodeAttrLinuxMustUseNfTables, c2n /netfilter-kind // - 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 type StableID string
@ -1877,6 +1878,13 @@ type MapResponse struct {
// download and whether the client is using it. A nil value means no change // download and whether the client is using it. A nil value means no change
// or nothing to report. // or nothing to report.
ClientVersion *ClientVersion `json:",omitempty"` 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 // ClientVersion is information about the latest client version that's available

@ -176,5 +176,6 @@ func mapResponseContainsNonPatchFields(res *tailcfg.MapResponse) bool {
// PeersChanged to PeersChangedPatch in patchifyPeersChanged before this // PeersChanged to PeersChangedPatch in patchifyPeersChanged before this
// function is called, so it should never be set anyway. But for // function is called, so it should never be set anyway. But for
// completedness, and for tests, check it too: // completedness, and for tests, check it too:
res.PeersChanged != nil res.PeersChanged != nil ||
res.DefaultAutoUpdate != ""
} }

@ -13,6 +13,7 @@ import (
"github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp"
"tailscale.com/tailcfg" "tailscale.com/tailcfg"
"tailscale.com/types/logger" "tailscale.com/types/logger"
"tailscale.com/types/opt"
"tailscale.com/types/ptr" "tailscale.com/types/ptr"
) )
@ -26,6 +27,9 @@ func TestMapResponseContainsNonPatchFields(t *testing.T) {
case reflect.Bool: case reflect.Bool:
return reflect.ValueOf(true) return reflect.ValueOf(true)
case reflect.String: case reflect.String:
if reflect.TypeOf(opt.Bool("")) == t {
return reflect.ValueOf("true").Convert(t)
}
return reflect.ValueOf("foo").Convert(t) return reflect.ValueOf("foo").Convert(t)
case reflect.Int64: case reflect.Int64:
return reflect.ValueOf(int64(1)) return reflect.ValueOf(int64(1))

@ -18,6 +18,12 @@ import (
// field without it being dropped. // field without it being dropped.
type Bool string 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) { func (b *Bool) Set(v bool) {
*b = Bool(strconv.FormatBool(v)) *b = Bool(strconv.FormatBool(v))
} }

Loading…
Cancel
Save