pax_global_header00006660000000000000000000000064143375253560014527gustar00rootroot0000000000000052 comment=11518ad3de40954fb119e554f8236e18eb81c611 golang-github-tailscale-tscert-0.0~git20220316.54bbcb9/000077500000000000000000000000001433752535600222315ustar00rootroot00000000000000golang-github-tailscale-tscert-0.0~git20220316.54bbcb9/LICENSE000066400000000000000000000027671433752535600232520ustar00rootroot00000000000000BSD 3-Clause License Copyright (c) 2020 Tailscale & AUTHORS. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. golang-github-tailscale-tscert-0.0~git20220316.54bbcb9/README.md000066400000000000000000000006431433752535600235130ustar00rootroot00000000000000# tscert This is a stripped down version of the `tailscale.com/client/tailscale` Go package but with minimal dependencies and supporting older versions of Go. It's meant for use by Caddy, so they don't need to depend on Go 1.17 yet. Also, it has the nice side effect of not polluting their `go.sum` file because `tailscale.com` is a somewhat large module. ## Docs See https://pkg.go.dev/github.com/tailscale/tscert golang-github-tailscale-tscert-0.0~git20220316.54bbcb9/go.mod000066400000000000000000000002211433752535600233320ustar00rootroot00000000000000module github.com/tailscale/tscert go 1.15 require ( github.com/mitchellh/go-ps v1.0.0 golang.org/x/sys v0.0.0-20220114195835-da31bd327af9 ) golang-github-tailscale-tscert-0.0~git20220316.54bbcb9/go.sum000066400000000000000000000005721433752535600233700ustar00rootroot00000000000000github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc= github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg= golang.org/x/sys v0.0.0-20220114195835-da31bd327af9 h1:XfKQ4OlFl8okEOr5UvAqFRVj8pY/4yfcXrddB8qAbU0= golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang-github-tailscale-tscert-0.0~git20220316.54bbcb9/internal/000077500000000000000000000000001433752535600240455ustar00rootroot00000000000000golang-github-tailscale-tscert-0.0~git20220316.54bbcb9/internal/paths/000077500000000000000000000000001433752535600251645ustar00rootroot00000000000000golang-github-tailscale-tscert-0.0~git20220316.54bbcb9/internal/paths/paths.go000066400000000000000000000032631433752535600266360ustar00rootroot00000000000000// 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. // Package paths returns platform and user-specific default paths to // Tailscale files and directories. package paths import ( "os" "path/filepath" "runtime" "sync/atomic" ) // AppSharedDir is a string set by the iOS or Android app on start // containing a directory we can read/write in. var AppSharedDir atomic.Value // DefaultTailscaledSocket returns the path to the tailscaled Unix socket // or the empty string if there's no reasonable default. func DefaultTailscaledSocket() string { if runtime.GOOS == "windows" { return "" } if runtime.GOOS == "darwin" { return "/var/run/tailscaled.socket" } if fi, err := os.Stat("/var/run"); err == nil && fi.IsDir() { return "/var/run/tailscale/tailscaled.sock" } return "tailscaled.sock" } var stateFileFunc func() string // DefaultTailscaledStateFile returns the default path to the // tailscaled state file, or the empty string if there's no reasonable // default value. func DefaultTailscaledStateFile() string { if f := stateFileFunc; f != nil { return f() } if runtime.GOOS == "windows" { return filepath.Join(os.Getenv("ProgramData"), "Tailscale", "server-state.conf") } return "" } // MkStateDir ensures that dirPath, the daemon's configurtaion directory // containing machine keys etc, both exists and has the correct permissions. // We want it to only be accessible to the user the daemon is running under. func MkStateDir(dirPath string) error { if err := os.MkdirAll(dirPath, 0700); err != nil { return err } return ensureStateDirPerms(dirPath) } golang-github-tailscale-tscert-0.0~git20220316.54bbcb9/internal/paths/paths_unix.go000066400000000000000000000033411433752535600276760ustar00rootroot00000000000000// 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. //go:build !windows && !js // +build !windows,!js package paths import ( "fmt" "os" "path/filepath" "runtime" "golang.org/x/sys/unix" ) func init() { stateFileFunc = stateFileUnix } func statePath() string { switch runtime.GOOS { case "linux": return "/var/lib/tailscale/tailscaled.state" case "freebsd", "openbsd": return "/var/db/tailscale/tailscaled.state" case "darwin": return "/Library/Tailscale/tailscaled.state" default: return "" } } func stateFileUnix() string { path := statePath() if path == "" { return "" } try := path for i := 0; i < 3; i++ { // check writability of the file, /var/lib/tailscale, and /var/lib err := unix.Access(try, unix.O_RDWR) if err == nil { return path } try = filepath.Dir(try) } if os.Getuid() == 0 { return "" } // For non-root users, fall back to $XDG_DATA_HOME/tailscale/*. return filepath.Join(xdgDataHome(), "tailscale", "tailscaled.state") } func xdgDataHome() string { if e := os.Getenv("XDG_DATA_HOME"); e != "" { return e } return filepath.Join(os.Getenv("HOME"), ".local/share") } func ensureStateDirPerms(dir string) error { if filepath.Base(dir) != "tailscale" { return nil } fi, err := os.Stat(dir) if err != nil { return err } if !fi.IsDir() { return fmt.Errorf("expected %q to be a directory; is %v", dir, fi.Mode()) } const perm = 0700 if fi.Mode().Perm() == perm { // Already correct. return nil } return os.Chmod(dir, perm) } // LegacyStateFilePath is not applicable to UNIX; it is just stubbed out. func LegacyStateFilePath() string { return "" } golang-github-tailscale-tscert-0.0~git20220316.54bbcb9/internal/paths/paths_windows.go000066400000000000000000000112431433752535600304050ustar00rootroot00000000000000// Copyright (c) 2021 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. package paths import ( "os" "path/filepath" "strings" "unsafe" "golang.org/x/sys/windows" ) func getTokenInfo(token windows.Token, infoClass uint32) ([]byte, error) { var desiredLen uint32 err := windows.GetTokenInformation(token, infoClass, nil, 0, &desiredLen) if err != nil && err != windows.ERROR_INSUFFICIENT_BUFFER { return nil, err } buf := make([]byte, desiredLen) actualLen := desiredLen err = windows.GetTokenInformation(token, infoClass, &buf[0], desiredLen, &actualLen) return buf, err } func getTokenUserInfo(token windows.Token) (*windows.Tokenuser, error) { buf, err := getTokenInfo(token, windows.TokenUser) if err != nil { return nil, err } return (*windows.Tokenuser)(unsafe.Pointer(&buf[0])), nil } func getTokenPrimaryGroupInfo(token windows.Token) (*windows.Tokenprimarygroup, error) { buf, err := getTokenInfo(token, windows.TokenPrimaryGroup) if err != nil { return nil, err } return (*windows.Tokenprimarygroup)(unsafe.Pointer(&buf[0])), nil } type userSids struct { User *windows.SID PrimaryGroup *windows.SID } func getCurrentUserSids() (*userSids, error) { token, err := windows.OpenCurrentProcessToken() if err != nil { return nil, err } defer token.Close() userInfo, err := getTokenUserInfo(token) if err != nil { return nil, err } primaryGroup, err := getTokenPrimaryGroupInfo(token) if err != nil { return nil, err } return &userSids{userInfo.User.Sid, primaryGroup.PrimaryGroup}, nil } // ensureStateDirPerms applies a restrictive ACL to the directory specified by dirPath. // It sets the following security attributes on the directory: // Owner: The user for the current process; // Primary Group: The primary group for the current process; // DACL: Full control to the current user and to the Administrators group. // (We include Administrators so that admin users may still access logs; // granting access exclusively to LocalSystem would require admins to use // special tools to access the Log directory) // Inheritance: The directory does not inherit the ACL from its parent. // However, any directories and/or files created within this // directory *do* inherit the ACL that we are setting. func ensureStateDirPerms(dirPath string) error { fi, err := os.Stat(dirPath) if err != nil { return err } if !fi.IsDir() { return os.ErrInvalid } if strings.ToLower(filepath.Base(dirPath)) != "tailscale" { return nil } // We need the info for our current user as SIDs sids, err := getCurrentUserSids() if err != nil { return err } // We also need the SID for the Administrators group so that admins may // easily access logs. adminGroupSid, err := windows.CreateWellKnownSid(windows.WinBuiltinAdministratorsSid) if err != nil { return err } // Munge the SIDs into the format required by EXPLICIT_ACCESS. userTrustee := windows.TRUSTEE{nil, windows.NO_MULTIPLE_TRUSTEE, windows.TRUSTEE_IS_SID, windows.TRUSTEE_IS_USER, windows.TrusteeValueFromSID(sids.User)} adminTrustee := windows.TRUSTEE{nil, windows.NO_MULTIPLE_TRUSTEE, windows.TRUSTEE_IS_SID, windows.TRUSTEE_IS_WELL_KNOWN_GROUP, windows.TrusteeValueFromSID(adminGroupSid)} // We declare our access rights via this array of EXPLICIT_ACCESS structures. // We set full access to our user and to Administrators. // We configure the DACL such that any files or directories created within // dirPath will also inherit this DACL. explicitAccess := []windows.EXPLICIT_ACCESS{ { windows.GENERIC_ALL, windows.SET_ACCESS, windows.SUB_CONTAINERS_AND_OBJECTS_INHERIT, userTrustee, }, { windows.GENERIC_ALL, windows.SET_ACCESS, windows.SUB_CONTAINERS_AND_OBJECTS_INHERIT, adminTrustee, }, } dacl, err := windows.ACLFromEntries(explicitAccess, nil) if err != nil { return err } // We now reset the file's owner, primary group, and DACL. // We also must pass PROTECTED_DACL_SECURITY_INFORMATION so that our new ACL // does not inherit any ACL entries from the parent directory. const flags = windows.OWNER_SECURITY_INFORMATION | windows.GROUP_SECURITY_INFORMATION | windows.DACL_SECURITY_INFORMATION | windows.PROTECTED_DACL_SECURITY_INFORMATION return windows.SetNamedSecurityInfo(dirPath, windows.SE_FILE_OBJECT, flags, sids.User, sids.PrimaryGroup, dacl, nil) } // LegacyStateFilePath returns the legacy path to the state file when it was stored under the // current user's %LocalAppData%. func LegacyStateFilePath() string { return filepath.Join(os.Getenv("LocalAppData"), "Tailscale", "server-state.conf") } golang-github-tailscale-tscert-0.0~git20220316.54bbcb9/internal/safesocket/000077500000000000000000000000001433752535600261745ustar00rootroot00000000000000golang-github-tailscale-tscert-0.0~git20220316.54bbcb9/internal/safesocket/basic_test.go000066400000000000000000000026411433752535600306460ustar00rootroot00000000000000// 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. package safesocket import ( "fmt" "path/filepath" "testing" ) func TestBasics(t *testing.T) { // Make the socket in a temp dir rather than the cwd // so that the test can be run from a mounted filesystem (#2367). dir := t.TempDir() sock := filepath.Join(dir, "test") l, port, err := Listen(sock, 0) if err != nil { t.Fatal(err) } errs := make(chan error, 2) go func() { s, err := l.Accept() if err != nil { errs <- err return } l.Close() s.Write([]byte("hello")) b := make([]byte, 1024) n, err := s.Read(b) if err != nil { errs <- err return } t.Logf("server read %d bytes.", n) if string(b[:n]) != "world" { errs <- fmt.Errorf("got %#v, expected %#v\n", string(b[:n]), "world") return } s.Close() errs <- nil }() go func() { s := DefaultConnectionStrategy(sock) s.UsePort(port) c, err := Connect(s) if err != nil { errs <- err return } c.Write([]byte("world")) b := make([]byte, 1024) n, err := c.Read(b) if err != nil { errs <- err return } if string(b[:n]) != "hello" { errs <- fmt.Errorf("got %#v, expected %#v\n", string(b[:n]), "hello") } c.Close() errs <- nil }() for i := 0; i < 2; i++ { if err := <-errs; err != nil { t.Fatal(err) } } } golang-github-tailscale-tscert-0.0~git20220316.54bbcb9/internal/safesocket/pipe_windows.go000066400000000000000000000025511433752535600312350ustar00rootroot00000000000000// 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. package safesocket import ( "context" "fmt" "net" "syscall" ) func connect(s *ConnectionStrategy) (net.Conn, error) { pipe, err := net.Dial("tcp", fmt.Sprintf("127.0.0.1:%d", s.port)) if err != nil { return nil, err } return pipe, err } func setFlags(network, address string, c syscall.RawConn) error { return c.Control(func(fd uintptr) { syscall.SetsockoptInt(syscall.Handle(fd), syscall.SOL_SOCKET, syscall.SO_REUSEADDR, 1) }) } // TODO(apenwarr): use named pipes instead of sockets? // I tried to use winio.ListenPipe() here, but that code is a disaster, // built on top of an API that's a disaster. So for now we'll hack it by // just always using a TCP session on a fixed port on localhost. As a // result, on Windows we ignore the vendor and name strings. // NOTE(bradfitz): Jason did a new pipe package: https://go-review.googlesource.com/c/sys/+/299009 func listen(path string, port uint16) (_ net.Listener, gotPort uint16, _ error) { lc := net.ListenConfig{ Control: setFlags, } pipe, err := lc.Listen(context.Background(), "tcp", fmt.Sprintf("127.0.0.1:%d", port)) if err != nil { return nil, 0, err } return pipe, uint16(pipe.Addr().(*net.TCPAddr).Port), err } golang-github-tailscale-tscert-0.0~git20220316.54bbcb9/internal/safesocket/safesocket.go000066400000000000000000000135571433752535600306650ustar00rootroot00000000000000// 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. // Package safesocket creates either a Unix socket, if possible, or // otherwise a localhost TCP connection. package safesocket import ( "errors" "net" "runtime" "time" ) // WindowsLocalPort is the default localhost TCP port // used by safesocket on Windows. const WindowsLocalPort = 41112 type closeable interface { CloseRead() error CloseWrite() error } // ConnCloseRead calls c's CloseRead method. c is expected to be // either a UnixConn or TCPConn as returned from this package. func ConnCloseRead(c net.Conn) error { return c.(closeable).CloseRead() } // ConnCloseWrite calls c's CloseWrite method. c is expected to be // either a UnixConn or TCPConn as returned from this package. func ConnCloseWrite(c net.Conn) error { return c.(closeable).CloseWrite() } var processStartTime = time.Now() var tailscaledProcExists = func() bool { return false } // set by safesocket_ps.go // tailscaledStillStarting reports whether tailscaled is probably // still starting up. That is, it reports whether the caller should // keep retrying to connect. func tailscaledStillStarting() bool { d := time.Since(processStartTime) if d < 2*time.Second { // Without even checking the process table, assume // that for the first two seconds that tailscaled is // probably still starting. That is, assume they're // running "tailscaled & tailscale up ...." and make // the tailscale client block for a bit for tailscaled // to start accepting on the socket. return true } if d > 5*time.Second { return false } return tailscaledProcExists() } // A ConnectionStrategy is a plan for how to connect to tailscaled or equivalent (e.g. IPNExtension on macOS). type ConnectionStrategy struct { // For now, a ConnectionStrategy is just a unix socket path, a TCP port, // and a flag indicating whether to try fallback connections options. path string port uint16 fallback bool // Longer term, a ConnectionStrategy should be an ordered list of things to attempt, // with just the information required to connection for each. // // We have at least these cases to consider (see issue 3530): // // tailscale sandbox | tailscaled sandbox | OS | connection // ------------------|--------------------|---------|----------- // no | no | unix | unix socket // no | no | Windows | TCP/port // no | no | wasm | memconn // no | Network Extension | macOS | TCP/port/token, port/token from lsof // no | System Extension | macOS | TCP/port/token, port/token from lsof // yes | Network Extension | macOS | TCP/port/token, port/token from readdir // yes | System Extension | macOS | TCP/port/token, port/token from readdir // // Note e.g. that port is only relevant as an input to Connect on Windows, // that path is not relevant to Windows, and that neither matters to wasm. } // DefaultConnectionStrategy returns a default connection strategy. // The default strategy is to attempt to connect in as many ways as possible. // It uses path as the unix socket path, when applicable, // and defaults to WindowsLocalPort for the TCP port when applicable. // It falls back to auto-discovery across sandbox boundaries on macOS. // TODO: maybe take no arguments, since path is irrelevant on Windows? Discussion in PR 3499. func DefaultConnectionStrategy(path string) *ConnectionStrategy { return &ConnectionStrategy{path: path, port: WindowsLocalPort, fallback: true} } // UsePort modifies s to use port for the TCP port when applicable. // UsePort is only applicable on Windows, and only then // when not using the default for Windows. func (s *ConnectionStrategy) UsePort(port uint16) { s.port = port } // UseFallback modifies s to set whether it should fall back // to connecting to the macOS GUI's tailscaled // if the Unix socket path wasn't reachable. func (s *ConnectionStrategy) UseFallback(b bool) { s.fallback = b } // ExactPath returns a connection strategy that only attempts to connect via path. func ExactPath(path string) *ConnectionStrategy { return &ConnectionStrategy{path: path, fallback: false} } // Connect connects to tailscaled using s func Connect(s *ConnectionStrategy) (net.Conn, error) { for { c, err := connect(s) if err != nil && tailscaledStillStarting() { time.Sleep(250 * time.Millisecond) continue } return c, err } } // Listen returns a listener either on Unix socket path (on Unix), or // the localhost port (on Windows). // If port is 0, the returned gotPort says which port was selected on Windows. func Listen(path string, port uint16) (_ net.Listener, gotPort uint16, _ error) { return listen(path, port) } var ( ErrTokenNotFound = errors.New("no token found") ErrNoTokenOnOS = errors.New("no token on " + runtime.GOOS) ) var localTCPPortAndToken func() (port int, token string, err error) // LocalTCPPortAndToken returns the port number and auth token to connect to // the local Tailscale daemon. It's currently only applicable on macOS // when tailscaled is being run in the Mac Sandbox from the App Store version // of Tailscale. func LocalTCPPortAndToken() (port int, token string, err error) { if localTCPPortAndToken == nil { return 0, "", ErrNoTokenOnOS } return localTCPPortAndToken() } // PlatformUsesPeerCreds reports whether the current platform uses peer credentials // to authenticate connections. func PlatformUsesPeerCreds() bool { return GOOSUsesPeerCreds(runtime.GOOS) } // GOOSUsesPeerCreds is like PlatformUsesPeerCreds but takes a // runtime.GOOS value instead of using the current one. func GOOSUsesPeerCreds(goos string) bool { switch goos { case "linux", "darwin", "freebsd": return true } return false } golang-github-tailscale-tscert-0.0~git20220316.54bbcb9/internal/safesocket/safesocket_darwin.go000066400000000000000000000101771433752535600322240ustar00rootroot00000000000000// Copyright (c) 2021 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. package safesocket import ( "bufio" "bytes" "errors" "fmt" "io/ioutil" "os" "os/exec" "path/filepath" "strconv" "strings" ) func init() { localTCPPortAndToken = localTCPPortAndTokenDarwin } // localTCPPortAndTokenMacsys returns the localhost TCP port number and auth token // from /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 with auth func localTCPPortAndTokenMacsys() (port int, token string, err error) { const dir = "/Library/Tailscale" 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") } return port, auth, nil } func localTCPPortAndTokenDarwin() (port int, token string, err error) { // 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 != "" { // First see if we're running as the non-AppStore "macsys" variant. if strings.Contains(os.Getenv("HOME"), "/Containers/io.tailscale.ipn.macsys/") { 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 := ioutil.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 } } } } 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", "-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 ) out, err := cmd.Output() if err != 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, "", 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 } golang-github-tailscale-tscert-0.0~git20220316.54bbcb9/internal/safesocket/safesocket_ps.go000066400000000000000000000014521433752535600313560ustar00rootroot00000000000000// Copyright (c) 2021 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. //go:build linux || windows || darwin || freebsd // +build linux windows darwin freebsd package safesocket import ( "strings" ps "github.com/mitchellh/go-ps" ) func init() { tailscaledProcExists = func() bool { procs, err := ps.Processes() if err != nil { return false } for _, proc := range procs { name := proc.Executable() const tailscaled = "tailscaled" if len(name) < len(tailscaled) { continue } // Do case insensitive comparison for Windows, // notably, and ignore any ".exe" suffix. if strings.EqualFold(name[:len(tailscaled)], tailscaled) { return true } } return false } } golang-github-tailscale-tscert-0.0~git20220316.54bbcb9/internal/safesocket/safesocket_test.go000066400000000000000000000006421433752535600317130ustar00rootroot00000000000000// Copyright (c) 2021 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. package safesocket import "testing" func TestLocalTCPPortAndToken(t *testing.T) { // Just test that it compiles for now (is available on all platforms). port, token, err := LocalTCPPortAndToken() t.Logf("got %v, %s, %v", port, token, err) } golang-github-tailscale-tscert-0.0~git20220316.54bbcb9/internal/safesocket/unixsocket.go000066400000000000000000000152521433752535600307240ustar00rootroot00000000000000// 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. //go:build !windows && !js // +build !windows,!js package safesocket import ( "errors" "fmt" "io" "io/ioutil" "log" "net" "os" "os/exec" "path/filepath" "runtime" "strconv" "strings" ) // TODO(apenwarr): handle magic cookie auth func connect(s *ConnectionStrategy) (net.Conn, error) { if runtime.GOOS == "js" { return nil, errors.New("safesocket.Connect not yet implemented on js/wasm") } if runtime.GOOS == "darwin" && s.fallback && s.path == "" && s.port == 0 { return connectMacOSAppSandbox() } pipe, err := net.Dial("unix", s.path) if err != nil { if runtime.GOOS == "darwin" && s.fallback { extConn, extErr := connectMacOSAppSandbox() if extErr != nil { return nil, fmt.Errorf("safesocket: failed to connect to %v: %v; failed to connect to Tailscale IPNExtension: %v", s.path, err, extErr) } return extConn, nil } return nil, err } return pipe, nil } // 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() if tailscaledRunningUnderLaunchd() { return nil, 0, fmt.Errorf("%v: address already in use; tailscaled already running under launchd (to stop, run: $ sudo launchctl stop com.tailscale.tailscaled)", path) } return nil, 0, fmt.Errorf("%v: address already in use", path) } _ = os.Remove(path) perm := socketPermissionsForOS() sockDir := filepath.Dir(path) if _, err := os.Stat(sockDir); os.IsNotExist(err) { os.MkdirAll(sockDir, 0755) // best effort // If we're on a platform where we want the socket // world-readable, open up the permissions on the // just-created directory too, in case a umask ate // it. This primarily affects running tailscaled by // hand as root in a shell, as there is no umask when // running under systemd. if perm == 0666 { if fi, err := os.Stat(sockDir); err == nil && fi.Mode()&0077 == 0 { if err := os.Chmod(sockDir, 0755); err != nil { log.Print(err) } } } } pipe, err := net.Listen("unix", path) if err != nil { return nil, 0, err } os.Chmod(path, perm) return pipe, 0, err } func tailscaledRunningUnderLaunchd() bool { if runtime.GOOS != "darwin" { return false } plist, err := exec.Command("launchctl", "list", "com.tailscale.tailscaled").Output() _ = plist // parse it? https://github.com/DHowett/go-plist if we need something. running := err == nil return running } // socketPermissionsForOS returns the permissions to use for the // tailscaled.sock. func socketPermissionsForOS() os.FileMode { if PlatformUsesPeerCreds() { 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 // network 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 ), // 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] port, err := strconv.Atoi(portStr) if err != nil { return nil, fmt.Errorf("invalid port %q", portStr) } return connectMacTCP(port, token) } // Otherwise, assume we're running the cmd/tailscale binary from outside the // App Sandbox. port, token, err := LocalTCPPortAndToken() if err != nil { return nil, err } return connectMacTCP(port, token) } func connectMacTCP(port int, token string) (net.Conn, error) { c, err := net.Dial("tcp", "localhost:"+strconv.Itoa(port)) 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 } golang-github-tailscale-tscert-0.0~git20220316.54bbcb9/tscert.go000066400000000000000000000201711433752535600240650ustar00rootroot00000000000000// Copyright (c) 2022 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. // Package tscert fetches HTTPS certs from the local machine's // Tailscale daemon (tailscaled). package tscert import ( "bytes" "context" "crypto/tls" "encoding/json" "errors" "fmt" "io" "io/ioutil" "net" "net/http" "strconv" "strings" "sync" "time" "github.com/tailscale/tscert/internal/paths" "github.com/tailscale/tscert/internal/safesocket" ) var ( // TailscaledSocket is the tailscaled Unix socket. It's used by the TailscaledDialer. TailscaledSocket = paths.DefaultTailscaledSocket() // TailscaledSocketSetExplicitly reports whether the user explicitly set TailscaledSocket. TailscaledSocketSetExplicitly bool // TailscaledDialer is the DialContext func that connects to the local machine's // tailscaled or equivalent. TailscaledDialer = defaultDialer ) func defaultDialer(ctx context.Context, network, addr string) (net.Conn, error) { if addr != "local-tailscaled.sock:80" { return nil, fmt.Errorf("unexpected URL address %q", addr) } // TODO: make this part of a safesocket.ConnectionStrategy if !TailscaledSocketSetExplicitly { // On macOS, when dialing from non-sandboxed program to sandboxed GUI running // a TCP server on a random port, find the random port. For HTTP connections, // we don't send the token. It gets added in an HTTP Basic-Auth header. if port, _, err := safesocket.LocalTCPPortAndToken(); err == nil { var d net.Dialer return d.DialContext(ctx, "tcp", "localhost:"+strconv.Itoa(port)) } } s := safesocket.DefaultConnectionStrategy(TailscaledSocket) // The user provided a non-default tailscaled socket address. // Connect only to exactly what they provided. s.UseFallback(false) return safesocket.Connect(s) } var ( // tsClient does HTTP requests to the local Tailscale daemon. // We lazily initialize the client in case the caller wants to // override TailscaledDialer. tsClient *http.Client tsClientOnce sync.Once ) // DoLocalRequest makes an HTTP request to the local machine's Tailscale daemon. // // URLs are of the form http://local-tailscaled.sock/localapi/v0/whois?ip=1.2.3.4. // // The hostname must be "local-tailscaled.sock", even though it // doesn't actually do any DNS lookup. The actual means of connecting to and // authenticating to the local Tailscale daemon vary by platform. // // DoLocalRequest may mutate the request to add Authorization headers. func DoLocalRequest(req *http.Request) (*http.Response, error) { tsClientOnce.Do(func() { tsClient = &http.Client{ Transport: &http.Transport{ DialContext: TailscaledDialer, }, } }) if _, token, err := safesocket.LocalTCPPortAndToken(); err == nil { req.SetBasicAuth("", token) } return tsClient.Do(req) } func doLocalRequestNiceError(req *http.Request) (*http.Response, error) { res, err := DoLocalRequest(req) if err == nil { if res.StatusCode == 403 { all, _ := ioutil.ReadAll(res.Body) return nil, &AccessDeniedError{errors.New(errorMessageFromBody(all))} } return res, nil } return nil, err } type errorJSON struct { Error string } // AccessDeniedError is an error due to permissions. type AccessDeniedError struct { err error } func (e *AccessDeniedError) Error() string { return fmt.Sprintf("Access denied: %v", e.err) } func (e *AccessDeniedError) Unwrap() error { return e.err } // IsAccessDeniedError reports whether err is or wraps an AccessDeniedError. func IsAccessDeniedError(err error) bool { var ae *AccessDeniedError return errors.As(err, &ae) } // bestError returns either err, or if body contains a valid JSON // object of type errorJSON, its non-empty error body. func bestError(err error, body []byte) error { var j errorJSON if err := json.Unmarshal(body, &j); err == nil && j.Error != "" { return errors.New(j.Error) } return err } func errorMessageFromBody(body []byte) string { var j errorJSON if err := json.Unmarshal(body, &j); err == nil && j.Error != "" { return j.Error } return strings.TrimSpace(string(body)) } func send(ctx context.Context, method, path string, wantStatus int, body io.Reader) ([]byte, error) { req, err := http.NewRequestWithContext(ctx, method, "http://local-tailscaled.sock"+path, body) if err != nil { return nil, err } res, err := doLocalRequestNiceError(req) if err != nil { return nil, err } defer res.Body.Close() slurp, err := ioutil.ReadAll(res.Body) if err != nil { return nil, err } if res.StatusCode != wantStatus { return nil, bestError(err, slurp) } return slurp, nil } func get200(ctx context.Context, path string) ([]byte, error) { return send(ctx, "GET", path, 200, nil) } // Status is a stripped down version of tailscale.com/ipn/ipnstate.Status // for the tscert package. type Status struct { // Version is the daemon's long version (see version.Long). Version string // BackendState is an ipn.State string value: // "NoState", "NeedsLogin", "NeedsMachineAuth", "Stopped", // "Starting", "Running". BackendState string // Health contains health check problems. // Empty means everything is good. (or at least that no known // problems are detected) Health []string // TailscaleIPs are the Tailscale IP(s) assigned to this node TailscaleIPs []string // MagicDNSSuffix is the network's MagicDNS suffix for nodes // in the network such as "userfoo.tailscale.net". // There are no surrounding dots. // MagicDNSSuffix should be populated regardless of whether a domain // has MagicDNS enabled. MagicDNSSuffix string // CertDomains are the set of DNS names for which the control // plane server will assist with provisioning TLS // certificates. See SetDNSRequest for dns-01 ACME challenges // for e.g. LetsEncrypt. These names are FQDNs without // trailing periods, and without any "_acme-challenge." prefix. CertDomains []string } // GetStatus returns a stripped down status from tailscaled. For a full // version, use tailscale.com/client/tailscale.Status. func GetStatus(ctx context.Context) (*Status, error) { body, err := get200(ctx, "/localapi/v0/status") if err != nil { return nil, err } st := new(Status) if err := json.Unmarshal(body, st); err != nil { return nil, err } return st, nil } // CertPair returns a cert and private key for the provided DNS domain. // // It returns a cached certificate from disk if it's still valid. func CertPair(ctx context.Context, domain string) (certPEM, keyPEM []byte, err error) { res, err := send(ctx, "GET", "/localapi/v0/cert/"+domain+"?type=pair", 200, nil) if err != nil { return nil, nil, err } // with ?type=pair, the response PEM is first the one private // key PEM block, then the cert PEM blocks. i := bytes.Index(res, []byte("--\n--")) if i == -1 { return nil, nil, fmt.Errorf("unexpected output: no delimiter") } i += len("--\n") keyPEM, certPEM = res[:i], res[i:] if bytes.Contains(certPEM, []byte(" PRIVATE KEY-----")) { return nil, nil, fmt.Errorf("unexpected output: key in cert") } return certPEM, keyPEM, nil } // GetCertificate fetches a TLS certificate for the TLS ClientHello in hi. // // It returns a cached certificate from disk if it's still valid. // // It's the right signature to use as the value of // tls.Config.GetCertificate. func GetCertificate(hi *tls.ClientHelloInfo) (*tls.Certificate, error) { if hi == nil || hi.ServerName == "" { return nil, errors.New("no SNI ServerName") } ctx, cancel := context.WithTimeout(context.Background(), time.Minute) defer cancel() name := hi.ServerName if !strings.Contains(name, ".") { if v, ok := ExpandSNIName(ctx, name); ok { name = v } } certPEM, keyPEM, err := CertPair(ctx, name) if err != nil { return nil, err } cert, err := tls.X509KeyPair(certPEM, keyPEM) if err != nil { return nil, err } return &cert, nil } // ExpandSNIName expands bare label name into the the most likely actual TLS cert name. func ExpandSNIName(ctx context.Context, name string) (fqdn string, ok bool) { st, err := GetStatus(ctx) if err != nil { return "", false } for _, d := range st.CertDomains { if len(d) > len(name)+1 && strings.HasPrefix(d, name) && d[len(name)] == '.' { return d, true } } return "", false }