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 <awly@tailscale.com>
This commit is contained in:
Andrew Lytvynov 2023-09-01 14:45:12 -06:00 committed by GitHub
parent 055fdb235f
commit 34e3450734
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 138 additions and 16 deletions

View File

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

View File

@ -11,6 +11,7 @@
"net/netip" "net/netip"
"github.com/peterbourgon/ff/v3/ffcli" "github.com/peterbourgon/ff/v3/ffcli"
"tailscale.com/clientupdate"
"tailscale.com/ipn" "tailscale.com/ipn"
"tailscale.com/net/netutil" "tailscale.com/net/netutil"
"tailscale.com/net/tsaddr" "tailscale.com/net/tsaddr"
@ -46,6 +47,8 @@ type setArgsT struct {
acceptedRisks string acceptedRisks string
profileName string profileName string
forceDaemon bool forceDaemon bool
updateCheck bool
updateApply bool
} }
func newSetFlagSet(goos string, setArgs *setArgsT) *flag.FlagSet { 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.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.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.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) { if safesocket.GOOSUsesPeerCreds(goos) {
setf.StringVar(&setArgs.opUser, "operator", "", "Unix username to allow to operate on tailscaled without sudo") 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, Hostname: setArgs.hostname,
OperatorUser: setArgs.opUser, OperatorUser: setArgs.opUser,
ForceDaemon: setArgs.forceDaemon, 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 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 := curPrefs.Clone()
checkPrefs.ApplyEdits(maskedPrefs) checkPrefs.ApplyEdits(maskedPrefs)
if err := localClient.CheckPrefs(ctx, checkPrefs); err != nil { if err := localClient.CheckPrefs(ctx, checkPrefs); err != nil {

View File

@ -97,6 +97,8 @@ func newUpFlagSet(goos string, upArgs *upArgsT, cmd string) *flag.FlagSet {
} }
upf := newFlagSet(cmd) 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.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`) 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("operator", "OperatorUser")
addPrefFlagMapping("ssh", "RunSSH") addPrefFlagMapping("ssh", "RunSSH")
addPrefFlagMapping("nickname", "ProfileName") addPrefFlagMapping("nickname", "ProfileName")
addPrefFlagMapping("update-check", "AutoUpdate")
addPrefFlagMapping("auto-update", "AutoUpdate")
} }
func addPrefFlagMapping(flagName string, prefNames ...string) { func addPrefFlagMapping(flagName string, prefNames ...string) {

View File

@ -51,6 +51,7 @@ func (src *Prefs) Clone() *Prefs {
NetfilterMode preftype.NetfilterMode NetfilterMode preftype.NetfilterMode
OperatorUser string OperatorUser string
ProfileName string ProfileName string
AutoUpdate AutoUpdatePrefs
Persist *persist.Persist Persist *persist.Persist
}{}) }{})

View File

@ -86,6 +86,7 @@ func (v PrefsView) NoSNAT() bool { return v.ж.NoSNAT }
func (v PrefsView) NetfilterMode() preftype.NetfilterMode { return v.ж.NetfilterMode } func (v PrefsView) NetfilterMode() preftype.NetfilterMode { return v.ж.NetfilterMode }
func (v PrefsView) OperatorUser() string { return v.ж.OperatorUser } func (v PrefsView) OperatorUser() string { return v.ж.OperatorUser }
func (v PrefsView) ProfileName() string { return v.ж.ProfileName } 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() } 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. // A compilation failure here means this code must be regenerated, with the command at the top of this file.
@ -111,6 +112,7 @@ func (v PrefsView) Persist() persist.PersistView { return v.ж.Persist.
NetfilterMode preftype.NetfilterMode NetfilterMode preftype.NetfilterMode
OperatorUser string OperatorUser string
ProfileName string ProfileName string
AutoUpdate AutoUpdatePrefs
Persist *persist.Persist Persist *persist.Persist
}{}) }{})

View File

@ -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 // 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. // invoke it here. For this purpose, it is ok to pass it a zero Arguments.
prefs := b.Prefs().AutoUpdate()
_, err := clientupdate.NewUpdater(clientupdate.Arguments{}) _, err := clientupdate.NewUpdater(clientupdate.Arguments{})
res := tailcfg.C2NUpdateResponse{ res := tailcfg.C2NUpdateResponse{
Enabled: envknob.AllowsRemoteUpdate(), Enabled: envknob.AllowsRemoteUpdate() || prefs.Apply,
Supported: err == nil && !version.IsMacSysExt(), Supported: err == nil && !version.IsMacSysExt(),
} }

View File

@ -196,6 +196,10 @@ type Prefs struct {
// and CLI. // and CLI.
ProfileName string `json:",omitempty"` 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 // The Persist field is named 'Config' in the file for backward
// compatibility with earlier versions. // compatibility with earlier versions.
// TODO(apenwarr): We should move this out of here, it's not a pref. // 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"` 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. // MaskedPrefs is a Prefs with an associated bitmask of which fields are set.
type MaskedPrefs struct { type MaskedPrefs struct {
Prefs Prefs
@ -229,6 +245,7 @@ type MaskedPrefs struct {
NetfilterModeSet bool `json:",omitempty"` NetfilterModeSet bool `json:",omitempty"`
OperatorUserSet bool `json:",omitempty"` OperatorUserSet bool `json:",omitempty"`
ProfileNameSet bool `json:",omitempty"` ProfileNameSet bool `json:",omitempty"`
AutoUpdateSet bool `json:",omitempty"`
} }
// ApplyEdits mutates p, assigning fields from m.Prefs for each MaskedPrefs // 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 { if v.Type().Elem().Kind() == reflect.String {
return "%s=%q" 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" return "%s=%v"
} }
@ -360,6 +383,7 @@ func (p *Prefs) pretty(goos string) string {
if p.OperatorUser != "" { if p.OperatorUser != "" {
fmt.Fprintf(&sb, "op=%q ", p.OperatorUser) fmt.Fprintf(&sb, "op=%q ", p.OperatorUser)
} }
sb.WriteString(p.AutoUpdate.Pretty())
if p.Persist != nil { if p.Persist != nil {
sb.WriteString(p.Persist.Pretty()) sb.WriteString(p.Persist.Pretty())
} else { } else {
@ -414,7 +438,18 @@ func (p *Prefs) Equals(p2 *Prefs) bool {
compareIPNets(p.AdvertiseRoutes, p2.AdvertiseRoutes) && compareIPNets(p.AdvertiseRoutes, p2.AdvertiseRoutes) &&
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
}
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 { func compareIPNets(a, b []netip.Prefix) bool {
@ -459,6 +494,10 @@ func NewPrefs() *Prefs {
CorpDNS: true, CorpDNS: true,
WantRunning: false, WantRunning: false,
NetfilterMode: preftype.NetfilterOn, NetfilterMode: preftype.NetfilterOn,
AutoUpdate: AutoUpdatePrefs{
Check: true,
Apply: false,
},
} }
} }

View File

@ -56,6 +56,7 @@ func TestPrefsEqual(t *testing.T) {
"NetfilterMode", "NetfilterMode",
"OperatorUser", "OperatorUser",
"ProfileName", "ProfileName",
"AutoUpdate",
"Persist", "Persist",
} }
if have := fieldsOf(reflect.TypeOf(Prefs{})); !reflect.DeepEqual(have, prefsHandles) { if have := fieldsOf(reflect.TypeOf(Prefs{})); !reflect.DeepEqual(have, prefsHandles) {
@ -288,6 +289,21 @@ func TestPrefsEqual(t *testing.T) {
&Prefs{ProfileName: "home"}, &Prefs{ProfileName: "home"},
false, 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 { for i, tt := range tests {
got := tt.a.Equals(tt.b) got := tt.a.Equals(tt.b)
@ -372,22 +388,22 @@ func TestPrefsPretty(t *testing.T) {
{ {
Prefs{}, Prefs{},
"linux", "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{}, Prefs{},
"windows", "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}, Prefs{ShieldsUp: true},
"windows", "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}, Prefs{AllowSingleHosts: true},
"windows", "windows",
"Prefs{ra=false dns=false want=false Persist=nil}", "Prefs{ra=false dns=false want=false update=off Persist=nil}",
}, },
{ {
Prefs{ Prefs{
@ -395,7 +411,7 @@ func TestPrefsPretty(t *testing.T) {
AllowSingleHosts: true, AllowSingleHosts: true,
}, },
"windows", "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{ Prefs{
@ -404,7 +420,7 @@ func TestPrefsPretty(t *testing.T) {
ForceDaemon: true, // server mode ForceDaemon: true, // server mode
}, },
"windows", "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{ Prefs{
@ -414,14 +430,14 @@ func TestPrefsPretty(t *testing.T) {
AdvertiseTags: []string{"tag:foo", "tag:bar"}, AdvertiseTags: []string{"tag:foo", "tag:bar"},
}, },
"darwin", "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{ Prefs{
Persist: &persist.Persist{}, Persist: &persist.Persist{},
}, },
"linux", "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{ Prefs{
@ -430,21 +446,21 @@ func TestPrefsPretty(t *testing.T) {
}, },
}, },
"linux", "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{ Prefs{
ExitNodeIP: netip.MustParseAddr("1.2.3.4"), ExitNodeIP: netip.MustParseAddr("1.2.3.4"),
}, },
"linux", "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{ Prefs{
ExitNodeID: tailcfg.StableNodeID("myNodeABC"), ExitNodeID: tailcfg.StableNodeID("myNodeABC"),
}, },
"linux", "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{ Prefs{
@ -452,21 +468,41 @@ func TestPrefsPretty(t *testing.T) {
ExitNodeAllowLANAccess: true, ExitNodeAllowLANAccess: true,
}, },
"linux", "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{ Prefs{
ExitNodeAllowLANAccess: true, ExitNodeAllowLANAccess: true,
}, },
"linux", "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{ Prefs{
Hostname: "foo", Hostname: "foo",
}, },
"linux", "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 { for i, tt := range tests {