From d9144c73a8ed520c8b59b604323acca7d879a568 Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Wed, 4 Jan 2023 21:31:26 -0800 Subject: [PATCH] cmd/tailscale: add start of "tailscale update" command Goal: one way for users to update Tailscale, downgrade, switch tracks, regardless of platform (Windows, most Linux distros, macOS, Synology). This is a start. Updates #755, etc Change-Id: I23466da1ba41b45f0029ca79a17f5796c2eedd92 Signed-off-by: Brad Fitzpatrick --- cmd/tailscale/cli/cli.go | 2 + cmd/tailscale/cli/update.go | 205 +++++++++++++++++++++++++++++ cmd/tailscale/depaware.txt | 2 +- util/winutil/winutil_notwindows.go | 2 + 4 files changed, 210 insertions(+), 1 deletion(-) create mode 100644 cmd/tailscale/cli/update.go diff --git a/cmd/tailscale/cli/cli.go b/cmd/tailscale/cli/cli.go index 984e0762b..ec32ad969 100644 --- a/cmd/tailscale/cli/cli.go +++ b/cmd/tailscale/cli/cli.go @@ -196,6 +196,8 @@ change in the future. rootCmd.Subcommands = append(rootCmd.Subcommands, debugCmd) case slices.Contains(args, "serve"): rootCmd.Subcommands = append(rootCmd.Subcommands, serveCmd) + case slices.Contains(args, "update"): + rootCmd.Subcommands = append(rootCmd.Subcommands, updateCmd) } if runtime.GOOS == "linux" && distro.Get() == distro.Synology { rootCmd.Subcommands = append(rootCmd.Subcommands, configureHostCmd) diff --git a/cmd/tailscale/cli/update.go b/cmd/tailscale/cli/update.go new file mode 100644 index 000000000..514f75e89 --- /dev/null +++ b/cmd/tailscale/cli/update.go @@ -0,0 +1,205 @@ +// Copyright (c) 2023 Tailscale Inc & AUTHORS All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package cli + +import ( + "context" + "encoding/json" + "errors" + "flag" + "fmt" + "net/http" + "os" + "runtime" + "strings" + + "github.com/peterbourgon/ff/v3/ffcli" + "tailscale.com/util/winutil" + "tailscale.com/version" + "tailscale.com/version/distro" +) + +var updateCmd = &ffcli.Command{ + Name: "update", + ShortUsage: "update", + ShortHelp: "Update Tailscale to the latest/different version", + Exec: runUpdate, + FlagSet: (func() *flag.FlagSet { + fs := newFlagSet("update") + fs.BoolVar(&updateArgs.yes, "yes", false, "update without interactive prompts") + fs.BoolVar(&updateArgs.dryRun, "dry-run", false, "print what update would do without doing it, or prompts") + fs.StringVar(&updateArgs.track, "track", "", `which track to check for updates: "stable" or "unstable" (dev); empty means same as current`) + fs.StringVar(&updateArgs.version, "version", "", `explicit version to update/downgrade to`) + return fs + })(), +} + +var updateArgs struct { + yes bool + dryRun bool + track string // explicit track; empty means same as current + version string // explicit version; empty means auto +} + +func runUpdate(ctx context.Context, args []string) error { + if len(args) > 0 { + return flag.ErrHelp + } + if updateArgs.version != "" && updateArgs.track != "" { + return errors.New("cannot specify both --version and --track") + } + up, err := newUpdater() + if err != nil { + return err + } + return up.update() +} + +func newUpdater() (*updater, error) { + up := &updater{ + track: updateArgs.track, + } + switch up.track { + case "stable", "unstable": + case "": + if version.IsUnstableBuild() { + up.track = "unstable" + } else { + up.track = "stable" + } + default: + return nil, fmt.Errorf("unknown track %q; must be 'stable' or 'unstable'", up.track) + } + switch runtime.GOOS { + case "windows": + up.update = up.updateWindows + case "linux": + switch distro.Get() { + case distro.Synology: + up.update = up.updateSynology + case distro.Debian: // includes Ubuntu + up.update = up.updateDebLike + } + case "darwin": + switch { + case !version.IsSandboxedMacOS(): + return nil, errors.New("The 'update' command is not yet supported on this platform; see https://github.com/tailscale/tailscale/wiki/Tailscaled-on-macOS/ for now") + case strings.HasSuffix(os.Getenv("HOME"), "/io.tailscale.ipn.macsys/Data"): + up.update = up.updateMacSys + default: + return nil, errors.New("This is the macOS App Store version of Tailscale; update in the App Store, or see https://tailscale.com/kb/1083/install-unstable/ to use TestFlight or to install the non-App Store version") + } + } + if up.update == nil { + return nil, errors.New("The 'update' command is not supported on this platform; see https://tailscale.com/kb/1067/update/") + } + return up, nil +} + +type updater struct { + track string + update func() error +} + +func (up *updater) currentOrDryRun(ver string) bool { + if version.Short == ver { + fmt.Printf("already running %v; no update needed\n", ver) + return true + } + if updateArgs.dryRun { + fmt.Printf("Current: %v, Latest: %v\n", version.Short, ver) + return true + } + return false +} + +func (up *updater) updateSynology() error { + // TODO(bradfitz): detect, map GOARCH+CPU to the right Synology arch. + // TODO(bradfitz): add pkgs.tailscale.com endpoint to get release info + // TODO(bradfitz): require root/sudo + // TODO(bradfitz): run /usr/syno/bin/synopkg install tailscale.spk + return errors.New("The 'update' command is not yet implemented on Synology.") +} + +func (up *updater) updateDebLike() error { + ver := updateArgs.version + if ver == "" { + res, err := http.Get("https://pkgs.tailscale.com/" + up.track + "/?mode=json") + if err != nil { + return err + } + var latest struct { + Tarballs map[string]string // ~goarch (ignoring "geode") => "tailscale_1.34.2_mips.tgz" + } + err = json.NewDecoder(res.Body).Decode(&latest) + res.Body.Close() + if err != nil { + return fmt.Errorf("decoding JSON: %v: %w", res.Status, err) + } + f, ok := latest.Tarballs[runtime.GOARCH] + if !ok { + return fmt.Errorf("can't update architecture %q", runtime.GOARCH) + } + ver, _, ok = strings.Cut(strings.TrimPrefix(f, "tailscale_"), "_") + if !ok { + return fmt.Errorf("can't parse version from %q", f) + } + } + if up.currentOrDryRun(ver) { + return nil + } + url := fmt.Sprintf("https://pkgs.tailscale.com/%s/debian/pool/tailscale_%s_%s.deb", up.track, ver, runtime.GOARCH) + // TODO(bradfitz): require root/sudo + // TODO(bradfitz): check https://pkgs.tailscale.com/stable/debian/dists/sid/InRelease, check gpg, get sha256 + // And https://pkgs.tailscale.com/stable/debian/dists/sid/main/binary-amd64/Packages.gz and sha256 of it + // + + return errors.New("TODO: Debian/Ubuntu deb download of " + url) +} + +func (up *updater) updateMacSys() error { + // use sparkle? do we have permissions from this context? does sudo help? + // We can at least fail with a command they can run to update from the shell. + // Like "tailscale update --macsys | sudo sh" or something. + // + // TODO(bradfitz,mihai): implement. But for now: + return errors.New("The 'update' command is not yet implemented on macOS.") +} + +func (up *updater) updateWindows() error { + ver := updateArgs.version + if ver == "" { + res, err := http.Get("https://pkgs.tailscale.com/" + up.track + "/?mode=json&os=windows") + if err != nil { + return err + } + var latest struct { + Version string + } + err = json.NewDecoder(res.Body).Decode(&latest) + res.Body.Close() + if err != nil { + return fmt.Errorf("decoding JSON: %v: %w", res.Status, err) + } + ver = latest.Version + if ver == "" { + return errors.New("no version found") + } + } + arch := runtime.GOARCH + if arch == "386" { + arch = "x86" + } + url := fmt.Sprintf("https://pkgs.tailscale.com/%s/tailscale-setup-%s-%s.msi", up.track, ver, arch) + + if up.currentOrDryRun(ver) { + return nil + } + if !winutil.IsCurrentProcessElevated() { + return errors.New("must be run as Administrator") + } + // TODO(bradfitz): require elevated mode + return errors.New("TODO: download + msiexec /i /quiet " + url) +} diff --git a/cmd/tailscale/depaware.txt b/cmd/tailscale/depaware.txt index a1e558d31..d3e15055e 100644 --- a/cmd/tailscale/depaware.txt +++ b/cmd/tailscale/depaware.txt @@ -115,7 +115,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep tailscale.com/util/quarantine from tailscale.com/cmd/tailscale/cli tailscale.com/util/singleflight from tailscale.com/net/dnscache tailscale.com/util/strs from tailscale.com/hostinfo+ - W 💣 tailscale.com/util/winutil from tailscale.com/hostinfo+ + 💣 tailscale.com/util/winutil from tailscale.com/hostinfo+ tailscale.com/version from tailscale.com/cmd/tailscale/cli+ tailscale.com/version/distro from tailscale.com/cmd/tailscale/cli+ tailscale.com/wgengine/filter from tailscale.com/types/netmap diff --git a/util/winutil/winutil_notwindows.go b/util/winutil/winutil_notwindows.go index c859a9c63..9d573568c 100644 --- a/util/winutil/winutil_notwindows.go +++ b/util/winutil/winutil_notwindows.go @@ -27,3 +27,5 @@ func isSIDValidPrincipal(uid string) bool { return false } func lookupPseudoUser(uid string) (*user.User, error) { return nil, fmt.Errorf("unimplemented on %v", runtime.GOOS) } + +func IsCurrentProcessElevated() bool { return false }