ipn/ipnlocal: support most Linuxes in handleC2NUpdate (#9114)

* ipn/ipnlocal: support most Linuxes in handleC2NUpdate

Updates #6995

Signed-off-by: Chris Palmer <cpalmer@tailscale.com>
pull/9144/head
Chris Palmer 1 year ago committed by GitHub
parent c621141746
commit ce1e02096a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -57,14 +57,8 @@ func versionToTrack(v string) (string, error) {
return "unstable", nil return "unstable", nil
} }
type updater struct { // Arguments contains arguments needed to run an update.
UpdateArgs type Arguments struct {
track string
update func() error
}
// UpdateArgs contains arguments needed to run an update.
type UpdateArgs struct {
// Version can be a specific version number or one of the predefined track // Version can be a specific version number or one of the predefined track
// constants: // constants:
// //
@ -76,7 +70,7 @@ type UpdateArgs struct {
// Leaving this empty is the same as using CurrentTrack. // Leaving this empty is the same as using CurrentTrack.
Version string Version string
// AppStore forces a local app store check, even if the current binary was // 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 AppStore bool
// Logf is a logger for update progress messages. // Logf is a logger for update progress messages.
Logf logger.Logf Logf logger.Logf
@ -89,30 +83,31 @@ type UpdateArgs struct {
PkgsAddr string PkgsAddr string
} }
func (args UpdateArgs) validate() error { func (args Arguments) validate() error {
if args.Confirm == nil { if args.Confirm == nil {
return errors.New("missing Confirm callback in UpdateArgs") return errors.New("missing Confirm callback in Arguments")
} }
if args.Logf == nil { if args.Logf == nil {
return errors.New("missing Logf callback in UpdateArgs") return errors.New("missing Logf callback in Arguments")
} }
return nil return nil
} }
// Update runs a single update attempt using the platform-specific mechanism. type Updater struct {
// Arguments
// On Windows, this copies the calling binary and re-executes it to apply the track string
// update. The calling binary should handle an "update" subcommand and call // Update is a platform-specific method that updates the installation. May be
// this function again for the re-executed binary to proceed. // nil (not all platforms support updates from within Tailscale).
func Update(args UpdateArgs) error { Update func() error
if err := args.validate(); err != nil { }
return err
} func NewUpdater(args Arguments) (*Updater, error) {
if args.PkgsAddr == "" { up := Updater{
args.PkgsAddr = "https://pkgs.tailscale.com" Arguments: args,
} }
up := &updater{ up.Update = up.getUpdateFunction()
UpdateArgs: args, if up.Update == nil {
return nil, errors.ErrUnsupported
} }
switch up.Version { switch up.Version {
case StableTrack, UnstableTrack: case StableTrack, UnstableTrack:
@ -127,60 +122,82 @@ func Update(args UpdateArgs) error {
var err error var err error
up.track, err = versionToTrack(args.Version) up.track, err = versionToTrack(args.Version)
if err != nil { 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 { switch runtime.GOOS {
case "windows": case "windows":
up.update = up.updateWindows return up.updateWindows
case "linux": case "linux":
switch distro.Get() { switch distro.Get() {
case distro.Synology: case distro.Synology:
up.update = up.updateSynology return up.updateSynology
case distro.Debian: // includes Ubuntu case distro.Debian: // includes Ubuntu
up.update = up.updateDebLike return up.updateDebLike
case distro.Arch: case distro.Arch:
up.update = up.updateArchLike return up.updateArchLike
case distro.Alpine: case distro.Alpine:
up.update = up.updateAlpineLike return up.updateAlpineLike
} }
switch { switch {
case haveExecutable("pacman"): case haveExecutable("pacman"):
up.update = up.updateArchLike return up.updateArchLike
case haveExecutable("apt-get"): // TODO(awly): add support for "apt" case haveExecutable("apt-get"): // TODO(awly): add support for "apt"
// The distro.Debian switch case above should catch most apt-based // The distro.Debian switch case above should catch most apt-based
// systems, but add this fallback just in case. // systems, but add this fallback just in case.
up.update = up.updateDebLike return up.updateDebLike
case haveExecutable("dnf"): case haveExecutable("dnf"):
up.update = up.updateFedoraLike("dnf") return up.updateFedoraLike("dnf")
case haveExecutable("yum"): case haveExecutable("yum"):
up.update = up.updateFedoraLike("yum") return up.updateFedoraLike("yum")
case haveExecutable("apk"): case haveExecutable("apk"):
up.update = up.updateAlpineLike return up.updateAlpineLike
} }
// If nothing matched, fall back to tarball updates. // If nothing matched, fall back to tarball updates.
if up.update == nil { if up.Update == nil {
up.update = up.updateLinuxBinary return up.updateLinuxBinary
} }
case "darwin": case "darwin":
switch { switch {
case !args.AppStore && !version.IsSandboxedMacOS(): case !up.Arguments.AppStore && !version.IsSandboxedMacOS():
return errors.ErrUnsupported return nil
case !args.AppStore && strings.HasSuffix(os.Getenv("HOME"), "/io.tailscale.ipn.macsys/Data"): case !up.Arguments.AppStore && strings.HasSuffix(os.Getenv("HOME"), "/io.tailscale.ipn.macsys/Data"):
up.update = up.updateMacSys return up.updateMacSys
default: default:
up.update = up.updateMacAppStore return up.updateMacAppStore
} }
case "freebsd": case "freebsd":
up.update = up.updateFreeBSD return up.updateFreeBSD
}
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
} }
if up.update == nil { up, err := NewUpdater(args)
return errors.ErrUnsupported 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 { if version.Short() == ver {
up.Logf("already running %v; no update needed", ver) up.Logf("already running %v; no update needed", ver)
return false return false
@ -193,7 +210,7 @@ func (up *updater) confirm(ver string) bool {
const synoinfoConfPath = "/etc/synoinfo.conf" const synoinfoConfPath = "/etc/synoinfo.conf"
func (up *updater) updateSynology() error { func (up *Updater) updateSynology() error {
if up.Version != "" { if up.Version != "" {
return errors.New("installing a specific version on Synology is not supported") 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) return "", fmt.Errorf(`missing "unique=" field in %q`, path)
} }
func (up *updater) updateDebLike() error { func (up *Updater) updateDebLike() error {
if err := requireRoot(); err != nil { if err := requireRoot(); err != nil {
return err return err
} }
@ -409,7 +426,7 @@ func updateDebianAptSourcesListBytes(was []byte, dstTrack string) (newContent []
return buf.Bytes(), nil 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) { if err := exec.Command("pacman", "--query", "tailscale").Run(); err != nil && isExitError(err) {
// Tailscale was not installed via pacman, update via tarball download // Tailscale was not installed via pacman, update via tarball download
// instead. // instead.
@ -427,7 +444,7 @@ const yumRepoConfigFile = "/etc/yum.repos.d/tailscale.repo"
// updateFedoraLike updates tailscale on any distros in the Fedora family, // updateFedoraLike updates tailscale on any distros in the Fedora family,
// specifically anything that uses "dnf" or "yum" package managers. The actual // specifically anything that uses "dnf" or "yum" package managers. The actual
// package manager is passed via packageManager. // 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) { return func() (err error) {
if err := requireRoot(); err != nil { if err := requireRoot(); err != nil {
return err return err
@ -508,7 +525,7 @@ func updateYUMRepoTrack(repoFile, dstTrack string) (rewrote bool, err error) {
return true, os.WriteFile(repoFile, newContent.Bytes(), 0644) return true, os.WriteFile(repoFile, newContent.Bytes(), 0644)
} }
func (up *updater) updateAlpineLike() (err error) { func (up *Updater) updateAlpineLike() (err error) {
if up.Version != "" { if up.Version != "" {
return errors.New("installing a specific version on Alpine-based distros is not supported") 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") 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") 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() out, err := exec.Command("defaults", "read", "/Library/Preferences/com.apple.commerce.plist", "AutoUpdate").CombinedOutput()
if err != nil { if err != nil {
return fmt.Errorf("can't check App Store auto-update setting: %w, output: %q", err, string(out)) 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 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 != "" { if msi := os.Getenv(winMSIEnv); msi != "" {
up.Logf("installing %v ...", msi) up.Logf("installing %v ...", msi)
if err := up.installMSI(msi); err != nil { if err := up.installMSI(msi); err != nil {
@ -705,7 +722,7 @@ func (up *updater) updateWindows() error {
panic("unreachable") panic("unreachable")
} }
func (up *updater) installMSI(msi string) error { func (up *Updater) installMSI(msi string) error {
var err error var err error
for tries := 0; tries < 2; tries++ { for tries := 0; tries < 2; tries++ {
cmd := exec.Command("msiexec.exe", "/i", filepath.Base(msi), "/quiet", "/promptrestart", "/qn") 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() 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) c, err := distsign.NewClient(up.Logf, up.PkgsAddr)
if err != nil { if err != nil {
return err return err
@ -780,7 +797,7 @@ func (up *updater) downloadURLToFile(pathSrc, fileDst string) (ret error) {
return c.Download(context.Background(), pathSrc, fileDst) return c.Download(context.Background(), pathSrc, fileDst)
} }
func (up *updater) updateFreeBSD() (err error) { func (up *Updater) updateFreeBSD() (err error) {
if up.Version != "" { if up.Version != "" {
return errors.New("installing a specific version on FreeBSD is not supported") return errors.New("installing a specific version on FreeBSD is not supported")
} }
@ -821,7 +838,7 @@ func (up *updater) updateFreeBSD() (err error) {
return nil 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") return errors.New("Linux binary updates without a package manager are not supported yet")
} }

@ -60,7 +60,7 @@ func runUpdate(ctx context.Context, args []string) error {
if updateArgs.track != "" { if updateArgs.track != "" {
ver = updateArgs.track ver = updateArgs.track
} }
err := clientupdate.Update(clientupdate.UpdateArgs{ err := clientupdate.Update(clientupdate.Arguments{
Version: ver, Version: ver,
AppStore: updateArgs.appStore, AppStore: updateArgs.appStore,
Logf: func(format string, args ...any) { fmt.Printf(format+"\n", args...) }, Logf: func(format string, args ...any) { fmt.Printf(format+"\n", args...) },

@ -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/expr from github.com/google/nftables+
L github.com/google/nftables/internal/parseexprfunc 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+ L github.com/google/nftables/xt from github.com/google/nftables/expr+
github.com/google/uuid from tailscale.com/ipn/ipnlocal github.com/google/uuid from tailscale.com/ipn/ipnlocal+
github.com/hdevalence/ed25519consensus from tailscale.com/tka github.com/hdevalence/ed25519consensus from tailscale.com/tka+
L 💣 github.com/illarion/gonotify from tailscale.com/net/dns 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/dhcpv4 from tailscale.com/net/tstun
L github.com/insomniacslk/dhcp/iana from github.com/insomniacslk/dhcp/dhcpv4 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 LD tailscale.com/chirp from tailscale.com/cmd/tailscaled
tailscale.com/client/tailscale from tailscale.com/derp tailscale.com/client/tailscale from tailscale.com/derp
tailscale.com/client/tailscale/apitype from tailscale.com/ipn/ipnlocal+ 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/cmd/tailscaled/childproc from tailscale.com/ssh/tailssh+
tailscale.com/control/controlbase from tailscale.com/control/controlclient+ tailscale.com/control/controlbase from tailscale.com/control/controlclient+
tailscale.com/control/controlclient from tailscale.com/ipn/ipnlocal+ 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+ L tailscale.com/util/linuxfw from tailscale.com/net/netns+
tailscale.com/util/mak from tailscale.com/control/controlclient+ tailscale.com/util/mak from tailscale.com/control/controlclient+
tailscale.com/util/multierr 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+ 💣 tailscale.com/util/osdiag from tailscale.com/cmd/tailscaled+
W 💣 tailscale.com/util/osdiag/internal/wsc from tailscale.com/util/osdiag W 💣 tailscale.com/util/osdiag/internal/wsc from tailscale.com/util/osdiag
tailscale.com/util/osshare from tailscale.com/ipn/ipnlocal+ 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/testenv from tailscale.com/ipn/ipnlocal+
tailscale.com/util/uniq from tailscale.com/wgengine/magicsock+ tailscale.com/util/uniq from tailscale.com/wgengine/magicsock+
💣 tailscale.com/util/winutil from tailscale.com/control/controlclient+ 💣 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 W tailscale.com/util/winutil/policy from tailscale.com/ipn/ipnlocal
tailscale.com/version from tailscale.com/derp+ tailscale.com/version from tailscale.com/derp+
tailscale.com/version/distro from tailscale.com/hostinfo+ tailscale.com/version/distro from tailscale.com/hostinfo+

@ -16,13 +16,13 @@ import (
"strconv" "strconv"
"time" "time"
"tailscale.com/clientupdate"
"tailscale.com/envknob" "tailscale.com/envknob"
"tailscale.com/net/sockstats" "tailscale.com/net/sockstats"
"tailscale.com/tailcfg" "tailscale.com/tailcfg"
"tailscale.com/util/clientmetric" "tailscale.com/util/clientmetric"
"tailscale.com/util/goroutines" "tailscale.com/util/goroutines"
"tailscale.com/version" "tailscale.com/version"
"tailscale.com/version/distro"
) )
var c2nLogHeap func(http.ResponseWriter, *http.Request) // non-nil on most platforms (c2n_pprof.go) 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 // TODO(bradfitz): add some sort of semaphore that prevents two concurrent
// updates, or if one happened in the past 5 minutes, or something. // updates, or if one happened in the past 5 minutes, or something.
var res tailcfg.C2NUpdateResponse // GET returns the current status, and POST actually begins an update.
res.Enabled = envknob.AllowsRemoteUpdate() if r.Method != "GET" && r.Method != "POST" {
res.Supported = runtime.GOOS == "windows" || (runtime.GOOS == "linux" && distro.Get() == distro.Debian)
switch r.Method {
case "GET", "POST":
default:
http.Error(w, "bad method", http.StatusMethodNotAllowed) http.Error(w, "bad method", http.StatusMethodNotAllowed)
return 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() { defer func() {
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(res) json.NewEncoder(w).Encode(res)
@ -134,16 +141,15 @@ func (b *LocalBackend) handleC2NUpdate(w http.ResponseWriter, r *http.Request) {
if r.Method == "GET" { if r.Method == "GET" {
return return
} }
if !res.Enabled { if !res.Enabled {
res.Err = "not enabled" res.Err = "not enabled"
return return
} }
if !res.Supported { if !res.Supported {
res.Err = "not supported" res.Err = "not supported"
return return
} }
cmdTS, err := findCmdTailscale() cmdTS, err := findCmdTailscale()
if err != nil { if err != nil {
res.Err = fmt.Sprintf("failed to find cmd/tailscale binary: %v", err) res.Err = fmt.Sprintf("failed to find cmd/tailscale binary: %v", err)

Loading…
Cancel
Save