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>
This commit is contained in:
Anton Tolchanov 2022-10-06 20:06:49 +01:00 committed by Anton Tolchanov
parent 51d488673a
commit c070d39287
1 changed files with 70 additions and 26 deletions

View File

@ -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
}