cmd/tailscaled: handle tailscaled symlink on macOS

When Tailscale is installed via Homebrew, `/usr/local/bin/tailscaled`
is a symlink to the actual binary.

Now when `tailscaled install-system-daemon` runs, it will not attempt
to overwrite that symlink if it already points to the tailscaled binary.
However, if executed binary and the link target differ, the path will
he overwritten - this can happen when a user decides to replace
Homebrew-installed tailscaled with a one compiled from source code.

Fixes #5353

Signed-off-by: Anton Tolchanov <anton@tailscale.com>
pull/5093/head
Anton Tolchanov 2 years ago committed by Anton Tolchanov
parent 51d488673a
commit c070d39287

@ -11,6 +11,7 @@ import (
"errors"
"fmt"
"io"
"io/fs"
"os"
"os/exec"
"path/filepath"
@ -83,6 +84,13 @@ func uninstallSystemDaemonDarwin(args []string) (ret error) {
ret = err
}
}
// Do not delete targetBin if it's a symlink, which happens if it was installed via
// Homebrew.
if isSymlink(targetBin) {
return ret
}
if err := os.Remove(targetBin); err != nil {
if os.IsNotExist(err) {
err = nil
@ -107,26 +115,56 @@ func installSystemDaemonDarwin(args []string) (err error) {
// Best effort:
uninstallSystemDaemonDarwin(nil)
// Copy ourselves to /usr/local/bin/tailscaled.
if err := os.MkdirAll(filepath.Dir(targetBin), 0755); err != nil {
return err
}
exe, err := os.Executable()
if err != nil {
return fmt.Errorf("failed to find our own executable path: %w", err)
}
tmpBin := targetBin + ".tmp"
same, err := sameFile(exe, targetBin)
if err != nil {
return err
}
// Do not overwrite targetBin with the binary file if it it's already
// pointing to it. This is primarily to handle Homebrew that writes
// /usr/local/bin/tailscaled is a symlink to the actual binary.
if !same {
if err := copyBinary(exe, targetBin); err != nil {
return err
}
}
if err := os.WriteFile(sysPlist, []byte(darwinLaunchdPlist), 0700); err != nil {
return err
}
if out, err := exec.Command("launchctl", "load", sysPlist).CombinedOutput(); err != nil {
return fmt.Errorf("error running launchctl load %s: %v, %s", sysPlist, err, out)
}
if out, err := exec.Command("launchctl", "start", service).CombinedOutput(); err != nil {
return fmt.Errorf("error running launchctl start %s: %v, %s", service, err, out)
}
return nil
}
// copyBinary copies binary file `src` into `dst`.
func copyBinary(src, dst string) error {
if err := os.MkdirAll(filepath.Dir(dst), 0755); err != nil {
return err
}
tmpBin := dst + ".tmp"
f, err := os.Create(tmpBin)
if err != nil {
return err
}
self, err := os.Open(exe)
srcf, err := os.Open(src)
if err != nil {
f.Close()
return err
}
_, err = io.Copy(f, self)
self.Close()
_, err = io.Copy(f, srcf)
srcf.Close()
if err != nil {
f.Close()
return err
@ -137,21 +175,27 @@ func installSystemDaemonDarwin(args []string) (err error) {
if err := os.Chmod(tmpBin, 0755); err != nil {
return err
}
if err := os.Rename(tmpBin, targetBin); err != nil {
if err := os.Rename(tmpBin, dst); err != nil {
return err
}
if err := os.WriteFile(sysPlist, []byte(darwinLaunchdPlist), 0700); err != nil {
return err
return nil
}
if out, err := exec.Command("launchctl", "load", sysPlist).CombinedOutput(); err != nil {
return fmt.Errorf("error running launchctl load %s: %v, %s", sysPlist, err, out)
func isSymlink(path string) bool {
fi, err := os.Lstat(path)
return err == nil && (fi.Mode()&os.ModeSymlink == os.ModeSymlink)
}
if out, err := exec.Command("launchctl", "start", service).CombinedOutput(); err != nil {
return fmt.Errorf("error running launchctl start %s: %v, %s", service, err, out)
// sameFile returns true if both file paths exist and resolve to the same file.
func sameFile(path1, path2 string) (bool, error) {
dst1, err := filepath.EvalSymlinks(path1)
if err != nil && !errors.Is(err, fs.ErrNotExist) {
return false, fmt.Errorf("EvalSymlinks(%s): %w", path1, err)
}
return nil
dst2, err := filepath.EvalSymlinks(path2)
if err != nil && !errors.Is(err, fs.ErrNotExist) {
return false, fmt.Errorf("EvalSymlinks(%s): %w", path2, err)
}
return dst1 == dst2, nil
}

Loading…
Cancel
Save