ipn,ipn/local: broadcast ClientVersion if AutoUpdate.Check (#19107)

If AutoUpdate.Check is false, the client has opted out of checking for updates, so we shouldn't broadcast ClientVersion. If the client has opted in, it should be included in the initial Notify.

Updates tailscale/corp#32629

Signed-off-by: kari-ts <kari@tailscale.com>
main
kari-ts 1 month ago committed by GitHub
parent 1e51d57cdd
commit 9992b7c817
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 2
      ipn/backend.go
  2. 20
      ipn/ipnlocal/local.go
  3. 92
      ipn/ipnlocal/local_test.go

@ -85,6 +85,8 @@ const (
NotifyHealthActions NotifyWatchOpt = 1 << 9 // if set, include PrimaryActions in health.State. Otherwise append the action URL to the text
NotifyInitialSuggestedExitNode NotifyWatchOpt = 1 << 10 // if set, the first Notify message (sent immediately) will contain the current SuggestedExitNode if available
NotifyInitialClientVersion NotifyWatchOpt = 1 << 11 // if set, the first Notify message (sent immediately) will contain the current ClientVersion if available and if update checks are enabled
)
// Notify is a communication from a backend (e.g. tailscaled) to a frontend

@ -3143,7 +3143,7 @@ func (b *LocalBackend) WatchNotificationsAs(ctx context.Context, actor ipnauth.A
b.mu.Lock()
const initialBits = ipn.NotifyInitialState | ipn.NotifyInitialPrefs | ipn.NotifyInitialNetMap | ipn.NotifyInitialDriveShares | ipn.NotifyInitialSuggestedExitNode
const initialBits = ipn.NotifyInitialState | ipn.NotifyInitialPrefs | ipn.NotifyInitialNetMap | ipn.NotifyInitialDriveShares | ipn.NotifyInitialSuggestedExitNode | ipn.NotifyInitialClientVersion
if mask&initialBits != 0 {
cn := b.currentNode()
ini = &ipn.Notify{Version: version.Long()}
@ -3171,6 +3171,11 @@ func (b *LocalBackend) WatchNotificationsAs(ctx context.Context, actor ipnauth.A
ini.SuggestedExitNode = &en.ID
}
}
if mask&ipn.NotifyInitialClientVersion != 0 {
if prefs := b.pm.CurrentPrefs(); prefs.Valid() && prefs.AutoUpdate().Check {
ini.ClientVersion = b.lastClientVersion
}
}
}
ctx, cancel := context.WithCancel(ctx)
@ -3551,10 +3556,13 @@ func (b *LocalBackend) tellRecipientToBrowseToURLLocked(url string, recipient no
// a non-nil ClientVersion message.
func (b *LocalBackend) onClientVersion(v *tailcfg.ClientVersion) {
b.mu.Lock()
defer b.mu.Unlock()
b.lastClientVersion = v
b.health.SetLatestVersion(v)
b.mu.Unlock()
b.send(ipn.Notify{ClientVersion: v})
prefs := b.pm.CurrentPrefs()
if prefs.Valid() && prefs.AutoUpdate().Check {
b.sendLocked(ipn.Notify{ClientVersion: v})
}
}
func (b *LocalBackend) onTailnetDefaultAutoUpdate(au bool) {
@ -4757,6 +4765,12 @@ func (b *LocalBackend) setPrefsLocked(newp *ipn.Prefs) ipn.PrefsView {
b.authReconfigLocked()
}
if newp.AutoUpdate.Check && !oldp.AutoUpdate().Check {
if cv := b.lastClientVersion; cv != nil {
b.sendLocked(ipn.Notify{ClientVersion: cv})
}
}
b.sendLocked(ipn.Notify{Prefs: &prefs})
return prefs
}

@ -7400,6 +7400,98 @@ func TestDeps(t *testing.T) {
}.Check(t)
}
func TestOnClientVersionRespectsAutoUpdateCheck(t *testing.T) {
lb := newTestLocalBackend(t)
cv := &tailcfg.ClientVersion{
RunningLatest: false,
LatestVersion: "1.96.0",
}
// With Check disabled, onClientVersion should cache but not broadcast.
lb.SetPrefsForTest(&ipn.Prefs{
AutoUpdate: ipn.AutoUpdatePrefs{Check: false},
})
nw := newNotificationWatcher(t, lb, ipnauth.Self)
nw.watch(0, nil, unexpectedClientVersion)
lb.onClientVersion(cv)
nw.check()
// Verify it was cached despite not being broadcast.
lb.mu.Lock()
cached := lb.lastClientVersion
lb.mu.Unlock()
if cached == nil || cached.LatestVersion != "1.96.0" {
t.Fatalf("lastClientVersion not cached: got %v", cached)
}
// With Check enabled, onClientVersion should broadcast.
lb.SetPrefsForTest(&ipn.Prefs{
AutoUpdate: ipn.AutoUpdatePrefs{Check: true},
})
nw.watch(0, []wantedNotification{
wantClientVersionNotify("1.96.0"),
})
lb.onClientVersion(cv)
nw.check()
}
func TestWatchNotificationsInitialClientVersion(t *testing.T) {
lb := newTestLocalBackend(t)
cv := &tailcfg.ClientVersion{
RunningLatest: false,
LatestVersion: "1.96.0",
}
// Set Check=true and cache a ClientVersion.
lb.SetPrefsForTest(&ipn.Prefs{
AutoUpdate: ipn.AutoUpdatePrefs{Check: true},
})
lb.mu.Lock()
lb.lastClientVersion = cv
lb.mu.Unlock()
// Watch with NotifyInitialClientVersion should include ClientVersion.
nw := newNotificationWatcher(t, lb, ipnauth.Self)
nw.watch(ipn.NotifyInitialClientVersion, []wantedNotification{
wantClientVersionNotify("1.96.0"),
})
nw.check()
// Watch without the flag, should not include it.
nw2 := newNotificationWatcher(t, lb, ipnauth.Self)
nw2.watch(0, nil, unexpectedClientVersion)
nw2.check()
// Watch with the flag but Check=false, should not include it.
lb.SetPrefsForTest(&ipn.Prefs{
AutoUpdate: ipn.AutoUpdatePrefs{Check: false},
})
nw3 := newNotificationWatcher(t, lb, ipnauth.Self)
nw3.watch(ipn.NotifyInitialClientVersion, nil, unexpectedClientVersion)
nw3.check()
}
func wantClientVersionNotify(wantLatest string) wantedNotification {
return wantedNotification{
name: fmt.Sprintf("ClientVersion-%s", wantLatest),
cond: func(_ testing.TB, _ ipnauth.Actor, n *ipn.Notify) bool {
return n.ClientVersion != nil && n.ClientVersion.LatestVersion == wantLatest
},
}
}
func unexpectedClientVersion(t testing.TB, _ ipnauth.Actor, n *ipn.Notify) bool {
if n.ClientVersion != nil {
t.Errorf("unexpected ClientVersion: %v", n.ClientVersion)
return true
}
return false
}
func checkError(tb testing.TB, got, want error, fatal bool) {
tb.Helper()
f := tb.Errorf

Loading…
Cancel
Save