2023-08-11 00:01:22 +00:00
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
// Package clientupdate implements tailscale client update for all supported
// platforms. This package can be used from both tailscaled and tailscale
// binaries.
package clientupdate
import (
2023-08-30 23:25:06 +00:00
"archive/tar"
2023-08-11 00:01:22 +00:00
"bufio"
"bytes"
2023-08-30 23:25:06 +00:00
"compress/gzip"
2023-08-11 00:01:22 +00:00
"context"
"encoding/json"
"errors"
"fmt"
"io"
2023-08-30 23:25:06 +00:00
"maps"
2023-08-11 00:01:22 +00:00
"net/http"
"os"
"os/exec"
"path"
"path/filepath"
"regexp"
"runtime"
"strconv"
"strings"
"github.com/google/uuid"
2023-08-28 20:48:33 +00:00
"tailscale.com/clientupdate/distsign"
2023-08-11 00:01:22 +00:00
"tailscale.com/types/logger"
2023-10-11 00:01:44 +00:00
"tailscale.com/util/cmpver"
2023-08-11 00:01:22 +00:00
"tailscale.com/util/winutil"
"tailscale.com/version"
"tailscale.com/version/distro"
)
const (
CurrentTrack = ""
StableTrack = "stable"
UnstableTrack = "unstable"
)
func versionToTrack ( v string ) ( string , error ) {
_ , rest , ok := strings . Cut ( v , "." )
if ! ok {
return "" , fmt . Errorf ( "malformed version %q" , v )
}
minorStr , _ , ok := strings . Cut ( rest , "." )
if ! ok {
return "" , fmt . Errorf ( "malformed version %q" , v )
}
minor , err := strconv . Atoi ( minorStr )
if err != nil {
return "" , fmt . Errorf ( "malformed version %q" , v )
}
if minor % 2 == 0 {
return "stable" , nil
}
return "unstable" , nil
}
2023-08-30 21:50:03 +00:00
// Arguments contains arguments needed to run an update.
type Arguments struct {
2023-08-11 00:01:22 +00:00
// Version can be a specific version number or one of the predefined track
// constants:
//
// - CurrentTrack will use the latest version from the same track as the
// running binary
// - StableTrack and UnstableTrack will use the latest versions of the
// corresponding tracks
//
// Leaving this empty is the same as using CurrentTrack.
Version string
// AppStore forces a local app store check, even if the current binary was
2023-08-30 21:50:03 +00:00
// not installed via an app store. TODO(cpalmer): Remove this.
2023-08-11 00:01:22 +00:00
AppStore bool
// Logf is a logger for update progress messages.
Logf logger . Logf
2023-10-06 19:00:15 +00:00
// Stdout and Stderr should be used for output instead of os.Stdout and
// os.Stderr.
Stdout io . Writer
Stderr io . Writer
2023-08-11 00:01:22 +00:00
// Confirm is called when a new version is available and should return true
// if this new version should be installed. When Confirm returns false, the
// update is aborted.
Confirm func ( newVer string ) bool
2023-08-28 20:48:33 +00:00
// PkgsAddr is the address of the pkgs server to fetch updates from.
// Defaults to "https://pkgs.tailscale.com".
PkgsAddr string
2023-08-11 00:01:22 +00:00
}
2023-08-30 21:50:03 +00:00
func ( args Arguments ) validate ( ) error {
2023-08-11 00:01:22 +00:00
if args . Confirm == nil {
2023-08-30 21:50:03 +00:00
return errors . New ( "missing Confirm callback in Arguments" )
2023-08-11 00:01:22 +00:00
}
if args . Logf == nil {
2023-08-30 21:50:03 +00:00
return errors . New ( "missing Logf callback in Arguments" )
2023-08-11 00:01:22 +00:00
}
return nil
}
2023-08-30 21:50:03 +00:00
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 ,
2023-08-28 20:48:33 +00:00
}
2023-10-06 19:00:15 +00:00
if up . Stdout == nil {
up . Stdout = os . Stdout
}
if up . Stderr == nil {
up . Stderr = os . Stderr
}
2023-08-30 21:50:03 +00:00
up . Update = up . getUpdateFunction ( )
if up . Update == nil {
return nil , errors . ErrUnsupported
2023-08-11 00:01:22 +00:00
}
switch up . Version {
case StableTrack , UnstableTrack :
up . track = up . Version
case CurrentTrack :
if version . IsUnstableBuild ( ) {
up . track = UnstableTrack
} else {
up . track = StableTrack
}
default :
var err error
up . track , err = versionToTrack ( args . Version )
if err != nil {
2023-08-30 21:50:03 +00:00
return nil , err
2023-08-11 00:01:22 +00:00
}
}
2023-09-08 21:26:55 +00:00
if up . Arguments . PkgsAddr == "" {
up . Arguments . PkgsAddr = "https://pkgs.tailscale.com"
2023-08-30 21:50:03 +00:00
}
return & up , nil
}
type updateFunction func ( ) error
func ( up * Updater ) getUpdateFunction ( ) updateFunction {
2023-08-11 00:01:22 +00:00
switch runtime . GOOS {
case "windows" :
2023-08-30 21:50:03 +00:00
return up . updateWindows
2023-08-11 00:01:22 +00:00
case "linux" :
switch distro . Get ( ) {
case distro . Synology :
2023-08-30 21:50:03 +00:00
return up . updateSynology
2023-08-11 00:01:22 +00:00
case distro . Debian : // includes Ubuntu
2023-08-30 21:50:03 +00:00
return up . updateDebLike
2023-08-11 00:01:22 +00:00
case distro . Arch :
2023-08-30 21:50:03 +00:00
return up . updateArchLike
2023-08-11 00:01:22 +00:00
case distro . Alpine :
2023-08-30 21:50:03 +00:00
return up . updateAlpineLike
2023-08-11 00:01:22 +00:00
}
switch {
case haveExecutable ( "pacman" ) :
2023-08-30 21:50:03 +00:00
return up . updateArchLike
2023-08-11 00:01:22 +00:00
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.
2023-08-30 21:50:03 +00:00
return up . updateDebLike
2023-08-11 00:01:22 +00:00
case haveExecutable ( "dnf" ) :
2023-08-30 21:50:03 +00:00
return up . updateFedoraLike ( "dnf" )
2023-08-11 00:01:22 +00:00
case haveExecutable ( "yum" ) :
2023-08-30 21:50:03 +00:00
return up . updateFedoraLike ( "yum" )
2023-08-11 00:01:22 +00:00
case haveExecutable ( "apk" ) :
2023-08-30 21:50:03 +00:00
return up . updateAlpineLike
2023-08-11 00:01:22 +00:00
}
2023-08-30 00:36:05 +00:00
// If nothing matched, fall back to tarball updates.
2023-08-30 21:50:03 +00:00
if up . Update == nil {
return up . updateLinuxBinary
2023-08-30 00:36:05 +00:00
}
2023-08-11 00:01:22 +00:00
case "darwin" :
switch {
2023-08-30 21:50:03 +00:00
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
2023-08-11 00:01:22 +00:00
default :
2023-08-30 21:50:03 +00:00
return up . updateMacAppStore
2023-08-11 00:01:22 +00:00
}
case "freebsd" :
2023-08-30 21:50:03 +00:00
return up . updateFreeBSD
2023-08-11 00:01:22 +00:00
}
2023-08-30 21:50:03 +00:00
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
2023-08-11 00:01:22 +00:00
}
2023-08-30 21:50:03 +00:00
return up . Update ( )
2023-08-11 00:01:22 +00:00
}
2023-08-30 21:50:03 +00:00
func ( up * Updater ) confirm ( ver string ) bool {
2023-10-11 00:01:44 +00:00
switch cmpver . Compare ( version . Short ( ) , ver ) {
case 0 :
2023-08-11 00:01:22 +00:00
up . Logf ( "already running %v; no update needed" , ver )
return false
2023-10-11 00:01:44 +00:00
case 1 :
up . Logf ( "installed version %v is newer than the latest available version %v; no update needed" , version . Short ( ) , ver )
return false
2023-08-11 00:01:22 +00:00
}
if up . Confirm != nil {
return up . Confirm ( ver )
}
return true
}
2023-08-17 23:45:50 +00:00
const synoinfoConfPath = "/etc/synoinfo.conf"
2023-08-30 21:50:03 +00:00
func ( up * Updater ) updateSynology ( ) error {
2023-08-11 21:55:07 +00:00
if up . Version != "" {
return errors . New ( "installing a specific version on Synology is not supported" )
}
// Get the latest version and list of SPKs from pkgs.tailscale.com.
2023-09-12 18:08:00 +00:00
dsmVersion := distro . DSMVersion ( )
osName := fmt . Sprintf ( "dsm%d" , dsmVersion )
2023-08-17 23:45:50 +00:00
arch , err := synoArch ( runtime . GOARCH , synoinfoConfPath )
2023-08-11 21:55:07 +00:00
if err != nil {
return err
}
latest , err := latestPackages ( up . track )
if err != nil {
return err
}
spkName := latest . SPKs [ osName ] [ arch ]
if spkName == "" {
return fmt . Errorf ( "cannot find Synology package for os=%s arch=%s, please report a bug with your device model" , osName , arch )
}
2023-08-28 21:26:19 +00:00
if ! up . confirm ( latest . SPKsVersion ) {
2023-08-11 21:55:07 +00:00
return nil
}
if err := requireRoot ( ) ; err != nil {
return err
}
// Download the SPK into a temporary directory.
spkDir , err := os . MkdirTemp ( "" , "tailscale-update" )
if err != nil {
return err
}
2023-08-28 20:48:33 +00:00
pkgsPath := fmt . Sprintf ( "%s/%s" , up . track , spkName )
spkPath := filepath . Join ( spkDir , path . Base ( pkgsPath ) )
if err := up . downloadURLToFile ( pkgsPath , spkPath ) ; err != nil {
2023-08-11 21:55:07 +00:00
return err
}
// Install the SPK. Run via nohup to allow install to succeed when we're
// connected over tailscale ssh and this parent process dies. Otherwise, if
// you abort synopkg install mid-way, tailscaled is not restarted.
cmd := exec . Command ( "nohup" , "synopkg" , "install" , spkPath )
2023-10-06 19:00:15 +00:00
// Don't attach cmd.Stdout to Stdout because nohup will redirect that into
// nohup.out file. synopkg doesn't have any progress output anyway, it just
// spits out a JSON result when done.
2023-08-11 21:55:07 +00:00
out , err := cmd . CombinedOutput ( )
if err != nil {
2023-09-12 18:08:00 +00:00
if dsmVersion == 6 && bytes . Contains ( out , [ ] byte ( "error = [290]" ) ) {
return fmt . Errorf ( "synopkg install failed: %w\noutput:\n%s\nplease make sure that packages from 'Any publisher' are allowed in the Package Center (Package Center -> Settings -> Trust Level -> Any publisher)" , err , out )
}
2023-08-11 21:55:07 +00:00
return fmt . Errorf ( "synopkg install failed: %w\noutput:\n%s" , err , out )
}
2023-09-12 18:08:00 +00:00
if dsmVersion == 6 {
// DSM6 does not automatically restart the package on install. Do it
// manually.
cmd := exec . Command ( "nohup" , "synopkg" , "start" , "Tailscale" )
out , err := cmd . CombinedOutput ( )
if err != nil {
return fmt . Errorf ( "synopkg start failed: %w\noutput:\n%s" , err , out )
}
}
2023-08-11 21:55:07 +00:00
return nil
}
// synoArch returns the Synology CPU architecture matching one of the SPK
// architectures served from pkgs.tailscale.com.
2023-08-17 23:45:50 +00:00
func synoArch ( goArch , synoinfoPath string ) ( string , error ) {
2023-08-11 21:55:07 +00:00
// Most Synology boxes just use a different arch name from GOARCH.
arch := map [ string ] string {
"amd64" : "x86_64" ,
"386" : "i686" ,
"arm64" : "armv8" ,
2023-08-17 23:45:50 +00:00
} [ goArch ]
2023-08-11 21:55:07 +00:00
if arch == "" {
2023-08-17 23:45:50 +00:00
// Here's the fun part, some older ARM boxes require you to use SPKs
// specifically for their CPU. See
// https://github.com/SynoCommunity/spksrc/wiki/Synology-and-SynoCommunity-Package-Architectures
// for a complete list.
//
// Some CPUs will map to neither this list nor the goArch map above, and we
// don't have SPKs for them.
cpu , err := parseSynoinfo ( synoinfoPath )
if err != nil {
return "" , fmt . Errorf ( "failed to get CPU architecture: %w" , err )
}
switch cpu {
case "88f6281" , "88f6282" , "hi3535" , "alpine" , "armada370" ,
"armada375" , "armada38x" , "armadaxp" , "comcerto2k" , "monaco" :
arch = cpu
default :
return "" , fmt . Errorf ( "unsupported Synology CPU architecture %q (Go arch %q), please report a bug at https://github.com/tailscale/tailscale/issues/new/choose" , cpu , goArch )
}
2023-08-11 21:55:07 +00:00
}
return arch , nil
2023-08-11 00:01:22 +00:00
}
2023-08-17 23:45:50 +00:00
func parseSynoinfo ( path string ) ( string , error ) {
f , err := os . Open ( path )
if err != nil {
return "" , err
}
defer f . Close ( )
// Look for a line like:
// unique="synology_88f6282_413j"
// Extract the CPU in the middle (88f6282 in the above example).
s := bufio . NewScanner ( f )
for s . Scan ( ) {
l := s . Text ( )
if ! strings . HasPrefix ( l , "unique=" ) {
continue
}
parts := strings . SplitN ( l , "_" , 3 )
if len ( parts ) != 3 {
return "" , fmt . Errorf ( ` malformed %q: found %q, expected format like 'unique="synology_$cpu_$model' ` , path , l )
}
return parts [ 1 ] , nil
}
return "" , fmt . Errorf ( ` missing "unique=" field in %q ` , path )
}
2023-08-30 21:50:03 +00:00
func ( up * Updater ) updateDebLike ( ) error {
2023-08-30 00:36:05 +00:00
if err := requireRoot ( ) ; err != nil {
return err
}
if err := exec . Command ( "dpkg" , "--status" , "tailscale" ) . Run ( ) ; err != nil && isExitError ( err ) {
// Tailscale was not installed via apt, update via tarball download
// instead.
return up . updateLinuxBinary ( )
}
2023-08-11 00:01:22 +00:00
ver , err := requestedTailscaleVersion ( up . Version , up . track )
if err != nil {
return err
}
if ! up . confirm ( ver ) {
return nil
}
if updated , err := updateDebianAptSourcesList ( up . track ) ; err != nil {
return err
} else if updated {
up . Logf ( "Updated %s to use the %s track" , aptSourcesFile , up . track )
}
cmd := exec . Command ( "apt-get" , "update" ,
// Only update the tailscale repo, not the other ones, treating
// the tailscale.list file as the main "sources.list" file.
"-o" , "Dir::Etc::SourceList=sources.list.d/tailscale.list" ,
// Disable the "sources.list.d" directory:
"-o" , "Dir::Etc::SourceParts=-" ,
// Don't forget about packages in the other repos just because
// we're not updating them:
"-o" , "APT::Get::List-Cleanup=0" ,
)
2023-10-06 19:00:15 +00:00
cmd . Stdout = up . Stdout
cmd . Stderr = up . Stderr
2023-08-11 00:01:22 +00:00
if err := cmd . Run ( ) ; err != nil {
return err
}
cmd = exec . Command ( "apt-get" , "install" , "--yes" , "--allow-downgrades" , "tailscale=" + ver )
2023-10-06 19:00:15 +00:00
cmd . Stdout = up . Stdout
cmd . Stderr = up . Stderr
2023-08-11 00:01:22 +00:00
if err := cmd . Run ( ) ; err != nil {
return err
}
return nil
}
const aptSourcesFile = "/etc/apt/sources.list.d/tailscale.list"
// updateDebianAptSourcesList updates the /etc/apt/sources.list.d/tailscale.list
// file to make sure it has the provided track (stable or unstable) in it.
//
// If it already has the right track (including containing both stable and
// unstable), it does nothing.
func updateDebianAptSourcesList ( dstTrack string ) ( rewrote bool , err error ) {
was , err := os . ReadFile ( aptSourcesFile )
if err != nil {
return false , err
}
newContent , err := updateDebianAptSourcesListBytes ( was , dstTrack )
if err != nil {
return false , err
}
if bytes . Equal ( was , newContent ) {
return false , nil
}
return true , os . WriteFile ( aptSourcesFile , newContent , 0644 )
}
func updateDebianAptSourcesListBytes ( was [ ] byte , dstTrack string ) ( newContent [ ] byte , err error ) {
trackURLPrefix := [ ] byte ( "https://pkgs.tailscale.com/" + dstTrack + "/" )
var buf bytes . Buffer
var changes int
bs := bufio . NewScanner ( bytes . NewReader ( was ) )
hadCorrect := false
commentLine := regexp . MustCompile ( ` ^\s*\# ` )
pkgsURL := regexp . MustCompile ( ` \bhttps://pkgs\.tailscale\.com/((un)?stable)/ ` )
for bs . Scan ( ) {
line := bs . Bytes ( )
if ! commentLine . Match ( line ) {
line = pkgsURL . ReplaceAllFunc ( line , func ( m [ ] byte ) [ ] byte {
if bytes . Equal ( m , trackURLPrefix ) {
hadCorrect = true
} else {
changes ++
}
return trackURLPrefix
} )
}
buf . Write ( line )
buf . WriteByte ( '\n' )
}
if hadCorrect || ( changes == 1 && bytes . Equal ( bytes . TrimSpace ( was ) , bytes . TrimSpace ( buf . Bytes ( ) ) ) ) {
// Unchanged or close enough.
return was , nil
}
if changes != 1 {
// No changes, or an unexpected number of changes (what?). Bail.
// They probably editted it by hand and we don't know what to do.
return nil , fmt . Errorf ( "unexpected/unsupported %s contents" , aptSourcesFile )
}
return buf . Bytes ( ) , nil
}
2023-08-30 21:50:03 +00:00
func ( up * Updater ) updateArchLike ( ) error {
2023-08-30 00:36:05 +00:00
if err := exec . Command ( "pacman" , "--query" , "tailscale" ) . Run ( ) ; err != nil && isExitError ( err ) {
// Tailscale was not installed via pacman, update via tarball download
// instead.
return up . updateLinuxBinary ( )
}
2023-08-24 22:23:13 +00:00
// Arch maintainer asked us not to implement "tailscale update" or
// auto-updates on Arch-based distros:
// https://github.com/tailscale/tailscale/issues/6995#issuecomment-1687080106
return errors . New ( ` individual package updates are not supported on Arch - based distros , only full - system updates are : https : //wiki.archlinux.org/title/System_maintenance#Partial_upgrades_are_unsupported.
you can use "pacman --sync --refresh --sysupgrade" or "pacman -Syu" to upgrade the system , including Tailscale . ` )
2023-08-11 00:01:22 +00:00
}
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.
2023-08-30 21:50:03 +00:00
func ( up * Updater ) updateFedoraLike ( packageManager string ) func ( ) error {
2023-08-11 00:01:22 +00:00
return func ( ) ( err error ) {
if err := requireRoot ( ) ; err != nil {
return err
}
2023-08-30 00:36:05 +00:00
if err := exec . Command ( packageManager , "info" , "--installed" , "tailscale" ) . Run ( ) ; err != nil && isExitError ( err ) {
// Tailscale was not installed via yum/dnf, update via tarball
// download instead.
return up . updateLinuxBinary ( )
}
2023-08-11 00:01:22 +00:00
defer func ( ) {
if err != nil {
err = fmt . Errorf ( ` %w; you can try updating using "%s upgrade tailscale" ` , err , packageManager )
}
} ( )
ver , err := requestedTailscaleVersion ( up . Version , up . track )
if err != nil {
return err
}
if ! up . confirm ( ver ) {
return nil
}
if updated , err := updateYUMRepoTrack ( yumRepoConfigFile , up . track ) ; err != nil {
return err
} else if updated {
up . Logf ( "Updated %s to use the %s track" , yumRepoConfigFile , up . track )
}
cmd := exec . Command ( packageManager , "install" , "--assumeyes" , fmt . Sprintf ( "tailscale-%s-1" , ver ) )
2023-10-06 19:00:15 +00:00
cmd . Stdout = up . Stdout
cmd . Stderr = up . Stderr
2023-08-11 00:01:22 +00:00
if err := cmd . Run ( ) ; err != nil {
return err
}
return nil
}
}
// updateYUMRepoTrack updates the repoFile file to make sure it has the
// provided track (stable or unstable) in it.
func updateYUMRepoTrack ( repoFile , dstTrack string ) ( rewrote bool , err error ) {
was , err := os . ReadFile ( repoFile )
if err != nil {
return false , err
}
urlRe := regexp . MustCompile ( ` ^(baseurl|gpgkey)=https://pkgs\.tailscale\.com/(un)?stable/ ` )
urlReplacement := fmt . Sprintf ( "$1=https://pkgs.tailscale.com/%s/" , dstTrack )
s := bufio . NewScanner ( bytes . NewReader ( was ) )
newContent := bytes . NewBuffer ( make ( [ ] byte , 0 , len ( was ) ) )
for s . Scan ( ) {
line := s . Text ( )
// Handle repo section name, like "[tailscale-stable]".
if len ( line ) > 0 && line [ 0 ] == '[' {
if ! strings . HasPrefix ( line , "[tailscale-" ) {
return false , fmt . Errorf ( "%q does not look like a tailscale repo file, it contains an unexpected %q section" , repoFile , line )
}
fmt . Fprintf ( newContent , "[tailscale-%s]\n" , dstTrack )
continue
}
// Update the track mentioned in repo name.
if strings . HasPrefix ( line , "name=" ) {
fmt . Fprintf ( newContent , "name=Tailscale %s\n" , dstTrack )
continue
}
// Update the actual repo URLs.
if strings . HasPrefix ( line , "baseurl=" ) || strings . HasPrefix ( line , "gpgkey=" ) {
fmt . Fprintln ( newContent , urlRe . ReplaceAllString ( line , urlReplacement ) )
continue
}
fmt . Fprintln ( newContent , line )
}
if bytes . Equal ( was , newContent . Bytes ( ) ) {
return false , nil
}
return true , os . WriteFile ( repoFile , newContent . Bytes ( ) , 0644 )
}
2023-08-30 21:50:03 +00:00
func ( up * Updater ) updateAlpineLike ( ) ( err error ) {
2023-08-11 00:01:22 +00:00
if up . Version != "" {
return errors . New ( "installing a specific version on Alpine-based distros is not supported" )
}
if err := requireRoot ( ) ; err != nil {
return err
}
2023-08-30 00:36:05 +00:00
if err := exec . Command ( "apk" , "info" , "--installed" , "tailscale" ) . Run ( ) ; err != nil && isExitError ( err ) {
// Tailscale was not installed via apk, update via tarball download
// instead.
return up . updateLinuxBinary ( )
}
2023-08-11 00:01:22 +00:00
defer func ( ) {
if err != nil {
err = fmt . Errorf ( ` %w; you can try updating using "apk upgrade tailscale" ` , err )
}
} ( )
out , err := exec . Command ( "apk" , "update" ) . CombinedOutput ( )
if err != nil {
return fmt . Errorf ( "failed refresh apk repository indexes: %w, output: %q" , err , out )
}
out , err = exec . Command ( "apk" , "info" , "tailscale" ) . CombinedOutput ( )
if err != nil {
return fmt . Errorf ( "failed checking apk for latest tailscale version: %w, output: %q" , err , out )
}
ver , err := parseAlpinePackageVersion ( out )
if err != nil {
return fmt . Errorf ( ` failed to parse latest version from "apk info tailscale": %w ` , err )
}
if ! up . confirm ( ver ) {
return nil
}
cmd := exec . Command ( "apk" , "upgrade" , "tailscale" )
2023-10-06 19:00:15 +00:00
cmd . Stdout = up . Stdout
cmd . Stderr = up . Stderr
2023-08-11 00:01:22 +00:00
if err := cmd . Run ( ) ; err != nil {
return fmt . Errorf ( "failed tailscale update using apk: %w" , err )
}
return nil
}
func parseAlpinePackageVersion ( out [ ] byte ) ( string , error ) {
s := bufio . NewScanner ( bytes . NewReader ( out ) )
for s . Scan ( ) {
// The line should look like this:
// tailscale-1.44.2-r0 description:
line := strings . TrimSpace ( s . Text ( ) )
if ! strings . HasPrefix ( line , "tailscale-" ) {
continue
}
parts := strings . SplitN ( line , "-" , 3 )
if len ( parts ) < 3 {
return "" , fmt . Errorf ( "malformed info line: %q" , line )
}
return parts [ 1 ] , nil
}
return "" , errors . New ( "tailscale version not found in output" )
}
2023-08-30 21:50:03 +00:00
func ( up * Updater ) updateMacSys ( ) error {
2023-08-16 21:01:10 +00:00
return errors . New ( "NOTREACHED: On MacSys builds, `tailscale update` is handled in Swift to launch the GUI updater" )
2023-08-11 00:01:22 +00:00
}
2023-08-30 21:50:03 +00:00
func ( up * Updater ) updateMacAppStore ( ) error {
2023-08-11 00:01:22 +00:00
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 ) )
}
const on = "1\n"
if string ( out ) != on {
up . Logf ( "NOTE: Automatic updating for App Store apps is turned off. You can change this setting in System Settings (search for ‘ update’ )." )
}
out , err = exec . Command ( "softwareupdate" , "--list" ) . CombinedOutput ( )
if err != nil {
return fmt . Errorf ( "can't check App Store for available updates: %w, output: %q" , err , string ( out ) )
}
newTailscale := parseSoftwareupdateList ( out )
if newTailscale == "" {
up . Logf ( "no Tailscale update available" )
return nil
}
newTailscaleVer := strings . TrimPrefix ( newTailscale , "Tailscale-" )
if ! up . confirm ( newTailscaleVer ) {
return nil
}
cmd := exec . Command ( "sudo" , "softwareupdate" , "--install" , newTailscale )
2023-10-06 19:00:15 +00:00
cmd . Stdout = up . Stdout
cmd . Stderr = up . Stderr
2023-08-11 00:01:22 +00:00
if err := cmd . Run ( ) ; err != nil {
return fmt . Errorf ( "can't install App Store update for Tailscale: %w" , err )
}
return nil
}
var macOSAppStoreListPattern = regexp . MustCompile ( ` (?m)^\s+\*\s+Label:\s*(Tailscale-\d[\d\.]+) ` )
// parseSoftwareupdateList searches the output of `softwareupdate --list` on
// Darwin and returns the matching Tailscale package label. If there is none,
// returns the empty string.
//
// See TestParseSoftwareupdateList for example inputs.
func parseSoftwareupdateList ( stdout [ ] byte ) string {
matches := macOSAppStoreListPattern . FindSubmatch ( stdout )
if len ( matches ) < 2 {
return ""
}
return string ( matches [ 1 ] )
}
// winMSIEnv is the environment variable that, if set, is the MSI file for the
// update command to install. It's passed like this so we can stop the
// tailscale.exe process from running before the msiexec process runs and tries
// to overwrite ourselves.
const winMSIEnv = "TS_UPDATE_WIN_MSI"
var (
verifyAuthenticode func ( string ) error // or nil on non-Windows
markTempFileFunc func ( string ) error // or nil on non-Windows
)
2023-08-30 21:50:03 +00:00
func ( up * Updater ) updateWindows ( ) error {
2023-08-11 00:01:22 +00:00
if msi := os . Getenv ( winMSIEnv ) ; msi != "" {
up . Logf ( "installing %v ..." , msi )
if err := up . installMSI ( msi ) ; err != nil {
up . Logf ( "MSI install failed: %v" , err )
return err
}
up . Logf ( "success." )
return nil
}
ver , err := requestedTailscaleVersion ( up . Version , up . track )
if err != nil {
return err
}
arch := runtime . GOARCH
if arch == "386" {
arch = "x86"
}
if ! up . confirm ( ver ) {
return nil
}
if ! winutil . IsCurrentProcessElevated ( ) {
return errors . New ( "must be run as Administrator" )
}
tsDir := filepath . Join ( os . Getenv ( "ProgramData" ) , "Tailscale" )
msiDir := filepath . Join ( tsDir , "MSICache" )
if fi , err := os . Stat ( tsDir ) ; err != nil {
return fmt . Errorf ( "expected %s to exist, got stat error: %w" , tsDir , err )
} else if ! fi . IsDir ( ) {
return fmt . Errorf ( "expected %s to be a directory; got %v" , tsDir , fi . Mode ( ) )
}
if err := os . MkdirAll ( msiDir , 0700 ) ; err != nil {
return err
}
2023-08-28 20:48:33 +00:00
pkgsPath := fmt . Sprintf ( "%s/tailscale-setup-%s-%s.msi" , up . track , ver , arch )
msiTarget := filepath . Join ( msiDir , path . Base ( pkgsPath ) )
if err := up . downloadURLToFile ( pkgsPath , msiTarget ) ; err != nil {
2023-08-11 00:01:22 +00:00
return err
}
up . Logf ( "verifying MSI authenticode..." )
if err := verifyAuthenticode ( msiTarget ) ; err != nil {
return fmt . Errorf ( "authenticode verification of %s failed: %w" , msiTarget , err )
}
up . Logf ( "authenticode verification succeeded" )
up . Logf ( "making tailscale.exe copy to switch to..." )
selfCopy , err := makeSelfCopy ( )
if err != nil {
return err
}
defer os . Remove ( selfCopy )
up . Logf ( "running tailscale.exe copy for final install..." )
cmd := exec . Command ( selfCopy , "update" )
cmd . Env = append ( os . Environ ( ) , winMSIEnv + "=" + msiTarget )
2023-10-06 19:00:15 +00:00
cmd . Stdout = up . Stderr
cmd . Stderr = up . Stderr
2023-08-11 00:01:22 +00:00
cmd . Stdin = os . Stdin
if err := cmd . Start ( ) ; err != nil {
return err
}
// Once it's started, exit ourselves, so the binary is free
// to be replaced.
os . Exit ( 0 )
panic ( "unreachable" )
}
2023-08-30 21:50:03 +00:00
func ( up * Updater ) installMSI ( msi string ) error {
2023-08-11 00:01:22 +00:00
var err error
for tries := 0 ; tries < 2 ; tries ++ {
cmd := exec . Command ( "msiexec.exe" , "/i" , filepath . Base ( msi ) , "/quiet" , "/promptrestart" , "/qn" )
cmd . Dir = filepath . Dir ( msi )
2023-10-06 19:00:15 +00:00
cmd . Stdout = up . Stdout
cmd . Stderr = up . Stderr
2023-08-11 00:01:22 +00:00
cmd . Stdin = os . Stdin
err = cmd . Run ( )
if err == nil {
break
}
uninstallVersion := version . Short ( )
if v := os . Getenv ( "TS_DEBUG_UNINSTALL_VERSION" ) ; v != "" {
uninstallVersion = v
}
// Assume it's a downgrade, which msiexec won't permit. Uninstall our current version first.
up . Logf ( "Uninstalling current version %q for downgrade..." , uninstallVersion )
cmd = exec . Command ( "msiexec.exe" , "/x" , msiUUIDForVersion ( uninstallVersion ) , "/norestart" , "/qn" )
2023-10-06 19:00:15 +00:00
cmd . Stdout = up . Stdout
cmd . Stderr = up . Stderr
2023-08-11 00:01:22 +00:00
cmd . Stdin = os . Stdin
err = cmd . Run ( )
up . Logf ( "msiexec uninstall: %v" , err )
}
return err
}
func msiUUIDForVersion ( ver string ) string {
arch := runtime . GOARCH
if arch == "386" {
arch = "x86"
}
track , err := versionToTrack ( ver )
if err != nil {
track = UnstableTrack
}
msiURL := fmt . Sprintf ( "https://pkgs.tailscale.com/%s/tailscale-setup-%s-%s.msi" , track , ver , arch )
return "{" + strings . ToUpper ( uuid . NewSHA1 ( uuid . NameSpaceURL , [ ] byte ( msiURL ) ) . String ( ) ) + "}"
}
func makeSelfCopy ( ) ( tmpPathExe string , err error ) {
selfExe , err := os . Executable ( )
if err != nil {
return "" , err
}
f , err := os . Open ( selfExe )
if err != nil {
return "" , err
}
defer f . Close ( )
f2 , err := os . CreateTemp ( "" , "tailscale-updater-*.exe" )
if err != nil {
return "" , err
}
if f := markTempFileFunc ; f != nil {
if err := f ( f2 . Name ( ) ) ; err != nil {
return "" , err
}
}
if _ , err := io . Copy ( f2 , f ) ; err != nil {
f2 . Close ( )
return "" , err
}
return f2 . Name ( ) , f2 . Close ( )
}
2023-08-30 21:50:03 +00:00
func ( up * Updater ) downloadURLToFile ( pathSrc , fileDst string ) ( ret error ) {
2023-08-28 20:48:33 +00:00
c , err := distsign . NewClient ( up . Logf , up . PkgsAddr )
2023-08-11 00:01:22 +00:00
if err != nil {
return err
}
2023-08-28 20:48:33 +00:00
return c . Download ( context . Background ( ) , pathSrc , fileDst )
2023-08-11 00:01:22 +00:00
}
2023-08-30 21:50:03 +00:00
func ( up * Updater ) updateFreeBSD ( ) ( err error ) {
2023-08-11 00:01:22 +00:00
if up . Version != "" {
return errors . New ( "installing a specific version on FreeBSD is not supported" )
}
if err := requireRoot ( ) ; err != nil {
return err
}
2023-08-30 00:36:05 +00:00
if err := exec . Command ( "pkg" , "query" , "%n" , "tailscale" ) . Run ( ) ; err != nil && isExitError ( err ) {
// Tailscale was not installed via pkg and we don't pre-compile
// binaries for it.
return errors . New ( "Tailscale was not installed via pkg, binary updates on FreeBSD are not supported; please reinstall Tailscale using pkg or update manually" )
}
2023-08-11 00:01:22 +00:00
defer func ( ) {
if err != nil {
err = fmt . Errorf ( ` %w; you can try updating using "pkg upgrade tailscale" ` , err )
}
} ( )
out , err := exec . Command ( "pkg" , "update" ) . CombinedOutput ( )
if err != nil {
return fmt . Errorf ( "failed refresh pkg repository indexes: %w, output: %q" , err , out )
}
out , err = exec . Command ( "pkg" , "rquery" , "%v" , "tailscale" ) . CombinedOutput ( )
if err != nil {
return fmt . Errorf ( "failed checking pkg for latest tailscale version: %w, output: %q" , err , out )
}
ver := string ( bytes . TrimSpace ( out ) )
if ! up . confirm ( ver ) {
return nil
}
cmd := exec . Command ( "pkg" , "upgrade" , "tailscale" )
2023-10-06 19:00:15 +00:00
cmd . Stdout = up . Stdout
cmd . Stderr = up . Stderr
2023-08-11 00:01:22 +00:00
if err := cmd . Run ( ) ; err != nil {
return fmt . Errorf ( "failed tailscale update using pkg: %w" , err )
}
return nil
}
2023-08-30 21:50:03 +00:00
func ( up * Updater ) updateLinuxBinary ( ) error {
2023-08-30 23:25:06 +00:00
ver , err := requestedTailscaleVersion ( up . Version , up . track )
if err != nil {
return err
}
if ! up . confirm ( ver ) {
return nil
}
// Root is needed to overwrite binaries and restart systemd unit.
if err := requireRoot ( ) ; err != nil {
return err
}
dlPath , err := up . downloadLinuxTarball ( ver )
if err != nil {
return err
}
up . Logf ( "Extracting %q" , dlPath )
if err := up . unpackLinuxTarball ( dlPath ) ; err != nil {
return err
}
if err := os . Remove ( dlPath ) ; err != nil {
2023-09-02 18:09:38 +00:00
up . Logf ( "failed to clean up %q: %v" , dlPath , err )
2023-08-30 23:25:06 +00:00
}
if err := restartSystemdUnit ( context . Background ( ) ) ; err != nil {
if errors . Is ( err , errors . ErrUnsupported ) {
up . Logf ( "Tailscale binaries updated successfully.\nPlease restart tailscaled to finish the update." )
} else {
up . Logf ( "Tailscale binaries updated successfully, but failed to restart tailscaled: %s.\nPlease restart tailscaled to finish the update." , err )
}
} else {
up . Logf ( "Success" )
}
return nil
}
func ( up * Updater ) downloadLinuxTarball ( ver string ) ( string , error ) {
dlDir , err := os . UserCacheDir ( )
if err != nil {
return "" , err
}
dlDir = filepath . Join ( dlDir , "tailscale-update" )
if err := os . MkdirAll ( dlDir , 0700 ) ; err != nil {
return "" , err
}
pkgsPath := fmt . Sprintf ( "%s/tailscale_%s_%s.tgz" , up . track , ver , runtime . GOARCH )
dlPath := filepath . Join ( dlDir , path . Base ( pkgsPath ) )
if err := up . downloadURLToFile ( pkgsPath , dlPath ) ; err != nil {
return "" , err
}
return dlPath , nil
}
func ( up * Updater ) unpackLinuxTarball ( path string ) error {
tailscale , tailscaled , err := binaryPaths ( )
if err != nil {
return err
}
f , err := os . Open ( path )
if err != nil {
return err
}
defer f . Close ( )
gr , err := gzip . NewReader ( f )
if err != nil {
return err
}
defer gr . Close ( )
tr := tar . NewReader ( gr )
files := make ( map [ string ] int )
wantFiles := map [ string ] int {
"tailscale" : 1 ,
"tailscaled" : 1 ,
}
for {
th , err := tr . Next ( )
if err == io . EOF {
break
}
if err != nil {
return fmt . Errorf ( "failed extracting %q: %w" , path , err )
}
// TODO(awly): try to also extract tailscaled.service. The tricky part
// is fixing up binary paths in that file if they differ from where
// local tailscale/tailscaled are installed. Also, this may not be a
// systemd distro.
switch filepath . Base ( th . Name ) {
case "tailscale" :
files [ "tailscale" ] ++
if err := writeFile ( tr , tailscale + ".new" , 0755 ) ; err != nil {
return fmt . Errorf ( "failed extracting the new tailscale binary from %q: %w" , path , err )
}
case "tailscaled" :
files [ "tailscaled" ] ++
if err := writeFile ( tr , tailscaled + ".new" , 0755 ) ; err != nil {
return fmt . Errorf ( "failed extracting the new tailscaled binary from %q: %w" , path , err )
}
}
}
if ! maps . Equal ( files , wantFiles ) {
return fmt . Errorf ( "%q has missing or duplicate files: got %v, want %v" , path , files , wantFiles )
}
// Only place the files in final locations after everything extracted correctly.
if err := os . Rename ( tailscale + ".new" , tailscale ) ; err != nil {
return err
}
up . Logf ( "Updated %s" , tailscale )
if err := os . Rename ( tailscaled + ".new" , tailscaled ) ; err != nil {
return err
}
up . Logf ( "Updated %s" , tailscaled )
return nil
}
func writeFile ( r io . Reader , path string , perm os . FileMode ) error {
if err := os . Remove ( path ) ; err != nil && ! os . IsNotExist ( err ) {
return fmt . Errorf ( "failed to remove existing file at %q: %w" , path , err )
}
f , err := os . OpenFile ( path , os . O_WRONLY | os . O_CREATE | os . O_EXCL , perm )
if err != nil {
return err
}
defer f . Close ( )
if _ , err := io . Copy ( f , r ) ; err != nil {
return err
}
return f . Close ( )
}
// Var allows overriding this in tests.
var binaryPaths = func ( ) ( tailscale , tailscaled string , err error ) {
// This can be either tailscale or tailscaled.
this , err := os . Executable ( )
if err != nil {
return "" , "" , err
}
otherName := "tailscaled"
if filepath . Base ( this ) == "tailscaled" {
otherName = "tailscale"
}
// Try to find the other binary in the same directory.
other := filepath . Join ( filepath . Dir ( this ) , otherName )
_ , err = os . Stat ( other )
if os . IsNotExist ( err ) {
// If it's not in the same directory, try to find it in $PATH.
other , err = exec . LookPath ( otherName )
}
if err != nil {
return "" , "" , fmt . Errorf ( "cannot find %q in neither %q nor $PATH: %w" , otherName , filepath . Dir ( this ) , err )
}
if otherName == "tailscaled" {
return this , other , nil
} else {
return other , this , nil
}
2023-08-30 00:36:05 +00:00
}
2023-08-11 00:01:22 +00:00
func haveExecutable ( name string ) bool {
path , err := exec . LookPath ( name )
return err == nil && path != ""
}
func requestedTailscaleVersion ( ver , track string ) ( string , error ) {
if ver != "" {
return ver , nil
}
return LatestTailscaleVersion ( track )
}
// LatestTailscaleVersion returns the latest released version for the given
// track from pkgs.tailscale.com.
func LatestTailscaleVersion ( track string ) ( string , error ) {
if track == CurrentTrack {
if version . IsUnstableBuild ( ) {
track = UnstableTrack
} else {
track = StableTrack
}
}
2023-08-11 21:55:07 +00:00
latest , err := latestPackages ( track )
2023-08-11 00:01:22 +00:00
if err != nil {
2023-08-11 21:55:07 +00:00
return "" , err
2023-08-11 00:01:22 +00:00
}
2023-08-11 21:55:07 +00:00
if latest . Version == "" {
return "" , fmt . Errorf ( "no latest version found for %q track" , track )
2023-08-11 00:01:22 +00:00
}
2023-08-11 21:55:07 +00:00
return latest . Version , nil
}
type trackPackages struct {
2023-08-28 21:26:19 +00:00
Version string
Tarballs map [ string ] string
TarballsVersion string
Exes [ ] string
ExesVersion string
MSIs map [ string ] string
MSIsVersion string
MacZips map [ string ] string
MacZipsVersion string
SPKs map [ string ] map [ string ] string
SPKsVersion string
2023-08-11 21:55:07 +00:00
}
func latestPackages ( track string ) ( * trackPackages , error ) {
url := fmt . Sprintf ( "https://pkgs.tailscale.com/%s/?mode=json&os=%s" , track , runtime . GOOS )
res , err := http . Get ( url )
2023-08-11 00:01:22 +00:00
if err != nil {
2023-08-11 21:55:07 +00:00
return nil , fmt . Errorf ( "fetching latest tailscale version: %w" , err )
2023-08-11 00:01:22 +00:00
}
2023-08-11 21:55:07 +00:00
defer res . Body . Close ( )
var latest trackPackages
if err := json . NewDecoder ( res . Body ) . Decode ( & latest ) ; err != nil {
return nil , fmt . Errorf ( "decoding JSON: %v: %w" , res . Status , err )
2023-08-11 00:01:22 +00:00
}
2023-08-11 21:55:07 +00:00
return & latest , nil
2023-08-11 00:01:22 +00:00
}
func requireRoot ( ) error {
if os . Geteuid ( ) == 0 {
return nil
}
switch runtime . GOOS {
case "linux" :
return errors . New ( "must be root; use sudo" )
case "freebsd" , "openbsd" :
return errors . New ( "must be root; use doas" )
default :
return errors . New ( "must be root" )
}
}
2023-08-30 00:36:05 +00:00
func isExitError ( err error ) bool {
var exitErr * exec . ExitError
return errors . As ( err , & exitErr )
}