2023-01-27 13:37:20 -08:00
|
|
|
// Copyright (c) Tailscale Inc & AUTHORS
|
|
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
2021-01-29 14:32:56 -08:00
|
|
|
|
|
|
|
package safesocket
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bufio"
|
|
|
|
"bytes"
|
2025-02-06 09:51:00 -05:00
|
|
|
crand "crypto/rand"
|
2021-09-24 12:33:30 -07:00
|
|
|
"errors"
|
2021-01-29 14:32:56 -08:00
|
|
|
"fmt"
|
2025-02-06 09:51:00 -05:00
|
|
|
"io/fs"
|
|
|
|
"log"
|
2023-09-06 12:43:10 +01:00
|
|
|
"net"
|
2021-01-29 14:32:56 -08:00
|
|
|
"os"
|
|
|
|
"os/exec"
|
2021-03-05 13:29:03 -08:00
|
|
|
"path/filepath"
|
2021-01-29 14:32:56 -08:00
|
|
|
"strconv"
|
|
|
|
"strings"
|
2022-04-28 16:22:19 -07:00
|
|
|
"sync"
|
2023-09-06 12:43:10 +01:00
|
|
|
"time"
|
2024-01-09 16:39:39 -08:00
|
|
|
|
2025-02-06 09:51:00 -05:00
|
|
|
"golang.org/x/sys/unix"
|
2024-01-09 16:39:39 -08:00
|
|
|
"tailscale.com/version"
|
2021-01-29 14:32:56 -08:00
|
|
|
)
|
|
|
|
|
|
|
|
func init() {
|
|
|
|
localTCPPortAndToken = localTCPPortAndTokenDarwin
|
|
|
|
}
|
|
|
|
|
2025-02-06 09:51:00 -05:00
|
|
|
const sameUserProofTokenLength = 10
|
|
|
|
|
|
|
|
type safesocketDarwin struct {
|
|
|
|
mu sync.Mutex
|
|
|
|
token string // safesocket auth token
|
|
|
|
port int // safesocket port
|
|
|
|
sameuserproofFD *os.File // file descriptor for macos app store sameuserproof file
|
|
|
|
sharedDir string // shared directory for location of sameuserproof file
|
|
|
|
|
|
|
|
checkConn bool // Check macsys safesocket port before returning it
|
|
|
|
isMacSysExt func() bool // For testing only to force macsys
|
|
|
|
}
|
|
|
|
|
|
|
|
var ssd = safesocketDarwin{
|
|
|
|
isMacSysExt: version.IsMacSysExt,
|
|
|
|
checkConn: true,
|
|
|
|
sharedDir: "/Library/Tailscale",
|
|
|
|
}
|
|
|
|
|
|
|
|
// There are three ways a Darwin binary can be run: as the Mac App Store (macOS)
|
|
|
|
// standalone notarized (macsys), or a separate CLI (tailscale) that was
|
|
|
|
// built or downloaded.
|
2022-08-02 09:33:46 -07:00
|
|
|
//
|
2025-02-06 09:51:00 -05:00
|
|
|
// The macOS and macsys binaries can communicate directly via XPC with
|
|
|
|
// the NEPacketTunnelProvider managed tailscaled process and are responsible for
|
|
|
|
// calling SetCredentials when they need to operate as a CLI.
|
|
|
|
|
|
|
|
// A built/downloaded CLI binary will not be managing the NEPacketTunnelProvider
|
|
|
|
// hosting tailscaled directly and must source the credentials from a 'sameuserproof' file.
|
|
|
|
// This file is written to sharedDir when tailscaled/NEPacketTunnelProvider
|
|
|
|
// calls InitListenerDarwin.
|
|
|
|
|
|
|
|
// localTCPPortAndTokenDarwin returns the localhost TCP port number and auth token
|
|
|
|
// either generated, or sourced from the NEPacketTunnelProvider managed tailscaled process.
|
|
|
|
func localTCPPortAndTokenDarwin() (port int, token string, err error) {
|
|
|
|
ssd.mu.Lock()
|
|
|
|
defer ssd.mu.Unlock()
|
|
|
|
|
|
|
|
if ssd.port != 0 && ssd.token != "" {
|
|
|
|
return ssd.port, ssd.token, nil
|
|
|
|
}
|
2021-09-24 13:58:26 -07:00
|
|
|
|
2025-02-06 09:51:00 -05:00
|
|
|
// Credentials were not explicitly, this is likely a standalone CLI binary.
|
|
|
|
// Fallback to reading the sameuserproof file.
|
|
|
|
return portAndTokenFromSameUserProof()
|
|
|
|
}
|
|
|
|
|
|
|
|
// SetCredentials sets an token and port used to authenticate safesocket generated
|
|
|
|
// by the NEPacketTunnelProvider tailscaled process. This is only used when running
|
|
|
|
// the CLI via Tailscale.app.
|
|
|
|
func SetCredentials(token string, port int) {
|
|
|
|
ssd.mu.Lock()
|
|
|
|
defer ssd.mu.Unlock()
|
|
|
|
|
|
|
|
if ssd.token != "" || ssd.port != 0 {
|
|
|
|
// Not fatal, but likely programmer error. Credentials do not change.
|
|
|
|
log.Printf("warning: SetCredentials credentials already set")
|
|
|
|
}
|
|
|
|
|
|
|
|
ssd.token = token
|
|
|
|
ssd.port = port
|
|
|
|
}
|
|
|
|
|
|
|
|
// InitListenerDarwin initializes the listener for the CLI commands
|
|
|
|
// and localapi HTTP server and sets the port/token. This will override
|
|
|
|
// any credentials set explicitly via SetCredentials(). Calling this mulitple times
|
|
|
|
// has no effect. The listener and it's corresponding token/port is initialized only once.
|
|
|
|
func InitListenerDarwin(sharedDir string) (*net.Listener, error) {
|
|
|
|
ssd.mu.Lock()
|
|
|
|
defer ssd.mu.Unlock()
|
|
|
|
|
|
|
|
ln := onceListener.ln
|
|
|
|
if ln != nil {
|
|
|
|
return ln, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
var err error
|
|
|
|
ln, err = localhostListener()
|
2021-09-24 12:33:30 -07:00
|
|
|
if err != nil {
|
2025-02-06 09:51:00 -05:00
|
|
|
log.Printf("InitListenerDarwin: listener initialization failed")
|
|
|
|
return nil, err
|
2021-09-24 12:33:30 -07:00
|
|
|
}
|
2025-02-06 09:51:00 -05:00
|
|
|
|
|
|
|
port, err := localhostTCPPort()
|
2021-09-24 12:33:30 -07:00
|
|
|
if err != nil {
|
2025-02-06 09:51:00 -05:00
|
|
|
log.Printf("localhostTCPPort: listener initialization failed")
|
|
|
|
return nil, err
|
2021-09-24 12:33:30 -07:00
|
|
|
}
|
2025-02-06 09:51:00 -05:00
|
|
|
|
|
|
|
token, err := getToken()
|
2021-09-24 12:33:30 -07:00
|
|
|
if err != nil {
|
2025-02-06 09:51:00 -05:00
|
|
|
log.Printf("localhostTCPPort: getToken failed")
|
|
|
|
return nil, err
|
2021-09-24 12:33:30 -07:00
|
|
|
}
|
2025-02-06 09:51:00 -05:00
|
|
|
|
|
|
|
if port == 0 || token == "" {
|
|
|
|
log.Printf("localhostTCPPort: Invalid token or port")
|
|
|
|
return nil, fmt.Errorf("invalid localhostTCPPort: returned 0")
|
2021-09-24 12:33:30 -07:00
|
|
|
}
|
2023-09-06 12:43:10 +01:00
|
|
|
|
2025-02-06 09:51:00 -05:00
|
|
|
ssd.sharedDir = sharedDir
|
|
|
|
ssd.token = token
|
|
|
|
ssd.port = port
|
|
|
|
|
|
|
|
// Write the port and token to a sameuserproof file
|
|
|
|
err = initSameUserProofToken(sharedDir, port, token)
|
2023-09-06 12:43:10 +01:00
|
|
|
if err != nil {
|
2025-02-06 09:51:00 -05:00
|
|
|
// Not fatal
|
|
|
|
log.Printf("initSameUserProofToken: failed: %v", err)
|
2023-09-06 12:43:10 +01:00
|
|
|
}
|
|
|
|
|
2025-02-06 09:51:00 -05:00
|
|
|
return ln, nil
|
2021-09-24 12:33:30 -07:00
|
|
|
}
|
|
|
|
|
2025-02-06 09:51:00 -05:00
|
|
|
var onceListener struct {
|
|
|
|
once sync.Once
|
|
|
|
ln *net.Listener
|
|
|
|
}
|
2022-04-28 16:22:19 -07:00
|
|
|
|
2025-02-06 09:51:00 -05:00
|
|
|
func localhostTCPPort() (int, error) {
|
|
|
|
if onceListener.ln == nil {
|
|
|
|
return 0, fmt.Errorf("listener not initialized")
|
|
|
|
}
|
|
|
|
|
|
|
|
ln, err := localhostListener()
|
|
|
|
if err != nil {
|
|
|
|
return 0, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return (*ln).Addr().(*net.TCPAddr).Port, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func localhostListener() (*net.Listener, error) {
|
|
|
|
onceListener.once.Do(func() {
|
|
|
|
ln, err := net.Listen("tcp4", "127.0.0.1:0")
|
|
|
|
if err != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
onceListener.ln = &ln
|
|
|
|
})
|
|
|
|
if onceListener.ln == nil {
|
|
|
|
return nil, fmt.Errorf("failed to get TCP listener")
|
|
|
|
}
|
|
|
|
return onceListener.ln, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
var onceToken struct {
|
|
|
|
once sync.Once
|
|
|
|
token string
|
|
|
|
}
|
|
|
|
|
|
|
|
func getToken() (string, error) {
|
|
|
|
onceToken.once.Do(func() {
|
|
|
|
buf := make([]byte, sameUserProofTokenLength)
|
|
|
|
if _, err := crand.Read(buf); err != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
t := fmt.Sprintf("%x", buf)
|
|
|
|
onceToken.token = t
|
|
|
|
})
|
|
|
|
if onceToken.token == "" {
|
|
|
|
return "", fmt.Errorf("failed to generate token")
|
|
|
|
}
|
|
|
|
|
|
|
|
return onceToken.token, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// initSameUserProofToken writes the port and token to a sameuserproof
|
|
|
|
// file owned by the current user. We leave the file open to allow us
|
|
|
|
// to discover it via lsof.
|
|
|
|
//
|
|
|
|
// "sameuserproof" is intended to convey that the user attempting to read
|
|
|
|
// the credentials from the file is the same user that wrote them. For
|
|
|
|
// standalone macsys where tailscaled is running as root, we set group
|
|
|
|
// permissions to allow users in the admin group to read the file.
|
|
|
|
func initSameUserProofToken(sharedDir string, port int, token string) error {
|
|
|
|
var err error
|
|
|
|
|
|
|
|
// Guard against bad sharedDir
|
|
|
|
old, err := os.ReadDir(sharedDir)
|
|
|
|
if err == os.ErrNotExist {
|
|
|
|
log.Printf("failed to read shared dir %s: %v", sharedDir, err)
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Remove all old sameuserproof files
|
|
|
|
for _, fi := range old {
|
|
|
|
if name := fi.Name(); strings.HasPrefix(name, "sameuserproof-") {
|
|
|
|
err := os.Remove(filepath.Join(sharedDir, name))
|
|
|
|
if err != nil {
|
|
|
|
log.Printf("failed to remove %s: %v", name, err)
|
2021-09-24 13:58:26 -07:00
|
|
|
}
|
2021-09-24 12:33:30 -07:00
|
|
|
}
|
2025-02-06 09:51:00 -05:00
|
|
|
}
|
2021-09-24 12:33:30 -07:00
|
|
|
|
2025-02-06 09:51:00 -05:00
|
|
|
var baseFile string
|
|
|
|
var perm fs.FileMode
|
|
|
|
if ssd.isMacSysExt() {
|
|
|
|
perm = 0640 // allow wheel to read
|
|
|
|
baseFile = fmt.Sprintf("sameuserproof-%d", port)
|
|
|
|
portFile := filepath.Join(sharedDir, "ipnport")
|
|
|
|
err := os.Remove(portFile)
|
2021-03-05 13:29:03 -08:00
|
|
|
if err != nil {
|
2025-02-06 09:51:00 -05:00
|
|
|
log.Printf("failed to remove portfile %s: %v", portFile, err)
|
2021-03-05 13:29:03 -08:00
|
|
|
}
|
2025-02-06 09:51:00 -05:00
|
|
|
symlinkErr := os.Symlink(fmt.Sprint(port), portFile)
|
|
|
|
if symlinkErr != nil {
|
|
|
|
log.Printf("failed to symlink portfile: %v", symlinkErr)
|
2021-03-05 13:29:03 -08:00
|
|
|
}
|
2025-02-06 09:51:00 -05:00
|
|
|
} else {
|
|
|
|
perm = 0666
|
|
|
|
baseFile = fmt.Sprintf("sameuserproof-%d-%s", port, token)
|
|
|
|
}
|
|
|
|
|
|
|
|
path := filepath.Join(sharedDir, baseFile)
|
|
|
|
ssd.sameuserproofFD, err = os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, perm)
|
|
|
|
log.Printf("initSameUserProofToken : done=%v", err == nil)
|
|
|
|
|
|
|
|
if ssd.isMacSysExt() && err == nil {
|
|
|
|
fmt.Fprintf(ssd.sameuserproofFD, "%s\n", token)
|
|
|
|
|
|
|
|
// Macsys runs as root so ownership of this file will be
|
|
|
|
// root/wheel. Change ownership to root/admin which will let all members
|
|
|
|
// of the admin group to read it.
|
|
|
|
unix.Fchown(int(ssd.sameuserproofFD.Fd()), 0, 80 /* admin */)
|
|
|
|
}
|
|
|
|
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
// readMacsysSameuserproof returns the localhost TCP port number and auth token
|
|
|
|
// from a sameuserproof file written to /Library/Tailscale.
|
|
|
|
//
|
|
|
|
// In that case the files are:
|
|
|
|
//
|
|
|
|
// /Library/Tailscale/ipnport => $port (symlink with localhost port number target)
|
|
|
|
// /Library/Tailscale/sameuserproof-$port is a file containing only the auth token as a hex string.
|
|
|
|
func readMacsysSameUserProof() (port int, token string, err error) {
|
|
|
|
portStr, err := os.Readlink(filepath.Join(ssd.sharedDir, "ipnport"))
|
|
|
|
if err != nil {
|
|
|
|
return 0, "", err
|
|
|
|
}
|
|
|
|
port, err = strconv.Atoi(portStr)
|
|
|
|
if err != nil {
|
|
|
|
return 0, "", err
|
|
|
|
}
|
|
|
|
authb, err := os.ReadFile(filepath.Join(ssd.sharedDir, "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")
|
|
|
|
}
|
|
|
|
|
|
|
|
if ssd.checkConn {
|
|
|
|
// Files may be stale and there is no guarantee that the sameuserproof
|
|
|
|
// derived port is open and valid. Check it before returning it.
|
|
|
|
conn, err := net.DialTimeout("tcp", "127.0.0.1:"+portStr, time.Second)
|
|
|
|
if err != nil {
|
|
|
|
return 0, "", err
|
2022-04-28 16:22:19 -07:00
|
|
|
}
|
2025-02-06 09:51:00 -05:00
|
|
|
conn.Close()
|
2021-03-05 13:29:03 -08:00
|
|
|
}
|
|
|
|
|
2025-02-06 09:51:00 -05:00
|
|
|
return port, auth, nil
|
|
|
|
}
|
2021-03-05 13:29:03 -08:00
|
|
|
|
2025-02-06 09:51:00 -05:00
|
|
|
// readMacosSameUserProof searches for open sameuserproof files belonging
|
|
|
|
// to the current user and the IPNExtension (macOS App Store) process and returns a
|
|
|
|
// port and token.
|
|
|
|
func readMacosSameUserProof() (port int, token string, err error) {
|
2021-07-20 12:20:01 -07:00
|
|
|
cmd := exec.Command("lsof",
|
2021-03-05 13:43:54 -08:00
|
|
|
"-n", // numeric sockets; don't do DNS lookups, etc
|
|
|
|
"-a", // logical AND remaining options
|
2021-01-29 14:32:56 -08:00
|
|
|
fmt.Sprintf("-u%d", os.Getuid()), // process of same user only
|
2021-03-05 13:43:54 -08:00
|
|
|
"-c", "IPNExtension", // starting with IPNExtension
|
2021-01-29 14:32:56 -08:00
|
|
|
"-F", // machine-readable output
|
2021-07-20 12:20:01 -07:00
|
|
|
)
|
|
|
|
out, err := cmd.Output()
|
2025-02-06 09:51:00 -05:00
|
|
|
|
|
|
|
if err == nil {
|
|
|
|
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)
|
|
|
|
}
|
|
|
|
|
2021-09-24 12:33:30 -07:00
|
|
|
return port, token, nil
|
|
|
|
}
|
2021-01-29 14:32:56 -08:00
|
|
|
}
|
2025-02-06 09:51:00 -05:00
|
|
|
return 0, "", ErrTokenNotFound
|
|
|
|
}
|
|
|
|
|
|
|
|
func portAndTokenFromSameUserProof() (port int, token string, err error) {
|
|
|
|
if port, token, err := readMacosSameUserProof(); err == nil {
|
2021-01-29 14:32:56 -08:00
|
|
|
return port, token, nil
|
|
|
|
}
|
2021-09-24 12:33:30 -07:00
|
|
|
|
2025-02-06 09:51:00 -05:00
|
|
|
if port, token, err := readMacsysSameUserProof(); err == nil {
|
2021-09-24 12:33:30 -07:00
|
|
|
return port, token, nil
|
|
|
|
}
|
2025-02-06 09:51:00 -05:00
|
|
|
|
|
|
|
return 0, "", err
|
2021-01-29 14:32:56 -08:00
|
|
|
}
|