Compare commits

...

358 Commits

Author SHA1 Message Date
Christina Wen
77f1591d68 API.md: revise documentation to be more consistent
Signed-off-by: Christina Wen <christina@tailscale.com>
2021-01-05 14:28:14 -05:00
Christina Wen
309d113f93 API.md: release API documentation
Co-authored-by: Daniel Chung <daniel@tailscale.com>
Signed-off-by: Christina Wen <christina@tailscale.com>
2021-01-05 13:37:11 -05:00
David Anderson
8fc11d582d go.sum: update to match wireguard-go version update.
Signed-off-by: David Anderson <dave@natulte.net>
2021-01-02 16:27:06 -08:00
Josh Bleecher Snyder
14af677332 go.mod: update wireguard-go version
To pick up netaddr deps change
2020-12-30 17:41:14 -08:00
David Anderson
86fe22a1b1 Update netaddr, and adjust wgengine/magicsock due to API change.
Signed-off-by: David Anderson <danderson@tailscale.com>
2020-12-30 17:36:03 -08:00
Josh Bleecher Snyder
56a7652dc9 wgkey: new package
This is a replacement for the key-related parts
of the wireguard-go wgcfg package.

This is almost a straight copy/paste from the wgcfg package.
I have slightly changed some of the exported functions and types
to avoid stutter, added and tweaked some comments,
and removed some now-unused code.

To avoid having wireguard-go depend on this new package,
wgcfg will keep its key types.

