Compare commits

...

82 Commits

Author SHA1 Message Date
Brad Fitzpatrick
8e643357dc VERSION.txt: this is v1.20.2
Change-Id: I0d0905dd4f709591002e52a53846af8fba0aa660
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-01-20 08:26:03 -08:00
Aaron Bieber
59a1b849f0 net/dns/resolvd: store nameservers
Currently only search domains are stored. This was an oversight
(under?) on my part.

As things are now, when MagicDNS is on and "Override local DNS" is
off, the dns forwarder has to timeout before names resolve. This
introduces a pretty annoying lang that makes everything feel
extremely slow. You will also see an error: "upstream nameservers
not set".

I tested with "Override local DNS" on and off. In both situations
things seem to function as expected (and quickly).

Signed-off-by: Aaron Bieber <aaron@bolddaemon.com>
(cherry picked from commit 411c6c316c)
2022-01-20 08:12:22 -08:00
Brad Fitzpatrick
296d10a05d wgengine/netstack: clear TCP ECN bits before giving to gvisor
Updates #2642

Change-Id: Ic219442a2656dd9dc99ae1dd91e907fd3d924987
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
(cherry picked from commit c64af5e676)
2022-01-20 08:12:19 -08:00
Josh Bleecher Snyder
f37cdaefa7 wgengine/magicsock: fix deadlock on shutdown
This fixes a deadlock on shutdown.
One goroutine is waiting to send on c.derpRecvCh before unlocking c.mu.
The other goroutine is waiting to lock c.mu before receiving from c.derpRecvCh.

#3736 has a more detailed explanation of the sequence of events.

Fixes #3736

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
(cherry picked from commit de4696da10)
2022-01-20 08:12:16 -08:00
Brad Fitzpatrick
1e6ca50e3b net/packet: fix typo in comment
Change-Id: Ia666609fde18db44bf38d4e656f490fc372ac3b6
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
(cherry picked from commit 390490e7b1)
2022-01-20 08:12:13 -08:00
Brad Fitzpatrick
dbb6597381 wgengine/netstack: add a missing refcount decrement after packet injection
Fixes #3762
Updates #3745 (probably fixes?)

Change-Id: I1d3f0590fd5b8adfbc9110bc45ff717bb9e79aae
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
(cherry picked from commit 185825df11)
2022-01-20 08:12:05 -08:00
Brad Fitzpatrick
2e80227276 wgengine/netstack: add an Impl.Close method for tests
Change-Id: Idbb3fd6d749d3e4effdf96de77a1106584822fef
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
(cherry picked from commit 790e41645b)
2022-01-20 08:11:58 -08:00
Brad Fitzpatrick
96b76e4cc6 wgengine/netstack: add missing error logging in a RST case
Updates #2642

Change-Id: I9f2f8fd28fc980208b0739eb9caf9db7b0977c09
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
(cherry picked from commit 166fe3fb12)
2022-01-20 08:11:35 -08:00
Brad Fitzpatrick
ec04759c41 wgengine/netstack: fix netstack ping timeout on darwin
-W is milliseconds on darwin, not seconds, and empirically it's
milliseconds after a 1 second base.

Change-Id: I2520619e6699d9c505d9645ce4dfee4973555227
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
(cherry picked from commit 6be48dfcc6)
2022-01-20 08:11:29 -08:00
Denton Gentry
88c4bde778 VERSION.txt: this is v1.20.1
Signed-off-by: Denton Gentry <dgentry@tailscale.com>
2022-01-13 16:01:27 -08:00
Brad Fitzpatrick
7052c6fe25 wgengine/magicsock: fix lock ordering deadlock with derphttp
Fixes #3726

Change-Id: I32631a44dcc1da3ae47764728ec11ace1c78190d
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
(cherry picked from commit c6c39930cc)
2022-01-13 15:45:17 -08:00
David Anderson
e510abc8d0 net/dnscache: don't cancel the TLS context before writing to the result channel.
Cancelling the context makes the timeout goroutine race with the write that
reports a successful TLS handshake, so you can end up with a successful TLS
handshake that mysteriously reports that it timed out after ~0s in flight.

The context is always canceled and cleaned up as the function exits, which
happens mere microseconds later, so just let function exit clean up and
thereby avoid races.

Signed-off-by: David Anderson <danderson@tailscale.com>
(cherry picked from commit a9da6b73a8)
2022-01-13 15:02:30 -08:00
Denton Gentry
958917dce8 VERSION.txt: this is v1.20.0
Signed-off-by: Denton Gentry <dgentry@tailscale.com>
2022-01-12 17:11:42 -08:00
Brad Fitzpatrick
7c1a1aa5d9 tailcfg: no-op bump of MapRequest.Version
So 1.18 and 1.20 don't have the same.

Change-Id: Ib2cac7c11eb37d9a0c2fcb66630f1cae619a97f4
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
(cherry picked from commit a7da236d3d)
2022-01-12 17:04:53 -08:00
Brad Fitzpatrick
90423bf3de wgengine/netstack: make userspace ping work when tailscaled has CAP_NET_RAW
Updates #3710

Change-Id: Ief56c7ac20f5f09a2f940a1906b9efbf1b0d6932
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
(cherry picked from commit a93937abc3)
2022-01-12 14:24:04 -08:00
Maisem Ali
0028a8d4d5 cmd/tailscale/cli/web: fix typo where the html template data was being
replaced instead of being appended to.

Signed-off-by: Maisem Ali <maisem@tailscale.com>
(cherry picked from commit 9e8a432146)
2022-01-12 12:45:32 -08:00
Brad Fitzpatrick
8519cab83d net/dns/resolver: handle tabs as whitespace when ExitDNS parses resolv.conf
On Synology, the /etc/resolv.conf has tabs in it, which this
resolv.conf parser (we have two, sigh) didn't handle.

Updates #3710

Change-Id: I86f8e09ad1867ee32fa211e85c382a27191418ea
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
(cherry picked from commit 24a04d07d1)
2022-01-12 12:45:31 -08:00
Brad Fitzpatrick
4cd062071c net/netns: remove a useless probe of the "ip" command
We stopped using it in 1.18.

Change-Id: If5adf1d99275286a89e2a05f0bce5193d9f6e5e3
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
(cherry picked from commit 51bc9a6d9d)
2022-01-11 20:27:24 -08:00
Brad Fitzpatrick
04a7f5066d cmd/tailscale: let 'tailscale up --reset' do a pref edit
The --reset shouldn't imply that a Backend.Start is necessary.  With
this, it can do a Backend.EditPrefs instead, which then doesn't do all
the heavy work that Start does. Also, Start on Windows behaves
slightly differently than Linux etc in some cases because of tailscaled
running in client mode on Windows (where the GUI supplies the prefs).

Fixes #3702

Change-Id: I75c9f08d5e0052bf623074030a3a7fcaa677abf6
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
(cherry picked from commit e6626366a2)
2022-01-11 13:02:22 -08:00
Brad Fitzpatrick
a14d445fc7 net/dns: make WSL network configuration opt-in for now
Tailscale seems to be breaking WSL configurations lately.  Until we
understand what changed, turn off Tailscale's involvement by default
and make it opt-in.

Updates #2815

Change-Id: I9977801f8debec7d489d97761f74000a4a33f71b
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
(cherry picked from commit 8df3fa4638)
2022-01-11 10:11:02 -08:00
Aaron Bieber
1a4293c15c net/dns: teach OpenBSD's manager to talk to resolvd(8). (#2789)
OpenBSD 6.9 and up has a daemon which handles nameserver configuration. This PR
teaches the OpenBSD dns manager to check if resolvd is being used. If it is, it
will use the route(8) command to tell resolvd to add the Tailscale dns entries
to resolv.conf

Signed-off-by: Aaron Bieber <aaron@bolddaemon.com>
2022-01-11 08:59:18 -08:00
Brad Fitzpatrick
b8ad90c2bf cmd/derper: in manual cert mode, don't discard error from VerifyHostname
Updates #3701

Change-Id: If8ca5104bd8221c99cc390ca49ee3401aff09b62
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-01-11 08:40:42 -08:00
dependabot[bot]
b1b0fd119b .github: Bump actions/setup-go from 2.1.4 to 2.1.5
Bumps [actions/setup-go](https://github.com/actions/setup-go) from 2.1.4 to 2.1.5.
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](https://github.com/actions/setup-go/compare/v2.1.4...v2.1.5)

---
updated-dependencies:
- dependency-name: actions/setup-go
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-01-10 16:39:26 -08:00
Josh Bleecher Snyder
1dc1c8b709 .github/workflows: upgrade to setup-go@v2
The rest of our workflows use v2.1.4.
For reasons I do not understand, we must set GOPATH here.
Maybe the GitHub Action builds come with GOPATH already set?

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2022-01-10 16:19:06 -08:00
Josh Bleecher Snyder
408522ddad go.toolchain.rev: add update script
Also go.toolchain.branch.

Both migrated from the corp repo.

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2022-01-10 14:55:04 -08:00
Brad Fitzpatrick
1ffc21ad71 go.toolchain.rev: update to Go 1.17.6
Co-authored-by: Josh Bleecher Snyder <josh@tailscale.com>
Change-Id: I572b2b36aa8acb53ea7fde638aa89a7510c84915
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-01-10 13:22:18 -08:00
dependabot[bot]
dee0833b27 .github: Bump actions/upload-artifact from 2.2.4 to 2.3.1
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 2.2.4 to 2.3.1.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v2.2.4...v2.3.1)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-01-10 12:58:41 -08:00
Brad Fitzpatrick
b03170b901 ipn/ipnserver: provide means of setting debug env vars on Windows
Fixes #3688

Change-Id: I2f88ca32bf764e2b084fe544d821c43b6a20ffa5
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-01-10 10:48:18 -08:00
Brad Fitzpatrick
c5243562d7 version: bump date
Change-Id: Ib314eccff199fc0854553216762737ce6af19586
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-01-07 12:40:23 -08:00
Brad Fitzpatrick
1a4e8da084 wgengine/netstack: fake pings through netstack on Android too
Every OS ping binary is slightly different. Adjust for Android's.

Updates #1738

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-01-07 10:05:32 -08:00
Maisem Ali
138662e248 Dockerfile.base: update to alpine:3.15
Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-01-07 07:33:25 -08:00
Brad Fitzpatrick
1b426cc232 wgengine/netstack: add env knob to turn on netstack debug logs
Except for the super verbose packet-level dumps. Keep those disabled
by default with a const.

Updates #2642

Change-Id: Ia9eae1677e8b3fe6f457a59e44896a335d95d547
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-01-06 16:59:35 -08:00
Brad Fitzpatrick
8d0ed1c9ba net/dns/resolver: on Android, make ExitDNS use net package for DNS
Like Windows.

Updates #1738

Change-Id: I9f26bc58ce7e760c749786fbe5d9952c99eeb91c
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-01-06 16:17:24 -08:00
Brad Fitzpatrick
e68d87eb44 ipn: rename SetRunExitNode to SetAdvertiseExitNode
From Maisem's code review feedback where he mashed the merge
button by mistake.

Change-Id: I55abce036a6c25dc391250514983125dda10126c
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-01-06 16:17:03 -08:00
Brad Fitzpatrick
2cfc96aa90 ipn: add methods on Prefs to get/set exit node being advertised
This code was copied in a few places (Windows, Android), so unify it
and add tests.

Change-Id: Id0510c0f5974761365a2045279d1fb498feca11e
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-01-06 16:14:14 -08:00
Brad Fitzpatrick
addda5b96f wgengine/magicsock: fix watchdog timeout on Close when IPv6 not available
The blockForeverConn was only using its sync.Cond one side. Looks like it
was just forgotten.

Fixes #3671

Change-Id: I4ed0191982cdd0bfd451f133139428a4fa48238c
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-01-06 13:24:59 -08:00
Brad Fitzpatrick
64c2657448 cmd/printdep: add flag to print out Go toolchain tarball URL
Updates #3669

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-01-06 08:44:17 -08:00
Brad Fitzpatrick
3690bfecb0 ipn/ipnlocal: fix cert fetching on macOS GUI platforms
And clarify the directory they get written to when under the sandbox.

Fixes #3667

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-01-06 07:59:20 -08:00
Brad Fitzpatrick
28bf53f502 wgengine/magicsock: reduce disco ping heartbeat aggressiveness a bit
Bigger changes coming later, but this should improve things a bit in
the meantime.

Rationale:

* 2 minutes -> 45 seconds: 2 minutes was overkill and never considered
  phones/battery at the time. It was totally arbitrary. 45 seconds is
  also arbitrary but is less than 2 minutes.

* heartbeat from 2 seconds to 3 seconds: in practice this meant two
  packets per second (2 pings and 2 pongs every 2 seconds) because the
  other side was also pinging us every 2 seconds on their own.
  That's just overkill. (see #540 too)

So in the worst case before: when we sent a single packet (say: a DNS
packet), we ended up sending 61 packets over 2 minutes: the 1 DNS
query and then then 60 disco pings (2 minutes / 2 seconds) & received
the same (1 DNS response + 60 pongs).  Now it's 15. In 1.22 we plan to
remove this whole timer-based heartbeat mechanism entirely.

The 5 seconds to 6.5 seconds change is just stretching out that
interval so you can still miss two heartbeats (other 3 + 3 seconds
would be greater than 5 seconds). This means that if your peer moves
without telling you, you can have a path out for 6.5 seconds
now instead of 5 seconds before disco finds a new one. That will also
improve in 1.22 when we start doing UDP+DERP at the same time
when confidence starts to go down on a UDP path.

Updates #3363

Change-Id: Ic2314bbdaf42edcdd7103014b775db9cf4facb47
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-01-05 14:05:16 -08:00
Brad Fitzpatrick
c8b63a409e cmd/hello: also redirect https://hello.ipn.dev to hello.ts.net
I apparently only did HTTP before, not HTTPS.

Updates tailscale/corp#1327

Change-Id: I7d5265a0a25fcab5b142c8c3f21a0920f6cae39f
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-01-04 19:45:02 -08:00
Brad Fitzpatrick
a201b89e4a wgengine/magicsock: reconnect to DERP when its definition changes
Change-Id: I7c560feb9e4a6e155a35ec764a68354f19f694e4
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-01-04 15:19:21 -08:00
Brad Fitzpatrick
506c727e30 ipnlocal, net/{dns,tsaddr,tstun}, wgengine: support MagicDNS on IPv6
Fixes #3660

RELNOTE=MagicDNS now works over IPv6 when CGNAT IPv4 is disabled.

Change-Id: I001e983df5feeb65289abe5012dedd177b841b45
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-01-04 14:37:22 -08:00
Brad Fitzpatrick
e2d9c99e5b cmd/hello: migrate to hello.ts.net as the hostname
But still support hello.ipn.dev for a bit.

Updates tailscale/corp#1327

Change-Id: Iab59cca0b260d69858af16f4e42677e54f9fe54a
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-01-04 09:50:47 -08:00
Maisem Ali
01a9906bf8 tool/go: add wrapper to download and use go.toolchain.rev go version.
Also update build_dist.sh and build_docker.sh to use the wrapper.

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-01-04 09:45:29 -08:00
Brad Fitzpatrick
2aeb93003f derp: add metrics to server got pings, sent pongs
Updates #3652

Change-Id: I1d350bcaee39ea36b0c71912028624d18fb541b4
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-01-03 14:04:20 -08:00
Brad Fitzpatrick
2513d2d728 net/{neterror,dns/resolver}: move PacketWasTruncated to neterror from DNS code
And delete the unused code in net/dns/resolver/neterr_*.go.

Change-Id: Ibe62c486bacce2733eb9968c96a98cbbdb2758bd
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-01-03 14:03:30 -08:00
Maisem Ali
dd45bba76b tsnet: add Start method to allow connecting to the tailnet without
requiring a call to Dial/Listen.

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-01-03 11:56:54 -08:00
Brad Fitzpatrick
ebdd25920e go.toolchain.rev: add Go toolchain rev, tool to print it out
Updates tailscale/corp#3385

Change-Id: Ia0e285a0ae836744539c97ff6eff207588159688
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-01-03 11:56:27 -08:00
Maisem Ali
431329e47c build_docker.sh: add env overrides
Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-01-03 10:59:55 -08:00
Brad Fitzpatrick
7d9b1de3aa netcheck,portmapper,magicsock: ignore some UDP write errors on Linux
Treat UDP send EPERM errors as a lost UDP packet, not something super
fatal. That's just the Linux firewall preventing it from going out.

And add a leaf package net/neterror for that (and future) policy that
all three packages can share, with tests.

Updates #3619

Change-Id: Ibdb838c43ee9efe70f4f25f7fc7fdf4607ba9c1d
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-12-31 08:27:21 -08:00
Brad Fitzpatrick
2c94e3c4ad wgengine/magicsock: don't unconditionally close DERP connections on rebind
Only if the source address isn't on the currently active interface or
a ping of the DERP server fails.

Updates #3619

Change-Id: I6bf06503cff4d781f518b437c8744ac29577acc8
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-12-29 13:21:05 -08:00
Brad Fitzpatrick
04c2c5bd80 net/interfaces: define DefaultRouteInterface and State.DefaultRouteInterface
It was pretty ill-defined before and mostly for logging. But I wanted
to start depending on it, so define what it is and make Windows match
the other operating systems, without losing the log output we had
before. (and add tests for that)

Change-Id: I0fbbba1cfc67a265d09dd6cb738b73f0f6005247
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-12-29 12:13:15 -08:00
Brad Fitzpatrick
96cab21383 cmd/tailscale: add debug restun, rebind subcommands
In the hidden debug menu.

Change-Id: I20213f1f4e2290d36f9ff561bac0cc767400d5fd
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-12-28 21:26:45 -08:00
Brad Fitzpatrick
63d9c7b9b3 derp: add Client.LocalAddr method
So magicsock can later ask a DERP connection whether its source IP
would've changed if it reconnected.

Updates #3619

Change-Id: Ibc8810340c511d6786b60c78c1a61c09f5800e40
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-12-28 15:13:53 -08:00
Brad Fitzpatrick
b09000ad5d ipn/localapi: add debug handler to Rebind, ReSTUN magicsock
And more later probably.

Updates #3619

Change-Id: Ia4cba34a7c0dcce4d2eddec8aae17f32b51c207f
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-12-28 15:13:44 -08:00
Brad Fitzpatrick
eb26c081b1 net/dns: bound time we wait on restarting systemd-resolved in another place
Fixes #3629
Updates #3537

Change-Id: I5d4b6acfcfdf0e3efcf6eb49c5e6cf4521e4baed
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-12-28 11:09:07 -08:00
Maisem Ali
44937b59e7 tsnet: add Dial method to allow dialing out to the tailnet.
Signed-off-by: Maisem Ali <maisem@tailscale.com>
2021-12-28 10:19:36 -08:00
Brad Fitzpatrick
535b925d1b derp/derphttp: add Client.Ping, SendPing methods
Continuing work in 434af15a04, to make it possible for magicsock to
probe whether a DERP server is still there.

Updates #3619

Change-Id: I366a77c27e93b876734e64f445b85ef01eb590f2
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-12-28 09:52:10 -08:00
Brad Fitzpatrick
434af15a04 derp: support client->server ping (and server->client pong)
In prep for a future change to have client ping derp connections
when their state is questionable, rather than aggressively tearing
them down and doing a heavy reconnect when their state is unknown.

We already support ping/pong in the other direction (servers probing
clients) so we already had the two frame types, but I'd never finished
this direction.

Updates #3619

Change-Id: I024b815d9db1bc57c20f82f80f95fb55fc9e2fcc
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-12-27 14:49:50 -08:00
Brad Fitzpatrick
bc537adb1a tailcfg: add Hostinfo.HowUnequal method
Change-Id: I80ee49c2ab581feccc4aa6ab47bc3c8392d9989d
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-12-23 09:04:53 -08:00
Brad Fitzpatrick
0aa4c6f147 net/dns/resolver: add debug HTML handler to see what DNS traffic was forwarded
Change-Id: I6b790e92dcc608515ac8b178f2271adc9fd98f78
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-12-21 14:32:36 -08:00
Brad Fitzpatrick
ae319b4636 wgengine/magicsock: add HTML debug handler to see magicsock state
Change-Id: Ibc46f4e9651e1c86ec6f5d139f5e9bdc7a488415
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-12-21 14:26:52 -08:00
Brad Fitzpatrick
c7f5bc0f69 wgengine/magicsock: add metrics for sent disco messages
We only tracked the transport type (UDP vs DERP), not what they were.

Change-Id: Ia4430c1c53afd4634e2d9893d96751a885d77955
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-12-20 09:39:38 -08:00
Brad Fitzpatrick
81bc812402 Dockerfile: remove long-obsolete -tags=xversion
That build tag hasn't been used since 5088af68cf
(June 2nd, 2021, for 1.10.0)

Change-Id: Ib6093a975505339872a0dc663fff9fc09c13bbc0
2021-12-19 18:15:09 -08:00
Brad Fitzpatrick
0848b36dd2 net/dns/resolver: add metrics to PTR lookup misses
Updates tailscale/corp#3326

Change-Id: I58077d889a3b58ef0633267c92ffb265686ce152
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-12-19 09:50:16 -08:00
Brad Fitzpatrick
39f22a357d net/dns/resolver: send NXDOMAIN to iOS DNS-SD/Bonjour queries
Don't just ignore them. See if this makes them calm down.

Updates #3363

Change-Id: Id1d66308e26660d26719b2538b577522a1e36b63
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-12-18 19:24:19 -08:00
Brad Fitzpatrick
394c9de02b net/dns/resolver: add nameFromQuery benchmark
To convince me it's not as alloc-y as it looks.

Change-Id: I503a0cc267268a23d2973dfde9833c420be4e868
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-12-18 19:03:45 -08:00
Brad Fitzpatrick
c7052154d5 net/dns/resolver: fix the subject in a func comment
Change-Id: I519268c20dbd2c2da92da565839d3c1c84612dcc
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-12-18 15:11:01 -08:00
Brad Fitzpatrick
3dedcd1640 logpolicy, ipn/ipnserver: connect to logtail via tailscaled when needed
This is for use by the Windows GUI client to log via when an
exit node is in use, so the logs don't go out via the exit node and
instead go directly, like tailscaled's. The dialer tried to do that
in the unprivileged GUI by binding to a specific interface, but the
"Internet Kill Switch" installed by tailscaled for exit nodes
precludes that from working and instead the GUI fails to dial out.
So, go through tailscaled (with a CONNECT request) instead.

Fixes tailscale/corp#3169

Change-Id: I17a8efdc1d4b8fed53a29d1c19995592b651b215
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-12-17 14:40:26 -08:00
Brad Fitzpatrick
5a9914a92f wgengine/netstack: don't remove 255.255.255.255/32 from netstack
The intent of the updateIPs code is to add & remove IP addresses
to netstack based on what we get from the netmap.

But netstack itself adds 255.255.255.255/32 apparently and we always
fight it (and it adds it back?). So stop fighting it.

Updates #2642 (maybe fixes? maybe.)

