Compare commits

...

19 Commits

Author SHA1 Message Date
Brad Fitzpatrick dce2409b15 VERSION.txt: this is v1.24.2
Change-Id: I8905ed336c0b998776aede903436004e38ca280a
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2 years ago
Maisem Ali 258f251af3 go.mod: bump github.com/tailscale/golang-x-crypto/ssh
Updates #3802

Change-Id: If3b5fe42e7dd7858dcce02a3a24a5e59736815a2
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
(cherry picked from commit 6f5b91c94c
 ... which had a git rebase mistake where the wrong commit message was used)
2 years ago
James Tucker 4a1f4c2cad net/tshttpproxy: synology: pick proxy by scheme
This updates the fix from #4562 to pick the proxy based on the request
scheme.

Updates #4395, #2605, #4562
Signed-off-by: James Tucker <james@tailscale.com>
(cherry picked from commit 96fec4b969)
2 years ago
Maisem Ali 5651fa1e60 net/tshttpproxy: use http as the scheme for proxies
Currently we try to use `https://` when we see `https_host`, however
that doesn't work and results in errors like `Received error: fetch
control key: Get "https://controlplane.tailscale.com/key?v=32":
proxyconnect tcp: tls: first record does not look like a TLS handshake`

This indiciates that we are trying to do a HTTPS request to a HTTP
server. Googling suggests that the standard is to use `http` regardless
of `https` or `http` proxy

Updates #4395, #2605

Signed-off-by: Maisem Ali <maisem@tailscale.com>
(cherry picked from commit eff6a404a6)
2 years ago
Brad Fitzpatrick 73f169e8f5 control/controlhttp: don't assume port 80 upgrade response will work
Just because we get an HTTP upgrade response over port 80, don't
assume we'll be able to do bi-di Noise over it. There might be a MITM
corp proxy or anti-virus/firewall interfering. Do a bit more work to
validate the connection before proceeding to give up on the TLS port
443 dial.

Updates #4557 (probably fixes)

Change-Id: I0e1bcc195af21ad3d360ffe79daead730dfd86f1
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
(cherry picked from commit 1237000efe)
2 years ago
Brad Fitzpatrick 759a2bd546 VERSION.txt: this is v1.24.1
Change-Id: Iee1c07bdca08609117153d42a0cd46b034222524
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2 years ago
Maisem Ali c0746cf25c net/tsdial: add SystemDial as a wrapper on netns.Dial
The connections returned from SystemDial are automatically closed when
there is a major link change.

Also plumb through the dialer to the noise client so that connections
are auto-reset when moving from cellular to WiFi etc.

Updates #3363

Signed-off-by: Maisem Ali <maisem@tailscale.com>
(cherry picked from commit 5a1ef1bbb9)
2 years ago
Brad Fitzpatrick 5ff23cb1ce control/controlhttp: start port 443 fallback sooner if 80's stuck
Fixes #4544

Change-Id: I39877e71915ad48c6668351c45cd8e33e2f5dbae
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
(cherry picked from commit e38d3dfc76)
2 years ago
Maisem Ali 497fab5640 ipn/ipnlocal/peerapi: add endpoint to list local interfaces
Signed-off-by: Maisem Ali <maisem@tailscale.com>
(cherry picked from commit 637cc1b5fc)
2 years ago
James Tucker 2226bd99fa wgengine/netstack: always set TCP keepalive
Setting keepalive ensures that idle connections will eventually be
closed. In userspace mode, any application configured TCP keepalive is
effectively swallowed by the host kernel, and is not easy to detect.
Failure to close connections when a peer tailscaled goes offline or
restarts may result in an otherwise indefinite connection for any
protocol endpoint that does not initiate new traffic.

This patch does not take any new opinion on a sensible default for the
keepalive timers, though as noted in the TODO, doing so likely deserves
further consideration.

Update #4522

Signed-off-by: James Tucker <james@tailscale.com>
(cherry picked from commit 1aa75b1c9e)
2 years ago
Brad Fitzpatrick 465642b249 control/controlclient: fix log print with always-empty key
In debugging #4541, I noticed this log print was always empty.
The value printed was always zero at this point.

Updates #4541

Change-Id: I0eef60c32717c293c1c853879446be65d9b2cef6
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
(cherry picked from commit adcb7e59d2)
2 years ago
Brad Fitzpatrick a51123022a ipn: always treat login.tailscale.com as controlplane.tailscale.com
Like 888e50e1, but more aggressive.

Updates #4538 (likely fixes)
Updates #3488

Change-Id: I3924eee9110e47bdba926ce12954253bf2413040
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
(cherry picked from commit 3f7cc3563f)
2 years ago
Brad Fitzpatrick f90052c86c net/tshttpproxy: fix typo
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
(cherry picked from commit c6c752cf64)
2 years ago
Brad Fitzpatrick 4be1222701 cmd/tailscale: mostly fix 'tailscale ssh' on macOS (sandbox)
Still a little wonky, though. See the tcsetattr error and inability to
hit Ctrl-D, for instance:

    bradfitz@laptop ~ % tailscale.app ssh foo@bar
    tcsetattr: Operation not permitted
    # Authentication checked with Tailscale SSH.
    # Time since last authentication: 1h13m22s
    foo@bar:~$ ^D
    ^D
    ^D

Updates #4518
Updates #4529

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
(cherry picked from commit 50eb8c5add)
2 years ago
Brad Fitzpatrick 70a6d87b16 safesocket: fix CLI on standalone mac GUI build
Tested three macOS Tailscale daemons:

- App Store (Network Extension)
- Standalone (macsys)
- tailscaled

And two types of local IPC each:

- IPN
- HTTP

And two CLI modes:

- sandboxed (running the GUI binary as the CLI; normal way)
- open source CLI hitting GUI (with #4525)

Bonus: simplifies the code.

Fixes tailscale/corp#4559

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
(cherry picked from commit 21413392cf)
2 years ago
James Tucker de1ebee14f cmd/tailscale: s/-authkey/-auth-key/ in help text
Signed-off-by: James Tucker <james@tailscale.com>
(cherry picked from commit 928d1fddd2)
2 years ago
Maisem Ali 3a7f71df63 wgengine/monitor: do not ignore changes to pdp_ip*
One current theory (among other things) on battery consumption is that
magicsock is resorting to using the IPv6 over LTE even on WiFi.
One thing that could explain this is that we do not get link change updates
for the LTE modem as we ignore them in this list.
This commit makes us not ignore changes to `pdp_ip` as a test.

Updates #3363

Signed-off-by: Maisem Ali <maisem@tailscale.com>
(cherry picked from commit 80ba161c40)
2 years ago
Maisem Ali b16e27db9e tsnet: fix mem.Store check for normal nodes
There was a typo in the check it was doing `!ok` instead of `ok`, this
restructures it a bit to read better.

Fixes #4506

Signed-off-by: Maisem Ali <maisem@tailscale.com>
(cherry picked from commit bbca2c78cb)
2 years ago
Denton Gentry f0e71f4a20 VERSION.txt: this is v1.24.0
Signed-off-by: Denton Gentry <dgentry@tailscale.com>
2 years ago

@ -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",
)
// 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)
argv = append(argv, username+"@"+hostForSSH)
argv = append(argv, argRest...)
if envknob.Bool("TS_DEBUG_SSH_EXEC") {
log.Printf("Running: %q, %q ...", ssh, 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...)
if runtime.GOOS == "windows" || version.IsSandboxedMacOS() {
// Don't use syscall.Exec on Windows or in the macOS sandbox.
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.)
`),

@ -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)

@ -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
}
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()
u := &url.URL{
// 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
u443 := &url.URL{
Scheme: "https",
Host: net.JoinHostPort(a.host, a.httpsPort),
Path: serverUpgradePath,
}
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)
}
}
return ret, nil
}
}
// 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)
// 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()
}

@ -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

@ -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"
httpsProxyURL = addHostPort(httpsProxyURL, cfg["https_host"], cfg["https_port"])
httpProxyURL = addHostPort(httpProxyURL, cfg["http_host"], cfg["http_port"])
return httpProxyURL, httpsProxyURL, nil
}
// 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, nil
return nil
}
if port != "" {
proxyURL.Host = net.JoinHostPort(host, port)
if port == "" {
u.Host = host
} else {
proxyURL.Host = host
u.Host = net.JoinHostPort(host, port)
}
return proxyURL, nil
return u
}
// mtime stat's path and returns it's modification time. If path does not exist,
// 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 example.httpsProxy == nil && httpsProxy != nil {
t.Fatalf("got %s, want nil", httpProxy)
}
if got, want := example.url.String(), url.String(); got != want {
t.Fatalf("got %s, want %s", got, want)
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")
}
}

@ -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

Loading…
Cancel
Save