Browse Source

cmd/tailscale,ipn: add auto-update flags and prefs (#8861)

The flags are hidden for now. Adding propagation to tailscaled and
persistence only. The prefs field is wrapped in a struct to allow for
future expansion (like update schedule).

Updates #6907

Signed-off-by: Andrew Lytvynov <[email protected]>
Andrew Lytvynov 2 years ago
parent
commit
34e3450734
8 changed files with 138 additions and 16 deletions
  1. 24 0
      cmd/tailscale/cli/cli_test.go
  2. 15 0
      cmd/tailscale/cli/set.go
  3. 4 0
      cmd/tailscale/cli/up.go
  4. 1 0
      ipn/ipn_clone.go
  5. 2 0
      ipn/ipn_view.go
  6. 2 1
      ipn/ipnlocal/c2n.go
  7. 40 1
      ipn/prefs.go
  8. 50 14
      ipn/prefs_test.go

+ 24 - 0
cmd/tailscale/cli/cli_test.go

@@ -556,6 +556,10 @@ func TestPrefsFromUpArgs(t *testing.T) {
 				NetfilterMode:    preftype.NetfilterOn,
 				CorpDNS:          true,
 				AllowSingleHosts: true,
+				AutoUpdate: ipn.AutoUpdatePrefs{
+					Check: true,
+					Apply: false,
+				},
 			},
 		},
 		{
@@ -569,6 +573,10 @@ func TestPrefsFromUpArgs(t *testing.T) {
 				AllowSingleHosts: true,
 				RouteAll:         true,
 				NetfilterMode:    preftype.NetfilterOn,
+				AutoUpdate: ipn.AutoUpdatePrefs{
+					Check: true,
+					Apply: false,
+				},
 			},
 		},
 		{
@@ -584,6 +592,10 @@ func TestPrefsFromUpArgs(t *testing.T) {
 					netip.MustParsePrefix("::/0"),
 				},
 				NetfilterMode: preftype.NetfilterOn,
+				AutoUpdate: ipn.AutoUpdatePrefs{
+					Check: true,
+					Apply: false,
+				},
 			},
 		},
 		{
@@ -670,6 +682,10 @@ func TestPrefsFromUpArgs(t *testing.T) {
 				WantRunning:   true,
 				NetfilterMode: preftype.NetfilterNoDivert,
 				NoSNAT:        true,
+				AutoUpdate: ipn.AutoUpdatePrefs{
+					Check: true,
+					Apply: false,
+				},
 			},
 		},
 		{
@@ -683,6 +699,10 @@ func TestPrefsFromUpArgs(t *testing.T) {
 				WantRunning:   true,
 				NetfilterMode: preftype.NetfilterOff,
 				NoSNAT:        true,
+				AutoUpdate: ipn.AutoUpdatePrefs{
+					Check: true,
+					Apply: false,
+				},
 			},
 		},
 		{
@@ -698,6 +718,10 @@ func TestPrefsFromUpArgs(t *testing.T) {
 				AdvertiseRoutes: []netip.Prefix{
 					netip.MustParsePrefix("fd7a:115c:a1e0:b1a::bb:10.0.0.0/112"),
 				},
+				AutoUpdate: ipn.AutoUpdatePrefs{
+					Check: true,
+					Apply: false,
+				},
 			},
 		},
 		{

+ 15 - 0
cmd/tailscale/cli/set.go

@@ -11,6 +11,7 @@ import (
 	"net/netip"
 
 	"github.com/peterbourgon/ff/v3/ffcli"
+	"tailscale.com/clientupdate"
 	"tailscale.com/ipn"
 	"tailscale.com/net/netutil"
 	"tailscale.com/net/tsaddr"
@@ -46,6 +47,8 @@ type setArgsT struct {
 	acceptedRisks          string
 	profileName            string
 	forceDaemon            bool
+	updateCheck            bool
+	updateApply            bool
 }
 
 func newSetFlagSet(goos string, setArgs *setArgsT) *flag.FlagSet {
@@ -61,6 +64,8 @@ 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.updateCheck, "update-check", true, "HIDDEN: notify about available Tailscale updates")
+	setf.BoolVar(&setArgs.updateApply, "auto-update", false, "HIDDEN: automatically update to the latest available version")
 	if safesocket.GOOSUsesPeerCreds(goos) {
 		setf.StringVar(&setArgs.opUser, "operator", "", "Unix username to allow to operate on tailscaled without sudo")
 	}
@@ -99,6 +104,10 @@ func runSet(ctx context.Context, args []string) (retErr error) {
 			Hostname:               setArgs.hostname,
 			OperatorUser:           setArgs.opUser,
 			ForceDaemon:            setArgs.forceDaemon,
+			AutoUpdate: ipn.AutoUpdatePrefs{
+				Check: setArgs.updateCheck,
+				Apply: setArgs.updateApply,
+			},
 		},
 	}
 
@@ -143,6 +152,12 @@ func runSet(ctx context.Context, args []string) (retErr error) {
 			return err
 		}
 	}
