2023-01-27 21:37:20 +00:00
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
2021-01-29 22:32:56 +00:00
package safesocket
import (
"bufio"
"bytes"
2021-09-24 20:33:30 +01:00
"errors"
2021-01-29 22:32:56 +00:00
"fmt"
2023-09-06 12:43:10 +01:00
"net"
2021-01-29 22:32:56 +00:00
"os"
"os/exec"
2021-03-05 21:29:03 +00:00
"path/filepath"
2021-01-29 22:32:56 +00:00
"strconv"
"strings"
2022-04-29 00:22:19 +01:00
"sync"
2023-09-06 12:43:10 +01:00
"time"
2024-01-10 00:39:39 +00:00
"tailscale.com/version"
2021-01-29 22:32:56 +00:00
)
func init ( ) {
localTCPPortAndToken = localTCPPortAndTokenDarwin
}
2021-09-24 20:33:30 +01:00
// localTCPPortAndTokenMacsys returns the localhost TCP port number and auth token
2021-09-24 21:58:26 +01:00
// from /Library/Tailscale.
2021-09-24 20:33:30 +01:00
//
// In that case the files are:
2022-08-02 17:33:46 +01:00
//
// /Library/Tailscale/ipnport => $port (symlink with localhost port number target)
// /Library/Tailscale/sameuserproof-$port is a file with auth
2021-09-24 21:58:26 +01:00
func localTCPPortAndTokenMacsys ( ) ( port int , token string , err error ) {
const dir = "/Library/Tailscale"
2021-09-24 20:33:30 +01:00
portStr , err := os . Readlink ( filepath . Join ( dir , "ipnport" ) )
if err != nil {
return 0 , "" , err
}
port , err = strconv . Atoi ( portStr )
if err != nil {
return 0 , "" , err
}
authb , err := os . ReadFile ( filepath . Join ( dir , "sameuserproof-" + portStr ) )
if err != nil {
return 0 , "" , err
}
auth := strings . TrimSpace ( string ( authb ) )
if auth == "" {
return 0 , "" , errors . New ( "empty auth token in sameuserproof file" )
}
2023-09-06 12:43:10 +01:00
// The above files exist forever after the first run of
// /Applications/Tailscale.app, so check we can connect to avoid returning a
// port nothing is listening on. Connect to "127.0.0.1" rather than
// "localhost" due to #7851.
conn , err := net . DialTimeout ( "tcp" , "127.0.0.1:" + portStr , time . Second )
if err != nil {
return 0 , "" , err
}
conn . Close ( )
2021-09-24 20:33:30 +01:00
return port , auth , nil
}
2022-04-29 00:22:19 +01:00
var warnAboutRootOnce sync . Once
2021-01-29 22:32:56 +00:00
func localTCPPortAndTokenDarwin ( ) ( port int , token string , err error ) {
2021-03-05 21:29:03 +00:00
// There are two ways this binary can be run: as the Mac App Store sandboxed binary,
// or a normal binary that somebody built or download and are being run from outside
// the sandbox. Detect which way we're running and then figure out how to connect
// to the local daemon.
if dir := os . Getenv ( "TS_MACOS_CLI_SHARED_DIR" ) ; dir != "" {
2021-09-24 20:33:30 +01:00
// First see if we're running as the non-AppStore "macsys" variant.
2024-03-14 21:28:06 +00:00
if version . IsMacSys ( ) {
2021-09-24 21:58:26 +01:00
if port , token , err := localTCPPortAndTokenMacsys ( ) ; err == nil {
return port , token , nil
}
2021-09-24 20:33:30 +01:00
}
2021-03-05 21:29:03 +00:00
// The current binary (this process) is sandboxed. The user is
// running the CLI via /Applications/Tailscale.app/Contents/MacOS/Tailscale
// which sets the TS_MACOS_CLI_SHARED_DIR environment variable.
2022-09-15 13:06:59 +01:00
fis , err := os . ReadDir ( dir )
2021-03-05 21:29:03 +00:00
if err != nil {
return 0 , "" , err
}
for _ , fi := range fis {
name := filepath . Base ( fi . Name ( ) )
// Look for name like "sameuserproof-61577-2ae2ec9e0aa2005784f1"
// to extract out the port number and token.
if strings . HasPrefix ( name , "sameuserproof-" ) {
f := strings . SplitN ( name , "-" , 3 )
if len ( f ) == 3 {
if port , err := strconv . Atoi ( f [ 1 ] ) ; err == nil {
return port , f [ 2 ] , nil
}
}
}
}
2022-04-29 00:22:19 +01:00
if os . Geteuid ( ) == 0 {
// Log a warning as the clue to the user, in case the error
// message is swallowed. Only do this once since we may retry
// multiple times to connect, and don't want to spam.
warnAboutRootOnce . Do ( func ( ) {
fmt . Fprintf ( os . Stderr , "Warning: The CLI is running as root from within a sandboxed binary. It cannot reach the local tailscaled, please try again as a regular user.\n" )
} )
}
2021-03-05 21:29:03 +00:00
return 0 , "" , fmt . Errorf ( "failed to find sandboxed sameuserproof-* file in TS_MACOS_CLI_SHARED_DIR %q" , dir )
}
// The current process is running outside the sandbox, so use
2021-09-24 20:33:30 +01:00
// lsof to find the IPNExtension (the Mac App Store variant).
2021-03-05 21:29:03 +00:00
2021-07-20 20:20:01 +01:00
cmd := exec . Command ( "lsof" ,
2021-03-05 21:43:54 +00:00
"-n" , // numeric sockets; don't do DNS lookups, etc
"-a" , // logical AND remaining options
2021-01-29 22:32:56 +00:00
fmt . Sprintf ( "-u%d" , os . Getuid ( ) ) , // process of same user only
2021-03-05 21:43:54 +00:00
"-c" , "IPNExtension" , // starting with IPNExtension
2021-01-29 22:32:56 +00:00
"-F" , // machine-readable output
2021-07-20 20:20:01 +01:00
)
out , err := cmd . Output ( )
2021-01-29 22:32:56 +00:00
if err != nil {
2021-09-24 20:33:30 +01:00
// Before returning an error, see if we're running the
// macsys variant at the normal location.
2021-09-24 21:58:26 +01:00
if port , token , err := localTCPPortAndTokenMacsys ( ) ; err == nil {
2021-09-24 20:33:30 +01:00
return port , token , nil
}
2021-07-20 20:20:01 +01:00
return 0 , "" , fmt . Errorf ( "failed to run '%s' looking for IPNExtension: %w" , cmd , err )
2021-01-29 22:32:56 +00:00
}
bs := bufio . NewScanner ( bytes . NewReader ( out ) )
subStr := [ ] byte ( ".tailscale.ipn.macos/sameuserproof-" )
for bs . Scan ( ) {
line := bs . Bytes ( )
i := bytes . Index ( line , subStr )
if i == - 1 {
continue
}
f := strings . SplitN ( string ( line [ i + len ( subStr ) : ] ) , "-" , 2 )
if len ( f ) != 2 {
continue
}
portStr , token := f [ 0 ] , f [ 1 ]
port , err := strconv . Atoi ( portStr )
if err != nil {
return 0 , "" , fmt . Errorf ( "invalid port %q found in lsof" , portStr )
}
return port , token , nil
}
2021-09-24 20:33:30 +01:00
// Before returning an error, see if we're running the
// macsys variant at the normal location.
2021-09-24 21:58:26 +01:00
if port , token , err := localTCPPortAndTokenMacsys ( ) ; err == nil {
2021-09-24 20:33:30 +01:00
return port , token , nil
}
2021-01-29 22:32:56 +00:00
return 0 , "" , ErrTokenNotFound
}