We translate into and out of those types at the last minute.
These few remaining uses will be eliminated alongside
the rest of the wgcfg package.

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2020-12-30 17:33:02 -08:00
Brad Fitzpatrick
13b554fed9 version: bump for the last time in 2020 2020-12-30 12:07:25 -08:00
Brad Fitzpatrick
c2edb2865b go.sum: update 2020-12-30 12:07:25 -08:00
Christine Dodrill
70f14af21e add nix-shell boilerplate (#1028)
This enables users of nix-shell to automagically have the correct 
development environment by simply changing directory into a
checkout of this repo. For more information on this see the following
links:

- https://christine.website/blog/how-i-start-nix-2020-03-08
- https://direnv.net/
2020-12-29 12:17:03 -05:00
Brad Fitzpatrick
0d94fe5f69 wgengine/router: disable IPv6 on Linux if ip rule -6 fails (#1074)
Updates #562
Fixes #973

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2020-12-29 08:26:17 -08:00
Josh Bleecher Snyder
1e88050403 net/tsaddr: add ChromeOS contains tests
Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2020-12-28 16:48:43 -08:00
Josh Bleecher Snyder
cf2ac2d123 go.mod: upgrade inet.af/netaddr
To pick up IPPrefix.Contains fix.
2020-12-28 15:46:46 -08:00
Josh Bleecher Snyder
2fe770ed72 all: replace wgcfg.IP and wgcfg.CIDR with netaddr types
Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2020-12-28 13:00:42 -08:00
Charlotte Brandhorst-Satzkorn
ff2b3d02e6 Fix typo in cmd/tailscale/cli/cli.go (#1069)
Remove duplicate 'to connect' in error message.

Fixes #1068

Signed-off-by: Charlotte Brandhorst-Satzkorn <charlotte@catzkorn.dev>
2020-12-25 07:32:37 -08:00
Frederik “Freso” S. Olesen
a9a80ab372 tailscaled.service: Harden systemd unit somewhat (#1062)
While not a full capability lockdown of the systemd unit, this still
improves sandboxing and security of the running process a good deal.

Signed-off-by: Frederik “Freso” S. Olesen <freso.dk@gmail.com>
2020-12-24 16:14:58 -08:00
Matt Layher
1a42cef3a2 cmd/tailscale*: make updatedeps
Signed-off-by: Matt Layher <mdlayher@gmail.com>
2020-12-23 08:08:02 -08:00
Matt Layher
bfbd6b9241 go.mod: bump github.com/mdlayher/netlink to v1.2.0
Signed-off-by: Matt Layher <mdlayher@gmail.com>
2020-12-23 08:08:02 -08:00
Brad Fitzpatrick
80c94168ae wgengine: finish updating isTrimmablePeer
I accidentally merged Dave's change with the XXXX DO NOT SUBMIT comment
in it.
2020-12-22 14:48:24 -08:00
David Anderson
cb96b14bf4 net/packet: remove the custom IP4/IP6 types in favor of netaddr.IP.
Upstream netaddr has a change that makes it alloc-free, so it's safe to
use in hot codepaths. This gets rid of one of the many IP types in our
codebase.

Performance is currently worse across the board. This is likely due in
part to netaddr.IP being a larger value type (4b -> 24b for IPv4,
16b -> 24b for IPv6), and in other part due to missing low-hanging fruit
optimizations in netaddr. However, the regression is less bad than
it looks at first glance, because we'd micro-optimized packet.IP* in
the past few weeks. This change drops us back to roughly where we
were at the 1.2 release, but with the benefit of a significant
code and architectural simplification.

name                   old time/op    new time/op    delta
pkg:tailscale.com/net/packet goos:linux goarch:amd64
Decode/tcp4-8            12.2ns ± 5%    29.7ns ± 2%  +142.32%  (p=0.008 n=5+5)
Decode/tcp6-8            12.6ns ± 3%    65.1ns ± 2%  +418.47%  (p=0.008 n=5+5)
Decode/udp4-8            11.8ns ± 3%    30.5ns ± 2%  +157.94%  (p=0.008 n=5+5)
Decode/udp6-8            27.1ns ± 1%    65.7ns ± 2%  +142.36%  (p=0.016 n=4+5)
Decode/icmp4-8           24.6ns ± 2%    30.5ns ± 2%   +23.65%  (p=0.016 n=4+5)
Decode/icmp6-8           22.9ns ±51%    65.5ns ± 2%  +186.19%  (p=0.008 n=5+5)
Decode/igmp-8            18.1ns ±44%    30.2ns ± 1%   +66.89%  (p=0.008 n=5+5)
Decode/unknown-8         20.8ns ± 1%    10.6ns ± 9%   -49.11%  (p=0.016 n=4+5)
pkg:tailscale.com/wgengine/filter goos:linux goarch:amd64
Filter/icmp4-8           30.5ns ± 1%    77.9ns ± 3%  +155.01%  (p=0.008 n=5+5)
Filter/tcp4_syn_in-8     43.7ns ± 3%   123.0ns ± 3%  +181.72%  (p=0.008 n=5+5)
Filter/tcp4_syn_out-8    24.5ns ± 2%    45.7ns ± 6%   +86.22%  (p=0.008 n=5+5)
Filter/udp4_in-8         64.8ns ± 1%   210.0ns ± 2%  +223.87%  (p=0.008 n=5+5)
Filter/udp4_out-8         119ns ± 0%     278ns ± 0%  +133.78%  (p=0.016 n=4+5)
Filter/icmp6-8           40.3ns ± 2%   204.4ns ± 4%  +407.70%  (p=0.008 n=5+5)
Filter/tcp6_syn_in-8     35.3ns ± 3%   199.2ns ± 2%  +464.95%  (p=0.008 n=5+5)
Filter/tcp6_syn_out-8    32.8ns ± 2%    81.0ns ± 2%  +147.10%  (p=0.008 n=5+5)
Filter/udp6_in-8          106ns ± 2%     290ns ± 2%  +174.48%  (p=0.008 n=5+5)
Filter/udp6_out-8         184ns ± 2%     314ns ± 3%   +70.43%  (p=0.016 n=4+5)
pkg:tailscale.com/wgengine/tstun goos:linux goarch:amd64
Write-8                  9.02ns ± 3%    8.92ns ± 1%      ~     (p=0.421 n=5+5)

name                   old alloc/op   new alloc/op   delta
pkg:tailscale.com/net/packet goos:linux goarch:amd64
Decode/tcp4-8             0.00B          0.00B           ~     (all equal)
Decode/tcp6-8             0.00B          0.00B           ~     (all equal)
Decode/udp4-8             0.00B          0.00B           ~     (all equal)
Decode/udp6-8             0.00B          0.00B           ~     (all equal)
Decode/icmp4-8            0.00B          0.00B           ~     (all equal)
Decode/icmp6-8            0.00B          0.00B           ~     (all equal)
Decode/igmp-8             0.00B          0.00B           ~     (all equal)
Decode/unknown-8          0.00B          0.00B           ~     (all equal)
pkg:tailscale.com/wgengine/filter goos:linux goarch:amd64
Filter/icmp4-8            0.00B          0.00B           ~     (all equal)
Filter/tcp4_syn_in-8      0.00B          0.00B           ~     (all equal)
Filter/tcp4_syn_out-8     0.00B          0.00B           ~     (all equal)
Filter/udp4_in-8          0.00B          0.00B           ~     (all equal)
Filter/udp4_out-8         16.0B ± 0%     64.0B ± 0%  +300.00%  (p=0.008 n=5+5)
Filter/icmp6-8            0.00B          0.00B           ~     (all equal)
Filter/tcp6_syn_in-8      0.00B          0.00B           ~     (all equal)
Filter/tcp6_syn_out-8     0.00B          0.00B           ~     (all equal)
Filter/udp6_in-8          0.00B          0.00B           ~     (all equal)
Filter/udp6_out-8         48.0B ± 0%     64.0B ± 0%   +33.33%  (p=0.008 n=5+5)

name                   old allocs/op  new allocs/op  delta
pkg:tailscale.com/net/packet goos:linux goarch:amd64
Decode/tcp4-8              0.00           0.00           ~     (all equal)
Decode/tcp6-8              0.00           0.00           ~     (all equal)
Decode/udp4-8              0.00           0.00           ~     (all equal)
Decode/udp6-8              0.00           0.00           ~     (all equal)
Decode/icmp4-8             0.00           0.00           ~     (all equal)
Decode/icmp6-8             0.00           0.00           ~     (all equal)
Decode/igmp-8              0.00           0.00           ~     (all equal)
Decode/unknown-8           0.00           0.00           ~     (all equal)
pkg:tailscale.com/wgengine/filter goos:linux goarch:amd64
Filter/icmp4-8             0.00           0.00           ~     (all equal)
Filter/tcp4_syn_in-8       0.00           0.00           ~     (all equal)
Filter/tcp4_syn_out-8      0.00           0.00           ~     (all equal)
Filter/udp4_in-8           0.00           0.00           ~     (all equal)
Filter/udp4_out-8          1.00 ± 0%      1.00 ± 0%      ~     (all equal)
Filter/icmp6-8             0.00           0.00           ~     (all equal)
Filter/tcp6_syn_in-8       0.00           0.00           ~     (all equal)
Filter/tcp6_syn_out-8      0.00           0.00           ~     (all equal)
Filter/udp6_in-8           0.00           0.00           ~     (all equal)
Filter/udp6_out-8          1.00 ± 0%      1.00 ± 0%      ~     (all equal)

Signed-off-by: David Anderson <danderson@tailscale.com>
2020-12-22 14:44:36 -08:00
Brad Fitzpatrick
d0baece5fa go.mod: bump inet.af/netaddr to non-allocating version 2020-12-22 14:25:32 -08:00
Brad Fitzpatrick
ef15096a7d control/controlclient, version/distro: detect NixOS explicitly
The fallthrough happened to work in controlclient already due to the
/etc/os-release PRETTY_NAME default, but make it explicit so it
doesn't look like an accident.

Also add it to version/distro, even though nothing needs it yet.
2020-12-21 21:03:04 -08:00
David Crawshaw
2b2a16d9a2 wgengine/router/dns: reduce windows registry key open timeout
The windows key timeout is longer than the wgengine watchdog timeout,
which means we never reach the timeout, instead the process exits.
Reduce the timeout so if we do hit it, at least the process continues.

Signed-off-by: David Crawshaw <crawshaw@tailscale.com>
2020-12-21 17:24:58 -05:00
David Crawshaw
b4f70d8232 wgengine/router/dns: use the correct interface GUID
On Win10, there's a hardcoded GUID and this works.
On Win7, this GUID changes and we need to ask the tun for its
LUID and convert that from the GUID.

This commit uses the computed GUID that is placed in InterfaceName.

Diagnosed by Jason Donnenfeld. (Thanks!)
2020-12-21 16:43:24 -05:00
Brad Fitzpatrick
15c064f76f wgengine/router/dns: remove unsafe endianness detection on Linux 2020-12-21 13:11:09 -08:00
Brad Fitzpatrick
f9659323df wgengine/router/dns: fix typo in comment 2020-12-21 13:07:30 -08:00
Brad Fitzpatrick
053a1d1340 all: annotate log verbosity levels on most egregiously spammy log prints
Fixes #924
Fixes #282

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2020-12-21 12:59:33 -08:00
Brad Fitzpatrick
57dd247376 cmd/tailscaled, logpolicy, logtail: support log levels
Log levels can now be specified with "[v1] " or "[v2] " substrings
that are then stripped and filtered at the final logger. This follows
our existing "[unexpected]" etc convention and doesn't require a
wholesale reworking of our logging at the moment.

cmd/tailscaled then gets a new --verbose=N flag to take a log level
that controls what gets logged to stderr (and thus systemd, syslog,
etc). Logtail is unaffected by --verbose.

This commit doesn't add annotations to any existing log prints. That
is in the next commit.

Updates #924
Updates #282

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2020-12-21 12:59:33 -08:00
Brad Fitzpatrick
d97ee12179 logtail, logpolicy: remove an unidiomatic use of an interface 2020-12-21 09:03:39 -08:00
Brad Fitzpatrick
83f45ae2dd version: bump date 2020-12-21 08:33:46 -08:00
Brad Fitzpatrick
c348fb554f control/controlclient: clarify a comment 2020-12-21 08:33:05 -08:00
Brad Fitzpatrick
90c8519765 go.sum: update 2020-12-21 08:32:51 -08:00
David Anderson
ca676ea645 tailcfg: introduce map version 8, for clients that support v6 node config.
For now, the server will only send v6 configuration to mapversion 8 clients
as part of an early-adopter program, while we verify that the functionality
is robust.

Signed-off-by: David Anderson <danderson@tailscale.com>
2020-12-19 18:28:27 -08:00
David Anderson
03a039d48d go.mod: bump wireguard-go version.
Signed-off-by: David Anderson <danderson@tailscale.com>
2020-12-19 17:26:05 -08:00
David Anderson
f5e33ad761 go.mod: update inet.af/netaddr, go mod tidy.
Signed-off-by: David Anderson <danderson@tailscale.com>
2020-12-19 14:01:43 -08:00
David Anderson
89be4037bb control/controlclient: report broken routing for v4 and v6.
Signed-off-by: David Anderson <danderson@tailscale.com>
2020-12-19 13:50:44 -08:00
David Anderson
baa7937998 net/interfaces: return IPv6 addresses from LocalAddresses.
In practice, we already provide IPv6 endpoint addresses via netcheck,
and that address is likely to match a local address anyway (i.e. no NAT66).
The comment at that piece of the code mentions needing to figure out a
good priority ordering, but that only applies to non-active-discovery
clients, who already don't do anything with IPv6 addresses.

Signed-off-by: David Anderson <danderson@tailscale.com>
2020-12-19 13:50:44 -08:00
David Anderson
294ceb513c ipn, wgengine/magicsock: fix tailscale status display.
Signed-off-by: David Anderson <danderson@tailscale.com>
2020-12-19 13:50:44 -08:00
David Anderson
891110e64c wgengine: expand lazy config to work with dual-stacked peers.
Lazy wg configuration now triggers if a peer has only endpoint
addresses (/32 for IPv4, /128 for IPv6). Subnet routers still
trigger eager configuration to avoid the need for a CIDR match
in the hot packet path.

Signed-off-by: David Anderson <danderson@tailscale.com>
2020-12-19 13:50:44 -08:00
David Anderson
aa353b8d0f net/packet: add an IP6 constructor from a raw byte array.
Signed-off-by: David Anderson <danderson@tailscale.com>
2020-12-19 13:50:44 -08:00
Smitty
f0b0a62873 Clarify that raw format strings are intentional
This caused some confusion in issue #460, since usually raw format
strings aren't printed directly. Hopefully by directly logging that
they are intended to be raw format strings, this will be more clear.
Rate limited format strings now look like:

  [RATE LIMITED] format string "control: sendStatus: %s: %v"

Closes #460.

Signed-off-by: Smitty <me@smitop.com>
2020-12-19 13:49:14 -08:00
David Anderson
c8c493f3d9 wgengine/magicsock: make ReceiveIPv4 a little easier to follow.
The previous code used a lot of whole-function variables and shared
behavior that only triggered based on prior action from a single codepath.
Instead of that, move the small amounts of "shared" code into each switch
case.

Signed-off-by: David Anderson <danderson@tailscale.com>
2020-12-18 01:15:53 -08:00
David Anderson
0ad109f63d wgengine/magicsock: move legacy endpoint creation into legacy.go.
Signed-off-by: David Anderson <danderson@tailscale.com>
2020-12-18 01:15:53 -08:00
David Anderson
f873da5b16 wgengine/magicsock: move more legacy endpoint handling.
Signed-off-by: David Anderson <danderson@tailscale.com>
2020-12-18 01:15:53 -08:00
David Anderson
58fcd103c4 wgengine/magicsock: move legacy sending code to legacy.go.
Signed-off-by: David Anderson <danderson@tailscale.com>
2020-12-18 01:15:53 -08:00
David Anderson
65ae66260f wgengine/magicsock: unexport AddrSet.
Signed-off-by: David Anderson <danderson@tailscale.com>
2020-12-18 01:15:53 -08:00
David Anderson
c9b9afd761 wgengine/magicsock: move most legacy nat traversal bits to another file.
Signed-off-by: David Anderson <danderson@tailscale.com>
2020-12-18 01:15:53 -08:00
Brad Fitzpatrick
5f07da4854 util/systemd: don't log warnings when not running under systemd
It caused our integration tests to fail, which prohibit logging to
os.Stderr for test cleanliness reasons.
2020-12-17 12:59:05 -08:00
Brad Fitzpatrick
741c513e51 wgengine/tsdns: fix error response marshaling, improve bad query logs
Updates #995

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2020-12-17 12:46:08 -08:00
David Anderson
554a20becb wgengine/magicsock: only log about lazy config when actually doing lazy config.
Before, tailscaled would log every 10 seconds when the periodic noteRecvActivity
call happens. This is noisy, but worse it's misleading, because the message
suggests that the disco code is starting a lazy config run for a missing peer,
whereas in fact it's just an internal piece of keepalive logic.

With this change, we still log when going from 0->1 tunnel for the peer, but
not every 10s thereafter.

Signed-off-by: David Anderson <danderson@tailscale.com>
2020-12-17 12:11:36 -08:00
Brad Fitzpatrick
da1bad51cd tailcfg: document new OmitPeers endpoint updating functionality 2020-12-15 12:16:15 -08:00
Brad Fitzpatrick
fa412c8760 wgengine/filter, wgengine/magicsock: use new IP.BitLen to simplify some code 2020-12-15 12:12:56 -08:00
Brad Fitzpatrick
afcf134812 wgengine/filter, tailcfg: support CIDRs+ranges in PacketFilter (mapver 7)
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2020-12-15 11:00:37 -08:00
Christine Dodrill
0681c6da49 Merge pull request #967 from Xe/report-status-systemd
ipn/ipnserver: enable systemd-notify support
2020-12-15 11:44:20 -05:00
Christine Dodrill
2485faf69a Merge branch 'main' into report-status-systemd 2020-12-15 08:40:46 -05:00
Christine Dodrill
7ea809897d ipn/ipnserver: enable systemd-notify support
Addresses #964

Still to be done:
- Figure out the correct logging lines in util/systemd
- Figure out if we need to slip the systemd.Status function anywhere
  else
- Log util/systemd errors? (most of the errors are of the "you cannot do
  anything about this, but it might be a bad idea to crash the program if
  it errors" kind)

Assistance in getting this over the finish line would help a lot.

Signed-off-by: Christine Dodrill <me@christine.website>

util/systemd: rename the nonlinux file to appease the magic

Signed-off-by: Christine Dodrill <me@christine.website>

util/systemd: fix package name

Signed-off-by: Christine Dodrill <me@christine.website>

util/systemd: fix review feedback from @mdlayher

Signed-off-by: Christine Dodrill <me@christine.website>

cmd/tailscale{,d}: update depaware manifests

Signed-off-by: Christine Dodrill <me@christine.website>

util/systemd: use sync.Once instead of func init

Signed-off-by: Christine Dodrill <me@christine.website>

control/controlclient: minor review feedback fixes

Signed-off-by: Christine Dodrill <me@christine.website>

{control,ipn,systemd}: fix review feedback

Signed-off-by: Christine Dodrill <me@christine.website>

review feedback fixes

Signed-off-by: Christine Dodrill <me@christine.website>

ipn: fix sprintf call

Signed-off-by: Christine Dodrill <me@christine.website>

ipn: make staticcheck less sad

Signed-off-by: Christine Dodrill <me@christine.website>

ipn: print IP address in connected status

Signed-off-by: Christine Dodrill <me@christine.website>

ipn: review feedback

Signed-off-by: Christine Dodrill <me@christine.website>

final fixups

Signed-off-by: Christine Dodrill <me@christine.website>
2020-12-15 08:39:06 -05:00
David Anderson
9cee0bfa8c wgengine/magicsock: sprinkle more docstrings.
Magicsock is too damn big, but this might help me page it back
in faster next time.

Signed-off-by: David Anderson <danderson@tailscale.com>
2020-12-14 23:59:17 -08:00
Josh Bleecher Snyder
34a0292433 depaware.txt: update
Upgrading staticcheck upgraded golang.org/x/sync
(one downside of mixing our tools in with our regular go.mod),
which introduced a new dependency via
https://go-review.googlesource.com/c/sync/+/251677

That CL could and probably should be written without runtime/debug,
but it's not clear to me that that is better at this moment
than simply accepting the additional package as a dependency.

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2020-12-14 14:01:29 -08:00
Josh Bleecher Snyder
ce4d68b416 go.mod: upgrade depaware version
Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2020-12-14 14:01:29 -08:00
Josh Bleecher Snyder
a6cad71fb2 go.mod: upgrade staticcheck to 0.1.0
Also run go.mod and fix some staticcheck warnings.

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2020-12-14 14:01:29 -08:00
Brad Fitzpatrick
a0a8b9d76a control/controlclient: don't spin when starting up when node key is expired
Fixes #1018

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2020-12-14 11:51:04 -08:00
Smitty
b895bf853a Require at least Go 1.15
This was actually required before this commit, this just updates
go.mod with that fact.

Signed-off-by: Smitty <me@smitop.com>
2020-12-13 16:36:25 -08:00
Smitty
8a57f920ae Remove unused .gitignore lines
These ignore built files that don't exist anymore, and just serve
to clutter up the .gitignore file. (I was initially confused when
I saw those lines, since I (correctly) thought that the only
Tailscale binaries were tailscale and tailscaled):

- taillogin was removed in d052586
- relaynode was removed in a56e853

Signed-off-by: Smitty <me@smitop.com>
2020-12-12 16:11:58 -08:00
Josh Bleecher Snyder
6db9c4a173 wgenginer/router/dns: use constant from golang.org/x/sys/windows
Made available in https://golang.org/cl/277153
2020-12-10 17:23:01 -08:00
Aleksandar Pesic
0dc295a640 Isolate WireGuard code into a separate file with appropriate copyright info in header.
Signed-off-by: Aleksandar Pesic <peske.nis@gmail.com>
2020-12-11 01:08:41 +01:00
Aleksandar Pesic
d854fe95d2 Trivial change in function description.
Signed-off-by: Aleksandar Pesic <peske.nis@gmail.com>
2020-12-11 00:55:37 +01:00
Aleksandar Pesic
4749a96a5b Update depaware.txt files.
Signed-off-by: Aleksandar Pesic <peske.nis@gmail.com>
2020-12-11 00:45:31 +01:00
Aleksandar Pesic
338fd44657 Replace registry-access code, update wireguard-go and x/sys/windows.
Signed-off-by: Aleksandar Pesic <peske.nis@gmail.com>
2020-12-11 00:37:24 +01:00
Aleksandar Pesic
274d32d0aa Prepare for the new wireguard-go API.
Signed-off-by: Aleksandar Pesic <peske.nis@gmail.com>
2020-12-11 00:08:28 +01:00
Adrian Dewhurst
943860fde7 version: relax git detection logic (again)
This is a repeat of commit 3aa68cd397
which was lost in a rework of version.sh.

git worktrees have a .git file rather than a .git directory, so building
in a worktree caused version.sh to generate an error.

Signed-off-by: Adrian Dewhurst <adrian@tailscale.com>
2020-12-09 21:55:41 -05:00
Brad Fitzpatrick
bce865b61b logpolicy: migrate from x/crypto/ssh/terminal to x/term 2020-12-09 15:28:31 -08:00
David Anderson
57cd7738c2 tsweb: add an endpoint to manually trigger a GC.
Signed-off-by: David Anderson <danderson@tailscale.com>
2020-12-08 16:49:42 -08:00
Brad Fitzpatrick
9cb6ee3777 go.mod, go.sum: update 2020-12-08 15:23:56 -08:00
Brad Fitzpatrick
08f94b3b50 net/netcheck: fix offset of unspecified address in PCP request packet
Fixes #810
2020-12-08 15:22:26 -08:00
Brad Fitzpatrick
442d1873ec go.mod: bump tailscale/wireguard-go 2020-12-07 14:02:05 -08:00
Brad Fitzpatrick
19c2c6403d Update go.sum 2020-12-07 14:00:53 -08:00
Brad Fitzpatrick
b3c7b631c2 tailcfg, control/controlclient: make nil PacketFilter mean unchanged (mapver 6)
After mapver 5's incremental netmap updates & user profiles, much of
the remaining bandwidth for streamed MapResponses were redundant,
unchanged PacketFilters. So make MapRequest.Version 6 mean that nil
means unchanged from the previous value.
2020-12-07 09:17:42 -08:00
Brad Fitzpatrick
05e5233e07 net/netcheck: don't send flood of PCP unmap requests to router
Updates #810
2020-12-06 19:46:11 -08:00
Brad Fitzpatrick
9503be083d tailcfg: update comments a bit 2020-12-03 12:16:10 -08:00
Brad Fitzpatrick
88179121e3 version: bump date 2020-12-03 12:08:07 -08:00
Brad Fitzpatrick
7b92f8e718 wgengine/magicsock: add start of magicsock benchmarks (Conn.ReceiveIPv4 for now)
And only single-threaded for now. Will get fancier later.

Updates #414
2020-12-02 20:26:54 -08:00
Brad Fitzpatrick
713cbe84c1 wgengine/magicsock: use net.JoinHostPort when host might have colons (udp6)
Only affected tests. (where it just generated log spam)
2020-12-02 20:19:28 -08:00
David Anderson
be6fe393c5 wgengine: don't try pinging IPv6 addresses in legacy pinger.
Signed-off-by: David Anderson <danderson@tailscale.com>
2020-12-01 20:09:32 -08:00
David Anderson
dfbde3d3aa ipn: pass through the prefix length from control.
Control sets this to /32 for IPv4 and /128 for IPv6.

Signed-off-by: David Anderson <danderson@tailscale.com>
2020-12-01 20:09:16 -08:00
David Anderson
4c8ccd6dd6 tailcfg: document new debug flag. 2020-12-01 18:17:09 -08:00
Brad Fitzpatrick
c0af7deb86 tailcfg, cmd/tailscale: add Hostinfo.ShareeNode, hide in "tailscale status" 2020-12-01 15:29:18 -08:00
Brad Fitzpatrick
ab482118ad tailcfg: add some missing json omitempty
Noticed these in MapResponses to clients.

MachineAuthorized was set true, but once we fix the coordination server
to zero out that field, then it can be omittted.
2020-11-25 10:27:01 -08:00
Dmytro Tananayskiy
c431382720 Fix receiver in order to be consistent: syncs.WaitGroupChan
Signed-off-by: Dmytro Tananayskiy <dmitriyminer@gmail.com>
2020-11-24 17:20:34 -08:00
Josh Bleecher Snyder
3a7402aa2d logtail: help the server be more efficient
Add content length hints to headers.
The server can use these hints to more efficiently select buffers.

Stop attempting to compress tiny requests.
The bandwidth savings are negligible (and sometimes negative!),
and it makes extra work for the server.

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2020-11-24 12:00:32 -08:00
Brad Fitzpatrick
cd6099113f ipn: add a comment about skipping files with null bytes
Updates #954
2020-11-24 11:07:49 -08:00
Alex Brainman
72e082aaf5 ipn: make LoadPrefs return os.ErrNotExist when reading corrupted files
It appears some users have corrupted pref.conf files. Have LoadPrefs
treat these files as non-existent. This way tailscale will make user
login, and not crash.

Fixes #954

Signed-off-by: Alex Brainman <alex.brainman@gmail.com>
2020-11-24 11:05:42 -08:00
David Crawshaw
2c48b4ee14 tailcfg: remove outdated comments about Clone methods
The cloner tool adds static checks that the Clone methods are up to
date, so failing to update Clone causes a compiler error.

Signed-off-by: David Crawshaw <crawshaw@tailscale.com>
2020-11-24 13:16:21 -05:00
Sonia Appasamy
0710fca0cd tailcfg: include ShieldsUp in HostInfo 2020-11-24 10:51:13 -05:00
Josh Bleecher Snyder
aa9d7f4665 tstime: add Parse3339B, for byte slices
Use go4.org/mem for memory safety.
A slight performance hit, but a huge performance win
for clients who start with a []byte.
The perf hit is due largely to the MapHash call, which adds ~25ns.
That is necessary to keep the fast path allocation-free.

name                     old time/op    new time/op    delta
GoParse3339/Z-8             281ns ± 1%     283ns ± 2%     ~     (p=0.366 n=9+9)
GoParse3339/TZ-8            509ns ± 0%     510ns ± 1%     ~     (p=0.059 n=9+9)
GoParse3339InLocation-8     330ns ± 1%     330ns ± 0%     ~     (p=0.802 n=10+6)
Parse3339/Z-8              69.3ns ± 1%    74.4ns ± 1%   +7.45%  (p=0.000 n=9+10)
Parse3339/TZ-8              110ns ± 1%     140ns ± 3%  +27.42%  (p=0.000 n=9+10)
ParseInt-8                 8.20ns ± 1%    8.17ns ± 1%     ~     (p=0.452 n=9+9)

name                     old alloc/op   new alloc/op   delta
GoParse3339/Z-8             0.00B          0.00B          ~     (all equal)
GoParse3339/TZ-8             160B ± 0%      160B ± 0%     ~     (all equal)
GoParse3339InLocation-8     0.00B          0.00B          ~     (all equal)
Parse3339/Z-8               0.00B          0.00B          ~     (all equal)
Parse3339/TZ-8              0.00B          0.00B          ~     (all equal)

name                     old allocs/op  new allocs/op  delta
GoParse3339/Z-8              0.00           0.00          ~     (all equal)
GoParse3339/TZ-8             3.00 ± 0%      3.00 ± 0%     ~     (all equal)
GoParse3339InLocation-8      0.00           0.00          ~     (all equal)
Parse3339/Z-8                0.00           0.00          ~     (all equal)
Parse3339/TZ-8               0.00           0.00          ~     (all equal)


Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2020-11-19 14:47:11 -08:00
Josh Bleecher Snyder
a5dd0bcb09 util/jsonutil: new package
The cornerstone API is a more memory-efficient Unmarshal.
The savings come from re-using a json.Decoder.

BenchmarkUnmarshal-8      	 4016418	       288 ns/op	       8 B/op	       1 allocs/op
BenchmarkStdUnmarshal-8   	 4189261	       283 ns/op	     184 B/op	       2 allocs/op

It also includes a Bytes type to reduce allocations
when unmarshalling a non-hex-encoded JSON string into a []byte.

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2020-11-19 13:58:35 -08:00
Josh Bleecher Snyder
b65eee0745 util/lineread: add docs to Reader
In particular, point out how to stop reading
and detect it on the other side.

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2020-11-19 12:14:58 -08:00
Josh Bleecher Snyder
1ebbaaaebb net/interfaces: make syscall and netstat agree when multiple gateways are present
likelyHomeRouterIPDarwinSyscall iterates through the list of routes,
looking for a private gateway, returning the first one it finds.

likelyHomeRouterIPDarwinExec does the same thing,
except that it returns the last one it finds.

As a result, when there are multiple gateways,
TestLikelyHomeRouterIPSyscallExec fails.
(At least, I think that that is what is happening;
I am going inferring from observed behavior.)

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2020-11-19 12:14:58 -08:00
Brad Fitzpatrick
eccc167733 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>
2020-11-18 14:51:27 -08:00
Brad Fitzpatrick
8f76548fd9 tempfork/osexec: remove old fork of os/exec
This package was a temporary fork of os/exec to fix an EINTR loop
bug that was fixed upstream for Go 1.15 in
8c1db77a92
(https://go-review.googlesource.com/c/go/+/232862), in
src/os/exec_unix.go:

8c1db77a92 (diff-72072cbd53a7240debad8aa506ff7ec795f9cfac7322e779f9bac29a4d0d0bd4)
2020-11-18 08:42:43 -08:00
Brad Fitzpatrick
5b338bf011 tempfork/registry: delete
It's unused.
2020-11-18 08:29:38 -08:00
Brad Fitzpatrick
acade77c86 ipn/ipnserver: add knob to disable babysitter 2020-11-17 15:26:39 -08:00
Brad Fitzpatrick
5d96ecd5e6 net/netstat: remove a bit more unsafe
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2020-11-17 13:49:24 -08:00
Brad Fitzpatrick
c8939ab7c7 util/endian: add Native variable to get the platform's native binary.ByteOrder
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2020-11-17 13:49:24 -08:00
Josh Bleecher Snyder
883a11f2a8 logtail: fix typo in comment
Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2020-11-17 13:43:54 -08:00
Brad Fitzpatrick
d9e2edb5ae 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>
2020-11-16 15:26:42 -08:00
David Anderson
3c508a58cc wgengine/filter: don't filter GCP DNS.
Signed-off-by: David Anderson <danderson@tailscale.com>
2020-11-16 14:08:27 -08:00
Brad Fitzpatrick
51c8fd1dfc logpolicy: add -race suffix to Go version when race detector in use 2020-11-16 10:13:06 -08:00
Brad Fitzpatrick
ff50ddf1ee util/racebuild: add package to export a race-is-enabled const 2020-11-16 10:11:53 -08:00
Brad Fitzpatrick
fc8bc76e58 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
2020-11-16 09:55:44 -08:00
Brad Fitzpatrick
7a01cd27ca net/netstat: remove some unsafe
Just removing any unnecessary unsafe while auditing unsafe usage for #921.
2020-11-14 21:24:09 -08:00
Brad Fitzpatrick
45d96788b5 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.
2020-11-14 19:53:10 -08:00
Brad Fitzpatrick
000347d4cf util/endian: add package with const for whether platform is big endian 2020-11-14 19:53:10 -08:00
Josh Bleecher Snyder
b0526e8284 net/packet: remove unnecessary mark
There's no need to mask out the bottom four bits
of b[0] if we are about to shift them away.
2020-11-13 18:31:38 -08:00
Josh Bleecher Snyder
efad55cf86 net/packet: speed up packet decoding
The compiler is failing to draw the connection between
slice cap and slice len, so is missing some obvious BCE opportunities.
Give it a hint by making the cap equal to the length.
The generated code is smaller and cleaner, and a bit faster.

name              old time/op    new time/op    delta
Decode/tcp4-8       12.2ns ± 1%    11.6ns ± 3%  -5.31%  (p=0.000 n=28+29)
Decode/tcp6-8       12.5ns ± 2%    11.9ns ± 2%  -4.84%  (p=0.000 n=30+30)
Decode/udp4-8       11.5ns ± 1%    11.1ns ± 1%  -3.11%  (p=0.000 n=25+24)
Decode/udp6-8       11.8ns ± 3%    11.4ns ± 1%  -3.08%  (p=0.000 n=30+26)
Decode/icmp4-8      11.0ns ± 3%    10.6ns ± 1%  -3.38%  (p=0.000 n=25+30)
Decode/icmp6-8      11.4ns ± 1%    11.1ns ± 2%  -2.29%  (p=0.000 n=27+30)
Decode/igmp-8       10.3ns ± 0%    10.0ns ± 1%  -3.26%  (p=0.000 n=19+23)
Decode/unknown-8    8.68ns ± 1%    8.38ns ± 1%  -3.55%  (p=0.000 n=28+29)
2020-11-13 18:31:38 -08:00
Brad Fitzpatrick
cccdd81441 go.mod: update some deps to get past a wireguard-windows checkptr fix 2020-11-13 11:55:13 -08:00
David Anderson
2eb474dd8d wgengine/filter: add test cases for len(dsts) > 1.
While the code was correct, I broke it during a refactoring and
tests didn't detect it. This fixes that glitch.

Signed-off-by: David Anderson <danderson@tailscale.com>
2020-11-12 21:41:52 -08:00
David Anderson
ce45f4f3ff wgengine/filter: inline ip6InList into match.
matchIPsOnly gets 5% slower when inlining, despite significantly reduced
memory ops and slightly tighter code.

Part of #19.

Filter/tcp6_syn_in-8     45.5ns ± 1%    42.4ns ± 2%   -6.86%  (p=0.000 n=10+10)
Filter/udp6_in-8          107ns ± 2%      94ns ± 2%  -11.50%  (p=0.000 n=9+10)

Signed-off-by: David Anderson <danderson@tailscale.com>
2020-11-12 21:41:47 -08:00
David Anderson
3fdae12f0c wgengine/filter: eliminate unnecessary memory loads.
Doesn't materially affect benchmarks, but shrinks match6 by 30 instructions
and halves memory loads.

Part of #19.

Signed-off-by: David Anderson <danderson@tailscale.com>
2020-11-12 21:41:40 -08:00
Josh Bleecher Snyder
47380ebcfb wgengine/filter: twiddle bits to optimize
Part of #19.

name            old time/op    new time/op    delta
Filter/icmp4-8    32.2ns ± 3%    32.5ns ± 2%     ~     (p=0.524 n=10+8)
Filter/icmp6-8    49.7ns ± 6%    43.1ns ± 4%  -13.12%  (p=0.000 n=9+10)

Signed-off-by: David Anderson <danderson@tailscale.com>
2020-11-12 21:41:33 -08:00
David Anderson
5062131aad wgengine/filter: treat * as both a v4 and v6 wildcard.
Part of #19.

Signed-off-by: David Anderson <danderson@tailscale.com>
2020-11-12 21:41:29 -08:00
David Anderson
2d604b3791 net/packet: represent IP6 as two uint64s.
For the operations we perform on these types (mostly net6.Contains),
this encoding is much faster.

Part of #19.

name                   old time/op    new time/op    delta
Filter/icmp4-8           27.5ns ± 1%    28.0ns ± 2%   +1.89%  (p=0.016 n=5+5)
Filter/tcp4_syn_in-8     38.8ns ± 2%    38.3ns ± 1%   -1.24%  (p=0.024 n=5+5)
Filter/tcp4_syn_out-8    27.6ns ±12%    24.6ns ± 1%     ~     (p=0.063 n=5+5)
Filter/udp4_in-8         71.5ns ± 5%    65.9ns ± 1%   -7.94%  (p=0.008 n=5+5)
Filter/udp4_out-8         132ns ±13%     119ns ± 1%  -10.29%  (p=0.008 n=5+5)
Filter/icmp6-8            169ns ±10%      54ns ± 1%  -68.35%  (p=0.008 n=5+5)
Filter/tcp6_syn_in-8      149ns ± 6%      43ns ± 1%  -71.11%  (p=0.008 n=5+5)
Filter/tcp6_syn_out-8    37.7ns ± 4%    24.3ns ± 3%  -35.51%  (p=0.008 n=5+5)
Filter/udp6_in-8          179ns ± 5%     103ns ± 1%  -42.75%  (p=0.008 n=5+5)
Filter/udp6_out-8         156ns ± 3%     191ns ± 1%  +22.54%  (p=0.008 n=5+5)

Signed-off-by: David Anderson <danderson@tailscale.com>
2020-11-12 21:41:21 -08:00
David Anderson
04ff3c91ee wgengine/filter: add full IPv6 support.
Part of #19.

Signed-off-by: David Anderson <danderson@tailscale.com>
2020-11-12 21:41:15 -08:00
Brad Fitzpatrick
fac2b30eff control/controlclient: diagnose zero bytes from control
Updates #921

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2020-11-12 14:45:08 -08:00
David Anderson
a664aac877 wgengine/router: disable IPv6 if v6 policy routing is unavailable.
Fixes #895.

Signed-off-by: David Anderson <danderson@tailscale.com>
2020-11-11 15:31:15 -08:00
Brad Fitzpatrick
a2d78b4d3e net/dnscache, control/controlclient: use DNS cache when dialing control
Cache DNS results of earlier login.tailscale.com control dials, and use
them for future dials if DNS is slow or broken.

Fixes various issues with trickier setups with the domain's DNS server
behind a subnet router.

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2020-11-11 12:50:04 -08:00
Brad Fitzpatrick
97e82c6cc0 net/netcheck: remove unused DNSCache from netcheck
It's easy to add back later if/when the TODO is implemented.
2020-11-11 11:52:35 -08:00
Brad Fitzpatrick
19b0cfe89e all: prepare for GOOS=ios in Go 1.16
Work with either way for now on iOS (darwin/arm64 vs ios/arm64).

In February when Go 1.16 comes out we'll have a universal binary for
darwin/arm64 (macOS) and will drop support for Go 1.15 and its
darwin/amd64 meaning iOS. (it'll mean macOS).

Context:

* https://tip.golang.org/doc/go1.16#darwin
* https://github.com/golang/go/issues/38485
* https://github.com/golang/go/issues/42100
2020-11-11 09:17:04 -08:00
Sean Klein
258b680bc5 Patch docker to use valid Go version
As documented in the README, tailscale only build with the latest Go
version (Go 1.15).  As a result, a handful of undefined errors would pop
up using an older verison.

This patch updates the base image to 1.15, allowing "docker build"
to function correctly once more.

Signed-off-by: Sean Klein <seanmarionklein@gmail.com>
2020-11-11 06:27:15 -08:00
Avery Pennarun
563d43b2a5 Merge remote-tracking branch 'origin/main' into HEAD
* origin/main:
  net/packet: documentation pass.
  net/packet: remove NewIP, offer only a netaddr constructor.
  net/packet: documentation cleanups.
  net/packet: fix panic on invalid IHL field.
  net/packet: remove {get,put}{16,32} indirection to encoding/binary.
  net/packet: support full IPv6 decoding.
  net/packet: add IPv6 source and destination IPs to Parsed.
2020-11-11 03:34:20 -05:00
Avery Pennarun
b246810377 .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 03:32:00 -05:00
Avery Pennarun
c03543dbe2 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 03:31:55 -05:00
Avery Pennarun
0050070493 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 03:31:40 -05:00
Avery Pennarun
f99f6608ff 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-11 03:30:36 -05:00
David Anderson
a38e28da07 net/packet: documentation pass.
Signed-off-by: David Anderson <danderson@tailscale.com>
2020-11-10 22:29:00 -08:00
David Anderson
c2cc3acbaf net/packet: remove NewIP, offer only a netaddr constructor.
Signed-off-by: David Anderson <danderson@tailscale.com>
2020-11-10 22:03:47 -08:00
David Anderson
d7ee3096dd net/packet: documentation cleanups.
Signed-off-by: David Anderson <danderson@tailscale.com>
2020-11-10 21:12:55 -08:00
David Anderson
9ef39af2f2 net/packet: fix panic on invalid IHL field.
Signed-off-by: David Anderson <danderson@tailscale.com>
2020-11-10 20:23:54 -08:00
David Anderson
22bf48f37c net/packet: remove {get,put}{16,32} indirection to encoding/binary.
name              old time/op    new time/op    delta
Decode/tcp4-8       28.8ns ± 2%    13.1ns ± 4%  -54.44%  (p=0.008 n=5+5)
Decode/tcp6-8       20.6ns ± 1%    12.6ns ± 2%  -38.72%  (p=0.008 n=5+5)
Decode/udp4-8       28.2ns ± 1%    12.1ns ± 4%  -57.01%  (p=0.008 n=5+5)
Decode/udp6-8       20.0ns ± 6%    12.1ns ± 2%  -39.38%  (p=0.008 n=5+5)
Decode/icmp4-8      21.7ns ± 2%    11.5ns ± 1%  -47.01%  (p=0.008 n=5+5)
Decode/icmp6-8      14.1ns ± 2%    11.8ns ± 4%  -16.60%  (p=0.008 n=5+5)
Decode/unknown-8    9.43ns ± 2%    9.30ns ± 3%     ~     (p=0.222 n=5+5)

Signed-off-by: David Anderson <danderson@tailscale.com>
2020-11-10 20:23:54 -08:00
David Anderson
55b1221db2 net/packet: support full IPv6 decoding.
The packet filter still rejects all IPv6, but decodes enough from v6
packets to do something smarter in a followup.

name              time/op
Decode/tcp4-8     28.8ns ± 2%
Decode/tcp6-8     20.6ns ± 1%
Decode/udp4-8     28.2ns ± 1%
Decode/udp6-8     20.0ns ± 6%
Decode/icmp4-8    21.7ns ± 2%
Decode/icmp6-8    14.1ns ± 2%
Decode/unknown-8  9.43ns ± 2%

Signed-off-by: David Anderson <danderson@tailscale.com>
2020-11-10 20:23:54 -08:00
David Anderson
89894c6930 net/packet: add IPv6 source and destination IPs to Parsed.
Signed-off-by: David Anderson <danderson@tailscale.com>
2020-11-10 20:23:54 -08:00
Brad Fitzpatrick
d192bd0f86 net/interfaces: ignore bogus proxy URLs from winhttp [windows]
Updates tailscale/corp#853
2020-11-10 11:30:18 -08:00
Brad Fitzpatrick
d21956436a 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>
2020-11-10 10:31:08 -08:00
Brad Fitzpatrick
450cfedeba 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.
2020-11-10 09:14:29 -08:00
chungdaniel
e7ac9a4b90 tsweb: refactor JSONHandler to take status code from error if it is present (#905)
This change is to make JSONHandler error handling intuitive, as before there would be two sources of HTTP status code when HTTPErrors were generated: one as the first return value of the handler function, and one nested inside the HTTPError. Previously, it took the first return value as the status code, and ignored the code inside the HTTPError. Now, it should expect the first return value to be 0 if there is an error, and it takes the status code of the HTTPError to set as the response code.

Signed-off-by: Daniel Chung <daniel@tailscale.com>
2020-11-10 09:52:26 -05:00
David Anderson
6e52633c53 net/packet: record allocations in benchmark. 2020-11-10 02:19:55 -08:00
David Anderson
093431f5dd net/packet: s/ParsedPacket/Parsed/ to avoid package stuttering.
Signed-off-by: David Anderson <danderson@tailscale.com>
2020-11-09 23:52:54 -08:00
David Anderson
c48253e63b wgengine/filter: add a method to run the packet filter without a packet.
The goal is to move some of the shenanigans we have elsewhere into the filter
package, so that all the weird things to do with poking at the filter is in
a single place, behind clean APIs.

Signed-off-by: David Anderson <danderson@tailscale.com>
2020-11-09 23:34:01 -08:00
David Anderson
7a54910990 wgengine/filter: remove helper vars, mark NewAllowAll test-only.
Signed-off-by: David Anderson <danderson@tailscale.com>
2020-11-09 22:02:37 -08:00
David Anderson
76d99cf01a wgengine/filter: remove the Matches type.
It only served to obscure the underlying slice type without
adding much value.

Signed-off-by: David Anderson <danderson@tailscale.com>
2020-11-09 21:39:25 -08:00
David Anderson
b950bd60bf wgengine/filter: add and clean up documentation.
Signed-off-by: David Anderson <danderson@tailscale.com>
2020-11-09 21:39:25 -08:00
David Anderson
a8589636a8 wgengine/filter: remove unused Clone methods.
Signed-off-by: David Anderson <danderson@tailscale.com>
2020-11-09 21:39:25 -08:00
David Anderson
b3634f020d wgengine/filter: use netaddr types in public API.
We still use the packet.* alloc-free types in the data path, but
the compilation from netaddr to packet happens within the filter
package.

Signed-off-by: David Anderson <danderson@tailscale.com>
2020-11-09 21:39:25 -08:00
David Anderson
7988f75b87 tailscaled.service: also cleanup prior to starting.
Fixes #813.

Signed-off-by: David Anderson <danderson@tailscale.com>
2020-11-09 20:16:11 -08:00
David Anderson
427bf2134f net/packet: rename from wgengine/packet.
Signed-off-by: David Anderson <danderson@tailscale.com>
2020-11-09 16:25:24 -08:00
David Anderson
19df6a2ee2 wgengine/packet: rename types to reflect their v4-only-ness, document.
Signed-off-by: David Anderson <danderson@tailscale.com>
2020-11-09 16:25:24 -08:00
David Anderson
ebd96bf4a9 wgengine/router/dns: use OpenKeyWait to set DNS configuration.
Fixes tailscale/corp#839.

Signed-off-by: David Anderson <danderson@tailscale.com>
2020-11-09 14:08:39 -08:00
David Crawshaw
e9bca0c00b 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:32:58 -05:00
Brad Fitzpatrick
b1de2020d7 version: bump date 2020-11-06 18:36:47 -08:00
Brad Fitzpatrick
b4e19b95ed ipn: debug zero bytes in IPN json messages
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2020-11-06 13:19:16 -08:00
Brad Fitzpatrick
8f30fa67aa ipn: treat zero-length file state store file as missing
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2020-11-06 12:58:52 -08:00
Adrian Dewhurst
3aa68cd397 version: relax git detection logic
git worktrees have a .git file rather than a .git directory, so building
in a worktree caused version.sh to generate an error.

Signed-off-by: Adrian Dewhurst <adrian@tailscale.com>
2020-11-06 15:55:21 -05:00
Brad Fitzpatrick
119101962c wgengine/router: don't double-prefix dns log messages [Windows] 2020-11-06 11:42:46 -08:00
Brad Fitzpatrick
bda53897b5 tailcfg: document FilterRule
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2020-11-05 12:15:17 -08:00
Brad Fitzpatrick
782e07c0ae control/controlclient: send warning flag in map request when IP forwarding off
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2020-11-04 14:46:05 -08:00
Brad Fitzpatrick
4f4e84236a ipn: clean up Prefs logging at start
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2020-11-04 11:39:57 -08:00
Brad Fitzpatrick
6bcb466096 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>
2020-11-04 11:39:57 -08:00
Brad Fitzpatrick
696e160cfc cmd/tailscale/cli: fix double tag: prefix in tailscale up
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2020-11-04 08:18:49 -08:00
Josh Bleecher Snyder
946c1edb42 tailcfg: improve error returned by Hostinfo.CheckRequestTags
That's what I get for pushing too fast.

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2020-11-03 16:19:20 -08:00
Josh Bleecher Snyder
fb9f80cd61 tailcfg: add Hostinfo.CheckRequestTags helper method
Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2020-11-03 16:10:23 -08:00
David Anderson
ed17f5ddae VERSION.txt: this is now 1.3.x. 2020-11-03 15:09:02 -08:00
David Anderson
39bbb86b09 build_dist: fix after version refactor. 2020-11-03 14:40:09 -08: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
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
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
David Crawshaw
dea3ef0597 tsweb: make JSONHandlerFunc implement ReturnHandler, not http.Handler
This way something is capable of logging errors on the server.

Fixes #766

Signed-off-by: David Crawshaw <crawshaw@tailscale.com>
2020-09-17 09:07:32 -04:00
Brad Fitzpatrick
3aeb2e204c go.mod: bump github.com/tailscale/winipcfg-go for some Windows fixes 2020-09-16 14:49:00 -07:00
Brad Fitzpatrick
acafe9811f wgengine/router: run netsh advfirewall less, rename, document setFirewall 2020-09-16 14:42:37 -07:00
Christina Wen
48fbe93e72 wgengine/magicsock: clarify pre-disco 'tailscale ping' error message
This change clarifies the error message when a user pings a peer that is using an outdated version of Tailscale.
2020-09-16 11:54:00 -04:00
Brad Fitzpatrick
96fd20e3c0 ipn: bail out a bit earlier when a peer doesn't have a DNS name
It's properly handled later in tsdns.NewMap anyway, but there's work
done in the meantime that can be skipped when a peer lacks a DNS name.
It's also more clear that it's okay for it to be blank.
2020-09-16 07:55:16 -07:00
Josh Bleecher Snyder
7f97cf654d cmd/microproxy: add -insecure flag
This makes it easier to run microproxy locally during development.

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2020-09-15 15:07:56 -07:00
Josh Bleecher Snyder
3fa863e6d9 cmd/derper: add missing html.EscapeString calls in /debug page
Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2020-09-15 15:00:19 -07:00
Christina Wen
e862f90e34 wgengine/router/router_linux.go: fixed " route del failed" error (#756)
* wgengine/router/router_linux.go: Switched `cidrDiff("addr")` and `cidrDiff("route")` order

Signed-off-by: Christina Wen <christina@tailscale.com>

Co-authored-by: Christina Wen <christina@tailscale.com>
2020-09-15 14:14:33 -04:00
Brad Fitzpatrick
761fe19e5f control/controlclient: don't accept /32 routes without --accept-routes
Fixes tailscale/corp#500

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2020-09-15 10:18:14 -07:00
Brad Fitzpatrick
88107b1287 control/controlclient: add TS_DEBUG_MAPRESPONSE to dump all MapResponses
I'm always adding this by hand during development. Time to check it in.
2020-09-15 09:54:52 -07:00
Brad Fitzpatrick
931bcd44cb control/controlclient: report Synology "distro" + its version to control 2020-09-15 08:32:06 -07:00
Brad Fitzpatrick
7e9d1f7808 wgengine/router: tighten isMissingIPv6Err
So it doesn't false positive if misused.

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2020-09-15 08:21:56 -07:00
David Anderson
8f5b52e571 net/netns: add windows support.
Also remove rebinding logic from the windows router. Magicsock will
instead rebind based on link change signals.

Signed-off-by: David Anderson <danderson@tailscale.com>
2020-09-14 16:28:49 -07:00
Josh Bleecher Snyder
3f4d93feb2 go.mod: bump depaware to get diffs out of -check, again
I had to use

go get -u github.com/tailscale/depaware@e09ee10c18249e4bf198e66bbd47babcd502637a

to force it to the correct version; it kept selecting head~1.

Maybe because the branch is called main instead of master?
Maybe because of some delay?
2020-09-14 16:25:09 -07:00
Josh Bleecher Snyder
a5d701095b wgengine/magicsock: increase test timeout to reduce flakiness
Updates #654. See that issue for a discussion of why
this timeout reduces flakiness, and what next steps are.

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2020-09-14 14:29:28 -07:00
Josh Bleecher Snyder
0c0239242c wgengine/magicsock: make discoPingPurpose a stringer
It was useful for debugging once, it'll probably be useful again.

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2020-09-14 14:29:28 -07:00
Josh Bleecher Snyder
6e38d29485 wgengine/magicsock: improve test logging output
This fixes line numbers and reduces timestamp
precision to overwhelming the output.

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2020-09-14 14:29:28 -07:00
Josh Bleecher Snyder
41f6c78c53 go.mod: bump depaware to get diffs out of -check 2020-09-14 13:25:28 -07:00
Brad Fitzpatrick
662c19551a control/controlclient: deal with localized 'Version' string getting Windows version 2020-09-13 10:06:20 -07:00
Brad Fitzpatrick
4f7751e025 Update depaware for previous ipnserver change. 2020-09-11 19:35:12 -07:00
Brad Fitzpatrick
4f71319f7c ipn/ipnserver: make ipnserver also be an HTTP server for localhost clients
For now it just says hello to show auth works. More later.
2020-09-11 15:11:28 -07:00
Brad Fitzpatrick
3af64765fd ipn: fix Windows crash from improperly strict assertion 2020-09-11 15:10:29 -07:00
Brad Fitzpatrick
a084c44afc wgengine, wgengine/router, cmd/tailscale: force netfilter mode off on Synology
For now. Get it working again so it's not stuck on 0.98.

Subnet relay can come later.

Updates #451

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2020-09-11 13:12:40 -07:00
Brad Fitzpatrick
31c13013ae wgengine/router: tolerate disabled IPv6 on Windows
Fixes #412
Updates #524 (maybe fixes?)

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2020-09-11 12:50:35 -07:00
Josh Bleecher Snyder
9ab2b32569 syncs: add Watch, for monitoring mutex contention
Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2020-09-11 11:36:07 -07:00
Brad Fitzpatrick
5a94317628 Makefile: remove tsshd from depaware
I'd removed it from the GitHub actions checks before submitted earlier
but forgot to update it here.
2020-09-11 11:09:32 -07:00
Josh Bleecher Snyder
37b40b035b wgengine/router/dns: appease staticcheck (again) 2020-09-11 11:02:57 -07:00
Brad Fitzpatrick
bc1751a376 util/pidowner: add missing copyright header 2020-09-11 08:57:10 -07:00
Brad Fitzpatrick
b14288f96c util/pidowner: add two missing copyright headers 2020-09-11 08:25:23 -07:00
Brad Fitzpatrick
23f01174ea util/pidowner: new package to map from process ID to its user ID 2020-09-11 08:19:21 -07:00
Brad Fitzpatrick
40e12c17ec net/netcheck: remove mistaken double Mutex.Unlock in error path
Thanks to @dotaheor for noticing.

Fixes #751
2020-09-11 07:55:49 -07:00
Brad Fitzpatrick
f65eb4e5c1 net/netstat: start of new netstat package, with Windows for now
This will be used in a future change to do localhost connection
authentication. This lets us quickly map a localhost TCP connection to
a PID. (A future change will then map a pid to a user)

TODO: pull portlist's netstat code into this package. Then portlist
will be fast on Windows without requiring shelling out to netstat.exe.
2020-09-10 15:24:49 -07:00
Brad Fitzpatrick
8b60936913 depaware: update deps 2020-09-10 09:29:15 -07:00
Brad Fitzpatrick
edb47b98a8 scripts: don't descend into .git for license header check 2020-09-10 09:23:33 -07:00
Josh Bleecher Snyder
a877dd575c wgengine/router/dns: remove unnecessary lint ignore line
Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2020-09-09 12:27:52 -07:00
Josh Bleecher Snyder
bf24d54143 syncs: add AssertLocked
This allows us to check lock invariants.

It was proposed upstream and rejected in:
https://github.com/golang/go/issues/1366

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2020-09-09 12:27:52 -07:00
Brad Fitzpatrick
158202dbb1 go mod tidy 2020-09-09 12:17:44 -07:00
Brad Fitzpatrick
7795fcf464 Add tooldeps package to keep depaware pinned in go.mod. 2020-09-09 12:13:30 -07:00
Brad Fitzpatrick
22ed3c503e Add depaware.txt files and GitHub checks. (#745)
See https://github.com/tailscale/depaware

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2020-09-09 12:11:46 -07:00
Josh Bleecher Snyder
2e40c4b564 tstest: don't log on success
This is particularly important for benchmarks,
where stray output disrupts tooling.

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2020-09-08 15:18:36 -07:00
Brad Fitzpatrick
913c1bd04f wgengine/router: on Windows, add a firewall rule to allow Tailscale
Updates tailscale/tailscale#454
2020-09-08 13:24:02 -07:00
Emmanuel T Odeke
688f923db1 log/logheap: properly document LogHeap as performing HTTP upload (#741)
LogHeap no longer logs to os.Stderr and instead uploads
the heap profile by means of an HTTP POST request to the
target URL endpoint.

While here, also ensured that an error from pprof.WriteHeapProfile
isn't ignored and will prevent the HTTP request from being made
if non-nil.

Signed-off-by: Emmanuel T Odeke <emmanuel@orijtech.com>
2020-09-07 19:17:53 -07:00
Josh Bleecher Snyder
96160973ce tailcfg: regenerate Clone methods
cmd/cloner has changed. Regenerate.

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2020-09-04 16:25:18 -07:00
Josh Bleecher Snyder
7bd89359c9 cmd/cloner: generate a package-level Clone function
This Clone function knows how to clone any types
for which it has generated Clone methods.
This allows callers to efficiently clone
an inbound interface{} that might contain one of these types.

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2020-09-04 16:25:18 -07:00
Josh Bleecher Snyder
99d223130c cmd/cloner: fix found type detection
This was causing any type to be reported as found,
as long as there were any type decls at all. Oops.

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2020-09-04 13:43:17 -07:00
Josh Bleecher Snyder
2352690bde cmd/cloner: ensure cloner gets re-run when structs change
If you change a struct and don't re-run cloner,
your Cloner method might be inaccurate, leading to bad things.

To prevent this, write out the struct as it is at the moment that
cloner is caller, and attempt a conversion from that type.
If the struct gets changed in any way, this conversion will fail.

This will yield false positives: If you change a non-pointer field,
you will be forced to re-run cloner, even though the actual generated
code won't change. I think this is an acceptable cost: It is a minor
annoyance, which will prevent real bugs.

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2020-09-04 13:11:21 -07:00
Brad Fitzpatrick
8ecee476f6 ipn: simplify TestLocalLogLines, defer a Shutdown of its LocalBackend
The test's LocalBackend was not shut down (Shutdown both releases
resources and waits for its various goroutines to end). This should
fix the test race we were seeing. It definitely fixes the file
descriptor leak that preventing -race -count=500 from passing before.
2020-09-04 08:36:07 -07:00
Brad Fitzpatrick
7fddc33481 tstest: make LogLineTracker pass variadic format args through
Omitting the "..." passed a literal []interface{} to the underlying
logger always.
2020-09-04 08:31:43 -07:00
Brad Fitzpatrick
68c42530e9 tstest: rename LogListener to LogLineTracker
But mostly to rename tstest.ListenFor which has no mention of log lines in it.
It sounded like a net.Listener or something.
2020-09-04 08:09:56 -07:00
David Crawshaw
95cddfcc75 tailcfg: add Clone methods to Login and DNSConfig
Signed-off-by: David Crawshaw <crawshaw@tailscale.com>
2020-09-04 07:48:45 -04:00
David Crawshaw
3baa084548 tstest: take testing.TB in helper for benchmarks
Signed-off-by: David Crawshaw <crawshaw@tailscale.com>
2020-09-04 07:48:45 -04:00
222 changed files with 12631 additions and 7364 deletions

28
.github/workflows/depaware.yml vendored Normal file
View File

@@ -0,0 +1,28 @@
name: depaware
on:
push:
branches:
- main
pull_request:
branches:
- '*'
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Set up Go
uses: actions/setup-go@v1
with:
go-version: 1.15
- name: Check out code
uses: actions/checkout@v1
- name: depaware tailscaled
run: go run github.com/tailscale/depaware --check tailscale.com/cmd/tailscaled
- name: depaware tailscale
run: go run github.com/tailscale/depaware --check tailscale.com/cmd/tailscale

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'

7
.gitignore vendored
View File

@@ -1,12 +1,11 @@
# Binaries for programs and plugins
*~
*.tmp
*.exe
*.dll
*.so
*.dylib
cmd/relaynode/relaynode
cmd/taillogin/taillogin
cmd/tailscale/tailscale
cmd/tailscaled/tailscaled
@@ -18,3 +17,7 @@ cmd/tailscaled/tailscaled
# Dependency directories (remove the comment below to include it)
# vendor/
# direnv config, this may be different for other people so it's probably safer
# to make this nonspecific.
.envrc

View File

@@ -21,7 +21,7 @@
# $ docker exec tailscaled tailscale status
FROM golang:1.14-alpine AS build-env
FROM golang:1.15-alpine AS build-env
WORKDIR /go/src/tailscale

View File

@@ -4,7 +4,15 @@ usage:
vet:
go vet ./...
check: staticcheck vet
updatedeps:
go run github.com/tailscale/depaware --update tailscale.com/cmd/tailscaled
go run github.com/tailscale/depaware --update tailscale.com/cmd/tailscale
depaware:
go run github.com/tailscale/depaware --check tailscale.com/cmd/tailscaled
go run github.com/tailscale/depaware --check tailscale.com/cmd/tailscale
check: staticcheck vet depaware
staticcheck:
go run honnef.co/go/tools/cmd/staticcheck -- $$(go list ./... | grep -v tempfork)

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.3.0

661
api.md Normal file
View File

@@ -0,0 +1,661 @@
# Tailscale API
The Tailscale API is a (mostly) RESTful API. Typically, POST bodies should be JSON encoded and responses will be JSON encoded.
# Authentication
Currently based on {some authentication method}. Visit the [admin panel](https://api.tailscale.com/admin) and navigate to the `Keys` page. Generate an API Key and keep it safe. Provide the key as the user key in basic auth when making calls to Tailscale API endpoints.
# APIS
## Device
<!-- TODO: description about what devices are -->
#### `GET /api/v2/device/:deviceid` - lists the details for a device
Returns the details for the specified device.
Supply the device of interest in the path using its ID.
Use the `fields` query parameter to explicitly indicate which fields are returned.
##### Parameters
##### Query Parameters
`fields` - Controls which fields will be included in the returned response.
Currently, supported options are:
* `all`: returns all fields in the response.
* `default`: return all fields except:
* `enabledRoutes`
* `advertisedRoutes`
* `clientConnectivity` (which contains the following fields: `mappingVariesByDestIP`, `derp`, `endpoints`, `latency`, and `clientSupports`)
Use commas to separate multiple options.
If more than one option is indicated, then the union is used.
For example, for `fields=default,all`, all fields are returned.
If the `fields` parameter is not provided, then the default option is used.
##### Example
```
GET /api/v2/device/12345
curl https://api.tailscale.com/api/v2/device/12345?fields=all \
-u "tskey-yourapikey123:"
```
Response
```
{
"addresses":[
"100.105.58.116"
],
"id":"12345",
"user":"user1@example.com",
"name":"user1-device.example.com",
"hostname":"User1-Device",
"clientVersion":"date.20201107",
"updateAvailable":false,
"os":"macOS",
"created":"2020-11-20T20:56:49Z",
"lastSeen":"2020-11-20T16:15:55-05:00",
"keyExpiryDisabled":false,
"expires":"2021-05-19T20:56:49Z",
"authorized":true,
"isExternal":false,
"machineKey":"mkey:user1-machine-key",
"nodeKey":"nodekey:user1-node-key",
"blocksIncomingConnections":false,
"enabledRoutes":[
],
"advertisedRoutes":[
],
"clientConnectivity": {
"endpoints":[
"209.195.87.231:59128",
"192.168.0.173:59128"
],
"derp":"",
"mappingVariesByDestIP":false,
"latency":{
"Dallas":{
"latencyMs":60.463043
},
"New York City":{
"preferred":true,
"latencyMs":31.323811
},
"San Francisco":{
"latencyMs":81.313389
}
},
"clientSupports":{
"hairPinning":false,
"ipv6":false,
"pcp":false,
"pmp":false,
"udp":true,
"upnp":false
}
}
}
```
#### `GET /api/v2/device/:deviceID/routes` - fetch subnet routes that are advertised and enabled for a device
Retrieves the list of subnet routes that a device is advertising, as well as those that are enabled for it. Enabled routes are not necessarily advertised (e.g. for pre-enabling), and likewise, advertised routes are not necessarily enabled.
##### Parameters
No parameters.
##### Example
```
curl https://api.tailscale.com/api/v2/device/11055/routes \
-u "tskey-yourapikey123:"
```
Response
```
{
"advertisedRoutes" : [
"10.0.1.0/24",
"1.2.0.0/16",
"2.0.0.0/24"
],
"enabledRoutes" : []
}
```
#### `POST /api/v2/device/:deviceID/routes` - set the subnet routes that are enabled for a device
Sets which subnet routes are enabled to be routed by a device by replacing the existing list of subnet routes with the supplied parameters. Routes can be enabled without a device advertising them (e.g. for preauth). Returns a list of enabled subnet routes and a list of advertised subnet routes for a device.
##### Parameters
###### POST Body
`routes` - The new list of enabled subnet routes in JSON.
```
{
"routes": ["10.0.1.0/24", "1.2.0.0/16", "2.0.0.0/24"]
}
```
##### Example
```
curl https://api.tailscale.com/api/v2/device/11055/routes \
-u "tskey-yourapikey123:" \
--data-binary '{"routes": ["10.0.1.0/24", "1.2.0.0/16", "2.0.0.0/24"]}'
```
Response
```
{
"advertisedRoutes" : [
"10.0.1.0/24",
"1.2.0.0/16",
"2.0.0.0/24"
],
"enabledRoutes" : [
"10.0.1.0/24",
"1.2.0.0/16",
"2.0.0.0/24"
]
}
```
## Domain
<!---
TODO: ctrl+f domain, replace with {workgroup/tailnet/other}
Domain is a top level resource. ACL is an example of a resource that is tied to a top level domain.
--->
### ACL
#### `GET /api/v2/domain/:domain/acl` - fetch ACL for a domain
Retrieves the ACL that is currently set for the given domain. Supply the domain of interest in the path. This endpoint can send back either the HuJSON of the ACL or a parsed JSON, depending on the `Accept` header.
##### Parameters
###### Headers
`Accept` - Response is parsed `JSON` if `application/json` is explicitly named, otherwise HuJSON will be returned.
##### Returns
Returns the ACL HuJSON by default. Returns a parsed JSON of the ACL (sans comments) if the `Accept` type is explicitly set to `application/json`. An `ETag` header is also sent in the response, which can be optionally used in POST requests to avoid missed updates.
<!-- TODO (chungdaniel): define error types and a set of docs for them -->
##### Example
###### Requesting a HuJSON response:
```
GET /api/v2/domain/example.com/acl
curl https://api.tailscale.com/api/v2/domain/example.com/acl \
-u "tskey-yourapikey123:" \
-H "Accept: application/hujson" \
-v
```
Response
```
...
Content-Type: application/hujson
Etag: "e0b2816b418b3f266309d94426ac7668ab3c1fa87798785bf82f1085cc2f6d9c"
...
// Example/default ACLs for unrestricted connections.
{
"Tests": [],
// Declare static groups of users beyond those in the identity service.
"Groups": {
"group:example": [
"user1@example.com",
"user2@example.com"
],
},
// Declare convenient hostname aliases to use in place of IP addresses.
"Hosts": {
"example-host-1": "100.100.100.100",
},
// Access control lists.
"ACLs": [
// Match absolutely everything. Comment out this section if you want
// to define specific ACL restrictions.
{
"Action": "accept",
"Users": [
"*"
],
"Ports": [
"*:*"
]
},
]
}
```
###### Requesting a JSON response:
```
GET /api/v2/domain/example.com/acl
curl https://api.tailscale.com/api/v2/domain/example.com/acl \
-u "tskey-yourapikey123:" \
-H "Accept: application/json" \
-v
```
Response
```
...
Content-Type: application/json
Etag: "e0b2816b418b3f266309d94426ac7668ab3c1fa87798785bf82f1085cc2f6d9c"
...
{
"acls" : [
{
"action" : "accept",
"ports" : [
"*:*"
],
"users" : [
"*"
]
}
],
"groups" : {
"group:example" : [
"user1@example.com",
"user2@example.com"
]
},
"hosts" : {
"example-host-1" : "100.100.100.100"
}
}
```
#### `POST /api/v2/domain/:domain/acl` - set ACL for a domain
Sets the ACL for the given domain. HuJSON and JSON are both accepted inputs. An `If-Match` header can be set to avoid missed updates.
Returns error for invalid ACLs.
Returns error if using an `If-Match` header and the ETag does not match.
##### Parameters
###### Headers
`If-Match` - A request header. Set this value to the ETag header provided in an `ACL GET` request to avoid missed updates.
`Accept` - Sets the return type of the updated ACL. Response is parsed `JSON` if `application/json` is explicitly named, otherwise HuJSON will be returned.
###### POST Body
ACL JSON or HuJSON (see https://tailscale.com/kb/1018/acls)
##### Example
```
POST /api/v2/domain/example.com/acl
curl https://api.tailscale.com/api/v2/domain/example.com/acl \
-u "tskey-yourapikey123:" \
-H "If-Match: \"e0b2816b418b3f266309d94426ac7668ab3c1fa87798785bf82f1085cc2f6d9c\""
--data-binary '// Example/default ACLs for unrestricted connections.
{
// Declare tests to check functionality of ACL rules. User must be a valid user with registered machines.
"Tests": [
// {"User": "user1@example.com", "Allow": ["example-host-1:22"], "Deny": ["example-host-2:100"]},
],
// Declare static groups of users beyond those in the identity service.
"Groups": {
"group:example": [ "user1@example.com", "user2@example.com" ],
},
// Declare convenient hostname aliases to use in place of IP addresses.
"Hosts": {
"example-host-1": "100.100.100.100",
},
// Access control lists.
"ACLs": [
// Match absolutely everything. Comment out this section if you want
// to define specific ACL restrictions.
{ "Action": "accept", "Users": ["*"], "Ports": ["*:*"] },
]
}'
```
Response
```
// Example/default ACLs for unrestricted connections.
{
// Declare tests to check functionality of ACL rules. User must be a valid user with registered machines.
"Tests": [
// {"User": "user1@example.com", "Allow": ["example-host-1:22"], "Deny": ["example-host-2:100"]},
],
// Declare static groups of users beyond those in the identity service.
"Groups": {
"group:example": [ "user1@example.com", "user2@example.com" ],
},
// Declare convenient hostname aliases to use in place of IP addresses.
"Hosts": {
"example-host-1": "100.100.100.100",
},
// Access control lists.
"ACLs": [
// Match absolutely everything. Comment out this section if you want
// to define specific ACL restrictions.
{ "Action": "accept", "Users": ["*"], "Ports": ["*:*"] },
]
}
```
#### `POST /api/v2/domain/:domain/acl/preview` - preview rule matches on an ACL for a resource
Determines what rules match for a user on an ACL without saving the ACL to the server.
##### Parameters
###### Query Parameters
`user` - A user's email. The provided ACL is queried with this user to determine which rules match.
###### POST Body
ACL JSON or HuJSON (see https://tailscale.com/kb/1018/acls)
##### Example
```
POST /api/v2/domain/example.com/acl/preiew
curl https://api.tailscale.com/api/v2/domain/example.com/acl?user=user1@example.com \
-u "tskey-yourapikey123:" \
--data-binary '// Example/default ACLs for unrestricted connections.
{
// Declare tests to check functionality of ACL rules. User must be a valid user with registered machines.
"Tests": [
// {"User": "user1@example.com", "Allow": ["example-host-1:22"], "Deny": ["example-host-2:100"]},
],
// Declare static groups of users beyond those in the identity service.
"Groups": {
"group:example": [ "user1@example.com", "user2@example.com" ],
},
// Declare convenient hostname aliases to use in place of IP addresses.
"Hosts": {
"example-host-1": "100.100.100.100",
},
// Access control lists.
"ACLs": [
// Match absolutely everything. Comment out this section if you want
// to define specific ACL restrictions.
{ "Action": "accept", "Users": ["*"], "Ports": ["*:*"] },
]
}'
```
Response
```
{"matches":[{"users":["*"],"ports":["*:*"],"lineNumber":19}],"user":"user1@example.com"}
```
### Devices
#### `GET /api/v2/domain/:domain/devices` - list the devices for a domain
Lists the devices for a domain.
Supply the domain of interest in the path.
Use the `fields` query parameter to explicitly indicate which fields are returned.
##### Parameters
###### Query Parameters
`fields` - Controls which fields will be included in the returned response.
Currently, supported options are:
* `all`: Returns all fields in the response.
* `default`: return all fields except:
* `enabledRoutes`
* `advertisedRoutes`
* `clientConnectivity` (which contains the following fields: `mappingVariesByDestIP`, `derp`, `endpoints`, `latency`, and `clientSupports`)
Use commas to separate multiple options.
If more than one option is indicated, then the union is used.
For example, for `fields=default,all`, all fields are returned.
If the `fields` parameter is not provided, then the default option is used.
##### Example
```
GET /api/v2/domain/example.com/devices
curl https://api.tailscale.com/api/v2/domain/example.com/devices \
-u "tskey-yourapikey123:"
```
Response
```
{
"devices":[
{
"addresses":[
"100.68.203.125"
],
"clientVersion":"date.20201107",
"os":"macOS",
"name":"user1-device.example.com",
"created":"2020-11-30T22:20:04Z",
"lastSeen":"2020-11-30T17:20:04-05:00",
"hostname":"User1-Device",
"machineKey":"mkey:user1-node-key",
"nodeKey":"nodekey:user1-node-key",
"id":"12345",
"user":"user1@example.com",
"expires":"2021-05-29T22:20:04Z",
"keyExpiryDisabled":false,
"authorized":false,
"isExternal":false,
"updateAvailable":false,
"blocksIncomingConnections":false,
},
{
"addresses":[
"100.111.63.90"
],
"clientVersion":"date.20201107",
"os":"macOS",
"name":"user2-device.example.com",
"created":"2020-11-30T22:21:03Z",
"lastSeen":"2020-11-30T17:21:03-05:00",
"hostname":"User2-Device",
"machineKey":"mkey:user2-machine-key",
"nodeKey":"nodekey:user2-node-key",
"id":"48810",
"user":"user2@example.com",
"expires":"2021-05-29T22:21:03Z",
"keyExpiryDisabled":false,
"authorized":false,
"isExternal":false,
"updateAvailable":false,
"blocksIncomingConnections":false,
}
]
}
```
### DNS
#### `GET /api/v2/domain/:domain/dns/nameservers` - list the DNS nameservers for a domain
Lists the DNS nameservers for a domain.
Supply the domain of interest in the path.
##### Parameters
No parameters.
##### Example
```
GET /api/v2/domain/example.com/dns/nameservers
curl https://api.tailscale.com/api/v2/domain/example.com/dns/nameservers \
-u "tskey-yourapikey123:"
```
Response
```
{
"dns": ["8.8.8.8"],
}
```
#### `POST /api/v2/domain/:domain/dns/nameservers` - replaces the list of DNS nameservers for a domain
Replaces the list of DNS nameservers for the given domain with the list supplied by the user.
Supply the domain of interest in the path.
Note that changing the list of DNS nameservers may also affect the status of MagicDNS (if MagicDNS is on).
##### Parameters
###### POST Body
`dns` - The new list of DNS nameservers in JSON.
```
{
"dns":["8.8.8.8"]
}
```
##### Returns
Returns the new list of nameservers and the status of MagicDNS.
If all nameservers have been removed, MagicDNS will be automatically disabled (until explicitly turned back on by the user).
##### Example
###### Adding DNS nameservers with the MagicDNS on:
```
POST /api/v2/domain/example.com/dns/nameservers
curl -X POST 'https://api.tailscale.com/api/v2/domain/example.com/dns/nameservers' \
-u "tskey-yourapikey123:" \
--data-binary '{"dns": ["8.8.8.8"]}'
```
Response:
```
{
"dns":["8.8.8.8"],
"magicDNS":true,
}
```
###### Removing all DNS nameservers with the MagicDNS on:
```
POST /api/v2/domain/example.com/dns/nameservers
curl -X POST 'https://api.tailscale.com/api/v2/domain/example.com/dns/nameservers' \
-u "tskey-yourapikey123:" \
--data-binary '{"dns": []}'
```
Response:
```
{
"dns":[],
"magicDNS": false,
}
```
#### `GET /api/v2/domain/:domain/dns/preferences` - retrieves the DNS preferences for a domain
Retrieves the DNS preferences that are currently set for the given domain.
Supply the domain of interest in the path.
##### Parameters
No parameters.
##### Example
```
GET /api/v2/domain/example.com/dns/preferences
curl 'https://api.tailscale.com/api/v2/domain/example.com/dns/preferences' \
-u "tskey-yourapikey123:"
```
Response:
```
{
"magicDNS":false,
}
```
#### `POST /api/v2/domain/:domain/dns/preferences` - replaces the DNS preferences for a domain
Replaces the DNS preferences for a domain, specifically, the MagicDNS setting.
Note that MagicDNS is dependent on DNS servers.
If there is at least one DNS server, then MagicDNS can be enabled.
Otherwise, it returns an error.
Note that removing all nameservers will turn off MagicDNS.
To reenable it, nameservers must be added back, and MagicDNS must be explicity turned on.
##### Parameters
###### POST Body
The DNS preferences in JSON. Currently, MagicDNS is the only setting available.
`magicDNS` - Automatically registers DNS names for devices in your network.
```
{
"magicDNS": true
}
```
##### Example
```
POST /api/v2/domain/example.com/dns/preferences
curl -X POST 'https://api.tailscale.com/api/v2/domain/example.com/dns/preferences' \
-u "tskey-yourapikey123:" \
--data-binary '{"magicDNS": true}'
```
Response:
If there are no DNS servers, it returns an error message:
```
{
"message":"need at least one nameserver to enable MagicDNS"
}
```
If there are DNS servers:
```
{
"magicDNS":true,
}
```
#### `GET /api/v2/domain/:domain/dns/searchpaths` - retrieves the search paths for a domain
Retrieves the list of search paths that is currently set for the given domain.
Supply the domain of interest in the path.
##### Parameters
No parameters.
##### Example
```
GET /api/v2/domain/example.com/dns/searchpaths
curl 'https://api.tailscale.com/api/v2/domain/example.com/dns/searchpaths' \
-u "tskey-yourapikey123:"
```
Response:
```
{
"searchPaths": ["user1.example.com"],
}
```
#### `POST /api/v2/domain/:domain/dns/searchpaths` - replaces the search paths for a domain
Replaces the list of search paths with the list supplied by the user and returns an error otherwise.
##### Parameters
###### POST Body
`searchPaths` - A list of searchpaths in JSON format.
```
{
"searchPaths: ["user1.example.com", "user2.example.com"]
}
```
##### Example
```
POST /api/v2/domain/example.com/dns/searchpaths
curl -X POST 'https://api.tailscale.com/api/v2/domain/example.com/dns/searchpaths' \
-u "tskey-yourapikey123:" \
--data-binary '{"searchPaths": ["user1.example.com", "user2.example.com"]}'
```
Response:
```
{
"searchPaths": ["user1.example.com", "user2.example.com"],
}
```

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() {
@@ -86,8 +87,8 @@ func main() {
}
pkg := typeNameObj.Pkg()
gen(buf, imports, typeName, typ, pkg)
found = true
}
found = true
}
}
if !found {
@@ -95,6 +96,31 @@ func main() {
}
}
w := func(format string, args ...interface{}) {
fmt.Fprintf(buf, format+"\n", args...)
}
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("}")
}
contents := new(bytes.Buffer)
fmt.Fprintf(contents, header, *flagTypes, pkg.Name)
fmt.Fprintf(contents, "import (\n")
@@ -143,7 +169,19 @@ func gen(buf *bytes.Buffer, imports map[string]struct{}, name string, typ *types
switch t := typ.Underlying().(type) {
case *types.Struct:
_ = t
// We generate two bits of code simultaneously while we walk the struct.
// One is the Clone method itself, which we write directly to buf.
// The other is a variable assignment that will fail if the struct
// changes without the Clone method getting regenerated.
// We write that to regenBuf, and then append it to buf at the end.
regenBuf := new(bytes.Buffer)
writeRegen := func(format string, args ...interface{}) {
fmt.Fprintf(regenBuf, format+"\n", args...)
}
writeRegen("// A compilation failure here means this code must be regenerated, with command:")
writeRegen("// tailscale.com/cmd/cloner -type %s", *flagTypes)
writeRegen("var _%sNeedsRegeneration = %s(struct {", name, name)
name := typ.Obj().Name()
fmt.Fprintf(buf, "// Clone makes a deep copy of %s.\n", name)
fmt.Fprintf(buf, "// The result aliases no memory with the original.\n")
@@ -159,6 +197,9 @@ func gen(buf *bytes.Buffer, imports map[string]struct{}, name string, typ *types
for i := 0; i < t.NumFields(); i++ {
fname := t.Field(i).Name()
ft := t.Field(i).Type()
writeRegen("\t%s %s", fname, importedName(ft))
if !containsPointers(ft) {
continue
}
@@ -220,6 +261,10 @@ func gen(buf *bytes.Buffer, imports map[string]struct{}, name string, typ *types
}
writef("return dst")
fmt.Fprintf(buf, "}\n\n")
writeRegen("}{})\n")
buf.Write(regenBuf.Bytes())
}
}

View File

@@ -13,6 +13,7 @@ import (
"expvar"
"flag"
"fmt"
"html"
"io"
"io/ioutil"
"log"
@@ -24,7 +25,6 @@ import (
"strings"
"time"
"github.com/tailscale/wireguard-go/wgcfg"
"golang.org/x/crypto/acme/autocert"
"tailscale.com/atomicfile"
"tailscale.com/derp"
@@ -34,6 +34,7 @@ import (
"tailscale.com/net/stun"
"tailscale.com/tsweb"
"tailscale.com/types/key"
"tailscale.com/types/wgkey"
"tailscale.com/version"
)
@@ -50,7 +51,7 @@ var (
)
type config struct {
PrivateKey wgcfg.PrivateKey
PrivateKey wgkey.Private
}
func loadConfig() config {
@@ -62,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)
@@ -76,8 +77,8 @@ func loadConfig() config {
}
}
func mustNewKey() wgcfg.PrivateKey {
key, err := wgcfg.NewPrivateKey()
func mustNewKey() wgkey.Private {
key, err := wgkey.NewPrivate()
if err != nil {
log.Fatal(err)
}
@@ -229,10 +230,10 @@ func debugHandler(s *derp.Server) http.Handler {
<h1>DERP debug</h1>
<ul>
`)
f("<li><b>Hostname:</b> %v</li>\n", *hostname)
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", 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

@@ -34,6 +34,7 @@ var (
logCollection = flag.String("logcollection", "", "If non-empty, logtail collection to log to")
nodeExporter = flag.String("node-exporter", "http://localhost:9100", "URL of the local prometheus node exporter")
goVarsURL = flag.String("go-vars-url", "http://localhost:8383/debug/vars", "URL of a local Go server's /debug/vars endpoint")
insecure = flag.Bool("insecure", false, "serve over http, for development")
)
func main() {
@@ -66,12 +67,15 @@ func main() {
httpsrv := &http.Server{
Addr: *addr,
Handler: mux,
TLSConfig: &tls.Config{
GetCertificate: ch.GetCertificate,
},
}
if err := httpsrv.ListenAndServeTLS("", ""); err != nil && err != http.ErrServerClosed {
if !*insecure {
httpsrv.TLSConfig = &tls.Config{GetCertificate: ch.GetCertificate}
err = httpsrv.ListenAndServeTLS("", "")
} else {
err = httpsrv.ListenAndServe()
}
if err != nil && err != http.ErrServerClosed {
log.Fatal(err)
}
}

View File

@@ -99,7 +99,7 @@ func connect(ctx context.Context) (net.Conn, *ipn.BackendClient, context.Context
if runtime.GOOS != "windows" && rootArgs.socket == "" {
fatalf("--socket cannot be empty")
}
fatalf("Failed to connect to connect to tailscaled. (safesocket.Connect: %v)\n", err)
fatalf("Failed to connect to tailscaled. (safesocket.Connect: %v)\n", err)
}
clientToServer := func(b []byte) {
ipn.WriteMsg(c, b)

View File

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

View File

@@ -17,7 +17,6 @@ import (
"github.com/peterbourgon/ff/v2/ffcli"
"tailscale.com/derp/derpmap"
"tailscale.com/net/dnscache"
"tailscale.com/net/netcheck"
"tailscale.com/tailcfg"
"tailscale.com/types/logger"
@@ -44,9 +43,7 @@ var netcheckArgs struct {
}
func runNetcheck(ctx context.Context, args []string) error {
c := &netcheck.Client{
DNSCache: dnscache.Get(),
}
c := &netcheck.Client{}
if netcheckArgs.verbose {
c.Logf = logger.WithPrefix(log.Printf, "netcheck: ")
c.Verbose = true

View File

@@ -169,6 +169,9 @@ func runStatus(ctx context.Context, args []string) error {
}
for _, peer := range st.Peers() {
ps := st.Peer[peer]
if ps.ShareeNode {
continue
}
active := peerActive(ps)
if statusArgs.active && !active {
continue

View File

@@ -7,6 +7,7 @@ package cli
import (
"bytes"
"context"
"errors"
"flag"
"fmt"
"log"
@@ -18,23 +19,14 @@ import (
"sync"
"github.com/peterbourgon/ff/v2/ffcli"
"github.com/tailscale/wireguard-go/wgcfg"
"inet.af/netaddr"
"tailscale.com/ipn"
"tailscale.com/tailcfg"
"tailscale.com/version"
"tailscale.com/version/distro"
"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]",
@@ -58,19 +50,25 @@ 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)")
}
if runtime.GOOS == "linux" {
upf.BoolVar(&upArgs.snat, "snat-subnet-routes", true, "source NAT traffic to local routes advertised with -advertise-routes")
upf.StringVar(&upArgs.netfilterMode, "netfilter-mode", "on", "netfilter mode (one of on, nodivert, off)")
upf.BoolVar(&upArgs.snat, "snat-subnet-routes", true, "source NAT traffic to local routes advertised with --advertise-routes")
upf.StringVar(&upArgs.netfilterMode, "netfilter-mode", defaultNetfilterMode(), "netfilter mode (one of on, nodivert, off)")
}
return upf
})(),
Exec: runUp,
}
func defaultNetfilterMode() string {
if distro.Get() == distro.Synology {
return "off"
}
return "on"
}
var upArgs struct {
server string
acceptRoutes bool
@@ -80,36 +78,12 @@ var upArgs struct {
forceReauth bool
advertiseRoutes string
advertiseTags string
enableDERP bool
snat bool
netfilterMode string
authKey string
hostname string
}
// parseIPOrCIDR parses an IP address or a CIDR prefix. If the input
// is an IP address, it is returned in CIDR form with a /32 mask for
// IPv4 or a /128 mask for IPv6.
func parseIPOrCIDR(s string) (wgcfg.CIDR, bool) {
if strings.Contains(s, "/") {
ret, err := wgcfg.ParseCIDR(s)
if err != nil {
return wgcfg.CIDR{}, false
}
return ret, true
}
ip, ok := wgcfg.ParseIP(s)
if !ok {
return wgcfg.CIDR{}, false
}
if ip.Is4() {
return wgcfg.CIDR{IP: ip, Mask: 32}, true
} else {
return wgcfg.CIDR{IP: ip, Mask: 128}, true
}
}
func isBSD(s string) bool {
return s == "dragonfly" || s == "freebsd" || s == "netbsd" || s == "openbsd"
}
@@ -151,19 +125,31 @@ func runUp(ctx context.Context, args []string) error {
log.Fatalf("too many non-flag arguments: %q", args)
}
var routes []wgcfg.CIDR
if distro.Get() == distro.Synology {
notSupported := "not yet supported on Synology; see https://github.com/tailscale/tailscale/issues/451"
if upArgs.advertiseRoutes != "" {
return errors.New("--advertise-routes is " + notSupported)
}
if upArgs.acceptRoutes {
return errors.New("--accept-routes is " + notSupported)
}
if upArgs.netfilterMode != "off" {
return errors.New("--netfilter-mode values besides \"off\" " + notSupported)
}
}
var routes []netaddr.IPPrefix
if upArgs.advertiseRoutes != "" {
advroutes := strings.Split(upArgs.advertiseRoutes, ",")
for _, s := range advroutes {
cidr, ok := parseIPOrCIDR(s)
ipp, err := netaddr.ParseIPPrefix(s) // parse it with other pawith both packages
if !ok || err != nil {
ipp, err := netaddr.ParseIPPrefix(s)
if err != nil {
fatalf("%q is not a valid IP address or CIDR prefix", s)
}
if ipp != ipp.Masked() {
fatalf("%s has non-address bits set; expected %s", ipp, ipp.Masked())
}
routes = append(routes, cidr)
routes = append(routes, ipp)
}
checkIPForwarding()
}
@@ -194,8 +180,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":
@@ -219,8 +206,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 {
@@ -248,6 +236,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()

212
cmd/tailscale/depaware.txt Normal file
View File

@@ -0,0 +1,212 @@
tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/depaware)
W 💣 github.com/alexbrainman/sspi from github.com/alexbrainman/sspi/negotiate
W 💣 github.com/alexbrainman/sspi/negotiate from tailscale.com/net/tshttpproxy
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
github.com/golang/groupcache/lru from tailscale.com/wgengine/filter+
L 💣 github.com/jsimonetti/rtnetlink from tailscale.com/wgengine/monitor
L github.com/jsimonetti/rtnetlink/internal/unix from github.com/jsimonetti/rtnetlink
L 💣 github.com/mdlayher/netlink from github.com/jsimonetti/rtnetlink+
L 💣 github.com/mdlayher/netlink/nlenc from github.com/jsimonetti/rtnetlink+
L github.com/mdlayher/sdnotify from tailscale.com/util/systemd
github.com/peterbourgon/ff/v2 from github.com/peterbourgon/ff/v2/ffcli
github.com/peterbourgon/ff/v2/ffcli from tailscale.com/cmd/tailscale/cli
💣 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
💣 github.com/tailscale/wireguard-go/ipc from github.com/tailscale/wireguard-go/device
W 💣 github.com/tailscale/wireguard-go/ipc/winpipe from github.com/tailscale/wireguard-go/ipc
github.com/tailscale/wireguard-go/ratelimiter from github.com/tailscale/wireguard-go/device
github.com/tailscale/wireguard-go/replay from github.com/tailscale/wireguard-go/device
github.com/tailscale/wireguard-go/rwcancel from github.com/tailscale/wireguard-go/device+
github.com/tailscale/wireguard-go/tai64n from github.com/tailscale/wireguard-go/device
💣 github.com/tailscale/wireguard-go/tun from github.com/tailscale/wireguard-go/device+
W 💣 github.com/tailscale/wireguard-go/tun/wintun from github.com/tailscale/wireguard-go/tun
github.com/tailscale/wireguard-go/wgcfg from github.com/tailscale/wireguard-go/conn+
github.com/tcnksm/go-httpstat from tailscale.com/net/netcheck
github.com/toqueteos/webbrowser from tailscale.com/cmd/tailscale/cli
💣 go4.org/intern from inet.af/netaddr
💣 go4.org/mem from tailscale.com/control/controlclient+
go4.org/unsafe/assume-no-moving-gc from go4.org/intern
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+
tailscale.com/cmd/tailscale/cli from tailscale.com/cmd/tailscale
tailscale.com/control/controlclient from tailscale.com/ipn+
tailscale.com/derp from tailscale.com/derp/derphttp+
tailscale.com/derp/derphttp from tailscale.com/cmd/tailscale/cli+
tailscale.com/derp/derpmap from tailscale.com/cmd/tailscale/cli
tailscale.com/disco from tailscale.com/derp+
tailscale.com/internal/deepprint from tailscale.com/ipn+
tailscale.com/ipn from tailscale.com/cmd/tailscale/cli
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/metrics from tailscale.com/derp
tailscale.com/net/dnscache from tailscale.com/control/controlclient+
💣 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/packet from tailscale.com/wgengine+
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+
💣 tailscale.com/net/tshttpproxy from tailscale.com/cmd/tailscale/cli+
tailscale.com/paths from tailscale.com/cmd/tailscale/cli
tailscale.com/portlist from tailscale.com/ipn
tailscale.com/safesocket from tailscale.com/cmd/tailscale/cli
💣 tailscale.com/syncs from tailscale.com/net/interfaces+
tailscale.com/tailcfg from tailscale.com/cmd/tailscale/cli+
W tailscale.com/tsconst from tailscale.com/net/interfaces
tailscale.com/types/empty from tailscale.com/control/controlclient+
tailscale.com/types/key from tailscale.com/cmd/tailscale/cli+
tailscale.com/types/logger from tailscale.com/cmd/tailscale/cli+
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/net/packet
tailscale.com/types/structs from tailscale.com/control/controlclient+
tailscale.com/types/wgkey from tailscale.com/control/controlclient+
LW tailscale.com/util/endian from tailscale.com/net/netns+
tailscale.com/util/lineread from tailscale.com/control/controlclient+
tailscale.com/util/systemd from tailscale.com/control/controlclient+
tailscale.com/version from tailscale.com/cmd/tailscale/cli+
tailscale.com/version/distro from tailscale.com/cmd/tailscale/cli+
tailscale.com/wgengine from tailscale.com/ipn
tailscale.com/wgengine/filter from tailscale.com/control/controlclient+
tailscale.com/wgengine/magicsock from tailscale.com/wgengine
💣 tailscale.com/wgengine/monitor from tailscale.com/cmd/tailscale/cli+
tailscale.com/wgengine/router from tailscale.com/cmd/tailscale/cli+
tailscale.com/wgengine/router/dns from tailscale.com/ipn+
tailscale.com/wgengine/tsdns from tailscale.com/ipn+
tailscale.com/wgengine/tstun from tailscale.com/wgengine
W 💣 tailscale.com/wgengine/winnet from tailscale.com/wgengine/router
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 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 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
golang.org/x/sync/singleflight from tailscale.com/net/dnscache
golang.org/x/sys/cpu from golang.org/x/crypto/blake2b+
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 golang.zx2c4.com/wireguard/windows/tunnel/winipcfg+
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 golang.org/x/net/idna
golang.org/x/time/rate from tailscale.com/types/logger+
bufio from compress/flate+
bytes from bufio+
compress/flate from compress/gzip+
compress/gzip from net/http+
compress/zlib from debug/elf+
container/list from crypto/tls+
context from crypto/tls+
crypto from crypto/ecdsa+
crypto/aes from crypto/ecdsa+
crypto/cipher from crypto/aes+
crypto/des from crypto/tls+
crypto/dsa from crypto/x509
crypto/ecdsa from crypto/tls+
crypto/ed25519 from crypto/tls+
crypto/elliptic from crypto/ecdsa+
crypto/hmac from crypto/tls+
crypto/md5 from crypto/tls+
crypto/rand from crypto/ed25519+
crypto/rc4 from crypto/tls
crypto/rsa from crypto/tls+
crypto/sha1 from crypto/tls+
crypto/sha256 from crypto/tls+
crypto/sha512 from crypto/ecdsa+
crypto/subtle from crypto/aes+
crypto/tls from github.com/tcnksm/go-httpstat+
crypto/x509 from crypto/tls+
crypto/x509/pkix from crypto/x509+
debug/dwarf from debug/elf+
debug/elf from rsc.io/goversion/version
debug/macho from rsc.io/goversion/version
debug/pe from rsc.io/goversion/version
encoding from encoding/json+
encoding/asn1 from crypto/x509+
encoding/base64 from encoding/json+
encoding/binary from compress/gzip+
encoding/hex from crypto/x509+
encoding/json from expvar+
encoding/pem from crypto/tls+
errors from bufio+
expvar from tailscale.com/derp+
flag from github.com/peterbourgon/ff/v2+
fmt from compress/flate+
hash from compress/zlib+
hash/adler32 from compress/zlib
hash/crc32 from compress/gzip+
hash/fnv from tailscale.com/wgengine/magicsock
hash/maphash from go4.org/mem
html from tailscale.com/ipn/ipnstate
io from bufio+
io/ioutil from crypto/tls+
log from expvar+
math from compress/flate+
math/big from crypto/dsa+
math/bits from compress/flate+
math/rand from github.com/mdlayher/netlink+
mime from golang.org/x/oauth2/internal+
mime/multipart from net/http
mime/quotedprintable from mime/multipart
net from crypto/tls+
net/http from expvar+
net/http/httptrace from github.com/tcnksm/go-httpstat+
net/http/internal from net/http
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+
os/signal from tailscale.com/cmd/tailscale/cli
L os/user from github.com/godbus/dbus/v5
path from debug/dwarf+
path/filepath from crypto/x509+
reflect from crypto/x509+
regexp from github.com/coreos/go-iptables/iptables+
regexp/syntax from regexp
runtime/debug from golang.org/x/sync/singleflight
runtime/pprof from tailscale.com/log/logheap+
sort from compress/flate+
strconv from compress/flate+
strings from bufio+
sync from compress/flate+
sync/atomic from context+
syscall from crypto/rand+
text/tabwriter from github.com/peterbourgon/ff/v2/ffcli+
time from compress/gzip+
unicode from bytes+
unicode/utf16 from encoding/asn1+
unicode/utf8 from bufio+

228
cmd/tailscaled/depaware.txt Normal file
View File

@@ -0,0 +1,228 @@
tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/depaware)
W 💣 github.com/alexbrainman/sspi from github.com/alexbrainman/sspi/negotiate
W 💣 github.com/alexbrainman/sspi/negotiate from tailscale.com/net/tshttpproxy
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
github.com/golang/groupcache/lru from tailscale.com/wgengine/filter+
L 💣 github.com/jsimonetti/rtnetlink from tailscale.com/wgengine/monitor
L github.com/jsimonetti/rtnetlink/internal/unix from github.com/jsimonetti/rtnetlink
github.com/klauspost/compress/fse from github.com/klauspost/compress/huff0
github.com/klauspost/compress/huff0 from github.com/klauspost/compress/zstd
github.com/klauspost/compress/snappy from github.com/klauspost/compress/zstd
github.com/klauspost/compress/zstd from tailscale.com/smallzstd
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+
L github.com/mdlayher/sdnotify from tailscale.com/util/systemd
💣 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
💣 github.com/tailscale/wireguard-go/ipc from github.com/tailscale/wireguard-go/device
W 💣 github.com/tailscale/wireguard-go/ipc/winpipe from github.com/tailscale/wireguard-go/ipc
github.com/tailscale/wireguard-go/ratelimiter from github.com/tailscale/wireguard-go/device
github.com/tailscale/wireguard-go/replay from github.com/tailscale/wireguard-go/device
github.com/tailscale/wireguard-go/rwcancel from github.com/tailscale/wireguard-go/device+
github.com/tailscale/wireguard-go/tai64n from github.com/tailscale/wireguard-go/device
💣 github.com/tailscale/wireguard-go/tun from github.com/tailscale/wireguard-go/device+
W 💣 github.com/tailscale/wireguard-go/tun/wintun from github.com/tailscale/wireguard-go/tun
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/intern from inet.af/netaddr
💣 go4.org/mem from tailscale.com/control/controlclient+
go4.org/unsafe/assume-no-moving-gc from go4.org/intern
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+
tailscale.com/control/controlclient from tailscale.com/ipn+
tailscale.com/derp from tailscale.com/derp/derphttp+
tailscale.com/derp/derphttp from tailscale.com/net/netcheck+
tailscale.com/disco from tailscale.com/derp+
tailscale.com/internal/deepprint from tailscale.com/ipn+
tailscale.com/ipn from tailscale.com/ipn/ipnserver
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
tailscale.com/logtail/backoff from tailscale.com/control/controlclient+
tailscale.com/logtail/filch from tailscale.com/logpolicy
tailscale.com/metrics from tailscale.com/derp
tailscale.com/net/dnscache from tailscale.com/control/controlclient+
💣 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/netstat from tailscale.com/ipn/ipnserver
tailscale.com/net/packet from tailscale.com/wgengine+
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+
💣 tailscale.com/net/tshttpproxy from tailscale.com/control/controlclient+
tailscale.com/paths from tailscale.com/cmd/tailscaled+
tailscale.com/portlist from tailscale.com/ipn
tailscale.com/safesocket from tailscale.com/ipn/ipnserver
tailscale.com/smallzstd from tailscale.com/ipn/ipnserver+
💣 tailscale.com/syncs from tailscale.com/net/interfaces+
tailscale.com/tailcfg from tailscale.com/control/controlclient+
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/net/packet
tailscale.com/types/structs from tailscale.com/control/controlclient+
tailscale.com/types/wgkey from tailscale.com/control/controlclient+
LW 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/util/racebuild from tailscale.com/logpolicy
tailscale.com/util/systemd 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+
tailscale.com/wgengine/magicsock from tailscale.com/cmd/tailscaled+
💣 tailscale.com/wgengine/monitor from tailscale.com/wgengine
tailscale.com/wgengine/router from tailscale.com/cmd/tailscaled+
tailscale.com/wgengine/router/dns from tailscale.com/ipn+
tailscale.com/wgengine/tsdns from tailscale.com/ipn+
tailscale.com/wgengine/tstun from tailscale.com/wgengine
W 💣 tailscale.com/wgengine/winnet from tailscale.com/wgengine/router
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 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 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
golang.org/x/sync/singleflight from tailscale.com/net/dnscache
golang.org/x/sys/cpu from golang.org/x/crypto/blake2b+
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 golang.zx2c4.com/wireguard/windows/tunnel/winipcfg+
golang.org/x/term from tailscale.com/logpolicy
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 golang.org/x/net/idna
golang.org/x/time/rate from tailscale.com/types/logger+
bufio from compress/flate+
bytes from bufio+
compress/flate from compress/gzip+
compress/gzip from internal/profile+
compress/zlib from debug/elf+
container/list from crypto/tls+
context from crypto/tls+
crypto from crypto/ecdsa+
crypto/aes from crypto/ecdsa+
crypto/cipher from crypto/aes+
crypto/des from crypto/tls+
crypto/dsa from crypto/x509
crypto/ecdsa from crypto/tls+
crypto/ed25519 from crypto/tls+
crypto/elliptic from crypto/ecdsa+
crypto/hmac from crypto/tls+
crypto/md5 from crypto/tls+
crypto/rand from crypto/ed25519+
crypto/rc4 from crypto/tls
crypto/rsa from crypto/tls+
crypto/sha1 from crypto/tls+
crypto/sha256 from crypto/tls+
crypto/sha512 from crypto/ecdsa+
crypto/subtle from crypto/aes+
crypto/tls from github.com/tcnksm/go-httpstat+
crypto/x509 from crypto/tls+
crypto/x509/pkix from crypto/x509+
debug/dwarf from debug/elf+
debug/elf from rsc.io/goversion/version
debug/macho from rsc.io/goversion/version
debug/pe from rsc.io/goversion/version
encoding from encoding/json+
encoding/asn1 from crypto/x509+
encoding/base64 from encoding/json+
encoding/binary from compress/gzip+
encoding/hex from crypto/x509+
encoding/json from expvar+
encoding/pem from crypto/tls+
errors from bufio+
expvar from tailscale.com/derp+
flag from tailscale.com/cmd/tailscaled+
fmt from compress/flate+
hash from compress/zlib+
hash/adler32 from compress/zlib
hash/crc32 from compress/gzip+
hash/fnv from tailscale.com/wgengine/magicsock
hash/maphash from go4.org/mem
html from html/template+
html/template from net/http/pprof
io from bufio+
io/ioutil from crypto/tls+
log from expvar+
math from compress/flate+
math/big from crypto/dsa+
math/bits from compress/flate+
math/rand from github.com/mdlayher/netlink+
mime from golang.org/x/oauth2/internal+
mime/multipart from net/http
mime/quotedprintable from mime/multipart
net from crypto/tls+
net/http from expvar+
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 golang.org/x/net/http/httpguts+
net/url from crypto/x509+
os from crypto/rand+
os/exec from github.com/coreos/go-iptables/iptables+
os/signal from tailscale.com/cmd/tailscaled+
os/user from github.com/godbus/dbus/v5+
path from debug/dwarf+
path/filepath from crypto/x509+
reflect from crypto/x509+
regexp from github.com/coreos/go-iptables/iptables+
regexp/syntax from regexp
runtime/debug from github.com/klauspost/compress/zstd+
runtime/pprof from net/http/pprof+
runtime/trace from net/http/pprof
sort from compress/flate+
strconv from compress/flate+
strings from bufio+
sync from compress/flate+
sync/atomic from context+
syscall from crypto/rand+
text/tabwriter from runtime/pprof
text/template from html/template
text/template/parse from html/template+
time from compress/gzip+
unicode from bytes+
unicode/utf16 from encoding/asn1+
unicode/utf8 from bufio+

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"
@@ -60,6 +64,7 @@ var args struct {
port uint16
statepath string
socketpath string
verbose int
}
func main() {
@@ -71,28 +76,30 @@ 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.IntVar(&args.verbose, "verbose", 0, "log verbosity level; 0 is default, 1 or higher are increasingly verbose")
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 == "" {
@@ -113,6 +120,7 @@ func run() error {
var err error
pol := logpolicy.New("tailnode.log.tailscale.io")
pol.SetVerbosityLevel(args.verbose)
defer func() {
// Finish uploading logs after closing everything else.
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
@@ -120,7 +128,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 +147,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
@@ -17,6 +18,18 @@ StateDirectory=tailscale
StateDirectoryMode=0750
CacheDirectory=tailscale
CacheDirectoryMode=0750
Type=notify
LockPersonality=true
MemoryDenyWriteExecute=true
PrivateTmp=true
ProtectControlGroups=true
ProtectHome=true
ProtectKernelTunables=true
ProtectSystem=strict
ReadWritePaths=/etc/
RestrictSUIDSGID=true
SystemCallArchitectures=native
[Install]
WantedBy=multi-user.target

View File

@@ -17,13 +17,13 @@ import (
"sync"
"time"
"github.com/tailscale/wireguard-go/wgcfg"
"golang.org/x/oauth2"
"tailscale.com/logtail/backoff"
"tailscale.com/tailcfg"
"tailscale.com/types/empty"
"tailscale.com/types/logger"
"tailscale.com/types/structs"
"tailscale.com/types/wgkey"
)
// State is the high-level state of the client. It is used only in
@@ -182,7 +182,8 @@ func (c *Client) SetPaused(paused bool) {
}
c.paused = paused
if paused {
// Just cancel the map routine. The auth routine isn't expensive.
// Only cancel the map routine. (The auth routine isn't expensive
// so it's fine to keep it running.)
c.cancelMapLocked()
} else {
for _, ch := range c.unpauseWaiters {
@@ -230,7 +231,7 @@ func (c *Client) cancelMapSafely() {
c.mu.Lock()
defer c.mu.Unlock()
c.logf("cancelMapSafely: synced=%v", c.synced)
c.logf("[v1] cancelMapSafely: synced=%v", c.synced)
if c.inPollNetMap {
// received at least one netmap since the last
@@ -252,12 +253,12 @@ func (c *Client) cancelMapSafely() {
// request.
select {
case c.newMapCh <- struct{}{}:
c.logf("cancelMapSafely: wrote to channel")
c.logf("[v1] cancelMapSafely: wrote to channel")
default:
// if channel write failed, then there was already
// an outstanding newMapCh request. One is enough,
// since it'll always use the latest endpoints.
c.logf("cancelMapSafely: channel was full")
c.logf("[v1] cancelMapSafely: channel was full")
}
}
}
@@ -268,76 +269,40 @@ func (c *Client) authRoutine() {
for {
c.mu.Lock()
c.logf("authRoutine: %s", c.state)
expiry := c.expiry
goal := c.loginGoal
ctx := c.authCtx
synced := c.synced
if goal != nil {
c.logf("authRoutine: %s; wantLoggedIn=%v", c.state, goal.wantLoggedIn)
} else {
c.logf("authRoutine: %s; goal=nil", c.state)
}
c.mu.Unlock()
select {
case <-c.quit:
c.logf("authRoutine: quit")
c.logf("[v1] authRoutine: quit")
return
default:
}
report := func(err error, msg string) {
c.logf("%s: %v", msg, err)
c.logf("[v1] %s: %v", msg, err)
err = fmt.Errorf("%s: %v", msg, err)
// 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)
}
}
if goal == nil {
// Wait for something interesting to happen
var exp <-chan time.Time
var expTimer *time.Timer
if expiry != nil && !expiry.IsZero() {
// if expiry is in the future, don't delay
// past that time.
// If it's in the past, then it's already
// being handled by someone, so no need to
// wake ourselves up again.
now := c.timeNow()
if expiry.Before(now) {
delay := expiry.Sub(now)
if delay > 5*time.Second {
delay = time.Second
}
expTimer = time.NewTimer(delay)
exp = expTimer.C
}
}
select {
case <-ctx.Done():
if expTimer != nil {
expTimer.Stop()
}
c.logf("authRoutine: context done.")
case <-exp:
// Unfortunately the key expiry isn't provided
// by the control server until mapRequest.
// So we have to do some hackery with c.expiry
// in here.
// TODO(apenwarr): add a key expiry field in RegisterResponse.
c.logf("authRoutine: key expiration check.")
if synced && expiry != nil && !expiry.IsZero() && expiry.Before(c.timeNow()) {
c.logf("Key expired; setting loggedIn=false.")
// Wait for user to Login or Logout.
<-ctx.Done()
c.logf("[v1] authRoutine: context done.")
continue
}
c.mu.Lock()
c.loginGoal = &LoginGoal{
wantLoggedIn: c.loggedIn,
}
c.loggedIn = false
c.expiry = nil
c.mu.Unlock()
}
}
} else if !goal.wantLoggedIn {
if !goal.wantLoggedIn {
err := c.direct.TryLogout(ctx)
if err != nil {
report(err, "TryLogout")
@@ -353,7 +318,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 +359,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 +371,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)
}
@@ -469,7 +434,7 @@ func (c *Client) mapRoutine() {
}
report := func(err error, msg string) {
c.logf("%s: %v", msg, err)
c.logf("[v1] %s: %v", msg, err)
err = fmt.Errorf("%s: %v", msg, err)
// don't send status updates for context errors,
// since context cancelation is always on purpose.
@@ -504,7 +469,7 @@ func (c *Client) mapRoutine() {
select {
case <-c.newMapCh:
c.logf("mapRoutine: new map request during PollNetMap. canceling.")
c.logf("[v1] mapRoutine: new map request during PollNetMap. canceling.")
c.cancelMapLocked()
// Don't emit this netmap; we're
@@ -526,9 +491,9 @@ func (c *Client) mapRoutine() {
c.mu.Unlock()
c.logf("mapRoutine: netmap received: %s", state)
c.logf("[v1] mapRoutine: netmap received: %s", state)
if stillAuthed {
c.sendStatus("mapRoutine2", nil, "", nm)
c.sendStatus("mapRoutine-got-netmap", nil, "", nm)
}
})
@@ -607,7 +572,7 @@ func (c *Client) sendStatus(who string, err error, url string, nm *NetworkMap) {
c.inSendStatus++
c.mu.Unlock()
c.logf("sendStatus: %s: %v", who, state)
c.logf("[v1] sendStatus: %s: %v", who, state)
var p *Persist
var fin *empty.Message
@@ -700,7 +665,7 @@ func (c *Client) Shutdown() {
// NodePublicKey returns the node public key currently in use. This is
// used exclusively in tests.
func (c *Client) TestOnlyNodePublicKey() wgcfg.Key {
func (c *Client) TestOnlyNodePublicKey() wgkey.Key {
priv := c.direct.GetPersist()
return priv.PrivateNodeKey.Public()
}

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"
@@ -29,11 +31,11 @@ import (
"sync/atomic"
"time"
"github.com/tailscale/wireguard-go/wgcfg"
"golang.org/x/crypto/nacl/box"
"golang.org/x/oauth2"
"inet.af/netaddr"
"tailscale.com/log/logheap"
"tailscale.com/net/dnscache"
"tailscale.com/net/netns"
"tailscale.com/net/tlsdial"
"tailscale.com/net/tshttpproxy"
@@ -41,14 +43,28 @@ import (
"tailscale.com/types/logger"
"tailscale.com/types/opt"
"tailscale.com/types/structs"
"tailscale.com/types/wgkey"
"tailscale.com/util/systemd"
"tailscale.com/version"
"tailscale.com/wgengine/filter"
)
type Persist struct {
_ structs.Incomparable
PrivateMachineKey wgcfg.PrivateKey
PrivateNodeKey wgcfg.PrivateKey
OldPrivateNodeKey wgcfg.PrivateKey // needed to request key rotation
_ 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 wgkey.Private `json:"PrivateMachineKey"`
PrivateNodeKey wgkey.Private
OldPrivateNodeKey wgkey.Private // needed to request key rotation
Provider string
LoginName string
}
@@ -61,7 +77,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 &&
@@ -69,9 +85,9 @@ 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()
var mk, ok, nk wgkey.Key
if !p.LegacyFrontendPrivateMachineKey.IsZero() {
mk = p.LegacyFrontendPrivateMachineKey.Public()
}
if !p.OldPrivateNodeKey.IsZero() {
ok = p.OldPrivateNodeKey.Public()
@@ -79,9 +95,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 wgkey.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,30 +115,35 @@ type Direct struct {
keepAlive bool
logf logger.Logf
discoPubKey tailcfg.DiscoKey
machinePrivKey wgkey.Private
debugFlags []string
mu sync.Mutex // mutex guards the following fields
serverKey wgcfg.Key
serverKey wgkey.Key
persist Persist
authKey string
tryingNewKey wgcfg.PrivateKey
tryingNewKey wgkey.Private
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 wgkey.Private // 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 +156,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 {
@@ -146,11 +175,15 @@ func NewDirect(opts Options) (*Direct, error) {
httpc := opts.HTTPTestClient
if httpc == nil {
dnsCache := &dnscache.Resolver{
Forward: dnscache.Get().Forward, // use default cache's forwarder
UseLastGood: true,
}
dialer := netns.NewDialer()
tr := http.DefaultTransport.(*http.Transport).Clone()
tr.Proxy = tshttpproxy.ProxyFromEnvironment
tshttpproxy.SetTransportGetProxyConnectHeader(tr)
tr.DialContext = dialer.DialContext
tr.DialContext = dnscache.Dialer(dialer.DialContext, dnsCache)
tr.ForceAttemptHTTP2 = true
tr.TLSClientConfig = tlsdial.Config(serverURL.Host, tr.TLSClientConfig)
httpc = &http.Client{Transport: tr}
@@ -158,6 +191,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 +200,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 +219,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 +240,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 +286,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, "")
}
@@ -275,6 +310,7 @@ func (c *Direct) doLoginOrRegen(ctx context.Context, t *oauth2.Token, flags Logi
if mustregen {
_, url, err = c.doLogin(ctx, t, flags, true, url)
}
return url, err
}
@@ -289,17 +325,13 @@ 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 {
c.logf("Old key expired -> regen=true")
systemd.Status("key expired; run 'tailscale up' to authenticate")
regen = true
}
if (flags & LoginInteractive) != 0 {
@@ -308,7 +340,7 @@ func (c *Direct) doLogin(ctx context.Context, t *oauth2.Token, flags LoginFlags,
}
c.logf("doLogin(regen=%v, hasUrl=%v)", regen, url != "")
if serverKey == (wgcfg.Key{}) {
if serverKey.IsZero() {
var err error
serverKey, err = loadServerKey(ctx, c.httpc, c.serverURL)
if err != nil {
@@ -320,12 +352,12 @@ func (c *Direct) doLogin(ctx context.Context, t *oauth2.Token, flags LoginFlags,
c.mu.Unlock()
}
var oldNodeKey wgcfg.Key
var oldNodeKey wgkey.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()
key, err := wgkey.NewPrivate()
if err != nil {
c.logf("login keygen: %v", err)
return regen, url, err
@@ -335,11 +367,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 +392,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 +409,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 +498,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 +513,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 +529,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,44 +537,61 @@ 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("[v1] PollNetMap: stream=%v :%v ep=%v", allowStream, localPort, ep)
vlogf := logger.Discard
if Debug.NetMap {
// TODO(bradfitz): update this to use "[v2]" prefix perhaps? but we don't
// want to upload it always.
vlogf = c.logf
}
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: 8,
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 +602,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 +641,8 @@ func (c *Direct) PollNetMap(ctx context.Context, maxPolls int, cb func(*NetworkM
}()
var lastDERPMap *tailcfg.DERPMap
var lastUserProfile = map[tailcfg.UserID]tailcfg.UserProfile{}
var lastParsedPacketFilter []filter.Match
// If allowStream, then the server will use an HTTP long poll to
// return incremental results. There is always one response right
@@ -612,7 +683,7 @@ func (c *Direct) PollNetMap(ctx context.Context, maxPolls int, cb func(*NetworkM
case timeoutReset <- struct{}{}:
vlogf("netmap: sent timer reset")
case <-ctx.Done():
c.logf("netmap: not resetting timer; context done: %v", ctx.Err())
c.logf("[v1] netmap: not resetting timer; context done: %v", ctx.Err())
return ctx.Err()
}
if resp.KeepAlive {
@@ -621,6 +692,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")
@@ -646,9 +720,14 @@ func (c *Direct) PollNetMap(ctx context.Context, maxPolls int, cb func(*NetworkM
resp.Peers = filtered
}
if pf := resp.PacketFilter; pf != nil {
lastParsedPacketFilter = c.parsePacketFilter(pf)
}
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 +736,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),
PacketFilter: lastParsedPacketFilter,
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
@@ -673,7 +761,7 @@ func (c *Direct) PollNetMap(ctx context.Context, maxPolls int, cb func(*NetworkM
nm.MachineStatus = tailcfg.MachineUnauthorized
}
if len(resp.DNS) > 0 {
nm.DNS.Nameservers = wgIPToNetaddr(resp.DNS)
nm.DNS.Nameservers = resp.DNS
}
if len(resp.SearchPaths) > 0 {
nm.DNS.Domains = resp.SearchPaths
@@ -690,7 +778,7 @@ func (c *Direct) PollNetMap(ctx context.Context, maxPolls int, cb func(*NetworkM
now := c.timeNow()
if now.Sub(c.lastPrintMap) >= 5*time.Minute {
c.lastPrintMap = now
c.logf("new network map[%d]:\n%s", i, nm.Concise())
c.logf("[v1] new network map[%d]:\n%s", i, nm.Concise())
}
c.mu.Lock()
@@ -705,7 +793,7 @@ func (c *Direct) PollNetMap(ctx context.Context, maxPolls int, cb func(*NetworkM
return nil
}
func decode(res *http.Response, v interface{}, serverKey *wgcfg.Key, mkey *wgcfg.PrivateKey) error {
func decode(res *http.Response, v interface{}, serverKey *wgkey.Key, mkey *wgkey.Private) error {
defer res.Body.Close()
msg, err := ioutil.ReadAll(io.LimitReader(res.Body, 1<<20))
if err != nil {
@@ -717,13 +805,16 @@ func decode(res *http.Response, v interface{}, serverKey *wgcfg.Key, mkey *wgcfg
return decodeMsg(msg, v, serverKey, mkey)
}
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
}
@@ -741,6 +832,15 @@ func (c *Direct) decodeMsg(msg []byte, v interface{}) error {
return err
}
}
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)
}
@@ -748,18 +848,21 @@ func (c *Direct) decodeMsg(msg []byte, v interface{}) error {
}
func decodeMsg(msg []byte, v interface{}, serverKey *wgcfg.Key, mkey *wgcfg.PrivateKey) error {
func decodeMsg(msg []byte, v interface{}, serverKey *wgkey.Key, mkey *wgkey.Private) error {
decrypted, err := decryptMsg(msg, serverKey, mkey)
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)
}
return nil
}
func decryptMsg(msg []byte, serverKey *wgcfg.Key, mkey *wgcfg.PrivateKey) ([]byte, error) {
func decryptMsg(msg []byte, serverKey *wgkey.Key, mkey *wgkey.Private) ([]byte, error) {
var nonce [24]byte
if len(msg) < len(nonce)+1 {
return nil, fmt.Errorf("response missing nonce, len=%d", len(msg))
@@ -770,18 +873,17 @@ 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
}
func encode(v interface{}, serverKey *wgcfg.Key, mkey *wgcfg.PrivateKey) ([]byte, error) {
func encode(v interface{}, serverKey *wgkey.Key, mkey *wgkey.Private) ([]byte, error) {
b, err := json.Marshal(v)
if err != nil {
return nil, err
}
const debugMapRequests = false
if debugMapRequests {
if debugMap {
if _, ok := v.(tailcfg.MapRequest); ok {
log.Printf("MapRequest: %s", b)
}
@@ -795,66 +897,50 @@ func encode(v interface{}, serverKey *wgcfg.Key, mkey *wgcfg.PrivateKey) ([]byte
return msg, nil
}
func loadServerKey(ctx context.Context, httpc *http.Client, serverURL string) (wgcfg.Key, error) {
func loadServerKey(ctx context.Context, httpc *http.Client, serverURL string) (wgkey.Key, error) {
req, err := http.NewRequest("GET", serverURL+"/key", nil)
if err != nil {
return wgcfg.Key{}, fmt.Errorf("create control key request: %v", err)
return wgkey.Key{}, fmt.Errorf("create control key request: %v", err)
}
req = req.WithContext(ctx)
res, err := httpc.Do(req)
if err != nil {
return wgcfg.Key{}, fmt.Errorf("fetch control key: %v", err)
return wgkey.Key{}, fmt.Errorf("fetch control key: %v", err)
}
defer res.Body.Close()
b, err := ioutil.ReadAll(io.LimitReader(res.Body, 1<<16))
if err != nil {
return wgcfg.Key{}, fmt.Errorf("fetch control key response: %v", err)
return wgkey.Key{}, fmt.Errorf("fetch control key response: %v", err)
}
if res.StatusCode != 200 {
return wgcfg.Key{}, fmt.Errorf("fetch control key: %d: %s", res.StatusCode, string(b))
return wgkey.Key{}, fmt.Errorf("fetch control key: %d: %s", res.StatusCode, string(b))
}
key, err := wgcfg.ParseHexKey(string(b))
key, err := wgkey.ParseHex(string(b))
if err != nil {
return wgcfg.Key{}, fmt.Errorf("fetch control key: %v", err)
return wgkey.Key{}, fmt.Errorf("fetch control key: %v", err)
}
return key, nil
}
func wgIPToNetaddr(ips []wgcfg.IP) (ret []netaddr.IP) {
for _, ip := range ips {
nip, ok := netaddr.FromStdIP(ip.IP())
if !ok {
panic(fmt.Sprintf("conversion of %s from wgcfg to netaddr IP failed", ip))
}
ret = append(ret, nip.Unmap())
}
return ret
}
// Debug contains temporary internal-only debug knobs.
// They're unexported to not draw attention to them.
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 {
@@ -989,3 +1075,75 @@ 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 []netaddr.IPPrefix) 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
}
v4Routes, v6Routes := false, false
for _, r := range routes {
if r.IP.Is4() {
v4Routes = true
} else {
v6Routes = true
}
}
if v4Routes {
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
}
if strings.TrimSpace(string(out)) == "0" {
return true
}
}
if v6Routes {
// Note: you might be wondering why we check only the state of
// conf.all.forwarding, rather than per-interface forwarding
// configuration. According to kernel documentation, it seems
// that to actually forward packets, you need to enable
// forwarding globally, and the per-interface forwarding
// setting only alters other things such as how router
// advertisements are handled. The kernel itself warns that
// enabling forwarding per-interface and not globally will
// probably not work, so I feel okay calling those configs
// broken until we have proof otherwise.
out, err := ioutil.ReadFile("/proc/sys/net/ipv6/conf/all/forwarding")
if err != nil {
out, err = exec.Command("sysctl", "-n", "net.ipv6.conf.all.forwarding").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
}
if strings.TrimSpace(string(out)) == "0" {
return true
}
}
return false
}

View File

@@ -9,9 +9,9 @@ import (
"tailscale.com/wgengine/filter"
)
// Parse a backward-compatible FilterRule used by control's wire format,
// producing the most current filter.Matches format.
func (c *Direct) parsePacketFilter(pf []tailcfg.FilterRule) filter.Matches {
// Parse a backward-compatible FilterRule used by control's wire
// format, producing the most current filter format.
func (c *Direct) parsePacketFilter(pf []tailcfg.FilterRule) []filter.Match {
mm, err := filter.MatchesFromFilterRules(pf)
if err != nil {
c.logf("parsePacketFilter: %s\n", err)

View File

@@ -16,6 +16,7 @@ import (
"go4.org/mem"
"tailscale.com/util/lineread"
"tailscale.com/version/distro"
)
func init() {
@@ -23,13 +24,22 @@ func init() {
}
func osVersionLinux() string {
dist := distro.Get()
propFile := "/etc/os-release"
switch dist {
case distro.Synology:
propFile = "/etc.defaults/VERSION"
case distro.OpenWrt:
propFile = "/etc/openwrt_release"
}
m := map[string]string{}
lineread.File("/etc/os-release", func(line []byte) error {
lineread.File(propFile, func(line []byte) error {
eq := bytes.IndexByte(line, '=')
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
})
@@ -63,7 +73,7 @@ func osVersionLinux() string {
return fmt.Sprintf("%s%s", bytes.TrimSpace(cr), attr)
}
fallthrough
case "fedora", "rhel", "alpine":
case "fedora", "rhel", "alpine", "nixos":
// Their PRETTY_NAME is fine as-is for all versions I tested.
fallthrough
default:
@@ -71,6 +81,12 @@ func osVersionLinux() string {
return fmt.Sprintf("%s%s", v, attr)
}
}
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

@@ -21,6 +21,10 @@ func osVersionWindows() string {
s := strings.TrimSpace(string(out))
s = strings.TrimPrefix(s, "Microsoft Windows [")
s = strings.TrimSuffix(s, "]")
s = strings.TrimPrefix(s, "Version ") // is this localized? do it last in case.
return s // "10.0.19041.388", ideally
// "Version 10.x.y.z", with "Version" localized. Keep only stuff after the space.
if sp := strings.Index(s, " "); sp != -1 {
s = s[sp+1:]
}
return s // "10.0.19041.388", ideally
}

View File

@@ -14,8 +14,10 @@ import (
"time"
"github.com/tailscale/wireguard-go/wgcfg"
"inet.af/netaddr"
"tailscale.com/tailcfg"
"tailscale.com/types/logger"
"tailscale.com/types/wgkey"
"tailscale.com/wgengine/filter"
)
@@ -23,17 +25,18 @@ type NetworkMap struct {
// Core networking
NodeKey tailcfg.NodeKey
PrivateKey wgcfg.PrivateKey
PrivateKey wgkey.Private
Expiry time.Time
// Name is the DNS name assigned to this node.
Name string
Addresses []wgcfg.CIDR
Addresses []netaddr.IPPrefix
LocalPort uint16 // used for debugging
MachineStatus tailcfg.MachineStatus
MachineKey tailcfg.MachineKey
Peers []*tailcfg.Node // sorted by Node.ID
DNS tailcfg.DNSConfig
Hostinfo tailcfg.Hostinfo
PacketFilter filter.Matches
PacketFilter []filter.Match
// DERPMap is the last DERP server map received. It's reused
// between updates and should not be modified.
@@ -49,7 +52,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 +77,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 +103,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
}
@@ -219,7 +231,6 @@ const (
AllowSingleHosts WGConfigFlags = 1 << iota
AllowSubnetRoutes
AllowDefaultRoute
HackDefaultRoute
)
// EndpointDiscoSuffix is appended to the hex representation of a peer's discovery key
@@ -231,7 +242,7 @@ const EndpointDiscoSuffix = ".disco.tailscale:12345"
func (nm *NetworkMap) WGCfg(logf logger.Logf, flags WGConfigFlags) (*wgcfg.Config, error) {
cfg := &wgcfg.Config{
Name: "tailscale",
PrivateKey: nm.PrivateKey,
PrivateKey: wgcfg.PrivateKey(nm.PrivateKey),
Addresses: nm.Addresses,
ListenPort: nm.LocalPort,
Peers: make([]wgcfg.Peer, 0, len(nm.Peers)),
@@ -269,18 +280,14 @@ func (nm *NetworkMap) WGCfg(logf logger.Logf, flags WGConfigFlags) (*wgcfg.Confi
}
}
for _, allowedIP := range peer.AllowedIPs {
if allowedIP.Mask == 0 {
if allowedIP.Bits == 0 {
if (flags & AllowDefaultRoute) == 0 {
logf("wgcfg: %v skipping default route", peer.Key.ShortString())
logf("[v1] wgcfg: %v skipping default route", peer.Key.ShortString())
continue
}
if (flags & HackDefaultRoute) != 0 {
allowedIP = wgcfg.CIDR{IP: wgcfg.IPv4(10, 0, 0, 0), Mask: 8}
logf("wgcfg: %v converting default route => %v", peer.Key.ShortString(), allowedIP.String())
}
} else if allowedIP.Mask < 32 {
} else if cidrIsSubnet(peer, allowedIP) {
if (flags & AllowSubnetRoutes) == 0 {
logf("wgcfg: %v skipping subnet route", peer.Key.ShortString())
logf("[v1] wgcfg: %v skipping subnet route", peer.Key.ShortString())
continue
}
}
@@ -291,6 +298,23 @@ func (nm *NetworkMap) WGCfg(logf logger.Logf, flags WGConfigFlags) (*wgcfg.Confi
return cfg, nil
}
// cidrIsSubnet reports whether cidr is a non-default-route subnet
// exported by node that is not one of its own self addresses.
func cidrIsSubnet(node *tailcfg.Node, cidr netaddr.IPPrefix) bool {
if cidr.Bits == 0 {
return false
}
if !cidr.IsSingleIP() {
return true
}
for _, selfCIDR := range node.Addresses {
if cidr == selfCIDR {
return false
}
}
return true
}
func appendEndpoint(peer *wgcfg.Peer, epStr string) error {
if epStr == "" {
return nil
@@ -323,7 +347,7 @@ func eqStringsIgnoreNil(a, b []string) bool {
// eqCIDRsIgnoreNil reports whether a and b have the same length and
// contents, but ignore whether a or b are nil.
func eqCIDRsIgnoreNil(a, b []wgcfg.CIDR) bool {
func eqCIDRsIgnoreNil(a, b []netaddr.IPPrefix) bool {
if len(a) != len(b) {
return false
}

View File

@@ -6,9 +6,10 @@ package controlclient
import (
"encoding/hex"
"encoding/json"
"testing"
"github.com/tailscale/wireguard-go/wgcfg"
"inet.af/netaddr"
"tailscale.com/tailcfg"
)
@@ -51,7 +52,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 +60,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 +68,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 +136,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",
@@ -250,7 +251,7 @@ func TestConciseDiffFrom(t *testing.T) {
DERP: "127.3.3.40:2",
Endpoints: []string{"192.168.0.100:41641", "1.1.1.1:41641"},
DiscoKey: testDiscoKey("f00f00f00f"),
AllowedIPs: []wgcfg.CIDR{{IP: wgcfg.IPv4(100, 102, 103, 104), Mask: 32}},
AllowedIPs: []netaddr.IPPrefix{{IP: netaddr.IPv4(100, 102, 103, 104), Bits: 32}},
},
},
},
@@ -263,7 +264,7 @@ func TestConciseDiffFrom(t *testing.T) {
DERP: "127.3.3.40:2",
Endpoints: []string{"192.168.0.100:41641", "1.1.1.1:41641"},
DiscoKey: testDiscoKey("ba4ba4ba4b"),
AllowedIPs: []wgcfg.CIDR{{IP: wgcfg.IPv4(100, 102, 103, 104), Mask: 32}},
AllowedIPs: []netaddr.IPPrefix{{IP: netaddr.IPv4(100, 102, 103, 104), Bits: 32}},
},
},
},
@@ -282,3 +283,15 @@ func TestConciseDiffFrom(t *testing.T) {
})
}
}
func TestNewHostinfo(t *testing.T) {
hi := NewHostinfo()
if hi == nil {
t.Fatal("no Hostinfo")
}
j, err := json.MarshalIndent(hi, " ", "")
if err != nil {
t.Fatal(err)
}
t.Logf("Got: %s", j)
}

View File

@@ -8,18 +8,18 @@ import (
"reflect"
"testing"
"github.com/tailscale/wireguard-go/wgcfg"
"tailscale.com/types/wgkey"
)
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)
}
newPrivate := func() wgcfg.PrivateKey {
k, err := wgcfg.NewPrivateKey()
newPrivate := func() wgkey.Private {
k, err := wgkey.NewPrivate()
if err != nil {
panic(err)
}
@@ -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
}

34
go.mod
View File

@@ -1,6 +1,6 @@
module tailscale.com
go 1.14
go 1.15
require (
github.com/alexbrainman/sspi v0.0.0-20180613141037-e580b900e9f5
@@ -9,31 +9,37 @@ 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
github.com/google/go-cmp v0.4.0
github.com/google/go-cmp v0.5.4
github.com/goreleaser/nfpm v1.1.10
github.com/jsimonetti/rtnetlink v0.0.0-20200117123717-f846d4f6c1f4
github.com/jsimonetti/rtnetlink v0.0.0-20201216134343-bde56ed16391
github.com/klauspost/compress v1.10.10
github.com/kr/pty v1.1.1
github.com/mdlayher/netlink v1.1.0
github.com/mdlayher/netlink v1.2.0
github.com/mdlayher/sdnotify v0.0.0-20200625151349-e4a4f32afc4a
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/winipcfg-go v0.0.0-20200413171540-609dcf2df55f
github.com/tailscale/wireguard-go v0.0.0-20200902185615-1997cf6f9fe4
github.com/tailscale/depaware v0.0.0-20201214215404-77d1e9757027
github.com/tailscale/wireguard-go v0.0.0-20201228234719-da0d2727455d
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
go4.org/intern v0.0.0-20201223061701-969c7e87e7cb // indirect
go4.org/mem v0.0.0-20201119185036-c04c5a6ff174
go4.org/unsafe/assume-no-moving-gc v0.0.0-20201222180813-1025295fd063 // indirect
golang.org/x/crypto v0.0.0-20201124201722-c8d3bf9c5392
golang.org/x/net v0.0.0-20201216054612-986b41b23924
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e
golang.org/x/sys v0.0.0-20200812155832-6a926be9bd1d
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9
golang.org/x/sys v0.0.0-20201218084310-7d0127a74742
golang.org/x/term v0.0.0-20201207232118-ee85cb95a76b
golang.org/x/time v0.0.0-20191024005414-555d28b269f0
golang.org/x/tools v0.0.0-20191216052735-49a3e744a425
honnef.co/go/tools v0.0.1-2020.1.4
inet.af/netaddr v0.0.0-20200810144936-56928fe48a98
golang.org/x/tools v0.0.0-20201211185031-d93e913c1a58
golang.zx2c4.com/wireguard/windows v0.1.2-0.20201113162609-9b85be97fdf8
honnef.co/go/tools v0.1.0
inet.af/netaddr v0.0.0-20201231012616-c5dc91d2a016
rsc.io/goversion v1.2.0
)

147
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=
@@ -26,10 +23,13 @@ github.com/coreos/go-iptables v0.4.5/go.mod h1:/mVI274lEDI2ns62jHCDnCyBF9Iwsmeka
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dvyukov/go-fuzz v0.0.0-20201127111758-49e582c6c23d/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw=
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ=
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=
@@ -42,6 +42,11 @@ github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5a
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0 h1:/QaMHBdZ26BB3SSst0Iwl10Epc+xhTquomWX0oZEB6w=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4 h1:L8R9j+yAqZuZjsqh/z+F1NCffTKKLShY6zXTItVIZ8M=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/rpmpack v0.0.0-20191226140753-aa36bfddb3a0 h1:BW6OvS3kpT5UEPbCZ+KyX/OB4Ks9/MNMhWjqPPkZxsE=
github.com/google/rpmpack v0.0.0-20191226140753-aa36bfddb3a0/go.mod h1:RaTPr0KUf2K7fnZYLNDrr8rxAamWs3iNywJLtQ2AzBg=
@@ -52,6 +57,9 @@ github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJ
github.com/jsimonetti/rtnetlink v0.0.0-20190606172950-9527aa82566a/go.mod h1:Oz+70psSo5OFh8DBl0Zv2ACw7Esh6pPUphlvZG9x7uw=
github.com/jsimonetti/rtnetlink v0.0.0-20200117123717-f846d4f6c1f4 h1:nwOc1YaOrYJ37sEBrtWZrdqzK22hiJs3GpDmP3sR2Yw=
github.com/jsimonetti/rtnetlink v0.0.0-20200117123717-f846d4f6c1f4/go.mod h1:WGuG/smIU4J/54PblvSbh+xvCZmpJnFgr3ds6Z55XMQ=
github.com/jsimonetti/rtnetlink v0.0.0-20201009170750-9c6f07d100c1/go.mod h1:hqoO/u39cqLeBLebZ8fWdE96O7FxrAsRYhnVOdgHxok=
github.com/jsimonetti/rtnetlink v0.0.0-20201216134343-bde56ed16391 h1:Dqu/4JhMV1vpXHDjzQCuDCEsjNi0xfuSmQlMOyqayKA=
github.com/jsimonetti/rtnetlink v0.0.0-20201216134343-bde56ed16391/go.mod h1:cR77jAZG3Y3bsb8hF6fHJbFoyFukLFOkQ98S0pQz3xw=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.10.10 h1:a/y8CglcM7gLGYmlbP/stPE5sR3hbhFRUjCBfd/0B3I=
github.com/klauspost/compress v1.10.10/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
@@ -61,12 +69,21 @@ 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/walk v0.0.0-20201110160827-18ea5e372cdb/go.mod h1:E23UucZGqpuUANJooIbHWCufXvOcT6E7Stq81gU+CSQ=
github.com/lxn/win v0.0.0-20191128105842-2da648fda5b4/go.mod h1:ouWl4wViUNh8tPSIwxTVMuS014WakR1hqvBc2I0bMoA=
github.com/lxn/win v0.0.0-20201111105847-2a20daff6a55/go.mod h1:KxxjdtRkfNoYDCUP5ryK7XJJNTnpC8atvtmTheChOtk=
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=
github.com/mdlayher/netlink v1.0.0/go.mod h1:KxeJAFOFLG6AjpyDkQ/iIhxygIUKD+vcwqcnu43w/+M=
github.com/mdlayher/netlink v1.1.0 h1:mpdLgm+brq10nI9zM1BpX1kpDbh3NLl3RSnVq6ZSkfg=
github.com/mdlayher/netlink v1.1.0/go.mod h1:H4WCitaheIsdF9yOYu8CFmCgQthAPIWZmcKp9uZHgmY=
github.com/mdlayher/netlink v1.1.1/go.mod h1:WTYpFb/WTvlRJAyKhZL5/uy69TDDpHHu2VZmb2XgV7o=
github.com/mdlayher/netlink v1.2.0 h1:zPolhRjfuabdf8ofZsl56eoU+92cvSlAn13lw4veCZ0=
github.com/mdlayher/netlink v1.2.0/go.mod h1:kwVW1io0AZy9A1E2YYgaD4Cj+C+GPkU6klXCMzIJ9p8=
github.com/mdlayher/sdnotify v0.0.0-20200625151349-e4a4f32afc4a h1:wMv2mvcHRH4jqIxaVL5t6gSq1hjPiaWH7TOcA0Z+uNo=
github.com/mdlayher/sdnotify v0.0.0-20200625151349-e4a4f32afc4a/go.mod h1:HtjVsQfsrBm1GDcDTUFn4ZXhftxTwO/hxrvEiRc61U4=
github.com/miekg/dns v1.1.30 h1:Qww6FseFn8PRfw07jueqIXqodm0JKiiKuK0DeXSqfyo=
github.com/miekg/dns v1.1.30/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 h1:lDH9UUVJtmYCjyT0CI4q8xvlXPxeZ0gYCVvWbmPlp88=
@@ -76,6 +93,8 @@ github.com/pborman/getopt v0.0.0-20190409184431-ee0cd42419d3/go.mod h1:85jBQOZwp
github.com/pelletier/go-toml v1.6.0/go.mod h1:5N711Q9dKgbdkxHL+MEfF31hpT7l0S0s/t2kKREewys=
github.com/peterbourgon/ff/v2 v2.0.0 h1:lx0oYI5qr/FU1xnpNhQ+EZM04gKgn46jyYvGEEqBBbY=
github.com/peterbourgon/ff/v2 v2.0.0/go.mod h1:xjwr+t+SjWm4L46fcj/D+Ap+6ME7+HqFzaP22pP5Ggk=
github.com/pkg/diff v0.0.0-20200914180035-5b29258ca4f7 h1:+/+DxvQaYifJ+grD4klzrS5y+KJXldn/2YTl5JG+vZ8=
github.com/pkg/diff v0.0.0-20200914180035-5b29258ca4f7/go.mod h1:zO8QMzTeZd5cpnIkz/Gn6iK0jDfGicM1nynOkkPIl28=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
@@ -83,15 +102,20 @@ 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/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/wireguard-go v0.0.0-20200806235025-91988cfbaa3a h1:dQEgNpoOJf+8MswlvXJicb8ZDQqZAGe8f/WfzbDMvtE=
github.com/tailscale/wireguard-go v0.0.0-20200806235025-91988cfbaa3a/go.mod h1:WXq+IkSOJGIgfF1XW+4z4oW+LX/TXzU9DcKlT5EZLi4=
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/go.mod h1:jissDaJNHiyV2tFdr3QyNEfsZrax/i2yQiSO+CljThI=
github.com/tailscale/depaware v0.0.0-20201214215404-77d1e9757027 h1:lK99QQdH3yBWY6aGilF+IRlQIdmhzLrsEmF6JgN+Ryw=
github.com/tailscale/depaware v0.0.0-20201214215404-77d1e9757027/go.mod h1:p9lPsd+cx33L3H9nNoecRRxPssFKUwwI50I3pZ0yT+8=
github.com/tailscale/wireguard-go v0.0.0-20201220011020-db78fad0bebf h1:HuBwLWbDNIh/G72KSImSEx+dnd7FPGFI1e60LMJtLjU=
github.com/tailscale/wireguard-go v0.0.0-20201220011020-db78fad0bebf/go.mod h1:9PbAnF5CAklkURoO0uQhm+YUjDmm9T9oCyTGlCHuTPQ=
github.com/tailscale/wireguard-go v0.0.0-20201228234719-da0d2727455d h1:ha3qx0YBsEYM1VpLoAxVyLsz74H2a/Kv/id+2Bo/WLU=
github.com/tailscale/wireguard-go v0.0.0-20201228234719-da0d2727455d/go.mod h1:FEGDKc5yHNWtTS5ugWnHMNF0d9LlaHv/zQwOrVogo2U=
github.com/tailscale/wireguard-go v0.0.20201119-0.20201228205120-066446d1733a h1:RUJeuZlAm1DT6Mhk9UTsaHrDeDZhPrbKfNsaEtKF6+0=
github.com/tailscale/wireguard-go v0.0.20201119-0.20201228205120-066446d1733a/go.mod h1:UIAx57STfAZOrNVj8QGP2zG3ovWPMTD4DDubFHqMlYI=
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=
@@ -100,16 +124,36 @@ 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=
go4.org/mem v0.0.0-20200706164138-185c595c3ecc h1:paujszgN6SpsO/UsXC7xax3gQAKz/XQKCYZLQdU34Tw=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
go4.org/intern v0.0.0-20201223054237-ef8cbcb8edd7 h1:yeDrXaQ3VRXbTN7lHj70DxW4LdPow83MVwPPRjpP70U=
go4.org/intern v0.0.0-20201223054237-ef8cbcb8edd7/go.mod h1:vLqJ+12kCw61iCWsPto0EOHhBS+o4rO5VIucbc9g2Cc=
go4.org/intern v0.0.0-20201223061701-969c7e87e7cb h1:yuqO0E4bHRsTPUocDpRKXfLE40lwWplVxENQ2WOV7Gc=
go4.org/intern v0.0.0-20201223061701-969c7e87e7cb/go.mod h1:vLqJ+12kCw61iCWsPto0EOHhBS+o4rO5VIucbc9g2Cc=
go4.org/mem v0.0.0-20200706164138-185c595c3ecc/go.mod h1:NEYvpHWemiG/E5UWfaN5QAIGZeT1sa0Z2UNk6oeMb/k=
go4.org/mem v0.0.0-20201119185036-c04c5a6ff174 h1:vSug/WNOi2+4jrKdivxayTN/zd8EA1UrStjpWvvo1jk=
go4.org/mem v0.0.0-20201119185036-c04c5a6ff174/go.mod h1:reUoABIJ9ikfM5sgtSF3Wushcza7+WeD01VB9Lirh3g=
go4.org/unsafe/assume-no-moving-gc v0.0.0-20201222175341-b30ae309168e h1:ExUmGi0ZsQmiVo9giDQqXkr7vreeXPMkOGIusfsfbzI=
go4.org/unsafe/assume-no-moving-gc v0.0.0-20201222175341-b30ae309168e/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E=
go4.org/unsafe/assume-no-moving-gc v0.0.0-20201222180813-1025295fd063 h1:1tk03FUNpulq2cuWpXZWj649rwJpk0d20rxWiopKRmc=
go4.org/unsafe/assume-no-moving-gc v0.0.0-20201222180813-1025295fd063/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E=
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/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201112155050-0c6587e931a9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201124201722-c8d3bf9c5392 h1:xYJJ3S178yv++9zXV/hnr29plCAGO9vAFG9dorqaFQc=
golang.org/x/crypto v0.0.0-20201124201722-c8d3bf9c5392/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
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.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.0 h1:8pl+sMODzuvGJkmj2W4kZihvVb5mKm8pB/X44PIQHv8=
golang.org/x/mod v0.4.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=
@@ -117,66 +161,107 @@ 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-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
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/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b h1:uwuIcX0g4Yl1NC5XAz37xsr2lTtcqevgzYNVt49waME=
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201216054612-986b41b23924 h1:QsnDpLLOKwHBBDa8nDws4DYNc/ryVW2vCpxCs09d4PY=
golang.org/x/net v0.0.0-20201216054612-986b41b23924/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
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/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9 h1:SQFwaSi55rU7vdNs9Yr0Z324VNlrF+0wMqRXT4St8ck=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/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-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
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-20200812155832-6a926be9bd1d h1:QQrM/CCYEzTs91GZylDCQjGHudbPTxF/1fvXdVh5lMo=
golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200812155832-6a926be9bd1d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201009025420-dfb3f7c4e634/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201107080550-4d91cf3a1aaf/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201112073958-5cba982894dd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201118182958-a01c418693c7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201202213521-69691e467435/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201218084310-7d0127a74742 h1:+CBz4km/0KPU3RGTwARGh/noP3bEwtHcq+0YcBQM2JQ=
golang.org/x/sys v0.0.0-20201218084310-7d0127a74742/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20201207232118-ee85cb95a76b h1:a0ErnNnPKmhDyIXQvdZr+Lq8dc8xpMeqkF8y5PgQU4Q=
golang.org/x/term v0.0.0-20201207232118-ee85cb95a76b/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
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/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.4 h1:0YWbFKbhXG/wIiuHDSKpS0Iy7FSA+u45VtBMfQcFTTc=
golang.org/x/text v0.3.4/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-20200609164405-eb789aa7ce50/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20201001230009-b5b87423c93b/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU=
golang.org/x/tools v0.0.0-20201002184944-ecd9fd270d5d/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU=
golang.org/x/tools v0.0.0-20201211185031-d93e913c1a58 h1:1Bs6RVeBFtLZ8Yi1Hk07DiOqzvwLD/4hln4iahvFlag=
golang.org/x/tools v0.0.0-20201211185031-d93e913c1a58/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
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=
google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508=
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/go.mod h1:GJvYs5O24/ASlwPiRklVnjMx2xQzrOic0DuU6GvYJL4=
golang.zx2c4.com/wireguard v0.0.20200321-0.20201111175144-60b3766b89b9 h1:qowcZ56hhpeoESmWzI4Exhx4Y78TpCyXUJur4/c0CoE=
golang.zx2c4.com/wireguard v0.0.20200321-0.20201111175144-60b3766b89b9/go.mod h1:LMeNfjlcPZTrBC1juwgbQyA4Zy2XVcsrdO/fIJxwyuA=
golang.zx2c4.com/wireguard/windows v0.1.2-0.20201004085714-dd60d0447f81/go.mod h1:GaK5zcgr5XE98WaRzIDilumDBp5/yP8j2kG/LCDnvAM=
golang.zx2c4.com/wireguard/windows v0.1.2-0.20201113162609-9b85be97fdf8 h1:nlXPqGA98n+qcq1pwZ28KjM5EsFQvamKS00A+VUeVjs=
golang.zx2c4.com/wireguard/windows v0.1.2-0.20201113162609-9b85be97fdf8/go.mod h1:psva4yDnAHLuh7lUzOK7J7bLYxNFfo0iKWz+mi9gzkA=
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=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.7 h1:VUgggvou5XRW9mHwD/yXxIYSMtY0zoKQf/v226p2nyo=
gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
honnef.co/go/tools v0.0.1-2020.1.4 h1:UoveltGrhghAA7ePc+e+QYDHXrBps2PqFZiHkGR/xK8=
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
inet.af/netaddr v0.0.0-20200718043157-99321d6ad24c h1:si3Owrfem175Ry6gKqnh59eOXxDojyBTIHxUKuvK/Eo=
inet.af/netaddr v0.0.0-20200718043157-99321d6ad24c/go.mod h1:qqYzz/2whtrbWJvt+DNWQyvekNN4ePQZcg2xc2/Yjww=
inet.af/netaddr v0.0.0-20200810144936-56928fe48a98 h1:bWyWDZP0l6VnQ1TDKf6yNwuiEDV6Q3q1Mv34m+lzT1I=
honnef.co/go/tools v0.1.0 h1:AWNL1W1i7f0wNZ8VwOKNJ0sliKvOF/adn0EHenfUh+c=
honnef.co/go/tools v0.1.0/go.mod h1:XtegFAyX/PfluP4921rXU5IkjkqBCDnUq4W8VCIoKvM=
inet.af/netaddr v0.0.0-20200810144936-56928fe48a98/go.mod h1:qqYzz/2whtrbWJvt+DNWQyvekNN4ePQZcg2xc2/Yjww=
inet.af/netaddr v0.0.0-20201218162718-658fec415e52/go.mod h1:qqYzz/2whtrbWJvt+DNWQyvekNN4ePQZcg2xc2/Yjww=
inet.af/netaddr v0.0.0-20201223185330-97d366981fac h1:aqMW8vft7VmOIhtQhsTWhAuZzOBGYBv+Otyvwj+VGSU=
inet.af/netaddr v0.0.0-20201223185330-97d366981fac/go.mod h1:9NdhtHLglxJliAZB6aC5ws3mfnUArdAzHG/iJq7cB/o=
inet.af/netaddr v0.0.0-20201224214825-a55841caa437 h1:Li2QBwaT/hU3wE7GdyoqaX+TzIlI+V1zs/CuWrjX8e4=
inet.af/netaddr v0.0.0-20201224214825-a55841caa437/go.mod h1:9NdhtHLglxJliAZB6aC5ws3mfnUArdAzHG/iJq7cB/o=
inet.af/netaddr v0.0.0-20201226233944-2d1876c01610 h1:9Nnw3NS9SL4SlFtBWSdv7onMbdY+B8nflRNZvhgxuMY=
inet.af/netaddr v0.0.0-20201226233944-2d1876c01610/go.mod h1:9NdhtHLglxJliAZB6aC5ws3mfnUArdAzHG/iJq7cB/o=
inet.af/netaddr v0.0.0-20201228234250-33d0a924ebbf h1:0eHZ8v6j5wIiOVyoYPd70ueZ/RPEQtRlzi60uneDbRU=
inet.af/netaddr v0.0.0-20201228234250-33d0a924ebbf/go.mod h1:9NdhtHLglxJliAZB6aC5ws3mfnUArdAzHG/iJq7cB/o=
inet.af/netaddr v0.0.0-20201231012616-c5dc91d2a016 h1:CEeeAJW60aRKE6gGJC5krs2xC/uM2l8SasvgeDXFN5Q=
inet.af/netaddr v0.0.0-20201231012616-c5dc91d2a016/go.mod h1:lbePDLSB5c45kkUmF7ETNE5X9z/yuQvWJIv1hhb5rFI=
rsc.io/goversion v1.2.0 h1:SPn+NLTiAG7w30IRK/DKp1BjvpWabYgxlLp/+kx5J8w=
rsc.io/goversion v1.2.0/go.mod h1:Eih9y/uIBS3ulggl7KNJ09xGSLcuNaLgmvvqa07sgfo=
tailscale.com v1.2.10/go.mod h1:JEJiCce3MHtPCTdX2ahLc4tcnxZ7b5etish1Yt0B6+w=

View File

@@ -37,7 +37,7 @@ func getVal() []interface{} {
return []interface{}{
&wgcfg.Config{
Name: "foo",
Addresses: []wgcfg.CIDR{{Mask: 5, IP: wgcfg.IP{Addr: [16]byte{3: 3}}}},
Addresses: []netaddr.IPPrefix{{Bits: 5, IP: netaddr.IPFrom16([16]byte{3: 3})}},
ListenPort: 5,
Peers: []wgcfg.Peer{
{

View File

@@ -0,0 +1,9 @@
// 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 tooldeps
import (
_ "github.com/tailscale/depaware/depaware"
)

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

@@ -8,8 +8,8 @@ import (
"sync"
"time"
"github.com/tailscale/wireguard-go/wgcfg"
"golang.org/x/oauth2"
"inet.af/netaddr"
"tailscale.com/control/controlclient"
"tailscale.com/types/logger"
)
@@ -118,7 +118,7 @@ func (h *Handle) EngineStatus() EngineStatus {
return h.engineStatusCache
}
func (h *Handle) LocalAddrs() []wgcfg.CIDR {
func (h *Handle) LocalAddrs() []netaddr.IPPrefix {
h.mu.Lock()
defer h.mu.Unlock()
@@ -126,7 +126,7 @@ func (h *Handle) LocalAddrs() []wgcfg.CIDR {
if nm != nil {
return nm.Addresses
}
return []wgcfg.CIDR{}
return []netaddr.IPPrefix{}
}
func (h *Handle) NetMap() *controlclient.NetworkMap {

View File

@@ -7,23 +7,34 @@ package ipnserver
import (
"bufio"
"context"
"errors"
"fmt"
"io"
"io/ioutil"
"log"
"net"
"net/http"
"os"
"os/exec"
"os/signal"
"os/user"
"runtime"
"strings"
"sync"
"syscall"
"time"
"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"
"tailscale.com/smallzstd"
"tailscale.com/types/logger"
"tailscale.com/util/pidowner"
"tailscale.com/util/systemd"
"tailscale.com/version"
"tailscale.com/wgengine"
)
@@ -61,6 +72,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.
@@ -74,23 +91,189 @@ 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) {
s.addConn(c)
logf("incoming control connection")
// 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{})
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
}
defer s.removeAndCloseConn(c)
logf("[v1] incoming control connection")
for ctx.Err() == nil {
msg, err := ipn.ReadMsg(c)
msg, err := ipn.ReadMsg(br)
if err != nil {
if ctx.Err() == nil {
if errors.Is(err, io.EOF) {
logf("[v1] ReadMsg: %v", err)
} else if ctx.Err() == nil {
logf("ReadMsg: %v", err)
}
return
@@ -107,25 +290,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()
}
@@ -140,9 +424,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)
}
@@ -160,6 +483,7 @@ func Run(ctx context.Context, logf logger.Logf, logid string, getEngine func() (
}
server := &server{
logf: logf,
resetOnZero: !opts.SurviveDisconnects,
}
@@ -176,12 +500,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 {
@@ -189,14 +512,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()
@@ -217,6 +540,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{}
}
@@ -232,18 +573,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,
@@ -253,6 +592,7 @@ func Run(ctx context.Context, logf logger.Logf, logid string, getEngine func() (
})
}
systemd.Ready()
for i := 1; ctx.Err() == nil; i++ {
var c net.Conn
var err error
@@ -274,6 +614,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()
@@ -281,6 +626,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
@@ -375,6 +728,10 @@ func BabysitProc(ctx context.Context, args []string, logf logger.Logf) {
// pipe. We'll make a new one when we restart the subproc.
wStdin.Close()
if os.Getenv("TS_DEBUG_RESTART_CRASHED") == "0" {
log.Fatalf("Process ended.")
}
if time.Since(startTime) < 60*time.Second {
bo.BackOff(ctx, fmt.Errorf("subproc early exit: %v", err))
} else {
@@ -394,3 +751,69 @@ func BabysitProc(ctx context.Context, args []string, logf logger.Logf) {
func FixedEngine(eng wgengine.Engine) func() (wgengine.Engine, error) {
return func() (wgengine.Engine, error) { return eng, nil }
}
type dummyAddr string
type oneConnListener struct {
conn net.Conn
}
func (l *oneConnListener) Accept() (c net.Conn, err error) {
c = l.conn
if c == nil {
err = io.EOF
return
}
err = nil
l.conn = nil
return
}
func (l *oneConnListener) Close() error { return nil }
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) }
// 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 (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 (s *server) localhostHandler(ci connIdentity) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if ci.Unknown {
io.WriteString(w, "<html><title>Tailscale</title><body><h1>Tailscale</h1>This is the local Tailscale daemon.")
return
}
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 {
return e.Pid
}
}
return 0
}

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

@@ -64,6 +64,12 @@ type PeerStatus struct {
LastHandshake time.Time // with local wireguard
KeepAlive bool
// ShareeNode indicates this node exists in the netmap because
// it's owned by a shared-to user and that node might connect
// to us. These nodes should be hidden by "tailscale status"
// etc by default.
ShareeNode bool `json:",omitempty"`
// InNetworkMap means that this peer was seen in our latest network map.
// In theory, all of InNetworkMap and InMagicSock and InEngine should all be true.
InNetworkMap bool
@@ -218,6 +224,9 @@ func (sb *StatusBuilder) AddPeer(peer key.Public, st *PeerStatus) {
if st.KeepAlive {
e.KeepAlive = true
}
if st.ShareeNode {
e.ShareeNode = true
}
}
type StatusUpdater interface {

File diff suppressed because it is too large Load Diff

View File

@@ -5,6 +5,7 @@
package ipn
import (
"reflect"
"testing"
"time"
@@ -20,10 +21,10 @@ import (
// being logged by the expected functions. Update these tests if moving log lines between
// functions.
func TestLocalLogLines(t *testing.T) {
logListen := tstest.ListenFor(t.Logf, []string{
logListen := tstest.NewLogLineTracker(t.Logf, []string{
"SetPrefs: %v",
"peer keys: %s",
"v%v peers: %v",
"[v1] peer keys: %s",
"[v1] v%v peers: %v",
})
logid := func(hex byte) logtail.PublicID {
@@ -48,6 +49,7 @@ func TestLocalLogLines(t *testing.T) {
if err != nil {
t.Fatal(err)
}
defer lb.Shutdown()
// custom adjustments for required non-nil fields
lb.prefs = NewPrefs()
@@ -55,30 +57,10 @@ func TestLocalLogLines(t *testing.T) {
// hacky manual override of the usual log-on-change behaviour of keylogf
lb.keyLogf = logListen.Logf
// testing infrastructure
type linesTest struct {
name string
want []string
}
tests := []linesTest{
{
name: "after prefs",
want: []string{
"peer keys: %s",
"v%v peers: %v",
},
},
{
name: "after peers",
want: []string{},
},
}
testLogs := func(want linesTest) func(t *testing.T) {
testWantRemain := func(wantRemain ...string) func(t *testing.T) {
return func(t *testing.T) {
if linesLeft := logListen.Check(); len(linesLeft) != len(want.want) {
t.Errorf("got %v, expected %v", linesLeft, want)
if remain := logListen.Check(); !reflect.DeepEqual(remain, wantRemain) {
t.Errorf("remain %q, want %q", remain, wantRemain)
}
}
}
@@ -89,7 +71,7 @@ func TestLocalLogLines(t *testing.T) {
prefs.Persist = persist
lb.SetPrefs(prefs)
t.Run(tests[0].name, testLogs(tests[0]))
t.Run("after_prefs", testWantRemain("[v1] peer keys: %s", "[v1] v%v peers: %v"))
// log peers, peer keys
status := &wgengine.Status{
@@ -101,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(tests[1].name, testLogs(tests[1]))
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

@@ -5,57 +5,73 @@
package ipn
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"log"
"os"
"path/filepath"
"runtime"
"strings"
"github.com/tailscale/wireguard-go/wgcfg"
"inet.af/netaddr"
"tailscale.com/atomicfile"
"tailscale.com/control/controlclient"
"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.
// connections. This overrides tailcfg.Hostinfo's ShieldsUp.
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,15 +83,25 @@ 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.
// AdvertiseRoutes specifies CIDR prefixes to advertise into the
// Tailscale network as reachable through the current
// node.
AdvertiseRoutes []wgcfg.CIDR
AdvertiseRoutes []netaddr.IPPrefix
// 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 +113,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 +129,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,24 +194,24 @@ 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)
}
func compareIPNets(a, b []wgcfg.CIDR) bool {
func compareIPNets(a, b []netaddr.IPPrefix) bool {
if len(a) != len(b) {
return false
}
for i := range a {
if !a[i].IP.Equal(b[i].IP) || a[i].Mask != b[i].Mask {
if a[i] != b[i] {
return false
}
}
@@ -213,26 +270,24 @@ 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
}
if bytes.Contains(data, jsonEscapedZero) {
// Tailscale 1.2.0 - 1.2.8 on Windows had a memory corruption bug
// in the backend process that ended up sending NULL bytes over JSON
// to the frontend which wrote them out to JSON files on disk.
// So if we see one, treat is as corrupt and the user will need
// to log in again. (better than crashing)
return nil, os.ErrNotExist
}
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 (
"inet.af/netaddr"
"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 []netaddr.IPPrefix
NoSNAT bool
NetfilterMode router.NetfilterMode
Persist *controlclient.Persist
}{})

View File

@@ -5,12 +5,18 @@
package ipn
import (
"errors"
"fmt"
"io/ioutil"
"os"
"reflect"
"testing"
"time"
"github.com/tailscale/wireguard-go/wgcfg"
"inet.af/netaddr"
"tailscale.com/control/controlclient"
"tailscale.com/tstest"
"tailscale.com/types/wgkey"
"tailscale.com/wgengine/router"
)
@@ -24,15 +30,15 @@ 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)
}
nets := func(strs ...string) (ns []wgcfg.CIDR) {
nets := func(strs ...string) (ns []netaddr.IPPrefix) {
for _, s := range strs {
n, err := wgcfg.ParseCIDR(s)
n, err := netaddr.ParseIPPrefix(s)
if err != nil {
panic(err)
}
@@ -161,12 +167,12 @@ func TestPrefsEqual(t *testing.T) {
{
&Prefs{AdvertiseRoutes: nil},
&Prefs{AdvertiseRoutes: []wgcfg.CIDR{}},
&Prefs{AdvertiseRoutes: []netaddr.IPPrefix{}},
true,
},
{
&Prefs{AdvertiseRoutes: []wgcfg.CIDR{}},
&Prefs{AdvertiseRoutes: []wgcfg.CIDR{}},
&Prefs{AdvertiseRoutes: []netaddr.IPPrefix{}},
&Prefs{AdvertiseRoutes: []netaddr.IPPrefix{}},
true,
},
{
@@ -278,3 +284,114 @@ 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: wgkey.Private{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)
}
// TestLoadPrefsFileWithZeroInIt verifies that LoadPrefs hanldes corrupted input files.
// See issue #954 for details.
func TestLoadPrefsFileWithZeroInIt(t *testing.T) {
f, err := ioutil.TempFile("", "TestLoadPrefsFileWithZeroInIt")
if err != nil {
t.Fatal(err)
}
path := f.Name()
if _, err := f.Write(jsonEscapedZero); err != nil {
t.Fatal(err)
}
f.Close()
defer os.Remove(path)
p, err := LoadPrefs(path)
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 wgkey.Private.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

@@ -15,15 +15,18 @@ import (
"time"
)
// LogHeap writes a JSON logtail record with the base64 heap pprof to
// os.Stderr.
// LogHeap uploads a JSON logtail record with the base64 heap pprof by means
// of an HTTP POST request to the endpoint referred to in postURL.
func LogHeap(postURL string) {
if postURL == "" {
return
}
runtime.GC()
buf := new(bytes.Buffer)
pprof.WriteHeapProfile(buf)
if err := pprof.WriteHeapProfile(buf); err != nil {
log.Printf("LogHeap: %v", err)
return
}
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()

View File

@@ -25,7 +25,7 @@ import (
"strings"
"time"
"golang.org/x/crypto/ssh/terminal"
"golang.org/x/term"
"tailscale.com/atomicfile"
"tailscale.com/logtail"
"tailscale.com/logtail/filch"
@@ -35,6 +35,7 @@ import (
"tailscale.com/paths"
"tailscale.com/smallzstd"
"tailscale.com/types/logger"
"tailscale.com/util/racebuild"
"tailscale.com/version"
)
@@ -48,7 +49,7 @@ type Config struct {
// Policy is a logger and its public ID.
type Policy struct {
// Logtail is the logger.
Logtail logtail.Logger
Logtail *logtail.Logger
// PublicID is the logger's instance identifier.
PublicID logtail.PublicID
}
@@ -310,11 +311,14 @@ func tryFixLogStateLocation(dir, cmdname string) {
// given collection name.
func New(collection string) *Policy {
var lflags int
if terminal.IsTerminal(2) || runtime.GOOS == "windows" {
if term.IsTerminal(2) || runtime.GOOS == "windows" {
lflags = 0
} 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 +360,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")
@@ -387,13 +391,13 @@ func New(collection string) *Policy {
if filchBuf != nil {
c.Buffer = filchBuf
}
lw := logtail.Log(c, log.Printf)
lw := logtail.NewLogger(c, log.Printf)
log.SetFlags(0) // other logflags are set on console, not here
log.SetOutput(lw)
log.Printf("Program starting: v%v, Go %v: %#v",
version.LONG,
strings.TrimPrefix(runtime.Version(), "go"),
version.Long,
goVersion(),
os.Args)
log.Printf("LogID: %v", newc.PublicID)
if filchErr != nil {
@@ -409,6 +413,15 @@ func New(collection string) *Policy {
}
}
// SetVerbosityLevel controls the verbosity level that should be
// written to stderr. 0 is the default (not verbose). Levels 1 or higher
// are increasingly verbose.
//
// It should not be changed concurrently with log writes.
func (p *Policy) SetVerbosityLevel(level int) {
p.Logtail.SetVerbosityLevel(level)
}
// Close immediately shuts down the logger.
func (p *Policy) Close() {
ctx, cancel := context.WithCancel(context.Background())
@@ -476,3 +489,11 @@ func newLogtailTransport(host string) *http.Transport {
return tr
}
func goVersion() string {
v := strings.TrimPrefix(runtime.Version(), "go")
if racebuild.On {
return v + "-race"
}
return v
}

View File

@@ -31,7 +31,7 @@ func main() {
log.Fatalf("logtail: bad -privateid: %v", err)
}
logger := logtail.Log(logtail.Config{
logger := logtail.NewLogger(logtail.Config{
Collection: *collection,
PrivateID: id,
}, log.Printf)

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

@@ -14,6 +14,7 @@ import (
"io/ioutil"
"net/http"
"os"
"strconv"
"time"
"tailscale.com/logtail/backoff"
@@ -24,34 +25,6 @@ import (
// Config.BaseURL isn't provided.
const DefaultHost = "log.tailscale.io"
type Logger interface {
// Write logs an encoded JSON blob.
//
// If the []byte passed to Write is not an encoded JSON blob,
// then contents is fit into a JSON blob and written.
//
// This is intended as an interface for the stdlib "log" package.
Write([]byte) (int, error)
// Flush uploads all logs to the server.
// It blocks until complete or there is an unrecoverable error.
Flush() error
// Shutdown gracefully shuts down the logger while completing any
// remaining uploads.
//
// It will block, continuing to try and upload unless the passed
// context object interrupts it by being done.
// If the shutdown is interrupted, an error is returned.
Shutdown(context.Context) error
// Close shuts down this logger object, the background log uploader
// process, and any associated goroutines.
//
// DEPRECATED: use Shutdown
Close()
}
type Encoder interface {
EncodeAll(src, dst []byte) []byte
Close() error
@@ -66,15 +39,16 @@ type Config struct {
LowMemory bool // if true, logtail minimizes memory use
TimeNow func() time.Time // if set, subsitutes uses of time.Now
Stderr io.Writer // if set, logs are sent here instead of os.Stderr
StderrLevel int // max verbosity level to write to stderr; 0 means the non-verbose messages only
Buffer Buffer // temp storage, if nil a MemoryBuffer
NewZstdEncoder func() Encoder // if set, used to compress logs for transmission
// DrainLogs, if non-nil, disables autmatic uploading of new logs,
// DrainLogs, if non-nil, disables automatic uploading of new logs,
// so that logs are only uploaded when a token is sent to DrainLogs.
DrainLogs <-chan struct{}
}
func Log(cfg Config, logf tslogger.Logf) Logger {
func NewLogger(cfg Config, logf tslogger.Logf) *Logger {
if cfg.BaseURL == "" {
cfg.BaseURL = "https://" + DefaultHost
}
@@ -94,8 +68,9 @@ func Log(cfg Config, logf tslogger.Logf) Logger {
}
cfg.Buffer = NewMemoryBuffer(pendingSize)
}
l := &logger{
l := &Logger{
stderr: cfg.Stderr,
stderrLevel: cfg.StderrLevel,
httpc: cfg.HTTPC,
url: cfg.BaseURL + "/c/" + cfg.Collection + "/" + cfg.PrivateID.String(),
lowMem: cfg.LowMemory,
@@ -122,8 +97,11 @@ func Log(cfg Config, logf tslogger.Logf) Logger {
return l
}
type logger struct {
// Logger writes logs, splitting them as configured between local
// logging facilities and uploading to a log server.
type Logger struct {
stderr io.Writer
stderrLevel int
httpc *http.Client
url string
lowMem bool
@@ -141,7 +119,22 @@ type logger struct {
shutdownDone chan struct{} // closd when shutdown complete
}
func (l *logger) Shutdown(ctx context.Context) error {
// SetVerbosityLevel controls the verbosity level that should be
// written to stderr. 0 is the default (not verbose). Levels 1 or higher
// are increasingly verbose.
//
// It should not be changed concurrently with log writes.
func (l *Logger) SetVerbosityLevel(level int) {
l.stderrLevel = level
}
// Shutdown gracefully shuts down the logger while completing any
// remaining uploads.
//
// It will block, continuing to try and upload unless the passed
// context object interrupts it by being done.
// If the shutdown is interrupted, an error is returned.
func (l *Logger) Shutdown(ctx context.Context) error {
done := make(chan struct{})
go func() {
select {
@@ -163,7 +156,11 @@ func (l *logger) Shutdown(ctx context.Context) error {
return nil
}
func (l *logger) Close() {
// Close shuts down this logger object, the background log uploader
// process, and any associated goroutines.
//
// Deprecated: use Shutdown
func (l *Logger) Close() {
l.Shutdown(context.Background())
}
@@ -174,7 +171,7 @@ func (l *logger) Close() {
//
// If the caller provides a DrainLogs channel, then unblock-drain-on-Write
// is disabled, and it is up to the caller to trigger unblock the drain.
func (l *logger) drainBlock() (shuttingDown bool) {
func (l *Logger) drainBlock() (shuttingDown bool) {
if l.drainLogs == nil {
select {
case <-l.shutdownStart:
@@ -193,7 +190,7 @@ func (l *logger) drainBlock() (shuttingDown bool) {
// drainPending drains and encodes a batch of logs from the buffer for upload.
// If no logs are available, drainPending blocks until logs are available.
func (l *logger) drainPending() (res []byte) {
func (l *Logger) drainPending() (res []byte) {
buf := new(bytes.Buffer)
entries := 0
@@ -254,13 +251,22 @@ func (l *logger) drainPending() (res []byte) {
}
// This is the goroutine that repeatedly uploads logs in the background.
func (l *logger) uploading(ctx context.Context) {
func (l *Logger) uploading(ctx context.Context) {
defer close(l.shutdownDone)
for {
body := l.drainPending()
if l.zstdEncoder != nil {
body = l.zstdEncoder.EncodeAll(body, nil)
origlen := -1 // sentinel value: uncompressed
// Don't attempt to compress tiny bodies; not worth the CPU cycles.
if l.zstdEncoder != nil && len(body) > 256 {
zbody := l.zstdEncoder.EncodeAll(body, nil)
// Only send it compressed if the bandwidth savings are sufficient.
// Just the extra headers associated with enabling compression
// are 50 bytes by themselves.
if len(body)-len(zbody) > 64 {
origlen = len(body)
body = zbody
}
}
for len(body) > 0 {
@@ -269,7 +275,7 @@ func (l *logger) uploading(ctx context.Context) {
return
default:
}
uploaded, err := l.upload(ctx, body)
uploaded, err := l.upload(ctx, body, origlen)
if err != nil {
fmt.Fprintf(l.stderr, "logtail: upload: %v\n", err)
}
@@ -287,7 +293,10 @@ func (l *logger) uploading(ctx context.Context) {
}
}
func (l *logger) upload(ctx context.Context, body []byte) (uploaded bool, err error) {
// upload uploads body to the log server.
// origlen indicates the pre-compression body length.
// origlen of -1 indicates that the body is not compressed.
func (l *Logger) upload(ctx context.Context, body []byte, origlen int) (uploaded bool, err error) {
req, err := http.NewRequest("POST", l.url, bytes.NewReader(body))
if err != nil {
// I know of no conditions under which this could fail.
@@ -295,8 +304,9 @@ func (l *logger) upload(ctx context.Context, body []byte) (uploaded bool, err er
// TODO record logs to disk
panic("logtail: cannot build http request: " + err.Error())
}
if l.zstdEncoder != nil {
if origlen != -1 {
req.Header.Add("Content-Encoding", "zstd")
req.Header.Add("Orig-Content-Length", strconv.Itoa(origlen))
}
req.Header["User-Agent"] = nil // not worth writing one; save some bytes
@@ -306,7 +316,7 @@ func (l *logger) upload(ctx context.Context, body []byte) (uploaded bool, err er
req = req.WithContext(ctx)
compressedNote := "not-compressed"
if l.zstdEncoder != nil {
if origlen != -1 {
compressedNote = "compressed"
}
@@ -333,11 +343,13 @@ func (l *logger) upload(ctx context.Context, body []byte) (uploaded bool, err er
return true, nil
}
func (l *logger) Flush() error {
// Flush uploads all logs to the server.
// It blocks until complete or there is an unrecoverable error.
func (l *Logger) Flush() error {
return nil
}
func (l *logger) send(jsonBlob []byte) (int, error) {
func (l *Logger) send(jsonBlob []byte) (int, error) {
n, err := l.buffer.Write(jsonBlob)
if l.drainLogs == nil {
select {
@@ -350,7 +362,7 @@ func (l *logger) send(jsonBlob []byte) (int, error) {
// TODO: instead of allocating, this should probably just append
// directly into the output log buffer.
func (l *logger) encodeText(buf []byte, skipClientTime bool) []byte {
func (l *Logger) encodeText(buf []byte, skipClientTime bool) []byte {
now := l.timeNow()
// Factor in JSON encoding overhead to try to only do one alloc
@@ -406,7 +418,7 @@ func (l *logger) encodeText(buf []byte, skipClientTime bool) []byte {
return b
}
func (l *logger) encode(buf []byte) []byte {
func (l *Logger) encode(buf []byte) []byte {
if buf[0] != '{' {
return l.encodeText(buf, l.skipClientTime) // text fast-path
}
@@ -447,11 +459,18 @@ func (l *logger) encode(buf []byte) []byte {
return b
}
func (l *logger) Write(buf []byte) (int, error) {
// Write logs an encoded JSON blob.
//
// If the []byte passed to Write is not an encoded JSON blob,
// then contents is fit into a JSON blob and written.
//
// This is intended as an interface for the stdlib "log" package.
func (l *Logger) Write(buf []byte) (int, error) {
if len(buf) == 0 {
return 0, nil
}
if l.stderr != nil && l.stderr != ioutil.Discard {
level, buf := parseAndRemoveLogLevel(buf)
if l.stderr != nil && l.stderr != ioutil.Discard && level <= l.stderrLevel {
if buf[len(buf)-1] == '\n' {
l.stderr.Write(buf)
} else {
@@ -465,3 +484,23 @@ func (l *logger) Write(buf []byte) (int, error) {
_, err := l.send(b)
return len(buf), err
}
var (
openBracketV = []byte("[v")
v1 = []byte("[v1] ")
v2 = []byte("[v2] ")
)
// level 0 is normal (or unknown) level; 1+ are increasingly verbose
func parseAndRemoveLogLevel(buf []byte) (level int, cleanBuf []byte) {
if len(buf) == 0 || buf[0] == '{' || !bytes.Contains(buf, openBracketV) {
return 0, buf
}
if bytes.Contains(buf, v1) {
return 1, bytes.ReplaceAll(buf, v1, nil)
}
if bytes.Contains(buf, v2) {
return 2, bytes.ReplaceAll(buf, v2, nil)
}
return 0, buf
}

View File

@@ -14,7 +14,7 @@ func TestFastShutdown(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
cancel()
l := Log(Config{
l := NewLogger(Config{
BaseURL: "http://localhost:1234",
}, t.Logf)
l.Shutdown(ctx)
@@ -23,7 +23,7 @@ func TestFastShutdown(t *testing.T) {
var sink []byte
func TestLoggerEncodeTextAllocs(t *testing.T) {
lg := &logger{timeNow: time.Now}
lg := &Logger{timeNow: time.Now}
inBuf := []byte("some text to encode")
n := testing.AllocsPerRun(1000, func() {
sink = lg.encodeText(inBuf, false)
@@ -34,7 +34,7 @@ func TestLoggerEncodeTextAllocs(t *testing.T) {
}
func TestLoggerWriteLength(t *testing.T) {
lg := &logger{
lg := &Logger{
timeNow: time.Now,
buffer: NewMemoryBuffer(1024),
}

View File

@@ -9,8 +9,11 @@ package dnscache
import (
"context"
"fmt"
"log"
"net"
"os"
"runtime"
"strconv"
"sync"
"time"
@@ -25,7 +28,7 @@ func preferGoResolver() bool {
// There does not appear to be a local resolver running
// on iOS, and NetworkExtension is good at isolating DNS.
// So do not use the Go resolver on macOS/iOS.
if runtime.GOOS == "darwin" {
if runtime.GOOS == "darwin" || runtime.GOOS == "ios" {
return false
}
@@ -42,8 +45,6 @@ func preferGoResolver() bool {
// Get returns a caching Resolver singleton.
func Get() *Resolver { return single }
const fixedTTL = 10 * time.Minute
// Resolver is a minimal DNS caching resolver.
//
// The TTL is always fixed for now. It's not intended for general use.
@@ -54,6 +55,15 @@ type Resolver struct {
// If nil, net.DefaultResolver is used.
Forward *net.Resolver
// TTL is how long to keep entries cached
//
// If zero, a default (currently 10 minutes) is used.
TTL time.Duration
// UseLastGood controls whether a cached entry older than TTL is used
// if a refresh fails.
UseLastGood bool
sf singleflight.Group
mu sync.Mutex
@@ -72,16 +82,31 @@ func (r *Resolver) fwd() *net.Resolver {
return net.DefaultResolver
}
func (r *Resolver) ttl() time.Duration {
if r.TTL > 0 {
return r.TTL
}
return 10 * time.Minute
}
var debug, _ = strconv.ParseBool(os.Getenv("TS_DEBUG_DNS_CACHE"))
// LookupIP returns the first IPv4 address found, otherwise the first IPv6 address.
func (r *Resolver) LookupIP(ctx context.Context, host string) (net.IP, error) {
if ip := net.ParseIP(host); ip != nil {
if ip4 := ip.To4(); ip4 != nil {
return ip4, nil
}
if debug {
log.Printf("dnscache: %q is an IP", host)
}
return ip, nil
}
if ip, ok := r.lookupIPCache(host); ok {
if debug {
log.Printf("dnscache: %q = %v (cached)", host, ip)
}
return ip, nil
}
@@ -95,10 +120,24 @@ func (r *Resolver) LookupIP(ctx context.Context, host string) (net.IP, error) {
select {
case res := <-ch:
if res.Err != nil {
if r.UseLastGood {
if ip, ok := r.lookupIPCacheExpired(host); ok {
if debug {
log.Printf("dnscache: %q using %v after error", host, ip)
}
return ip, nil
}
}
if debug {
log.Printf("dnscache: error resolving %q: %v", host, res.Err)
}
return nil, res.Err
}
return res.Val.(net.IP), nil
case <-ctx.Done():
if debug {
log.Printf("dnscache: context done while resolving %q: %v", host, ctx.Err())
}
return nil, ctx.Err()
}
}
@@ -112,12 +151,41 @@ func (r *Resolver) lookupIPCache(host string) (ip net.IP, ok bool) {
return nil, false
}
func (r *Resolver) lookupIPCacheExpired(host string) (ip net.IP, ok bool) {
r.mu.Lock()
defer r.mu.Unlock()
if ent, ok := r.ipCache[host]; ok {
return ent.ip, true
}
return nil, false
}
func (r *Resolver) lookupTimeoutForHost(host string) time.Duration {
if r.UseLastGood {
if _, ok := r.lookupIPCacheExpired(host); ok {
// If we have some previous good value for this host,
// don't give this DNS lookup much time. If we're in a
// situation where the user's DNS server is unreachable
// (e.g. their corp DNS server is behind a subnet router
// that can't come up due to Tailscale needing to
// connect to itself), then we want to fail fast and let
// our caller (who set UseLastGood) fall back to using
// the last-known-good IP address.
return 3 * time.Second
}
}
return 10 * time.Second
}
func (r *Resolver) lookupIP(host string) (net.IP, error) {
if ip, ok := r.lookupIPCache(host); ok {
if debug {
log.Printf("dnscache: %q found in cache as %v", host, ip)
}
return ip, nil
}
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
ctx, cancel := context.WithTimeout(context.Background(), r.lookupTimeoutForHost(host))
defer cancel()
ips, err := r.fwd().LookupIPAddr(ctx, host)
if err != nil {
@@ -129,19 +197,26 @@ func (r *Resolver) lookupIP(host string) (net.IP, error) {
for _, ipa := range ips {
if ip4 := ipa.IP.To4(); ip4 != nil {
return r.addIPCache(host, ip4, fixedTTL), nil
return r.addIPCache(host, ip4, r.ttl()), nil
}
}
return r.addIPCache(host, ips[0].IP, fixedTTL), nil
return r.addIPCache(host, ips[0].IP, r.ttl()), nil
}
func (r *Resolver) addIPCache(host string, ip net.IP, d time.Duration) net.IP {
if isPrivateIP(ip) {
// Don't cache obviously wrong entries from captive portals.
// TODO: use DoH or DoT for the forwarding resolver?
if debug {
log.Printf("dnscache: %q resolved to private IP %v; using but not caching", host, ip)
}
return ip
}
if debug {
log.Printf("dnscache: %q resolved to IP %v; caching", host, ip)
}
r.mu.Lock()
defer r.mu.Unlock()
if r.ipCache == nil {
@@ -168,3 +243,26 @@ var (
private2 = mustCIDR("172.16.0.0/12")
private3 = mustCIDR("192.168.0.0/16")
)
type DialContextFunc func(ctx context.Context, network, address string) (net.Conn, error)
// Dialer returns a wrapped DialContext func that uses the provided dnsCache.
func Dialer(fwd DialContextFunc, dnsCache *Resolver) DialContextFunc {
return func(ctx context.Context, network, address string) (net.Conn, error) {
host, port, err := net.SplitHostPort(address)
if err != nil {
// Bogus. But just let the real dialer return an error rather than
// inventing a similar one.
return fwd(ctx, network, address)
}
ip, err := dnsCache.LookupIP(ctx, host)
if err != nil {
return nil, fmt.Errorf("failed to resolve %q: %w", host, err)
}
dst := net.JoinHostPort(ip.String(), port)
if debug {
log.Printf("dnscache: dialing %s, %s for %s", network, dst, address)
}
return fwd(ctx, network, dst)
}
}

View File

@@ -10,6 +10,7 @@ import (
"net"
"net/http"
"reflect"
"sort"
"strings"
"inet.af/netaddr"
@@ -93,11 +94,6 @@ func LocalAddresses() (regular, loopback []string, err error) {
if !ok {
continue
}
if ip.Is6() {
// TODO(crawshaw): IPv6 support.
// Easy to do here, but we need good endpoint ordering logic.
continue
}
// TODO(apenwarr): don't special case cgNAT.
// In the general wireguard case, it might
// very well be something we can route to
@@ -160,10 +156,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 +170,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 +277,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 +391,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

@@ -5,6 +5,7 @@
package interfaces
import (
"errors"
"os/exec"
"go4.org/mem"
@@ -62,8 +63,12 @@ func likelyHomeRouterIPDarwinExec() (ret netaddr.IP, ok bool) {
ip, err := netaddr.ParseIP(string(mem.Append(nil, ipm)))
if err == nil && isPrivateIP(ip) {
ret = ip
// We've found what we're looking for.
return errStopReadingNetstatTable
}
return nil
})
return ret, !ret.IsZero()
}
var errStopReadingNetstatTable = errors.New("found private gateway")

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,16 +5,24 @@
package interfaces
import (
"fmt"
"log"
"net/url"
"os/exec"
"syscall"
"unsafe"
"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"
)
func init() {
likelyHomeRouterIP = likelyHomeRouterIPWindows
getPAC = getPACWindows
}
/*
@@ -71,3 +79,118 @@ func likelyHomeRouterIPWindows() (ret netaddr.IP, ok bool) {
})
return ret, !ret.IsZero()
}
// NonTailscaleMTUs returns a map of interface LUID to interface MTU,
// for all interfaces except Tailscale tunnels.
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[winipcfg.LUID]*winipcfg.IPAdapterAddresses{}
for _, iface := range ifs {
if iface.Description() == tsconst.WintunInterfaceDesc {
continue
}
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

@@ -28,7 +28,6 @@ import (
"go4.org/mem"
"inet.af/netaddr"
"tailscale.com/derp/derphttp"
"tailscale.com/net/dnscache"
"tailscale.com/net/interfaces"
"tailscale.com/net/netns"
"tailscale.com/net/stun"
@@ -134,10 +133,6 @@ func cloneDurationMap(m map[int]time.Duration) map[int]time.Duration {
// Client generates a netcheck Report.
type Client struct {
// DNSCache optionally specifies a DNSCache to use.
// If nil, a DNS cache is not used.
DNSCache *dnscache.Resolver
// Verbose enables verbose logging.
Verbose bool
@@ -156,6 +151,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
@@ -236,7 +240,6 @@ func (c *Client) ReceiveSTUNPacket(pkt []byte, src netaddr.IPPort) {
tx, addr, port, err := stun.ParseResponse(pkt)
if err != nil {
c.mu.Unlock()
if _, err := stun.ParseBindingRequest(pkt); err == nil {
// This was probably our own netcheck hairpin
// check probe coming in late. Ignore.
@@ -689,7 +692,7 @@ func (rs *reportState) probePortMapServices() {
port1900 := netaddr.IPPort{IP: gw, Port: 1900}.UDPAddr()
port5351 := netaddr.IPPort{IP: gw, Port: 5351}.UDPAddr()
rs.c.logf("probePortMapServices: me %v -> gw %v", myIP, gw)
rs.c.logf("[v1] probePortMapServices: me %v -> gw %v", myIP, gw)
// Create a UDP4 socket used just for querying for UPnP, NAT-PMP, and PCP.
uc, err := netns.Listener().ListenPacket(context.Background(), "udp4", ":0")
@@ -707,6 +710,7 @@ func (rs *reportState) probePortMapServices() {
uc.WriteTo(pcpPacket(myIP, tempPort, false), port5351)
res := make([]byte, 1500)
sentPCPDelete := false
for {
n, addr, err := uc.ReadFrom(res)
if err != nil {
@@ -724,11 +728,14 @@ func (rs *reportState) probePortMapServices() {
if n == 60 && res[0] == 0x02 { // right length and version 2
rs.setOptBool(&rs.report.PCP, true)
// And now delete the mapping.
// (PCP is the only protocol of the three that requires
// we cause a side effect to detect whether it's present,
// so we need to redo that side effect now.)
uc.WriteTo(pcpPacket(myIP, tempPort, true), port5351)
if !sentPCPDelete {
sentPCPDelete = true
// And now delete the mapping.
// (PCP is the only protocol of the three that requires
// we cause a side effect to detect whether it's present,
// so we need to redo that side effect now.)
uc.WriteTo(pcpPacket(myIP, tempPort, true), port5351)
}
}
}
}
@@ -744,6 +751,7 @@ var uPnPPacket = []byte("M-SEARCH * HTTP/1.1\r\n" +
var v4unspec, _ = netaddr.ParseIP("0.0.0.0")
// pcpPacket generates a PCP packet with a MAP opcode.
func pcpPacket(myIP netaddr.IP, mapToLocalPort int, delete bool) []byte {
const udpProtoNumber = 17
lifetimeSeconds := uint32(1)
@@ -751,17 +759,24 @@ func pcpPacket(myIP netaddr.IP, mapToLocalPort int, delete bool) []byte {
lifetimeSeconds = 0
}
const opMap = 1
// 24 byte header + 36 byte map opcode
pkt := make([]byte, (32+32+128)/8+(96+8+24+16+16+128)/8)
// The header (https://tools.ietf.org/html/rfc6887#section-7.1)
pkt[0] = 2 // version
pkt[1] = opMap
binary.BigEndian.PutUint32(pkt[4:8], lifetimeSeconds)
myIP16 := myIP.As16()
copy(pkt[8:], myIP16[:])
rand.Read(pkt[24 : 24+12])
pkt[36] = udpProtoNumber
binary.BigEndian.PutUint16(pkt[40:], uint16(mapToLocalPort))
// The map opcode body (https://tools.ietf.org/html/rfc6887#section-11.1)
mapOp := pkt[24:]
rand.Read(mapOp[:12]) // 96 bit mappping nonce
mapOp[12] = udpProtoNumber
binary.BigEndian.PutUint16(mapOp[16:], uint16(mapToLocalPort))
v4unspec16 := v4unspec.As16()
copy(pkt[40:], v4unspec16[:])
copy(mapOp[20:], v4unspec16[:])
return pkt
}
@@ -773,6 +788,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.
@@ -820,7 +842,7 @@ func (c *Client) GetReport(ctx context.Context, dm *tailcfg.DERPMap) (*Report, e
ifState, err := interfaces.GetState()
if err != nil {
c.logf("interfaces: %v", err)
c.logf("[v1] interfaces: %v", err)
return nil, err
}
@@ -832,8 +854,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
@@ -851,7 +875,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
@@ -864,7 +888,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 {
@@ -903,8 +927,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.
@@ -925,7 +951,7 @@ func (c *Client) GetReport(ctx context.Context, dm *tailcfg.DERPMap) (*Report, e
go func(reg *tailcfg.DERPRegion) {
defer wg.Done()
if d, ip, err := c.measureHTTPSLatency(ctx, reg); err != nil {
c.logf("netcheck: measuring HTTPS latency of %v (%d): %v", reg.RegionCode, reg.RegionID, err)
c.logf("[v1] netcheck: measuring HTTPS latency of %v (%d): %v", reg.RegionCode, reg.RegionID, err)
} else {
rs.mu.Lock()
rs.report.RegionLatency[reg.RegionID] = d
@@ -1019,7 +1045,7 @@ func (c *Client) measureHTTPSLatency(ctx context.Context, reg *tailcfg.DERPRegio
}
func (c *Client) logConciseReport(r *Report, dm *tailcfg.DERPMap) {
c.logf("%v", logger.ArgWriter(func(w *bufio.Writer) {
c.logf("[v1] report: %v", logger.ArgWriter(func(w *bufio.Writer) {
fmt.Fprintf(w, "udp=%v", r.UDP)
if !r.IPv4 {
fmt.Fprintf(w, " v4=%v", r.IPv4)

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)
@@ -553,7 +554,7 @@ func TestLogConciseReport(t *testing.T) {
var buf bytes.Buffer
c := &Client{Logf: func(f string, a ...interface{}) { fmt.Fprintf(&buf, f, a...) }}
c.logConciseReport(tt.r, dm)
if got := buf.String(); got != tt.want {
if got := strings.TrimPrefix(buf.String(), "[v1] report: "); got != tt.want {
t.Errorf("unexpected result.\n got: %#q\nwant: %#q\n", got, tt.want)
}
})

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 netns

121
net/netns/netns_windows.go Normal file
View File

@@ -0,0 +1,121 @@
// 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 netns
import (
"math/bits"
"strings"
"syscall"
"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":
canV4, canV6 = true, true
case "tcp4", "udp4":
canV4 = true
case "tcp6", "udp6":
canV6 = true
}
if canV4 {
iface, err := interfaces.GetWindowsDefault(windows.AF_INET)
if err != nil {
return err
}
if err := bindSocket4(c, interfaceIndex(iface)); err != nil {
return err
}
}
if canV6 {
iface, err := interfaces.GetWindowsDefault(windows.AF_INET6)
if err != nil {
return err
}
if err := bindSocket6(c, interfaceIndex(iface)); err != nil {
return err
}
}
return 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 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(indexAsAddr))
})
if err != nil {
return err
}
return controlErr
}
// bindSocket6 binds the given RawConn to the network interface with
// index ifidx, for IPv6 traffic only.
func bindSocket6(c syscall.RawConn, ifidx uint32) error {
var controlErr error
err := c.Control(func(fd uintptr) {
controlErr = windows.SetsockoptInt(windows.Handle(fd), windows.IPPROTO_IPV6, sockoptBoundInterface, int(ifidx))
})
if err != nil {
return err
}
return controlErr
}
// nativeToBigEndian returns i converted into big-endian
// representation, suitable for passing to Windows APIs that require a
// mangled uint32.
func nativeToBigEndian(i uint32) uint32 {
if endian.Big {
return i
}
return bits.ReverseBytes32(i)
}

36
net/netstat/netstat.go Normal file
View File

@@ -0,0 +1,36 @@
// 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 netstat returns the local machine's network connection table.
package netstat
import (
"errors"
"runtime"
"inet.af/netaddr"
)
var ErrNotImplemented = errors.New("not implemented for GOOS=" + runtime.GOOS)
type Entry struct {
Local, Remote netaddr.IPPort
Pid int
State string // TODO: type?
}
// Table contains local machine's TCP connection entries.
//
// Currently only TCP (IPv4 and IPv6) are included.
type Table struct {
Entries []Entry
}
// Get returns the connection table.
//
// It returns ErrNotImplemented if the table is not available for the
// current operating system.
func Get() (*Table, error) {
return get()
}

View File

@@ -0,0 +1,11 @@
// 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 !windows
package netstat
func get() (*Table, error) {
return nil, ErrNotImplemented
}

View File

@@ -0,0 +1,22 @@
// 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 netstat
import (
"testing"
)
func TestGet(t *testing.T) {
nt, err := Get()
if err == ErrNotImplemented {
t.Skip("TODO: not implemented")
}
if err != nil {
t.Fatal(err)
}
for _, e := range nt.Entries {
t.Logf("Entry: %+v", e)
}
}

View File

@@ -0,0 +1,183 @@
// 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 netstat returns the local machine's network connection table.
package netstat
import (
"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
// TCP_TABLE_OWNER_PID_ALL means to include the PID info. The table type
// we get back from Windows depends on AF_INET vs AF_INET6:
// MIB_TCPTABLE_OWNER_PID for v4 or MIB_TCP6TABLE_OWNER_PID for v6.
const tcpTableOwnerPidAll = 5
var (
iphlpapi = syscall.NewLazyDLL("iphlpapi.dll")
getTCPTable = iphlpapi.NewProc("GetExtendedTcpTable")
// TODO: GetExtendedUdpTable also? if/when needed.
)
type _MIB_TCPROW_OWNER_PID struct {
state uint32
localAddr uint32
localPort uint32
remoteAddr uint32
remotePort uint32
pid uint32
}
type _MIB_TCP6ROW_OWNER_PID struct {
localAddr [16]byte
localScope uint32
localPort uint32
remoteAddr [16]byte
remoteScope uint32
remotePort uint32
state uint32
pid uint32
}
func get() (*Table, error) {
t := new(Table)
if err := t.addEntries(windows.AF_INET); err != nil {
return nil, fmt.Errorf("failed to get IPv4 entries: %w", err)
}
if err := t.addEntries(windows.AF_INET6); err != nil {
return nil, fmt.Errorf("failed to get IPv6 entries: %w", err)
}
return t, nil
}
func (t *Table) addEntries(fam int) error {
var size uint32
var addr unsafe.Pointer
var buf []byte
for {
err, _, _ := getTCPTable.Call(
uintptr(addr),
uintptr(unsafe.Pointer(&size)),
1, // sorted
uintptr(fam),
tcpTableOwnerPidAll,
0, // reserved; "must be zero"
)
if err == 0 {
break
}
if err == uintptr(syscall.ERROR_INSUFFICIENT_BUFFER) {
const maxSize = 10 << 20
if size > maxSize || size < 4 {
return fmt.Errorf("unreasonable kernel-reported size %d", size)
}
buf = make([]byte, size)
addr = unsafe.Pointer(&buf[0])
continue
}
return syscall.Errno(err)
}
if len(buf) < int(size) {
return errors.New("unexpected size growth from system call")
}
buf = buf[:size]
numEntries := endian.Native.Uint32(buf[:4])
buf = buf[4:]
var recSize int
switch fam {
case windows.AF_INET:
recSize = 6 * 4
case windows.AF_INET6:
recSize = 6*4 + 16*2
}
dataLen := numEntries * uint32(recSize)
if uint32(len(buf)) > dataLen {
buf = buf[:dataLen]
}
for len(buf) >= recSize {
switch fam {
case windows.AF_INET:
row := (*_MIB_TCPROW_OWNER_PID)(unsafe.Pointer(&buf[0]))
t.Entries = append(t.Entries, Entry{
Local: ipport4(row.localAddr, port(&row.localPort)),
Remote: ipport4(row.remoteAddr, port(&row.remotePort)),
Pid: int(row.pid),
State: state(row.state),
})
case windows.AF_INET6:
row := (*_MIB_TCP6ROW_OWNER_PID)(unsafe.Pointer(&buf[0]))
t.Entries = append(t.Entries, Entry{
Local: ipport6(row.localAddr, row.localScope, port(&row.localPort)),
Remote: ipport6(row.remoteAddr, row.remoteScope, port(&row.remotePort)),
Pid: int(row.pid),
State: state(row.state),
})
}
buf = buf[recSize:]
}
return nil
}
var states = []string{
"",
"CLOSED",
"LISTEN",
"SYN-SENT",
"SYN-RECEIVED",
"ESTABLISHED",
"FIN-WAIT-1",
"FIN-WAIT-2",
"CLOSE-WAIT",
"CLOSING",
"LAST-ACK",
"DELETE-TCB",
}
func state(v uint32) string {
if v < uint32(len(states)) {
return states[v]
}
return fmt.Sprintf("unknown-state-%d", v)
}
func ipport4(addr uint32, port uint16) netaddr.IPPort {
if !endian.Big {
addr = bits.ReverseBytes32(addr)
}
return netaddr.IPPort{
IP: netaddr.IPv4(byte(addr>>24), byte(addr>>16), byte(addr>>8), byte(addr)),
Port: port,
}
}
func ipport6(addr [16]byte, scope uint32, port uint16) netaddr.IPPort {
ip := netaddr.IPFrom16(addr)
if scope != 0 {
// TODO: something better here?
ip = ip.WithZone(fmt.Sprint(scope))
}
return netaddr.IPPort{
IP: ip,
Port: port,
}
}
func port(v *uint32) uint16 {
if !endian.Big {
return uint16(bits.ReverseBytes32(*v) >> 16)
}
return uint16(*v >> 16)
}

16
net/packet/doc.go Normal file
View File

@@ -0,0 +1,16 @@
// 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 packet contains packet parsing and marshaling utilities.
//
// Parsed provides allocation-free minimal packet header decoding, for
// use in packet filtering. The other types in the package are for
// constructing and marshaling packets into []bytes.
//
// To support allocation-free parsing, this package defines IPv4 and
// IPv6 address types. You should prefer to use netaddr's types,
// except where you absolutely need allocation-free IP handling
// (i.e. in the tunnel datapath) and are willing to implement all
// codepaths and data structures twice, once per IP family.
package packet

View File

@@ -16,27 +16,34 @@ const tcpHeaderLength = 20
const maxPacketLength = math.MaxUint16
var (
// errSmallBuffer is returned when Marshal receives a buffer
// too small to contain the header to marshal.
errSmallBuffer = errors.New("buffer too small")
// errLargePacket is returned when Marshal receives a payload
// larger than the maximum representable size in header
// fields.
errLargePacket = errors.New("packet too large")
)
// Header is a packet header capable of marshaling itself into a byte buffer.
// Header is a packet header capable of marshaling itself into a byte
// buffer.
type Header interface {
// Len returns the length of the header after marshaling.
// Len returns the length of the marshaled packet.
Len() int
// Marshal serializes the header into buf in wire format.
// It clobbers the header region, which is the first h.Length() bytes of buf.
// It explicitly initializes every byte of the header region,
// so pre-zeroing it on reuse is not required. It does not allocate memory.
// It fails if and only if len(buf) < Length().
// Marshal serializes the header into buf, which must be at
// least Len() bytes long. Implementations of Marshal assume
// that bytes after the first Len() are payload bytes for the
// purpose of computing length and checksum fields. Marshal
// implementations must not allocate memory.
Marshal(buf []byte) error
// ToResponse transforms the header into one for a response packet.
// For instance, this swaps the source and destination IPs.
ToResponse()
}
// Generate generates a new packet with the given header and payload.
// Unlike Header.Marshal, this does allocate memory.
// Generate generates a new packet with the given Header and
// payload. This function allocates memory, see Header.Marshal for an
// allocation-free option.
func Generate(h Header, payload []byte) []byte {
hlen := h.Len()
buf := make([]byte, hlen+len(payload))

90
net/packet/icmp4.go Normal file
View File

@@ -0,0 +1,90 @@
// 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 packet
import "encoding/binary"
// icmp4HeaderLength is the size of the ICMPv4 packet header, not
// including the outer IP layer or the variable "response data"
// trailer.
const icmp4HeaderLength = 4
// ICMP4Type is an ICMPv4 type, as specified in
// https://www.iana.org/assignments/icmp-parameters/icmp-parameters.xhtml
type ICMP4Type uint8
const (
ICMP4EchoReply ICMP4Type = 0x00
ICMP4EchoRequest ICMP4Type = 0x08
ICMP4Unreachable ICMP4Type = 0x03
ICMP4TimeExceeded ICMP4Type = 0x0b
)
func (t ICMP4Type) String() string {
switch t {
case ICMP4EchoReply:
return "EchoReply"
case ICMP4EchoRequest:
return "EchoRequest"
case ICMP4Unreachable:
return "Unreachable"
case ICMP4TimeExceeded:
return "TimeExceeded"
default:
return "Unknown"
}
}
// ICMP4Code is an ICMPv4 code, as specified in
// https://www.iana.org/assignments/icmp-parameters/icmp-parameters.xhtml
type ICMP4Code uint8
const (
ICMP4NoCode ICMP4Code = 0
)
// ICMP4Header is an IPv4+ICMPv4 header.
type ICMP4Header struct {
IP4Header
Type ICMP4Type
Code ICMP4Code
}
// Len implements Header.
func (h ICMP4Header) Len() int {
return h.IP4Header.Len() + icmp4HeaderLength
}
// Marshal implements Header.
func (h ICMP4Header) Marshal(buf []byte) error {
if len(buf) < h.Len() {
return errSmallBuffer
}
if len(buf) > maxPacketLength {
return errLargePacket
}
// The caller does not need to set this.
h.IPProto = ICMPv4
buf[20] = uint8(h.Type)
buf[21] = uint8(h.Code)
h.IP4Header.Marshal(buf)
binary.BigEndian.PutUint16(buf[22:24], ip4Checksum(buf))
return nil
}
// ToResponse implements Header. TODO: it doesn't implement it
// correctly, instead it statically generates an ICMP Echo Reply
// packet.
func (h *ICMP4Header) ToResponse() {
// TODO: this doesn't implement ToResponse correctly, as it
// assumes the ICMP request type.
h.Type = ICMP4EchoReply
h.Code = ICMP4NoCode
h.IP4Header.ToResponse()
}

44
net/packet/icmp6.go Normal file
View File

@@ -0,0 +1,44 @@
// 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 packet
// icmp6HeaderLength is the size of the ICMPv6 packet header, not
// including the outer IP layer or the variable "response data"
// trailer.
const icmp6HeaderLength = 4
// ICMP6Type is an ICMPv6 type, as specified in
// https://www.iana.org/assignments/icmpv6-parameters/icmpv6-parameters.xhtml
type ICMP6Type uint8
const (
ICMP6Unreachable ICMP6Type = 1
ICMP6TimeExceeded ICMP6Type = 3
ICMP6EchoRequest ICMP6Type = 128
ICMP6EchoReply ICMP6Type = 129
)
func (t ICMP6Type) String() string {
switch t {
case ICMP6Unreachable:
return "Unreachable"
case ICMP6TimeExceeded:
return "TimeExceeded"
case ICMP6EchoRequest:
return "EchoRequest"
case ICMP6EchoReply:
return "EchoReply"
default:
return "Unknown"
}
}
// ICMP6Code is an ICMPv6 code, as specified in
// https://www.iana.org/assignments/icmpv6-parameters/icmpv6-parameters.xhtml
type ICMP6Code uint8
const (
ICMP6NoCode ICMP6Code = 0
)

53
net/packet/ip.go Normal file
View File

@@ -0,0 +1,53 @@
// 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 packet
// IPProto is an IP subprotocol as defined by the IANA protocol
// numbers list
// (https://www.iana.org/assignments/protocol-numbers/protocol-numbers.xhtml),
// or the special values Unknown or Fragment.
type IPProto uint8
const (
// Unknown represents an unknown or unsupported protocol; it's
// deliberately the zero value. Strictly speaking the zero
// value is IPv6 hop-by-hop extensions, but we don't support
// those, so this is still technically correct.
Unknown IPProto = 0x00
// Values from the IANA registry.
ICMPv4 IPProto = 0x01
IGMP IPProto = 0x02
ICMPv6 IPProto = 0x3a
TCP IPProto = 0x06
UDP IPProto = 0x11
// Fragment represents any non-first IP fragment, for which we
// don't have the sub-protocol header (and therefore can't
// figure out what the sub-protocol is).
//
// 0xFF is reserved in the IANA registry, so we steal it for
// internal use.
Fragment IPProto = 0xFF
)
func (p IPProto) String() string {
switch p {
case Fragment:
return "Frag"
case ICMPv4:
return "ICMPv4"
case IGMP:
return "IGMP"
case ICMPv6:
return "ICMPv6"
case UDP:
return "UDP"
case TCP:
return "TCP"
default:
return "Unknown"
}
}

116
net/packet/ip4.go Normal file
View File

@@ -0,0 +1,116 @@
// 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 packet
import (
"encoding/binary"
"errors"
"inet.af/netaddr"
)
// ip4HeaderLength is the length of an IPv4 header with no IP options.
const ip4HeaderLength = 20
// IP4Header represents an IPv4 packet header.
type IP4Header struct {
IPProto IPProto
IPID uint16
Src netaddr.IP
Dst netaddr.IP
}
// Len implements Header.
func (h IP4Header) Len() int {
return ip4HeaderLength
}
var errWrongFamily = errors.New("wrong address family for src/dst IP")
// Marshal implements Header.
func (h IP4Header) Marshal(buf []byte) error {
if len(buf) < h.Len() {
return errSmallBuffer
}
if len(buf) > maxPacketLength {
return errLargePacket
}
if !h.Src.Is4() || !h.Dst.Is4() {
return errWrongFamily
}
buf[0] = 0x40 | (byte(h.Len() >> 2)) // IPv4 + IHL
buf[1] = 0x00 // DSCP + ECN
binary.BigEndian.PutUint16(buf[2:4], uint16(len(buf))) // Total length
binary.BigEndian.PutUint16(buf[4:6], h.IPID) // ID
binary.BigEndian.PutUint16(buf[6:8], 0) // Flags + fragment offset
buf[8] = 64 // TTL
buf[9] = uint8(h.IPProto) // Inner protocol
// Blank checksum. This is necessary even though we overwrite
// it later, because the checksum computation runs over these
// bytes and expects them to be zero.
binary.BigEndian.PutUint16(buf[10:12], 0)
src := h.Src.As4()
dst := h.Dst.As4()
copy(buf[12:16], src[:])
copy(buf[16:20], dst[:])
binary.BigEndian.PutUint16(buf[10:12], ip4Checksum(buf[0:20])) // Checksum
return nil
}
// ToResponse implements Header.
func (h *IP4Header) ToResponse() {
h.Src, h.Dst = h.Dst, h.Src
// Flip the bits in the IPID. If incoming IPIDs are distinct, so are these.
h.IPID = ^h.IPID
}
// ip4Checksum computes an IPv4 checksum, as specified in
// https://tools.ietf.org/html/rfc1071
func ip4Checksum(b []byte) uint16 {
var ac uint32
i := 0
n := len(b)
for n >= 2 {
ac += uint32(binary.BigEndian.Uint16(b[i : i+2]))
n -= 2
i += 2
}
if n == 1 {
ac += uint32(b[i]) << 8
}
for (ac >> 16) > 0 {
ac = (ac >> 16) + (ac & 0xffff)
}
return uint16(^ac)
}
// ip4PseudoHeaderOffset is the number of bytes by which the IPv4 UDP
// pseudo-header is smaller than the real IPv4 header.
const ip4PseudoHeaderOffset = 8
// marshalPseudo serializes h into buf in the "pseudo-header" form
// required when calculating UDP checksums. The pseudo-header starts
// at buf[ip4PseudoHeaderOffset] so as to abut the following UDP
// header, while leaving enough space in buf for a full IPv4 header.
func (h IP4Header) marshalPseudo(buf []byte) error {
if len(buf) < h.Len() {
return errSmallBuffer
}
if len(buf) > maxPacketLength {
return errLargePacket
}
length := len(buf) - h.Len()
src, dst := h.Src.As4(), h.Dst.As4()
copy(buf[8:12], src[:])
copy(buf[12:16], dst[:])
buf[16] = 0x0
buf[17] = uint8(h.IPProto)
binary.BigEndian.PutUint16(buf[18:20], uint16(length))
return nil
}

76
net/packet/ip6.go Normal file
View File

@@ -0,0 +1,76 @@
// 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 packet
import (
"encoding/binary"
"inet.af/netaddr"
)
// ip6HeaderLength is the length of an IPv6 header with no IP options.
const ip6HeaderLength = 40
// IP6Header represents an IPv6 packet header.
type IP6Header struct {
IPProto IPProto
IPID uint32 // only lower 20 bits used
Src netaddr.IP
Dst netaddr.IP
}
// Len implements Header.
func (h IP6Header) Len() int {
return ip6HeaderLength
}
// Marshal implements Header.
func (h IP6Header) Marshal(buf []byte) error {
if len(buf) < h.Len() {
return errSmallBuffer
}
if len(buf) > maxPacketLength {
return errLargePacket
}
binary.BigEndian.PutUint32(buf[:4], h.IPID&0x000FFFFF)
buf[0] = 0x60
binary.BigEndian.PutUint16(buf[4:6], uint16(len(buf)-ip6HeaderLength)) // Total length
buf[6] = uint8(h.IPProto) // Inner protocol
buf[7] = 64 // TTL
src, dst := h.Src.As16(), h.Dst.As16()
copy(buf[8:24], src[:])
copy(buf[24:40], dst[:])
return nil
}
// ToResponse implements Header.
func (h *IP6Header) ToResponse() {
h.Src, h.Dst = h.Dst, h.Src
// Flip the bits in the IPID. If incoming IPIDs are distinct, so are these.
h.IPID = (^h.IPID) & 0x000FFFFF
}
// marshalPseudo serializes h into buf in the "pseudo-header" form
// required when calculating UDP checksums.
func (h IP6Header) marshalPseudo(buf []byte) error {
if len(buf) < h.Len() {
return errSmallBuffer
}
if len(buf) > maxPacketLength {
return errLargePacket
}
src, dst := h.Src.As16(), h.Dst.As16()
copy(buf[:16], src[:])
copy(buf[16:32], dst[:])
binary.BigEndian.PutUint32(buf[32:36], uint32(len(buf)-h.Len()))
buf[36] = 0
buf[37] = 0
buf[38] = 0
buf[39] = 17 // NextProto
return nil
}

422
net/packet/packet.go Normal file
View File

@@ -0,0 +1,422 @@
// 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 packet
import (
"encoding/binary"
"fmt"
"net"
"strings"
"inet.af/netaddr"
"tailscale.com/types/strbuilder"
)
// RFC1858: prevent overlapping fragment attacks.
const minFrag = 60 + 20 // max IPv4 header + basic TCP header
const (
TCPSyn = 0x02
TCPAck = 0x10
TCPSynAck = TCPSyn | TCPAck
)
// Parsed is a minimal decoding of a packet suitable for use in filters.
type Parsed struct {
// b is the byte buffer that this decodes.
b []byte
// subofs is the offset of IP subprotocol.
subofs int
// dataofs is the offset of IP subprotocol payload.
dataofs int
// length is the total length of the packet.
// This is not the same as len(b) because b can have trailing zeros.
length int
// IPVersion is the IP protocol version of the packet (4 or
// 6), or 0 if the packet doesn't look like IPv4 or IPv6.
IPVersion uint8
// IPProto is the IP subprotocol (UDP, TCP, etc.). Valid iff IPVersion != 0.
IPProto IPProto
// SrcIP4 is the source address. Family matches IPVersion. Port is
// valid iff IPProto == TCP || IPProto == UDP.
Src netaddr.IPPort
// DstIP4 is the destination address. Family matches IPVersion.
Dst netaddr.IPPort
// TCPFlags is the packet's TCP flag bigs. Valid iff IPProto == TCP.
TCPFlags uint8
}
func (p *Parsed) String() string {
if p.IPVersion != 4 && p.IPVersion != 6 {
return "Unknown{???}"
}
sb := strbuilder.Get()
sb.WriteString(p.IPProto.String())
sb.WriteByte('{')
writeIPPort(sb, p.Src)
sb.WriteString(" > ")
writeIPPort(sb, p.Dst)
sb.WriteByte('}')
return sb.String()
}
// writeIPPort writes ipp.String() into sb, with fewer allocations.
//
// TODO: make netaddr more efficient in this area, and retire this func.
func writeIPPort(sb *strbuilder.Builder, ipp netaddr.IPPort) {
if ipp.IP.Is4() {
raw := ipp.IP.As4()
sb.WriteUint(uint64(raw[0]))
sb.WriteByte('.')
sb.WriteUint(uint64(raw[1]))
sb.WriteByte('.')
sb.WriteUint(uint64(raw[2]))
sb.WriteByte('.')
sb.WriteUint(uint64(raw[3]))
sb.WriteByte(':')
} else {
sb.WriteByte('[')
sb.WriteString(ipp.IP.String()) // TODO: faster?
sb.WriteString("]:")
}
sb.WriteUint(uint64(ipp.Port))
}
// Decode extracts data from the packet in b into q.
// It performs extremely simple packet decoding for basic IPv4 packet types.
// It extracts only the subprotocol id, IP addresses, and (if any) ports,
// and shouldn't need any memory allocation.
func (q *Parsed) Decode(b []byte) {
q.b = b
if len(b) < 1 {
q.IPVersion = 0
q.IPProto = Unknown
return
}
q.IPVersion = b[0] >> 4
switch q.IPVersion {
case 4:
q.decode4(b)
case 6:
q.decode6(b)
default:
q.IPVersion = 0
q.IPProto = Unknown
}
}
func (q *Parsed) decode4(b []byte) {
if len(b) < ip4HeaderLength {
q.IPVersion = 0
q.IPProto = Unknown
return
}
// Check that it's IPv4.
q.IPProto = IPProto(b[9])
q.length = int(binary.BigEndian.Uint16(b[2:4]))
if len(b) < q.length {
// Packet was cut off before full IPv4 length.
q.IPProto = Unknown
return
}
// If it's valid IPv4, then the IP addresses are valid
q.Src.IP = netaddr.IPv4(b[12], b[13], b[14], b[15])
q.Dst.IP = netaddr.IPv4(b[16], b[17], b[18], b[19])
q.subofs = int((b[0] & 0x0F) << 2)
if q.subofs > q.length {
// next-proto starts beyond end of packet.
q.IPProto = Unknown
return
}
sub := b[q.subofs:]
sub = sub[:len(sub):len(sub)] // help the compiler do bounds check elimination
// We don't care much about IP fragmentation, except insofar as it's
// used for firewall bypass attacks. The trick is make the first
// fragment of a TCP or UDP packet so short that it doesn't fit
// the TCP or UDP header, so we can't read the port, in hope that
// it'll sneak past. Then subsequent fragments fill it in, but we're
// missing the first part of the header, so we can't read that either.
//
// A "perfectly correct" implementation would have to reassemble
// fragments before deciding what to do. But the truth is there's
// zero reason to send such a short first fragment, so we can treat
// it as Unknown. We can also treat any subsequent fragment that starts
// at such a low offset as Unknown.
fragFlags := binary.BigEndian.Uint16(b[6:8])
moreFrags := (fragFlags & 0x20) != 0
fragOfs := fragFlags & 0x1FFF
if fragOfs == 0 {
// This is the first fragment
if moreFrags && len(sub) < minFrag {
// Suspiciously short first fragment, dump it.
q.IPProto = Unknown
return
}
// otherwise, this is either non-fragmented (the usual case)
// or a big enough initial fragment that we can read the
// whole subprotocol header.
switch q.IPProto {
case ICMPv4:
if len(sub) < icmp4HeaderLength {
q.IPProto = Unknown
return
}
q.Src.Port = 0
q.Dst.Port = 0
q.dataofs = q.subofs + icmp4HeaderLength
return
case IGMP:
// Keep IPProto, but don't parse anything else
// out.
return
case TCP:
if len(sub) < tcpHeaderLength {
q.IPProto = Unknown
return
}
q.Src.Port = binary.BigEndian.Uint16(sub[0:2])
q.Dst.Port = binary.BigEndian.Uint16(sub[2:4])
q.TCPFlags = sub[13] & 0x3F
headerLength := (sub[12] & 0xF0) >> 2
q.dataofs = q.subofs + int(headerLength)
return
case UDP:
if len(sub) < udpHeaderLength {
q.IPProto = Unknown
return
}
q.Src.Port = binary.BigEndian.Uint16(sub[0:2])
q.Dst.Port = binary.BigEndian.Uint16(sub[2:4])
q.dataofs = q.subofs + udpHeaderLength
return
default:
q.IPProto = Unknown
return
}
} else {
// This is a fragment other than the first one.
if fragOfs < minFrag {
// First frag was suspiciously short, so we can't
// trust the followup either.
q.IPProto = Unknown
return
}
// otherwise, we have to permit the fragment to slide through.
// Second and later fragments don't have sub-headers.
// Ideally, we would drop fragments that we can't identify,
// but that would require statefulness. Anyway, receivers'
// kernels know to drop fragments where the initial fragment
// doesn't arrive.
q.IPProto = Fragment
return
}
}
func (q *Parsed) decode6(b []byte) {
if len(b) < ip6HeaderLength {
q.IPVersion = 0
q.IPProto = Unknown
return
}
q.IPProto = IPProto(b[6])
q.length = int(binary.BigEndian.Uint16(b[4:6])) + ip6HeaderLength
if len(b) < q.length {
// Packet was cut off before the full IPv6 length.
q.IPProto = Unknown
return
}
// okay to ignore `ok` here, because IPs pulled from packets are
// always well-formed stdlib IPs.
q.Src.IP, _ = netaddr.FromStdIP(net.IP(b[8:24]))
q.Dst.IP, _ = netaddr.FromStdIP(net.IP(b[24:40]))
// We don't support any IPv6 extension headers. Don't try to
// be clever. Therefore, the IP subprotocol always starts at
// byte 40.
//
// Note that this means we don't support fragmentation in
// IPv6. This is fine, because IPv6 strongly mandates that you
// should not fragment, which makes fragmentation on the open
// internet extremely uncommon.
//
// This also means we don't support IPSec headers (AH/ESP), or
// IPv6 jumbo frames. Those will get marked Unknown and
// dropped.
q.subofs = 40
sub := b[q.subofs:]
sub = sub[:len(sub):len(sub)] // help the compiler do bounds check elimination
switch q.IPProto {
case ICMPv6:
if len(sub) < icmp6HeaderLength {
q.IPProto = Unknown
return
}
q.Src.Port = 0
q.Dst.Port = 0
q.dataofs = q.subofs + icmp6HeaderLength
case TCP:
if len(sub) < tcpHeaderLength {
q.IPProto = Unknown
return
}
q.Src.Port = binary.BigEndian.Uint16(sub[0:2])
q.Dst.Port = binary.BigEndian.Uint16(sub[2:4])
q.TCPFlags = sub[13] & 0x3F
headerLength := (sub[12] & 0xF0) >> 2
q.dataofs = q.subofs + int(headerLength)
return
case UDP:
if len(sub) < udpHeaderLength {
q.IPProto = Unknown
return
}
q.Src.Port = binary.BigEndian.Uint16(sub[0:2])
q.Dst.Port = binary.BigEndian.Uint16(sub[2:4])
q.dataofs = q.subofs + udpHeaderLength
default:
q.IPProto = Unknown
return
}
}
func (q *Parsed) IP4Header() IP4Header {
if q.IPVersion != 4 {
panic("IP4Header called on non-IPv4 Parsed")
}
ipid := binary.BigEndian.Uint16(q.b[4:6])
return IP4Header{
IPID: ipid,
IPProto: q.IPProto,
Src: q.Src.IP,
Dst: q.Dst.IP,
}
}
func (q *Parsed) ICMP4Header() ICMP4Header {
if q.IPVersion != 4 {
panic("IP4Header called on non-IPv4 Parsed")
}
return ICMP4Header{
IP4Header: q.IP4Header(),
Type: ICMP4Type(q.b[q.subofs+0]),
Code: ICMP4Code(q.b[q.subofs+1]),
}
}
func (q *Parsed) UDP4Header() UDP4Header {
if q.IPVersion != 4 {
panic("IP4Header called on non-IPv4 Parsed")
}
return UDP4Header{
IP4Header: q.IP4Header(),
SrcPort: q.Src.Port,
DstPort: q.Dst.Port,
}
}
// Buffer returns the entire packet buffer.
// This is a read-only view; that is, q retains the ownership of the buffer.
func (q *Parsed) Buffer() []byte {
return q.b
}
// Payload returns the payload of the IP subprotocol section.
// This is a read-only view; that is, q retains the ownership of the buffer.
func (q *Parsed) Payload() []byte {
return q.b[q.dataofs:q.length]
}
// IsTCPSyn reports whether q is a TCP SYN packet
// (i.e. the first packet in a new connection).
func (q *Parsed) IsTCPSyn() bool {
return (q.TCPFlags & TCPSynAck) == TCPSyn
}
// IsError reports whether q is an ICMP "Error" packet.
func (q *Parsed) IsError() bool {
switch q.IPProto {
case ICMPv4:
if len(q.b) < q.subofs+8 {
return false
}
t := ICMP4Type(q.b[q.subofs])
return t == ICMP4Unreachable || t == ICMP4TimeExceeded
case ICMPv6:
if len(q.b) < q.subofs+8 {
return false
}
t := ICMP6Type(q.b[q.subofs])
return t == ICMP6Unreachable || t == ICMP6TimeExceeded
default:
return false
}
}
// IsEchoRequest reports whether q is an ICMP Echo Request.
func (q *Parsed) IsEchoRequest() bool {
switch q.IPProto {
case ICMPv4:
return len(q.b) >= q.subofs+8 && ICMP4Type(q.b[q.subofs]) == ICMP4EchoRequest && ICMP4Code(q.b[q.subofs+1]) == ICMP4NoCode
case ICMPv6:
return len(q.b) >= q.subofs+8 && ICMP6Type(q.b[q.subofs]) == ICMP6EchoRequest && ICMP6Code(q.b[q.subofs+1]) == ICMP6NoCode
default:
return false
}
}
// IsEchoRequest reports whether q is an IPv4 ICMP Echo Response.
func (q *Parsed) IsEchoResponse() bool {
switch q.IPProto {
case ICMPv4:
return len(q.b) >= q.subofs+8 && ICMP4Type(q.b[q.subofs]) == ICMP4EchoReply && ICMP4Code(q.b[q.subofs+1]) == ICMP4NoCode
case ICMPv6:
return len(q.b) >= q.subofs+8 && ICMP6Type(q.b[q.subofs]) == ICMP6EchoReply && ICMP6Code(q.b[q.subofs+1]) == ICMP6NoCode
default:
return false
}
}
func Hexdump(b []byte) string {
out := new(strings.Builder)
for i := 0; i < len(b); i += 16 {
if i > 0 {
fmt.Fprintf(out, "\n")
}
fmt.Fprintf(out, " %04x ", i)
j := 0
for ; j < 16 && i+j < len(b); j++ {
if j == 8 {
fmt.Fprintf(out, " ")
}
fmt.Fprintf(out, "%02x ", b[i+j])
}
for ; j < 16; j++ {
if j == 8 {
fmt.Fprintf(out, " ")
}
fmt.Fprintf(out, " ")
}
fmt.Fprintf(out, " ")
for j = 0; j < 16 && i+j < len(b); j++ {
if b[i+j] >= 32 && b[i+j] < 128 {
fmt.Fprintf(out, "%c", b[i+j])
} else {
fmt.Fprintf(out, ".")
}
}
}
return out.String()
}

446
net/packet/packet_test.go Normal file
View File

@@ -0,0 +1,446 @@
// 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 packet
import (
"bytes"
"reflect"
"testing"
"inet.af/netaddr"
)
func mustIPPort(s string) netaddr.IPPort {
ipp, err := netaddr.ParseIPPort(s)
if err != nil {
panic(err)
}
return ipp
}
var icmp4RequestBuffer = []byte{
// IP header up to checksum
0x45, 0x00, 0x00, 0x27, 0xde, 0xad, 0x00, 0x00, 0x40, 0x01, 0x8c, 0x15,
// source ip
0x01, 0x02, 0x03, 0x04,
// destination ip
0x05, 0x06, 0x07, 0x08,
// ICMP header
0x08, 0x00, 0x7d, 0x22,
// "request_payload"
0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x5f, 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64,
}
var icmp4RequestDecode = Parsed{
b: icmp4RequestBuffer,
subofs: 20,
dataofs: 24,
length: len(icmp4RequestBuffer),
IPVersion: 4,
IPProto: ICMPv4,
Src: mustIPPort("1.2.3.4:0"),
Dst: mustIPPort("5.6.7.8:0"),
}
var icmp4ReplyBuffer = []byte{
0x45, 0x00, 0x00, 0x25, 0x21, 0x52, 0x00, 0x00, 0x40, 0x01, 0x49, 0x73,
// source ip
0x05, 0x06, 0x07, 0x08,
// destination ip
0x01, 0x02, 0x03, 0x04,
// ICMP header
0x00, 0x00, 0xe6, 0x9e,
// "reply_payload"
0x72, 0x65, 0x70, 0x6c, 0x79, 0x5f, 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64,
}
var icmp4ReplyDecode = Parsed{
b: icmp4ReplyBuffer,
subofs: 20,
dataofs: 24,
length: len(icmp4ReplyBuffer),
IPVersion: 4,
IPProto: ICMPv4,
Src: mustIPPort("1.2.3.4:0"),
Dst: mustIPPort("5.6.7.8:0"),
}
// ICMPv6 Router Solicitation
var icmp6PacketBuffer = []byte{
0x60, 0x00, 0x00, 0x00, 0x00, 0x08, 0x3a, 0xff,
0xfe, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0xfb, 0x57, 0x1d, 0xea, 0x9c, 0x39, 0x8f, 0xb7,
0xff, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02,
0x85, 0x00, 0x38, 0x04, 0x00, 0x00, 0x00, 0x00,
}
var icmp6PacketDecode = Parsed{
b: icmp6PacketBuffer,
subofs: 40,
dataofs: 44,
length: len(icmp6PacketBuffer),
IPVersion: 6,
IPProto: ICMPv6,
Src: mustIPPort("[fe80::fb57:1dea:9c39:8fb7]:0"),
Dst: mustIPPort("[ff02::2]:0"),
}
// This is a malformed IPv4 packet.
// Namely, the string "tcp_payload" follows the first byte of the IPv4 header.
var unknownPacketBuffer = []byte{
0x45, 0x74, 0x63, 0x70, 0x5f, 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64,
}
var unknownPacketDecode = Parsed{
b: unknownPacketBuffer,
IPVersion: 0,
IPProto: Unknown,
}
var tcp4PacketBuffer = []byte{
// IP header up to checksum
0x45, 0x00, 0x00, 0x37, 0xde, 0xad, 0x00, 0x00, 0x40, 0x06, 0x49, 0x5f,
// source ip
0x01, 0x02, 0x03, 0x04,
// destination ip
0x05, 0x06, 0x07, 0x08,
// TCP header with SYN, ACK set
0x00, 0x7b, 0x02, 0x37, 0x00, 0x00, 0x12, 0x34, 0x00, 0x00, 0x00, 0x00,
0x50, 0x12, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00,
// "request_payload"
0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x5f, 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64,
}
var tcp4PacketDecode = Parsed{
b: tcp4PacketBuffer,
subofs: 20,
dataofs: 40,
length: len(tcp4PacketBuffer),
IPVersion: 4,
IPProto: TCP,
Src: mustIPPort("1.2.3.4:123"),
Dst: mustIPPort("5.6.7.8:567"),
TCPFlags: TCPSynAck,
}
var tcp6RequestBuffer = []byte{
// IPv6 header up to hop limit
0x60, 0x06, 0xef, 0xcc, 0x00, 0x28, 0x06, 0x40,
// Src addr
0x20, 0x01, 0x05, 0x59, 0xbc, 0x13, 0x54, 0x00, 0x17, 0x49, 0x46, 0x28, 0x39, 0x34, 0x0e, 0x1b,
// Dst addr
0x26, 0x07, 0xf8, 0xb0, 0x40, 0x0a, 0x08, 0x09, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x0e,
// TCP SYN segment, no payload
0xa4, 0x60, 0x00, 0x50, 0xf3, 0x82, 0xa1, 0x25, 0x00, 0x00, 0x00, 0x00, 0xa0, 0x02, 0xfd, 0x20,
0xb1, 0xc6, 0x00, 0x00, 0x02, 0x04, 0x05, 0xa0, 0x04, 0x02, 0x08, 0x0a, 0xca, 0x76, 0xa6, 0x8e,
0x00, 0x00, 0x00, 0x00, 0x01, 0x03, 0x03, 0x07,
}
var tcp6RequestDecode = Parsed{
b: tcp6RequestBuffer,
subofs: 40,
dataofs: len(tcp6RequestBuffer),
length: len(tcp6RequestBuffer),
IPVersion: 6,
IPProto: TCP,
Src: mustIPPort("[2001:559:bc13:5400:1749:4628:3934:e1b]:42080"),
Dst: mustIPPort("[2607:f8b0:400a:809::200e]:80"),
TCPFlags: TCPSyn,
}
var udp4RequestBuffer = []byte{
// IP header up to checksum
0x45, 0x00, 0x00, 0x2b, 0xde, 0xad, 0x00, 0x00, 0x40, 0x11, 0x8c, 0x01,
// source ip
0x01, 0x02, 0x03, 0x04,
// destination ip
0x05, 0x06, 0x07, 0x08,
// UDP header
0x00, 0x7b, 0x02, 0x37, 0x00, 0x17, 0x72, 0x1d,
// "request_payload"
0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x5f, 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64,
}
var udp4RequestDecode = Parsed{
b: udp4RequestBuffer,
subofs: 20,
dataofs: 28,
length: len(udp4RequestBuffer),
IPVersion: 4,
IPProto: UDP,
Src: mustIPPort("1.2.3.4:123"),
Dst: mustIPPort("5.6.7.8:567"),
}
var invalid4RequestBuffer = []byte{
// IP header up to checksum. IHL field points beyond end of packet.
0x4a, 0x00, 0x00, 0x14, 0xde, 0xad, 0x00, 0x00, 0x40, 0x11, 0x8c, 0x01,
// source ip
0x01, 0x02, 0x03, 0x04,
// destination ip
0x05, 0x06, 0x07, 0x08,
}
// Regression check for the IHL field pointing beyond the end of the
// packet.
var invalid4RequestDecode = Parsed{
b: invalid4RequestBuffer,
subofs: 40,
length: len(invalid4RequestBuffer),
IPVersion: 4,
IPProto: Unknown,
Src: mustIPPort("1.2.3.4:0"),
Dst: mustIPPort("5.6.7.8:0"),
}
var udp6RequestBuffer = []byte{
// IPv6 header up to hop limit
0x60, 0x0e, 0xc9, 0x67, 0x00, 0x29, 0x11, 0x40,
// Src addr
0x20, 0x01, 0x05, 0x59, 0xbc, 0x13, 0x54, 0x00, 0x17, 0x49, 0x46, 0x28, 0x39, 0x34, 0x0e, 0x1b,
// Dst addr
0x26, 0x07, 0xf8, 0xb0, 0x40, 0x0a, 0x08, 0x09, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x0e,
// UDP header
0xd4, 0x04, 0x01, 0xbb, 0x00, 0x29, 0x96, 0x84,
// Payload
0x5c, 0x06, 0xae, 0x85, 0x02, 0xf5, 0xdb, 0x90, 0xe0, 0xe0, 0x93, 0xed, 0x9a, 0xd9, 0x92, 0x69, 0xbe, 0x36, 0x8a, 0x7d, 0xd7, 0xce, 0xd0, 0x8a, 0xf2, 0x51, 0x95, 0xff, 0xb6, 0x92, 0x70, 0x10, 0xd7,
}
var udp6RequestDecode = Parsed{
b: udp6RequestBuffer,
subofs: 40,
dataofs: 48,
length: len(udp6RequestBuffer),
IPVersion: 6,
IPProto: UDP,
Src: mustIPPort("[2001:559:bc13:5400:1749:4628:3934:e1b]:54276"),
Dst: mustIPPort("[2607:f8b0:400a:809::200e]:443"),
}
var udp4ReplyBuffer = []byte{
// IP header up to checksum
0x45, 0x00, 0x00, 0x29, 0x21, 0x52, 0x00, 0x00, 0x40, 0x11, 0x49, 0x5f,
// source ip
0x05, 0x06, 0x07, 0x08,
// destination ip
0x01, 0x02, 0x03, 0x04,
// UDP header
0x02, 0x37, 0x00, 0x7b, 0x00, 0x15, 0xd3, 0x9d,
// "reply_payload"
0x72, 0x65, 0x70, 0x6c, 0x79, 0x5f, 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64,
}
var udp4ReplyDecode = Parsed{
b: udp4ReplyBuffer,
subofs: 20,
dataofs: 28,
length: len(udp4ReplyBuffer),
IPProto: UDP,
Src: mustIPPort("1.2.3.4:567"),
Dst: mustIPPort("5.6.7.8:123"),
}
var igmpPacketBuffer = []byte{
// IP header up to checksum
0x46, 0xc0, 0x00, 0x20, 0x00, 0x00, 0x40, 0x00, 0x01, 0x02, 0x41, 0x22,
// source IP
0xc0, 0xa8, 0x01, 0x52,
// destination IP
0xe0, 0x00, 0x00, 0xfb,
// IGMP Membership Report
0x94, 0x04, 0x00, 0x00, 0x16, 0x00, 0x09, 0x04, 0xe0, 0x00, 0x00, 0xfb,
//0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
}
var igmpPacketDecode = Parsed{
b: igmpPacketBuffer,
subofs: 24,
length: len(igmpPacketBuffer),
IPVersion: 4,
IPProto: IGMP,
Src: mustIPPort("192.168.1.82:0"),
Dst: mustIPPort("224.0.0.251:0"),
}
func TestParsed(t *testing.T) {
tests := []struct {
name string
qdecode Parsed
want string
}{
{"tcp4", tcp4PacketDecode, "TCP{1.2.3.4:123 > 5.6.7.8:567}"},
{"tcp6", tcp6RequestDecode, "TCP{[2001:559:bc13:5400:1749:4628:3934:e1b]:42080 > [2607:f8b0:400a:809::200e]:80}"},
{"udp4", udp4RequestDecode, "UDP{1.2.3.4:123 > 5.6.7.8:567}"},
{"udp6", udp6RequestDecode, "UDP{[2001:559:bc13:5400:1749:4628:3934:e1b]:54276 > [2607:f8b0:400a:809::200e]:443}"},
{"icmp4", icmp4RequestDecode, "ICMPv4{1.2.3.4:0 > 5.6.7.8:0}"},
{"icmp6", icmp6PacketDecode, "ICMPv6{[fe80::fb57:1dea:9c39:8fb7]:0 > [ff02::2]:0}"},
{"igmp", igmpPacketDecode, "IGMP{192.168.1.82:0 > 224.0.0.251:0}"},
{"unknown", unknownPacketDecode, "Unknown{???}"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := tt.qdecode.String()
if got != tt.want {
t.Errorf("got %q; want %q", got, tt.want)
}
})
}
var sink string
allocs := testing.AllocsPerRun(1000, func() {
sink = tests[0].qdecode.String()
})
_ = sink
if allocs != 1 {
t.Errorf("allocs = %v; want 1", allocs)
}
}
func TestDecode(t *testing.T) {
tests := []struct {
name string
buf []byte
want Parsed
}{
{"icmp4", icmp4RequestBuffer, icmp4RequestDecode},
{"icmp6", icmp6PacketBuffer, icmp6PacketDecode},
{"tcp4", tcp4PacketBuffer, tcp4PacketDecode},
{"tcp6", tcp6RequestBuffer, tcp6RequestDecode},
{"udp4", udp4RequestBuffer, udp4RequestDecode},
{"udp6", udp6RequestBuffer, udp6RequestDecode},
{"igmp", igmpPacketBuffer, igmpPacketDecode},
{"unknown", unknownPacketBuffer, unknownPacketDecode},
{"invalid4", invalid4RequestBuffer, invalid4RequestDecode},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var got Parsed
got.Decode(tt.buf)
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("mismatch\n got: %#v\nwant: %#v", got, tt.want)
}
})
}
allocs := testing.AllocsPerRun(1000, func() {
var got Parsed
got.Decode(tests[0].buf)
})
if allocs != 0 {
t.Errorf("allocs = %v; want 0", allocs)
}
}
func BenchmarkDecode(b *testing.B) {
benches := []struct {
name string
buf []byte
}{
{"tcp4", tcp4PacketBuffer},
{"tcp6", tcp6RequestBuffer},
{"udp4", udp4RequestBuffer},
{"udp6", udp6RequestBuffer},
{"icmp4", icmp4RequestBuffer},
{"icmp6", icmp6PacketBuffer},
{"igmp", igmpPacketBuffer},
{"unknown", unknownPacketBuffer},
}
for _, bench := range benches {
b.Run(bench.name, func(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
var p Parsed
p.Decode(bench.buf)
}
})
}
}
func TestMarshalRequest(t *testing.T) {
// Too small to hold our packets, but only barely.
var small [20]byte
var large [64]byte
icmpHeader := icmp4RequestDecode.ICMP4Header()
udpHeader := udp4RequestDecode.UDP4Header()
tests := []struct {
name string
header Header
want []byte
}{
{"icmp", &icmpHeader, icmp4RequestBuffer},
{"udp", &udpHeader, udp4RequestBuffer},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.header.Marshal(small[:])
if err != errSmallBuffer {
t.Errorf("got err: nil; want: %s", errSmallBuffer)
}
dataOffset := tt.header.Len()
dataLength := copy(large[dataOffset:], []byte("request_payload"))
end := dataOffset + dataLength
err = tt.header.Marshal(large[:end])
if err != nil {
t.Errorf("got err: %s; want nil", err)
}
if !bytes.Equal(large[:end], tt.want) {
t.Errorf("got %x; want %x", large[:end], tt.want)
}
})
}
}
func TestMarshalResponse(t *testing.T) {
var buf [64]byte
icmpHeader := icmp4RequestDecode.ICMP4Header()
udpHeader := udp4RequestDecode.UDP4Header()
tests := []struct {
name string
header Header
want []byte
}{
{"icmp", &icmpHeader, icmp4ReplyBuffer},
{"udp", &udpHeader, udp4ReplyBuffer},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tt.header.ToResponse()
dataOffset := tt.header.Len()
dataLength := copy(buf[dataOffset:], []byte("reply_payload"))
end := dataOffset + dataLength
err := tt.header.Marshal(buf[:end])
if err != nil {
t.Errorf("got err: %s; want nil", err)
}
if !bytes.Equal(buf[:end], tt.want) {
t.Errorf("got %x; want %x", buf[:end], tt.want)
}
})
}
}

55
net/packet/udp4.go Normal file
View File

@@ -0,0 +1,55 @@
// 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 packet
import "encoding/binary"
// udpHeaderLength is the size of the UDP packet header, not including
// the outer IP header.
const udpHeaderLength = 8
// UDP4Header is an IPv4+UDP header.
type UDP4Header struct {
IP4Header
SrcPort uint16
DstPort uint16
}
// Len implements Header.
func (h UDP4Header) Len() int {
return h.IP4Header.Len() + udpHeaderLength
}
// Marshal implements Header.
func (h UDP4Header) Marshal(buf []byte) error {
if len(buf) < h.Len() {
return errSmallBuffer
}
if len(buf) > maxPacketLength {
return errLargePacket
}
// The caller does not need to set this.
h.IPProto = UDP
length := len(buf) - h.IP4Header.Len()
binary.BigEndian.PutUint16(buf[20:22], h.SrcPort)
binary.BigEndian.PutUint16(buf[22:24], h.DstPort)
binary.BigEndian.PutUint16(buf[24:26], uint16(length))
binary.BigEndian.PutUint16(buf[26:28], 0) // blank checksum
// UDP checksum with IP pseudo header.
h.IP4Header.marshalPseudo(buf)
binary.BigEndian.PutUint16(buf[26:28], ip4Checksum(buf[ip4PseudoHeaderOffset:]))
h.IP4Header.Marshal(buf)
return nil
}
// ToResponse implements Header.
func (h *UDP4Header) ToResponse() {
h.SrcPort, h.DstPort = h.DstPort, h.SrcPort
h.IP4Header.ToResponse()
}

51
net/packet/udp6.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.
package packet
import "encoding/binary"
// UDP6Header is an IPv6+UDP header.
type UDP6Header struct {
IP6Header
SrcPort uint16
DstPort uint16
}
// Len implements Header.
func (h UDP6Header) Len() int {
return h.IP6Header.Len() + udpHeaderLength
}
// Marshal implements Header.
func (h UDP6Header) Marshal(buf []byte) error {
if len(buf) < h.Len() {
return errSmallBuffer
}
if len(buf) > maxPacketLength {
return errLargePacket
}
// The caller does not need to set this.
h.IPProto = UDP
length := len(buf) - h.IP6Header.Len()
binary.BigEndian.PutUint16(buf[40:42], h.SrcPort)
binary.BigEndian.PutUint16(buf[42:44], h.DstPort)
binary.BigEndian.PutUint16(buf[44:46], uint16(length))
binary.BigEndian.PutUint16(buf[46:48], 0) // blank checksum
// UDP checksum with IP pseudo header.
h.IP6Header.marshalPseudo(buf)
binary.BigEndian.PutUint16(buf[46:48], ip4Checksum(buf[:]))
h.IP6Header.Marshal(buf)
return nil
}
// ToResponse implements Header.
func (h *UDP6Header) ToResponse() {
h.SrcPort, h.DstPort = h.DstPort, h.SrcPort
h.IP6Header.ToResponse()
}

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

@@ -4,7 +4,32 @@
package tsaddr
import "testing"
import (
"testing"
"inet.af/netaddr"
)
func TestInCrostiniRange(t *testing.T) {
tests := []struct {
ip netaddr.IP
want bool
}{
{netaddr.IPv4(192, 168, 0, 1), false},
{netaddr.IPv4(100, 101, 102, 103), false},
{netaddr.IPv4(100, 115, 92, 0), true},
{netaddr.IPv4(100, 115, 92, 5), true},
{netaddr.IPv4(100, 115, 92, 255), true},
{netaddr.IPv4(100, 115, 93, 40), true},
{netaddr.IPv4(100, 115, 94, 1), false},
}
for _, test := range tests {
if got := ChromeOSVMRange().Contains(test.ip); got != test.want {
t.Errorf("inCrostiniRange(%q) = %v, want %v", test.ip, got, test.want)
}
}
}
func TestChromeOSVMRange(t *testing.T) {
if got, want := ChromeOSVMRange().String(), "100.115.92.0/23"; got != want {

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,9 +8,8 @@ package portlist
import (
"fmt"
"os/exec"
"strings"
exec "tailscale.com/tempfork/osexec"
)
var osHideWindow func(*exec.Cmd) // non-nil on Windows; see portlist_windows.go

View File

@@ -7,7 +7,6 @@ package portlist
import (
"context"
"errors"
"runtime"
"time"
"tailscale.com/version"
@@ -34,7 +33,7 @@ type Poller struct {
// NewPoller returns a new portlist Poller. It returns an error
// if the portlist couldn't be obtained.
func NewPoller() (*Poller, error) {
if runtime.GOOS == "darwin" && version.IsMobile() {
if version.OS() == "iOS" {
return nil, errors.New("not available on iOS")
}
p := &Poller{

View File

@@ -12,11 +12,10 @@ import (
"fmt"
"log"
"os"
"os/exec"
"strings"
"sync/atomic"
"time"
exec "tailscale.com/tempfork/osexec"
)
// We have to run netstat, which is a bit expensive, so don't do it too often.

View File

@@ -5,10 +5,11 @@
package portlist
import (
"os/exec"
"syscall"
"time"
exec "tailscale.com/tempfork/osexec"
"golang.org/x/sys/windows"
)
// Forking on Windows is insanely expensive, so don't do it too often.
@@ -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

@@ -30,7 +30,7 @@ if [ $# != 1 ]; then
fi
fail=0
for file in $(find $1 -name '*.go'); do
for file in $(find $1 -name '*.go' -not -path '*/.git/*'); do
case $file in
$1/tempfork/*)
# Skip, tempfork of third-party code

24
shell.nix Normal file
View File

@@ -0,0 +1,24 @@
# This is a shell.nix file used to describe the environment that tailscale needs
# for development. This includes a lot of the basic tools that you need in order
# to get started. We hope this file will be useful for users of Nix on macOS or
# Linux.
#
# For more information about this and why this file is useful, see here:
# https://nixos.org/guides/nix-pills/developing-with-nix-shell.html
#
# Also look into direnv: https://direnv.net/, this can make it so that you can
# automatically get your environment set up when you change folders into the
# project.
{ pkgs ? import <nixpkgs> {} }:
pkgs.mkShell {
# This specifies the tools that are needed for people to get started with
# development. These tools include:
# - The Go compiler toolchain (and all additional tooling with it)
# - goimports, a robust formatting tool for Go source code
# - gopls, the language server for Go to increase editor integration
# - git, the version control program (used in some scripts)
buildInputs = with pkgs; [
go goimports gopls git
];
}

58
syncs/locked.go Normal file
View File

@@ -0,0 +1,58 @@
// 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 go1.13,!go1.16
// This file makes assumptions about the inner workings of sync.Mutex and sync.RWMutex.
// This includes not just their memory layout but their invariants and functionality.
// To prevent accidents, it is limited to a known good subset of Go versions.
package syncs
import (
"sync"
"sync/atomic"
"unsafe"
)
const (
mutexLocked = 1
// sync.Mutex field offsets
stateOffset = 0
// sync.RWMutext field offsets
mutexOffset = 0
readerCountOffset = 16
)
// add returns a pointer with value p + off.
func add(p unsafe.Pointer, off uintptr) unsafe.Pointer {
return unsafe.Pointer(uintptr(p) + off)
}
// AssertLocked panics if m is not locked.
func AssertLocked(m *sync.Mutex) {
p := add(unsafe.Pointer(m), stateOffset)
if atomic.LoadInt32((*int32)(p))&mutexLocked == 0 {
panic("mutex is not locked")
}
}
// AssertRLocked panics if rw is not locked for reading or writing.
func AssertRLocked(rw *sync.RWMutex) {
p := add(unsafe.Pointer(rw), readerCountOffset)
if atomic.LoadInt32((*int32)(p)) != 0 {
// There are readers present or writers pending, so someone has a read lock.
return
}
// No readers.
AssertWLocked(rw)
}
// AssertWLocked panics if rw is not locked for writing.
func AssertWLocked(rw *sync.RWMutex) {
m := (*sync.Mutex)(add(unsafe.Pointer(rw), mutexOffset))
AssertLocked(m)
}

123
syncs/locked_test.go Normal file
View File

@@ -0,0 +1,123 @@
// 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 go1.13,!go1.16
//lint:file-ignore SA2001 the empty critical sections are part of triggering different internal mutex states
package syncs
import (
"sync"
"testing"
"time"
)
func wantPanic(t *testing.T, fn func()) {
t.Helper()
defer func() {
recover()
}()
fn()
t.Fatal("failed to panic")
}
func TestAssertLocked(t *testing.T) {
m := new(sync.Mutex)
wantPanic(t, func() { AssertLocked(m) })
m.Lock()
AssertLocked(m)
m.Unlock()
wantPanic(t, func() { AssertLocked(m) })
// Test correct handling of mutex with waiter.
m.Lock()
AssertLocked(m)
go func() {
m.Lock()
m.Unlock()
}()
// Give the goroutine above a few moments to get started.
// The test will pass whether or not we win the race,
// but we want to run sometimes, to get the test coverage.
time.Sleep(10 * time.Millisecond)
AssertLocked(m)
}
func TestAssertWLocked(t *testing.T) {
m := new(sync.RWMutex)
wantPanic(t, func() { AssertWLocked(m) })
m.Lock()
AssertWLocked(m)
m.Unlock()
wantPanic(t, func() { AssertWLocked(m) })
// Test correct handling of mutex with waiter.
m.Lock()
AssertWLocked(m)
go func() {
m.Lock()
m.Unlock()
}()
// Give the goroutine above a few moments to get started.
// The test will pass whether or not we win the race,
// but we want to run sometimes, to get the test coverage.
time.Sleep(10 * time.Millisecond)
AssertWLocked(m)
}
func TestAssertRLocked(t *testing.T) {
m := new(sync.RWMutex)
wantPanic(t, func() { AssertRLocked(m) })
m.Lock()
AssertRLocked(m)
m.Unlock()
m.RLock()
AssertRLocked(m)
m.RUnlock()
wantPanic(t, func() { AssertRLocked(m) })
// Test correct handling of mutex with waiter.
m.RLock()
AssertRLocked(m)
go func() {
m.RLock()
m.RUnlock()
}()
// Give the goroutine above a few moments to get started.
// The test will pass whether or not we win the race,
// but we want to run sometimes, to get the test coverage.
time.Sleep(10 * time.Millisecond)
AssertRLocked(m)
m.RUnlock()
// Test correct handling of rlock with write waiter.
m.RLock()
AssertRLocked(m)
go func() {
m.Lock()
m.Unlock()
}()
// Give the goroutine above a few moments to get started.
// The test will pass whether or not we win the race,
// but we want to run sometimes, to get the test coverage.
time.Sleep(10 * time.Millisecond)
AssertRLocked(m)
m.RUnlock()
// Test correct handling of rlock with other rlocks.
// This is a bit racy, but losing the race hurts nothing,
// and winning the race means correct test coverage.
m.RLock()
AssertRLocked(m)
go func() {
m.RLock()
time.Sleep(10 * time.Millisecond)
m.RUnlock()
}()
time.Sleep(5 * time.Millisecond)
AssertRLocked(m)
m.RUnlock()
}

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 syncs contains addition sync types.
// Package syncs contains additional sync types and functionality.
package syncs
import "sync/atomic"
@@ -33,7 +33,7 @@ func NewWaitGroupChan() *WaitGroupChan {
}
// DoneChan returns a channel that's closed on completion.
func (c *WaitGroupChan) DoneChan() <-chan struct{} { return c.done }
func (wg *WaitGroupChan) DoneChan() <-chan struct{} { return wg.done }
// Add adds delta, which may be negative, to the WaitGroupChan
// counter. If the counter becomes zero, all goroutines blocked on
@@ -46,10 +46,10 @@ func (c *WaitGroupChan) DoneChan() <-chan struct{} { return c.done }
// than zero, may happen at any time. Typically this means the calls
// to Add should execute before the statement creating the goroutine
// or other event to be waited for.
func (c *WaitGroupChan) Add(delta int) {
n := atomic.AddInt64(&c.n, int64(delta))
func (wg *WaitGroupChan) Add(delta int) {
n := atomic.AddInt64(&wg.n, int64(delta))
if n == 0 {
close(c.done)
close(wg.done)
}
}

95
syncs/watchdog.go Normal file
View File

@@ -0,0 +1,95 @@
// 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 syncs
import (
"context"
"sync"
"time"
)
// Watch monitors mu for contention.
// On first call, and at every tick, Watch locks and unlocks mu.
// (Tick should be large to avoid adding contention to mu.)
// Max is the maximum length of time Watch will wait to acquire the lock.
// The time required to lock mu is sent on the returned channel.
// Watch exits when ctx is done, and closes the returned channel.
func Watch(ctx context.Context, mu sync.Locker, tick, max time.Duration) chan time.Duration {
// Set up the return channel.
c := make(chan time.Duration)
var (
closemu sync.Mutex
closed bool
)
sendc := func(d time.Duration) {
closemu.Lock()
defer closemu.Unlock()
if closed {
// Drop values written after c is closed.
return
}
c <- d
}
closec := func() {
closemu.Lock()
defer closemu.Unlock()
close(c)
closed = true
}
// check locks the mutex and writes how long it took to c.
// check returns ~immediately.
check := func() {
// Start a race between two goroutines.
// One locks the mutex; the other times out.
// Ensure that only one of the two gets to write its result.
// Since the common case is that locking the mutex is fast,
// let the timeout goroutine exit early when that happens.
var sendonce sync.Once
done := make(chan bool)
go func() {
start := time.Now()
mu.Lock()
mu.Unlock() //lint:ignore SA2001 ignore the empty critical section
elapsed := time.Since(start)
if elapsed > max {
elapsed = max
}
close(done)
sendonce.Do(func() { sendc(elapsed) })
}()
go func() {
select {
case <-time.After(max):
// the other goroutine may not have sent a value
sendonce.Do(func() { sendc(max) })
case <-done:
// the other goroutine sent a value
}
}()
}
// Check once at startup.
// This is mainly to make testing easier.
check()
// Start the watchdog goroutine.
// It checks the mutex every tick, until ctx is done.
go func() {
ticker := time.NewTicker(tick)
for {
select {
case <-ctx.Done():
closec()
ticker.Stop()
return
case <-ticker.C:
check()
}
}
}()
return c
}

71
syncs/watchdog_test.go Normal file
View File

@@ -0,0 +1,71 @@
// 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 syncs
import (
"context"
"sync"
"testing"
"time"
)
// Time-based tests are fundamentally flaky.
// We use exaggerated durations in the hopes of minimizing such issues.
func TestWatchUncontended(t *testing.T) {
mu := new(sync.Mutex)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// Once an hour, and now, check whether we can lock mu in under an hour.
tick := time.Hour
max := time.Hour
c := Watch(ctx, mu, tick, max)
d := <-c
if d == max {
t.Errorf("uncontended mutex did not lock in under %v", max)
}
}
func TestWatchContended(t *testing.T) {
mu := new(sync.Mutex)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// Every hour, and now, check whether we can lock mu in under a millisecond,
// which is enough time for an uncontended mutex by several orders of magnitude.
tick := time.Hour
max := time.Millisecond
mu.Lock()
defer mu.Unlock()
c := Watch(ctx, mu, tick, max)
d := <-c
if d != max {
t.Errorf("contended mutex locked in under %v", max)
}
}
func TestWatchMultipleValues(t *testing.T) {
mu := new(sync.Mutex)
ctx, cancel := context.WithCancel(context.Background())
defer cancel() // not necessary, but keep vet happy
// Check the mutex every millisecond.
// The goal is to see that we get a sufficient number of values out of the channel.
tick := time.Millisecond
max := time.Millisecond
c := Watch(ctx, mu, tick, max)
start := time.Now()
n := 0
for d := range c {
n++
if d == max {
t.Errorf("uncontended mutex did not lock in under %v", max)
}
if n == 10 {
cancel()
}
}
if elapsed := time.Since(start); elapsed > 100*time.Millisecond {
t.Errorf("expected 1 event per millisecond, got only %v events in %v", n, elapsed)
}
}

View File

@@ -4,7 +4,7 @@
package tailcfg
//go:generate go run tailscale.com/cmd/cloner -type=User,Node,Hostinfo,NetInfo,Group,Role,Capability -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"
@@ -14,7 +14,6 @@ import (
"strings"
"time"
"github.com/tailscale/wireguard-go/wgcfg"
"go4.org/mem"
"golang.org/x/oauth2"
"inet.af/netaddr"
@@ -27,14 +26,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.
@@ -92,8 +111,6 @@ type User struct {
Logins []LoginID
Roles []RoleID
Created time.Time
// Note: be sure to update Clone when adding new fields
}
type Login struct {
@@ -114,7 +131,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 {
@@ -125,20 +142,17 @@ type Node struct {
KeyExpiry time.Time
Machine MachineKey
DiscoKey DiscoKey
Addresses []wgcfg.CIDR // IP addresses of this Node directly
AllowedIPs []wgcfg.CIDR // range of IP addresses to route to this node
Endpoints []string `json:",omitempty"` // IP+port (public via STUN, and local LANs)
DERP string `json:",omitempty"` // DERP-in-IP:port ("127.3.3.40:N") endpoint
Addresses []netaddr.IPPrefix // IP addresses of this Node directly
AllowedIPs []netaddr.IPPrefix // range of IP addresses to route to this node
Endpoints []string `json:",omitempty"` // IP+port (public via STUN, and local LANs)
DERP string `json:",omitempty"` // DERP-in-IP:port ("127.3.3.40:N") endpoint
Hostinfo Hostinfo
Created time.Time
LastSeen *time.Time `json:",omitempty"`
KeepAlive bool // open and keep open a connection to this peer
KeepAlive bool `json:",omitempty"` // open and keep open a connection to this peer
MachineAuthorized bool // TODO(crawshaw): replace with MachineStatus
// NOTE: any new fields containing pointers in this type
// require changes to Node.Clone.
MachineAuthorized bool `json:",omitempty"` // TODO(crawshaw): replace with MachineStatus
}
type MachineStatus int
@@ -198,7 +212,7 @@ func isAlpha(b byte) bool {
return (b >= 'A' && b <= 'Z') || (b >= 'a' && b <= 'z')
}
// CheckTag valids whether a given string can be used as an ACL tag.
// CheckTag validates tag for use as an ACL tag.
// For now we allow only ascii alphanumeric tags, and they need to start
// with a letter. No unicode shenanigans allowed, and we reserve punctuation
// marks other than '-' for a possible future URI scheme.
@@ -228,6 +242,19 @@ func CheckTag(tag string) error {
return nil
}
// CheckRequestTags checks that all of h.RequestTags are valid.
func (h *Hostinfo) CheckRequestTags() error {
if h == nil {
return nil
}
for _, tag := range h.RequestTags {
if err := CheckTag(tag); err != nil {
return fmt.Errorf("tag(%#v): %w", tag, err)
}
}
return nil
}
type ServiceProto string
const (
@@ -239,12 +266,9 @@ 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?
// NOTE: any new fields containing pointers in this type
// require changes to Hostinfo.Clone.
}
// Hostinfo contains a summary of a Tailscale host.
@@ -254,21 +278,23 @@ type Service struct {
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
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")
Hostname string // name of the host the client runs on
GoArch string // 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
NetInfo *NetInfo `json:",omitempty"`
IPNVersion string `json:",omitempty"` // version of this code
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 `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
ShieldsUp bool `json:",omitempty"` // indicates whether the host is blocking incoming connections
ShareeNode bool `json:",omitempty"` // indicates this node exists in netmap because it's owned by a shared-to user
GoArch string `json:",omitempty"` // the host's GOARCH value (of the running binary)
RoutableIPs []netaddr.IPPrefix `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
NetInfo *NetInfo `json:",omitempty"`
// NOTE: any new fields containing pointers in this type
// require changes to Hostinfo.Clone and Hostinfo.Equal.
// require changes to Hostinfo.Equal.
}
// NetInfo contains information about the host's network state.
@@ -308,7 +334,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
@@ -320,7 +346,7 @@ type NetInfo struct {
// the control plane.
DERPLatency map[string]float64 `json:",omitempty"`
// Update Clone and BasicallyEqual when adding fields.
// Update BasicallyEqual when adding fields.
}
func (ni *NetInfo) String() string {
@@ -441,22 +467,60 @@ 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, delta Peers/UserProfiles, supports MagicDNS
// 6: 2020-12-07: means MapResponse.PacketFilter nil means unchanged
// 7: 2020-12-15: FilterRule.SrcIPs accepts CIDRs+ranges, doesn't warn about 0.0.0.0/::
// 8: 2020-12-19: client can receive IPv6 addresses and routes if beta enabled server-side
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.)
//
// If OmitPeers is true, Stream is false, and ReadOnly is false,
// then the server will let clients update their endpoints without
// breaking existing long-polling (Stream == true) connections.
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
// * "minimize-netmap": have control minimize the netmap, removing
// peers that are unreachable per ACLS.
DebugFlags []string `json:",omitempty"`
}
// PortRange represents a range of UDP or TCP port numbers.
@@ -467,26 +531,52 @@ type PortRange struct {
var PortRangeAny = PortRange{0, 65535}
// NetPortRange represents a single subnet:portrange.
// NetPortRange represents a range of ports that's allowed for one or more IPs.
type NetPortRange struct {
_ structs.Incomparable
IP string
Bits *int // backward compatibility: if missing, means "all" bits
IP string // IP, CIDR, Range, or "*" (same formats as FilterRule.SrcIPs)
Bits *int // deprecated; the old way to turn IP into a CIDR
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.
//
// It may take the following forms:
// * an IP address (IPv4 or IPv6)
// * the string "*" to match everything (both IPv4 & IPv6)
// * a CIDR (e.g. "192.168.0.0/16")
// * a range of two IPs, inclusive, separated by hyphen ("2eff::1-2eff::0800")
SrcIPs []string
// SrcBits is deprecated; it's the old way to specify a CIDR
// prior to MapRequest.Version 7. Its values correspond to the
// SrcIPs above.
//
// If an entry of SrcBits is present for the same index as a
// SrcIPs entry, 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.
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},
@@ -519,14 +609,15 @@ type MapResponse struct {
// Peers, if non-empty, is the complete list of peers.
// It will be set in the first MapResponse for a long-polled request/response.
// Subsequent responses will be delta-encoded if DeltaPeers was set in the request.
// Subsequent responses will be delta-encoded if MapRequest.Version >= 5 and server
// chooses, in which case Peers will be nil or zero length.
// If Peers is non-empty, PeersChanged and PeersRemoved should
// be ignored (and should be empty).
// Peers is always returned sorted by Node.ID.
Peers []*Node `json:",omitempty"`
// PeersChanged are the Nodes (identified by their ID) that
// have changed or been added since the past update on the
// HTTP response. It's only set if MapRequest.DeltaPeers was true.
// HTTP response. It's not used by the server if MapRequest.Version < 5.
// PeersChanged is always returned sorted by Node.ID.
PeersChanged []*Node `json:",omitempty"`
// PeersRemoved are the NodeIDs that are no longer in the peer list.
@@ -535,18 +626,32 @@ type MapResponse struct {
// DNS is the same as DNSConfig.Nameservers.
//
// TODO(dmytro): should be sent in DNSConfig.Nameservers once clients have updated.
DNS []wgcfg.IP `json:",omitempty"`
DNS []netaddr.IP `json:",omitempty"`
// SearchPaths are the same as DNSConfig.Domains.
//
// TODO(dmytro): should be sent in DNSConfig.Domains once clients have updated.
SearchPaths []string `json:",omitempty"`
DNSConfig DNSConfig `json:",omitempty"`
// ACLs
Domain string
// Domain is the name of the network that this node is
// in. It's either of the form "example.com" (for user
// foo@example.com, for multi-user networks) or
// "foo@gmail.com" (for siloed users on shared email
// providers). Its exact form should not be depended on; new
// forms are coming later.
Domain string
// PacketFilter are the firewall rules.
//
// For MapRequest.Version >= 6, a nil value means the most
// previously streamed non-nil MapResponse.PacketFilter within
// the same HTTP response. A non-nil but empty list always means
// no PacketFilter (that is, to block everything).
PacketFilter []FilterRule
UserProfiles []UserProfile
Roles []Role
UserProfiles []UserProfile // as of 1.1.541 (mapver 5): 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 +687,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 +725,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) }
@@ -666,7 +779,7 @@ func eqStrings(a, b []string) bool {
return true
}
func eqCIDRs(a, b []wgcfg.CIDR) bool {
func eqCIDRs(a, b []netaddr.IPPrefix) bool {
if len(a) != len(b) || ((a == nil) != (b == nil)) {
return false
}

View File

@@ -2,11 +2,14 @@
// 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; 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
import (
"inet.af/netaddr"
"tailscale.com/types/opt"
"tailscale.com/types/structs"
"time"
)
@@ -23,6 +26,19 @@ func (src *User) Clone() *User {
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 _UserNeedsRegeneration = User(struct {
ID UserID
LoginName string
DisplayName string
ProfilePicURL string
Domain string
Logins []LoginID
Roles []RoleID
Created time.Time
}{})
// Clone makes a deep copy of Node.
// The result aliases no memory with the original.
func (src *Node) Clone() *Node {
@@ -42,6 +58,27 @@ func (src *Node) Clone() *Node {
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 _NodeNeedsRegeneration = Node(struct {
ID NodeID
Name string
User UserID
Key NodeKey
KeyExpiry time.Time
Machine MachineKey
DiscoKey DiscoKey
Addresses []netaddr.IPPrefix
AllowedIPs []netaddr.IPPrefix
Endpoints []string
DERP string
Hostinfo Hostinfo
Created time.Time
LastSeen *time.Time
KeepAlive bool
MachineAuthorized bool
}{})
// Clone makes a deep copy of Hostinfo.
// The result aliases no memory with the original.
func (src *Hostinfo) Clone() *Hostinfo {
@@ -57,6 +94,25 @@ func (src *Hostinfo) Clone() *Hostinfo {
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 _HostinfoNeedsRegeneration = Hostinfo(struct {
IPNVersion string
FrontendLogID string
BackendLogID string
OS string
OSVersion string
DeviceModel string
Hostname string
ShieldsUp bool
ShareeNode bool
GoArch string
RoutableIPs []netaddr.IPPrefix
RequestTags []string
Services []Service
NetInfo *NetInfo
}{})
// Clone makes a deep copy of NetInfo.
// The result aliases no memory with the original.
func (src *NetInfo) Clone() *NetInfo {
@@ -74,6 +130,21 @@ func (src *NetInfo) Clone() *NetInfo {
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 _NetInfoNeedsRegeneration = NetInfo(struct {
MappingVariesByDestIP opt.Bool
HairPinning opt.Bool
WorkingIPv6 opt.Bool
WorkingUDP opt.Bool
UPnP opt.Bool
PMP opt.Bool
PCP opt.Bool
PreferredDERP int
LinkType string
DERPLatency map[string]float64
}{})
// Clone makes a deep copy of Group.
// The result aliases no memory with the original.
func (src *Group) Clone() *Group {
@@ -86,6 +157,14 @@ func (src *Group) Clone() *Group {
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 _GroupNeedsRegeneration = Group(struct {
ID GroupID
Name string
Members []ID
}{})
// Clone makes a deep copy of Role.
// The result aliases no memory with the original.
func (src *Role) Clone() *Role {
@@ -98,6 +177,14 @@ func (src *Role) Clone() *Role {
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 _RoleNeedsRegeneration = Role(struct {
ID RoleID
Name string
Capabilities []CapabilityID
}{})
// Clone makes a deep copy of Capability.
// The result aliases no memory with the original.
func (src *Capability) Clone() *Capability {
@@ -108,3 +195,177 @@ func (src *Capability) Clone() *Capability {
*dst = *src
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 _CapabilityNeedsRegeneration = Capability(struct {
ID CapabilityID
Type CapType
Val ID
}{})
// Clone makes a deep copy of Login.
// The result aliases no memory with the original.
func (src *Login) Clone() *Login {
if src == nil {
return nil
}
dst := new(Login)
*dst = *src
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 _LoginNeedsRegeneration = Login(struct {
_ structs.Incomparable
ID LoginID
Provider string
LoginName string
DisplayName string
ProfilePicURL string
Domain string
}{})
// Clone makes a deep copy of DNSConfig.
// The result aliases no memory with the original.
func (src *DNSConfig) Clone() *DNSConfig {
if src == nil {
return nil
}
dst := new(DNSConfig)
*dst = *src
dst.Nameservers = append(src.Nameservers[:0:0], src.Nameservers...)
dst.Domains = append(src.Domains[:0:0], src.Domains...)
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 _DNSConfigNeedsRegeneration = DNSConfig(struct {
Nameservers []netaddr.IP
Domains []string
PerDomain bool
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,RegisterResponse.
func Clone(dst, src interface{}) bool {
switch src := src.(type) {
case *User:
switch dst := dst.(type) {
case *User:
*dst = *src.Clone()
return true
case **User:
*dst = src.Clone()
return true
}
case *Node:
switch dst := dst.(type) {
case *Node:
*dst = *src.Clone()
return true
case **Node:
*dst = src.Clone()
return true
}
case *Hostinfo:
switch dst := dst.(type) {
case *Hostinfo:
*dst = *src.Clone()
return true
case **Hostinfo:
*dst = src.Clone()
return true
}
case *NetInfo:
switch dst := dst.(type) {
case *NetInfo:
*dst = *src.Clone()
return true
case **NetInfo:
*dst = src.Clone()
return true
}
case *Group:
switch dst := dst.(type) {
case *Group:
*dst = *src.Clone()
return true
case **Group:
*dst = src.Clone()
return true
}
case *Role:
switch dst := dst.(type) {
case *Role:
*dst = *src.Clone()
return true
case **Role:
*dst = src.Clone()
return true
}
case *Capability:
switch dst := dst.(type) {
case *Capability:
*dst = *src.Clone()
return true
case **Capability:
*dst = src.Clone()
return true
}
case *Login:
switch dst := dst.(type) {
case *Login:
*dst = *src.Clone()
return true
case **Login:
*dst = src.Clone()
return true
}
case *DNSConfig:
switch dst := dst.(type) {
case *DNSConfig:
*dst = *src.Clone()
return true
case **DNSConfig:
*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

@@ -11,7 +11,8 @@ import (
"testing"
"time"
"github.com/tailscale/wireguard-go/wgcfg"
"inet.af/netaddr"
"tailscale.com/types/wgkey"
)
func fieldsOf(t reflect.Type) (fields []string) {
@@ -23,18 +24,21 @@ func fieldsOf(t reflect.Type) (fields []string) {
func TestHostinfoEqual(t *testing.T) {
hiHandles := []string{
"IPNVersion", "FrontendLogID", "BackendLogID", "OS", "OSVersion",
"DeviceModel", "Hostname", "GoArch", "RoutableIPs", "RequestTags", "Services",
"NetInfo",
"IPNVersion", "FrontendLogID", "BackendLogID",
"OS", "OSVersion", "DeviceModel", "Hostname",
"ShieldsUp", "ShareeNode",
"GoArch",
"RoutableIPs", "RequestTags",
"Services", "NetInfo",
}
if have := fieldsOf(reflect.TypeOf(Hostinfo{})); !reflect.DeepEqual(have, hiHandles) {
t.Errorf("Hostinfo.Equal check might be out of sync\nfields: %q\nhandled: %q\n",
have, hiHandles)
}
nets := func(strs ...string) (ns []wgcfg.CIDR) {
nets := func(strs ...string) (ns []netaddr.IPPrefix) {
for _, s := range strs {
n, err := wgcfg.ParseCIDR(s)
n, err := netaddr.ParseIPPrefix(s)
if err != nil {
panic(err)
}
@@ -169,6 +173,11 @@ func TestHostinfoEqual(t *testing.T) {
&Hostinfo{Services: []Service{Service{Proto: TCP, Port: 1234, Description: "foo"}}},
true,
},
{
&Hostinfo{ShareeNode: true},
&Hostinfo{},
false,
},
}
for i, tt := range tests {
got := tt.a.Equal(tt.b)
@@ -185,9 +194,9 @@ func TestNodeEqual(t *testing.T) {
have, nodeHandles)
}
newPublicKey := func(t *testing.T) wgcfg.Key {
newPublicKey := func(t *testing.T) wgkey.Key {
t.Helper()
k, err := wgcfg.NewPrivateKey()
k, err := wgkey.NewPrivate()
if err != nil {
t.Fatal(err)
}
@@ -256,23 +265,23 @@ func TestNodeEqual(t *testing.T) {
true,
},
{
&Node{Addresses: []wgcfg.CIDR{}},
&Node{Addresses: []netaddr.IPPrefix{}},
&Node{Addresses: nil},
false,
},
{
&Node{Addresses: []wgcfg.CIDR{}},
&Node{Addresses: []wgcfg.CIDR{}},
&Node{Addresses: []netaddr.IPPrefix{}},
&Node{Addresses: []netaddr.IPPrefix{}},
true,
},
{
&Node{AllowedIPs: []wgcfg.CIDR{}},
&Node{AllowedIPs: []netaddr.IPPrefix{}},
&Node{AllowedIPs: nil},
false,
},
{
&Node{Addresses: []wgcfg.CIDR{}},
&Node{Addresses: []wgcfg.CIDR{}},
&Node{Addresses: []netaddr.IPPrefix{}},
&Node{Addresses: []netaddr.IPPrefix{}},
true,
},
{
@@ -421,8 +430,8 @@ func TestCloneNode(t *testing.T) {
}{
{"nil_fields", &Node{}},
{"zero_fields", &Node{
Addresses: make([]wgcfg.CIDR, 0),
AllowedIPs: make([]wgcfg.CIDR, 0),
Addresses: make([]netaddr.IPPrefix, 0),
AllowedIPs: make([]netaddr.IPPrefix, 0),
Endpoints: make([]string, 0),
}},
}

View File

@@ -1,47 +0,0 @@
This is a temporary fork of Go 1.13's os/exec package,
to work around https://github.com/golang/go/issues/36644.
The main modification (outside of removing some tests that require
internal-only packages to run) is:
```
commit 3c66be240f1ee1f1b5f03bed79eb0d9f8c08965a
Author: Avery Pennarun <apenwarr@gmail.com>
Date: Sun Jan 19 03:17:30 2020 -0500
Cmd.Wait(): handle EINTR return code from os.Process.Wait().
This is probably not actually the correct fix; most likely
os.Process.Wait() itself should be fixed to retry on EINTR so that it
never leaks out of that function. But if we're going to patch a
particular module, it's safer to patch a higher-level one like os/exec
rather than the os module itself.
diff --git a/exec.go b/exec.go
index 17ef003e..5375e673 100644
--- a/exec.go
+++ b/exec.go
@@ -498,7 +498,21 @@ func (c *Cmd) Wait() error {
}
c.finished = true
- state, err := c.Process.Wait()
+ var err error
+ var state *os.ProcessState
+ for {
+ state, err = c.Process.Wait()
+ if err != nil {
+ xe, ok := err.(*os.SyscallError)
+ if ok {
+ if xe.Unwrap() == syscall.EINTR {
+ // temporary error, retry wait syscall
+ continue
+ }
+ }
+ }
+ break
+ }
if c.waitDone != nil {
close(c.waitDone)
}
```

View File

@@ -1,23 +0,0 @@
// Copyright 2019 The Go 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 exec
import (
"testing"
)
func BenchmarkExecHostname(b *testing.B) {
b.ReportAllocs()
path, err := LookPath("hostname")
if err != nil {
b.Fatalf("could not find hostname: %v", err)
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
if err := Command(path).Run(); err != nil {
b.Fatalf("hostname: %v", err)
}
}
}

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