diff --git a/cmd/tailscale/cli/set.go b/cmd/tailscale/cli/set.go index fdd332060ee61..077c2cd7a5a92 100644 --- a/cmd/tailscale/cli/set.go +++ b/cmd/tailscale/cli/set.go @@ -45,6 +45,7 @@ type setArgsT struct { acceptedRisks string profileName string forceDaemon bool + autoUpdate bool } func newSetFlagSet(goos string, setArgs *setArgsT) *flag.FlagSet { @@ -60,6 +61,7 @@ func newSetFlagSet(goos string, setArgs *setArgsT) *flag.FlagSet { setf.StringVar(&setArgs.hostname, "hostname", "", "hostname to use instead of the one provided by the OS") setf.StringVar(&setArgs.advertiseRoutes, "advertise-routes", "", "routes to advertise to other nodes (comma-separated, e.g. \"10.0.0.0/8,192.168.0.0/24\") or empty string to not advertise routes") setf.BoolVar(&setArgs.advertiseDefaultRoute, "advertise-exit-node", false, "offer to be an exit node for internet traffic for the tailnet") + setf.BoolVar(&setArgs.autoUpdate, "auto-update", false, "HIDDEN: automatically update to the latest available version in the background") if safesocket.GOOSUsesPeerCreds(goos) { setf.StringVar(&setArgs.opUser, "operator", "", "Unix username to allow to operate on tailscaled without sudo") } @@ -98,6 +100,7 @@ func runSet(ctx context.Context, args []string) (retErr error) { Hostname: setArgs.hostname, OperatorUser: setArgs.opUser, ForceDaemon: setArgs.forceDaemon, + AutoUpdate: &ipn.AutoUpdatePrefs{Enable: setArgs.autoUpdate}, }, } diff --git a/cmd/tailscale/cli/up.go b/cmd/tailscale/cli/up.go index d0cded47e38b3..9838983603759 100644 --- a/cmd/tailscale/cli/up.go +++ b/cmd/tailscale/cli/up.go @@ -97,6 +97,8 @@ func newUpFlagSet(goos string, upArgs *upArgsT, cmd string) *flag.FlagSet { } upf := newFlagSet(cmd) + // When adding new flags, prefer to put them under "tailscale set" instead + // of here. Setting preferences via "tailscale up" is deprecated. upf.BoolVar(&upArgs.qr, "qr", false, "show QR code for login URLs") upf.StringVar(&upArgs.authKeyOrFile, "auth-key", "", `node authorization key; if it begins with "file:", then it's a path to a file containing the authkey`) @@ -712,6 +714,7 @@ func init() { addPrefFlagMapping("operator", "OperatorUser") addPrefFlagMapping("ssh", "RunSSH") addPrefFlagMapping("nickname", "ProfileName") + addPrefFlagMapping("auto-update", "AutoUpdate") } func addPrefFlagMapping(flagName string, prefNames ...string) { diff --git a/ipn/ipn_clone.go b/ipn/ipn_clone.go index 97207d03986ca..1bcbdc80b35ea 100644 --- a/ipn/ipn_clone.go +++ b/ipn/ipn_clone.go @@ -23,6 +23,10 @@ func (src *Prefs) Clone() *Prefs { *dst = *src dst.AdvertiseTags = append(src.AdvertiseTags[:0:0], src.AdvertiseTags...) dst.AdvertiseRoutes = append(src.AdvertiseRoutes[:0:0], src.AdvertiseRoutes...) + if dst.AutoUpdate != nil { + dst.AutoUpdate = new(AutoUpdatePrefs) + *dst.AutoUpdate = *src.AutoUpdate + } dst.Persist = src.Persist.Clone() return dst } @@ -50,6 +54,7 @@ var _PrefsCloneNeedsRegeneration = Prefs(struct { NetfilterMode preftype.NetfilterMode OperatorUser string ProfileName string + AutoUpdate *AutoUpdatePrefs Persist *persist.Persist }{}) diff --git a/ipn/ipn_view.go b/ipn/ipn_view.go index 1abeb6709d3be..48c623d5ea5a6 100644 --- a/ipn/ipn_view.go +++ b/ipn/ipn_view.go @@ -86,7 +86,15 @@ func (v PrefsView) NoSNAT() bool { return v.ж.NoSNAT } func (v PrefsView) NetfilterMode() preftype.NetfilterMode { return v.ж.NetfilterMode } func (v PrefsView) OperatorUser() string { return v.ж.OperatorUser } func (v PrefsView) ProfileName() string { return v.ж.ProfileName } -func (v PrefsView) Persist() persist.PersistView { return v.ж.Persist.View() } +func (v PrefsView) AutoUpdate() *AutoUpdatePrefs { + if v.ж.AutoUpdate == nil { + return nil + } + x := *v.ж.AutoUpdate + return &x +} + +func (v PrefsView) Persist() persist.PersistView { return v.ж.Persist.View() } // A compilation failure here means this code must be regenerated, with the command at the top of this file. var _PrefsViewNeedsRegeneration = Prefs(struct { @@ -111,6 +119,7 @@ var _PrefsViewNeedsRegeneration = Prefs(struct { NetfilterMode preftype.NetfilterMode OperatorUser string ProfileName string + AutoUpdate *AutoUpdatePrefs Persist *persist.Persist }{}) diff --git a/ipn/prefs.go b/ipn/prefs.go index e81c44ad2394d..682d9b57d8056 100644 --- a/ipn/prefs.go +++ b/ipn/prefs.go @@ -195,6 +195,9 @@ type Prefs struct { // and CLI. ProfileName string `json:",omitempty"` + // AutoUpdate settings for the node agent. + AutoUpdate *AutoUpdatePrefs `json:",omitempty"` + // 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. @@ -203,6 +206,14 @@ type Prefs struct { Persist *persist.Persist `json:"Config"` } +// AutoUpdatePrefs are the auto update settings for the node agent. +type AutoUpdatePrefs struct { + // Enable specifies whether background auto-updates are enabled. When + // enabled, tailscaled will periodically check for available updates and + // apply them. + Enable bool +} + // MaskedPrefs is a Prefs with an associated bitmask of which fields are set. type MaskedPrefs struct { Prefs @@ -228,6 +239,7 @@ type MaskedPrefs struct { NetfilterModeSet bool `json:",omitempty"` OperatorUserSet bool `json:",omitempty"` ProfileNameSet bool `json:",omitempty"` + AutoUpdateSet bool `json:",omitempty"` } // ApplyEdits mutates p, assigning fields from m.Prefs for each MaskedPrefs @@ -283,6 +295,12 @@ func (m *MaskedPrefs) Pretty() 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" } @@ -333,6 +351,9 @@ func (p *Prefs) pretty(goos string) string { if p.ShieldsUp { sb.WriteString("shields=true ") } + if p.AutoUpdate != nil { + fmt.Fprintf(&sb, "autoUpdate={enable=%v} ", p.AutoUpdate.Enable) + } if p.ExitNodeIP.IsValid() { fmt.Fprintf(&sb, "exit=%v lan=%t ", p.ExitNodeIP, p.ExitNodeAllowLANAccess) } else if !p.ExitNodeID.IsZero() { @@ -413,7 +434,19 @@ func (p *Prefs) Equals(p2 *Prefs) bool { compareIPNets(p.AdvertiseRoutes, p2.AdvertiseRoutes) && compareStrings(p.AdvertiseTags, p2.AdvertiseTags) && p.Persist.Equals(p2.Persist) && - p.ProfileName == p2.ProfileName + p.ProfileName == p2.ProfileName && + p.AutoUpdate.Equals(p2.AutoUpdate) +} + +func (au *AutoUpdatePrefs) Equals(au2 *AutoUpdatePrefs) bool { + if (au == nil) != (au2 == nil) { + return false + } + if au == nil { + return true + } + return au.Enable == au2.Enable + } func compareIPNets(a, b []netip.Prefix) bool { diff --git a/ipn/prefs_test.go b/ipn/prefs_test.go index 150d740981907..67bef5c055b98 100644 --- a/ipn/prefs_test.go +++ b/ipn/prefs_test.go @@ -56,6 +56,7 @@ func TestPrefsEqual(t *testing.T) { "NetfilterMode", "OperatorUser", "ProfileName", + "AutoUpdate", "Persist", } if have := fieldsOf(reflect.TypeOf(Prefs{})); !reflect.DeepEqual(have, prefsHandles) { @@ -288,6 +289,26 @@ func TestPrefsEqual(t *testing.T) { &Prefs{ProfileName: "home"}, false, }, + { + &Prefs{AutoUpdate: &AutoUpdatePrefs{Enable: true}}, + &Prefs{AutoUpdate: &AutoUpdatePrefs{Enable: false}}, + false, + }, + { + &Prefs{AutoUpdate: &AutoUpdatePrefs{Enable: true}}, + &Prefs{AutoUpdate: nil}, + false, + }, + { + &Prefs{AutoUpdate: nil}, + &Prefs{AutoUpdate: &AutoUpdatePrefs{Enable: false}}, + false, + }, + { + &Prefs{AutoUpdate: &AutoUpdatePrefs{Enable: true}}, + &Prefs{AutoUpdate: &AutoUpdatePrefs{Enable: true}}, + true, + }, } for i, tt := range tests { got := tt.a.Equals(tt.b)