diff --git a/cmd/tailscale/cli/ssh.go b/cmd/tailscale/cli/ssh.go index 60a8db7eb..229ac85b1 100644 --- a/cmd/tailscale/cli/ssh.go +++ b/cmd/tailscale/cli/ssh.go @@ -48,8 +48,8 @@ The 'tailscale ssh' wrapper adds a few things: } func runSSH(ctx context.Context, args []string) error { - if runtime.GOOS == "darwin" && version.IsSandboxedMacOS() && !envknob.UseWIPCode() { - return errors.New("The 'tailscale ssh' subcommand is not available on sandboxed macOS builds.\nUse the regular 'ssh' client instead.") + if runtime.GOOS == "darwin" && version.IsMacAppStore() && !envknob.UseWIPCode() { + return errors.New("The 'tailscale ssh' subcommand is not available on macOS builds distributed through the App Store or TestFlight.\nInstall the Standalone variant of Tailscale (download it from https://pkgs.tailscale.com), or use the regular 'ssh' client instead.") } if len(args) == 0 { return errors.New("usage: ssh [user@]") diff --git a/safesocket/safesocket_darwin.go b/safesocket/safesocket_darwin.go index 12aa7f3eb..62e6f7e6d 100644 --- a/safesocket/safesocket_darwin.go +++ b/safesocket/safesocket_darwin.go @@ -74,7 +74,7 @@ func localTCPPortAndTokenDarwin() (port int, token string, err error) { if dir := os.Getenv("TS_MACOS_CLI_SHARED_DIR"); dir != "" { // First see if we're running as the non-AppStore "macsys" variant. - if version.IsMacSysExt() { + if version.IsMacSys() { if port, token, err := localTCPPortAndTokenMacsys(); err == nil { return port, token, nil } diff --git a/version/prop.go b/version/prop.go index 83644f69d..11cc69c03 100644 --- a/version/prop.go +++ b/version/prop.go @@ -48,10 +48,38 @@ func IsSandboxedMacOS() bool { return IsMacAppStore() || IsMacSysExt() } +// IsMacSys reports whether this process is part of the Standalone variant of +// Tailscale for macOS, either the main GUI process (non-sandboxed) or the +// system extension (sandboxed). +func IsMacSys() bool { + return IsMacSysExt() || IsMacSysApp() +} + +var isMacSysApp lazy.SyncValue[bool] + +// IsMacSysApp reports whether this process is the main, non-sandboxed GUI process +// that ships with the Standalone variant of Tailscale for macOS. +func IsMacSysApp() bool { + if runtime.GOOS != "darwin" { + return false + } + + return isMacSysApp.Get(func() bool { + exe, err := os.Executable() + if err != nil { + return false + } + // Check that this is the GUI binary, and it is not sandboxed. The GUI binary + // shipped in the App Store will always have the App Sandbox enabled. + return strings.HasSuffix(exe, "/Contents/MacOS/Tailscale") && !IsMacAppSandboxEnabled() + }) +} + var isMacSysExt lazy.SyncValue[bool] -// IsMacSysExt whether this binary is from the standalone "System -// Extension" (a.k.a. "macsys") version of Tailscale for macOS. +// IsMacSysExt reports whether this binary is the system extension shipped as part of +// the standalone "System Extension" (a.k.a. "macsys") version of Tailscale +// for macOS. func IsMacSysExt() bool { if runtime.GOOS != "darwin" { return false @@ -68,6 +96,19 @@ func IsMacSysExt() bool { }) } +var isMacAppSandboxEnabled lazy.SyncValue[bool] + +// IsMacAppSandboxEnabled reports whether this process is subject to the App Sandbox +// on macOS. +func IsMacAppSandboxEnabled() bool { + if runtime.GOOS != "darwin" { + return false + } + return isMacAppSandboxEnabled.Get(func() bool { + return os.Getenv("APP_SANDBOX_CONTAINER_ID") != "" + }) +} + var isMacAppStore lazy.SyncValue[bool] // IsMacAppStore whether this binary is from the App Store version of Tailscale @@ -80,6 +121,11 @@ func IsMacAppStore() bool { // Both macsys and app store versions can run CLI executable with // suffix /Contents/MacOS/Tailscale. Check $HOME to filter out running // as macsys. + if !IsMacAppSandboxEnabled() { + // If no sandbox found, we're definitely not on an App Store release, as you cannot push + // anything to the App Store that has the App Sandbox disabled. + return false + } if strings.Contains(os.Getenv("HOME"), "/Containers/io.tailscale.ipn.macsys/") { return false }