2020-02-05 22:16:58 +00:00
|
|
|
// 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.
|
|
|
|
|
2021-11-07 20:11:50 +00:00
|
|
|
//go:build !windows && !js
|
2020-02-05 22:16:58 +00:00
|
|
|
|
|
|
|
package safesocket
|
|
|
|
|
|
|
|
import (
|
2021-10-27 21:53:28 +00:00
|
|
|
"errors"
|
2020-02-05 22:16:58 +00:00
|
|
|
"fmt"
|
2020-03-30 05:04:20 +00:00
|
|
|
"io"
|
|
|
|
"log"
|
2020-02-05 22:16:58 +00:00
|
|
|
"net"
|
|
|
|
"os"
|
2021-02-15 16:40:52 +00:00
|
|
|
"os/exec"
|
2020-03-03 19:47:21 +00:00
|
|
|
"path/filepath"
|
2020-03-30 05:04:20 +00:00
|
|
|
"runtime"
|
2021-01-29 22:32:56 +00:00
|
|
|
"strconv"
|
2020-02-05 22:16:58 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
// TODO(apenwarr): handle magic cookie auth
|
safesocket: add ConnectionStrategy, provide control over fallbacks
fee2d9fad added support for cmd/tailscale to connect to IPNExtension.
It came in two parts: If no socket was provided, dial IPNExtension first,
and also, if dialing the socket failed, fall back to IPNExtension.
The second half of that support caused the integration tests to fail
when run on a machine that was also running IPNExtension.
The integration tests want to wait until the tailscaled instances
that they spun up are listening. They do that by dialing the new
instance. But when that dial failed, it was falling back to IPNExtension,
so it appeared (incorrectly) that tailscaled was running.
Hilarity predictably ensued.
If a user (or a test) explicitly provides a socket to dial,
it is a reasonable assumption that they have a specific tailscaled
in mind and don't want to fall back to IPNExtension.
It is certainly true of the integration tests.
Instead of adding a bool to Connect, split out the notion of a
connection strategy. For now, the implementation remains the same,
but with the details hidden a bit. Later, we can improve that.
Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-12-08 21:55:55 +00:00
|
|
|
func connect(s *ConnectionStrategy) (net.Conn, error) {
|
2021-10-27 21:53:28 +00:00
|
|
|
if runtime.GOOS == "js" {
|
|
|
|
return nil, errors.New("safesocket.Connect not yet implemented on js/wasm")
|
|
|
|
}
|
safesocket: add ConnectionStrategy, provide control over fallbacks
fee2d9fad added support for cmd/tailscale to connect to IPNExtension.
It came in two parts: If no socket was provided, dial IPNExtension first,
and also, if dialing the socket failed, fall back to IPNExtension.
The second half of that support caused the integration tests to fail
when run on a machine that was also running IPNExtension.
The integration tests want to wait until the tailscaled instances
that they spun up are listening. They do that by dialing the new
instance. But when that dial failed, it was falling back to IPNExtension,
so it appeared (incorrectly) that tailscaled was running.
Hilarity predictably ensued.
If a user (or a test) explicitly provides a socket to dial,
it is a reasonable assumption that they have a specific tailscaled
in mind and don't want to fall back to IPNExtension.
It is certainly true of the integration tests.
Instead of adding a bool to Connect, split out the notion of a
connection strategy. For now, the implementation remains the same,
but with the details hidden a bit. Later, we can improve that.
Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-12-08 21:55:55 +00:00
|
|
|
if runtime.GOOS == "darwin" && s.fallback && s.path == "" && s.port == 0 {
|
2020-03-30 05:04:20 +00:00
|
|
|
return connectMacOSAppSandbox()
|
|
|
|
}
|
safesocket: add ConnectionStrategy, provide control over fallbacks
fee2d9fad added support for cmd/tailscale to connect to IPNExtension.
It came in two parts: If no socket was provided, dial IPNExtension first,
and also, if dialing the socket failed, fall back to IPNExtension.
The second half of that support caused the integration tests to fail
when run on a machine that was also running IPNExtension.
The integration tests want to wait until the tailscaled instances
that they spun up are listening. They do that by dialing the new
instance. But when that dial failed, it was falling back to IPNExtension,
so it appeared (incorrectly) that tailscaled was running.
Hilarity predictably ensued.
If a user (or a test) explicitly provides a socket to dial,
it is a reasonable assumption that they have a specific tailscaled
in mind and don't want to fall back to IPNExtension.
It is certainly true of the integration tests.
Instead of adding a bool to Connect, split out the notion of a
connection strategy. For now, the implementation remains the same,
but with the details hidden a bit. Later, we can improve that.
Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-12-08 21:55:55 +00:00
|
|
|
pipe, err := net.Dial("unix", s.path)
|
2020-02-05 22:16:58 +00:00
|
|
|
if err != nil {
|
safesocket: add ConnectionStrategy, provide control over fallbacks
fee2d9fad added support for cmd/tailscale to connect to IPNExtension.
It came in two parts: If no socket was provided, dial IPNExtension first,
and also, if dialing the socket failed, fall back to IPNExtension.
The second half of that support caused the integration tests to fail
when run on a machine that was also running IPNExtension.
The integration tests want to wait until the tailscaled instances
that they spun up are listening. They do that by dialing the new
instance. But when that dial failed, it was falling back to IPNExtension,
so it appeared (incorrectly) that tailscaled was running.
Hilarity predictably ensued.
If a user (or a test) explicitly provides a socket to dial,
it is a reasonable assumption that they have a specific tailscaled
in mind and don't want to fall back to IPNExtension.
It is certainly true of the integration tests.
Instead of adding a bool to Connect, split out the notion of a
connection strategy. For now, the implementation remains the same,
but with the details hidden a bit. Later, we can improve that.
Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-12-08 21:55:55 +00:00
|
|
|
if runtime.GOOS == "darwin" && s.fallback {
|
2021-07-20 19:18:40 +00:00
|
|
|
extConn, extErr := connectMacOSAppSandbox()
|
|
|
|
if extErr != nil {
|
safesocket: add ConnectionStrategy, provide control over fallbacks
fee2d9fad added support for cmd/tailscale to connect to IPNExtension.
It came in two parts: If no socket was provided, dial IPNExtension first,
and also, if dialing the socket failed, fall back to IPNExtension.
The second half of that support caused the integration tests to fail
when run on a machine that was also running IPNExtension.
The integration tests want to wait until the tailscaled instances
that they spun up are listening. They do that by dialing the new
instance. But when that dial failed, it was falling back to IPNExtension,
so it appeared (incorrectly) that tailscaled was running.
Hilarity predictably ensued.
If a user (or a test) explicitly provides a socket to dial,
it is a reasonable assumption that they have a specific tailscaled
in mind and don't want to fall back to IPNExtension.
It is certainly true of the integration tests.
Instead of adding a bool to Connect, split out the notion of a
connection strategy. For now, the implementation remains the same,
but with the details hidden a bit. Later, we can improve that.
Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-12-08 21:55:55 +00:00
|
|
|
return nil, fmt.Errorf("safesocket: failed to connect to %v: %v; failed to connect to Tailscale IPNExtension: %v", s.path, err, extErr)
|
2020-03-30 05:04:20 +00:00
|
|
|
}
|
2021-07-20 19:18:40 +00:00
|
|
|
return extConn, nil
|
2020-03-30 05:04:20 +00:00
|
|
|
}
|
2020-02-05 22:16:58 +00:00
|
|
|
return nil, err
|
|
|
|
}
|
2021-07-20 19:18:40 +00:00
|
|
|
return pipe, nil
|
2020-02-05 22:16:58 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// TODO(apenwarr): handle magic cookie auth
|
2020-02-25 16:46:26 +00:00
|
|
|
func listen(path string, port uint16) (ln net.Listener, _ uint16, err error) {
|
2020-02-05 22:16:58 +00:00
|
|
|
// 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.
|
2020-02-18 20:33:28 +00:00
|
|
|
c, err := net.Dial("unix", path)
|
2020-02-05 22:16:58 +00:00
|
|
|
if err == nil {
|
|
|
|
c.Close()
|
2021-02-15 16:40:52 +00:00
|
|
|
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)
|
|
|
|
}
|
2020-02-18 20:33:28 +00:00
|
|
|
return nil, 0, fmt.Errorf("%v: address already in use", path)
|
2020-02-05 22:16:58 +00:00
|
|
|
}
|
2020-02-18 20:33:28 +00:00
|
|
|
_ = os.Remove(path)
|
2021-01-21 19:29:38 +00:00
|
|
|
|
|
|
|
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)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2020-02-18 20:33:28 +00:00
|
|
|
pipe, err := net.Listen("unix", path)
|
2020-02-05 22:16:58 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, 0, err
|
|
|
|
}
|
2021-01-21 19:29:38 +00:00
|
|
|
os.Chmod(path, perm)
|
2020-02-05 22:16:58 +00:00
|
|
|
return pipe, 0, err
|
|
|
|
}
|
2020-03-30 05:04:20 +00:00
|
|
|
|
2021-02-15 16:40:52 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2021-01-15 16:43:23 +00:00
|
|
|
// socketPermissionsForOS returns the permissions to use for the
|
|
|
|
// tailscaled.sock.
|
|
|
|
func socketPermissionsForOS() os.FileMode {
|
2021-03-02 19:12:14 +00:00
|
|
|
if PlatformUsesPeerCreds() {
|
2021-01-15 16:43:23 +00:00
|
|
|
return 0666
|
|
|
|
}
|
|
|
|
// Otherwise, root only.
|
|
|
|
return 0600
|
|
|
|
}
|
|
|
|
|
2022-04-26 04:37:03 +00:00
|
|
|
// connectMacOSAppSandbox connects to the Tailscale Network Extension (macOS App
|
|
|
|
// Store build) or App Extension (macsys standalone build), where the CLI itself
|
|
|
|
// is either running within the macOS App Sandbox or built separately (e.g.
|
|
|
|
// homebrew or go install). This little dance to connect a regular user binary
|
|
|
|
// to the sandboxed network extension is:
|
2020-03-30 05:04:20 +00:00
|
|
|
//
|
2022-08-02 16:33:46 +00:00
|
|
|
// - the sandboxed IPNExtension picks a random localhost:0 TCP port
|
2020-03-30 05:04:20 +00:00
|
|
|
// to listen on
|
2022-08-02 16:33:46 +00:00
|
|
|
// - it also picks a random hex string that acts as an auth token
|
|
|
|
// - the CLI looks on disk for that TCP port + auth token (see localTCPPortAndTokenDarwin)
|
|
|
|
// - we send it upon TCP connect to prove to the Tailscale daemon that
|
2022-04-26 04:37:03 +00:00
|
|
|
// we're a suitably privileged user to have access the files on disk
|
|
|
|
// which the Network/App Extension wrote.
|
2020-03-30 05:04:20 +00:00
|
|
|
func connectMacOSAppSandbox() (net.Conn, error) {
|
2021-01-29 22:32:56 +00:00
|
|
|
port, token, err := LocalTCPPortAndToken()
|
2020-03-30 05:04:20 +00:00
|
|
|
if err != nil {
|
2022-04-26 04:37:03 +00:00
|
|
|
return nil, fmt.Errorf("failed to find local Tailscale daemon: %w", err)
|
2020-03-30 05:04:20 +00:00
|
|
|
}
|
2021-01-29 22:32:56 +00:00
|
|
|
return connectMacTCP(port, token)
|
2020-03-30 05:04:20 +00:00
|
|
|
}
|
2020-07-20 21:23:50 +00:00
|
|
|
|
2022-04-26 04:37:03 +00:00
|
|
|
// connectMacTCP creates an authenticated net.Conn to the local macOS Tailscale
|
|
|
|
// daemon for used by the "IPN" JSON message bus protocol (Tailscale's original
|
|
|
|
// local non-HTTP IPC protocol).
|
2021-01-29 22:32:56 +00:00
|
|
|
func connectMacTCP(port int, token string) (net.Conn, error) {
|
|
|
|
c, err := net.Dial("tcp", "localhost:"+strconv.Itoa(port))
|
2020-07-20 21:23:50 +00:00
|
|
|
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
|
|
|
|
}
|