From ce1e02096ab3e9fa8b724bd50c8ac1046b8dcbf4 Mon Sep 17 00:00:00 2001 From: Chris Palmer Date: Wed, 30 Aug 2023 14:50:03 -0700 Subject: [PATCH] ipn/ipnlocal: support most Linuxes in handleC2NUpdate (#9114) * ipn/ipnlocal: support most Linuxes in handleC2NUpdate Updates #6995 Signed-off-by: Chris Palmer --- clientupdate/clientupdate.go | 137 ++++++++++++++++++++--------------- cmd/tailscale/cli/update.go | 2 +- cmd/tailscaled/depaware.txt | 10 ++- ipn/ipnlocal/c2n.go | 26 ++++--- 4 files changed, 100 insertions(+), 75 deletions(-) diff --git a/clientupdate/clientupdate.go b/clientupdate/clientupdate.go index e7c379592..70bd7dc14 100644 --- a/clientupdate/clientupdate.go +++ b/clientupdate/clientupdate.go @@ -57,14 +57,8 @@ func versionToTrack(v string) (string, error) { return "unstable", nil } -type updater struct { - UpdateArgs - track string - update func() error -} - -// UpdateArgs contains arguments needed to run an update. -type UpdateArgs struct { +// Arguments contains arguments needed to run an update. +type Arguments struct { // Version can be a specific version number or one of the predefined track // constants: // @@ -76,7 +70,7 @@ type UpdateArgs struct { // Leaving this empty is the same as using CurrentTrack. Version string // AppStore forces a local app store check, even if the current binary was - // not installed via an app store. + // not installed via an app store. TODO(cpalmer): Remove this. AppStore bool // Logf is a logger for update progress messages. Logf logger.Logf @@ -89,30 +83,31 @@ type UpdateArgs struct { PkgsAddr string } -func (args UpdateArgs) validate() error { +func (args Arguments) validate() error { if args.Confirm == nil { - return errors.New("missing Confirm callback in UpdateArgs") + return errors.New("missing Confirm callback in Arguments") } if args.Logf == nil { - return errors.New("missing Logf callback in UpdateArgs") + return errors.New("missing Logf callback in Arguments") } return nil } -// Update runs a single update attempt using the platform-specific mechanism. -// -// On Windows, this copies the calling binary and re-executes it to apply the -// update. The calling binary should handle an "update" subcommand and call -// this function again for the re-executed binary to proceed. -func Update(args UpdateArgs) error { - if err := args.validate(); err != nil { - return err - } - if args.PkgsAddr == "" { - args.PkgsAddr = "https://pkgs.tailscale.com" +type Updater struct { + Arguments + track string + // Update is a platform-specific method that updates the installation. May be + // nil (not all platforms support updates from within Tailscale). + Update func() error +} + +func NewUpdater(args Arguments) (*Updater, error) { + up := Updater{ + Arguments: args, } - up := &updater{ - UpdateArgs: args, + up.Update = up.getUpdateFunction() + if up.Update == nil { + return nil, errors.ErrUnsupported } switch up.Version { case StableTrack, UnstableTrack: @@ -127,60 +122,82 @@ func Update(args UpdateArgs) error { var err error up.track, err = versionToTrack(args.Version) if err != nil { - return err + return nil, err } } + if args.PkgsAddr == "" { + args.PkgsAddr = "https://pkgs.tailscale.com" + } + return &up, nil +} + +type updateFunction func() error + +func (up *Updater) getUpdateFunction() updateFunction { switch runtime.GOOS { case "windows": - up.update = up.updateWindows + return up.updateWindows case "linux": switch distro.Get() { case distro.Synology: - up.update = up.updateSynology + return up.updateSynology case distro.Debian: // includes Ubuntu - up.update = up.updateDebLike + return up.updateDebLike case distro.Arch: - up.update = up.updateArchLike + return up.updateArchLike case distro.Alpine: - up.update = up.updateAlpineLike + return up.updateAlpineLike } switch { case haveExecutable("pacman"): - up.update = up.updateArchLike + return up.updateArchLike case haveExecutable("apt-get"): // TODO(awly): add support for "apt" // The distro.Debian switch case above should catch most apt-based // systems, but add this fallback just in case. - up.update = up.updateDebLike + return up.updateDebLike case haveExecutable("dnf"): - up.update = up.updateFedoraLike("dnf") + return up.updateFedoraLike("dnf") case haveExecutable("yum"): - up.update = up.updateFedoraLike("yum") + return up.updateFedoraLike("yum") case haveExecutable("apk"): - up.update = up.updateAlpineLike + return up.updateAlpineLike } // If nothing matched, fall back to tarball updates. - if up.update == nil { - up.update = up.updateLinuxBinary + if up.Update == nil { + return up.updateLinuxBinary } case "darwin": switch { - case !args.AppStore && !version.IsSandboxedMacOS(): - return errors.ErrUnsupported - case !args.AppStore && strings.HasSuffix(os.Getenv("HOME"), "/io.tailscale.ipn.macsys/Data"): - up.update = up.updateMacSys + case !up.Arguments.AppStore && !version.IsSandboxedMacOS(): + return nil + case !up.Arguments.AppStore && strings.HasSuffix(os.Getenv("HOME"), "/io.tailscale.ipn.macsys/Data"): + return up.updateMacSys default: - up.update = up.updateMacAppStore + return up.updateMacAppStore } case "freebsd": - up.update = up.updateFreeBSD + return up.updateFreeBSD } - if up.update == nil { - return errors.ErrUnsupported + return nil +} + +// Update runs a single update attempt using the platform-specific mechanism. +// +// On Windows, this copies the calling binary and re-executes it to apply the +// update. The calling binary should handle an "update" subcommand and call +// this function again for the re-executed binary to proceed. +func Update(args Arguments) error { + if err := args.validate(); err != nil { + return err + } + up, err := NewUpdater(args) + if err != nil { + return err } - return up.update() + return up.Update() } -func (up *updater) confirm(ver string) bool { +func (up *Updater) confirm(ver string) bool { if version.Short() == ver { up.Logf("already running %v; no update needed", ver) return false @@ -193,7 +210,7 @@ func (up *updater) confirm(ver string) bool { const synoinfoConfPath = "/etc/synoinfo.conf" -func (up *updater) updateSynology() error { +func (up *Updater) updateSynology() error { if up.Version != "" { return errors.New("installing a specific version on Synology is not supported") } @@ -303,7 +320,7 @@ func parseSynoinfo(path string) (string, error) { return "", fmt.Errorf(`missing "unique=" field in %q`, path) } -func (up *updater) updateDebLike() error { +func (up *Updater) updateDebLike() error { if err := requireRoot(); err != nil { return err } @@ -409,7 +426,7 @@ func updateDebianAptSourcesListBytes(was []byte, dstTrack string) (newContent [] return buf.Bytes(), nil } -func (up *updater) updateArchLike() error { +func (up *Updater) updateArchLike() error { if err := exec.Command("pacman", "--query", "tailscale").Run(); err != nil && isExitError(err) { // Tailscale was not installed via pacman, update via tarball download // instead. @@ -427,7 +444,7 @@ const yumRepoConfigFile = "/etc/yum.repos.d/tailscale.repo" // updateFedoraLike updates tailscale on any distros in the Fedora family, // specifically anything that uses "dnf" or "yum" package managers. The actual // package manager is passed via packageManager. -func (up *updater) updateFedoraLike(packageManager string) func() error { +func (up *Updater) updateFedoraLike(packageManager string) func() error { return func() (err error) { if err := requireRoot(); err != nil { return err @@ -508,7 +525,7 @@ func updateYUMRepoTrack(repoFile, dstTrack string) (rewrote bool, err error) { return true, os.WriteFile(repoFile, newContent.Bytes(), 0644) } -func (up *updater) updateAlpineLike() (err error) { +func (up *Updater) updateAlpineLike() (err error) { if up.Version != "" { return errors.New("installing a specific version on Alpine-based distros is not supported") } @@ -570,11 +587,11 @@ func parseAlpinePackageVersion(out []byte) (string, error) { return "", errors.New("tailscale version not found in output") } -func (up *updater) updateMacSys() error { +func (up *Updater) updateMacSys() error { return errors.New("NOTREACHED: On MacSys builds, `tailscale update` is handled in Swift to launch the GUI updater") } -func (up *updater) updateMacAppStore() error { +func (up *Updater) updateMacAppStore() error { out, err := exec.Command("defaults", "read", "/Library/Preferences/com.apple.commerce.plist", "AutoUpdate").CombinedOutput() if err != nil { return fmt.Errorf("can't check App Store auto-update setting: %w, output: %q", err, string(out)) @@ -635,7 +652,7 @@ var ( markTempFileFunc func(string) error // or nil on non-Windows ) -func (up *updater) updateWindows() error { +func (up *Updater) updateWindows() error { if msi := os.Getenv(winMSIEnv); msi != "" { up.Logf("installing %v ...", msi) if err := up.installMSI(msi); err != nil { @@ -705,7 +722,7 @@ func (up *updater) updateWindows() error { panic("unreachable") } -func (up *updater) installMSI(msi string) error { +func (up *Updater) installMSI(msi string) error { var err error for tries := 0; tries < 2; tries++ { cmd := exec.Command("msiexec.exe", "/i", filepath.Base(msi), "/quiet", "/promptrestart", "/qn") @@ -772,7 +789,7 @@ func makeSelfCopy() (tmpPathExe string, err error) { return f2.Name(), f2.Close() } -func (up *updater) downloadURLToFile(pathSrc, fileDst string) (ret error) { +func (up *Updater) downloadURLToFile(pathSrc, fileDst string) (ret error) { c, err := distsign.NewClient(up.Logf, up.PkgsAddr) if err != nil { return err @@ -780,7 +797,7 @@ func (up *updater) downloadURLToFile(pathSrc, fileDst string) (ret error) { return c.Download(context.Background(), pathSrc, fileDst) } -func (up *updater) updateFreeBSD() (err error) { +func (up *Updater) updateFreeBSD() (err error) { if up.Version != "" { return errors.New("installing a specific version on FreeBSD is not supported") } @@ -821,7 +838,7 @@ func (up *updater) updateFreeBSD() (err error) { return nil } -func (up *updater) updateLinuxBinary() error { +func (up *Updater) updateLinuxBinary() error { return errors.New("Linux binary updates without a package manager are not supported yet") } diff --git a/cmd/tailscale/cli/update.go b/cmd/tailscale/cli/update.go index 076408e6f..5ee47ac97 100644 --- a/cmd/tailscale/cli/update.go +++ b/cmd/tailscale/cli/update.go @@ -60,7 +60,7 @@ func runUpdate(ctx context.Context, args []string) error { if updateArgs.track != "" { ver = updateArgs.track } - err := clientupdate.Update(clientupdate.UpdateArgs{ + err := clientupdate.Update(clientupdate.Arguments{ Version: ver, AppStore: updateArgs.appStore, Logf: func(format string, args ...any) { fmt.Printf(format+"\n", args...) }, diff --git a/cmd/tailscaled/depaware.txt b/cmd/tailscaled/depaware.txt index c04391ec3..d71d50008 100644 --- a/cmd/tailscaled/depaware.txt +++ b/cmd/tailscaled/depaware.txt @@ -94,8 +94,8 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de L github.com/google/nftables/expr from github.com/google/nftables+ L github.com/google/nftables/internal/parseexprfunc from github.com/google/nftables+ L github.com/google/nftables/xt from github.com/google/nftables/expr+ - github.com/google/uuid from tailscale.com/ipn/ipnlocal - github.com/hdevalence/ed25519consensus from tailscale.com/tka + github.com/google/uuid from tailscale.com/ipn/ipnlocal+ + github.com/hdevalence/ed25519consensus from tailscale.com/tka+ L 💣 github.com/illarion/gonotify from tailscale.com/net/dns L github.com/insomniacslk/dhcp/dhcpv4 from tailscale.com/net/tstun L github.com/insomniacslk/dhcp/iana from github.com/insomniacslk/dhcp/dhcpv4 @@ -216,6 +216,8 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de LD tailscale.com/chirp from tailscale.com/cmd/tailscaled tailscale.com/client/tailscale from tailscale.com/derp tailscale.com/client/tailscale/apitype from tailscale.com/ipn/ipnlocal+ + tailscale.com/clientupdate from tailscale.com/ipn/ipnlocal + tailscale.com/clientupdate/distsign from tailscale.com/clientupdate tailscale.com/cmd/tailscaled/childproc from tailscale.com/ssh/tailssh+ tailscale.com/control/controlbase from tailscale.com/control/controlclient+ tailscale.com/control/controlclient from tailscale.com/ipn/ipnlocal+ @@ -334,7 +336,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de L tailscale.com/util/linuxfw from tailscale.com/net/netns+ tailscale.com/util/mak from tailscale.com/control/controlclient+ tailscale.com/util/multierr from tailscale.com/control/controlclient+ - tailscale.com/util/must from tailscale.com/logpolicy + tailscale.com/util/must from tailscale.com/logpolicy+ 💣 tailscale.com/util/osdiag from tailscale.com/cmd/tailscaled+ W 💣 tailscale.com/util/osdiag/internal/wsc from tailscale.com/util/osdiag tailscale.com/util/osshare from tailscale.com/ipn/ipnlocal+ @@ -349,7 +351,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de tailscale.com/util/testenv from tailscale.com/ipn/ipnlocal+ tailscale.com/util/uniq from tailscale.com/wgengine/magicsock+ 💣 tailscale.com/util/winutil from tailscale.com/control/controlclient+ - W 💣 tailscale.com/util/winutil/authenticode from tailscale.com/util/osdiag + W 💣 tailscale.com/util/winutil/authenticode from tailscale.com/util/osdiag+ W tailscale.com/util/winutil/policy from tailscale.com/ipn/ipnlocal tailscale.com/version from tailscale.com/derp+ tailscale.com/version/distro from tailscale.com/hostinfo+ diff --git a/ipn/ipnlocal/c2n.go b/ipn/ipnlocal/c2n.go index 4559a8461..3ce5c9cdd 100644 --- a/ipn/ipnlocal/c2n.go +++ b/ipn/ipnlocal/c2n.go @@ -16,13 +16,13 @@ import ( "strconv" "time" + "tailscale.com/clientupdate" "tailscale.com/envknob" "tailscale.com/net/sockstats" "tailscale.com/tailcfg" "tailscale.com/util/clientmetric" "tailscale.com/util/goroutines" "tailscale.com/version" - "tailscale.com/version/distro" ) var c2nLogHeap func(http.ResponseWriter, *http.Request) // non-nil on most platforms (c2n_pprof.go) @@ -115,17 +115,24 @@ func (b *LocalBackend) handleC2NUpdate(w http.ResponseWriter, r *http.Request) { // TODO(bradfitz): add some sort of semaphore that prevents two concurrent // updates, or if one happened in the past 5 minutes, or something. - var res tailcfg.C2NUpdateResponse - res.Enabled = envknob.AllowsRemoteUpdate() - res.Supported = runtime.GOOS == "windows" || (runtime.GOOS == "linux" && distro.Get() == distro.Debian) - - switch r.Method { - case "GET", "POST": - default: + // GET returns the current status, and POST actually begins an update. + if r.Method != "GET" && r.Method != "POST" { http.Error(w, "bad method", http.StatusMethodNotAllowed) return } + // If NewUpdater does not return an error, we can update the installation. + // Exception: When version.IsMacSysExt returns true, we don't support that + // yet. TODO(cpalmer, #6995): Implement it. + // + // 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. + _, err := clientupdate.NewUpdater(clientupdate.Arguments{}) + res := tailcfg.C2NUpdateResponse{ + Enabled: envknob.AllowsRemoteUpdate(), + Supported: err == nil && !version.IsMacSysExt(), + } + defer func() { w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(res) @@ -134,16 +141,15 @@ func (b *LocalBackend) handleC2NUpdate(w http.ResponseWriter, r *http.Request) { if r.Method == "GET" { return } - if !res.Enabled { res.Err = "not enabled" return } - if !res.Supported { res.Err = "not supported" return } + cmdTS, err := findCmdTailscale() if err != nil { res.Err = fmt.Sprintf("failed to find cmd/tailscale binary: %v", err)