From b3d0ebcca3ca5b603912d6ed6f874a41615d50b4 Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Fri, 10 Jul 2026 00:59:51 +0000 Subject: [PATCH] ipn/ipnlocal: only send AllowsUpdate if clientupdate feature is linked in Like the earlier RemoteConfig change, gate Hostinfo.AllowsUpdate on feature.IsRegistered("clientupdate") in addition to the buildfeatures.HasClientUpdate build-tag const. tsnet binaries don't import feature/clientupdate even though ts_omit_clientupdate isn't set, so they shouldn't tell control they can be remotely updated. Add the previously missing feature.Register call to feature/clientupdate, document the binary-support requirement on tailcfg.Hostinfo.AllowsUpdate, and make tsnet's dep test verify it doesn't depend on feature/clientupdate. Updates #12614 Signed-off-by: Brad Fitzpatrick Change-Id: I526ef11f2a4141f5fce161b1f77263324014b5c4 --- feature/clientupdate/clientupdate.go | 1 + ipn/ipnlocal/local.go | 4 +++- tailcfg/tailcfg.go | 12 +++++++++--- tailcfg/tailcfg_view.go | 5 ++++- tsnet/tsnet_test.go | 1 + 5 files changed, 18 insertions(+), 5 deletions(-) diff --git a/feature/clientupdate/clientupdate.go b/feature/clientupdate/clientupdate.go index 999dd7920..4bbd5e1ba 100644 --- a/feature/clientupdate/clientupdate.go +++ b/feature/clientupdate/clientupdate.go @@ -36,6 +36,7 @@ import ( ) func init() { + feature.Register("clientupdate") ipnext.RegisterExtension("clientupdate", newExt) // C2N diff --git a/ipn/ipnlocal/local.go b/ipn/ipnlocal/local.go index dfbfba191..53e98e33e 100644 --- a/ipn/ipnlocal/local.go +++ b/ipn/ipnlocal/local.go @@ -6626,7 +6626,9 @@ func (b *LocalBackend) applyPrefsToHostinfoLocked(hi *tailcfg.Hostinfo, prefs ip // feature/remoteconfig even though ts_omit_remoteconfig is not // set, so we must not claim RemoteConfig is active there. hi.RemoteConfig = buildfeatures.HasRemoteConfig && prefs.RemoteConfig() && feature.IsRegistered("remoteconfig") - hi.AllowsUpdate = buildfeatures.HasClientUpdate && (envknob.AllowsRemoteUpdate() || prefs.AutoUpdate().Apply.EqualBool(true)) + // Likewise for AllowsUpdate: require the clientupdate feature's init to + // have run, not just the build tag being enabled. + hi.AllowsUpdate = buildfeatures.HasClientUpdate && (envknob.AllowsRemoteUpdate() || prefs.AutoUpdate().Apply.EqualBool(true)) && feature.IsRegistered("clientupdate") if buildfeatures.HasAdvertiseRoutes { b.metrics.advertisedRoutes.Set(float64(tsaddr.WithoutExitRoute(prefs.AdvertiseRoutes()).Len())) diff --git a/tailcfg/tailcfg.go b/tailcfg/tailcfg.go index 6614567b5..ceb423536 100644 --- a/tailcfg/tailcfg.go +++ b/tailcfg/tailcfg.go @@ -913,9 +913,15 @@ type Hostinfo struct { // away, even if it's disabled most of the time. As an optimization, this is // only sent if IngressEnabled is false, as IngressEnabled implies that this // option is true. - WireIngress bool `json:",omitzero"` - IngressEnabled bool `json:",omitzero"` // if the node has any funnel endpoint enabled - AllowsUpdate bool `json:",omitzero"` // indicates that the node has opted-in to admin-console-drive remote updates + WireIngress bool `json:",omitzero"` + IngressEnabled bool `json:",omitzero"` // if the node has any funnel endpoint enabled + + // AllowsUpdate reports that the node has opted in to + // admin-console-driven remote updates and that the running binary + // includes client update support (the feature/clientupdate package, + // which tsnet apps don't include). + AllowsUpdate bool `json:",omitzero"` + Machine string `json:",omitzero"` // the current host's machine type (uname -m) GoArch string `json:",omitzero"` // GOARCH value (of the built binary) GoArchVar string `json:",omitzero"` // GOARM, GOAMD64, etc (of the built binary) diff --git a/tailcfg/tailcfg_view.go b/tailcfg/tailcfg_view.go index c2403c9ba..2423120c6 100644 --- a/tailcfg/tailcfg_view.go +++ b/tailcfg/tailcfg_view.go @@ -570,7 +570,10 @@ func (v HostinfoView) WireIngress() bool { return v.ж.WireIngress } // if the node has any funnel endpoint enabled func (v HostinfoView) IngressEnabled() bool { return v.ж.IngressEnabled } -// indicates that the node has opted-in to admin-console-drive remote updates +// AllowsUpdate reports that the node has opted in to +// admin-console-driven remote updates and that the running binary +// includes client update support (the feature/clientupdate package, +// which tsnet apps don't include). func (v HostinfoView) AllowsUpdate() bool { return v.ж.AllowsUpdate } // the current host's machine type (uname -m) diff --git a/tsnet/tsnet_test.go b/tsnet/tsnet_test.go index 6a8aff4f2..6767430f1 100644 --- a/tsnet/tsnet_test.go +++ b/tsnet/tsnet_test.go @@ -3409,6 +3409,7 @@ func TestDeps(t *testing.T) { BadDeps: map[string]string{ "golang.org/x/crypto/ssh": "tsnet should not depend on SSH", "golang.org/x/crypto/ssh/internal/bcrypt_pbkdf": "tsnet should not depend on SSH", + "tailscale.com/feature/clientupdate": "tsnet should not depend on feature/clientupdate", "tailscale.com/feature/remoteconfig": "tsnet should not depend on feature/remoteconfig", "tailscale.com/feature/syspolicy": "tsnet should not depend on syspolicy", "tailscale.com/ipn/store/awsstore": "tsnet callers wanting AWS state storage should import awsstore themselves",