mirror of
https://github.com/tailscale/tailscale.git
synced 2025-05-06 07:37:38 +00:00
safesocket: add ability for Darwin clients to set explicit credentials (#14702)
updates tailscale/corp#25687 The darwin appstore and standalone clients now support XPC and the keychain for passing user credentials securely between the gui process and an NEVPNExtension hosted tailscaled. Clients that can communicate directly with the network extension, via XPC or the keychain, are now expected to call SetCredentials and supply credentials explicitly, fixing issues with the cli breaking if the current user cannot read the contents of /Library/Tailscale due to group membership restrictions. This matches how those clients source and supply credentials to the localAPI http client. Non-platform-specific code that has traditionally been in the client is moved to safesocket. /Libraray/Tailscaled/sameuserproof has its permissions changed to that it's readably only by users in the admin group. This restricts standalone CLI access for and direct use of localAPI to admins. Signed-off-by: Jonathan Nobels <jonathan@tailscale.com>
This commit is contained in:
parent
05ac21ebe4
commit
1bf4c6481a
@ -318,6 +318,13 @@ func (s *Server) blockWhileIdentityInUse(ctx context.Context, actor ipnauth.Acto
|
|||||||
// Unix-like platforms and specifies the ID of a local user
|
// Unix-like platforms and specifies the ID of a local user
|
||||||
// (in the os/user.User.Uid string form) who is allowed
|
// (in the os/user.User.Uid string form) who is allowed
|
||||||
// to operate tailscaled without being root or using sudo.
|
// to operate tailscaled without being root or using sudo.
|
||||||
|
//
|
||||||
|
// Sandboxed macos clients must directly supply, or be able to read,
|
||||||
|
// an explicit token. Permission is inferred by validating that
|
||||||
|
// token. Sandboxed macos clients also don't use ipnserver.actor at all
|
||||||
|
// (and prior to that, they didn't use ipnauth.ConnIdentity)
|
||||||
|
//
|
||||||
|
// See safesocket and safesocket_darwin.
|
||||||
func (a *actor) Permissions(operatorUID string) (read, write bool) {
|
func (a *actor) Permissions(operatorUID string) (read, write bool) {
|
||||||
switch envknob.GOOS() {
|
switch envknob.GOOS() {
|
||||||
case "windows":
|
case "windows":
|
||||||
|
@ -6,8 +6,11 @@ package safesocket
|
|||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"bytes"
|
"bytes"
|
||||||
|
crand "crypto/rand"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io/fs"
|
||||||
|
"log"
|
||||||
"net"
|
"net"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
@ -17,6 +20,7 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
"tailscale.com/version"
|
"tailscale.com/version"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -24,17 +28,245 @@ func init() {
|
|||||||
localTCPPortAndToken = localTCPPortAndTokenDarwin
|
localTCPPortAndToken = localTCPPortAndTokenDarwin
|
||||||
}
|
}
|
||||||
|
|
||||||
// localTCPPortAndTokenMacsys returns the localhost TCP port number and auth token
|
const sameUserProofTokenLength = 10
|
||||||
// from /Library/Tailscale.
|
|
||||||
|
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.
|
||||||
|
//
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("InitListenerDarwin: listener initialization failed")
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
port, err := localhostTCPPort()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("localhostTCPPort: listener initialization failed")
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
token, err := getToken()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("localhostTCPPort: getToken failed")
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if port == 0 || token == "" {
|
||||||
|
log.Printf("localhostTCPPort: Invalid token or port")
|
||||||
|
return nil, fmt.Errorf("invalid localhostTCPPort: returned 0")
|
||||||
|
}
|
||||||
|
|
||||||
|
ssd.sharedDir = sharedDir
|
||||||
|
ssd.token = token
|
||||||
|
ssd.port = port
|
||||||
|
|
||||||
|
// Write the port and token to a sameuserproof file
|
||||||
|
err = initSameUserProofToken(sharedDir, port, token)
|
||||||
|
if err != nil {
|
||||||
|
// Not fatal
|
||||||
|
log.Printf("initSameUserProofToken: failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ln, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var onceListener struct {
|
||||||
|
once sync.Once
|
||||||
|
ln *net.Listener
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("failed to remove portfile %s: %v", portFile, err)
|
||||||
|
}
|
||||||
|
symlinkErr := os.Symlink(fmt.Sprint(port), portFile)
|
||||||
|
if symlinkErr != nil {
|
||||||
|
log.Printf("failed to symlink portfile: %v", symlinkErr)
|
||||||
|
}
|
||||||
|
} 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:
|
// In that case the files are:
|
||||||
//
|
//
|
||||||
// /Library/Tailscale/ipnport => $port (symlink with localhost port number target)
|
// /Library/Tailscale/ipnport => $port (symlink with localhost port number target)
|
||||||
// /Library/Tailscale/sameuserproof-$port is a file with auth
|
// /Library/Tailscale/sameuserproof-$port is a file containing only the auth token as a hex string.
|
||||||
func localTCPPortAndTokenMacsys() (port int, token string, err error) {
|
func readMacsysSameUserProof() (port int, token string, err error) {
|
||||||
|
portStr, err := os.Readlink(filepath.Join(ssd.sharedDir, "ipnport"))
|
||||||
const dir = "/Library/Tailscale"
|
|
||||||
portStr, err := os.Readlink(filepath.Join(dir, "ipnport"))
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, "", err
|
return 0, "", err
|
||||||
}
|
}
|
||||||
@ -42,7 +274,7 @@ func localTCPPortAndTokenMacsys() (port int, token string, err error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, "", err
|
return 0, "", err
|
||||||
}
|
}
|
||||||
authb, err := os.ReadFile(filepath.Join(dir, "sameuserproof-"+portStr))
|
authb, err := os.ReadFile(filepath.Join(ssd.sharedDir, "sameuserproof-"+portStr))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, "", err
|
return 0, "", err
|
||||||
}
|
}
|
||||||
@ -51,69 +283,23 @@ func localTCPPortAndTokenMacsys() (port int, token string, err error) {
|
|||||||
return 0, "", errors.New("empty auth token in sameuserproof file")
|
return 0, "", errors.New("empty auth token in sameuserproof file")
|
||||||
}
|
}
|
||||||
|
|
||||||
// The above files exist forever after the first run of
|
if ssd.checkConn {
|
||||||
// /Applications/Tailscale.app, so check we can connect to avoid returning a
|
// Files may be stale and there is no guarantee that the sameuserproof
|
||||||
// port nothing is listening on. Connect to "127.0.0.1" rather than
|
// derived port is open and valid. Check it before returning it.
|
||||||
// "localhost" due to #7851.
|
conn, err := net.DialTimeout("tcp", "127.0.0.1:"+portStr, time.Second)
|
||||||
conn, err := net.DialTimeout("tcp", "127.0.0.1:"+portStr, time.Second)
|
if err != nil {
|
||||||
if err != nil {
|
return 0, "", err
|
||||||
return 0, "", err
|
}
|
||||||
|
conn.Close()
|
||||||
}
|
}
|
||||||
conn.Close()
|
|
||||||
|
|
||||||
return port, auth, nil
|
return port, auth, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var warnAboutRootOnce sync.Once
|
// readMacosSameUserProof searches for open sameuserproof files belonging
|
||||||
|
// to the current user and the IPNExtension (macOS App Store) process and returns a
|
||||||
func localTCPPortAndTokenDarwin() (port int, token string, err error) {
|
// port and token.
|
||||||
// There are two ways this binary can be run: as the Mac App Store sandboxed binary,
|
func readMacosSameUserProof() (port int, token string, err error) {
|
||||||
// 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 != "" {
|
|
||||||
// First see if we're running as the non-AppStore "macsys" variant.
|
|
||||||
if version.IsMacSys() {
|
|
||||||
if port, token, err := localTCPPortAndTokenMacsys(); err == nil {
|
|
||||||
return port, token, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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.
|
|
||||||
fis, err := os.ReadDir(dir)
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
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
|
|
||||||
// lsof to find the IPNExtension (the Mac App Store variant).
|
|
||||||
|
|
||||||
cmd := exec.Command("lsof",
|
cmd := exec.Command("lsof",
|
||||||
"-n", // numeric sockets; don't do DNS lookups, etc
|
"-n", // numeric sockets; don't do DNS lookups, etc
|
||||||
"-a", // logical AND remaining options
|
"-a", // logical AND remaining options
|
||||||
@ -122,39 +308,40 @@ func localTCPPortAndTokenDarwin() (port int, token string, err error) {
|
|||||||
"-F", // machine-readable output
|
"-F", // machine-readable output
|
||||||
)
|
)
|
||||||
out, err := cmd.Output()
|
out, err := cmd.Output()
|
||||||
if err != nil {
|
|
||||||
// Before returning an error, see if we're running the
|
if err == nil {
|
||||||
// macsys variant at the normal location.
|
bs := bufio.NewScanner(bytes.NewReader(out))
|
||||||
if port, token, err := localTCPPortAndTokenMacsys(); err == nil {
|
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
|
return port, token, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return 0, "", fmt.Errorf("failed to run '%s' looking for IPNExtension: %w", cmd, err)
|
|
||||||
}
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
// Before returning an error, see if we're running the
|
|
||||||
// macsys variant at the normal location.
|
|
||||||
if port, token, err := localTCPPortAndTokenMacsys(); err == nil {
|
|
||||||
return port, token, nil
|
|
||||||
}
|
}
|
||||||
return 0, "", ErrTokenNotFound
|
return 0, "", ErrTokenNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func portAndTokenFromSameUserProof() (port int, token string, err error) {
|
||||||
|
if port, token, err := readMacosSameUserProof(); err == nil {
|
||||||
|
return port, token, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if port, token, err := readMacsysSameUserProof(); err == nil {
|
||||||
|
return port, token, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0, "", err
|
||||||
|
}
|
||||||
|
149
safesocket/safesocket_darwin_test.go
Normal file
149
safesocket/safesocket_darwin_test.go
Normal file
@ -0,0 +1,149 @@
|
|||||||
|
// Copyright (c) Tailscale Inc & AUTHORS
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
package safesocket
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"tailscale.com/tstest"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestSetCredentials verifies that calling SetCredentials
|
||||||
|
// sets the port and token correctly and that LocalTCPPortAndToken
|
||||||
|
// returns the given values.
|
||||||
|
func TestSetCredentials(t *testing.T) {
|
||||||
|
wantPort := 123
|
||||||
|
wantToken := "token"
|
||||||
|
SetCredentials(wantToken, wantPort)
|
||||||
|
|
||||||
|
gotPort, gotToken, err := LocalTCPPortAndToken()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if gotPort != wantPort {
|
||||||
|
t.Errorf("got port %d, want %d", gotPort, wantPort)
|
||||||
|
}
|
||||||
|
|
||||||
|
if gotToken != wantToken {
|
||||||
|
t.Errorf("got token %s, want %s", gotToken, wantToken)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestInitListenerDarwin verifies that InitListenerDarwin
|
||||||
|
// returns a listener and a non-zero port and non-empty token.
|
||||||
|
func TestInitListenerDarwin(t *testing.T) {
|
||||||
|
temp := t.TempDir()
|
||||||
|
ln, err := InitListenerDarwin(temp)
|
||||||
|
if err != nil || ln == nil {
|
||||||
|
t.Fatalf("InitListenerDarwin failed: %v", err)
|
||||||
|
}
|
||||||
|
defer (*ln).Close()
|
||||||
|
|
||||||
|
port, token, err := LocalTCPPortAndToken()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("LocalTCPPortAndToken failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if port == 0 {
|
||||||
|
t.Errorf("expected non-zero port, got %d", port)
|
||||||
|
}
|
||||||
|
|
||||||
|
if token == "" {
|
||||||
|
t.Errorf("expected non-empty token, got empty string")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestTokenGeneration verifies token generation behavior
|
||||||
|
func TestTokenGeneration(t *testing.T) {
|
||||||
|
token, err := getToken()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("getToken: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify token length (hex string is 2x byte length)
|
||||||
|
wantLen := sameUserProofTokenLength * 2
|
||||||
|
if got := len(token); got != wantLen {
|
||||||
|
t.Errorf("token length = %d, want %d", got, wantLen)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify token persistence
|
||||||
|
subsequentToken, err := getToken()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("subsequent getToken: %v", err)
|
||||||
|
}
|
||||||
|
if subsequentToken != token {
|
||||||
|
t.Errorf("subsequent token = %q, want %q", subsequentToken, token)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestSameUserProofToken verifies that the sameuserproof file
|
||||||
|
// is created and read correctly for the macsys variant
|
||||||
|
func TestMacsysSameuserproof(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
|
||||||
|
tstest.Replace(t, &ssd.isMacSysExt, func() bool { return true })
|
||||||
|
tstest.Replace(t, &ssd.checkConn, false)
|
||||||
|
tstest.Replace(t, &ssd.sharedDir, dir)
|
||||||
|
|
||||||
|
const (
|
||||||
|
wantToken = "token"
|
||||||
|
wantPort = 123
|
||||||
|
)
|
||||||
|
|
||||||
|
if err := initSameUserProofToken(dir, wantPort, wantToken); err != nil {
|
||||||
|
t.Fatalf("initSameUserProofToken: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
gotPort, gotToken, err := readMacsysSameUserProof()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("readMacOSSameUserProof: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if gotPort != wantPort {
|
||||||
|
t.Errorf("got port = %d, want %d", gotPort, wantPort)
|
||||||
|
}
|
||||||
|
if wantToken != gotToken {
|
||||||
|
t.Errorf("got token = %s, want %s", wantToken, gotToken)
|
||||||
|
}
|
||||||
|
assertFileCount(t, dir, 1, "sameuserproof-")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestMacosSameuserproof verifies that the sameuserproof file
|
||||||
|
// is created correctly for the macos variant
|
||||||
|
func TestMacosSameuserproof(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
wantToken := "token"
|
||||||
|
wantPort := 123
|
||||||
|
|
||||||
|
initSameUserProofToken(dir, wantPort, wantToken)
|
||||||
|
|
||||||
|
// initSameUserProofToken should never leave duplicates
|
||||||
|
initSameUserProofToken(dir, wantPort, wantToken)
|
||||||
|
|
||||||
|
// we can't just call readMacosSameUserProof because it relies on lsof
|
||||||
|
// and makes some assumptions about the user. But we can make sure
|
||||||
|
// the file exists
|
||||||
|
assertFileCount(t, dir, 1, "sameuserproof-")
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertFileCount(t *testing.T, dir string, want int, prefix string) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
files, err := os.ReadDir(dir)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
count := 0
|
||||||
|
for _, file := range files {
|
||||||
|
if strings.HasPrefix(file.Name(), prefix) {
|
||||||
|
count += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if count != want {
|
||||||
|
t.Errorf("expected 1 file, got %d", count)
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user