+	if maskedPrefs.AutoUpdateSet {
+		_, err := clientupdate.NewUpdater(clientupdate.Arguments{})
+		if errors.Is(err, errors.ErrUnsupported) {
+			return errors.New("automatic updates are not supported on this platform")
+		}
+	}
 	checkPrefs := curPrefs.Clone()
 	checkPrefs.ApplyEdits(maskedPrefs)
 	if err := localClient.CheckPrefs(ctx, checkPrefs); err != nil {

+ 4 - 0
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,8 @@ func init() {
 	addPrefFlagMapping("operator", "OperatorUser")
 	addPrefFlagMapping("ssh", "RunSSH")
 	addPrefFlagMapping("nickname", "ProfileName")
+	addPrefFlagMapping("update-check", "AutoUpdate")
+	addPrefFlagMapping("auto-update", "AutoUpdate")
 }
 
 func addPrefFlagMapping(flagName string, prefNames ...string) {

+ 1 - 0
ipn/ipn_clone.go

@@ -51,6 +51,7 @@ var _PrefsCloneNeedsRegeneration = Prefs(struct {
 	NetfilterMode          preftype.NetfilterMode
 	OperatorUser           string
 	ProfileName            string
+	AutoUpdate             AutoUpdatePrefs
 	Persist                *persist.Persist
 }{})
 

+ 2 - 0
ipn/ipn_view.go

@@ -86,6 +86,7 @@ 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) AutoUpdate() AutoUpdatePrefs           { return v.ж.AutoUpdate }
 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.
@@ -111,6 +112,7 @@ var _PrefsViewNeedsRegeneration = Prefs(struct {
 	NetfilterMode          preftype.NetfilterMode
 	OperatorUser           string
 	ProfileName            string
+	AutoUpdate             AutoUpdatePrefs
 	Persist                *persist.Persist
 }{})
 

+ 2 - 1
ipn/ipnlocal/c2n.go

@@ -127,9 +127,10 @@ func (b *LocalBackend) handleC2NUpdate(w http.ResponseWriter, r *http.Request) {
 	//
 	// Note that we create the Updater solely to check for errors; we do not
 	// invoke it here. For this purpose, it is ok to pass it a zero Arguments.
+	prefs := b.Prefs().AutoUpdate()
 	_, err := clientupdate.NewUpdater(clientupdate.Arguments{})
 	res := tailcfg.C2NUpdateResponse{
-		Enabled:   envknob.AllowsRemoteUpdate(),
+		Enabled:   envknob.AllowsRemoteUpdate() || prefs.Apply,
 		Supported: err == nil && !version.IsMacSysExt(),
 	}
 

+ 40 - 1
ipn/prefs.go

@@ -196,6 +196,10 @@ type Prefs struct {
 	// and CLI.
 	ProfileName string `json:",omitempty"`
 
+	// AutoUpdate sets the auto-update preferences for the node agent. See
+	// AutoUpdatePrefs docs for more details.
+	AutoUpdate AutoUpdatePrefs
+
 	// 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.
@@ -204,6 +208,18 @@ type Prefs struct {
 	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 bool
+}
+
 // MaskedPrefs is a Prefs with an associated bitmask of which fields are set.
 type MaskedPrefs struct {
 	Prefs
@@ -229,6 +245,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
@@ -284,6 +301,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"
 	}
@@ -360,6 +383,7 @@ func (p *Prefs) pretty(goos string) string {
 	if p.OperatorUser != "" {
 		fmt.Fprintf(&sb, "op=%q ", p.OperatorUser)
 	}
+	sb.WriteString(p.AutoUpdate.Pretty())
 	if p.Persist != nil {
 		sb.WriteString(p.Persist.Pretty())
 	} else {
@@ -414,7 +438,18 @@ 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 == p2.AutoUpdate
+}
+
+func (au AutoUpdatePrefs) Pretty() string {
+	if au.Apply {
+		return "update=on "
+	}
+	if au.Check {
+		return "update=check "
+	}
+	return "update=off "
 }
 
 func compareIPNets(a, b []netip.Prefix) bool {
@@ -459,6 +494,10 @@ func NewPrefs() *Prefs {
 		CorpDNS:          true,
 		WantRunning:      false,
 		NetfilterMode:    preftype.NetfilterOn,
+		AutoUpdate: AutoUpdatePrefs{
+			Check: true,
+			Apply: false,
+		},
 	}
 }
 

+ 50 - 14
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,21 @@ func TestPrefsEqual(t *testing.T) {
 			&Prefs{ProfileName: "home"},
 			false,
 		},
+		{
+			&Prefs{AutoUpdate: AutoUpdatePrefs{Check: true, Apply: false}},
+			&Prefs{AutoUpdate: AutoUpdatePrefs{Check: false, Apply: false}},
+			false,
+		},
+		{
+			&Prefs{AutoUpdate: AutoUpdatePrefs{Check: true, Apply: true}},
+			&Prefs{AutoUpdate: AutoUpdatePrefs{Check: true, Apply: false}},
+			false,
+		},
+		{
+			&Prefs{AutoUpdate: AutoUpdatePrefs{Check: true, Apply: false}},
+			&Prefs{AutoUpdate: AutoUpdatePrefs{Check: true, Apply: false}},
+			true,
+		},
 	}
 	for i, tt := range tests {
 		got := tt.a.Equals(tt.b)
@@ -372,22 +388,22 @@ func TestPrefsPretty(t *testing.T) {
 		{
 			Prefs{},
 			"linux",
-			"Prefs{ra=false mesh=false dns=false want=false routes=[] nf=off Persist=nil}",
+			"Prefs{ra=false mesh=false dns=false want=false routes=[] nf=off update=off Persist=nil}",
 		},
 		{
 			Prefs{},
 			"windows",
-			"Prefs{ra=false mesh=false dns=false want=false Persist=nil}",
+			"Prefs{ra=false mesh=false dns=false want=false update=off Persist=nil}",
 		},
 		{
 			Prefs{ShieldsUp: true},
 			"windows",
-			"Prefs{ra=false mesh=false dns=false want=false shields=true Persist=nil}",
+			"Prefs{ra=false mesh=false dns=false want=false shields=true update=off Persist=nil}",
 		},
 		{
 			Prefs{AllowSingleHosts: true},
 			"windows",
-			"Prefs{ra=false dns=false want=false Persist=nil}",
+			"Prefs{ra=false dns=false want=false update=off Persist=nil}",
 		},
 		{
 			Prefs{
@@ -395,7 +411,7 @@ func TestPrefsPretty(t *testing.T) {
 				AllowSingleHosts: true,
 			},
 			"windows",
-			"Prefs{ra=false dns=false want=false notepad=true Persist=nil}",
+			"Prefs{ra=false dns=false want=false notepad=true update=off Persist=nil}",
 		},
 		{
 			Prefs{
@@ -404,7 +420,7 @@ func TestPrefsPretty(t *testing.T) {
 				ForceDaemon:      true, // server mode
 			},
 			"windows",
-			"Prefs{ra=false dns=false want=true server=true Persist=nil}",
+			"Prefs{ra=false dns=false want=true server=true update=off Persist=nil}",
 		},
 		{
 			Prefs{
@@ -414,14 +430,14 @@ func TestPrefsPretty(t *testing.T) {
 				AdvertiseTags:    []string{"tag:foo", "tag:bar"},
 			},
 			"darwin",
-			`Prefs{ra=false dns=false want=true tags=tag:foo,tag:bar url="http://localhost:1234" Persist=nil}`,
+			`Prefs{ra=false dns=false want=true tags=tag:foo,tag:bar url="http://localhost:1234" update=off Persist=nil}`,
 		},
 		{
 			Prefs{
 				Persist: &persist.Persist{},
 			},
 			"linux",
-			`Prefs{ra=false mesh=false dns=false want=false routes=[] nf=off Persist{lm=, o=, n= u=""}}`,
+			`Prefs{ra=false mesh=false dns=false want=false routes=[] nf=off update=off Persist{lm=, o=, n= u=""}}`,
 		},
 		{
 			Prefs{
@@ -430,21 +446,21 @@ func TestPrefsPretty(t *testing.T) {
 				},
 			},
 			"linux",
-			`Prefs{ra=false mesh=false dns=false want=false routes=[] nf=off Persist{lm=, o=, n=[B1VKl] u=""}}`,
+			`Prefs{ra=false mesh=false dns=false want=false routes=[] nf=off update=off Persist{lm=, o=, n=[B1VKl] u=""}}`,
 		},
 		{
 			Prefs{
 				ExitNodeIP: netip.MustParseAddr("1.2.3.4"),
 			},
 			"linux",
-			`Prefs{ra=false mesh=false dns=false want=false exit=1.2.3.4 lan=false routes=[] nf=off Persist=nil}`,
+			`Prefs{ra=false mesh=false dns=false want=false exit=1.2.3.4 lan=false routes=[] nf=off update=off Persist=nil}`,
 		},
 		{
 			Prefs{
 				ExitNodeID: tailcfg.StableNodeID("myNodeABC"),
 			},
 			"linux",
-			`Prefs{ra=false mesh=false dns=false want=false exit=myNodeABC lan=false routes=[] nf=off Persist=nil}`,
+			`Prefs{ra=false mesh=false dns=false want=false exit=myNodeABC lan=false routes=[] nf=off update=off Persist=nil}`,
 		},
 		{
 			Prefs{
@@ -452,21 +468,41 @@ func TestPrefsPretty(t *testing.T) {
 				ExitNodeAllowLANAccess: true,
 			},
 			"linux",
-			`Prefs{ra=false mesh=false dns=false want=false exit=myNodeABC lan=true routes=[] nf=off Persist=nil}`,
+			`Prefs{ra=false mesh=false dns=false want=false exit=myNodeABC lan=true routes=[] nf=off update=off Persist=nil}`,
 		},
 		{
 			Prefs{
 				ExitNodeAllowLANAccess: true,
 			},
 			"linux",
-			`Prefs{ra=false mesh=false dns=false want=false routes=[] nf=off Persist=nil}`,
+			`Prefs{ra=false mesh=false dns=false want=false routes=[] nf=off update=off Persist=nil}`,
 		},
 		{
 			Prefs{
 				Hostname: "foo",
 			},
 			"linux",
-			`Prefs{ra=false mesh=false dns=false want=false routes=[] nf=off host="foo" Persist=nil}`,
+			`Prefs{ra=false mesh=false dns=false want=false routes=[] nf=off host="foo" update=off Persist=nil}`,
+		},
+		{
+			Prefs{
+				AutoUpdate: AutoUpdatePrefs{
+					Check: true,
+					Apply: false,
+				},
+			},
+			"linux",
+			`Prefs{ra=false mesh=false dns=false want=false routes=[] nf=off update=check Persist=nil}`,
+		},
+		{
+			Prefs{
+				AutoUpdate: AutoUpdatePrefs{
+					Check: true,
+					Apply: true,
+				},
+			},
+			"linux",
+			`Prefs{ra=false mesh=false dns=false want=false routes=[] nf=off update=on Persist=nil}`,
 		},
 	}
 	for i, tt := range tests {