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>
This commit is contained in:
parent
c621141746
commit
ce1e02096a
|
@ -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
|
||||
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
|
||||
}
|
||||
if args.PkgsAddr == "" {
|
||||
args.PkgsAddr = "https://pkgs.tailscale.com"
|
||||
|
||||
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 up.update()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (up *updater) confirm(ver string) bool {
|
||||
// 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()
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
|
|
|
@ -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...) },
|
||||
|
|
|
@ -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+
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in New Issue