Change-Id: I37cb23f8e3f07a42a1a55a585689ca51c2be7c60
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-12-16 14:15:07 -08:00
Joe Tsai
66164b9307 api: document API endpoint to manipulate keys (#3544)
The new /keys endpoint allows you to list API and machine auth keys.
You can also create machine auth key.
It currently does not support creating another API key.

Signed-off-by: Joe Tsai <joetsai@digital-static.net>
2021-12-16 13:50:14 -08:00
Brad Fitzpatrick
40e2b312b6 ipn/ipnserver, logpolicy: move Windows disk logging up earlier
This moves the Windows-only initialization of the filelogger into
logpolicy. Previously we only did it when babysitting the tailscaled
subprocess, but this meant that log messages from the service itself
never made it to disk. Examples that weren't logged to disk:

* logtail unable to dial out,
* DNS flush messages from the service
* svc.ChangeRequest messages (#3581)

This is basically the same fix as #3571 but staying in the Logf type,
and avoiding build-tagged file (which wasn't quite a goal, but
happened and seemed nice)

Fixes #3570

Co-authored-by: Aaron Klotz <aaron@tailscale.com>
Change-Id: Iacd80c4720b7218365ec80ae143339d030842702
2021-12-16 12:33:04 -08:00
Brad Fitzpatrick
689426d6bc cmd/tailscaled: log Windows service change requests
And add a little comment.

Change-Id: If0bedf8aefd8d528149548fba829e7a9a8b2e114
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-12-16 12:24:05 -08:00
Josh Bleecher Snyder
add6dc8ccc ipn/ipnlocal: make TestShrinkDefaultRoute hermetic
Make shrinkDefaultRoute a pure function.
Instead of calling interfaceRoutes, accept that information as parameters.
Hard-code those parameters in TestShrinkDefaultRoute.

Fixes #3580

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-12-16 11:31:20 -08:00
Jay Stapleton
894693f352 scripts/installer.sh fix APT_SYSTEMCTL_START error
fix error: 'sh: 411: APT_SYSTEMCTL_START: parameter not set' on certain debian distributions

Signed-off-by: Jay Stapleton <jay@tailscale.com>
2021-12-16 13:55:11 -05:00
Josh Bleecher Snyder
4512e213d5 cmd/tailscale: improve ping error message when logged out
Refactor out the pretty status printing code from status, use it in ping.

Fixes #3549

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-12-16 10:46:18 -08:00
Brad Fitzpatrick
8f43ddf1a2 ipn/ipnlocal, health: populate self node's Online bit in tailscale status
One option was to just hide "offline" in the text output, but that
doesn't fix the JSON output.

The next option was to lie and say it's online in the JSON (which then
fixes the "offline" in the text output).

But instead, this sets the self node's "Online" to whether we're in an
active map poll.

Fixes #3564

Change-Id: I9b379989bd14655198959e37eec39bb570fb814a
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-12-16 10:14:08 -08:00
Josh Bleecher Snyder
681d4897cc tstest/integration: remove t param in testNode methods
testNodes have a reference to a testing.TB via their env.
Use it instead of making the caller pass theirs.
We did this in some methods but not others; finish the job.
This simplifies the call sites.

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-12-15 18:06:14 -08:00
Josh Bleecher Snyder
93ae11105d ipn/ipnlocal: clear magicsock's netmap on logout
magicsock was hanging onto its netmap on logout,
which caused tailscale status to display partial
information about a bunch of zombie peers.
After logout, there should be no peers.

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-12-15 17:00:08 -08:00
Josh Bleecher Snyder
84a1106fa7 tstest/integration: make -verbose-tailscaled pass -verbose=2 to tailscaled
If you're using -verbose-tailscaled, you're doing in-the-weeds debugging,
so you probably want the verbose output.

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-12-15 17:00:08 -08:00
Brad Fitzpatrick
aac974a5e5 ipn/ipnlocal: deflake (mostly) TestStateMachine
I'm sick of this flaking. Even if this isn't the right fix, it
stops the alert fatigue.

Updates #3020

Change-Id: I4001c127d78f1056302f7741adec34210a72ee61
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-12-15 13:41:06 -08:00
Brad Fitzpatrick
6590fc3a94 wgengine/netstack: remove some logging on forwarding connections
Change-Id: Ib1165b918cd5da38583f8e7d4be8cda54af3c81d
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-12-15 11:38:25 -08:00
104 changed files with 3075 additions and 414 deletions

View File

@@ -19,7 +19,7 @@ jobs:
dry-run: false
language: go
- name: Upload Crash
uses: actions/upload-artifact@v2.2.4
uses: actions/upload-artifact@v2.3.1
if: failure() && steps.build.outcome == 'success'
with:
name: artifacts

View File

@@ -17,7 +17,7 @@ jobs:
steps:
- name: Set up Go
uses: actions/setup-go@v2.1.4
uses: actions/setup-go@v2.1.5
with:
go-version: 1.17
id: go

View File

@@ -17,7 +17,7 @@ jobs:
steps:
- name: Set up Go
uses: actions/setup-go@v2.1.4
uses: actions/setup-go@v2.1.5
with:
go-version: 1.17
id: go

View File

@@ -17,7 +17,7 @@ jobs:
steps:
- name: Set up Go
uses: actions/setup-go@v2.1.4
uses: actions/setup-go@v2.1.5
with:
go-version: 1.17
id: go

View File

@@ -17,7 +17,7 @@ jobs:
steps:
- name: Set up Go
uses: actions/setup-go@v2.1.4
uses: actions/setup-go@v2.1.5
with:
go-version: 1.17
id: go

View File

@@ -14,7 +14,7 @@ jobs:
steps:
- name: Set up Go
uses: actions/setup-go@v2.1.4
uses: actions/setup-go@v2.1.5
with:
go-version: 1.17

View File

@@ -15,7 +15,7 @@ jobs:
steps:
- name: Set up Go
uses: actions/setup-go@v2.1.4
uses: actions/setup-go@v2.1.5
with:
go-version: 1.17

View File

@@ -14,7 +14,7 @@ jobs:
steps:
- name: Set up Go
uses: actions/setup-go@v2.1.4
uses: actions/setup-go@v2.1.5
with:
go-version: 1.17

View File

@@ -17,7 +17,7 @@ jobs:
steps:
- name: Set up Go
uses: actions/setup-go@v2.1.4
uses: actions/setup-go@v2.1.5
with:
go-version: 1.17
id: go

View File

@@ -17,7 +17,7 @@ jobs:
steps:
- name: Set up Go
uses: actions/setup-go@v2.1.4
uses: actions/setup-go@v2.1.5
with:
go-version: 1.17
id: go

View File

@@ -17,7 +17,7 @@ jobs:
steps:
- name: Set up Go
uses: actions/setup-go@v2.1.4
uses: actions/setup-go@v2.1.5
with:
go-version: 1.17
id: go

View File

@@ -14,7 +14,7 @@ jobs:
steps:
- name: Set up Go
uses: actions/setup-go@v2.1.4
uses: actions/setup-go@v2.1.5
with:
go-version: 1.17

View File

@@ -12,11 +12,13 @@ jobs:
if: "!contains(github.event.head_commit.message, '[ci skip]')"
steps:
- name: Set GOPATH
run: echo "GOPATH=$HOME/go" >> $GITHUB_ENV
- name: Set up Go
uses: actions/setup-go@v1
uses: actions/setup-go@v2.1.5
with:
go-version: 1.17
id: go
- name: Checkout Code
uses: actions/checkout@v1

View File

@@ -17,7 +17,7 @@ jobs:
steps:
- name: Install Go
uses: actions/setup-go@v2.1.4
uses: actions/setup-go@v2.1.5
with:
go-version: 1.17.x

View File

@@ -17,7 +17,7 @@ jobs:
steps:
- name: Install Go
uses: actions/setup-go@v2.1.4
uses: actions/setup-go@v2.1.5
with:
go-version: 1.17.x

View File

@@ -50,7 +50,7 @@ ARG VERSION_GIT_HASH=""
ENV VERSION_GIT_HASH=$VERSION_GIT_HASH
ARG TARGETARCH
RUN GOARCH=$TARGETARCH go install -tags=xversion -ldflags="\
RUN GOARCH=$TARGETARCH go install -ldflags="\
-X tailscale.com/version.Long=$VERSION_LONG \
-X tailscale.com/version.Short=$VERSION_SHORT \
-X tailscale.com/version.GitCommit=$VERSION_GIT_HASH" \

View File

@@ -2,5 +2,5 @@
# Use of this source code is governed by a BSD-style
# license that can be found in the LICENSE file.
FROM alpine:3.14
FROM alpine:3.15
RUN apk add --no-cache ca-certificates iptables iproute2 ip6tables

View File

@@ -1 +1 @@
1.19.0
1.20.2

158
api.md
View File

@@ -23,6 +23,11 @@ Currently based on {some authentication method}. Visit the [admin panel](https:/
- [POST tailnet ACL validate](#tailnet-acl-validate-post): run validation tests against the tailnet's existing ACL
- [Devices](#tailnet-devices)
- [GET tailnet devices](#tailnet-devices-get)
- [Keys](#tailnet-keys)
- [GET tailnet keys](#tailnet-keys-get)
- [POST tailnet key](#tailnet-keys-post)
- [GET tailnet key](#tailnet-keys-key-get)
- [DELETE tailnet key](#tailnet-keys-key-delete)
- [DNS](#tailnet-dns)
- [GET tailnet DNS nameservers](#tailnet-dns-nameservers-get)
- [POST tailnet DNS nameservers](#tailnet-dns-nameservers-post)
@@ -670,6 +675,159 @@ Response
}
```
<a name=tailnet-keys></a>
### Keys
<a name=tailnet-keys-get></a>
#### `GET /api/v2/tailnet/:tailnet/keys` - list the keys for a tailnet
Returns a list of active keys for a tailnet
for the user who owns the API key used to perform this query.
Supply the tailnet of interest in the path.
##### Parameters
No parameters.
##### Returns
Returns a JSON object with the IDs of all active keys.
This includes both API keys and also machine authentication keys.
In the future, this may provide more information about each key than just the ID.
##### Example
```
curl 'https://api.tailscale.com/api/v2/tailnet/example.com/keys' \
-u "tskey-yourapikey123:"
```
Response:
```
{"keys": [
{"id": "kYKVU14CNTRL"},
{"id": "k68VdZ3CNTRL"},
{"id": "kJ9nq43CNTRL"},
{"id": "kkThgj1CNTRL"}
]}
```
<a name=tailnet-keys-post></a>
#### `POST /api/v2/tailnet/:tailnet/keys` - create a new key for a tailnet
Create a new key in a tailnet associated
with the user who owns the API key used to perform this request.
Supply the tailnet in the path.
##### Parameters
###### POST Body
`capabilities` - A mapping of resources to permissible actions.
```
{
"capabilities": {
"devices": {
"create": {
"reusable": false,
"ephemeral": false
}
}
}
}
```
##### Returns
Returns a JSON object with the provided capabilities in addition to the
generated key. The key should be recorded and kept safe and secure as it
wields the capabilities specified in the request. The identity of the key
is embedded in the key itself and can be used to perform operations on
the key (e.g., revoking it or retrieving information about it).
The full key can no longer be retrieved by the server.
##### Example
```
echo '{
"capabilities": {
"devices": {
"create": {
"reusable": false,
"ephemeral": false
}
}
}
}' | curl -X POST --data-binary @- https://api.tailscale.com/api/v2/tailnet/example.com/keys \
-u "tskey-yourapikey123:" \
-H "Content-Type: application/json" | jsonfmt
```
Response:
```
{
"id": "k123456CNTRL",
"key": "tskey-k123456CNTRL-abcdefghijklmnopqrstuvwxyz",
"created": "2021-12-09T23:22:39Z",
"expires": "2022-03-09T23:22:39Z",
"capabilities": {"devices": {"create": {"reusable": false, "ephemeral": false}}}
}
```
<a name=tailnet-keys-key-get></a>
#### `GET /api/v2/tailnet/:tailnet/keys/:keyid` - get information for a specific key
Returns a JSON object with information about specific key.
Supply the tailnet and key ID of interest in the path.
##### Parameters
No parameters.
##### Returns
Returns a JSON object with information about the key such as
when it was created and when it expires.
It also lists the capabilities associated with the key.
##### Example
```
curl 'https://api.tailscale.com/api/v2/tailnet/example.com/keys/k123456CNTRL' \
-u "tskey-yourapikey123:"
```
Response:
```
{
"id": "k123456CNTRL",
"created": "2021-12-09T22:13:53Z",
"expires": "2022-03-09T22:13:53Z",
"capabilities": {"devices": {"create": {"reusable": false, "ephemeral": false}}}
}
```
<a name=tailnet-keys-key-delete></a>
#### `DELETE /api/v2/tailnet/:tailnet/keys/:keyid` - delete a specific key
Deletes a specific key.
Supply the tailnet and key ID of interest in the path.
##### Parameters
No parameters.
##### Returns
This reports status 200 upon success.
##### Example
```
curl -X DELETE 'https://api.tailscale.com/api/v2/tailnet/example.com/keys/k123456CNTRL' \
-u "tskey-yourapikey123:"
```
<a name=tailnet-dns></a>
### DNS

View File

@@ -45,4 +45,4 @@ EOF
exit 0
fi
exec go build -ldflags "-X tailscale.com/version.Long=${LONG} -X tailscale.com/version.Short=${SHORT} -X tailscale.com/version.GitCommit=${GIT_HASH}" "$@"
exec ./tool/go build -ldflags "-X tailscale.com/version.Long=${LONG} -X tailscale.com/version.Short=${SHORT} -X tailscale.com/version.GitCommit=${GIT_HASH}" "$@"

View File

@@ -19,10 +19,20 @@
set -eu
# Use the "go" binary from the "tool" directory (which is github.com/tailscale/go)
export PATH=$PWD/tool:$PATH
eval $(./build_dist.sh shellvars)
DEFAULT_TAGS="v${VERSION_SHORT},v${VERSION_MINOR}"
DEFAULT_REPOS="tailscale/tailscale,ghcr.io/tailscale/tailscale"
DEFAULT_BASE="ghcr.io/tailscale/alpine-base:3.14"
PUSH="${PUSH:-false}"
REPOS="${REPOS:-${DEFAULT_REPOS}}"
TAGS="${TAGS:-${DEFAULT_TAGS}}"
BASE="${BASE:-${DEFAULT_BASE}}"
go run github.com/tailscale/mkctr@latest \
--base="ghcr.io/tailscale/alpine-base:3.14" \
--gopaths="\
tailscale.com/cmd/tailscale:/usr/local/bin/tailscale, \
tailscale.com/cmd/tailscaled:/usr/local/bin/tailscaled" \
@@ -30,6 +40,7 @@ go run github.com/tailscale/mkctr@latest \
-X tailscale.com/version.Long=${VERSION_LONG} \
-X tailscale.com/version.Short=${VERSION_SHORT} \
-X tailscale.com/version.GitCommit=${VERSION_GIT_HASH}" \
--tags="v${VERSION_SHORT},v${VERSION_MINOR}" \
--repos="tailscale/tailscale,ghcr.io/tailscale/tailscale" \
--push
--base="${BASE}" \
--tags="${TAGS}" \
--repos="${REPOS}" \
--push="${PUSH}"

View File

@@ -240,6 +240,16 @@ func BugReport(ctx context.Context, note string) (string, error) {
return strings.TrimSpace(string(body)), nil
}
// DebugAction invokes a debug action, such as "rebind" or "restun".
// These are development tools and subject to change or removal over time.
func DebugAction(ctx context.Context, action string) error {
body, err := send(ctx, "POST", "/localapi/v0/debug?action="+url.QueryEscape(action), 200, nil)
if err != nil {
return fmt.Errorf("error %w: %s", err, body)
}
return nil
}
// Status returns the Tailscale daemon's status.
func Status(ctx context.Context) (*ipnstate.Status, error) {
return status(ctx, "")

View File

@@ -67,8 +67,8 @@ func NewManualCertManager(certdir, hostname string) (certProvider, error) {
if err != nil {
return nil, fmt.Errorf("can not load cert: %w", err)
}
if x509Cert.VerifyHostname(hostname) != nil {
return nil, errors.New("refuse to load cert: hostname mismatch with key")
if err := x509Cert.VerifyHostname(hostname); err != nil {
return nil, fmt.Errorf("cert invalid for hostname %q: %w", hostname, err)
}
return &manualCertManager{cert: &cert, hostname: hostname}, nil
}

View File

@@ -2,13 +2,15 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// The hello binary runs hello.ipn.dev.
// The hello binary runs hello.ts.net.
package main // import "tailscale.com/cmd/hello"
import (
"context"
"crypto/tls"
_ "embed"
"encoding/json"
"errors"
"flag"
"html/template"
"io/ioutil"
@@ -16,6 +18,7 @@ import (
"net/http"
"os"
"strings"
"time"
"tailscale.com/client/tailscale"
"tailscale.com/client/tailscale/apitype"
@@ -69,11 +72,31 @@ func main() {
if *httpsAddr != "" {
log.Printf("running HTTPS server on %s", *httpsAddr)
go func() {
errc <- http.ListenAndServeTLS(*httpsAddr,
"/etc/hello/hello.ipn.dev.crt",
"/etc/hello/hello.ipn.dev.key",
nil,
)
hs := &http.Server{
Addr: *httpsAddr,
TLSConfig: &tls.Config{
GetCertificate: func(hi *tls.ClientHelloInfo) (*tls.Certificate, error) {
switch hi.ServerName {
case "hello.ts.net":
return tailscale.GetCertificate(hi)
case "hello.ipn.dev":
c, err := tls.LoadX509KeyPair(
"/etc/hello/hello.ipn.dev.crt",
"/etc/hello/hello.ipn.dev.key",
)
if err != nil {
return nil, err
}
return &c, nil
}
return nil, errors.New("invalid SNI name")
},
},
IdleTimeout: 30 * time.Second,
ReadHeaderTimeout: 20 * time.Second,
MaxHeaderBytes: 10 << 10,
}
errc <- hs.ListenAndServeTLS("", "")
}()
}
log.Fatal(<-errc)
@@ -127,8 +150,9 @@ func tailscaleIP(who *apitype.WhoIsResponse) string {
func root(w http.ResponseWriter, r *http.Request) {
if r.TLS == nil && *httpsAddr != "" {
host := r.Host
if strings.Contains(r.Host, "100.101.102.103") {
host = "hello.ipn.dev"
if strings.Contains(r.Host, "100.101.102.103") ||
strings.Contains(r.Host, "hello.ipn.dev") {
host = "hello.ts.net"
}
http.Redirect(w, r, "https://"+host, http.StatusFound)
return
@@ -137,6 +161,10 @@ func root(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/", http.StatusFound)
return
}
if r.TLS != nil && *httpsAddr != "" && strings.Contains(r.Host, "hello.ipn.dev") {
http.Redirect(w, r, "https://hello.ts.net", http.StatusFound)
return
}
tmpl, err := getTmpl()
if err != nil {
w.Header().Set("Content-Type", "text/plain")

46
cmd/printdep/printdep.go Normal file
View File

@@ -0,0 +1,46 @@
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// The printdep command is a build system tool for printing out information
// about dependencies.
package main
import (
"flag"
"fmt"
"log"
"runtime"
"strings"
ts "tailscale.com"
)
var (
goToolchain = flag.Bool("go", false, "print the supported Go toolchain git hash (a github.com/tailscale/go commit)")
goToolchainURL = flag.Bool("go-url", false, "print the URL to the tarball of the Tailscale Go toolchain")
)
func main() {
flag.Parse()
if *goToolchain {
fmt.Println(strings.TrimSpace(ts.GoToolchainRev))
}
if *goToolchainURL {
var suffix string
switch runtime.GOARCH {
case "amd64":
// None
case "arm64":
suffix = "-" + runtime.GOARCH
default:
log.Fatalf("unsupported GOARCH %q", runtime.GOARCH)
}
switch runtime.GOOS {
case "linux", "darwin":
default:
log.Fatalf("unsupported GOOS %q", runtime.GOOS)
}
fmt.Printf("https://github.com/tailscale/go/releases/download/build-%s/%s%s.tar.gz\n", strings.TrimSpace(ts.GoToolchainRev), runtime.GOOS, suffix)
}
}

View File

@@ -108,7 +108,7 @@ func runCert(ctx context.Context, args []string) error {
if version.IsMacSysExt() {
dir = "io.tailscale.ipn.macsys"
}
printf("Warning: the macOS CLI runs in a sandbox; this binary's filesystem writes go to $HOME/Library/Containers/%s\n", dir)
printf("Warning: the macOS CLI runs in a sandbox; this binary's filesystem writes go to $HOME/Library/Containers/%s/Data\n", dir)
}
if certArgs.certFile != "" {
certChanged, err := writeIfChanged(certArgs.certFile, certPEM, 0644)

View File

@@ -786,6 +786,16 @@ func TestUpdatePrefs(t *testing.T) {
wantSimpleUp: true,
wantJustEditMP: &ipn.MaskedPrefs{WantRunningSet: true},
},
{
name: "just_edit_reset",
flags: []string{"--reset"},
curPrefs: &ipn.Prefs{
ControlURL: ipn.DefaultControlURL,
Persist: &persist.Persist{LoginName: "crawshaw.github"},
},
env: upCheckEnv{backendState: "Running"},
wantJustEditMP: &ipn.MaskedPrefs{WantRunningSet: true},
},
{
name: "control_synonym",
flags: []string{},

View File

@@ -70,6 +70,16 @@ var debugCmd = &ffcli.Command{
Exec: runLocalCreds,
ShortHelp: "print how to access Tailscale local API",
},
{
Name: "restun",
Exec: localAPIAction("restun"),
ShortHelp: "force a magicsock restun",
},
{
Name: "rebind",
Exec: localAPIAction("rebind"),
ShortHelp: "force a magicsock rebind",
},
{
Name: "prefs",
Exec: runPrefs,
@@ -244,6 +254,15 @@ func runDERPMap(ctx context.Context, args []string) error {
return nil
}
func localAPIAction(action string) func(context.Context, []string) error {
return func(ctx context.Context, args []string) error {
if len(args) > 0 {
return errors.New("unexpected arguments")
}
return tailscale.DebugAction(ctx, action)
}
}
func runEnv(ctx context.Context, args []string) error {
for _, e := range os.Environ() {
outln(e)

View File

@@ -11,6 +11,7 @@ import (
"fmt"
"log"
"net"
"os"
"strings"
"time"
@@ -64,6 +65,16 @@ var pingArgs struct {
}
func runPing(ctx context.Context, args []string) error {
st, err := tailscale.Status(ctx)
if err != nil {
return fixTailscaledConnectError(err)
}
description, ok := isRunningOrStarting(st)
if !ok {
printf("%s\n", description)
os.Exit(1)
}
c, bc, ctx, cancel := connect(ctx)
defer cancel()

View File

@@ -121,24 +121,10 @@ func runStatus(ctx context.Context, args []string) error {
return err
}
switch st.BackendState {
default:
fmt.Fprintf(Stderr, "unexpected state: %s\n", st.BackendState)
description, ok := isRunningOrStarting(st)
if !ok {
outln(description)
os.Exit(1)
case ipn.Stopped.String():
outln("Tailscale is stopped.")
os.Exit(1)
case ipn.NeedsLogin.String():
outln("Logged out.")
if st.AuthURL != "" {
printf("\nLog in at: %s\n", st.AuthURL)
}
os.Exit(1)
case ipn.NeedsMachineAuth.String():
outln("Machine is not yet authorized by tailnet admin.")
os.Exit(1)
case ipn.Running.String(), ipn.Starting.String():
// Run below.
}
if len(st.Health) > 0 {
@@ -222,6 +208,27 @@ func runStatus(ctx context.Context, args []string) error {
return nil
}
// isRunningOrStarting reports whether st is in state Running or Starting.
// It also returns a description of the status suitable to display to a user.
func isRunningOrStarting(st *ipnstate.Status) (description string, ok bool) {
switch st.BackendState {
default:
return fmt.Sprintf("unexpected state: %s", st.BackendState), false
case ipn.Stopped.String():
return "Tailscale is stopped.", false
case ipn.NeedsLogin.String():
s := "Logged out."
if st.AuthURL != "" {
s += fmt.Sprintf("\nLog in at: %s", st.AuthURL)
}
return s, false
case ipn.NeedsMachineAuth.String():
return "Machine is not yet authorized by tailnet admin.", false
case ipn.Running.String(), ipn.Starting.String():
return st.BackendState, true
}
}
func dnsOrQuoteHostname(st *ipnstate.Status, ps *ipnstate.PeerStatus) string {
baseName := dnsname.TrimSuffix(ps.DNSName, st.MagicDNSSuffix)
if baseName != "" {

View File

@@ -413,7 +413,6 @@ func updatePrefs(prefs, curPrefs *ipn.Prefs, env upCheckEnv) (simpleUp bool, jus
justEdit := env.backendState == ipn.Running.String() &&
!env.upArgs.forceReauth &&
!env.upArgs.reset &&
env.upArgs.authKeyOrFile == "" &&
!controlURLChanged &&
!tagsChanged

View File

@@ -375,7 +375,7 @@ func webHandler(w http.ResponseWriter, r *http.Request) {
data.AdvertiseExitNode = true
} else {
if data.AdvertiseRoutes != "" {
data.AdvertiseRoutes = ","
data.AdvertiseRoutes += ","
}
data.AdvertiseRoutes += r.String()
}

View File

@@ -47,6 +47,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
tailscale.com/net/flowtrack from tailscale.com/wgengine/filter+
💣 tailscale.com/net/interfaces from tailscale.com/cmd/tailscale/cli+
tailscale.com/net/netcheck from tailscale.com/cmd/tailscale/cli
tailscale.com/net/neterror from tailscale.com/net/netcheck+
tailscale.com/net/netknob from tailscale.com/net/netns
tailscale.com/net/netns from tailscale.com/derp/derphttp+
tailscale.com/net/packet from tailscale.com/wgengine/filter

View File

@@ -181,9 +181,9 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
tailscale.com/ipn/policy from tailscale.com/ipn/ipnlocal
tailscale.com/ipn/store/aws from tailscale.com/ipn/ipnserver
tailscale.com/kube from tailscale.com/ipn
tailscale.com/log/filelogger from tailscale.com/ipn/ipnserver
tailscale.com/log/filelogger from tailscale.com/logpolicy
tailscale.com/log/logheap from tailscale.com/control/controlclient
tailscale.com/logpolicy from tailscale.com/cmd/tailscaled
tailscale.com/logpolicy from tailscale.com/cmd/tailscaled+
tailscale.com/logtail from tailscale.com/logpolicy+
tailscale.com/logtail/backoff from tailscale.com/cmd/tailscaled+
tailscale.com/logtail/filch from tailscale.com/logpolicy
@@ -195,6 +195,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
tailscale.com/net/flowtrack from tailscale.com/net/packet+
💣 tailscale.com/net/interfaces from tailscale.com/cmd/tailscaled+
tailscale.com/net/netcheck from tailscale.com/wgengine/magicsock
tailscale.com/net/neterror from tailscale.com/net/netcheck+
tailscale.com/net/netknob from tailscale.com/logpolicy+
tailscale.com/net/netns from tailscale.com/cmd/tailscaled+
💣 tailscale.com/net/netstat from tailscale.com/ipn/ipnserver

View File

@@ -295,7 +295,6 @@ func run() error {
var debugMux *http.ServeMux
if args.debug != "" {
debugMux = newDebugMux()
go runDebugServer(debugMux, args.debug)
}
linkMon, err := monitor.New(logf)
@@ -314,6 +313,14 @@ func run() error {
if _, ok := e.(wgengine.ResolvingEngine).GetResolver(); !ok {
panic("internal error: exit node resolver not wired up")
}
if debugMux != nil {
if ig, ok := e.(wgengine.InternalsGetter); ok {
if _, mc, ok := ig.GetInternals(); ok {
debugMux.HandleFunc("/debug/magicsock", mc.ServeHTTPDebug)
}
}
go runDebugServer(debugMux, args.debug)
}
ns, err := newNetstack(logf, dialer, e)
if err != nil {

View File

@@ -55,6 +55,11 @@ func isWindowsService() bool {
return v
}
// runWindowsService starts running Tailscale under the Windows
// Service environment.
//
// At this point we're still the parent process that
// Windows started.
func runWindowsService(pol *logpolicy.Policy) error {
return svc.Run(serviceName, &ipnService{Policy: pol})
}
@@ -93,6 +98,7 @@ func (service *ipnService) Execute(args []string, r <-chan svc.ChangeRequest, ch
select {
case <-doneCh:
case cmd := <-r:
log.Printf("Got Windows Service event: %v", cmdName(cmd.Cmd))
switch cmd.Cmd {
case svc.Stop:
cancel()
@@ -109,6 +115,42 @@ func (service *ipnService) Execute(args []string, r <-chan svc.ChangeRequest, ch
return false, windows.NO_ERROR
}
func cmdName(c svc.Cmd) string {
switch c {
case svc.Stop:
return "Stop"
case svc.Pause:
return "Pause"
case svc.Continue:
return "Continue"
case svc.Interrogate:
return "Interrogate"
case svc.Shutdown:
return "Shutdown"
case svc.ParamChange:
return "ParamChange"
case svc.NetBindAdd:
return "NetBindAdd"
case svc.NetBindRemove:
return "NetBindRemove"
case svc.NetBindEnable:
return "NetBindEnable"
case svc.NetBindDisable:
return "NetBindDisable"
case svc.DeviceEvent:
return "DeviceEvent"
case svc.HardwareProfileChange:
return "HardwareProfileChange"
case svc.PowerEvent:
return "PowerEvent"
case svc.SessionChange:
return "SessionChange"
case svc.PreShutdown:
return "PreShutdown"
}
return fmt.Sprintf("Unknown-Service-Cmd-%d", c)
}
func beWindowsSubprocess() bool {
if beFirewallKillswitch() {
return true

View File

@@ -12,10 +12,12 @@ import (
"fmt"
"io"
"sync"
"sync/atomic"
"time"
"go4.org/mem"
"golang.org/x/time/rate"
"inet.af/netaddr"
"tailscale.com/types/key"
"tailscale.com/types/logger"
)
@@ -37,8 +39,8 @@ type Client struct {
rate *rate.Limiter // if non-nil, rate limiter to use
// Owned by Recv:
peeked int // bytes to discard on next Recv
readErr error // sticky read error
peeked int // bytes to discard on next Recv
readErr atomic.Value // of error; sticky (set by Recv)
}
// ClientOpt is an option passed to NewClient.
@@ -261,10 +263,18 @@ func (c *Client) ForwardPacket(srcKey, dstKey key.NodePublic, pkt []byte) (err e
func (c *Client) writeTimeoutFired() { c.nc.Close() }
func (c *Client) SendPing(data [8]byte) error {
return c.sendPingOrPong(framePing, data)
}
func (c *Client) SendPong(data [8]byte) error {
return c.sendPingOrPong(framePong, data)
}
func (c *Client) sendPingOrPong(typ frameType, data [8]byte) error {
c.wmu.Lock()
defer c.wmu.Unlock()
if err := writeFrameHeader(c.bw, framePong, 8); err != nil {
if err := writeFrameHeader(c.bw, typ, 8); err != nil {
return err
}
if _, err := c.bw.Write(data[:]); err != nil {
@@ -375,6 +385,12 @@ type PingMessage [8]byte
func (PingMessage) msg() {}
// PongMessage is a reply to a PingMessage from a client or server
// with the payload sent previously in a PingMessage.
type PongMessage [8]byte
func (PongMessage) msg() {}
// KeepAliveMessage is a one-way empty message from server to client, just to
// keep the connection alive. It's like a PingMessage, but doesn't solicit
// a reply from the client.
@@ -427,13 +443,14 @@ func (c *Client) Recv() (m ReceivedMessage, err error) {
}
func (c *Client) recvTimeout(timeout time.Duration) (m ReceivedMessage, err error) {
if c.readErr != nil {
return nil, c.readErr
readErr, _ := c.readErr.Load().(error)
if readErr != nil {
return nil, readErr
}
defer func() {
if err != nil {
err = fmt.Errorf("derp.Recv: %w", err)
c.readErr = err
c.readErr.Store(err)
}
}()
@@ -536,6 +553,15 @@ func (c *Client) recvTimeout(timeout time.Duration) (m ReceivedMessage, err erro
copy(pm[:], b[:])
return pm, nil
case framePong:
var pm PongMessage
if n < 8 {
c.logf("[unexpected] dropping short ping frame")
continue
}
copy(pm[:], b[:])
return pm, nil
case frameHealth:
return HealthMessage{Problem: string(b[:])}, nil
@@ -564,3 +590,22 @@ func (c *Client) setSendRateLimiter(sm ServerInfoMessage) {
sm.TokenBucketBytesBurst)
}
}
// LocalAddr returns the TCP connection's local address.
//
// If the client is broken in some previously detectable way, it
// returns an error.
func (c *Client) LocalAddr() (netaddr.IPPort, error) {
readErr, _ := c.readErr.Load().(error)
if readErr != nil {
return netaddr.IPPort{}, readErr
}
if c.nc == nil {
return netaddr.IPPort{}, errors.New("nil conn")
}
a := c.nc.LocalAddr()
if a == nil {
return netaddr.IPPort{}, errors.New("nil addr")
}
return netaddr.ParseIPPort(a.String())
}

View File

@@ -23,6 +23,7 @@ import (
"math"
"math/big"
"math/rand"
"net"
"net/http"
"os"
"os/exec"
@@ -124,6 +125,8 @@ type Server struct {
packetsForwardedOut expvar.Int
packetsForwardedIn expvar.Int
peerGoneFrames expvar.Int // number of peer gone frames sent
gotPing expvar.Int // number of ping frames from client
sentPong expvar.Int // number of pong frames enqueued to client
accepts expvar.Int
curClients expvar.Int
curHomeClients expvar.Int // ones with preferred
@@ -283,9 +286,8 @@ type PacketForwarder interface {
// It is a defined type so that non-net connections can be used.
type Conn interface {
io.WriteCloser
LocalAddr() net.Addr
// The *Deadline methods follow the semantics of net.Conn.
SetDeadline(time.Time) error
SetReadDeadline(time.Time) error
SetWriteDeadline(time.Time) error
@@ -662,6 +664,7 @@ func (s *Server) accept(nc Conn, brw *bufio.ReadWriter, remoteAddr string, connN
connectedAt: time.Now(),
sendQueue: make(chan pkt, perClientSendQueueDepth),
discoSendQueue: make(chan pkt, perClientSendQueueDepth),
sendPongCh: make(chan [8]byte, 1),
peerGone: make(chan key.NodePublic),
canMesh: clientInfo.MeshKey != "" && clientInfo.MeshKey == s.meshKey,
}
@@ -729,6 +732,8 @@ func (c *sclient) run(ctx context.Context) error {
err = c.handleFrameWatchConns(ft, fl)
case frameClosePeer:
err = c.handleFrameClosePeer(ft, fl)
case framePing:
err = c.handleFramePing(ft, fl)
default:
err = c.handleUnknownFrame(ft, fl)
}
@@ -766,6 +771,33 @@ func (c *sclient) handleFrameWatchConns(ft frameType, fl uint32) error {
return nil
}
func (c *sclient) handleFramePing(ft frameType, fl uint32) error {
c.s.gotPing.Add(1)
var m PingMessage
if fl < uint32(len(m)) {
return fmt.Errorf("short ping: %v", fl)
}
if fl > 1000 {
// unreasonably extra large. We leave some extra
// space for future extensibility, but not too much.
return fmt.Errorf("ping body too large: %v", fl)
}
_, err := io.ReadFull(c.br, m[:])
if err != nil {
return err
}
if extra := int64(fl) - int64(len(m)); extra > 0 {
_, err = io.CopyN(ioutil.Discard, c.br, extra)
}
select {
case c.sendPongCh <- [8]byte(m):
default:
// They're pinging too fast. Ignore.
// TODO(bradfitz): add a rate limiter too.
}
return err
}
func (c *sclient) handleFrameClosePeer(ft frameType, fl uint32) error {
if fl != keyLen {
return fmt.Errorf("handleFrameClosePeer wrong size")
@@ -1202,6 +1234,7 @@ type sclient struct {
remoteIPPort netaddr.IPPort // zero if remoteAddr is not ip:port.
sendQueue chan pkt // packets queued to this client; never closed
discoSendQueue chan pkt // important packets queued to this client; never closed
sendPongCh chan [8]byte // pong replies to send to the client; never closed
peerGone chan key.NodePublic // write request that a previous sender has disconnected (not used by mesh peers)
meshUpdate chan struct{} // write request to write peerStateChange
canMesh bool // clientInfo had correct mesh token for inter-region routing
@@ -1342,6 +1375,9 @@ func (c *sclient) sendLoop(ctx context.Context) error {
werr = c.sendPacket(msg.src, msg.bs)
c.recordQueueTime(msg.enqueuedAt)
continue
case msg := <-c.sendPongCh:
werr = c.sendPong(msg)
continue
case <-keepAliveTick.C:
werr = c.sendKeepAlive()
continue
@@ -1368,6 +1404,9 @@ func (c *sclient) sendLoop(ctx context.Context) error {
case msg := <-c.discoSendQueue:
werr = c.sendPacket(msg.src, msg.bs)
c.recordQueueTime(msg.enqueuedAt)
case msg := <-c.sendPongCh:
werr = c.sendPong(msg)
continue
case <-keepAliveTick.C:
werr = c.sendKeepAlive()
}
@@ -1384,6 +1423,17 @@ func (c *sclient) sendKeepAlive() error {
return writeFrameHeader(c.bw.bw(), frameKeepAlive, 0)
}
// sendPong sends a pong reply, without flushing.
func (c *sclient) sendPong(data [8]byte) error {
c.s.sentPong.Add(1)
c.setWriteDeadline()
if err := writeFrameHeader(c.bw.bw(), framePong, uint32(len(data))); err != nil {
return err
}
_, err := c.bw.Write(data[:])
return err
}
// sendPeerGone sends a peerGone frame, without flushing.
func (c *sclient) sendPeerGone(peer key.NodePublic) error {
c.s.peerGoneFrames.Add(1)
@@ -1625,6 +1675,8 @@ func (s *Server) ExpVar() expvar.Var {
m.Set("unknown_frames", &s.unknownFrames)
m.Set("home_moves_in", &s.homeMovesIn)
m.Set("home_moves_out", &s.homeMovesOut)
m.Set("got_ping", &s.gotPing)
m.Set("sent_pong", &s.sentPong)
m.Set("peer_gone_frames", &s.peerGoneFrames)
m.Set("packets_forwarded_out", &s.packetsForwardedOut)
m.Set("packets_forwarded_in", &s.packetsForwardedIn)

View File

@@ -812,6 +812,14 @@ func TestClientRecv(t *testing.T) {
},
want: PingMessage{1, 2, 3, 4, 5, 6, 7, 8},
},
{
name: "pong",
input: []byte{
byte(framePong), 0, 0, 0, 8,
1, 2, 3, 4, 5, 6, 7, 8,
},
want: PongMessage{1, 2, 3, 4, 5, 6, 7, 8},
},
{
name: "health_bad",
input: []byte{
@@ -858,6 +866,23 @@ func TestClientRecv(t *testing.T) {
}
}
func TestClientSendPing(t *testing.T) {
var buf bytes.Buffer
c := &Client{
bw: bufio.NewWriter(&buf),
}
if err := c.SendPing([8]byte{1, 2, 3, 4, 5, 6, 7, 8}); err != nil {
t.Fatal(err)
}
want := []byte{
byte(framePing), 0, 0, 0, 8,
1, 2, 3, 4, 5, 6, 7, 8,
}
if !bytes.Equal(buf.Bytes(), want) {
t.Errorf("unexpected output\nwrote: % 02x\n want: % 02x", buf.Bytes(), want)
}
}
func TestClientSendPong(t *testing.T) {
var buf bytes.Buffer
c := &Client{
@@ -873,7 +898,6 @@ func TestClientSendPong(t *testing.T) {
if !bytes.Equal(buf.Bytes(), want) {
t.Errorf("unexpected output\nwrote: % 02x\n want: % 02x", buf.Bytes(), want)
}
}
func TestServerDupClients(t *testing.T) {
@@ -1316,3 +1340,30 @@ func TestClientSendRateLimiting(t *testing.T) {
t.Errorf("limited conn's bytes count = %v; want >=%v, <%v", bytesLimited, bytes1K*2, bytes1K)
}
}
func TestServerRepliesToPing(t *testing.T) {
ts := newTestServer(t)
defer ts.close(t)
tc := newRegularClient(t, ts, "alice")
data := [8]byte{1, 2, 3, 4, 5, 6, 7, 42}
if err := tc.c.SendPing(data); err != nil {
t.Fatal(err)
}
for {
m, err := tc.c.recvTimeout(time.Second)
if err != nil {
t.Fatal(err)
}
switch m := m.(type) {
case PongMessage:
if ([8]byte(m)) != data {
t.Fatalf("got pong %2x; want %2x", [8]byte(m), data)
}
return
}
}
}

View File

@@ -13,6 +13,7 @@ package derphttp
import (
"bufio"
"context"
"crypto/rand"
"crypto/tls"
"crypto/x509"
"errors"
@@ -72,6 +73,7 @@ type Client struct {
client *derp.Client
connGen int // incremented once per new connection; valid values are >0
serverPubKey key.NodePublic
pingOut map[derp.PingMessage]chan<- bool // chan to send to on pong
}
// NewRegionClient returns a new DERP-over-HTTP client. It connects lazily.
@@ -698,6 +700,95 @@ func (c *Client) Send(dstKey key.NodePublic, b []byte) error {
return err
}
func (c *Client) registerPing(m derp.PingMessage, ch chan<- bool) {
c.mu.Lock()
defer c.mu.Unlock()
if c.pingOut == nil {
c.pingOut = map[derp.PingMessage]chan<- bool{}
}
c.pingOut[m] = ch
}
func (c *Client) unregisterPing(m derp.PingMessage) {
c.mu.Lock()
defer c.mu.Unlock()
delete(c.pingOut, m)
}
func (c *Client) handledPong(m derp.PongMessage) bool {
c.mu.Lock()
defer c.mu.Unlock()
k := derp.PingMessage(m)
if ch, ok := c.pingOut[k]; ok {
ch <- true
delete(c.pingOut, k)
return true
}
return false
}
// Ping sends a ping to the peer and waits for it either to be
// acknowledged (in which case Ping returns nil) or waits for ctx to
// be over and returns an error. It will wait at most 5 seconds
// before returning an error.
//
// Another goroutine must be in a loop calling Recv or
// RecvDetail or ping responses won't be handled.
func (c *Client) Ping(ctx context.Context) error {
maxDL := time.Now().Add(5 * time.Second)
if dl, ok := ctx.Deadline(); !ok || dl.After(maxDL) {
var cancel context.CancelFunc
ctx, cancel = context.WithDeadline(ctx, maxDL)
defer cancel()
}
var data derp.PingMessage
rand.Read(data[:])
gotPing := make(chan bool, 1)
c.registerPing(data, gotPing)
defer c.unregisterPing(data)
if err := c.SendPing(data); err != nil {
return err
}
select {
case <-gotPing:
return nil
case <-ctx.Done():
return ctx.Err()
}
}
// SendPing writes a ping message, without any implicit connect or
// reconnect. This is a lower-level interface that writes a frame
// without any implicit handling of the response pong, if any. For a
// higher-level interface, use Ping.
func (c *Client) SendPing(data [8]byte) error {
c.mu.Lock()
closed, client := c.closed, c.client
c.mu.Unlock()
if closed {
return ErrClientClosed
}
if client == nil {
return errors.New("client not connected")
}
return client.SendPing(data)
}
// LocalAddr reports c's local TCP address, without any implicit
// connect or reconnect.
func (c *Client) LocalAddr() (netaddr.IPPort, error) {
c.mu.Lock()
closed, client := c.closed, c.client
c.mu.Unlock()
if closed {
return netaddr.IPPort{}, ErrClientClosed
}
if client == nil {
return netaddr.IPPort{}, errors.New("client not connected")
}
return client.LocalAddr()
}
func (c *Client) ForwardPacket(from, to key.NodePublic, b []byte) error {
client, _, err := c.connect(context.TODO(), "derphttp.Client.ForwardPacket")
if err != nil {
@@ -805,14 +896,22 @@ func (c *Client) RecvDetail() (m derp.ReceivedMessage, connGen int, err error) {
if err != nil {
return nil, 0, err
}
m, err = client.Recv()
if err != nil {
c.closeForReconnect(client)
if c.isClosed() {
err = ErrClientClosed
for {
m, err = client.Recv()
switch m := m.(type) {
case derp.PongMessage:
if c.handledPong(m) {
continue
}
}
if err != nil {
c.closeForReconnect(client)
if c.isClosed() {
err = ErrClientClosed
}
}
return m, connGen, err
}
return m, connGen, err
}
func (c *Client) isClosed() bool {

View File

@@ -154,3 +154,55 @@ func waitConnect(t testing.TB, c *Client) {
t.Fatalf("client first Recv was unexpected type %T", v)
}
}
func TestPing(t *testing.T) {
serverPrivateKey := key.NewNode()
s := derp.NewServer(serverPrivateKey, t.Logf)
defer s.Close()
httpsrv := &http.Server{
TLSNextProto: make(map[string]func(*http.Server, *tls.Conn, http.Handler)),
Handler: Handler(s),
}
ln, err := net.Listen("tcp4", "localhost:0")
if err != nil {
t.Fatal(err)
}
serverURL := "http://" + ln.Addr().String()
t.Logf("server URL: %s", serverURL)
go func() {
if err := httpsrv.Serve(ln); err != nil {
if err == http.ErrServerClosed {
return
}
panic(err)
}
}()
c, err := NewClient(key.NewNode(), serverURL, t.Logf)
if err != nil {
t.Fatalf("NewClient: %v", err)
}
defer c.Close()
if err := c.Connect(context.Background()); err != nil {
t.Fatalf("client Connect: %v", err)
}
errc := make(chan error, 1)
go func() {
for {
m, err := c.Recv()
if err != nil {
errc <- err
return
}
t.Logf("Recv: %T", m)
}
}()
err = c.Ping(context.Background())
if err != nil {
t.Fatalf("Ping: %v", err)
}
}

1
go.toolchain.branch Normal file
View File

@@ -0,0 +1 @@
tailscale.go1.17

1
go.toolchain.rev Normal file
View File

@@ -0,0 +1 @@
e44d304e5486d10f5485250ed5ba58fffd438d29

View File

@@ -9,6 +9,7 @@ package health
import (
"errors"
"fmt"
"net/http"
"os"
"runtime"
"sort"
@@ -28,6 +29,8 @@ var (
watchers = map[*watchHandle]func(Subsystem, error){} // opt func to run if error state changes
timer *time.Timer
debugHandler = map[string]http.Handler{}
inMapPoll bool
inMapPollSince time.Time
lastMapPollEndedAt time.Time
@@ -116,6 +119,18 @@ func SetNetworkCategoryHealth(err error) { set(SysNetworkCategory, err) }
func NetworkCategoryHealth() error { return get(SysNetworkCategory) }
func RegisterDebugHandler(typ string, h http.Handler) {
mu.Lock()
defer mu.Unlock()
debugHandler[typ] = h
}
func DebugHandler(typ string) http.Handler {
mu.Lock()
defer mu.Unlock()
return debugHandler[typ]
}
func get(key Subsystem) error {
mu.Lock()
defer mu.Unlock()
@@ -168,7 +183,8 @@ func GotStreamedMapResponse() {
selfCheckLocked()
}
// SetInPollNetMap records that we're in
// SetInPollNetMap records whether the client has an open
// HTTP long poll open to the control plane.
func SetInPollNetMap(v bool) {
mu.Lock()
defer mu.Unlock()
@@ -183,6 +199,14 @@ func SetInPollNetMap(v bool) {
}
}
// GetInPollNetMap reports whether the client has an open
// HTTP long poll open to the control plane.
func GetInPollNetMap() bool {
mu.Lock()
defer mu.Unlock()
return inMapPoll
}
// SetMagicSockDERPHome notes what magicsock's view of its home DERP is.
func SetMagicSockDERPHome(region int) {
mu.Lock()

View File

@@ -111,7 +111,8 @@ func TestDNSConfigForNetmap(t *testing.T) {
},
prefs: &ipn.Prefs{},
want: &dns.Config{
Routes: map[dnsname.FQDN][]dnstype.Resolver{},
OnlyIPv6: true,
Routes: map[dnsname.FQDN][]dnstype.Resolver{},
Hosts: map[dnsname.FQDN][]netaddr.IP{
"b.net.": ips("fe75::2"),
"myname.net.": ips("fe75::1"),

View File

@@ -55,6 +55,7 @@ import (
"tailscale.com/version/distro"
"tailscale.com/wgengine"
"tailscale.com/wgengine/filter"
"tailscale.com/wgengine/magicsock"
"tailscale.com/wgengine/router"
"tailscale.com/wgengine/wgcfg"
"tailscale.com/wgengine/wgcfg/nmcfg"
@@ -380,6 +381,7 @@ func (b *LocalBackend) updateStatus(sb *ipnstate.StatusBuilder, extraLocked func
}
})
sb.MutateSelfStatus(func(ss *ipnstate.PeerStatus) {
ss.Online = health.GetInPollNetMap()
if b.netMap != nil {
ss.HostName = b.netMap.Hostinfo.Hostname
ss.DNSName = b.netMap.Name
@@ -536,6 +538,7 @@ func (b *LocalBackend) setClientStatus(st controlclient.Status) {
// Since st.NetMap==nil means "netmap is unchanged", there is
// no other way to represent this change.
b.setNetMapLocked(nil)
b.e.SetNetworkMap(new(netmap.NetworkMap))
}
prefs := b.prefs
@@ -1018,7 +1021,12 @@ func (b *LocalBackend) updateFilter(netMap *netmap.NetworkMap, prefs *ipn.Prefs)
// wifi": you get internet access, but to additionally
// get LAN access the LAN(s) need to be offered
// explicitly as well.
s, err := shrinkDefaultRoute(r)
localInterfaceRoutes, hostIPs, err := interfaceRoutes()
if err != nil {
b.logf("getting local interface routes: %v", err)
continue
}
s, err := shrinkDefaultRoute(r, localInterfaceRoutes, hostIPs)
if err != nil {
b.logf("computing default route filter: %v", err)
continue
@@ -1162,17 +1170,14 @@ func interfaceRoutes() (ips *netaddr.IPSet, hostIPs []netaddr.IP, err error) {
}
// shrinkDefaultRoute returns an IPSet representing the IPs in route,
// minus those in removeFromDefaultRoute and local interface subnets.
func shrinkDefaultRoute(route netaddr.IPPrefix) (*netaddr.IPSet, error) {
interfaceRoutes, hostIPs, err := interfaceRoutes()
if err != nil {
return nil, err
}
// minus those in removeFromDefaultRoute and localInterfaceRoutes,
// plus the IPs in hostIPs.
func shrinkDefaultRoute(route netaddr.IPPrefix, localInterfaceRoutes *netaddr.IPSet, hostIPs []netaddr.IP) (*netaddr.IPSet, error) {
var b netaddr.IPSetBuilder
// Add the default route.
b.AddPrefix(route)
// Remove the local interface routes.
b.RemoveSet(interfaceRoutes)
b.RemoveSet(localInterfaceRoutes)
// Having removed all the LAN subnets, re-add the hosts's own
// IPs. It's fine for clients to connect to an exit node's public
@@ -1946,6 +1951,7 @@ func dnsConfigForNetmap(nm *netmap.NetworkMap, prefs *ipn.Prefs, logf logger.Log
// selfV6Only is whether we only have IPv6 addresses ourselves.
selfV6Only := tsaddr.PrefixesContainsFunc(nm.Addresses, tsaddr.PrefixIs6) &&
!tsaddr.PrefixesContainsFunc(nm.Addresses, tsaddr.PrefixIs4)
dcfg.OnlyIPv6 = selfV6Only
// Populate MagicDNS records. We do this unconditionally so that
// quad-100 can always respond to MagicDNS queries, even if the OS
@@ -2115,7 +2121,7 @@ func (b *LocalBackend) TailscaleVarRoot() string {
return b.varRoot
}
switch runtime.GOOS {
case "ios", "android":
case "ios", "android", "darwin":
dir, _ := paths.AppSharedDir.Load().(string)
return dir
}
@@ -2377,7 +2383,9 @@ func (b *LocalBackend) routerConfig(cfg *wgcfg.Config, prefs *ipn.Prefs) *router
}
}
rs.Routes = append(rs.Routes, netaddr.IPPrefixFrom(tsaddr.TailscaleServiceIP(), 32))
if tsaddr.PrefixesContainsFunc(rs.LocalAddrs, tsaddr.PrefixIs4) {
rs.Routes = append(rs.Routes, netaddr.IPPrefixFrom(tsaddr.TailscaleServiceIP(), 32))
}
return rs
}
@@ -3134,3 +3142,33 @@ func exitNodeCanProxyDNS(nm *netmap.NetworkMap, exitNodeID tailcfg.StableNodeID)
}
return "", false
}
func (b *LocalBackend) DebugRebind() error {
mc, err := b.magicConn()
if err != nil {
return err
}
mc.Rebind()
return nil
}
func (b *LocalBackend) DebugReSTUN() error {
mc, err := b.magicConn()
if err != nil {
return err
}
mc.ReSTUN("explicit-debug")
return nil
}
func (b *LocalBackend) magicConn() (*magicsock.Conn, error) {
ig, ok := b.e.(wgengine.InternalsGetter)
if !ok {
return nil, errors.New("engine isn't InternalsGetter")
}
_, mc, ok := ig.GetInternals()
if !ok {
return nil, errors.New("failed to get internals")
}
return mc, nil
}

View File

@@ -178,9 +178,31 @@ func TestShrinkDefaultRoute(t *testing.T) {
},
}
// Construct a fake local network environment to make this test hermetic.
// localInterfaceRoutes and hostIPs would normally come from calling interfaceRoutes,
// and localAddresses would normally come from calling interfaces.LocalAddresses.
var b netaddr.IPSetBuilder
for _, c := range []string{"127.0.0.0/8", "192.168.9.0/24", "fe80::/32"} {
p := netaddr.MustParseIPPrefix(c)
b.AddPrefix(p)
}
localInterfaceRoutes, err := b.IPSet()
if err != nil {
t.Fatal(err)
}
hostIPs := []netaddr.IP{
netaddr.MustParseIP("127.0.0.1"),
netaddr.MustParseIP("192.168.9.39"),
netaddr.MustParseIP("fe80::1"),
netaddr.MustParseIP("fe80::437d:feff:feca:49a7"),
}
localAddresses := []netaddr.IP{
netaddr.MustParseIP("192.168.9.39"),
}
for _, test := range tests {
def := netaddr.MustParseIPPrefix(test.route)
got, err := shrinkDefaultRoute(def)
got, err := shrinkDefaultRoute(def, localInterfaceRoutes, hostIPs)
if err != nil {
t.Fatalf("shrinkDefaultRoute(%q): %v", test.route, err)
}
@@ -194,11 +216,7 @@ func TestShrinkDefaultRoute(t *testing.T) {
t.Errorf("shrink(%q).Contains(%v) = true, want false", test.route, ip)
}
}
ips, _, err := interfaces.LocalAddresses()
if err != nil {
t.Fatal(err)
}
for _, ip := range ips {
for _, ip := range localAddresses {
want := test.localIPFn(ip)
if gotContains := got.Contains(ip); gotContains != want {
t.Errorf("shrink(%q).Contains(%v) = %v, want %v", test.route, ip, gotContains, want)

View File

@@ -32,6 +32,7 @@ import (
"golang.org/x/net/dns/dnsmessage"
"inet.af/netaddr"
"tailscale.com/client/tailscale/apitype"
"tailscale.com/health"
"tailscale.com/hostinfo"
"tailscale.com/ipn"
"tailscale.com/logtail/backoff"
@@ -553,6 +554,12 @@ func (h *peerAPIHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
case "/v0/metrics":
h.handleServeMetrics(w, r)
return
case "/v0/magicsock":
h.handleServeMagicsock(w, r)
return
case "/v0/dnsfwd":
h.handleServeDNSFwd(w, r)
return
}
who := h.peerUser.DisplayName
fmt.Fprintf(w, `<html>
@@ -781,6 +788,21 @@ func (h *peerAPIHandler) handleServeEnv(w http.ResponseWriter, r *http.Request)
json.NewEncoder(w).Encode(data)
}
func (h *peerAPIHandler) handleServeMagicsock(w http.ResponseWriter, r *http.Request) {
if !h.isSelf {
http.Error(w, "not owner", http.StatusForbidden)
return
}
eng := h.ps.b.e
if ig, ok := eng.(wgengine.InternalsGetter); ok {
if _, mc, ok := ig.GetInternals(); ok {
mc.ServeHTTPDebug(w, r)
return
}
}
http.Error(w, "miswired", 500)
}
func (h *peerAPIHandler) handleServeMetrics(w http.ResponseWriter, r *http.Request) {
if !h.isSelf {
http.Error(w, "not owner", http.StatusForbidden)
@@ -790,6 +812,19 @@ func (h *peerAPIHandler) handleServeMetrics(w http.ResponseWriter, r *http.Reque
clientmetric.WritePrometheusExpositionFormat(w)
}
func (h *peerAPIHandler) handleServeDNSFwd(w http.ResponseWriter, r *http.Request) {
if !h.isSelf {
http.Error(w, "not owner", http.StatusForbidden)
return
}
dh := health.DebugHandler("dnsfwd")
if dh == nil {
http.Error(w, "not wired up", 500)
return
}
dh.ServeHTTP(w, r)
}
func (h *peerAPIHandler) replyToDNSQueries() bool {
if h.isSelf {
// If the peer is owned by the same user, just allow it

View File

@@ -87,8 +87,9 @@ func (nt *notifyThrottler) drain(count int) []ipn.Notify {
type mockControl struct {
tb testing.TB
opts controlclient.Options
logf logger.Logf
logfActual logger.Logf
statusFunc func(controlclient.Status)
preventLog syncs.AtomicBool
mu sync.Mutex
calls []string
@@ -104,6 +105,13 @@ func newMockControl(tb testing.TB) *mockControl {
}
}
func (cc *mockControl) logf(format string, args ...interface{}) {
if cc.preventLog.Get() || cc.logfActual == nil {
return
}
cc.logfActual(format, args...)
}
func (cc *mockControl) SetStatusFunc(fn func(controlclient.Status)) {
cc.statusFunc = fn
}
@@ -284,6 +292,7 @@ func TestStateMachine(t *testing.T) {
t.Cleanup(e.Close)
cc := newMockControl(t)
t.Cleanup(func() { cc.preventLog.Set(true) }) // hacky way to pacify issue 3020
b, err := NewLocalBackend(logf, "logid", store, nil, e)
if err != nil {
t.Fatalf("NewLocalBackend: %v", err)
@@ -291,7 +300,7 @@ func TestStateMachine(t *testing.T) {
b.SetControlClientGetterForTesting(func(opts controlclient.Options) (controlclient.Client, error) {
cc.mu.Lock()
cc.opts = opts
cc.logf = opts.Logf
cc.logfActual = opts.Logf
cc.authBlocked = true
cc.persist = cc.opts.Persist
cc.mu.Unlock()
@@ -305,6 +314,9 @@ func TestStateMachine(t *testing.T) {
notifies.expect(0)
b.SetNotifyCallback(func(n ipn.Notify) {
if cc.preventLog.Get() {
return
}
if n.State != nil ||
n.Prefs != nil ||
n.BrowseToURL != nil ||
@@ -315,6 +327,7 @@ func TestStateMachine(t *testing.T) {
logf("\n(ignored) %v\n\n", n)
}
})
t.Cleanup(func() { b.SetNotifyCallback(nil) }) // hacky way to pacify issue 3020
// Check that it hasn't called us right away.
// The state machine should be idle until we call Start().
@@ -948,7 +961,7 @@ func TestWGEngineStatusRace(t *testing.T) {
b.SetControlClientGetterForTesting(func(opts controlclient.Options) (controlclient.Client, error) {
cc.mu.Lock()
defer cc.mu.Unlock()
cc.logf = opts.Logf
cc.logfActual = opts.Logf
return cc, nil
})

View File

@@ -0,0 +1,74 @@
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package ipnserver
import (
"bufio"
"context"
"io"
"net"
"net/http"
"time"
"tailscale.com/logpolicy"
"tailscale.com/types/logger"
)
// handleProxyConnectConn handles a CONNECT request to
// log.tailscale.io (or whatever the configured log server is). This
// is intended for use by the Windows GUI client to log via when an
// exit node is in use, so the logs don't go out via the exit node and
// instead go directly, like tailscaled's. The dialer tried to do that
// in the unprivileged GUI by binding to a specific interface, but the
// "Internet Kill Switch" installed by tailscaled for exit nodes
// precludes that from working and instead the GUI fails to dial out.
// So, go through tailscaled (with a CONNECT request) instead.
func (s *Server) handleProxyConnectConn(ctx context.Context, br *bufio.Reader, c net.Conn, logf logger.Logf) {
defer c.Close()
c.SetReadDeadline(time.Now().Add(5 * time.Second)) // should be long enough to send the HTTP headers
req, err := http.ReadRequest(br)
if err != nil {
logf("ReadRequest: %v", err)
return
}
c.SetReadDeadline(time.Time{})
if req.Method != "CONNECT" {
logf("ReadRequest: unexpected method %q, not CONNECT", req.Method)
return
}
hostPort := req.RequestURI
logHost := logpolicy.LogHost()
allowed := net.JoinHostPort(logHost, "443")
if hostPort != allowed {
logf("invalid CONNECT target %q; want %q", hostPort, allowed)
io.WriteString(c, "HTTP/1.1 403 Forbidden\r\n\r\nBad CONNECT target.\n")
return
}
tr := logpolicy.NewLogtailTransport(logHost)
back, err := tr.DialContext(ctx, "tcp", hostPort)
if err != nil {
logf("error CONNECT dialing %v: %v", hostPort, err)
io.WriteString(c, "HTTP/1.1 502 Fail\r\n\r\nConnect failure.\n")
return
}
defer back.Close()
io.WriteString(c, "HTTP/1.1 200 OK\r\n\r\n")
errc := make(chan error, 2)
go func() {
_, err := io.Copy(c, back)
errc <- err
}()
go func() {
_, err := io.Copy(back, br)
errc <- err
}()
<-errc
}

View File

@@ -20,6 +20,7 @@ import (
"os/exec"
"os/signal"
"os/user"
"path/filepath"
"runtime"
"strconv"
"strings"
@@ -35,7 +36,6 @@ import (
"tailscale.com/ipn/ipnlocal"
"tailscale.com/ipn/localapi"
"tailscale.com/ipn/store/aws"
"tailscale.com/log/filelogger"
"tailscale.com/logtail/backoff"
"tailscale.com/net/netstat"
"tailscale.com/net/tsdial"
@@ -239,12 +239,28 @@ func bufferHasHTTPRequest(br *bufio.Reader) bool {
mem.Contains(mem.B(peek), mem.S(" HTTP/"))
}
// bufferIsConnect reports whether br looks like it's likely an HTTP
// CONNECT request.
//
// Invariant: br has already had at least 4 bytes Peek'ed.
func bufferIsConnect(br *bufio.Reader) bool {
peek, _ := br.Peek(br.Buffered())
return mem.HasPrefix(mem.B(peek), mem.S("CONN"))
}
func (s *Server) serveConn(ctx context.Context, c net.Conn, logf logger.Logf) {
// First see if it's an HTTP request.
br := bufio.NewReader(c)
c.SetReadDeadline(time.Now().Add(time.Second))
br.Peek(4)
c.SetReadDeadline(time.Time{})
// Handle logtail CONNECT requests early. (See docs on handleProxyConnectConn)
if bufferIsConnect(br) {
s.handleProxyConnectConn(ctx, br, c, logf)
return
}
isHTTPReq := bufferHasHTTPRequest(br)
ci, err := s.addConn(c, isHTTPReq)
@@ -869,14 +885,6 @@ func BabysitProc(ctx context.Context, args []string, logf logger.Logf) {
panic("cannot determine executable: " + err.Error())
}
if runtime.GOOS == "windows" {
if len(args) != 2 && args[0] != "/subproc" {
panic(fmt.Sprintf("unexpected arguments %q", args))
}
logID := args[1]
logf = filelogger.New("tailscale-service", logID, logf)
}
var proc struct {
mu sync.Mutex
p *os.Process
@@ -908,6 +916,14 @@ func BabysitProc(ctx context.Context, args []string, logf logger.Logf) {
startTime := time.Now()
log.Printf("exec: %#v %v", executable, args)
cmd := exec.Command(executable, args...)
if runtime.GOOS == "windows" {
extraEnv, err := loadExtraEnv()
if err != nil {
logf("errors loading extra env file; ignoring: %v", err)
} else {
cmd.Env = append(os.Environ(), extraEnv...)
}
}
// Create a pipe object to use as the subproc's stdin.
// When the writer goes away, the reader gets EOF.
@@ -1177,3 +1193,47 @@ func findTrueNASTaildropDir(name string) (dir string, err error) {
}
return "", fmt.Errorf("shared folder %q not found", name)
}
func loadExtraEnv() (env []string, err error) {
if runtime.GOOS != "windows" {
return nil, nil
}
name := filepath.Join(os.Getenv("ProgramData"), "Tailscale", "tailscaled-env.txt")
contents, err := os.ReadFile(name)
if os.IsNotExist(err) {
return nil, nil
}
if err != nil {
return nil, err
}
for _, line := range strings.Split(string(contents), "\n") {
line = strings.TrimSpace(line)
if line == "" || line[0] == '#' {
continue
}
k, v, ok := stringsCut(line, "=")
if !ok || k == "" {
continue
}
if strings.HasPrefix(v, `"`) {
var err error
v, err = strconv.Unquote(v)
if err != nil {
return nil, fmt.Errorf("invalid value in line %q: %v", line, err)
}
env = append(env, k+"="+v)
} else {
env = append(env, line)
}
}
return env, nil
}
// stringsCut is Go 1.18's strings.Cut.
// TODO(bradfitz): delete this when we depend on Go 1.18.
func stringsCut(s, sep string) (before, after string, found bool) {
if i := strings.Index(s, sep); i >= 0 {
return s[:i], s[i+len(sep):], true
}
return s, "", false
}

View File

@@ -113,6 +113,8 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
h.serveDERPMap(w, r)
case "/localapi/v0/metrics":
h.serveMetrics(w, r)
case "/localapi/v0/debug":
h.serveDebug(w, r)
case "/":
io.WriteString(w, "tailscaled\n")
default:
@@ -195,6 +197,35 @@ func (h *Handler) serveMetrics(w http.ResponseWriter, r *http.Request) {
clientmetric.WritePrometheusExpositionFormat(w)
}
func (h *Handler) serveDebug(w http.ResponseWriter, r *http.Request) {
if !h.PermitWrite {
http.Error(w, "debug access denied", http.StatusForbidden)
return
}
if r.Method != "POST" {
http.Error(w, "POST required", http.StatusMethodNotAllowed)
return
}
action := r.FormValue("action")
var err error
switch action {
case "rebind":
err = h.b.DebugRebind()
case "restun":
err = h.b.DebugReSTUN()
case "":
err = fmt.Errorf("missing parameter 'action'")
default:
err = fmt.Errorf("unknown action %q", action)
}
if err != nil {
http.Error(w, err.Error(), 400)
return
}
w.Header().Set("Content-Type", "text/plain")
io.WriteString(w, "done\n")
}
// serveProfileFunc is the implementation of Handler.serveProfile, after auth,
// for platforms where we want to link it in.
var serveProfileFunc func(http.ResponseWriter, *http.Request)

View File

@@ -426,6 +426,47 @@ func (p *Prefs) AdminPageURL() string {
return url + "/admin/machines"
}
// AdvertisesExitNode reports whether p is advertising both the v4 and
// v6 /0 exit node routes.
func (p *Prefs) AdvertisesExitNode() bool {
if p == nil {
return false
}
var v4, v6 bool
for _, r := range p.AdvertiseRoutes {
if r.Bits() != 0 {
continue
}
if r.IP().Is4() {
v4 = true
} else if r.IP().Is6() {
v6 = true
}
}
return v4 && v6
}
// SetAdvertiseExitNode mutates p (if non-nil) to add or remove the two
// /0 exit node routes.
func (p *Prefs) SetAdvertiseExitNode(runExit bool) {
if p == nil {
return
}
all := p.AdvertiseRoutes
p.AdvertiseRoutes = p.AdvertiseRoutes[:0]
for _, r := range all {
if r.Bits() != 0 {
p.AdvertiseRoutes = append(p.AdvertiseRoutes, r)
}
}
if !runExit {
return
}
p.AdvertiseRoutes = append(p.AdvertiseRoutes,
netaddr.IPPrefixFrom(netaddr.IPv4(0, 0, 0, 0), 0),
netaddr.IPPrefixFrom(netaddr.IPv6Unspecified(), 0))
}
// PrefsFromBytes deserializes Prefs from a JSON blob. If
// enforceDefaults is true, Prefs.RouteAll and Prefs.AllowSingleHosts
// are forced on.

View File

@@ -646,3 +646,35 @@ func TestMaskedPrefsPretty(t *testing.T) {
}
}
}
func TestPrefsExitNode(t *testing.T) {
var p *Prefs
if p.AdvertisesExitNode() {
t.Errorf("nil shouldn't advertise exit node")
}
p = NewPrefs()
if p.AdvertisesExitNode() {
t.Errorf("default shouldn't advertise exit node")
}
p.AdvertiseRoutes = []netaddr.IPPrefix{
netaddr.MustParseIPPrefix("10.0.0.0/16"),
}
p.SetAdvertiseExitNode(true)
if got, want := len(p.AdvertiseRoutes), 3; got != want {
t.Errorf("routes = %d; want %d", got, want)
}
p.SetAdvertiseExitNode(true)
if got, want := len(p.AdvertiseRoutes), 3; got != want {
t.Errorf("routes = %d; want %d", got, want)
}
if !p.AdvertisesExitNode() {
t.Errorf("not advertising after enable")
}
p.SetAdvertiseExitNode(false)
if p.AdvertisesExitNode() {
t.Errorf("advertising after disable")
}
if got, want := len(p.AdvertiseRoutes), 1; got != want {
t.Errorf("routes = %d; want %d", got, want)
}
}

View File

@@ -8,11 +8,14 @@
package logpolicy
import (
"bufio"
"bytes"
"context"
"crypto/tls"
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"log"
"net"
@@ -29,6 +32,7 @@ import (
"golang.org/x/term"
"tailscale.com/atomicfile"
"tailscale.com/log/filelogger"
"tailscale.com/logtail"
"tailscale.com/logtail/filch"
"tailscale.com/net/dnscache"
@@ -38,6 +42,7 @@ import (
"tailscale.com/net/tlsdial"
"tailscale.com/net/tshttpproxy"
"tailscale.com/paths"
"tailscale.com/safesocket"
"tailscale.com/smallzstd"
"tailscale.com/types/logger"
"tailscale.com/util/clientmetric"
@@ -65,6 +70,15 @@ func getLogTarget() string {
return getLogTargetOnce.v
}
// LogHost returns the hostname only (without port) of the configured
// logtail server, or the default.
func LogHost() string {
if v := getLogTarget(); v != "" {
return v
}
return logtail.DefaultHost
}
// Config represents an instance of logs in a collection.
type Config struct {
Collection string
@@ -524,8 +538,20 @@ func New(collection string) *Policy {
}
}
lw := logtail.NewLogger(c, log.Printf)
var logOutput io.Writer = lw
if runtime.GOOS == "windows" && c.Collection == logtail.CollectionNode {
logID := newc.PublicID.String()
exe, _ := os.Executable()
if strings.EqualFold(filepath.Base(exe), "tailscaled.exe") {
diskLogf := filelogger.New("tailscale-service", logID, lw.Logf)
logOutput = logger.FuncWriter(diskLogf)
}
}
log.SetFlags(0) // other logflags are set on console, not here
log.SetOutput(lw)
log.SetOutput(logOutput)
log.Printf("Program starting: v%v, Go %v: %#v",
version.Long,
@@ -602,6 +628,24 @@ func NewLogtailTransport(host string) *http.Transport {
return c, nil
}
if version.IsWindowsGUI() && strings.HasPrefix(netw, "tcp") {
if c, err := safesocket.Connect(safesocket.DefaultConnectionStrategy("")); err == nil {
fmt.Fprintf(c, "CONNECT %s HTTP/1.0\r\n\r\n", addr)
br := bufio.NewReader(c)
res, err := http.ReadResponse(br, nil)
if err == nil && res.StatusCode != 200 {
err = errors.New(res.Status)
}
if err != nil {
log.Printf("logtail: CONNECT response from tailscaled: %v", err)
c.Close()
} else {
log.Printf("logtail: connected via tailscaled")
return c, nil
}
}
}
// If we failed to dial, try again with bootstrap DNS.
log.Printf("logtail: dial %q failed: %v (in %v), trying bootstrap...", addr, err, d)
dnsCache := &dnscache.Resolver{

View File

@@ -523,6 +523,11 @@ func (l *Logger) encode(buf []byte) []byte {
return b
}
// Logf logs to l using the provided fmt-style format and optional arguments.
func (l *Logger) Logf(format string, args ...interface{}) {
fmt.Fprintf(l, format, args...)
}
// Write logs an encoded JSON blob.
//
// If the []byte passed to Write is not an encoded JSON blob,

View File

@@ -11,6 +11,7 @@ import (
"inet.af/netaddr"
"tailscale.com/net/dns/resolver"
"tailscale.com/net/tsaddr"
"tailscale.com/types/dnstype"
"tailscale.com/util/dnsname"
)
@@ -40,6 +41,16 @@ type Config struct {
// it to resolve, you also need to add appropriate routes to
// Routes.
Hosts map[dnsname.FQDN][]netaddr.IP
// OnlyIPv6, if true, uses the IPv6 service IP (for MagicDNS)
// instead of the IPv4 version (100.100.100.100).
OnlyIPv6 bool
}
func (c *Config) serviceIP() netaddr.IP {
if c.OnlyIPv6 {
return tsaddr.TailscaleServiceIPv6()
}
return tsaddr.TailscaleServiceIP()
}
// WriteToBufioWriter write a debug version of c for logs to w, omitting

View File

@@ -344,7 +344,14 @@ func (m *directManager) SetDNS(config OSConfig) (err error) {
// cause a disruptive DNS outage each time we reset an empty
// OS configuration.
if changed && isResolvedRunning() && !runningAsGUIDesktopUser() {
exec.Command("systemctl", "restart", "systemd-resolved.service").Run()
t0 := time.Now()
err := restartResolved()
d := time.Since(t0).Round(time.Millisecond)
if err != nil {
m.logf("error restarting resolved after %v: %v", d, err)
} else {
m.logf("restarted resolved after %v", d)
}
}
return nil

View File

@@ -12,7 +12,6 @@ import (
"inet.af/netaddr"
"tailscale.com/health"
"tailscale.com/net/dns/resolver"
"tailscale.com/net/tsaddr"
"tailscale.com/net/tsdial"
"tailscale.com/types/dnstype"
"tailscale.com/types/logger"
@@ -122,7 +121,7 @@ func (m *Manager) compileConfig(cfg Config) (rcfg resolver.Config, ocfg OSConfig
// through quad-100.
rcfg.Routes = routes
rcfg.Routes["."] = cfg.DefaultResolvers
ocfg.Nameservers = []netaddr.IP{tsaddr.TailscaleServiceIP()}
ocfg.Nameservers = []netaddr.IP{cfg.serviceIP()}
return rcfg, ocfg, nil
}
@@ -159,7 +158,7 @@ func (m *Manager) compileConfig(cfg Config) (rcfg resolver.Config, ocfg OSConfig
// or routes + MagicDNS, or just MagicDNS, or on an OS that cannot
// split-DNS. Install a split config pointing at quad-100.
rcfg.Routes = routes
ocfg.Nameservers = []netaddr.IP{tsaddr.TailscaleServiceIP()}
ocfg.Nameservers = []netaddr.IP{cfg.serviceIP()}
// If the OS can't do native split-dns, read out the underlying
// resolver config and blend it into our config.

View File

@@ -4,8 +4,71 @@
package dns
import "tailscale.com/types/logger"
import (
"bytes"
"fmt"
"os"
func NewOSConfigurator(logf logger.Logf, _ string) (OSConfigurator, error) {
"tailscale.com/types/logger"
)
type kv struct {
k, v string
}
func (kv kv) String() string {
return fmt.Sprintf("%s=%s", kv.k, kv.v)
}
func NewOSConfigurator(logf logger.Logf, interfaceName string) (OSConfigurator, error) {
return newOSConfigurator(logf, interfaceName,
newOSConfigEnv{
rcIsResolvd: rcIsResolvd,
fs: directFS{},
})
}
// newOSConfigEnv are the funcs newOSConfigurator needs, pulled out for testing.
type newOSConfigEnv struct {
fs directFS
rcIsResolvd func(resolvConfContents []byte) bool
}
func newOSConfigurator(logf logger.Logf, interfaceName string, env newOSConfigEnv) (ret OSConfigurator, err error) {
var debug []kv
dbg := func(k, v string) {
debug = append(debug, kv{k, v})
}
defer func() {
if ret != nil {
dbg("ret", fmt.Sprintf("%T", ret))
}
logf("dns: %v", debug)
}()
bs, err := env.fs.ReadFile(resolvConf)
if os.IsNotExist(err) {
dbg("rc", "missing")
return newDirectManager(logf), nil
}
if err != nil {
return nil, fmt.Errorf("reading /etc/resolv.conf: %w", err)
}
if env.rcIsResolvd(bs) {
dbg("resolvd", "yes")
return newResolvdManager(logf, interfaceName)
}
dbg("resolvd", "missing")
return newDirectManager(logf), nil
}
func rcIsResolvd(resolvConfContents []byte) bool {
// If we have the string "# resolvd:" in resolv.conf resolvd(8) is
// managing things.
if bytes.Contains(resolvConfContents, []byte("# resolvd:")) {
return true
}
return false
}

View File

@@ -7,8 +7,10 @@ package dns
import (
"errors"
"fmt"
"os"
"os/exec"
"sort"
"strconv"
"strings"
"syscall"
"time"
@@ -34,6 +36,8 @@ const (
versionKey = `SOFTWARE\Microsoft\Windows NT\CurrentVersion`
)
var configureWSL, _ = strconv.ParseBool(os.Getenv("TS_DEBUG_CONFIGURE_WSL"))
type windowsManager struct {
logf logger.Logf
guid string
@@ -307,13 +311,15 @@ func (m windowsManager) SetDNS(cfg OSConfig) error {
// On initial setup of WSL, the restart caused by --shutdown is slow,
// so we do it out-of-line.
go func() {
if err := m.wslManager.SetDNS(cfg); err != nil {
m.logf("WSL SetDNS: %v", err) // continue
} else {
m.logf("WSL SetDNS: success")
}
}()
if configureWSL {
go func() {
if err := m.wslManager.SetDNS(cfg); err != nil {
m.logf("WSL SetDNS: %v", err) // continue
} else {
m.logf("WSL SetDNS: success")
}
}()
}
return nil
}

161
net/dns/resolvd.go Normal file
View File

@@ -0,0 +1,161 @@
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build openbsd
// +build openbsd
package dns
import (
"bufio"
"bytes"
"fmt"
"os"
"os/exec"
"regexp"
"strings"
"inet.af/netaddr"
"tailscale.com/types/logger"
"tailscale.com/util/dnsname"
)
func newResolvdManager(logf logger.Logf, interfaceName string) (*resolvdManager, error) {
return &resolvdManager{
logf: logf,
ifName: interfaceName,
fs: directFS{},
}, nil
}
// resolvdManager is an OSConfigurator which uses route(1) to teach OpenBSD's
// resolvd(8) about DNS servers.
type resolvdManager struct {
logf logger.Logf
ifName string
fs directFS
}
func (m *resolvdManager) SetDNS(config OSConfig) error {
args := []string{
"nameserver",
m.ifName,
}
origResolv, err := m.readAndCopy(resolvConf, backupConf, 0644)
if err != nil {
return err
}
newResolvConf := removeSearchLines(origResolv)
for _, ns := range config.Nameservers {
args = append(args, ns.String())
}
var newSearch = []string{
"search",
}
for _, s := range config.SearchDomains {
newSearch = append(newSearch, s.WithoutTrailingDot())
}
newResolvConf = append(newResolvConf, []byte(strings.Join(newSearch, " "))...)
err = m.fs.WriteFile(resolvConf, newResolvConf, 0644)
if err != nil {
return err
}
cmd := exec.Command("/sbin/route", args...)
return cmd.Run()
}
func (m *resolvdManager) SupportsSplitDNS() bool {
return false
}
func (m *resolvdManager) GetBaseConfig() (OSConfig, error) {
cfg, err := m.readResolvConf()
if err != nil {
return OSConfig{}, err
}
return cfg, nil
}
func (m *resolvdManager) Close() error {
// resolvd handles teardown of nameservers so we only need to write back the original
// config and be done.
_, err := m.readAndCopy(backupConf, resolvConf, 0644)
if err != nil {
return err
}
return m.fs.Remove(backupConf)
}
func (m *resolvdManager) readAndCopy(a, b string, mode os.FileMode) ([]byte, error) {
orig, err := m.fs.ReadFile(a)
if err != nil {
return nil, err
}
err = m.fs.WriteFile(b, orig, mode)
if err != nil {
return nil, err
}
return orig, nil
}
func (m resolvdManager) readResolvConf() (config OSConfig, err error) {
b, err := m.fs.ReadFile(resolvConf)
if err != nil {
return OSConfig{}, err
}
scanner := bufio.NewScanner(bytes.NewReader(b))
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
// resolvd manages "nameserver" lines, we only need to handle
// "search".
if strings.HasPrefix(line, "search") {
domain := strings.TrimPrefix(line, "search")
domain = strings.TrimSpace(domain)
fqdn, err := dnsname.ToFQDN(domain)
if err != nil {
return OSConfig{}, fmt.Errorf("parsing search domains %q: %w", line, err)
}
config.SearchDomains = append(config.SearchDomains, fqdn)
continue
}
if strings.HasPrefix(line, "nameserver") {
s := strings.TrimPrefix(line, "nameserver")
parts := strings.Split(s, " # ")
if len(parts) == 0 {
return OSConfig{}, err
}
nameserver := strings.TrimSpace(parts[0])
ip, err := netaddr.ParseIP(nameserver)
if err != nil {
return OSConfig{}, err
}
config.Nameservers = append(config.Nameservers, ip)
continue
}
}
if err = scanner.Err(); err != nil {
return OSConfig{}, err
}
return config, nil
}
func removeSearchLines(orig []byte) []byte {
re := regexp.MustCompile(`(?m)^search\s+.+$`)
return re.ReplaceAll(orig, []byte(""))
}

78
net/dns/resolver/debug.go Normal file
View File

@@ -0,0 +1,78 @@
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package resolver
import (
"fmt"
"html"
"net/http"
"strconv"
"sync"
"sync/atomic"
"time"
"tailscale.com/health"
)
func init() {
health.RegisterDebugHandler("dnsfwd", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
n, _ := strconv.Atoi(r.FormValue("n"))
if n <= 0 {
n = 100
} else if n > 10000 {
n = 10000
}
fl, ok := fwdLogAtomic.Load().(*fwdLog)
if !ok || n != len(fl.ent) {
fl = &fwdLog{ent: make([]fwdLogEntry, n)}
fwdLogAtomic.Store(fl)
}
fl.ServeHTTP(w, r)
}))
}
var fwdLogAtomic atomic.Value // of *fwdLog
type fwdLog struct {
mu sync.Mutex
pos int // ent[pos] is next entry
ent []fwdLogEntry
}
type fwdLogEntry struct {
Domain string
Time time.Time
}
func (fl *fwdLog) addName(name string) {
if fl == nil {
return
}
fl.mu.Lock()
defer fl.mu.Unlock()
if len(fl.ent) == 0 {
return
}
fl.ent[fl.pos] = fwdLogEntry{Domain: name, Time: time.Now()}
fl.pos++
if fl.pos == len(fl.ent) {
fl.pos = 0
}
}
func (fl *fwdLog) ServeHTTP(w http.ResponseWriter, r *http.Request) {
fl.mu.Lock()
defer fl.mu.Unlock()
fmt.Fprintf(w, "<html><h1>DNS forwards</h1>")
now := time.Now()
for i := 0; i < len(fl.ent); i++ {
ent := fl.ent[(i+fl.pos)%len(fl.ent)]
if ent.Domain == "" {
continue
}
fmt.Fprintf(w, "%v ago: %v<br>\n", now.Sub(ent.Time).Round(time.Second), html.EscapeString(ent.Domain))
}
}

View File

@@ -25,6 +25,7 @@ import (
dns "golang.org/x/net/dns/dnsmessage"
"inet.af/netaddr"
"tailscale.com/hostinfo"
"tailscale.com/net/neterror"
"tailscale.com/net/netns"
"tailscale.com/net/tsdial"
"tailscale.com/types/dnstype"
@@ -482,7 +483,7 @@ func (f *forwarder) send(ctx context.Context, fq *forwardQuery, rr resolverAndDe
if err := ctx.Err(); err != nil {
return nil, err
}
if packetWasTruncated(err) {
if neterror.PacketWasTruncated(err) {
err = nil
} else {
metricDNSFwdUDPErrorRead.Add(1)
@@ -576,8 +577,8 @@ func (f *forwarder) forward(query packet) error {
return f.forwardWithDestChan(ctx, query, f.responses)
}
// forward forwards the query to all upstream nameservers and waits
// for the first response.
// forwardWithDestChan forwards the query to all upstream nameservers
// and waits for the first response.
//
// It either sends to responseChan and returns nil, or returns a
// non-nil error (without sending to the channel).
@@ -598,7 +599,21 @@ func (f *forwarder) forwardWithDestChan(ctx context.Context, query packet, respo
// out, playing on Sonos still works.
if hasRDNSBonjourPrefix(domain) {
metricDNSFwdDropBonjour.Add(1)
return nil
res, err := nxDomainResponse(query)
if err != nil {
f.logf("error parsing bonjour query: %v", err)
return nil
}
select {
case <-ctx.Done():
return ctx.Err()
case responseChan <- res:
return nil
}
}
if fl, ok := fwdLogAtomic.Load().(*fwdLog); ok {
fl.addName(string(domain))
}
clampEDNSSize(query.bs, maxResponseBytes)
@@ -696,6 +711,28 @@ func nameFromQuery(bs []byte) (dnsname.FQDN, error) {
return dnsname.ToFQDN(rawNameToLower(n))
}
// nxDomainResponse returns an NXDomain DNS reply for the provided request.
func nxDomainResponse(req packet) (res packet, err error) {
p := dnsParserPool.Get().(*dnsParser)
defer dnsParserPool.Put(p)
if err := p.parseQuery(req.bs); err != nil {
return packet{}, err
}
h := p.Header
h.Response = true
h.RecursionAvailable = h.RecursionDesired
h.RCode = dns.RCodeNameError
b := dns.NewBuilder(nil, h)
// TODO(bradfitz): should we add an SOA record in the Authority
// section too? (for the nxdomain negative caching TTL)
// For which zone? Does iOS care?
res.bs, err = b.Finish()
res.addr = req.addr
return res, err
}
// closePool is a dynamic set of io.Closers to close as a group.
// It's intended to be Closed at most once.
//

View File

@@ -168,3 +168,25 @@ func TestMaxDoHInFlight(t *testing.T) {
})
}
}
func BenchmarkNameFromQuery(b *testing.B) {
builder := dns.NewBuilder(nil, dns.Header{})
builder.StartQuestions()
builder.Question(dns.Question{
Name: dns.MustNewName("foo.example."),
Type: dns.TypeA,
Class: dns.ClassINET,
})
msg, err := builder.Finish()
if err != nil {
b.Fatal(err)
}
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_, err := nameFromQuery(msg)
if err != nil {
b.Fatal(err)
}
}
}

View File

@@ -1,30 +0,0 @@
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package resolver
import (
"errors"
"syscall"
)
// Avoid allocation when calling errors.Is below
// by converting syscall.Errno to error here.
var (
networkDown error = syscall.ENETDOWN
networkUnreachable error = syscall.ENETUNREACH
)
func networkIsDown(err error) bool {
return errors.Is(err, networkDown)
}
func networkIsUnreachable(err error) bool {
return errors.Is(err, networkUnreachable)
}
// packetWasTruncated returns true if err indicates truncation but the RecvFrom
// that generated err was otherwise successful. It always returns false on this
// platform.
func packetWasTruncated(err error) bool { return false }

View File

@@ -1,16 +0,0 @@
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build !darwin && !windows
// +build !darwin,!windows
package resolver
func networkIsDown(err error) bool { return false }
func networkIsUnreachable(err error) bool { return false }
// packetWasTruncated returns true if err indicates truncation but the RecvFrom
// that generated err was otherwise successful. It always returns false on this
// platform.
func packetWasTruncated(err error) bool { return false }

View File

@@ -1,43 +0,0 @@
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package resolver
import (
"errors"
"net"
"os"
"golang.org/x/sys/windows"
)
func networkIsDown(err error) bool {
if oe, ok := err.(*net.OpError); ok && oe.Op == "write" {
if se, ok := oe.Err.(*os.SyscallError); ok {
if se.Syscall == "wsasendto" && se.Err == windows.WSAENETUNREACH {
return true
}
}
}
return false
}
func networkIsUnreachable(err error) bool {
// TODO(bradfitz,josharian): something here? what is the
// difference between down and unreachable? Add comments.
return false
}
// packetWasTruncated returns true if err indicates truncation but the RecvFrom
// that generated err was otherwise successful. On Windows, Go's UDP RecvFrom
// calls WSARecvFrom which returns the WSAEMSGSIZE error code when the received
// datagram is larger than the provided buffer. When that happens, both a valid
// size and an error are returned (as per the partial fix for golang/go#14074).
// If the WSAEMSGSIZE error is returned, then we ignore the error to get
// semantics similar to the POSIX operating systems. One caveat is that it
// appears that the source address is not returned when WSAEMSGSIZE occurs, but
// we do not currently look at the source address.
func packetWasTruncated(err error) bool {
return errors.Is(err, windows.WSAEMSGSIZE)
}

View File

@@ -357,10 +357,7 @@ func (r *Resolver) HandleExitNodeDNSQuery(ctx context.Context, q []byte, from ne
switch runtime.GOOS {
default:
return nil, errors.New("unsupported exit node OS")
case "windows":
// TODO: use DnsQueryEx and write to ch.
// See https://docs.microsoft.com/en-us/windows/win32/api/windns/nf-windns-dnsqueryex.
// For now just use the net package:
case "windows", "android":
return handleExitNodeDNSQueryWithNetPkg(ctx, nil, resp)
case "darwin":
// /etc/resolv.conf is a lie and only says one upstream DNS
@@ -377,7 +374,7 @@ func (r *Resolver) HandleExitNodeDNSQuery(ctx context.Context, q []byte, from ne
// TODO: more than 1 resolver from /etc/resolv.conf?
var resolvers []resolverAndDelay
if nameserver == tsaddr.TailscaleServiceIP() {
if nameserver == tsaddr.TailscaleServiceIP() || nameserver == tsaddr.TailscaleServiceIPv6() {
// If resolv.conf says 100.100.100.100, it's coming right back to us anyway
// so avoid the loop through the kernel and just do what we
// would've done anyway. By not passing any resolvers, the forwarder
@@ -532,6 +529,10 @@ func stubResolverForOS() (ip netaddr.IP, err error) {
if c, ok := resolvConfCacheValue.Load().(resolvConfCache); ok && c.mod == cur.mod && c.size == cur.size {
return c.ip, nil
}
// TODO(bradfitz): unify this /etc/resolv.conf parsing code with readResolv
// in net/dns, which we can't use due to circular dependency reasons.
// Move it to a leaf, including the OSConfig type (perhaps in its own dnstype
// package?)
err = lineread.File("/etc/resolv.conf", func(line []byte) error {
if !ip.IsZero() {
return nil
@@ -540,6 +541,12 @@ func stubResolverForOS() (ip netaddr.IP, err error) {
if len(line) == 0 || line[0] == '#' {
return nil
}
// Normalize tabs to spaces to simplify parsing code later.
for i, b := range line {
if b == '\t' {
line[i] = ' '
}
}
if mem.HasPrefix(mem.B(line), mem.S("nameserver ")) {
s := strings.TrimSpace(strings.TrimPrefix(string(line), "nameserver "))
ip, err = netaddr.ParseIP(s)
@@ -1087,11 +1094,13 @@ func rdnsNameToIPv6(name dnsname.FQDN) (ip netaddr.IP, ok bool) {
// It is assumed that resp.Question is populated by respond before this is called.
func (r *Resolver) respondReverse(query []byte, name dnsname.FQDN, resp *response) ([]byte, error) {
if hasRDNSBonjourPrefix(name) {
metricDNSReverseMissBonjour.Add(1)
return nil, errNotOurName
}
resp.Name, resp.Header.RCode = r.resolveLocalReverse(name)
if resp.Header.RCode == dns.RCodeRefused {
metricDNSReverseMissOther.Add(1)
return nil, errNotOurName
}
@@ -1235,4 +1244,7 @@ var (
metricDNSResolveLocalNoAll = clientmetric.NewCounter("dns_resolve_local_no_all")
metricDNSResolveNotImplType = clientmetric.NewCounter("dns_resolve_local_not_impl_type")
metricDNSResolveNoRecordType = clientmetric.NewCounter("dns_resolve_local_no_record_type")
metricDNSReverseMissBonjour = clientmetric.NewCounter("dns_reverse_miss_bonjour")
metricDNSReverseMissOther = clientmetric.NewCounter("dns_reverse_miss_other")
)

View File

@@ -457,9 +457,7 @@ func TLSDialer(fwd DialContextFunc, dnsCache *Resolver, tlsConfigBase *tls.Confi
}
}()
go func() {
err := tlsConn.Handshake()
handshakeTimeoutCancel()
errc <- err
errc <- tlsConn.Handshake()
}()
if err := <-errc; err != nil {
tcpConn.Close()

View File

@@ -172,6 +172,7 @@ func sortIPs(s []netaddr.IP) {
type Interface struct {
*net.Interface
AltAddrs []net.Addr // if non-nil, returned by Addrs
Desc string // extra description (used on Windows)
}
func (i Interface) IsLoopback() bool { return isLoopback(i.Interface) }
@@ -278,13 +279,16 @@ type State struct {
// instead of Wifi. This field is not populated by GetState.
IsExpensive bool
// DefaultRouteInterface is the interface name for the machine's default route.
// DefaultRouteInterface is the interface name for the
// machine's default route.
//
// It is not yet populated on all OSes.
// Its exact value should not be assumed to be a map key for
// the Interface maps above; it's only used for debugging.
//
// When non-empty, its value is the map key into Interface and
// InterfaceIPs.
DefaultRouteInterface string
// HTTPProxy is the HTTP proxy to use.
// HTTPProxy is the HTTP proxy to use, if any.
HTTPProxy string
// PAC is the URL to the Proxy Autoconfig URL, if applicable.
@@ -293,7 +297,13 @@ type State struct {
func (s *State) String() string {
var sb strings.Builder
fmt.Fprintf(&sb, "interfaces.State{defaultRoute=%v ifs={", s.DefaultRouteInterface)
fmt.Fprintf(&sb, "interfaces.State{defaultRoute=%v ", s.DefaultRouteInterface)
if s.DefaultRouteInterface != "" {
if iface, ok := s.Interface[s.DefaultRouteInterface]; ok && iface.Desc != "" {
fmt.Fprintf(&sb, "(%s) ", iface.Desc)
}
}
sb.WriteString("ifs={")
ifs := make([]string, 0, len(s.Interface))
for k := range s.Interface {
if anyInterestingIP(s.InterfaceIPs[k]) {
@@ -507,7 +517,16 @@ func GetState() (*State, error) {
return nil, err
}
s.DefaultRouteInterface, _ = DefaultRouteInterface()
dr, _ := DefaultRoute()
s.DefaultRouteInterface = dr.InterfaceName
// Populate description (for Windows, primarily) if present.
if desc := dr.InterfaceDesc; desc != "" {
if iface, ok := s.Interface[dr.InterfaceName]; ok {
iface.Desc = desc
s.Interface[dr.InterfaceName] = iface
}
}
if s.AnyInterfaceUp() {
req, err := http.NewRequest("GET", LoginEndpointForProxyDetermination, nil)
@@ -667,3 +686,36 @@ func netInterfaces() ([]Interface, error) {
}
return ret, nil
}
// DefaultRouteDetails are the
type DefaultRouteDetails struct {
// InterfaceName is the interface name. It must always be populated.
// It's like "eth0" (Linux), "Ethernet 2" (Windows), "en0" (macOS).
InterfaceName string
// InterfaceDesc is populated on Windows at least. It's a
// longer description, like "Red Hat VirtIO Ethernet Adapter".
InterfaceDesc string
// InterfaceIndex is like net.Interface.Index.
// Zero means not populated.
InterfaceIndex int
// TODO(bradfitz): break this out into v4-vs-v6 once that need arises.
}
// DefaultRouteInterface is like DefaultRoute but only returns the
// interface name.
func DefaultRouteInterface() (string, error) {
dr, err := DefaultRoute()
if err != nil {
return "", err
}
return dr.InterfaceName, nil
}
// DefaultRoute returns details of the network interface that owns
// the default route, not including any tailscale interfaces.
func DefaultRoute() (DefaultRouteDetails, error) {
return defaultRoute()
}

View File

@@ -16,16 +16,18 @@ import (
"inet.af/netaddr"
)
func DefaultRouteInterface() (string, error) {
func defaultRoute() (d DefaultRouteDetails, err error) {
idx, err := DefaultRouteInterfaceIndex()
if err != nil {
return "", err
return d, err
}
iface, err := net.InterfaceByIndex(idx)
if err != nil {
return "", err
return d, err
}
return iface.Name, nil
d.InterfaceName = iface.Name
d.InterfaceIndex = idx
return d, nil
}
// fetchRoutingTable calls route.FetchRIB, fetching NET_RT_DUMP2.

View File

@@ -11,6 +11,6 @@ import "errors"
var errTODO = errors.New("TODO")
func DefaultRouteInterface() (string, error) {
return "TODO", errTODO
func defaultRoute() (DefaultRouteDetails, error) {
return DefaultRouteDetails{}, errTODO
}

View File

@@ -122,17 +122,18 @@ func likelyHomeRouterIPAndroid() (ret netaddr.IP, ok bool) {
return ret, !ret.IsZero()
}
// DefaultRouteInterface returns the name of the network interface that owns
// the default route, not including any tailscale interfaces.
func DefaultRouteInterface() (string, error) {
func defaultRoute() (d DefaultRouteDetails, err error) {
v, err := defaultRouteInterfaceProcNet()
if err == nil {
return v, nil
d.InterfaceName = v
return d, nil
}
if runtime.GOOS == "android" {
return defaultRouteInterfaceAndroidIPRoute()
v, err = defaultRouteInterfaceAndroidIPRoute()
d.InterfaceName = v
return d, err
}
return v, err
return d, err
}
var zeroRouteBytes = []byte("00000000")

View File

@@ -104,3 +104,55 @@ func TestStateEqualFilteredIPFilter(t *testing.T) {
t.Errorf("%+v == %+v when restricting to interesting interfaces and IPs", s1, s2)
}
}
func TestStateString(t *testing.T) {
tests := []struct {
name string
s *State
want string
}{
{
name: "typical_linux",
s: &State{
DefaultRouteInterface: "eth0",
Interface: map[string]Interface{
"eth0": {
Interface: &net.Interface{
Flags: net.FlagUp,
},
},
"wlan0": {
Interface: &net.Interface{},
},
},
InterfaceIPs: map[string][]netaddr.IPPrefix{
"eth0": []netaddr.IPPrefix{
netaddr.MustParseIPPrefix("10.0.0.2/8"),
},
},
HaveV4: true,
},
want: `interfaces.State{defaultRoute=eth0 ifs={eth0:[10.0.0.2/8]} v4=true v6=false}`,
},
{
name: "default_desc",
s: &State{
DefaultRouteInterface: "foo",
Interface: map[string]Interface{
"foo": {
Desc: "a foo thing",
},
},
},
want: `interfaces.State{defaultRoute=foo (a foo thing) ifs={} v4=false v6=false}`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := tt.s.String()
if got != tt.want {
t.Errorf("wrong\n got: %s\nwant: %s\n", got, tt.want)
}
})
}
}

View File

@@ -5,7 +5,6 @@
package interfaces
import (
"fmt"
"log"
"net"
"net/url"
@@ -217,18 +216,20 @@ func GetWindowsDefault(family winipcfg.AddressFamily) (*winipcfg.IPAdapterAddres
return bestIface, nil
}
func DefaultRouteInterface() (string, error) {
func defaultRoute() (d DefaultRouteDetails, err error) {
// We always return the IPv4 default route.
// TODO(bradfitz): adjust API if/when anything cares. They could in theory differ, though,
// in which case we might send traffic to the wrong interface.
iface, err := GetWindowsDefault(windows.AF_INET)
if err != nil {
return "", err
return d, err
}
if iface == nil {
return "(none)", nil
if iface != nil {
d.InterfaceName = iface.FriendlyName()
d.InterfaceDesc = iface.Description()
d.InterfaceIndex = int(iface.IfIndex)
}
return fmt.Sprintf("%s (%s)", iface.FriendlyName(), iface.Description()), nil
return d, nil
}
var (

View File

@@ -27,6 +27,7 @@ import (
"inet.af/netaddr"
"tailscale.com/derp/derphttp"
"tailscale.com/net/interfaces"
"tailscale.com/net/neterror"
"tailscale.com/net/netns"
"tailscale.com/net/portmapper"
"tailscale.com/net/stun"
@@ -1234,7 +1235,7 @@ func (rs *reportState) runProbe(ctx context.Context, dm *tailcfg.DERPMap, probe
case probeIPv4:
metricSTUNSend4.Add(1)
n, err := rs.pc4.WriteTo(req, addr)
if n == len(req) && err == nil {
if n == len(req) && err == nil || neterror.TreatAsLostUDP(err) {
rs.mu.Lock()
rs.report.IPv4CanSend = true
rs.mu.Unlock()
@@ -1242,7 +1243,7 @@ func (rs *reportState) runProbe(ctx context.Context, dm *tailcfg.DERPMap, probe
case probeIPv6:
metricSTUNSend6.Add(1)
n, err := rs.pc6.WriteTo(req, addr)
if n == len(req) && err == nil {
if n == len(req) && err == nil || neterror.TreatAsLostUDP(err) {
rs.mu.Lock()
rs.report.IPv6CanSend = true
rs.mu.Unlock()

60
net/neterror/neterror.go Normal file
View File

@@ -0,0 +1,60 @@
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package neterror classifies network errors.
package neterror
import (
"errors"
"runtime"
"syscall"
)
var errEPERM error = syscall.EPERM // box it into interface just once
// TreatAsLostUDP reports whether err is an error from a UDP send
// operation that should be treated as a UDP packet that just got
// lost.
//
// Notably, on Linux this reports true for EPERM errors (from outbound
// firewall blocks) which aren't really send errors; they're just
// sends that are never going to make it because the local OS blocked
// it.
func TreatAsLostUDP(err error) bool {
if err == nil {
return false
}
switch runtime.GOOS {
case "linux":
// Linux, while not documented in the man page,
// returns EPERM when there's an OUTPUT rule with -j
// DROP or -j REJECT. We use this very specific
// Linux+EPERM check rather than something super broad
// like net.Error.Temporary which could be anything.
//
// For now we only do this on Linux, as such outgoing
// firewall violations mapping to syscall errors
// hasn't yet been observed on other OSes.
return errors.Is(err, errEPERM)
}
return false
}
var packetWasTruncated func(error) bool // non-nil on Windows at least
// PacketWasTruncated reports whether err indicates truncation but the RecvFrom
// that generated err was otherwise successful. On Windows, Go's UDP RecvFrom
// calls WSARecvFrom which returns the WSAEMSGSIZE error code when the received
// datagram is larger than the provided buffer. When that happens, both a valid
// size and an error are returned (as per the partial fix for golang/go#14074).
// If the WSAEMSGSIZE error is returned, then we ignore the error to get
// semantics similar to the POSIX operating systems. One caveat is that it
// appears that the source address is not returned when WSAEMSGSIZE occurs, but
// we do not currently look at the source address.
func PacketWasTruncated(err error) bool {
if packetWasTruncated == nil {
return false
}
return packetWasTruncated(err)
}

View File

@@ -0,0 +1,55 @@
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package neterror
import (
"errors"
"net"
"os"
"syscall"
"testing"
)
func TestTreatAsLostUDP(t *testing.T) {
tests := []struct {
name string
err error
want bool
}{
{"nil", nil, false},
{"non-nil", errors.New("foo"), false},
{"eperm", syscall.EPERM, true},
{
name: "operror",
err: &net.OpError{
Op: "write",
Err: &os.SyscallError{
Syscall: "sendto",
Err: syscall.EPERM,
},
},
want: true,
},
{
name: "host_unreach",
err: &net.OpError{
Op: "write",
Err: &os.SyscallError{
Syscall: "sendto",
Err: syscall.EHOSTUNREACH,
},
},
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := TreatAsLostUDP(tt.err); got != tt.want {
t.Errorf("got = %v; want %v", got, tt.want)
}
})
}
}

View File

@@ -0,0 +1,17 @@
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package neterror
import (
"errors"
"golang.org/x/sys/windows"
)
func init() {
packetWasTruncated = func(err error) bool {
return errors.Is(err, windows.WSAEMSGSIZE)
}
}

View File

@@ -11,7 +11,6 @@ import (
"fmt"
"net"
"os"
"os/exec"
"sync"
"syscall"
@@ -67,8 +66,7 @@ func socketMarkWorks() bool {
// If it doesn't, we have to use SO_BINDTODEVICE on our sockets instead.
func useSocketMark() bool {
socketMarkWorksOnce.Do(func() {
ipRuleWorks := exec.Command("ip", "rule").Run() == nil
socketMarkWorksOnce.v = ipRuleWorks && socketMarkWorks()
socketMarkWorksOnce.v = socketMarkWorks()
})
return socketMarkWorksOnce.v
}

View File

@@ -22,12 +22,16 @@ const minFrag = 60 + 20 // max IPv4 header + basic TCP header
type TCPFlag uint8
const (
TCPFin TCPFlag = 0x01
TCPSyn TCPFlag = 0x02
TCPRst TCPFlag = 0x04
TCPPsh TCPFlag = 0x08
TCPAck TCPFlag = 0x10
TCPSynAck TCPFlag = TCPSyn | TCPAck
TCPFin TCPFlag = 0x01
TCPSyn TCPFlag = 0x02
TCPRst TCPFlag = 0x04
TCPPsh TCPFlag = 0x08
TCPAck TCPFlag = 0x10
TCPUrg TCPFlag = 0x20
TCPECNEcho TCPFlag = 0x40
TCPCWR TCPFlag = 0x80
TCPSynAck TCPFlag = TCPSyn | TCPAck
TCPECNBits TCPFlag = TCPECNEcho | TCPCWR
)
// Parsed is a minimal decoding of a packet suitable for use in filters.
@@ -52,7 +56,7 @@ type Parsed struct {
Src netaddr.IPPort
// DstIP4 is the destination address. Family matches IPVersion.
Dst netaddr.IPPort
// TCPFlags is the packet's TCP flag bigs. Valid iff IPProto == TCP.
// TCPFlags is the packet's TCP flag bits. Valid iff IPProto == TCP.
TCPFlags TCPFlag
}
@@ -180,7 +184,7 @@ func (q *Parsed) decode4(b []byte) {
}
q.Src = q.Src.WithPort(binary.BigEndian.Uint16(sub[0:2]))
q.Dst = q.Dst.WithPort(binary.BigEndian.Uint16(sub[2:4]))
q.TCPFlags = TCPFlag(sub[13]) & 0x3F
q.TCPFlags = TCPFlag(sub[13])
headerLength := (sub[12] & 0xF0) >> 2
q.dataofs = q.subofs + int(headerLength)
return
@@ -282,7 +286,7 @@ func (q *Parsed) decode6(b []byte) {
}
q.Src = q.Src.WithPort(binary.BigEndian.Uint16(sub[0:2]))
q.Dst = q.Dst.WithPort(binary.BigEndian.Uint16(sub[2:4]))
q.TCPFlags = TCPFlag(sub[13]) & 0x3F
q.TCPFlags = TCPFlag(sub[13])
headerLength := (sub[12] & 0xF0) >> 2
q.dataofs = q.subofs + int(headerLength)
return
@@ -374,8 +378,14 @@ func (q *Parsed) Payload() []byte {
return q.b[q.dataofs:q.length]
}
// IsTCPSyn reports whether q is a TCP SYN packet
// (i.e. the first packet in a new connection).
// Transport returns the transport header and payload (IP subprotocol, such as TCP or UDP).
// This is a read-only view; that is, p retains the ownership of the buffer.
func (p *Parsed) Transport() []byte {
return p.b[p.subofs:]
}
// IsTCPSyn reports whether q is a TCP SYN packet,
// without ACK set. (i.e. the first packet in a new connection)
func (q *Parsed) IsTCPSyn() bool {
return (q.TCPFlags & TCPSynAck) == TCPSyn
}
@@ -424,6 +434,40 @@ func (q *Parsed) IsEchoResponse() bool {
}
}
// RemoveECNBits modifies p and its underlying memory buffer to remove
// ECN bits, if any. It reports whether it did so.
//
// It currently only does the TCP flags.
func (p *Parsed) RemoveECNBits() bool {
if p.IPVersion == 0 {
return false
}
if p.IPProto != ipproto.TCP {
// TODO(bradfitz): handle non-TCP too? for now only trying to
// fix the Issue 2642 problem.
return false
}
if p.TCPFlags&TCPECNBits == 0 {
// Nothing to do.
return false
}
// Clear flags.
// First in the parsed output.
p.TCPFlags = p.TCPFlags & ^TCPECNBits
// Then in the underlying memory.
tcp := p.Transport()
old := binary.BigEndian.Uint16(tcp[12:14])
tcp[13] = byte(p.TCPFlags)
new := binary.BigEndian.Uint16(tcp[12:14])
oldSum := binary.BigEndian.Uint16(tcp[16:18])
newSum := ^checksumUpdate2ByteAlignedUint16(^oldSum, old, new)
binary.BigEndian.PutUint16(tcp[16:18], newSum)
return true
}
func Hexdump(b []byte) string {
out := new(strings.Builder)
for i := 0; i < len(b); i += 16 {
@@ -455,3 +499,26 @@ func Hexdump(b []byte) string {
}
return out.String()
}
// From gVisor's unexported API:
// checksumUpdate2ByteAlignedUint16 updates a uint16 value in a calculated
// checksum.
//
// The value MUST begin at a 2-byte boundary in the original buffer.
func checksumUpdate2ByteAlignedUint16(xsum, old, new uint16) uint16 {
// As per RFC 1071 page 4,
//(4) Incremental Update
//
// ...
//
// To update the checksum, simply add the differences of the
// sixteen bit integers that have been changed. To see why this
// works, observe that every 16-bit integer has an additive inverse
// and that addition is associative. From this it follows that
// given the original value m, the new value m', and the old
// checksum C, the new checksum C' is:
//
// C' = C + (-m) + m' = C + (m' - m)
return checksumCombine(xsum, checksumCombine(new, ^old))
}

View File

@@ -6,7 +6,9 @@ package packet
import (
"bytes"
"encoding/hex"
"reflect"
"regexp"
"testing"
"inet.af/netaddr"
@@ -561,3 +563,57 @@ func BenchmarkString(b *testing.B) {
})
}
}
func TestRemoveECNBits(t *testing.T) {
// withECNHex is a TCP SYN packet with ECN bits set in the TCP
// header as captured by Wireshark on macOS against the
// Tailscale interface. In this packet (because it's a SYN
// control packet), the ECN bits are not set in the IP header.
const withECNHex = `45 00 00 40 00 00 40 00
40 06 0c 66 64 7b 65 28 64 7f 00 30 f1 ab 00 16
5a 7a 63 e8 00 00 00 00 b0 c2 ff ff 97 76 00 00
02 04 04 d8 01 03 03 06 01 01 08 0a 03 e1 bd 49
00 00 00 00 04 02 00 00`
// Generated by hand-editing a pcap file in hexl-mode to set
// the TCP flags to just SYN (0x02), then loading that pcap
// file in wireshark to get the expected checksum value, then
// putting that checksum value (0x9836) in the file.
const wantStrippedHex = `45 00 00 40 00 00 40 00
40 06 0c 66 64 7b 65 28 64 7f 00 30 f1 ab 00 16
5a 7a 63 e8 00 00 00 00 b0 02 ff ff 98 36 00 00
02 04 04 d8 01 03 03 06 01 01 08 0a 03 e1 bd 49
00 00 00 00 04 02 00 00`
var p Parsed
pktBuf := bytesOfHex(withECNHex)
p.Decode(pktBuf)
if want := TCPCWR | TCPECNEcho | TCPSyn; p.TCPFlags != want {
t.Fatalf("pre flags = %v; want %v", p.TCPFlags, want)
}
if !p.RemoveECNBits() {
t.Fatal("didn't remove bits")
}
if want := TCPSyn; p.TCPFlags != want {
t.Fatalf("post flags = %v; want %v", p.TCPFlags, want)
}
wantPkt := bytesOfHex(wantStrippedHex)
if !bytes.Equal(pktBuf, wantPkt) {
t.Fatalf("wrong result.\n got: % 2x\nwant: % 2x\n", pktBuf, wantPkt)
}
if p.RemoveECNBits() {
t.Fatal("unexpected true return value on second call")
}
}
var nonHex = regexp.MustCompile(`[^0-9a-fA-F]+`)
func bytesOfHex(s string) []byte {
b, err := hex.DecodeString(nonHex.ReplaceAllString(s, ""))
if err != nil {
panic(err)
}
return b
}

View File

@@ -20,6 +20,7 @@ import (
"go4.org/mem"
"inet.af/netaddr"
"tailscale.com/net/interfaces"
"tailscale.com/net/neterror"
"tailscale.com/net/netns"
"tailscale.com/types/logger"
"tailscale.com/util/clientmetric"
@@ -478,18 +479,27 @@ func (c *Client) createOrGetMapping(ctx context.Context) (external netaddr.IPPor
// Only do PCP mapping in the case when PMP did not appear to be available recently.
pkt := buildPCPRequestMappingPacket(myIP, localPort, prevPort, pcpMapLifetimeSec, wildcardIP)
if _, err := uc.WriteTo(pkt, pxpAddru); err != nil {
if neterror.TreatAsLostUDP(err) {
err = NoMappingError{ErrNoPortMappingServices}
}
return netaddr.IPPort{}, err
}
} else {
// Ask for our external address if needed.
if m.external.IP().IsZero() {
if _, err := uc.WriteTo(pmpReqExternalAddrPacket, pxpAddru); err != nil {
if neterror.TreatAsLostUDP(err) {
err = NoMappingError{ErrNoPortMappingServices}
}
return netaddr.IPPort{}, err
}
}
pkt := buildPMPRequestMappingPacket(localPort, prevPort, pmpMapLifetimeSec)
if _, err := uc.WriteTo(pkt, pxpAddru); err != nil {
if neterror.TreatAsLostUDP(err) {
err = NoMappingError{ErrNoPortMappingServices}
}
return netaddr.IPPort{}, err
}
}

View File

@@ -36,14 +36,26 @@ var (
tsUlaRange oncePrefix
ula4To6Range oncePrefix
ulaEph6Range oncePrefix
serviceIPv6 oncePrefix
)
// TailscaleServiceIP returns the listen address of services
// TailscaleServiceIP returns the IPv4 listen address of services
// provided by Tailscale itself such as the MagicDNS proxy.
//
// For IPv6, use TailscaleServiceIPv6.
func TailscaleServiceIP() netaddr.IP {
return netaddr.IPv4(100, 100, 100, 100) // "100.100.100.100" for those grepping
}
// TailscaleServiceIPv6 returns the IPv6 listen address of the services
// provided by Tailscale itself such as the MagicDNS proxy.
//
// For IPv4, use TailscaleServiceIP.
func TailscaleServiceIPv6() netaddr.IP {
serviceIPv6.Do(func() { mustPrefix(&serviceIPv6.v, "fd7a:115c:a1e0::53/128") })
return serviceIPv6.v.IP()
}
// IsTailscaleIP reports whether ip is an IP address in a range that
// Tailscale assigns from.
func IsTailscaleIP(ip netaddr.IP) bool {
@@ -176,6 +188,16 @@ func PrefixesContainsFunc(ipp []netaddr.IPPrefix, f func(netaddr.IPPrefix) bool)
return false
}
// PrefixesContainsIP reports whether any prefix in ipp contains ip.
func PrefixesContainsIP(ipp []netaddr.IPPrefix, ip netaddr.IP) bool {
for _, r := range ipp {
if r.Contains(ip) {
return true
}
}
return false
}
// IPsContainsFunc reports whether f is true for any IP in ips.
func IPsContainsFunc(ips []netaddr.IP, f func(netaddr.IP) bool) bool {
for _, v := range ips {

View File

@@ -31,6 +31,14 @@ func TestInCrostiniRange(t *testing.T) {
}
}
func TestTailscaleServiceIPv6(t *testing.T) {
got := TailscaleServiceIPv6().String()
want := "fd7a:115c:a1e0::53"
if got != want {
t.Errorf("got %q; want %q", got, want)
}
}
func TestChromeOSVMRange(t *testing.T) {
if got, want := ChromeOSVMRange().String(), "100.115.92.0/23"; got != want {
t.Errorf("got %q; want %q", got, want)

View File

@@ -22,6 +22,7 @@ import (
"inet.af/netaddr"
"tailscale.com/disco"
"tailscale.com/net/packet"
"tailscale.com/net/tsaddr"
"tailscale.com/tstime/mono"
"tailscale.com/types/ipproto"
"tailscale.com/types/key"
@@ -407,17 +408,30 @@ func (t *Wrapper) sendOutbound(r tunReadResult) {
t.outbound <- r
}
var magicDNSIPPort = netaddr.MustParseIPPort("100.100.100.100:0")
var (
magicDNSIPPort = netaddr.IPPortFrom(tsaddr.TailscaleServiceIP(), 0) // 100.100.100.100:0
magicDNSIPPortv6 = netaddr.IPPortFrom(tsaddr.TailscaleServiceIPv6(), 0)
)
func (t *Wrapper) filterOut(p *packet.Parsed) filter.Response {
// Fake ICMP echo responses to MagicDNS (100.100.100.100).
if p.IsEchoRequest() && p.Dst == magicDNSIPPort {
header := p.ICMP4Header()
header.ToResponse()
outp := packet.Generate(&header, p.Payload())
t.InjectInboundCopy(outp)
return filter.DropSilently // don't pass on to OS; already handled
if p.IsEchoRequest() {
switch p.Dst {
case magicDNSIPPort:
header := p.ICMP4Header()
header.ToResponse()
outp := packet.Generate(&header, p.Payload())
t.InjectInboundCopy(outp)
return filter.DropSilently // don't pass on to OS; already handled
case magicDNSIPPortv6:
header := p.ICMP6Header()
header.ToResponse()
outp := packet.Generate(&header, p.Payload())
t.InjectInboundCopy(outp)
return filter.DropSilently // don't pass on to OS; already handled
}
}
// TODO(bradfitz): support pinging TailscaleServiceIPv6 too.
// Issue 1526 workaround: if we sent disco packets over
// Tailscale from ourselves, then drop them, as that shouldn't

16
pull-toolchain.sh Executable file
View File

@@ -0,0 +1,16 @@
#!/bin/sh
# Retrieve the latest Go toolchain.
#
set -eu
cd "$(dirname "$0")"
read -r go_branch <go.toolchain.branch
upstream=$(git ls-remote https://github.com/tailscale/go "$go_branch" | awk '{print $1}')
current=$(cat go.toolchain.rev)
if [ "$upstream" != "$current" ]; then
echo "$upstream" >go.toolchain.rev
fi
if [ -n "$(git diff-index --name-only HEAD -- go.toolchain.rev)" ]; then
echo "pull-toolchain.sh: changes imported. Use git commit to make them permanent." >&2
fi

View File

@@ -24,6 +24,7 @@ main() {
VERSION=""
PACKAGETYPE=""
APT_KEY_TYPE="" # Only for apt-based distros
APT_SYSTEMCTL_START=false # Only needs to be true for Kali
if [ -f /etc/os-release ]; then
# /etc/os-release populates a number of shell variables. We care about the following:

View File

@@ -48,7 +48,8 @@ import (
// 23: 2021-08-25: DNSConfig.Routes values may be empty (for ExtraRecords support in 1.14.1+)
// 24: 2021-09-18: MapResponse.Health from control to node; node shows in "tailscale status"
// 25: 2021-11-01: MapResponse.Debug.Exit
const CurrentMapRequestVersion = 25
// 26: 2022-01-12: (nothing, just bumping for 1.20.0)
const CurrentMapRequestVersion = 26
type StableID string
@@ -577,6 +578,68 @@ func (h *Hostinfo) Equal(h2 *Hostinfo) bool {
return reflect.DeepEqual(h, h2)
}
// HowUnequal returns a list of paths through Hostinfo where h and h2 differ.
// If they differ in nil-ness, the path is "nil", otherwise the path is like
// "ShieldsUp" or "NetInfo.nil" or "NetInfo.PCP".
func (h *Hostinfo) HowUnequal(h2 *Hostinfo) (path []string) {
return appendStructPtrDiff(nil, "", reflect.ValueOf(h), reflect.ValueOf(h2))
}
func appendStructPtrDiff(base []string, pfx string, p1, p2 reflect.Value) (ret []string) {
ret = base
if p1.IsNil() && p2.IsNil() {
return base
}
mkPath := func(b string) string {
if pfx == "" {
return b
}
return pfx + "." + b
}
if p1.IsNil() || p2.IsNil() {
return append(base, mkPath("nil"))
}
v1, v2 := p1.Elem(), p2.Elem()
t := v1.Type()
for i, n := 0, t.NumField(); i < n; i++ {
sf := t.Field(i)
switch sf.Type.Kind() {
case reflect.String:
if v1.Field(i).String() != v2.Field(i).String() {
ret = append(ret, mkPath(sf.Name))
}
continue
case reflect.Bool:
if v1.Field(i).Bool() != v2.Field(i).Bool() {
ret = append(ret, mkPath(sf.Name))
}
continue
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
if v1.Field(i).Int() != v2.Field(i).Int() {
ret = append(ret, mkPath(sf.Name))
}
continue
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
if v1.Field(i).Uint() != v2.Field(i).Uint() {
ret = append(ret, mkPath(sf.Name))
}
continue
case reflect.Slice, reflect.Map:
if !reflect.DeepEqual(v1.Field(i).Interface(), v2.Field(i).Interface()) {
ret = append(ret, mkPath(sf.Name))
}
continue
case reflect.Ptr:
if sf.Type.Elem().Kind() == reflect.Struct {
ret = appendStructPtrDiff(ret, sf.Name, v1.Field(i), v2.Field(i))
continue
}
}
panic(fmt.Sprintf("unsupported type at %s: %s", mkPath(sf.Name), sf.Type.String()))
}
return ret
}
// SignatureType specifies a scheme for signing RegisterRequest messages. It
// specifies the crypto algorithms to use, the contents of what is signed, and
// any other relevant details. Historically, requests were unsigned so the zero

View File

@@ -190,6 +190,82 @@ func TestHostinfoEqual(t *testing.T) {
}
}
func TestHostinfoHowEqual(t *testing.T) {
tests := []struct {
a, b *Hostinfo
want []string
}{
{
a: nil,
b: nil,
want: nil,
},
{
a: new(Hostinfo),
b: nil,
want: []string{"nil"},
},
{
a: nil,
b: new(Hostinfo),
want: []string{"nil"},
},
{
a: new(Hostinfo),
b: new(Hostinfo),
want: nil,
},
{
a: &Hostinfo{
IPNVersion: "1",
ShieldsUp: false,
RoutableIPs: []netaddr.IPPrefix{netaddr.MustParseIPPrefix("1.2.3.0/24")},
},
b: &Hostinfo{
IPNVersion: "2",
ShieldsUp: true,
RoutableIPs: []netaddr.IPPrefix{netaddr.MustParseIPPrefix("1.2.3.0/25")},
},
want: []string{"IPNVersion", "ShieldsUp", "RoutableIPs"},
},
{
a: &Hostinfo{
IPNVersion: "1",
},
b: &Hostinfo{
IPNVersion: "2",
NetInfo: new(NetInfo),
},
want: []string{"IPNVersion", "NetInfo.nil"},
},
{
a: &Hostinfo{
IPNVersion: "1",
NetInfo: &NetInfo{
WorkingIPv6: "true",
HavePortMap: true,
LinkType: "foo",
PreferredDERP: 123,
DERPLatency: map[string]float64{
"foo": 1.0,
},
},
},
b: &Hostinfo{
IPNVersion: "2",
NetInfo: &NetInfo{},
},
want: []string{"IPNVersion", "NetInfo.WorkingIPv6", "NetInfo.HavePortMap", "NetInfo.PreferredDERP", "NetInfo.LinkType", "NetInfo.DERPLatency"},
},
}
for i, tt := range tests {
got := tt.a.HowUnequal(tt.b)
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("%d. got %q; want %q", i, got, tt.want)
}
}
}
func TestNodeEqual(t *testing.T) {
nodeHandles := []string{
"ID", "StableID", "Name", "User", "Sharer",

89
tool/go Executable file
View File

@@ -0,0 +1,89 @@
#!/bin/sh
#
# This script acts like the "go" command, but uses Tailscale's
# currently-desired version from https://github.com/tailscale/go,
# downloading it first if necessary.
set -eu
log() {
echo "$@" >&2
}
DEFAULT_TOOLCHAIN_DIR="${HOME}/.cache/tailscale-go"
TOOLCHAIN="${TOOLCHAIN-${DEFAULT_TOOLCHAIN_DIR}}"
TOOLCHAIN_GO="${TOOLCHAIN}/bin/go"
read -r REV < "$(dirname "$0")/../go.toolchain.rev"
# Fast, quiet path, when Tailscale is already current.
if [ -e "${TOOLCHAIN_GO}" ]; then
short_hash=$("${TOOLCHAIN_GO}" version | sed 's/.*-ts//; s/ .*//')
case $REV in
"$short_hash"*)
unset GOROOT
exec "${TOOLCHAIN_GO}" "$@"
esac
fi
# This works for linux and darwin, which is sufficient
# (we do not build tailscale-go for other targets).
GOOS=$(uname -s | tr A-Z a-z)
ARCH="$(uname -m)"
if [ "$ARCH" = "aarch64" ]; then
# Go uses the name "arm64".
ARCH="arm64"
elif [ "$ARCH" = "x86_64" ]; then
# Go uses the name "amd64".
ARCH="amd64"
fi
get_cached() {
if [ ! -d "$TOOLCHAIN" ]; then
mkdir -p "$TOOLCHAIN"
fi
archive="$TOOLCHAIN-$REV.tar.gz"
mark="$TOOLCHAIN.extracted"
extracted=
[ ! -e "$mark" ] || read -r extracted junk <$mark
if [ "$extracted" = "$REV" ] && [ -e "${TOOLCHAIN_GO}" ]; then
# already ok
log "Go toolchain '$REV' already extracted."
return 0
fi
rm -f "$archive.new" "$TOOLCHAIN.extracted"
if [ ! -e "$archive" ]; then
log "Need to download go '$REV'."
if [ "$ARCH" = "amd64" ]; then
# For historic reasons, the tailscale/go amd64 release artifacts don't
# have the arch in their name.
BUILD="$GOOS"
else
BUILD="$GOOS-$ARCH"
fi
curl -f -L -o "$archive.new" "https://github.com/tailscale/go/releases/download/build-${REV}/${BUILD}.tar.gz"
rm -f "$archive"
mv "$archive.new" "$archive"
fi
log "Extracting tailscale/go rev '$REV'" >&2
log " into '$TOOLCHAIN'." >&2
rm -rf "$TOOLCHAIN"
mkdir -p "$TOOLCHAIN"
(cd "$TOOLCHAIN" && tar --strip-components=1 -xf "$archive")
echo "$REV" >$mark
}
if [ "${REV}" = "SKIP" ] ||
[ "${GOOS}" != "darwin" -a "${GOOS}" != "linux" ] ||
[ "${ARCH}" != "amd64" -a "${ARCH}" != "arm64" ]; then
# Use whichever go is available
exec go "$@"
else
get_cached
fi
unset GOROOT
exec "${TOOLCHAIN_GO}" "$@"

View File

@@ -63,6 +63,16 @@ type Server struct {
mu sync.Mutex
listeners map[listenKey]*listener
dialer *tsdial.Dialer
}
// Dial connects to the address on the tailnet.
// It will start the server if it has not been started yet.
func (s *Server) Dial(ctx context.Context, network, address string) (net.Conn, error) {
if err := s.Start(); err != nil {
return nil, err
}
return s.dialer.UserDial(ctx, network, address)
}
func (s *Server) doInit() {
@@ -71,6 +81,13 @@ func (s *Server) doInit() {
}
}
// Start connects the server to the tailnet.
// Optional: any calls to Dial/Listen will also call Start.
func (s *Server) Start() error {
s.initOnce.Do(s.doInit)
return s.initErr
}
func (s *Server) start() error {
if v, _ := strconv.ParseBool(os.Getenv("TAILSCALE_USE_WIP_CODE")); !v {
return errors.New("code disabled without environment variable TAILSCALE_USE_WIP_CODE set true")
@@ -117,11 +134,11 @@ func (s *Server) start() error {
return err
}
dialer := new(tsdial.Dialer) // mutated below (before used)
s.dialer = new(tsdial.Dialer) // mutated below (before used)
eng, err := wgengine.NewUserspaceEngine(logf, wgengine.Config{
ListenPort: 0,
LinkMonitor: linkMon,
Dialer: dialer,
Dialer: s.dialer,
})
if err != nil {
return err
@@ -132,7 +149,7 @@ func (s *Server) start() error {
return fmt.Errorf("%T is not a wgengine.InternalsGetter", eng)
}
ns, err := netstack.Create(logf, tunDev, eng, magicConn, dialer)
ns, err := netstack.Create(logf, tunDev, eng, magicConn, s.dialer)
if err != nil {
return fmt.Errorf("netstack.Create: %w", err)
}
@@ -141,11 +158,11 @@ func (s *Server) start() error {
if err := ns.Start(); err != nil {
return fmt.Errorf("failed to start netstack: %w", err)
}
dialer.UseNetstackForIP = func(ip netaddr.IP) bool {
s.dialer.UseNetstackForIP = func(ip netaddr.IP) bool {
_, ok := eng.PeerForIP(ip)
return ok
}
dialer.NetstackDialTCP = func(ctx context.Context, dst netaddr.IPPort) (net.Conn, error) {
s.dialer.NetstackDialTCP = func(ctx context.Context, dst netaddr.IPPort) (net.Conn, error) {
return ns.DialContextTCP(ctx, dst)
}
@@ -156,7 +173,7 @@ func (s *Server) start() error {
}
logid := "tslib-TODO"
lb, err := ipnlocal.NewLocalBackend(logf, logid, store, dialer, eng)
lb, err := ipnlocal.NewLocalBackend(logf, logid, store, s.dialer, eng)
if err != nil {
return fmt.Errorf("NewLocalBackend: %v", err)
}
@@ -217,15 +234,16 @@ func (s *Server) forwardTCP(c net.Conn, port uint16) {
}
}
// Listen announces only on the Tailscale network.
// It will start the server if it has not been started yet.
func (s *Server) Listen(network, addr string) (net.Listener, error) {
host, port, err := net.SplitHostPort(addr)
if err != nil {
return nil, fmt.Errorf("tsnet: %w", err)
}
s.initOnce.Do(s.doInit)
if s.initErr != nil {
return nil, s.initErr
if err := s.Start(); err != nil {
return nil, err
}
key := listenKey{network, host, port}

View File

@@ -68,12 +68,12 @@ func TestOneNodeUpNoAuth(t *testing.T) {
env := newTestEnv(t)
n1 := newTestNode(t, env)
d1 := n1.StartDaemon(t)
n1.AwaitResponding(t)
d1 := n1.StartDaemon()
n1.AwaitResponding()
n1.MustUp()
t.Logf("Got IP: %v", n1.AwaitIP(t))
n1.AwaitRunning(t)
t.Logf("Got IP: %v", n1.AwaitIP())
n1.AwaitRunning()
d1.MustCleanShutdown(t)
@@ -85,10 +85,10 @@ func TestOneNodeExpiredKey(t *testing.T) {
env := newTestEnv(t)
n1 := newTestNode(t, env)
d1 := n1.StartDaemon(t)
n1.AwaitResponding(t)
d1 := n1.StartDaemon()
n1.AwaitResponding()
n1.MustUp()
n1.AwaitRunning(t)
n1.AwaitRunning()
nodes := env.Control.AllNodes()
if len(nodes) != 1 {
@@ -103,7 +103,7 @@ func TestOneNodeExpiredKey(t *testing.T) {
cancel()
env.Control.SetExpireAllNodes(true)
n1.AwaitNeedsLogin(t)
n1.AwaitNeedsLogin()
ctx, cancel = context.WithTimeout(context.Background(), 5*time.Second)
if err := env.Control.AwaitNodeInMapRequest(ctx, nodeKey); err != nil {
t.Fatal(err)
@@ -111,7 +111,7 @@ func TestOneNodeExpiredKey(t *testing.T) {
cancel()
env.Control.SetExpireAllNodes(false)
n1.AwaitRunning(t)
n1.AwaitRunning()
d1.MustCleanShutdown(t)
}
@@ -152,14 +152,14 @@ func TestStateSavedOnStart(t *testing.T) {
env := newTestEnv(t)
n1 := newTestNode(t, env)
d1 := n1.StartDaemon(t)
n1.AwaitResponding(t)
d1 := n1.StartDaemon()
n1.AwaitResponding()
n1.MustUp()
t.Logf("Got IP: %v", n1.AwaitIP(t))
n1.AwaitRunning(t)
t.Logf("Got IP: %v", n1.AwaitIP())
n1.AwaitRunning()
p1 := n1.diskPrefs(t)
p1 := n1.diskPrefs()
t.Logf("Prefs1: %v", p1.Pretty())
// Bring it down, to prevent an EditPrefs call in the
@@ -172,7 +172,7 @@ func TestStateSavedOnStart(t *testing.T) {
t.Fatalf("up: %v", err)
}
p2 := n1.diskPrefs(t)
p2 := n1.diskPrefs()
if pretty := p1.Pretty(); pretty == p2.Pretty() {
t.Errorf("Prefs didn't change on disk after 'up', still: %s", pretty)
}
@@ -190,11 +190,11 @@ func TestOneNodeUpAuth(t *testing.T) {
}))
n1 := newTestNode(t, env)
d1 := n1.StartDaemon(t)
d1 := n1.StartDaemon()
n1.AwaitListening(t)
n1.AwaitListening()
st := n1.MustStatus(t)
st := n1.MustStatus()
t.Logf("Status: %s", st.BackendState)
t.Logf("Running up --login-server=%s ...", env.ControlServer.URL)
@@ -215,9 +215,9 @@ func TestOneNodeUpAuth(t *testing.T) {
if err := cmd.Run(); err != nil {
t.Fatalf("up: %v", err)
}
t.Logf("Got IP: %v", n1.AwaitIP(t))
t.Logf("Got IP: %v", n1.AwaitIP())
n1.AwaitRunning(t)
n1.AwaitRunning()
if n := atomic.LoadInt32(&authCountAtomic); n != 1 {
t.Errorf("Auth URLs completed = %d; want 1", n)
@@ -233,26 +233,26 @@ func TestTwoNodes(t *testing.T) {
// Create two nodes:
n1 := newTestNode(t, env)
n1SocksAddrCh := n1.socks5AddrChan()
d1 := n1.StartDaemon(t)
d1 := n1.StartDaemon()
n2 := newTestNode(t, env)
n2SocksAddrCh := n2.socks5AddrChan()
d2 := n2.StartDaemon(t)
d2 := n2.StartDaemon()
n1Socks := n1.AwaitSocksAddr(t, n1SocksAddrCh)
n2Socks := n1.AwaitSocksAddr(t, n2SocksAddrCh)
n1Socks := n1.AwaitSocksAddr(n1SocksAddrCh)
n2Socks := n1.AwaitSocksAddr(n2SocksAddrCh)
t.Logf("node1 SOCKS5 addr: %v", n1Socks)
t.Logf("node2 SOCKS5 addr: %v", n2Socks)
n1.AwaitListening(t)
n2.AwaitListening(t)
n1.AwaitListening()
n2.AwaitListening()
n1.MustUp()
n2.MustUp()
n1.AwaitRunning(t)
n2.AwaitRunning(t)
n1.AwaitRunning()
n2.AwaitRunning()
if err := tstest.WaitFor(2*time.Second, func() error {
st := n1.MustStatus(t)
st := n1.MustStatus()
if len(st.Peer) == 0 {
return errors.New("no peers")
}
@@ -276,11 +276,11 @@ func TestNodeAddressIPFields(t *testing.T) {
t.Parallel()
env := newTestEnv(t)
n1 := newTestNode(t, env)
d1 := n1.StartDaemon(t)
d1 := n1.StartDaemon()
n1.AwaitListening(t)
n1.AwaitListening()
n1.MustUp()
n1.AwaitRunning(t)
n1.AwaitRunning()
testNodes := env.Control.AllNodes()
@@ -302,11 +302,11 @@ func TestAddPingRequest(t *testing.T) {
t.Parallel()
env := newTestEnv(t)
n1 := newTestNode(t, env)
n1.StartDaemon(t)
n1.StartDaemon()
n1.AwaitListening(t)
n1.AwaitListening()
n1.MustUp()
n1.AwaitRunning(t)
n1.AwaitRunning()
gotPing := make(chan bool, 1)
waitPing := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
@@ -357,28 +357,28 @@ func TestNoControlConnWhenDown(t *testing.T) {
env := newTestEnv(t)
n1 := newTestNode(t, env)
d1 := n1.StartDaemon(t)
n1.AwaitResponding(t)
d1 := n1.StartDaemon()
n1.AwaitResponding()
// Come up the first time.
n1.MustUp()
ip1 := n1.AwaitIP(t)
n1.AwaitRunning(t)
ip1 := n1.AwaitIP()
n1.AwaitRunning()
// Then bring it down and stop the daemon.
n1.MustDown()
d1.MustCleanShutdown(t)
env.LogCatcher.Reset()
d2 := n1.StartDaemon(t)
n1.AwaitResponding(t)
d2 := n1.StartDaemon()
n1.AwaitResponding()
st := n1.MustStatus(t)
st := n1.MustStatus()
if got, want := st.BackendState, "Stopped"; got != want {
t.Fatalf("after restart, state = %q; want %q", got, want)
}
ip2 := n1.AwaitIP(t)
ip2 := n1.AwaitIP()
if ip1 != ip2 {
t.Errorf("IPs different: %q vs %q", ip1, ip2)
}
@@ -399,16 +399,66 @@ func TestOneNodeUpWindowsStyle(t *testing.T) {
n1 := newTestNode(t, env)
n1.upFlagGOOS = "windows"
d1 := n1.StartDaemonAsIPNGOOS(t, "windows")
n1.AwaitResponding(t)
d1 := n1.StartDaemonAsIPNGOOS("windows")
n1.AwaitResponding()
n1.MustUp("--unattended")
t.Logf("Got IP: %v", n1.AwaitIP(t))
n1.AwaitRunning(t)
t.Logf("Got IP: %v", n1.AwaitIP())
n1.AwaitRunning()
d1.MustCleanShutdown(t)
}
func TestLogoutRemovesAllPeers(t *testing.T) {
t.Parallel()
env := newTestEnv(t)
// Spin up some nodes.
nodes := make([]*testNode, 2)
for i := range nodes {
nodes[i] = newTestNode(t, env)
nodes[i].StartDaemon()
nodes[i].AwaitResponding()
nodes[i].MustUp()
nodes[i].AwaitIP()
nodes[i].AwaitRunning()
}
// Make every node ping every other node.
// This makes sure magicsock is fully populated.
for i := range nodes {
for j := range nodes {
if i <= j {
continue
}
if err := tstest.WaitFor(20*time.Second, func() error {
return nodes[i].Ping(nodes[j])
}); err != nil {
t.Fatalf("ping %v -> %v: %v", nodes[i].AwaitIP(), nodes[j].AwaitIP(), err)
}
}
}
// wantNode0PeerCount waits until node[0] status includes exactly want peers.
wantNode0PeerCount := func(want int) {
if err := tstest.WaitFor(20*time.Second, func() error {
s := nodes[0].MustStatus()
if peers := s.Peers(); len(peers) != want {
return fmt.Errorf("want %d peer(s) in status, got %v", want, peers)
}
return nil
}); err != nil {
t.Fatal(err)
}
}
wantNode0PeerCount(len(nodes) - 1) // all other nodes are peers
nodes[0].MustLogOut()
wantNode0PeerCount(0) // node[0] is logged out, so it should not have any peers
nodes[0].MustUp()
nodes[0].AwaitIP()
wantNode0PeerCount(len(nodes) - 1) // all other nodes are peers again
}
// testEnv contains the test environment (set of servers) used by one
// or more nodes.
type testEnv struct {
@@ -508,7 +558,8 @@ func newTestNode(t *testing.T, env *testEnv) *testNode {
}
}
func (n *testNode) diskPrefs(t testing.TB) *ipn.Prefs {
func (n *testNode) diskPrefs() *ipn.Prefs {
t := n.env.t
t.Helper()
if _, err := ioutil.ReadFile(n.stateFile); err != nil {
t.Fatalf("reading prefs: %v", err)
@@ -530,11 +581,12 @@ func (n *testNode) diskPrefs(t testing.TB) *ipn.Prefs {
// AwaitResponding waits for n's tailscaled to be up enough to be
// responding, but doesn't wait for any particular state.
func (n *testNode) AwaitResponding(t testing.TB) {
func (n *testNode) AwaitResponding() {
t := n.env.t
t.Helper()
n.AwaitListening(t)
n.AwaitListening()
st := n.MustStatus(t)
st := n.MustStatus()
t.Logf("Status: %s", st.BackendState)
if err := tstest.WaitFor(20*time.Second, func() error {
@@ -575,7 +627,8 @@ func (n *testNode) socks5AddrChan() <-chan string {
return ch
}
func (n *testNode) AwaitSocksAddr(t testing.TB, ch <-chan string) string {
func (n *testNode) AwaitSocksAddr(ch <-chan string) string {
t := n.env.t
t.Helper()
timer := time.NewTimer(10 * time.Second)
defer timer.Stop()
@@ -644,17 +697,21 @@ func (d *Daemon) MustCleanShutdown(t testing.TB) {
// StartDaemon starts the node's tailscaled, failing if it fails to start.
// StartDaemon ensures that the process will exit when the test completes.
func (n *testNode) StartDaemon(t testing.TB) *Daemon {
return n.StartDaemonAsIPNGOOS(t, runtime.GOOS)
func (n *testNode) StartDaemon() *Daemon {
return n.StartDaemonAsIPNGOOS(runtime.GOOS)
}
func (n *testNode) StartDaemonAsIPNGOOS(t testing.TB, ipnGOOS string) *Daemon {
func (n *testNode) StartDaemonAsIPNGOOS(ipnGOOS string) *Daemon {
t := n.env.t
cmd := exec.Command(n.env.daemon,
"--tun=userspace-networking",
"--state="+n.stateFile,
"--socket="+n.sockFile,
"--socks5-server=localhost:0",
)
if *verboseTailscaled {
cmd.Args = append(cmd.Args, "-verbose=2")
}
cmd.Env = append(os.Environ(),
"TS_LOG_TARGET="+n.env.LogCatcherServer.URL,
"HTTP_PROXY="+n.env.TrafficTrapServer.URL,
@@ -700,9 +757,25 @@ func (n *testNode) MustDown() {
}
}
func (n *testNode) MustLogOut() {
t := n.env.t
t.Logf("Running logout ...")
if err := n.Tailscale("logout").Run(); err != nil {
t.Fatalf("logout: %v", err)
}
}
func (n *testNode) Ping(otherNode *testNode) error {
t := n.env.t
ip := otherNode.AwaitIP().String()
t.Logf("Running ping %v (from %v)...", ip, n.AwaitIP())
return n.Tailscale("ping", ip).Run()
}
// AwaitListening waits for the tailscaled to be serving local clients
// over its localhost IPC mechanism. (Unix socket, etc)
func (n *testNode) AwaitListening(t testing.TB) {
func (n *testNode) AwaitListening() {
t := n.env.t
s := safesocket.DefaultConnectionStrategy(n.sockFile)
s.UseFallback(false) // connect only to the tailscaled that we started
if err := tstest.WaitFor(20*time.Second, func() (err error) {
@@ -717,7 +790,8 @@ func (n *testNode) AwaitListening(t testing.TB) {
}
}
func (n *testNode) AwaitIPs(t testing.TB) []netaddr.IP {
func (n *testNode) AwaitIPs() []netaddr.IP {
t := n.env.t
t.Helper()
var addrs []netaddr.IP
if err := tstest.WaitFor(20*time.Second, func() error {
@@ -750,14 +824,16 @@ func (n *testNode) AwaitIPs(t testing.TB) []netaddr.IP {
}
// AwaitIP returns the IP address of n.
func (n *testNode) AwaitIP(t testing.TB) netaddr.IP {
func (n *testNode) AwaitIP() netaddr.IP {
t := n.env.t
t.Helper()
ips := n.AwaitIPs(t)
ips := n.AwaitIPs()
return ips[0]
}
// AwaitRunning waits for n to reach the IPN state "Running".
func (n *testNode) AwaitRunning(t testing.TB) {
func (n *testNode) AwaitRunning() {
t := n.env.t
t.Helper()
if err := tstest.WaitFor(20*time.Second, func() error {
st, err := n.Status()
@@ -774,7 +850,8 @@ func (n *testNode) AwaitRunning(t testing.TB) {
}
// AwaitNeedsLogin waits for n to reach the IPN state "NeedsLogin".
func (n *testNode) AwaitNeedsLogin(t testing.TB) {
func (n *testNode) AwaitNeedsLogin() {
t := n.env.t
t.Helper()
if err := tstest.WaitFor(20*time.Second, func() error {
st, err := n.Status()
@@ -822,7 +899,8 @@ func (n *testNode) Status() (*ipnstate.Status, error) {
return st, nil
}
func (n *testNode) MustStatus(tb testing.TB) *ipnstate.Status {
func (n *testNode) MustStatus() *ipnstate.Status {
tb := n.env.t
tb.Helper()
st, err := n.Status()
if err != nil {

View File

@@ -9,3 +9,9 @@ import _ "embed"
//go:embed VERSION.txt
var Version string
// GoToolchainRev is the git hash from github.com/tailscale/go that this release
// should be built using. It may end in a newline.
//
//go:embed go.toolchain.rev
var GoToolchainRev string

View File

@@ -62,3 +62,13 @@ func IsMacSysExt() bool {
isMacSysExt.Store(v)
return v
}
// IsWindowsGUI reports whether the current process is the Windows GUI.
func IsWindowsGUI() bool {
if runtime.GOOS != "windows" {
return false
}
exe, _ := os.Executable()
exe = filepath.Base(exe)
return strings.EqualFold(exe, "tailscale-ipn.exe") || strings.EqualFold(exe, "tailscale-ipn")
}

View File

@@ -14,7 +14,7 @@ import (
// Long is a full version number for this build, of the form
// "x.y.z-commithash", or "date.yyyymmdd" if no actual version was
// provided.
var Long = "date.20211101"
var Long = "date.20220107"
// Short is a short version number for this build, of the form
// "x.y.z", or "date.yyyymmdd" if no actual version was provided.

View File

@@ -0,0 +1,202 @@
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package magicsock
import (
"fmt"
"html"
"io"
"net/http"
"sort"
"strings"
"time"
"inet.af/netaddr"
"tailscale.com/tailcfg"
"tailscale.com/tstime/mono"
"tailscale.com/types/key"
)
// ServeHTTPDebug serves an HTML representation of the innards of c for debugging.
//
// It's accessible either from tailscaled's debug port (at
// /debug/magicsock) or via peerapi to a peer that's owned by the same
// user (so they can e.g. inspect their phones).
func (c *Conn) ServeHTTPDebug(w http.ResponseWriter, r *http.Request) {
c.mu.Lock()
defer c.mu.Unlock()
now := time.Now()
w.Header().Set("Content-Type", "text/html; charset=utf-8")
fmt.Fprintf(w, "<h1>magicsock</h1>")
fmt.Fprintf(w, "<h2 id=derp><a href=#derp>#</a> DERP</h2><ul>")
if c.derpMap != nil {
type D struct {
regionID int
lastWrite time.Time
createTime time.Time
}
ent := make([]D, 0, len(c.activeDerp))
for rid, ad := range c.activeDerp {
ent = append(ent, D{
regionID: rid,
lastWrite: *ad.lastWrite,
createTime: ad.createTime,
})
}
sort.Slice(ent, func(i, j int) bool {
return ent[i].regionID < ent[j].regionID
})
for _, e := range ent {
r, ok := c.derpMap.Regions[e.regionID]
if !ok {
continue
}
home := ""
if e.regionID == c.myDerp {
home = "🏠"
}
fmt.Fprintf(w, "<li>%s %d - %v: created %v ago, write %v ago</li>\n",
home, e.regionID, html.EscapeString(r.RegionCode),
now.Sub(e.createTime).Round(time.Second),
now.Sub(e.lastWrite).Round(time.Second),
)
}
}
fmt.Fprintf(w, "</ul>\n")
fmt.Fprintf(w, "<h2 id=ipport><a href=#ipport>#</a> ip:port to endpoint</h2><ul>")
{
type kv struct {
ipp netaddr.IPPort
pi *peerInfo
}
ent := make([]kv, 0, len(c.peerMap.byIPPort))
for k, v := range c.peerMap.byIPPort {
ent = append(ent, kv{k, v})
}
sort.Slice(ent, func(i, j int) bool { return ipPortLess(ent[i].ipp, ent[j].ipp) })
for _, e := range ent {
ep := e.pi.ep
shortStr := ep.publicKey.ShortString()
fmt.Fprintf(w, "<li>%v: <a href='#%v'>%v</a></li>\n", e.ipp, strings.Trim(shortStr, "[]"), shortStr)
}
}
fmt.Fprintf(w, "</ul>\n")
fmt.Fprintf(w, "<h2 id=bykey><a href=#bykey>#</a> endpoints by key</h2>")
{
type kv struct {
pub key.NodePublic
pi *peerInfo
}
ent := make([]kv, 0, len(c.peerMap.byNodeKey))
for k, v := range c.peerMap.byNodeKey {
ent = append(ent, kv{k, v})
}
sort.Slice(ent, func(i, j int) bool { return ent[i].pub.Less(ent[j].pub) })
peers := map[key.NodePublic]*tailcfg.Node{}
if c.netMap != nil {
for _, p := range c.netMap.Peers {
peers[p.Key] = p
}
}
for _, e := range ent {
ep := e.pi.ep
shortStr := e.pub.ShortString()
name := peerDebugName(peers[e.pub])
fmt.Fprintf(w, "<h3 id=%v><a href='#%v'>%v</a> - %s</h3>\n",
strings.Trim(shortStr, "[]"),
strings.Trim(shortStr, "[]"),
shortStr,
html.EscapeString(name))
printEndpointHTML(w, ep)
}
}
}
func printEndpointHTML(w io.Writer, ep *endpoint) {
lastRecv := ep.lastRecv.LoadAtomic()
ep.mu.Lock()
defer ep.mu.Unlock()
if ep.lastSend == 0 && lastRecv == 0 {
return // no activity ever
}
now := time.Now()
mnow := mono.Now()
fmtMono := func(m mono.Time) string {
if m == 0 {
return "-"
}
return mnow.Sub(m).Round(time.Millisecond).String()
}
fmt.Fprintf(w, "<p>Best: <b>%+v</b>, %v ago (for %v)</p>\n", ep.bestAddr, fmtMono(ep.bestAddrAt), ep.trustBestAddrUntil.Sub(mnow).Round(time.Millisecond))
fmt.Fprintf(w, "<p>heartbeating: %v</p>\n", ep.heartBeatTimer != nil)
fmt.Fprintf(w, "<p>lastSend: %v ago</p>\n", fmtMono(ep.lastSend))
fmt.Fprintf(w, "<p>lastFullPing: %v ago</p>\n", fmtMono(ep.lastFullPing))
eps := make([]netaddr.IPPort, 0, len(ep.endpointState))
for ipp := range ep.endpointState {
eps = append(eps, ipp)
}
sort.Slice(eps, func(i, j int) bool { return ipPortLess(eps[i], eps[j]) })
io.WriteString(w, "<p>Endpoints:</p><ul>")
for _, ipp := range eps {
s := ep.endpointState[ipp]
if ipp == ep.bestAddr.IPPort {
fmt.Fprintf(w, "<li><b>%s</b>: (best)<ul>", ipp)
} else {
fmt.Fprintf(w, "<li>%s: ...<ul>", ipp)
}
fmt.Fprintf(w, "<li>lastPing: %v ago</li>\n", fmtMono(s.lastPing))
if s.lastGotPing.IsZero() {
fmt.Fprintf(w, "<li>disco-learned-at: -</li>\n")
} else {
fmt.Fprintf(w, "<li>disco-learned-at: %v ago</li>\n", now.Sub(s.lastGotPing).Round(time.Second))
}
fmt.Fprintf(w, "<li>callMeMaybeTime: %v</li>\n", s.callMeMaybeTime)
for i := range s.recentPongs {
if i == 5 {
break
}
pos := (int(s.recentPong) - i) % len(s.recentPongs)
pr := s.recentPongs[pos]
fmt.Fprintf(w, "<li>pong %v ago: in %v, from %v src %v</li>\n",
fmtMono(pr.pongAt), pr.latency.Round(time.Millisecond/10),
pr.from, pr.pongSrc)
}
fmt.Fprintf(w, "</ul></li>\n")
}
io.WriteString(w, "</ul>")
}
func peerDebugName(p *tailcfg.Node) string {
if p == nil {
return ""
}
n := p.Name
if i := strings.Index(n, "."); i != -1 {
return n[:i]
}
return p.Hostinfo.Hostname
}
func ipPortLess(a, b netaddr.IPPort) bool {
if v := a.IP().Compare(b.IP()); v != 0 {
return v < 0
}
return a.Port() < b.Port()
}

View File

@@ -39,9 +39,11 @@ import (
"tailscale.com/net/dnscache"
"tailscale.com/net/interfaces"
"tailscale.com/net/netcheck"
"tailscale.com/net/neterror"
"tailscale.com/net/netns"
"tailscale.com/net/portmapper"
"tailscale.com/net/stun"
"tailscale.com/net/tsaddr"
"tailscale.com/syncs"
"tailscale.com/tailcfg"
"tailscale.com/tstime"
@@ -233,6 +235,7 @@ type Conn struct {
idleFunc func() time.Duration // nil means unknown
testOnlyPacketListener nettype.PacketListener
noteRecvActivity func(key.NodePublic) // or nil, see Options.NoteRecvActivity
linkMon *monitor.Mon // or nil
// ================================================================
// No locking required to access these fields, either because
@@ -262,6 +265,7 @@ type Conn struct {
stunReceiveFunc atomic.Value // of func(p []byte, fromAddr *net.UDPAddr)
// derpRecvCh is used by receiveDERP to read DERP messages.
// It must have buffer size > 0; see issue 3736.
derpRecvCh chan derpReadResult
// bind is the wireguard-go conn.Bind for Conn.
@@ -296,11 +300,18 @@ type Conn struct {
havePrivateKey syncs.AtomicBool
publicKeyAtomic atomic.Value // of key.NodePublic (or NodeKey zero value if !havePrivateKey)
// derpMapAtomic is the same as derpMap, but without requiring
// sync.Mutex. For use with NewRegionClient's callback, to avoid
// lock ordering deadlocks. See issue 3726 and mu field docs.
derpMapAtomic atomic.Value // of *tailcfg.DERPMap
// port is the preferred port from opts.Port; 0 means auto.
port syncs.AtomicUint32
// ============================================================
// mu guards all following fields; see userspaceEngine lock ordering rules
// mu guards all following fields; see userspaceEngine lock
// ordering rules against the engine. For derphttp, mu must
// be held before derphttp.Client.mu.
mu sync.Mutex
muCond *sync.Cond
@@ -519,7 +530,7 @@ func (o *Options) derpActiveFunc() func() {
// of NewConn. Mostly for tests.
func newConn() *Conn {
c := &Conn{
derpRecvCh: make(chan derpReadResult),
derpRecvCh: make(chan derpReadResult, 1), // must be buffered, see issue 3736
derpStarted: make(chan struct{}),
peerLastDerp: make(map[key.NodePublic]int),
peerMap: newPeerMap(),
@@ -549,6 +560,7 @@ func NewConn(opts Options) (*Conn, error) {
if opts.LinkMonitor != nil {
c.portMapper.SetGatewayLookupFunc(opts.LinkMonitor.GatewayAndSelfIP)
}
c.linkMon = opts.LinkMonitor
if err := c.initialBind(); err != nil {
return nil, err
@@ -1209,7 +1221,7 @@ func (c *Conn) sendUDPStd(addr *net.UDPAddr, b []byte) (sent bool, err error) {
switch {
case addr.IP.To4() != nil:
_, err = c.pconn4.WriteTo(b, addr)
if err != nil && c.noV4.Get() {
if err != nil && (c.noV4.Get() || neterror.TreatAsLostUDP(err)) {
return false, nil
}
case len(addr.IP) == net.IPv6len:
@@ -1218,7 +1230,7 @@ func (c *Conn) sendUDPStd(addr *net.UDPAddr, b []byte) (sent bool, err error) {
return false, nil
}
_, err = c.pconn6.WriteTo(b, addr)
if err != nil && c.noV6.Get() {
if err != nil && (c.noV6.Get() || neterror.TreatAsLostUDP(err)) {
return false, nil
}
default:
@@ -1347,19 +1359,23 @@ func (c *Conn) derpWriteChanOfAddr(addr netaddr.IPPort, peer key.NodePublic) cha
}
// Note that derphttp.NewRegionClient does not dial the server
// so it is safe to do under the mu lock.
// (it doesn't block) so it is safe to do under the c.mu lock.
dc := derphttp.NewRegionClient(c.privateKey, c.logf, func() *tailcfg.DERPRegion {
// Warning: it is not legal to acquire
// magicsock.Conn.mu from this callback.
// It's run from derphttp.Client.connect (via Send, etc)
// and the lock ordering rules are that magicsock.Conn.mu
// must be acquired before derphttp.Client.mu.
// See https://github.com/tailscale/tailscale/issues/3726
if c.connCtx.Err() != nil {
// If we're closing, don't try to acquire the lock.
// We might already be in Conn.Close and the Lock would deadlock.
// We're closing anyway; return nil to stop dialing.
return nil
}
c.mu.Lock()
defer c.mu.Unlock()
if c.derpMap == nil {
derpMap, _ := c.derpMapAtomic.Load().(*tailcfg.DERPMap)
if derpMap == nil {
return nil
}
return c.derpMap.Regions[regionID]
return derpMap.Regions[regionID]
})
dc.SetCanAckPings(true)
@@ -1801,6 +1817,14 @@ func (c *Conn) sendDiscoMessage(dst netaddr.IPPort, dstKey key.NodePublic, dstDi
} else {
metricSentDiscoUDP.Add(1)
}
switch m.(type) {
case *disco.Ping:
metricSentDiscoPing.Add(1)
case *disco.Pong:
metricSentDiscoPong.Add(1)
case *disco.CallMeMaybe:
metricSentDiscoCallMeMaybe.Add(1)
}
} else if err == nil {
// Can't send. (e.g. no IPv6 locally)
} else {
@@ -2240,12 +2264,32 @@ func (c *Conn) SetDERPMap(dm *tailcfg.DERPMap) {
return
}
c.derpMapAtomic.Store(dm)
old := c.derpMap
c.derpMap = dm
if dm == nil {
c.closeAllDerpLocked("derp-disabled")
return
}
// Reconnect any DERP region that changed definitions.
if old != nil {
changes := false
for rid, oldDef := range old.Regions {
if reflect.DeepEqual(oldDef, dm.Regions[rid]) {
continue
}
changes = true
if rid == c.myDerp {
c.myDerp = 0
}
c.closeDerpLocked(rid, "derp-region-redefined")
}
if changes {
c.logActiveDerpLocked()
}
}
go c.ReSTUN("derp-map-update")
}
@@ -2385,14 +2429,61 @@ func (c *Conn) closeAllDerpLocked(why string) {
c.logActiveDerpLocked()
}
// maybeCloseDERPsOnRebind, in response to a rebind, closes all
// DERP connections that don't have a local address in okayLocalIPs
// and pings all those that do.
func (c *Conn) maybeCloseDERPsOnRebind(okayLocalIPs []netaddr.IPPrefix) {
c.mu.Lock()
defer c.mu.Unlock()
for regionID, ad := range c.activeDerp {
la, err := ad.c.LocalAddr()
if err != nil {
c.closeOrReconectDERPLocked(regionID, "rebind-no-localaddr")
continue
}
if !tsaddr.PrefixesContainsIP(okayLocalIPs, la.IP()) {
c.closeOrReconectDERPLocked(regionID, "rebind-default-route-change")
continue
}
regionID := regionID
dc := ad.c
go func() {
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
if err := dc.Ping(ctx); err != nil {
c.mu.Lock()
defer c.mu.Unlock()
c.closeOrReconectDERPLocked(regionID, "rebind-ping-fail")
return
}
c.logf("post-rebind ping of DERP region %d okay", regionID)
}()
}
c.logActiveDerpLocked()
}
// closeOrReconectDERPLocked closes the DERP connection to the
// provided regionID and starts reconnecting it if it's our current
// home DERP.
//
// why is a reason for logging.
//
// c.mu must be held.
func (c *Conn) closeOrReconectDERPLocked(regionID int, why string) {
c.closeDerpLocked(regionID, why)
if !c.privateKey.IsZero() && c.myDerp == regionID {
c.startDerpHomeConnectLocked()
}
}
// c.mu must be held.
// It is the responsibility of the caller to call logActiveDerpLocked after any set of closes.
func (c *Conn) closeDerpLocked(node int, why string) {
if ad, ok := c.activeDerp[node]; ok {
c.logf("magicsock: closing connection to derp-%v (%v), age %v", node, why, time.Since(ad.createTime).Round(time.Second))
func (c *Conn) closeDerpLocked(regionID int, why string) {
if ad, ok := c.activeDerp[regionID]; ok {
c.logf("magicsock: closing connection to derp-%v (%v), age %v", regionID, why, time.Since(ad.createTime).Round(time.Second))
go ad.c.Close()
ad.cancel()
delete(c.activeDerp, node)
delete(c.activeDerp, regionID)
metricNumDERPConns.Set(int64(len(c.activeDerp)))
}
}
@@ -2551,6 +2642,7 @@ func (c *connBind) Close() error {
}
// Send an empty read result to unblock receiveDERP,
// which will then check connBind.Closed.
// connBind.Closed takes c.mu, but c.derpRecvCh is buffered.
c.derpRecvCh <- derpReadResult{}
return nil
}
@@ -2823,13 +2915,15 @@ func (c *Conn) Rebind() {
return
}
c.mu.Lock()
c.closeAllDerpLocked("rebind")
if !c.privateKey.IsZero() {
c.startDerpHomeConnectLocked()
var ifIPs []netaddr.IPPrefix
if c.linkMon != nil {
st := c.linkMon.InterfaceState()
defIf := st.DefaultRouteInterface
ifIPs = st.InterfaceIPs[defIf]
c.logf("Rebind; defIf=%q, ips=%v", defIf, ifIPs)
}
c.mu.Unlock()
c.maybeCloseDERPsOnRebind(ifIPs)
c.resetEndpointStates()
}
@@ -3041,6 +3135,7 @@ func (c *blockForeverConn) Close() error {
return net.ErrClosed
}
c.closed = true
c.cond.Broadcast()
return nil
}
@@ -3194,7 +3289,7 @@ const (
// try to keep an established endpoint peering alive.
// It's also the idle time at which we stop doing STUN queries to
// keep NAT mappings alive.
sessionActiveTimeout = 2 * time.Minute
sessionActiveTimeout = 45 * time.Second
// upgradeInterval is how often we try to upgrade to a better path
// even if we have some non-DERP route that works.
@@ -3202,11 +3297,11 @@ const (
// heartbeatInterval is how often pings to the best UDP address
// are sent.
heartbeatInterval = 2 * time.Second
heartbeatInterval = 3 * time.Second
// trustUDPAddrDuration is how long we trust a UDP address as the exclusive
// path (without using DERP) without having heard a Pong reply.
trustUDPAddrDuration = 5 * time.Second
trustUDPAddrDuration = 6500 * time.Millisecond
// goodEnoughLatency is the latency at or under which we don't
// try to upgrade to a better path.
@@ -4047,13 +4142,16 @@ var (
metricRecvDataIPv6 = clientmetric.NewCounter("magicsock_recv_data_ipv6")
// Disco packets
metricSendDiscoUDP = clientmetric.NewCounter("magicsock_disco_send_udp")
metricSendDiscoDERP = clientmetric.NewCounter("magicsock_disco_send_derp")
metricSentDiscoUDP = clientmetric.NewCounter("magicsock_disco_sent_udp")
metricSentDiscoDERP = clientmetric.NewCounter("magicsock_disco_sent_derp")
metricRecvDiscoBadPeer = clientmetric.NewCounter("magicsock_disco_recv_bad_peer")
metricRecvDiscoBadKey = clientmetric.NewCounter("magicsock_disco_recv_bad_key")
metricRecvDiscoBadParse = clientmetric.NewCounter("magicsock_disco_recv_bad_parse")
metricSendDiscoUDP = clientmetric.NewCounter("magicsock_disco_send_udp")
metricSendDiscoDERP = clientmetric.NewCounter("magicsock_disco_send_derp")
metricSentDiscoUDP = clientmetric.NewCounter("magicsock_disco_sent_udp")
metricSentDiscoDERP = clientmetric.NewCounter("magicsock_disco_sent_derp")
metricSentDiscoPing = clientmetric.NewCounter("magicsock_disco_sent_ping")
metricSentDiscoPong = clientmetric.NewCounter("magicsock_disco_sent_pong")
metricSentDiscoCallMeMaybe = clientmetric.NewCounter("magicsock_disco_sent_callmemaybe")
metricRecvDiscoBadPeer = clientmetric.NewCounter("magicsock_disco_recv_bad_peer")
metricRecvDiscoBadKey = clientmetric.NewCounter("magicsock_disco_recv_bad_key")
metricRecvDiscoBadParse = clientmetric.NewCounter("magicsock_disco_recv_bad_parse")
metricRecvDiscoUDP = clientmetric.NewCounter("magicsock_disco_recv_udp")
metricRecvDiscoDERP = clientmetric.NewCounter("magicsock_disco_recv_derp")

View File

@@ -1763,3 +1763,27 @@ func (m *peerMap) validate() error {
return nil
}
func TestBlockForeverConnUnblocks(t *testing.T) {
c := newBlockForeverConn()
done := make(chan error, 1)
go func() {
defer close(done)
_, _, err := c.ReadFrom(make([]byte, 1))
done <- err
}()
time.Sleep(50 * time.Millisecond) // give ReadFrom time to get blocked
if err := c.Close(); err != nil {
t.Fatal(err)
}
timer := time.NewTimer(5 * time.Second)
defer timer.Stop()
select {
case err := <-done:
if err != net.ErrClosed {
t.Errorf("got %v; want net.ErrClosed", err)
}
case <-timer.C:
t.Fatal("timeout")
}
}

Some files were not shown because too many files have changed in this diff Show More