2023-01-27 21:37:20 +00:00
|
|
|
// Copyright (c) Tailscale Inc & AUTHORS
|
|
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
2020-02-25 16:46:26 +00:00
|
|
|
|
|
|
|
// Package safesocket creates either a Unix socket, if possible, or
|
|
|
|
// otherwise a localhost TCP connection.
|
|
|
|
package safesocket
|
|
|
|
|
|
|
|
import (
|
2021-01-29 22:32:56 +00:00
|
|
|
"errors"
|
2020-02-25 16:46:26 +00:00
|
|
|
"net"
|
2021-01-29 22:32:56 +00:00
|
|
|
"runtime"
|
2021-08-31 21:36:10 +00:00
|
|
|
"time"
|
2020-02-25 16:46:26 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
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()
|
|
|
|
}
|
|
|
|
|
2021-08-31 21:36:10 +00:00
|
|
|
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()
|
|
|
|
}
|
|
|
|
|
2022-11-29 04:08:35 +00:00
|
|
|
// A ConnectionStrategy is a plan for how to connect to tailscaled or equivalent
|
|
|
|
// (e.g. IPNExtension on macOS).
|
|
|
|
//
|
|
|
|
// This is a struct because prior to Tailscale 1.34.0 it was more complicated
|
|
|
|
// and there were multiple protocols that could be used. See LocalClient's
|
|
|
|
// dialer for what happens in practice these days (2022-11-28).
|
|
|
|
//
|
|
|
|
// TODO(bradfitz): we can remove this struct now and revert this package closer
|
|
|
|
// to its original smaller API.
|
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
|
|
|
type ConnectionStrategy struct {
|
2022-11-29 04:08:35 +00:00
|
|
|
path string // unix socket path
|
|
|
|
port uint16 // TCP port
|
|
|
|
|
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
|
|
|
// 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
|
|
|
|
// ------------------|--------------------|---------|-----------
|
2022-11-29 04:08:35 +00:00
|
|
|
// no | no | unix* | unix socket *includes tailscaled on darwin
|
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
|
|
|
// 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 {
|
2023-01-30 17:34:51 +00:00
|
|
|
return &ConnectionStrategy{path: path}
|
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
|
|
|
}
|
|
|
|
|
|
|
|
// Connect connects to tailscaled using s
|
|
|
|
func Connect(s *ConnectionStrategy) (net.Conn, error) {
|
2021-08-31 21:36:10 +00:00
|
|
|
for {
|
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
|
|
|
c, err := connect(s)
|
2021-08-31 21:36:10 +00:00
|
|
|
if err != nil && tailscaledStillStarting() {
|
|
|
|
time.Sleep(250 * time.Millisecond)
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
return c, err
|
|
|
|
}
|
2020-02-25 16:46:26 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Listen returns a listener either on Unix socket path (on Unix), or
|
2023-01-30 17:34:51 +00:00
|
|
|
// the NamedPipe path (on Windows).
|
|
|
|
func Listen(path string) (net.Listener, error) {
|
|
|
|
return listen(path)
|
2020-02-25 16:46:26 +00:00
|
|
|
}
|
2021-01-29 22:32:56 +00:00
|
|
|
|
|
|
|
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()
|
|
|
|
}
|
2021-03-02 19:12:14 +00:00
|
|
|
|
|
|
|
// PlatformUsesPeerCreds reports whether the current platform uses peer credentials
|
|
|
|
// to authenticate connections.
|
2021-05-03 16:23:01 +00:00
|
|
|
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 {
|
2021-03-03 19:41:32 +00:00
|
|
|
case "linux", "darwin", "freebsd":
|
2021-03-02 19:12:14 +00:00
|
|
|
return true
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
}
|