mirror of
https://github.com/tailscale/tailscale.git
synced 2024-12-01 14:05:39 +00:00
5611f290eb
This partially revertsd6e9fb1df0
, which modified the permissions on the tailscaled Unix socket and thus required "sudo tailscale" even for "tailscale status". Instead, open the permissions back up (on Linux only) but have the server look at the peer creds and only permit read-only actions unless you're root. In the future we'll also have a group that can do mutable actions. On OpenBSD and FreeBSD, the permissions on the socket remain locked down to 0600 fromd6e9fb1df0
. Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
201 lines
6.6 KiB
Go
201 lines
6.6 KiB
Go
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
|
// Use of this source code is governed by a BSD-style
|
|
// license that can be found in the LICENSE file.
|
|
|
|
// +build !windows
|
|
|
|
package safesocket
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"fmt"
|
|
"io"
|
|
"io/ioutil"
|
|
"log"
|
|
"net"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
)
|
|
|
|
// TODO(apenwarr): handle magic cookie auth
|
|
func connect(path string, port uint16) (net.Conn, error) {
|
|
if runtime.GOOS == "darwin" && path == "" && port == 0 {
|
|
return connectMacOSAppSandbox()
|
|
}
|
|
pipe, err := net.Dial("unix", path)
|
|
if err != nil {
|
|
if runtime.GOOS == "darwin" {
|
|
extConn, err := connectMacOSAppSandbox()
|
|
if err != nil {
|
|
log.Printf("safesocket: failed to connect to Tailscale IPNExtension: %v", err)
|
|
} else {
|
|
return extConn, nil
|
|
}
|
|
}
|
|
return nil, err
|
|
}
|
|
return pipe, err
|
|
}
|
|
|
|
// TODO(apenwarr): handle magic cookie auth
|
|
func listen(path string, port uint16) (ln net.Listener, _ uint16, err error) {
|
|
// Unix sockets hang around in the filesystem even after nobody
|
|
// is listening on them. (Which is really unfortunate but long-
|
|
// entrenched semantics.) Try connecting first; if it works, then
|
|
// the socket is still live, so let's not replace it. If it doesn't
|
|
// work, then replace it.
|
|
//
|
|
// Note that there's a race condition between these two steps. A
|
|
// "proper" daemon usually uses a dance involving pidfiles to first
|
|
// ensure that no other instances of itself are running, but that's
|
|
// beyond the scope of our simple socket library.
|
|
c, err := net.Dial("unix", path)
|
|
if err == nil {
|
|
c.Close()
|
|
return nil, 0, fmt.Errorf("%v: address already in use", path)
|
|
}
|
|
_ = os.Remove(path)
|
|
os.MkdirAll(filepath.Dir(path), 0755) // best effort
|
|
pipe, err := net.Listen("unix", path)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
os.Chmod(path, socketPermissionsForOS())
|
|
return pipe, 0, err
|
|
}
|
|
|
|
// socketPermissionsForOS returns the permissions to use for the
|
|
// tailscaled.sock.
|
|
func socketPermissionsForOS() os.FileMode {
|
|
if runtime.GOOS == "linux" {
|
|
// On Linux, the ipn/ipnserver package looks at the Unix peer creds
|
|
// and only permits read-only actions from non-root users, so we want
|
|
// this opened up wider.
|
|
//
|
|
// TODO(bradfitz): unify this all one in place probably, moving some
|
|
// of ipnserver (which does much of the "safe" bits) here. Maybe
|
|
// instead of net.Listener, we should return a type that returns
|
|
// an identity in addition to a net.Conn? (returning a wrapped net.Conn
|
|
// would surprise downstream callers probably)
|
|
//
|
|
// TODO(bradfitz): if OpenBSD and FreeBSD do the equivalent peercreds
|
|
// stuff that's in ipn/ipnserver/conn_ucred.go, they should also
|
|
// return 0666 here.
|
|
return 0666
|
|
}
|
|
// Otherwise, root only.
|
|
return 0600
|
|
}
|
|
|
|
// connectMacOSAppSandbox connects to the Tailscale Network Extension,
|
|
// which is necessarily running within the macOS App Sandbox. Our
|
|
// little dance to connect a regular user binary to the sandboxed
|
|
// nework extension is:
|
|
//
|
|
// * the sandboxed IPNExtension picks a random localhost:0 TCP port
|
|
// to listen on
|
|
// * it also picks a random hex string that acts as an auth token
|
|
// * it then creates a file named "sameuserproof-$PORT-$TOKEN" and leaves
|
|
// that file descriptor open forever.
|
|
//
|
|
// Then, we do different things depending on whether the user is
|
|
// running cmd/tailscale that they built themselves (running as
|
|
// themselves, outside the App Sandbox), or whether the user is
|
|
// running the CLI via the GUI binary
|
|
// (e.g. /Applications/Tailscale.app/Contents/MacOS/Tailscale <args>),
|
|
// in which case we're running within the App Sandbox.
|
|
//
|
|
// If we're outside the App Sandbox:
|
|
//
|
|
// * then we come along here, running as the same UID, but outside
|
|
// of the sandbox, and look for it. We can run lsof on our own processes,
|
|
// but other users on the system can't.
|
|
// * we parse out the localhost port number and the auth token
|
|
// * we connect to TCP localhost:$PORT
|
|
// * we send $TOKEN + "\n"
|
|
// * server verifies $TOKEN, sends "#IPN\n" if okay.
|
|
// * server is now protocol switched
|
|
// * we return the net.Conn and the caller speaks the normal protocol
|
|
//
|
|
// If we're inside the App Sandbox, then TS_MACOS_CLI_SHARED_DIR has
|
|
// been set to our shared directory. We now have to find the most
|
|
// recent "sameuserproof" file (there should only be 1, but previous
|
|
// versions of the macOS app didn't clean them up).
|
|
func connectMacOSAppSandbox() (net.Conn, error) {
|
|
// Are we running the Tailscale.app GUI binary as a CLI, running within the App Sandbox?
|
|
if d := os.Getenv("TS_MACOS_CLI_SHARED_DIR"); d != "" {
|
|
fis, err := ioutil.ReadDir(d)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("reading TS_MACOS_CLI_SHARED_DIR: %w", err)
|
|
}
|
|
var best os.FileInfo
|
|
for _, fi := range fis {
|
|
if !strings.HasPrefix(fi.Name(), "sameuserproof-") || strings.Count(fi.Name(), "-") != 2 {
|
|
continue
|
|
}
|
|
if best == nil || fi.ModTime().After(best.ModTime()) {
|
|
best = fi
|
|
}
|
|
}
|
|
if best == nil {
|
|
return nil, fmt.Errorf("no sameuserproof token found in TS_MACOS_CLI_SHARED_DIR %q", d)
|
|
}
|
|
f := strings.SplitN(best.Name(), "-", 3)
|
|
portStr, token := f[1], f[2]
|
|
return connectMacTCP(portStr, token)
|
|
}
|
|
|
|
// Otherwise, assume we're running the cmd/tailscale binary from outside the
|
|
// App Sandbox.
|
|
|
|
out, err := exec.Command("lsof",
|
|
"-n", // numeric sockets; don't do DNS lookups, etc
|
|
"-a", // logical AND remaining options
|
|
fmt.Sprintf("-u%d", os.Getuid()), // process of same user only
|
|
"-c", "IPNExtension", // starting with IPNExtension
|
|
"-F", // machine-readable output
|
|
).Output()
|
|
if err != nil {
|
|
return nil, 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]
|
|
return connectMacTCP(portStr, token)
|
|
}
|
|
return nil, fmt.Errorf("failed to find Tailscale's IPNExtension process")
|
|
}
|
|
|
|
func connectMacTCP(portStr, token string) (net.Conn, error) {
|
|
c, err := net.Dial("tcp", "localhost:"+portStr)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error dialing IPNExtension: %w", err)
|
|
}
|
|
if _, err := io.WriteString(c, token+"\n"); err != nil {
|
|
return nil, fmt.Errorf("error writing auth token: %w", err)
|
|
}
|
|
buf := make([]byte, 5)
|
|
const authOK = "#IPN\n"
|
|
if _, err := io.ReadFull(c, buf); err != nil {
|
|
return nil, fmt.Errorf("error reading from IPNExtension post-auth: %w", err)
|
|
}
|
|
if string(buf) != authOK {
|
|
return nil, fmt.Errorf("invalid response reading from IPNExtension post-auth")
|
|
}
|
|
return c, nil
|
|
}
|