Compare commits
20 Commits
tom/iptabl
...
release-br
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
00fe04c951 | ||
|
|
dce2409b15 | ||
|
|
258f251af3 | ||
|
|
4a1f4c2cad | ||
|
|
5651fa1e60 | ||
|
|
73f169e8f5 | ||
|
|
759a2bd546 | ||
|
|
c0746cf25c | ||
|
|
5ff23cb1ce | ||
|
|
497fab5640 | ||
|
|
2226bd99fa | ||
|
|
465642b249 | ||
|
|
a51123022a | ||
|
|
f90052c86c | ||
|
|
4be1222701 | ||
|
|
70a6d87b16 | ||
|
|
de1ebee14f | ||
|
|
3a7f71df63 | ||
|
|
b16e27db9e | ||
|
|
f0e71f4a20 |
@@ -1 +1 @@
|
||||
1.23.0
|
||||
1.24.2
|
||||
|
||||
@@ -23,7 +23,6 @@ import (
|
||||
"tailscale.com/client/tailscale"
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/version"
|
||||
)
|
||||
|
||||
var sshCmd = &ffcli.Command{
|
||||
@@ -76,36 +75,52 @@ func runSSH(ctx context.Context, args []string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
argv := append([]string{
|
||||
ssh,
|
||||
argv := []string{ssh}
|
||||
|
||||
if envknob.Bool("TS_DEBUG_SSH_EXEC") {
|
||||
argv = append(argv, "-vvv")
|
||||
}
|
||||
argv = append(argv,
|
||||
// Only trust SSH hosts that we know about.
|
||||
"-o", fmt.Sprintf("UserKnownHostsFile %q", knownHostsFile),
|
||||
"-o", "UpdateHostKeys no",
|
||||
"-o", "StrictHostKeyChecking yes",
|
||||
)
|
||||
|
||||
"-o", fmt.Sprintf("ProxyCommand %q --socket=%q nc %%h %%p",
|
||||
tailscaleBin,
|
||||
rootArgs.socket,
|
||||
),
|
||||
// TODO(bradfitz): nc is currently broken on macOS:
|
||||
// https://github.com/tailscale/tailscale/issues/4529
|
||||
// So don't use it for now. MagicDNS is usually working on macOS anyway
|
||||
// and they're not in userspace mode, so 'nc' isn't very useful.
|
||||
if runtime.GOOS != "darwin" {
|
||||
argv = append(argv,
|
||||
"-o", fmt.Sprintf("ProxyCommand %q --socket=%q nc %%h %%p",
|
||||
tailscaleBin,
|
||||
rootArgs.socket,
|
||||
))
|
||||
}
|
||||
|
||||
// Explicitly rebuild the user@host argument rather than
|
||||
// passing it through. In general, the use of OpenSSH's ssh
|
||||
// binary is a crutch for now. We don't want to be
|
||||
// Hyrum-locked into passing through all OpenSSH flags to the
|
||||
// OpenSSH client forever. We try to make our flags and args
|
||||
// be compatible, but only a subset. The "tailscale ssh"
|
||||
// command should be a simple and portable one. If they want
|
||||
// to use a different one, we'll later be making stock ssh
|
||||
// work well by default too. (doing things like automatically
|
||||
// setting known_hosts, etc)
|
||||
username + "@" + hostForSSH,
|
||||
}, argRest...)
|
||||
// Explicitly rebuild the user@host argument rather than
|
||||
// passing it through. In general, the use of OpenSSH's ssh
|
||||
// binary is a crutch for now. We don't want to be
|
||||
// Hyrum-locked into passing through all OpenSSH flags to the
|
||||
// OpenSSH client forever. We try to make our flags and args
|
||||
// be compatible, but only a subset. The "tailscale ssh"
|
||||
// command should be a simple and portable one. If they want
|
||||
// to use a different one, we'll later be making stock ssh
|
||||
// work well by default too. (doing things like automatically
|
||||
// setting known_hosts, etc)
|
||||
argv = append(argv, username+"@"+hostForSSH)
|
||||
|
||||
if runtime.GOOS == "windows" || version.IsSandboxedMacOS() {
|
||||
// Don't use syscall.Exec on Windows or in the macOS sandbox.
|
||||
argv = append(argv, argRest...)
|
||||
|
||||
if envknob.Bool("TS_DEBUG_SSH_EXEC") {
|
||||
log.Printf("Running: %q, %q ...", ssh, argv)
|
||||
}
|
||||
|
||||
if runtime.GOOS == "windows" {
|
||||
// Don't use syscall.Exec on Windows.
|
||||
cmd := exec.Command(ssh, argv[1:]...)
|
||||
cmd.Stderr = os.Stderr
|
||||
cmd.Stdin = os.Stdin
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
var ee *exec.ExitError
|
||||
@@ -116,9 +131,6 @@ func runSSH(ctx context.Context, args []string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if envknob.Bool("TS_DEBUG_SSH_EXEC") {
|
||||
log.Printf("Running: %q, %q ...", ssh, argv)
|
||||
}
|
||||
if err := syscall.Exec(ssh, argv, os.Environ()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ down").
|
||||
If flags are specified, the flags must be the complete set of desired
|
||||
settings. An error is returned if any setting would be changed as a
|
||||
result of an unspecified flag's default value, unless the --reset flag
|
||||
is also used. (The flags --authkey, --force-reauth, and --qr are not
|
||||
is also used. (The flags --auth-key, --force-reauth, and --qr are not
|
||||
considered settings that need to be re-specified when modifying
|
||||
settings.)
|
||||
`),
|
||||
|
||||
@@ -322,6 +322,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
W golang.org/x/sys/windows from github.com/go-ole/go-ole+
|
||||
W golang.org/x/sys/windows/registry from golang.zx2c4.com/wireguard/windows/tunnel/winipcfg+
|
||||
W golang.org/x/sys/windows/svc from golang.org/x/sys/windows/svc/mgr+
|
||||
W golang.org/x/sys/windows/svc/eventlog from tailscale.com/cmd/tailscaled
|
||||
W golang.org/x/sys/windows/svc/mgr from tailscale.com/cmd/tailscaled
|
||||
golang.org/x/term from tailscale.com/logpolicy
|
||||
golang.org/x/text/secure/bidirule from golang.org/x/net/idna
|
||||
|
||||
@@ -332,6 +332,7 @@ func run() error {
|
||||
socksListener, httpProxyListener := mustStartProxyListeners(args.socksAddr, args.httpProxyAddr)
|
||||
|
||||
dialer := new(tsdial.Dialer) // mutated below (before used)
|
||||
dialer.Logf = logf
|
||||
e, useNetstack, err := createEngine(logf, linkMon, dialer)
|
||||
if err != nil {
|
||||
return fmt.Errorf("createEngine: %w", err)
|
||||
@@ -394,6 +395,7 @@ func run() error {
|
||||
// want to keep running.
|
||||
signal.Ignore(syscall.SIGPIPE)
|
||||
go func() {
|
||||
defer dialer.Close()
|
||||
select {
|
||||
case s := <-interrupt:
|
||||
logf("tailscaled got signal %v; shutting down", s)
|
||||
|
||||
@@ -30,6 +30,7 @@ import (
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
"golang.org/x/sys/windows/svc"
|
||||
"golang.org/x/sys/windows/svc/eventlog"
|
||||
"golang.zx2c4.com/wireguard/windows/tunnel/winipcfg"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/envknob"
|
||||
@@ -60,12 +61,31 @@ func isWindowsService() bool {
|
||||
return v
|
||||
}
|
||||
|
||||
// syslogf is a logger function that writes to the Windows event log (ie, the
|
||||
// one that you see in the Windows Event Viewer). tailscaled may optionally
|
||||
// generate diagnostic messages in the same event timeline as the Windows
|
||||
// Service Control Manager to assist with diagnosing issues with tailscaled's
|
||||
// lifetime (such as slow shutdowns).
|
||||
var syslogf logger.Logf = logger.Discard
|
||||
|
||||
// runWindowsService starts running Tailscale under the Windows
|
||||
// Service environment.
|
||||
//
|
||||
// At this point we're still the parent process that
|
||||
// Windows started.
|
||||
func runWindowsService(pol *logpolicy.Policy) error {
|
||||
if winutil.GetPolicyInteger("LogSCMInteractions", 0) != 0 {
|
||||
syslog, err := eventlog.Open(serviceName)
|
||||
if err == nil {
|
||||
syslogf = func(format string, args ...any) {
|
||||
syslog.Info(0, fmt.Sprintf(format, args...))
|
||||
}
|
||||
defer syslog.Close()
|
||||
}
|
||||
}
|
||||
|
||||
syslogf("Service entering svc.Run")
|
||||
defer syslogf("Service exiting svc.Run")
|
||||
return svc.Run(serviceName, &ipnService{Policy: pol})
|
||||
}
|
||||
|
||||
@@ -75,7 +95,10 @@ type ipnService struct {
|
||||
|
||||
// Called by Windows to execute the windows service.
|
||||
func (service *ipnService) Execute(args []string, r <-chan svc.ChangeRequest, changes chan<- svc.Status) (bool, uint32) {
|
||||
defer syslogf("SvcStopped notification imminent")
|
||||
|
||||
changes <- svc.Status{State: svc.StartPending}
|
||||
syslogf("Service start pending")
|
||||
|
||||
svcAccepts := svc.AcceptStop
|
||||
if winutil.GetPolicyInteger("FlushDNSOnSessionUnlock", 0) != 0 {
|
||||
@@ -98,26 +121,29 @@ func (service *ipnService) Execute(args []string, r <-chan svc.ChangeRequest, ch
|
||||
}()
|
||||
|
||||
changes <- svc.Status{State: svc.Running, Accepts: svcAccepts}
|
||||
syslogf("Service running")
|
||||
|
||||
for ctx.Err() == nil {
|
||||
for {
|
||||
select {
|
||||
case <-doneCh:
|
||||
return false, windows.NO_ERROR
|
||||
case cmd := <-r:
|
||||
log.Printf("Got Windows Service event: %v", cmdName(cmd.Cmd))
|
||||
switch cmd.Cmd {
|
||||
case svc.Stop:
|
||||
cancel()
|
||||
changes <- svc.Status{State: svc.StopPending}
|
||||
syslogf("Service stop pending")
|
||||
cancel() // so BabysitProc will kill the child process
|
||||
case svc.Interrogate:
|
||||
syslogf("Service interrogation")
|
||||
changes <- cmd.CurrentStatus
|
||||
case svc.SessionChange:
|
||||
syslogf("Service session change notification")
|
||||
handleSessionChange(cmd)
|
||||
changes <- cmd.CurrentStatus
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
changes <- svc.Status{State: svc.StopPending}
|
||||
return false, windows.NO_ERROR
|
||||
}
|
||||
|
||||
func cmdName(c svc.Cmd) string {
|
||||
|
||||
@@ -38,9 +38,9 @@ import (
|
||||
"tailscale.com/net/dnscache"
|
||||
"tailscale.com/net/dnsfallback"
|
||||
"tailscale.com/net/interfaces"
|
||||
"tailscale.com/net/netns"
|
||||
"tailscale.com/net/netutil"
|
||||
"tailscale.com/net/tlsdial"
|
||||
"tailscale.com/net/tsdial"
|
||||
"tailscale.com/net/tshttpproxy"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/key"
|
||||
@@ -57,7 +57,8 @@ import (
|
||||
// Direct is the client that connects to a tailcontrol server for a node.
|
||||
type Direct struct {
|
||||
httpc *http.Client // HTTP client used to talk to tailcontrol
|
||||
serverURL string // URL of the tailcontrol server
|
||||
dialer *tsdial.Dialer
|
||||
serverURL string // URL of the tailcontrol server
|
||||
timeNow func() time.Time
|
||||
lastPrintMap time.Time
|
||||
newDecompressor func() (Decompressor, error)
|
||||
@@ -106,6 +107,7 @@ type Options struct {
|
||||
DebugFlags []string // debug settings to send to control
|
||||
LinkMonitor *monitor.Mon // optional link monitor
|
||||
PopBrowserURL func(url string) // optional func to open browser
|
||||
Dialer *tsdial.Dialer // non-nil
|
||||
|
||||
// KeepSharerAndUserSplit controls whether the client
|
||||
// understands Node.Sharer. If false, the Sharer is mapped to the User.
|
||||
@@ -170,13 +172,12 @@ func NewDirect(opts Options) (*Direct, error) {
|
||||
UseLastGood: true,
|
||||
LookupIPFallback: dnsfallback.Lookup,
|
||||
}
|
||||
dialer := netns.NewDialer(opts.Logf)
|
||||
tr := http.DefaultTransport.(*http.Transport).Clone()
|
||||
tr.Proxy = tshttpproxy.ProxyFromEnvironment
|
||||
tshttpproxy.SetTransportGetProxyConnectHeader(tr)
|
||||
tr.TLSClientConfig = tlsdial.Config(serverURL.Hostname(), tr.TLSClientConfig)
|
||||
tr.DialContext = dnscache.Dialer(dialer.DialContext, dnsCache)
|
||||
tr.DialTLSContext = dnscache.TLSDialer(dialer.DialContext, dnsCache, tr.TLSClientConfig)
|
||||
tr.DialContext = dnscache.Dialer(opts.Dialer.SystemDial, dnsCache)
|
||||
tr.DialTLSContext = dnscache.TLSDialer(opts.Dialer.SystemDial, dnsCache, tr.TLSClientConfig)
|
||||
tr.ForceAttemptHTTP2 = true
|
||||
// Disable implicit gzip compression; the various
|
||||
// handlers (register, map, set-dns, etc) do their own
|
||||
@@ -202,6 +203,7 @@ func NewDirect(opts Options) (*Direct, error) {
|
||||
skipIPForwardingCheck: opts.SkipIPForwardingCheck,
|
||||
pinger: opts.Pinger,
|
||||
popBrowser: opts.PopBrowserURL,
|
||||
dialer: opts.Dialer,
|
||||
}
|
||||
if opts.Hostinfo == nil {
|
||||
c.SetHostinfo(hostinfo.New())
|
||||
@@ -376,7 +378,7 @@ func (c *Direct) doLogin(ctx context.Context, opt loginOpt) (mustRegen bool, new
|
||||
if err != nil {
|
||||
return regen, opt.URL, err
|
||||
}
|
||||
c.logf("control server key %s from %s", serverKey.ShortString(), c.serverURL)
|
||||
c.logf("control server key from %s: ts2021=%s, legacy=%v", c.serverURL, keys.PublicKey.ShortString(), keys.LegacyPublicKey.ShortString())
|
||||
|
||||
c.mu.Lock()
|
||||
c.serverKey = keys.LegacyPublicKey
|
||||
@@ -1278,7 +1280,7 @@ func (c *Direct) getNoiseClient() (*noiseClient, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
nc, err = newNoiseClient(k, serverNoiseKey, c.serverURL)
|
||||
nc, err = newNoiseClient(k, serverNoiseKey, c.serverURL, c.dialer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/hostinfo"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/net/tsdial"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/key"
|
||||
)
|
||||
@@ -30,6 +31,7 @@ func TestNewDirect(t *testing.T) {
|
||||
GetMachinePrivateKey: func() (key.MachinePrivate, error) {
|
||||
return k, nil
|
||||
},
|
||||
Dialer: new(tsdial.Dialer),
|
||||
}
|
||||
c, err := NewDirect(opts)
|
||||
if err != nil {
|
||||
@@ -106,6 +108,7 @@ func TestTsmpPing(t *testing.T) {
|
||||
GetMachinePrivateKey: func() (key.MachinePrivate, error) {
|
||||
return k, nil
|
||||
},
|
||||
Dialer: new(tsdial.Dialer),
|
||||
}
|
||||
|
||||
c, err := NewDirect(opts)
|
||||
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
"golang.org/x/net/http2"
|
||||
"tailscale.com/control/controlbase"
|
||||
"tailscale.com/control/controlhttp"
|
||||
"tailscale.com/net/tsdial"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/util/mak"
|
||||
@@ -46,6 +47,7 @@ func (c *noiseConn) Close() error {
|
||||
// the ts2021 protocol.
|
||||
type noiseClient struct {
|
||||
*http.Client // HTTP client used to talk to tailcontrol
|
||||
dialer *tsdial.Dialer
|
||||
privKey key.MachinePrivate
|
||||
serverPubKey key.MachinePublic
|
||||
serverHost string // the host:port part of serverURL
|
||||
@@ -58,7 +60,7 @@ type noiseClient struct {
|
||||
|
||||
// newNoiseClient returns a new noiseClient for the provided server and machine key.
|
||||
// serverURL is of the form https://<host>:<port> (no trailing slash).
|
||||
func newNoiseClient(priKey key.MachinePrivate, serverPubKey key.MachinePublic, serverURL string) (*noiseClient, error) {
|
||||
func newNoiseClient(priKey key.MachinePrivate, serverPubKey key.MachinePublic, serverURL string, dialer *tsdial.Dialer) (*noiseClient, error) {
|
||||
u, err := url.Parse(serverURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -75,6 +77,7 @@ func newNoiseClient(priKey key.MachinePrivate, serverPubKey key.MachinePublic, s
|
||||
serverPubKey: serverPubKey,
|
||||
privKey: priKey,
|
||||
serverHost: host,
|
||||
dialer: dialer,
|
||||
}
|
||||
|
||||
// Create the HTTP/2 Transport using a net/http.Transport
|
||||
@@ -151,7 +154,7 @@ func (nc *noiseClient) dial(_, _ string, _ *tls.Config) (net.Conn, error) {
|
||||
// thousand version numbers before getting to this point.
|
||||
panic("capability version is too high to fit in the wire protocol")
|
||||
}
|
||||
conn, err := controlhttp.Dial(ctx, nc.serverHost, nc.privKey, nc.serverPubKey, uint16(tailcfg.CurrentCapabilityVersion))
|
||||
conn, err := controlhttp.Dial(ctx, nc.serverHost, nc.privKey, nc.serverPubKey, uint16(tailcfg.CurrentCapabilityVersion), nc.dialer.SystemDial)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -25,16 +25,15 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptrace"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"tailscale.com/control/controlbase"
|
||||
"tailscale.com/net/dnscache"
|
||||
"tailscale.com/net/dnsfallback"
|
||||
"tailscale.com/net/netns"
|
||||
"tailscale.com/net/netutil"
|
||||
"tailscale.com/net/tlsdial"
|
||||
"tailscale.com/net/tshttpproxy"
|
||||
@@ -65,13 +64,12 @@ const (
|
||||
//
|
||||
// The provided ctx is only used for the initial connection, until
|
||||
// Dial returns. It does not affect the connection once established.
|
||||
func Dial(ctx context.Context, addr string, machineKey key.MachinePrivate, controlKey key.MachinePublic, protocolVersion uint16) (*controlbase.Conn, error) {
|
||||
func Dial(ctx context.Context, addr string, machineKey key.MachinePrivate, controlKey key.MachinePublic, protocolVersion uint16, dialer dnscache.DialContextFunc) (*controlbase.Conn, error) {
|
||||
host, port, err := net.SplitHostPort(addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
a := &dialParams{
|
||||
ctx: ctx,
|
||||
host: host,
|
||||
httpPort: port,
|
||||
httpsPort: "443",
|
||||
@@ -79,12 +77,12 @@ func Dial(ctx context.Context, addr string, machineKey key.MachinePrivate, contr
|
||||
controlKey: controlKey,
|
||||
version: protocolVersion,
|
||||
proxyFunc: tshttpproxy.ProxyFromEnvironment,
|
||||
dialer: dialer,
|
||||
}
|
||||
return a.dial()
|
||||
return a.dial(ctx)
|
||||
}
|
||||
|
||||
type dialParams struct {
|
||||
ctx context.Context
|
||||
host string
|
||||
httpPort string
|
||||
httpsPort string
|
||||
@@ -92,65 +90,132 @@ type dialParams struct {
|
||||
controlKey key.MachinePublic
|
||||
version uint16
|
||||
proxyFunc func(*http.Request) (*url.URL, error) // or nil
|
||||
dialer dnscache.DialContextFunc
|
||||
|
||||
// For tests only
|
||||
insecureTLS bool
|
||||
insecureTLS bool
|
||||
testFallbackDelay time.Duration
|
||||
}
|
||||
|
||||
func (a *dialParams) dial() (*controlbase.Conn, error) {
|
||||
init, cont, err := controlbase.ClientDeferred(a.machineKey, a.controlKey, a.version)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
// httpsFallbackDelay is how long we'll wait for a.httpPort to work before
|
||||
// starting to try a.httpsPort.
|
||||
func (a *dialParams) httpsFallbackDelay() time.Duration {
|
||||
if v := a.testFallbackDelay; v != 0 {
|
||||
return v
|
||||
}
|
||||
return 500 * time.Millisecond
|
||||
}
|
||||
|
||||
u := &url.URL{
|
||||
func (a *dialParams) dial(ctx context.Context) (*controlbase.Conn, error) {
|
||||
// Create one shared context used by both port 80 and port 443 dials.
|
||||
// If port 80 is still in flight when 443 returns, this deferred cancel
|
||||
// will stop the port 80 dial.
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
|
||||
// u80 and u443 are the URLs we'll try to hit over HTTP or HTTPS,
|
||||
// respectively, in order to do the HTTP upgrade to a net.Conn over which
|
||||
// we'll speak Noise.
|
||||
u80 := &url.URL{
|
||||
Scheme: "http",
|
||||
Host: net.JoinHostPort(a.host, a.httpPort),
|
||||
Path: serverUpgradePath,
|
||||
}
|
||||
conn, httpErr := a.tryURL(u, init)
|
||||
if httpErr == nil {
|
||||
ret, err := cont(a.ctx, conn)
|
||||
if err != nil {
|
||||
conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
return ret, nil
|
||||
u443 := &url.URL{
|
||||
Scheme: "https",
|
||||
Host: net.JoinHostPort(a.host, a.httpsPort),
|
||||
Path: serverUpgradePath,
|
||||
}
|
||||
|
||||
// Connecting over plain HTTP failed, assume it's an HTTP proxy
|
||||
// being difficult and see if we can get through over HTTPS.
|
||||
u.Scheme = "https"
|
||||
u.Host = net.JoinHostPort(a.host, a.httpsPort)
|
||||
init, cont, err = controlbase.ClientDeferred(a.machineKey, a.controlKey, a.version)
|
||||
type tryURLRes struct {
|
||||
u *url.URL // input (the URL conn+err are for/from)
|
||||
conn *controlbase.Conn // result (mutually exclusive with err)
|
||||
err error
|
||||
}
|
||||
ch := make(chan tryURLRes) // must be unbuffered
|
||||
try := func(u *url.URL) {
|
||||
cbConn, err := a.dialURL(ctx, u)
|
||||
select {
|
||||
case ch <- tryURLRes{u, cbConn, err}:
|
||||
case <-ctx.Done():
|
||||
if cbConn != nil {
|
||||
cbConn.Close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Start the plaintext HTTP attempt first.
|
||||
go try(u80)
|
||||
|
||||
// In case outbound port 80 blocked or MITM'ed poorly, start a backup timer
|
||||
// to dial port 443 if port 80 doesn't either succeed or fail quickly.
|
||||
try443Timer := time.AfterFunc(a.httpsFallbackDelay(), func() { try(u443) })
|
||||
defer try443Timer.Stop()
|
||||
|
||||
var err80, err443 error
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, fmt.Errorf("connection attempts aborted by context: %w", ctx.Err())
|
||||
case res := <-ch:
|
||||
if res.err == nil {
|
||||
return res.conn, nil
|
||||
}
|
||||
switch res.u {
|
||||
case u80:
|
||||
// Connecting over plain HTTP failed; assume it's an HTTP proxy
|
||||
// being difficult and see if we can get through over HTTPS.
|
||||
err80 = res.err
|
||||
// Stop the fallback timer and run it immediately. We don't use
|
||||
// Timer.Reset(0) here because on AfterFuncs, that can run it
|
||||
// again.
|
||||
if try443Timer.Stop() {
|
||||
go try(u443)
|
||||
} // else we lost the race and it started already which is what we want
|
||||
case u443:
|
||||
err443 = res.err
|
||||
default:
|
||||
panic("invalid")
|
||||
}
|
||||
if err80 != nil && err443 != nil {
|
||||
return nil, fmt.Errorf("all connection attempts failed (HTTP: %v, HTTPS: %v)", err80, err443)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// dialURL attempts to connect to the given URL.
|
||||
func (a *dialParams) dialURL(ctx context.Context, u *url.URL) (*controlbase.Conn, error) {
|
||||
init, cont, err := controlbase.ClientDeferred(a.machineKey, a.controlKey, a.version)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
conn, tlsErr := a.tryURL(u, init)
|
||||
if tlsErr == nil {
|
||||
ret, err := cont(a.ctx, conn)
|
||||
if err != nil {
|
||||
conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
return ret, nil
|
||||
netConn, err := a.tryURLUpgrade(ctx, u, init)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("all connection attempts failed (HTTP: %v, HTTPS: %v)", httpErr, tlsErr)
|
||||
cbConn, err := cont(ctx, netConn)
|
||||
if err != nil {
|
||||
netConn.Close()
|
||||
return nil, err
|
||||
}
|
||||
return cbConn, nil
|
||||
}
|
||||
|
||||
func (a *dialParams) tryURL(u *url.URL, init []byte) (net.Conn, error) {
|
||||
// tryURLUpgrade connects to u, and tries to upgrade it to a net.Conn.
|
||||
//
|
||||
// Only the provided ctx is used, not a.ctx.
|
||||
func (a *dialParams) tryURLUpgrade(ctx context.Context, u *url.URL, init []byte) (net.Conn, error) {
|
||||
dns := &dnscache.Resolver{
|
||||
Forward: dnscache.Get().Forward,
|
||||
LookupIPFallback: dnsfallback.Lookup,
|
||||
UseLastGood: true,
|
||||
}
|
||||
dialer := netns.NewDialer(log.Printf)
|
||||
tr := http.DefaultTransport.(*http.Transport).Clone()
|
||||
defer tr.CloseIdleConnections()
|
||||
tr.Proxy = a.proxyFunc
|
||||
tshttpproxy.SetTransportGetProxyConnectHeader(tr)
|
||||
tr.DialContext = dnscache.Dialer(dialer.DialContext, dns)
|
||||
tr.DialContext = dnscache.Dialer(a.dialer, dns)
|
||||
// Disable HTTP2, since h2 can't do protocol switching.
|
||||
tr.TLSClientConfig.NextProtos = []string{}
|
||||
tr.TLSNextProto = map[string]func(string, *tls.Conn) http.RoundTripper{}
|
||||
@@ -159,7 +224,7 @@ func (a *dialParams) tryURL(u *url.URL, init []byte) (net.Conn, error) {
|
||||
tr.TLSClientConfig.InsecureSkipVerify = true
|
||||
tr.TLSClientConfig.VerifyConnection = nil
|
||||
}
|
||||
tr.DialTLSContext = dnscache.TLSDialer(dialer.DialContext, dns, tr.TLSClientConfig)
|
||||
tr.DialTLSContext = dnscache.TLSDialer(a.dialer, dns, tr.TLSClientConfig)
|
||||
tr.DisableCompression = true
|
||||
|
||||
// (mis)use httptrace to extract the underlying net.Conn from the
|
||||
@@ -189,7 +254,7 @@ func (a *dialParams) tryURL(u *url.URL, init []byte) (net.Conn, error) {
|
||||
connCh <- info.Conn
|
||||
},
|
||||
}
|
||||
ctx := httptrace.WithClientTrace(a.ctx, &trace)
|
||||
ctx = httptrace.WithClientTrace(ctx, &trace)
|
||||
req := &http.Request{
|
||||
Method: "POST",
|
||||
URL: u,
|
||||
|
||||
@@ -17,22 +17,36 @@ import (
|
||||
"strconv"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"tailscale.com/control/controlbase"
|
||||
"tailscale.com/net/socks5"
|
||||
"tailscale.com/net/tsdial"
|
||||
"tailscale.com/types/key"
|
||||
)
|
||||
|
||||
type httpTestParam struct {
|
||||
name string
|
||||
proxy proxy
|
||||
|
||||
// makeHTTPHangAfterUpgrade makes the HTTP response hang after sending a
|
||||
// 101 switching protocols.
|
||||
makeHTTPHangAfterUpgrade bool
|
||||
}
|
||||
|
||||
func TestControlHTTP(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
proxy proxy
|
||||
}{
|
||||
tests := []httpTestParam{
|
||||
// direct connection
|
||||
{
|
||||
name: "no_proxy",
|
||||
proxy: nil,
|
||||
},
|
||||
// direct connection but port 80 is MITM'ed and broken
|
||||
{
|
||||
name: "port80_broken_mitm",
|
||||
proxy: nil,
|
||||
makeHTTPHangAfterUpgrade: true,
|
||||
},
|
||||
// SOCKS5
|
||||
{
|
||||
name: "socks5",
|
||||
@@ -96,12 +110,13 @@ func TestControlHTTP(t *testing.T) {
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
testControlHTTP(t, test.proxy)
|
||||
testControlHTTP(t, test)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func testControlHTTP(t *testing.T, proxy proxy) {
|
||||
func testControlHTTP(t *testing.T, param httpTestParam) {
|
||||
proxy := param.proxy
|
||||
client, server := key.NewMachine(), key.NewMachine()
|
||||
|
||||
const testProtocolVersion = 1
|
||||
@@ -132,7 +147,11 @@ func testControlHTTP(t *testing.T, proxy proxy) {
|
||||
t.Fatalf("HTTPS listen: %v", err)
|
||||
}
|
||||
|
||||
httpServer := &http.Server{Handler: handler}
|
||||
var httpHandler http.Handler = handler
|
||||
if param.makeHTTPHangAfterUpgrade {
|
||||
httpHandler = http.HandlerFunc(brokenMITMHandler)
|
||||
}
|
||||
httpServer := &http.Server{Handler: httpHandler}
|
||||
go httpServer.Serve(httpLn)
|
||||
defer httpServer.Close()
|
||||
|
||||
@@ -143,18 +162,24 @@ func testControlHTTP(t *testing.T, proxy proxy) {
|
||||
go httpsServer.ServeTLS(httpsLn, "", "")
|
||||
defer httpsServer.Close()
|
||||
|
||||
//ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
//defer cancel()
|
||||
ctx := context.Background()
|
||||
const debugTimeout = false
|
||||
if debugTimeout {
|
||||
var cancel context.CancelFunc
|
||||
ctx, cancel = context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
}
|
||||
|
||||
a := dialParams{
|
||||
ctx: context.Background(), //ctx,
|
||||
host: "localhost",
|
||||
httpPort: strconv.Itoa(httpLn.Addr().(*net.TCPAddr).Port),
|
||||
httpsPort: strconv.Itoa(httpsLn.Addr().(*net.TCPAddr).Port),
|
||||
machineKey: client,
|
||||
controlKey: server.Public(),
|
||||
version: testProtocolVersion,
|
||||
insecureTLS: true,
|
||||
host: "localhost",
|
||||
httpPort: strconv.Itoa(httpLn.Addr().(*net.TCPAddr).Port),
|
||||
httpsPort: strconv.Itoa(httpsLn.Addr().(*net.TCPAddr).Port),
|
||||
machineKey: client,
|
||||
controlKey: server.Public(),
|
||||
version: testProtocolVersion,
|
||||
insecureTLS: true,
|
||||
dialer: new(tsdial.Dialer).SystemDial,
|
||||
testFallbackDelay: 50 * time.Millisecond,
|
||||
}
|
||||
|
||||
if proxy != nil {
|
||||
@@ -173,7 +198,7 @@ func testControlHTTP(t *testing.T, proxy proxy) {
|
||||
}
|
||||
}
|
||||
|
||||
conn, err := a.dial()
|
||||
conn, err := a.dial(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("dialing controlhttp: %v", err)
|
||||
}
|
||||
@@ -215,6 +240,7 @@ type proxy interface {
|
||||
|
||||
type socksProxy struct {
|
||||
sync.Mutex
|
||||
closed bool
|
||||
proxy socks5.Server
|
||||
ln net.Listener
|
||||
clientConnAddrs map[string]bool // addrs of the local end of outgoing conns from proxy
|
||||
@@ -230,7 +256,14 @@ func (s *socksProxy) Start(t *testing.T) (url string) {
|
||||
}
|
||||
s.ln = ln
|
||||
s.clientConnAddrs = map[string]bool{}
|
||||
s.proxy.Logf = t.Logf
|
||||
s.proxy.Logf = func(format string, a ...any) {
|
||||
s.Lock()
|
||||
defer s.Unlock()
|
||||
if s.closed {
|
||||
return
|
||||
}
|
||||
t.Logf(format, a...)
|
||||
}
|
||||
s.proxy.Dialer = s.dialAndRecord
|
||||
go s.proxy.Serve(ln)
|
||||
return fmt.Sprintf("socks5://%s", ln.Addr().String())
|
||||
@@ -239,6 +272,10 @@ func (s *socksProxy) Start(t *testing.T) (url string) {
|
||||
func (s *socksProxy) Close() {
|
||||
s.Lock()
|
||||
defer s.Unlock()
|
||||
if s.closed {
|
||||
return
|
||||
}
|
||||
s.closed = true
|
||||
s.ln.Close()
|
||||
}
|
||||
|
||||
@@ -398,3 +435,11 @@ EKTcWGekdmdDPsHloRNtsiCa697B2O9IFA==
|
||||
Certificates: []tls.Certificate{cert},
|
||||
}
|
||||
}
|
||||
|
||||
func brokenMITMHandler(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Upgrade", upgradeHeaderValue)
|
||||
w.Header().Set("Connection", "upgrade")
|
||||
w.WriteHeader(http.StatusSwitchingProtocols)
|
||||
w.(http.Flusher).Flush()
|
||||
<-r.Context().Done()
|
||||
}
|
||||
|
||||
6
go.mod
6
go.mod
@@ -39,7 +39,7 @@ require (
|
||||
github.com/tailscale/certstore v0.1.1-0.20220316223106-78d6e1c49d8d
|
||||
github.com/tailscale/depaware v0.0.0-20210622194025-720c4b409502
|
||||
github.com/tailscale/goexpect v0.0.0-20210902213824-6e8c725cea41
|
||||
github.com/tailscale/golang-x-crypto v0.0.0-20220420224200-c602b5dfaa7f
|
||||
github.com/tailscale/golang-x-crypto v0.0.0-20220428210705-0b941c09a5e1
|
||||
github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05
|
||||
github.com/tailscale/hujson v0.0.0-20211105212140-3a0adc019d83
|
||||
github.com/tailscale/netlink v1.1.1-0.20211101221916-cabfb018fe85
|
||||
@@ -48,10 +48,10 @@ require (
|
||||
github.com/u-root/u-root v0.8.0
|
||||
github.com/vishvananda/netlink v1.1.1-0.20211118161826-650dca95af54
|
||||
go4.org/mem v0.0.0-20210711025021-927187094b94
|
||||
golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4
|
||||
golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f
|
||||
golang.org/x/net v0.0.0-20220407224826-aac1ed45d8e3
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
|
||||
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad
|
||||
golang.org/x/sys v0.0.0-20220422013727-9388b58f7150
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211
|
||||
golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11
|
||||
golang.org/x/tools v0.1.11-0.20220413170336-afc6aad76eb1
|
||||
|
||||
12
go.sum
12
go.sum
@@ -1065,8 +1065,8 @@ github.com/tailscale/depaware v0.0.0-20210622194025-720c4b409502 h1:34icjjmqJ2HP
|
||||
github.com/tailscale/depaware v0.0.0-20210622194025-720c4b409502/go.mod h1:p9lPsd+cx33L3H9nNoecRRxPssFKUwwI50I3pZ0yT+8=
|
||||
github.com/tailscale/goexpect v0.0.0-20210902213824-6e8c725cea41 h1:/V2rCMMWcsjYaYO2MeovLw+ClP63OtXgCF2Y1eb8+Ns=
|
||||
github.com/tailscale/goexpect v0.0.0-20210902213824-6e8c725cea41/go.mod h1:/roCdA6gg6lQyw/Oz6gIIGu3ggJKYhF+WC/AQReE5XQ=
|
||||
github.com/tailscale/golang-x-crypto v0.0.0-20220420224200-c602b5dfaa7f h1:3CuODoSnBXS+ZkQlGakDqtX1o2RteR1870yF+dS61PY=
|
||||
github.com/tailscale/golang-x-crypto v0.0.0-20220420224200-c602b5dfaa7f/go.mod h1:95n9fbUCixVSI4QXLEvdKJjnYK2eUlkTx9+QwLPXFKU=
|
||||
github.com/tailscale/golang-x-crypto v0.0.0-20220428210705-0b941c09a5e1 h1:vsFV6BKSIgjRd8m8UfrGW4r+cc28fRF71K6IRo46rKs=
|
||||
github.com/tailscale/golang-x-crypto v0.0.0-20220428210705-0b941c09a5e1/go.mod h1:95n9fbUCixVSI4QXLEvdKJjnYK2eUlkTx9+QwLPXFKU=
|
||||
github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05 h1:4chzWmimtJPxRs2O36yuGRW3f9SYV+bMTTvMBI0EKio=
|
||||
github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05/go.mod h1:PdCqy9JzfWMJf1H5UJW2ip33/d4YkoKN0r67yKH1mG8=
|
||||
github.com/tailscale/hujson v0.0.0-20211105212140-3a0adc019d83 h1:f7nwzdAHTUUOJjHZuDvLz9CEAlUM228amCRvwzlPvsA=
|
||||
@@ -1225,8 +1225,8 @@ golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5y
|
||||
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20211117183948-ae814b36b871/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4 h1:kUhD7nTDoI3fVd9G4ORWrbV5NY0liEs/Jg2pv5f+bBA=
|
||||
golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f h1:OeJjE6G4dgCY4PIXvIRQbE8+RX+uXZyGhUy/ksMGJoc=
|
||||
golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
@@ -1476,8 +1476,8 @@ golang.org/x/sys v0.0.0-20211102192858-4dd72447c267/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20211105183446-c75c47738b0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad h1:ntjMns5wyP/fN65tdBD4g8J5w8n015+iIIs9rtjXkY0=
|
||||
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220422013727-9388b58f7150 h1:xHms4gcpe1YE7A3yIllJXP16CMAGuqwO2lX1mTyyRRc=
|
||||
golang.org/x/sys v0.0.0-20220422013727-9388b58f7150/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
|
||||
|
||||
@@ -1034,6 +1034,7 @@ func (b *LocalBackend) Start(opts ipn.Options) error {
|
||||
LinkMonitor: b.e.GetLinkMonitor(),
|
||||
Pinger: b.e,
|
||||
PopBrowserURL: b.tellClientToBrowseToURL,
|
||||
Dialer: b.Dialer(),
|
||||
|
||||
// Don't warn about broken Linux IP forwarding when
|
||||
// netstack is being used.
|
||||
|
||||
@@ -563,6 +563,9 @@ func (h *peerAPIHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
case "/v0/dnsfwd":
|
||||
h.handleServeDNSFwd(w, r)
|
||||
return
|
||||
case "/v0/interfaces":
|
||||
h.handleServeInterfaces(w, r)
|
||||
return
|
||||
}
|
||||
who := h.peerUser.DisplayName
|
||||
fmt.Fprintf(w, `<html>
|
||||
@@ -577,6 +580,40 @@ This is my Tailscale device. Your device is %v.
|
||||
}
|
||||
}
|
||||
|
||||
func (h *peerAPIHandler) handleServeInterfaces(w http.ResponseWriter, r *http.Request) {
|
||||
if !h.canDebug() {
|
||||
http.Error(w, "denied; no debug access", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
i, err := interfaces.GetList()
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
}
|
||||
|
||||
dr, err := interfaces.DefaultRoute()
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
}
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
fmt.Fprintln(w, "<h1>Interfaces</h1>")
|
||||
fmt.Fprintf(w, "<h3>Default route is %q(%d)</h3>\n", dr.InterfaceName, dr.InterfaceIndex)
|
||||
|
||||
fmt.Fprintln(w, "<table>")
|
||||
fmt.Fprint(w, "<tr>")
|
||||
for _, v := range []any{"Index", "Name", "MTU", "Flags", "Addrs"} {
|
||||
fmt.Fprintf(w, "<th>%v</th> ", v)
|
||||
}
|
||||
fmt.Fprint(w, "</tr>\n")
|
||||
i.ForeachInterface(func(iface interfaces.Interface, ipps []netaddr.IPPrefix) {
|
||||
fmt.Fprint(w, "<tr>")
|
||||
for _, v := range []any{iface.Index, iface.Name, iface.MTU, iface.Flags, ipps} {
|
||||
fmt.Fprintf(w, "<td>%v</td> ", v)
|
||||
}
|
||||
fmt.Fprint(w, "</tr>\n")
|
||||
})
|
||||
fmt.Fprintln(w, "</table>")
|
||||
}
|
||||
|
||||
type incomingFile struct {
|
||||
name string // "foo.jpg"
|
||||
started time.Time
|
||||
|
||||
@@ -428,9 +428,14 @@ func NewPrefs() *Prefs {
|
||||
}
|
||||
|
||||
// ControlURLOrDefault returns the coordination server's URL base.
|
||||
// If not configured, DefaultControlURL is returned instead.
|
||||
//
|
||||
// If not configured, or if the configured value is a legacy name equivalent to
|
||||
// the default, then DefaultControlURL is returned instead.
|
||||
func (p *Prefs) ControlURLOrDefault() string {
|
||||
if p.ControlURL != "" {
|
||||
if p.ControlURL != DefaultControlURL && IsLoginServerSynonym(p.ControlURL) {
|
||||
return DefaultControlURL
|
||||
}
|
||||
return p.ControlURL
|
||||
}
|
||||
return DefaultControlURL
|
||||
|
||||
@@ -810,3 +810,18 @@ func TestExitNodeIPOfArg(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestControlURLOrDefault(t *testing.T) {
|
||||
var p Prefs
|
||||
if got, want := p.ControlURLOrDefault(), DefaultControlURL; got != want {
|
||||
t.Errorf("got %q; want %q", got, want)
|
||||
}
|
||||
p.ControlURL = "http://foo.bar"
|
||||
if got, want := p.ControlURLOrDefault(), "http://foo.bar"; got != want {
|
||||
t.Errorf("got %q; want %q", got, want)
|
||||
}
|
||||
p.ControlURL = "https://login.tailscale.com"
|
||||
if got, want := p.ControlURLOrDefault(), DefaultControlURL; got != want {
|
||||
t.Errorf("got %q; want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,8 +20,12 @@ import (
|
||||
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/net/dnscache"
|
||||
"tailscale.com/net/interfaces"
|
||||
"tailscale.com/net/netknob"
|
||||
"tailscale.com/net/netns"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/netmap"
|
||||
"tailscale.com/util/mak"
|
||||
"tailscale.com/wgengine/monitor"
|
||||
)
|
||||
|
||||
@@ -30,6 +34,7 @@ import (
|
||||
// (TUN, netstack), the OS network sandboxing style (macOS/iOS
|
||||
// Extension, none), user-selected route acceptance prefs, etc.
|
||||
type Dialer struct {
|
||||
Logf logger.Logf
|
||||
// UseNetstackForIP if non-nil is whether NetstackDialTCP (if
|
||||
// it's non-nil) should be used to dial the provided IP.
|
||||
UseNetstackForIP func(netaddr.IP) bool
|
||||
@@ -46,12 +51,33 @@ type Dialer struct {
|
||||
peerDialerOnce sync.Once
|
||||
peerDialer *net.Dialer
|
||||
|
||||
mu sync.Mutex
|
||||
dns dnsMap
|
||||
tunName string // tun device name
|
||||
linkMon *monitor.Mon
|
||||
exitDNSDoHBase string // non-empty if DoH-proxying exit node in use; base URL+path (without '?')
|
||||
dnsCache *dnscache.MessageCache // nil until first first non-empty SetExitDNSDoH
|
||||
netnsDialerOnce sync.Once
|
||||
netnsDialer netns.Dialer
|
||||
|
||||
mu sync.Mutex
|
||||
closed bool
|
||||
dns dnsMap
|
||||
tunName string // tun device name
|
||||
linkMon *monitor.Mon
|
||||
linkMonUnregister func()
|
||||
exitDNSDoHBase string // non-empty if DoH-proxying exit node in use; base URL+path (without '?')
|
||||
dnsCache *dnscache.MessageCache // nil until first first non-empty SetExitDNSDoH
|
||||
nextSysConnID int
|
||||
activeSysConns map[int]net.Conn // active connections not yet closed
|
||||
}
|
||||
|
||||
// sysConn wraps a net.Conn that was created using d.SystemDial.
|
||||
// It exists to track which connections are still open, and should be
|
||||
// closed on major link changes.
|
||||
type sysConn struct {
|
||||
net.Conn
|
||||
id int
|
||||
d *Dialer
|
||||
}
|
||||
|
||||
func (c sysConn) Close() error {
|
||||
c.d.closeSysConn(c.id)
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetTUNName sets the name of the tun device in use ("tailscale0", "utun6",
|
||||
@@ -91,10 +117,53 @@ func (d *Dialer) SetExitDNSDoH(doh string) {
|
||||
}
|
||||
}
|
||||
|
||||
func (d *Dialer) Close() error {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
d.closed = true
|
||||
if d.linkMonUnregister != nil {
|
||||
d.linkMonUnregister()
|
||||
d.linkMonUnregister = nil
|
||||
}
|
||||
for _, c := range d.activeSysConns {
|
||||
c.Close()
|
||||
}
|
||||
d.activeSysConns = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Dialer) SetLinkMonitor(mon *monitor.Mon) {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
if d.linkMonUnregister != nil {
|
||||
go d.linkMonUnregister()
|
||||
d.linkMonUnregister = nil
|
||||
}
|
||||
d.linkMon = mon
|
||||
d.linkMonUnregister = d.linkMon.RegisterChangeCallback(d.linkChanged)
|
||||
}
|
||||
|
||||
func (d *Dialer) linkChanged(major bool, state *interfaces.State) {
|
||||
if !major {
|
||||
return
|
||||
}
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
for id, c := range d.activeSysConns {
|
||||
go c.Close()
|
||||
delete(d.activeSysConns, id)
|
||||
}
|
||||
}
|
||||
|
||||
func (d *Dialer) closeSysConn(id int) {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
c, ok := d.activeSysConns[id]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
delete(d.activeSysConns, id)
|
||||
go c.Close() // ignore the error
|
||||
}
|
||||
|
||||
func (d *Dialer) interfaceIndexLocked(ifName string) (index int, ok bool) {
|
||||
@@ -197,6 +266,42 @@ func ipNetOfNetwork(n string) string {
|
||||
return "ip"
|
||||
}
|
||||
|
||||
// SystemDial connects to the provided network address without going over
|
||||
// Tailscale. It prefers going over the default interface and closes existing
|
||||
// connections if the default interface changes. It is used to connect to
|
||||
// Control and (in the future, as of 2022-04-27) DERPs..
|
||||
func (d *Dialer) SystemDial(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
d.mu.Lock()
|
||||
closed := d.closed
|
||||
d.mu.Unlock()
|
||||
if closed {
|
||||
return nil, net.ErrClosed
|
||||
}
|
||||
|
||||
d.netnsDialerOnce.Do(func() {
|
||||
logf := d.Logf
|
||||
if logf == nil {
|
||||
logf = logger.Discard
|
||||
}
|
||||
d.netnsDialer = netns.NewDialer(logf)
|
||||
})
|
||||
c, err := d.netnsDialer.DialContext(ctx, network, addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
id := d.nextSysConnID
|
||||
d.nextSysConnID++
|
||||
mak.Set(&d.activeSysConns, id, c)
|
||||
|
||||
return sysConn{
|
||||
id: id,
|
||||
d: d,
|
||||
Conn: c,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// UserDial connects to the provided network address as if a user were initiating the dial.
|
||||
// (e.g. from a SOCKS or HTTP outbound proxy)
|
||||
func (d *Dialer) UserDial(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
|
||||
@@ -33,8 +33,9 @@ var (
|
||||
|
||||
var cache struct {
|
||||
sync.Mutex
|
||||
proxy *url.URL
|
||||
updated time.Time
|
||||
httpProxy *url.URL
|
||||
httpsProxy *url.URL
|
||||
updated time.Time
|
||||
}
|
||||
|
||||
func synologyProxyFromConfigCached(req *http.Request) (*url.URL, error) {
|
||||
@@ -45,34 +46,36 @@ func synologyProxyFromConfigCached(req *http.Request) (*url.URL, error) {
|
||||
cache.Lock()
|
||||
defer cache.Unlock()
|
||||
|
||||
var err error
|
||||
modtime := mtime(synologyProxyConfigPath)
|
||||
|
||||
if cache.updated == modtime {
|
||||
return cache.proxy, nil
|
||||
if modtime != cache.updated {
|
||||
cache.httpProxy, cache.httpsProxy, err = synologyProxiesFromConfig()
|
||||
cache.updated = modtime
|
||||
}
|
||||
|
||||
val, err := synologyProxyFromConfig(req)
|
||||
cache.proxy = val
|
||||
|
||||
cache.updated = modtime
|
||||
|
||||
return val, err
|
||||
if req.URL.Scheme == "https" {
|
||||
return cache.httpsProxy, err
|
||||
}
|
||||
return cache.httpProxy, err
|
||||
}
|
||||
|
||||
func synologyProxyFromConfig(req *http.Request) (*url.URL, error) {
|
||||
func synologyProxiesFromConfig() (*url.URL, *url.URL, error) {
|
||||
r, err := openSynologyProxyConf()
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil, nil
|
||||
return nil, nil, nil
|
||||
}
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
defer r.Close()
|
||||
|
||||
return parseSynologyConfig(r)
|
||||
}
|
||||
|
||||
func parseSynologyConfig(r io.Reader) (*url.URL, error) {
|
||||
// parseSynologyConfig parses the Synology proxy configuration, and returns any
|
||||
// http proxy, and any https proxy respectively, or an error if parsing fails.
|
||||
func parseSynologyConfig(r io.Reader) (*url.URL, *url.URL, error) {
|
||||
cfg := map[string]string{}
|
||||
|
||||
if err := lineread.Reader(r, func(line []byte) error {
|
||||
@@ -89,39 +92,46 @@ func parseSynologyConfig(r io.Reader) (*url.URL, error) {
|
||||
cfg[string(key)] = string(value)
|
||||
return nil
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
if cfg["proxy_enabled"] != "yes" {
|
||||
return nil, nil
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
proxyURL := new(url.URL)
|
||||
httpProxyURL := new(url.URL)
|
||||
httpsProxyURL := new(url.URL)
|
||||
if cfg["auth_enabled"] == "yes" {
|
||||
proxyURL.User = url.UserPassword(cfg["proxy_user"], cfg["proxy_pwd"])
|
||||
httpProxyURL.User = url.UserPassword(cfg["proxy_user"], cfg["proxy_pwd"])
|
||||
httpsProxyURL.User = url.UserPassword(cfg["proxy_user"], cfg["proxy_pwd"])
|
||||
}
|
||||
|
||||
proxyURL.Scheme = "https"
|
||||
host, port := cfg["https_host"], cfg["https_port"]
|
||||
if host == "" {
|
||||
proxyURL.Scheme = "http"
|
||||
host, port = cfg["http_host"], cfg["http_port"]
|
||||
}
|
||||
// As far as we are aware, synology does not support tls proxies.
|
||||
httpProxyURL.Scheme = "http"
|
||||
httpsProxyURL.Scheme = "http"
|
||||
|
||||
if host == "" {
|
||||
return nil, nil
|
||||
}
|
||||
httpsProxyURL = addHostPort(httpsProxyURL, cfg["https_host"], cfg["https_port"])
|
||||
httpProxyURL = addHostPort(httpProxyURL, cfg["http_host"], cfg["http_port"])
|
||||
|
||||
if port != "" {
|
||||
proxyURL.Host = net.JoinHostPort(host, port)
|
||||
} else {
|
||||
proxyURL.Host = host
|
||||
}
|
||||
|
||||
return proxyURL, nil
|
||||
return httpProxyURL, httpsProxyURL, nil
|
||||
}
|
||||
|
||||
// mtime stat's path and returns it's modification time. If path does not exist,
|
||||
// addHostPort adds to u the given host and port and returns the updated url, or
|
||||
// if host is empty, it returns nil.
|
||||
func addHostPort(u *url.URL, host, port string) *url.URL {
|
||||
if host == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
if port == "" {
|
||||
u.Host = host
|
||||
} else {
|
||||
u.Host = net.JoinHostPort(host, port)
|
||||
}
|
||||
return u
|
||||
}
|
||||
|
||||
// mtime stat's path and returns its modification time. If path does not exist,
|
||||
// it returns the unix epoch.
|
||||
func mtime(path string) time.Time {
|
||||
fi, err := os.Stat(path)
|
||||
|
||||
@@ -22,7 +22,7 @@ import (
|
||||
)
|
||||
|
||||
func TestSynologyProxyFromConfigCached(t *testing.T) {
|
||||
req, err := http.NewRequest("GET", "https://example.org/", nil)
|
||||
req, err := http.NewRequest("GET", "http://example.org/", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -37,7 +37,8 @@ func TestSynologyProxyFromConfigCached(t *testing.T) {
|
||||
}
|
||||
|
||||
cache.updated = time.Time{}
|
||||
cache.proxy = nil
|
||||
cache.httpProxy = nil
|
||||
cache.httpsProxy = nil
|
||||
|
||||
if val, err := synologyProxyFromConfigCached(req); val != nil || err != nil {
|
||||
t.Fatalf("got %s, %v; want nil, nil", val, err)
|
||||
@@ -46,19 +47,25 @@ func TestSynologyProxyFromConfigCached(t *testing.T) {
|
||||
if got, want := cache.updated, time.Unix(0, 0); got != want {
|
||||
t.Fatalf("got %s, want %s", got, want)
|
||||
}
|
||||
if cache.proxy != nil {
|
||||
t.Fatalf("got %s, want nil", cache.proxy)
|
||||
if cache.httpProxy != nil {
|
||||
t.Fatalf("got %s, want nil", cache.httpProxy)
|
||||
}
|
||||
if cache.httpsProxy != nil {
|
||||
t.Fatalf("got %s, want nil", cache.httpsProxy)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("config file updated", func(t *testing.T) {
|
||||
cache.updated = time.Now()
|
||||
cache.proxy = nil
|
||||
cache.httpProxy = nil
|
||||
cache.httpsProxy = nil
|
||||
|
||||
if err := ioutil.WriteFile(synologyProxyConfigPath, []byte(`
|
||||
proxy_enabled=yes
|
||||
http_host=10.0.0.55
|
||||
http_port=80
|
||||
https_host=10.0.0.66
|
||||
https_port=443
|
||||
`), 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -67,6 +74,14 @@ http_port=80
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if cache.httpProxy == nil {
|
||||
t.Fatal("http proxy was not cached")
|
||||
}
|
||||
if cache.httpsProxy == nil {
|
||||
t.Fatal("https proxy was not cached")
|
||||
}
|
||||
|
||||
if want := urlMustParse("http://10.0.0.55:80"); val.String() != want.String() {
|
||||
t.Fatalf("got %s; want %s", val, want)
|
||||
}
|
||||
@@ -74,7 +89,8 @@ http_port=80
|
||||
|
||||
t.Run("config file removed", func(t *testing.T) {
|
||||
cache.updated = time.Now()
|
||||
cache.proxy = urlMustParse("http://127.0.0.1/")
|
||||
cache.httpProxy = urlMustParse("http://127.0.0.1/")
|
||||
cache.httpsProxy = urlMustParse("http://127.0.0.1/")
|
||||
|
||||
if err := os.Remove(synologyProxyConfigPath); err != nil && !os.IsNotExist(err) {
|
||||
t.Fatal(err)
|
||||
@@ -87,13 +103,62 @@ http_port=80
|
||||
if val != nil {
|
||||
t.Fatalf("got %s; want nil", val)
|
||||
}
|
||||
if cache.proxy != nil {
|
||||
t.Fatalf("got %s, want nil", cache.proxy)
|
||||
if cache.httpProxy != nil {
|
||||
t.Fatalf("got %s, want nil", cache.httpProxy)
|
||||
}
|
||||
if cache.httpsProxy != nil {
|
||||
t.Fatalf("got %s, want nil", cache.httpsProxy)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("picks proxy from request scheme", func(t *testing.T) {
|
||||
cache.updated = time.Now()
|
||||
cache.httpProxy = nil
|
||||
cache.httpsProxy = nil
|
||||
|
||||
if err := ioutil.WriteFile(synologyProxyConfigPath, []byte(`
|
||||
proxy_enabled=yes
|
||||
http_host=10.0.0.55
|
||||
http_port=80
|
||||
https_host=10.0.0.66
|
||||
https_port=443
|
||||
`), 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
httpReq, err := http.NewRequest("GET", "http://example.com", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
val, err := synologyProxyFromConfigCached(httpReq)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if val == nil {
|
||||
t.Fatalf("got nil, want an http URL")
|
||||
}
|
||||
if got, want := val.String(), "http://10.0.0.55:80"; got != want {
|
||||
t.Fatalf("got %q, want %q", got, want)
|
||||
}
|
||||
|
||||
httpsReq, err := http.NewRequest("GET", "https://example.com", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
val, err = synologyProxyFromConfigCached(httpsReq)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if val == nil {
|
||||
t.Fatalf("got nil, want an http URL")
|
||||
}
|
||||
if got, want := val.String(), "http://10.0.0.66:443"; got != want {
|
||||
t.Fatalf("got %q, want %q", got, want)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestSynologyProxyFromConfig(t *testing.T) {
|
||||
func TestSynologyProxiesFromConfig(t *testing.T) {
|
||||
var (
|
||||
openReader io.ReadCloser
|
||||
openErr error
|
||||
@@ -104,11 +169,6 @@ func TestSynologyProxyFromConfig(t *testing.T) {
|
||||
}
|
||||
defer func() { openSynologyProxyConf = origOpen }()
|
||||
|
||||
req, err := http.NewRequest("GET", "https://example.com/", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
t.Run("with config", func(t *testing.T) {
|
||||
mc := &mustCloser{Reader: strings.NewReader(`
|
||||
proxy_user=foo
|
||||
@@ -125,13 +185,21 @@ http_port=80
|
||||
defer mc.check(t)
|
||||
openReader = mc
|
||||
|
||||
proxyURL, err := synologyProxyFromConfig(req)
|
||||
httpProxy, httpsProxy, err := synologyProxiesFromConfig()
|
||||
|
||||
if got, want := err, openErr; got != want {
|
||||
t.Fatalf("got %s, want %s", got, want)
|
||||
}
|
||||
|
||||
if got, want := proxyURL, urlMustParse("https://foo:bar@10.0.0.66:8443"); got.String() != want.String() {
|
||||
if got, want := httpsProxy, urlMustParse("http://foo:bar@10.0.0.66:8443"); got.String() != want.String() {
|
||||
t.Fatalf("got %s, want %s", got, want)
|
||||
}
|
||||
|
||||
if got, want := err, openErr; got != want {
|
||||
t.Fatalf("got %s, want %s", got, want)
|
||||
}
|
||||
|
||||
if got, want := httpProxy, urlMustParse("http://foo:bar@10.0.0.55:80"); got.String() != want.String() {
|
||||
t.Fatalf("got %s, want %s", got, want)
|
||||
}
|
||||
|
||||
@@ -141,12 +209,15 @@ http_port=80
|
||||
openReader = nil
|
||||
openErr = os.ErrNotExist
|
||||
|
||||
proxyURL, err := synologyProxyFromConfig(req)
|
||||
httpProxy, httpsProxy, err := synologyProxiesFromConfig()
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %s", err)
|
||||
}
|
||||
if proxyURL != nil {
|
||||
t.Fatalf("expected no url, got %s", proxyURL)
|
||||
if httpProxy != nil {
|
||||
t.Fatalf("expected no url, got %s", httpProxy)
|
||||
}
|
||||
if httpsProxy != nil {
|
||||
t.Fatalf("expected no url, got %s", httpsProxy)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -154,12 +225,15 @@ http_port=80
|
||||
openReader = nil
|
||||
openErr = errors.New("example error")
|
||||
|
||||
proxyURL, err := synologyProxyFromConfig(req)
|
||||
httpProxy, httpsProxy, err := synologyProxiesFromConfig()
|
||||
if err != openErr {
|
||||
t.Fatalf("expected %s, got %s", openErr, err)
|
||||
}
|
||||
if proxyURL != nil {
|
||||
t.Fatalf("expected no url, got %s", proxyURL)
|
||||
if httpProxy != nil {
|
||||
t.Fatalf("expected no url, got %s", httpProxy)
|
||||
}
|
||||
if httpsProxy != nil {
|
||||
t.Fatalf("expected no url, got %s", httpsProxy)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -167,9 +241,10 @@ http_port=80
|
||||
|
||||
func TestParseSynologyConfig(t *testing.T) {
|
||||
cases := map[string]struct {
|
||||
input string
|
||||
url *url.URL
|
||||
err error
|
||||
input string
|
||||
httpProxy *url.URL
|
||||
httpsProxy *url.URL
|
||||
err error
|
||||
}{
|
||||
"populated": {
|
||||
input: `
|
||||
@@ -184,8 +259,9 @@ https_port=8443
|
||||
http_host=10.0.0.55
|
||||
http_port=80
|
||||
`,
|
||||
url: urlMustParse("https://foo:bar@10.0.0.66:8443"),
|
||||
err: nil,
|
||||
httpProxy: urlMustParse("http://foo:bar@10.0.0.55:80"),
|
||||
httpsProxy: urlMustParse("http://foo:bar@10.0.0.66:8443"),
|
||||
err: nil,
|
||||
},
|
||||
"no-auth": {
|
||||
input: `
|
||||
@@ -200,10 +276,11 @@ https_port=8443
|
||||
http_host=10.0.0.55
|
||||
http_port=80
|
||||
`,
|
||||
url: urlMustParse("https://10.0.0.66:8443"),
|
||||
err: nil,
|
||||
httpProxy: urlMustParse("http://10.0.0.55:80"),
|
||||
httpsProxy: urlMustParse("http://10.0.0.66:8443"),
|
||||
err: nil,
|
||||
},
|
||||
"http": {
|
||||
"http-only": {
|
||||
input: `
|
||||
proxy_user=foo
|
||||
proxy_pwd=bar
|
||||
@@ -216,8 +293,9 @@ https_port=8443
|
||||
http_host=10.0.0.55
|
||||
http_port=80
|
||||
`,
|
||||
url: urlMustParse("http://foo:bar@10.0.0.55:80"),
|
||||
err: nil,
|
||||
httpProxy: urlMustParse("http://foo:bar@10.0.0.55:80"),
|
||||
httpsProxy: nil,
|
||||
err: nil,
|
||||
},
|
||||
"empty": {
|
||||
input: `
|
||||
@@ -232,14 +310,15 @@ https_port=
|
||||
http_host=
|
||||
http_port=
|
||||
`,
|
||||
url: nil,
|
||||
err: nil,
|
||||
httpProxy: nil,
|
||||
httpsProxy: nil,
|
||||
err: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for name, example := range cases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
url, err := parseSynologyConfig(strings.NewReader(example.input))
|
||||
httpProxy, httpsProxy, err := parseSynologyConfig(strings.NewReader(example.input))
|
||||
if err != example.err {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -247,18 +326,32 @@ http_port=
|
||||
return
|
||||
}
|
||||
|
||||
if url == nil && example.url == nil {
|
||||
return
|
||||
if example.httpProxy == nil && httpProxy != nil {
|
||||
t.Fatalf("got %s, want nil", httpProxy)
|
||||
}
|
||||
|
||||
if example.url == nil {
|
||||
if url != nil {
|
||||
t.Fatalf("got %s, want nil", url)
|
||||
if example.httpProxy != nil {
|
||||
if httpProxy == nil {
|
||||
t.Fatalf("got nil, want %s", example.httpProxy)
|
||||
}
|
||||
|
||||
if got, want := example.httpProxy.String(), httpProxy.String(); got != want {
|
||||
t.Fatalf("got %s, want %s", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
if got, want := example.url.String(), url.String(); got != want {
|
||||
t.Fatalf("got %s, want %s", got, want)
|
||||
if example.httpsProxy == nil && httpsProxy != nil {
|
||||
t.Fatalf("got %s, want nil", httpProxy)
|
||||
}
|
||||
|
||||
if example.httpsProxy != nil {
|
||||
if httpsProxy == nil {
|
||||
t.Fatalf("got nil, want %s", example.httpsProxy)
|
||||
}
|
||||
|
||||
if got, want := example.httpsProxy.String(), httpsProxy.String(); got != want {
|
||||
t.Fatalf("got %s, want %s", got, want)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
@@ -19,7 +18,6 @@ import (
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// TODO(apenwarr): handle magic cookie auth
|
||||
@@ -114,77 +112,30 @@ func socketPermissionsForOS() os.FileMode {
|
||||
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:
|
||||
// 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:
|
||||
//
|
||||
// * 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).
|
||||
// * 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
|
||||
// we're a suitably privileged user to have access the files on disk
|
||||
// which the Network/App Extension wrote.
|
||||
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 nil, fmt.Errorf("failed to find local Tailscale daemon: %w", err)
|
||||
}
|
||||
return connectMacTCP(port, token)
|
||||
}
|
||||
|
||||
// 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).
|
||||
func connectMacTCP(port int, token string) (net.Conn, error) {
|
||||
c, err := net.Dial("tcp", "localhost:"+strconv.Itoa(port))
|
||||
if err != nil {
|
||||
|
||||
@@ -105,6 +105,7 @@ func (s *Server) Close() error {
|
||||
s.shutdownCancel()
|
||||
s.lb.Shutdown()
|
||||
s.linkMon.Close()
|
||||
s.dialer.Close()
|
||||
s.localAPIListener.Close()
|
||||
|
||||
s.mu.Lock()
|
||||
@@ -137,8 +138,9 @@ func (s *Server) start() error {
|
||||
}
|
||||
|
||||
s.rootPath = s.Dir
|
||||
if s.Store != nil && !s.Ephemeral {
|
||||
if _, ok := s.Store.(*mem.Store); !ok {
|
||||
if s.Store != nil {
|
||||
_, isMemStore := s.Store.(*mem.Store)
|
||||
if isMemStore && !s.Ephemeral {
|
||||
return fmt.Errorf("in-memory store is only supported for Ephemeral nodes")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
// process and can cache a prior success when a dependency changes.
|
||||
_ "golang.org/x/sys/windows"
|
||||
_ "golang.org/x/sys/windows/svc"
|
||||
_ "golang.org/x/sys/windows/svc/eventlog"
|
||||
_ "golang.org/x/sys/windows/svc/mgr"
|
||||
_ "golang.zx2c4.com/wireguard/windows/tunnel/winipcfg"
|
||||
_ "inet.af/netaddr"
|
||||
|
||||
@@ -115,7 +115,8 @@ func addrType(addrs []route.Addr, rtaxType int) route.Addr {
|
||||
func (m *darwinRouteMon) IsInterestingInterface(iface string) bool {
|
||||
baseName := strings.TrimRight(iface, "0123456789")
|
||||
switch baseName {
|
||||
case "llw", "awdl", "pdp_ip", "ipsec":
|
||||
// TODO(maisem): figure out what this list should actually be.
|
||||
case "llw", "awdl", "ipsec":
|
||||
return false
|
||||
}
|
||||
return true
|
||||
|
||||
@@ -651,6 +651,21 @@ func (ns *Impl) acceptTCP(r *tcp.ForwarderRequest) {
|
||||
}
|
||||
r.Complete(false)
|
||||
|
||||
// SetKeepAlive so that idle connections to peers that have forgotten about
|
||||
// the connection or gone completely offline eventually time out.
|
||||
// Applications might be setting this on a forwarded connection, but from
|
||||
// userspace we can not see those, so the best we can do is to always
|
||||
// perform them with conservative timing.
|
||||
// TODO(tailscale/tailscale#4522): Netstack defaults match the Linux
|
||||
// defaults, and results in a little over two hours before the socket would
|
||||
// be closed due to keepalive. A shorter default might be better, or seeking
|
||||
// a default from the host IP stack. This also might be a useful
|
||||
// user-tunable, as in userspace mode this can have broad implications such
|
||||
// as lingering connections to fork style daemons. On the other side of the
|
||||
// fence, the long duration timers are low impact values for battery powered
|
||||
// peers.
|
||||
ep.SocketOptions().SetKeepAlive(true)
|
||||
|
||||
// The ForwarderRequest.CreateEndpoint above asynchronously
|
||||
// starts the TCP handshake. Note that the gonet.TCPConn
|
||||
// methods c.RemoteAddr() and c.LocalAddr() will return nil
|
||||
|
||||
Reference in New Issue
Block a user