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/client/tailscale"
"tailscale.com/envknob" "tailscale.com/envknob"
"tailscale.com/ipn/ipnstate" "tailscale.com/ipn/ipnstate"
"tailscale.com/version"
) )
var sshCmd = &ffcli.Command{ var sshCmd = &ffcli.Command{
@ -76,36 +75,52 @@ func runSSH(ctx context.Context, args []string) error {
return err return err
} }
argv := append([]string{ argv := []string{ssh}
ssh,
if envknob.Bool("TS_DEBUG_SSH_EXEC") {
argv = append(argv, "-vvv")
}
argv = append(argv,
// Only trust SSH hosts that we know about. // Only trust SSH hosts that we know about.
"-o", fmt.Sprintf("UserKnownHostsFile %q", knownHostsFile), "-o", fmt.Sprintf("UserKnownHostsFile %q", knownHostsFile),
"-o", "UpdateHostKeys no", "-o", "UpdateHostKeys no",
"-o", "StrictHostKeyChecking yes", "-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", if runtime.GOOS == "windows" {
tailscaleBin, // Don't use syscall.Exec on Windows.
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.
cmd := exec.Command(ssh, argv[1:]...) cmd := exec.Command(ssh, argv[1:]...)
cmd.Stderr = os.Stderr cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr cmd.Stderr = os.Stderr
var ee *exec.ExitError var ee *exec.ExitError
@ -116,9 +131,6 @@ func runSSH(ctx context.Context, args []string) error {
return err 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 { if err := syscall.Exec(ssh, argv, os.Environ()); err != nil {
return err return err
} }

@ -52,7 +52,7 @@ down").
If flags are specified, the flags must be the complete set of desired 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 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 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 considered settings that need to be re-specified when modifying
settings.) settings.)
`), `),

@ -332,6 +332,7 @@ func run() error {
socksListener, httpProxyListener := mustStartProxyListeners(args.socksAddr, args.httpProxyAddr) socksListener, httpProxyListener := mustStartProxyListeners(args.socksAddr, args.httpProxyAddr)
dialer := new(tsdial.Dialer) // mutated below (before used) dialer := new(tsdial.Dialer) // mutated below (before used)
dialer.Logf = logf
e, useNetstack, err := createEngine(logf, linkMon, dialer) e, useNetstack, err := createEngine(logf, linkMon, dialer)
if err != nil { if err != nil {
return fmt.Errorf("createEngine: %w", err) return fmt.Errorf("createEngine: %w", err)
@ -394,6 +395,7 @@ func run() error {
// want to keep running. // want to keep running.
signal.Ignore(syscall.SIGPIPE) signal.Ignore(syscall.SIGPIPE)
go func() { go func() {
defer dialer.Close()
select { select {
case s := <-interrupt: case s := <-interrupt:
logf("tailscaled got signal %v; shutting down", s) logf("tailscaled got signal %v; shutting down", s)

@ -38,9 +38,9 @@ import (
"tailscale.com/net/dnscache" "tailscale.com/net/dnscache"
"tailscale.com/net/dnsfallback" "tailscale.com/net/dnsfallback"
"tailscale.com/net/interfaces" "tailscale.com/net/interfaces"
"tailscale.com/net/netns"
"tailscale.com/net/netutil" "tailscale.com/net/netutil"
"tailscale.com/net/tlsdial" "tailscale.com/net/tlsdial"
"tailscale.com/net/tsdial"
"tailscale.com/net/tshttpproxy" "tailscale.com/net/tshttpproxy"
"tailscale.com/tailcfg" "tailscale.com/tailcfg"
"tailscale.com/types/key" "tailscale.com/types/key"
@ -57,7 +57,8 @@ import (
// Direct is the client that connects to a tailcontrol server for a node. // Direct is the client that connects to a tailcontrol server for a node.
type Direct struct { type Direct struct {
httpc *http.Client // HTTP client used to talk to tailcontrol 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 timeNow func() time.Time
lastPrintMap time.Time lastPrintMap time.Time
newDecompressor func() (Decompressor, error) newDecompressor func() (Decompressor, error)
@ -106,6 +107,7 @@ type Options struct {
DebugFlags []string // debug settings to send to control DebugFlags []string // debug settings to send to control
LinkMonitor *monitor.Mon // optional link monitor LinkMonitor *monitor.Mon // optional link monitor
PopBrowserURL func(url string) // optional func to open browser PopBrowserURL func(url string) // optional func to open browser
Dialer *tsdial.Dialer // non-nil
// KeepSharerAndUserSplit controls whether the client // KeepSharerAndUserSplit controls whether the client
// understands Node.Sharer. If false, the Sharer is mapped to the User. // understands Node.Sharer. If false, the Sharer is mapped to the User.
@ -170,13 +172,12 @@ func NewDirect(opts Options) (*Direct, error) {
UseLastGood: true, UseLastGood: true,
LookupIPFallback: dnsfallback.Lookup, LookupIPFallback: dnsfallback.Lookup,
} }
dialer := netns.NewDialer(opts.Logf)
tr := http.DefaultTransport.(*http.Transport).Clone() tr := http.DefaultTransport.(*http.Transport).Clone()
tr.Proxy = tshttpproxy.ProxyFromEnvironment tr.Proxy = tshttpproxy.ProxyFromEnvironment
tshttpproxy.SetTransportGetProxyConnectHeader(tr) tshttpproxy.SetTransportGetProxyConnectHeader(tr)
tr.TLSClientConfig = tlsdial.Config(serverURL.Hostname(), tr.TLSClientConfig) tr.TLSClientConfig = tlsdial.Config(serverURL.Hostname(), tr.TLSClientConfig)
tr.DialContext = dnscache.Dialer(dialer.DialContext, dnsCache) tr.DialContext = dnscache.Dialer(opts.Dialer.SystemDial, dnsCache)
tr.DialTLSContext = dnscache.TLSDialer(dialer.DialContext, dnsCache, tr.TLSClientConfig) tr.DialTLSContext = dnscache.TLSDialer(opts.Dialer.SystemDial, dnsCache, tr.TLSClientConfig)
tr.ForceAttemptHTTP2 = true tr.ForceAttemptHTTP2 = true
// Disable implicit gzip compression; the various // Disable implicit gzip compression; the various
// handlers (register, map, set-dns, etc) do their own // handlers (register, map, set-dns, etc) do their own
@ -202,6 +203,7 @@ func NewDirect(opts Options) (*Direct, error) {
skipIPForwardingCheck: opts.SkipIPForwardingCheck, skipIPForwardingCheck: opts.SkipIPForwardingCheck,
pinger: opts.Pinger, pinger: opts.Pinger,
popBrowser: opts.PopBrowserURL, popBrowser: opts.PopBrowserURL,
dialer: opts.Dialer,
} }
if opts.Hostinfo == nil { if opts.Hostinfo == nil {
c.SetHostinfo(hostinfo.New()) c.SetHostinfo(hostinfo.New())
@ -376,7 +378,7 @@ func (c *Direct) doLogin(ctx context.Context, opt loginOpt) (mustRegen bool, new
if err != nil { if err != nil {
return regen, opt.URL, err 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.mu.Lock()
c.serverKey = keys.LegacyPublicKey c.serverKey = keys.LegacyPublicKey
@ -1278,7 +1280,7 @@ func (c *Direct) getNoiseClient() (*noiseClient, error) {
return nil, err return nil, err
} }
nc, err = newNoiseClient(k, serverNoiseKey, c.serverURL) nc, err = newNoiseClient(k, serverNoiseKey, c.serverURL, c.dialer)
if err != nil { if err != nil {
return nil, err return nil, err
} }

@ -14,6 +14,7 @@ import (
"inet.af/netaddr" "inet.af/netaddr"
"tailscale.com/hostinfo" "tailscale.com/hostinfo"
"tailscale.com/ipn/ipnstate" "tailscale.com/ipn/ipnstate"
"tailscale.com/net/tsdial"
"tailscale.com/tailcfg" "tailscale.com/tailcfg"
"tailscale.com/types/key" "tailscale.com/types/key"
) )
@ -30,6 +31,7 @@ func TestNewDirect(t *testing.T) {
GetMachinePrivateKey: func() (key.MachinePrivate, error) { GetMachinePrivateKey: func() (key.MachinePrivate, error) {
return k, nil return k, nil
}, },
Dialer: new(tsdial.Dialer),
} }
c, err := NewDirect(opts) c, err := NewDirect(opts)
if err != nil { if err != nil {
@ -106,6 +108,7 @@ func TestTsmpPing(t *testing.T) {
GetMachinePrivateKey: func() (key.MachinePrivate, error) { GetMachinePrivateKey: func() (key.MachinePrivate, error) {
return k, nil return k, nil
}, },
Dialer: new(tsdial.Dialer),
} }
c, err := NewDirect(opts) c, err := NewDirect(opts)

@ -18,6 +18,7 @@ import (
"golang.org/x/net/http2" "golang.org/x/net/http2"
"tailscale.com/control/controlbase" "tailscale.com/control/controlbase"
"tailscale.com/control/controlhttp" "tailscale.com/control/controlhttp"
"tailscale.com/net/tsdial"
"tailscale.com/tailcfg" "tailscale.com/tailcfg"
"tailscale.com/types/key" "tailscale.com/types/key"
"tailscale.com/util/mak" "tailscale.com/util/mak"
@ -46,6 +47,7 @@ func (c *noiseConn) Close() error {
// the ts2021 protocol. // the ts2021 protocol.
type noiseClient struct { type noiseClient struct {
*http.Client // HTTP client used to talk to tailcontrol *http.Client // HTTP client used to talk to tailcontrol
dialer *tsdial.Dialer
privKey key.MachinePrivate privKey key.MachinePrivate
serverPubKey key.MachinePublic serverPubKey key.MachinePublic
serverHost string // the host:port part of serverURL 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. // newNoiseClient returns a new noiseClient for the provided server and machine key.
// serverURL is of the form https://<host>:<port> (no trailing slash). // 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) u, err := url.Parse(serverURL)
if err != nil { if err != nil {
return nil, err return nil, err
@ -75,6 +77,7 @@ func newNoiseClient(priKey key.MachinePrivate, serverPubKey key.MachinePublic, s
serverPubKey: serverPubKey, serverPubKey: serverPubKey,
privKey: priKey, privKey: priKey,
serverHost: host, serverHost: host,
dialer: dialer,
} }
// Create the HTTP/2 Transport using a net/http.Transport // 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. // thousand version numbers before getting to this point.
panic("capability version is too high to fit in the wire protocol") 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 { if err != nil {
return nil, err return nil, err
} }

@ -25,16 +25,15 @@ import (
"errors" "errors"
"fmt" "fmt"
"io" "io"
"log"
"net" "net"
"net/http" "net/http"
"net/http/httptrace" "net/http/httptrace"
"net/url" "net/url"
"time"
"tailscale.com/control/controlbase" "tailscale.com/control/controlbase"
"tailscale.com/net/dnscache" "tailscale.com/net/dnscache"
"tailscale.com/net/dnsfallback" "tailscale.com/net/dnsfallback"
"tailscale.com/net/netns"
"tailscale.com/net/netutil" "tailscale.com/net/netutil"
"tailscale.com/net/tlsdial" "tailscale.com/net/tlsdial"
"tailscale.com/net/tshttpproxy" "tailscale.com/net/tshttpproxy"
@ -65,13 +64,12 @@ const (
// //
// The provided ctx is only used for the initial connection, until // The provided ctx is only used for the initial connection, until
// Dial returns. It does not affect the connection once established. // 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) host, port, err := net.SplitHostPort(addr)
if err != nil { if err != nil {
return nil, err return nil, err
} }
a := &dialParams{ a := &dialParams{
ctx: ctx,
host: host, host: host,
httpPort: port, httpPort: port,
httpsPort: "443", httpsPort: "443",
@ -79,12 +77,12 @@ func Dial(ctx context.Context, addr string, machineKey key.MachinePrivate, contr
controlKey: controlKey, controlKey: controlKey,
version: protocolVersion, version: protocolVersion,
proxyFunc: tshttpproxy.ProxyFromEnvironment, proxyFunc: tshttpproxy.ProxyFromEnvironment,
dialer: dialer,
} }
return a.dial() return a.dial(ctx)
} }
type dialParams struct { type dialParams struct {
ctx context.Context
host string host string
httpPort string httpPort string
httpsPort string httpsPort string
@ -92,65 +90,132 @@ type dialParams struct {
controlKey key.MachinePublic controlKey key.MachinePublic
version uint16 version uint16
proxyFunc func(*http.Request) (*url.URL, error) // or nil proxyFunc func(*http.Request) (*url.URL, error) // or nil
dialer dnscache.DialContextFunc
// For tests only // For tests only
insecureTLS bool insecureTLS bool
testFallbackDelay time.Duration
} }
func (a *dialParams) dial() (*controlbase.Conn, error) { // httpsFallbackDelay is how long we'll wait for a.httpPort to work before
init, cont, err := controlbase.ClientDeferred(a.machineKey, a.controlKey, a.version) // starting to try a.httpsPort.
if err != nil { func (a *dialParams) httpsFallbackDelay() time.Duration {
return nil, err 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", Scheme: "http",
Host: net.JoinHostPort(a.host, a.httpPort), Host: net.JoinHostPort(a.host, a.httpPort),
Path: serverUpgradePath, Path: serverUpgradePath,
} }
conn, httpErr := a.tryURL(u, init) u443 := &url.URL{
if httpErr == nil { Scheme: "https",
ret, err := cont(a.ctx, conn) Host: net.JoinHostPort(a.host, a.httpsPort),
if err != nil { Path: serverUpgradePath,
conn.Close() }
return nil, err
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 // dialURL attempts to connect to the given URL.
// being difficult and see if we can get through over HTTPS. func (a *dialParams) dialURL(ctx context.Context, u *url.URL) (*controlbase.Conn, error) {
u.Scheme = "https" init, cont, err := controlbase.ClientDeferred(a.machineKey, a.controlKey, a.version)
u.Host = net.JoinHostPort(a.host, a.httpsPort)
init, cont, err = controlbase.ClientDeferred(a.machineKey, a.controlKey, a.version)
if err != nil { if err != nil {
return nil, err return nil, err
} }
conn, tlsErr := a.tryURL(u, init) netConn, err := a.tryURLUpgrade(ctx, u, init)
if tlsErr == nil { if err != nil {
ret, err := cont(a.ctx, conn) return nil, err
if err != nil {
conn.Close()
return nil, err
}
return ret, nil
} }
cbConn, err := cont(ctx, netConn)
return nil, fmt.Errorf("all connection attempts failed (HTTP: %v, HTTPS: %v)", httpErr, tlsErr) 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{ dns := &dnscache.Resolver{
Forward: dnscache.Get().Forward, Forward: dnscache.Get().Forward,
LookupIPFallback: dnsfallback.Lookup, LookupIPFallback: dnsfallback.Lookup,
UseLastGood: true, UseLastGood: true,
} }
dialer := netns.NewDialer(log.Printf)
tr := http.DefaultTransport.(*http.Transport).Clone() tr := http.DefaultTransport.(*http.Transport).Clone()
defer tr.CloseIdleConnections() defer tr.CloseIdleConnections()
tr.Proxy = a.proxyFunc tr.Proxy = a.proxyFunc
tshttpproxy.SetTransportGetProxyConnectHeader(tr) 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. // Disable HTTP2, since h2 can't do protocol switching.
tr.TLSClientConfig.NextProtos = []string{} tr.TLSClientConfig.NextProtos = []string{}
tr.TLSNextProto = map[string]func(string, *tls.Conn) http.RoundTripper{} 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.InsecureSkipVerify = true
tr.TLSClientConfig.VerifyConnection = nil 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 tr.DisableCompression = true
// (mis)use httptrace to extract the underlying net.Conn from the // (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 connCh <- info.Conn
}, },
} }
ctx := httptrace.WithClientTrace(a.ctx, &trace) ctx = httptrace.WithClientTrace(ctx, &trace)
req := &http.Request{ req := &http.Request{
Method: "POST", Method: "POST",
URL: u, URL: u,

@ -17,22 +17,36 @@ import (
"strconv" "strconv"
"sync" "sync"
"testing" "testing"
"time"
"tailscale.com/control/controlbase" "tailscale.com/control/controlbase"
"tailscale.com/net/socks5" "tailscale.com/net/socks5"
"tailscale.com/net/tsdial"
"tailscale.com/types/key" "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) { func TestControlHTTP(t *testing.T) {
tests := []struct { tests := []httpTestParam{
name string
proxy proxy
}{
// direct connection // direct connection
{ {
name: "no_proxy", name: "no_proxy",
proxy: nil, proxy: nil,
}, },
// direct connection but port 80 is MITM'ed and broken
{
name: "port80_broken_mitm",
proxy: nil,
makeHTTPHangAfterUpgrade: true,
},
// SOCKS5 // SOCKS5
{ {
name: "socks5", name: "socks5",
@ -96,12 +110,13 @@ func TestControlHTTP(t *testing.T) {
for _, test := range tests { for _, test := range tests {
t.Run(test.name, func(t *testing.T) { 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() client, server := key.NewMachine(), key.NewMachine()
const testProtocolVersion = 1 const testProtocolVersion = 1
@ -132,7 +147,11 @@ func testControlHTTP(t *testing.T, proxy proxy) {
t.Fatalf("HTTPS listen: %v", err) 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) go httpServer.Serve(httpLn)
defer httpServer.Close() defer httpServer.Close()
@ -143,18 +162,24 @@ func testControlHTTP(t *testing.T, proxy proxy) {
go httpsServer.ServeTLS(httpsLn, "", "") go httpsServer.ServeTLS(httpsLn, "", "")
defer httpsServer.Close() defer httpsServer.Close()
//ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) ctx := context.Background()
//defer cancel() const debugTimeout = false
if debugTimeout {
var cancel context.CancelFunc
ctx, cancel = context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
}
a := dialParams{ a := dialParams{
ctx: context.Background(), //ctx, host: "localhost",
host: "localhost", httpPort: strconv.Itoa(httpLn.Addr().(*net.TCPAddr).Port),
httpPort: strconv.Itoa(httpLn.Addr().(*net.TCPAddr).Port), httpsPort: strconv.Itoa(httpsLn.Addr().(*net.TCPAddr).Port),
httpsPort: strconv.Itoa(httpsLn.Addr().(*net.TCPAddr).Port), machineKey: client,
machineKey: client, controlKey: server.Public(),
controlKey: server.Public(), version: testProtocolVersion,
version: testProtocolVersion, insecureTLS: true,
insecureTLS: true, dialer: new(tsdial.Dialer).SystemDial,
testFallbackDelay: 50 * time.Millisecond,
} }
if proxy != nil { 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 { if err != nil {
t.Fatalf("dialing controlhttp: %v", err) t.Fatalf("dialing controlhttp: %v", err)
} }
@ -215,6 +240,7 @@ type proxy interface {
type socksProxy struct { type socksProxy struct {
sync.Mutex sync.Mutex
closed bool
proxy socks5.Server proxy socks5.Server
ln net.Listener ln net.Listener
clientConnAddrs map[string]bool // addrs of the local end of outgoing conns from proxy 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.ln = ln
s.clientConnAddrs = map[string]bool{} 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 s.proxy.Dialer = s.dialAndRecord
go s.proxy.Serve(ln) go s.proxy.Serve(ln)
return fmt.Sprintf("socks5://%s", ln.Addr().String()) 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() { func (s *socksProxy) Close() {
s.Lock() s.Lock()
defer s.Unlock() defer s.Unlock()
if s.closed {
return
}
s.closed = true
s.ln.Close() s.ln.Close()
} }
@ -398,3 +435,11 @@ EKTcWGekdmdDPsHloRNtsiCa697B2O9IFA==
Certificates: []tls.Certificate{cert}, 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/certstore v0.1.1-0.20220316223106-78d6e1c49d8d
github.com/tailscale/depaware v0.0.0-20210622194025-720c4b409502 github.com/tailscale/depaware v0.0.0-20210622194025-720c4b409502
github.com/tailscale/goexpect v0.0.0-20210902213824-6e8c725cea41 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/goupnp v1.0.1-0.20210804011211-c64d0f06ea05
github.com/tailscale/hujson v0.0.0-20211105212140-3a0adc019d83 github.com/tailscale/hujson v0.0.0-20211105212140-3a0adc019d83
github.com/tailscale/netlink v1.1.1-0.20211101221916-cabfb018fe85 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/u-root/u-root v0.8.0
github.com/vishvananda/netlink v1.1.1-0.20211118161826-650dca95af54 github.com/vishvananda/netlink v1.1.1-0.20211118161826-650dca95af54
go4.org/mem v0.0.0-20210711025021-927187094b94 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/net v0.0.0-20220407224826-aac1ed45d8e3
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c 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/term v0.0.0-20210927222741-03fcf44c2211
golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11 golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11
golang.org/x/tools v0.1.11-0.20220413170336-afc6aad76eb1 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/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 h1:/V2rCMMWcsjYaYO2MeovLw+ClP63OtXgCF2Y1eb8+Ns=
github.com/tailscale/goexpect v0.0.0-20210902213824-6e8c725cea41/go.mod h1:/roCdA6gg6lQyw/Oz6gIIGu3ggJKYhF+WC/AQReE5XQ= 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-20220428210705-0b941c09a5e1 h1:vsFV6BKSIgjRd8m8UfrGW4r+cc28fRF71K6IRo46rKs=
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/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 h1:4chzWmimtJPxRs2O36yuGRW3f9SYV+bMTTvMBI0EKio=
github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05/go.mod h1:PdCqy9JzfWMJf1H5UJW2ip33/d4YkoKN0r67yKH1mG8= 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= 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-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-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-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-20220427172511-eb4f295cb31f h1:OeJjE6G4dgCY4PIXvIRQbE8+RX+uXZyGhUy/ksMGJoc=
golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= 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-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= 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-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-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-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-20220422013727-9388b58f7150 h1:xHms4gcpe1YE7A3yIllJXP16CMAGuqwO2lX1mTyyRRc=
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 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-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-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY= 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(), LinkMonitor: b.e.GetLinkMonitor(),
Pinger: b.e, Pinger: b.e,
PopBrowserURL: b.tellClientToBrowseToURL, PopBrowserURL: b.tellClientToBrowseToURL,
Dialer: b.Dialer(),
// Don't warn about broken Linux IP forwarding when // Don't warn about broken Linux IP forwarding when
// netstack is being used. // netstack is being used.

@ -563,6 +563,9 @@ func (h *peerAPIHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
case "/v0/dnsfwd": case "/v0/dnsfwd":
h.handleServeDNSFwd(w, r) h.handleServeDNSFwd(w, r)
return return
case "/v0/interfaces":
h.handleServeInterfaces(w, r)
return
} }
who := h.peerUser.DisplayName who := h.peerUser.DisplayName
fmt.Fprintf(w, `<html> 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 { type incomingFile struct {
name string // "foo.jpg" name string // "foo.jpg"
started time.Time started time.Time

@ -428,9 +428,14 @@ func NewPrefs() *Prefs {
} }
// ControlURLOrDefault returns the coordination server's URL base. // 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 { func (p *Prefs) ControlURLOrDefault() string {
if p.ControlURL != "" { if p.ControlURL != "" {
if p.ControlURL != DefaultControlURL && IsLoginServerSynonym(p.ControlURL) {
return DefaultControlURL
}
return p.ControlURL return p.ControlURL
} }
return DefaultControlURL 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" "inet.af/netaddr"
"tailscale.com/net/dnscache" "tailscale.com/net/dnscache"
"tailscale.com/net/interfaces"
"tailscale.com/net/netknob" "tailscale.com/net/netknob"
"tailscale.com/net/netns"
"tailscale.com/types/logger"
"tailscale.com/types/netmap" "tailscale.com/types/netmap"
"tailscale.com/util/mak"
"tailscale.com/wgengine/monitor" "tailscale.com/wgengine/monitor"
) )
@ -30,6 +34,7 @@ import (
// (TUN, netstack), the OS network sandboxing style (macOS/iOS // (TUN, netstack), the OS network sandboxing style (macOS/iOS
// Extension, none), user-selected route acceptance prefs, etc. // Extension, none), user-selected route acceptance prefs, etc.
type Dialer struct { type Dialer struct {
Logf logger.Logf
// UseNetstackForIP if non-nil is whether NetstackDialTCP (if // UseNetstackForIP if non-nil is whether NetstackDialTCP (if
// it's non-nil) should be used to dial the provided IP. // it's non-nil) should be used to dial the provided IP.
UseNetstackForIP func(netaddr.IP) bool UseNetstackForIP func(netaddr.IP) bool
@ -46,12 +51,33 @@ type Dialer struct {
peerDialerOnce sync.Once peerDialerOnce sync.Once
peerDialer *net.Dialer peerDialer *net.Dialer
mu sync.Mutex netnsDialerOnce sync.Once
dns dnsMap netnsDialer netns.Dialer
tunName string // tun device name
linkMon *monitor.Mon mu sync.Mutex
exitDNSDoHBase string // non-empty if DoH-proxying exit node in use; base URL+path (without '?') closed bool
dnsCache *dnscache.MessageCache // nil until first first non-empty SetExitDNSDoH 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", // 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) { func (d *Dialer) SetLinkMonitor(mon *monitor.Mon) {
d.mu.Lock() d.mu.Lock()
defer d.mu.Unlock() defer d.mu.Unlock()
if d.linkMonUnregister != nil {
go d.linkMonUnregister()
d.linkMonUnregister = nil
}
d.linkMon = mon 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) { func (d *Dialer) interfaceIndexLocked(ifName string) (index int, ok bool) {
@ -197,6 +266,42 @@ func ipNetOfNetwork(n string) string {
return "ip" 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. // UserDial connects to the provided network address as if a user were initiating the dial.
// (e.g. from a SOCKS or HTTP outbound proxy) // (e.g. from a SOCKS or HTTP outbound proxy)
func (d *Dialer) UserDial(ctx context.Context, network, addr string) (net.Conn, error) { func (d *Dialer) UserDial(ctx context.Context, network, addr string) (net.Conn, error) {

@ -33,8 +33,9 @@ var (
var cache struct { var cache struct {
sync.Mutex sync.Mutex
proxy *url.URL httpProxy *url.URL
updated time.Time httpsProxy *url.URL
updated time.Time
} }
func synologyProxyFromConfigCached(req *http.Request) (*url.URL, error) { func synologyProxyFromConfigCached(req *http.Request) (*url.URL, error) {
@ -45,34 +46,36 @@ func synologyProxyFromConfigCached(req *http.Request) (*url.URL, error) {
cache.Lock() cache.Lock()
defer cache.Unlock() defer cache.Unlock()
var err error
modtime := mtime(synologyProxyConfigPath) modtime := mtime(synologyProxyConfigPath)
if cache.updated == modtime { if modtime != cache.updated {
return cache.proxy, nil cache.httpProxy, cache.httpsProxy, err = synologyProxiesFromConfig()
cache.updated = modtime
} }
val, err := synologyProxyFromConfig(req) if req.URL.Scheme == "https" {
cache.proxy = val return cache.httpsProxy, err
}
cache.updated = modtime return cache.httpProxy, err
return val, err
} }
func synologyProxyFromConfig(req *http.Request) (*url.URL, error) { func synologyProxiesFromConfig() (*url.URL, *url.URL, error) {
r, err := openSynologyProxyConf() r, err := openSynologyProxyConf()
if err != nil { if err != nil {
if os.IsNotExist(err) { if os.IsNotExist(err) {
return nil, nil return nil, nil, nil
} }
return nil, err return nil, nil, err
} }
defer r.Close() defer r.Close()
return parseSynologyConfig(r) 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{} cfg := map[string]string{}
if err := lineread.Reader(r, func(line []byte) error { 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) cfg[string(key)] = string(value)
return nil return nil
}); err != nil { }); err != nil {
return nil, err return nil, nil, err
} }
if cfg["proxy_enabled"] != "yes" { 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" { 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" // As far as we are aware, synology does not support tls proxies.
host, port := cfg["https_host"], cfg["https_port"] httpProxyURL.Scheme = "http"
if host == "" { httpsProxyURL.Scheme = "http"
proxyURL.Scheme = "http"
host, port = cfg["http_host"], cfg["http_port"]
}
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 == "" { if host == "" {
return nil, nil return nil
} }
if port != "" { if port == "" {
proxyURL.Host = net.JoinHostPort(host, port) u.Host = host
} else { } else {
proxyURL.Host = host u.Host = net.JoinHostPort(host, port)
} }
return u
return proxyURL, nil
} }
// 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. // it returns the unix epoch.
func mtime(path string) time.Time { func mtime(path string) time.Time {
fi, err := os.Stat(path) fi, err := os.Stat(path)

@ -22,7 +22,7 @@ import (
) )
func TestSynologyProxyFromConfigCached(t *testing.T) { 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 { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -37,7 +37,8 @@ func TestSynologyProxyFromConfigCached(t *testing.T) {
} }
cache.updated = time.Time{} cache.updated = time.Time{}
cache.proxy = nil cache.httpProxy = nil
cache.httpsProxy = nil
if val, err := synologyProxyFromConfigCached(req); val != nil || err != nil { if val, err := synologyProxyFromConfigCached(req); val != nil || err != nil {
t.Fatalf("got %s, %v; want nil, nil", val, err) 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 { if got, want := cache.updated, time.Unix(0, 0); got != want {
t.Fatalf("got %s, want %s", got, want) t.Fatalf("got %s, want %s", got, want)
} }
if cache.proxy != nil { if cache.httpProxy != nil {
t.Fatalf("got %s, want nil", cache.proxy) 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) { t.Run("config file updated", func(t *testing.T) {
cache.updated = time.Now() cache.updated = time.Now()
cache.proxy = nil cache.httpProxy = nil
cache.httpsProxy = nil
if err := ioutil.WriteFile(synologyProxyConfigPath, []byte(` if err := ioutil.WriteFile(synologyProxyConfigPath, []byte(`
proxy_enabled=yes proxy_enabled=yes
http_host=10.0.0.55 http_host=10.0.0.55
http_port=80 http_port=80
https_host=10.0.0.66
https_port=443
`), 0600); err != nil { `), 0600); err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -67,6 +74,14 @@ http_port=80
if err != nil { if err != nil {
t.Fatal(err) 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() { if want := urlMustParse("http://10.0.0.55:80"); val.String() != want.String() {
t.Fatalf("got %s; want %s", val, want) t.Fatalf("got %s; want %s", val, want)
} }
@ -74,7 +89,8 @@ http_port=80
t.Run("config file removed", func(t *testing.T) { t.Run("config file removed", func(t *testing.T) {
cache.updated = time.Now() 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) { if err := os.Remove(synologyProxyConfigPath); err != nil && !os.IsNotExist(err) {
t.Fatal(err) t.Fatal(err)
@ -87,13 +103,62 @@ http_port=80
if val != nil { if val != nil {
t.Fatalf("got %s; want nil", val) t.Fatalf("got %s; want nil", val)
} }
if cache.proxy != nil { if cache.httpProxy != nil {
t.Fatalf("got %s, want nil", cache.proxy) 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 ( var (
openReader io.ReadCloser openReader io.ReadCloser
openErr error openErr error
@ -104,11 +169,6 @@ func TestSynologyProxyFromConfig(t *testing.T) {
} }
defer func() { openSynologyProxyConf = origOpen }() 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) { t.Run("with config", func(t *testing.T) {
mc := &mustCloser{Reader: strings.NewReader(` mc := &mustCloser{Reader: strings.NewReader(`
proxy_user=foo proxy_user=foo
@ -125,13 +185,21 @@ http_port=80
defer mc.check(t) defer mc.check(t)
openReader = mc openReader = mc
proxyURL, err := synologyProxyFromConfig(req) httpProxy, httpsProxy, err := synologyProxiesFromConfig()
if got, want := err, openErr; got != want { if got, want := err, openErr; got != want {
t.Fatalf("got %s, want %s", 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) t.Fatalf("got %s, want %s", got, want)
} }
@ -141,12 +209,15 @@ http_port=80
openReader = nil openReader = nil
openErr = os.ErrNotExist openErr = os.ErrNotExist
proxyURL, err := synologyProxyFromConfig(req) httpProxy, httpsProxy, err := synologyProxiesFromConfig()
if err != nil { if err != nil {
t.Fatalf("expected no error, got %s", err) t.Fatalf("expected no error, got %s", err)
} }
if proxyURL != nil { if httpProxy != nil {
t.Fatalf("expected no url, got %s", proxyURL) 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 openReader = nil
openErr = errors.New("example error") openErr = errors.New("example error")
proxyURL, err := synologyProxyFromConfig(req) httpProxy, httpsProxy, err := synologyProxiesFromConfig()
if err != openErr { if err != openErr {
t.Fatalf("expected %s, got %s", openErr, err) t.Fatalf("expected %s, got %s", openErr, err)
} }
if proxyURL != nil { if httpProxy != nil {
t.Fatalf("expected no url, got %s", proxyURL) 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) { func TestParseSynologyConfig(t *testing.T) {
cases := map[string]struct { cases := map[string]struct {
input string input string
url *url.URL httpProxy *url.URL
err error httpsProxy *url.URL
err error
}{ }{
"populated": { "populated": {
input: ` input: `
@ -184,8 +259,9 @@ https_port=8443
http_host=10.0.0.55 http_host=10.0.0.55
http_port=80 http_port=80
`, `,
url: urlMustParse("https://foo:bar@10.0.0.66:8443"), httpProxy: urlMustParse("http://foo:bar@10.0.0.55:80"),
err: nil, httpsProxy: urlMustParse("http://foo:bar@10.0.0.66:8443"),
err: nil,
}, },
"no-auth": { "no-auth": {
input: ` input: `
@ -200,10 +276,11 @@ https_port=8443
http_host=10.0.0.55 http_host=10.0.0.55
http_port=80 http_port=80
`, `,
url: urlMustParse("https://10.0.0.66:8443"), httpProxy: urlMustParse("http://10.0.0.55:80"),
err: nil, httpsProxy: urlMustParse("http://10.0.0.66:8443"),
err: nil,
}, },
"http": { "http-only": {
input: ` input: `
proxy_user=foo proxy_user=foo
proxy_pwd=bar proxy_pwd=bar
@ -216,8 +293,9 @@ https_port=8443
http_host=10.0.0.55 http_host=10.0.0.55
http_port=80 http_port=80
`, `,
url: urlMustParse("http://foo:bar@10.0.0.55:80"), httpProxy: urlMustParse("http://foo:bar@10.0.0.55:80"),
err: nil, httpsProxy: nil,
err: nil,
}, },
"empty": { "empty": {
input: ` input: `
@ -232,14 +310,15 @@ https_port=
http_host= http_host=
http_port= http_port=
`, `,
url: nil, httpProxy: nil,
err: nil, httpsProxy: nil,
err: nil,
}, },
} }
for name, example := range cases { for name, example := range cases {
t.Run(name, func(t *testing.T) { 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 { if err != example.err {
t.Fatal(err) t.Fatal(err)
} }
@ -247,18 +326,32 @@ http_port=
return return
} }
if url == nil && example.url == nil { if example.httpProxy == nil && httpProxy != nil {
return t.Fatalf("got %s, want nil", httpProxy)
} }
if example.url == nil { if example.httpProxy != nil {
if url != nil { if httpProxy == nil {
t.Fatalf("got %s, want nil", url) 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 { if example.httpsProxy != nil {
t.Fatalf("got %s, want %s", got, want) 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" "errors"
"fmt" "fmt"
"io" "io"
"io/ioutil"
"log" "log"
"net" "net"
"os" "os"
@ -19,7 +18,6 @@ import (
"path/filepath" "path/filepath"
"runtime" "runtime"
"strconv" "strconv"
"strings"
) )
// TODO(apenwarr): handle magic cookie auth // TODO(apenwarr): handle magic cookie auth
@ -114,77 +112,30 @@ func socketPermissionsForOS() os.FileMode {
return 0600 return 0600
} }
// connectMacOSAppSandbox connects to the Tailscale Network Extension, // connectMacOSAppSandbox connects to the Tailscale Network Extension (macOS App
// which is necessarily running within the macOS App Sandbox. Our // Store build) or App Extension (macsys standalone build), where the CLI itself
// little dance to connect a regular user binary to the sandboxed // is either running within the macOS App Sandbox or built separately (e.g.
// network extension is: // 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 // * the sandboxed IPNExtension picks a random localhost:0 TCP port
// to listen on // to listen on
// * it also picks a random hex string that acts as an auth token // * it also picks a random hex string that acts as an auth token
// * it then creates a file named "sameuserproof-$PORT-$TOKEN" and leaves // * the CLI looks on disk for that TCP port + auth token (see localTCPPortAndTokenDarwin)
// that file descriptor open forever. // * 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
// Then, we do different things depending on whether the user is // which the Network/App Extension wrote.
// 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).
func connectMacOSAppSandbox() (net.Conn, error) { 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() port, token, err := LocalTCPPortAndToken()
if err != nil { if err != nil {
return nil, err return nil, fmt.Errorf("failed to find local Tailscale daemon: %w", err)
} }
return connectMacTCP(port, token) 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) { func connectMacTCP(port int, token string) (net.Conn, error) {
c, err := net.Dial("tcp", "localhost:"+strconv.Itoa(port)) c, err := net.Dial("tcp", "localhost:"+strconv.Itoa(port))
if err != nil { if err != nil {

@ -105,6 +105,7 @@ func (s *Server) Close() error {
s.shutdownCancel() s.shutdownCancel()
s.lb.Shutdown() s.lb.Shutdown()
s.linkMon.Close() s.linkMon.Close()
s.dialer.Close()
s.localAPIListener.Close() s.localAPIListener.Close()
s.mu.Lock() s.mu.Lock()
@ -137,8 +138,9 @@ func (s *Server) start() error {
} }
s.rootPath = s.Dir s.rootPath = s.Dir
if s.Store != nil && !s.Ephemeral { if s.Store != nil {
if _, ok := s.Store.(*mem.Store); !ok { _, isMemStore := s.Store.(*mem.Store)
if isMemStore && !s.Ephemeral {
return fmt.Errorf("in-memory store is only supported for Ephemeral nodes") 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 { func (m *darwinRouteMon) IsInterestingInterface(iface string) bool {
baseName := strings.TrimRight(iface, "0123456789") baseName := strings.TrimRight(iface, "0123456789")
switch baseName { 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 false
} }
return true return true

@ -651,6 +651,21 @@ func (ns *Impl) acceptTCP(r *tcp.ForwarderRequest) {
} }
r.Complete(false) 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 // The ForwarderRequest.CreateEndpoint above asynchronously
// starts the TCP handshake. Note that the gonet.TCPConn // starts the TCP handshake. Note that the gonet.TCPConn
// methods c.RemoteAddr() and c.LocalAddr() will return nil // methods c.RemoteAddr() and c.LocalAddr() will return nil

Loading…
Cancel
Save