From c070d39287d7febcadaf6958b0667e7bcb001af0 Mon Sep 17 00:00:00 2001 From: Anton Tolchanov Date: Thu, 6 Oct 2022 20:06:49 +0100 Subject: [PATCH] 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 --- cmd/tailscaled/install_darwin.go | 96 +++++++++++++++++++++++--------- 1 file changed, 70 insertions(+), 26 deletions(-) diff --git a/cmd/tailscaled/install_darwin.go b/cmd/tailscaled/install_darwin.go index 0b3a1fee2..ca930a91d 100644 --- a/cmd/tailscaled/install_darwin.go +++ b/cmd/tailscaled/install_darwin.go @@ -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,40 +115,24 @@ 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" - f, err := os.Create(tmpBin) + + same, err := sameFile(exe, targetBin) if err != nil { return err } - self, err := os.Open(exe) - if err != nil { - f.Close() - return err - } - _, err = io.Copy(f, self) - self.Close() - if err != nil { - f.Close() - return err - } - if err := f.Close(); err != nil { - return err - } - if err := os.Chmod(tmpBin, 0755); err != nil { - return err - } - if err := os.Rename(tmpBin, targetBin); 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 } @@ -155,3 +147,55 @@ func installSystemDaemonDarwin(args []string) (err error) { 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 + } + srcf, err := os.Open(src) + if err != nil { + f.Close() + return err + } + _, err = io.Copy(f, srcf) + srcf.Close() + if err != nil { + f.Close() + return err + } + if err := f.Close(); err != nil { + return err + } + if err := os.Chmod(tmpBin, 0755); err != nil { + return err + } + if err := os.Rename(tmpBin, dst); err != nil { + return err + } + + return nil +} + +func isSymlink(path string) bool { + fi, err := os.Lstat(path) + return err == nil && (fi.Mode()&os.ModeSymlink == os.ModeSymlink) +} + +// 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) + } + 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 +}