Compare commits

...

176 Commits

Author SHA1 Message Date
Avery Pennarun
daebded286 VERSION.txt: this is v1.2.9
Signed-off-by: Avery Pennarun <apenwarr@tailscale.com>
2020-11-18 23:45:10 -05:00
Brad Fitzpatrick
1867f784ed wgengine/monitor: fix memory corruption in Windows implementation
I used the Windows APIs wrong previously, but it had worked just
enough.

Updates #921

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
(cherry picked from commit eccc167733)
2020-11-18 14:52:38 -08:00
David Anderson
cde3a23b66 VERSION.txt: this is 1.2.8.
Signed-off-by: David Anderson <danderson@tailscale.com>
2020-11-16 15:52:28 -08:00
Brad Fitzpatrick
a86f5dc1fd wgengine: reconfigure wireguard peer in two steps when its disco key changes
First remove the device (to clear its wireguard session key), and then
add it back.

Fixes #929

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
(cherry picked from commit d9e2edb5ae)
2020-11-16 15:27:48 -08:00
David Anderson
fa4dc33eab VERSION.txt: this is 1.2.7.
Signed-off-by: David Anderson <danderson@tailscale.com>
2020-11-16 14:22:03 -08:00
David Anderson
b6e541e2eb wgengine/filter: don't drop GCP DNS.
Manual backport of 3c508a58cc.

Signed-off-by: David Anderson <danderson@tailscale.com>
2020-11-16 14:14:26 -08:00
Brad Fitzpatrick
3b75550ad0 wgengine/router: lock goroutine to OS thread before using OLE [windows]
See https://github.com/tailscale/tailscale/issues/921#issuecomment-727526807

Not yet sure whether this is our problem, but it can't hurt at least,
and seems like what we're supposed to do.

Updates #921

(cherry picked from commit fc8bc76e58)
2020-11-16 14:10:31 -08:00
Brad Fitzpatrick
8ae146478c net/netstat: remove some unsafe
Just removing any unnecessary unsafe while auditing unsafe usage for #921.

(cherry picked from commit 7a01cd27ca)
2020-11-16 14:10:14 -08:00
Brad Fitzpatrick
e854b433aa net/netns: remove use of unsafe on Windows
Found while auditing unsafe for #921 via the list at:

https://github.com/tailscale/tailscale/issues/921#issuecomment-727365383

No need for unsafe here, so remove it.

(cherry picked from commit 45d96788b5)
2020-11-16 14:08:49 -08:00
Brad Fitzpatrick
d285b548bf util/endian: add package with const for whether platform is big endian
(cherry picked from commit 000347d4cf)
2020-11-16 14:08:16 -08:00
David Anderson
3f4e6d959a VERSION.txt: this is 1.2.6.
Signed-off-by: David Anderson <danderson@tailscale.com>
2020-11-12 22:01:50 -08:00
Brad Fitzpatrick
449cbf5cfb control/controlclient: diagnose zero bytes from control
Updates #921

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
(cherry picked from commit fac2b30eff)
2020-11-12 14:45:20 -08:00
David Anderson
c242540a97 wgengine/router: disable IPv6 if v6 policy routing is unavailable.
Fixes #895.

Signed-off-by: David Anderson <danderson@tailscale.com>
(cherry picked from commit a664aac877)
2020-11-11 17:22:38 -08:00
Avery Pennarun
e29f92f653 VERSION.txt: this is v1.2.5
Signed-off-by: Avery Pennarun <apenwarr@tailscale.com>
2020-11-11 02:57:08 -05:00
Avery Pennarun
82dbf148a3 .gitignore: ignore *.tmp files.
This fixes the problem where, while running `redo version-info.sh`, the
repo would always show up as dirty, because redo creates a temp file
named *.tmp. This caused the version code to always have a -dirty tag,
but not when you run version.sh by hand.

Signed-off-by: Avery Pennarun <apenwarr@tailscale.com>
2020-11-11 01:54:54 -05:00
Avery Pennarun
df38ea4d65 version.sh: keep the short version even if there are patches on top.
Instead of reverting to 0.0.0, keep the same version number (eg. 1.2.4)
but add an extra suffix with the change count,
eg. 1.2.4-6-tb35d95ad7-gcb8be72e6. This avoids the problem where a
small patch causes the code to report a totally different version to
the server, which might change its behaviour based on version code.
(The server might enable various bug workarounds since it thinks
0.0.0 is very old.)

Signed-off-by: Avery Pennarun <apenwarr@tailscale.com>
2020-11-11 00:10:57 -05:00
Avery Pennarun
28f3136611 version.sh: remove use of git describe --exclude
This option isn't available on slightly older versions of git. We were
no longer using the real describe functionality anyway, so let's just do
something simpler to detect a dirty worktree.

While we're here, fix up a little bit of sh style.

Signed-off-by: Avery Pennarun <apenwarr@tailscale.com>
2020-11-11 00:10:57 -05:00
Avery Pennarun
1be01ddc6e Reverse earlier "allow tag without 'tag:' prefix" changes.
These accidentally make the tag syntax more flexible than was intended,
which will create forward compatibility problems later. Let's go back
to the old stricter parser.

Revert "cmd/tailscale/cli: fix double tag: prefix in tailscale up"
Revert "cmd/tailscale/cli, tailcfg: allow tag without "tag:" prefix in 'tailscale up'"

This reverts commit a702921620.
This reverts commit cd07437ade.

Affects #861.

Signed-off-by: Avery Pennarun <apenwarr@tailscale.com>
2020-11-10 22:30:02 -05:00
Brad Fitzpatrick
bddc882f7d net/interfaces: ignore bogus proxy URLs from winhttp [windows]
Updates tailscale/corp#853

(cherry picked from commit d192bd0f86)
2020-11-10 14:42:58 -08:00
Brad Fitzpatrick
33505097c4 ipn, tailcfg: change Windows subnet disabling behavior w/ WPAD
In 1.0, subnet relays were not specially handled when WPAD+PAC was
present on the network.

In 1.2, on Windows, subnet relays were disabled if WPAD+PAC was
present. That was what some users wanted, but not others.

This makes it configurable per domain, reverting back to the 1.0
default state of them not being special. Users who want that behavior
can then enable it.

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
(cherry picked from commit d21956436a)
2020-11-10 14:42:53 -08:00
Brad Fitzpatrick
d8a531108d wgengine/magicsock: quiet an IPv6 warning in tests
In tests, we force binding to localhost to avoid OS firewall warning
dialogs.

But for IPv6, we were trying (and failing) to bind to 127.0.0.1.

You'd think we'd just say "localhost", but that's apparently ill
defined. See
https://tools.ietf.org/html/draft-ietf-dnsop-let-localhost-be-localhost
and golang/go#22826. (It's bitten me in the past, but I can't
remember specific bugs.)

So use "::1" explicitly for "udp6", which makes the test quieter.

(cherry picked from commit 450cfedeba)
2020-11-10 14:42:49 -08:00
David Anderson
c73c3001a4 tailscaled.service: also cleanup prior to starting.
Fixes #813.

Signed-off-by: David Anderson <danderson@tailscale.com>
(cherry picked from commit 7988f75b87)
2020-11-09 20:17:25 -08:00
David Crawshaw
c572d622d7 VERSION.txt: this is v1.2.4 2020-11-08 10:39:42 -05:00
David Crawshaw
fead79a02f version/version.sh: strip wc whitespace on macos
The output of `wc -l` on darwin starts with a tab:

	git rev-list 266f6548611ad0de93e7470eb13731db819f184b..HEAD | wc -l
	       0

Signed-off-by: David Crawshaw <crawshaw@tailscale.com>
2020-11-08 10:39:05 -05:00
David Crawshaw
266f654861 VERSION.txt: this is 1.2.3 2020-11-08 10:09:20 -05:00
Brad Fitzpatrick
d91a9131b1 ipn: debug zero bytes in IPN json messages
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
(cherry picked from commit b4e19b95ed)
2020-11-06 13:19:52 -08:00
Brad Fitzpatrick
dced1d6a37 ipn: treat zero-length file state store file as missing
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
(cherry picked from commit 8f30fa67aa)
2020-11-06 13:01:29 -08:00
Brad Fitzpatrick
d5bc375b0e wgengine/router: don't double-prefix dns log messages [Windows]
(cherry picked from commit 119101962c)
2020-11-06 13:01:29 -08:00
Brad Fitzpatrick
c1bae7ad64 tailcfg: document FilterRule
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
(cherry picked from commit bda53897b5)
2020-11-06 13:01:29 -08:00
David Anderson
76c2982d88 VERSION.txt: this is 1.2.2. 2020-11-05 12:25:25 -08:00
Brad Fitzpatrick
3d64eef37b control/controlclient: send warning flag in map request when IP forwarding off
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
(cherry picked from commit 782e07c0ae)
2020-11-04 14:53:01 -08:00
Brad Fitzpatrick
4f292740b0 ipn: clean up Prefs logging at start
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
(cherry picked from commit 4f4e84236a)
2020-11-04 12:06:25 -08:00
Brad Fitzpatrick
e1327154bb ipn: disambiguate how machine key was initialized
Seeing "frontend-provided legacy machine key" was weird (and not quite
accurate) on Linux machines where it comes from the _daemon key's
persist prefs, not the "frontend".

Make the log message distinguish between the cases.

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
(cherry picked from commit 6bcb466096)
2020-11-04 12:06:16 -08:00
Brad Fitzpatrick
a702921620 cmd/tailscale/cli: fix double tag: prefix in tailscale up
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
(cherry picked from commit 696e160cfc)
2020-11-04 08:19:39 -08:00
David Anderson
b9b7fbdd21 build_dist: fix after version refactor.
(cherry picked from commit 39bbb86b09)
2020-11-03 14:50:27 -08:00
Avery Pennarun
9446e5c170 VERSION.txt: this is v1.2.1. 2020-11-03 17:34:05 -05:00
Avery Pennarun
75cd82791e Merge remote-tracking branch 'origin/main' into HEAD
* origin/main:
  wgengine/router/dns: run ipconfig /registerdns async, log timing
  net/tshttpproxy: aggressively rate-limit error logs in Transport.Proxy path
  ipn: only use Prefs, not computed stateKey, to determine server mode
  VERSION: rename to version.txt to work around macOS limitations.
  version: greatly simplify redo nonsense, now that we use VERSION.
  ipn, ipn/ipnserver: add IPN state for server in use, handle explicitly
  version: calculate version info without using git tags.
  version: use -g as the "other" suffix, so that `git show` works.
  ipn/ipnserver: remove "Server mode" from a user-visible error message
  ipn: fix crash generating machine key on new installs
  Change some os.IsNotExist to errors.Is(err, os.ErrNotExist) for non-os errors.
  .github/workflows: use cache to speed up Windows tests
  tsweb: add StatusCodeCounters to HandlerOptions
  tsweb: add StdHandlerOpts that accepts an options struct
  ipn: don't temporarilySetMachineKeyInPersist for Android clients
2020-11-03 17:33:23 -05:00
Brad Fitzpatrick
28f6552646 wgengine/router/dns: run ipconfig /registerdns async, log timing
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2020-11-03 14:27:37 -08:00
Brad Fitzpatrick
1036f51a56 net/tshttpproxy: aggressively rate-limit error logs in Transport.Proxy path
Otherwise log upload HTTP requests generate proxy errrors which
generate logs which generate HTTP requests which generate proxy
errors which generate more logs, etc.

Fixes #879
2020-11-03 09:23:57 -08:00
Brad Fitzpatrick
07b6ffd55c ipn: only use Prefs, not computed stateKey, to determine server mode
When the service was running without a client (e.g. after a reboot)
and then the owner logs in and the GUI attaches, the computed state
key changed to "" (driven by frontend prefs), and then it was falling
out of server mode, despite the GUI-provided prefs still saying it
wanted server mode.

Also add some logging. And remove a scary "Access denied" from a
user-visible error, making the two possible already-in-use error
messages consistent with each other.
2020-11-02 21:13:51 -08:00
David Anderson
de5da37a22 VERSION: rename to version.txt to work around macOS limitations.
Signed-off-by: David Anderson <danderson@tailscale.com>
2020-11-02 20:39:10 -08:00
David Anderson
65bad9a8bd version: greatly simplify redo nonsense, now that we use VERSION.
Signed-off-by: David Anderson <danderson@tailscale.com>
2020-11-02 19:54:44 -08:00
Brad Fitzpatrick
20a357b386 ipn, ipn/ipnserver: add IPN state for server in use, handle explicitly
On Windows, we were previously treating a server used by different
users as a fatal error, which meant the second user (upon starting
Tailscale, explicitly or via Start Up programs) got an invasive error
message dialog.

Instead, give it its own IPN state and change the Notify.ErrMessage to
be details in that state. Then the Windows GUI can be less aggresive
about that happening.

Also,

* wait to close the IPN connection until the server ownership state
  changes so the GUI doesn't need to repeatedly reconnect to discover
  changes.

* fix a bug discovered during testing: on system reboot, the
  ipnserver's serverModeUser was getting cleared while the state
  transitioned from Unknown to Running. Instead, track 'inServerMode'
  explicitly and remove the old accessor method which was error prone.

* fix a rare bug where the client could start up and set the server
  mode prefs in its Start call and we wouldn't persist that to the
  StateStore storage's prefs start key. (Previously it was only via a
  prefs toggle at runtime)
2020-11-02 15:25:11 -08:00
David Anderson
4d5d5f89a3 version: calculate version info without using git tags.
This makes it easier to integrate this version math into a submodule-ful
world. We'll continue to have regular git tags that parallel the information
in VERSION, so that builds out of this repository behave the same.

Signed-off-by: David Anderson <danderson@tailscale.com>
(cherry picked from commit 437142daa5)
(altered VERSION to reflect correct information for this release branch)
2020-11-02 15:24:36 -08:00
David Anderson
437142daa5 version: calculate version info without using git tags.
This makes it easier to integrate this version math into a submodule-ful
world. We'll continue to have regular git tags that parallel the information
in VERSION, so that builds out of this repository behave the same.

Signed-off-by: David Anderson <danderson@tailscale.com>
2020-11-02 15:23:35 -08:00
David Anderson
710b105f38 version: use -g as the "other" suffix, so that git show works.
Fixes #880.

Signed-off-by: David Anderson <danderson@tailscale.com>
2020-11-02 13:12:34 -08:00
Brad Fitzpatrick
f3aa08de76 ipn/ipnserver: remove "Server mode" from a user-visible error message
That's an internal nickname.
2020-11-02 09:22:21 -08:00
Brad Fitzpatrick
cc3259f8d9 ipn: fix crash generating machine key on new installs
Regression from d6ad41dcea (for #732).

Probably also means eab6e9ea4e was unnecessary, but it's fine.

Fixes #887
2020-11-02 08:54:04 -08:00
Brad Fitzpatrick
01ee638cca Change some os.IsNotExist to errors.Is(err, os.ErrNotExist) for non-os errors.
os.IsNotExist doesn't unwrap errors. errors.Is does.

The ioutil.ReadFile ones happened to be fine but I changed them so
we're consistent with the rule: if the error comes from os, you can
use os.IsNotExist, but from any other package, use errors.Is.
(errors.Is always would also work, but not worth updating all the code)

The motivation here was that we were logging about failure to migrate
legacy relay node prefs file on startup, even though the code tried
to avoid that.

See golang/go#41122
2020-11-02 08:33:34 -08:00
Alex Brainman
037daad47a .github/workflows: use cache to speed up Windows tests
Fixes #872

Signed-off-by: Alex Brainman <alex.brainman@gmail.com>
2020-11-02 07:45:48 -08:00
Josh Bleecher Snyder
3b46655dbb tsweb: add StatusCodeCounters to HandlerOptions
This lets servers using tsweb register expvars
that will track the number of requests ending
in 200s/300s/400s/500s.

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2020-10-30 11:07:57 -07:00
Josh Bleecher Snyder
e98f2c57d6 tsweb: add StdHandlerOpts that accepts an options struct
I'm about to add yet another StdHandler option.
Time to refactor.

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2020-10-30 11:07:57 -07:00
Elias Naur
eab6e9ea4e ipn: don't temporarilySetMachineKeyInPersist for Android clients
Without this change, newly installed Android clients crash on startup
with

panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x9881b9f8]

goroutine 29 [running]:
tailscale.com/ipn.(*LocalBackend).initMachineKeyLocked.func1(0x50cb1b9c, 0x503c9a00)
	/home/elias/proj/tailscale/ipn/local.go:711 +0x2c
tailscale.com/ipn.(*LocalBackend).initMachineKeyLocked(0x503c9a00, 0x0, 0x0)
	/home/elias/proj/tailscale/ipn/local.go:736 +0x728
tailscale.com/ipn.(*LocalBackend).loadStateLocked(0x503c9a00, 0x988be40e, 0xb, 0x0, 0x0, 0x0, 0x0, 0x0)
	/home/elias/proj/tailscale/ipn/local.go:817 +0x1e8
tailscale.com/ipn.(*LocalBackend).Start(0x503c9a00, 0x0, 0x0, 0x988be40e, 0xb, 0x0, 0x0, 0x0, 0x0, 0x0, ...)
	/home/elias/proj/tailscale/ipn/local.go:412 +0x200
main.(*backend).Start(...)
	/home/elias/proj/tailscale-android/cmd/tailscale/backend.go:116
main.(*App).runBackend.func3(0x50106340, 0x5000c060, 0x50d9a280)
	/home/elias/proj/tailscale-android/cmd/tailscale/main.go:169 +0x90
created by main.(*App).runBackend
	/home/elias/proj/tailscale-android/cmd/tailscale/main.go:168 +0x27c

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2020-10-30 06:50:43 -07:00
Avery Pennarun
bb058703ee Meaningless change so that v1.2.0 tag doesn't match the "main" branch. 2020-10-30 04:04:50 -04:00
David Anderson
68ddf134d7 wgengine/router/dns: issue ipconfig /registerdns when applying DNS settings.
Amazingly, there doesn't seem to be a documented way of updating network
configuration programmatically in a way that Windows takes notice of.
The naturopathic remedy for this is to invoke ipconfig /registerdns, which
does a variety of harmless things and also invokes the private API that
tells windows to notice new adapter settings. This makes our DNS config
changes stick within a few seconds of us setting them.

If we're invoking a shell command anyway, why futz with the registry at
all? Because netsh has no command for changing the DNS suffix list, and
its commands for setting resolvers requires parsing its output and
keeping track of which server is in what index. Amazingly, twiddling
the registry directly is the less painful option.

Fixes #853.

Signed-off-by: David Anderson <danderson@tailscale.com>
2020-10-29 20:05:38 -07:00
Brad Fitzpatrick
7e1a146e6c cmd/tailscaled: update depaware.txt 2020-10-29 15:30:55 -07:00
Brad Fitzpatrick
2b819ab38c ipn: don't log redundant peer stats so often
It was especially bad on our GUI platforms with a frontend that polls it.

No need to log it every few seconds if it's unchanged. Make it slightly
less allocate-y while I'm here.
2020-10-29 15:26:10 -07:00
Brad Fitzpatrick
8b904b1493 types/logger: fix LogOnChange to pass through format/args to underlying logger
So they don't get interpretted as a format pattern or get rate-limited away
in the wrong way.
2020-10-29 15:22:29 -07:00
Brad Fitzpatrick
ff7ddd9d20 ipn/ipnserver: move Windows local disk logging up to the parent process
To capture panics, log.Printf writes to os.Stderr, etc.

Fixes #726
2020-10-29 15:02:04 -07:00
Brad Fitzpatrick
420838f90e log/filelogger: move our Windows disk file writing+rotation package here
It's still Windows-only for now but it's easy to de-Windows-ify when needed.

Moving it out of corp repo and into tailscale/tailscale so we can use
it in ipnserver.BabysitProc.

Updates #726
2020-10-29 14:59:44 -07:00
Brad Fitzpatrick
508f5c3ae0 wgengine/router: fix bug where getInterfaceRoutes always returned an empty list
Regression from f2ce64f0c6 (r43710860)

Fixes #870
2020-10-29 14:38:59 -07:00
Brad Fitzpatrick
38bde61b3d wgengine/router: make Windows firewall configuration async
Updating the Windows firewall is usually reasonably fast, but
sometimes blocks for 20 seconds, 4 minutes, etc. Not sure why.

Until we understand that's happening, configure it in the background
without blocking the normal control flow.

Updates #785

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2020-10-29 13:40:20 -07:00
Brad Fitzpatrick
c64718e9a0 ipn/ipnserver: work around os/user.LookupId failure on Windows
If we can't find the mapping from SID ("user ID") -> username, don't
treat that as a fatal. Apparently that happens in the wild for Reasons.
Ignore it for now. It's just a nice-to-have for error messages in the
rare multi-user case.

Updates #869

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2020-10-29 13:16:53 -07:00
David Anderson
09721fede8 version: fix documentation. 2020-10-28 16:29:26 -07:00
David Anderson
54e6c3a290 version: use OSS repo's version when building.
When building with redo, also include the git commit hash
from the proprietary repo, so that we have a precise commit
that identifies all build info (including Go toolchain version).

Add a top-level build script demonstrating to downstream distros
how to burn the right information into builds.

Adjust `tailscale version` to print commit hashes when available.

Fixes #841.

Signed-off-by: David Anderson <danderson@tailscale.com>
2020-10-28 16:17:21 -07:00
Brad Fitzpatrick
a1ccaa9658 .github/workflows: add tests on Windows
Fixes #50

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2020-10-28 09:23:42 -07:00
Brad Fitzpatrick
4a92fc9dc5 portlist: fix tests on Windows when not running as Administrator
Updates #50
2020-10-28 09:19:41 -07:00
Brad Fitzpatrick
7ac91c15bd net/netcheck: fix tests on Windows
Updates #50
2020-10-28 09:10:35 -07:00
Brad Fitzpatrick
fd2a30cd32 wgengine/magicsock: make test pass on Windows and without firewall dialog box
Updates #50
2020-10-28 09:02:08 -07:00
Brad Fitzpatrick
cd07437ade cmd/tailscale/cli, tailcfg: allow tag without "tag:" prefix in 'tailscale up'
Fixes #861
2020-10-28 07:59:57 -07:00
Brad Fitzpatrick
d6ad41dcea ipn: send machine key to clients so they can downgrade to 1.0.x if needed
Fixes #732

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2020-10-27 15:01:20 -07:00
Brad Fitzpatrick
e72f480d22 ipn: convert an int to a bool 2020-10-27 13:57:10 -07:00
Brad Fitzpatrick
a3f17b8108 control/controlclient: also log active account in netmaps
Updates tailscale/corp#461
2020-10-27 13:46:05 -07:00
Brad Fitzpatrick
999bc93a4d ipn: log active account on change
Updates tailscale/corp#461
2020-10-27 12:51:48 -07:00
Brad Fitzpatrick
66d196326f ipn: rename 'new' variable to 'newp'
Both to avoid shadowing new and because new is a little vague for such
a long method handling multiple new & old things.
2020-10-27 12:33:48 -07:00
Brad Fitzpatrick
5b1d03f016 control/controlclient: remove prior temporary macos debugging
It was an x/net/http2 bug, since fixed.
2020-10-27 09:25:38 -07:00
Brad Fitzpatrick
f33da73a82 go.sum: update 2020-10-27 09:25:29 -07:00
Alex Brainman
311899709b version: skip TestMkversion on windows
TestMkversion requires UNIX shell to run mkversion.sh. No such shell
is present on Windows. Just skip the test.

Updates #50

Signed-off-by: Alex Brainman <alex.brainman@gmail.com>
2020-10-27 07:47:58 -07:00
David Anderson
3d34128171 go.mod: update to new wireguard-go. 2020-10-26 19:23:01 +00:00
Brad Fitzpatrick
4f55ebf2d9 tailcfg: add some comments, remove some redundant types in literal 2020-10-26 08:53:07 -07:00
Brad Fitzpatrick
c44e244276 control/controlclient: add some temporary debugging for #839 2020-10-20 13:47:58 -07:00
Brad Fitzpatrick
9957c45995 tailcfg: bump, document MapRequest.Version value
Fixes tailscale/corp#634

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2020-10-20 10:48:59 -07:00
Brad Fitzpatrick
3909c82f3d control/controlclient: rename map debug knob, make it do both request+response
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2020-10-20 10:48:59 -07:00
Brad Fitzpatrick
6b1d2a5630 ipn: don't set DebugFlags to len 1 slice of empty string [""]
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2020-10-20 10:48:59 -07:00
Brad Fitzpatrick
691f1d5c1d types/flagtype: fix bug showing the default port value (shown in --help) 2020-10-19 20:18:31 -07:00
David Anderson
62d941dc26 tailcfg: add a DebugFlags field for experiments and debugging.
Also replaces the IPv6Overlay bool with use of DebugFlags, since
it's currently an experimental configuration.

Signed-off-by: David Anderson <danderson@tailscale.com>
2020-10-19 17:03:04 -07:00
Brad Fitzpatrick
ac866054c7 wgengine/magicsock: add a backoff on DERP reconnects
Fixes #808
2020-10-19 15:15:40 -07:00
Brad Fitzpatrick
22024a38c3 control/controlclient: log Hostinfo on change
Fixes #830

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2020-10-19 14:49:22 -07:00
Brad Fitzpatrick
7c8ca28c74 ipn: use cmd/cloner for Prefs.Clone
Also, make cmd/cloner's top-level "func Clone" generation opt-in.

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2020-10-19 12:15:49 -07:00
Brad Fitzpatrick
6cc6e251a9 logpolicy: add debug knob to force logging time to terminal 2020-10-19 08:10:05 -07:00
Brad Fitzpatrick
86c271caba types/logger: move RusagePrefixLog to logger package, disable by default
The RusagePrefixLog is rarely useful, hasn't been useful in a long
time, is rarely the measurement we need, and is pretty spammy (and
syscall-heavy). Disable it by default. We can enable it when we're
debugging memory.
2020-10-19 07:56:23 -07:00
David Anderson
ff0cf6340a wgengine/router: fix configuration of loopback netfilter rules for v6.
Signed-off-by: David Anderson <danderson@tailscale.com>
2020-10-15 15:15:24 -07:00
David Anderson
5c35c35e7f tsaddr: add helpers for the Tailscale IPv6 range, and 4to6 conversion.
Signed-off-by: David Anderson <danderson@tailscale.com>
2020-10-15 15:15:24 -07:00
David Anderson
c6dbd24f67 tailcfg: add a field to advertise support for IPv6 tailscale config.
Signed-off-by: David Anderson <danderson@tailscale.com>
2020-10-15 15:15:24 -07:00
Brad Fitzpatrick
7a2a3955d3 logtail/filch: skip a broken test on Windows
Add a TODO with some notes about why it's skipped for now.

Updates #50
2020-10-14 21:33:01 -07:00
Brad Fitzpatrick
a6c34bdc28 control/controlclient: also include our own profile (for when no self-owned peers)
Fix from regression in previous commit
(0e3048d8e0) that was caught by e2e
tests.

In that previous commit, the user's own profile was omitted from the
NetworkMap in the case where the user only had one node.
2020-10-14 19:07:31 -07:00
Brad Fitzpatrick
0e3048d8e0 control/controlclient: support delta userprofiles from control
I was going to make support for this advertised from the client, but
turns out only "tailscale status" even uses the UserProfiles field and
fails gracefully (omits that field) if a user profile for a user is
missing, so I think we can just reuse the DeltaPeers field from the
client to ask the control server to also delta encode the user
profiles.

For the few users running 1.1.x (unstable) versions between DeltaPeers
support (1.1.82) and this (~1.1.541), they'll just sometimes have
missing names in "tailscale status --json" or "tailscale status --web"
(the only places the UserProfile is used).
2020-10-14 18:46:07 -07:00
Brad Fitzpatrick
82f2fdc194 control/controlclient: adjust some logging point names
The previous code read too explicitly like log.Printf("I am here1"),
log.Printf("I am here2"). It still is with this change, but prettier, and
less subject to code rearranging order.
2020-10-14 14:39:42 -07:00
Brad Fitzpatrick
1fd9958e9d ipn: wait for initial portpoll result before starting controlclient
We were creating the controlclient and starting the portpoll concurrently,
which frequently resulted in the first controlclient connection being canceled
by the firsdt portpoll result ~milliseconds later, resulting in another
HTTP request.

Instead, wait a bit for the first portpoll result so it's much less likely to
interrupt our controlclient connection.

Updates tailscale/corp#557
2020-10-14 14:07:40 -07:00
Brad Fitzpatrick
1819f6f8c8 control/controlclient: set MapRequest.ReadOnly on initial empty endpoint request
On startup, clients do a MapRequest with empty endpoints while they
learn the DERP map to discover the STUN servers they then query to
learn their endpoints.

Set MapRequest.ReadOnly on those initial queries to not broadcast the
empty endpoints out to peers. The read results will come a half second
later (or less).

Updates tailscale/corp#557
2020-10-14 14:01:33 -07:00
Brad Fitzpatrick
105a820622 wgengine/magicsock: skip an endpoint update at start-up
At startup the client doesn't yet have the DERP map so can't do STUN
queries against DERP servers, so it only knows it local interface
addresses, not its STUN-mapped addresses.

We were reporting the interface-local addresses to control, getting
the DERP map, and then immediately reporting the full set of
updates. That was an extra HTTP request to control, but worse: it was
an extra broadcast from control out to all the peers in the network.

Now, skip the initial update if there are no stun results and we don't
have a DERP map.

More work remains optimizing start-up requests/map updates, but this
is a start.

Updates tailscale/corp#557
2020-10-14 11:01:19 -07:00
Brad Fitzpatrick
551e1e99e9 net/netns: don't bind to device for localhost connections
Fixes derphttp test failures on Windows (for #50).
2020-10-13 15:24:07 -07:00
Brad Fitzpatrick
746f03669c wgengine: fix lazy wireguard config bug when disco keys change
There was a bug with the lazy wireguard config code where, if the
minimum set of peers to tell wireguard didn't change, we skipped
calling userspaceEngine.updateActivityMapsLocked which updated
the various data structures that matched incoming traffic to later
reconfigure the minimum config.

That meant if an idle peer restarted and changed discovery keys, we
skipped updating our maps of disco keys/IPs that would caused us to
lazily inflate the config for that peer later if/when it did send
traffic.
2020-10-13 12:10:51 -07:00
Brad Fitzpatrick
2076a50862 wgengine/magicsock: finish a comment sentence that ended prematurely 2020-10-13 12:10:51 -07:00
Brad Fitzpatrick
371f1a9502 go.sum: add a missing entry that Go keeps adding 2020-10-13 12:10:51 -07:00
Alex Brainman
f2ce64f0c6 wgengine/router: unfork winipcfg-go package, use upstream
Use golang.zx2c4.com/wireguard/windows/tunnel/winipcfg
instead of github.com/tailscale/winipcfg-go package.

Updates #760

Signed-off-by: Alex Brainman <alex.brainman@gmail.com>
2020-10-13 09:21:22 -07:00
Brad Fitzpatrick
515866d7c6 ipn, ipnserver, cmd/tailscale: add "server mode" support on Windows
This partially (but not yet fully) migrates Windows to tailscaled's
StateStore storage system.

This adds a new bool Pref, ForceDaemon, defined as:

// ForceDaemon specifies whether a platform that normally
// operates in "client mode" (that is, requires an active user
// logged in with the GUI app running) should keep running after the
// GUI ends and/or the user logs out.
//
// The only current applicable platform is Windows. This
// forced Windows to go into "server mode" where Tailscale is
// running even with no users logged in. This might also be
// used for macOS in the future. This setting has no effect
// for Linux/etc, which always operate in daemon mode.

Then, when ForceDaemon becomes true, we now write use the StateStore
to track which user started it in server mode, and store their prefs
under that key.

The ipnserver validates the connections/identities and informs that
LocalBackend which userid is currently in charge.

The GUI can then enable/disable server mode at runtime, without using
the CLI.

But the "tailscale up" CLI was also fixed, so Windows users can use
authkeys or ACL tags, etc.

Updates #275
2020-10-12 14:28:21 -07:00
Josh Bleecher Snyder
d027cd81df tailcfg: restore Roles field to UserProfile 2020-10-09 15:56:39 -07:00
Brad Fitzpatrick
638127530b ipn/ipnserver: prevent use by multiple Windows users, add HTML status page
It was previously possible for two different Windows users to connect
to the IPN server at once, but it didn't really work. They mostly
stepped on each other's toes and caused chaos.

Now only one can control it, but it can be active for everybody else.

Necessary dependency step for Windows server/headless mode (#275)

While here, finish wiring up the HTTP status page on Windows, now that
all the dependent pieces are available.
2020-10-09 12:20:47 -07:00
Josh Bleecher Snyder
400e89367c tailcfg: restore Role field to MapResponse
Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2020-10-09 12:07:32 -07:00
Brad Fitzpatrick
22c462bd91 wgengine/monitor: fix copy/paste-o to actually monitor route changes
Due to a copy/paste-o, we were monitoring address changes twice, and
not monitoring route changes at all.

Verified with 'tailscale debug --monitor' that this actually works now (while
running 'route add 10.3.0.0 mask 255.255.0.0 10.0.0.1' and 'route delete (same)'
back and forth in cmd.exe)

In practice route changes are accompanied by address changes and this
doesn't fix any known issues. I just noticed this while reading this
code again. But at least the code does what it was trying to do now.
2020-10-09 09:04:26 -07:00
Brad Fitzpatrick
63d65368db go.mod: bump wireguard-go for x/sys/unix symbol loss
Updates golang/go#41868
2020-10-08 09:47:58 -07:00
Avery Pennarun
6332bc5e08 controlclient: print http errors if result code != 200.
Turns out for the particular error I was chasing, it actually returns
200 and zero data. But this code mirrors the same check in the map
poll, and is the right thing to do in the name of future debugging.

Signed-off-by: Avery Pennarun <apenwarr@tailscale.com>
2020-10-08 01:01:47 -04:00
Avery Pennarun
0e5f2b90a5 echoRespondToAll: filter.Accept rather than filter.Drop on a match.
This function is only called in fake mode, which won't do anything more
with the packet after we respond to it anyway, so dropping it in the
prefilter is not necessary. And it's kinda semantically wrong: we did
not reject it, so telling the upper layer that it was rejected produces
an ugly error message.

Signed-off-by: Avery Pennarun <apenwarr@tailscale.com>
2020-10-08 01:01:43 -04:00
Avery Pennarun
5041800ac6 wgengine/tstun/faketun: it's a null tunnel, not a loopback.
At some point faketun got implemented as a loopback (put a packet in
from wireguard, the same packet goes back to wireguard) which is not
useful. It's supposed to be an interface that just sinks all packets,
and then wgengine adds *only* and ICMP Echo responder as a layer on
top.

This caused extremely odd bugs on darwin, where the special case that
reinjects packets from local->local was filling the loopback channel
and creating an infinite loop (which became jammed since the reader and
writer were in the same goroutine).

Signed-off-by: Avery Pennarun <apenwarr@tailscale.com>
2020-10-08 01:01:39 -04:00
Brad Fitzpatrick
3e4c46259d wgengine/magicsock: don't do netchecks either when network is down
A continuation of 6ee219a25d

Updates #640
2020-10-06 20:24:10 -07:00
Brad Fitzpatrick
6ee219a25d ipn, wgengine, magicsock, tsdns: be quieter and less aggressive when offline
If no interfaces are up, calm down and stop spamming so much. It was
noticed as especially bad on Windows, but probably was bad
everywhere. I just have the best network conditions testing on a
Windows VM.

Updates #604
2020-10-06 15:26:53 -07:00
David Crawshaw
7616acd118 tailcfg: add Clone method for RegisterResponse
Signed-off-by: David Crawshaw <crawshaw@tailscale.com>
2020-10-06 14:06:11 -04:00
David Crawshaw
15297a3a09 control/controlclient: some extra debug info in errors
Signed-off-by: David Crawshaw <crawshaw@tailscale.com>
2020-10-06 14:06:11 -04:00
Brad Fitzpatrick
587bdc4280 ipn, wgengine: disable subnet routes if network has PAC configuration
Not configurable yet.

Updates tailscale/corp#653

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2020-10-05 21:04:23 -07:00
Josh Bleecher Snyder
a5103a4cae all: upgrade to latest version of depaware 2020-10-02 20:35:13 -07:00
Josh Bleecher Snyder
585a0d8997 all: use testing.T.TempDir
Bit of Friday cleanup.

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2020-10-02 20:31:31 -07:00
Brad Fitzpatrick
ed5d5f920f net/interfaces: add interfaces.State.String method 2020-10-02 12:15:05 -07:00
Josh Bleecher Snyder
9784cae23b util/uniq: add new package
This makes it easy to compact slices that contain duplicate elements
by sorting and then uniqing.

This is an alternative to constructing an intermediate map
and then extracting elements from it. It also provides
more control over equality than using a map key does.

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2020-10-02 11:00:56 -07:00
Brad Fitzpatrick
12e28aa87d ipn: on transition from no PAC to PAC, reset state
So previous routes aren't shadowing resources that the operating
system might need (Windows Domain Controller, DNS server, corp HTTP
proxy, WinHTTP fetching the PAC file itself, etc).

This effectively detects when we're transitioning from, say, public
wifi to corp wifi and makes Tailscale remove all its routes and stops
its TCP connections and tries connecting to everything anew.

Updates tailscale/corp#653
2020-10-01 22:03:25 -07:00
Brad Fitzpatrick
cab3eb995f net/interfaces: quiet PAC detection logging in no-PAC case, add benchmark 2020-10-01 22:02:39 -07:00
Josh Bleecher Snyder
38dda1ea9e all: update depaware.txt
Broken by 8051ecff55.


Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2020-10-01 16:35:32 -07:00
Brad Fitzpatrick
8051ecff55 net/interfaces: add State.PAC field, populate it on Windows
Not used for anything yet (except logging), but populate the current
proxy autoconfig PAC URL in Interfaces.State.

A future change will do things based on it.
2020-10-01 15:33:37 -07:00
Brad Fitzpatrick
b5a3850d29 control/controlclient, ipn: store machine key separately from user prefs/persist
Updates #610 (fixes after some win/xcode changes in a separate repo)
2020-10-01 14:30:20 -07:00
Josh Bleecher Snyder
e1596d655a tstest: skip resource check when test has failed
Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2020-10-01 11:27:22 -07:00
Josh Bleecher Snyder
ce6aca13f0 tailcfg: add yet another IsZero method
Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2020-09-30 17:55:12 -07:00
Josh Bleecher Snyder
070dfa0c3d tailcfg: add more IsZero methods
Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2020-09-30 17:47:07 -07:00
Josh Bleecher Snyder
efb08e4fee all: use IsZero methods
Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2020-09-30 17:13:41 -07:00
Brad Fitzpatrick
c8f257df00 Revert "all: keep UserProfiles a slice instead of a map for longer"
This reverts commit e5894aba42.

Breaks macOS/iOS build. Reverting per chat with Josh; he'll fix later today.
2020-09-30 08:43:31 -07:00
Brad Fitzpatrick
90b7293b3b ipn: add/move some constants, update a comment
And make the StateStore implementations be Stringers, for error messages.
2020-09-29 20:53:32 -07:00
Josh Bleecher Snyder
1fecf87363 control/controlclient: use wgcfg.PrivateKey.IsZero
Generated by eg using template:

---

package p

import "github.com/tailscale/wireguard-go/wgcfg"

func before(k wgcfg.PrivateKey) bool { return k == wgcfg.PrivateKey{} }
func after(k wgcfg.PrivateKey) bool  { return k.IsZero() }


Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2020-09-29 17:50:40 -07:00
Josh Bleecher Snyder
2b8d2babfa tailcfg: add IsZero methods to UserID and NodeID
These will be helpful for doing some automated refactoring.

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2020-09-29 17:38:56 -07:00
Josh Bleecher Snyder
e5894aba42 all: keep UserProfiles a slice instead of a map for longer
Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2020-09-29 11:36:35 -07:00
Josh Bleecher Snyder
4d4ca2e496 control/controlclient: remove Roles fields from client
They are unused.

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2020-09-29 11:36:35 -07:00
David Anderson
c493e5804f wgengine/router: make v6-ness configurable in test, for consistent results.
Signed-off-by: David Anderson <danderson@tailscale.com>
2020-09-28 23:47:05 +00:00
Josh Bleecher Snyder
d3701417fc tailcfg: fix typo in comment
Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2020-09-28 14:44:34 -07:00
Brad Fitzpatrick
c86761cfd1 Remove tuntap references. We only use TUN. 2020-09-25 13:13:13 -07:00
Brad Fitzpatrick
8b94a769be cmd/tailscaled: use the standard flag page instead of getopt
Per discussion with @crawshaw. The CLI tool already used std flag anyway.
If either of them, it would've made more sense for the CLI to use getopt.
2020-09-25 13:12:10 -07:00
Brad Fitzpatrick
94a68a113b go.sum: tidy 2020-09-25 12:44:46 -07:00
Brad Fitzpatrick
01098f41d0 wgengine/tstun: fix typo in comment 2020-09-25 12:24:44 -07:00
Brad Fitzpatrick
73cc2d8f89 wgengine/filter: also silently drop link-local unicast traffic
Updates #629
2020-09-25 11:47:38 -07:00
Brad Fitzpatrick
5f807c389e wgengine/filter: drop multicast packets out, don't log about them
Eventually we'll probably support multicast. For now it's just log spam.

Fixes #629
2020-09-25 11:27:57 -07:00
Brad Fitzpatrick
bbb56f2303 wgengine/router: fix tests on Debian Buster as regular user on machine with IPv6 2020-09-25 11:27:57 -07:00
David Anderson
fddbcb0c7b wgengine/router: support various degrees of broken IPv6.
Gracefully skips touching the v6 NAT table on systems that don't have
it, and doesn't configure IPv6 at all if IPv6 is globally disabled.

Signed-off-by: David Anderson <danderson@tailscale.com>
2020-09-24 18:37:00 -07:00
David Anderson
0d80904fc2 wgengine/router: set up basic IPv6 routing/firewalling.
Part of #19.

Signed-off-by: David Anderson <danderson@tailscale.com>
2020-09-24 18:37:00 -07:00
Josh Bleecher Snyder
f0ef561049 wgengine/tsdns: use netns to obtain a socket
Fixes #789

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2020-09-24 15:48:40 -07:00
Josh Bleecher Snyder
6e8328cba5 wgengine/tsdns: replace connections when net link changes (macOS)
When the network link changes, existing UDP sockets fail immediately
and permanently on macOS.

The forwarder set up a single UDP conn and never changed it.
As a result, any time there was a network link change,
all forwarded DNS queries failed.

To fix this, create a new connection when send requests
fail because of network unreachability.

This change is darwin-only, although extended it to other platforms
should be straightforward.

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2020-09-24 15:31:27 -07:00
Josh Bleecher Snyder
1fd10061fd wgengine/tsdns: delegate bonjour service rdns requests
While we're here, parseQuery into a plain function.
This is helpful for fuzzing. (Which I did a bit of. Didn't find anything.)

And clean up a few minor things.

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2020-09-24 12:26:12 -07:00
Brad Fitzpatrick
2d0ed99672 wgengine, wgengine/router: add a bunch of (temporary?) engine creation logging
Trying to debug what's slow on a user's machine.

Updates #785
2020-09-23 15:27:30 -07:00
Brad Fitzpatrick
7c11f71ac5 wgengine/router: ignore errors deleting 169.254.255.255/32 route on Windows
Updates #785
2020-09-23 14:01:00 -07:00
David Anderson
b7e0ff598a wgengine: don't close tundev in NewUserspaceEngine.
newUserspaceEngineAdvanced closes the tun device on error already.

Fixes #783.

Signed-off-by: David Anderson <danderson@tailscale.com>
2020-09-23 19:55:34 +00:00
Brad Fitzpatrick
a601a760ba version: add Windows MAJOR,MINOR,BUILD,REVISON value
Updates #778

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2020-09-22 20:34:57 -07:00
Brad Fitzpatrick
8893c2ee78 net/interfaces, net/netns: move default route interface code to interfaces
To populate interfaces.State.DefaultRouteInterface.

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2020-09-22 19:02:17 -07:00
Brad Fitzpatrick
fda9dc8815 net/netns: document Windows socket binding a bit more
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2020-09-22 13:05:26 -07:00
Brad Fitzpatrick
5d8b88be88 control/controlclient, version/distro, wgengine: recognize OpenWrt
And help out with missing packages.

Thanks to @willangley for tips.

Updates #724
2020-09-22 10:28:40 -07:00
Brad Fitzpatrick
ec95e901e6 go.sum: update 2020-09-22 10:27:21 -07:00
Brad Fitzpatrick
3528d28ed1 wgengine/router: move Tailscale's winipcfg additions into wgengine/router
Part of unforking our winipcfg-go and using upstream (#760), move our
additions into our repo. (We might upstream them later if upstream has
interest)

Originally these were:

@apenwarr: "Add ifc.SyncAddresses() and SyncRoutes()."
609dcf2df5

@bradfitz: "winipcfg: make Interface.AddRoutes do as much as possible, return combined error"
e9f93d53f3

@bradfitz: "prevent unnecessary Interface.SyncAddresses work; normalize IPNets in deltaNets"
decb9ee8e1
2020-09-22 09:24:10 -07:00
Brad Fitzpatrick
56a787fff8 go.mod, go.sum: bump wireguard-go 2020-09-21 15:22:56 -07:00
Brad Fitzpatrick
fb03c60c9e version: bump date 2020-09-21 15:21:05 -07:00
Brad Fitzpatrick
963b927d5b net/tshttpproxy: appease staticcheck 2020-09-21 15:01:30 -07:00
Brad Fitzpatrick
fd77268770 wgengine/router: enumerate all interfaces when finding Tailscale adapter by GUID
Might fix it. I've spent too much time failing to reproduce the issue. This doesn't
seem to make it worse, though (it still runs for me), so I'll include this and
see if it helps others while I still work on a reliable way to reproduce it.

Updates tailscale/corp#474
2020-09-21 14:52:52 -07:00
Brad Fitzpatrick
5bcac4eaac net/tshttpproxy: add GetProxyForURL negative cache
Otherwise when PAC server is down, we log, and each log entry is a new
HTTP request (from logtail) and a new GetProxyForURL call, which again
logs, non-stop. This is also nicer to the WinHTTP service.

Then also hook up link change notifications to the cache to reset it
if there's a chance the network might work sooner.
2020-09-21 14:05:28 -07:00
Josh Bleecher Snyder
4cc0ed67f9 tailcfg: add MachineKey.IsZero
Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2020-09-21 12:19:59 -07:00
Brad Fitzpatrick
64a24e796b wgengine/tstun: fix 32-bit alignment again 2020-09-18 08:18:38 -07:00
Brad Fitzpatrick
afb2be71de wgengine: add two missing TUN close calls 2020-09-18 08:04:15 -07:00
Brad Fitzpatrick
abe095f036 wgengine/tstun: make Close safe for concurrent use 2020-09-18 08:03:10 -07:00
Brad Fitzpatrick
3bdcfa7193 ipn: remove DisableDERP pref
We depend on DERP for NAT traversal now[0] so disabling it entirely can't
work.

What we'll do instead in the future is let people specify
alternate/additional DERP servers. And perhaps in the future we could
also add a pref for nodes to say when they expect to never need/want
to use DERP for data (but allow it for NAT traversal communication).

But this isn't the right pref and it doesn't work, so delete it.

Fixes #318

[0] https://tailscale.com/blog/how-nat-traversal-works/
2020-09-18 07:44:01 -07:00
Christina Wen
f0e9dcdc0a wgengine/router: restore /etc/resolv.conf after tailscale down is called
This change is to restore /etc/resolv.conf after tailscale down is called. This is done by setting the dns.Manager before errors occur. Error collection is also added.

Fixes #723
2020-09-17 16:40:22 -04:00
Brad Fitzpatrick
904a91038a tailcfg: add MapRequest.ReadOnly and OmitPeers; remove DebugForceDisco
DebugForceDisco was a development & safety knob during the the transition
to discovery. It's no longer needed.

Add MapRequest.ReadOnly to prevent clients needing to do two
peer-spamming MapRequest at start-up.

This only adds the field, not the use of the field. (The control server
needs to support it first.)

Updates tailscale/corp#557

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2020-09-17 12:07:49 -07:00
Brad Fitzpatrick
c41947903a ipn: don't log if legacy prefs don't exist (the normal case these days) 2020-09-17 08:00:45 -07:00
David Crawshaw
815bf017fc tsweb: when unwrapping HTTPError, record the user-facing message also in the log
There's often some useful piece of information in there not already
repeated in the internal error.

Signed-off-by: David Crawshaw <crawshaw@tailscale.com>
2020-09-17 10:12:48 -04:00
111 changed files with 4992 additions and 1594 deletions

52
.github/workflows/windows.yml vendored Normal file
View File

@@ -0,0 +1,52 @@
name: Windows
on:
push:
branches:
- main
pull_request:
branches:
- '*'
jobs:
test:
runs-on: windows-latest
if: "!contains(github.event.head_commit.message, '[ci skip]')"
steps:
- name: Install Go
uses: actions/setup-go@v2
with:
go-version: 1.15.x
- name: Checkout code
uses: actions/checkout@v2
- name: Restore Cache
uses: actions/cache@v2
with:
path: ~/go/pkg/mod
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-
- name: Test
run: go test ./...
- uses: k0kubun/action-slack@v2.0.0
with:
payload: |
{
"attachments": [{
"text": "${{ job.status }}: ${{ github.workflow }} <https://github.com/${{ github.repository }}/commit/${{ github.sha }}/checks|${{ env.COMMIT_DATE }} #${{ env.COMMIT_NUMBER_OF_DAY }}> " +
"(<https://github.com/${{ github.repository }}/commit/${{ github.sha }}|" + "${{ github.sha }}".substring(0, 10) + ">) " +
"of ${{ github.repository }}@" + "${{ github.ref }}".split('/').reverse()[0] + " by ${{ github.event.head_commit.committer.name }}",
"color": "danger"
}]
}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
if: failure() && github.event_name == 'push'

1
.gitignore vendored
View File

@@ -1,5 +1,6 @@
# Binaries for programs and plugins
*~
*.tmp
*.exe
*.dll
*.so

View File

@@ -30,6 +30,18 @@ wrappers that are not open source.
go install tailscale.com/cmd/tailscale{,d}
```
If you're packaging Tailscale for distribution, use `build_dist.sh`
instead, to burn commit IDs and version info into the binaries:
```
./build_dist.sh tailscale.com/cmd/tailscale
./build_dist.sh tailscale.com/cmd/tailscaled
```
If your distro has conventions that preclude the use of
`build_dist.sh`, please do the equivalent of what it does in your
distro's way, so that bug reports contain useful version information.
We only guarantee to support the latest Go release and any Go beta or
release candidate builds (currently Go 1.15) in module mode. It might
work in earlier Go versions or in GOPATH mode, but we're making no

1
VERSION.txt Normal file
View File

@@ -0,0 +1 @@
1.2.9

16
build_dist.sh Executable file
View File

@@ -0,0 +1,16 @@
#!/usr/bin/env sh
#
# Runs `go build` with flags configured for binary distribution. All
# it does differently from `go build` is burn git commit and version
# information into the binaries, so that we can track down user
# issues.
#
# If you're packaging Tailscale for a distro, please consider using
# this script, or executing equivalent commands in your
# distro-specific build system.
set -eu
eval $(./version/version.sh)
exec go build -tags xversion -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

@@ -33,6 +33,7 @@ var (
flagTypes = flag.String("type", "", "comma-separated list of types; required")
flagOutput = flag.String("output", "", "output file; required")
flagBuildTags = flag.String("tags", "", "compiler build tags to apply")
flagCloneFunc = flag.Bool("clonefunc", false, "add a top-level Clone func")
)
func main() {
@@ -98,25 +99,27 @@ func main() {
w := func(format string, args ...interface{}) {
fmt.Fprintf(buf, format+"\n", args...)
}
w("// Clone duplicates src into dst and reports whether it succeeded.")
w("// To succeed, <src, dst> must be of types <*T, *T> or <*T, **T>,")
w("// where T is one of %s.", *flagTypes)
w("func Clone(dst, src interface{}) bool {")
w(" switch src := src.(type) {")
for _, typeName := range typeNames {
w(" case *%s:", typeName)
w(" switch dst := dst.(type) {")
w(" case *%s:", typeName)
w(" *dst = *src.Clone()")
w(" return true")
w(" case **%s:", typeName)
w(" *dst = src.Clone()")
w(" return true")
w(" }")
if *flagCloneFunc {
w("// Clone duplicates src into dst and reports whether it succeeded.")
w("// To succeed, <src, dst> must be of types <*T, *T> or <*T, **T>,")
w("// where T is one of %s.", *flagTypes)
w("func Clone(dst, src interface{}) bool {")
w(" switch src := src.(type) {")
for _, typeName := range typeNames {
w(" case *%s:", typeName)
w(" switch dst := dst.(type) {")
w(" case *%s:", typeName)
w(" *dst = *src.Clone()")
w(" return true")
w(" case **%s:", typeName)
w(" *dst = src.Clone()")
w(" return true")
w(" }")
}
w(" }")
w(" return false")
w("}")
}
w(" }")
w(" return false")
w("}")
contents := new(bytes.Buffer)
fmt.Fprintf(contents, header, *flagTypes, pkg.Name)

View File

@@ -63,7 +63,7 @@ func loadConfig() config {
}
b, err := ioutil.ReadFile(*configPath)
switch {
case os.IsNotExist(err):
case errors.Is(err, os.ErrNotExist):
return writeNewConfig()
case err != nil:
log.Fatal(err)
@@ -233,7 +233,7 @@ func debugHandler(s *derp.Server) http.Handler {
f("<li><b>Hostname:</b> %v</li>\n", html.EscapeString(*hostname))
f("<li><b>Uptime:</b> %v</li>\n", tsweb.Uptime())
f("<li><b>Mesh Key:</b> %v</li>\n", s.HasMeshKey())
f("<li><b>Version:</b> %v</li>\n", html.EscapeString(version.LONG))
f("<li><b>Version:</b> %v</li>\n", html.EscapeString(version.Long))
f(`<li><a href="/debug/vars">/debug/vars</a> (Go)</li>
<li><a href="/debug/varz">/debug/varz</a> (Prometheus)</li>

View File

@@ -56,7 +56,6 @@ func runDown(ctx context.Context, args []string) error {
}
return
}
log.Printf("Notify: %#v", n)
})
bc.RequestStatus()

View File

@@ -28,15 +28,6 @@ import (
"tailscale.com/wgengine/router"
)
// globalStateKey is the ipn.StateKey that tailscaled loads on
// startup.
//
// We have to support multiple state keys for other OSes (Windows in
// particular), but right now Unix daemons run with a single
// node-global state. To keep open the option of having per-user state
// later, the global state key doesn't look like a username.
const globalStateKey = "_daemon"
var upCmd = &ffcli.Command{
Name: "up",
ShortUsage: "up [flags]",
@@ -60,7 +51,6 @@ specify any flags, options are reset to their default.
upf.StringVar(&upArgs.advertiseTags, "advertise-tags", "", "ACL tags to request (comma-separated, e.g. eng,montreal,ssh)")
upf.StringVar(&upArgs.authKey, "authkey", "", "node authorization key")
upf.StringVar(&upArgs.hostname, "hostname", "", "hostname to use instead of the one provided by the OS")
upf.BoolVar(&upArgs.enableDERP, "enable-derp", true, "enable the use of DERP servers")
if runtime.GOOS == "linux" || isBSD(runtime.GOOS) || version.OS() == "macOS" {
upf.StringVar(&upArgs.advertiseRoutes, "advertise-routes", "", "routes to advertise to other nodes (comma-separated, e.g. 10.0.0.0/8,192.168.0.0/24)")
}
@@ -89,7 +79,6 @@ var upArgs struct {
forceReauth bool
advertiseRoutes string
advertiseTags string
enableDERP bool
snat bool
netfilterMode string
authKey string
@@ -216,8 +205,9 @@ func runUp(ctx context.Context, args []string) error {
prefs.AdvertiseRoutes = routes
prefs.AdvertiseTags = tags
prefs.NoSNAT = !upArgs.snat
prefs.DisableDERP = !upArgs.enableDERP
prefs.Hostname = upArgs.hostname
prefs.ForceDaemon = (runtime.GOOS == "windows")
if runtime.GOOS == "linux" {
switch upArgs.netfilterMode {
case "on":
@@ -241,8 +231,9 @@ func runUp(ctx context.Context, args []string) error {
startLoginInteractive := func() { loginOnce.Do(func() { bc.StartLoginInteractive() }) }
bc.SetPrefs(prefs)
opts := ipn.Options{
StateKey: globalStateKey,
StateKey: ipn.GlobalDaemonStateKey,
AuthKey: upArgs.authKey,
Notify: func(n ipn.Notify) {
if n.ErrMessage != nil {
@@ -270,6 +261,22 @@ func runUp(ctx context.Context, args []string) error {
}
},
}
// On Windows, we still run in mostly the "legacy" way that
// predated the server's StateStore. That is, we send an empty
// StateKey and send the prefs directly. Although the Windows
// supports server mode, though, the transition to StateStore
// is only half complete. Only server mode uses it, and the
// Windows service (~tailscaled) is the one that computes the
// StateKey based on the connection idenity. So for now, just
// do as the Windows GUI's always done:
if runtime.GOOS == "windows" {
// The Windows service will set this as needed based
// on our connection's identity.
opts.StateKey = ""
opts.Prefs = prefs
}
// We still have to Start right now because it's the only way to
// set up notifications and whatnot. This causes a bunch of churn
// every time the CLI touches anything.

View File

@@ -36,10 +36,11 @@ func runVersion(ctx context.Context, args []string) error {
log.Fatalf("too many non-flag arguments: %q", args)
}
if !versionArgs.daemon {
fmt.Println(version.LONG)
fmt.Println(version.String())
return nil
}
fmt.Printf("Client: %s\n", version.LONG)
fmt.Printf("Client: %s\n", version.String())
c, bc, ctx, cancel := connect(ctx)
defer cancel()

View File

@@ -5,6 +5,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
github.com/apenwarr/fixconsole from tailscale.com/cmd/tailscale
W 💣 github.com/apenwarr/w32 from github.com/apenwarr/fixconsole
L github.com/coreos/go-iptables/iptables from tailscale.com/wgengine/router
LW github.com/go-multierror/multierror from tailscale.com/wgengine/router
W 💣 github.com/go-ole/go-ole from github.com/go-ole/go-ole/oleutil+
W 💣 github.com/go-ole/go-ole/oleutil from tailscale.com/wgengine/winnet
L 💣 github.com/godbus/dbus/v5 from tailscale.com/wgengine/router/dns
@@ -15,7 +16,6 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
L 💣 github.com/mdlayher/netlink/nlenc from github.com/jsimonetti/rtnetlink+
github.com/peterbourgon/ff/v2 from github.com/peterbourgon/ff/v2/ffcli
github.com/peterbourgon/ff/v2/ffcli from tailscale.com/cmd/tailscale/cli
W 💣 github.com/tailscale/winipcfg-go from tailscale.com/net/interfaces+
💣 github.com/tailscale/wireguard-go/conn from github.com/tailscale/wireguard-go/device+
💣 github.com/tailscale/wireguard-go/device from tailscale.com/wgengine+
github.com/tailscale/wireguard-go/device/tokenbucket from github.com/tailscale/wireguard-go/device
@@ -36,6 +36,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
github.com/tcnksm/go-httpstat from tailscale.com/net/netcheck
github.com/toqueteos/webbrowser from tailscale.com/cmd/tailscale/cli
💣 go4.org/mem from tailscale.com/control/controlclient+
W 💣 golang.zx2c4.com/wireguard/windows/tunnel/winipcfg from tailscale.com/net/interfaces+
inet.af/netaddr from tailscale.com/cmd/tailscale/cli+
rsc.io/goversion/version from tailscale.com/version
tailscale.com/atomicfile from tailscale.com/ipn+
@@ -50,12 +51,12 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
tailscale.com/ipn/ipnstate from tailscale.com/cmd/tailscale/cli+
tailscale.com/ipn/policy from tailscale.com/ipn
tailscale.com/log/logheap from tailscale.com/control/controlclient
tailscale.com/logtail/backoff from tailscale.com/control/controlclient
tailscale.com/logtail/backoff from tailscale.com/control/controlclient+
tailscale.com/metrics from tailscale.com/derp
tailscale.com/net/dnscache from tailscale.com/cmd/tailscale/cli+
tailscale.com/net/interfaces from tailscale.com/cmd/tailscale/cli+
💣 tailscale.com/net/interfaces from tailscale.com/cmd/tailscale/cli+
tailscale.com/net/netcheck from tailscale.com/cmd/tailscale/cli+
💣 tailscale.com/net/netns from tailscale.com/control/controlclient+
tailscale.com/net/netns from tailscale.com/control/controlclient+
tailscale.com/net/stun from tailscale.com/net/netcheck+
tailscale.com/net/tlsdial from tailscale.com/control/controlclient+
tailscale.com/net/tsaddr from tailscale.com/ipn+
@@ -74,6 +75,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
tailscale.com/types/opt from tailscale.com/control/controlclient+
tailscale.com/types/strbuilder from tailscale.com/wgengine/packet
tailscale.com/types/structs from tailscale.com/control/controlclient+
W tailscale.com/util/endian from tailscale.com/net/netns
tailscale.com/util/lineread from tailscale.com/control/controlclient+
tailscale.com/version from tailscale.com/cmd/tailscale/cli+
tailscale.com/version/distro from tailscale.com/cmd/tailscale/cli+
@@ -90,18 +92,26 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
golang.org/x/crypto/blake2b from golang.org/x/crypto/nacl/box
golang.org/x/crypto/blake2s from github.com/tailscale/wireguard-go/device+
golang.org/x/crypto/chacha20 from golang.org/x/crypto/chacha20poly1305
golang.org/x/crypto/chacha20poly1305 from github.com/tailscale/wireguard-go/device+
golang.org/x/crypto/curve25519 from github.com/tailscale/wireguard-go/wgcfg+
golang.org/x/crypto/chacha20poly1305 from crypto/tls+
golang.org/x/crypto/cryptobyte from crypto/ecdsa+
golang.org/x/crypto/cryptobyte/asn1 from crypto/ecdsa+
golang.org/x/crypto/curve25519 from crypto/tls+
golang.org/x/crypto/hkdf from crypto/tls
golang.org/x/crypto/nacl/box from tailscale.com/control/controlclient+
golang.org/x/crypto/nacl/secretbox from golang.org/x/crypto/nacl/box
golang.org/x/crypto/poly1305 from github.com/tailscale/wireguard-go/device+
golang.org/x/crypto/salsa20/salsa from golang.org/x/crypto/nacl/box+
golang.org/x/net/bpf from github.com/mdlayher/netlink+
golang.org/x/net/context/ctxhttp from golang.org/x/oauth2/internal
golang.org/x/net/dns/dnsmessage from tailscale.com/wgengine/tsdns
golang.org/x/net/dns/dnsmessage from net+
golang.org/x/net/http/httpguts from net/http
golang.org/x/net/http/httpproxy from net/http
golang.org/x/net/http2/hpack from net/http
golang.org/x/net/idna from golang.org/x/net/http/httpguts+
golang.org/x/net/ipv4 from github.com/tailscale/wireguard-go/device
golang.org/x/net/ipv6 from github.com/tailscale/wireguard-go/device+
golang.org/x/net/proxy from tailscale.com/net/netns
D golang.org/x/net/route from net
golang.org/x/oauth2 from tailscale.com/control/controlclient+
golang.org/x/oauth2/internal from golang.org/x/oauth2
golang.org/x/sync/errgroup from tailscale.com/derp
@@ -110,27 +120,11 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
LD golang.org/x/sys/unix from github.com/jsimonetti/rtnetlink/internal/unix+
W golang.org/x/sys/windows from github.com/apenwarr/fixconsole+
W golang.org/x/sys/windows/registry from github.com/tailscale/wireguard-go/tun/wintun+
W golang.org/x/text/transform from golang.org/x/text/unicode/norm
W golang.org/x/text/unicode/norm from github.com/tailscale/wireguard-go/tun/wintun
golang.org/x/text/secure/bidirule from golang.org/x/net/idna
golang.org/x/text/transform from golang.org/x/text/secure/bidirule+
golang.org/x/text/unicode/bidi from golang.org/x/net/idna+
golang.org/x/text/unicode/norm from github.com/tailscale/wireguard-go/tun/wintun+
golang.org/x/time/rate from tailscale.com/types/logger+
vendor/golang.org/x/crypto/chacha20 from vendor/golang.org/x/crypto/chacha20poly1305
vendor/golang.org/x/crypto/chacha20poly1305 from crypto/tls
vendor/golang.org/x/crypto/cryptobyte from crypto/ecdsa+
vendor/golang.org/x/crypto/cryptobyte/asn1 from crypto/ecdsa+
vendor/golang.org/x/crypto/curve25519 from crypto/tls
vendor/golang.org/x/crypto/hkdf from crypto/tls
vendor/golang.org/x/crypto/poly1305 from vendor/golang.org/x/crypto/chacha20poly1305
vendor/golang.org/x/net/dns/dnsmessage from net
vendor/golang.org/x/net/http/httpguts from net/http
vendor/golang.org/x/net/http/httpproxy from net/http
vendor/golang.org/x/net/http2/hpack from net/http
vendor/golang.org/x/net/idna from net/http+
D vendor/golang.org/x/net/route from net
vendor/golang.org/x/sys/cpu from vendor/golang.org/x/crypto/chacha20poly1305
vendor/golang.org/x/text/secure/bidirule from vendor/golang.org/x/net/idna
vendor/golang.org/x/text/transform from vendor/golang.org/x/text/secure/bidirule+
vendor/golang.org/x/text/unicode/bidi from vendor/golang.org/x/net/idna+
vendor/golang.org/x/text/unicode/norm from vendor/golang.org/x/net/idna
bufio from compress/flate+
bytes from bufio+
compress/flate from compress/gzip+
@@ -192,7 +186,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
net/http from expvar+
net/http/httptrace from github.com/tcnksm/go-httpstat+
net/http/internal from net/http
net/textproto from mime/multipart+
net/textproto from golang.org/x/net/http/httpguts+
net/url from crypto/x509+
os from crypto/rand+
os/exec from github.com/coreos/go-iptables/iptables+
@@ -203,7 +197,6 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
reflect from crypto/x509+
regexp from github.com/coreos/go-iptables/iptables+
regexp/syntax from regexp
LD runtime/cgo
runtime/pprof from tailscale.com/log/logheap+
sort from compress/flate+
strconv from compress/flate+
@@ -216,4 +209,3 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
unicode from bytes+
unicode/utf16 from encoding/asn1+
unicode/utf8 from bufio+
unsafe from crypto/internal/subtle+

View File

@@ -5,6 +5,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
github.com/apenwarr/fixconsole from tailscale.com/cmd/tailscaled
W 💣 github.com/apenwarr/w32 from github.com/apenwarr/fixconsole
L github.com/coreos/go-iptables/iptables from tailscale.com/wgengine/router
LW github.com/go-multierror/multierror from tailscale.com/wgengine/router
W 💣 github.com/go-ole/go-ole from github.com/go-ole/go-ole/oleutil+
W 💣 github.com/go-ole/go-ole/oleutil from tailscale.com/wgengine/winnet
L 💣 github.com/godbus/dbus/v5 from tailscale.com/wgengine/router/dns
@@ -18,8 +19,6 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
github.com/klauspost/compress/zstd/internal/xxhash from github.com/klauspost/compress/zstd
L 💣 github.com/mdlayher/netlink from github.com/jsimonetti/rtnetlink+
L 💣 github.com/mdlayher/netlink/nlenc from github.com/jsimonetti/rtnetlink+
github.com/pborman/getopt/v2 from tailscale.com/cmd/tailscaled
W 💣 github.com/tailscale/winipcfg-go from tailscale.com/net/interfaces+
💣 github.com/tailscale/wireguard-go/conn from github.com/tailscale/wireguard-go/device+
💣 github.com/tailscale/wireguard-go/device from tailscale.com/wgengine+
github.com/tailscale/wireguard-go/device/tokenbucket from github.com/tailscale/wireguard-go/device
@@ -39,6 +38,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
github.com/tailscale/wireguard-go/wgcfg from github.com/tailscale/wireguard-go/conn+
github.com/tcnksm/go-httpstat from tailscale.com/net/netcheck
💣 go4.org/mem from tailscale.com/control/controlclient+
W 💣 golang.zx2c4.com/wireguard/windows/tunnel/winipcfg from tailscale.com/net/interfaces+
inet.af/netaddr from tailscale.com/control/controlclient+
rsc.io/goversion/version from tailscale.com/version
tailscale.com/atomicfile from tailscale.com/ipn+
@@ -51,6 +51,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
tailscale.com/ipn/ipnserver from tailscale.com/cmd/tailscaled
tailscale.com/ipn/ipnstate from tailscale.com/ipn+
tailscale.com/ipn/policy from tailscale.com/ipn
tailscale.com/log/filelogger from tailscale.com/ipn/ipnserver
tailscale.com/log/logheap from tailscale.com/control/controlclient
tailscale.com/logpolicy from tailscale.com/cmd/tailscaled
tailscale.com/logtail from tailscale.com/logpolicy
@@ -58,9 +59,9 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
tailscale.com/logtail/filch from tailscale.com/logpolicy
tailscale.com/metrics from tailscale.com/derp
tailscale.com/net/dnscache from tailscale.com/derp/derphttp+
tailscale.com/net/interfaces from tailscale.com/ipn+
💣 tailscale.com/net/interfaces from tailscale.com/ipn+
tailscale.com/net/netcheck from tailscale.com/wgengine/magicsock
💣 tailscale.com/net/netns from tailscale.com/control/controlclient+
tailscale.com/net/netns from tailscale.com/control/controlclient+
💣 tailscale.com/net/netstat from tailscale.com/ipn/ipnserver
tailscale.com/net/stun from tailscale.com/net/netcheck+
tailscale.com/net/tlsdial from tailscale.com/control/controlclient+
@@ -75,15 +76,17 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
DW tailscale.com/tempfork/osexec from tailscale.com/portlist
W tailscale.com/tsconst from tailscale.com/net/interfaces
tailscale.com/types/empty from tailscale.com/control/controlclient+
tailscale.com/types/flagtype from tailscale.com/cmd/tailscaled
tailscale.com/types/key from tailscale.com/derp+
tailscale.com/types/logger from tailscale.com/cmd/tailscaled+
tailscale.com/types/nettype from tailscale.com/wgengine/magicsock
tailscale.com/types/opt from tailscale.com/control/controlclient+
tailscale.com/types/strbuilder from tailscale.com/wgengine/packet
tailscale.com/types/structs from tailscale.com/control/controlclient+
W tailscale.com/util/endian from tailscale.com/net/netns+
tailscale.com/util/lineread from tailscale.com/control/controlclient+
tailscale.com/util/pidowner from tailscale.com/ipn/ipnserver
tailscale.com/version from tailscale.com/control/controlclient+
tailscale.com/version from tailscale.com/cmd/tailscaled+
tailscale.com/version/distro from tailscale.com/control/controlclient+
tailscale.com/wgengine from tailscale.com/cmd/tailscaled+
tailscale.com/wgengine/filter from tailscale.com/control/controlclient+
@@ -98,8 +101,11 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
golang.org/x/crypto/blake2b from golang.org/x/crypto/nacl/box
golang.org/x/crypto/blake2s from github.com/tailscale/wireguard-go/device+
golang.org/x/crypto/chacha20 from golang.org/x/crypto/chacha20poly1305
golang.org/x/crypto/chacha20poly1305 from github.com/tailscale/wireguard-go/device+
golang.org/x/crypto/curve25519 from github.com/tailscale/wireguard-go/wgcfg+
golang.org/x/crypto/chacha20poly1305 from crypto/tls+
golang.org/x/crypto/cryptobyte from crypto/ecdsa+
golang.org/x/crypto/cryptobyte/asn1 from crypto/ecdsa+
golang.org/x/crypto/curve25519 from crypto/tls+
golang.org/x/crypto/hkdf from crypto/tls
golang.org/x/crypto/nacl/box from tailscale.com/control/controlclient+
golang.org/x/crypto/nacl/secretbox from golang.org/x/crypto/nacl/box
golang.org/x/crypto/poly1305 from github.com/tailscale/wireguard-go/device+
@@ -107,10 +113,15 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
golang.org/x/crypto/ssh/terminal from tailscale.com/logpolicy
golang.org/x/net/bpf from github.com/mdlayher/netlink+
golang.org/x/net/context/ctxhttp from golang.org/x/oauth2/internal
golang.org/x/net/dns/dnsmessage from tailscale.com/wgengine/tsdns
golang.org/x/net/dns/dnsmessage from net+
golang.org/x/net/http/httpguts from net/http
golang.org/x/net/http/httpproxy from net/http
golang.org/x/net/http2/hpack from net/http
golang.org/x/net/idna from golang.org/x/net/http/httpguts+
golang.org/x/net/ipv4 from github.com/tailscale/wireguard-go/device
golang.org/x/net/ipv6 from github.com/tailscale/wireguard-go/device+
golang.org/x/net/proxy from tailscale.com/net/netns
D golang.org/x/net/route from net
golang.org/x/oauth2 from tailscale.com/control/controlclient+
golang.org/x/oauth2/internal from golang.org/x/oauth2
golang.org/x/sync/errgroup from tailscale.com/derp
@@ -119,27 +130,11 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
LD golang.org/x/sys/unix from github.com/jsimonetti/rtnetlink/internal/unix+
W golang.org/x/sys/windows from github.com/apenwarr/fixconsole+
W golang.org/x/sys/windows/registry from github.com/tailscale/wireguard-go/tun/wintun+
W golang.org/x/text/transform from golang.org/x/text/unicode/norm
W golang.org/x/text/unicode/norm from github.com/tailscale/wireguard-go/tun/wintun
golang.org/x/text/secure/bidirule from golang.org/x/net/idna
golang.org/x/text/transform from golang.org/x/text/secure/bidirule+
golang.org/x/text/unicode/bidi from golang.org/x/net/idna+
golang.org/x/text/unicode/norm from github.com/tailscale/wireguard-go/tun/wintun+
golang.org/x/time/rate from tailscale.com/types/logger+
vendor/golang.org/x/crypto/chacha20 from vendor/golang.org/x/crypto/chacha20poly1305
vendor/golang.org/x/crypto/chacha20poly1305 from crypto/tls
vendor/golang.org/x/crypto/cryptobyte from crypto/ecdsa+
vendor/golang.org/x/crypto/cryptobyte/asn1 from crypto/ecdsa+
vendor/golang.org/x/crypto/curve25519 from crypto/tls
vendor/golang.org/x/crypto/hkdf from crypto/tls
vendor/golang.org/x/crypto/poly1305 from vendor/golang.org/x/crypto/chacha20poly1305
vendor/golang.org/x/net/dns/dnsmessage from net
vendor/golang.org/x/net/http/httpguts from net/http
vendor/golang.org/x/net/http/httpproxy from net/http
vendor/golang.org/x/net/http2/hpack from net/http
vendor/golang.org/x/net/idna from net/http+
D vendor/golang.org/x/net/route from net
vendor/golang.org/x/sys/cpu from vendor/golang.org/x/crypto/chacha20poly1305
vendor/golang.org/x/text/secure/bidirule from vendor/golang.org/x/net/idna
vendor/golang.org/x/text/transform from vendor/golang.org/x/text/secure/bidirule+
vendor/golang.org/x/text/unicode/bidi from vendor/golang.org/x/net/idna+
vendor/golang.org/x/text/unicode/norm from vendor/golang.org/x/net/idna
bufio from compress/flate+
bytes from bufio+
compress/flate from compress/gzip+
@@ -180,7 +175,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
encoding/pem from crypto/tls+
errors from bufio+
expvar from tailscale.com/derp+
L flag from tailscale.com/net/netns
flag from tailscale.com/cmd/tailscaled+
fmt from compress/flate+
hash from compress/zlib+
hash/adler32 from compress/zlib
@@ -203,7 +198,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
net/http/httptrace from github.com/tcnksm/go-httpstat+
net/http/internal from net/http
net/http/pprof from tailscale.com/cmd/tailscaled
net/textproto from mime/multipart+
net/textproto from golang.org/x/net/http/httpguts+
net/url from crypto/x509+
os from crypto/rand+
os/exec from github.com/coreos/go-iptables/iptables+
@@ -214,7 +209,6 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
reflect from crypto/x509+
regexp from github.com/coreos/go-iptables/iptables+
regexp/syntax from regexp
LD runtime/cgo
runtime/debug from github.com/klauspost/compress/zstd+
runtime/pprof from net/http/pprof+
runtime/trace from net/http/pprof
@@ -231,4 +225,3 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
unicode from bytes+
unicode/utf16 from encoding/asn1+
unicode/utf8 from bufio+
unsafe from crypto/internal/subtle+

View File

@@ -11,6 +11,8 @@ package main // import "tailscale.com/cmd/tailscaled"
import (
"context"
"flag"
"fmt"
"log"
"net/http"
"net/http/pprof"
@@ -18,15 +20,17 @@ import (
"os/signal"
"runtime"
"runtime/debug"
"strconv"
"syscall"
"time"
"github.com/apenwarr/fixconsole"
"github.com/pborman/getopt/v2"
"tailscale.com/ipn/ipnserver"
"tailscale.com/logpolicy"
"tailscale.com/paths"
"tailscale.com/types/flagtype"
"tailscale.com/types/logger"
"tailscale.com/version"
"tailscale.com/wgengine"
"tailscale.com/wgengine/magicsock"
"tailscale.com/wgengine/router"
@@ -71,28 +75,29 @@ func main() {
debug.SetGCPercent(10)
}
// Set default values for getopt.
args.tunname = defaultTunName()
args.port = magicsock.DefaultPort
args.statepath = paths.DefaultTailscaledStateFile()
args.socketpath = paths.DefaultTailscaledSocket()
getopt.FlagLong(&args.cleanup, "cleanup", 0, "clean up system state and exit")
getopt.FlagLong(&args.fake, "fake", 0, "fake tunnel+routing instead of tuntap")
getopt.FlagLong(&args.debug, "debug", 0, "address of debug server")
getopt.FlagLong(&args.tunname, "tun", 0, "tunnel interface name")
getopt.FlagLong(&args.port, "port", 'p', "WireGuard port (0=autoselect)")
getopt.FlagLong(&args.statepath, "state", 0, "path of state file")
getopt.FlagLong(&args.socketpath, "socket", 's', "path of the service unix socket")
printVersion := false
flag.BoolVar(&args.cleanup, "cleanup", false, "clean up system state and exit")
flag.BoolVar(&args.fake, "fake", false, "use userspace fake tunnel+routing instead of kernel TUN interface")
flag.StringVar(&args.debug, "debug", "", "listen address ([ip]:port) of optional debug server")
flag.StringVar(&args.tunname, "tun", defaultTunName(), "tunnel interface name")
flag.Var(flagtype.PortValue(&args.port, magicsock.DefaultPort), "port", "UDP port to listen on for WireGuard and peer-to-peer traffic; 0 means automatically select")
flag.StringVar(&args.statepath, "state", paths.DefaultTailscaledStateFile(), "path of state file")
flag.StringVar(&args.socketpath, "socket", paths.DefaultTailscaledSocket(), "path of the service unix socket")
flag.BoolVar(&printVersion, "version", false, "print version information and exit")
err := fixconsole.FixConsoleIfNeeded()
if err != nil {
log.Fatalf("fixConsoleOutput: %v", err)
}
getopt.Parse()
if len(getopt.Args()) > 0 {
log.Fatalf("too many non-flag arguments: %#v", getopt.Args()[0])
flag.Parse()
if flag.NArg() > 0 {
log.Fatalf("tailscaled does not take non-flag arguments: %q", flag.Args())
}
if printVersion {
fmt.Println(version.String())
os.Exit(0)
}
if args.statepath == "" {
@@ -120,7 +125,10 @@ func run() error {
pol.Shutdown(ctx)
}()
logf := wgengine.RusagePrefixLog(log.Printf)
var logf logger.Logf = log.Printf
if v, _ := strconv.ParseBool(os.Getenv("TS_DEBUG_MEMORY")); v {
logf = logger.RusagePrefixLog(logf)
}
logf = logger.RateLimitedFn(logf, 5*time.Second, 5, 100)
if args.cleanup {
@@ -136,7 +144,7 @@ func run() error {
var e wgengine.Engine
if args.fake {
e, err = wgengine.NewFakeUserspaceEngine(logf, 0)
e, err = wgengine.NewFakeUserspaceEngine(logf, args.port)
} else {
e, err = wgengine.NewUserspaceEngine(logf, args.tunname, args.port)
}

View File

@@ -6,6 +6,7 @@ After=network-pre.target
[Service]
EnvironmentFile=/etc/default/tailscaled
ExecStartPre=/usr/sbin/tailscaled --cleanup
ExecStart=/usr/sbin/tailscaled --state=/var/lib/tailscale/tailscaled.state --socket=/run/tailscale/tailscaled.sock --port $PORT $FLAGS
ExecStopPost=/usr/sbin/tailscaled --cleanup

View File

@@ -288,7 +288,7 @@ func (c *Client) authRoutine() {
// don't send status updates for context errors,
// since context cancelation is always on purpose.
if ctx.Err() == nil {
c.sendStatus("authRoutine1", err, "", nil)
c.sendStatus("authRoutine-report", err, "", nil)
}
}
@@ -353,7 +353,7 @@ func (c *Client) authRoutine() {
c.synced = false
c.mu.Unlock()
c.sendStatus("authRoutine2", nil, "", nil)
c.sendStatus("authRoutine-wantout", nil, "", nil)
bo.BackOff(ctx, nil)
} else { // ie. goal.wantLoggedIn
c.mu.Lock()
@@ -394,7 +394,7 @@ func (c *Client) authRoutine() {
c.synced = false
c.mu.Unlock()
c.sendStatus("authRoutine3", err, url, nil)
c.sendStatus("authRoutine-url", err, url, nil)
bo.BackOff(ctx, err)
continue
}
@@ -406,7 +406,7 @@ func (c *Client) authRoutine() {
c.state = StateAuthenticated
c.mu.Unlock()
c.sendStatus("authRoutine4", nil, "", nil)
c.sendStatus("authRoutine-success", nil, "", nil)
c.cancelMapSafely()
bo.BackOff(ctx, nil)
}
@@ -528,7 +528,7 @@ func (c *Client) mapRoutine() {
c.logf("mapRoutine: netmap received: %s", state)
if stillAuthed {
c.sendStatus("mapRoutine2", nil, "", nm)
c.sendStatus("mapRoutine-got-netmap", nil, "", nm)
}
})

View File

@@ -13,6 +13,7 @@ import (
"encoding/binary"
"encoding/json"
"errors"
"flag"
"fmt"
"io"
"io/ioutil"
@@ -20,6 +21,7 @@ import (
"net/http"
"net/url"
"os"
"os/exec"
"reflect"
"runtime"
"sort"
@@ -45,8 +47,19 @@ import (
)
type Persist struct {
_ structs.Incomparable
PrivateMachineKey wgcfg.PrivateKey
_ structs.Incomparable
// LegacyFrontendPrivateMachineKey is here temporarily
// (starting 2020-09-28) during migration of Windows users'
// machine keys from frontend storage to the backend. On the
// first LocalBackend.Start call, the backend will initialize
// the real (backend-owned) machine key from the frontend's
// provided value (if non-zero), picking a new random one if
// needed. This field should be considered read-only from GUI
// frontends. The real value should not be written back in
// this field, lest the frontend persist it to disk.
LegacyFrontendPrivateMachineKey wgcfg.PrivateKey `json:"PrivateMachineKey"`
PrivateNodeKey wgcfg.PrivateKey
OldPrivateNodeKey wgcfg.PrivateKey // needed to request key rotation
Provider string
@@ -61,7 +74,7 @@ func (p *Persist) Equals(p2 *Persist) bool {
return false
}
return p.PrivateMachineKey.Equal(p2.PrivateMachineKey) &&
return p.LegacyFrontendPrivateMachineKey.Equal(p2.LegacyFrontendPrivateMachineKey) &&
p.PrivateNodeKey.Equal(p2.PrivateNodeKey) &&
p.OldPrivateNodeKey.Equal(p2.OldPrivateNodeKey) &&
p.Provider == p2.Provider &&
@@ -70,8 +83,8 @@ func (p *Persist) Equals(p2 *Persist) bool {
func (p *Persist) Pretty() string {
var mk, ok, nk wgcfg.Key
if !p.PrivateMachineKey.IsZero() {
mk = p.PrivateMachineKey.Public()
if !p.LegacyFrontendPrivateMachineKey.IsZero() {
mk = p.LegacyFrontendPrivateMachineKey.Public()
}
if !p.OldPrivateNodeKey.IsZero() {
ok = p.OldPrivateNodeKey.Public()
@@ -79,9 +92,14 @@ func (p *Persist) Pretty() string {
if !p.PrivateNodeKey.IsZero() {
nk = p.PrivateNodeKey.Public()
}
return fmt.Sprintf("Persist{m=%v, o=%v, n=%v u=%#v}",
mk.ShortString(), ok.ShortString(), nk.ShortString(),
p.LoginName)
ss := func(k wgcfg.Key) string {
if k.IsZero() {
return ""
}
return k.ShortString()
}
return fmt.Sprintf("Persist{lm=%v, o=%v, n=%v u=%#v}",
ss(mk), ss(ok), ss(nk), p.LoginName)
}
// Direct is the client that connects to a tailcontrol server for a node.
@@ -94,6 +112,8 @@ type Direct struct {
keepAlive bool
logf logger.Logf
discoPubKey tailcfg.DiscoKey
machinePrivKey wgcfg.PrivateKey
debugFlags []string
mu sync.Mutex // mutex guards the following fields
serverKey wgcfg.Key
@@ -102,22 +122,25 @@ type Direct struct {
tryingNewKey wgcfg.PrivateKey
expiry *time.Time
// hostinfo is mutated in-place while mu is held.
hostinfo *tailcfg.Hostinfo // always non-nil
endpoints []string
localPort uint16 // or zero to mean auto
hostinfo *tailcfg.Hostinfo // always non-nil
endpoints []string
everEndpoints bool // whether we've ever had non-empty endpoints
localPort uint16 // or zero to mean auto
}
type Options struct {
Persist Persist // initial persistent data
ServerURL string // URL of the tailcontrol server
AuthKey string // optional node auth key for auto registration
TimeNow func() time.Time // time.Now implementation used by Client
Hostinfo *tailcfg.Hostinfo // non-nil passes ownership, nil means to use default using os.Hostname, etc
DiscoPublicKey tailcfg.DiscoKey
NewDecompressor func() (Decompressor, error)
KeepAlive bool
Logf logger.Logf
HTTPTestClient *http.Client // optional HTTP client to use (for tests only)
Persist Persist // initial persistent data
MachinePrivateKey wgcfg.PrivateKey // the machine key to use
ServerURL string // URL of the tailcontrol server
AuthKey string // optional node auth key for auto registration
TimeNow func() time.Time // time.Now implementation used by Client
Hostinfo *tailcfg.Hostinfo // non-nil passes ownership, nil means to use default using os.Hostname, etc
DiscoPublicKey tailcfg.DiscoKey
NewDecompressor func() (Decompressor, error)
KeepAlive bool
Logf logger.Logf
HTTPTestClient *http.Client // optional HTTP client to use (for tests only)
DebugFlags []string // debug settings to send to control
}
type Decompressor interface {
@@ -130,6 +153,9 @@ func NewDirect(opts Options) (*Direct, error) {
if opts.ServerURL == "" {
return nil, errors.New("controlclient.New: no server URL specified")
}
if opts.MachinePrivateKey.IsZero() {
return nil, errors.New("controlclient.New: no MachinePrivateKey specified")
}
opts.ServerURL = strings.TrimRight(opts.ServerURL, "/")
serverURL, err := url.Parse(opts.ServerURL)
if err != nil {
@@ -158,6 +184,7 @@ func NewDirect(opts Options) (*Direct, error) {
c := &Direct{
httpc: httpc,
machinePrivKey: opts.MachinePrivateKey,
serverURL: opts.ServerURL,
timeNow: opts.TimeNow,
logf: opts.Logf,
@@ -166,6 +193,7 @@ func NewDirect(opts Options) (*Direct, error) {
persist: opts.Persist,
authKey: opts.AuthKey,
discoPubKey: opts.DiscoPublicKey,
debugFlags: opts.DebugFlags,
}
if opts.Hostinfo == nil {
c.SetHostinfo(NewHostinfo())
@@ -184,7 +212,7 @@ func NewHostinfo() *tailcfg.Hostinfo {
osv = osVersion()
}
return &tailcfg.Hostinfo{
IPNVersion: version.LONG,
IPNVersion: version.Long,
Hostname: hostname,
OS: version.OS(),
OSVersion: osv,
@@ -205,6 +233,8 @@ func (c *Direct) SetHostinfo(hi *tailcfg.Hostinfo) bool {
return false
}
c.hostinfo = hi.Clone()
j, _ := json.Marshal(c.hostinfo)
c.logf("HostInfo: %s", j)
return true
}
@@ -249,16 +279,14 @@ func (c *Direct) TryLogout(ctx context.Context) error {
// TODO(crawshaw): Tell the server. This node key should be
// immediately invalidated.
//if c.persist.PrivateNodeKey != (wgcfg.PrivateKey{}) {
//if !c.persist.PrivateNodeKey.IsZero() {
//}
c.persist = Persist{
PrivateMachineKey: c.persist.PrivateMachineKey,
}
c.persist = Persist{}
return nil
}
func (c *Direct) TryLogin(ctx context.Context, t *oauth2.Token, flags LoginFlags) (url string, err error) {
c.logf("direct.TryLogin(%v, %v)", t != nil, flags)
c.logf("direct.TryLogin(token=%v, flags=%v)", t != nil, flags)
return c.doLoginOrRegen(ctx, t, flags, false, "")
}
@@ -289,13 +317,8 @@ func (c *Direct) doLogin(ctx context.Context, t *oauth2.Token, flags LoginFlags,
expired := c.expiry != nil && !c.expiry.IsZero() && c.expiry.Before(c.timeNow())
c.mu.Unlock()
if persist.PrivateMachineKey == (wgcfg.PrivateKey{}) {
c.logf("Generating a new machinekey.")
mkey, err := wgcfg.NewPrivateKey()
if err != nil {
log.Fatal(err)
}
persist.PrivateMachineKey = mkey
if c.machinePrivKey.IsZero() {
return false, "", errors.New("controlclient.Direct requires a machine private key")
}
if expired {
@@ -322,7 +345,7 @@ func (c *Direct) doLogin(ctx context.Context, t *oauth2.Token, flags LoginFlags,
var oldNodeKey wgcfg.Key
if url != "" {
} else if regen || persist.PrivateNodeKey == (wgcfg.PrivateKey{}) {
} else if regen || persist.PrivateNodeKey.IsZero() {
c.logf("Generating a new nodekey.")
persist.OldPrivateNodeKey = persist.PrivateNodeKey
key, err := wgcfg.NewPrivateKey()
@@ -335,11 +358,11 @@ func (c *Direct) doLogin(ctx context.Context, t *oauth2.Token, flags LoginFlags,
// Try refreshing the current key first
tryingNewKey = persist.PrivateNodeKey
}
if persist.OldPrivateNodeKey != (wgcfg.PrivateKey{}) {
if !persist.OldPrivateNodeKey.IsZero() {
oldNodeKey = persist.OldPrivateNodeKey.Public()
}
if tryingNewKey == (wgcfg.PrivateKey{}) {
if tryingNewKey.IsZero() {
log.Fatalf("tryingNewKey is empty, give up")
}
if backendLogID == "" {
@@ -360,13 +383,13 @@ func (c *Direct) doLogin(ctx context.Context, t *oauth2.Token, flags LoginFlags,
request.Auth.Provider = persist.Provider
request.Auth.LoginName = persist.LoginName
request.Auth.AuthKey = authKey
bodyData, err := encode(request, &serverKey, &persist.PrivateMachineKey)
bodyData, err := encode(request, &serverKey, &c.machinePrivKey)
if err != nil {
return regen, url, err
}
body := bytes.NewReader(bodyData)
u := fmt.Sprintf("%s/machine/%s", c.serverURL, persist.PrivateMachineKey.Public().HexString())
u := fmt.Sprintf("%s/machine/%s", c.serverURL, c.machinePrivKey.Public().HexString())
req, err := http.NewRequest("POST", u, body)
if err != nil {
return regen, url, err
@@ -377,11 +400,20 @@ func (c *Direct) doLogin(ctx context.Context, t *oauth2.Token, flags LoginFlags,
if err != nil {
return regen, url, fmt.Errorf("register request: %v", err)
}
c.logf("RegisterReq: returned.")
if res.StatusCode != 200 {
msg, _ := ioutil.ReadAll(res.Body)
res.Body.Close()
return regen, url, fmt.Errorf("register request: http %d: %.200s",
res.StatusCode, strings.TrimSpace(string(msg)))
}
resp := tailcfg.RegisterResponse{}
if err := decode(res, &resp, &serverKey, &persist.PrivateMachineKey); err != nil {
if err := decode(res, &resp, &serverKey, &c.machinePrivKey); err != nil {
c.logf("error decoding RegisterResponse with server key %s and machine key %s: %v", serverKey, c.machinePrivKey.Public(), err)
return regen, url, fmt.Errorf("register request: %v", err)
}
// Log without PII:
c.logf("RegisterReq: got response; nodeKeyExpired=%v, machineAuthorized=%v; authURL=%v",
resp.NodeKeyExpired, resp.MachineAuthorized, resp.AuthURL != "")
if resp.NodeKeyExpired {
if regen {
@@ -457,6 +489,9 @@ func (c *Direct) newEndpoints(localPort uint16, endpoints []string) (changed boo
c.logf("client.newEndpoints(%v, %v)", localPort, endpoints)
c.localPort = localPort
c.endpoints = append(c.endpoints[:0], endpoints...)
if len(endpoints) > 0 {
c.everEndpoints = true
}
return true // changed
}
@@ -469,6 +504,13 @@ func (c *Direct) SetEndpoints(localPort uint16, endpoints []string) (changed boo
return c.newEndpoints(localPort, endpoints)
}
func inTest() bool { return flag.Lookup("test.v") != nil }
// PollNetMap makes a /map request to download the network map, calling cb with
// each new netmap.
//
// maxPolls is how many network maps to download; common values are 1
// or -1 (to keep a long-poll query open to the server).
func (c *Direct) PollNetMap(ctx context.Context, maxPolls int, cb func(*NetworkMap)) error {
c.mu.Lock()
persist := c.persist
@@ -478,6 +520,7 @@ func (c *Direct) PollNetMap(ctx context.Context, maxPolls int, cb func(*NetworkM
backendLogID := hostinfo.BackendLogID
localPort := c.localPort
ep := append([]string(nil), c.endpoints...)
everEndpoints := c.everEndpoints
c.mu.Unlock()
if backendLogID == "" {
@@ -485,7 +528,7 @@ func (c *Direct) PollNetMap(ctx context.Context, maxPolls int, cb func(*NetworkM
}
allowStream := maxPolls != 1
c.logf("PollNetMap: stream=%v :%v %v", maxPolls, localPort, ep)
c.logf("PollNetMap: stream=%v :%v ep=%v", allowStream, localPort, ep)
vlogf := logger.Discard
if Debug.NetMap {
@@ -493,36 +536,51 @@ func (c *Direct) PollNetMap(ctx context.Context, maxPolls int, cb func(*NetworkM
}
request := tailcfg.MapRequest{
Version: 4,
IncludeIPv6: true,
DeltaPeers: true,
KeepAlive: c.keepAlive,
NodeKey: tailcfg.NodeKey(persist.PrivateNodeKey.Public()),
DiscoKey: c.discoPubKey,
Endpoints: ep,
Stream: allowStream,
Hostinfo: hostinfo,
DebugForceDisco: Debug.ForceDisco,
Version: 5,
KeepAlive: c.keepAlive,
NodeKey: tailcfg.NodeKey(persist.PrivateNodeKey.Public()),
DiscoKey: c.discoPubKey,
Endpoints: ep,
Stream: allowStream,
Hostinfo: hostinfo,
DebugFlags: c.debugFlags,
}
if hostinfo != nil && ipForwardingBroken(hostinfo.RoutableIPs) {
old := request.DebugFlags
request.DebugFlags = append(old[:len(old):len(old)], "warn-ip-forwarding-off")
}
if c.newDecompressor != nil {
request.Compress = "zstd"
}
// On initial startup before we know our endpoints, set the ReadOnly flag
// to tell the control server not to distribute out our (empty) endpoints to peers.
// Presumably we'll learn our endpoints in a half second and do another post
// with useful results. The first POST just gets us the DERP map which we
// need to do the STUN queries to discover our endpoints.
// TODO(bradfitz): we skip this optimization in tests, though,
// because the e2e tests are currently hyperspecific about the
// ordering of things. The e2e tests need love.
if len(ep) == 0 && !everEndpoints && !inTest() {
request.ReadOnly = true
}
bodyData, err := encode(request, &serverKey, &persist.PrivateMachineKey)
bodyData, err := encode(request, &serverKey, &c.machinePrivKey)
if err != nil {
vlogf("netmap: encode: %v", err)
return err
}
ctx, cancel := context.WithCancel(ctx)
defer cancel()
machinePubKey := tailcfg.MachineKey(c.machinePrivKey.Public())
t0 := time.Now()
u := fmt.Sprintf("%s/machine/%s/map", serverURL, persist.PrivateMachineKey.Public().HexString())
req, err := http.NewRequest("POST", u, bytes.NewReader(bodyData))
u := fmt.Sprintf("%s/machine/%s/map", serverURL, machinePubKey.HexString())
req, err := http.NewRequestWithContext(ctx, "POST", u, bytes.NewReader(bodyData))
if err != nil {
return err
}
ctx, cancel := context.WithCancel(ctx)
defer cancel()
req = req.WithContext(ctx)
res, err := c.httpc.Do(req)
if err != nil {
@@ -533,7 +591,7 @@ func (c *Direct) PollNetMap(ctx context.Context, maxPolls int, cb func(*NetworkM
if res.StatusCode != 200 {
msg, _ := ioutil.ReadAll(res.Body)
res.Body.Close()
return fmt.Errorf("initial fetch failed %d: %s",
return fmt.Errorf("initial fetch failed %d: %.200s",
res.StatusCode, strings.TrimSpace(string(msg)))
}
defer res.Body.Close()
@@ -572,6 +630,7 @@ func (c *Direct) PollNetMap(ctx context.Context, maxPolls int, cb func(*NetworkM
}()
var lastDERPMap *tailcfg.DERPMap
var lastUserProfile = map[tailcfg.UserID]tailcfg.UserProfile{}
// If allowStream, then the server will use an HTTP long poll to
// return incremental results. There is always one response right
@@ -621,6 +680,9 @@ func (c *Direct) PollNetMap(ctx context.Context, maxPolls int, cb func(*NetworkM
undeltaPeers(&resp, previousPeers)
previousPeers = cloneNodes(resp.Peers) // defensive/lazy clone, since this escapes to who knows where
for _, up := range resp.UserProfiles {
lastUserProfile[up.ID] = up
}
if resp.DERPMap != nil {
vlogf("netmap: new map contains DERP map")
@@ -649,6 +711,7 @@ func (c *Direct) PollNetMap(ctx context.Context, maxPolls int, cb func(*NetworkM
nm := &NetworkMap{
NodeKey: tailcfg.NodeKey(persist.PrivateNodeKey.Public()),
PrivateKey: persist.PrivateNodeKey,
MachineKey: machinePubKey,
Expiry: resp.Node.KeyExpiry,
Name: resp.Node.Name,
Addresses: resp.Node.Addresses,
@@ -657,15 +720,24 @@ func (c *Direct) PollNetMap(ctx context.Context, maxPolls int, cb func(*NetworkM
User: resp.Node.User,
UserProfiles: make(map[tailcfg.UserID]tailcfg.UserProfile),
Domain: resp.Domain,
Roles: resp.Roles,
DNS: resp.DNSConfig,
Hostinfo: resp.Node.Hostinfo,
PacketFilter: c.parsePacketFilter(resp.PacketFilter),
DERPMap: lastDERPMap,
Debug: resp.Debug,
}
for _, profile := range resp.UserProfiles {
nm.UserProfiles[profile.ID] = profile
addUserProfile := func(userID tailcfg.UserID) {
if _, dup := nm.UserProfiles[userID]; dup {
// Already populated it from a previous peer.
return
}
if up, ok := lastUserProfile[userID]; ok {
nm.UserProfiles[userID] = up
}
}
addUserProfile(nm.User)
for _, peer := range resp.Peers {
addUserProfile(peer.User)
}
if resp.Node.MachineAuthorized {
nm.MachineStatus = tailcfg.MachineAuthorized
@@ -717,15 +789,16 @@ func decode(res *http.Response, v interface{}, serverKey *wgcfg.Key, mkey *wgcfg
return decodeMsg(msg, v, serverKey, mkey)
}
var dumpMapResponse, _ = strconv.ParseBool(os.Getenv("TS_DEBUG_MAPRESPONSE"))
var debugMap, _ = strconv.ParseBool(os.Getenv("TS_DEBUG_MAP"))
var jsonEscapedZero = []byte(`\u0000`)
func (c *Direct) decodeMsg(msg []byte, v interface{}) error {
c.mu.Lock()
mkey := c.persist.PrivateMachineKey
serverKey := c.serverKey
c.mu.Unlock()
decrypted, err := decryptMsg(msg, &serverKey, &mkey)
decrypted, err := decryptMsg(msg, &serverKey, &c.machinePrivKey)
if err != nil {
return err
}
@@ -743,11 +816,15 @@ func (c *Direct) decodeMsg(msg []byte, v interface{}) error {
return err
}
}
if dumpMapResponse {
if debugMap {
var buf bytes.Buffer
json.Indent(&buf, b, "", " ")
log.Printf("MapResponse: %s", buf.Bytes())
}
if bytes.Contains(b, jsonEscapedZero) {
log.Printf("[unexpected] zero byte in controlclient.Direct.decodeMsg into %T: %q", v, b)
}
if err := json.Unmarshal(b, v); err != nil {
return fmt.Errorf("response: %v", err)
}
@@ -760,6 +837,9 @@ func decodeMsg(msg []byte, v interface{}, serverKey *wgcfg.Key, mkey *wgcfg.Priv
if err != nil {
return err
}
if bytes.Contains(decrypted, jsonEscapedZero) {
log.Printf("[unexpected] zero byte in controlclient decodeMsg into %T: %q", v, decrypted)
}
if err := json.Unmarshal(decrypted, v); err != nil {
return fmt.Errorf("response: %v", err)
}
@@ -777,7 +857,7 @@ func decryptMsg(msg []byte, serverKey *wgcfg.Key, mkey *wgcfg.PrivateKey) ([]byt
pub, pri := (*[32]byte)(serverKey), (*[32]byte)(mkey)
decrypted, ok := box.Open(nil, msg, &nonce, pub, pri)
if !ok {
return nil, fmt.Errorf("cannot decrypt response")
return nil, fmt.Errorf("cannot decrypt response (len %d + nonce %d = %d)", len(msg), len(nonce), len(msg)+len(nonce))
}
return decrypted, nil
}
@@ -787,8 +867,7 @@ func encode(v interface{}, serverKey *wgcfg.Key, mkey *wgcfg.PrivateKey) ([]byte
if err != nil {
return nil, err
}
const debugMapRequests = false
if debugMapRequests {
if debugMap {
if _, ok := v.(tailcfg.MapRequest); ok {
log.Printf("MapRequest: %s", b)
}
@@ -843,25 +922,20 @@ func wgIPToNetaddr(ips []wgcfg.IP) (ret []netaddr.IP) {
var Debug = initDebug()
type debug struct {
NetMap bool
ProxyDNS bool
OnlyDisco bool
Disco bool
ForceDisco bool // ask control server to not filter out our disco key
NetMap bool
ProxyDNS bool
OnlyDisco bool
Disco bool
}
func initDebug() debug {
d := debug{
NetMap: envBool("TS_DEBUG_NETMAP"),
ProxyDNS: envBool("TS_DEBUG_PROXY_DNS"),
OnlyDisco: os.Getenv("TS_DEBUG_USE_DISCO") == "only",
ForceDisco: os.Getenv("TS_DEBUG_USE_DISCO") == "only" || envBool("TS_DEBUG_USE_DISCO"),
use := os.Getenv("TS_DEBUG_USE_DISCO")
return debug{
NetMap: envBool("TS_DEBUG_NETMAP"),
ProxyDNS: envBool("TS_DEBUG_PROXY_DNS"),
OnlyDisco: use == "only",
Disco: use == "only" || use == "" || envBool("TS_DEBUG_USE_DISCO"),
}
if d.ForceDisco || os.Getenv("TS_DEBUG_USE_DISCO") == "" {
// This is now defaults to on.
d.Disco = true
}
return d
}
func envBool(k string) bool {
@@ -996,3 +1070,34 @@ func TrimWGConfig() opt.Bool {
v, _ := controlTrimWGConfig.Load().(opt.Bool)
return v
}
// ipForwardingBroken reports whether the system's IP forwarding is disabled
// and will definitely not work for the routes provided.
//
// It should not return false positives.
func ipForwardingBroken(routes []wgcfg.CIDR) bool {
if len(routes) == 0 {
// Nothing to route, so no need to warn.
return false
}
if runtime.GOOS != "linux" {
// We only do subnet routing on Linux for now.
// It might work on darwin/macOS when building from source, so
// don't return true for other OSes. We can OS-based warnings
// already in the admin panel.
return false
}
out, err := ioutil.ReadFile("/proc/sys/net/ipv4/ip_forward")
if err != nil {
// Try another way.
out, err = exec.Command("sysctl", "-n", "net.ipv4.ip_forward").Output()
}
if err != nil {
// Oh well, we tried. This is just for debugging.
// We don't want false positives.
// TODO: maybe we want a different warning for inability to check?
return false
}
return strings.TrimSpace(string(out)) == "0"
// TODO: also check IPv6 if 'routes' contains any IPv6 routes
}

View File

@@ -26,8 +26,11 @@ func init() {
func osVersionLinux() string {
dist := distro.Get()
propFile := "/etc/os-release"
if dist == distro.Synology {
switch dist {
case distro.Synology:
propFile = "/etc.defaults/VERSION"
case distro.OpenWrt:
propFile = "/etc/openwrt_release"
}
m := map[string]string{}
@@ -36,7 +39,7 @@ func osVersionLinux() string {
if eq == -1 {
return nil
}
k, v := string(line[:eq]), strings.Trim(string(line[eq+1:]), `"`)
k, v := string(line[:eq]), strings.Trim(string(line[eq+1:]), `"'`)
m[k] = v
return nil
})
@@ -78,8 +81,11 @@ func osVersionLinux() string {
return fmt.Sprintf("%s%s", v, attr)
}
}
if dist == distro.Synology {
switch dist {
case distro.Synology:
return fmt.Sprintf("Synology %s%s", m["productversion"], attr)
case distro.OpenWrt:
return fmt.Sprintf("OpenWrt %s%s", m["DISTRIB_RELEASE"], attr)
}
return fmt.Sprintf("Other%s", attr)
}

View File

@@ -30,6 +30,7 @@ type NetworkMap struct {
Addresses []wgcfg.CIDR
LocalPort uint16 // used for debugging
MachineStatus tailcfg.MachineStatus
MachineKey tailcfg.MachineKey
Peers []*tailcfg.Node // sorted by Node.ID
DNS tailcfg.DNSConfig
Hostinfo tailcfg.Hostinfo
@@ -49,7 +50,6 @@ type NetworkMap struct {
// TODO(crawshaw): reduce UserProfiles to []tailcfg.UserProfile?
// There are lots of ways to slice this data, leave it up to users.
UserProfiles map[tailcfg.UserID]tailcfg.UserProfile
Roles []tailcfg.Role
// TODO(crawshaw): Groups []tailcfg.Group
// TODO(crawshaw): Capabilities []tailcfg.Capability
}
@@ -75,6 +75,15 @@ func (nm *NetworkMap) Concise() string {
func (nm *NetworkMap) printConciseHeader(buf *strings.Builder) {
fmt.Fprintf(buf, "netmap: self: %v auth=%v",
nm.NodeKey.ShortString(), nm.MachineStatus)
login := nm.UserProfiles[nm.User].LoginName
if login == "" {
if nm.User.IsZero() {
login = "?"
} else {
login = fmt.Sprint(nm.User)
}
}
fmt.Fprintf(buf, " u=%s", login)
if nm.LocalPort != 0 {
fmt.Fprintf(buf, " port=%v", nm.LocalPort)
}
@@ -92,6 +101,7 @@ func (a *NetworkMap) equalConciseHeader(b *NetworkMap) bool {
if a.NodeKey != b.NodeKey ||
a.MachineStatus != b.MachineStatus ||
a.LocalPort != b.LocalPort ||
a.User != b.User ||
len(a.Addresses) != len(b.Addresses) {
return false
}

View File

@@ -51,7 +51,7 @@ func TestNetworkMapConcise(t *testing.T) {
},
},
},
want: "netmap: self: [AQEBA] auth=machine-unknown []\n [AgICA] D2 : 192.168.0.100:12 192.168.0.100:12354\n [AwMDA] D4 : 10.2.0.100:12 10.1.0.100:12345\n",
want: "netmap: self: [AQEBA] auth=machine-unknown u=? []\n [AgICA] D2 : 192.168.0.100:12 192.168.0.100:12354\n [AwMDA] D4 : 10.2.0.100:12 10.1.0.100:12345\n",
},
{
name: "debug_non_nil",
@@ -59,7 +59,7 @@ func TestNetworkMapConcise(t *testing.T) {
NodeKey: testNodeKey(1),
Debug: &tailcfg.Debug{},
},
want: "netmap: self: [AQEBA] auth=machine-unknown debug={} []\n",
want: "netmap: self: [AQEBA] auth=machine-unknown u=? debug={} []\n",
},
{
name: "debug_values",
@@ -67,7 +67,7 @@ func TestNetworkMapConcise(t *testing.T) {
NodeKey: testNodeKey(1),
Debug: &tailcfg.Debug{LogHeapPprof: true},
},
want: "netmap: self: [AQEBA] auth=machine-unknown debug={\"LogHeapPprof\":true} []\n",
want: "netmap: self: [AQEBA] auth=machine-unknown u=? debug={\"LogHeapPprof\":true} []\n",
},
} {
t.Run(tt.name, func(t *testing.T) {
@@ -135,7 +135,7 @@ func TestConciseDiffFrom(t *testing.T) {
},
},
},
want: "-netmap: self: [AQEBA] auth=machine-unknown []\n+netmap: self: [AgICA] auth=machine-unknown []\n",
want: "-netmap: self: [AQEBA] auth=machine-unknown u=? []\n+netmap: self: [AgICA] auth=machine-unknown u=? []\n",
},
{
name: "peer_add",

View File

@@ -12,7 +12,7 @@ import (
)
func TestPersistEqual(t *testing.T) {
persistHandles := []string{"PrivateMachineKey", "PrivateNodeKey", "OldPrivateNodeKey", "Provider", "LoginName"}
persistHandles := []string{"LegacyFrontendPrivateMachineKey", "PrivateNodeKey", "OldPrivateNodeKey", "Provider", "LoginName"}
if have := fieldsOf(reflect.TypeOf(Persist{})); !reflect.DeepEqual(have, persistHandles) {
t.Errorf("Persist.Equal check might be out of sync\nfields: %q\nhandled: %q\n",
have, persistHandles)
@@ -36,13 +36,13 @@ func TestPersistEqual(t *testing.T) {
{&Persist{}, &Persist{}, true},
{
&Persist{PrivateMachineKey: k1},
&Persist{PrivateMachineKey: newPrivate()},
&Persist{LegacyFrontendPrivateMachineKey: k1},
&Persist{LegacyFrontendPrivateMachineKey: newPrivate()},
false,
},
{
&Persist{PrivateMachineKey: k1},
&Persist{PrivateMachineKey: k1},
&Persist{LegacyFrontendPrivateMachineKey: k1},
&Persist{LegacyFrontendPrivateMachineKey: k1},
true,
},

View File

@@ -1291,7 +1291,7 @@ func (s *Server) ExpVar() expvar.Var {
m.Set("multiforwarder_deleted", &s.multiForwarderDeleted)
m.Set("packet_forwarder_delete_other_value", &s.removePktForwardOther)
var expvarVersion expvar.String
expvarVersion.Set(version.LONG)
expvarVersion.Set(version.Long)
m.Set("version", &expvarVersion)
return m
}

15
go.mod
View File

@@ -9,6 +9,7 @@ require (
github.com/coreos/go-iptables v0.4.5
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 // indirect
github.com/gliderlabs/ssh v0.2.2
github.com/go-multierror/multierror v1.0.2
github.com/go-ole/go-ole v1.2.4
github.com/godbus/dbus/v5 v5.0.3
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e
@@ -21,19 +22,19 @@ require (
github.com/miekg/dns v1.1.30
github.com/pborman/getopt v0.0.0-20190409184431-ee0cd42419d3
github.com/peterbourgon/ff/v2 v2.0.0
github.com/tailscale/depaware v0.0.0-20200914232109-e09ee10c1824
github.com/tailscale/winipcfg-go v0.0.0-20200916205758-decb9ee8e170
github.com/tailscale/wireguard-go v0.0.0-20200902185615-1997cf6f9fe4
github.com/tailscale/depaware v0.0.0-20201003033024-5d95aab075be
github.com/tailscale/wireguard-go v0.0.0-20201021041318-a6168fd06b3f
github.com/tcnksm/go-httpstat v0.2.0
github.com/toqueteos/webbrowser v1.2.0
go4.org/mem v0.0.0-20200706164138-185c595c3ecc
golang.org/x/crypto v0.0.0-20200429183012-4b2356b1ed79
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5
golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899
golang.org/x/net v0.0.0-20200822124328-c89045814202
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208
golang.org/x/sys v0.0.0-20200812155832-6a926be9bd1d
golang.org/x/time v0.0.0-20191024005414-555d28b269f0
golang.org/x/tools v0.0.0-20191216052735-49a3e744a425
golang.org/x/tools v0.0.0-20201002184944-ecd9fd270d5d
golang.zx2c4.com/wireguard/windows v0.1.2-0.20201004085714-dd60d0447f81
honnef.co/go/tools v0.0.1-2020.1.4
inet.af/netaddr v0.0.0-20200810144936-56928fe48a98
rsc.io/goversion v1.2.0

71
go.sum
View File

@@ -3,11 +3,8 @@ github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/Masterminds/semver/v3 v3.0.3 h1:znjIyLfpXEDQjOIEWh+ehwpTU14UzUPub3c3sm36u14=
github.com/Masterminds/semver/v3 v3.0.3/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
github.com/alecthomas/kingpin v2.2.6+incompatible h1:5svnBTFgJjZvGKyYBtMB0+m5wvrbUHiqye8wRJMlnYI=
github.com/alecthomas/kingpin v2.2.6+incompatible/go.mod h1:59OFYbFVLKQKq+mqrL6Rw5bR0c3ACQaawgXx0QYndlE=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d h1:UQZhZ2O0vMHr2cI+DC1Mbh0TJxzA3RcLoMsFw+aXw7E=
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
github.com/alexbrainman/sspi v0.0.0-20180613141037-e580b900e9f5 h1:P5U+E4x5OkVEKQDklVPmzs71WM56RTTRqV4OrDC//Y4=
github.com/alexbrainman/sspi v0.0.0-20180613141037-e580b900e9f5/go.mod h1:976q2ETgjT2snVCf2ZaBnyBbVoPERGjUz+0sofzEfro=
@@ -30,6 +27,8 @@ github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjr
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
github.com/gliderlabs/ssh v0.2.2 h1:6zsha5zo/TWhRhwqCD3+EarCAgZ2yN28ipRnGPnwkI0=
github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
github.com/go-multierror/multierror v1.0.2 h1:AwsKbEXkmf49ajdFJgcFXqSG0aLo0HEyAE9zk9JguJo=
github.com/go-multierror/multierror v1.0.2/go.mod h1:U7SZR/D9jHgt2nkSj8XcbCWdmVM2igraCHQ3HC1HiKY=
github.com/go-ole/go-ole v1.2.4 h1:nNBDSCOigTSiarFpYE9J/KtEA1IOW4CNeqT9TQDqCxI=
github.com/go-ole/go-ole v1.2.4/go.mod h1:XCwSNxSkXRo4vlyPy93sltvi/qJq0jqQhjqQNIwKuxM=
github.com/godbus/dbus/v5 v5.0.3 h1:ZqHaoEF7TBzh4jzPmqVhE/5A1z9of6orkAe5uHoAeME=
@@ -61,6 +60,8 @@ github.com/kr/pty v1.1.1 h1:VkoXIwSboBpnk99O/KFauAEILuNHv5DVFKZMBN/gUgw=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/lxn/walk v0.0.0-20191128110447-55ccb3a9f5c1/go.mod h1:E23UucZGqpuUANJooIbHWCufXvOcT6E7Stq81gU+CSQ=
github.com/lxn/win v0.0.0-20191128105842-2da648fda5b4/go.mod h1:ouWl4wViUNh8tPSIwxTVMuS014WakR1hqvBc2I0bMoA=
github.com/mattn/go-zglob v0.0.1 h1:xsEx/XUoVlI6yXjqBK062zYhRTZltCNmYPx6v+8DNaY=
github.com/mattn/go-zglob v0.0.1/go.mod h1:9fxibJccNxU2cnpIKLRRFA7zX7qhkJIQWBb449FYHOo=
github.com/mdlayher/netlink v0.0.0-20190409211403-11939a169225/go.mod h1:eQB3mZE4aiYnlUsyGGCOpPETfdQq4Jhsgf1fk3cwQaA=
@@ -85,24 +86,19 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/sassoftware/go-rpmutils v0.0.0-20190420191620-a8f1baeba37b h1:+gCnWOZV8Z/8jehJ2CdqB47Z3S+SREmQcuXkRFLNsiI=
github.com/sassoftware/go-rpmutils v0.0.0-20190420191620-a8f1baeba37b/go.mod h1:am+Fp8Bt506lA3Rk3QCmSqmYmLMnPDhdDUcosQCAx+I=
github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/tailscale/depaware v0.0.0-20200909185729-8ca448326e3a h1:PjVmKyzFfgQrdrmX7kpRkKXkvwMZP/MF3nJT/WJyjW8=
github.com/tailscale/depaware v0.0.0-20200909185729-8ca448326e3a/go.mod h1:H0k9mKUzaDpb22Zn2FiSzY3zeRbAiZ7wUFxKJ7kp8GE=
github.com/tailscale/depaware v0.0.0-20200910145248-cb751026f10d h1:tjLqVTL0IZdV2kBvM2WqCjug6IBJTWOYiM8wqPk2Xp0=
github.com/tailscale/depaware v0.0.0-20200910145248-cb751026f10d/go.mod h1:H0k9mKUzaDpb22Zn2FiSzY3zeRbAiZ7wUFxKJ7kp8GE=
github.com/tailscale/depaware v0.0.0-20200914201916-3f1070fd0d55 h1:hLAgSpbb0rfOq9jziQbvOsOarpfTDxnFbG8kG6INFeY=
github.com/tailscale/depaware v0.0.0-20200914201916-3f1070fd0d55/go.mod h1:nyzwKFaLuckPu3dAJHH7B6lMi4xDBWzD0r3pEpGZm2Y=
github.com/tailscale/depaware v0.0.0-20200914232109-e09ee10c1824 h1:MD/YQ8xI070ZqFC3SnLAlhPPUNfeRKErQaAaXc/r4dQ=
github.com/tailscale/depaware v0.0.0-20200914232109-e09ee10c1824/go.mod h1:nyzwKFaLuckPu3dAJHH7B6lMi4xDBWzD0r3pEpGZm2Y=
github.com/tailscale/winipcfg-go v0.0.0-20200413171540-609dcf2df55f h1:uFj5bslHsMzxIM8UTjAhq4VXeo6GfNW91rpoh/WMJaY=
github.com/tailscale/winipcfg-go v0.0.0-20200413171540-609dcf2df55f/go.mod h1:x880GWw5fvrl2DVTQ04ttXQD4DuppTt1Yz6wLibbjNE=
github.com/tailscale/winipcfg-go v0.0.0-20200916205758-decb9ee8e170 h1:vJ0twi0120W/LKiDxzXROSVx1F4pIKZBQqvtPahnH60=
github.com/tailscale/winipcfg-go v0.0.0-20200916205758-decb9ee8e170/go.mod h1:x880GWw5fvrl2DVTQ04ttXQD4DuppTt1Yz6wLibbjNE=
github.com/tailscale/wireguard-go v0.0.0-20200902185615-1997cf6f9fe4 h1:UiTXdZChEWxxci7bx+jS9OyHQx2IA8zmMWQqp5wfP7c=
github.com/tailscale/wireguard-go v0.0.0-20200902185615-1997cf6f9fe4/go.mod h1:WXq+IkSOJGIgfF1XW+4z4oW+LX/TXzU9DcKlT5EZLi4=
github.com/tailscale/depaware v0.0.0-20201003033024-5d95aab075be h1:ZKe3kVGbu/goUVxXcaCPbQ4b0STQ5NsCpG90CG6mw/c=
github.com/tailscale/depaware v0.0.0-20201003033024-5d95aab075be/go.mod h1:jissDaJNHiyV2tFdr3QyNEfsZrax/i2yQiSO+CljThI=
github.com/tailscale/wireguard-go v0.0.0-20200921221757-11a958a67bdd h1:yEWpro9EdxGgkt24NInVnONIJxRLURH5c37Ki5+06EE=
github.com/tailscale/wireguard-go v0.0.0-20200921221757-11a958a67bdd/go.mod h1:WXq+IkSOJGIgfF1XW+4z4oW+LX/TXzU9DcKlT5EZLi4=
github.com/tailscale/wireguard-go v0.0.0-20201008164108-2c83f43a9859 h1:Z7bXXCYRg/8sjSyKTk0V8Yso/gQjNvPb10DBemKuz+A=
github.com/tailscale/wireguard-go v0.0.0-20201008164108-2c83f43a9859/go.mod h1:WXq+IkSOJGIgfF1XW+4z4oW+LX/TXzU9DcKlT5EZLi4=
github.com/tailscale/wireguard-go v0.0.0-20201021041318-a6168fd06b3f h1:KMx58dbn2YCutzOvjNHgmvbwQH7nGE8H+J42Nenjl/M=
github.com/tailscale/wireguard-go v0.0.0-20201021041318-a6168fd06b3f/go.mod h1:WXq+IkSOJGIgfF1XW+4z4oW+LX/TXzU9DcKlT5EZLi4=
github.com/tcnksm/go-httpstat v0.2.0 h1:rP7T5e5U2HfmOBmZzGgGZjBQ5/GluWUylujl0tJ04I0=
github.com/tcnksm/go-httpstat v0.2.0/go.mod h1:s3JVJFtQxtBEBC9dwcdTTXS9xFnM3SXAZwPG41aurT8=
github.com/toqueteos/webbrowser v1.2.0 h1:tVP/gpK69Fx+qMJKsLE7TD8LuGWPnEV71wBN9rrstGQ=
@@ -111,16 +107,22 @@ github.com/ulikunitz/xz v0.5.6 h1:jGHAfXawEGZQ3blwU5wnWKQJvAraT7Ftq9EXjnXYgt8=
github.com/ulikunitz/xz v0.5.6/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8=
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo=
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
go4.org/mem v0.0.0-20200706164138-185c595c3ecc h1:paujszgN6SpsO/UsXC7xax3gQAKz/XQKCYZLQdU34Tw=
go4.org/mem v0.0.0-20200706164138-185c595c3ecc/go.mod h1:NEYvpHWemiG/E5UWfaN5QAIGZeT1sa0Z2UNk6oeMb/k=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191002192127-34f69633bfdc/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200429183012-4b2356b1ed79 h1:IaQbIIB2X/Mp/DKctl6ROxz1KyMlKp4uyvL6+kQ7C88=
golang.org/x/crypto v0.0.0-20200429183012-4b2356b1ed79/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899 h1:DZhuSZLsGlFL4CmhA8BcRA0mnthyA/nZ00AqCUo7vHg=
golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
@@ -128,48 +130,57 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191003171128-d98b1b443823/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191007182048-72f939374954/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2 h1:CCH4IOTTfewWjGOlSp+zGcjutRKlBEZQ6wTn8ozI/nI=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5 h1:WQ8q63x+f/zpC8Ac1s9wLElVoHhm32p6tudrU72n1QA=
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200822124328-c89045814202 h1:VvcQYSHwXgi7W+TpUR6A9g6Up98WAHf3f/ulnJ62IyA=
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d h1:TzXSXBo42m9gQenoE3b9BGiEpg5IG2JkU5FkPIawgtw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208 h1:qwRHBd0NqMbJxfbotnDhm2ByMI1Shq4Y6oRJo21SGJA=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190310054646-10058d7d4faa/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190405154228-4b34438f7a67/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190411185658-b44545bcd369/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191003212358-c178f38b412c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5 h1:LfCXLvNmTYH9kEmVgqbnsWfruoXZIrh4YBgqVHtDvw0=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200501145240-bc7a7d42d5c3 h1:5B6i6EAiSYyejWfvc5Rc9BbI3rzIsrrXfAQBWnYfn+w=
golang.org/x/sys v0.0.0-20200501145240-bc7a7d42d5c3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200812155832-6a926be9bd1d h1:QQrM/CCYEzTs91GZylDCQjGHudbPTxF/1fvXdVh5lMo=
golang.org/x/sys v0.0.0-20200812155832-6a926be9bd1d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0 h1:/5xXl8Y5W96D+TtHSlonuFqGHIWVuyCkGJLwGh9JJFs=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d h1:/iIZNFGxc/a7C3yWjGcnboV+Tkc7mxr+p6fDztwoxuM=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191216052735-49a3e744a425 h1:VvQyQJN0tSuecqgcIxMWnnfG5kSmgy9KZR9sW3W5QeA=
golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20201001230009-b5b87423c93b/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU=
golang.org/x/tools v0.0.0-20201002184944-ecd9fd270d5d h1:vWQvJ/Z0Lu+9/8oQ/pAYXNzbc7CMnBl+tULGVHOy3oE=
golang.org/x/tools v0.0.0-20201002184944-ecd9fd270d5d/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.zx2c4.com/wireguard v0.0.20200321-0.20200715051853-507f148e1c42 h1:SrR1hmxGKKarHEEDvaHxatwnqE3uT+7jvMcin6SHOkw=
golang.zx2c4.com/wireguard v0.0.20200321-0.20200715051853-507f148e1c42/go.mod h1:GJvYs5O24/ASlwPiRklVnjMx2xQzrOic0DuU6GvYJL4=
golang.zx2c4.com/wireguard/windows v0.1.2-0.20201004085714-dd60d0447f81 h1:cT2oWlz8v9g7bjFZclT362akxJJfGv9d7ccKu6GQUbA=
golang.zx2c4.com/wireguard/windows v0.1.2-0.20201004085714-dd60d0447f81/go.mod h1:GaK5zcgr5XE98WaRzIDilumDBp5/yP8j2kG/LCDnvAM=
google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

View File

@@ -21,6 +21,7 @@ type State int
const (
NoState = State(iota)
InUseOtherUser
NeedsLogin
NeedsMachineAuth
Stopped
@@ -33,8 +34,14 @@ const (
const GoogleIDTokenType = "ts_android_google_login"
func (s State) String() string {
return [...]string{"NoState", "NeedsLogin", "NeedsMachineAuth",
"Stopped", "Starting", "Running"}[s]
return [...]string{
"NoState",
"InUseOtherUser",
"NeedsLogin",
"NeedsMachineAuth",
"Stopped",
"Starting",
"Running"}[s]
}
// EngineStatus contains WireGuard engine stats.
@@ -53,7 +60,7 @@ type EngineStatus struct {
type Notify struct {
_ structs.Incomparable
Version string // version number of IPN backend
ErrMessage *string // critical error message, if any
ErrMessage *string // critical error message, if any; for InUseOtherUser, the details
LoginFinished *empty.Message // event: non-nil when login process succeeded
State *State // current IPN state has changed
Prefs *Prefs // preferences were changed
@@ -81,14 +88,14 @@ type Notify struct {
// shared by several consecutive users. Ideally we would just use the
// username of the connected frontend as the StateKey.
//
// However, on Windows, there seems to be no safe way to figure out
// the owning user of a process connected over IPC mechanisms
// (sockets, named pipes). So instead, on Windows, we use a
// capability-oriented system where the frontend generates a random
// identifier for itself, and uses that as the StateKey when talking
// to the backend. That way, while we can't identify an OS user by
// name, we can tell two different users apart, because they'll have
// different opaque state keys (and no access to each others's keys).
// Various platforms currently set StateKey in different ways:
//
// * the macOS/iOS GUI apps set it to "ipn-go-bridge"
// * the Android app sets it to "ipn-android"
// * on Windows, it's the empty string (in client mode) or, via
// LocalBackend.userID, a string like "user-$USER_ID" (used in
// server mode).
// * on Linux/etc, it's always "_daemon" (ipn.GlobalDaemonStateKey)
type StateKey string
type Options struct {
@@ -97,7 +104,8 @@ type Options struct {
// StateKey and Prefs together define the state the backend should
// use:
// - StateKey=="" && Prefs!=nil: use Prefs for internal state,
// don't persist changes in the backend.
// don't persist changes in the backend, except for the machine key
// for migration purposes.
// - StateKey!="" && Prefs==nil: load the given backend-side
// state and use/update that.
// - StateKey!="" && Prefs!=nil: like the previous case, but do

View File

@@ -7,9 +7,10 @@ package ipnserver
import (
"bufio"
"context"
"errors"
"fmt"
"html"
"io"
"io/ioutil"
"log"
"net"
"net/http"
@@ -18,6 +19,7 @@ import (
"os/signal"
"os/user"
"runtime"
"strings"
"sync"
"syscall"
"time"
@@ -25,6 +27,7 @@ import (
"inet.af/netaddr"
"tailscale.com/control/controlclient"
"tailscale.com/ipn"
"tailscale.com/log/filelogger"
"tailscale.com/logtail/backoff"
"tailscale.com/net/netstat"
"tailscale.com/safesocket"
@@ -68,6 +71,12 @@ type Options struct {
// its existing state, and accepts new frontend connections. If
// false, the server dumps its state and becomes idle.
//
// This is effectively whether the platform is in "server
// mode" by default. On Linux, it's true; on Windows, it's
// false. But on some platforms (currently only Windows), the
// "server mode" can be overridden at runtime with a change in
// Prefs.ForceDaemon/WantRunning.
//
// To support CLI connections (notably, "tailscale status"),
// the actual definition of "disconnect" is when the
// connection count transitions from 1 to 0.
@@ -81,30 +90,183 @@ type Options struct {
// server is an IPN backend and its set of 0 or more active connections
// talking to an IPN backend.
type server struct {
resetOnZero bool // call bs.Reset on transition from 1->0 connections
b *ipn.LocalBackend
logf logger.Logf
// resetOnZero is whether to call bs.Reset on transition from
// 1->0 connections. That is, this is whether the backend is
// being run in "client mode" that requires an active GUI
// connection (such as on Windows by default). Even if this
// is true, the ForceDaemon pref can override this.
resetOnZero bool
bsMu sync.Mutex // lock order: bsMu, then mu
bs *ipn.BackendServer
mu sync.Mutex
clients map[net.Conn]bool
mu sync.Mutex
serverModeUser *user.User // or nil if not in server mode
lastUserID string // tracks last userid; on change, Reset state for paranoia
allClients map[net.Conn]connIdentity // HTTP or IPN
clients map[net.Conn]bool // subset of allClients; only IPN protocol
disconnectSub map[chan<- struct{}]struct{} // keys are subscribers of disconnects
}
// connIdentity represents the owner of a localhost TCP connection.
type connIdentity struct {
Unknown bool
Pid int
UserID string
User *user.User
}
// getConnIdentity returns the localhost TCP connection's identity information
// (pid, userid, user). If it's not Windows (for now), it returns a nil error
// and a ConnIdentity with Unknown set true. It's only an error if we expected
// to be able to map it and couldn't.
func (s *server) getConnIdentity(c net.Conn) (ci connIdentity, err error) {
if runtime.GOOS != "windows" { // for now; TODO: expand to other OSes
return connIdentity{Unknown: true}, nil
}
la, err := netaddr.ParseIPPort(c.LocalAddr().String())
if err != nil {
return ci, fmt.Errorf("parsing local address: %w", err)
}
ra, err := netaddr.ParseIPPort(c.RemoteAddr().String())
if err != nil {
return ci, fmt.Errorf("parsing local remote: %w", err)
}
if !la.IP.IsLoopback() || !ra.IP.IsLoopback() {
return ci, errors.New("non-loopback connection")
}
tab, err := netstat.Get()
if err != nil {
return ci, fmt.Errorf("failed to get local connection table: %w", err)
}
pid := peerPid(tab.Entries, la, ra)
if pid == 0 {
return ci, errors.New("no local process found matching localhost connection")
}
ci.Pid = pid
uid, err := pidowner.OwnerOfPID(pid)
if err != nil {
var hint string
if runtime.GOOS == "windows" {
hint = " (WSL?)"
}
return ci, fmt.Errorf("failed to map connection's pid to a user%s: %w", hint, err)
}
ci.UserID = uid
u, err := s.lookupUserFromID(uid)
if err != nil {
return ci, fmt.Errorf("failed to look up user from userid: %w", err)
}
ci.User = u
return ci, nil
}
func (s *server) lookupUserFromID(uid string) (*user.User, error) {
u, err := user.LookupId(uid)
if err != nil && runtime.GOOS == "windows" && errors.Is(err, syscall.Errno(0x534)) {
s.logf("[warning] issue 869: os/user.LookupId failed; ignoring")
// Work around https://github.com/tailscale/tailscale/issues/869 for
// now. We don't strictly need the username. It's just a nice-to-have.
// So make up a *user.User if their machine is broken in this way.
return &user.User{
Uid: uid,
Username: "unknown-user-" + uid,
Name: "unknown user " + uid,
}, nil
}
return u, err
}
// blockWhileInUse blocks while until either a Read from conn fails
// (i.e. it's closed) or until the server is able to accept ci as a
// user.
func (s *server) blockWhileInUse(conn io.Reader, ci connIdentity) {
s.logf("blocking client while server in use; connIdentity=%v", ci)
connDone := make(chan struct{})
go func() {
io.Copy(ioutil.Discard, conn)
close(connDone)
}()
ch := make(chan struct{}, 1)
s.registerDisconnectSub(ch, true)
defer s.registerDisconnectSub(ch, false)
for {
select {
case <-connDone:
s.logf("blocked client Read completed; connIdentity=%v", ci)
return
case <-ch:
s.mu.Lock()
err := s.checkConnIdentityLocked(ci)
s.mu.Unlock()
if err == nil {
s.logf("unblocking client, server is free; connIdentity=%v", ci)
// Server is now available again for a new user.
// TODO(bradfitz): keep this connection alive. But for
// now just return and have our caller close the connection
// (which unblocks the io.Copy goroutine we started above)
// and then the client (e.g. Windows) will reconnect and
// discover that it works.
return
}
}
}
}
func (s *server) serveConn(ctx context.Context, c net.Conn, logf logger.Logf) {
br := bufio.NewReader(c)
// First see if it's an HTTP request.
br := bufio.NewReader(c)
c.SetReadDeadline(time.Now().Add(time.Second))
peek, _ := br.Peek(4)
c.SetReadDeadline(time.Time{})
if string(peek) == "GET " {
http.Serve(&oneConnListener{altReaderNetConn{br, c}}, localhostHandler(c))
isHTTPReq := string(peek) == "GET "
ci, err := s.addConn(c, isHTTPReq)
if err != nil {
if isHTTPReq {
fmt.Fprintf(c, "HTTP/1.0 500 Nope\r\nContent-Type: text/plain\r\nX-Content-Type-Options: nosniff\r\n\r\n%s\n", err.Error())
c.Close()
return
}
defer c.Close()
serverToClient := func(b []byte) { ipn.WriteMsg(c, b) }
bs := ipn.NewBackendServer(logf, nil, serverToClient)
_, occupied := err.(inUseOtherUserError)
if occupied {
bs.SendInUseOtherUserErrorMessage(err.Error())
s.blockWhileInUse(c, ci)
} else {
bs.SendErrorMessage(err.Error())
time.Sleep(time.Second)
}
return
}
// Tell the LocalBackend about the identity we're now running as.
s.b.SetCurrentUserID(ci.UserID)
if isHTTPReq {
httpServer := http.Server{
// Localhost connections are cheap; so only do
// keep-alives for a short period of time, as these
// active connections lock the server into only serving
// that user. If the user has this page open, we don't
// want another switching user to be locked out for
// minutes. 5 seconds is enough to let browser hit
// favicon.ico and such.
IdleTimeout: 5 * time.Second,
ErrorLog: logger.StdLogger(logf),
Handler: s.localhostHandler(ci),
}
httpServer.Serve(&oneConnListener{&protoSwitchConn{s: s, br: br, Conn: c}})
return
}
s.addConn(c)
logf("incoming control connection")
defer s.removeAndCloseConn(c)
logf("incoming control connection")
for ctx.Err() == nil {
msg, err := ipn.ReadMsg(br)
if err != nil {
@@ -125,25 +287,126 @@ func (s *server) serveConn(ctx context.Context, c net.Conn, logf logger.Logf) {
}
}
func (s *server) addConn(c net.Conn) {
// inUseOtherUserError is the error type for when the server is in use
// by a different local user.
type inUseOtherUserError struct{ error }
func (e inUseOtherUserError) Unwrap() error { return e.error }
// checkConnIdentityLocked checks whether the provided identity is
// allowed to connect to the server.
//
// The returned error, when non-nil, will be of type inUseOtherUserError.
//
// s.mu must be held.
func (s *server) checkConnIdentityLocked(ci connIdentity) error {
// If clients are already connected, verify they're the same user.
// This mostly matters on Windows at the moment.
if len(s.allClients) > 0 {
var active connIdentity
for _, active = range s.allClients {
break
}
if ci.UserID != active.UserID {
//lint:ignore ST1005 we want to capitalize Tailscale here
return inUseOtherUserError{fmt.Errorf("Tailscale already in use by %s, pid %d", active.User.Username, active.Pid)}
}
}
if su := s.serverModeUser; su != nil && ci.UserID != su.Uid {
//lint:ignore ST1005 we want to capitalize Tailscale here
return inUseOtherUserError{fmt.Errorf("Tailscale already in use by %s", su.Username)}
}
return nil
}
// registerDisconnectSub adds ch as a subscribe to connection disconnect
// events. If add is false, the subscriber is removed.
func (s *server) registerDisconnectSub(ch chan<- struct{}, add bool) {
s.mu.Lock()
defer s.mu.Unlock()
if add {
if s.disconnectSub == nil {
s.disconnectSub = make(map[chan<- struct{}]struct{})
}
s.disconnectSub[ch] = struct{}{}
} else {
delete(s.disconnectSub, ch)
}
}
// addConn adds c to the server's list of clients.
//
// If the returned error is of type inUseOtherUserError then the
// returned connIdentity is also valid.
func (s *server) addConn(c net.Conn, isHTTP bool) (ci connIdentity, err error) {
ci, err = s.getConnIdentity(c)
if err != nil {
return
}
// If the connected user changes, reset the backend server state to make
// sure node keys don't leak between users.
var doReset bool
defer func() {
if doReset {
s.logf("identity changed; resetting server")
s.bsMu.Lock()
s.bs.Reset()
s.bsMu.Unlock()
}
}()
s.mu.Lock()
defer s.mu.Unlock()
if s.clients == nil {
s.clients = map[net.Conn]bool{}
}
s.clients[c] = true
if s.allClients == nil {
s.allClients = map[net.Conn]connIdentity{}
}
if err := s.checkConnIdentityLocked(ci); err != nil {
return ci, err
}
if !isHTTP {
s.clients[c] = true
}
s.allClients[c] = ci
if s.lastUserID != ci.UserID {
if s.lastUserID != "" {
doReset = true
}
s.lastUserID = ci.UserID
}
return ci, nil
}
func (s *server) removeAndCloseConn(c net.Conn) {
s.mu.Lock()
delete(s.clients, c)
remain := len(s.clients)
delete(s.allClients, c)
remain := len(s.allClients)
for sub := range s.disconnectSub {
select {
case sub <- struct{}{}:
default:
}
}
s.mu.Unlock()
if remain == 0 && s.resetOnZero {
s.bsMu.Lock()
s.bs.Reset()
s.bsMu.Unlock()
if s.b.InServerMode() {
s.logf("client disconnected; staying alive in server mode")
} else {
s.logf("client disconnected; stopping server")
s.bsMu.Lock()
s.bs.Reset()
s.bsMu.Unlock()
}
}
c.Close()
}
@@ -158,9 +421,48 @@ func (s *server) stopAll() {
s.clients = nil
}
// setServerModeUserLocked is called when we're in server mode but our s.serverModeUser is nil.
//
// s.mu must be held
func (s *server) setServerModeUserLocked() {
var ci connIdentity
var ok bool
for _, ci = range s.allClients {
ok = true
break
}
if !ok {
s.logf("ipnserver: [unexpected] now in server mode, but no connected client")
return
}
if ci.Unknown {
return
}
if ci.User != nil {
s.logf("ipnserver: now in server mode; user=%v", ci.User.Username)
s.serverModeUser = ci.User
} else {
s.logf("ipnserver: [unexpected] now in server mode, but nil User")
}
}
func (s *server) writeToClients(b []byte) {
inServerMode := s.b.InServerMode()
s.mu.Lock()
defer s.mu.Unlock()
if inServerMode {
if s.serverModeUser == nil {
s.setServerModeUserLocked()
}
} else {
if s.serverModeUser != nil {
s.logf("ipnserver: no longer in server mode")
s.serverModeUser = nil
}
}
for c := range s.clients {
ipn.WriteMsg(c, b)
}
@@ -178,6 +480,7 @@ func Run(ctx context.Context, logf logger.Logf, logid string, getEngine func() (
}
server := &server{
logf: logf,
resetOnZero: !opts.SurviveDisconnects,
}
@@ -194,12 +497,11 @@ func Run(ctx context.Context, logf logger.Logf, logid string, getEngine func() (
logf("Listening on %v", listen.Addr())
bo := backoff.NewBackoff("ipnserver", logf, 30*time.Second)
var unservedConn net.Conn // if non-nil, accepted, but hasn't served yet
eng, err := getEngine()
if err != nil {
logf("Initial getEngine call: %v", err)
logf("ipnserver: initial getEngine call: %v", err)
for i := 1; ctx.Err() == nil; i++ {
c, err := listen.Accept()
if err != nil {
@@ -207,14 +509,14 @@ func Run(ctx context.Context, logf logger.Logf, logid string, getEngine func() (
bo.BackOff(ctx, err)
continue
}
logf("%d: trying getEngine again...", i)
logf("ipnserver: try%d: trying getEngine again...", i)
eng, err = getEngine()
if err == nil {
logf("%d: GetEngine worked; exiting failure loop", i)
unservedConn = c
break
}
logf("%d: getEngine failed again: %v", i, err)
logf("ipnserver%d: getEngine failed again: %v", i, err)
errMsg := err.Error()
go func() {
defer c.Close()
@@ -235,6 +537,24 @@ func Run(ctx context.Context, logf logger.Logf, logid string, getEngine func() (
if err != nil {
return fmt.Errorf("ipn.NewFileStore(%q): %v", opts.StatePath, err)
}
if opts.AutostartStateKey == "" {
autoStartKey, err := store.ReadState(ipn.ServerModeStartKey)
if err != nil && err != ipn.ErrStateNotExist {
return fmt.Errorf("calling ReadState on %s: %w", opts.StatePath, err)
}
key := string(autoStartKey)
if strings.HasPrefix(key, "user-") {
uid := strings.TrimPrefix(key, "user-")
u, err := server.lookupUserFromID(uid)
if err != nil {
logf("ipnserver: found server mode auto-start key %q; failed to load user: %v", key, err)
} else {
logf("ipnserver: found server mode auto-start key %q (user %s)", key, u.Username)
server.serverModeUser = u
}
opts.AutostartStateKey = ipn.StateKey(key)
}
}
} else {
store = &ipn.MemoryStore{}
}
@@ -250,18 +570,16 @@ func Run(ctx context.Context, logf logger.Logf, logid string, getEngine func() (
if opts.DebugMux != nil {
opts.DebugMux.HandleFunc("/debug/ipn", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
st := b.Status()
// TODO(bradfitz): add LogID and opts to st?
st.WriteHTML(w)
serveHTMLStatus(w, b)
})
}
server.b = b
server.bs = ipn.NewBackendServer(logf, b, server.writeToClients)
if opts.AutostartStateKey != "" {
server.bs.GotCommand(&ipn.Command{
Version: version.LONG,
Version: version.Long,
Start: &ipn.StartArgs{
Opts: ipn.Options{
StateKey: opts.AutostartStateKey,
@@ -292,6 +610,11 @@ func Run(ctx context.Context, logf logger.Logf, logid string, getEngine func() (
return ctx.Err()
}
// BabysitProc runs the current executable as a child process with the
// provided args, capturing its output, writing it to files, and
// restarting the process on any crashes.
//
// It's only currently (2020-10-29) used on Windows.
func BabysitProc(ctx context.Context, args []string, logf logger.Logf) {
executable, err := os.Executable()
@@ -299,6 +622,14 @@ 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
@@ -436,54 +767,40 @@ func (l *oneConnListener) Addr() net.Addr { return dummyAddr("unused-address") }
func (a dummyAddr) Network() string { return string(a) }
func (a dummyAddr) String() string { return string(a) }
type altReaderNetConn struct {
r io.Reader
// protoSwitchConn is a net.Conn that's we want to speak HTTP to but
// it's already had a few bytes read from it to determine that it's
// HTTP. So we Read from its bufio.Reader. On Close, we we tell the
// server it's closed, so the server can account the who's connected.
type protoSwitchConn struct {
s *server
net.Conn
br *bufio.Reader
closeOnce sync.Once
}
func (a altReaderNetConn) Read(p []byte) (int, error) { return a.r.Read(p) }
func (psc *protoSwitchConn) Read(p []byte) (int, error) { return psc.br.Read(p) }
func (psc *protoSwitchConn) Close() error {
psc.closeOnce.Do(func() { psc.s.removeAndCloseConn(psc.Conn) })
return nil
}
func localhostHandler(c net.Conn) http.Handler {
la, lerr := netaddr.ParseIPPort(c.LocalAddr().String())
ra, rerr := netaddr.ParseIPPort(c.RemoteAddr().String())
func (s *server) localhostHandler(ci connIdentity) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "<html><body><h1>tailscale</h1>\n")
if lerr != nil || rerr != nil {
io.WriteString(w, "failed to parse remote address")
if ci.Unknown {
io.WriteString(w, "<html><title>Tailscale</title><body><h1>Tailscale</h1>This is the local Tailscale daemon.")
return
}
if !la.IP.IsLoopback() || !ra.IP.IsLoopback() {
io.WriteString(w, "not loopback")
return
}
tab, err := netstat.Get()
if err == netstat.ErrNotImplemented {
io.WriteString(w, "status page not available on "+runtime.GOOS)
return
}
if err != nil {
io.WriteString(w, "failed to get netstat table")
return
}
pid := peerPid(tab.Entries, la, ra)
if pid == 0 {
io.WriteString(w, "peer pid not found")
return
}
uid, err := pidowner.OwnerOfPID(pid)
if err != nil {
io.WriteString(w, "owner of peer pid not found")
return
}
u, err := user.LookupId(uid)
if err != nil {
io.WriteString(w, "User lookup failed")
return
}
fmt.Fprintf(w, "Hello, %s", html.EscapeString(u.Username))
serveHTMLStatus(w, s.b)
})
}
func serveHTMLStatus(w http.ResponseWriter, b *ipn.LocalBackend) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
st := b.Status()
// TODO(bradfitz): add LogID and opts to st?
st.WriteHTML(w)
}
func peerPid(entries []netstat.Entry, la, ra netaddr.IPPort) int {
for _, e := range entries {
if e.Local == ra && e.Remote == la {

View File

@@ -7,8 +7,6 @@ package ipnserver_test
import (
"context"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strings"
"testing"
@@ -25,11 +23,7 @@ func TestRunMultipleAccepts(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
td, err := ioutil.TempDir("", "TestRunMultipleAccepts")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(td)
td := t.TempDir()
socketPath := filepath.Join(td, "tailscale.sock")
logf := func(format string, args ...interface{}) {

View File

@@ -5,9 +5,12 @@
package ipn
import (
"bytes"
"context"
"errors"
"fmt"
"os"
"runtime"
"strings"
"sync"
"time"
@@ -34,6 +37,15 @@ import (
"tailscale.com/wgengine/tsdns"
)
var controlDebugFlags = getControlDebugFlags()
func getControlDebugFlags() []string {
if e := os.Getenv("TS_DEBUG_CONTROL_FLAGS"); e != "" {
return strings.Split(e, ",")
}
return nil
}
// LocalBackend is the glue between the major pieces of the Tailscale
// network software: the cloud control plane (via controlclient), the
// network data plane (via wgengine), and the user-facing UIs and CLIs
@@ -50,32 +62,39 @@ type LocalBackend struct {
ctxCancel context.CancelFunc // cancels ctx
logf logger.Logf // general logging
keyLogf logger.Logf // for printing list of peers on change
statsLogf logger.Logf // for printing peers stats on change
e wgengine.Engine
store StateStore
backendLogID string
portpoll *portlist.Poller // may be nil
portpollOnce sync.Once
serverURL string // tailcontrol URL
portpollOnce sync.Once // guards starting readPoller
gotPortPollRes chan struct{} // closed upon first readPoller result
serverURL string // tailcontrol URL
newDecompressor func() (controlclient.Decompressor, error)
filterHash string
// The mutex protects the following elements.
mu sync.Mutex
notify func(Notify)
c *controlclient.Client
stateKey StateKey
prefs *Prefs
state State
mu sync.Mutex
notify func(Notify)
c *controlclient.Client
stateKey StateKey // computed in part from user-provided value
userID string // current controlling user ID (for Windows, primarily)
prefs *Prefs
inServerMode bool
machinePrivKey wgcfg.PrivateKey
state State
// hostinfo is mutated in-place while mu is held.
hostinfo *tailcfg.Hostinfo
// netMap is not mutated in-place once set.
netMap *controlclient.NetworkMap
activeLogin string // last logged LoginName from netMap
engineStatus EngineStatus
endpoints []string
blocked bool
authURL string
interact int
interact bool
prevIfState *interfaces.State
// statusLock must be held before calling statusChanged.Wait() or
// statusChanged.Broadcast().
@@ -100,15 +119,17 @@ func NewLocalBackend(logf logger.Logf, logid string, store StateStore, e wgengin
}
b := &LocalBackend{
ctx: ctx,
ctxCancel: cancel,
logf: logf,
keyLogf: logger.LogOnChange(logf, 5*time.Minute, time.Now),
e: e,
store: store,
backendLogID: logid,
state: NoState,
portpoll: portpoll,
ctx: ctx,
ctxCancel: cancel,
logf: logf,
keyLogf: logger.LogOnChange(logf, 5*time.Minute, time.Now),
statsLogf: logger.LogOnChange(logf, 5*time.Minute, time.Now),
e: e,
store: store,
backendLogID: logid,
state: NoState,
portpoll: portpoll,
gotPortPollRes: make(chan struct{}),
}
e.SetLinkChangeCallback(b.linkChange)
b.statusChanged = sync.NewCond(&b.statusLock)
@@ -116,15 +137,31 @@ func NewLocalBackend(logf logger.Logf, logid string, store StateStore, e wgengin
return b, nil
}
// linkChange is called (in a new goroutine) by wgengine when its link monitor
// detects a network change.
func (b *LocalBackend) linkChange(major bool, ifst *interfaces.State) {
// TODO(bradfitz): on a major link change, ask controlclient
// whether its host (e.g. login.tailscale.com) is reachable.
// If not, down the world and poll for a bit. Windows' WinHTTP
// service might be unable to resolve its WPAD PAC URL if we
// have DNS/routes configured. So we need to remove that DNS
// and those routes to let it figure out its proxy
// settings. Once it's back up and happy, then we can resume
// and our connection to the control server would work again.
b.mu.Lock()
defer b.mu.Unlock()
hadPAC := b.prevIfState.HasPAC()
b.prevIfState = ifst
networkUp := ifst.AnyInterfaceUp()
if b.c != nil {
go b.c.SetPaused(b.state == Stopped || !networkUp)
}
// If the PAC-ness of the network changed, reconfig wireguard+route to
// add/remove subnets.
if hadPAC != ifst.HasPAC() {
b.logf("linkChange: in state %v; PAC changed from %v->%v", b.state, hadPAC, ifst.HasPAC())
switch b.state {
case NoState, Stopped:
// Do nothing.
default:
go b.authReconfig()
}
}
}
// Shutdown halts the backend and all its sub-components. The backend
@@ -232,8 +269,14 @@ func (b *LocalBackend) setClientStatus(st controlclient.Status) {
b.prefs.Persist = st.Persist.Clone()
}
}
if temporarilySetMachineKeyInPersist() && b.prefs.Persist != nil &&
b.prefs.Persist.LegacyFrontendPrivateMachineKey.IsZero() {
b.prefs.Persist.LegacyFrontendPrivateMachineKey = b.machinePrivKey
prefsChanged = true
}
if st.NetMap != nil {
b.netMap = st.NetMap
b.setNetMapLocked(st.NetMap)
}
if st.URL != "" {
b.authURL = st.URL
@@ -272,23 +315,16 @@ func (b *LocalBackend) setClientStatus(st controlclient.Status) {
b.updateFilter(st.NetMap, prefs)
b.e.SetNetworkMap(st.NetMap)
if !dnsMapsEqual(st.NetMap, netMap) {
b.updateDNSMap(st.NetMap)
}
disableDERP := prefs != nil && prefs.DisableDERP
if disableDERP {
b.e.SetDERPMap(nil)
} else {
b.e.SetDERPMap(st.NetMap.DERPMap)
}
b.e.SetDERPMap(st.NetMap.DERPMap)
b.send(Notify{NetMap: st.NetMap})
}
if st.URL != "" {
b.logf("Received auth URL: %.20v...", st.URL)
if interact > 0 {
if interact {
b.popBrowserAuthNow()
}
}
@@ -310,9 +346,8 @@ func (b *LocalBackend) setWgengineStatus(s *wgengine.Status, err error) {
return
}
es := b.parseWgStatus(s)
b.mu.Lock()
es := b.parseWgStatusLocked(s)
c := b.c
b.engineStatus = es
b.endpoints = append([]string{}, s.LocalAddrs...)
@@ -380,18 +415,44 @@ func (b *LocalBackend) Start(opts Options) error {
return fmt.Errorf("loading requested state: %v", err)
}
b.inServerMode = b.prefs.ForceDaemon
b.serverURL = b.prefs.ControlURL
hostinfo.RoutableIPs = append(hostinfo.RoutableIPs, b.prefs.AdvertiseRoutes...)
hostinfo.RequestTags = append(hostinfo.RequestTags, b.prefs.AdvertiseTags...)
if b.inServerMode || runtime.GOOS == "windows" {
b.logf("Start: serverMode=%v", b.inServerMode)
}
applyPrefsToHostinfo(hostinfo, b.prefs)
b.notify = opts.Notify
b.netMap = nil
b.setNetMapLocked(nil)
persist := b.prefs.Persist
machinePrivKey := b.machinePrivKey
b.mu.Unlock()
b.updateFilter(nil, nil)
if b.portpoll != nil {
b.portpollOnce.Do(func() {
go b.portpoll.Run(b.ctx)
go b.readPoller()
// Give the poller a second to get results to
// prevent it from restarting our map poll
// HTTP request (via doSetHostinfoFilterServices >
// cli.SetHostinfo). In practice this is very quick.
t0 := time.Now()
timer := time.NewTimer(time.Second)
select {
case <-b.gotPortPollRes:
b.logf("got initial portlist info in %v", time.Since(t0).Round(time.Millisecond))
timer.Stop()
case <-timer.C:
b.logf("timeout waiting for initial portlist")
}
})
}
var discoPublic tailcfg.DiscoKey
if controlclient.Debug.Disco {
discoPublic = b.e.DiscoPublicKey()
@@ -403,29 +464,22 @@ func (b *LocalBackend) Start(opts Options) error {
persist = &controlclient.Persist{}
}
cli, err := controlclient.New(controlclient.Options{
Logf: logger.WithPrefix(b.logf, "control: "),
Persist: *persist,
ServerURL: b.serverURL,
AuthKey: opts.AuthKey,
Hostinfo: hostinfo,
KeepAlive: true,
NewDecompressor: b.newDecompressor,
HTTPTestClient: opts.HTTPTestClient,
DiscoPublicKey: discoPublic,
MachinePrivateKey: machinePrivKey,
Logf: logger.WithPrefix(b.logf, "control: "),
Persist: *persist,
ServerURL: b.serverURL,
AuthKey: opts.AuthKey,
Hostinfo: hostinfo,
KeepAlive: true,
NewDecompressor: b.newDecompressor,
HTTPTestClient: opts.HTTPTestClient,
DiscoPublicKey: discoPublic,
DebugFlags: controlDebugFlags,
})
if err != nil {
return err
}
// At this point, we have finished using hostinfo without synchronization,
// so it is safe to start readPoller which concurrently writes to it.
if b.portpoll != nil {
b.portpollOnce.Do(func() {
go b.portpoll.Run(b.ctx)
go b.readPoller()
})
}
b.mu.Lock()
b.c = cli
endpoints := b.endpoints
@@ -441,6 +495,12 @@ func (b *LocalBackend) Start(opts Options) error {
b.mu.Lock()
prefs := b.prefs.Clone()
if temporarilySetMachineKeyInPersist() && prefs.Persist != nil &&
prefs.Persist.LegacyFrontendPrivateMachineKey.IsZero() {
prefs.Persist.LegacyFrontendPrivateMachineKey = b.machinePrivKey
}
b.mu.Unlock()
blid := b.backendLogID
@@ -574,6 +634,7 @@ func (b *LocalBackend) updateDNSMap(netMap *controlclient.NetworkMap) {
// readPoller is a goroutine that receives service lists from
// b.portpoll and propagates them into the controlclient's HostInfo.
func (b *LocalBackend) readPoller() {
n := 0
for {
ports, ok := <-b.portpoll.C
if !ok {
@@ -600,6 +661,11 @@ func (b *LocalBackend) readPoller() {
b.mu.Unlock()
b.doSetHostinfoFilterServices(hi)
n++
if n == 1 {
close(b.gotPortPollRes)
}
}
}
@@ -611,7 +677,7 @@ func (b *LocalBackend) send(n Notify) {
b.mu.Unlock()
if notify != nil {
n.Version = version.LONG
n.Version = version.Long
notify(n)
} else {
b.logf("nil notify callback; dropping %+v", n)
@@ -623,7 +689,7 @@ func (b *LocalBackend) send(n Notify) {
func (b *LocalBackend) popBrowserAuthNow() {
b.mu.Lock()
url := b.authURL
b.interact = 0
b.interact = false
b.authURL = ""
b.mu.Unlock()
@@ -637,48 +703,171 @@ func (b *LocalBackend) popBrowserAuthNow() {
}
}
// initMachineKeyLocked is called to initialize b.machinePrivKey.
//
// b.prefs must already be initialized.
// b.stateKey should be set too, but just for nicer log messages.
// b.mu must be held.
func (b *LocalBackend) initMachineKeyLocked() (err error) {
if temporarilySetMachineKeyInPersist() {
defer func() {
if err != nil {
return
}
if b.prefs != nil && b.prefs.Persist != nil {
b.prefs.Persist.LegacyFrontendPrivateMachineKey = b.machinePrivKey
}
}()
}
if !b.machinePrivKey.IsZero() {
// Already set.
return nil
}
var legacyMachineKey wgcfg.PrivateKey
if b.prefs.Persist != nil {
legacyMachineKey = b.prefs.Persist.LegacyFrontendPrivateMachineKey
}
keyText, err := b.store.ReadState(MachineKeyStateKey)
if err == nil {
if err := b.machinePrivKey.UnmarshalText(keyText); err != nil {
return fmt.Errorf("invalid key in %s key of %v: %w", MachineKeyStateKey, b.store, err)
}
if b.machinePrivKey.IsZero() {
return fmt.Errorf("invalid zero key stored in %v key of %v", MachineKeyStateKey, b.store)
}
if !legacyMachineKey.IsZero() && !bytes.Equal(legacyMachineKey[:], b.machinePrivKey[:]) {
b.logf("frontend-provided legacy machine key ignored; used value from server state")
}
return nil
}
if err != ErrStateNotExist {
return fmt.Errorf("error reading %v key of %v: %w", MachineKeyStateKey, b.store, err)
}
// If we didn't find one already on disk and the prefs already
// have a legacy machine key, use that. Otherwise generate a
// new one.
if !legacyMachineKey.IsZero() {
if b.stateKey == "" {
b.logf("using frontend-provided legacy machine key")
} else {
b.logf("using legacy machine key from state key %q", b.stateKey)
}
b.machinePrivKey = legacyMachineKey
} else {
b.logf("generating new machine key")
var err error
b.machinePrivKey, err = wgcfg.NewPrivateKey()
if err != nil {
return fmt.Errorf("initializing new machine key: %w", err)
}
}
keyText, _ = b.machinePrivKey.MarshalText()
if err := b.store.WriteState(MachineKeyStateKey, keyText); err != nil {
b.logf("error writing machine key to store: %v", err)
return err
}
b.logf("machine key written to store")
return nil
}
// writeServerModeStartState stores the ServerModeStartKey value based on the current
// user and prefs. If userID is blank or prefs is blank, no work is done.
//
// b.mu may either be held or not.
func (b *LocalBackend) writeServerModeStartState(userID string, prefs *Prefs) {
if userID == "" || prefs == nil {
return
}
if prefs.ForceDaemon {
stateKey := StateKey("user-" + userID)
if err := b.store.WriteState(ServerModeStartKey, []byte(stateKey)); err != nil {
b.logf("WriteState error: %v", err)
}
// It's important we do this here too, even if it looks
// redundant with the one in the 'if stateKey != ""'
// check block above. That one won't fire in the case
// where the Windows client started up in client mode.
// This happens when we transition into server mode:
if err := b.store.WriteState(stateKey, prefs.ToBytes()); err != nil {
b.logf("WriteState error: %v", err)
}
} else {
if err := b.store.WriteState(ServerModeStartKey, nil); err != nil {
b.logf("WriteState error: %v", err)
}
}
}
// loadStateLocked sets b.prefs and b.stateKey based on a complex
// combination of key, prefs, and legacyPath. b.mu must be held when
// calling.
func (b *LocalBackend) loadStateLocked(key StateKey, prefs *Prefs, legacyPath string) error {
func (b *LocalBackend) loadStateLocked(key StateKey, prefs *Prefs, legacyPath string) (err error) {
if prefs == nil && key == "" {
panic("state key and prefs are both unset")
}
// Optimistically set stateKey (for initMachineKeyLocked's
// logging), but revert it if we return an error so a later SetPrefs
// call can't pick it up if it's bogus.
b.stateKey = key
defer func() {
if err != nil {
b.stateKey = ""
}
}()
if key == "" {
// Frontend fully owns the state, we just need to obey it.
b.logf("Using frontend prefs")
// Frontend owns the state, we just need to obey it.
//
// If the frontend (e.g. on Windows) supplied the
// optional/legacy machine key then it's used as the
// value instead of making up a new one.
b.logf("using frontend prefs: %s", prefs.Pretty())
b.prefs = prefs.Clone()
b.stateKey = ""
if err := b.initMachineKeyLocked(); err != nil {
return fmt.Errorf("initMachineKeyLocked: %w", err)
}
b.writeServerModeStartState(b.userID, b.prefs)
return nil
}
if prefs != nil {
// Backend owns the state, but frontend is trying to migrate
// state into the backend.
b.logf("Importing frontend prefs into backend store")
b.logf("importing frontend prefs into backend store; frontend prefs: %s", prefs.Pretty())
if err := b.store.WriteState(key, prefs.ToBytes()); err != nil {
return fmt.Errorf("store.WriteState: %v", err)
}
}
b.logf("Using backend prefs")
b.logf("using backend prefs")
bs, err := b.store.ReadState(key)
if err != nil {
if errors.Is(err, ErrStateNotExist) {
if legacyPath != "" {
b.prefs, err = LoadPrefs(legacyPath)
if err != nil {
b.logf("Failed to load legacy prefs: %v", err)
if !errors.Is(err, os.ErrNotExist) {
b.logf("failed to load legacy prefs: %v", err)
}
b.prefs = NewPrefs()
} else {
b.logf("Imported state from relaynode for %q", key)
b.logf("imported prefs from relaynode for %q: %v", key, b.prefs.Pretty())
}
} else {
b.prefs = NewPrefs()
b.logf("Created empty state for %q", key)
b.logf("created empty state for %q: %s", key, b.prefs.Pretty())
}
if err := b.initMachineKeyLocked(); err != nil {
return fmt.Errorf("initMachineKeyLocked: %w", err)
}
b.stateKey = key
return nil
}
return fmt.Errorf("store.ReadState(%q): %v", key, err)
@@ -687,7 +876,10 @@ func (b *LocalBackend) loadStateLocked(key StateKey, prefs *Prefs, legacyPath st
if err != nil {
return fmt.Errorf("PrefsFromBytes: %v", err)
}
b.stateKey = key
b.logf("backend prefs for %q: %s", key, b.prefs.Pretty())
if err := b.initMachineKeyLocked(); err != nil {
return fmt.Errorf("initMachineKeyLocked: %w", err)
}
return nil
}
@@ -699,6 +891,12 @@ func (b *LocalBackend) State() State {
return b.state
}
func (b *LocalBackend) InServerMode() bool {
b.mu.Lock()
defer b.mu.Unlock()
return b.inServerMode
}
// getEngineStatus returns a copy of b.engineStatus.
//
// TODO(bradfitz): remove this and use Status() throughout.
@@ -726,7 +924,7 @@ func (b *LocalBackend) Login(token *oauth2.Token) {
func (b *LocalBackend) StartLoginInteractive() {
b.mu.Lock()
b.assertClientLocked()
b.interact++
b.interact = true
url := b.authURL
c := b.c
b.mu.Unlock()
@@ -757,7 +955,7 @@ func (b *LocalBackend) FakeExpireAfter(x time.Duration) {
if e.IsZero() || time.Until(e) > x {
mapCopy.Expiry = time.Now().Add(x)
}
b.netMap = &mapCopy
b.setNetMapLocked(&mapCopy)
b.send(Notify{NetMap: b.netMap})
}
@@ -772,30 +970,33 @@ func (b *LocalBackend) Ping(ipStr string) {
})
}
func (b *LocalBackend) parseWgStatus(s *wgengine.Status) (ret EngineStatus) {
var (
peerStats []string
peerKeys []string
)
// parseWgStatusLocked returns an EngineStatus based on s.
//
// b.mu must be held; mostly because the caller is about to anyway, and doing so
// gives us slightly better guarantees about the two peers stats lines not
// being intermixed if there are concurrent calls to our caller.
func (b *LocalBackend) parseWgStatusLocked(s *wgengine.Status) (ret EngineStatus) {
var peerStats, peerKeys strings.Builder
ret.LiveDERPs = s.DERPs
ret.LivePeers = map[tailcfg.NodeKey]wgengine.PeerStatus{}
for _, p := range s.Peers {
if !p.LastHandshake.IsZero() {
peerStats = append(peerStats, fmt.Sprintf("%d/%d", p.RxBytes, p.TxBytes))
fmt.Fprintf(&peerStats, "%d/%d ", p.RxBytes, p.TxBytes)
fmt.Fprintf(&peerKeys, "%s ", p.NodeKey.ShortString())
ret.NumLive++
ret.LivePeers[p.NodeKey] = p
peerKeys = append(peerKeys, p.NodeKey.ShortString())
}
ret.RBytes += p.RxBytes
ret.WBytes += p.TxBytes
}
if len(peerStats) > 0 {
// [GRINDER STATS LINE] - please don't remove (used for log parsing)
b.keyLogf("peer keys: %s", strings.Join(peerKeys, " "))
// [GRINDER STATS LINE] - please don't remove (used for log parsing)
b.logf("v%v peers: %v", version.LONG, strings.Join(peerStats, " "))
// [GRINDER STATS LINES] - please don't remove (used for log parsing)
if peerStats.Len() > 0 {
b.keyLogf("peer keys: %s", strings.TrimSpace(peerKeys.String()))
b.statsLogf("v%v peers: %v", version.Long, strings.TrimSpace(peerStats.String()))
}
return ret
}
@@ -812,6 +1013,12 @@ func (b *LocalBackend) shieldsAreUp() bool {
return b.prefs.ShieldsUp
}
func (b *LocalBackend) SetCurrentUserID(uid string) {
b.mu.Lock()
b.userID = uid
b.mu.Unlock()
}
func (b *LocalBackend) SetWantRunning(wantRunning bool) {
b.mu.Lock()
new := b.prefs.Clone()
@@ -826,8 +1033,8 @@ func (b *LocalBackend) SetWantRunning(wantRunning bool) {
// SetPrefs saves new user preferences and propagates them throughout
// the system. Implements Backend.
func (b *LocalBackend) SetPrefs(new *Prefs) {
if new == nil {
func (b *LocalBackend) SetPrefs(newp *Prefs) {
if newp == nil {
panic("SetPrefs got nil prefs")
}
@@ -836,51 +1043,64 @@ func (b *LocalBackend) SetPrefs(new *Prefs) {
netMap := b.netMap
stateKey := b.stateKey
old := b.prefs
new.Persist = old.Persist // caller isn't allowed to override this
b.prefs = new
oldp := b.prefs
newp.Persist = oldp.Persist // caller isn't allowed to override this
b.prefs = newp
b.inServerMode = newp.ForceDaemon
// We do this to avoid holding the lock while doing everything else.
new = b.prefs.Clone()
newp = b.prefs.Clone()
oldHi := b.hostinfo
newHi := oldHi.Clone()
newHi.RoutableIPs = append([]wgcfg.CIDR(nil), b.prefs.AdvertiseRoutes...)
applyPrefsToHostinfo(newHi, new)
applyPrefsToHostinfo(newHi, newp)
b.hostinfo = newHi
hostInfoChanged := !oldHi.Equal(newHi)
userID := b.userID
b.mu.Unlock()
if stateKey != "" {
if err := b.store.WriteState(stateKey, new.ToBytes()); err != nil {
if err := b.store.WriteState(stateKey, newp.ToBytes()); err != nil {
b.logf("Failed to save new controlclient state: %v", err)
}
}
b.writeServerModeStartState(userID, newp)
// [GRINDER STATS LINE] - please don't remove (used for log parsing)
b.logf("SetPrefs: %v", new.Pretty())
b.logf("SetPrefs: %v", newp.Pretty())
if netMap != nil {
if login := netMap.UserProfiles[netMap.User].LoginName; login != "" {
if newp.Persist == nil {
b.logf("active login: %s", login)
} else if newp.Persist.LoginName != login {
// Corp issue 461: sometimes the wrong prefs are
// logged; the frontend isn't always getting
// notified (to update its prefs/persist) on
// account switch. Log this while we figure it
// out.
b.logf("active login: %s ([unexpected] corp#461, not %s)", newp.Persist.LoginName)
}
}
}
if old.ShieldsUp != new.ShieldsUp || hostInfoChanged {
if oldp.ShieldsUp != newp.ShieldsUp || hostInfoChanged {
b.doSetHostinfoFilterServices(newHi)
}
b.updateFilter(netMap, new)
b.updateFilter(netMap, newp)
turnDERPOff := new.DisableDERP && !old.DisableDERP
turnDERPOn := !new.DisableDERP && old.DisableDERP
if turnDERPOff {
b.e.SetDERPMap(nil)
} else if turnDERPOn && netMap != nil {
if netMap != nil {
b.e.SetDERPMap(netMap.DERPMap)
}
if old.WantRunning != new.WantRunning {
if oldp.WantRunning != newp.WantRunning {
b.stateMachine()
} else {
b.authReconfig()
}
b.send(Notify{Prefs: new})
b.send(Notify{Prefs: newp})
}
// doSetHostinfoFilterServices calls SetHostinfo on the controlclient,
@@ -933,6 +1153,8 @@ func (b *LocalBackend) authReconfig() {
blocked := b.blocked
uc := b.prefs
nm := b.netMap
hasPAC := b.prevIfState.HasPAC()
disableSubnetsIfPAC := nm != nil && nm.Debug != nil && nm.Debug.DisableSubnetsIfPAC.EqualBool(true)
b.mu.Unlock()
if blocked {
@@ -957,6 +1179,12 @@ func (b *LocalBackend) authReconfig() {
if uc.AllowSingleHosts {
flags |= controlclient.AllowSingleHosts
}
if hasPAC && disableSubnetsIfPAC {
if flags&controlclient.AllowSubnetRoutes != 0 {
b.logf("authReconfig: have PAC; disabling subnet routes")
flags &^= controlclient.AllowSubnetRoutes
}
}
cfg, err := nm.WGCfg(b.logf, flags)
if err != nil {
@@ -1106,6 +1334,7 @@ func (b *LocalBackend) enterState(newState State) {
prefs := b.prefs
notify := b.notify
bc := b.c
networkUp := b.prevIfState.AnyInterfaceUp()
b.mu.Unlock()
if state == newState {
@@ -1118,7 +1347,7 @@ func (b *LocalBackend) enterState(newState State) {
}
if bc != nil {
bc.SetPaused(newState == Stopped)
bc.SetPaused(newState == Stopped || !networkUp)
}
switch newState {
@@ -1246,7 +1475,7 @@ func (b *LocalBackend) requestEngineStatusAndWait() {
func (b *LocalBackend) Logout() {
b.mu.Lock()
c := b.c
b.netMap = nil
b.setNetMapLocked(nil)
b.mu.Unlock()
if c == nil {
@@ -1263,7 +1492,7 @@ func (b *LocalBackend) Logout() {
c.Logout()
b.mu.Lock()
b.netMap = nil
b.setNetMapLocked(nil)
b.mu.Unlock()
b.stateMachine()
@@ -1292,19 +1521,52 @@ func (b *LocalBackend) setNetInfo(ni *tailcfg.NetInfo) {
c.SetNetInfo(ni)
}
func (b *LocalBackend) setNetMapLocked(nm *controlclient.NetworkMap) {
var login string
if nm != nil {
login = nm.UserProfiles[nm.User].LoginName
if login == "" {
login = "<missing-profile>"
}
}
b.netMap = nm
if login != b.activeLogin {
b.logf("active login: %v", login)
b.activeLogin = login
}
}
// TestOnlyPublicKeys returns the current machine and node public
// keys. Used in tests only to facilitate automated node authorization
// in the test harness.
func (b *LocalBackend) TestOnlyPublicKeys() (machineKey tailcfg.MachineKey, nodeKey tailcfg.NodeKey) {
b.mu.Lock()
prefs := b.prefs
machinePrivKey := b.machinePrivKey
b.mu.Unlock()
if prefs == nil {
if prefs == nil || machinePrivKey.IsZero() {
return
}
mk := prefs.Persist.PrivateMachineKey.Public()
mk := machinePrivKey.Public()
nk := prefs.Persist.PrivateNodeKey.Public()
return tailcfg.MachineKey(mk), tailcfg.NodeKey(nk)
}
// temporarilySetMachineKeyInPersist reports whether we should set
// the machine key in Prefs.Persist.LegacyFrontendPrivateMachineKey
// for the frontend to write out to its preferences for use later.
//
// TODO: remove this in Tailscale 1.3.x (so it effectively always
// returns false). It just exists so users can downgrade from 1.2.x to
// 1.0.x. But eventually we want to stop sending the machine key to
// clients. We can't do that until 1.0.x is no longer supported.
func temporarilySetMachineKeyInPersist() bool {
//lint:ignore S1008 for comments
if runtime.GOOS == "darwin" || runtime.GOOS == "android" {
// iOS, macOS, Android users can't downgrade anyway.
return false
}
return true
}

View File

@@ -83,7 +83,9 @@ func TestLocalLogLines(t *testing.T) {
}},
LocalAddrs: []string{"idk an address"},
}
lb.parseWgStatus(status)
lb.mu.Lock()
lb.parseWgStatusLocked(status)
lb.mu.Unlock()
t.Run("after_peers", testWantRemain())
}

View File

@@ -5,6 +5,7 @@
package ipn
import (
"bytes"
"encoding/binary"
"encoding/json"
"errors"
@@ -19,6 +20,8 @@ import (
"tailscale.com/version"
)
var jsonEscapedZero = []byte(`\u0000`)
type NoArgs struct{}
type StartArgs struct {
@@ -80,11 +83,14 @@ func NewBackendServer(logf logger.Logf, b Backend, sendNotifyMsg func(b []byte))
}
func (bs *BackendServer) send(n Notify) {
n.Version = version.LONG
n.Version = version.Long
b, err := json.Marshal(n)
if err != nil {
log.Fatalf("Failed json.Marshal(notify): %v\n%#v", err, n)
}
if bytes.Contains(b, jsonEscapedZero) {
log.Printf("[unexpected] zero byte in BackendServer.send notify message: %q", b)
}
bs.sendNotifyMsg(b)
}
@@ -92,6 +98,17 @@ func (bs *BackendServer) SendErrorMessage(msg string) {
bs.send(Notify{ErrMessage: &msg})
}
// SendInUseOtherUserErrorMessage sends a Notify message to the client that
// both sets the state to 'InUseOtherUser' and sets the associated reason
// to msg.
func (bs *BackendServer) SendInUseOtherUserErrorMessage(msg string) {
inUse := InUseOtherUser
bs.send(Notify{
State: &inUse,
ErrMessage: &msg,
})
}
// GotCommandMsg parses the incoming message b as a JSON Command and
// calls GotCommand with it.
func (bs *BackendServer) GotCommandMsg(b []byte) error {
@@ -106,14 +123,14 @@ func (bs *BackendServer) GotCommandMsg(b []byte) error {
}
func (bs *BackendServer) GotFakeCommand(cmd *Command) error {
cmd.Version = version.LONG
cmd.Version = version.Long
return bs.GotCommand(cmd)
}
func (bs *BackendServer) GotCommand(cmd *Command) error {
if cmd.Version != version.LONG && !cmd.AllowVersionSkew {
if cmd.Version != version.Long && !cmd.AllowVersionSkew {
vs := fmt.Sprintf("GotCommand: Version mismatch! frontend=%#v backend=%#v",
cmd.Version, version.LONG)
cmd.Version, version.Long)
bs.logf("%s", vs)
// ignore the command, but send a message back to the
// caller so it can realize the version mismatch too.
@@ -193,13 +210,16 @@ func (bc *BackendClient) GotNotifyMsg(b []byte) {
// not interesting
return
}
if bytes.Contains(b, jsonEscapedZero) {
log.Printf("[unexpected] zero byte in BackendClient.GotNotifyMsg message: %q", b)
}
n := Notify{}
if err := json.Unmarshal(b, &n); err != nil {
log.Fatalf("BackendClient.Notify: cannot decode message (length=%d)\n%#v", len(b), string(b))
}
if n.Version != version.LONG && !bc.AllowVersionSkew {
if n.Version != version.Long && !bc.AllowVersionSkew {
vs := fmt.Sprintf("GotNotify: Version mismatch! frontend=%#v backend=%#v",
version.LONG, n.Version)
version.Long, n.Version)
bc.logf("%s", vs)
// delete anything in the notification except the version,
// to prevent incorrect operation.
@@ -214,11 +234,14 @@ func (bc *BackendClient) GotNotifyMsg(b []byte) {
}
func (bc *BackendClient) send(cmd Command) {
cmd.Version = version.LONG
cmd.Version = version.Long
b, err := json.Marshal(cmd)
if err != nil {
log.Fatalf("Failed json.Marshal(cmd): %v\n%#v\n", err, cmd)
}
if bytes.Contains(b, jsonEscapedZero) {
log.Printf("[unexpected] zero byte in BackendClient.send command: %q", b)
}
bc.sendCommandMsg(b)
}

View File

@@ -11,6 +11,8 @@ import (
"log"
"os"
"path/filepath"
"runtime"
"strings"
"github.com/tailscale/wireguard-go/wgcfg"
"tailscale.com/atomicfile"
@@ -18,44 +20,57 @@ import (
"tailscale.com/wgengine/router"
)
//go:generate go run tailscale.com/cmd/cloner -type=Prefs -output=prefs_clone.go
// Prefs are the user modifiable settings of the Tailscale node agent.
type Prefs struct {
// ControlURL is the URL of the control server to use.
ControlURL string
// RouteAll specifies whether to accept subnet and default routes
// advertised by other nodes on the Tailscale network.
RouteAll bool
// AllowSingleHosts specifies whether to install routes for each
// node IP on the tailscale network, in addition to a route for
// the whole network.
// This corresponds to the "tailscale up --host-routes" value,
// which defaults to true.
//
// TODO(danderson): why do we have this? It dumps a lot of stuff
// into the routing table, and a single network route _should_ be
// all that we need. But when I turn this off in my tailscaled,
// packets stop flowing. What's up with that?
AllowSingleHosts bool
// CorpDNS specifies whether to install the Tailscale network's
// DNS configuration, if it exists.
CorpDNS bool
// WantRunning indicates whether networking should be active on
// this node.
WantRunning bool
// ShieldsUp indicates whether to block all incoming connections,
// regardless of the control-provided packet filter. If false, we
// use the packet filter as provided. If true, we block incoming
// connections.
ShieldsUp bool
// AdvertiseTags specifies groups that this node wants to join, for
// purposes of ACL enforcement. These can be referenced from the ACL
// security policy. Note that advertising a tag doesn't guarantee that
// the control server will allow you to take on the rights for that
// tag.
AdvertiseTags []string
// Hostname is the hostname to use for identifying the node. If
// not set, os.Hostname is used.
Hostname string
// OSVersion overrides tailcfg.Hostinfo's OSVersion.
OSVersion string
// DeviceModel overrides tailcfg.Hostinfo's DeviceModel.
DeviceModel string
@@ -67,8 +82,17 @@ type Prefs struct {
// users narrow it down a bit.
NotepadURLs bool
// DisableDERP prevents DERP from being used.
DisableDERP bool
// ForceDaemon specifies whether a platform that normally
// operates in "client mode" (that is, requires an active user
// logged in with the GUI app running) should keep running after the
// GUI ends and/or the user logs out.
//
// The only current applicable platform is Windows. This
// forced Windows to go into "server mode" where Tailscale is
// running even with no users logged in. This might also be
// used for macOS in the future. This setting has no effect
// for Linux/etc, which always operate in daemon mode.
ForceDaemon bool `json:"ForceDaemon,omitempty"`
// The following block of options only have an effect on Linux.
@@ -76,6 +100,7 @@ type Prefs struct {
// Tailscale network as reachable through the current
// node.
AdvertiseRoutes []wgcfg.CIDR
// NoSNAT specifies whether to source NAT traffic going to
// destinations in AdvertiseRoutes. The default is to apply source
// NAT, which makes the traffic appear to come from the router
@@ -87,6 +112,7 @@ type Prefs struct {
//
// Linux-only.
NoSNAT bool
// NetfilterMode specifies how much to manage netfilter rules for
// Tailscale, if at all.
NetfilterMode router.NetfilterMode
@@ -102,16 +128,46 @@ type Prefs struct {
// IsEmpty reports whether p is nil or pointing to a Prefs zero value.
func (p *Prefs) IsEmpty() bool { return p == nil || p.Equals(&Prefs{}) }
func (p *Prefs) Pretty() string {
var pp string
if p.Persist != nil {
pp = p.Persist.Pretty()
} else {
pp = "Persist=nil"
func (p *Prefs) Pretty() string { return p.pretty(runtime.GOOS) }
func (p *Prefs) pretty(goos string) string {
var sb strings.Builder
sb.WriteString("Prefs{")
fmt.Fprintf(&sb, "ra=%v ", p.RouteAll)
if !p.AllowSingleHosts {
sb.WriteString("mesh=false ")
}
return fmt.Sprintf("Prefs{ra=%v mesh=%v dns=%v want=%v notepad=%v derp=%v shields=%v routes=%v snat=%v nf=%v %v}",
p.RouteAll, p.AllowSingleHosts, p.CorpDNS, p.WantRunning,
p.NotepadURLs, !p.DisableDERP, p.ShieldsUp, p.AdvertiseRoutes, !p.NoSNAT, p.NetfilterMode, pp)
fmt.Fprintf(&sb, "dns=%v want=%v ", p.CorpDNS, p.WantRunning)
if p.ForceDaemon {
sb.WriteString("server=true ")
}
if p.NotepadURLs {
sb.WriteString("notepad=true ")
}
if p.ShieldsUp {
sb.WriteString("shields=true ")
}
if len(p.AdvertiseRoutes) > 0 || goos == "linux" {
fmt.Fprintf(&sb, "routes=%v ", p.AdvertiseRoutes)
}
if len(p.AdvertiseRoutes) > 0 || p.NoSNAT {
fmt.Fprintf(&sb, "snat=%v ", !p.NoSNAT)
}
if len(p.AdvertiseTags) > 0 {
fmt.Fprintf(&sb, "tags=%s ", strings.Join(p.AdvertiseTags, ","))
}
if goos == "linux" {
fmt.Fprintf(&sb, "nf=%v ", p.NetfilterMode)
}
if p.ControlURL != "" && p.ControlURL != "https://login.tailscale.com" {
fmt.Fprintf(&sb, "url=%q ", p.ControlURL)
}
if p.Persist != nil {
sb.WriteString(p.Persist.Pretty())
} else {
sb.WriteString("Persist=nil")
}
sb.WriteString("}")
return sb.String()
}
func (p *Prefs) ToBytes() []byte {
@@ -137,13 +193,13 @@ func (p *Prefs) Equals(p2 *Prefs) bool {
p.CorpDNS == p2.CorpDNS &&
p.WantRunning == p2.WantRunning &&
p.NotepadURLs == p2.NotepadURLs &&
p.DisableDERP == p2.DisableDERP &&
p.ShieldsUp == p2.ShieldsUp &&
p.NoSNAT == p2.NoSNAT &&
p.NetfilterMode == p2.NetfilterMode &&
p.Hostname == p2.Hostname &&
p.OSVersion == p2.OSVersion &&
p.DeviceModel == p2.DeviceModel &&
p.ForceDaemon == p2.ForceDaemon &&
compareIPNets(p.AdvertiseRoutes, p2.AdvertiseRoutes) &&
compareStrings(p.AdvertiseTags, p2.AdvertiseTags) &&
p.Persist.Equals(p2.Persist)
@@ -213,26 +269,16 @@ func PrefsFromBytes(b []byte, enforceDefaults bool) (*Prefs, error) {
return p, err
}
// Clone returns a deep copy of p.
func (p *Prefs) Clone() *Prefs {
// TODO: write a faster/non-Fatal-y Clone implementation?
p2, err := PrefsFromBytes(p.ToBytes(), false)
if err != nil {
log.Fatalf("Prefs was uncopyable: %v\n", err)
}
return p2
}
// LoadPrefs loads a legacy relaynode config file into Prefs
// with sensible migration defaults set.
func LoadPrefs(filename string) (*Prefs, error) {
data, err := ioutil.ReadFile(filename)
if err != nil {
return nil, fmt.Errorf("loading prefs from %q: %v", filename, err)
return nil, fmt.Errorf("LoadPrefs open: %w", err) // err includes path
}
p, err := PrefsFromBytes(data, false)
if err != nil {
return nil, fmt.Errorf("decoding prefs in %q: %v", filename, err)
return nil, fmt.Errorf("LoadPrefs(%q) decode: %w", filename, err)
}
return p, nil
}

51
ipn/prefs_clone.go Normal file
View File

@@ -0,0 +1,51 @@
// 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.
// Code generated by tailscale.com/cmd/cloner -type Prefs; DO NOT EDIT.
package ipn
import (
"github.com/tailscale/wireguard-go/wgcfg"
"tailscale.com/control/controlclient"
"tailscale.com/wgengine/router"
)
// Clone makes a deep copy of Prefs.
// The result aliases no memory with the original.
func (src *Prefs) Clone() *Prefs {
if src == nil {
return nil
}
dst := new(Prefs)
*dst = *src
dst.AdvertiseTags = append(src.AdvertiseTags[:0:0], src.AdvertiseTags...)
dst.AdvertiseRoutes = append(src.AdvertiseRoutes[:0:0], src.AdvertiseRoutes...)
if dst.Persist != nil {
dst.Persist = new(controlclient.Persist)
*dst.Persist = *src.Persist
}
return dst
}
// A compilation failure here means this code must be regenerated, with command:
// tailscale.com/cmd/cloner -type Prefs
var _PrefsNeedsRegeneration = Prefs(struct {
ControlURL string
RouteAll bool
AllowSingleHosts bool
CorpDNS bool
WantRunning bool
ShieldsUp bool
AdvertiseTags []string
Hostname string
OSVersion string
DeviceModel string
NotepadURLs bool
ForceDaemon bool
AdvertiseRoutes []wgcfg.CIDR
NoSNAT bool
NetfilterMode router.NetfilterMode
Persist *controlclient.Persist
}{})

View File

@@ -5,8 +5,12 @@
package ipn
import (
"errors"
"fmt"
"os"
"reflect"
"testing"
"time"
"github.com/tailscale/wireguard-go/wgcfg"
"tailscale.com/control/controlclient"
@@ -24,7 +28,7 @@ func fieldsOf(t reflect.Type) (fields []string) {
func TestPrefsEqual(t *testing.T) {
tstest.PanicOnLog()
prefsHandles := []string{"ControlURL", "RouteAll", "AllowSingleHosts", "CorpDNS", "WantRunning", "ShieldsUp", "AdvertiseTags", "Hostname", "OSVersion", "DeviceModel", "NotepadURLs", "DisableDERP", "AdvertiseRoutes", "NoSNAT", "NetfilterMode", "Persist"}
prefsHandles := []string{"ControlURL", "RouteAll", "AllowSingleHosts", "CorpDNS", "WantRunning", "ShieldsUp", "AdvertiseTags", "Hostname", "OSVersion", "DeviceModel", "NotepadURLs", "ForceDaemon", "AdvertiseRoutes", "NoSNAT", "NetfilterMode", "Persist"}
if have := fieldsOf(reflect.TypeOf(Prefs{})); !reflect.DeepEqual(have, prefsHandles) {
t.Errorf("Prefs.Equal check might be out of sync\nfields: %q\nhandled: %q\n",
have, prefsHandles)
@@ -278,3 +282,92 @@ func TestPrefsPersist(t *testing.T) {
}
checkPrefs(t, p)
}
func TestPrefsPretty(t *testing.T) {
tests := []struct {
p Prefs
os string
want string
}{
{
Prefs{},
"linux",
"Prefs{ra=false mesh=false dns=false want=false routes=[] nf=off Persist=nil}",
},
{
Prefs{},
"windows",
"Prefs{ra=false mesh=false dns=false want=false Persist=nil}",
},
{
Prefs{ShieldsUp: true},
"windows",
"Prefs{ra=false mesh=false dns=false want=false shields=true Persist=nil}",
},
{
Prefs{AllowSingleHosts: true},
"windows",
"Prefs{ra=false dns=false want=false Persist=nil}",
},
{
Prefs{
NotepadURLs: true,
AllowSingleHosts: true,
},
"windows",
"Prefs{ra=false dns=false want=false notepad=true Persist=nil}",
},
{
Prefs{
AllowSingleHosts: true,
WantRunning: true,
ForceDaemon: true, // server mode
},
"windows",
"Prefs{ra=false dns=false want=true server=true Persist=nil}",
},
{
Prefs{
AllowSingleHosts: true,
WantRunning: true,
ControlURL: "http://localhost:1234",
AdvertiseTags: []string{"tag:foo", "tag:bar"},
},
"darwin",
`Prefs{ra=false dns=false want=true tags=tag:foo,tag:bar url="http://localhost:1234" Persist=nil}`,
},
{
Prefs{
Persist: &controlclient.Persist{},
},
"linux",
`Prefs{ra=false mesh=false dns=false want=false routes=[] nf=off Persist{lm=, o=, n= u=""}}`,
},
{
Prefs{
Persist: &controlclient.Persist{
PrivateNodeKey: wgcfg.PrivateKey{1: 1},
},
},
"linux",
`Prefs{ra=false mesh=false dns=false want=false routes=[] nf=off Persist{lm=, o=, n=[B1VKl] u=""}}`,
},
}
for i, tt := range tests {
got := tt.p.pretty(tt.os)
if got != tt.want {
t.Errorf("%d. wrong String:\n got: %s\nwant: %s\n", i, got, tt.want)
}
}
}
func TestLoadPrefsNotExist(t *testing.T) {
bogusFile := fmt.Sprintf("/tmp/not-exist-%d", time.Now().UnixNano())
p, err := LoadPrefs(bogusFile)
if errors.Is(err, os.ErrNotExist) {
// expected.
return
}
t.Fatalf("unexpected prefs=%#v, err=%v", p, err)
}

View File

@@ -5,9 +5,12 @@
package ipn
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"log"
"os"
"path/filepath"
"sync"
@@ -19,6 +22,30 @@ import (
// requested state ID doesn't exist.
var ErrStateNotExist = errors.New("no state with given ID")
const (
// MachineKeyStateKey is the key under which we store the machine key,
// in its wgcfg.PrivateKey.MarshalText representation.
MachineKeyStateKey = StateKey("_machinekey")
// GlobalDaemonStateKey is the ipn.StateKey that tailscaled
// loads on startup.
//
// We have to support multiple state keys for other OSes (Windows in
// particular), but right now Unix daemons run with a single
// node-global state. To keep open the option of having per-user state
// later, the global state key doesn't look like a username.
GlobalDaemonStateKey = StateKey("_daemon")
// ServerModeStartKey's value, if non-empty, is the value of a
// StateKey containing the prefs to start with which to start the
// server.
//
// For example, the value might be "user-1234", meaning the
// the server should start with the Prefs JSON loaded from
// StateKey "user-1234".
ServerModeStartKey = StateKey("server-mode-start-key")
)
// StateStore persists state, and produces it back on request.
type StateStore interface {
// ReadState returns the bytes associated with ID. Returns (nil,
@@ -34,6 +61,8 @@ type MemoryStore struct {
cache map[StateKey][]byte
}
func (s *MemoryStore) String() string { return "MemoryStore" }
// ReadState implements the StateStore interface.
func (s *MemoryStore) ReadState(id StateKey) ([]byte, error) {
s.mu.Lock()
@@ -67,9 +96,19 @@ type FileStore struct {
cache map[StateKey][]byte
}
func (s *FileStore) String() string { return fmt.Sprintf("FileStore(%q)", s.path) }
// NewFileStore returns a new file store that persists to path.
func NewFileStore(path string) (*FileStore, error) {
bs, err := ioutil.ReadFile(path)
// Treat an empty file as a missing file.
// (https://github.com/tailscale/tailscale/issues/895#issuecomment-723255589)
if err == nil && len(bs) == 0 {
log.Printf("ipn.NewFileStore(%q): file empty; treating it like a missing file [warning]", path)
err = os.ErrNotExist
}
if err != nil {
if os.IsNotExist(err) {
// Write out an initial file, to verify that we can write
@@ -112,6 +151,9 @@ func (s *FileStore) ReadState(id StateKey) ([]byte, error) {
func (s *FileStore) WriteState(id StateKey, bs []byte) error {
s.mu.Lock()
defer s.mu.Unlock()
if bytes.Equal(s.cache[id], bs) {
return nil
}
s.cache[id] = append([]byte(nil), bs...)
bs, err := json.MarshalIndent(s.cache, "", " ")
if err != nil {

199
log/filelogger/log.go Normal file
View File

@@ -0,0 +1,199 @@
// 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 filelogger provides localdisk log writing & rotation, primarily for Windows
// clients. (We get this for free on other platforms.)
package filelogger
import (
"bytes"
"fmt"
"io/ioutil"
"log"
"os"
"path/filepath"
"runtime"
"strings"
"sync"
"time"
"tailscale.com/types/logger"
)
const (
maxSize = 100 << 20
maxFiles = 50
)
// New returns a logf wrapper that appends to local disk log
// files on Windows, rotating old log files as needed to stay under
// file count & byte limits.
func New(fileBasePrefix, logID string, logf logger.Logf) logger.Logf {
if runtime.GOOS != "windows" {
panic("not yet supported on any platform except Windows")
}
if logf == nil {
panic("nil logf")
}
dir := filepath.Join(os.Getenv("LocalAppData"), "Tailscale", "Logs")
if err := os.MkdirAll(dir, 0700); err != nil {
log.Printf("failed to create local log directory; not writing logs to disk: %v", err)
return logf
}
logf("local disk logdir: %v", dir)
lfw := &logFileWriter{
fileBasePrefix: fileBasePrefix,
logID: logID,
dir: dir,
wrappedLogf: logf,
}
return lfw.Logf
}
// logFileWriter is the state for the log writer & rotator.
type logFileWriter struct {
dir string // e.g. `C:\Users\FooBarUser\AppData\Local\Tailscale\Logs`
logID string // hex logID
fileBasePrefix string // e.g. "tailscale-service" or "tailscale-gui"
wrappedLogf logger.Logf // underlying logger to send to
mu sync.Mutex // guards following
buf bytes.Buffer // scratch buffer to avoid allocs
fday civilDay // day that f was opened; zero means no file yet open
f *os.File // file currently opened for append
}
// civilDay is a year, month, and day in the local timezone.
// It's a comparable value type.
type civilDay struct {
year int
month time.Month
day int
}
func dayOf(t time.Time) civilDay {
return civilDay{t.Year(), t.Month(), t.Day()}
}
func (w *logFileWriter) Logf(format string, a ...interface{}) {
w.mu.Lock()
defer w.mu.Unlock()
w.buf.Reset()
fmt.Fprintf(&w.buf, format, a...)
if w.buf.Len() == 0 {
return
}
out := w.buf.Bytes()
w.wrappedLogf("%s", out)
// Make sure there's a final newline before we write to the log file.
if out[len(out)-1] != '\n' {
w.buf.WriteByte('\n')
out = w.buf.Bytes()
}
w.appendToFileLocked(out)
}
// out should end in a newline.
// w.mu must be held.
func (w *logFileWriter) appendToFileLocked(out []byte) {
now := time.Now()
day := dayOf(now)
if w.fday != day {
w.startNewFileLocked()
}
if w.f != nil {
// RFC3339Nano but with a fixed number (3) of nanosecond digits:
const formatPre = "2006-01-02T15:04:05"
const formatPost = "Z07:00"
fmt.Fprintf(w.f, "%s.%03d%s: %s",
now.Format(formatPre),
now.Nanosecond()/int(time.Millisecond/time.Nanosecond),
now.Format(formatPost),
out)
}
}
// startNewFileLocked opens a new log file for writing
// and also cleans up any old files.
//
// w.mu must be held.
func (w *logFileWriter) startNewFileLocked() {
var oldName string
if w.f != nil {
oldName = filepath.Base(w.f.Name())
w.f.Close()
w.f = nil
w.fday = civilDay{}
}
w.cleanLocked()
now := time.Now()
day := dayOf(now)
name := filepath.Join(w.dir, fmt.Sprintf("%s-%04d%02d%02dT%02d%02d%02d-%d.txt",
w.fileBasePrefix,
day.year,
day.month,
day.day,
now.Hour(),
now.Minute(),
now.Second(),
now.Unix()))
var err error
w.f, err = os.Create(name)
if err != nil {
w.wrappedLogf("failed to create log file: %v", err)
return
}
if oldName != "" {
fmt.Fprintf(w.f, "(logID %q; continued from log file %s)\n", w.logID, oldName)
} else {
fmt.Fprintf(w.f, "(logID %q)\n", w.logID)
}
w.fday = day
}
// cleanLocked cleans up old log files.
//
// w.mu must be held.
func (w *logFileWriter) cleanLocked() {
fis, _ := ioutil.ReadDir(w.dir)
prefix := w.fileBasePrefix + "-"
fileSize := map[string]int64{}
var files []string
var sumSize int64
for _, fi := range fis {
baseName := filepath.Base(fi.Name())
if !strings.HasPrefix(baseName, prefix) {
continue
}
size := fi.Size()
fileSize[baseName] = size
sumSize += size
files = append(files, baseName)
}
if sumSize > maxSize {
w.wrappedLogf("cleaning log files; sum byte count %d > %d", sumSize, maxSize)
}
if len(files) > maxFiles {
w.wrappedLogf("cleaning log files; number of files %d > %d", len(files), maxFiles)
}
for (sumSize > maxSize || len(files) > maxFiles) && len(files) > 0 {
target := files[0]
files = files[1:]
targetSize := fileSize[target]
targetFull := filepath.Join(w.dir, target)
err := os.Remove(targetFull)
if err != nil {
w.wrappedLogf("error cleaning log file: %v", err)
} else {
sumSize -= targetSize
w.wrappedLogf("cleaned log file %s (size %d); new bytes=%v, files=%v", targetFull, targetSize, sumSize, len(files))
}
}
}

View File

@@ -315,6 +315,9 @@ func New(collection string) *Policy {
} else {
lflags = log.LstdFlags
}
if v, _ := strconv.ParseBool(os.Getenv("TS_DEBUG_LOG_TIME")); v {
lflags = log.LstdFlags | log.Lmicroseconds
}
if runningUnderSystemd() {
// If journalctl is going to prepend its own timestamp
// anyway, no need to add one.
@@ -356,7 +359,7 @@ func New(collection string) *Policy {
newc.PrivateID = logtail.PrivateID{}
newc.Collection = collection
}
if newc.PrivateID == (logtail.PrivateID{}) {
if newc.PrivateID.IsZero() {
newc.PrivateID, err = logtail.NewPrivateID()
if err != nil {
log.Fatalf("logpolicy: NewPrivateID() should never fail")
@@ -392,7 +395,7 @@ func New(collection string) *Policy {
log.SetOutput(lw)
log.Printf("Program starting: v%v, Go %v: %#v",
version.LONG,
version.Long,
strings.TrimPrefix(runtime.Version(), "go"),
os.Args)
log.Printf("LogID: %v", newc.PublicID)

View File

@@ -8,7 +8,7 @@ import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"runtime"
"strings"
"testing"
"unicode"
@@ -56,19 +56,8 @@ func (f *filchTest) close(t *testing.T) {
}
}
func genFilePrefix(t *testing.T) (dir, prefix string) {
t.Helper()
dir, err := ioutil.TempDir("", "filch")
if err != nil {
t.Fatal(err)
}
return dir, filepath.Join(dir, "ringbuffer-")
}
func TestQueue(t *testing.T) {
td, filePrefix := genFilePrefix(t)
defer os.RemoveAll(td)
filePrefix := t.TempDir()
f := newFilchTest(t, filePrefix, Options{ReplaceStderr: false})
f.readEOF(t)
@@ -90,8 +79,7 @@ func TestQueue(t *testing.T) {
func TestRecover(t *testing.T) {
t.Run("empty", func(t *testing.T) {
td, filePrefix := genFilePrefix(t)
defer os.RemoveAll(td)
filePrefix := t.TempDir()
f := newFilchTest(t, filePrefix, Options{ReplaceStderr: false})
f.write(t, "hello")
f.read(t, "hello")
@@ -104,8 +92,7 @@ func TestRecover(t *testing.T) {
})
t.Run("cur", func(t *testing.T) {
td, filePrefix := genFilePrefix(t)
defer os.RemoveAll(td)
filePrefix := t.TempDir()
f := newFilchTest(t, filePrefix, Options{ReplaceStderr: false})
f.write(t, "hello")
f.close(t)
@@ -123,8 +110,7 @@ func TestRecover(t *testing.T) {
filch_test.go:129: r.ReadLine()="hello", want "world"
*/
td, filePrefix := genFilePrefix(t)
defer os.RemoveAll(td)
filePrefix := t.TempDir()
f := newFilchTest(t, filePrefix, Options{ReplaceStderr: false})
f.write(t, "hello")
f.read(t, "hello")
@@ -143,6 +129,14 @@ func TestRecover(t *testing.T) {
}
func TestFilchStderr(t *testing.T) {
if runtime.GOOS == "windows" {
// TODO(bradfitz): this is broken on Windows but not
// fully sure why. Investigate. But notably, the
// stderrFD variable (defined in filch.go) and set
// below is only ever read in filch_unix.go. So just
// skip this for test for now.
t.Skip("test broken on Windows")
}
pipeR, pipeW, err := os.Pipe()
if err != nil {
t.Fatal(err)
@@ -155,8 +149,7 @@ func TestFilchStderr(t *testing.T) {
stderrFD = 2
}()
td, filePrefix := genFilePrefix(t)
defer os.RemoveAll(td)
filePrefix := t.TempDir()
f := newFilchTest(t, filePrefix, Options{ReplaceStderr: true})
f.write(t, "hello")
if _, err := fmt.Fprintf(pipeW, "filch\n"); err != nil {

View File

@@ -10,6 +10,7 @@ import (
"net"
"net/http"
"reflect"
"sort"
"strings"
"inet.af/netaddr"
@@ -160,10 +161,11 @@ type State struct {
InterfaceUp map[string]bool
// HaveV6Global is whether this machine has an IPv6 global address
// on some interface.
// on some non-Tailscale interface that's up.
HaveV6Global bool
// HaveV4 is whether the machine has some non-localhost IPv4 address.
// HaveV4 is whether the machine has some non-localhost,
// non-link-local IPv4 address on a non-Tailscale interface that's up.
HaveV4 bool
// IsExpensive is whether the current network interface is
@@ -173,30 +175,104 @@ type State struct {
// 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.
DefaultRouteInterface string
// HTTPProxy is the HTTP proxy to use.
HTTPProxy string
// PAC is the URL to the Proxy Autoconfig URL, if applicable.
PAC string
}
func (s *State) String() string {
var sb strings.Builder
fmt.Fprintf(&sb, "interfaces.State{defaultRoute=%v ifs={", s.DefaultRouteInterface)
ifs := make([]string, 0, len(s.InterfaceUp))
for k := range s.InterfaceUp {
if allLoopbackIPs(s.InterfaceIPs[k]) {
continue
}
ifs = append(ifs, k)
}
sort.Slice(ifs, func(i, j int) bool {
upi, upj := s.InterfaceUp[ifs[i]], s.InterfaceUp[ifs[j]]
if upi != upj {
// Up sorts before down.
return upi
}
return ifs[i] < ifs[j]
})
for i, ifName := range ifs {
if i > 0 {
sb.WriteString(" ")
}
if s.InterfaceUp[ifName] {
fmt.Fprintf(&sb, "%s:[", ifName)
needSpace := false
for _, ip := range s.InterfaceIPs[ifName] {
if ip.IsLinkLocalUnicast() {
continue
}
if needSpace {
sb.WriteString(" ")
}
fmt.Fprintf(&sb, "%s", ip)
needSpace = true
}
sb.WriteString("]")
} else {
fmt.Fprintf(&sb, "%s:down", ifName)
}
}
sb.WriteString("}")
if s.IsExpensive {
sb.WriteString(" expensive")
}
if s.HTTPProxy != "" {
fmt.Fprintf(&sb, " httpproxy=%s", s.HTTPProxy)
}
if s.PAC != "" {
fmt.Fprintf(&sb, " pac=%s", s.PAC)
}
fmt.Fprintf(&sb, " v4=%v v6global=%v}", s.HaveV4, s.HaveV6Global)
return sb.String()
}
func (s *State) Equal(s2 *State) bool {
return reflect.DeepEqual(s, s2)
}
func (s *State) HasPAC() bool { return s != nil && s.PAC != "" }
// AnyInterfaceUp reports whether any interface seems like it has Internet access.
func (s *State) AnyInterfaceUp() bool {
return s != nil && (s.HaveV4 || s.HaveV6Global)
}
// RemoveTailscaleInterfaces modifes s to remove any interfaces that
// are owned by this process. (TODO: make this true; currently it
// makes the Linux-only assumption that the interface is named
// /^tailscale/)
func (s *State) RemoveTailscaleInterfaces() {
for name := range s.InterfaceIPs {
if name == "Tailscale" || // as it is on Windows
strings.HasPrefix(name, "tailscale") { // TODO: use --tun flag value, etc; see TODO in method doc
if isTailscaleInterfaceName(name) {
delete(s.InterfaceIPs, name)
delete(s.InterfaceUp, name)
}
}
}
func isTailscaleInterfaceName(name string) bool {
return name == "Tailscale" || // as it is on Windows
strings.HasPrefix(name, "tailscale") // TODO: use --tun flag value, etc; see TODO in method doc
}
// getPAC, if non-nil, returns the current PAC file URL.
var getPAC func() string
// GetState returns the state of all the current machine's network interfaces.
//
// It does not set the returned State.IsExpensive. The caller can populate that.
@@ -206,21 +282,29 @@ func GetState() (*State, error) {
InterfaceUp: make(map[string]bool),
}
if err := ForeachInterfaceAddress(func(ni Interface, ip netaddr.IP) {
ifUp := ni.IsUp()
s.InterfaceIPs[ni.Name] = append(s.InterfaceIPs[ni.Name], ip)
s.InterfaceUp[ni.Name] = ni.IsUp()
s.HaveV6Global = s.HaveV6Global || isGlobalV6(ip)
s.HaveV4 = s.HaveV4 || (ip.Is4() && !ip.IsLoopback())
s.InterfaceUp[ni.Name] = ifUp
if ifUp && !ip.IsLoopback() && !ip.IsLinkLocalUnicast() && !isTailscaleInterfaceName(ni.Name) {
s.HaveV6Global = s.HaveV6Global || isGlobalV6(ip)
s.HaveV4 = s.HaveV4 || ip.Is4()
}
}); err != nil {
return nil, err
}
s.DefaultRouteInterface, _ = DefaultRouteInterface()
req, err := http.NewRequest("GET", LoginEndpointForProxyDetermination, nil)
if err != nil {
return nil, err
}
if u, err := tshttpproxy.ProxyFromEnvironment(req); err == nil && u != nil {
s.HTTPProxy = u.String()
if s.AnyInterfaceUp() {
req, err := http.NewRequest("GET", LoginEndpointForProxyDetermination, nil)
if err != nil {
return nil, err
}
if u, err := tshttpproxy.ProxyFromEnvironment(req); err == nil && u != nil {
s.HTTPProxy = u.String()
}
if getPAC != nil {
s.PAC = getPAC()
}
}
return s, nil
@@ -312,3 +396,15 @@ var (
linkLocalIPv4 = mustCIDR("169.254.0.0/16")
v6Global1 = mustCIDR("2000::/3")
)
func allLoopbackIPs(ips []netaddr.IP) bool {
if len(ips) == 0 {
return false
}
for _, ip := range ips {
if !ip.IsLoopback() {
return false
}
}
return true
}

View File

@@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build !linux
// +build !linux,!windows
package interfaces

View File

@@ -35,6 +35,7 @@ func TestGetState(t *testing.T) {
t.Fatal(err)
}
t.Logf("Got: %#v", st)
t.Logf("As string: %s", st)
st2, err := GetState()
if err != nil {
@@ -46,6 +47,9 @@ func TestGetState(t *testing.T) {
// the two GetState calls.
t.Fatal("two States back-to-back were not equal")
}
st.RemoveTailscaleInterfaces()
t.Logf("As string without Tailscale:\n\t%s", st)
}
func TestLikelyHomeRouterIP(t *testing.T) {

View File

@@ -5,11 +5,16 @@
package interfaces
import (
"fmt"
"log"
"net/url"
"os/exec"
"syscall"
"unsafe"
"github.com/tailscale/winipcfg-go"
"go4.org/mem"
"golang.org/x/sys/windows"
"golang.zx2c4.com/wireguard/windows/tunnel/winipcfg"
"inet.af/netaddr"
"tailscale.com/tsconst"
"tailscale.com/util/lineread"
@@ -17,6 +22,7 @@ import (
func init() {
likelyHomeRouterIP = likelyHomeRouterIPWindows
getPAC = getPACWindows
}
/*
@@ -76,19 +82,115 @@ func likelyHomeRouterIPWindows() (ret netaddr.IP, ok bool) {
// NonTailscaleMTUs returns a map of interface LUID to interface MTU,
// for all interfaces except Tailscale tunnels.
func NonTailscaleMTUs() (map[uint64]uint32, error) {
ifs, err := winipcfg.GetInterfaces()
func NonTailscaleMTUs() (map[winipcfg.LUID]uint32, error) {
mtus := map[winipcfg.LUID]uint32{}
ifs, err := NonTailscaleInterfaces()
for luid, iface := range ifs {
mtus[luid] = iface.MTU
}
return mtus, err
}
// NonTailscaleInterfaces returns a map of interface LUID to interface
// for all interfaces except Tailscale tunnels.
func NonTailscaleInterfaces() (map[winipcfg.LUID]*winipcfg.IPAdapterAddresses, error) {
ifs, err := winipcfg.GetAdaptersAddresses(windows.AF_UNSPEC, winipcfg.GAAFlagIncludeAllInterfaces)
if err != nil {
return nil, err
}
ret := map[uint64]uint32{}
ret := map[winipcfg.LUID]*winipcfg.IPAdapterAddresses{}
for _, iface := range ifs {
if iface.Description == tsconst.WintunInterfaceDesc {
if iface.Description() == tsconst.WintunInterfaceDesc {
continue
}
ret[iface.Luid] = iface.Mtu
ret[iface.LUID] = iface
}
return ret, nil
}
// GetWindowsDefault returns the interface that has the non-Tailscale
// default route for the given address family.
//
// It returns (nil, nil) if no interface is found.
func GetWindowsDefault(family winipcfg.AddressFamily) (*winipcfg.IPAdapterAddresses, error) {
ifs, err := NonTailscaleInterfaces()
if err != nil {
return nil, err
}
routes, err := winipcfg.GetIPForwardTable2(family)
if err != nil {
return nil, err
}
bestMetric := ^uint32(0)
var bestIface *winipcfg.IPAdapterAddresses
for _, route := range routes {
iface := ifs[route.InterfaceLUID]
if route.DestinationPrefix.PrefixLength != 0 || iface == nil {
continue
}
if iface.OperStatus == winipcfg.IfOperStatusUp && route.Metric < bestMetric {
bestMetric = route.Metric
bestIface = iface
}
}
return bestIface, nil
}
func DefaultRouteInterface() (string, error) {
iface, err := GetWindowsDefault(windows.AF_INET)
if err != nil {
return "", err
}
if iface == nil {
return "(none)", nil
}
return fmt.Sprintf("%s (%s)", iface.FriendlyName(), iface.Description()), nil
}
var (
winHTTP = windows.NewLazySystemDLL("winhttp.dll")
detectAutoProxyConfigURL = winHTTP.NewProc("WinHttpDetectAutoProxyConfigUrl")
kernel32 = windows.NewLazySystemDLL("kernel32.dll")
globalFree = kernel32.NewProc("GlobalFree")
)
const (
winHTTP_AUTO_DETECT_TYPE_DHCP = 0x00000001
winHTTP_AUTO_DETECT_TYPE_DNS_A = 0x00000002
)
func getPACWindows() string {
var res *uint16
r, _, e := detectAutoProxyConfigURL.Call(
winHTTP_AUTO_DETECT_TYPE_DHCP|winHTTP_AUTO_DETECT_TYPE_DNS_A,
uintptr(unsafe.Pointer(&res)),
)
if r == 1 {
if res == nil {
log.Printf("getPACWindows: unexpected success with nil result")
return ""
}
defer globalFree.Call(uintptr(unsafe.Pointer(res)))
s := windows.UTF16PtrToString(res)
if _, err := url.Parse(s); err != nil {
log.Printf("getPACWindows: invalid URL %q from winhttp; ignoring", s)
return ""
}
return s
}
const (
ERROR_WINHTTP_AUTODETECTION_FAILED = 12180
)
if e == syscall.Errno(ERROR_WINHTTP_AUTODETECTION_FAILED) {
// Common case on networks without advertised PAC.
return ""
}
log.Printf("getPACWindows: %T=%v", e, e) // syscall.Errno=0x....
return ""
}

View File

@@ -0,0 +1,17 @@
// 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 interfaces
import "testing"
func BenchmarkGetPACWindows(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
v := getPACWindows()
if i == 0 {
b.Logf("Got: %q", v)
}
}
}

View File

@@ -156,6 +156,15 @@ type Client struct {
// GetSTUNConn6 is like GetSTUNConn4, but for IPv6.
GetSTUNConn6 func() STUNConn
// SkipExternalNetwork controls whether the client should not try
// to reach things other than localhost. This is set to true
// in tests to avoid probing the local LAN's router, etc.
SkipExternalNetwork bool
// UDPBindAddr, if non-empty, is the address to listen on for UDP.
// It defaults to ":0".
UDPBindAddr string
mu sync.Mutex // guards following
nextFull bool // do a full region scan, even if last != nil
prev map[time.Time]*Report // some previous reports
@@ -772,6 +781,13 @@ func newReport() *Report {
}
}
func (c *Client) udpBindAddr() string {
if v := c.UDPBindAddr; v != "" {
return v
}
return ":0"
}
// GetReport gets a report.
//
// It may not be called concurrently with itself.
@@ -831,8 +847,10 @@ func (c *Client) GetReport(ctx context.Context, dm *tailcfg.DERPMap) (*Report, e
}
defer rs.pc4Hair.Close()
rs.waitPortMap.Add(1)
go rs.probePortMapServices()
if !c.SkipExternalNetwork {
rs.waitPortMap.Add(1)
go rs.probePortMapServices()
}
// At least the Apple Airport Extreme doesn't allow hairpin
// sends from a private socket until it's seen traffic from
@@ -850,7 +868,7 @@ func (c *Client) GetReport(ctx context.Context, dm *tailcfg.DERPMap) (*Report, e
if f := c.GetSTUNConn4; f != nil {
rs.pc4 = f()
} else {
u4, err := netns.Listener().ListenPacket(ctx, "udp4", ":0")
u4, err := netns.Listener().ListenPacket(ctx, "udp4", c.udpBindAddr())
if err != nil {
c.logf("udp4: %v", err)
return nil, err
@@ -863,7 +881,7 @@ func (c *Client) GetReport(ctx context.Context, dm *tailcfg.DERPMap) (*Report, e
if f := c.GetSTUNConn6; f != nil {
rs.pc6 = f()
} else {
u6, err := netns.Listener().ListenPacket(ctx, "udp6", ":0")
u6, err := netns.Listener().ListenPacket(ctx, "udp6", c.udpBindAddr())
if err != nil {
c.logf("udp6: %v", err)
} else {
@@ -902,8 +920,10 @@ func (c *Client) GetReport(ctx context.Context, dm *tailcfg.DERPMap) (*Report, e
rs.waitHairCheck(ctx)
c.vlogf("hairCheck done")
rs.waitPortMap.Wait()
c.vlogf("portMap done")
if !c.SkipExternalNetwork {
rs.waitPortMap.Wait()
c.vlogf("portMap done")
}
rs.stopTimers()
// Try HTTPS latency check if all STUN probes failed due to UDP presumably being blocked.

View File

@@ -50,7 +50,8 @@ func TestBasic(t *testing.T) {
defer cleanup()
c := &Client{
Logf: t.Logf,
Logf: t.Logf,
UDPBindAddr: "127.0.0.1:0",
}
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)

View File

@@ -5,18 +5,37 @@
package netns
import (
"encoding/binary"
"math/bits"
"strings"
"syscall"
"unsafe"
"github.com/tailscale/winipcfg-go"
"golang.org/x/sys/windows"
"golang.zx2c4.com/wireguard/windows/tunnel/winipcfg"
"tailscale.com/net/interfaces"
"tailscale.com/util/endian"
)
func interfaceIndex(iface *winipcfg.IPAdapterAddresses) uint32 {
if iface == nil {
// The zero ifidx means "unspecified". If we end up passing zero
// to bindSocket*(), it unsets the binding and lets the socket
// behave as normal again, which is what we want if there's no
// default route we can use.
return 0
}
return iface.IfIndex
}
// control binds c to the Windows interface that holds a default
// route, and is not the Tailscale WinTun interface.
func control(network, address string, c syscall.RawConn) error {
if strings.HasPrefix(address, "127.") {
// Don't bind to an interface for localhost connections,
// otherwise we get:
// connectex: The requested address is not valid in its context
// (The derphttp tests were failing)
return nil
}
canV4, canV6 := false, false
switch network {
case "tcp", "udp":
@@ -28,21 +47,21 @@ func control(network, address string, c syscall.RawConn) error {
}
if canV4 {
if4, err := getDefaultInterface(winipcfg.AF_INET)
iface, err := interfaces.GetWindowsDefault(windows.AF_INET)
if err != nil {
return err
}
if err := bindSocket4(c, if4); err != nil {
if err := bindSocket4(c, interfaceIndex(iface)); err != nil {
return err
}
}
if canV6 {
if6, err := getDefaultInterface(winipcfg.AF_INET6)
iface, err := interfaces.GetWindowsDefault(windows.AF_INET6)
if err != nil {
return err
}
if err := bindSocket6(c, if6); err != nil {
if err := bindSocket6(c, interfaceIndex(iface)); err != nil {
return err
}
}
@@ -50,49 +69,27 @@ func control(network, address string, c syscall.RawConn) error {
return nil
}
// getDefaultInterface returns the index of the interface that has the
// non-Tailscale default route for the given address family.
func getDefaultInterface(family winipcfg.AddressFamily) (ifidx uint32, err error) {
ifs, err := interfaces.NonTailscaleMTUs()
if err != nil {
return 0, err
}
routes, err := winipcfg.GetRoutes(family)
if err != nil {
return 0, err
}
bestMetric := ^uint32(0)
// The zero index means "unspecified". If we end up passing zero
// to bindSocket*(), it unsets the binding and lets the socket
// behave as normal again, which is what we want if there's no
// default route we can use.
var index uint32
for _, route := range routes {
if route.DestinationPrefix.PrefixLength != 0 || ifs[route.InterfaceLuid] == 0 {
continue
}
if route.Metric < bestMetric {
bestMetric = route.Metric
index = route.InterfaceIndex
}
}
return index, nil
}
// sockoptBoundInterface is the value of IP_UNICAST_IF and IPV6_UNICAST_IF.
//
// See https://docs.microsoft.com/en-us/windows/win32/winsock/ipproto-ip-socket-options
// and https://docs.microsoft.com/en-us/windows/win32/winsock/ipproto-ipv6-socket-options
const sockoptBoundInterface = 31
// bindSocket4 binds the given RawConn to the network interface with
// index ifidx, for IPv4 traffic only.
func bindSocket4(c syscall.RawConn, ifidx uint32) error {
// For v4 the interface index must be passed as a big-endian
// integer, regardless of platform endianness.
index := nativeToBigEndian(ifidx)
// For IPv4 (but NOT IPv6) the interface index must be passed
// as a big-endian integer (regardless of platform endianness)
// because the underlying sockopt takes either an IPv4 address
// or an index shoved into IPv4 address representation (an IP
// in 0.0.0.0/8 means it's actually an index).
//
// See https://docs.microsoft.com/en-us/windows/win32/winsock/ipproto-ip-socket-options
// and IP_UNICAST_IF.
indexAsAddr := nativeToBigEndian(ifidx)
var controlErr error
err := c.Control(func(fd uintptr) {
controlErr = windows.SetsockoptInt(windows.Handle(fd), windows.IPPROTO_IP, sockoptBoundInterface, int(index))
controlErr = windows.SetsockoptInt(windows.Handle(fd), windows.IPPROTO_IP, sockoptBoundInterface, int(indexAsAddr))
})
if err != nil {
return err
@@ -117,7 +114,8 @@ func bindSocket6(c syscall.RawConn, ifidx uint32) error {
// representation, suitable for passing to Windows APIs that require a
// mangled uint32.
func nativeToBigEndian(i uint32) uint32 {
var b [4]byte
binary.BigEndian.PutUint32(b[:], i)
return *(*uint32)(unsafe.Pointer(&b[0]))
if endian.Big {
return i
}
return bits.ReverseBytes32(i)
}

View File

@@ -6,14 +6,15 @@
package netstat
import (
"encoding/binary"
"errors"
"fmt"
"math/bits"
"syscall"
"unsafe"
"golang.org/x/sys/windows"
"inet.af/netaddr"
"tailscale.com/util/endian"
)
// See https://docs.microsoft.com/en-us/windows/win32/api/iphlpapi/nf-iphlpapi-getextendedtcptable
@@ -153,9 +154,11 @@ func state(v uint32) string {
}
func ipport4(addr uint32, port uint16) netaddr.IPPort {
a4 := (*[4]byte)(unsafe.Pointer(&addr))
if !endian.Big {
addr = bits.ReverseBytes32(addr)
}
return netaddr.IPPort{
IP: netaddr.IPv4(a4[0], a4[1], a4[2], a4[3]),
IP: netaddr.IPv4(byte(addr>>24), byte(addr>>16), byte(addr>>8), byte(addr)),
Port: port,
}
}
@@ -173,6 +176,8 @@ func ipport6(addr [16]byte, scope uint32, port uint16) netaddr.IPPort {
}
func port(v *uint32) uint16 {
p := (*[4]byte)(unsafe.Pointer(v))
return binary.BigEndian.Uint16(p[:2])
if !endian.Big {
return uint16(bits.ReverseBytes32(*v) >> 16)
}
return uint16(*v >> 16)
}

View File

@@ -30,7 +30,11 @@ func CGNATRange() netaddr.IPPrefix {
return cgnatRange.v
}
var cgnatRange oncePrefix
var (
cgnatRange oncePrefix
ulaRange oncePrefix
ula4To6Range oncePrefix
)
// TailscaleServiceIP returns the listen address of services
// provided by Tailscale itself such as the Magic DNS proxy.
@@ -44,7 +48,40 @@ var serviceIP onceIP
// IsTailscaleIP reports whether ip is an IP address in a range that
// Tailscale assigns from.
func IsTailscaleIP(ip netaddr.IP) bool {
return CGNATRange().Contains(ip) && !ChromeOSVMRange().Contains(ip)
if ip.Is4() {
return CGNATRange().Contains(ip) && !ChromeOSVMRange().Contains(ip)
}
return TailscaleULARange().Contains(ip)
}
// TailscaleULARange returns the IPv6 Unique Local Address range that
// is the superset range that Tailscale assigns out of.
func TailscaleULARange() netaddr.IPPrefix {
ulaRange.Do(func() { mustPrefix(&ulaRange.v, "fd7a:115c:a1e0::/48") })
return ulaRange.v
}
// Tailscale4To6Range returns the subset of TailscaleULARange used for
// auto-translated Tailscale ipv4 addresses.
func Tailscale4To6Range() netaddr.IPPrefix {
// This IP range has no significance, beyond being a subset of
// TailscaleULARange. The bits from /48 to /104 were picked at
// random.
ula4To6Range.Do(func() { mustPrefix(&ula4To6Range.v, "fd7a:115c:a1e0:ab12:4843:cd96:6200::/104") })
return ula4To6Range.v
}
// Tailscale4To6 returns a Tailscale IPv6 address that maps 1:1 to the
// given Tailscale IPv4 address. Returns a zero IP if ipv4 isn't a
// Tailscale IPv4 address.
func Tailscale4To6(ipv4 netaddr.IP) netaddr.IP {
if !ipv4.Is4() || !IsTailscaleIP(ipv4) {
return netaddr.IP{}
}
ret := Tailscale4To6Range().IP.As16()
v4 := ipv4.As4()
copy(ret[13:], v4[1:])
return netaddr.IPFrom16(ret)
}
func mustPrefix(v *netaddr.IPPrefix, prefix string) {

View File

@@ -10,14 +10,45 @@ import (
"net/http"
"net/url"
"os"
"sync"
"time"
)
// InvalidateCache invalidates the package-level cache for ProxyFromEnvironment.
//
// It's intended to be called on network link/routing table changes.
func InvalidateCache() {
mu.Lock()
defer mu.Unlock()
noProxyUntil = time.Time{}
}
var (
mu sync.Mutex
noProxyUntil time.Time // if non-zero, time at which ProxyFromEnvironment should check again
)
func setNoProxyUntil(d time.Duration) {
mu.Lock()
defer mu.Unlock()
noProxyUntil = time.Now().Add(d)
}
var _ = setNoProxyUntil // quiet staticcheck; Windows uses the above, more might later
// sysProxyFromEnv, if non-nil, specifies a platform-specific ProxyFromEnvironment
// func to use if http.ProxyFromEnvironment doesn't return a proxy.
// For example, WPAD PAC files on Windows.
var sysProxyFromEnv func(*http.Request) (*url.URL, error)
func ProxyFromEnvironment(req *http.Request) (*url.URL, error) {
mu.Lock()
noProxyTime := noProxyUntil
mu.Unlock()
if time.Now().Before(noProxyTime) {
return nil, nil
}
u, err := http.ProxyFromEnvironment(req)
if u != nil && err == nil {
return u, nil

View File

@@ -19,6 +19,7 @@ import (
"github.com/alexbrainman/sspi/negotiate"
"golang.org/x/sys/windows"
"tailscale.com/types/logger"
)
var (
@@ -38,6 +39,13 @@ var cachedProxy struct {
val *url.URL
}
// proxyErrorf is a rate-limited logger specifically for errors asking
// WinHTTP for the proxy information. We don't want to log about
// errors often, otherwise the log message itself will generate a new
// HTTP request which ultimately will call back into us to log again,
// forever. So for errors, we only log a bit.
var proxyErrorf = logger.RateLimitedFn(log.Printf, 10*time.Minute, 2 /* burst*/, 10 /* maxCache */)
func proxyFromWinHTTPOrCache(req *http.Request) (*url.URL, error) {
if req.URL == nil {
return nil, nil
@@ -71,16 +79,31 @@ func proxyFromWinHTTPOrCache(req *http.Request) (*url.URL, error) {
}
// See https://docs.microsoft.com/en-us/windows/win32/winhttp/error-messages
const ERROR_WINHTTP_AUTODETECTION_FAILED = 12180
const (
ERROR_WINHTTP_AUTODETECTION_FAILED = 12180
ERROR_WINHTTP_UNABLE_TO_DOWNLOAD_SCRIPT = 12167
)
if err == syscall.Errno(ERROR_WINHTTP_AUTODETECTION_FAILED) {
setNoProxyUntil(10 * time.Second)
return nil, nil
}
if err == windows.ERROR_INVALID_PARAMETER {
// Seen on Windows 8.1. (https://github.com/tailscale/tailscale/issues/879)
// TODO(bradfitz): figure this out.
setNoProxyUntil(time.Hour)
proxyErrorf("tshttpproxy: winhttp: GetProxyForURL(%q): ERROR_INVALID_PARAMETER [unexpected]", urlStr)
return nil, nil
}
proxyErrorf("tshttpproxy: winhttp: GetProxyForURL(%q): %v/%#v", urlStr, err, err)
if err == syscall.Errno(ERROR_WINHTTP_UNABLE_TO_DOWNLOAD_SCRIPT) {
setNoProxyUntil(10 * time.Second)
return nil, nil
}
log.Printf("tshttpproxy: winhttp: GetProxyForURL(%q): %v/%#v", urlStr, err, err)
return nil, err
case <-ctx.Done():
cachedProxy.Lock()
defer cachedProxy.Unlock()
log.Printf("tshttpproxy: winhttp: GetProxyForURL(%q): timeout; using cached proxy %v", urlStr, cachedProxy.val)
proxyErrorf("tshttpproxy: winhttp: GetProxyForURL(%q): timeout; using cached proxy %v", urlStr, cachedProxy.val)
return cachedProxy.val, nil
}
}
@@ -88,7 +111,7 @@ func proxyFromWinHTTPOrCache(req *http.Request) (*url.URL, error) {
func proxyFromWinHTTP(ctx context.Context, urlStr string) (proxy *url.URL, err error) {
whi, err := winHTTPOpen()
if err != nil {
log.Printf("winhttp: Open: %v", err)
proxyErrorf("winhttp: Open: %v", err)
return nil, err
}
defer whi.Close()

View File

@@ -8,6 +8,7 @@ import (
"syscall"
"time"
"golang.org/x/sys/windows"
exec "tailscale.com/tempfork/osexec"
)
@@ -19,6 +20,9 @@ func listPorts() (List, error) {
}
func addProcesses(pl []Port) ([]Port, error) {
if t := windows.GetCurrentProcessToken(); !t.IsElevated() {
return listPortsNetstat("-na")
}
return listPortsNetstat("-nab")
}

View File

@@ -4,7 +4,7 @@
package tailcfg
//go:generate go run tailscale.com/cmd/cloner -type=User,Node,Hostinfo,NetInfo,Group,Role,Capability,Login,DNSConfig -output=tailcfg_clone.go
//go:generate go run tailscale.com/cmd/cloner --type=User,Node,Hostinfo,NetInfo,Group,Role,Capability,Login,DNSConfig,RegisterResponse --clonefunc=true --output=tailcfg_clone.go
import (
"bytes"
@@ -27,14 +27,34 @@ type ID int64
type UserID ID
func (u UserID) IsZero() bool {
return u == 0
}
type LoginID ID
func (u LoginID) IsZero() bool {
return u == 0
}
type NodeID ID
func (u NodeID) IsZero() bool {
return u == 0
}
type GroupID ID
func (u GroupID) IsZero() bool {
return u == 0
}
type RoleID ID
func (u RoleID) IsZero() bool {
return u == 0
}
type CapabilityID ID
// MachineKey is the curve25519 public key for a machine.
@@ -114,7 +134,7 @@ type UserProfile struct {
LoginName string // "alice@smith.com"; for display purposes only (provider is not listed)
DisplayName string // "Alice Smith"
ProfilePicURL string
Roles []RoleID
Roles []RoleID // deprecated; clients should not rely on Roles
}
type Node struct {
@@ -239,7 +259,7 @@ type Service struct {
_ structs.Incomparable
Proto ServiceProto // TCP or UDP
Port uint16 // port number service is listening on
Description string // text description of service
Description string `json:",omitempty"` // text description of service
// TODO(apenwarr): allow advertising services on subnet IPs?
// TODO(apenwarr): add "tags" here for each service?
@@ -255,13 +275,13 @@ type Hostinfo struct {
// TODO(crawshaw): mark all these fields ",omitempty" when all the
// iOS apps are updated with the latest swift version of this struct.
IPNVersion string // version of this code
FrontendLogID string // logtail ID of frontend instance
BackendLogID string // logtail ID of backend instance
FrontendLogID string `json:",omitempty"` // logtail ID of frontend instance
BackendLogID string `json:",omitempty"` // logtail ID of backend instance
OS string // operating system the client runs on (a version.OS value)
OSVersion string // operating system version, with optional distro prefix ("Debian 10.4", "Windows 10 Pro 10.0.19041")
DeviceModel string // mobile phone model ("Pixel 3a", "iPhone 11 Pro")
OSVersion string `json:",omitempty"` // operating system version, with optional distro prefix ("Debian 10.4", "Windows 10 Pro 10.0.19041")
DeviceModel string `json:",omitempty"` // mobile phone model ("Pixel 3a", "iPhone 11 Pro")
Hostname string // name of the host the client runs on
GoArch string // the host's GOARCH value (of the running binary)
GoArch string `json:",omitempty"` // the host's GOARCH value (of the running binary)
RoutableIPs []wgcfg.CIDR `json:",omitempty"` // set of IP ranges this client can route
RequestTags []string `json:",omitempty"` // set of ACL tags this node wants to claim
Services []Service `json:",omitempty"` // services advertised by this machine
@@ -308,7 +328,7 @@ type NetInfo struct {
PreferredDERP int
// LinkType is the current link type, if known.
LinkType string // "wired", "wifi", "mobile" (LTE, 4G, 3G, etc)
LinkType string `json:",omitempty"` // "wired", "wifi", "mobile" (LTE, 4G, 3G, etc)
// DERPLatency is the fastest recent time to reach various
// DERP STUN servers, in seconds. The map key is the
@@ -441,22 +461,51 @@ type RegisterResponse struct {
// using the local machine key, and sent to:
// https://login.tailscale.com/machine/<mkey hex>/map
type MapRequest struct {
Version int // current version is 4
// Version is incremented whenever the client code changes enough that
// we want to signal to the control server that we're capable of something
// different.
//
// History of versions:
// 3: implicit compression, keep-alives
// 4: opt-in keep-alives via KeepAlive field, opt-in compression via Compress
// 5: 2020-10-19, implies IncludeIPv6, DeltaPeers/DeltaUserProfiles, supports MagicDNS
Version int
Compress string // "zstd" or "" (no compression)
KeepAlive bool // whether server should send keep-alives back to us
NodeKey NodeKey
DiscoKey DiscoKey
Endpoints []string // caller's endpoints (IPv4 or IPv6)
IncludeIPv6 bool // include IPv6 endpoints in returned Node Endpoints
DeltaPeers bool // whether the 2nd+ network map in response should be deltas, using PeersChanged, PeersRemoved
IncludeIPv6 bool `json:",omitempty"` // include IPv6 endpoints in returned Node Endpoints (for Version 4 clients)
Stream bool // if true, multiple MapResponse objects are returned
Hostinfo *Hostinfo
// DebugForceDisco is a temporary flag during the deployment
// of magicsock active discovery. It says that that the client
// has environment variables explicitly turning discovery on,
// so control should not disable it.
DebugForceDisco bool `json:"debugForceDisco,omitempty"`
// ReadOnly is whether the client just wants to fetch the
// MapResponse, without updating their Endpoints. The
// Endpoints field will be ignored and LastSeen will not be
// updated and peers will not be notified of changes.
//
// The intended use is for clients to discover the DERP map at
// start-up before their first real endpoint update.
ReadOnly bool `json:",omitempty"`
// OmitPeers is whether the client is okay with the Peers list
// being omitted in the response. (For example, a client on
// start up using ReadOnly to get the DERP map.)
OmitPeers bool `json:",omitempty"`
// DebugFlags is a list of strings specifying debugging and
// development features to enable in handling this map
// request. The values are deliberately unspecified, as they get
// added and removed all the time during development, and offer no
// compatibility promise. To roll out semantic changes, bump
// Version instead.
//
// Current DebugFlags values are:
// * "warn-ip-forwarding-off": client is trying to be a subnet
// router but their IP forwarding is broken.
// * "v6-overlay": IPv6 development flag to have control send
// v6 node addrs
DebugFlags []string `json:",omitempty"`
}
// PortRange represents a range of UDP or TCP port numbers.
@@ -470,23 +519,42 @@ var PortRangeAny = PortRange{0, 65535}
// NetPortRange represents a single subnet:portrange.
type NetPortRange struct {
_ structs.Incomparable
IP string
Bits *int // backward compatibility: if missing, means "all" bits
IP string // "*" means all
Bits *int // backward compatibility: if missing, means "all" bits
Ports PortRange
}
// FilterRule represents one rule in a packet filter.
//
// A rule is logically a set of source CIDRs to match (described by
// SrcIPs and SrcBits), and a set of destination targets that are then
// allowed if a source IP is mathces of those CIDRs.
type FilterRule struct {
SrcIPs []string
SrcBits []int
// SrcIPs are the source IPs/networks to match.
// The special value "*" means to match all.
SrcIPs []string
// SrcBits values correspond to the SrcIPs above.
//
// If present at the same index, it changes the SrcIP above to
// be a network with /n CIDR bits. If the slice is nil or
// insufficiently long, the default value (for an IPv4
// address) for a position is 32, as if the SrcIPs above were
// a /32 mask. For a "*" SrcIPs value, the corresponding
// SrcBits value is ignored.
// TODO: for IPv6, clarify default bits length.
SrcBits []int
// DstPorts are the port ranges to allow once a source IP
// matches (is in the CIDR described by SrcIPs & SrcBits).
DstPorts []NetPortRange
}
var FilterAllowAll = []FilterRule{
FilterRule{
{
SrcIPs: []string{"*"},
SrcBits: nil,
DstPorts: []NetPortRange{NetPortRange{
DstPorts: []NetPortRange{{
IP: "*",
Bits: nil,
Ports: PortRange{0, 65535},
@@ -545,8 +613,8 @@ type MapResponse struct {
// ACLs
Domain string
PacketFilter []FilterRule
UserProfiles []UserProfile
Roles []Role
UserProfiles []UserProfile // as of 1.1.541: may be new or updated user profiles only
Roles []Role // deprecated; clients should not rely on Roles
// TODO: Groups []Group
// TODO: Capabilities []Capability
@@ -582,10 +650,15 @@ type Debug struct {
// TrimWGConfig controls whether Tailscale does lazy, on-demand
// wireguard configuration of peers.
TrimWGConfig opt.Bool `json:",omitempty"`
// DisableSubnetsIfPAC controls whether subnet routers should be
// disabled if WPAD is present on the network.
DisableSubnetsIfPAC opt.Bool `json:",omitempty"`
}
func (k MachineKey) String() string { return fmt.Sprintf("mkey:%x", k[:]) }
func (k MachineKey) MarshalText() ([]byte, error) { return keyMarshalText("mkey:", k), nil }
func (k MachineKey) HexString() string { return fmt.Sprintf("%x", k[:]) }
func (k *MachineKey) UnmarshalText(text []byte) error { return keyUnmarshalText(k[:], "mkey:", text) }
func keyMarshalText(prefix string, k [32]byte) []byte {
@@ -615,6 +688,9 @@ func (k *NodeKey) UnmarshalText(text []byte) error { return keyUnmarshalText(k[:
// IsZero reports whether k is the zero value.
func (k NodeKey) IsZero() bool { return k == NodeKey{} }
// IsZero reports whether k is the zero value.
func (k MachineKey) IsZero() bool { return k == MachineKey{} }
func (k DiscoKey) String() string { return fmt.Sprintf("discokey:%x", k[:]) }
func (k DiscoKey) MarshalText() ([]byte, error) { return keyMarshalText("discokey:", k), nil }
func (k *DiscoKey) UnmarshalText(text []byte) error { return keyUnmarshalText(k[:], "discokey:", text) }

View File

@@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Code generated by tailscale.com/cmd/cloner -type User,Node,Hostinfo,NetInfo,Group,Role,Capability,Login,DNSConfig; DO NOT EDIT.
// Code generated by tailscale.com/cmd/cloner -type User,Node,Hostinfo,NetInfo,Group,Role,Capability,Login,DNSConfig,RegisterResponse; DO NOT EDIT.
package tailcfg
@@ -28,7 +28,7 @@ func (src *User) Clone() *User {
}
// A compilation failure here means this code must be regenerated, with command:
// tailscale.com/cmd/cloner -type User,Node,Hostinfo,NetInfo,Group,Role,Capability,Login,DNSConfig
// tailscale.com/cmd/cloner -type User,Node,Hostinfo,NetInfo,Group,Role,Capability,Login,DNSConfig,RegisterResponse
var _UserNeedsRegeneration = User(struct {
ID UserID
LoginName string
@@ -60,7 +60,7 @@ func (src *Node) Clone() *Node {
}
// A compilation failure here means this code must be regenerated, with command:
// tailscale.com/cmd/cloner -type User,Node,Hostinfo,NetInfo,Group,Role,Capability,Login,DNSConfig
// tailscale.com/cmd/cloner -type User,Node,Hostinfo,NetInfo,Group,Role,Capability,Login,DNSConfig,RegisterResponse
var _NodeNeedsRegeneration = Node(struct {
ID NodeID
Name string
@@ -96,7 +96,7 @@ func (src *Hostinfo) Clone() *Hostinfo {
}
// A compilation failure here means this code must be regenerated, with command:
// tailscale.com/cmd/cloner -type User,Node,Hostinfo,NetInfo,Group,Role,Capability,Login,DNSConfig
// tailscale.com/cmd/cloner -type User,Node,Hostinfo,NetInfo,Group,Role,Capability,Login,DNSConfig,RegisterResponse
var _HostinfoNeedsRegeneration = Hostinfo(struct {
IPNVersion string
FrontendLogID string
@@ -130,7 +130,7 @@ func (src *NetInfo) Clone() *NetInfo {
}
// A compilation failure here means this code must be regenerated, with command:
// tailscale.com/cmd/cloner -type User,Node,Hostinfo,NetInfo,Group,Role,Capability,Login,DNSConfig
// tailscale.com/cmd/cloner -type User,Node,Hostinfo,NetInfo,Group,Role,Capability,Login,DNSConfig,RegisterResponse
var _NetInfoNeedsRegeneration = NetInfo(struct {
MappingVariesByDestIP opt.Bool
HairPinning opt.Bool
@@ -157,7 +157,7 @@ func (src *Group) Clone() *Group {
}
// A compilation failure here means this code must be regenerated, with command:
// tailscale.com/cmd/cloner -type User,Node,Hostinfo,NetInfo,Group,Role,Capability,Login,DNSConfig
// tailscale.com/cmd/cloner -type User,Node,Hostinfo,NetInfo,Group,Role,Capability,Login,DNSConfig,RegisterResponse
var _GroupNeedsRegeneration = Group(struct {
ID GroupID
Name string
@@ -177,7 +177,7 @@ func (src *Role) Clone() *Role {
}
// A compilation failure here means this code must be regenerated, with command:
// tailscale.com/cmd/cloner -type User,Node,Hostinfo,NetInfo,Group,Role,Capability,Login,DNSConfig
// tailscale.com/cmd/cloner -type User,Node,Hostinfo,NetInfo,Group,Role,Capability,Login,DNSConfig,RegisterResponse
var _RoleNeedsRegeneration = Role(struct {
ID RoleID
Name string
@@ -196,7 +196,7 @@ func (src *Capability) Clone() *Capability {
}
// A compilation failure here means this code must be regenerated, with command:
// tailscale.com/cmd/cloner -type User,Node,Hostinfo,NetInfo,Group,Role,Capability,Login,DNSConfig
// tailscale.com/cmd/cloner -type User,Node,Hostinfo,NetInfo,Group,Role,Capability,Login,DNSConfig,RegisterResponse
var _CapabilityNeedsRegeneration = Capability(struct {
ID CapabilityID
Type CapType
@@ -215,7 +215,7 @@ func (src *Login) Clone() *Login {
}
// A compilation failure here means this code must be regenerated, with command:
// tailscale.com/cmd/cloner -type User,Node,Hostinfo,NetInfo,Group,Role,Capability,Login,DNSConfig
// tailscale.com/cmd/cloner -type User,Node,Hostinfo,NetInfo,Group,Role,Capability,Login,DNSConfig,RegisterResponse
var _LoginNeedsRegeneration = Login(struct {
_ structs.Incomparable
ID LoginID
@@ -240,7 +240,7 @@ func (src *DNSConfig) Clone() *DNSConfig {
}
// A compilation failure here means this code must be regenerated, with command:
// tailscale.com/cmd/cloner -type User,Node,Hostinfo,NetInfo,Group,Role,Capability,Login,DNSConfig
// tailscale.com/cmd/cloner -type User,Node,Hostinfo,NetInfo,Group,Role,Capability,Login,DNSConfig,RegisterResponse
var _DNSConfigNeedsRegeneration = DNSConfig(struct {
Nameservers []netaddr.IP
Domains []string
@@ -248,9 +248,31 @@ var _DNSConfigNeedsRegeneration = DNSConfig(struct {
Proxied bool
}{})
// Clone makes a deep copy of RegisterResponse.
// The result aliases no memory with the original.
func (src *RegisterResponse) Clone() *RegisterResponse {
if src == nil {
return nil
}
dst := new(RegisterResponse)
*dst = *src
dst.User = *src.User.Clone()
return dst
}
// A compilation failure here means this code must be regenerated, with command:
// tailscale.com/cmd/cloner -type User,Node,Hostinfo,NetInfo,Group,Role,Capability,Login,DNSConfig,RegisterResponse
var _RegisterResponseNeedsRegeneration = RegisterResponse(struct {
User User
Login Login
NodeKeyExpired bool
MachineAuthorized bool
AuthURL string
}{})
// Clone duplicates src into dst and reports whether it succeeded.
// To succeed, <src, dst> must be of types <*T, *T> or <*T, **T>,
// where T is one of User,Node,Hostinfo,NetInfo,Group,Role,Capability,Login,DNSConfig.
// where T is one of User,Node,Hostinfo,NetInfo,Group,Role,Capability,Login,DNSConfig,RegisterResponse.
func Clone(dst, src interface{}) bool {
switch src := src.(type) {
case *User:
@@ -334,6 +356,15 @@ func Clone(dst, src interface{}) bool {
*dst = src.Clone()
return true
}
case *RegisterResponse:
switch dst := dst.(type) {
case *RegisterResponse:
*dst = *src.Clone()
return true
case **RegisterResponse:
*dst = src.Clone()
return true
}
}
return false
}

View File

@@ -7,17 +7,12 @@
package exec
import (
"io/ioutil"
"os"
"testing"
)
func TestLookPathUnixEmptyPath(t *testing.T) {
tmp, err := ioutil.TempDir("", "TestLookPathUnixEmptyPath")
if err != nil {
t.Fatal("TempDir failed: ", err)
}
defer os.RemoveAll(tmp)
tmp := t.TempDir()
wd, err := os.Getwd()
if err != nil {
t.Fatal("Getwd failed: ", err)

View File

@@ -5,6 +5,8 @@
package tstest
import (
"bytes"
"fmt"
"log"
"os"
"sync"
@@ -35,7 +37,16 @@ func UnfixLogs(t *testing.T) {
type panicLogWriter struct{}
func (panicLogWriter) Write(b []byte) (int, error) {
panic("please use tailscale.com/logger.Logf instead of the log package")
// Allow certain phrases for now, in the interest of getting
// CI working on Windows and not having to refactor all the
// interfaces.GetState & tshttpproxy code to allow pushing
// down a Logger yet. TODO(bradfitz): do that refactoring once
// 1.2.0 is out.
if bytes.Contains(b, []byte("tshttpproxy: ")) {
os.Stderr.Write(b)
return len(b), nil
}
panic(fmt.Sprintf("please use tailscale.com/logger.Logf instead of the log package (tried to log: %q)", b))
}
// PanicOnLog modifies the standard library log package's default output to

View File

@@ -40,6 +40,12 @@ func goroutineDump() (int, string) {
}
func (r *ResourceCheck) Assert(t testing.TB) {
if t.Failed() {
// Something else went wrong.
// Assume that that is responsible for the leak
// and don't pile on a bunch of extra of output.
return
}
t.Helper()
want := r.startNumRoutines

View File

@@ -58,6 +58,9 @@ func (fn JSONHandlerFunc) ServeHTTPReturn(w http.ResponseWriter, r *http.Request
// the client in this handler. We don't want the wrapping
// ReturnHandler to do it too.
err = werr.Err
if werr.Msg != "" {
err = fmt.Errorf("%s: %w", werr.Msg, err)
}
} else {
resp = &response{
Status: "error",

View File

@@ -157,11 +157,22 @@ type ReturnHandler interface {
ServeHTTPReturn(http.ResponseWriter, *http.Request) error
}
type HandlerOptions struct {
Quiet200s bool // if set, do not log successfully handled HTTP requests
Logf logger.Logf
Now func() time.Time // if nil, defaults to time.Now
// If non-nil, StatusCodeCounters maintains counters
// of status codes for handled responses.
// The keys are "1xx", "2xx", "3xx", "4xx", and "5xx".
StatusCodeCounters *expvar.Map
}
// StdHandler converts a ReturnHandler into a standard http.Handler.
// Handled requests are logged using logf, as are any errors. Errors
// are handled as specified by the Handler interface.
func StdHandler(h ReturnHandler, logf logger.Logf) http.Handler {
return stdHandler(h, logf, time.Now, true)
return StdHandlerOpts(h, HandlerOptions{Logf: logf, Now: time.Now})
}
// ReturnHandlerFunc is an adapter to allow the use of ordinary
@@ -178,27 +189,32 @@ func (f ReturnHandlerFunc) ServeHTTPReturn(w http.ResponseWriter, r *http.Reques
// StdHandlerNo200s is like StdHandler, but successfully handled HTTP
// requests don't write an access log entry to logf.
//
// TODO(danderson): quick stopgap, probably want ...Options on StdHandler instead?
// TODO(josharian): eliminate this and StdHandler in favor of StdHandlerOpts,
// rename StdHandlerOpts to StdHandler. Will be a breaking API change.
func StdHandlerNo200s(h ReturnHandler, logf logger.Logf) http.Handler {
return stdHandler(h, logf, time.Now, false)
return StdHandlerOpts(h, HandlerOptions{Logf: logf, Now: time.Now, Quiet200s: true})
}
func stdHandler(h ReturnHandler, logf logger.Logf, now func() time.Time, log200s bool) http.Handler {
return retHandler{h, logf, now, log200s}
// StdHandlerOpts converts a ReturnHandler into a standard http.Handler.
// Handled requests are logged using opts.Logf, as are any errors.
// Errors are handled as specified by the Handler interface.
func StdHandlerOpts(h ReturnHandler, opts HandlerOptions) http.Handler {
if opts.Now == nil {
opts.Now = time.Now
}
return retHandler{h, opts}
}
// retHandler is an http.Handler that wraps a Handler and handles errors.
type retHandler struct {
rh ReturnHandler
logf logger.Logf
timeNow func() time.Time
log200s bool
rh ReturnHandler
opts HandlerOptions
}
// ServeHTTP implements the http.Handler interface.
func (h retHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
msg := AccessLogRecord{
When: h.timeNow(),
When: h.opts.Now(),
RemoteAddr: r.RemoteAddr,
Proto: r.Proto,
TLS: r.TLS != nil,
@@ -209,7 +225,7 @@ func (h retHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
Referer: r.Referer(),
}
lw := &loggingResponseWriter{ResponseWriter: w, logf: h.logf}
lw := &loggingResponseWriter{ResponseWriter: w, logf: h.opts.Logf}
err := h.rh.ServeHTTPReturn(lw, r)
hErr, hErrOK := err.(HTTPError)
@@ -219,7 +235,7 @@ func (h retHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
lw.code = 200
}
msg.Seconds = h.timeNow().Sub(msg.When).Seconds()
msg.Seconds = h.opts.Now().Sub(msg.When).Seconds()
msg.Code = lw.code
msg.Bytes = lw.bytes
@@ -236,16 +252,21 @@ func (h retHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
case hErrOK:
// Handler asked us to send an error. Do so, if we haven't
// already sent a response.
msg.Err = hErr.Msg
if hErr.Err != nil {
msg.Err = hErr.Err.Error()
if msg.Err == "" {
msg.Err = hErr.Err.Error()
} else {
msg.Err = msg.Err + ": " + hErr.Err.Error()
}
}
if lw.code != 0 {
h.logf("[unexpected] handler returned HTTPError %v, but already sent a response with code %d", hErr, lw.code)
h.opts.Logf("[unexpected] handler returned HTTPError %v, but already sent a response with code %d", hErr, lw.code)
break
}
msg.Code = hErr.Code
if msg.Code == 0 {
h.logf("[unexpected] HTTPError %v did not contain an HTTP status code, sending internal server error", hErr)
h.opts.Logf("[unexpected] HTTPError %v did not contain an HTTP status code, sending internal server error", hErr)
msg.Code = http.StatusInternalServerError
}
http.Error(lw, hErr.Msg, msg.Code)
@@ -259,8 +280,13 @@ func (h retHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}
}
if msg.Code != 200 || h.log200s {
h.logf("%s", msg)
if msg.Code != 200 || !h.opts.Quiet200s {
h.opts.Logf("%s", msg)
}
if h.opts.StatusCodeCounters != nil {
key := fmt.Sprintf("%dxx", msg.Code/100)
h.opts.StatusCodeCounters.Add(key, 1)
}
}

View File

@@ -122,7 +122,7 @@ func TestStdHandler(t *testing.T) {
Host: "example.com",
Method: "GET",
RequestURI: "/foo",
Err: testErr.Error(),
Err: "not found: " + testErr.Error(),
Code: 404,
},
},
@@ -139,6 +139,7 @@ func TestStdHandler(t *testing.T) {
Host: "example.com",
Method: "GET",
RequestURI: "/foo",
Err: "not found",
Code: 404,
},
},
@@ -189,7 +190,7 @@ func TestStdHandler(t *testing.T) {
Host: "example.com",
Method: "GET",
RequestURI: "/foo",
Err: testErr.Error(),
Err: "not found: " + testErr.Error(),
Code: 200,
},
},
@@ -247,7 +248,7 @@ func TestStdHandler(t *testing.T) {
clock.Reset()
rec := noopHijacker{httptest.NewRecorder(), false}
h := stdHandler(test.rh, logf, clock.Now, true)
h := StdHandlerOpts(test.rh, HandlerOptions{Logf: logf, Now: clock.Now})
h.ServeHTTP(&rec, test.r)
res := rec.Result()
if res.StatusCode != test.wantCode {

View File

@@ -0,0 +1,46 @@
// 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 flagtype defines flag.Value types.
package flagtype
import (
"errors"
"flag"
"fmt"
"math"
"strconv"
"strings"
)
type portValue struct{ n *uint16 }
func PortValue(dst *uint16, defaultPort uint16) flag.Value {
*dst = defaultPort
return portValue{dst}
}
func (p portValue) String() string {
if p.n == nil {
return ""
}
return fmt.Sprint(*p.n)
}
func (p portValue) Set(v string) error {
if v == "" {
return errors.New("can't be the empty string")
}
if strings.Contains(v, ":") {
return errors.New("expecting just a port number, without a colon")
}
n, err := strconv.ParseUint(v, 10, 64) // use 64 instead of 16 to return nicer error message
if err != nil {
return fmt.Errorf("not a valid number")
}
if n > math.MaxUint16 {
return errors.New("out of range for port number")
}
*p.n = uint16(n)
return nil
}

View File

@@ -158,7 +158,10 @@ func LogOnChange(logf Logf, maxInterval time.Duration, timeNow func() time.Time)
tLastLogged = timeNow()
mu.Unlock()
logf(s)
// Re-stringify it (instead of using "%s", s) so something like "%s"
// doesn't end up getting rate-limited. (And can't use 's' as the pattern,
// as it might contain formatting directives.)
logf(format, args...)
}
}

View File

@@ -2,19 +2,17 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package wgengine
package logger
import (
"fmt"
"runtime"
"tailscale.com/types/logger"
)
// RusagePrefixLog returns a Logf func wrapping the provided logf func that adds
// a prefixed log message to each line with the current binary memory usage
// and max RSS.
func RusagePrefixLog(logf logger.Logf) logger.Logf {
func RusagePrefixLog(logf Logf) Logf {
return func(f string, argv ...interface{}) {
var m runtime.MemStats
runtime.ReadMemStats(&m)

View File

@@ -4,7 +4,7 @@
// +build !windows
package wgengine
package logger
import (
"runtime"

View File

@@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package wgengine
package logger
func rusageMaxRSS() float64 {
// TODO(apenwarr): Substitute Windows equivalent of Getrusage() here.

10
util/endian/big.go Normal file
View File

@@ -0,0 +1,10 @@
// 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.
// +build mips mips64 ppc64 s390x
package endian
// Big is whether the current platform is big endian.
const Big = true

6
util/endian/endian.go Normal file
View File

@@ -0,0 +1,6 @@
// 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 endian exports a constant about whether the machine is big endian.
package endian

10
util/endian/little.go Normal file
View File

@@ -0,0 +1,10 @@
// 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.
// +build 386 amd64 arm arm64 mips64le mipsle ppc64le riscv64 wasm
package endian
// Big is whether the current platform is big endian.
const Big = false

65
util/uniq/slice.go Normal file
View File

@@ -0,0 +1,65 @@
// 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 uniq provides removal of adjacent duplicate elements in slices.
// It is similar to the unix command uniq.
package uniq
import (
"fmt"
"reflect"
)
type badTypeError struct {
typ reflect.Type
}
func (e badTypeError) Error() string {
return fmt.Sprintf("uniq.ModifySlice's first argument must have type *[]T, got %v", e.typ)
}
// ModifySlice removes adjacent duplicate elements from the slice pointed to by sliceptr.
// It adjusts the length of the slice appropriately and zeros the tail.
// eq reports whether (*sliceptr)[i] and (*sliceptr)[j] are equal.
// ModifySlice does O(len(*sliceptr)) operations.
func ModifySlice(sliceptr interface{}, eq func(i, j int) bool) {
rvp := reflect.ValueOf(sliceptr)
if rvp.Type().Kind() != reflect.Ptr {
panic(badTypeError{rvp.Type()})
}
rv := rvp.Elem()
if rv.Type().Kind() != reflect.Slice {
panic(badTypeError{rvp.Type()})
}
length := rv.Len()
dst := 0
for i := 1; i < length; i++ {
if eq(dst, i) {
continue
}
dst++
// slice[dst] = slice[i]
rv.Index(dst).Set(rv.Index(i))
}
end := dst + 1
var zero reflect.Value
if end < length {
zero = reflect.Zero(rv.Type().Elem())
}
// for i := range slice[end:] {
// size[i] = 0/nil/{}
// }
for i := end; i < length; i++ {
// slice[i] = 0/nil/{}
rv.Index(i).Set(zero)
}
// slice = slice[:end]
if end < length {
rv.SetLen(end)
}
}

88
util/uniq/slice_test.go Normal file
View File

@@ -0,0 +1,88 @@
// 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 uniq_test
import (
"reflect"
"strconv"
"testing"
"tailscale.com/util/uniq"
)
func TestModifySlice(t *testing.T) {
tests := []struct {
in []int
want []int
}{
{in: []int{0, 1, 2}, want: []int{0, 1, 2}},
{in: []int{0, 1, 2, 2}, want: []int{0, 1, 2}},
{in: []int{0, 0, 1, 2}, want: []int{0, 1, 2}},
{in: []int{0, 1, 0, 2}, want: []int{0, 1, 0, 2}},
{in: []int{0}, want: []int{0}},
{in: []int{0, 0}, want: []int{0}},
{in: []int{}, want: []int{}},
}
for _, test := range tests {
in := make([]int, len(test.in))
copy(in, test.in)
uniq.ModifySlice(&test.in, func(i, j int) bool { return test.in[i] == test.in[j] })
if !reflect.DeepEqual(test.in, test.want) {
t.Errorf("uniq.Slice(%v) = %v, want %v", in, test.in, test.want)
}
start := len(test.in)
test.in = test.in[:cap(test.in)]
for i := start; i < len(in); i++ {
if test.in[i] != 0 {
t.Errorf("uniq.Slice(%v): non-0 in tail of %v at index %v", in, test.in, i)
}
}
}
}
func Benchmark(b *testing.B) {
benches := []struct {
name string
reset func(s []byte)
}{
{name: "AllDups",
reset: func(s []byte) {
for i := range s {
s[i] = '*'
}
},
},
{name: "NoDups",
reset: func(s []byte) {
for i := range s {
s[i] = byte(i)
}
},
},
}
for _, bb := range benches {
b.Run(bb.name, func(b *testing.B) {
for size := 1; size <= 4096; size *= 16 {
b.Run(strconv.Itoa(size), func(b *testing.B) {
benchmark(b, 64, bb.reset)
})
}
})
}
}
func benchmark(b *testing.B, size int64, reset func(s []byte)) {
b.ReportAllocs()
b.SetBytes(size)
s := make([]byte, size)
b.ResetTimer()
for i := 0; i < b.N; i++ {
s = s[:size]
reset(s)
uniq.ModifySlice(&s, func(i, j int) bool { return s[i] == s[j] })
}
}

3
version/.gitignore vendored
View File

@@ -1,6 +1,9 @@
describe.txt
long.txt
short.txt
gitcommit.txt
extragitcommit.txt
version-info.sh
version.h
version.xcconfig
ver.go

View File

@@ -1,4 +0,0 @@
describe=$(cd ../.. && git describe --long --abbrev=9)
echo "$describe" >$3
redo-always
redo-stamp <$3

View File

@@ -16,6 +16,7 @@ const (
Debian = Distro("debian")
Arch = Distro("arch")
Synology = Distro("synology")
OpenWrt = Distro("openwrt")
)
// Get returns the current distro, or the empty string if unknown.
@@ -36,5 +37,8 @@ func linuxDistro() Distro {
if _, err := os.Stat("/etc/arch-release"); err == nil {
return Arch
}
if _, err := os.Stat("/etc/openwrt_version"); err == nil {
return OpenWrt
}
return ""
}

View File

@@ -1,4 +0,0 @@
redo-ifchange mkversion.sh describe.txt
read -r describe <describe.txt
ver=$(./mkversion.sh long "$describe")
echo "$ver" >$3

View File

@@ -1,111 +0,0 @@
#!/bin/sh
set -eu
mode=$1
describe=$2
# Git describe output overall looks like
# MAJOR.MINOR.PATCH-NUMCOMMITS-GITHASH. Depending on the tag being
# described and the state of the repo, ver can be missing PATCH,
# NUMCOMMITS, or both.
#
# Valid values look like: 1.2.3-1234-abcdef, 0.98-1234-abcdef,
# 1.0.0-abcdef, 0.99-abcdef.
ver="${describe#v}"
stem="${ver%%-*}" # Just the semver-ish bit e.g. 1.2.3, 0.98
suffix="${ver#$stem}" # The rest e.g. -23-abcdef, -abcdef
# Normalize the stem into a full major.minor.patch semver. We might
# not use all those pieces depending on what kind of version we're
# making, but it's good to have them all on hand.
case "$stem" in
*.*.*)
# Full SemVer, nothing to do
stem="$stem"
;;
*.*)
# Old style major.minor, add a .0
stem="${stem}.0"
;;
*)
echo "Unparseable version $stem" >&2
exit 1
;;
esac
major=$(echo "$stem" | cut -f1 -d.)
minor=$(echo "$stem" | cut -f2 -d.)
patch=$(echo "$stem" | cut -f3 -d.)
# Extract the change count and git ID from the suffix.
case "$suffix" in
-*-*)
# Has both a change count and a commit hash.
changecount=$(echo "$suffix" | cut -f2 -d-)
githash=$(echo "$suffix" | cut -f3 -d-)
;;
-*)
# Git hash only, change count is zero.
changecount="0"
githash=$(echo "$suffix" | cut -f2 -d-)
;;
*)
echo "Unparseable version suffix $suffix" >&2
exit 1
;;
esac
# Validate that the version data makes sense. Rules:
# - Odd number minors are unstable. Patch must be 0, and gets
# replaced by changecount.
# - Even number minors are stable. Changecount must be 0, and
# gets removed.
#
# After this section, we only use major/minor/patch, which have been
# tweaked as needed.
if expr "$minor" : "[0-9]*[13579]$" >/dev/null; then
# Unstable
if [ "$patch" != "0" ]; then
# This is a fatal error, because a non-zero patch number
# indicates that we created an unstable git tag in violation
# of our versioning policy, and we want to blow up loudly to
# get that fixed.
echo "Unstable release $describe has a non-zero patch number, which is not allowed" >&2
exit 1
fi
patch="$changecount"
else
# Stable
if [ "$changecount" != "0" ]; then
# This is a commit that's sitting between two stable
# releases. We never want to release such a commit to the
# pbulic, but it's useful to be able to build it for
# debugging. Just force the version to 0.0.0, so that we're
# forced to rely on the git commit hash.
major=0
minor=0
patch=0
fi
fi
case "$1" in
long)
echo "${major}.${minor}.${patch}-${githash}"
;;
short)
echo "${major}.${minor}.${patch}"
;;
xcode)
# CFBundleShortVersionString: the "short name" used in the App
# Store. eg. 0.92.98
echo "VERSION_NAME = ${major}.${minor}.${patch}"
# CFBundleVersion: the build number. Needs to be 3 numeric
# sections that increment for each release according to SemVer
# rules.
#
# We start counting at 100 because we submitted using raw
# build numbers before, and Apple doesn't let you start over.
# e.g. 0.98.3 -> 100.98.3
echo "VERSION_ID = $((major + 100)).${minor}.${patch}"
;;
esac

View File

@@ -7,69 +7,96 @@ package version
import (
"fmt"
"os/exec"
"runtime"
"strconv"
"strings"
"testing"
"github.com/google/go-cmp/cmp"
)
func xcode(short, long string) string {
return fmt.Sprintf("VERSION_NAME = %s\nVERSION_ID = %s", short, long)
}
func mkversion(t *testing.T, mode, in string) (string, bool) {
func mkversion(t *testing.T, gitHash, otherHash string, major, minor, patch, changeCount int) (string, bool) {
t.Helper()
bs, err := exec.Command("./mkversion.sh", mode, in).CombinedOutput()
bs, err := exec.Command("./version.sh", gitHash, otherHash, strconv.Itoa(major), strconv.Itoa(minor), strconv.Itoa(patch), strconv.Itoa(changeCount)).CombinedOutput()
out := strings.TrimSpace(string(bs))
if err != nil {
t.Logf("mkversion.sh output: %s", string(bs))
return "", false
return out, false
}
return strings.TrimSpace(string(bs)), true
return out, true
}
func TestMkversion(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("skip test on Windows, because there is no shell to execute mkversion.sh.")
}
tests := []struct {
in string
ok bool
long string
short string
xcode string
gitHash, otherHash string
major, minor, patch, changeCount int
want string
}{
{"v0.98-abcdef", true, "0.98.0-abcdef", "0.98.0", xcode("0.98.0", "100.98.0")},
{"v0.98.1-abcdef", true, "0.98.1-abcdef", "0.98.1", xcode("0.98.1", "100.98.1")},
{"v1.1.0-37-abcdef", true, "1.1.37-abcdef", "1.1.37", xcode("1.1.37", "101.1.37")},
{"v1.2.9-abcdef", true, "1.2.9-abcdef", "1.2.9", xcode("1.2.9", "101.2.9")},
{"v1.2.9-0-abcdef", true, "1.2.9-abcdef", "1.2.9", xcode("1.2.9", "101.2.9")},
{"v1.15.0-129-abcdef", true, "1.15.129-abcdef", "1.15.129", xcode("1.15.129", "101.15.129")},
{"v0.98-123-abcdef", true, "0.0.0-abcdef", "0.0.0", xcode("0.0.0", "100.0.0")},
{"v1.0.0-37-abcdef", true, "0.0.0-abcdef", "0.0.0", xcode("0.0.0", "100.0.0")},
{"v0.99.5-0-abcdef", false, "", "", ""}, // unstable, patch not allowed
{"v0.99.5-123-abcdef", false, "", "", ""}, // unstable, patch not allowed
{"v1-abcdef", false, "", "", ""}, // bad semver
{"v1.0", false, "", "", ""}, // missing suffix
{"abcdef", "", 0, 98, 0, 0, `
VERSION_SHORT="0.98.0"
VERSION_LONG="0.98.0-tabcdef"
VERSION_GIT_HASH="abcdef"
VERSION_EXTRA_HASH=""
VERSION_XCODE="100.98.0"
VERSION_WINRES="0,98,0,0"`},
{"abcdef", "", 0, 98, 1, 0, `
VERSION_SHORT="0.98.1"
VERSION_LONG="0.98.1-tabcdef"
VERSION_GIT_HASH="abcdef"
VERSION_EXTRA_HASH=""
VERSION_XCODE="100.98.1"
VERSION_WINRES="0,98,1,0"`},
{"abcdef", "", 1, 1, 0, 37, `
VERSION_SHORT="1.1.1037"
VERSION_LONG="1.1.1037-tabcdef"
VERSION_GIT_HASH="abcdef"
VERSION_EXTRA_HASH=""
VERSION_XCODE="101.1.1037"
VERSION_WINRES="1,1,1037,0"`},
{"abcdef", "", 1, 2, 9, 0, `
VERSION_SHORT="1.2.9"
VERSION_LONG="1.2.9-tabcdef"
VERSION_GIT_HASH="abcdef"
VERSION_EXTRA_HASH=""
VERSION_XCODE="101.2.9"
VERSION_WINRES="1,2,9,0"`},
{"abcdef", "", 1, 15, 0, 129, `
VERSION_SHORT="1.15.129"
VERSION_LONG="1.15.129-tabcdef"
VERSION_GIT_HASH="abcdef"
VERSION_EXTRA_HASH=""
VERSION_XCODE="101.15.129"
VERSION_WINRES="1,15,129,0"`},
{"abcdef", "", 1, 2, 0, 17, `
VERSION_SHORT="1.2.0"
VERSION_LONG="1.2.0-17-tabcdef"
VERSION_GIT_HASH="abcdef"
VERSION_EXTRA_HASH=""
VERSION_XCODE="101.2.0"
VERSION_WINRES="1,2,0,0"`},
{"abcdef", "defghi", 1, 15, 0, 129, `
VERSION_SHORT="1.15.129"
VERSION_LONG="1.15.129-tabcdef-gdefghi"
VERSION_GIT_HASH="abcdef"
VERSION_EXTRA_HASH="defghi"
VERSION_XCODE="101.15.129"
VERSION_WINRES="1,15,129,0"`},
{"abcdef", "", 0, 99, 5, 0, ""}, // unstable, patch number not allowed
{"abcdef", "", 0, 99, 5, 123, ""}, // unstable, patch number not allowed
}
for _, test := range tests {
gotlong, longOK := mkversion(t, "long", test.in)
if longOK != test.ok {
t.Errorf("mkversion.sh long %q ok=%v, want %v", test.in, longOK, test.ok)
want := strings.ReplaceAll(strings.TrimSpace(test.want), " ", "")
got, ok := mkversion(t, test.gitHash, test.otherHash, test.major, test.minor, test.patch, test.changeCount)
invoc := fmt.Sprintf("version.sh %s %s %d %d %d %d", test.gitHash, test.otherHash, test.major, test.minor, test.patch, test.changeCount)
if want == "" && ok {
t.Errorf("%s ok=true, want false", invoc)
continue
}
gotshort, shortOK := mkversion(t, "short", test.in)
if shortOK != test.ok {
t.Errorf("mkversion.sh short %q ok=%v, want %v", test.in, shortOK, test.ok)
}
gotxcode, xcodeOK := mkversion(t, "xcode", test.in)
if xcodeOK != test.ok {
t.Errorf("mkversion.sh xcode %q ok=%v, want %v", test.in, xcodeOK, test.ok)
}
if longOK && gotlong != test.long {
t.Errorf("mkversion.sh long %q: got %q, want %q", test.in, gotlong, test.long)
}
if shortOK && gotshort != test.short {
t.Errorf("mkversion.sh short %q: got %q, want %q", test.in, gotshort, test.short)
}
if xcodeOK && gotxcode != test.xcode {
t.Errorf("mkversion.sh xcode %q: got %q, want %q", test.in, gotxcode, test.xcode)
if diff := cmp.Diff(got, want); want != "" && diff != "" {
t.Errorf("%s wrong output (-got+want):\n%s", invoc, diff)
}
}
}

25
version/print.go Normal file
View File

@@ -0,0 +1,25 @@
// 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 version
import (
"fmt"
"runtime"
"strings"
)
func String() string {
var ret strings.Builder
ret.WriteString(Short)
ret.WriteByte('\n')
if GitCommit != "" {
fmt.Fprintf(&ret, " tailscale commit: %s\n", GitCommit)
}
if ExtraGitCommit != "" {
fmt.Fprintf(&ret, " other commit: %s\n", ExtraGitCommit)
}
fmt.Fprintf(&ret, " go version: %s\n", runtime.Version())
return strings.TrimSpace(ret.String())
}

View File

@@ -1,4 +0,0 @@
redo-ifchange mkversion.sh describe.txt
read -r describe <describe.txt
ver=$(./mkversion.sh short "$describe")
echo "$ver" >$3

View File

@@ -1,8 +1,9 @@
redo-ifchange long.txt short.txt ver.go.in
redo-ifchange version-info.sh ver.go.in
read -r LONGVER <long.txt
read -r SHORTVER <short.txt
. ./version-info.sh
sed -e "s/{LONGVER}/$LONGVER/g" \
-e "s/{SHORTVER}/$SHORTVER/g" \
sed -e "s/{LONGVER}/$VERSION_LONG/g" \
-e "s/{SHORTVER}/$VERSION_SHORT/g" \
-e "s/{GITCOMMIT}/$VERSION_GIT_HASH/g" \
-e "s/{EXTRAGITCOMMIT}/$VERSION_EXTRA_HASH/g" \
<ver.go.in >$3

View File

@@ -6,5 +6,9 @@
package version
const LONG = "{LONGVER}"
const SHORT = "{SHORTVER}"
const Long = "{LONGVER}"
const Short = "{SHORTVER}"
const LONG = Long
const SHORT = Short
const GitCommit = "{GITCOMMIT}"
const ExtraGitCommit = "{EXTRAGITCOMMIT}"

View File

@@ -0,0 +1,3 @@
./version.sh ../.. >$3
redo-always
redo-stamp <$3

View File

@@ -7,5 +7,35 @@
// Package version provides the version that the binary was built at.
package version
const LONG = "date.20200820"
const SHORT = LONG
// 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.
const Long = "date.20200921"
// Short is a short version number for this build, of the form
// "x.y.z", or "date.yyyymmdd" if no actual version was provided.
const Short = Long
// LONG is a deprecated alias for Long. Don't use it.
const LONG = Long
// SHORT is a deprecated alias for Short. Don't use it.
const SHORT = Short
// GitCommit, if non-empty, is the git commit of the
// github.com/tailscale/tailscale repository at which Tailscale was
// built. Its format is the one returned by `git describe --always
// --exclude "*" --dirty --abbrev=200`.
const GitCommit = ""
// ExtraGitCommit, if non-empty, is the git commit of a "supplemental"
// repository at which Tailscale was built. Its format is the same as
// gitCommit.
//
// ExtraGitCommit is used to track the source revision when the main
// Tailscale repository is integrated into and built from another
// repository (for example, Tailscale's proprietary code, or the
// Android OSS repository). Together, GitCommit and ExtraGitCommit
// exactly describe what repositories and commits were used in a
// build.
const ExtraGitCommit = ""

View File

@@ -1,11 +1,9 @@
redo-ifchange long.txt short.txt
read -r long <long.txt
read -r short <short.txt
redo-ifchange version-info.sh
# get it into "major.minor.patch" format
ver=$(echo "$ver" | sed -e 's/-/./')
. ./version-info.sh
(
printf '#define TAILSCALE_VERSION_LONG "%s"\n' "$long"
printf '#define TAILSCALE_VERSION_SHORT "%s"\n' "$short"
) >$3
cat >$3 <<EOF
#define TAILSCALE_VERSION_LONG "$VERSION_LONG"
#define TAILSCALE_VERSION_SHORT "$VERSION_SHORT"
#define TAILSCALE_VERSION_WIN_RES $VERSION_WINRES
EOF

130
version/version.sh Executable file
View File

@@ -0,0 +1,130 @@
#!/bin/sh
set -eu
# Return the commitid of the given ref in the given repo dir. If the worktree
# or index is dirty, also appends -dirty.
#
# $ git_hash_dirty ../.. HEAD
# 1be01ddc6e430ca3aa9beea3587d16750efb3241-dirty
git_hash_dirty() {
(
cd "$1"
x=$(git rev-parse HEAD)
if ! git diff-index --quiet HEAD; then
x="$x-dirty"
fi
echo "$x"
)
}
case $# in
0|1)
# extra_hash_or_dir is either:
# - a git commitid
# or
# - the path to a git repo from which to calculate the real hash.
#
# It gets embedded as an additional commit hash in built
# binaries, to help us locate the exact set of tools and code
# that were used.
extra_hash_or_dir="${1:-}"
if [ -z "$extra_hash_or_dir" ]; then
# Nothing, empty extra hash is fine.
extra_hash=""
elif [ -d "$extra_hash_or_dir/.git" ]; then
extra_hash=$(git_hash_dirty "$extra_hash_or_dir" HEAD)
elif ! expr "$extra_hash_or_dir" : "^[0-9a-f]*$"; then
echo "Invalid extra hash '$extra_hash_or_dir', must be a git commit or path to a git repo" >&2
exit 1
else
extra_hash="$extra_hash_or_dir"
fi
# Load the base version and optional corresponding git hash
# from the VERSION file. If there is no git hash in the file,
# we use the hash of the last change to the VERSION file.
version_file="$(dirname $0)/../VERSION.txt"
IFS=".$IFS" read -r major minor patch base_git_hash <"$version_file"
if [ -z "$base_git_hash" ]; then
base_git_hash=$(git rev-list --max-count=1 HEAD -- "$version_file")
fi
git_hash=$(git_hash_dirty . HEAD)
# The number of extra commits between the release base to git_hash.
change_count=$(git rev-list --count HEAD "^$base_git_hash")
;;
6)
# Test mode: rather than run git commands and whatnot, take in
# all the version pieces as arguments.
git_hash=$1
extra_hash=$2
major=$3
minor=$4
patch=$5
change_count=$6
;;
*)
echo "Usage: $0 [extra-git-commitid-or-dir]"
exit 1
esac
# Shortened versions of git hashes, so that they fit neatly into an
# "elongated" but still human-readable version number.
short_git_hash=$(echo "$git_hash" | cut -c1-9)
short_extra_hash=$(echo "$extra_hash" | cut -c1-9)
# Convert major/minor/patch/change_count into an adjusted
# major/minor/patch. This block is where all our policies on
# versioning are.
if expr "$minor" : "[0-9]*[13579]$" >/dev/null; then
# Odd minor numbers are unstable builds.
if [ "$patch" != "0" ]; then
# This is a fatal error, because a non-zero patch number
# indicates that we created an unstable VERSION.txt in violation
# of our versioning policy, and we want to blow up loudly to
# get that fixed.
echo "Unstable release $major.$minor.$patch has a non-zero patch number, which is not allowed" >&2
exit 1
fi
patch="$change_count"
change_suffix=""
elif [ "$change_count" != "0" ]; then
# Even minor numbers are stable builds, but stable builds are
# supposed to have a zero change count. Therefore, we're currently
# describing a commit that's on a release branch, but hasn't been
# tagged as a patch release yet.
#
# We used to change the version number to 0.0.0 in that case, but that
# caused some features to get disabled due to the low version number.
# Instead, add yet another suffix to the version number, with a change
# count.
change_suffix="-$change_count"
else
# Even minor number with no extra changes.
change_suffix=""
fi
# Hack for 1.1: add 1000 to the patch number. We switched from using
# the proprietary repo's change_count over to using the OSS repo's
# change_count, and this was necessary to avoid a backwards jump in
# release numbers.
if [ "$major.$minor" = "1.1" ]; then
patch="$((patch + 1000))"
fi
# At this point, the version number correctly reflects our
# policies. All that remains is to output the various vars that other
# code can use to embed version data.
if [ -z "$extra_hash" ]; then
long_version_suffix="$change_suffix-t$short_git_hash"
else
long_version_suffix="$change_suffix-t$short_git_hash-g$short_extra_hash"
fi
cat <<EOF
VERSION_SHORT="$major.$minor.$patch"
VERSION_LONG="$major.$minor.$patch$long_version_suffix"
VERSION_GIT_HASH="$git_hash"
VERSION_EXTRA_HASH="$extra_hash"
VERSION_XCODE="$((major + 100)).$minor.$patch"
VERSION_WINRES="$major,$minor,$patch,0"
EOF

View File

@@ -1,4 +1,14 @@
redo-ifchange mkversion.sh describe.txt
read -r describe <describe.txt
ver=$(./mkversion.sh xcode "$describe")
echo "$ver" >$3
redo-ifchange version-info.sh
. ./version-info.sh
# CFBundleShortVersionString: the "short name" used in the App Store.
# eg. 0.92.98
echo "VERSION_NAME = $VERSION_SHORT"
# CFBundleVersion: the build number. Needs to be 3 numeric sections
# that increment for each release according to SemVer rules.
#
# We start counting at 100 because we submitted using raw build
# numbers before, and Apple doesn't let you start over. e.g. 0.98.3
# -> 100.98.3
echo "VERSION_ID = $VERSION_XCODE"

View File

@@ -6,6 +6,12 @@
package version
// Replaced at build time with the Go linker flag -X.
var LONG string = "<not set>"
var SHORT string = "<not set>"
// Replaced at build time with the Go linker flag -X. See
// ../build_dist.sh for example usage, and version.go for field
// documentation.
var Long string = "<not set>"
var Short string = "<not set>"
var LONG = Long
var SHORT = Short
var GitCommit = ""
var ExtraGitCommit = ""

View File

@@ -367,6 +367,15 @@ func (f *Filter) pre(q *packet.ParsedPacket, rf RunFlags, dir direction) Respons
f.logRateLimit(rf, q, dir, Drop, "ipv6")
return Drop
}
if q.DstIP.IsMulticast() {
f.logRateLimit(rf, q, dir, Drop, "multicast")
return Drop
}
if q.DstIP.IsMostLinkLocalUnicast() {
f.logRateLimit(rf, q, dir, Drop, "link-local-unicast")
return Drop
}
switch q.IPProto {
case packet.Unknown:
// Unknown packets are dangerous; always drop them.
@@ -409,6 +418,9 @@ func omitDropLogging(p *packet.ParsedPacket, dir direction) bool {
if ipProto == packet.IGMP {
return true
}
if p.DstIP.IsMulticast() || p.DstIP.IsMostLinkLocalUnicast() {
return true
}
case 6:
if len(b) < 40 {
return false

View File

@@ -379,6 +379,24 @@ func TestOmitDropLogging(t *testing.T) {
dir: out,
want: true,
},
{
name: "v4_multicast_out_low",
pkt: &packet.ParsedPacket{IPVersion: 4, DstIP: packet.NewIP(net.ParseIP("224.0.0.0"))},
dir: out,
want: true,
},
{
name: "v4_multicast_out_high",
pkt: &packet.ParsedPacket{IPVersion: 4, DstIP: packet.NewIP(net.ParseIP("239.255.255.255"))},
dir: out,
want: true,
},
{
name: "v4_link_local_unicast",
pkt: &packet.ParsedPacket{IPVersion: 4, DstIP: packet.NewIP(net.ParseIP("169.254.1.2"))},
dir: out,
want: true,
},
}
for _, tt := range tests {

View File

@@ -40,6 +40,7 @@ import (
"tailscale.com/derp/derphttp"
"tailscale.com/disco"
"tailscale.com/ipn/ipnstate"
"tailscale.com/logtail/backoff"
"tailscale.com/net/dnscache"
"tailscale.com/net/interfaces"
"tailscale.com/net/netcheck"
@@ -119,6 +120,7 @@ type Conn struct {
netChecker *netcheck.Client
idleFunc func() time.Duration // nil means unknown
noteRecvActivity func(tailcfg.DiscoKey) // or nil, see Options.NoteRecvActivity
simulatedNetwork bool
// bufferedIPv4From and bufferedIPv4Packet are owned by
// ReceiveIPv4, and used when both a DERP and IPv4 packet arrive
@@ -208,6 +210,11 @@ type Conn struct {
// necessarily have a netcheck.Report and don't want to skip
// logging.
noV4, noV6 syncs.AtomicBool
// networkUp is whether the network is up (some interface is up
// with IPv4 or IPv6). It's used to suppress log spam and prevent
// new connection that'll fail.
networkUp syncs.AtomicBool
}
// derpRoute is a route entry for a public key, saying that a certain
@@ -301,11 +308,18 @@ type Options struct {
// sole user just doesn't need or want it called on every
// packet, just every minute or two for Wireguard timeouts,
// and 10 seconds seems like a good trade-off between often
// enough and not too often.) The provided func is called while
// holding userspaceEngine.wgLock and likely calls
// Conn.CreateEndpoint, which acquires Conn.mu. As such, you should
// not hold
// enough and not too often.) The provided func is called
// while holding userspaceEngine.wgLock and likely calls
// Conn.CreateEndpoint, which acquires Conn.mu. As such, you
// should not hold Conn.mu while calling it.
NoteRecvActivity func(tailcfg.DiscoKey)
// SimulatedNetwork can be set true in tests to signal that
// the network is simulated and thus it's okay to bind on the
// unspecified address (which we'd normally avoid to avoid
// triggering macOS and Windows firwall dialog boxes during
// "go test").
SimulatedNetwork bool
}
func (o *Options) logf() logger.Logf {
@@ -345,6 +359,7 @@ func newConn() *Conn {
discoOfAddr: make(map[netaddr.IPPort]tailcfg.DiscoKey),
}
c.muCond = sync.NewCond(&c.mu)
c.networkUp.Set(true) // assume up until told otherwise
return c
}
@@ -362,6 +377,7 @@ func NewConn(opts Options) (*Conn, error) {
c.idleFunc = opts.IdleFunc
c.packetListener = opts.PacketListener
c.noteRecvActivity = opts.NoteRecvActivity
c.simulatedNetwork = opts.SimulatedNetwork
if err := c.initialBind(); err != nil {
return nil, err
@@ -369,8 +385,9 @@ func NewConn(opts Options) (*Conn, error) {
c.connCtx, c.connCtxCancel = context.WithCancel(context.Background())
c.netChecker = &netcheck.Client{
Logf: logger.WithPrefix(c.logf, "netcheck: "),
GetSTUNConn4: func() netcheck.STUNConn { return c.pconn4 },
Logf: logger.WithPrefix(c.logf, "netcheck: "),
GetSTUNConn4: func() netcheck.STUNConn { return c.pconn4 },
SkipExternalNetwork: inTest(),
}
if c.pconn6 != nil {
c.netChecker.GetSTUNConn6 = func() netcheck.STUNConn { return c.pconn6 }
@@ -431,7 +448,7 @@ func (c *Conn) updateEndpoints(why string) {
return
}
if c.setEndpoints(endpoints) {
if c.setEndpoints(endpoints, reasons) {
c.logEndpointChange(endpoints, reasons)
c.epFunc(endpoints)
}
@@ -439,9 +456,31 @@ func (c *Conn) updateEndpoints(why string) {
// setEndpoints records the new endpoints, reporting whether they're changed.
// It takes ownership of the slice.
func (c *Conn) setEndpoints(endpoints []string) (changed bool) {
func (c *Conn) setEndpoints(endpoints []string, reasons map[string]string) (changed bool) {
anySTUN := false
for _, reason := range reasons {
if reason == "stun" {
anySTUN = true
}
}
c.mu.Lock()
defer c.mu.Unlock()
if !anySTUN && c.derpMap == nil && !inTest() {
// Don't bother storing or reporting this yet. We
// don't have a DERP map or any STUN entries, so we're
// just starting up. A DERP map should arrive shortly
// and then we'll have more interesting endpoints to
// report. This saves a map update.
// TODO(bradfitz): this optimization is currently
// skipped during the e2e tests because they depend
// too much on the exact sequence of updates. Fix the
// tests. But a protocol rewrite might happen first.
c.logf("magicsock: ignoring pre-DERP map, STUN-less endpoint update: %v", endpoints)
return false
}
if stringsEqual(endpoints, c.lastEndpoints) {
return false
}
@@ -454,7 +493,7 @@ func (c *Conn) updateNetInfo(ctx context.Context) (*netcheck.Report, error) {
dm := c.derpMap
c.mu.Unlock()
if dm == nil {
if dm == nil || c.networkDown() {
return new(netcheck.Report), nil
}
@@ -969,8 +1008,15 @@ func (as *AddrSet) appendDests(dsts []netaddr.IPPort, b []byte) (_ []netaddr.IPP
}
var errNoDestinations = errors.New("magicsock: no destinations")
var errNetworkDown = errors.New("magicsock: network down")
func (c *Conn) networkDown() bool { return !c.networkUp.Get() }
func (c *Conn) Send(b []byte, ep conn.Endpoint) error {
if c.networkDown() {
return errNetworkDown
}
var as *AddrSet
switch v := ep.(type) {
default:
@@ -1111,6 +1157,10 @@ func (c *Conn) derpWriteChanOfAddr(addr netaddr.IPPort, peer key.Public) chan<-
}
regionID := int(addr.Port)
if c.networkDown() {
return nil
}
c.mu.Lock()
defer c.mu.Unlock()
if !c.wantDerpLocked() || c.closed {
@@ -1301,36 +1351,45 @@ func (c *Conn) runDerpReader(ctx context.Context, derpFakeAddr netaddr.IPPort, d
// peerPresent is the set of senders we know are present on this
// connection, based on messages we've received from the server.
peerPresent := map[key.Public]bool{}
bo := backoff.NewBackoff(fmt.Sprintf("derp-%d", regionID), c.logf, 5*time.Second)
for {
msg, err := dc.Recv()
if err == derphttp.ErrClientClosed {
return
}
if err != nil {
// Forget that all these peers have routes.
for peer := range peerPresent {
delete(peerPresent, peer)
c.removeDerpPeerRoute(peer, regionID, dc)
}
if err == derphttp.ErrClientClosed {
return
}
if c.networkDown() {
c.logf("magicsock: derp.Recv(derp-%d): network down, closing", regionID)
return
}
select {
case <-ctx.Done():
return
default:
}
c.ReSTUN("derp-close")
c.logf("magicsock: [%p] derp.Recv(derp-%d): %v", dc, regionID, err)
// Avoid excessive spinning.
// TODO: use a backoff timer, perhaps between 10ms and 500ms?
// Don't want to sleep too long. For now 250ms seems fine.
// If our DERP connection broke, it might be because our network
// conditions changed. Start that check.
c.ReSTUN("derp-recv-error")
// Back off a bit before reconnecting.
bo.BackOff(ctx, err)
select {
case <-ctx.Done():
return
case <-time.After(250 * time.Millisecond):
default:
}
continue
}
bo.BackOff(ctx, nil) // reset
switch m := msg.(type) {
case derp.ReceivedPacket:
pkt = m
@@ -1691,7 +1750,9 @@ func (c *Conn) sendDiscoMessage(dst netaddr.IPPort, dstKey tailcfg.NodeKey, dstD
} else if err == nil {
// Can't send. (e.g. no IPv6 locally)
} else {
c.logf("magicsock: disco: failed to send %T to %v: %v", m, dst, err)
if !c.networkDown() {
c.logf("magicsock: disco: failed to send %T to %v: %v", m, dst, err)
}
}
return sent, err
}
@@ -1956,6 +2017,21 @@ func (c *Conn) sharedDiscoKeyLocked(k tailcfg.DiscoKey) *[32]byte {
return shared
}
func (c *Conn) SetNetworkUp(up bool) {
c.mu.Lock()
defer c.mu.Unlock()
if c.networkUp.Get() == up {
return
}
c.logf("magicsock: SetNetworkUp(%v)", up)
c.networkUp.Set(up)
if !up {
c.closeAllDerpLocked("network-down")
}
}
// SetPrivateKey sets the connection's private key.
//
// This is only used to be able prove our identity when connecting to
@@ -2282,6 +2358,10 @@ func maxIdleBeforeSTUNShutdown() time.Duration {
}
func (c *Conn) shouldDoPeriodicReSTUN() bool {
if c.networkDown() {
return false
}
c.mu.Lock()
defer c.mu.Unlock()
if len(c.peerSet) == 0 {
@@ -2410,8 +2490,11 @@ func (c *Conn) listenPacket(ctx context.Context, network, addr string) (net.Pack
func (c *Conn) bind1(ruc **RebindingUDPConn, which string) error {
host := ""
if inTest() {
if inTest() && !c.simulatedNetwork {
host = "127.0.0.1"
if which == "udp6" {
host = "::1"
}
}
var pc net.PacketConn
var err error
@@ -2440,7 +2523,7 @@ func (c *Conn) bind1(ruc **RebindingUDPConn, which string) error {
// It should be followed by a call to ReSTUN.
func (c *Conn) Rebind() {
host := ""
if inTest() {
if inTest() && !c.simulatedNetwork {
host = "127.0.0.1"
}
listenCtx := context.Background() // unused without DNS name to resolve

View File

@@ -46,6 +46,10 @@ import (
"tailscale.com/wgengine/tstun"
)
func init() {
os.Setenv("IN_TS_TEST", "1")
}
// WaitReady waits until the magicsock is entirely initialized and connected
// to its home DERP server. This is normally not necessary, since magicsock
// is intended to be entirely asynchronous, but it helps eliminate race
@@ -118,7 +122,7 @@ type magicStack struct {
privateKey wgcfg.PrivateKey
epCh chan []string // endpoint updates produced by this peer
conn *Conn // the magicsock itself
tun *tuntest.ChannelTUN // tuntap device to send/receive packets
tun *tuntest.ChannelTUN // TUN device to send/receive packets
tsTun *tstun.TUN // wrapped tun that implements filtering and wgengine hooks
dev *device.Device // the wireguard-go Device that connects the previous things
}
@@ -141,6 +145,7 @@ func newMagicStack(t *testing.T, logf logger.Logf, l nettype.PacketListener, der
EndpointsFunc: func(eps []string) {
epCh <- eps
},
SimulatedNetwork: l != nettype.Std{},
})
if err != nil {
t.Fatalf("constructing magicsock: %v", err)
@@ -374,7 +379,7 @@ collectEndpoints:
func pickPort(t *testing.T) uint16 {
t.Helper()
conn, err := net.ListenPacket("udp4", ":0")
conn, err := net.ListenPacket("udp4", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}

View File

@@ -7,6 +7,8 @@ package monitor
import (
"context"
"errors"
"fmt"
"runtime"
"sync"
"syscall"
"time"
@@ -24,9 +26,15 @@ const (
)
var (
iphlpapi = syscall.NewLazyDLL("iphlpapi.dll")
notifyAddrChangeProc = iphlpapi.NewProc("NotifyAddrChange")
notifyRouteChangeProc = iphlpapi.NewProc("NotifyRouteChange")
iphlpapi = syscall.NewLazyDLL("iphlpapi.dll")
notifyAddrChangeProc = iphlpapi.NewProc("NotifyAddrChange")
notifyRouteChangeProc = iphlpapi.NewProc("NotifyRouteChange")
cancelIPChangeNotifyProc = iphlpapi.NewProc("CancelIPChangeNotify")
)
const (
_STATUS_PENDING = 0x00000103 // 259
_STATUS_WAIT_0 = 0
)
type unspecifiedMessage struct{}
@@ -43,27 +51,33 @@ type messageOrError struct {
}
type winMon struct {
ctx context.Context
cancel context.CancelFunc
messagec chan messageOrError
logf logger.Logf
pollTicker *time.Ticker
lastState *interfaces.State
ctx context.Context
cancel context.CancelFunc
messagec chan messageOrError
logf logger.Logf
pollTicker *time.Ticker
lastState *interfaces.State
closeHandle windows.Handle // signaled upon close
mu sync.Mutex
event windows.Handle
lastNetChange time.Time
inFastPoll bool // recent net change event made us go into fast polling mode (to detect proxy changes)
}
func newOSMon(logf logger.Logf) (osMon, error) {
closeHandle, err := windows.CreateEvent(nil, 1 /* manual reset */, 0 /* unsignaled */, nil /* no name */)
if err != nil {
return nil, fmt.Errorf("CreateEvent: %w", err)
}
ctx, cancel := context.WithCancel(context.Background())
m := &winMon{
logf: logf,
ctx: ctx,
cancel: cancel,
messagec: make(chan messageOrError, 1),
pollTicker: time.NewTicker(pollIntervalSlow),
logf: logf,
ctx: ctx,
cancel: cancel,
messagec: make(chan messageOrError, 1),
pollTicker: time.NewTicker(pollIntervalSlow),
closeHandle: closeHandle,
}
go m.awaitIPAndRouteChanges()
return m, nil
@@ -72,14 +86,7 @@ func newOSMon(logf logger.Logf) (osMon, error) {
func (m *winMon) Close() error {
m.cancel()
m.pollTicker.Stop()
m.mu.Lock()
defer m.mu.Unlock()
if h := m.event; h != 0 {
// Wake up any reader blocked in Receive.
windows.SetEvent(h)
}
windows.SetEvent(m.closeHandle) // wakes up any reader blocked in Receive
return nil
}
@@ -136,52 +143,80 @@ func (m *winMon) getIPOrRouteChangeMessage() (message, error) {
return nil, errClosed
}
var o windows.Overlapped
h, err := windows.CreateEvent(nil, 1 /* true*/, 0 /* unsignaled */, nil /* no name */)
if err != nil {
m.logf("CreateEvent: %v", err)
return nil, err
}
defer windows.CloseHandle(h)
// TODO(bradfitz): locking ourselves to an OS thread here
// likely isn't necessary, but also can't really hurt.
// We'll be blocked in windows.WaitForMultipleObjects below
// anyway, so might as well stay on this thread during the
// notify calls and cancel funcs.
// Given the past memory corruption from misuse of these APIs,
// and my continued lack of understanding of Windows APIs,
// I'll be paranoid. But perhaps we can remove this once
// we understand more.
runtime.LockOSThread()
defer runtime.UnlockOSThread()
m.mu.Lock()
m.event = h
m.mu.Unlock()
o.HEvent = h
err = notifyAddrChange(&h, &o)
addrHandle, oaddr, cancel, err := notifyAddrChange()
if err != nil {
m.logf("notifyAddrChange: %v", err)
return nil, err
}
err = notifyRouteChange(&h, &o)
defer cancel()
routeHandle, oroute, cancel, err := notifyRouteChange()
if err != nil {
m.logf("notifyRouteChange: %v", err)
return nil, err
}
defer cancel()
t0 := time.Now()
_, err = windows.WaitForSingleObject(o.HEvent, windows.INFINITE)
if m.ctx.Err() != nil {
eventNum, err := windows.WaitForMultipleObjects([]windows.Handle{
m.closeHandle, // eventNum 0
addrHandle, // eventNum 1
routeHandle, // eventNum 2
}, false, windows.INFINITE)
if m.ctx.Err() != nil || (err == nil && eventNum == 0) {
return nil, errClosed
}
if err != nil {
m.logf("waitForSingleObject: %v", err)
m.logf("waitForMultipleObjects: %v", err)
return nil, err
}
d := time.Since(t0)
m.logf("got windows change event after %v", d)
var eventStr string
// notifyAddrChange and notifyRouteChange both seem to return the same
// handle value. Determine which fired by looking at the "Internal" (sic)
// field of the Ovelapped instead.
// TODO(bradfitz): maybe clean this up; see TODO in callNotifyProc.
if (eventNum == 1 || eventNum == 2) && addrHandle == routeHandle {
if oaddr.Internal == _STATUS_WAIT_0 && oroute.Internal == _STATUS_PENDING {
eventStr = "addr-o" // "-o" overlapped suffix to distinguish from "addr" below
} else if oroute.Internal == _STATUS_WAIT_0 && oaddr.Internal == _STATUS_PENDING {
eventStr = "route-o"
} else {
eventStr = fmt.Sprintf("[unexpected] addr.internal=%d; route.internal=%d", oaddr.Internal, oroute.Internal)
}
} else {
switch eventNum {
case 1:
eventStr = "addr"
case 2:
eventStr = "route"
default:
eventStr = fmt.Sprintf("%d [unexpected]", eventNum)
}
}
m.logf("got windows change event after %v: evt=%s", d, eventStr)
m.mu.Lock()
{
m.lastNetChange = time.Now()
m.event = 0
// Something changed, so assume Windows is about to
// discover its new proxy settings from WPAD, which
// seems to take a bit. Poll heavily for awhile.
m.logf("starting quick poll, waiting for WPAD change")
m.inFastPoll = true
m.pollTicker.Reset(pollIntervalFast)
}
@@ -190,23 +225,46 @@ func (m *winMon) getIPOrRouteChangeMessage() (message, error) {
return unspecifiedMessage{}, nil
}
func notifyAddrChange(h *windows.Handle, o *windows.Overlapped) error {
return callNotifyProc(notifyAddrChangeProc, h, o)
func notifyAddrChange() (h windows.Handle, o *windows.Overlapped, cancel func(), err error) {
return callNotifyProc(notifyAddrChangeProc)
}
func notifyRouteChange(h *windows.Handle, o *windows.Overlapped) error {
return callNotifyProc(notifyAddrChangeProc, h, o)
func notifyRouteChange() (h windows.Handle, o *windows.Overlapped, cancel func(), err error) {
return callNotifyProc(notifyRouteChangeProc)
}
func callNotifyProc(p *syscall.LazyProc, h *windows.Handle, o *windows.Overlapped) error {
r1, _, e1 := p.Call(uintptr(unsafe.Pointer(h)), uintptr(unsafe.Pointer(o)))
expect := uintptr(0)
if h != nil || o != nil {
const ERROR_IO_PENDING = 997
expect = ERROR_IO_PENDING
func callNotifyProc(p *syscall.LazyProc) (h windows.Handle, o *windows.Overlapped, cancel func(), err error) {
o = new(windows.Overlapped)
// TODO(bradfitz): understand why this if-false code doesn't
// work, even though the docs online suggest we should pass an
// event in the overlapped.Hevent field.
// The docs at
// https://docs.microsoft.com/en-us/windows/win32/api/minwinbase/ns-minwinbase-overlapped
// says that o.HEvent can be zero, though, which seems to work.
// Note that the returned windows.Handle returns the same value for both
// notifyAddrChange and notifyRouteChange, which is why our caller needs
// to look at the returned Overlapped's Internal field to see which case
// fired. That's also worth understanding more.
// See crawshaw's comment at https://github.com/tailscale/tailscale/pull/944#discussion_r526469186
// too.
if false {
evt, err := windows.CreateEvent(nil, 0, 0, nil)
if err != nil {
return 0, nil, nil, err
}
o.HEvent = evt
}
if r1 == expect {
return nil
r1, _, e1 := syscall.Syscall(p.Addr(), 2, uintptr(unsafe.Pointer(&h)), uintptr(unsafe.Pointer(o)), 0)
// We expect ERROR_IO_PENDING.
if syscall.Errno(r1) != windows.ERROR_IO_PENDING {
return 0, nil, nil, e1
}
return e1
cancel = func() {
cancelIPChangeNotifyProc.Call(uintptr(unsafe.Pointer(o)))
}
return h, o, cancel, nil
}

View File

@@ -39,6 +39,18 @@ func (ip IP) String() string {
return fmt.Sprintf("%d.%d.%d.%d", byte(ip>>24), byte(ip>>16), byte(ip>>8), byte(ip))
}
func (ip IP) IsMulticast() bool {
return byte(ip>>24)&0xf0 == 0xe0
}
func (ip IP) IsLinkLocalUnicast() bool {
return byte(ip>>24) == 169 && byte(ip>>16) == 254
}
func (ip IP) IsMostLinkLocalUnicast() bool {
return ip.IsLinkLocalUnicast() && ip != 0xA9FEA9FE
}
// IPProto is either a real IP protocol (ITCP, UDP, ...) or an special value like Unknown.
// If it is a real IP protocol, its value corresponds to its IP protocol number.
type IPProto uint8

View File

@@ -63,7 +63,8 @@ func (lhs Config) Equal(rhs Config) bool {
// ManagerConfig is the set of parameters from which
// a manager implementation is chosen and initialized.
type ManagerConfig struct {
// logf is the logger for the manager to use.
// Logf is the logger for the manager to use.
// It is wrapped with a "dns: " prefix.
Logf logger.Logf
// InterfaceNAme is the name of the interface with which DNS settings should be associated.
InterfaceName string

View File

@@ -9,6 +9,7 @@ package dns
import (
"bufio"
"bytes"
"errors"
"fmt"
"io"
"io/ioutil"
@@ -130,7 +131,7 @@ func (m directManager) Up(config Config) error {
contents, err := ioutil.ReadFile(resolvConf)
// If the original did not exist, still back up an empty file.
// The presence of a backup file is the way we know that Up ran.
if err != nil && !os.IsNotExist(err) {
if err != nil && !errors.Is(err, os.ErrNotExist) {
return err
}
if err := atomicfile.WriteFile(backupConf, contents, 0644); err != nil {

View File

@@ -5,9 +5,11 @@
package dns
import (
"errors"
"fmt"
"os/exec"
"strings"
"syscall"
"time"
"github.com/tailscale/wireguard-go/tun"
"golang.org/x/sys/windows/registry"
@@ -46,68 +48,16 @@ func setRegistryString(path, name, value string) error {
return nil
}
func getRegistryString(path, name string) (string, error) {
key, err := registry.OpenKey(registry.LOCAL_MACHINE, path, registry.READ)
if err != nil {
return "", fmt.Errorf("opening %s: %w", path, err)
}
defer key.Close()
value, _, err := key.GetStringValue(name)
if err != nil {
return "", fmt.Errorf("getting %s[%s]: %w", path, name, err)
}
return value, nil
}
func (m windowsManager) setNameservers(basePath string, nameservers []string) error {
path := fmt.Sprintf(`%s\Interfaces\%s`, basePath, m.guid)
value := strings.Join(nameservers, ",")
return setRegistryString(path, "NameServer", value)
}
func (m windowsManager) setDomains(path string, oldDomains, newDomains []string) error {
// We reimplement setRegistryString to ensure that we hold the key for the whole operation.
key, err := registry.OpenKey(registry.LOCAL_MACHINE, path, registry.READ|registry.SET_VALUE)
if err != nil {
return fmt.Errorf("opening %s: %w", path, err)
}
defer key.Close()
searchList, _, err := key.GetStringValue("SearchList")
if err != nil && err != registry.ErrNotExist {
return fmt.Errorf("getting %s[SearchList]: %w", path, err)
}
currentDomains := strings.Split(searchList, ",")
var domainsToSet []string
for _, domain := range currentDomains {
inOld, inNew := false, false
// The number of domains should be small,
// so this is probaly faster than constructing a map.
for _, oldDomain := range oldDomains {
if domain == oldDomain {
inOld = true
}
}
for _, newDomain := range newDomains {
if domain == newDomain {
inNew = true
}
}
if !inNew && !inOld {
domainsToSet = append(domainsToSet, domain)
}
}
domainsToSet = append(domainsToSet, newDomains...)
searchList = strings.Join(domainsToSet, ",")
if err := key.SetStringValue("SearchList", searchList); err != nil {
return fmt.Errorf("setting %s[SearchList]: %w", path, err)
}
return nil
func (m windowsManager) setDomains(basePath string, domains []string) error {
path := fmt.Sprintf(`%s\Interfaces\%s`, basePath, m.guid)
value := strings.Join(domains, ",")
return setRegistryString(path, "SearchList", value)
}
func (m windowsManager) Up(config Config) error {
@@ -122,23 +72,17 @@ func (m windowsManager) Up(config Config) error {
}
}
lastSearchList, err := getRegistryString(tsRegBase, "SearchList")
if err != nil && !errors.Is(err, registry.ErrNotExist) {
return err
}
lastDomains := strings.Split(lastSearchList, ",")
if err := m.setNameservers(ipv4RegBase, ipsv4); err != nil {
return err
}
if err := m.setDomains(ipv4RegBase, lastDomains, config.Domains); err != nil {
if err := m.setDomains(ipv4RegBase, config.Domains); err != nil {
return err
}
if err := m.setNameservers(ipv6RegBase, ipsv6); err != nil {
return err
}
if err := m.setDomains(ipv6RegBase, lastDomains, config.Domains); err != nil {
if err := m.setDomains(ipv6RegBase, config.Domains); err != nil {
return err
}
@@ -147,6 +91,26 @@ func (m windowsManager) Up(config Config) error {
return err
}
// Force DNS re-registration in Active Directory. What we actually
// care about is that this command invokes the undocumented hidden
// function that forces Windows to notice that adapter settings
// have changed, which makes the DNS settings actually take
// effect.
//
// This command can take a few seconds to run, so run it async, best effort.
go func() {
t0 := time.Now()
m.logf("running ipconfig /registerdns ...")
cmd := exec.Command("ipconfig", "/registerdns")
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
d := time.Since(t0).Round(time.Millisecond)
if err := cmd.Run(); err != nil {
m.logf("error running ipconfig /registerdns after %v: %v", d, err)
} else {
m.logf("ran ipconfig /registerdns in %v", d)
}
}()
return nil
}

View File

@@ -11,14 +11,15 @@ import (
"fmt"
"log"
"net"
"os"
"runtime"
"sort"
"time"
"github.com/go-multierror/multierror"
ole "github.com/go-ole/go-ole"
winipcfg "github.com/tailscale/winipcfg-go"
"github.com/tailscale/wireguard-go/tun"
"golang.org/x/sys/windows"
"golang.zx2c4.com/wireguard/windows/tunnel/winipcfg"
"tailscale.com/net/interfaces"
"tailscale.com/wgengine/winnet"
)
@@ -39,51 +40,51 @@ import (
// help with MTU issues compared to a static 1280B implementation.
func monitorDefaultRoutes(tun *tun.NativeTun) (*winipcfg.RouteChangeCallback, error) {
guid := tun.GUID()
ourLuid, err := winipcfg.InterfaceGuidToLuid(&guid)
ourLuid, err := winipcfg.LUIDFromGUID(&guid)
lastMtu := uint32(0)
if err != nil {
return nil, err
return nil, fmt.Errorf("error mapping GUID %v to LUID: %w", guid, err)
}
doIt := func() error {
mtu, err := getDefaultRouteMTU()
if err != nil {
return err
return fmt.Errorf("error getting default route MTU: %w", err)
}
if mtu > 0 && (lastMtu == 0 || lastMtu != mtu) {
iface, err := winipcfg.GetIpInterface(ourLuid, winipcfg.AF_INET)
iface, err := ourLuid.IPInterface(windows.AF_INET)
if err != nil {
return err
return fmt.Errorf("error getting v4 interface: %w", err)
}
iface.NlMtu = mtu - 80
iface.NLMTU = mtu - 80
// If the TUN device was created with a smaller MTU,
// though, such as 1280, we don't want to go bigger than
// configured. (See the comment on minimalMTU in the
// wgengine package.)
if min, err := tun.MTU(); err == nil && min < int(iface.NlMtu) {
iface.NlMtu = uint32(min)
if min, err := tun.MTU(); err == nil && min < int(iface.NLMTU) {
iface.NLMTU = uint32(min)
}
if iface.NlMtu < 576 {
iface.NlMtu = 576
if iface.NLMTU < 576 {
iface.NLMTU = 576
}
err = iface.Set()
if err != nil {
return err
return fmt.Errorf("error setting v4 MTU: %w", err)
}
tun.ForceMTU(int(iface.NlMtu))
iface, err = winipcfg.GetIpInterface(ourLuid, winipcfg.AF_INET6)
tun.ForceMTU(int(iface.NLMTU))
iface, err = ourLuid.IPInterface(windows.AF_INET6)
if err != nil {
if !isMissingIPv6Err(err) {
return err
if !errors.Is(err, windows.ERROR_NOT_FOUND) {
return fmt.Errorf("error getting v6 interface: %w", err)
}
} else {
iface.NlMtu = mtu - 80
if iface.NlMtu < 1280 {
iface.NlMtu = 1280
iface.NLMTU = mtu - 80
if iface.NLMTU < 1280 {
iface.NLMTU = 1280
}
err = iface.Set()
if err != nil {
return err
return fmt.Errorf("error setting v6 MTU: %w", err)
}
}
lastMtu = mtu
@@ -94,7 +95,7 @@ func monitorDefaultRoutes(tun *tun.NativeTun) (*winipcfg.RouteChangeCallback, er
if err != nil {
return nil, err
}
cb, err := winipcfg.RegisterRouteChangeCallback(func(notificationType winipcfg.MibNotificationType, route *winipcfg.Route) {
cb, err := winipcfg.RegisterRouteChangeCallback(func(notificationType winipcfg.MibNotificationType, route *winipcfg.MibIPforwardRow2) {
//fmt.Printf("MonitorDefaultRoutes: changed: %v\n", route.DestinationPrefix)
if route.DestinationPrefix.PrefixLength == 0 {
_ = doIt()
@@ -112,7 +113,7 @@ func getDefaultRouteMTU() (uint32, error) {
return 0, err
}
routes, err := winipcfg.GetRoutes(winipcfg.AF_INET)
routes, err := winipcfg.GetIPForwardTable2(windows.AF_INET)
if err != nil {
return 0, err
}
@@ -122,7 +123,7 @@ func getDefaultRouteMTU() (uint32, error) {
if route.DestinationPrefix.PrefixLength != 0 {
continue
}
routeMTU := mtus[route.InterfaceLuid]
routeMTU := mtus[route.InterfaceLUID]
if routeMTU == 0 {
continue
}
@@ -132,7 +133,7 @@ func getDefaultRouteMTU() (uint32, error) {
}
}
routes, err = winipcfg.GetRoutes(winipcfg.AF_INET6)
routes, err = winipcfg.GetIPForwardTable2(windows.AF_INET6)
if err != nil {
return 0, err
}
@@ -141,7 +142,7 @@ func getDefaultRouteMTU() (uint32, error) {
if route.DestinationPrefix.PrefixLength != 0 {
continue
}
routeMTU := mtus[route.InterfaceLuid]
routeMTU := mtus[route.InterfaceLUID]
if routeMTU == 0 {
continue
}
@@ -165,6 +166,13 @@ func setPrivateNetwork(ifcGUID *windows.GUID) (bool, error) {
categoryPrivate = 1
categoryDomain = 2
)
// Lock OS thread when using OLE, which seems to be a requirement
// from the Microsoft docs. go-ole doesn't seem to handle it automatically.
// https://github.com/tailscale/tailscale/issues/921#issuecomment-727526807
runtime.LockOSThread()
defer runtime.UnlockOSThread()
var c ole.Connection
if err := c.Initialize(); err != nil {
return false, fmt.Errorf("c.Initialize: %v", err)
@@ -214,10 +222,34 @@ func setPrivateNetwork(ifcGUID *windows.GUID) (bool, error) {
return false, nil
}
// interfaceFromGUID returns IPAdapterAddresses with specified GUID.
func interfaceFromGUID(guid *windows.GUID, flags winipcfg.GAAFlags) (*winipcfg.IPAdapterAddresses, error) {
luid, err := winipcfg.LUIDFromGUID(guid)
if err != nil {
return nil, err
}
addresses, err := winipcfg.GetAdaptersAddresses(windows.AF_UNSPEC, flags)
if err != nil {
return nil, err
}
for _, addr := range addresses {
if addr.LUID == luid {
return addr, nil
}
}
return nil, fmt.Errorf("interfaceFromGUID: interface with LUID %v (from GUID %v) not found", luid, guid)
}
func configureInterface(cfg *Config, tun *tun.NativeTun) error {
const mtu = 0
guid := tun.GUID()
iface, err := winipcfg.InterfaceFromGUID(&guid)
iface, err := interfaceFromGUID(&guid,
// Issue 474: on early boot, when the network is still
// coming up, if the Tailscale service comes up first,
// the Tailscale adapter it finds might not have the
// IPv4 service available yet? Try this flag:
winipcfg.GAAFlagIncludeAllInterfaces,
)
if err != nil {
return err
}
@@ -242,7 +274,6 @@ func configureInterface(cfg *Config, tun *tun.NativeTun) error {
log.Printf("setPrivateNetwork: adapter %v not found after %d tries, giving up", guid, tries)
}()
routes := []winipcfg.RouteData{}
var firstGateway4 *net.IP
var firstGateway6 *net.IP
addresses := make([]*net.IPNet, len(cfg.LocalAddrs))
@@ -257,6 +288,7 @@ func configureInterface(cfg *Config, tun *tun.NativeTun) error {
}
}
var routes []winipcfg.RouteData
foundDefault4 := false
foundDefault6 := false
for _, route := range cfg.Routes {
@@ -301,7 +333,7 @@ func configureInterface(cfg *Config, tun *tun.NativeTun) error {
routes = append(routes, r)
}
err = iface.SyncAddresses(addresses)
err = syncAddresses(iface, addresses)
if err != nil {
return err
}
@@ -320,14 +352,26 @@ func configureInterface(cfg *Config, tun *tun.NativeTun) error {
deduplicatedRoutes = append(deduplicatedRoutes, &routes[i])
}
// Re-read interface after syncAddresses.
iface, err = interfaceFromGUID(&guid,
// Issue 474: on early boot, when the network is still
// coming up, if the Tailscale service comes up first,
// the Tailscale adapter it finds might not have the
// IPv4 service available yet? Try this flag:
winipcfg.GAAFlagIncludeAllInterfaces,
)
if err != nil {
return err
}
var errAcc error
err = iface.SyncRoutes(deduplicatedRoutes)
err = syncRoutes(iface, deduplicatedRoutes)
if err != nil && errAcc == nil {
log.Printf("setroutes: %v", err)
errAcc = err
}
ipif, err := iface.GetIpInterface(winipcfg.AF_INET)
ipif, err := iface.LUID.IPInterface(windows.AF_INET)
if err != nil {
log.Printf("getipif: %v", err)
return err
@@ -337,17 +381,17 @@ func configureInterface(cfg *Config, tun *tun.NativeTun) error {
ipif.Metric = 0
}
if mtu > 0 {
ipif.NlMtu = uint32(mtu)
tun.ForceMTU(int(ipif.NlMtu))
ipif.NLMTU = uint32(mtu)
tun.ForceMTU(int(ipif.NLMTU))
}
err = ipif.Set()
if err != nil && errAcc == nil {
errAcc = err
}
ipif, err = iface.GetIpInterface(winipcfg.AF_INET6)
ipif, err = iface.LUID.IPInterface(windows.AF_INET6)
if err != nil {
if !isMissingIPv6Err(err) {
if !errors.Is(err, windows.ERROR_NOT_FOUND) {
return err
}
} else {
@@ -356,7 +400,7 @@ func configureInterface(cfg *Config, tun *tun.NativeTun) error {
ipif.Metric = 0
}
if mtu > 0 {
ipif.NlMtu = uint32(mtu)
ipif.NLMTU = uint32(mtu)
}
ipif.DadTransmits = 0
ipif.RouterDiscoveryBehavior = winipcfg.RouterDiscoveryDisabled
@@ -369,22 +413,6 @@ func configureInterface(cfg *Config, tun *tun.NativeTun) error {
return errAcc
}
// isMissingIPv6Err reports whether err is due to IPv6 not being enabled on the machine.
//
// It only currently supports the errors returned by winipcfg.Interface.GetIpInterface.
func isMissingIPv6Err(err error) bool {
if se, ok := err.(*os.SyscallError); ok {
switch se.Syscall {
case "iphlpapi.GetIpInterfaceEntry":
// ERROR_NOT_FOUND from means the address family (IPv6) is not found.
// (ERROR_FILE_NOT_FOUND means that the interface doesn't exist.)
// https://docs.microsoft.com/en-us/windows/win32/api/netioapi/nf-netioapi-getipinterfaceentry
return se.Err == windows.ERROR_NOT_FOUND
}
}
return false
}
// routeLess reports whether ri should sort before rj.
// The actual sort order doesn't appear to matter. The caller just
// wants them sorted to be able to de-dup.
@@ -406,3 +434,257 @@ func routeLess(ri, rj *winipcfg.RouteData) bool {
}
return false
}
// unwrapIP returns the shortest version of ip.
func unwrapIP(ip net.IP) net.IP {
if ip4 := ip.To4(); ip4 != nil {
return ip4
}
return ip
}
func v4Mask(m net.IPMask) net.IPMask {
if len(m) == 16 {
return m[12:]
}
return m
}
func netCompare(a, b net.IPNet) int {
aip, bip := unwrapIP(a.IP), unwrapIP(b.IP)
v := bytes.Compare(aip, bip)
if v != 0 {
return v
}
amask, bmask := a.Mask, b.Mask
if len(aip) == 4 {
amask = v4Mask(a.Mask)
bmask = v4Mask(b.Mask)
}
// narrower first
return -bytes.Compare(amask, bmask)
}
func sortNets(a []*net.IPNet) {
sort.Slice(a, func(i, j int) bool {
return netCompare(*a[i], *a[j]) == -1
})
}
// deltaNets returns the changes to turn a into b.
func deltaNets(a, b []*net.IPNet) (add, del []*net.IPNet) {
add = make([]*net.IPNet, 0, len(b))
del = make([]*net.IPNet, 0, len(a))
sortNets(a)
sortNets(b)
i := 0
j := 0
for i < len(a) && j < len(b) {
switch netCompare(*a[i], *b[j]) {
case -1:
// a < b, delete
del = append(del, a[i])
i++
case 0:
// a == b, no diff
i++
j++
case 1:
// a > b, add missing entry
add = append(add, b[j])
j++
default:
panic("unexpected compare result")
}
}
del = append(del, a[i:]...)
add = append(add, b[j:]...)
return
}
func excludeIPv6LinkLocal(in []*net.IPNet) (out []*net.IPNet) {
out = in[:0]
for _, n := range in {
if len(n.IP) == 16 && n.IP.IsLinkLocalUnicast() {
continue
}
out = append(out, n)
}
return out
}
// ipAdapterUnicastAddressToIPNet converts windows.IpAdapterUnicastAddress to net.IPNet.
func ipAdapterUnicastAddressToIPNet(u *windows.IpAdapterUnicastAddress) *net.IPNet {
ip := u.Address.IP()
w := 32
if ip.To4() == nil {
w = 128
}
return &net.IPNet{
IP: ip,
Mask: net.CIDRMask(int(u.OnLinkPrefixLength), w),
}
}
// unicastIPNets returns all unicast net.IPNet for ifc interface.
func unicastIPNets(ifc *winipcfg.IPAdapterAddresses) []*net.IPNet {
nets := make([]*net.IPNet, 0)
for addr := ifc.FirstUnicastAddress; addr != nil; addr = addr.Next {
nets = append(nets, ipAdapterUnicastAddressToIPNet(addr))
}
return nets
}
// syncAddresses incrementally sets the interface's unicast IP addresses,
// doing the minimum number of AddAddresses & DeleteAddress calls.
// This avoids the full FlushAddresses.
//
// Any IPv6 link-local addresses are not deleted.
func syncAddresses(ifc *winipcfg.IPAdapterAddresses, want []*net.IPNet) error {
var erracc error
got := unicastIPNets(ifc)
add, del := deltaNets(got, want)
del = excludeIPv6LinkLocal(del)
for _, a := range del {
err := ifc.LUID.DeleteIPAddress(*a)
if err != nil {
erracc = err
}
}
for _, a := range add {
err := ifc.LUID.AddIPAddress(*a)
if err != nil {
erracc = err
}
}
return erracc
}
func routeDataCompare(a, b *winipcfg.RouteData) int {
v := bytes.Compare(a.Destination.IP, b.Destination.IP)
if v != 0 {
return v
}
// Narrower masks first
v = bytes.Compare(a.Destination.Mask, b.Destination.Mask)
if v != 0 {
return -v
}
// No nexthop before non-empty nexthop
v = bytes.Compare(a.NextHop, b.NextHop)
if v != 0 {
return v
}
// Lower metrics first
if a.Metric < b.Metric {
return -1
} else if a.Metric > b.Metric {
return 1
}
return 0
}
func sortRouteData(a []*winipcfg.RouteData) {
sort.Slice(a, func(i, j int) bool {
return routeDataCompare(a[i], a[j]) < 0
})
}
func deltaRouteData(a, b []*winipcfg.RouteData) (add, del []*winipcfg.RouteData) {
add = make([]*winipcfg.RouteData, 0, len(b))
del = make([]*winipcfg.RouteData, 0, len(a))
sortRouteData(a)
sortRouteData(b)
i := 0
j := 0
for i < len(a) && j < len(b) {
switch routeDataCompare(a[i], b[j]) {
case -1:
// a < b, delete
del = append(del, a[i])
i++
case 0:
// a == b, no diff
i++
j++
case 1:
// a > b, add missing entry
add = append(add, b[j])
j++
default:
panic("unexpected compare result")
}
}
del = append(del, a[i:]...)
add = append(add, b[j:]...)
return
}
// getInterfaceRoutes returns all the interface's routes.
// Corresponds to GetIpForwardTable2 function, but filtered by interface.
func getInterfaceRoutes(ifc *winipcfg.IPAdapterAddresses, family winipcfg.AddressFamily) (matches []*winipcfg.MibIPforwardRow2, err error) {
routes, err := winipcfg.GetIPForwardTable2(family)
if err != nil {
return nil, err
}
for i := range routes {
if routes[i].InterfaceLUID == ifc.LUID {
matches = append(matches, &routes[i])
}
}
return
}
// syncRoutes incrementally sets multiples routes on an interface.
// This avoids a full ifc.FlushRoutes call.
func syncRoutes(ifc *winipcfg.IPAdapterAddresses, want []*winipcfg.RouteData) error {
routes, err := getInterfaceRoutes(ifc, windows.AF_INET)
if err != nil {
return err
}
got := make([]*winipcfg.RouteData, 0, len(routes))
for _, r := range routes {
got = append(got, &winipcfg.RouteData{
Destination: r.DestinationPrefix.IPNet(),
NextHop: r.NextHop.IP(),
Metric: r.Metric,
})
}
add, del := deltaRouteData(got, want)
var errs []error
for _, a := range del {
err := ifc.LUID.DeleteRoute(a.Destination, a.NextHop)
if err != nil {
dstStr := a.Destination.String()
if dstStr == "169.254.255.255/32" {
// Issue 785. Ignore these routes
// failing to delete. Harmless.
continue
}
errs = append(errs, fmt.Errorf("deleting route %v: %w", dstStr, err))
}
}
for _, a := range add {
err := ifc.LUID.AddRoute(a.Destination, a.NextHop, a.Metric)
if err != nil {
errs = append(errs, fmt.Errorf("adding route %v: %w", &a.Destination, err))
}
}
return multierror.New(errs)
}

View File

@@ -5,11 +5,13 @@
package router
import (
"fmt"
"math/rand"
"net"
"strings"
"testing"
winipcfg "github.com/tailscale/winipcfg-go"
"golang.zx2c4.com/wireguard/windows/tunnel/winipcfg"
"inet.af/netaddr"
)
@@ -95,3 +97,144 @@ func TestRouteLessConsistent(t *testing.T) {
}
}
}
func equalNetIPs(a, b []*net.IPNet) bool {
if len(a) != len(b) {
return false
}
for i := range a {
if netCompare(*a[i], *b[i]) != 0 {
return false
}
}
return true
}
func ipnet4(ip string, bits int) *net.IPNet {
return &net.IPNet{
IP: net.ParseIP(ip),
Mask: net.CIDRMask(bits, 32),
}
}
// each cidr can end in "[4]" to mean To4 form.
func nets(cidrs ...string) (ret []*net.IPNet) {
for _, s := range cidrs {
to4 := strings.HasSuffix(s, "[4]")
if to4 {
s = strings.TrimSuffix(s, "[4]")
}
ip, ipNet, err := net.ParseCIDR(s)
if err != nil {
panic(fmt.Sprintf("Bogus CIDR %q in test", s))
}
if to4 {
ip = ip.To4()
}
ipNet.IP = ip
ret = append(ret, ipNet)
}
return
}
func TestDeltaNets(t *testing.T) {
tests := []struct {
a, b []*net.IPNet
wantAdd, wantDel []*net.IPNet
}{
{
a: nets("1.2.3.4/24", "1.2.3.4/31", "1.2.3.3/32", "10.0.1.1/32", "100.0.1.1/32"),
b: nets("10.0.1.1/32", "100.0.2.1/32", "1.2.3.3/32", "1.2.3.4/24"),
wantAdd: nets("100.0.2.1/32"),
wantDel: nets("1.2.3.4/31", "100.0.1.1/32"),
},
{
a: nets("fe80::99d0:ec2d:b2e7:536b/64", "100.84.36.11/32"),
b: nets("100.84.36.11/32"),
wantDel: nets("fe80::99d0:ec2d:b2e7:536b/64"),
},
{
a: nets("100.84.36.11/32", "fe80::99d0:ec2d:b2e7:536b/64"),
b: nets("100.84.36.11/32"),
wantDel: nets("fe80::99d0:ec2d:b2e7:536b/64"),
},
{
a: nets("100.84.36.11/32", "fe80::99d0:ec2d:b2e7:536b/64"),
b: nets("100.84.36.11/32[4]"),
wantDel: nets("fe80::99d0:ec2d:b2e7:536b/64"),
},
{
a: excludeIPv6LinkLocal(nets("100.84.36.11/32", "fe80::99d0:ec2d:b2e7:536b/64")),
b: nets("100.84.36.11/32"),
},
{
a: []*net.IPNet{
{
IP: net.ParseIP("1.2.3.4"),
Mask: net.IPMask{0xff, 0xff, 0xff, 0xff},
},
},
b: []*net.IPNet{
{
IP: net.ParseIP("1.2.3.4"),
Mask: net.IPMask{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff},
},
},
},
}
for i, tt := range tests {
add, del := deltaNets(tt.a, tt.b)
if !equalNetIPs(add, tt.wantAdd) {
t.Errorf("[%d] add:\n got: %v\n want: %v\n", i, add, tt.wantAdd)
}
if !equalNetIPs(del, tt.wantDel) {
t.Errorf("[%d] del:\n got: %v\n want: %v\n", i, del, tt.wantDel)
}
}
}
func equalRouteDatas(a, b []*winipcfg.RouteData) bool {
if len(a) != len(b) {
return false
}
for i := range a {
if routeDataCompare(a[i], b[i]) != 0 {
return false
}
}
return true
}
func TestDeltaRouteData(t *testing.T) {
var h0 net.IP
h1 := net.ParseIP("99.99.99.99")
h2 := net.ParseIP("99.99.9.99")
a := []*winipcfg.RouteData{
{*ipnet4("1.2.3.4", 32), h0, 1},
{*ipnet4("1.2.3.4", 24), h1, 2},
{*ipnet4("1.2.3.4", 24), h2, 1},
{*ipnet4("1.2.3.5", 32), h0, 1},
}
b := []*winipcfg.RouteData{
{*ipnet4("1.2.3.5", 32), h0, 1},
{*ipnet4("1.2.3.4", 24), h1, 2},
{*ipnet4("1.2.3.4", 24), h2, 2},
}
add, del := deltaRouteData(a, b)
wantAdd := []*winipcfg.RouteData{
{*ipnet4("1.2.3.4", 24), h2, 2},
}
wantDel := []*winipcfg.RouteData{
{*ipnet4("1.2.3.4", 32), h0, 1},
{*ipnet4("1.2.3.4", 24), h2, 1},
}
if !equalRouteDatas(add, wantAdd) {
t.Errorf("add:\n got: %v\n want: %v\n", add, wantAdd)
}
if !equalRouteDatas(del, wantDel) {
t.Errorf("del:\n got: %v\n want: %v\n", del, wantDel)
}
}

View File

@@ -12,8 +12,8 @@ import (
"tailscale.com/types/logger"
)
func newUserspaceRouter(logf logger.Logf, tunname string, dev *device.Device, tuntap tun.Device, netChanged func()) Router {
return NewFakeRouter(logf, tunname, dev, tuntap, netChanged)
func newUserspaceRouter(logf logger.Logf, tunname string, dev *device.Device, tunDev tun.Device, netChanged func()) Router {
return NewFakeRouter(logf, tunname, dev, tunDev, netChanged)
}
func cleanup(logf logger.Logf, interfaceName string) {

View File

@@ -5,11 +5,16 @@
package router
import (
"bytes"
"fmt"
"io/ioutil"
"os"
"os/exec"
"strconv"
"strings"
"github.com/coreos/go-iptables/iptables"
"github.com/go-multierror/multierror"
"github.com/tailscale/wireguard-go/device"
"github.com/tailscale/wireguard-go/tun"
"inet.af/netaddr"
@@ -79,16 +84,21 @@ type netfilterRunner interface {
type linuxRouter struct {
logf func(fmt string, args ...interface{})
ipRuleAvailable bool
tunname string
addrs map[netaddr.IPPrefix]bool
routes map[netaddr.IPPrefix]bool
snatSubnetRoutes bool
netfilterMode NetfilterMode
// Various feature checks for the network stack.
ipRuleAvailable bool
v6Available bool
v6NATAvailable bool
dns *dns.Manager
ipt4 netfilterRunner
ipt6 netfilterRunner
cmd commandRunner
}
@@ -103,12 +113,24 @@ func newUserspaceRouter(logf logger.Logf, _ *device.Device, tunDev tun.Device) (
return nil, err
}
return newUserspaceRouterAdvanced(logf, tunname, ipt4, osCommandRunner{})
supportsV6 := supportsV6()
supportsV6NAT := supportsV6 && supportsV6NAT()
var ipt6 netfilterRunner
if supportsV6 {
// The iptables package probes for `ip6tables` and errors out
// if unavailable. We want that to be a non-fatal error.
ipt6, err = iptables.NewWithProtocol(iptables.ProtocolIPv6)
if err != nil {
return nil, err
}
}
return newUserspaceRouterAdvanced(logf, tunname, ipt4, ipt6, osCommandRunner{}, supportsV6, supportsV6NAT)
}
func newUserspaceRouterAdvanced(logf logger.Logf, tunname string, netfilter netfilterRunner, cmd commandRunner) (Router, error) {
_, err := exec.Command("ip", "rule").Output()
ipRuleAvailable := (err == nil)
func newUserspaceRouterAdvanced(logf logger.Logf, tunname string, netfilter4, netfilter6 netfilterRunner, cmd commandRunner, supportsV6, supportsV6NAT bool) (Router, error) {
ipRuleAvailable := (cmd.run("ip", "rule") == nil)
mconfig := dns.ManagerConfig{
Logf: logf,
@@ -116,13 +138,18 @@ func newUserspaceRouterAdvanced(logf logger.Logf, tunname string, netfilter netf
}
return &linuxRouter{
logf: logf,
logf: logf,
tunname: tunname,
netfilterMode: NetfilterOff,
ipRuleAvailable: ipRuleAvailable,
tunname: tunname,
netfilterMode: NetfilterOff,
ipt4: netfilter,
cmd: cmd,
dns: dns.NewManager(mconfig),
v6Available: supportsV6,
v6NATAvailable: supportsV6NAT,
ipt4: netfilter4,
ipt6: netfilter6,
cmd: cmd,
dns: dns.NewManager(mconfig),
}, nil
}
@@ -145,7 +172,7 @@ func (r *linuxRouter) Up() error {
func (r *linuxRouter) Close() error {
if err := r.dns.Down(); err != nil {
return fmt.Errorf("dns down: %v", err)
return fmt.Errorf("dns down: %w", err)
}
if err := r.downInterface(); err != nil {
return err
@@ -165,23 +192,28 @@ func (r *linuxRouter) Close() error {
// Set implements the Router interface.
func (r *linuxRouter) Set(cfg *Config) error {
var errs []error
if cfg == nil {
cfg = &shutdownConfig
}
if err := r.dns.Set(cfg.DNS); err != nil {
errs = append(errs, fmt.Errorf("dns set: %w", err))
}
if err := r.setNetfilterMode(cfg.NetfilterMode); err != nil {
return err
errs = append(errs, err)
}
newRoutes, err := cidrDiff("route", r.routes, cfg.Routes, r.addRoute, r.delRoute, r.logf)
if err != nil {
return err
errs = append(errs, err)
}
r.routes = newRoutes
newAddrs, err := cidrDiff("addr", r.addrs, cfg.LocalAddrs, r.addAddress, r.delAddress, r.logf)
if err != nil {
return err
errs = append(errs, err)
}
r.addrs = newAddrs
@@ -190,20 +222,16 @@ func (r *linuxRouter) Set(cfg *Config) error {
// state already correct, nothing to do.
case cfg.SNATSubnetRoutes:
if err := r.addSNATRule(); err != nil {
return err
errs = append(errs, err)
}
default:
if err := r.delSNATRule(); err != nil {
return err
errs = append(errs, err)
}
}
r.snatSubnetRoutes = cfg.SNATSubnetRoutes
if err := r.dns.Set(cfg.DNS); err != nil {
return fmt.Errorf("dns set: %v", err)
}
return nil
return multierror.New(errs)
}
// setNetfilterMode switches the router to the given netfilter
@@ -330,6 +358,7 @@ func (r *linuxRouter) setNetfilterMode(mode NetfilterMode) error {
// address is already assigned to the interface, or if the addition
// fails.
func (r *linuxRouter) addAddress(addr netaddr.IPPrefix) error {
if err := r.cmd.run("ip", "addr", "add", addr.String(), "dev", r.tunname); err != nil {
return fmt.Errorf("adding address %q to tunnel interface: %w", addr, err)
}
@@ -358,7 +387,17 @@ func (r *linuxRouter) addLoopbackRule(addr netaddr.IP) error {
if r.netfilterMode == NetfilterOff {
return nil
}
if err := r.ipt4.Insert("filter", "ts-input", 1, "-i", "lo", "-s", addr.String(), "-j", "ACCEPT"); err != nil {
nf := r.ipt4
if addr.Is6() {
if !r.v6Available {
// IPv6 not available, ignore.
return nil
}
nf = r.ipt6
}
if err := nf.Insert("filter", "ts-input", 1, "-i", "lo", "-s", addr.String(), "-j", "ACCEPT"); err != nil {
return fmt.Errorf("adding loopback allow rule for %q: %w", addr, err)
}
return nil
@@ -370,7 +409,17 @@ func (r *linuxRouter) delLoopbackRule(addr netaddr.IP) error {
if r.netfilterMode == NetfilterOff {
return nil
}
if err := r.ipt4.Delete("filter", "ts-input", "-i", "lo", "-s", addr.String(), "-j", "ACCEPT"); err != nil {
nf := r.ipt4
if addr.Is6() {
if !r.v6Available {
// IPv6 not available, ignore.
return nil
}
nf = r.ipt6
}
if err := nf.Delete("filter", "ts-input", "-i", "lo", "-s", addr.String(), "-j", "ACCEPT"); err != nil {
return fmt.Errorf("deleting loopback allow rule for %q: %w", addr, err)
}
return nil
@@ -416,6 +465,13 @@ func (r *linuxRouter) downInterface() error {
return r.cmd.run("ip", "link", "set", "dev", r.tunname, "down")
}
func (r *linuxRouter) iprouteFamilies() []string {
if r.v6Available {
return []string{"-4", "-6"}
}
return []string{"-4"}
}
// addIPRules adds the policy routing rule that avoids tailscaled
// routing loops. If the rule exists and appears to be a
// tailscale-managed rule, it is gracefully replaced.
@@ -432,58 +488,60 @@ func (r *linuxRouter) addIPRules() error {
rg := newRunGroup(nil, r.cmd)
// NOTE(apenwarr): We leave spaces between each pref number.
// This is so the sysadmin can override by inserting rules in
// between if they want.
for _, family := range r.iprouteFamilies() {
// NOTE(apenwarr): We leave spaces between each pref number.
// This is so the sysadmin can override by inserting rules in
// between if they want.
// NOTE(apenwarr): This sequence seems complicated, right?
// If we could simply have a rule that said "match packets that
// *don't* have this fwmark", then we would only need to add one
// link to table 52 and we'd be done. Unfortunately, older kernels
// and 'ip rule' implementations (including busybox), don't support
// checking for the lack of a fwmark, only the presence. The technique
// below works even on very old kernels.
// NOTE(apenwarr): This sequence seems complicated, right?
// If we could simply have a rule that said "match packets that
// *don't* have this fwmark", then we would only need to add one
// link to table 52 and we'd be done. Unfortunately, older kernels
// and 'ip rule' implementations (including busybox), don't support
// checking for the lack of a fwmark, only the presence. The technique
// below works even on very old kernels.
// Packets from us, tagged with our fwmark, first try the kernel's
// main routing table.
rg.Run(
"ip", "rule", "add",
"pref", tailscaleRouteTable+"10",
"fwmark", tailscaleBypassMark,
"table", "main",
)
// ...and then we try the 'default' table, for correctness,
// even though it's been empty on every Linux system I've ever seen.
rg.Run(
"ip", "rule", "add",
"pref", tailscaleRouteTable+"30",
"fwmark", tailscaleBypassMark,
"table", "default",
)
// If neither of those matched (no default route on this system?)
// then packets from us should be aborted rather than falling through
// to the tailscale routes, because that would create routing loops.
rg.Run(
"ip", "rule", "add",
"pref", tailscaleRouteTable+"50",
"fwmark", tailscaleBypassMark,
"type", "unreachable",
)
// If we get to this point, capture all packets and send them
// through to the tailscale route table. For apps other than us
// (ie. with no fwmark set), this is the first routing table, so
// it takes precedence over all the others, ie. VPN routes always
// beat non-VPN routes.
//
// NOTE(apenwarr): tables >255 are not supported in busybox, so we
// can't use a table number that aligns with the rule preferences.
rg.Run(
"ip", "rule", "add",
"pref", tailscaleRouteTable+"70",
"table", tailscaleRouteTable,
)
// If that didn't match, then non-fwmark packets fall through to the
// usual rules (pref 32766 and 32767, ie. main and default).
// Packets from us, tagged with our fwmark, first try the kernel's
// main routing table.
rg.Run(
"ip", family, "rule", "add",
"pref", tailscaleRouteTable+"10",
"fwmark", tailscaleBypassMark,
"table", "main",
)
// ...and then we try the 'default' table, for correctness,
// even though it's been empty on every Linux system I've ever seen.
rg.Run(
"ip", family, "rule", "add",
"pref", tailscaleRouteTable+"30",
"fwmark", tailscaleBypassMark,
"table", "default",
)
// If neither of those matched (no default route on this system?)
// then packets from us should be aborted rather than falling through
// to the tailscale routes, because that would create routing loops.
rg.Run(
"ip", family, "rule", "add",
"pref", tailscaleRouteTable+"50",
"fwmark", tailscaleBypassMark,
"type", "unreachable",
)
// If we get to this point, capture all packets and send them
// through to the tailscale route table. For apps other than us
// (ie. with no fwmark set), this is the first routing table, so
// it takes precedence over all the others, ie. VPN routes always
// beat non-VPN routes.
//
// NOTE(apenwarr): tables >255 are not supported in busybox, so we
// can't use a table number that aligns with the rule preferences.
rg.Run(
"ip", family, "rule", "add",
"pref", tailscaleRouteTable+"70",
"table", tailscaleRouteTable,
)
// If that didn't match, then non-fwmark packets fall through to the
// usual rules (pref 32766 and 32767, ie. main and default).
}
return rg.ErrAcc
}
@@ -503,73 +561,105 @@ func (r *linuxRouter) delIPRules() error {
// unknown rules during deletion.
rg := newRunGroup([]int{2, 254}, r.cmd)
// When deleting rules, we want to be a bit specific (mention which
// table we were routing to) but not *too* specific (fwmarks, etc).
// That leaves us some flexibility to change these values in later
// versions without having ongoing hacks for every possible
// combination.
for _, family := range r.iprouteFamilies() {
// When deleting rules, we want to be a bit specific (mention which
// table we were routing to) but not *too* specific (fwmarks, etc).
// That leaves us some flexibility to change these values in later
// versions without having ongoing hacks for every possible
// combination.
// Delete old-style tailscale rules
// (never released in a stable version, so we can drop this
// support eventually).
rg.Run(
"ip", "rule", "del",
"pref", "10000",
"table", "main",
)
// Delete old-style tailscale rules
// (never released in a stable version, so we can drop this
// support eventually).
rg.Run(
"ip", family, "rule", "del",
"pref", "10000",
"table", "main",
)
// Delete new-style tailscale rules.
rg.Run(
"ip", family, "rule", "del",
"pref", tailscaleRouteTable+"10",
"table", "main",
)
rg.Run(
"ip", family, "rule", "del",
"pref", tailscaleRouteTable+"30",
"table", "default",
)
rg.Run(
"ip", family, "rule", "del",
"pref", tailscaleRouteTable+"50",
"type", "unreachable",
)
rg.Run(
"ip", family, "rule", "del",
"pref", tailscaleRouteTable+"70",
"table", tailscaleRouteTable,
)
}
// Delete new-style tailscale rules.
rg.Run(
"ip", "rule", "del",
"pref", tailscaleRouteTable+"10",
"table", "main",
)
rg.Run(
"ip", "rule", "del",
"pref", tailscaleRouteTable+"30",
"table", "default",
)
rg.Run(
"ip", "rule", "del",
"pref", tailscaleRouteTable+"50",
"type", "unreachable",
)
rg.Run(
"ip", "rule", "del",
"pref", tailscaleRouteTable+"70",
"table", tailscaleRouteTable,
)
return rg.ErrAcc
}
func (r *linuxRouter) netfilterFamilies() []netfilterRunner {
if r.v6Available {
return []netfilterRunner{r.ipt4, r.ipt6}
}
return []netfilterRunner{r.ipt4}
}
// addNetfilterChains creates custom Tailscale chains in netfilter.
func (r *linuxRouter) addNetfilterChains() error {
create := func(table, chain string) error {
err := r.ipt4.ClearChain(table, chain)
create := func(ipt netfilterRunner, table, chain string) error {
err := ipt.ClearChain(table, chain)
if errCode(err) == 1 {
// nonexistent chain. let's create it!
return r.ipt4.NewChain(table, chain)
return ipt.NewChain(table, chain)
}
if err != nil {
return fmt.Errorf("setting up %s/%s: %w", table, chain, err)
}
return nil
}
if err := create("filter", "ts-input"); err != nil {
for _, ipt := range r.netfilterFamilies() {
if err := create(ipt, "filter", "ts-input"); err != nil {
return err
}
if err := create(ipt, "filter", "ts-forward"); err != nil {
return err
}
}
if err := create(r.ipt4, "nat", "ts-postrouting"); err != nil {
return err
}
if err := create("filter", "ts-forward"); err != nil {
return err
}
if err := create("nat", "ts-postrouting"); err != nil {
return err
if r.v6NATAvailable {
if err := create(r.ipt6, "nat", "ts-postrouting"); err != nil {
return err
}
}
return nil
}
// addNetfilterBase adds with some basic processing rules to be supplemented
// by later calls to other helpers.
// addNetfilterBase adds some basic processing rules to be
// supplemented by later calls to other helpers.
func (r *linuxRouter) addNetfilterBase() error {
if err := r.addNetfilterBase4(); err != nil {
return err
}
if r.v6Available {
if err := r.addNetfilterBase6(); err != nil {
return err
}
}
return nil
}
// addNetfilterBase4 adds some basic IPv4 processing rules to be
// supplemented by later calls to other helpers.
func (r *linuxRouter) addNetfilterBase4() error {
// Only allow CGNAT range traffic to come from tailscale0. There
// is an exception carved out for ranges used by ChromeOS, for
// which we fall out of the Tailscale chain.
@@ -578,11 +668,11 @@ func (r *linuxRouter) addNetfilterBase() error {
// CGNAT range for other purposes :(.
args := []string{"!", "-i", r.tunname, "-s", tsaddr.ChromeOSVMRange().String(), "-j", "RETURN"}
if err := r.ipt4.Append("filter", "ts-input", args...); err != nil {
return fmt.Errorf("adding %v in filter/ts-input: %w", args, err)
return fmt.Errorf("adding %v in v4/filter/ts-input: %w", args, err)
}
args = []string{"!", "-i", r.tunname, "-s", tsaddr.CGNATRange().String(), "-j", "DROP"}
if err := r.ipt4.Append("filter", "ts-input", args...); err != nil {
return fmt.Errorf("adding %v in filter/ts-input: %w", args, err)
return fmt.Errorf("adding %v in v4/filter/ts-input: %w", args, err)
}
// Forward all traffic from the Tailscale interface, and drop
@@ -598,19 +688,43 @@ func (r *linuxRouter) addNetfilterBase() error {
// use to effectively run that same test again.
args = []string{"-i", r.tunname, "-j", "MARK", "--set-mark", tailscaleSubnetRouteMark}
if err := r.ipt4.Append("filter", "ts-forward", args...); err != nil {
return fmt.Errorf("adding %v in filter/ts-forward: %w", args, err)
return fmt.Errorf("adding %v in v4/filter/ts-forward: %w", args, err)
}
args = []string{"-m", "mark", "--mark", tailscaleSubnetRouteMark, "-j", "ACCEPT"}
if err := r.ipt4.Append("filter", "ts-forward", args...); err != nil {
return fmt.Errorf("adding %v in filter/ts-forward: %w", args, err)
return fmt.Errorf("adding %v in v4/filter/ts-forward: %w", args, err)
}
args = []string{"-o", r.tunname, "-s", tsaddr.CGNATRange().String(), "-j", "DROP"}
if err := r.ipt4.Append("filter", "ts-forward", args...); err != nil {
return fmt.Errorf("adding %v in filter/ts-forward: %w", args, err)
return fmt.Errorf("adding %v in v4/filter/ts-forward: %w", args, err)
}
args = []string{"-o", r.tunname, "-j", "ACCEPT"}
if err := r.ipt4.Append("filter", "ts-forward", args...); err != nil {
return fmt.Errorf("adding %v in filter/ts-forward: %w", args, err)
return fmt.Errorf("adding %v in v4/filter/ts-forward: %w", args, err)
}
return nil
}
// addNetfilterBase4 adds some basic IPv6 processing rules to be
// supplemented by later calls to other helpers.
func (r *linuxRouter) addNetfilterBase6() error {
// TODO: only allow traffic from Tailscale's ULA range to come
// from tailscale0.
args := []string{"-i", r.tunname, "-j", "MARK", "--set-mark", tailscaleSubnetRouteMark}
if err := r.ipt6.Append("filter", "ts-forward", args...); err != nil {
return fmt.Errorf("adding %v in v6/filter/ts-forward: %w", args, err)
}
args = []string{"-m", "mark", "--mark", tailscaleSubnetRouteMark, "-j", "ACCEPT"}
if err := r.ipt6.Append("filter", "ts-forward", args...); err != nil {
return fmt.Errorf("adding %v in v6/filter/ts-forward: %w", args, err)
}
// TODO: drop forwarded traffic to tailscale0 from tailscale's ULA
// (see corresponding IPv4 CGNAT rule).
args = []string{"-o", r.tunname, "-j", "ACCEPT"}
if err := r.ipt6.Append("filter", "ts-forward", args...); err != nil {
return fmt.Errorf("adding %v in v6/filter/ts-forward: %w", args, err)
}
return nil
@@ -618,8 +732,8 @@ func (r *linuxRouter) addNetfilterBase() error {
// delNetfilterChains removes the custom Tailscale chains from netfilter.
func (r *linuxRouter) delNetfilterChains() error {
del := func(table, chain string) error {
if err := r.ipt4.ClearChain(table, chain); err != nil {
del := func(ipt netfilterRunner, table, chain string) error {
if err := ipt.ClearChain(table, chain); err != nil {
if errCode(err) == 1 {
// nonexistent chain. That's fine, since it's
// the desired state anyway.
@@ -627,7 +741,7 @@ func (r *linuxRouter) delNetfilterChains() error {
}
return fmt.Errorf("flushing %s/%s: %w", table, chain, err)
}
if err := r.ipt4.DeleteChain(table, chain); err != nil {
if err := ipt.DeleteChain(table, chain); err != nil {
// this shouldn't fail, because if the chain didn't
// exist, we would have returned after ClearChain.
return fmt.Errorf("deleting %s/%s: %v", table, chain, err)
@@ -635,14 +749,21 @@ func (r *linuxRouter) delNetfilterChains() error {
return nil
}
if err := del("filter", "ts-input"); err != nil {
for _, ipt := range r.netfilterFamilies() {
if err := del(ipt, "filter", "ts-input"); err != nil {
return err
}
if err := del(ipt, "filter", "ts-forward"); err != nil {
return err
}
}
if err := del(r.ipt4, "nat", "ts-postrouting"); err != nil {
return err
}
if err := del("filter", "ts-forward"); err != nil {
return err
}
if err := del("nat", "ts-postrouting"); err != nil {
return err
if r.v6NATAvailable {
if err := del(r.ipt6, "nat", "ts-postrouting"); err != nil {
return err
}
}
return nil
@@ -651,8 +772,8 @@ func (r *linuxRouter) delNetfilterChains() error {
// delNetfilterBase empties but does not remove custom Tailscale chains from
// netfilter.
func (r *linuxRouter) delNetfilterBase() error {
del := func(table, chain string) error {
if err := r.ipt4.ClearChain(table, chain); err != nil {
del := func(ipt netfilterRunner, table, chain string) error {
if err := ipt.ClearChain(table, chain); err != nil {
if errCode(err) == 1 {
// nonexistent chain. That's fine, since it's
// the desired state anyway.
@@ -663,14 +784,21 @@ func (r *linuxRouter) delNetfilterBase() error {
return nil
}
if err := del("filter", "ts-input"); err != nil {
for _, ipt := range r.netfilterFamilies() {
if err := del(ipt, "filter", "ts-input"); err != nil {
return err
}
if err := del(ipt, "filter", "ts-forward"); err != nil {
return err
}
}
if err := del(r.ipt4, "nat", "ts-postrouting"); err != nil {
return err
}
if err := del("filter", "ts-forward"); err != nil {
return err
}
if err := del("nat", "ts-postrouting"); err != nil {
return err
if r.v6NATAvailable {
if err := del(r.ipt6, "nat", "ts-postrouting"); err != nil {
return err
}
}
return nil
@@ -680,31 +808,38 @@ func (r *linuxRouter) delNetfilterBase() error {
// the relevant main netfilter chains. The tailscale chains must
// already exist.
func (r *linuxRouter) addNetfilterHooks() error {
divert := func(table, chain string) error {
divert := func(ipt netfilterRunner, table, chain string) error {
tsChain := tsChain(chain)
args := []string{"-j", tsChain}
exists, err := r.ipt4.Exists(table, chain, args...)
exists, err := ipt.Exists(table, chain, args...)
if err != nil {
return fmt.Errorf("checking for %v in %s/%s: %w", args, table, chain, err)
}
if exists {
return nil
}
if err := r.ipt4.Insert(table, chain, 1, args...); err != nil {
if err := ipt.Insert(table, chain, 1, args...); err != nil {
return fmt.Errorf("adding %v in %s/%s: %w", args, table, chain, err)
}
return nil
}
if err := divert("filter", "INPUT"); err != nil {
for _, ipt := range r.netfilterFamilies() {
if err := divert(ipt, "filter", "INPUT"); err != nil {
return err
}
if err := divert(ipt, "filter", "FORWARD"); err != nil {
return err
}
}
if err := divert(r.ipt4, "nat", "POSTROUTING"); err != nil {
return err
}
if err := divert("filter", "FORWARD"); err != nil {
return err
}
if err := divert("nat", "POSTROUTING"); err != nil {
return err
if r.v6NATAvailable {
if err := divert(r.ipt6, "nat", "POSTROUTING"); err != nil {
return err
}
}
return nil
}
@@ -712,10 +847,10 @@ func (r *linuxRouter) addNetfilterHooks() error {
// delNetfilterHooks deletes the calls to tailscale's netfilter chains
// in the relevant main netfilter chains.
func (r *linuxRouter) delNetfilterHooks() error {
del := func(table, chain string) error {
del := func(ipt netfilterRunner, table, chain string) error {
tsChain := tsChain(chain)
args := []string{"-j", tsChain}
if err := r.ipt4.Delete(table, chain, args...); err != nil {
if err := ipt.Delete(table, chain, args...); err != nil {
// TODO(apenwarr): check for errCode(1) here.
// Unfortunately the error code from the iptables
// module resists unwrapping, unlike with other
@@ -727,14 +862,21 @@ func (r *linuxRouter) delNetfilterHooks() error {
return nil
}
if err := del("filter", "INPUT"); err != nil {
for _, ipt := range r.netfilterFamilies() {
if err := del(ipt, "filter", "INPUT"); err != nil {
return err
}
if err := del(ipt, "filter", "FORWARD"); err != nil {
return err
}
}
if err := del(r.ipt4, "nat", "POSTROUTING"); err != nil {
return err
}
if err := del("filter", "FORWARD"); err != nil {
return err
}
if err := del("nat", "POSTROUTING"); err != nil {
return err
if r.v6NATAvailable {
if err := del(r.ipt6, "nat", "POSTROUTING"); err != nil {
return err
}
}
return nil
}
@@ -748,7 +890,12 @@ func (r *linuxRouter) addSNATRule() error {
args := []string{"-m", "mark", "--mark", tailscaleSubnetRouteMark, "-j", "MASQUERADE"}
if err := r.ipt4.Append("nat", "ts-postrouting", args...); err != nil {
return fmt.Errorf("adding %v in nat/ts-postrouting: %w", args, err)
return fmt.Errorf("adding %v in v4/nat/ts-postrouting: %w", args, err)
}
if r.v6NATAvailable {
if err := r.ipt6.Append("nat", "ts-postrouting", args...); err != nil {
return fmt.Errorf("adding %v in v6/nat/ts-postrouting: %w", args, err)
}
}
return nil
}
@@ -762,7 +909,12 @@ func (r *linuxRouter) delSNATRule() error {
args := []string{"-m", "mark", "--mark", tailscaleSubnetRouteMark, "-j", "MASQUERADE"}
if err := r.ipt4.Delete("nat", "ts-postrouting", args...); err != nil {
return fmt.Errorf("deleting %v in nat/ts-postrouting: %w", args, err)
return fmt.Errorf("deleting %v in v4/nat/ts-postrouting: %w", args, err)
}
if r.v6NATAvailable {
if err := r.ipt6.Delete("nat", "ts-postrouting", args...); err != nil {
return fmt.Errorf("deleting %v in v6/nat/ts-postrouting: %w", args, err)
}
}
return nil
}
@@ -850,3 +1002,61 @@ func normalizeCIDR(cidr netaddr.IPPrefix) string {
func cleanup(logf logger.Logf, interfaceName string) {
// TODO(dmytro): clean up iptables.
}
// supportsV6 returns whether the system appears to have a working
// IPv6 network stack.
func supportsV6() bool {
_, err := os.Stat("/proc/sys/net/ipv6")
if os.IsNotExist(err) {
return false
}
bs, err := ioutil.ReadFile("/proc/sys/net/ipv6/conf/all/disable_ipv6")
if err != nil {
// Be conservative if we can't find the ipv6 configuration knob.
return false
}
disabled, err := strconv.ParseBool(strings.TrimSpace(string(bs)))
if err != nil {
return false
}
if disabled {
return false
}
// Older kernels don't support IPv6 policy routing.
bs, err = ioutil.ReadFile("/proc/sys/net/ipv6/conf/all/disable_policy")
if err != nil {
// Absent knob means policy routing is unsupported.
return false
}
disabled, err = strconv.ParseBool(strings.TrimSpace(string(bs)))
if err != nil {
return false
}
if disabled {
return false
}
// Some distros ship ip6tables separately from iptables.
if _, err := exec.LookPath("ip6tables"); err != nil {
return false
}
return true
}
// supportsV6NAT returns whether the system has a "nat" table in the
// IPv6 netfilter stack.
//
// The nat table was added after the initial release of ipv6
// netfilter, so some older distros ship a kernel that can't NAT IPv6
// traffic.
func supportsV6NAT() bool {
bs, err := ioutil.ReadFile("/proc/net/ip6_tables_names")
if err != nil {
// Can't read the file. Assume SNAT works.
return true
}
return bytes.Contains(bs, []byte("nat\n"))
}

View File

@@ -34,10 +34,14 @@ func mustCIDRs(ss ...string) []netaddr.IPPrefix {
func TestRouterStates(t *testing.T) {
basic := `
ip rule add pref 5210 fwmark 0x80000 table main
ip rule add pref 5230 fwmark 0x80000 table default
ip rule add pref 5250 fwmark 0x80000 type unreachable
ip rule add pref 5270 table 52
ip rule add -4 pref 5210 fwmark 0x80000 table main
ip rule add -4 pref 5230 fwmark 0x80000 table default
ip rule add -4 pref 5250 fwmark 0x80000 type unreachable
ip rule add -4 pref 5270 table 52
ip rule add -6 pref 5210 fwmark 0x80000 table main
ip rule add -6 pref 5230 fwmark 0x80000 table default
ip rule add -6 pref 5250 fwmark 0x80000 type unreachable
ip rule add -6 pref 5270 table 52
`
states := []struct {
name string
@@ -104,17 +108,24 @@ up
ip addr add 100.101.102.104/10 dev tailscale0
ip route add 10.0.0.0/8 dev tailscale0 table 52
ip route add 100.100.100.100/32 dev tailscale0 table 52` + basic +
`filter/FORWARD -j ts-forward
filter/INPUT -j ts-input
filter/ts-forward -i tailscale0 -j MARK --set-mark 0x40000
filter/ts-forward -m mark --mark 0x40000 -j ACCEPT
filter/ts-forward -o tailscale0 -s 100.64.0.0/10 -j DROP
filter/ts-forward -o tailscale0 -j ACCEPT
filter/ts-input -i lo -s 100.101.102.104 -j ACCEPT
filter/ts-input ! -i tailscale0 -s 100.115.92.0/23 -j RETURN
filter/ts-input ! -i tailscale0 -s 100.64.0.0/10 -j DROP
nat/POSTROUTING -j ts-postrouting
nat/ts-postrouting -m mark --mark 0x40000 -j MASQUERADE
`v4/filter/FORWARD -j ts-forward
v4/filter/INPUT -j ts-input
v4/filter/ts-forward -i tailscale0 -j MARK --set-mark 0x40000
v4/filter/ts-forward -m mark --mark 0x40000 -j ACCEPT
v4/filter/ts-forward -o tailscale0 -s 100.64.0.0/10 -j DROP
v4/filter/ts-forward -o tailscale0 -j ACCEPT
v4/filter/ts-input -i lo -s 100.101.102.104 -j ACCEPT
v4/filter/ts-input ! -i tailscale0 -s 100.115.92.0/23 -j RETURN
v4/filter/ts-input ! -i tailscale0 -s 100.64.0.0/10 -j DROP
v4/nat/POSTROUTING -j ts-postrouting
v4/nat/ts-postrouting -m mark --mark 0x40000 -j MASQUERADE
v6/filter/FORWARD -j ts-forward
v6/filter/INPUT -j ts-input
v6/filter/ts-forward -i tailscale0 -j MARK --set-mark 0x40000
v6/filter/ts-forward -m mark --mark 0x40000 -j ACCEPT
v6/filter/ts-forward -o tailscale0 -j ACCEPT
v6/nat/POSTROUTING -j ts-postrouting
v6/nat/ts-postrouting -m mark --mark 0x40000 -j MASQUERADE
`,
},
{
@@ -129,16 +140,22 @@ up
ip addr add 100.101.102.104/10 dev tailscale0
ip route add 10.0.0.0/8 dev tailscale0 table 52
ip route add 100.100.100.100/32 dev tailscale0 table 52` + basic +
`filter/FORWARD -j ts-forward
filter/INPUT -j ts-input
filter/ts-forward -i tailscale0 -j MARK --set-mark 0x40000
filter/ts-forward -m mark --mark 0x40000 -j ACCEPT
filter/ts-forward -o tailscale0 -s 100.64.0.0/10 -j DROP
filter/ts-forward -o tailscale0 -j ACCEPT
filter/ts-input -i lo -s 100.101.102.104 -j ACCEPT
filter/ts-input ! -i tailscale0 -s 100.115.92.0/23 -j RETURN
filter/ts-input ! -i tailscale0 -s 100.64.0.0/10 -j DROP
nat/POSTROUTING -j ts-postrouting
`v4/filter/FORWARD -j ts-forward
v4/filter/INPUT -j ts-input
v4/filter/ts-forward -i tailscale0 -j MARK --set-mark 0x40000
v4/filter/ts-forward -m mark --mark 0x40000 -j ACCEPT
v4/filter/ts-forward -o tailscale0 -s 100.64.0.0/10 -j DROP
v4/filter/ts-forward -o tailscale0 -j ACCEPT
v4/filter/ts-input -i lo -s 100.101.102.104 -j ACCEPT
v4/filter/ts-input ! -i tailscale0 -s 100.115.92.0/23 -j RETURN
v4/filter/ts-input ! -i tailscale0 -s 100.64.0.0/10 -j DROP
v4/nat/POSTROUTING -j ts-postrouting
v6/filter/FORWARD -j ts-forward
v6/filter/INPUT -j ts-input
v6/filter/ts-forward -i tailscale0 -j MARK --set-mark 0x40000
v6/filter/ts-forward -m mark --mark 0x40000 -j ACCEPT
v6/filter/ts-forward -o tailscale0 -j ACCEPT
v6/nat/POSTROUTING -j ts-postrouting
`,
},
@@ -156,16 +173,22 @@ up
ip addr add 100.101.102.104/10 dev tailscale0
ip route add 10.0.0.0/8 dev tailscale0 table 52
ip route add 100.100.100.100/32 dev tailscale0 table 52` + basic +
`filter/FORWARD -j ts-forward
filter/INPUT -j ts-input
filter/ts-forward -i tailscale0 -j MARK --set-mark 0x40000
filter/ts-forward -m mark --mark 0x40000 -j ACCEPT
filter/ts-forward -o tailscale0 -s 100.64.0.0/10 -j DROP
filter/ts-forward -o tailscale0 -j ACCEPT
filter/ts-input -i lo -s 100.101.102.104 -j ACCEPT
filter/ts-input ! -i tailscale0 -s 100.115.92.0/23 -j RETURN
filter/ts-input ! -i tailscale0 -s 100.64.0.0/10 -j DROP
nat/POSTROUTING -j ts-postrouting
`v4/filter/FORWARD -j ts-forward
v4/filter/INPUT -j ts-input
v4/filter/ts-forward -i tailscale0 -j MARK --set-mark 0x40000
v4/filter/ts-forward -m mark --mark 0x40000 -j ACCEPT
v4/filter/ts-forward -o tailscale0 -s 100.64.0.0/10 -j DROP
v4/filter/ts-forward -o tailscale0 -j ACCEPT
v4/filter/ts-input -i lo -s 100.101.102.104 -j ACCEPT
v4/filter/ts-input ! -i tailscale0 -s 100.115.92.0/23 -j RETURN
v4/filter/ts-input ! -i tailscale0 -s 100.64.0.0/10 -j DROP
v4/nat/POSTROUTING -j ts-postrouting
v6/filter/FORWARD -j ts-forward
v6/filter/INPUT -j ts-input
v6/filter/ts-forward -i tailscale0 -j MARK --set-mark 0x40000
v6/filter/ts-forward -m mark --mark 0x40000 -j ACCEPT
v6/filter/ts-forward -o tailscale0 -j ACCEPT
v6/nat/POSTROUTING -j ts-postrouting
`,
},
{
@@ -180,16 +203,22 @@ up
ip addr add 100.101.102.104/10 dev tailscale0
ip route add 10.0.0.0/8 dev tailscale0 table 52
ip route add 100.100.100.100/32 dev tailscale0 table 52` + basic +
`filter/FORWARD -j ts-forward
filter/INPUT -j ts-input
filter/ts-forward -i tailscale0 -j MARK --set-mark 0x40000
filter/ts-forward -m mark --mark 0x40000 -j ACCEPT
filter/ts-forward -o tailscale0 -s 100.64.0.0/10 -j DROP
filter/ts-forward -o tailscale0 -j ACCEPT
filter/ts-input -i lo -s 100.101.102.104 -j ACCEPT
filter/ts-input ! -i tailscale0 -s 100.115.92.0/23 -j RETURN
filter/ts-input ! -i tailscale0 -s 100.64.0.0/10 -j DROP
nat/POSTROUTING -j ts-postrouting
`v4/filter/FORWARD -j ts-forward
v4/filter/INPUT -j ts-input
v4/filter/ts-forward -i tailscale0 -j MARK --set-mark 0x40000
v4/filter/ts-forward -m mark --mark 0x40000 -j ACCEPT
v4/filter/ts-forward -o tailscale0 -s 100.64.0.0/10 -j DROP
v4/filter/ts-forward -o tailscale0 -j ACCEPT
v4/filter/ts-input -i lo -s 100.101.102.104 -j ACCEPT
v4/filter/ts-input ! -i tailscale0 -s 100.115.92.0/23 -j RETURN
v4/filter/ts-input ! -i tailscale0 -s 100.64.0.0/10 -j DROP
v4/nat/POSTROUTING -j ts-postrouting
v6/filter/FORWARD -j ts-forward
v6/filter/INPUT -j ts-input
v6/filter/ts-forward -i tailscale0 -j MARK --set-mark 0x40000
v6/filter/ts-forward -m mark --mark 0x40000 -j ACCEPT
v6/filter/ts-forward -o tailscale0 -j ACCEPT
v6/nat/POSTROUTING -j ts-postrouting
`,
},
@@ -205,13 +234,16 @@ up
ip addr add 100.101.102.104/10 dev tailscale0
ip route add 10.0.0.0/8 dev tailscale0 table 52
ip route add 100.100.100.100/32 dev tailscale0 table 52` + basic +
`filter/ts-forward -i tailscale0 -j MARK --set-mark 0x40000
filter/ts-forward -m mark --mark 0x40000 -j ACCEPT
filter/ts-forward -o tailscale0 -s 100.64.0.0/10 -j DROP
filter/ts-forward -o tailscale0 -j ACCEPT
filter/ts-input -i lo -s 100.101.102.104 -j ACCEPT
filter/ts-input ! -i tailscale0 -s 100.115.92.0/23 -j RETURN
filter/ts-input ! -i tailscale0 -s 100.64.0.0/10 -j DROP
`v4/filter/ts-forward -i tailscale0 -j MARK --set-mark 0x40000
v4/filter/ts-forward -m mark --mark 0x40000 -j ACCEPT
v4/filter/ts-forward -o tailscale0 -s 100.64.0.0/10 -j DROP
v4/filter/ts-forward -o tailscale0 -j ACCEPT
v4/filter/ts-input -i lo -s 100.101.102.104 -j ACCEPT
v4/filter/ts-input ! -i tailscale0 -s 100.115.92.0/23 -j RETURN
v4/filter/ts-input ! -i tailscale0 -s 100.64.0.0/10 -j DROP
v6/filter/ts-forward -i tailscale0 -j MARK --set-mark 0x40000
v6/filter/ts-forward -m mark --mark 0x40000 -j ACCEPT
v6/filter/ts-forward -o tailscale0 -j ACCEPT
`,
},
{
@@ -226,22 +258,28 @@ up
ip addr add 100.101.102.104/10 dev tailscale0
ip route add 10.0.0.0/8 dev tailscale0 table 52
ip route add 100.100.100.100/32 dev tailscale0 table 52` + basic +
`filter/FORWARD -j ts-forward
filter/INPUT -j ts-input
filter/ts-forward -i tailscale0 -j MARK --set-mark 0x40000
filter/ts-forward -m mark --mark 0x40000 -j ACCEPT
filter/ts-forward -o tailscale0 -s 100.64.0.0/10 -j DROP
filter/ts-forward -o tailscale0 -j ACCEPT
filter/ts-input -i lo -s 100.101.102.104 -j ACCEPT
filter/ts-input ! -i tailscale0 -s 100.115.92.0/23 -j RETURN
filter/ts-input ! -i tailscale0 -s 100.64.0.0/10 -j DROP
nat/POSTROUTING -j ts-postrouting
`v4/filter/FORWARD -j ts-forward
v4/filter/INPUT -j ts-input
v4/filter/ts-forward -i tailscale0 -j MARK --set-mark 0x40000
v4/filter/ts-forward -m mark --mark 0x40000 -j ACCEPT
v4/filter/ts-forward -o tailscale0 -s 100.64.0.0/10 -j DROP
v4/filter/ts-forward -o tailscale0 -j ACCEPT
v4/filter/ts-input -i lo -s 100.101.102.104 -j ACCEPT
v4/filter/ts-input ! -i tailscale0 -s 100.115.92.0/23 -j RETURN
v4/filter/ts-input ! -i tailscale0 -s 100.64.0.0/10 -j DROP
v4/nat/POSTROUTING -j ts-postrouting
v6/filter/FORWARD -j ts-forward
v6/filter/INPUT -j ts-input
v6/filter/ts-forward -i tailscale0 -j MARK --set-mark 0x40000
v6/filter/ts-forward -m mark --mark 0x40000 -j ACCEPT
v6/filter/ts-forward -o tailscale0 -j ACCEPT
v6/nat/POSTROUTING -j ts-postrouting
`,
},
}
fake := NewFakeOS(t)
router, err := newUserspaceRouterAdvanced(t.Logf, "tailscale0", fake, fake)
router, err := newUserspaceRouterAdvanced(t.Logf, "tailscale0", fake.netfilter4, fake.netfilter6, fake, true, true)
if err != nil {
t.Fatalf("failed to create router: %v", err)
}
@@ -275,21 +313,15 @@ nat/POSTROUTING -j ts-postrouting
}
}
// fakeOS implements netfilterRunner and commandRunner, but captures
// changes without touching the OS.
type fakeOS struct {
t *testing.T
up bool
ips []string
routes []string
rules []string
netfilter map[string][]string
type fakeNetfilter struct {
t *testing.T
n map[string][]string
}
func NewFakeOS(t *testing.T) *fakeOS {
return &fakeOS{
func newNetfilter(t *testing.T) *fakeNetfilter {
return &fakeNetfilter{
t: t,
netfilter: map[string][]string{
n: map[string][]string{
"filter/INPUT": nil,
"filter/OUTPUT": nil,
"filter/FORWARD": nil,
@@ -300,6 +332,118 @@ func NewFakeOS(t *testing.T) *fakeOS {
}
}
func (n *fakeNetfilter) Insert(table, chain string, pos int, args ...string) error {
k := table + "/" + chain
if rules, ok := n.n[k]; ok {
if pos > len(rules)+1 {
n.t.Errorf("bad position %d in %s", pos, k)
return errExec
}
rules = append(rules, "")
copy(rules[pos:], rules[pos-1:])
rules[pos-1] = strings.Join(args, " ")
n.n[k] = rules
} else {
n.t.Errorf("unknown table/chain %s", k)
return errExec
}
return nil
}
func (n *fakeNetfilter) Append(table, chain string, args ...string) error {
k := table + "/" + chain
return n.Insert(table, chain, len(n.n[k])+1, args...)
}
func (n *fakeNetfilter) Exists(table, chain string, args ...string) (bool, error) {
k := table + "/" + chain
if rules, ok := n.n[k]; ok {
for _, rule := range rules {
if rule == strings.Join(args, " ") {
return true, nil
}
}
return false, nil
} else {
n.t.Errorf("unknown table/chain %s", k)
return false, errExec
}
}
func (n *fakeNetfilter) Delete(table, chain string, args ...string) error {
k := table + "/" + chain
if rules, ok := n.n[k]; ok {
for i, rule := range rules {
if rule == strings.Join(args, " ") {
rules = append(rules[:i], rules[i+1:]...)
n.n[k] = rules
return nil
}
}
n.t.Errorf("delete of unknown rule %q from %s", strings.Join(args, " "), k)
return errExec
} else {
n.t.Errorf("unknown table/chain %s", k)
return errExec
}
}
func (n *fakeNetfilter) ClearChain(table, chain string) error {
k := table + "/" + chain
if _, ok := n.n[k]; ok {
n.n[k] = nil
return nil
} else {
n.t.Logf("note: ClearChain: unknown table/chain %s", k)
return errors.New("exitcode:1")
}
}
func (n *fakeNetfilter) NewChain(table, chain string) error {
k := table + "/" + chain
if _, ok := n.n[k]; ok {
n.t.Errorf("table/chain %s already exists", k)
return errExec
}
n.n[k] = nil
return nil
}
func (n *fakeNetfilter) DeleteChain(table, chain string) error {
k := table + "/" + chain
if rules, ok := n.n[k]; ok {
if len(rules) != 0 {
n.t.Errorf("%s is not empty", k)
return errExec
}
delete(n.n, k)
return nil
} else {
n.t.Errorf("%s does not exist", k)
return errExec
}
}
// fakeOS implements commandRunner and provides v4 and v6
// netfilterRunners, but captures changes without touching the OS.
type fakeOS struct {
t *testing.T
up bool
ips []string
routes []string
rules []string
netfilter4 *fakeNetfilter
netfilter6 *fakeNetfilter
}
func NewFakeOS(t *testing.T) *fakeOS {
return &fakeOS{
t: t,
netfilter4: newNetfilter(t),
netfilter6: newNetfilter(t),
}
}
var errExec = errors.New("execution failed")
func (o *fakeOS) String() string {
@@ -323,120 +467,30 @@ func (o *fakeOS) String() string {
}
var chains []string
for chain := range o.netfilter {
for chain := range o.netfilter4.n {
chains = append(chains, chain)
}
sort.Strings(chains)
for _, chain := range chains {
for _, rule := range o.netfilter[chain] {
fmt.Fprintf(&b, "%s %s\n", chain, rule)
for _, rule := range o.netfilter4.n[chain] {
fmt.Fprintf(&b, "v4/%s %s\n", chain, rule)
}
}
chains = nil
for chain := range o.netfilter6.n {
chains = append(chains, chain)
}
sort.Strings(chains)
for _, chain := range chains {
for _, rule := range o.netfilter6.n[chain] {
fmt.Fprintf(&b, "v6/%s %s\n", chain, rule)
}
}
return b.String()[:len(b.String())-1]
}
func (o *fakeOS) Insert(table, chain string, pos int, args ...string) error {
k := table + "/" + chain
if rules, ok := o.netfilter[k]; ok {
if pos > len(rules)+1 {
o.t.Errorf("bad position %d in %s", pos, k)
return errExec
}
rules = append(rules, "")
copy(rules[pos:], rules[pos-1:])
rules[pos-1] = strings.Join(args, " ")
o.netfilter[k] = rules
} else {
o.t.Errorf("unknown table/chain %s", k)
return errExec
}
return nil
}
func (o *fakeOS) Append(table, chain string, args ...string) error {
k := table + "/" + chain
return o.Insert(table, chain, len(o.netfilter[k])+1, args...)
}
func (o *fakeOS) Exists(table, chain string, args ...string) (bool, error) {
k := table + "/" + chain
if rules, ok := o.netfilter[k]; ok {
for _, rule := range rules {
if rule == strings.Join(args, " ") {
return true, nil
}
}
return false, nil
} else {
o.t.Errorf("unknown table/chain %s", k)
return false, errExec
}
}
func (o *fakeOS) Delete(table, chain string, args ...string) error {
k := table + "/" + chain
if rules, ok := o.netfilter[k]; ok {
for i, rule := range rules {
if rule == strings.Join(args, " ") {
rules = append(rules[:i], rules[i+1:]...)
o.netfilter[k] = rules
return nil
}
}
o.t.Errorf("delete of unknown rule %q from %s", strings.Join(args, " "), k)
return errExec
} else {
o.t.Errorf("unknown table/chain %s", k)
return errExec
}
}
func (o *fakeOS) ListChains(table string) (ret []string, err error) {
for chain := range o.netfilter {
pfx := table + "/"
if strings.HasPrefix(chain, pfx) {
ret = append(ret, chain[len(pfx):])
}
}
return ret, nil
}
func (o *fakeOS) ClearChain(table, chain string) error {
k := table + "/" + chain
if _, ok := o.netfilter[k]; ok {
o.netfilter[k] = nil
return nil
} else {
o.t.Logf("note: ClearChain: unknown table/chain %s", k)
return errors.New("exitcode:1")
}
}
func (o *fakeOS) NewChain(table, chain string) error {
k := table + "/" + chain
if _, ok := o.netfilter[k]; ok {
o.t.Errorf("table/chain %s already exists", k)
return errExec
}
o.netfilter[k] = nil
return nil
}
func (o *fakeOS) DeleteChain(table, chain string) error {
k := table + "/" + chain
if rules, ok := o.netfilter[k]; ok {
if len(rules) != 0 {
o.t.Errorf("%s is not empty", k)
return errExec
}
delete(o.netfilter, k)
return nil
} else {
o.t.Errorf("%s does not exist", k)
return errExec
}
}
func (o *fakeOS) run(args ...string) error {
unexpected := func() error {
o.t.Errorf("unexpected invocation %q", strings.Join(args, " "))
@@ -446,7 +500,20 @@ func (o *fakeOS) run(args ...string) error {
return unexpected()
}
if len(args) == 2 && args[1] == "rule" {
// naked invocation of `ip rule` is a feature test. Return
// successfully.
return nil
}
family := ""
rest := strings.Join(args[3:], " ")
if args[1] == "-4" || args[1] == "-6" {
family = args[1]
copy(args[1:], args[2:])
args = args[:len(args)-1]
rest = family + " " + strings.Join(args[3:], " ")
}
var l *[]string
switch args[1] {

View File

@@ -5,15 +5,17 @@
package router
import (
"context"
"fmt"
"log"
"os/exec"
"sync"
"syscall"
"time"
winipcfg "github.com/tailscale/winipcfg-go"
"github.com/tailscale/wireguard-go/device"
"github.com/tailscale/wireguard-go/tun"
"golang.zx2c4.com/wireguard/windows/tunnel/winipcfg"
"tailscale.com/logtail/backoff"
"tailscale.com/types/logger"
"tailscale.com/wgengine/router/dns"
)
@@ -25,9 +27,7 @@ type winRouter struct {
wgdev *device.Device
routeChangeCallback *winipcfg.RouteChangeCallback
dns *dns.Manager
mu sync.Mutex
firewallRuleIP string // the IP rule exists for, or "" if rule doesn't exist
firewall *firewallTweaker
}
func newUserspaceRouter(logf logger.Logf, wgdev *device.Device, tundev tun.Device) (Router, error) {
@@ -49,73 +49,34 @@ func newUserspaceRouter(logf logger.Logf, wgdev *device.Device, tundev tun.Devic
tunname: tunname,
nativeTun: nativeTun,
dns: dns.NewManager(mconfig),
firewall: &firewallTweaker{logf: logger.WithPrefix(logf, "firewall: ")},
}, nil
}
func (r *winRouter) Up() error {
r.removeFirewallAcceptRule()
r.firewall.clear()
var err error
t0 := time.Now()
r.routeChangeCallback, err = monitorDefaultRoutes(r.nativeTun)
d := time.Since(t0).Round(time.Millisecond)
if err != nil {
log.Fatalf("MonitorDefaultRoutes: %v", err)
return fmt.Errorf("monitorDefaultRoutes, after %v: %v", d, err)
}
r.logf("monitorDefaultRoutes done after %v", d)
return nil
}
// removeFirewallAcceptRule removes the "Tailscale-In" firewall rule.
//
// If it doesn't already exist, this currently returns an error but TODO: it should not.
//
// So callers should ignore its error for now.
func (r *winRouter) removeFirewallAcceptRule() error {
r.mu.Lock()
defer r.mu.Unlock()
r.firewallRuleIP = ""
cmd := exec.Command("netsh", "advfirewall", "firewall", "delete", "rule", "name=Tailscale-In", "dir=in")
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
return cmd.Run()
}
// addFirewallAcceptRule adds a firewall rule to allow all incoming
// traffic to the given IP (the Tailscale adapter's IP) for network
// adapters in category private. (as previously set by
// setPrivateNetwork)
//
// It returns (false, nil) if the firewall rule was already previously existed with this IP.
func (r *winRouter) addFirewallAcceptRule(ipStr string) (added bool, err error) {
r.mu.Lock()
defer r.mu.Unlock()
if ipStr == r.firewallRuleIP {
return false, nil
}
cmd := exec.Command("netsh", "advfirewall", "firewall", "add", "rule", "name=Tailscale-In", "dir=in", "action=allow", "localip="+ipStr, "profile=private", "enable=yes")
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
err = cmd.Run()
if err != nil {
return false, err
}
r.firewallRuleIP = ipStr
return true, nil
}
func (r *winRouter) Set(cfg *Config) error {
if cfg == nil {
cfg = &shutdownConfig
}
if len(cfg.LocalAddrs) == 1 && cfg.LocalAddrs[0].Bits == 32 {
ipStr := cfg.LocalAddrs[0].IP.String()
if ok, err := r.addFirewallAcceptRule(ipStr); err != nil {
r.logf("addFirewallRule(%q): %v", ipStr, err)
} else if ok {
r.logf("added firewall rule Tailscale-In for %v", ipStr)
}
} else {
r.removeFirewallAcceptRule()
var localAddrs []string
for _, la := range cfg.LocalAddrs {
localAddrs = append(localAddrs, la.String())
}
r.firewall.set(localAddrs)
err := configureInterface(cfg, r.nativeTun)
if err != nil {
@@ -131,7 +92,7 @@ func (r *winRouter) Set(cfg *Config) error {
}
func (r *winRouter) Close() error {
r.removeFirewallAcceptRule()
r.firewall.clear()
if err := r.dns.Down(); err != nil {
return fmt.Errorf("dns down: %w", err)
@@ -146,3 +107,110 @@ func (r *winRouter) Close() error {
func cleanup(logf logger.Logf, interfaceName string) {
// Nothing to do here.
}
// firewallTweaker changes the Windows firewall. Normally this wouldn't be so complicated,
// but it can be REALLY SLOW to change the Windows firewall for reasons not understood.
// Like 4 minutes slow. But usually it's tens of milliseconds.
// See https://github.com/tailscale/tailscale/issues/785.
// So this tracks the desired state and runs the actual adjusting code asynchrounsly.
type firewallTweaker struct {
logf logger.Logf
mu sync.Mutex
running bool // doAsyncSet goroutine is running
known bool // firewall is in known state (in lastVal)
want []string // next value we want, or "" to delete the firewall rule
lastVal []string // last set value, if known
}
func (ft *firewallTweaker) clear() { ft.set(nil) }
// set takes the IPv4 and/or IPv6 CIDRs to allow; an empty slice
// removes the firwall rules.
//
// set takes ownership of the slice.
func (ft *firewallTweaker) set(cidrs []string) {
ft.mu.Lock()
defer ft.mu.Unlock()
if len(cidrs) == 0 {
ft.logf("marking for removal")
} else {
ft.logf("marking allowed %v", cidrs)
}
ft.want = cidrs
if ft.running {
// The doAsyncSet goroutine will check ft.want
// before returning.
return
}
ft.logf("starting netsh goroutine")
ft.running = true
go ft.doAsyncSet()
}
func (ft *firewallTweaker) runFirewall(args ...string) (time.Duration, error) {
t0 := time.Now()
args = append([]string{"advfirewall", "firewall"}, args...)
cmd := exec.Command("netsh", args...)
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
err := cmd.Run()
return time.Since(t0).Round(time.Millisecond), err
}
func (ft *firewallTweaker) doAsyncSet() {
bo := backoff.NewBackoff("win-firewall", ft.logf, time.Minute)
ctx := context.Background()
ft.mu.Lock()
for { // invariant: ft.mu must be locked when beginning this block
val := ft.want
if ft.known && strsEqual(ft.lastVal, val) {
ft.running = false
ft.logf("ending netsh goroutine")
ft.mu.Unlock()
return
}
needClear := !ft.known || len(ft.lastVal) > 0 || len(val) == 0
ft.mu.Unlock()
if needClear {
ft.logf("clearing Tailscale-In firewall rules...")
// We ignore the error here, because netsh returns an error for
// deleting something that doesn't match.
// TODO(bradfitz): care? That'd involve querying it before/after to see
// whether it was necessary/worked. But the output format is localized,
// so can't rely on parsing English. Maybe need to use OLE, not netsh.exe?
d, _ := ft.runFirewall("delete", "rule", "name=Tailscale-In", "dir=in")
ft.logf("cleared Tailscale-In firewall rules in %v", d)
}
var err error
for _, cidr := range val {
ft.logf("adding Tailscale-In rule to allow %v ...", cidr)
var d time.Duration
d, err = ft.runFirewall("add", "rule", "name=Tailscale-In", "dir=in", "action=allow", "localip="+cidr, "profile=private", "enable=yes")
if err != nil {
ft.logf("error adding Tailscale-In rule to allow %v: %v", cidr, err)
break
}
ft.logf("added Tailscale-In rule to allow %v in %v", cidr, d)
}
bo.BackOff(ctx, err)
ft.mu.Lock()
ft.lastVal = val
ft.known = (err == nil)
}
}
func strsEqual(a, b []string) bool {
if len(a) != len(b) {
return false
}
for i := range a {
if a[i] != b[i] {
return false
}
}
return true
}

View File

@@ -24,6 +24,8 @@ type commandRunner interface {
type osCommandRunner struct{}
// errCode extracts and returns the process exit code from err, or
// zero if err is nil.
func errCode(err error) int {
if err == nil {
return 0

View File

@@ -6,8 +6,10 @@ package tsdns
import (
"bytes"
"context"
"encoding/binary"
"errors"
"fmt"
"hash/crc32"
"math/rand"
"net"
@@ -16,6 +18,8 @@ import (
"time"
"inet.af/netaddr"
"tailscale.com/logtail/backoff"
"tailscale.com/net/netns"
"tailscale.com/types/logger"
)
@@ -109,7 +113,7 @@ type forwarder struct {
// conns are the UDP connections used for forwarding.
// A random one is selected for each request, regardless of the target upstream.
conns []*net.UDPConn
conns []*fwdConn
mu sync.Mutex
// upstreams are the nameserver addresses that should be used for forwarding.
@@ -127,24 +131,16 @@ func newForwarder(logf logger.Logf, responses chan Packet) *forwarder {
logf: logger.WithPrefix(logf, "forward: "),
responses: responses,
closed: make(chan struct{}),
conns: make([]*net.UDPConn, connCount),
conns: make([]*fwdConn, connCount),
txMap: make(map[txid]forwardingRecord),
}
}
func (f *forwarder) Start() error {
var err error
for i := range f.conns {
f.conns[i], err = net.ListenUDP("udp", nil)
if err != nil {
return err
}
}
f.wg.Add(connCount + 1)
for idx, conn := range f.conns {
go f.recv(uint16(idx), conn)
for idx := range f.conns {
f.conns[idx] = newFwdConn(f.logf, idx)
go f.recv(f.conns[idx])
}
go f.cleanMap()
@@ -161,14 +157,10 @@ func (f *forwarder) Close() {
close(f.closed)
for _, conn := range f.conns {
conn.SetDeadline(aLongTimeAgo)
conn.close()
}
f.wg.Wait()
for _, conn := range f.conns {
conn.Close()
}
}
func (f *forwarder) setUpstreams(upstreams []net.Addr) {
@@ -177,31 +169,27 @@ func (f *forwarder) setUpstreams(upstreams []net.Addr) {
f.mu.Unlock()
}
// send sends packet to dst. It is best effort.
func (f *forwarder) send(packet []byte, dst net.Addr) {
connIdx := rand.Intn(connCount)
conn := f.conns[connIdx]
_, err := conn.WriteTo(packet, dst)
// Do not log errors due to expired deadline.
if err != nil && !errors.Is(err, os.ErrDeadlineExceeded) {
f.logf("send: %v", err)
}
conn.send(packet, dst)
}
func (f *forwarder) recv(connIdx uint16, conn *net.UDPConn) {
func (f *forwarder) recv(conn *fwdConn) {
defer f.wg.Done()
for {
out := make([]byte, maxResponseBytes)
n, err := conn.Read(out)
if err != nil {
// Do not log errors due to expired deadline.
if !errors.Is(err, os.ErrDeadlineExceeded) {
f.logf("recv: %v", err)
}
select {
case <-f.closed:
return
default:
}
out := make([]byte, maxResponseBytes)
n := conn.read(out)
if n == 0 {
continue
}
if n < headerBytes {
f.logf("recv: packet too small (%d bytes)", n)
}
@@ -285,3 +273,194 @@ func (f *forwarder) forward(query Packet) error {
return nil
}
// A fwdConn manages a single connection used to forward DNS requests.
// Net link changes can cause a *net.UDPConn to become permanently unusable, particularly on macOS.
// fwdConn detects such situations and transparently creates new connections.
type fwdConn struct {
// logf allows a fwdConn to log.
logf logger.Logf
// wg tracks the number of outstanding conn.Read and conn.Write calls.
wg sync.WaitGroup
// change allows calls to read to block until a the network connection has been replaced.
change *sync.Cond
// mu protects fields that follow it; it is also change's Locker.
mu sync.Mutex
// closed tracks whether fwdConn has been permanently closed.
closed bool
// conn is the current active connection.
conn net.PacketConn
}
func newFwdConn(logf logger.Logf, idx int) *fwdConn {
c := new(fwdConn)
c.logf = logger.WithPrefix(logf, fmt.Sprintf("fwdConn %d: ", idx))
c.change = sync.NewCond(&c.mu)
// c.conn is created lazily in send
return c
}
// send sends packet to dst using c's connection.
// It is best effort. It is UDP, after all. Failures are logged.
func (c *fwdConn) send(packet []byte, dst net.Addr) {
var b *backoff.Backoff // lazily initialized, since it is not needed in the common case
backOff := func(err error) {
if b == nil {
b = backoff.NewBackoff("tsdns-fwdConn-send", c.logf, 30*time.Second)
}
b.BackOff(context.Background(), err)
}
for {
// Gather the current connection.
// We can't hold the lock while we call WriteTo.
c.mu.Lock()
conn := c.conn
closed := c.closed
if closed {
c.mu.Unlock()
return
}
if conn == nil {
c.reconnectLocked()
c.mu.Unlock()
continue
}
c.mu.Unlock()
c.wg.Add(1)
_, err := conn.WriteTo(packet, dst)
c.wg.Done()
if err == nil {
// Success
return
}
if errors.Is(err, os.ErrDeadlineExceeded) {
// We intentionally closed this connection.
// It has been replaced by a new connection. Try again.
continue
}
// Something else went wrong.
// We have three choices here: try again, give up, or create a new connection.
var opErr *net.OpError
if !errors.As(err, &opErr) {
// Weird. All errors from the net package should be *net.OpError. Bail.
c.logf("send: non-*net.OpErr %v (%T)", err, err)
return
}
if opErr.Temporary() || opErr.Timeout() {
// I doubt that either of these can happen (this is UDP),
// but go ahead and try again.
backOff(err)
continue
}
if networkIsDown(err) {
// Fail.
c.logf("send: network is down")
return
}
if networkIsUnreachable(err) {
// This can be caused by a link change.
// Replace the existing connection with a new one.
c.mu.Lock()
// It's possible that multiple senders discovered simultaneously
// that the network is unreachable. Avoid reconnecting multiple times:
// Only reconnect if the current connection is the one that we
// discovered to be problematic.
if c.conn == conn {
backOff(err)
c.reconnectLocked()
}
c.mu.Unlock()
// Try again with our new network connection.
continue
}
// Unrecognized error. Fail.
c.logf("send: unrecognized error: %v", err)
return
}
}
// read waits for a response from c's connection.
// It returns the number of bytes read, which may be 0
// in case of an error or a closed connection.
func (c *fwdConn) read(out []byte) int {
for {
// Gather the current connection.
// We can't hold the lock while we call ReadFrom.
c.mu.Lock()
conn := c.conn
closed := c.closed
if closed {
c.mu.Unlock()
return 0
}
if conn == nil {
// There is no current connection.
// Wait for the connection to change, then try again.
c.change.Wait()
c.mu.Unlock()
continue
}
c.mu.Unlock()
c.wg.Add(1)
n, _, err := conn.ReadFrom(out)
c.wg.Done()
if err == nil {
// Success.
return n
}
if errors.Is(err, os.ErrDeadlineExceeded) {
// We intentionally closed this connection.
// It has been replaced by a new connection. Try again.
continue
}
c.logf("read: unrecognized error: %v", err)
return 0
}
}
// reconnectLocked replaces the current connection with a new one.
// c.mu must be locked.
func (c *fwdConn) reconnectLocked() {
c.closeConnLocked()
// Make a new connection.
conn, err := netns.Listener().ListenPacket(context.Background(), "udp", "")
if err != nil {
c.logf("ListenPacket failed: %v", err)
} else {
c.conn = conn
}
// Broadcast that a new connection is available.
c.change.Broadcast()
}
// closeCurrentConn closes the current connection.
// c.mu must be locked.
func (c *fwdConn) closeConnLocked() {
if c.conn == nil {
return
}
// Unblock all readers/writers, wait for them, close the connection.
c.conn.SetDeadline(aLongTimeAgo)
c.wg.Wait()
c.conn.Close()
c.conn = nil
}
// close permanently closes c.
func (c *fwdConn) close() {
c.mu.Lock()
defer c.mu.Unlock()
if c.closed {
return
}
c.closed = true
c.closeConnLocked()
// Unblock any remaining readers.
c.change.Broadcast()
}

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