Browse Source

version,cli,safesocket: detect non-sandboxed macOS GUI (#11369)

Updates ENG-2848

We can safely disable the App Sandbox for our macsys GUI, allowing us to use `tailscale ssh` and do a few other things that we've wanted to do for a while. This PR:

- allows Tailscale SSH to be used from the macsys GUI binary when called from a CLI
- tweaks the detection of client variants in prop.go, with new functions `IsMacSys()`, `IsMacSysApp()` and `IsMacAppSandboxEnabled()`

Signed-off-by: Andrea Gottardo <[email protected]>
Andrea Gottardo 2 years ago
parent
commit
08ebac9acb
3 changed files with 51 additions and 5 deletions
  1. 2 2
      cmd/tailscale/cli/ssh.go
  2. 1 1
      safesocket/safesocket_darwin.go
  3. 48 2
      version/prop.go

+ 2 - 2
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@]<host>")

+ 1 - 1
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
 			}

+ 48 - 2
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
 		}