Compare commits

...

731 Commits

Author SHA1 Message Date
Joe Tsai
2f0753be86 cmd/tailscale: add basic support for admin subcommand
The admin subcommand is a thin wrapper over the REST API.
It (hopefully) makes administration of tailnets easier
than vanilla curl.

Signed-off-by: Joe Tsai <joetsai@digital-static.net>
2021-08-06 11:08:21 -07:00
David Crawshaw
360223fccb types/dnstype: introduce new package for Resolver
So the type can be used in net/dns without introducing a tailcfg
dependency.

For #2596

Signed-off-by: David Crawshaw <crawshaw@tailscale.com>
2021-08-06 08:54:33 -07:00
Christine Dodrill
4d19db7c9f scripts/installer: work on Oracle Linux (#2604)
Before we didn't detect it properly. Since Oracle Linux is diet centos,
we can just make the centos logic detect Oracle linux and everything
should be fine.

Signed-off-by: Christine Dodrill <xe@tailscale.com>
2021-08-06 11:47:04 -04:00
Brad Fitzpatrick
e6d4ab2dd6 net/portmapper: add start of self-contained portmapper integration tests
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-08-06 08:34:45 -07:00
julianknodt
98d36ee18d net/portmapper: add hook for use with prev ip
PCP handles external IPs by allowing the client to specify them in the packet, which is more
explicit than requiring 2 packets from PMP, so allow for future changes to add it in easily.

Signed-off-by: julianknodt <julianknodt@gmail.com>
2021-08-06 07:51:30 -07:00
julianknodt
85304d7392 net/portmapper: check disable flags
Signed-off-by: julianknodt <julianknodt@gmail.com>
2021-08-06 07:51:30 -07:00
julianknodt
777b711d96 net/portmapper: add pcp portmapping
This adds PCP portmapping, hooking into the existing PMP portmapping.

Signed-off-by: julianknodt <julianknodt@gmail.com>
2021-08-06 07:51:30 -07:00
julianknodt
5c98b1b8d0 net/portmapper: move pcp code to separate file
This moves all the PCP code to a separate file in preparation for portmapping with PCP.

Signed-off-by: julianknodt <julianknodt@gmail.com>
2021-08-06 07:51:30 -07:00
Josh Bleecher Snyder
eee6b85b9b cmd/tailscaled: don't require root for --cleanup
Without this, the integration tests fail locally for me:

--- FAIL: TestCollectPanic (7.61s)
    integration.go:74: built [tailscale.com/cmd/tailscaled tailscale.com/cmd/tailscale] in 1.59s
    integration_test.go:102: initial run: tailscaled requires root; use sudo tailscaled (or use --tun=userspace-networking)
    integration_test.go:108: cleanup failed: exit status 1: "tailscaled requires root; use sudo tailscaled (or use --tun=userspace-networking)\n"
    stuntest.go:64: STUN server shutdown
FAIL
FAIL	tailscale.com/tstest/integration	9.678s
FAIL

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-08-05 15:55:11 -07:00
Josh Bleecher Snyder
a5da4ed981 all: gofmt with Go 1.17
This adds "//go:build" lines and tidies up existing "// +build" lines.

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-08-05 15:54:00 -07:00
Brad Fitzpatrick
a729070252 net/tstun: add start of Linux TAP support, with DHCP+ARP server
Still very much a prototype (hard-coded IPs, etc) but should be
non-invasive enough to submit at this point and iterate from here.

Updates #2589

Co-Author: David Crawshaw <crawshaw@tailscale.com>
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-08-05 10:01:45 -07:00
Brad Fitzpatrick
fd7b738e5b derp: use pad32 package for padding, reduce duplication
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-08-04 14:43:01 -07:00
Brad Fitzpatrick
fdc081c291 net/portmapper: fix UPnP probing, work against all ports
Prior to Tailscale 1.12 it detected UPnP on any port.
Starting with Tailscale 1.11.x, it stopped detecting UPnP on all ports.

Then start plumbing its discovered Location header port number to the
code that was assuming port 5000.

Fixes #2109

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-08-04 12:49:49 -07:00
Josh Bleecher Snyder
f013960d87 tstime/mono: make json.Unmarshal of a zero time.Time yield a zero Time
This was the proximate cause of #2579.
#2582 is a deeper fix, but this will remain
as a footgun, so may as well fix it too.

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-08-04 11:22:58 -07:00
Brad Fitzpatrick
f3c96df162 ipn/ipnstate: move tailscale status "active" determination to tailscaled
Fixes #2579

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-08-04 09:10:49 -07:00
Brad Fitzpatrick
0858673f1f tstest/integration: regenerate test deps
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-08-04 08:39:10 -07:00
Joe Tsai
9d0c86b6ec util/deephash: remove unnecessary formatting for structs and slices (#2571)
The index for every struct field or slice element and
the number of fields for the struct is unncessary.

The hashing of Go values is unambiguous because every type (except maps)
encodes in a parsable manner. So long as we know the type information,
we could theoretically decode every value (except for maps).

At a high level:
* numbers are encoded as fixed-width records according to precision.
* strings (and AppendTo output) are encoded with a fixed-width length,
followed by the contents of the buffer.
* slices are prefixed by a fixed-width length, followed by the encoding
of each value. So long as we know the type of each element, we could
theoretically decode each element.
* arrays are encoded just like slices, but elide the length
since it is determined from the Go type.
* maps are encoded first with a byte indicating whether it is a cycle.
If a cycle, it is followed by a fixed-width index for the pointer,
otherwise followed by the SHA-256 hash of its contents. The encoding of maps
is not decodeable, but a SHA-256 hash is sufficient to avoid ambiguities.
* interfaces are encoded first with a byte indicating whether it is nil.
If not nil, it is followed by a fixed-width index for the type,
and then the encoding for the underlying value. Having the type be encoded
first ensures that the value could theoretically be decoded next.
* pointers are encoded first with a byte indicating whether it is
1) nil, 2) a cycle, or 3) newly seen. If a cycle, it is followed by
a fixed-width index for the pointer. If newly seen, it is followed by
the encoding for the pointed-at value.

Removing unnecessary details speeds up hashing:

	name              old time/op    new time/op    delta
	Hash-8              76.0µs ± 1%    55.8µs ± 2%  -26.62%        (p=0.000 n=10+10)
	HashMapAcyclic-8    61.9µs ± 0%    62.0µs ± 0%     ~             (p=0.666 n=9+9)
	TailcfgNode-8       10.2µs ± 1%     7.5µs ± 1%  -26.90%         (p=0.000 n=10+9)
	HashArray-8         1.07µs ± 1%    0.70µs ± 1%  -34.67%         (p=0.000 n=10+9)

Signed-off-by: Joe Tsai <joetsai@digital-static.net>
2021-08-03 20:35:57 -07:00
Brad Fitzpatrick
1db9032ff5 cmd/tailscaled: let portmap debug mode have an gateway/IP override knob
For testing pfSense clients "behind" pfSense on Digital Ocean where
the main interface still exists. This is easier for debugging.

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-08-03 19:34:58 -07:00
Denton Gentry
260b85458c net/dns: correct log message.
Signed-off-by: Denton Gentry <dgentry@tailscale.com>
2021-08-03 13:58:29 -07:00
Brad Fitzpatrick
54e33b511a net/dns/resolver: add test that I forgot to git add earlier
This was meant to be part of 53a2f63658 earlier
but I guess I failed at git.

Updates #2436
Updates tailscale/corp#2250
Updates tailscale/corp#2238

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-08-03 08:32:18 -07:00
David Crawshaw
eab80e3877 logpolicy: only log panics when running under systemd
Given that https://github.com/golang/go/issues/42888 is coming, this
catches most practical panics without interfering in our development
environments.

Signed-off-by: David Crawshaw <crawshaw@tailscale.com>
2021-08-03 08:25:06 -07:00
Brad Fitzpatrick
24ee0ed3c3 tstest/integration: update test deps
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-08-02 22:15:34 -07:00
Brad Fitzpatrick
31ea073a73 cmd/tailscaled: add debug -portmap mode
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-08-02 22:11:51 -07:00
Joe Tsai
d8fbce7eef util/deephash: hash uint{8,16,32,64} explicitly (#2502)
Instead of hashing the humanly formatted forms of a number,
hash the native machine bits of the integers themselves.

There is a small performance gain for this:
	name              old time/op    new time/op    delta
	Hash-8              75.7µs ± 1%    76.0µs ± 2%    ~            (p=0.315 n=10+9)
	HashMapAcyclic-8    63.1µs ± 3%    61.3µs ± 1%  -2.77%        (p=0.000 n=10+10)
	TailcfgNode-8       10.3µs ± 1%    10.2µs ± 1%  -1.48%        (p=0.000 n=10+10)
	HashArray-8         1.07µs ± 1%    1.05µs ± 1%  -1.79%        (p=0.000 n=10+10)

Signed-off-by: Joe Tsai <joetsai@digital-static.net>
2021-08-02 21:44:13 -07:00
Joe Tsai
01d4dd331d util/deephash: simplify hasher.hashMap (#2503)
The swapping of bufio.Writer between hasher and mapHasher is subtle.
Just embed a hasher in mapHasher to avoid complexity here.

No notable change in performance:
	name              old time/op    new time/op    delta
	Hash-8              76.7µs ± 1%    77.0µs ± 1%    ~            (p=0.182 n=9+10)
	HashMapAcyclic-8    62.4µs ± 1%    62.5µs ± 1%    ~            (p=0.315 n=10+9)
	TailcfgNode-8       10.3µs ± 1%    10.3µs ± 1%  -0.62%         (p=0.004 n=10+9)
	HashArray-8         1.07µs ± 1%    1.06µs ± 1%  -0.98%          (p=0.001 n=8+9)

Signed-off-by: Joe Tsai <joetsai@digital-static.net>
2021-08-02 21:29:14 -07:00
Brad Fitzpatrick
be921d1a95 net/dns/resolver: fix skipped DoH test that bitrot
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-08-02 15:26:27 -07:00
Josh Bleecher Snyder
0373ba36f3 logtail: fix typo in comment
Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-08-02 14:32:02 -07:00
David Crawshaw
1606ef5219 logtail: print panics from previous runs on stderr
Signed-off-by: David Crawshaw <crawshaw@tailscale.com>
2021-08-02 14:31:35 -07:00
David Crawshaw
3e039daf95 logpolicy: actually collect panics
(Written with Josh)

For #2544

Signed-off-by: David Crawshaw <crawshaw@tailscale.com>
2021-08-02 14:31:35 -07:00
Brad Fitzpatrick
7298e777d4 derp: reduce server memory by 30% by removing persistent bufio.Writer
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-08-02 10:17:56 -07:00
Brad Fitzpatrick
5a7ff2b231 net/dnsfallback: re-run go generate 2021-08-01 19:14:33 -07:00
Brad Fitzpatrick
b622c60ed0 derp,wgengine/magicsock: don't assume stringer is in $PATH for go:generate
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-08-01 19:14:08 -07:00
Matt Layher
effee49e45 net/interfaces: explicitly check netaddr.IP.Is6 in isUsableV6
Signed-off-by: Matt Layher <mdlayher@gmail.com>
2021-07-30 19:56:11 -07:00
Matt Layher
3ff8a55fa7 net/tsaddr: remove IsULA, replace with netaddr.IP.IsPrivate
Signed-off-by: Matt Layher <mdlayher@gmail.com>
2021-07-30 19:56:11 -07:00
Brad Fitzpatrick
d37451bac6 cmd/derper: dial VPC address with right context
Fix bug from just-submitted e422e9f4c9.

Updates #2414

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-07-29 14:29:31 -07:00
Brad Fitzpatrick
e422e9f4c9 cmd/derper: mesh over VPC network
Updates #2414

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-07-29 14:08:16 -07:00
David Crawshaw
0554b64452 ipnlocal: allow access to guest VMs/containers while using an exit node
Signed-off-by: David Crawshaw <crawshaw@tailscale.com>
2021-07-29 13:31:09 -07:00
Josh Bleecher Snyder
9da4181606 tstime/rate: new package
This is a simplified rate limiter geared for exactly our needs:
A fast, mono.Time-based rate limiter for use in tstun.
It was generated by stripping down the x/time/rate rate limiter
to just our needs and switching it to use mono.Time.

It removes one time.Now call per packet.

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-07-29 12:56:58 -07:00
Josh Bleecher Snyder
f6e833748b wgengine: use mono.Time
Migrate wgengine to mono.Time for performance-sensitive call sites.

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-07-29 12:56:58 -07:00
Josh Bleecher Snyder
8a3d52e882 wgengine/magicsock: use mono.Time
magicsock makes multiple calls to Now per packet.
Move to mono.Now. Changing some of the calls to
use package mono has a cascading effect,
causing non-per-packet call sites to also switch.

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-07-29 12:56:58 -07:00
Josh Bleecher Snyder
c2202cc27c net/tstun: use mono.Time
There's a call to Now once per packet.
Move to mono.Now.

Though the current implementation provides high precision,
we document it to be coarse, to preserve the ability
to switch to a coarse monotonic time later.

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-07-29 12:56:58 -07:00
Josh Bleecher Snyder
142670b8c2 tstime/mono: new package
Package mono provides a fast monotonic time.

Its primary advantage is that it is fast:
It is approximately twice as fast as time.Now.
This is because time.Now uses two clock calls,
one for wall time and one for monotonic time.

We ask for the current time 4-6 times per network packet.
At ~50ns per call to time.Now, that's enough to show
up in CPU profiles.

Package mono is a first step towards addressing that.
It is designed to be a near drop-in replacement for package time.

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-07-29 12:56:58 -07:00
Josh Bleecher Snyder
881bb8bcdc net/dns/resolver: allow an extra alloc for go closure allocation
Go 1.17 switches to a register ABI on amd64 platforms.
Part of that switch is that go and defer calls use an argument-less
closure, which allocates. This means that we have an extra
alloc in some DNS work. That's unfortunate but not a showstopper,
and I don't see a clear path to fixing it.
The other performance benefits from the register ABI will all
but certainly outweigh this extra alloc.

Fixes #2545

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-07-29 12:56:28 -07:00
Brad Fitzpatrick
b6179b9e83 net/dnsfallback: add new nodes
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-07-29 10:50:49 -07:00
Pratik
2d35737a7a Dockerfile: remove extra COPY step (#2355)
Signed-off-by: pratikbalar <pratik@improwised.com>
2021-07-28 11:07:50 -07:00
Aaron Bieber
c179b9b535 cmd/tsshd: switch from github.com/kr/pty to github.com/creack/pty
The kr/pty module moved to creack/pty per the kr/pty README[1].

creack/pty brings in support for a number of OS/arch combos that
are lacking in kr/pty.

Run `go mod tidy` while here.

[1] https://github.com/kr/pty/blob/master/README.md

Signed-off-by: Aaron Bieber <aaron@bolddaemon.com>
2021-07-28 09:14:47 -07:00
Brad Fitzpatrick
690ade4ee1 ipn/ipnlocal: add URL to IP forwarding error message
Updates #606

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-07-28 08:00:53 -07:00
David Crawshaw
f414a9cc01 net/dns/resolver: EDNS OPT record off-by-one
I don't know how to get access to a real packet. Basing this commit
entirely off:

       +------------+--------------+------------------------------+
       | Field Name | Field Type   | Description                  |
       +------------+--------------+------------------------------+
       | NAME       | domain name  | MUST be 0 (root domain)      |
       | TYPE       | u_int16_t    | OPT (41)                     |
       | CLASS      | u_int16_t    | requestor's UDP payload size |
       | TTL        | u_int32_t    | extended RCODE and flags     |
       | RDLEN      | u_int16_t    | length of all RDATA          |
       | RDATA      | octet stream | {attribute,value} pairs      |
       +------------+--------------+------------------------------+

From https://datatracker.ietf.org/doc/html/rfc6891#section-6.1.2

Signed-off-by: David Crawshaw <crawshaw@tailscale.com>
2021-07-27 16:39:27 -07:00
Josh Bleecher Snyder
1034b17bc7 net/tstun: buffer outbound channel
The handoff between tstun.Wrap's Read and poll methods
is one of the per-packet hotspots. It shows up in pprof.

Making outbound buffered increases throughput.

It is hard to measure exactly how much, because the numbers
are highly variable, but I'd estimate it at about 1%,
using the best observed max throughput across three runs.

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-07-27 15:54:34 -07:00
Josh Bleecher Snyder
965dccd4fc net/tstun: buffer outbound channel
The handoff between tstun.Wrap's Read and poll methods
is one of the per-packet hotspots. It shows up in pprof.

Making outbound buffered increases throughput.

It is hard to measure exactly how much, because the numbers
are highly variable, but I'd estimate it at about 1%,
using the best observed max throughput across three runs.

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-07-27 15:54:34 -07:00
Brad Fitzpatrick
7b9f02fcb1 cmd/tailscale/cli: document that empty string disable exit nodes, routes
Updates #2529

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-07-27 13:00:50 -07:00
Brad Fitzpatrick
d8d9036dbb tailcfg: add Node.PrimaryRoutes
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-07-27 12:09:40 -07:00
Brad Fitzpatrick
1b14e1d6bd version: bump date
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-07-27 08:05:17 -07:00
Denton Gentry
bf7ad05230 VERSION.txt: this is v1.13.0.
Signed-off-by: Denton Gentry <dgentry@tailscale.com>
2021-07-27 07:15:59 -07:00
Brad Fitzpatrick
68df379a7d net/portmapper: rename ErrGatewayNotFound to ErrGatewayRange, reword text
It confused & scared people. And it was just bad.

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-07-26 20:30:28 -07:00
Brad Fitzpatrick
aaf2df7ab1 net/{dnscache,interfaces}: use netaddr.IP.IsPrivate, delete copied code
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-07-26 20:30:28 -07:00
Christine Dodrill
dde8e28f00 disable vm tests on every commit to main
This experiment apparently failed.

Signed-off-by: Christine Dodrill <xe@tailscale.com>
2021-07-26 16:42:56 -07:00
Brad Fitzpatrick
c17d743886 net/dnscache: update a comment
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-07-26 16:16:08 -07:00
Brad Fitzpatrick
281d503626 net/dnscache: make Dialer try all resolved IPs
Tested manually with:

$ go test -v ./net/dnscache/ -dial-test=bogusplane.dev.tailscale.com:80

Where bogusplane has three A records, only one of which works.

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-07-26 15:44:32 -07:00
Brad Fitzpatrick
dfa5e38fad control/controlclient: report whether we're in a snap package
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-07-26 15:16:40 -07:00
Brad Fitzpatrick
e299300b48 net/dnscache: cache all IPs per hostname
Not yet used in the dialer, but plumbed around.

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-07-26 12:27:46 -07:00
Brad Fitzpatrick
7428ecfebd ipn/ipnlocal: populate Hostinfo.Package on Android
Fixes tailscale/corp#2266

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-07-26 10:35:37 -07:00
Brad Fitzpatrick
5c266bdb73 wgengine: re-set DNS config on Linux after a major link change
Updates #2458 (maybe fixes it)

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-07-26 08:01:27 -07:00
julianknodt
3377089583 tsweb: add float64 to logged metrics
A previously added metric which was float64 was being ignored in tsweb, because it previously
only accepted int64 and ints. It can be handled in the same way as ints.

Signed-off-by: julianknodt <julianknodt@gmail.com>
2021-07-25 21:02:36 -07:00
Brad Fitzpatrick
53a2f63658 net/dns/resolver: race well-known resolvers less aggressively
Instead of blasting away at all upstream resolvers at the same time,
make a timing plan upon reconfiguration and have each upstream have an
associated start delay, depending on the overall forwarding config.

So now if you have two or four upstream Google or Cloudflare DNS
servers (e.g. two IPv4 and two IPv6), we now usually only send a
query, not four.

This is especially nice on iOS where we start fewer DoH queries and
thus fewer HTTP/1 requests (because we still disable HTTP/2 on iOS),
fewer sockets, fewer goroutines, and fewer associated HTTP buffers,
etc, saving overall memory burstiness.

Fixes #2436
Updates tailscale/corp#2250
Updates tailscale/corp#2238

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-07-25 20:45:47 -07:00
Brad Fitzpatrick
e94ec448a7 net/dns/resolver: add forwardQuery type as race work prep
Add a place to hang state in a future change for #2436.
For now this just simplifies the send signature without
any functional change.

Updates #2436

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-07-25 15:43:49 -07:00
Brad Fitzpatrick
064b916b1a net/dns/resolver: fix func used as netaddr.IP in printf
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-07-25 15:21:51 -07:00
Joe Tsai
d145c594ad util/deephash: improve cycle detection (#2470)
The previous algorithm used a map of all visited pointers.
The strength of this approach is that it quickly prunes any nodes
that we have ever visited before. The detriment of the approach
is that pruning is heavily dependent on the order that pointers
were visited. This is especially relevant for hashing a map
where map entries are visited in a non-deterministic manner,
which would cause the map hash to be non-deterministic
(which defeats the point of a hash).

This new algorithm uses a stack of all visited pointers,
similar to how github.com/google/go-cmp performs cycle detection.
When we visit a pointer, we push it onto the stack, and when
we leave a pointer, we pop it from the stack.
Before visiting a pointer, we first check whether the pointer exists
anywhere in the stack. If yes, then we prune the node.
The detriment of this approach is that we may hash a node more often
than before since we do not prune as aggressively.

The set of visited pointers up until any node is only the
path of nodes up to that node and not any other pointers
that may have been visited elsewhere. This provides us
deterministic hashing regardless of visit order.
We can now delete hashMapFallback and associated complexity,
which only exists because the previous approach was non-deterministic
in the presence of cycles.

This fixes a failure of the old algorithm where obviously different
values are treated as equal because the pruning was too aggresive.
See https://github.com/tailscale/tailscale/issues/2443#issuecomment-883653534

The new algorithm is slightly slower since it prunes less aggresively:
	name              old time/op    new time/op    delta
	Hash-8              66.1µs ± 1%    68.8µs ± 1%   +4.09%        (p=0.000 n=19+19)
	HashMapAcyclic-8    63.0µs ± 1%    62.5µs ± 1%   -0.76%        (p=0.000 n=18+19)
	TailcfgNode-8       9.79µs ± 2%    9.88µs ± 1%   +0.95%        (p=0.000 n=19+17)
	HashArray-8          643ns ± 1%     653ns ± 1%   +1.64%        (p=0.000 n=19+19)
However, a slower but more correct algorithm seems
more favorable than a faster but incorrect algorithm.

Signed-off-by: Joe Tsai <joetsai@digital-static.net>
2021-07-22 15:22:48 -07:00
Brad Fitzpatrick
7b295f3d21 net/portmapper: disable UPnP on iOS for now
Updates #2495

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-07-22 13:33:38 -07:00
Brad Fitzpatrick
4a2c3e2a0a control/controlclient: grow goroutine debug buffer as needed
To not allocate 1MB up front on iOS.

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-07-22 13:18:05 -07:00
Brad Fitzpatrick
1986d071c3 control/controlclient: don't use regexp in goroutine stack scrubbing
To reduce binary size on iOS.

Updates tailscale/corp#2238

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-07-22 13:18:05 -07:00
Christine Dodrill
60f34c70a2 tstest/integration/vms: disable rDNS for sshd on centos (#2492)
This prevents centos tests from timing out because sshd does reverse dns
lookups on every session being established instead of doing it once on
the acutal ssh connection being established. This is odd. Appending this
to the sshd config and restarting it seems to fix it though.

Signed-off-by: Christine Dodrill <xe@tailscale.com>
2021-07-22 15:24:52 -04:00
Christine Dodrill
8db26a2261 tstest/integration/vms: disable nixos unstable (#2491)
cloud-init broke with the upgrade to python 3.9:
https://github.com/NixOS/nixpkgs/issues/131098

Signed-off-by: Christine Dodrill <xe@tailscale.com>
2021-07-22 15:16:11 -04:00
Brad Fitzpatrick
cecfc14875 net/dns: don't build init*.go on non-windows
To remove the regexp dep on iOS, notably.

Updates tailscale/corp#2238

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-07-22 11:58:42 -07:00
Brad Fitzpatrick
2968893add net/dns/resolver: bound DoH usage on iOS
Updates tailscale/corp#2238

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-07-22 10:54:24 -07:00
Brad Fitzpatrick
95a9adbb97 wgengine/netstack: implement UDP relaying to advertised subnets
TCP was done in 662fbd4a09.

This does the same for UDP.

Tested by hand. Integration tests will have to come later. I'd wanted
to do it in this commit, but the SOCKS5 server needed for interop
testing between two userspace nodes doesn't yet support UDP and I
didn't want to invent some whole new userspace packet injection
interface at this point, as SOCKS seems like a better route, but
that's its own bug.

Fixes #2302

RELNOTE=netstack mode can now UDP relay to subnets

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-07-21 22:32:26 -07:00
Brad Fitzpatrick
3daf27eaad net/dns/resolver: fall back to IPv6 for well-known DoH servers if v4 fails
Should help with IPv6-only environments when the tailnet admin
only specified IPv4 DNS IPs.

See https://github.com/tailscale/tailscale/issues/2447#issuecomment-884188562

Co-Author: Adrian Dewhurst <adrian@tailscale.com>
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-07-21 12:45:25 -07:00
Brad Fitzpatrick
74eee4de1c net/dns/resolver: use correct Cloudflare DoH hostnames
We were using the wrong ones for the malware & adult content
variants. Docs:

https://developers.cloudflare.com/1.1.1.1/1.1.1.1-for-families/setup-instructions/dns-over-https

Earlier commit which added them:
236eb4d04d

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-07-21 12:24:36 -07:00
Joe Tsai
d666bd8533 util/deephash: disambiguate hashing of AppendTo (#2483)
Prepend size to AppendTo output.

Fixes #2443

Signed-off-by: Joe Tsai <joetsai@digital-static.net>
2021-07-21 11:29:08 -07:00
Joe Tsai
23ad028414 util/deephash: include type as part of hash for interfaces (#2476)
A Go interface may hold any number of different concrete types.
Just because two underlying values hash to the same thing
does not mean the two values are identical if they have different
concrete types. As such, include the type in the hash.
2021-07-21 10:26:04 -07:00
julianknodt
3a4201e773 net/portmapper: return correct upnp port
Previously, this was incorrectly returning the internal port, and using that with the external
exposed IP when it did not use WANIPConnection2. In the case when we must provide a port, we
return it instead.

Noticed this while implementing the integration test for upnp.

Signed-off-by: julianknodt <julianknodt@gmail.com>
2021-07-21 10:11:47 -07:00
Joe Tsai
a5fb8e0731 util/deephash: introduce deliberate instability (#2477)
Seed the hash upon first use with the current time.
This ensures that the stability of the hash is bounded within
the lifetime of one program execution.
Hopefully, this prevents future bugs where someone assumes that
this hash is stable.

Signed-off-by: Joe Tsai <joetsai@digital-static.net>
2021-07-21 09:23:04 -07:00
Brad Fitzpatrick
ecac74bb65 wgengine/netstack: fix doc comment
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-07-21 08:25:05 -07:00
Brad Fitzpatrick
e4fecfe31d wgengine/{monitor,router}: restore Linux ip rules when systemd deletes them
Thanks.

Fixes #1591

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-07-20 15:52:22 -07:00
Josh Bleecher Snyder
0aa77ba80f tstest/integration: fix filch test flake
Filch doesn't like having multiple processes competing
for the same log files (#937).

Parallel integration tests were all using the same log files.

Add a TS_LOGS_DIR env var that the integration test can use
to use separate log files per test.

Fixes #2269

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-07-20 14:16:28 -07:00
Brad Fitzpatrick
ed8587f90d wgengine/router: take a link monitor
Prep for #1591 which will need to make Linux's router react to changes
that the link monitor observes.

The router package already depended on the monitor package
transitively. Now it's explicit.

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-07-20 13:43:40 -07:00
Josh Bleecher Snyder
24db1a3c9b safesocket: print full lsof command on failure
This makes it easier to manually run the command
to discover why it is failing.

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-07-20 13:35:31 -07:00
Josh Bleecher Snyder
130c5e727b safesocket: reduce log spam while running integration tests
Instead of logging lsof execution failures to stdout,
incorporate them into the returned error.

While we're here, make it clear that the file
success case always returns a nil error.

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-07-20 13:35:31 -07:00
Josh Bleecher Snyder
f80193fa4c tstest/integration: shorten test names
The maximum unix domain socket path length on darwin is 104 bytes,
including the trailing NUL.

On my machine, the path created by some newly added tests (6eecf3c9)
was too long, resulting in cryptic test failures.

Shorten the names of the tests, and add a check to make
the diagnosis easier next time.

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-07-20 13:35:31 -07:00
Joe Tsai
81cdd2f26c Merge pull request #2464 from tailscale/dsnet/opaque-hash
util/deephash: make hash type opaque
2021-07-20 12:45:30 -07:00
Joe Tsai
9a0c8bdd20 util/deephash: make hash type opaque
The fact that Hash returns a [sha256.Size]byte leaks details about
the underlying hash implementation. This could very well be any other
hashing algorithm with a possible different block size.

Abstract this implementation detail away by declaring an opaque type
that is comparable. While we are changing the signature of UpdateHash,
rename it to just Update to reduce stutter (e.g., deephash.Update).

Signed-off-by: Joe Tsai <joetsai@digital-static.net>
2021-07-20 11:03:25 -07:00
Brad Fitzpatrick
a909d37a59 derp: rate limit how often same-key clients can kick each other off server
Updates #392
Updates #506

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-07-20 09:31:43 -07:00
Brad Fitzpatrick
e74d37d30f net/dns{,/resolver}: quiet DNS output logging
It was a huge chunk of the overall log output and made debugging
difficult. Omit and summarize the spammy *.arpa parts instead.

Fixes tailscale/corp#2066 (to which nobody had opinions, so)
2021-07-19 22:24:43 -07:00
Brad Fitzpatrick
b6d70203d3 ipn/ipnlocal: fix 'tailscale up' on Windows without GUI
With this, I can now:

* install Tailscale
* stop the GUI
* net stop Tailscale
* net start Tailscale
* tailscale up --unattended

(where the middle three steps simulate what would happen on a Windows
Server Core machine without a GUI)

Fixes #2137

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-07-19 15:52:47 -07:00
Brad Fitzpatrick
7f7a81e5ae cmd/tailscaled: add func to create ipnserver.Opts
To unify the Windows service and non-service/non-Windows paths a bit.

And provides a way to make Linux act like Windows for testing.
(notably, for testing the fix to #2137)

One perhaps visible change of this is that tailscaled.exe when run in
cmd.exe/powershell (not as a Windows Service) no longer uses the
"_daemon" autostart key. But in addition to being naturally what falls
out of this change, that's also what Windows users would likely want,
as otherwise the unattended mode user is ignored when the "_daemon"
autostart key is specified. Notably, this would let people debug what
their normally-run-as-a-service tailscaled is doing, even when they're
running in Unattended Mode.

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-07-19 15:52:47 -07:00
Brad Fitzpatrick
87244eda3f cmd/tailscale/cli: allow effective GOOS to be changed for integration tests
Adds TS_DEBUG_UP_FLAG_GOOS for integration tests to make "tailscale
up" act like other OSes.

For an upcoming change to test #2137.

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-07-19 15:41:35 -07:00
Josh Bleecher Snyder
787939a60c .github/workflows: add 'go generate' CI job
Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-07-19 15:31:56 -07:00
Josh Bleecher Snyder
84a6dcd9a9 net/dnsfallback: regenerate
Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-07-19 15:31:56 -07:00
Josh Bleecher Snyder
4dbbd0aa4a cmd/addlicense: add command to add licenseheaders to generated code
And use it to make our stringer invocations match the existing code.

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-07-19 15:31:56 -07:00
Josh Bleecher Snyder
0ec9040c5e scripts: remove special case for _strings.go files in check license headers
And add a license header for derp/dropreason_string.go to make it happy.

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-07-19 15:31:56 -07:00
Josh Bleecher Snyder
4b1f2ae382 tstest/integration: generate deps for all platforms in deps generator
We have different deps depending on the platform.
If we pick a privileged platform, we'll miss some deps.
If we use the union of all platforms, the integration test
won't compile on some platforms, because it'll import
packages that don't compile on that platform.

Give in to the madness and give each platform its own deps file.

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-07-19 15:31:56 -07:00
Josh Bleecher Snyder
41d06bdf86 tempfork/wireguard-windows: remove unnecessary build tag
The _windows.go suffix suffices.
This allows go:generate to run without creating a diff.

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-07-19 15:31:56 -07:00
Josh Bleecher Snyder
c179580599 wgengine/magicsock: add debug envvar to force all traffic over DERP
This would have been useful during debugging DERP issues recently.

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-07-19 15:30:50 -07:00
Brad Fitzpatrick
e41193ec4d wgengine/monitor: don't spam about Linux RTM_NEWRULE events
The earlier 2ba36c294b started listening
for ip rule changes and only cared about DELRULE events, buts its subscription
included all rule events, including new ones, which meant we were then
catching our own ip rule creations and logging about how they were unknown.

Stop that log spam.

Updates #1591

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-07-19 14:30:15 -07:00
Brad Fitzpatrick
ec4d721572 cmd/tailscaled: use state key constant from ipn package
Rather than redefining it again.

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-07-19 14:14:27 -07:00
Brad Fitzpatrick
e2eaae8224 cmd/derpprobe: add in a delay to wait for mesh info to sync 2021-07-19 07:52:55 -07:00
Brad Fitzpatrick
2ba36c294b wgengine/monitor: subscribe to Linux ip rule events, log on rule deletes
For debugging & working on #1591 where certain versions of systemd-networkd
delete Tailscale's ip rule entries.

Updates #1591

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-07-18 14:50:47 -07:00
Avery Pennarun
14135dd935 ipn/ipnlocal: make state_test catch the bug fixed by #2445
Updates #2434

Signed-off-by: Avery Pennarun <apenwarr@tailscale.com>
2021-07-17 08:41:39 -07:00
Brad Fitzpatrick
6eecf3c9d1 ipn/ipnlocal: stay out of map poll when down
Fixes #2434

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-07-17 08:41:39 -07:00
Christine Dodrill
798b0da470 tstest/integration/vms: codegen for top level tests (#2441)
This moves the distribution definitions into a maintainable hujson file
instead of just existing as constants in `distros.go`. Comments are
maintained from the inline definitions.

This uses jennifer[1] for hygenic source tree creation. This allows us
to generate a unique top-level test for each VM run. This should
hopefully help make the output of `go test` easier to read.

This also separates each test out into its own top-level test so that we
can better track the time that each distro takes. I really wish there
was a way to have the `test_codegen.go` file _always_ run as a part of
the compile process instead of having to rely on people remembering to
run `go generate`, but I am limited by my tools.

This will let us remove the `-distro-regex` flag and use `go test -run`
to pick which distros are run.

Signed-off-by: Christine Dodrill <xe@tailscale.com>
2021-07-16 15:25:16 -04:00
Christine Dodrill
391207bbcf tstest/integration/vms: use one testcontrol instance per VM (#2437)
This paves the way for future MagicDNS tests.

Signed-off-by: Christine Dodrill <xe@tailscale.com>
2021-07-16 11:53:12 -04:00
Brad Fitzpatrick
171ec9f8f4 control/{controlknobs,controlclient}: simplify knobs API, fix controlclient crash
From integration tests elsewhere:

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

goroutine 226 [running]:
tailscale.com/control/controlclient.(*Direct).sendMapRequest(0xc00053e1e0, 0x16670f0, 0xc000353780, 0xffffffffffffffff, 0xc0003e5f10, 0x0, 0x0)
   /home/runner/go/pkg/mod/tailscale.com@v1.1.1-0.20210715222212-1bb6abc604c1/control/controlclient/direct.go:803 +0x19bb
tailscale.com/control/controlclient.(*Direct).PollNetMap(...)
   /home/runner/go/pkg/mod/tailscale.com@v1.1.1-0.20210715222212-1bb6abc604c1/control/controlclient/direct.go:574
tailscale.com/control/controlclient.(*Auto).mapRoutine(0xc00052a1e0)
   /home/runner/go/pkg/mod/tailscale.com@v1.1.1-0.20210715222212-1bb6abc604c1/control/controlclient/auto.go:464 +0x571
created by tailscale.com/control/controlclient.(*Auto).Start
   /home/runner/go/pkg/mod/tailscale.com@v1.1.1-0.20210715222212-1bb6abc604c1/control/controlclient/auto.go:151 +0x65
exit status 2

Also remove types/opt.Bool API addition which is now unnecessary.

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-07-15 22:34:50 -07:00
julianknodt
1bb6abc604 net/portmapper: add upnp port mapping
Add in UPnP portmapping, using goupnp library in order to get the UPnP client and run the
portmapping functions. This rips out anywhere where UPnP used to be in portmapping, and has a
flow separate from PMP and PCP.

RELNOTE=portmapper now supports UPnP mappings

Fixes #682
Updates #2109

Signed-off-by: julianknodt <julianknodt@gmail.com>
2021-07-15 15:22:12 -07:00
Brad Fitzpatrick
236eb4d04d net/dns/resolver: upgrade forwarded MagicDNS queries to DoH when IP known
Recognize Cloudflare, Google, Quad9 which are by far the
majority of upstream DNS servers that people use.

RELNOTE=MagicDNS now uses DNS-over-HTTPS when querying popular upstream resolvers,
so DNS queries aren't sent in the clear over the Internet.

Updates #915 (might fix it?)
Updates #988 (gets us closer, if it fixes Android)
Updates #74 (not yet configurable, but progress)
Updates #2056 (not yet configurable, dup of #74?)

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-07-15 12:03:52 -07:00
Aaditya Chaudhary
4f89fe17a2 Implemented Commandline Download Speedtest (#2064)
Added the net/speedtest package that contains code for starting up a
speedtest server and a client. The speedtest command for starting a
client takes in a duration for the speedtest as well as the host and
port of the speedtest server to connect to. The speedtest command for
starting a server takes in a host:port pair to listen on.

Signed-off-by: Aaditya Chaudhary <32117362+AadityaChaudhary@users.noreply.github.com>
2021-07-15 14:43:13 -04:00
Christine Dodrill
3ebe16558c tstest/integration/vms: attempt to send more UDP packets (#2433)
Apparently this test was flaking because I critically misunderstood how
the kernel buffers UDP packets for senders. I'm trying to send more UDP
packets and will see if that helps.

Signed-off-by: Christine Dodrill <xe@tailscale.com>
2021-07-15 11:21:19 -04:00
Christine Dodrill
09e81b8ba1 tstest/integration/vms: deflake the tailscale_status test (#2427)
This test used to try to run this only once, but this variant of the
test attempts to run `tailscale status` up to 6 times in a loop with
exponential backoff.

This fixes the flakiness found in previous instances of this test.

Signed-off-by: Christine Dodrill <xe@tailscale.com>
2021-07-15 09:59:18 -04:00
julianknodt
b67a3007d5 cmd/derper: remove default for non-root users
Signed-off-by: julianknodt <julianknodt@gmail.com>
2021-07-14 17:48:31 -07:00
julianknodt
9d4eddcef8 cmd/derper: add default -c arguments
This adds some convenient defaults for -c, so that user-provided DERPs require less command line
flags.

Signed-off-by: julianknodt <julianknodt@gmail.com>
2021-07-14 17:20:56 -07:00
Brad Fitzpatrick
ee71c966fd cmd/derpprobe: bound node pair probe duration
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-07-14 12:05:39 -07:00
Denton Gentry
e28bc49e5f netns_linux: remove special handling for tests.
With netns handling localhost now, existing tests no longer
need special handling. The tests set up their connections to
localhost, and the connections work without fuss.

Remove the special handling for tests.

Also remove the hostinfo.TestCase support, since this was
the only use of it. It can be added back later if really
needed, but it would be better to try to make tests work
without special cases.

Signed-off-by: Denton Gentry <dgentry@tailscale.com>
2021-07-14 10:01:14 -07:00
Denton Gentry
d2480fd508 net/netns: support !CAP_NET_ADMIN
netns_linux checked whether "ip rule" could run to determine
whether to use SO_MARK for network namespacing. However in
Linux environments which lack CAP_NET_ADMIN, such as various
container runtimes, the "ip rule" command succeeds but SO_MARK
fails due to lack of permission. SO_BINDTODEVICE would work in
these environments, but isn't tried.

In addition to running "ip rule" check directly whether SO_MARK
works or not. Among others, this allows Microsoft Azure App
Service and AWS App Runner to work.

Signed-off-by: Denton Gentry <dgentry@tailscale.com>
2021-07-14 10:01:14 -07:00
Denton Gentry
1896bf99d9 netns_linux: No namespace for localhost.
Connections to a control server or log server on localhost,
used in a number of tests, are working right now because the
calls to SO_MARK in netns fail for non-root but then we ignore
the failure when running in tests.

Unfortunately that failure in SO_MARK also affects container
environments without CAP_NET_ADMIN, breaking Tailscale
connectivity. We're about to fix netns to recognize when SO_MARK
doesn't work and use SO_BINDTODEVICE instead. Doing so makes
tests fail, as their sockets now BINDTODEVICE of the default
route and cannot connect to localhost.

Add support to skip namespacing for localhost connections,
which Darwin and Windows already do. This is not conditional
on running within a test, if you tell tailscaled to connect
to localhost it will automatically use a non-namespaced
socket to do so.

Signed-off-by: Denton Gentry <dgentry@tailscale.com>
2021-07-14 10:01:14 -07:00
Denton Gentry
d3697053c9 hostinfo: add AWS Fargate.
Several other AWS services like App Run and Lightsail Containers
appear to be layers atop Fargate, to the point that we cannot easily
tell them apart from within the container. Contacting the metadata
service would distinguish them, but doing that from inside tailscaled
seems uncalled for. Just report them as Fargate.

Signed-off-by: Denton Gentry <dgentry@tailscale.com>
2021-07-14 10:01:14 -07:00
Denton Gentry
0ae2d2b3ab hostinfo: detect TestCase environment.
Treat automated tests as their own, unique environment
rather than the type of container they are running in.

Signed-off-by: Denton Gentry <dgentry@tailscale.com>
2021-07-14 10:01:14 -07:00
Denton Gentry
61622b18fa net/interface: add AWS App Runner /proc/net/route test
Signed-off-by: Denton Gentry <dgentry@tailscale.com>
2021-07-14 10:01:14 -07:00
Brad Fitzpatrick
98ad7f279c ipn/localapi: fix inability to receive taildrop files w/ escaped names
The localapi was double-unescaping: once by net/http populating
the URL, and once by ourselves later. We need to start with the raw
escaped URL if we're doing it ourselves.

Started to write a test but it got invasive. Will have to add those
tests later in a commit that's not being cherry-picked to a release
branch.

Fixes #2288

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-07-13 15:37:59 -07:00
Josh Bleecher Snyder
4f4dae32dd wgengine/magicsock: fix latent data race in test
logBufWriter had no serialization.
It just so happens that none of its users currently ever log concurrently.
Make it safe for concurrent use.

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-07-13 15:14:18 -07:00
Brad Fitzpatrick
01e159b610 ipn/ipnlocal: save prefs to disk on UpdatePrefs
Regression from 6d10655dc3, which added
UpdatePrefs but didn't write it out to disk.

I'd planned on adding tests to state_test.go which is why I'd earlier
added 46896a9311 to prepare for making
such persistence tests easier to write, but turns out state_test.go
didn't even test UpdatePrefs, so I'm staying out of there.
Instead, this is tested using integration tests.

Fixes #2321

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-07-13 15:01:38 -07:00
julianknodt
fb06ad19e7 wgcfg: Switch to using mem.RO
As Brad suggested, mem.RO allows for a lot of easy perf gains. There were also some smaller
changes outside of mem.RO, such as using hex.Decode instead of hex.DecodeString.

```
name        old time/op    new time/op    delta
FromUAPI-8    14.7µs ± 3%    12.3µs ± 4%  -16.58%  (p=0.008 n=5+5)

name        old alloc/op   new alloc/op   delta
FromUAPI-8    9.52kB ± 0%    7.04kB ± 0%  -26.05%  (p=0.008 n=5+5)

name        old allocs/op  new allocs/op  delta
FromUAPI-8      77.0 ± 0%      29.0 ± 0%  -62.34%  (p=0.008 n=5+5)
```

Signed-off-by: julianknodt <julianknodt@gmail.com>
2021-07-13 13:45:44 -07:00
julianknodt
d349a3231e wgcfg: use string cut instead of string split
Signed-off-by: julianknodt <julianknodt@gmail.com>
2021-07-13 13:45:44 -07:00
julianknodt
664edbe566 wgcfg: add benchmark for FromUAPI
Adds a benchmark for FromUAPI in wgcfg.
It appears that it's not actually that slow, the main allocations are from the scanner and new
config.
Updates #1912.

Signed-off-by: julianknodt <julianknodt@gmail.com>
2021-07-13 13:45:44 -07:00
Christine Dodrill
5b845631ce tstest/integration/vms: delete a log file i was using to debug the tests (#2416)
Signed-off-by: Christine Dodrill <xe@tailscale.com>
2021-07-13 15:37:30 -04:00
Dan Bond
ceb8c2b34e cmd/tailscale: update web server test copyright
Signed-off-by: Dan Bond <danbond@protonmail.com>
2021-07-13 12:30:49 -07:00
Dan Bond
52972679e6 cmd/tailscale: improve web server test structure
Signed-off-by: Dan Bond <danbond@protonmail.com>
2021-07-13 12:30:49 -07:00
Dan Bond
4c684fcf8c cmd/tailscale: test web server url func
Signed-off-by: Dan Bond <danbond@protonmail.com>
2021-07-13 12:30:49 -07:00
Dan Bond
652bbc9aa0 cmd/tailscale: log web listen addr
Signed-off-by: Dan Bond <danbond@protonmail.com>
2021-07-13 12:30:49 -07:00
Brad Fitzpatrick
1cedd944cf cmd/tailscale/cli: diagnose missing tailscaled on 'up'
Fixes #2029

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-07-13 12:23:11 -07:00
Christine Dodrill
ede8ec1e20 tstest/integration/vms: split up test framework into files (#2396)
My spatial memory functions poorly with large files and the vms_test.go
file recently surpassed the point where it functions adequately. This
patch splits up vms_test.go into more files to make my spatial memory
function like I need it to.

Signed-off-by: Christine Dodrill <xe@tailscale.com>
2021-07-13 13:45:09 -04:00
Christine Dodrill
1f2a877c61 tstest/integration/testcontrol: add ipv6 support (#2394)
Split from https://github.com/tailscale/tailscale/pull/2376.

This adds IPv6 support to testcontrol so each member of the tailscale
network gets an IPv6 address too.

Signed-off-by: Christine Dodrill <xe@tailscale.com>
2021-07-13 13:10:08 -04:00
Brad Fitzpatrick
61e8fd4698 cmd/derpprobe: move from cmd/derper/derpprobe
Because our corp build system is confused by nested
binaries.

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-07-13 09:31:00 -07:00
maddie
d976a84d7e derp: allow self node when verifying clients
Fixes #2408

Signed-off-by: Maddie Zhan <maddie.zhan@cynovan.com>
2021-07-13 08:37:37 -07:00
Brad Fitzpatrick
05da2691a5 cmd/derper/derpprobe: add derp prober
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-07-13 08:30:15 -07:00
David Crawshaw
87481282eb ipn: another controlplane synonym
This one doesn't bother me so much, as long term we want a synonym here.

Fixes #2384
Fixes #2386

Signed-off-by: David Crawshaw <crawshaw@tailscale.com>
2021-07-13 08:22:10 -07:00
Brad Fitzpatrick
4c0494185b derp: remove "fine for now" intentional slow memory leak from derp server
It was once believed that it might be useful. It wasn't. We never used it.

Remove it so we don't slowly leak memory.

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-07-13 08:21:54 -07:00
David Crawshaw
c84d7baf98 cmd/tailscale/cli: factor out more up code for testing
In theory, some of the other table-driven tests could be moved into this
form now but I didn't want to disturb too much good test code.

Includes a commented-out test for #2384 that is currently failing.

Signed-off-by: David Crawshaw <crawshaw@tailscale.com>
2021-07-13 07:51:10 -07:00
David Anderson
d98829583a derp: use a dedicated queue for disco traffic.
Signed-off-by: David Anderson <danderson@tailscale.com>
2021-07-12 14:23:27 -07:00
David Anderson
67158549ab derp: actually export the new drop counter.
Signed-off-by: David Anderson <danderson@tailscale.com>
2021-07-12 13:32:04 -07:00
David Anderson
36492ace9d derp: add counters to track the type of dropped packets.
Signed-off-by: David Anderson <danderson@tailscale.com>
2021-07-12 13:15:59 -07:00
Brad Fitzpatrick
1072397375 go.mod: bump wireguard/windows to a version that still exists
Fixes #2381

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-07-12 12:14:31 -07:00
Christine Dodrill
1b1c85d069 Revert "Revert "tstest/integration/vms: end-to-end UDP test (#2361)" (#2364)" (#2365)
This reverts commit 254fc7885b.

Signed-off-by: Christine Dodrill <xe@tailscale.com>
2021-07-12 14:53:16 -04:00
Simeng He
3d049a9d71 tstest/integration: change return type of AwaitIP from string to netaddr.IP
Signed-off-by: Simeng He <simeng@tailscale.com>
2021-07-12 07:45:48 -07:00
Brad Fitzpatrick
46896a9311 ipn/ipnlocal: start to test whether all state transitions save prefs to disk
Updates #2321

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-07-12 07:42:22 -07:00
Brad Fitzpatrick
22cac33fe7 ipn: remove an unnecessary lazy map init on read path
It's okay to read from a nil map.

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-07-11 19:53:23 -07:00
Brad Fitzpatrick
7e7c4c1bbe tailcfg: break DERPNode.DERPTestPort into DERPPort & InsecureForTests
The DERPTestPort int meant two things before: which port to use, and
whether to disable TLS verification. Users would like to set the port
without disabling TLS, so break it into two options.

Updates #1264

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-07-09 12:30:31 -07:00
Brad Fitzpatrick
92077ae78c wgengine/magicsock: make portmapping async
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-07-09 11:15:26 -07:00
Christine Dodrill
afbd35482d tstest/integration/vms: add NixOS unstable back (#2330)
The upstream NixOS issue was fixed.
https://github.com/NixOS/nixpkgs/issues/128783

Signed-off-by: Christine Dodrill <xe@tailscale.com>
2021-07-09 12:13:21 -04:00
Christine Dodrill
33cacb5284 tstest/integration/vms: unbreak nixos builds (#2372)
To avoid the generated nixos disk images from becoming immune from the
GC, I delete the symlink to the nix store at the end of tests.
`t.Cleanup` runs at the end of a test. I changed this part of the code
to have a separate timer for how long it takes to run NixOS builds, but
I did that by using a subtest. This means that it was creating the NixOS
image, deleting its symlink and then trying to use that symlink to find
the resulting disk image, making the whole thing ineffectual.

This was a mistake. I am reverting this change made in
https://github.com/tailscale/tailscale/pull/2360 to remove this layer of
subtesting.

Signed-off-by: Christine Dodrill <xe@tailscale.com>
2021-07-09 11:02:19 -04:00
Christine Dodrill
b90f149f5e Run tests on integration test changes (#2373)
Hopefully this will catch situations like
https://github.com/tailscale/tailscale/pull/2372

Signed-off-by: Christine Dodrill <xe@tailscale.com>
2021-07-09 10:50:23 -04:00
Josh Bleecher Snyder
7a08c159e6 safesocket: create the test socket in a temp dir
This allows the test to be run inside a mounted filesystem,
which I'm doing now as a I develop on a linux VM.

Fixes #2367.

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-07-08 16:57:37 -07:00
Christine Dodrill
254fc7885b Revert "tstest/integration/vms: end-to-end UDP test (#2361)" (#2364)
This reverts commit dc78be12c5.
2021-07-08 16:10:01 -04:00
Christine Dodrill
dc78be12c5 tstest/integration/vms: end-to-end UDP test (#2361)
This tests incoming and outgoing UDP traffic. It would test incoming UDP
traffic however our socks server doesn't seem to allow for connecting to
destinations over UDP. When the socks server gets that support the
incoming test should pass without issue.

Signed-off-by: Christine Dodrill <xe@tailscale.com>
2021-07-08 15:58:18 -04:00
Christine Dodrill
a19eea965f tstest/integration/vms: use an in-process logcatcher (#2360)
This adapts the existing in-process logcatcher from tstest/integration
into a public type and uses it on the side of testcontrol. This also
fixes a bug in the Alpine Linux OpenRC unit that makes every value in
`/etc/default/tailscaled` exported into tailscaled's environment, a-la
systemd [Service].EnviromentFile.

Signed-off-by: Christine Dodrill <xe@tailscale.com>
2021-07-08 14:39:45 -04:00
Brad Fitzpatrick
440566c5d2 net/interfaces: trim getPACWindows whitespace before parsing URL
Fixes #2357

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-07-08 11:26:50 -07:00
Christine Dodrill
1e83b97498 tstest/integration/vms: outgoing SSH test (#2349)
This does a few things:

1. Rewrites the tests so that we get a log of what individual tests
   failed at the end of a test run.
2. Adds a test that runs an HTTP server via the tester tailscale node and
   then has the VMs connect to that over Tailscale.
3. Dials the VM over Tailscale and ensures it answers SSH requests.
4. Other minor framework refactoring.

Signed-off-by: Christine Dodrill <xe@tailscale.com>
2021-07-08 11:38:01 -04:00
Christine Dodrill
97279a0fe0 tstest/integration/vms: add Oracle Linux image (#2328)
Oracle Linux[1] is a CentOS fork. It is not very special. I am adding it
to the integration jungle because I am adding it to pkgs and the website
directions.

[1]: https://www.oracle.com/linux/

Signed-off-by: Christine Dodrill <xe@tailscale.com>
2021-07-08 10:26:20 -04:00
Brad Fitzpatrick
a9fc583211 cmd/tailscale/cli: document the web subcommand a bit more
Fixes #2326

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-07-07 21:16:33 -07:00
Josh Bleecher Snyder
0ad92b89a6 net/tstun: fix data races
To remove some multi-case selects, we intentionally allowed
sends on closed channels (cc23049cd2).

However, we also introduced concurrent sends and closes,
which is a data race.

This commit fixes the data race. The mutexes here are uncontended,
and thus very cheap.

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-07-07 16:15:29 -07:00
Brad Fitzpatrick
7d417586a8 tstest/integration: help bust cmd/go's test caching
It was caching too aggressively, as it didn't see our deps due to our
running "go install tailscaled" as a child process.

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-07-07 13:14:21 -07:00
Brad Fitzpatrick
3dcd18b6c8 tailcfg: note RegionID 900-999 reservation 2021-07-07 12:23:41 -07:00
Brad Fitzpatrick
ddb8726c98 util/deephash: don't reflect.Copy if element type is a defined uint8
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-07-07 11:58:04 -07:00
Brad Fitzpatrick
df176c82f5 util/deephash: skip alloc test under race detector
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-07-07 11:40:28 -07:00
Brad Fitzpatrick
6dc38ff25c util/deephash: optimize hashing of byte arrays, reduce allocs in Hash
name              old time/op    new time/op    delta
Hash-6               173µs ± 4%     101µs ± 3%   -41.69%  (p=0.000 n=10+9)
HashMapAcyclic-6     101µs ± 5%     105µs ± 3%    +3.52%  (p=0.001 n=9+10)
TailcfgNode-6       29.4µs ± 2%    16.4µs ± 3%   -44.25%  (p=0.000 n=8+10)

name              old alloc/op   new alloc/op   delta
Hash-6              3.60kB ± 0%    1.13kB ± 0%   -68.70%  (p=0.000 n=10+10)
HashMapAcyclic-6    2.53kB ± 0%    2.53kB ± 0%      ~     (p=0.137 n=10+8)
TailcfgNode-6         528B ± 0%        0B       -100.00%  (p=0.000 n=10+10)

name              old allocs/op  new allocs/op  delta
Hash-6                84.0 ± 0%      40.0 ± 0%   -52.38%  (p=0.000 n=10+10)
HashMapAcyclic-6       202 ± 0%       202 ± 0%      ~     (all equal)
TailcfgNode-6         11.0 ± 0%       0.0       -100.00%  (p=0.000 n=10+10)

Updates tailscale/corp#2130

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-07-07 11:30:49 -07:00
Brad Fitzpatrick
3962744450 util/deephash: prevent infinite loop on map cycle
Fixes #2340

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-07-07 10:57:46 -07:00
Brad Fitzpatrick
aceaa70b16 util/deephash: move funcs to methods
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-07-07 08:17:18 -07:00
Irshad Pananilath
9288e0d61c build_docker.sh: use build_dist.sh to inject version information
version.sh was removed in commit 5088af68. Use `build_dist.sh shellvars`
to provide version information instead.

Signed-off-by: Irshad Pananilath <pmirshad+code@gmail.com>
2021-07-07 06:38:04 -07:00
Christine Dodrill
a8360050e7 tstest/integration/vms: make first end to end test (#2332)
This makes sure `tailscale status` and `tailscale ping` works. It also
switches goexpect to use a batch instead of manually banging out each
line, which makes the tests so much easier to read.

Signed-off-by: Christine Dodrill <xe@tailscale.com>
2021-07-06 12:50:19 -04:00
David Crawshaw
805d5d3cde ipnlocal: move log line inside if statement
Signed-off-by: David Crawshaw <crawshaw@tailscale.com>
2021-07-06 09:35:01 -07:00
Brad Fitzpatrick
14f901da6d util/deephash: fix sync.Pool usage
Whoops.

From yesterday's 9ae3bd0939 (not yet
used by anything, fortunately)

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-07-05 22:21:44 -07:00
Brad Fitzpatrick
e0258ffd92 util/deephash: use keyed struct literal, fix vet
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-07-05 21:31:30 -07:00
Brad Fitzpatrick
bf9f279768 util/deephash: optimize CPU a bit by by avoiding fmt in more places
name              old time/op    new time/op    delta
Hash-6               179µs ± 5%     173µs ± 4%   -3.12%  (p=0.004 n=10+10)
HashMapAcyclic-6     115µs ± 3%     101µs ± 5%  -11.51%  (p=0.000 n=9+9)
TailcfgNode-6       30.8µs ± 4%    29.4µs ± 2%   -4.51%  (p=0.000 n=10+8)

name              old alloc/op   new alloc/op   delta
Hash-6              3.60kB ± 0%    3.60kB ± 0%     ~     (p=0.445 n=9+10)
HashMapAcyclic-6    2.53kB ± 0%    2.53kB ± 0%     ~     (p=0.065 n=9+10)
TailcfgNode-6         528B ± 0%      528B ± 0%     ~     (all equal)

name              old allocs/op  new allocs/op  delta
Hash-6                84.0 ± 0%      84.0 ± 0%     ~     (all equal)
HashMapAcyclic-6       202 ± 0%       202 ± 0%     ~     (all equal)
TailcfgNode-6         11.0 ± 0%      11.0 ± 0%     ~     (all equal)

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-07-05 21:28:54 -07:00
Brad Fitzpatrick
58f2ef6085 util/deephash: add a benchmark and some benchmark data
No code changes.

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-07-05 21:21:52 -07:00
Brad Fitzpatrick
9ae3bd0939 util/deephash: export a Hash func for use by the control plane
name              old time/op    new time/op    delta
Hash-6              69.4µs ± 6%    68.4µs ± 4%     ~     (p=0.286 n=9+9)
HashMapAcyclic-6     115µs ± 5%     115µs ± 4%     ~     (p=1.000 n=10+10)

name              old alloc/op   new alloc/op   delta
Hash-6              2.29kB ± 0%    1.88kB ± 0%  -18.13%  (p=0.000 n=10+10)
HashMapAcyclic-6    2.53kB ± 0%    2.53kB ± 0%     ~     (all equal)

name              old allocs/op  new allocs/op  delta
Hash-6                58.0 ± 0%      54.0 ± 0%   -6.90%  (p=0.000 n=10+10)
HashMapAcyclic-6       202 ± 0%       202 ± 0%     ~     (all equal)

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-07-05 11:41:44 -07:00
Brad Fitzpatrick
700badd8f8 util/deephash: move internal/deephash to util/deephash
No code changes. Just a minor package doc addition about lack of API
stability.
2021-07-02 21:33:02 -07:00
Josh Bleecher Snyder
7f095617f2 internal/deephash: 8 bits of output is not enough
Running hex.Encode(b, b) is a bad idea.
The first byte of input will overwrite the first two bytes of output.
Subsequent bytes have no impact on the output.

Not related to today's IPv6 bug, but...wh::ps.

This caused us to spuriously ignore some wireguard config updates.

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-07-02 13:48:27 -07:00
Josh Bleecher Snyder
c35a832de6 net/tstun: add inner loop to poll
This avoids re-enqueuing to t.bufferConsumed,
which makes the code a bit clearer.

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-07-02 11:02:12 -07:00
Josh Bleecher Snyder
a4cc7b6d54 net/tstun: simplify code
Calculate whether the packet is injected directly,
rather than via an else branch.

Unify the exit paths. It is easier here than duplicating them.

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-07-02 11:02:12 -07:00
Josh Bleecher Snyder
cc23049cd2 net/tstun: remove multi-case selects from hot code
Every TUN Read went through several multi-case selects.
We know from past experience with wireguard-go that these are slow
and cause scheduler churn.

The selects served two purposes: they separated errors from data and
gracefully handled shutdown. The first is fairly easy to replace by sending
errors and data over a single channel. The second, less so.

We considered a few approaches: Intricate webs of channels,
global condition variables. They all get ugly fast.

Instead, let's embrace the ugly and handle shutdown ungracefully.
It's horrible, but the horror is simple and localized.

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-07-02 11:02:12 -07:00
Denton Gentry
64ee6cf64b api.md: update preview example
The implementation of the preview function has changed since the
API was documented, update the document to match.

Signed-off-by: Denton Gentry <dgentry@tailscale.com>
2021-07-02 08:24:19 -07:00
Brad Fitzpatrick
1e6d8a1043 version: don't allocate parsing unsupported versions, empty strings
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-07-01 14:25:50 -07:00
Josh Bleecher Snyder
f11a8928a6 ipn/ipnlocal: fix data race
We can't access b.netMap without holding b.mu.
We already grabbed it earlier in the function with the lock held.

Introduced in Nov 2020 in 7ea809897d.
Discovered during stress testing.
Apparently it's a pretty rare?

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-07-01 12:29:02 -07:00
Christine Dodrill
5813da885c tstest/integration/vms: verbosify nixos logs to fs, disable unstable (#2294)
This puts nix build logs on the filesystem so that we can debug them
later. This also disables nixos unstable until
https://github.com/NixOS/nixpkgs/issues/128783 is fixed.

Signed-off-by: Christine Dodrill <xe@tailscale.com>
2021-06-30 13:38:28 -04:00
David Crawshaw
6b9f8208f4 net/dns: do not run wsl.exe as LocalSystem
It doesn't work. It needs to run as the user.

	https://github.com/microsoft/WSL/issues/4803

The mechanism for doing this was extracted from:

	https://web.archive.org/web/20101009012531/http://blogs.msdn.com/b/winsdk/archive/2009/07/14/launching-an-interactive-process-from-windows-service-in-windows-vista-and-later.aspx

While here, we also reclaculate WSL distro set on SetDNS.
This accounts for:

	1. potential inability to access wsl.exe on startup
	2. WSL being installed while Tailscale is running
	3. A new WSL distrobution being installed

Signed-off-by: David Crawshaw <crawshaw@tailscale.com>
2021-06-30 10:11:33 -07:00
Christine Dodrill
6f3a5802a6 experimental VM test: add -v
Apparently if you don't add -v the tests don't report anything useful when they break. Joy.

Signed-Off-By: Christine Dodrill <xe@tailscale.com>
2021-06-30 09:28:58 -04:00
Maisem Ali
ec52760a3d wgengine/router_windows: support toggling local lan access when using
exit nodes.

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2021-06-29 09:22:10 -07:00
David Crawshaw
c37713b927 cmd/tailscale/cli: accept login server synonym
Fixes #2272

Signed-off-by: David Crawshaw <crawshaw@tailscale.com>
2021-06-29 07:20:02 -07:00
julianknodt
e68d4d5805 cmd/tailscale: add debug flag to dump derp map
This adds a flag in tailscale debug for dumping the derp map to stdout.

Fixes #2249.

Signed-off-by: julianknodt <julianknodt@gmail.com>
2021-06-28 22:50:59 -07:00
Brad Fitzpatrick
fd7fddd44f control/controlclient: add debug knob to force node to only IPv6 self addr
Updates #2268

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-06-28 15:26:58 -07:00
Brad Fitzpatrick
722859b476 wgengine/netstack: make SOCKS5 resolve names to IPv6 if self node when no IPv4
For instance, ephemeral nodes with only IPv6 addresses can now
SOCKS5-dial out to names like "foo" and resolve foo's IPv6 address
rather than foo's IPv4 address and get a "no route"
(*tcpip.ErrNoRoute) error from netstack's dialer.

Per https://github.com/tailscale/tailscale/issues/2268#issuecomment-870027626
which is only part of the isuse.

Updates #2268

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-06-28 15:20:37 -07:00
David Crawshaw
1147c7fd4f net/dns: set WSL /etc/resolv.conf
We also have to make a one-off change to /etc/wsl.conf to stop every
invocation of wsl.exe clobbering the /etc/resolv.conf. This appears to
be a safe change to make permanently, as even though the resolv.conf is
constantly clobbered, it is always the same stable internal IP that is
set as a nameserver. (I believe the resolv.conf clobbering predates the
MS stub resolver.)

Tested on WSL2, should work for WSL1 too.

Fixes #775

Signed-off-by: David Crawshaw <crawshaw@tailscale.com>
2021-06-28 14:18:15 -07:00
David Crawshaw
9b063b86c3 net/dns: factor directManager out over an FS interface
This is preliminary work for using the directManager as
part of a wslManager on windows, where in addition to configuring
windows we'll use wsl.exe to edit the linux file system and modify the
system resolv.conf.

The pinholeFS is a little funky, but it's designed to work through
simple unix tools via wsl.exe without invoking bash. I would not have
thought it would stand on its own like this, but it turns out it's
useful for writing a test for the directManager.

Signed-off-by: David Crawshaw <crawshaw@tailscale.com>
2021-06-28 14:18:15 -07:00
julianknodt
506c2fe8e2 cmd/tailscale: make netcheck use active DERP map, delete static copy
After allowing for custom DERP maps, it's convenient to be able to see their latency in
netcheck. This adds a query to the local tailscaled for the current DERPMap.

Updates #1264

Signed-off-by: julianknodt <julianknodt@gmail.com>
2021-06-28 14:08:47 -07:00
Brad Fitzpatrick
15677d8a0e net/socks5/tssocks: add a SOCKS5 dialer type, method-ifying code
https://twitter.com/bradfitz/status/1409605220376580097

Prep for #1970, #2264, #2268

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-06-28 13:12:42 -07:00
Brad Fitzpatrick
3910c1edaf net/socks5/tssocks: add new package, move SOCKS5 glue out of tailscaled
Prep for #1970, #2264, #2268

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-06-28 11:34:50 -07:00
Brad Fitzpatrick
5e19ac7adc tstest/integration: always run SOCK5 server, parse out its listening address
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-06-28 11:34:41 -07:00
David Crawshaw
54199d9d58 controlclient: log server key and URL
Turns out we never reliably log the control plane URL a client connects
to. Do it here, and include the server public key, which might
inadvertently tell us something interesting some day.

Signed-off-by: David Crawshaw <crawshaw@tailscale.com>
2021-06-28 09:38:23 -07:00
David Crawshaw
d6f4b5f5cb ipn, etc: use controlplane.tailscale.com
Signed-off-by: David Crawshaw <crawshaw@tailscale.com>
2021-06-28 09:38:23 -07:00
Brad Fitzpatrick
82e15d3450 cmd/tailscaled: log SOCKS5 port when port 0 requested
Part of #2158

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-06-28 08:32:50 -07:00
Christine Dodrill
2adbfc920d integration vm tests: run on every commit to main (#2159)
This is an experiment to see how often this test would fail if we run it
on every commit. This depends on #2145 to fix a flaky part of the test.

Signed-off-by: Christine Dodrill <xe@tailscale.com>
2021-06-28 10:01:30 -04:00
Christine Dodrill
b131a74f99 tstest/integration/vms: build and run NixOS (#2190)
Okay, so, at a high level testing NixOS is a lot different than
other distros due to NixOS' determinism. Normally NixOS wants packages to
be defined in either an overlay, a custom packageOverrides or even
yolo-inline as a part of the system configuration. This is going to have
us take a different approach compared to other distributions. The overall
plan here is as following:

1. make the binaries as normal
2. template in their paths as raw strings to the nixos system module
3. run `nixos-generators -f qcow -o $CACHE_DIR/tailscale/nixos/version -c generated-config.nix`
4. pass that to the steps that make the virtual machine

It doesn't really make sense for us to use a premade virtual machine image
for this as that will make it harder to deterministically create the image.

Nix commands generate a lot of output, so their output is hidden behind the
`-verbose-nix-output` flag.

This unfortunately makes this test suite have a hard dependency on
Nix/NixOS, however the test suite has only ever been run on NixOS (and I
am not sure if it runs on other distros at all), so this probably isn't too
big of an issue.

Signed-off-by: Christine Dodrill <xe@tailscale.com>
2021-06-28 09:45:45 -04:00
julianknodt
72a0b5f042 net/dns/resolver: fmt item
This has been bothering me for a while, but everytime I run format from the root directory
it also formats this file. I didn't want to add it to my other PRs but it's annoying to have to
revert it every time.

Signed-off-by: julianknodt <julianknodt@gmail.com>
2021-06-27 23:57:55 -07:00
Brad Fitzpatrick
10d7c2583c net/dnsfallback: don't depend on derpmap.Prod
Move derpmap.Prod to a static JSON file (go:generate'd) instead,
to make its role explicit. And add a TODO about making dnsfallback
use an update-over-time DERP map file instead of a baked-in one.

Updates #1264

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-06-27 22:07:40 -07:00
Christine Dodrill
194d5b8412 tstest/integration/vms: add in-process DERP server (#2108)
Previously this test would reach out to the public DERP servers in order
to help machines connect with eachother. This is not ideal given our
plans to run these tests completely disconnected from the internet. This
patch introduces an in-process DERP server running on its own randomly
assigned HTTP port.

Updates #1988

Signed-off-by: Christine Dodrill <xe@tailscale.com>
2021-06-25 15:59:45 -04:00
Christine Dodrill
6b234323a0 tstest/integration/vms: fix flake when testing (#2145)
Occasionally the test framework would fail with a timeout due to a
virtual machine not phoning home in time. This seems to be happen
whenever qemu can't bind the VNC or SSH ports for a virtual machine.
This was fixed by taking the following actions:

1. Don't listen on VNC unless the `-use-vnc` flag is passed, this
   removes the need to listen on VNC at all in most cases. The option to
   use VNC is still left in for debugging virtual machines, but removing
   this makes it easier to deal with (VNC uses this odd system of
   "displays" that are mapped to ports above 5900, and qemu doesn't
   offer a decent way to use a normal port number, so we just disable
   VNC by default as a compromise).
2. Use a (hopefully) inactive port for SSH. In an ideal world I'd just
   have the VM's SSH port be exposed via a Unix socket, however the QEMU
   documentation doesn't really say if you can do this or not. While I
   do more research, this stopgap will have to make do.
3. Strictly tie more VM resource lifetimes to the tests themselves.
   Previously the disk image layers for virtual machines were only
   cleaned up at the end of the test and existed in the parent
   test-scoped temporary folder. This can make your tmpfs run out of
   space, which is not ideal. This should minimize the use of temporary
   storage as much as I know how to.
4. Strictly tie the qemu process lifetime to the lifetime of the test
   using testing.T#Cleanup. Previously it used a defer statement to
   clean up the qemu process, however if the tests timed out this defer
   was not run. This left around an orphaned qemu process that had to be
   killed manually. This change ensures that all qemu processes exit
   when their relevant tests finish.

Signed-off-by: Christine Dodrill <xe@tailscale.com>
2021-06-25 14:45:12 -04:00
Brad Fitzpatrick
8a4dffee07 types/logger: fix deadlock RateLimitedFn reentrancy
Fix regression from 19c3e6cc9e
which made the locking coarser.

Found while debugging #2245, which ended up looking like a tswin/Windows
issue where Crawshaw had blocked cmd.exe's output.

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-06-25 08:38:08 -07:00
Christine Dodrill
59e9b44f53 wgengine/filter: add a debug flag for filter logs (#2241)
This uses a debug envvar to optionally disable filter logging rate
limits by setting the environment variable
TS_DEBUG_FILTER_RATE_LIMIT_LOGS to "all", and if it matches,
the code will effectively disable the limits on the log rate by
setting the limit to 1 millisecond. This should make sure that all
filter logs will be captured.

Signed-off-by: Christine Dodrill <xe@tailscale.com>
2021-06-25 10:10:26 -04:00
David Crawshaw
80b1308974 net/dns: remove ref to managerImpl
Signed-off-by: David Crawshaw <crawshaw@tailscale.com>
2021-06-25 07:06:23 -07:00
Adrian Dewhurst
bcaae3e074 net/dns/resolver: clamp EDNS size
This change (subject to some limitations) looks for the EDNS OPT record
in queries and responses, clamping the size field to fit within our DNS
receive buffer. If the size field is smaller than the DNS receive buffer
then it is left unchanged.

I think we will eventually need to transition to fully processing the
DNS queries to handle all situations, but this should cover the most
common case.

Mostly fixes #2066

Signed-off-by: Adrian Dewhurst <adrian@tailscale.com>
2021-06-25 08:56:34 -04:00
David Anderson
c69d30cdd7 VERSION.txt: this is v1.11.0.
Signed-off-by: David Anderson <danderson@tailscale.com>
2021-06-24 15:45:08 -07:00
julianknodt
148602a89a derp,cmd/derper: allow server to verify clients
This adds a flag to the DERP server which specifies to verify clients through a local
tailscaled. It is opt-in, so should not affect existing clients, and is mainly intended for
users who want to run their own DERP servers. It assumes there is a local tailscaled running and
will attempt to hit it for peer status information.

Updates #1264

Signed-off-by: julianknodt <julianknodt@gmail.com>
2021-06-24 14:11:16 -07:00
Brad Fitzpatrick
c45bfd4180 wgengine: make dnsIPsOverTailscale also consider DefaultResolvers
Found during a failed experiment debugging something on Android.

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-06-24 12:57:26 -07:00
Brad Fitzpatrick
7b8ed1fc09 net/netns: add Android implementation, allowing registration of JNI hook
Updates #2102
Updates #1809

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-06-24 12:50:47 -07:00
Brad Fitzpatrick
b92e2ebd24 wgengine/netstack: add Impl.DialContextUDP
Unused so far, but eventually we'll want this for SOCKS5 UDP binds (we
currently only do TCP with SOCKS5), and also for #2102 for forwarding
MagicDNS upstream to Tailscale IPs over netstack.

Updates #2102

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-06-23 22:12:17 -07:00
Brad Fitzpatrick
3d777c13b0 net/socks5: fix a typo
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-06-23 22:12:17 -07:00
David Anderson
084d48d22d net/dns: always proxy through quad-100 on windows 8.1.
Windows 8.1 incorrectly handles search paths on an interface with no
associated resolver, so we have to provide a full primary DNS config
rather than use Windows 8.1's nascent-but-present NRPT functionality.

Fixes #2237.

Signed-off-by: David Anderson <danderson@tailscale.com>
2021-06-23 17:50:19 -07:00
Brad Fitzpatrick
45e64f2e1a net/dns{,/resolver}: refactor DNS forwarder, send out of right link on macOS/iOS
Fixes #2224
Fixes tailscale/corp#2045

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-06-23 16:04:10 -07:00
julianknodt
597fa3d3c3 tailcfg/derpmap: add flag to omit ts's derps
This adds a flag to derp maps which specifies that default Tailscale DERP servers should not be
used. If true and there are entries in this map, it indicates that the entries in this map
should take precedent and not hit any of tailscale's DERP servers.

This change is backwards compatible, as the default behavior should be false.

Updates #1264

Signed-off-by: julianknodt <julianknodt@gmail.com>
2021-06-23 10:10:33 -07:00
Julian Knodt
48883272ea Merge pull request #2227 from tailscale/jknodt/cloner
cmd/cloner: support maps with clone ptrs
2021-06-23 09:50:45 -07:00
David Crawshaw
4ce15505cb wgengine: randomize client port if netmap says to
For testing out #2187

Signed-off-by: David Crawshaw <crawshaw@tailscale.com>
2021-06-23 08:51:37 -07:00
David Crawshaw
5f8ffbe166 magicsock: add SetPreferredPort method
Signed-off-by: David Crawshaw <crawshaw@tailscale.com>
2021-06-23 08:51:37 -07:00
David Crawshaw
676e32ad72 syncs: add AtomicUint32
Signed-off-by: David Crawshaw <crawshaw@tailscale.com>
2021-06-23 08:51:37 -07:00
Brad Fitzpatrick
733d52827b net/dns/resolver: skip test on macOS
Fixes #2229

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-06-23 08:13:55 -07:00
julianknodt
0f18801716 cmd/cloner: support maps with clone ptrs
In order to clone DERPMaps, it was necessary to extend the cloner so that it supports
nested pointers inside of maps which are also cloneable. This also adds cloning for DERPRegions
and DERPNodes because they are on DERPMap's maps.

Signed-off-by: julianknodt <julianknodt@gmail.com>
2021-06-22 22:11:38 -07:00
David Crawshaw
ece138ffc3 staticcheck.conf: remove unnecessary warning
Signed-off-by: David Crawshaw <crawshaw@tailscale.com>
2021-06-22 12:26:13 -07:00
Brad Fitzpatrick
bb363095a5 tailcfg: add Debug.RandomizeClientPort
Not yet used.

Updates #2187

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-06-22 12:07:53 -07:00
Brad Fitzpatrick
38be964c2b go.mod: update netstack
Fixes a atomic alignment crash on 32-bit machines.

Fixes #2129
Fixes tailscale/tailscale-synology#66 (same)

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-06-22 10:34:14 -07:00
Brad Fitzpatrick
a0c632f6b5 tstest/integration: fix a race
Noticed on a CI failure.

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-06-22 10:24:13 -07:00
Denton Gentry
ad288baaea net/interfaces: use IPv4 link local if nothing better
The only connectivity an AWS Lambda container has is an IPv4 link-local
169.254.x.x address using NAT:
12: vtarget_1@if11: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500
                    qdisc noqueue state UP group default qlen 1000
     link/ether 7e:1c:3f:00:00:00 brd ff:ff:ff:ff:ff:ff link-netnsid 1
     inet 169.254.79.1/32 scope global vtarget_1
     valid_lft forever preferred_lft forever

If there are no other IPv4/v6 addresses available, and we are running
in AWS Lambda, allow IPv4 169.254.x.x addresses to be used.

----

Similarly, a Google Cloud Run container's only connectivity is
a Unique Local Address fddf:3978:feb1:d745::c001/128.
If there are no other addresses available then allow IPv6
Unique Local Addresses to be used.
We actually did this in an earlier release, but now refactor it to
work the same way as the IPv4 link-local support is being done.

Signed-off-by: Denton Gentry <dgentry@tailscale.com>
2021-06-18 21:52:47 -07:00
julianknodt
3687e5352b derp: fix traffic handler peer addresses
Before it was using the local address and port, so fix that.
The fields in the response from `ss` are:

State, Recv-Q, Send-Q, Local Address:Port, Peer Address:Port, Process

Signed-off-by: julianknodt <julianknodt@gmail.com>
2021-06-18 16:14:26 -07:00
David Crawshaw
297b3d6fa4 staticcheck.conf: turn off noisy lint errors
Signed-off-by: David Crawshaw <crawshaw@tailscale.com>
2021-06-18 15:48:20 -07:00
julianknodt
3728634af9 derp: add debug traffic handler
This adds a handler on the DERP server for logging bytes send and received by clients of the
server, by holding open a connection and recording if there is a difference between the number
of bytes sent and received. It sends a JSON marshalled object if there is an increase in the
number of bytes.

Signed-off-by: julianknodt <julianknodt@gmail.com>
2021-06-18 15:47:55 -07:00
Brad Fitzpatrick
2f4817fe20 tstest/integration: fix race flake
Fixes #2172

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-06-18 10:10:23 -07:00
Brad Fitzpatrick
1ae35b6c59 net/{interfaces,netcheck}: rename some fields, funcs
Split out of Denton's #2164, to make that diff smaller to review.

This change has no behavior changes.

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-06-17 17:50:13 -07:00
Brad Fitzpatrick
03311bb0d6 hostinfo: add hostinfo package, move stuff out of controlclient
And make it cheaper, so other parts of the code can check the
environment.

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-06-17 14:27:01 -07:00
David Anderson
0022c3d2e2 tsweb: replace NewMux with a more flexible DebugHandler.
Signed-off-by: David Anderson <danderson@tailscale.com>
2021-06-16 19:00:47 -07:00
Brad Fitzpatrick
b461ba9554 control/controlclient: fix typo/braino in error message
Thanks to @normanr for noticing.

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-06-16 15:55:06 -07:00
Brad Fitzpatrick
0debb99f08 tailcfg: add DNSConfig.ExtraRecords
Updates #1748
Updates #1235
Updates #2055

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-06-16 15:52:21 -07:00
Christine Dodrill
e0f0d10672 tstest/integration/vms: log to t.Logf directly (#2147)
Previously we used t.Logf indirectly via package log. This worked, but
it was not ideal for our needs. It could cause the streams of output to
get crossed. This change uses a logger.FuncWriter every place log.Output
was previously used, which will more correctly write log information to
the right test output stream.

Signed-off-by: Christine Dodrill <xe@tailscale.com>
2021-06-16 14:57:11 -04:00
Maisem Ali
f482321f67 ipn/ipnlocal: support exit node local access switching on darwin.
Signed-off-by: Maisem Ali <maisem@tailscale.com>
2021-06-16 19:28:02 +05:00
Maisem Ali
2919b3e3e6 wf: loopback condition should use MatchTypeFlagsAllSet.
Signed-off-by: Maisem Ali <maisem@tailscale.com>
2021-06-16 12:57:57 +05:00
David Anderson
48c25fa36f tsweb: fold StdHandlerOpts and StdHandler200s with StdHandler.
Signed-off-by: David Anderson <danderson@tailscale.com>
2021-06-15 21:55:33 -07:00
David Anderson
72343fbbec tsweb: register expvars once at startup.
Signed-off-by: David Anderson <danderson@tailscale.com>
2021-06-15 21:27:54 -07:00
David Anderson
9337826011 net/dns: fix inverted test for NetworkManager version.
Signed-off-by: David Anderson <danderson@tailscale.com>
2021-06-15 20:53:03 -07:00
David Anderson
320cc8fa21 net/dns: verify that systemd-resolved is actually in charge.
It's possible to install a configuration that passes our current checks
for systemd-resolved, without actually pointing to systemd-resolved. In
that case, we end up programming DNS in resolved, but that config never
applies to any name resolution requests on the system.

This is quite a far-out edge case, but there's a simple additional check
we can do: if the header comment names systemd-resolved, there should be
a single nameserver in resolv.conf pointing to 127.0.0.53. If not, the
configuration should be treated as an unmanaged resolv.conf.

Fixes #2136.

Signed-off-by: David Anderson <danderson@tailscale.com>
2021-06-15 19:52:02 -07:00
David Anderson
e7164425b3 net/dns: don't use NetworkManager for DNS on very old NetworkManagers.
Fixes #1945.

Signed-off-by: David Anderson <danderson@tailscale.com>
2021-06-15 15:34:35 -07:00
David Anderson
ac07ff43bf cmd/tailscaled: start after NetworkManager and systemd-resolved.
The dependency is a "soft" ordering dependency only, meaning that
tailscaled will start after those services if those services were
going to be run anyway, but doesn't force either of them to run.
That's why it's safe to specify this dependency unconditionally,
even for systems that don't run those services.

Updates #2127.

Signed-off-by: David Anderson <danderson@tailscale.com>
2021-06-15 14:25:44 -07:00
Brad Fitzpatrick
cd282ec00f tailcfg: add DNSConfig.CertDomains
Updates #1235

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-06-15 14:05:46 -07:00
Brad Fitzpatrick
082cc1b0a7 tstest/integration: reenable TestAddPingRequest
Failure understood now; see:
https://github.com/tailscale/tailscale/pull/2088#issuecomment-859896598

As of 333e9e75d4, PingRequest is
now safe for the server to send multiple times, without fear
of the client handling it multiple times.

Fixes #2079

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-06-15 12:41:08 -07:00
Brad Fitzpatrick
333e9e75d4 tailcfg, control/controlclient: clarify more, enforce PingRequest.URL is unique
Updates #2079

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-06-15 12:28:34 -07:00
Denton Gentry
c61d777705 tstest/integration: disable TestAddPingRequest
Failing often now, we don't want people to get used to
routinely ignoring test failures.

Can be re-enabled when
https://github.com/tailscale/tailscale/issues/2079
is resolved.

Signed-off-by: Denton Gentry <dgentry@tailscale.com>
2021-06-14 22:24:27 -07:00
Denton Gentry
857bc4a752 hostinfo: capitalization of AWS
Missed one comment from https://github.com/tailscale/tailscale/pull/1868
should be isAWSLambda not isAwsLambda

Signed-off-by: Denton Gentry <dgentry@tailscale.com>
2021-06-14 15:26:57 -07:00
Denton Gentry
4b71291cdb hostinfo: detect when running in Azure App Service.
Signed-off-by: Denton Gentry <dgentry@tailscale.com>
2021-06-14 13:14:17 -07:00
Denton Gentry
3ab587abe7 hostinfo: detect Heroku Dyno.
Signed-off-by: Denton Gentry <dgentry@tailscale.com>
2021-06-14 13:14:17 -07:00
Denton Gentry
3c1a73d370 hostinfo: detect AWS Lambda as a container.
AWS Lambda uses Docker containers but does not
have the string "docker" in its /proc/1/cgroup.
Infer AWS Lambda via the environment variables
it sets.

Signed-off-by: Denton Gentry <dgentry@tailscale.com>
2021-06-14 13:14:17 -07:00
Brad Fitzpatrick
cc6ab0a70f ipn/ipnlocal: retry peerapi listen on Android, like we do on Windows
Updates #1960

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-06-14 08:59:09 -07:00
Julian Knodt
525eb5ce41 Merge pull request #2092 from tailscale/queue_latency
derp: add pkt queue latency timer
2021-06-11 09:48:38 -07:00
julianknodt
fe54721e31 derp: add pkt queue latency timer
It would be useful to know the time that packets spend inside of a queue before they are sent
off, as that can be indicative of the load the server is handling (and there was also an
existing TODO). This adds a simple exponential moving average metric to track the average packet
queue duration.
Changes during review:
Add CAS loop for recording queue timing w/ expvar.Func, rm snake_case, annotate in milliseconds,
convert

Signed-off-by: julianknodt <julianknodt@gmail.com>
2021-06-11 09:41:06 -07:00
Brad Fitzpatrick
80a4052593 cmd/tailscale, wgengine, tailcfg: don't assume LastSeen is present [mapver 20]
Updates #2107

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-06-11 08:41:16 -07:00
Christine Dodrill
8b2b899989 tstest/integration: test Alpine Linux (#2098)
Alpine Linux[1] is a minimal Linux distribution built around musl libc.
It boots very quickly, requires very little ram and is as close as you
can get to an ideal citizen for testing Tailscale on musl. Alpine has a
Tailscale package already[2], but this patch also makes it easier for us
to provide an Alpine Linux package off of pkgs in the future.

Alpine only offers Tailscale on the rolling-release edge branch.

[1]: https://alpinelinux.org/
[2]: https://pkgs.alpinelinux.org/packages?name=tailscale&branch=edge

Updates #1988

Signed-off-by: Christine Dodrill <xe@tailscale.com>
2021-06-11 09:20:13 -04:00
Brad Fitzpatrick
0affcd4e12 tstest/integration: add some debugging for TestAddPingRequest flakes
This fails pretty reliably with a lot of output now showing what's
happening:

TS_DEBUG_MAP=1 go test --failfast -v -run=Ping -race -count=20 ./tstest/integration --verbose-tailscaled

I haven't dug into the details yet, though.

Updates #2079
2021-06-10 15:13:14 -07:00
Brad Fitzpatrick
ee3df2f720 tstest/integration: rename ambiguous --verbose test flag
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-06-10 11:24:01 -07:00
Fletcher Nichol
a49df5cfda wgenine/router: fix OpenBSD route creation
The route creation for the `tun` device was augmented in #1469 but
didn't account for adding IPv4 vs. IPv6 routes. There are 2 primary
changes as a result:

* Ensure that either `-inet` or `-inet6` was used in the
  [`route(8)`](https://man.openbsd.org/route) command
* Use either the `localAddr4` or `localAddr6` for the gateway argument
  depending which destination network is being added

The basis for the approach is based on the implementation from
`router_userspace_bsd.go`, including the `inet()` helper function.

Fixes #2048
References #1469

Signed-off-by: Fletcher Nichol <fnichol@nichol.ca>
2021-06-10 10:48:33 -07:00
Dave Anderson
144c68b80b net/dns: avoid using NetworkManager as much as possible. (#1945)
Addresses #1699 as best as possible.

Signed-off-by: David Anderson <danderson@tailscale.com>
2021-06-10 10:46:08 -04:00
Maisem Ali
f944614c5c cmd/tailscale/web: add support for QNAP
Signed-off-by: Maisem Ali <maisem@tailscale.com>
2021-06-10 19:06:05 +05:00
Adrian Dewhurst
8b11937eaf net/dns/resolver: permit larger max responses, signal truncation
This raises the maximum DNS response message size from 512 to 4095. This
should be large enough for almost all situations that do not need TCP.
We still do not recognize EDNS, so we will still forward requests that
claim support for a larger response size than 4095 (that will be solved
later). For now, when a response comes back that is too large to fit in
our receive buffer, we now set the truncation flag in the DNS header,
which is an improvement from before but will prompt attempts to use TCP
which isn't supported yet.

On Windows, WSARecvFrom into a buffer that's too small returns an error
in addition to the data. On other OSes, the extra data is silently
discarded. In this case, we prefer the latter so need to catch the error
on Windows.

Partially addresses #1123

Signed-off-by: Adrian Dewhurst <adrian@tailscale.com>
2021-06-08 19:29:12 -04:00
Brad Fitzpatrick
fc5fba0fbf client/tailscale: document SetDNS more
Updates #1235

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-06-08 15:25:03 -07:00
Brad Fitzpatrick
796e222901 client/tailscale: add SetDNS func
Updates #1235

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-06-08 14:49:56 -07:00
Simeng He
f0121468f4 control/controlclient: add Pinger interface, Options.Pinger
Plumbs down a pinger to the direct to enable client to client Ping
functionality from control.

Signed-off-by: Simeng He <simeng@tailscale.com>
2021-06-08 16:30:06 -04:00
Matt Layher
6956645ec8 go.mod: bump github.com/mdlayher/netlink to v1.4.1
Signed-off-by: Matt Layher <mdlayher@gmail.com>
2021-06-08 12:01:38 -07:00
Christine Dodrill
b402e76185 .github/workflows: add integration test with a custom runner (#2044)
This runner is in my homelab while we muse about a better, more
permanent home for these tests.

Updates #1988

Signed-off-by: Christine Dodrill <xe@tailscale.com>
2021-06-08 12:49:23 -04:00
Christine Dodrill
622dc7b093 tstest/integration/vms: download images from s3 (#2035)
This makes integration tests pull pristine VM images from Amazon S3 if
they don't exist on disk. If the S3 fetch fails, it will fall back to
grabbing the image from the public internet. The VM images on the public
internet are known to be updated without warning and thusly change their
SHA256 checksum. This is not ideal for a test that we want to be able to
fire and forget, then run reliably for a very long time.

This requires an AWS profile to be configured at the default path. The
S3 bucket is rigged so that the requester pays. The VM images are
currently about 6.9 gigabytes. Please keep this in mind when running
these tests on your machine.

Documentation was added to the integration test folder to aid others in
running these tests on their machine.

Some wording in the logs of the tests was altered.

Updates #1988

Signed-off-by: Christine Dodrill <xe@tailscale.com>
2021-06-08 12:47:24 -04:00
Christine Dodrill
3f1405fa2a tstest/integration/vms: bump images, fix caching bug (#2052)
Before this redownloaded the image every time. Now it only redownloads
it when it needs to.

Signed-off-by: Christine Dodrill <xe@tailscale.com>
2021-06-08 10:15:59 -04:00
Brad Fitzpatrick
e29cec759a ipn/{ipnlocal,localapi}, control/controlclient: add SetDNS localapi
Updates #1235

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-06-07 20:35:56 -07:00
David Anderson
8236464252 packages/deb: add package to extract metadata from .deb files.
Signed-off-by: David Anderson <danderson@tailscale.com>
2021-06-07 16:22:23 -07:00
David Anderson
1c6946f971 cmd/mkpkg: allow zero files in a package.
Signed-off-by: David Anderson <danderson@tailscale.com>
2021-06-07 16:22:23 -07:00
David Anderson
7fab244614 net/dns/resolver: don't spam logs on EHOSTUNREACH.
Fixes #1719.

Signed-off-by: David Anderson <danderson@tailscale.com>
2021-06-07 10:45:29 -07:00
Simeng He
0141390365 tstest/integration/testcontrol: add Server.AddPingRequest
Signed-off-by: Simeng He <simeng@tailscale.com>
2021-06-07 13:40:35 -04:00
David Anderson
dfb1385fcc build_dist.sh: add a command to output the shell vars.
Some downstream distros eval'd version/version.sh to get at the shell variables
within their own build process. They can now `./build_dist.sh shellvars` to get
those.

Fixes #2058.

Signed-off-by: David Anderson <dave@natulte.net>
2021-06-05 19:02:42 -07:00
Josh Bleecher Snyder
e92fd19484 wgengine/wglog: match upstream wireguard-go's code for wireguardGoString
It is a bit faster.

But more importantly, it matches upstream byte-for-byte,
which ensures there'll be no corner cases in which we disagree.

name        old time/op    new time/op    delta
SetPeers-8    3.58µs ± 0%    3.16µs ± 2%  -11.74%  (p=0.016 n=4+5)

name        old alloc/op   new alloc/op   delta
SetPeers-8    2.53kB ± 0%    2.53kB ± 0%     ~     (all equal)

name        old allocs/op  new allocs/op  delta
SetPeers-8      99.0 ± 0%      99.0 ± 0%     ~     (all equal)

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-06-04 13:06:28 -07:00
Christine Dodrill
adaecd83c8 tstest/integration/vms: add DownloadImages test to download images (#2039)
The image downloads can take a significant amount of time for the tests.
This creates a new test that will download every distro image into the
local cache in parallel, optionally matching the distribution regex.

Updates #1988

Signed-off-by: Christine Dodrill <xe@tailscale.com>
2021-06-04 15:30:58 -04:00
Christine Dodrill
607b7ab692 tstest/integration/vms: aggressively re-verify shasums (#2050)
I've run into a couple issues where the tests time out while a VM image
is being downloaded, making the cache poisoned for the next run. This
moves the hash checking into its own function and calls it much sooner
in the testing chain. If the hash check fails, the OS is redownloaded.

Signed-off-by: Christine Dodrill <xe@tailscale.com>
2021-06-04 15:27:03 -04:00
David Anderson
df8a5d09c3 net/tstun: add a debug envvar to override tun MTU.
Signed-off-by: David Anderson <dave@natulte.net>
2021-06-04 11:55:11 -07:00
Christine Dodrill
6ce77b8eca tstest/integration/vms: log qemu output (#2047)
Most of the time qemu will output nothing when it is running. This is
expected behavior. However when qemu is unable to start due to some
problem, it prints that to either stdout or stderr. Previously this
output wasn't being captured. This patch captures that output to aid in
debugging qemu issues.

Updates #1988

Signed-off-by: Christine Dodrill <xe@tailscale.com>
2021-06-04 14:44:04 -04:00
Brad Fitzpatrick
58cc2cc921 tstest/integration/testcontrol: add Server.nodeLocked 2021-06-04 08:19:23 -07:00
David Anderson
aa6abc98f3 build_dist.sh: fix after the change to version stamping.
Signed-off-by: David Anderson <danderson@tailscale.com>
2021-06-03 13:14:32 -07:00
Brad Fitzpatrick
a573779c5c version: bump date
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-06-03 11:21:57 -07:00
Brad Fitzpatrick
5bf65c580d version: fix Short when link-stamped
And remove old SHORT, LONG deprecated variables.

Fixes tailscale/corp#1905

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-06-03 11:20:06 -07:00
Brad Fitzpatrick
ecfb2639cc ipn/ipnlocal: avoid initPeerAPIListener crash on certain concurrent actions
We were crashing on in initPeerAPIListener when called from
authReconfig when b.netMap is nil. But authReconfig already returns
before the call to initPeerAPIListener when b.netMap is nil, but it
releases the b.mu mutex before calling initPeerAPIListener which
reacquires it and assumes it's still nil.

The only thing that can be setting it to nil is setNetMapLocked, which
is called by ResetForClientDisconnect, Logout/logout, or Start, all of
which can happen during an authReconfig.

So be more defensive.

Fixes #1996

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-06-03 09:46:28 -07:00
Brad Fitzpatrick
713c5c9ab1 net/{interfaces,netns}: change which build tag means mac/ios Network/System Extension
We used to use "redo" for that, but it was pretty vague.

Also, fix the build tags broken in interfaces_default_route_test.go from
a9745a0b68, moving those Linux-specific
tests to interfaces_linux_test.go.

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-06-03 08:29:22 -07:00
Christine Dodrill
0a655309c6 tstest/integration/vms: only build binaries once (#2042)
Previously this built the binaries for every distro. This is a bit
overkill given we are using static binaries. This patch makes us only
build once.

There was also a weird issue with how processes were being managed.
Previously we just killed qemu with Process.Kill(), however that was
leaving behind zombies. This has been mended to not only kill qemu but
also waitpid() the process so it doesn't become a zombie.

Updates #1988

Signed-off-by: Christine Dodrill <xe@tailscale.com>
2021-06-03 10:58:35 -04:00
Christine Dodrill
a282819026 tstest/integration/vms: fix OpenSUSE Leap 15.1 (#2038)
The OpenSUSE 15.1 image we are using (and conseqentially the only one
that is really available easily given it is EOL) has cloud-init
hardcoded to use the OpenStack metadata thingy. Other OpenSUSE Leap
images function fine with the NoCloud backend, but this one seems to
just not work with it. No bother, we can just pretend to be OpenStack.

Thanks to Okami for giving me an example OpenStack configuration seed
image.

Updates #1988

Signed-off-by: Christine Dodrill <xe@tailscale.com>
2021-06-03 09:29:07 -04:00
Christine Dodrill
4da5e79c39 tstest/integration/vms: test on Arch Linux (#2040)
Arch is a bit of a weirder distro, however as a side effect it is much
more of a systemd purist experience. Adding it to our test suite will
make sure that we are working in the systemd happy path.

Updates #1988

Signed-off-by: Christine Dodrill <xe@tailscale.com>
2021-06-03 09:09:18 -04:00
Maisem Ali
95e296fd96 cmd/tailscale/web: restrict web access to synology admins.
Signed-off-by: Maisem Ali <maisem@tailscale.com>
2021-06-03 08:41:47 +05:00
David Anderson
5088af68cf version: remove all the redo stuff, only support embedding via go ldflags.
Signed-off-by: David Anderson <danderson@tailscale.com>
2021-06-02 14:17:46 -07:00
Brad Fitzpatrick
a321c24667 go.mod: update netaddr
Involves minor IPSetBuilder.Set API change.

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-06-02 09:05:06 -07:00
Brad Fitzpatrick
9794be375d tailcfg: add SetDNSRequest type
Updates #1235

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-06-01 20:05:01 -07:00
Christine Dodrill
ca96357d4b tstest/integration/vms: add OpenSUSE Leap 15.3 (#2026)
This distro is about to be released. OpenSUSE has historically had the
least coverage for functional testing, so this may prove useful in the
future.

Signed-off-by: Christine Dodrill <xe@tailscale.com>
2021-06-01 11:08:45 -04:00
David Anderson
33bc06795b go.mod: update for corp resync. 2021-05-31 21:47:37 -07:00
David Anderson
c54cc24e87 util/dnsname: make ToFQDN take exactly 0 or 1 allocs for everything.
name                                    old time/op    new time/op    delta
ToFQDN/www.tailscale.com.-32              9.55ns ± 2%   12.13ns ± 3%  +27.03%  (p=0.000 n=10+10)
ToFQDN/www.tailscale.com-32               86.3ns ± 1%    40.7ns ± 1%  -52.86%  (p=0.000 n=10+9)
ToFQDN/.www.tailscale.com-32              86.5ns ± 1%    40.4ns ± 1%  -53.29%  (p=0.000 n=10+9)
ToFQDN/_ssh._tcp.www.tailscale.com.-32    12.8ns ± 2%    14.7ns ± 2%  +14.24%  (p=0.000 n=9+10)
ToFQDN/_ssh._tcp.www.tailscale.com-32      104ns ± 1%      45ns ± 0%  -57.16%  (p=0.000 n=10+9)

name                                    old alloc/op   new alloc/op   delta
ToFQDN/www.tailscale.com.-32               0.00B          0.00B          ~     (all equal)
ToFQDN/www.tailscale.com-32                72.0B ± 0%     24.0B ± 0%  -66.67%  (p=0.000 n=10+10)
ToFQDN/.www.tailscale.com-32               72.0B ± 0%     24.0B ± 0%  -66.67%  (p=0.000 n=10+10)
ToFQDN/_ssh._tcp.www.tailscale.com.-32     0.00B          0.00B          ~     (all equal)
ToFQDN/_ssh._tcp.www.tailscale.com-32       112B ± 0%       32B ± 0%  -71.43%  (p=0.000 n=10+10)

name                                    old allocs/op  new allocs/op  delta
ToFQDN/www.tailscale.com.-32                0.00           0.00          ~     (all equal)
ToFQDN/www.tailscale.com-32                 2.00 ± 0%      1.00 ± 0%  -50.00%  (p=0.000 n=10+10)
ToFQDN/.www.tailscale.com-32                2.00 ± 0%      1.00 ± 0%  -50.00%  (p=0.000 n=10+10)
ToFQDN/_ssh._tcp.www.tailscale.com.-32      0.00           0.00          ~     (all equal)
ToFQDN/_ssh._tcp.www.tailscale.com-32       2.00 ± 0%      1.00 ± 0%  -50.00%  (p=0.000 n=10+10)

Signed-off-by: David Anderson <danderson@tailscale.com>
2021-05-31 21:13:50 -07:00
David Anderson
d7f6ef3a79 util/dnsname: add a benchmark for ToFQDN.
Signed-off-by: David Anderson <danderson@tailscale.com>
2021-05-31 21:13:50 -07:00
David Anderson
caaefa00a0 util/dnsname: don't validate the contents of DNS labels.
DNS names consist of labels, but outside of length limits, DNS
itself permits any content within the labels. Some records require
labels to conform to hostname limitations (which is what we implemented
before), but not all.

Fixes #2024.

Signed-off-by: David Anderson <danderson@tailscale.com>
2021-05-31 21:13:50 -07:00
Christine Dodrill
2802a01b81 tstest/integration/vms: test vms as they are ready (#2022)
Instead of testing all the VMs at once when they are all ready, this
patch changes the testing logic so that the vms are tested as soon as
they register with testcontrol. Also limit the amount of VM ram used at
once with the `-ram-limit` flag. That uses a semaphore to guard resource
use.

Also document CentOS' sins.

Updates #1988

Signed-off-by: Christine Dodrill <xe@tailscale.com>
2021-05-31 17:04:49 -04:00
Avery Pennarun
eaa6507cc9 ipnlocal: in Start() fast path, don't forget to send Prefs.
The resulting empty Prefs had AllowSingleHosts=false and
Routeall=false, so that on iOS if you did these steps:
- Login and leave running
- Terminate the frontend
- Restart the frontend (fast path restart, missing prefs)
- Set WantRunning=false
- Set WantRunning=true
...then you would have Tailscale running, but with no routes. You would
also accidentally disable the ExitNodeID/IP prefs (symptom: the current
exit node setting didn't appear in the UI), but since nothing
else worked either, you probably didn't notice.

The fix was easy enough. It turns out we already knew about the
problem, so this also fixes one of the BUG entries in state_test.

Fixes: #1918 (BUG-1) and some as-yet-unreported bugs with exit nodes.
Signed-off-by: Avery Pennarun <apenwarr@tailscale.com>
2021-05-31 14:53:49 -04:00
Avery Pennarun
8a7d35594d ipnlocal: don't assume NeedsLogin immediately after StartLogout().
Previously, there was no server round trip required to log out, so when
you asked ipnlocal to Logout(), it could clear the netmap immediately
and switch to NeedsLogin state.

In v1.8, we added a true Logout operation. ipn.Logout() would trigger
an async cc.StartLogout() and *also* immediately switch to NeedsLogin.
Unfortunately, some frontends would see NeedsLogin and immediately
trigger a new StartInteractiveLogin() operation, before the
controlclient auth state machine actually acted on the Logout command,
thus accidentally invalidating the entire logout operation, retaining
the netmap, and violating the user's expectations.

Instead, add a new LogoutFinished signal from controlclient
(paralleling LoginFinished) and, upon starting a logout, don't update
the ipn state machine until it's received.

Updates: #1918 (BUG-2)
Signed-off-by: Avery Pennarun <apenwarr@tailscale.com>
2021-05-31 14:53:49 -04:00
Christine Dodrill
36cb69002a tstest/integration/vms: regex-match distros using a flag (#2021)
If you set `-distro-regex` to match a subset of distros, only those
distros will be tested. Ex:

    $ go test -run-vm-tests -distro-regex='opensuse'

Signed-off-by: Christine Dodrill <xe@tailscale.com>
2021-05-31 13:23:38 -04:00
Christine Dodrill
e1b994f7ed tstest/integration/vms: maintain distro info (#2020)
This lets us see the names of distros in our tests.

Updates #1988

Signed-off-by: Christine Dodrill <xe@tailscale.com>
2021-05-31 13:14:30 -04:00
Brad Fitzpatrick
fa548c5b96 tstest/integration/vms: fix bindhost lookup (#2012)
Don't try to do heuristics on the name. Use the net/interfaces package
which we already have to do this sort of stuff.

Fixes #2011

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-05-31 12:00:50 -04:00
Christine Dodrill
14c1113d2b tstest/integration/vms: copy locally built binaries (#2006)
Instead of pulling packages from pkgs.tailscale.com, we should use the
tailscale binaries that are local to this git commit. This exposes a bit
of the integration testing stack in order to copy the binaries
correctly.

This commit also bumps our version of github.com/pkg/sftp to the latest
commit.

If you run into trouble with yaml, be sure to check out the
commented-out alpine linux image complete with instructions on how to
use it.

Updates #1988

Signed-off-by: Christine Dodrill <xe@tailscale.com>
2021-05-31 11:35:01 -04:00
Brad Fitzpatrick
ca455ac84b net/tsaddr: simplify TailscaleServiceIP
netaddr allocated at the time this was written. No longer.

name                    old time/op  new time/op  delta
TailscaleServiceAddr-4  5.46ns ± 4%  1.83ns ± 3%  -66.52%  (p=0.008 n=5+5)

A bunch of the others can probably be simplified too, but this
was the only one with just an IP and not an IPPrefix.

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-05-28 20:36:26 -07:00
Brad Fitzpatrick
f21982f854 tstest/integration/vms: skip a test for now
Updates #2011

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-05-28 20:31:36 -07:00
Josh Bleecher Snyder
ddf6c8c729 wgengine/magicsock: delete dead code
Co-authored-by: Adrian Dewhurst <adrian@tailscale.com>
Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-05-28 17:02:08 -07:00
Christine Dodrill
4cfaf489ac tstest/integration/vms: t.Log for VM output (#2007)
Previously we spewed a lot of output to stdout and stderr, even when
`-v` wasn't set. This is sub-optimal for various reasons. This patch
shunts that output to test logs so it only shows up when `-v` is set.

Updates #1988

Signed-off-by: Christine Dodrill <xe@tailscale.com>
2021-05-28 14:19:44 -04:00
Adrian Dewhurst
6d6cf88d82 control/controlclient: use our fork of certstore
The cyolosecurity fork of certstore did not update its module name and
thus can only be used with a replace directive. This interferes with
installing using `go install` so I created a tailscale fork with an
updated module name.

Signed-off-by: Adrian Dewhurst <adrian@tailscale.com>
2021-05-28 12:12:45 -04:00
Christine Dodrill
1f72b6f812 tstest/integration/vms: use dynamically discovered bindhost (#1992)
Instead of relying on a libvirtd bridge address that you probably won't
have on your system.

Updates #1988

Signed-off-by: Christine Dodrill <xe@tailscale.com>
2021-05-28 08:05:17 -04:00
Christine Dodrill
35749ec297 tstest/integration/vms: small cleanups (#1989)
Updates #1988

Signed-off-by: Christine Dodrill <xe@tailscale.com>
2021-05-27 14:29:29 -04:00
Brad Fitzpatrick
a04801e037 ipn/ipnlocal: ignore NetfilterMode pref on Synology
On clean installs we didn't set use iptables, but during upgrades it
looks like we could use old prefs that directed us to go into the iptables
paths that might fail on Synology.

Updates #1995
Fixes tailscale/tailscale-synology#57 (I think)

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-05-27 10:57:57 -07:00
David Crawshaw
82b217f82e cmd/tailscale: have web POST wait for authURL
Fixes #1939

Signed-off-by: David Crawshaw <crawshaw@tailscale.com>
2021-05-27 10:30:03 -07:00
David Crawshaw
50c976d3f1 cmd/tailscale: show web 'login' error message
For #1939

Signed-off-by: David Crawshaw <crawshaw@tailscale.com>
2021-05-27 10:30:03 -07:00
Brad Fitzpatrick
d2c4e75099 cmd/tailscale/cli: update URL in error message for Synology unsupported feature
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-05-27 08:43:53 -07:00
Brad Fitzpatrick
cdd231cb7d cmd/tailscale/cli: don't warn about iptables=off on Synology
We don't use iptables on Synology, so don't scare the user.

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-05-27 08:11:43 -07:00
Christine Dodrill
ba59c0391b tstest/integration: add experimental integration test (#1966)
This will spin up a few vms and then try and make them connect to a
testcontrol server.

Updates #1988

Signed-off-by: Christine Dodrill <xe@tailscale.com>
2021-05-26 14:10:10 -04:00
Josh Bleecher Snyder
60e920bf18 go.mod: go mod tidy
Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-05-25 17:14:43 -07:00
Josh Bleecher Snyder
bb8ce48a6b logtail: allow changing log level concurrently
When tailscaled starts up, these lines run:

func run() error {
	// ...
	pol := logpolicy.New("tailnode.log.tailscale.io")
	pol.SetVerbosityLevel(args.verbose)
	// ...
}

If there are old log entries present, they immediate start getting uploaded. This races with the call to pol.SetVerbosityLevel.

This manifested itself as a test failure in tailscale.com/tstest/integration
when run with -race:

WARNING: DATA RACE
Read at 0x00c0001bc970 by goroutine 24:
  tailscale.com/logtail.(*Logger).Write()
      /Users/josh/t/corp/oss/logtail/logtail.go:517 +0x27c
  log.(*Logger).Output()
      /Users/josh/go/ts/src/log/log.go:184 +0x2b8
  log.Printf()
      /Users/josh/go/ts/src/log/log.go:323 +0x94
  tailscale.com/logpolicy.newLogtailTransport.func1()
      /Users/josh/t/corp/oss/logpolicy/logpolicy.go:509 +0x36c
  net/http.(*Transport).dial()
      /Users/josh/go/ts/src/net/http/transport.go:1168 +0x238
  net/http.(*Transport).dialConn()
      /Users/josh/go/ts/src/net/http/transport.go:1606 +0x21d0
  net/http.(*Transport).dialConnFor()
      /Users/josh/go/ts/src/net/http/transport.go:1448 +0xe4

Previous write at 0x00c0001bc970 by main goroutine:
  tailscale.com/logtail.(*Logger).SetVerbosityLevel()
      /Users/josh/t/corp/oss/logtail/logtail.go:131 +0x98
  tailscale.com/logpolicy.(*Policy).SetVerbosityLevel()
      /Users/josh/t/corp/oss/logpolicy/logpolicy.go:463 +0x60
  main.run()
      /Users/josh/t/corp/oss/cmd/tailscaled/tailscaled.go:178 +0x50
  main.main()
      /Users/josh/t/corp/oss/cmd/tailscaled/tailscaled.go:163 +0x71c

Goroutine 24 (running) created at:
  net/http.(*Transport).queueForDial()
      /Users/josh/go/ts/src/net/http/transport.go:1417 +0x4d8
  net/http.(*Transport).getConn()
      /Users/josh/go/ts/src/net/http/transport.go:1371 +0x5b8
  net/http.(*Transport).roundTrip()
      /Users/josh/go/ts/src/net/http/transport.go:585 +0x7f4
  net/http.(*Transport).RoundTrip()
      /Users/josh/go/ts/src/net/http/roundtrip.go:17 +0x30
  net/http.send()
      /Users/josh/go/ts/src/net/http/client.go:251 +0x4f0
  net/http.(*Client).send()
      /Users/josh/go/ts/src/net/http/client.go:175 +0x148
  net/http.(*Client).do()
      /Users/josh/go/ts/src/net/http/client.go:717 +0x1d0
  net/http.(*Client).Do()
      /Users/josh/go/ts/src/net/http/client.go:585 +0x358
  tailscale.com/logtail.(*Logger).upload()
      /Users/josh/t/corp/oss/logtail/logtail.go:367 +0x334
  tailscale.com/logtail.(*Logger).uploading()
      /Users/josh/t/corp/oss/logtail/logtail.go:289 +0xec


Rather than complicate the logpolicy API,
allow the verbosity to be adjusted concurrently.

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-05-25 15:29:21 -07:00
Josh Bleecher Snyder
1ece91cede go.mod: upgrade wireguard-windows, de-fork wireguard-go
Pull in the latest version of wireguard-windows.

Switch to upstream wireguard-go.
This requires reverting all of our import paths.

Unfortunately, this has to happen at the same time.
The wireguard-go change is very low risk,
as that commit matches our fork almost exactly.
(The only changes are import paths, CI files, and a go.mod entry.)
So if there are issues as a result of this commit,
the first place to look is wireguard-windows changes.

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-05-25 13:18:21 -07:00
Josh Bleecher Snyder
ceaaa23962 wgengine/wglog: cache strings
We repeat many peers each time we call SetPeers.
Instead of constructing strings for them from scratch every time,
keep strings alive across iterations.

name        old time/op    new time/op    delta
SetPeers-8    3.58µs ± 1%    2.41µs ± 1%  -32.60%  (p=0.000 n=9+10)

name        old alloc/op   new alloc/op   delta
SetPeers-8    2.53kB ± 0%    1.30kB ± 0%  -48.73%  (p=0.000 n=10+10)

name        old allocs/op  new allocs/op  delta
SetPeers-8      99.0 ± 0%      16.0 ± 0%  -83.84%  (p=0.000 n=10+10)

We could reduce alloc/op 12% and allocs/op 23% if strs had
type map[string]strCache instead of map[string]*strCache,
but that wipes out the execution time impact.
Given that re-use is the most common scenario, let's optimize for it.

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-05-24 18:41:54 -07:00
Josh Bleecher Snyder
c065cc6169 internal/deephash: remove remaining type special cases
Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-05-24 15:22:44 -07:00
Josh Bleecher Snyder
4b51fbf48c internal/deephash: increase scratch space size
e66d4e4c81 added AppendTo methods
to some key types. Their marshaled form is longer than 64 bytes.

name    old time/op    new time/op    delta
Hash-8    15.5µs ± 1%    14.8µs ± 1%   -4.17%  (p=0.000 n=9+9)

name    old alloc/op   new alloc/op   delta
Hash-8    1.18kB ± 0%    0.47kB ± 0%  -59.87%  (p=0.000 n=10+10)

name    old allocs/op  new allocs/op  delta
Hash-8      12.0 ± 0%       6.0 ± 0%  -50.00%  (p=0.000 n=10+10)

This is still a bit worse than explicitly handling the types,
but much nicer.

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-05-24 15:22:44 -07:00
Brad Fitzpatrick
e66d4e4c81 tailcfg, types/wgkey: add AppendTo methods on some types
Add MarshalText-like appending variants. Like:
https://pkg.go.dev/inet.af/netaddr#IP.AppendTo

To be used by @josharian's pending deephash optimizations.

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-05-24 15:09:57 -07:00
Josh Bleecher Snyder
b340beff8e internal/deephash: reset scratch before appending to it
Oops. In practice this doesn't matter, but it's still wrong.

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-05-24 14:47:41 -07:00
Josh Bleecher Snyder
15a7ff83de internal/deephash: remove netaddr special cases
All netaddr types that we are concerned with now implement AppendTo.
Use the AppendTo method if available, and remove all references to netaddr.

This is slower but cleaner, and more readily re-usable by others.

name              old time/op    new time/op    delta
Hash-8              12.6µs ± 0%    14.8µs ± 1%  +18.05%  (p=0.000 n=8+10)
HashMapAcyclic-8    21.4µs ± 1%    21.9µs ± 1%   +2.39%  (p=0.000 n=10+9)

name              old alloc/op   new alloc/op   delta
Hash-8                408B ± 0%      408B ± 0%     ~     (p=1.000 n=10+10)
HashMapAcyclic-8     1.00B ± 0%     1.00B ± 0%     ~     (all equal)

name              old allocs/op  new allocs/op  delta
Hash-8                6.00 ± 0%      6.00 ± 0%     ~     (all equal)
HashMapAcyclic-8      0.00           0.00          ~     (all equal)

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-05-24 14:47:41 -07:00
Josh Bleecher Snyder
051d2f47e5 internal/deephash: re-use MapIter
name              old time/op    new time/op    delta
Hash-8              12.4µs ± 0%    12.4µs ± 0%    -0.33%  (p=0.002 n=10+9)
HashMapAcyclic-8    21.2µs ± 0%    21.3µs ± 0%    +0.45%  (p=0.000 n=8+8)

name              old alloc/op   new alloc/op   delta
Hash-8                793B ± 0%      408B ± 0%   -48.55%  (p=0.000 n=10+10)
HashMapAcyclic-8      128B ± 0%        0B       -100.00%  (p=0.000 n=10+10)

name              old allocs/op  new allocs/op  delta
Hash-8                9.00 ± 0%      6.00 ± 0%   -33.33%  (p=0.000 n=10+10)
HashMapAcyclic-8      1.00 ± 0%      0.00       -100.00%  (p=0.000 n=10+10)

Depends on https://github.com/golang/go/issues/46293.

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-05-24 14:47:41 -07:00
Josh Bleecher Snyder
c06ec45f09 internal/deephash: document MapIter shims
These exist so we can use the optimized MapIter APIs
while still working with released versions of Go.
They're pretty simple, but some docs won't hurt.

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-05-24 14:47:41 -07:00
Elias Naur
adfe8cf41d paths: generalize IOSSharedDir to cover Android
Also fix an error message while here.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2021-05-24 13:52:48 -07:00
Josh Bleecher Snyder
73adbb7a78 wgengine: pass an addressable value to deephash.UpdateHash
This makes deephash more efficient.

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-05-24 13:51:23 -07:00
Josh Bleecher Snyder
ce7a87e5e4 internal/deephash: use hash.BlockSize instead of a constant
Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-05-24 13:51:23 -07:00
Josh Bleecher Snyder
135b641332 internal/deephash: add re-usable scratch space
name    old time/op    new time/op    delta
Hash-8    13.9µs ± 0%    12.5µs ± 0%  -10.10%  (p=0.008 n=5+5)

name    old alloc/op   new alloc/op   delta
Hash-8      793B ± 0%      793B ± 0%     ~     (all equal)

name    old allocs/op  new allocs/op  delta
Hash-8      14.0 ± 0%      12.0 ± 0%  -14.29%  (p=0.008 n=5+5)

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-05-24 13:51:23 -07:00
Josh Bleecher Snyder
988dfcabef internal/deephash: simplify API
Reduce to just a single external endpoint.
Convert from a variadic number of interfaces to a slice there.

name    old time/op    new time/op    delta
Hash-8    14.4µs ± 0%    14.0µs ± 1%   -3.08%  (p=0.000 n=9+9)

name    old alloc/op   new alloc/op   delta
Hash-8      873B ± 0%      793B ± 0%   -9.16%  (p=0.000 n=9+6)

name    old allocs/op  new allocs/op  delta
Hash-8      18.0 ± 0%      14.0 ± 0%  -22.22%  (p=0.000 n=10+10)

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-05-24 13:51:23 -07:00
Josh Bleecher Snyder
b371588ce6 internal/deephash: use netaddr AppendTo methods
Slightly slower, but lots less garbage.
We will recover the speed lost in a follow-up commit.

name    old time/op    new time/op    delta
Hash-8    13.5µs ± 1%    14.3µs ± 0%   +5.84%  (p=0.000 n=10+9)

name    old alloc/op   new alloc/op   delta
Hash-8    1.46kB ± 0%    0.87kB ± 0%  -40.10%  (p=0.000 n=7+10)

name    old allocs/op  new allocs/op  delta
Hash-8      43.0 ± 0%      18.0 ± 0%  -58.14%  (p=0.000 n=10+10)

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-05-24 13:51:23 -07:00
Josh Bleecher Snyder
09afb8e35b internal/deephash: re-use map iteration values
This requires changes to the Go toolchain.
The changes are upstream at https://golang.org/cl/320929.
They haven't been pulled into our fork yet.

No need to allocate new iteration scratch values for every map.

name              old time/op    new time/op    delta
Hash-8              13.6µs ± 0%    13.5µs ± 0%   -1.01%  (p=0.008 n=5+5)
HashMapAcyclic-8    21.2µs ± 1%    21.1µs ± 2%     ~     (p=0.310 n=5+5)

name              old alloc/op   new alloc/op   delta
Hash-8              1.58kB ± 0%    1.46kB ± 0%   -7.60%  (p=0.008 n=5+5)
HashMapAcyclic-8      152B ± 0%      128B ± 0%  -15.79%  (p=0.008 n=5+5)

name              old allocs/op  new allocs/op  delta
Hash-8                49.0 ± 0%      43.0 ± 0%  -12.24%  (p=0.008 n=5+5)
HashMapAcyclic-8      4.00 ± 0%      2.00 ± 0%  -50.00%  (p=0.008 n=5+5)

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-05-24 13:51:23 -07:00
Josh Bleecher Snyder
a2d7a2aeb1 internal/deephash: use MapIter.Set{Key,Value}
To get the benefit of this optimization requires help from the Go toolchain.
The changes are upstream at https://golang.org/cl/320929,
and have been pulled into the Tailscale fork at
728ecc58fd.
It also requires building with the build tag tailscale_go.

name              old time/op    new time/op    delta
Hash-8              14.0µs ± 0%    13.6µs ± 0%   -2.88%  (p=0.008 n=5+5)
HashMapAcyclic-8    24.3µs ± 1%    21.2µs ± 1%  -12.47%  (p=0.008 n=5+5)

name              old alloc/op   new alloc/op   delta
Hash-8              2.16kB ± 0%    1.58kB ± 0%  -27.01%  (p=0.008 n=5+5)
HashMapAcyclic-8    2.53kB ± 0%    0.15kB ± 0%  -93.99%  (p=0.008 n=5+5)

name              old allocs/op  new allocs/op  delta
Hash-8                77.0 ± 0%      49.0 ± 0%  -36.36%  (p=0.008 n=5+5)
HashMapAcyclic-8       202 ± 0%         4 ± 0%  -98.02%  (p=0.008 n=5+5)

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>

setkey
2021-05-24 13:51:23 -07:00
Josh Bleecher Snyder
020e904f4e internal/deephash: add special handling for netaddr.IPPort
The acyclic map code interacts badly with netaddr.IPs.
One of the netaddr.IP fields is an *intern.Value,
and we use a few sentinel values.
Those sentinel values make many of the netaddr data structures appear cyclic.

One option would be to replace the cycle-detection code with
a Floyd-Warshall style algorithm. The downside is that this will take
longer to detect cycles, particularly if the cycle is long.

This problem is exacerbated by the fact that the acyclic cycle detection
code shares a single visited map for the entire data structure,
not just the subsection of the data structure localized to the map.
Unfortunately, the extra allocations and work (and code) to use per-map
visited maps make this option not viable.

Instead, continue to special-case netaddr data types.

name              old time/op    new time/op    delta
Hash-8              22.4µs ± 0%    14.0µs ± 0%  -37.59%  (p=0.008 n=5+5)
HashMapAcyclic-8    23.8µs ± 0%    24.3µs ± 1%   +1.75%  (p=0.008 n=5+5)

name              old alloc/op   new alloc/op   delta
Hash-8              2.49kB ± 0%    2.16kB ± 0%     ~     (p=0.079 n=4+5)
HashMapAcyclic-8    2.53kB ± 0%    2.53kB ± 0%     ~     (all equal)

name              old allocs/op  new allocs/op  delta
Hash-8                86.0 ± 0%      77.0 ± 0%  -10.47%  (p=0.008 n=5+5)
HashMapAcyclic-8       202 ± 0%       202 ± 0%     ~     (all equal)

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-05-24 13:51:23 -07:00
Josh Bleecher Snyder
bbb79f2d6a internal/deephash: fix accidental naked return
name              old time/op    new time/op    delta
Hash-8              23.0µs ± 1%    22.4µs ± 0%   -2.43%  (p=0.008 n=5+5)
HashMapAcyclic-8    24.0µs ± 0%    23.8µs ± 0%   -0.56%  (p=0.008 n=5+5)

name              old alloc/op   new alloc/op   delta
Hash-8              2.92kB ± 0%    2.49kB ± 0%  -14.80%  (p=0.000 n=5+4)
HashMapAcyclic-8    2.53kB ± 0%    2.53kB ± 0%     ~     (all equal)

name              old allocs/op  new allocs/op  delta
Hash-8                93.0 ± 0%      86.0 ± 0%   -7.53%  (p=0.008 n=5+5)
HashMapAcyclic-8       202 ± 0%       202 ± 0%     ~     (all equal)

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-05-24 13:51:23 -07:00
Brad Fitzpatrick
79b7fa9ac3 internal/deephash: hash maps without sorting in the acyclic common case
Hash and xor each entry instead, then write final xor'ed result.

name    old time/op    new time/op    delta
Hash-4    33.6µs ± 4%    34.6µs ± 3%  +3.03%  (p=0.013 n=10+9)

name    old alloc/op   new alloc/op   delta
Hash-4    1.86kB ± 0%    1.77kB ± 0%  -5.10%  (p=0.000 n=10+9)

name    old allocs/op  new allocs/op  delta
Hash-4      51.0 ± 0%      49.0 ± 0%  -3.92%  (p=0.000 n=10+10)

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-05-24 13:51:23 -07:00
Brad Fitzpatrick
a86a0361a7 go.mod: upgrade all deps
At the start of a dev cycle we'll upgrade all dependencies.

Done with:

$ for Dep in $(cat go.mod | perl -ne '/(\S+) v/ and print "$1\n"'); do go get $Dep@upgrade; done

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-05-24 13:04:59 -07:00
Josh Bleecher Snyder
8bf2a38f29 go.mod: update wireguard-go, taking control over iOS memory usage from our fork
Our wireguard-go fork used different values from upstream for
package device's memory limits on iOS.

This was the last blocker to removing our fork.

These values are now vars rather than consts for iOS.

c27ff9b9f6

Adjust them on startup to our preferred values.

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-05-24 12:03:57 -07:00
Josh Bleecher Snyder
5666663370 net/packet: use netaddr AppendTo methods
This lets us remote the types/strbuilder package,
which had only a single user.
And it's faster.

name              old time/op    new time/op    delta
String/tcp4-8        175ns ± 0%      58ns ± 1%  -66.95%  (p=0.000 n=10+9)
String/tcp6-8        226ns ± 1%     136ns ± 1%  -39.85%  (p=0.000 n=10+10)
String/udp4-8        175ns ± 1%      58ns ± 1%  -67.01%  (p=0.000 n=10+9)
String/udp6-8        230ns ± 1%     140ns ± 0%  -39.32%  (p=0.000 n=10+9)
String/icmp4-8       164ns ± 0%      50ns ± 1%  -69.89%  (p=0.000 n=10+10)
String/icmp6-8       217ns ± 1%     129ns ± 0%  -40.46%  (p=0.000 n=10+10)
String/igmp-8        196ns ± 0%      56ns ± 1%  -71.32%  (p=0.000 n=10+10)
String/unknown-8    2.06ns ± 1%    2.06ns ± 2%     ~     (p=0.985 n=10+10)

name              old alloc/op   new alloc/op   delta
String/tcp4-8        32.0B ± 0%     32.0B ± 0%     ~     (all equal)
String/tcp6-8         168B ± 0%       96B ± 0%  -42.86%  (p=0.000 n=10+10)
String/udp4-8        32.0B ± 0%     32.0B ± 0%     ~     (all equal)
String/udp6-8         168B ± 0%       96B ± 0%  -42.86%  (p=0.000 n=10+10)
String/icmp4-8       32.0B ± 0%     32.0B ± 0%     ~     (all equal)
String/icmp6-8        104B ± 0%       64B ± 0%  -38.46%  (p=0.000 n=10+10)
String/igmp-8        48.0B ± 0%     48.0B ± 0%     ~     (all equal)
String/unknown-8     0.00B          0.00B          ~     (all equal)

name              old allocs/op  new allocs/op  delta
String/tcp4-8         1.00 ± 0%      1.00 ± 0%     ~     (all equal)
String/tcp6-8         3.00 ± 0%      1.00 ± 0%  -66.67%  (p=0.000 n=10+10)
String/udp4-8         1.00 ± 0%      1.00 ± 0%     ~     (all equal)
String/udp6-8         3.00 ± 0%      1.00 ± 0%  -66.67%  (p=0.000 n=10+10)
String/icmp4-8        1.00 ± 0%      1.00 ± 0%     ~     (all equal)
String/icmp6-8        3.00 ± 0%      1.00 ± 0%  -66.67%  (p=0.000 n=10+10)
String/igmp-8         1.00 ± 0%      1.00 ± 0%     ~     (all equal)
String/unknown-8      0.00           0.00          ~     (all equal)

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-05-20 20:42:18 -07:00
Josh Bleecher Snyder
d6d1951897 net/packet: add BenchmarkString
Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-05-20 20:42:18 -07:00
David Anderson
df350e2069 ipn/ipnlocal: initialize DNS config maps unconditionally.
Fixes #1963.

Signed-off-by: David Anderson <danderson@tailscale.com>
2021-05-20 20:22:39 -07:00
Josh Bleecher Snyder
eb9757a290 go.mod: upgrade netaddr to get AppendTo methods
Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-05-19 11:58:48 -07:00
Josh Bleecher Snyder
cd54792fe9 internal/deephash: add a few more benchmarking map entries
Typical maps in production are considerably longer.
This helps benchmarks more accurately reflect the costs per key
vs the costs per map in deephash.

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-05-19 10:02:03 -07:00
David Crawshaw
293a2b11cd ipn: allow b to be nil in NewBackendServer
A couple of code paths in ipnserver use a NewBackendServer with a nil
backend just to call the callback with an encapsulated error message.
This covers a panic case seen in logs.

For #1920

Signed-off-by: David Crawshaw <crawshaw@tailscale.com>
2021-05-19 09:33:19 -07:00
David Anderson
e2dcf63420 net/dns: replace AuthoritativeSuffixes with nil Route entries.
This leads to a cleaner separation of intent vs. implementation
(Routes is now the only place specifying who handles DNS requests),
and allows for cleaner expression of a configuration that creates
MagicDNS records without serving them to the OS.

Signed-off-by: David Anderson <danderson@tailscale.com>
2021-05-18 14:15:17 -07:00
David Anderson
6690f86ef4 net/dns: always offer MagicDNS records at 100.100.100.100.
Fixes #1886.

Signed-off-by: David Anderson <danderson@tailscale.com>
2021-05-18 14:15:17 -07:00
simenghe
dd0b690e7b Added new Addresses / AllowedIPs fields to testcontrol when creating tailcfg.Node (#1948)
* Added new Addresses / AllowedIPs fields to testcontrol when creating new &tailcfg.Node

Signed-off-by: Simeng He <simeng@tailscale.com>

* Added single node test to check Addresses and AllowedIPs

Signed-off-by: Simeng He <simeng@tailscale.com>

Co-authored-by: Simeng He <simeng@tailscale.com>
2021-05-18 16:20:29 -04:00
David Anderson
85df1b0fa7 go.mod: bump wireguard-go.
Signed-off-by: David Anderson <danderson@tailscale.com>
2021-05-17 16:30:24 -07:00
Maisem Ali
234cc87f48 cmd/tailscaled: use the wf package instead of wireguard-windows/firewall
Signed-off-by: Maisem Ali <maisem@tailscale.com>
2021-05-17 13:07:25 -07:00
Josh Bleecher Snyder
25df067dd0 all: adapt to opaque netaddr types
This commit is a mishmash of automated edits using gofmt:

gofmt -r 'netaddr.IPPort{IP: a, Port: b} -> netaddr.IPPortFrom(a, b)' -w .
gofmt -r 'netaddr.IPPrefix{IP: a, Port: b} -> netaddr.IPPrefixFrom(a, b)' -w .

gofmt -r 'a.IP.Is4 -> a.IP().Is4' -w .
gofmt -r 'a.IP.As16 -> a.IP().As16' -w .
gofmt -r 'a.IP.Is6 -> a.IP().Is6' -w .
gofmt -r 'a.IP.As4 -> a.IP().As4' -w .
gofmt -r 'a.IP.String -> a.IP().String' -w .

And regexps:

\w*(.*)\.Port = (.*)  ->  $1 = $1.WithPort($2)
\w*(.*)\.IP = (.*)  ->  $1 = $1.WithIP($2)

And lots of manual fixups.

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-05-16 14:52:00 -07:00
David Anderson
4f92f405ee scripts: fix up installer script comments.
Signed-off-by: David Anderson <danderson@tailscale.com>
2021-05-14 14:13:31 -07:00
David Anderson
0e9ea9f779 scripts: detect curl vs. wget and use the right one.
Signed-off-by: David Anderson <danderson@tailscale.com>
2021-05-14 14:12:31 -07:00
David Anderson
783f125003 scripts: use codenames for ubuntu, since that's what our repo uses.
Signed-off-by: David Anderson <danderson@tailscale.com>
2021-05-14 14:12:28 -07:00
David Anderson
01a359cec9 scripts: add an install script.
The script detects one of the supported OS/version combos, and issues
the right install instructions for it.

Co-authored-by: Christine Dodrill <xe@tailscale.com>
Signed-off-by: David Anderson <danderson@tailscale.com>
2021-05-14 13:40:41 -07:00
Brad Fitzpatrick
5b52b64094 tsnet: add Tailscale-as-a-library package
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-05-14 12:46:42 -07:00
Josh Bleecher Snyder
6f62bbae79 cmd/tailscale: make ping --until-direct require direct connection to exit 0
If --until-direct is set, the goal is to make a direct connection.
If we failed at that, say so, and exit with an error.

RELNOTE=tailscale ping --until-direct (the default) now exits with
a non-zero exit code if no direct connection was established.

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-05-13 15:07:19 -07:00
Avery Pennarun
6fd4e8d244 ipnlocal: fix switching users while logged in + Stopped.
This code path is very tricky since it was originally designed for the
"re-authenticate to refresh my keys" use case, which didn't want to
lose the original session even if the refresh cycle failed. This is why
it acts differently from the Logout(); Login(); case.

Maybe that's too fancy, considering that it probably never quite worked
at all, for switching between users without logging out first. But it
works now.

This was more invasive than I hoped, but the necessary fixes actually
removed several other suspicious BUG: lines from state_test.go, so I'm
pretty confident this is a significant net improvement.

Fixes tailscale/corp#1756.

Signed-off-by: Avery Pennarun <apenwarr@tailscale.com>
2021-05-12 23:21:22 -04:00
Avery Pennarun
6307a9285d controlclient: update Persist.LoginName when it changes.
Well, that was anticlimactic.

Fixes tailscale/corp#461.

Signed-off-by: Avery Pennarun <apenwarr@tailscale.com>
2021-05-12 23:21:11 -04:00
Avery Pennarun
285d0e3b4d ipnlocal: fix deadlock in RequestEngineStatusAndWait() error path.
If the engine was shutting down from a previous session
(e.closing=true), it would return an error code when trying to get
status. In that case, ipnlocal would never unblock any callers that
were waiting on the status.

Not sure if this ever happened in real life, but I accidentally
triggered it while writing a test.

Signed-off-by: Avery Pennarun <apenwarr@tailscale.com>
2021-05-12 23:21:11 -04:00
Brad Fitzpatrick
5a7c6f1678 tstest/integration{,/testcontrol}: add node update support, two node test
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-05-12 14:43:43 -07:00
Brad Fitzpatrick
d32667011d tstest/integration: build test binaries with -race if test itself is
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-05-12 13:13:08 -07:00
Brad Fitzpatrick
314d15b3fb version: add func IsRace to report whether race detector enabled
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-05-12 13:12:41 -07:00
Brad Fitzpatrick
ed9d825552 tstest/integration: fix integration test on linux/386
Apparently can't use GOBIN with GOARCH.

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-05-12 11:56:00 -07:00
Brad Fitzpatrick
c0158bcd0b tstest/integration{,/testcontrol}: add testcontrol.RequireAuth mode, new test
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-05-12 11:37:27 -07:00
Josh Bleecher Snyder
ebcd7ab890 wgengine: remove wireguard-go DeviceOptions
We no longer need them.
This also removes the 32 bytes of prefix junk before endpoints.

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-05-11 15:30:39 -07:00
Josh Bleecher Snyder
aacb2107ae all: add extra information to serialized endpoints
magicsock.Conn.ParseEndpoint requires a peer's public key,
disco key, and legacy ip/ports in order to do its job.
We currently accomplish that by:

* adding the public key in our wireguard-go fork
* encoding the disco key as magic hostname
* using a bespoke comma-separated encoding

It's a bit messy.

Instead, switch to something simpler: use a json-encoded struct
containing exactly the information we need, in the form we use it.

Our wireguard-go fork still adds the public key to the
address when it passes it to ParseEndpoint, but now the code
compensating for that is just a couple of simple, well-commented lines.
Once this commit is in, we can remove that part of the fork
and remove the compensating code.

Signed-off-by: Josh Bleecher Snyder <josharian@gmail.com>
2021-05-11 15:13:42 -07:00
Josh Bleecher Snyder
98cae48e70 wgengine/wglog: optimize wireguardGoString
The new code is ugly, but much faster and leaner.

name        old time/op    new time/op    delta
SetPeers-8    7.81µs ± 1%    3.59µs ± 1%  -54.04%  (p=0.000 n=9+10)

name        old alloc/op   new alloc/op   delta
SetPeers-8    7.68kB ± 0%    2.53kB ± 0%  -67.08%  (p=0.000 n=10+10)

name        old allocs/op  new allocs/op  delta
SetPeers-8       237 ± 0%        99 ± 0%  -58.23%  (p=0.000 n=10+10)

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-05-11 14:28:47 -07:00
Josh Bleecher Snyder
9356912053 wgengine/wglog: add BenchmarkSetPeer
Because it showed up on hello profiles.

Cycle through some moderate-sized sets of peers.
This should cover the "small tweaks to netmap"
and the "up/down cycle" cases.

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-05-11 14:28:47 -07:00
Brad Fitzpatrick
36a26e6a71 internal/deephash: rename from deepprint
Yes, it printed, but that was an implementation detail for hashing.

And coming optimization will make it print even less.

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-05-11 12:11:16 -07:00
Josh Bleecher Snyder
6ab2176dc7 internal/deepprint: improve benchmark
This more closely matches our real usage of deepprint.

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-05-11 12:03:54 -07:00
Josh Bleecher Snyder
712774a697 internal/deepprint: close struct curly parens
Not that it matters, but we were missing a close parens.
It's cheap, so add it.

name    old time/op    new time/op    delta
Hash-8    6.64µs ± 0%    6.67µs ± 1%  +0.42%  (p=0.008 n=9+10)

name    old alloc/op   new alloc/op   delta
Hash-8    1.54kB ± 0%    1.54kB ± 0%    ~     (all equal)

name    old allocs/op  new allocs/op  delta
Hash-8      37.0 ± 0%      37.0 ± 0%    ~     (all equal)

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-05-11 11:33:17 -07:00
Josh Bleecher Snyder
8368bac847 internal/deepprint: stop printing struct field names
The struct field names don't change within a single run,
so they are irrelevant. Use the field index instead.

name    old time/op    new time/op    delta
Hash-8    6.52µs ± 0%    6.64µs ± 0%   +1.91%  (p=0.000 n=6+9)

name    old alloc/op   new alloc/op   delta
Hash-8    1.67kB ± 0%    1.54kB ± 0%   -7.66%  (p=0.000 n=10+10)

name    old allocs/op  new allocs/op  delta
Hash-8      53.0 ± 0%      37.0 ± 0%  -30.19%  (p=0.000 n=10+10)

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-05-11 11:33:17 -07:00
Josh Bleecher Snyder
dfa0c90955 internal/deepprint: replace Fprintf(w, const) with w.WriteString
name    old time/op    new time/op    delta
Hash-8    7.77µs ± 0%    6.29µs ± 1%  -19.11%  (p=0.000 n=9+10)

name    old alloc/op   new alloc/op   delta
Hash-8    1.67kB ± 0%    1.67kB ± 0%     ~     (all equal)

name    old allocs/op  new allocs/op  delta
Hash-8      53.0 ± 0%      53.0 ± 0%     ~     (all equal)

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-05-11 11:33:17 -07:00
Josh Bleecher Snyder
d4f805339e internal/deepprint: special-case some common types
These show up a lot in our data structures.

name    old time/op    new time/op    delta
Hash-8    11.5µs ± 1%     7.8µs ± 1%  -32.17%  (p=0.000 n=10+10)

name    old alloc/op   new alloc/op   delta
Hash-8    1.98kB ± 0%    1.67kB ± 0%  -15.73%  (p=0.000 n=10+10)

name    old allocs/op  new allocs/op  delta
Hash-8      82.0 ± 0%      53.0 ± 0%  -35.37%  (p=0.000 n=10+10)

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-05-11 11:33:17 -07:00
Josh Bleecher Snyder
752f8c0f2f internal/deepprint: buffer writes
The sha256 hash writer doesn't implement WriteString.
(See https://github.com/golang/go/issues/38776.)
As a consequence, we end up converting many strings to []byte.

Wrapping a bufio.Writer around the hash writer lets us
avoid these conversions by using WriteString.

Using a bufio.Writer is, perhaps surprisingly, almost as cheap as using unsafe.
The reason is that the sha256 writer does internal buffering,
but doesn't do any when handed larger writers.
Using a bufio.Writer merely shifts the data copying from one buffer
to a different one.

Using a concrete type for Print and print cuts 10% off of the execution time.

name    old time/op    new time/op    delta
Hash-8    15.3µs ± 0%    11.5µs ± 0%  -24.84%  (p=0.000 n=10+10)

name    old alloc/op   new alloc/op   delta
Hash-8    2.82kB ± 0%    1.98kB ± 0%  -29.57%  (p=0.000 n=10+10)

name    old allocs/op  new allocs/op  delta
Hash-8       140 ± 0%        82 ± 0%  -41.43%  (p=0.000 n=10+10)

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-05-11 11:33:17 -07:00
Josh Bleecher Snyder
7891b34266 internal/deepprint: add BenchmarkHash
deepprint currently accounts for 15% of allocs in tailscaled.
This is a useful benchmark to have.

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-05-11 11:33:17 -07:00
Josh Bleecher Snyder
cb97062bac go.mod: bump inet.af/netaddr
For IPPort.MarshalText optimizations.

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-05-11 11:33:04 -07:00
Josh Bleecher Snyder
773fcfd007 Revert "wgengine/bench: skip flaky test"
This reverts commit d707e2f7e5.

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-05-11 11:28:30 -07:00
Josh Bleecher Snyder
68911f6778 wgengine/bench: ignore "engine closing" errors
On benchmark completion, we shut down the wgengine.
If we happen to poll for status during shutdown,
we get an "engine closing" error.
It doesn't hurt anything; ignore it.

Fixes tailscale/corp#1776

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-05-11 11:28:30 -07:00
Brad Fitzpatrick
d707e2f7e5 wgengine/bench: skip flaky test
Updates tailscale/corp#1776

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-05-11 11:10:21 -07:00
David Anderson
cfde997699 net/dns: don't use interfaces.Tailscale to find the tailscale interface index.
interfaces.Tailscale only returns an interface if it has at least one Tailscale
IP assigned to it. In the resolved DNS manager, when we're called upon to tear
down DNS config, the interface no longer has IPs.

Instead, look up the interface index on construction and reuse it throughout
the daemon lifecycle.

Fixes #1892.

Signed-off-by: David Anderson <dave@natulte.net>
2021-05-10 15:24:42 -07:00
Brad Fitzpatrick
d82b28ba73 go.mod: bump wireguard-go 2021-05-10 14:41:39 -07:00
Brad Fitzpatrick
366b3d3f62 ipn{,/ipnserver}: delay JSON marshaling of ipn.Notifies
If nobody is connected to the IPN bus, don't burn CPU & waste
allocations (causing more GC) by encoding netmaps for nobody.

This will notably help hello.ipn.dev.

Updates tailscale/corp#1773

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-05-10 14:36:27 -07:00
David Anderson
dc32b4695c util/dnsname: normalize leading dots in ToFQDN.
Fixes #1888.

Signed-off-by: David Anderson <dave@natulte.net>
2021-05-10 13:07:03 -07:00
Josh Bleecher Snyder
c0a70f3a06 go.mod: pull in wintun alignment fix from upstream wireguard-go
6cd106ab13...030c638da3

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-05-10 11:10:09 -07:00
Maisem Ali
7027fa06c3 wf: implement windows firewall using inet.af/wf.
Signed-off-by: Maisem Ali <maisem@tailscale.com>
2021-05-10 09:57:07 -07:00
Josh Bleecher Snyder
8d2a90529e wgengine/bench: hold lock in TrafficGen.GotPacket while calling first packet callback
Without any synchronization here, the "first packet" callback can
be delayed indefinitely, while other work continues.
Since the callback starts the benchmark timer, this could skew results.
Worse, if the benchmark manages to complete before the benchmark timer begins,
it'll cause a data race with the benchmark shutdown performed by package testing.
That is what is reported in #1881.

This is a bit unfortunate, in that it means that users of TrafficGen have
to be careful to keep this callback speedy and lightweight and to avoid deadlocks.

Fixes #1881

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-05-10 09:45:35 -07:00
Josh Bleecher Snyder
a72fb7ac0b wgengine/bench: handle multiple Engine status callbacks
It is possible to get multiple status callbacks from an Engine.
We need to wait for at least one from each Engine.
Without limiting to one per Engine,
wait.Wait can exit early or can panic due to a negative counter.

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-05-10 09:45:35 -07:00
Josh Bleecher Snyder
6618e82ba2 wgengine/bench: close Engines on benchmark completion
This reduces the speed with which these benchmarks exhaust their supply fds.
Not to zero unfortunately, but it's still helpful when doing long runs.

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-05-10 09:45:35 -07:00
Josh Bleecher Snyder
e9066ee625 types/wgkey: optimize Key.ShortString
name           old time/op    new time/op    delta
ShortString-8    82.6ns ± 0%    15.6ns ± 0%  -81.07%  (p=0.008 n=5+5)

name           old alloc/op   new alloc/op   delta
ShortString-8      104B ± 0%        8B ± 0%  -92.31%  (p=0.008 n=5+5)

name           old allocs/op  new allocs/op  delta
ShortString-8      3.00 ± 0%      1.00 ± 0%  -66.67%  (p=0.008 n=5+5)

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-05-10 09:43:44 -07:00
Josh Bleecher Snyder
7cd4766d5e types/wgkey: add BenchmarkShortString
Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-05-10 09:43:44 -07:00
Brad Fitzpatrick
3173c5a65c net/interface: remove darwin fetchRoutingTable workaround
Fixed upstream. Bump dep.

Updates #1345

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-05-10 08:24:11 -07:00
Josh Bleecher Snyder
ceb568202b tailcfg: optimize keyMarshalText
This function accounted for ~1% of all allocs by tailscaled.
It is trivial to improve, so may as well.

name              old time/op    new time/op    delta
KeyMarshalText-8     197ns ± 0%      47ns ± 0%  -76.12%  (p=0.016 n=4+5)

name              old alloc/op   new alloc/op   delta
KeyMarshalText-8      200B ± 0%       80B ± 0%  -60.00%  (p=0.008 n=5+5)

name              old allocs/op  new allocs/op  delta
KeyMarshalText-8      5.00 ± 0%      1.00 ± 0%  -80.00%  (p=0.008 n=5+5)

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-05-07 18:50:10 -07:00
Brad Fitzpatrick
5190435d6e cmd/tailscale: rewrite the "up" checker, fix bugs
The old way was way too fragile and had felt like it had more special
cases than normal cases. (see #1874, #1860, #1834, etc) It became very
obvious the old algorithm didn't work when we made the output be
pretty and try to show the user the command they need to run in
5ecc7c7200 for #1746)

The new algorithm is to map the prefs (current and new) back to flags
and then compare flags. This nicely handles the OS-specific flags and
the n:1 and 1:n flag:pref cases.

No change in the existing already-massive test suite, except some ordering
differences (the missing items are now sorted), but some new tests are
added for behavior that was broken before. In particular, it now:

* preserves non-pref boolean flags set to false, and preserves exit
  node IPs (mapping them back from the ExitNodeID pref, as well as
  ExitNodeIP),

* doesn't ignore --advertise-exit-node when doing an EditPrefs call
  (#1880)

* doesn't lose the --operator on the non-EditPrefs paths (e.g. with
  --force-reauth, or when the backend was not in state Running).

Fixes #1880

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-05-07 09:31:55 -07:00
Brad Fitzpatrick
e72ed3fcc2 ipn/{ipnlocal,ipnstate}: add PeerStatus.ID stable ID to status --json output
Needed for the "up checker" to map back from exit node stable IDs (the
ipn.Prefs.ExitNodeID) back to an IP address in error messages.

But also previously requested so people can use it to then make API
calls. The upcoming "tailscale admin" subcommand will probably need it
too.

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-05-07 09:31:55 -07:00
David Anderson
3c8e230ee1 Revert "net/dns: set IPv4 auto mode in NM, so it lets us set DNS."
This reverts commit 7d16c8228b.

I have no idea how I ended up here. The bug I was fixing with this change
fails to reproduce on Ubuntu 18.04 now, and this change definitely does
break 20.04, 20.10, and Debian Buster. So, until we can reliably reproduce
the problem this was meant to fix, reverting.

Part of #1875

Signed-off-by: David Anderson <dave@natulte.net>
2021-05-06 22:31:54 -07:00
David Anderson
a3b15bdf7e .github: remove verbose issue templates, add triage label.
Signed-off-by: David Anderson <danderson@tailscale.com>
2021-05-06 19:14:19 -07:00
David Anderson
5bd38b10b4 net/dns: log the correct error when NM Reapply fails.
Found while debugging #1870.

Signed-off-by: David Anderson <danderson@tailscale.com>
2021-05-06 16:02:09 -07:00
David Anderson
7d16c8228b net/dns: set IPv4 auto mode in NM, so it lets us set DNS.
Part of #1870.

Signed-off-by: David Anderson <danderson@tailscale.com>
2021-05-06 16:02:09 -07:00
David Anderson
77e2375501 net/dns: don't try to configure LLMNR or mdns in NetworkManager.
Fixes #1870.

Signed-off-by: David Anderson <danderson@tailscale.com>
2021-05-06 16:02:09 -07:00
Brad Fitzpatrick
e78e26b6fb cmd/tailscale: fix another up warning with exit nodes
The --advertise-routes and --advertise-exit-node flags both mutating
one pref is the gift that keeps on giving.

I need to rewrite the this up warning code to first map prefs back to
flag values and then just compare flags instead of comparing prefs,
but this is the minimal fix for now.

This also includes work on the tests, to make them easier to write
(and more accurate), by letting you write the flag args directly and
have that parse into the upArgs/MaskedPrefs directly, the same as the
code, rather than them being possibly out of sync being written by
hand.

Fixes https://twitter.com/EXPbits/status/1390418145047887877

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-05-06 15:50:58 -07:00
Josh Bleecher Snyder
ddd85b9d91 wgengine/magicsock: rename discoEndpoint.wgEndpointHostPort to wgEndpoint
Fields rename only.

Part of the general effort to make our code agnostic about endpoint formatting.
It's just a name, but it will soon be a misleading one; be more generic.
Do this as a separate commit because it generates a lot of whitespace changes.

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-05-06 12:44:22 -07:00
Josh Bleecher Snyder
e0bd3cc70c wgengine/magicsock: use netaddr.MustParseIPPrefix
Delete our bespoke helper.

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-05-06 12:44:22 -07:00
Josh Bleecher Snyder
bc68e22c5b all: s/CreateEndpoint/ParseEndpoint/ in docs
Upstream wireguard-go renamed the interface method
from CreateEndpoint to ParseEndpoint.
I missed some comments. Fix them.

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-05-06 12:44:22 -07:00
Josh Bleecher Snyder
9bce1b7fc1 wgengine/wgcfg: make device test endpoint-format-agnostic
By using conn.NewDefaultBind, this test requires that our endpoints
be comprehensible to wireguard-go. Instead, use a no-op bind that
treats endpoints as opaque strings.

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-05-06 12:44:22 -07:00
Josh Bleecher Snyder
73ad1f804b wgengine/wgcfg: use autogenerated Clone methods
Delete the manually written ones named Copy.

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-05-06 12:44:22 -07:00
Josh Bleecher Snyder
05bed64772 types/wgkey: simplify Key.UnmarshalJSON
Instead of calling ParseHex, do the hex.Decode directly.

name             old time/op    new time/op    delta
UnmarshalJSON-8    86.9ns ± 0%    42.6ns ± 0%   -50.94%  (p=0.000 n=15+14)

name             old alloc/op   new alloc/op   delta
UnmarshalJSON-8      128B ± 0%        0B       -100.00%  (p=0.000 n=15+15)

name             old allocs/op  new allocs/op  delta
UnmarshalJSON-8      2.00 ± 0%      0.00       -100.00%  (p=0.000 n=15+15)

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-05-06 12:44:22 -07:00
Josh Bleecher Snyder
a0dacba877 wgengine/magicsock: simplify legacy endpoint DstToString
Legacy endpoints (addrSet) currently reconstruct their dst string when requested.

Instead, store the dst string we were given to begin with.
In addition to being simpler and cheaper, this makes less code
aware of how to interpret endpoint strings.

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-05-06 12:44:22 -07:00
Josh Bleecher Snyder
777c816b34 wgengine/wgcfg: return better errors from DeviceConfig, ReconfigDevice
Prefer the error from the actual wireguard-go device method call,
not {To,From}UAPI, as those tend to be less interesting I/O errors.

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-05-06 12:44:22 -07:00
Josh Bleecher Snyder
1f6c4ba7c3 wgengine/wgcfg: prevent ReconfigDevice from hanging on error
When wireguard-go's UAPI interface fails with an error, ReconfigDevice hangs.
Fix that by buffering the channel and closing the writer after the call.
The code now matches the corresponding code in DeviceConfig, where I got it right.

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-05-06 12:44:22 -07:00
Josh Bleecher Snyder
462f7e38fc tailcfg: fix typo in comment
Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-05-06 12:44:22 -07:00
Josh Bleecher Snyder
ed63a041bf wgengine/userspace: delete HandshakeDone
It is unused, and has been since early Feb 2021 (Tailscale 1.6).
We can't get delete the DeviceOptions entirely yet;
first #1831 and #1839 need to go in, along with some wireguard-go changes.
Deleting this chunk of code now will make the later commits more clearly correct.

Pingers can now go too.

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-05-06 11:20:46 -07:00
Brad Fitzpatrick
4b14f72f1f VERSION.txt: the 1.9.x dev cycle hath begun 2021-05-06 10:35:05 -07:00
Brad Fitzpatrick
b8fb8264a5 wgengine/netstack: avoid delivering incoming packets to both netstack + host
The earlier eb06ec172f fixed
the flaky SSH issue (tailscale/corp#1725) by making sure that packets
addressed to Tailscale IPs in hybrid netstack mode weren't delivered
to netstack, but another issue remained:

All traffic handled by netstack was also potentially being handled by
the host networking stack, as the filter hook returned "Accept", which
made it keep processing. This could lead to various random racey chaos
as a function of OS/firewalls/routes/etc.

Instead, once we inject into netstack, stop our caller's packet
processing.

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-05-06 06:43:16 -07:00
Brad Fitzpatrick
7f2eb1d87a net/tstun: fix TUN log spam when ACLs drop a packet
Whenever we dropped a packet due to ACLs, wireguard-go was logging:

Failed to write packet to TUN device: packet dropped by filter

Instead, just lie to wireguard-go and pretend everything is okay.

Fixes #1229

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-05-06 06:42:58 -07:00
Brad Fitzpatrick
2585edfaeb cmd/tailscale: fix tailscale up --advertise-exit-node validation
Fixes #1859

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-05-05 20:50:47 -07:00
Brad Fitzpatrick
1a1123d461 wgengine: fix pendopen debug to not track SYN+ACKs, show Node.Online state
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-05-05 15:25:11 -07:00
Brad Fitzpatrick
b2de34a45d version: bump date
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-05-05 14:49:20 -07:00
Brad Fitzpatrick
eb06ec172f wgengine/netstack: don't pass non-subnet traffic to netstack in hybrid mode
Fixes tailscale/corp#1725

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-05-05 13:38:55 -07:00
Brad Fitzpatrick
7629cd6120 net/tsaddr: add NewContainsIPFunc (move from wgengine)
I want to use this from netstack but it's not exported.

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-05-05 13:15:50 -07:00
Josh Bleecher Snyder
78d4c561b5 types/logger: add key grinder stats lines to rate-limiting exemption list
Updates #1749

Co-authored-by: Brad Fitzpatrick <bradfitz@tailscale.com>
Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-05-05 08:25:15 -07:00
Josh Bleecher Snyder
f116a4c44f types/logger: fix rate limiter allowlist
Upstream wireguard-go renamed the interface method
from CreateEndpoint to ParseEndpoint.
I updated the log call site but not the allowlist.

Signed-off-by: Josh Bleecher Snyder <josharian@gmail.com>
2021-05-04 21:59:05 -07:00
Josh Bleecher Snyder
be56aa4962 workflows: execute benchmarks
#1817 removed the only place in our CI where we executed our benchmark code.
Fix that by executing it everywhere.

The benchmarks are generally cheap and fast, 
so this should add minimal overhead.

Signed-off-by: Josh Bleecher Snyder <josharian@gmail.com>
2021-05-04 20:21:03 -07:00
Brad Fitzpatrick
52e1031428 cmd/tailscale: gofmt
From 6d10655dc3

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-05-04 13:04:33 -07:00
Josh Bleecher Snyder
ac75958d2e workflows: run staticcheck on more platforms
To prevent issues like #1786, run staticcheck on the primary GOOSes:
linux, mac, and windows.

Windows also has a fair amount of GOARCH-specific code.
If we ever have GOARCH staticcheck failures on other GOOSes,
we can expand the test matrix further.

This requires installing the staticcheck binary so that
we can execute it with different GOOSes.

Signed-off-by: Josh Bleecher Snyder <josharian@gmail.com>
2021-05-04 12:50:13 -07:00
Avery Pennarun
6d10655dc3 ipnlocal: accept a new opts.UpdatePrefs field.
This is needed because the original opts.Prefs field was at some point
subverted for use in frontend->backend state migration for backward
compatibility on some platforms. We still need that feature, but we
also need the feature of providing the full set of prefs from
`tailscale up`, *not* including overwriting the prefs.Persist keys, so
we can't use the original field from `tailscale up`.

`tailscale up` had attempted to compensate for that by doing SetPrefs()
before Start(), but that violates the ipn.Backend contract, which says
you should call Start() before anything else (that's why it's called
Start()). As a result, doing SetPrefs({ControlURL=...,
WantRunning=true}) would cause a connection to the *previous* control
server (because WantRunning=true), and then connect to the *new*
control server only after running Start().

This problem may have been avoided before, but only by pure luck.

It turned out to be relatively harmless since the connection to the old
control server was immediately closed and replaced anyway, but it
created a race condition that could have caused spurious notifications
or rejected keys if the server responded quickly.

As already covered by existing TODOs, a better fix would be to have
Start() get out of the business of state migration altogether. But
we're approaching a release so I want to make the minimum possible fix.

Fixes #1840.

Signed-off-by: Avery Pennarun <apenwarr@tailscale.com>
2021-05-04 15:19:25 -04:00
Josh Bleecher Snyder
7dbbe0c7c7 cmd/tailscale/cli: fix running from Xcode
We were over-eager in running tailscale in GUI mode.
f42ded7acf fixed that by
checking for a variety of shell-ish env vars and using those
to force us into CLI mode.

However, for reasons I don't understand, those shell env vars
are present when Xcode runs Tailscale.app on my machine.
(I've changed no configs, modified nothing on a brand new machine.)
Work around that by adding an additional "only in GUI mode" check.

Signed-off-by: Josh Bleecher Snyder <josharian@gmail.com>
2021-05-04 11:37:02 -07:00
Brad Fitzpatrick
4066c606df ipn/ipnlocal: update peerapi logging of received PUTs
Clarify direction and add duration.

(per chat with Avery)

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-05-04 11:09:02 -07:00
Josh Bleecher Snyder
d3ba860ffd syncs: stop running TestWatchMultipleValues on CI
It's flaky, and not just on Windows.

Signed-off-by: Josh Bleecher Snyder <josharian@gmail.com>
2021-05-04 10:21:21 -07:00
Brad Fitzpatrick
f5bccc0746 ipn/ipnlocal: redact more errors
Updates tailscale/corp#1636

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-05-04 09:58:09 -07:00
Josh Bleecher Snyder
47ebd1e9a2 wgengine/router: use net.IP.Equal instead of bytes.Equal to compare IPs
Signed-off-by: Josh Bleecher Snyder <josharian@gmail.com>
2021-05-04 08:54:50 -07:00
Josh Bleecher Snyder
737151ea4a safesocket: delete unused function
Signed-off-by: Josh Bleecher Snyder <josharian@gmail.com>
2021-05-04 08:54:50 -07:00
Josh Bleecher Snyder
f91c2dfaca wgengine/router: remove unused field
Signed-off-by: Josh Bleecher Snyder <josharian@gmail.com>
2021-05-04 08:54:50 -07:00
Josh Bleecher Snyder
bfd2b71926 portlist: suppress staticcheck error
Signed-off-by: Josh Bleecher Snyder <josharian@gmail.com>
2021-05-04 08:54:50 -07:00
Josh Bleecher Snyder
42c8b9ad53 net/tstun: remove unnecessary break statement
Signed-off-by: Josh Bleecher Snyder <josharian@gmail.com>
2021-05-04 08:54:50 -07:00
Josh Bleecher Snyder
61e411344f logtail/filch: add staticcheck annotation
To work around a staticcheck bug when running with GOOS=windows.

Signed-off-by: Josh Bleecher Snyder <josharian@gmail.com>
2021-05-04 08:54:50 -07:00
Josh Bleecher Snyder
9360f36ebd all: use lower-case letters at the start of error message
Signed-off-by: Josh Bleecher Snyder <josharian@gmail.com>
2021-05-04 08:54:50 -07:00
Brad Fitzpatrick
962bf74875 cmd/tailscale: fail if tailscaled closes the IPN connection
I was going to write a test for this using the tstest/integration test
stuff, but the testcontrol implementation isn't quite there yet (it
always registers nodes and doesn't provide AuthURLs). So, manually
tested for now.

Fixes #1843

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-05-04 07:51:23 -07:00
Brad Fitzpatrick
68fb51b833 tstest/integration: misc cleanups
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-05-03 14:22:18 -07:00
Brad Fitzpatrick
3237e140c4 tstest/integration: add testNode.AwaitListening, DERP+STUN, improve proxy trap
Updates #1840
2021-05-03 12:14:20 -07:00
David Crawshaw
1f48d3556f cmd/tailscale/cli: don't report outdated auth URL to web UI
This brings the web 'up' logic into line with 'tailscale up'.

Signed-off-by: David Crawshaw <crawshaw@tailscale.com>
2021-05-03 11:18:58 -07:00
David Crawshaw
1336ed8d9e cmd/tailscale/cli: skip new tab on web login
It doesn't work properly.

Signed-off-by: David Crawshaw <crawshaw@tailscale.com>
2021-05-03 11:18:58 -07:00
David Crawshaw
85beaa52b3 paths: add synology socket path
Signed-off-by: David Crawshaw <crawshaw@tailscale.com>
2021-05-03 11:18:58 -07:00
Josh Bleecher Snyder
64047815b0 wgenengine/magicsock: delete cursed tests
Signed-off-by: Josh Bleecher Snyder <josharian@gmail.com>
2021-05-03 11:09:44 -07:00
Brad Fitzpatrick
ca65c6cbdb cmd/tailscale: make 'file cp' have better error messages on bad targets
Say when target isn't owned by current user, and when target doesn't
exist in netmap.

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-05-03 10:33:55 -07:00
Josh Bleecher Snyder
96ef8d34ef ipn/ipnlocal: switch from testify to quicktest
Per discussion, we want to have only one test assertion library,
and we want to start by exploring quicktest.

This was a mostly mechanical translation.
I think we could make this nicer by defining a few helper
closures at the beginning of the test. Later.

Signed-off-by: Josh Bleecher Snyder <josharian@gmail.com>
2021-05-03 10:09:13 -07:00
Brad Fitzpatrick
90002be6c0 cmd/tailscale: make pref-revert checks ignore OS-irrelevant prefs
This fixes #1833 in two ways:

* stop setting NoSNAT on non-Linux. It only matters on Linux and the flag
  is hidden on non-Linux, but the code was still setting it. Because of
  that, the new pref-reverting safety checks were failing when it was
  changing.

* Ignore the two Linux-only prefs changing on non-Linux.

Fixes #1833

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-05-03 09:37:50 -07:00
Brad Fitzpatrick
fb67d8311c cmd/tailscale: pull out, parameterize up FlagSet creation for tests
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-05-03 09:23:55 -07:00
Brad Fitzpatrick
98d7c28faa tstest/integration: start factoring test types out to clean things up
To enable easy multi-node testing (including inter-node traffic) later.

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-30 20:27:05 -07:00
Brad Fitzpatrick
f6e3240dee cmd/tailscale/cli: add test to catch ipn.Pref additions 2021-04-30 13:29:06 -07:00
Avery Pennarun
6caa02428e cmd/tailscale/cli/up: "LoggedOut" pref is implicit.
There's no need to warn that it was not provided on the command line
after doing a sequence of up; logout; up --args. If you're asking for
tailscale to be up, you always mean that you prefer LoggedOut to become
false.

Fixes #1828

Signed-off-by: Avery Pennarun <apenwarr@tailscale.com>
2021-04-30 16:15:04 -04:00
Josh Bleecher Snyder
59026a291d wgengine/wglog: improve wireguard-go logging rate limiting
Prior to wireguard-go using printf-style logging,
all wireguard-go logging occurred using format string "%s".
We fixed that but continued to use %s when we rewrote
peer identifiers into Tailscale style.

This commit removes that %sl, which makes rate limiting work correctly.
As a happy side-benefit, it should generate less garbage.

Instead of replacing all wireguard-go peer identifiers
that might occur anywhere in a fully formatted log string,
assume that they only come from args.
Check all args for things that look like *device.Peers
and replace them with appropriately reformatted strings.

There is a variety of ways that this could go wrong
(unusual format verbs or modifiers, peer identifiers
occurring as part of a larger printed object, future API changes),
but none of them occur now, are likely to be added,
or would be hard to work around if they did.

Signed-off-by: Josh Bleecher Snyder <josharian@gmail.com>
2021-04-30 09:45:10 -07:00
Josh Bleecher Snyder
1f94d43b50 wgengine/wglog: delay formatting
The "stop phrases" we use all occur in wireguard-go in the format string.
We can avoid doing a bunch of fmt.Sprintf work when they appear.

Signed-off-by: Josh Bleecher Snyder <josharian@gmail.com>
2021-04-30 09:45:10 -07:00
Brad Fitzpatrick
544d8d0ab8 ipn/ipnlocal: remove NewLocalBackendWithClientGen
This removes the NewLocalBackendWithClientGen constructor added in
b4d04a065f and instead adds
LocalBackend.SetControlClientGetterForTesting, mirroring
LocalBackend.SetHTTPTestClient. NewLocalBackendWithClientGen was
weird in being exported but taking an unexported type. This was noted
during code review:

https://github.com/tailscale/tailscale/pull/1818#discussion_r623155669

which ended in:

"I'll leave it for y'all to clean up if you find some way to do it elegantly."

This is more idiomatic.

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-30 07:36:53 -07:00
Avery Pennarun
0181a4d0ac ipnlocal: don't pause the controlclient until we get at least one netmap.
Without this, macOS would fail to display its menu state correctly if you
started it while !WantRunning. It relies on the netmap in order to show
the logged-in username.

Signed-off-by: Avery Pennarun <apenwarr@tailscale.com>
2021-04-30 09:18:13 -04:00
Avery Pennarun
4ef207833b ipn: !WantRunning + !LoggedOut should not be idle on startup.
There was logic that would make a "down" tailscale backend (ie.
!WantRunning) refuse to do any network activity. Unfortunately, this
makes the macOS and iOS UI unable to render correctly if they start
while !WantRunning.

Now that we have Prefs.LoggedOut, use that instead. So `tailscale down`
will still allow the controlclient to connect its authroutine, but
pause the maproutine. `tailscale logout` will entirely stop all
activity.

This new behaviour is not obviously correct; it's a bit annoying that
`tailsale down` doesn't terminate all activity like you might expect.
Maybe we should redesign the UI code to render differently when
disconnected, and then revert this change.

Signed-off-by: Avery Pennarun <apenwarr@tailscale.com>
2021-04-30 09:18:13 -04:00
Avery Pennarun
4f3315f3da ipnlocal: setting WantRunning with EditPrefs was special.
EditPrefs should be just a wrapper around the action of changing prefs,
but someone had added a side effect of calling Login() sometimes. The
side effect happened *after* running the state machine, which would
sometimes result in us going into NeedsLogin immediately before calling
cc.Login().

This manifested as the macOS app not being able to Connect if you
launched it with LoggedOut=false and WantRunning=false. Trying to
Connect() would sent us to the NeedsLogin state instead.

Signed-off-by: Avery Pennarun <apenwarr@tailscale.com>
2021-04-30 09:18:13 -04:00
Avery Pennarun
2a4d1cf9e2 Add prefs.LoggedOut to fix several state machine bugs.
Fixes: tailscale/corp#1660

Signed-off-by: Avery Pennarun <apenwarr@tailscale.com>
2021-04-30 09:18:13 -04:00
Avery Pennarun
b0382ca167 ipn/ipnlocal: some state_test cleanups.
This doesn't change the actual functionality. Just some additional
comments and fine tuning.

Signed-off-by: Avery Pennarun <apenwarr@tailscale.com>
2021-04-30 09:18:12 -04:00
Avery Pennarun
ac9cd48c80 ipnlocal: fix deadlock when calling Shutdown() from Start().
Signed-off-by: Avery Pennarun <apenwarr@tailscale.com>
2021-04-30 09:17:47 -04:00
Avery Pennarun
ecdba913d0 Revert "ipn/ipnlocal: be authoritative for the entire MagicDNS record tree."
Unfortunately this broke MagicDNS almost entirely.

Updates: tailscale/corp#1706

This reverts commit 1d7e7b49eb.
2021-04-30 06:16:58 -04:00
Brad Fitzpatrick
5e9e11a77d tstest/integration/testcontrol: add start of test control server
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-29 22:51:22 -07:00
Avery Pennarun
19c3e6cc9e types/logger: rate limited: more hysteresis, better messages.
- Switch to our own simpler token bucket, since x/time/rate is missing
  necessary stuff (can't provide your own time func; can't check the
  current bucket contents) and it's overkill anyway.

- Add tests that actually include advancing time.

- Don't remove the rate limit on a message until there's enough room to
  print at least two more of them. When we do, we'll also print how
  many we dropped, as a contextual reminder that some were previously
  lost. (This is more like how the Linux kernel does it.)

- Reformat the [RATE LIMITED] messages to be shorter, and to not
  corrupt original message. Instead, we print the message, then print
  its format string.

- Use %q instead of \"%s\", for more accurate parsing later, if the
  format string contained quotes.

Fixes #1772

Signed-off-by: Avery Pennarun <apenwarr@tailscale.com>
2021-04-30 01:01:15 -04:00
Josh Bleecher Snyder
20e04418ff net/dns: add GOOS build tags
Fixes #1786

Signed-off-by: Josh Bleecher Snyder <josharian@gmail.com>
2021-04-29 21:34:55 -07:00
Avery Pennarun
b7e31ab1a4 ipn: mock controlclient.Client; big ipn.Backend state machine test.
A very long unit test that verifies the way the controlclient and
ipn.Backend interact.

This is a giant sequential test of the state machine. The test passes,
but only because it's asserting all the wrong behaviour. I marked all
the behaviour I think is wrong with BUG comments, and several
additional test opportunities with TODO.

Note: the new test supercedes TestStartsInNeedsLoginState, which was
checking for incorrect behaviour (although the new test still checks
for the same incorrect behaviour) and assumed .Start() would converge
before returning, which it happens to do, but only for this very
specific case, for the current implementation. You're supposed to wait
for the notifications.

Updates: tailscale/corp#1660

Signed-off-by: Avery Pennarun <apenwarr@tailscale.com>
2021-04-30 00:09:35 -04:00
Avery Pennarun
b4d04a065f controlclient: extract a Client interface and rename Client->Auto.
This will let us create a mock or fake Client implementation for use
with ipn.Backend.

Signed-off-by: Avery Pennarun <apenwarr@tailscale.com>
2021-04-30 00:09:35 -04:00
Avery Pennarun
cc3119e27e controlclient: extract State and Status stuff into its own file.
No changes other than moving stuff around.

Signed-off-by: Avery Pennarun <apenwarr@tailscale.com>
2021-04-29 23:18:25 -04:00
Brad Fitzpatrick
a07a504b16 tstest/integration: use go binary from runtime.GOROOT
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-29 17:04:29 -07:00
David Anderson
bf5fc8edda go.mod: update wireguard-go.
Signed-off-by: David Anderson <danderson@tailscale.com>
2021-04-29 16:36:55 -07:00
David Anderson
1d7e7b49eb ipn/ipnlocal: be authoritative for the entire MagicDNS record tree.
With this change, shared node names resolve correctly on split DNS-supporting
operating systems.

Fixes tailscale/corp#1706

Signed-off-by: David Anderson <danderson@tailscale.com>
2021-04-29 16:06:01 -07:00
Brad Fitzpatrick
f342d10dc5 tstest/integration: set an HTTP_PROXY to catch bogus requests
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-29 16:00:02 -07:00
Brad Fitzpatrick
80429b97e5 testing: add start of an integration test
Only minimal tailscale + tailscaled for now.

And a super minimal in-memory logcatcher.

No control ... yet.

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-29 15:32:27 -07:00
Brad Fitzpatrick
08782b92f7 tstest: add WaitFor helper
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-29 14:43:46 -07:00
Josh Bleecher Snyder
4037fc25c5 types/wgkey: use value receiver with MarshalJSON
Pointer receivers used with MarshalJSON are code rakes.

https://github.com/golang/go/issues/22967
https://github.com/dominikh/go-tools/issues/911

I just stepped on one, and it hurt. Turn it over.
While we're here, optimize the code a bit.

name           old time/op    new time/op    delta
MarshalJSON-8     184ns ± 0%      44ns ± 0%  -76.03%  (p=0.000 n=20+19)

name           old alloc/op   new alloc/op   delta
MarshalJSON-8      184B ± 0%       80B ± 0%  -56.52%  (p=0.000 n=20+20)

name           old allocs/op  new allocs/op  delta
MarshalJSON-8      4.00 ± 0%      1.00 ± 0%  -75.00%  (p=0.000 n=20+20)

Signed-off-by: Josh Bleecher Snyder <josharian@gmail.com>
2021-04-29 14:14:34 -07:00
Josh Bleecher Snyder
7ee891f5fd all: delete wgcfg.Key and wgcfg.PrivateKey
For historical reasons, we ended up with two near-duplicate
copies of curve25519 key types, one in the wireguard-go module
(wgcfg) and one in the tailscale module (types/wgkey).
Then we moved wgcfg to the tailscale module.
We can now remove the wgcfg key type in favor of wgkey.

Signed-off-by: Josh Bleecher Snyder <josharian@gmail.com>
2021-04-29 14:14:34 -07:00
David Anderson
bf9ef1ca27 net/dns: stop NetworkManager breaking v6 connectivity when setting DNS.
Tentative fix for #1699

Signed-off-by: David Anderson <danderson@tailscale.com>
2021-04-29 12:25:47 -07:00
David Anderson
72b6d98298 net/interfaces: return all Tailscale addresses from Tailscale().
Signed-off-by: David Anderson <danderson@tailscale.com>
2021-04-29 12:25:47 -07:00
Brad Fitzpatrick
b7a497a30b ipn/ipnlocal: make FileTargets check IPN state first
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-29 10:26:53 -07:00
Denton Gentry
b9f8dc7867 workflows: remove coverage
This workflow has been disabled for some time.
It can come back later, when appropriate.

Signed-off-by: Denton Gentry <dgentry@tailscale.com>
2021-04-28 17:04:30 -07:00
Brad Fitzpatrick
0c5c16327d version: add IsMacSysExt func
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-28 14:57:04 -07:00
Josh Bleecher Snyder
ae36b57b71 go.mod: upgrade wireguard-go
This should be the last bump before 1.8.

Signed-off-by: Josh Bleecher Snyder <josharian@gmail.com>
2021-04-28 11:25:52 -07:00
Josh Bleecher Snyder
9d542e08e2 wgengine/magicsock: always run ReceiveIPv6
One of the consequences of the 	bind refactoring in 6f23087175
is that attempting to bind an IPv6 socket will always
result in c.pconn6.pconn being non-nil.
If the bind fails, it'll be set to a placeholder packet conn
that blocks forever.

As a result, we can always run ReceiveIPv6 and health check it.
This removes IPv4/IPv6 asymmetry and also will allow health checks
to detect any IPv6 receive func failures.

Signed-off-by: Josh Bleecher Snyder <josharian@gmail.com>
2021-04-28 11:07:14 -07:00
Josh Bleecher Snyder
fe50ded95c health: track whether we have a functional udp4 bind
Suggested-by: Brad Fitzpatrick <bradfitz@tailscale.com>
Signed-off-by: Josh Bleecher Snyder <josharian@gmail.com>
2021-04-28 11:07:14 -07:00
Josh Bleecher Snyder
7dc7078d96 wgengine/magicsock: use netaddr.IP in listenPacket
It must be an IP address; enforce that at the type level.

Suggested-by: Brad Fitzpatrick <bradfitz@tailscale.com>
Signed-off-by: Josh Bleecher Snyder <josharian@gmail.com>
2021-04-28 11:07:14 -07:00
Brad Fitzpatrick
4bf6939ee0 ipn/ipnlocal: remove t.Parallel from recently added test
The test modifies a global; it shouldn't be parallel.

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-28 11:02:56 -07:00
Josh Bleecher Snyder
3c543c103a wgengine/magicsock: unify initial bind and rebind
We had two separate code paths for the initial UDP listener bind
and any subsequent rebinds.

IPv6 got left out of the rebind code.
Rather than duplicate it there, unify the two code paths.
Then improve the resulting code:

* Rebind had nested listen attempts to try the user-specified port first,
  and then fall back to :0 if that failed. Convert that into a loop.
* Initial bind tried only the user-specified port.
  Rebind tried the user-specified port and 0.
  But there are actually three ports of interest:
  The one the user specified, the most recent port in use, and 0.
  We now try all three in order, as appropriate.
* In the extremely rare case in which binding to port 0 fails,
  use a dummy net.PacketConn whose reads block until close.
  This will keep the wireguard-go receive func goroutine alive.

As a pleasant side-effect of this, if we decide that
we need to resuscitate #1796, it will now be much easier.

Fixes #1799

Co-authored-by: David Anderson <danderson@tailscale.com>
Signed-off-by: Josh Bleecher Snyder <josharian@gmail.com>
2021-04-28 10:39:28 -07:00
Josh Bleecher Snyder
8fb66e20a4 wgengine/magicsock: remove DefaultPort const
Assume it'll stay at 0 forever, so hard-code it
and delete code conditional on it being non-0.

Signed-off-by: Josh Bleecher Snyder <josharian@gmail.com>
2021-04-28 10:39:28 -07:00
Josh Bleecher Snyder
a8f61969b9 wgengine/magicsock: remove context arg from listenPacket
It was set to context.Background by all callers, for the same reasons.
Set it locally instead, to simplify call sites.

Signed-off-by: Josh Bleecher Snyder <josharian@gmail.com>
2021-04-28 10:39:28 -07:00
Brad Fitzpatrick
a48c8991f1 ipn/ipnlocal: add a test for earlier lazy machine key generation change
Updates #1573

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-28 08:52:04 -07:00
Brad Fitzpatrick
1e6d512bf0 cmd/tailscale: improve file cp error message in macOS GUI version
Fixes tailscale/corp#1684

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-28 08:35:55 -07:00
Brad Fitzpatrick
4512aad889 version: add IsSandboxedMacOS func
For when we need to tweak behavior or errors as a function of which of
3 macOS Tailscale variants we're using. (more accessors coming later
as needed)

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-28 08:34:19 -07:00
Brad Fitzpatrick
8efc7834f2 go.mod: bump wireguard-go
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-28 08:04:26 -07:00
David Anderson
306a094d4b ipn/ipnlocal: remove IPv6 records from MagicDNS.
Fixes #1813.

Signed-off-by: David Anderson <danderson@tailscale.com>
2021-04-28 01:01:56 -07:00
Brad Fitzpatrick
2840afabba version: bump date
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-27 19:10:25 -07:00
David Anderson
44c2b7dc79 net/dns: on windows, skip site-local v6 resolvers.
Further refinement for tailscale/corp#1662.

Signed-off-by: David Anderson <danderson@tailscale.com>
2021-04-27 18:24:54 -07:00
Brad Fitzpatrick
8554694616 cmd/tailscale: add 'tailscale file get' subcommand
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-27 15:28:50 -07:00
Brad Fitzpatrick
cafa037de0 cmd/tailscale/cli: rename 'tailscale push' to 'tailscale file cp'
And reverse order, require final colon, and support multiple files.

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-27 13:58:56 -07:00
Brad Fitzpatrick
bb2141e0cf wgengine: periodically poll engine status for logging side effect
Fixes tailscale/corp#1560

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-27 13:55:47 -07:00
Brad Fitzpatrick
3c9dea85e6 wgengine: update a log line from 'weird' to conventional 'unexpected'
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-27 09:59:25 -07:00
Brad Fitzpatrick
3bdc9e9cb2 ipn/ipnlocal: prevent a now-expected [unexpected] log message on Windows
Updates #1620

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-27 09:58:05 -07:00
Ross Zurowski
b062ac5e86 cmd/tailscale: fix typo in error message (#1807)
Signed-off-by: Ross Zurowski <ross@rosszurowski.com>
2021-04-27 10:16:08 -04:00
Brad Fitzpatrick
5ecc7c7200 cmd/tailscale: make the new 'up' errors prettier and more helpful
Fixes #1746

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-26 21:26:29 -07:00
Josh Bleecher Snyder
744de615f1 health, wgenegine: fix receive func health checks for the fourth time
The old implementation knew too much about how wireguard-go worked.
As a result, it missed genuine problems that occurred due to unrelated bugs.

This fourth attempt to fix the health checks takes a black box approach.
A receive func is healthy if one (or both) of these conditions holds:

* It is currently running and blocked.
* It has been executed recently.

The second condition is required because receive functions
are not continuously executing. wireguard-go calls them and then
processes their results before calling them again.

There is a theoretical false positive if wireguard-go go takes
longer than one minute to process the results of a receive func execution.
If that happens, we have other problems.

Updates #1790

Signed-off-by: Josh Bleecher Snyder <josharian@gmail.com>
2021-04-26 17:35:49 -07:00
Josh Bleecher Snyder
0d4c8cb2e1 health: delete ReceiveFunc health checks
They were not doing their job.
They need yet another conceptual re-think.
Start by clearing the decks.

Signed-off-by: Josh Bleecher Snyder <josharian@gmail.com>
2021-04-26 17:35:49 -07:00
Josh Bleecher Snyder
99705aa6b7 net/tstun: split TUN events channel into up/down and MTU
We had a long-standing bug in which our TUN events channel
was being received from simultaneously in two places.

The first is wireguard-go.

At wgengine/userspace.go:366, we pass e.tundev to wireguard-go,
which starts a goroutine (RoutineTUNEventReader)
that receives from that channel and uses events to adjust the MTU
and bring the device up/down.

At wgengine/userspace.go:374, we launch a goroutine that
receives from e.tundev, logs MTU changes, and triggers
state updates when up/down changes occur.

Events were getting delivered haphazardly between the two of them.

We don't really want wireguard-go to receive the up/down events;
we control the state of the device explicitly by calling device.Up.
And the userspace.go loop MTU logging duplicates logging that
wireguard-go does when it received MTU updates.

So this change splits the single TUN events channel into up/down
and other (aka MTU), and sends them to the parties that ought
to receive them.

I'm actually a bit surprised that this hasn't caused more visible trouble.
If a down event went to wireguard-go but the subsequent up event
went to userspace.go, we could end up with the wireguard-go device disappearing.

I believe that this may also (somewhat accidentally) be a fix for #1790.

Signed-off-by: Josh Bleecher Snyder <josharian@gmail.com>
2021-04-26 17:16:51 -07:00
David Anderson
97d2fa2f56 net/dns: work around WSL DNS implementation flaws.
Fixes tailscale/corp#1662

Signed-off-by: David Anderson <danderson@tailscale.com>
2021-04-26 16:54:50 -07:00
Brad Fitzpatrick
ffe6c8e335 cmd/tailscale/cli: don't do a simple up when in state NeedsLogin
Fixes #1780

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-26 11:38:56 -07:00
Brad Fitzpatrick
138921ae40 ipn/ipnlocal: always write files to partial files, even in buffered mode
The intention was always that files only get written to *.partial
files and renamed at the end once fully received, but somewhere in the
process that got lost in buffered mode and *.partial files were only
being used in direct receive mode. This fix prevents WaitingFiles
from returning files that are still being transferred.

Updates tailscale/corp#1626

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-26 11:34:05 -07:00
Brad Fitzpatrick
5e268e6153 ipn/ipnlocal: use delete marker files to work around Windows delete problems
If DeleteFile fails on Windows due to another process (anti-virus,
probably) having our file open, instead leave a marker file that the
file is logically deleted, and remove it from API calls and clean it
up lazily later.

Updates tailscale/corp#1626

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-26 10:59:25 -07:00
Avery Pennarun
a7fe1d7c46 wgengine/bench: improved rate selection.
The old decay-based one took a while to converge. This new one (based
very loosely on TCP BBR) seems to converge quickly on what seems to be
the best speed.

Signed-off-by: Avery Pennarun <apenwarr@tailscale.com>
2021-04-26 03:51:13 -04:00
Avery Pennarun
a92b9647c5 wgengine/bench: speed test for channels, sockets, and wireguard-go.
This tries to generate traffic at a rate that will saturate the
receiver, without overdoing it, even in the event of packet loss. It's
unrealistically more aggressive than TCP (which will back off quickly
in case of packet loss) but less silly than a blind test that just
generates packets as fast as it can (which can cause all the CPU to be
absorbed by the transmitter, giving an incorrect impression of how much
capacity the total system has).

Initial indications are that a syscall about every 10 packets (TCP bulk
delivery) is roughly the same speed as sending every packet through a
channel. A syscall per packet is about 5x-10x slower than that.

The whole tailscale wireguard-go + magicsock + packet filter
combination is about 4x slower again, which is better than I thought
we'd do, but probably has room for improvement.

Note that in "full" tailscale, there is also a tundev read/write for
every packet, effectively doubling the syscall overhead per packet.

Given these numbers, it seems like read/write syscalls are only 25-40%
of the total CPU time used in tailscale proper, so we do have
significant non-syscall optimization work to do too.

Sample output:

$ GOMAXPROCS=2 go test -bench . -benchtime 5s ./cmd/tailbench
goos: linux
goarch: amd64
pkg: tailscale.com/cmd/tailbench
cpu: Intel(R) Core(TM) i7-4785T CPU @ 2.20GHz
BenchmarkTrivialNoAlloc/32-2         	56340248	        93.85 ns/op	 340.98 MB/s	         0 %lost	       0 B/op	       0 allocs/op
BenchmarkTrivialNoAlloc/124-2        	57527490	        99.27 ns/op	1249.10 MB/s	         0 %lost	       0 B/op	       0 allocs/op
BenchmarkTrivialNoAlloc/1024-2       	52537773	       111.3 ns/op	9200.39 MB/s	         0 %lost	       0 B/op	       0 allocs/op
BenchmarkTrivial/32-2                	41878063	       135.6 ns/op	 236.04 MB/s	         0 %lost	       0 B/op	       0 allocs/op
BenchmarkTrivial/124-2               	41270439	       138.4 ns/op	 896.02 MB/s	         0 %lost	       0 B/op	       0 allocs/op
BenchmarkTrivial/1024-2              	36337252	       154.3 ns/op	6635.30 MB/s	         0 %lost	       0 B/op	       0 allocs/op
BenchmarkBlockingChannel/32-2           12171654	       494.3 ns/op	  64.74 MB/s	         0 %lost	    1791 B/op	       0 allocs/op
BenchmarkBlockingChannel/124-2          12149956	       507.8 ns/op	 244.17 MB/s	         0 %lost	    1792 B/op	       1 allocs/op
BenchmarkBlockingChannel/1024-2         11034754	       528.8 ns/op	1936.42 MB/s	         0 %lost	    1792 B/op	       1 allocs/op
BenchmarkNonlockingChannel/32-2          8960622	      2195 ns/op	  14.58 MB/s	         8.825 %lost	    1792 B/op	       1 allocs/op
BenchmarkNonlockingChannel/124-2         3014614	      2224 ns/op	  55.75 MB/s	        11.18 %lost	    1792 B/op	       1 allocs/op
BenchmarkNonlockingChannel/1024-2        3234915	      1688 ns/op	 606.53 MB/s	         3.765 %lost	    1792 B/op	       1 allocs/op
BenchmarkDoubleChannel/32-2          	 8457559	       764.1 ns/op	  41.88 MB/s	         5.945 %lost	    1792 B/op	       1 allocs/op
BenchmarkDoubleChannel/124-2         	 5497726	      1030 ns/op	 120.38 MB/s	        12.14 %lost	    1792 B/op	       1 allocs/op
BenchmarkDoubleChannel/1024-2        	 7985656	      1360 ns/op	 752.86 MB/s	        13.57 %lost	    1792 B/op	       1 allocs/op
BenchmarkUDP/32-2                    	 1652134	      3695 ns/op	   8.66 MB/s	         0 %lost	     176 B/op	       3 allocs/op
BenchmarkUDP/124-2                   	 1621024	      3765 ns/op	  32.94 MB/s	         0 %lost	     176 B/op	       3 allocs/op
BenchmarkUDP/1024-2                  	 1553750	      3825 ns/op	 267.72 MB/s	         0 %lost	     176 B/op	       3 allocs/op
BenchmarkTCP/32-2                    	11056336	       503.2 ns/op	  63.60 MB/s	         0 %lost	       0 B/op	       0 allocs/op
BenchmarkTCP/124-2                   	11074869	       533.7 ns/op	 232.32 MB/s	         0 %lost	       0 B/op	       0 allocs/op
BenchmarkTCP/1024-2                  	 8934968	       671.4 ns/op	1525.20 MB/s	         0 %lost	       0 B/op	       0 allocs/op
BenchmarkWireGuardTest/32-2          	 1403702	      4547 ns/op	   7.04 MB/s	        14.37 %lost	     467 B/op	       3 allocs/op
BenchmarkWireGuardTest/124-2         	  780645	      7927 ns/op	  15.64 MB/s	         1.537 %lost	     420 B/op	       3 allocs/op
BenchmarkWireGuardTest/1024-2        	  512671	     11791 ns/op	  86.85 MB/s	         0.5206 %lost	     411 B/op	       3 allocs/op
PASS
ok  	tailscale.com/wgengine/bench	195.724s

Updates #414.

Signed-off-by: Avery Pennarun <apenwarr@tailscale.com>
2021-04-26 03:51:13 -04:00
Maisem Ali
590792915a wgengine/router{win}: ignore broadcast routes added by Windows when removing routes.
Signed-off-by: Maisem Ali <maisem@tailscale.com>
2021-04-24 14:13:35 -07:00
David Anderson
f6b7d08aea net/dns: work around new NetworkManager in other selection paths.
Further bits of #1788

Signed-off-by: David Anderson <danderson@tailscale.com>
2021-04-23 22:09:00 -07:00
David Anderson
25ce9885a2 net/dns: don't use NM+resolved for NM >=1.26.6.
NetworkManager fixed the bug that forced us to use NetworkManager
if it's programming systemd-resolved, and in the same release also
made NetworkManager ignore DNS settings provided for unmanaged
interfaces... Which breaks what we used to do. So, with versions
1.26.6 and above, we MUST NOT use NetworkManager to indirectly
program systemd-resolved, but thankfully we can talk to resolved
directly and get the right outcome.

Fixes #1788

Signed-off-by: David Anderson <danderson@tailscale.com>
2021-04-23 21:13:19 -07:00
David Anderson
31f81b782e util/cmpver: move into OSS from corp repo.
Signed-off-by: David Anderson <danderson@tailscale.com>
2021-04-23 20:55:45 -07:00
Aleksandar Pesic
7c985e4944 ipn/ipnlocal: add file sharing to windows shell
Updates: tailscale/winmin#33

Signed-off-by: Aleksandar Pesic <peske.nis@gmail.com>
2021-04-23 13:32:33 -07:00
Brad Fitzpatrick
e41075dd4a net/interfaces: work around race fetching routing table
Fixes #1345
Updates golang/go#45736

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-23 13:23:19 -07:00
Brad Fitzpatrick
fe53a714bd ipn/ipnlocal: add a LocalBackend.Start fast path if already running
Updates tailscale/corp#1621

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-23 12:14:12 -07:00
Brad Fitzpatrick
ad1a595a75 ipn/ipnlocal: close peer API listeners on transition away from Running
Updates tailscale/corp#1621

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-23 12:13:50 -07:00
Brad Fitzpatrick
d94ed7310b cmd/tailscale/cli: add test for already-submitted #1777
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-23 10:56:26 -07:00
Josh Bleecher Snyder
8d7f7fc7ce health, wgenegine: fix receive func health checks yet again
The existing implementation was completely, embarrassingly conceptually broken.

We aren't able to see whether wireguard-go's receive function goroutines
are running or not. All we can do is model that based on what we have done.
This commit fixes that model.

Fixes #1781

Signed-off-by: Josh Bleecher Snyder <josharian@gmail.com>
2021-04-23 08:42:04 -07:00
David Anderson
30f5d706a1 net/dns/resolver: remove unnecessary/racy WaitGroup.
Fixes #1663

Signed-off-by: David Anderson <danderson@tailscale.com>
2021-04-22 19:17:37 -07:00
Brad Fitzpatrick
8a449c4dcd ipn: define NewBackendServer nil as not affecting Backend's NotifyCallback
Updates tailscale/corp#1646

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-22 15:56:54 -07:00
David Anderson
30629c430a cmd/tailscale/cli: don't force an interactive login on --reset.
Fixes #1778

Signed-off-by: David Anderson <danderson@tailscale.com>
2021-04-22 15:53:50 -07:00
David Anderson
36d030cc36 ipn/ipnlocal: use fallback default DNS whenever exit nodes are on.
Fixes #1625

Signed-off-by: David Anderson <danderson@tailscale.com>
2021-04-22 15:24:18 -07:00
David Anderson
67ba6aa9fd cmd/tailscale/cli: fix typo in ExitNodeID mapping.
Prevented turning off exit nodes.

Fixes #1777

Signed-off-by: David Anderson <danderson@tailscale.com>
2021-04-22 14:55:29 -07:00
Brad Fitzpatrick
86e85d8934 ipn/ipnlocal: add peerapi goroutine fetch
Between owners.

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-22 13:11:51 -07:00
Josh Bleecher Snyder
5835a3f553 health, wgengine/magicsock: avoid receive function false positives
Avery reported a sub-ms health transition from "receiveIPv4 not running" to "ok".

To avoid these transient false-positives, be more precise about
the expected lifetime of receive funcs. The problematic case is one in which
they were started but exited prior to a call to connBind.Close.
Explicitly represent started vs running state, taking care with the order of updates.

Signed-off-by: Josh Bleecher Snyder <josharian@gmail.com>
2021-04-22 12:48:10 -07:00
Brad Fitzpatrick
3411bb959a control/controlclient: fix signRegisterRequest log suppression check on Windows
Fixes #1774

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-22 11:59:19 -07:00
Brad Fitzpatrick
2d786821f6 ipn/ipnlocal: put a retry loop around Windows file deletes
oh, Windows.

Updates tailscale/corp#1626

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-22 10:04:50 -07:00
Brad Fitzpatrick
11780a4503 cmd/tailscale: only send file basename in push
Fixes #1640

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-22 09:33:59 -07:00
Josh Bleecher Snyder
f845aae761 health: track whether magicsock receive functions are running
Signed-off-by: Josh Bleecher Snyder <josharian@gmail.com>
2021-04-22 08:57:36 -07:00
Brad Fitzpatrick
529ef98b2a ipn/ipnlocal: fix approxSize operator precedence
Whoops.

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-22 08:44:50 -07:00
Brad Fitzpatrick
820952daba cmd/tailscale: don't print out old authURL on up --force-reauth
Fixes #1671

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-22 08:38:07 -07:00
Brad Fitzpatrick
12b4672add wgengine: quiet connection failure diagnostics for exit nodes
The connection failure diagnostic code was never updated enough for
exit nodes, so disable its misleading output when the node it picks
(incorrectly) to diagnose is only an exit node.

Fixes #1754

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-22 08:29:20 -07:00
Brad Fitzpatrick
b03c23d2ed ipn/ipnlocal: log on DeleteFile error
Updates tailscale/corp#1626

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-22 07:48:18 -07:00
Brad Fitzpatrick
6f52fa02a3 control/controlclient, tailcfg: add Debug.SleepSeconds (mapver 19)
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-21 22:05:41 -07:00
Brad Fitzpatrick
c91a22c82e cmd/tailscale: don't print auth URL when using a --authkey
Fixes #1755

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-21 21:59:00 -07:00
Brad Fitzpatrick
e40e5429c2 cmd/tailscale/cli: make 'tailscale up' protect --advertise-exit-node removal
The new "tailscale up" checks previously didn't protect against
--advertise-exit-node being omitted in the case that
--advertise-routes was also provided. It wasn't done before because
there is no corresponding pref for "--advertise-exit-node"; it's a
helper flag that augments --advertise-routes. But that's an
implementation detail and we can still help users. We just have to
special case that pref and look whether the current routes include
both the v4 and v6 /0 routes.

Fixes #1767

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-21 21:45:30 -07:00
Brad Fitzpatrick
a16eb6ac41 cmd/tailscale/cli: show online/offline status in push --file-targets
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-21 16:06:01 -07:00
Brad Fitzpatrick
dedbd483ea cmd/tailscale/cli: don't require explicit --operator if it matches $USER
This doesn't make --operator implicit (which we might do in the
future), but it at least doesn't require repeating it in the future
when it already matches $USER.

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-21 15:49:01 -07:00
Brad Fitzpatrick
2f17a34242 ipn/ipnlocal: fix tailscale status --json AuthURL field
It was getting cleared on notify.

Document that authURL is cleared on notify and add a new field that
isn't, using the new field for the JSON status.

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-21 13:42:26 -07:00
Brad Fitzpatrick
09891b9868 ipn/ipnlocal: on fresh lazy-connecting install, start in state NeedsLogin
Fixes #1759

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-21 13:25:31 -07:00
Josh Bleecher Snyder
a29b0cf55f wgengine/wglog: allow wireguard-go receive routines to log
I've spent two days searching for a theoretical wireguard-go bug
around receive functions exiting early.

I've found many bugs, but none of the flavor we're looking for.

Restore wireguard-go's logging around starting and stopping receive functions,
so that we can definitively rule in or out this particular theory.

Signed-off-by: Josh Bleecher Snyder <josharian@gmail.com>
2021-04-21 12:29:28 -07:00
Josh Bleecher Snyder
eb2a9d4ce3 wgengine/netstack: log error when acceptUDP fails
I see a bunch of these in some logs I'm looking at,
separated only by a few seconds.
Log the error so we can tell what's going on here.

Signed-off-by: Josh Bleecher Snyder <josharian@gmail.com>
2021-04-21 12:25:01 -07:00
Naman Sood
4a90a91d29 wgengine/netstack: log ForwarderRequest in readable form, only in debug mode (#1758)
* wgengine/netstack: log ForwarderRequest in readable form, only in debug mode

Fixes #1757

Signed-off-by: Naman Sood <mail@nsood.in>
2021-04-21 14:50:48 -04:00
Josh Bleecher Snyder
07c95a0219 wgengine/wgcfg/nmcfg: consolidate exit node log lines
These were getting rate-limited for nodes with many peers.
Consolate the output into single lines, which are nicer anyway.

Signed-off-by: Josh Bleecher Snyder <josharian@gmail.com>
2021-04-21 11:29:30 -07:00
Brad Fitzpatrick
3d4d97601a derp/derpmap: add São Paulo (derp11)
Updates #1499
2021-04-21 11:04:47 -07:00
Brad Fitzpatrick
91c9c33036 cmd/tailscaled: don't block ipnserver startup behind engine init on Windows
With this change, the ipnserver's safesocket.Listen (the localhost
tcp.Listen) happens right away, before any synchronous
TUN/DNS/Engine/etc setup work, which might be slow, especially on
early boot on Windows.

Because the safesocket.Listen starts up early, that means localhost
TCP dials (the safesocket.Connect from the GUI) complete successfully
and thus the GUI avoids the MessageBox error. (I verified that
pacifies it, even without a Listener.Accept; I'd feared that Windows
localhost was maybe special and avoided the normal listener backlog).

Once the GUI can then connect immediately without errors, the various
timeouts then matter less, because the backend is no longer trying to
race against the GUI's timeout. So keep retrying on errors for a
minute, or 10 minutes if the system just booted in the past 10
minutes.

This should fix the problem with Windows 10 desktops auto-logging in
and starting the Tailscale frontend which was then showing a
MessageBox error about failing to connect to tailscaled, which was
slow coming up because the Windows networking stack wasn't up
yet. Fingers crossed.

Fixes #1313 (previously #1187, etc)

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-20 22:26:27 -07:00
Alex Brainman
7d8f082ff7 .github/workflows: add --race tests on Linux and Windows
Updates #50
Updates #833

Signed-off-by: Alex Brainman <alex.brainman@gmail.com>
2021-04-20 21:50:26 -07:00
Alex Brainman
7689213aaa cmd/tailscaled: add subcommands to install and remove tailscaled Windows service
This change implements Windows version of install-system-daemon and
uninstall-system-daemon subcommands. When running the commands the
user will install or remove Tailscale Windows service.

Updates #1232

Signed-off-by: Alex Brainman <alex.brainman@gmail.com>
2021-04-20 21:40:59 -07:00
David Anderson
6fd9e28bd0 ipn/ipnlocal: add arpa suffixes to MagicDNS for reverse lookups.
This used to not be necessary, because MagicDNS always did full proxying.
But with split DNS, we need to know which names to route to our resolver,
otherwise reverse lookups break.

This captures the entire CGNAT range, as well as our Tailscale ULA.

Signed-off-by: David Anderson <danderson@tailscale.com>
2021-04-20 18:05:17 -07:00
David Anderson
89c81c26c5 net/dns: fix resolved match domains when no nameservers are provided.
Signed-off-by: David Anderson <danderson@tailscale.com>
2021-04-20 17:10:39 -07:00
David Anderson
4be26b269f net/dns: correctly capture all traffic in non-split configs.
Signed-off-by: David Anderson <danderson@tailscale.com>
2021-04-20 16:57:46 -07:00
David Anderson
ca283ac899 net/dns: remove config in openresolv when given an empty DNS config.
Part of #1720.

Signed-off-by: David Anderson <danderson@tailscale.com>
2021-04-20 16:19:34 -07:00
David Anderson
48d4f14652 ipn/ipnlocal: only set authoritative domains when using MagicDNS.
Otherwise, the existence of authoritative domains forces full
DNS proxying even when no other DNS config is present.

Signed-off-by: David Anderson <danderson@tailscale.com>
2021-04-20 15:52:19 -07:00
David Anderson
53213114ec net/dns: make debian_resolvconf correctly clear DNS configs.
More of #1720.

Signed-off-by: David Anderson <danderson@tailscale.com>
2021-04-20 15:51:14 -07:00
David Anderson
3b1ab78954 net/dns: restore resolv.conf when given an empty config in directManager.
Fixes #1720.

Signed-off-by: David Anderson <danderson@tailscale.com>
2021-04-20 15:14:40 -07:00
Brad Fitzpatrick
f99e63bb17 ipn: don't Logout when Windows GUI disconnects
Logout used to be a no-op, so the ipnserver previously synthensized a Logout
on disconnect. Now that Logout actually invalidates the node key that was
forcing all GUI closes to log people out.

Instead, add a method to LocalBackend to specifically mean "the
Windows GUI closed, please forget all the state".

Fixes tailscale/corp#1591 (ignoring the notification issues, tracked elsewhere)

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-20 13:14:10 -07:00
David Anderson
158328ba24 net/dns: remove ForceSplitDNSForTesting.
Signed-off-by: David Anderson <danderson@tailscale.com>
2021-04-20 12:50:08 -07:00
David Anderson
1e5c608fae ipn/ipnlocal: plumb fallback DNS in as a workaround for split DNS issues.
Cause of #1743.

Signed-off-by: David Anderson <danderson@tailscale.com>
2021-04-20 12:49:48 -07:00
David Anderson
28ba20d733 tailcfg: add FallbackResolvers to DNSConfig.
Signed-off-by: David Anderson <danderson@tailscale.com>
2021-04-20 12:49:48 -07:00
Brad Fitzpatrick
3d0599fca0 ipn{,/ipnlocal}: in direct file receive mode, don't rename partial file
Let caller (macOS) do it so Finder progress bar can be dismissed
without races.

Updates tailscale/corp#1575

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-20 12:40:42 -07:00
Josh Bleecher Snyder
48e30bb8de wgengine/magicsock: remove named return
Doesn't add anything.

Signed-off-by: Josh Bleecher Snyder <josharian@gmail.com>
2021-04-20 10:12:07 -07:00
Josh Bleecher Snyder
a2a2c0ce1c wgengine/magicsock: fix two comments
Signed-off-by: Josh Bleecher Snyder <josharian@gmail.com>
2021-04-20 10:12:07 -07:00
Josh Bleecher Snyder
b1e624ef04 wgengine/magicsock: remove unnecessary type assertions
Signed-off-by: Josh Bleecher Snyder <josharian@gmail.com>
2021-04-20 10:12:07 -07:00
Josh Bleecher Snyder
98714e784b wgengine/magicsock: improve Rebind logging
We were accidentally logging oldPort -> oldPort.

Log oldPort as well as c.port; if we failed to get the preferred port
in a previous rebind, oldPort might differ from c.port.

Signed-off-by: Josh Bleecher Snyder <josharian@gmail.com>
2021-04-20 10:12:07 -07:00
Josh Bleecher Snyder
15ceacc4c5 wgengine/magicsock: accept a host and port instead of an addr in listenPacket
This simplifies call sites and prevents accidental failure to use net.JoinHostPort.

Signed-off-by: Josh Bleecher Snyder <josharian@gmail.com>
2021-04-20 10:12:07 -07:00
Brad Fitzpatrick
f42ded7acf cmd/tailscale/cli: relax & improve the running-as-CLI check for macOS
On macOS, we link the CLI into the GUI executable so it can be included in
the Mac App Store build.

You then need to run it like:

/Applications/Tailscale.app/Contents/MacOS/Tailscale <command>

But our old detection of whether you're running that Tailscale binary
in CLI mode wasn't accurate and often bit people. For instance, when
they made a typo, it then launched in GUI mode and broke their
existing GUI connection (starting a new IPNExtension) and took down
their network.

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-20 09:18:33 -07:00
Brad Fitzpatrick
a58fbb4da9 ipn/ipnlocal: only fix peerapiListener on Windows when running
It's just logspam otherwise.

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-20 09:18:33 -07:00
Maisem Ali
36fa29feec ipn/ipnlocal: restrict local lan access to linux machines.
Signed-off-by: Maisem Ali <maisem@tailscale.com>
2021-04-20 08:11:06 -07:00
Brad Fitzpatrick
8570f82c8b ipn/ipnlocal: finish/fix up filename validation & encoding on disk
It used to just store received files URL-escaped on disk, but that was
a half done lazy implementation, and pushed the burden to callers to
validate and write things to disk in an unescaped way.

Instead, do all the validation in the receive handler and only
accept filenames that are UTF-8 and in the intersection of valid
names that all platforms support.

Fixes tailscale/corp#1594

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-19 22:39:53 -07:00
Brad Fitzpatrick
7f8519c88f version: simplify iOS detection now that we require Go 1.16
See https://golang.org/doc/go1.16#darwin

No need for build tag tricks anymore.
2021-04-19 21:59:55 -07:00
Brad Fitzpatrick
cad8df500c ipn/ipnlocal: add some more peerapi handlePeerPut tests
Updates tailscale/corp#1594

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-19 21:01:04 -07:00
Brad Fitzpatrick
0d1550898e ipn/ipnlocal: add some peerapi tests
Updates tailscale/corp#1594

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-19 20:28:53 -07:00
Josh Bleecher Snyder
f72a120016 go.mod: upgrade to latest wireguard-go
Pull in minor upstream changes.

Signed-off-by: Josh Bleecher Snyder <josharian@gmail.com>
2021-04-19 13:30:43 -07:00
Naman Sood
71b7e48547 net/tsaddr: expand ephemeral nodes range to /64
Signed-off-by: Naman Sood <mail@nsood.in>
2021-04-19 15:54:53 -04:00
Brad Fitzpatrick
e9d24341e0 tailcfg, control/controlclient: accept nil MapResponse.Node (mapver 18)
All MapResponse fields can not be omitted and are tagged "omitempty".

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-19 11:53:21 -07:00
Brad Fitzpatrick
97204fdc52 safesocket: remove/update some old TODOs
Windows auth is done by looking at the owner of the TCP connection.

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-19 11:35:02 -07:00
Brad Fitzpatrick
8f3e453356 ipn, cmd/tailscale/cli: add pref to configure sudo-free operator user
From discussion with @danderson.

Fixes #1684 (in a different way)

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-19 10:12:41 -07:00
Brad Fitzpatrick
3739cf22b0 tailcfg, control/controlclient: allow empty MapResponse.Domain (mapver17)
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-19 09:31:21 -07:00
Brad Fitzpatrick
5092cffd1f control/controlclient: add start of some MapResponse->NetworkMap tests
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-18 20:27:51 -07:00
Brad Fitzpatrick
aef3c0350c control/controlclient: break direct.go into map.go (+tests), add mapSession
So the NetworkMap-from-incremental-MapResponses can be tested easily.

And because direct.go was getting too big.

No change in behavior at this point. Just movement.

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-18 19:56:03 -07:00
Brad Fitzpatrick
6d64107f26 types/netmap: remove some old TODOs
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-18 19:30:13 -07:00
Brad Fitzpatrick
49808ae6ea ipn{,/ipnlocal}, cmd/tailscale/cli: don't check pref reverts on initial up
The ipn.NewPrefs func returns a populated ipn.Prefs for historical
reasons. It's not used or as important as it once was, but it hasn't
yet been removed. Meanwhile, it contains some default values that are
used on some platforms. Notably, for this bug (#1725), Windows/Mac use
its Prefs.RouteAll true value (to accept subnets), but Linux users
have always gotten a "false" value for that, because that's what
cmd/tailscale's CLI default flag is _for all operating systems_.  That
meant that "tailscale up" was rightfully reporting that the user was
changing an implicit setting: RouteAll was changing from true with
false with the user explicitly saying so.

An obvious fix might be to change ipn.NewPrefs to return
Prefs.RouteAll == false on some platforms, but the logic is
complicated by darwin: we want RouteAll true on windows, android, ios,
and the GUI mac app, but not the CLI tailscaled-on-macOS mode. But
even if we used build tags (e.g. the "redo" build tag) to determine
what the default is, that then means we have duplicated and differing
"defaults" between both the CLI up flags and ipn.NewPrefs. Furthering
that complication didn't seem like a good idea.

So, changing the NewPrefs defaults is too invasive at this stage of
the release, as is removing the NewPrefs func entirely.

Instead, tweak slightly the semantics of the ipn.Prefs.ControlURL
field. This now defines that a ControlURL of the empty string means
both "we're uninitialized" and also "just use the default".

Then, once we have the "empty-string-means-unintialized" semantics,
use that to suppress "tailscale up"'s recent implicit-setting-revert
checking safety net, if we've never initialized Tailscale yet.

And update/add tests.

Fixes #1725

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-18 08:12:18 -07:00
Brad Fitzpatrick
4df6e62fbc ipn: add DefaultControlURL const, replace few literals with it
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-18 07:48:53 -07:00
Brad Fitzpatrick
f1d45bc4bb cmd/tailscale/cli: pull out prefsFromUpArgs for testability, add tests
Will add more tests later but this locks in all the existing warnings
and errors at least, and some of the existing non-error behavior.

Mostly I want this to exist before I actually fix #1725.

Updates #1725

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-17 20:59:33 -07:00
Brad Fitzpatrick
4948ff6ecb cmd/tailscale/cli: treat nil and non-nil zero length slices as equiv prefs
Updates #1725

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-17 19:17:18 -07:00
Brad Fitzpatrick
eb6115e295 cmd/tailscaled: let SOCKS5 dial non-Tailscale addrs in userspace mode
Fixes #1617

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-16 16:20:31 -07:00
Naman Sood
b85d80b37f net/tsaddr: add new IP range for ephemeral nodes in Tailscale ULA (#1715)
Signed-off-by: Naman Sood <mail@nsood.in>
2021-04-16 14:47:55 -04:00
Brad Fitzpatrick
b993d9802a ipn/ipnlocal, etc: require file sharing capability to send/recv files
tailscale/corp#1582

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-16 10:58:19 -07:00
Brad Fitzpatrick
2f422434aa cmd/tailscale/cli: wait on the right contexts in up
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-16 10:27:56 -07:00
Brad Fitzpatrick
6da812b4cf cmd/tailscale/cli: avoid a spammy log message on SIGINT
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-16 08:00:31 -07:00
Brad Fitzpatrick
670838c45f tailcfg, control/controlclient: (mapver 16) add Node.Online, MapResponse.OnlineChange
And fix PeerSeenChange bug where it was ignored unless there were
other peer changes.

Updates tailscale/corp#1574

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-15 20:07:24 -07:00
David Anderson
7055f870f8 control/controlclient: only use a single DNS label as the hostname.
Fixes #971

Signed-off-by: David Anderson <danderson@tailscale.com>
2021-04-15 17:08:58 -07:00
Maisem Ali
4f3203556d wgengine/router: add the Tailscale ULA route on darwin.
Signed-off-by: Maisem Ali <maisem@tailscale.com>
2021-04-15 17:07:50 -07:00
Ross Zurowski
c748c20fba cmd/tailscale: fix command descriptions (#1710)
Signed-off-by: Ross Zurowski <ross@rosszurowski.com>
2021-04-15 18:33:23 -04:00
Brad Fitzpatrick
b34fbb24e8 logtail: reduce PublicID.UnmarshalText from 2 allocs to 0
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-15 10:42:12 -07:00
David Anderson
bb0710d51d net/dns: add debugging traces to DNS manager selection on linux.
Signed-off-by: David Anderson <danderson@tailscale.com>
2021-04-14 15:52:41 -07:00
David Anderson
4b70c7b717 net/dns: fix inverted test for NetworkManager.
Signed-off-by: David Anderson <danderson@tailscale.com>
2021-04-14 15:52:22 -07:00
David Anderson
4849a4d3c8 net/dns: error out on linux if /etc/resolv.conf can't be read.
Signed-off-by: David Anderson <danderson@tailscale.com>
2021-04-14 15:35:32 -07:00
David Anderson
1f9b73a531 net/dns: fix freebsd DNS manager selection.
Signed-off-by: David Anderson <danderson@tailscale.com>
2021-04-14 15:34:59 -07:00
Naman Sood
5ea53891fe cmd/tailscaled: populate netstack variable to use dialer in SOCKS5
Signed-off-by: Naman Sood <mail@nsood.in>
2021-04-14 13:13:10 -04:00
Brad Fitzpatrick
d6a95d807a ipn/ipnlocal: advertise netstack to control server
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-14 09:38:33 -07:00
AdamKorcz
2243bb48c2 stun fuzzer: Small fix
Signed-off-by: AdamKorcz <adam@adalogics.com>
2021-04-14 08:17:46 -07:00
Brad Fitzpatrick
75b99555f3 cmd/tailscale/cli: let ip take a peername
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-14 08:13:16 -07:00
Brad Fitzpatrick
762180595d ipn/ipnstate: add PeerStatus.TailscaleIPs slice, deprecate TailAddr
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-14 08:12:31 -07:00
Brad Fitzpatrick
c2ca2ac8c4 net/dns: fix FreeBSD build
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-13 17:38:17 -07:00
David Anderson
84bd50329a net/dns: fix staticheck.
Signed-off-by: David Anderson <danderson@tailscale.com>
2021-04-13 17:28:37 -07:00
David Anderson
d6bb11b5bf net/dns: implement correct manager detection on linux.
Part of #953.

Signed-off-by: David Anderson <danderson@tailscale.com>
2021-04-13 17:19:00 -07:00
David Anderson
9ef932517b net/dns: fix NM's GetBaseConfig when no configs exist.
Signed-off-by: David Anderson <danderson@tailscale.com>
2021-04-13 17:19:00 -07:00
David Anderson
fe3b1ab747 net/dns: refactor dbus connection setup in resolved manager.
Signed-off-by: David Anderson <danderson@tailscale.com>
2021-04-13 17:19:00 -07:00
Brad Fitzpatrick
2df6372b67 portlist: de-dup services on same (proto, port) on both IPv4/IPv6
Fixes #1703

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-13 14:40:33 -07:00
Brad Fitzpatrick
a8d95a18b2 cmd/tailscale/cli: add up --unattended for Windows
RELNOTE=Windows CLI behavior change: ForceDaemon now off by default

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-13 11:40:31 -07:00
Brad Fitzpatrick
34d2f5a3d9 tailcfg: add Endpoint, EndpointType, MapRequest.EndpointType
Track endpoints internally with a new tailcfg.Endpoint type that
includes a typed netaddr.IPPort (instead of just a string) and
includes a type for how that endpoint was discovered (STUN, local,
etc).

Use []tailcfg.Endpoint instead of []string internally.

At the last second, send it to the control server as the existing
[]string for endpoints, but also include a new parallel
MapRequest.EndpointType []tailcfg.EndpointType, so the control server
can start filtering out less-important endpoint changes from
new-enough clients. Notably, STUN-discovered endpoints can be filtered
out from 1.6+ clients, as they can discover them amongst each other
via CallMeMaybe disco exchanges started over DERP. And STUN endpoints
change a lot, causing a lot of MapResposne updates. But portmapped
endpoints are worth keeping for now, as they they work right away
without requiring the firewall traversal extra RTT dance.

End result will be less control->client bandwidth. (despite negligible
increase in client->control bandwidth)

Updates tailscale/corp#1543

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-13 10:12:14 -07:00
Brad Fitzpatrick
b91f3c4191 ipn/ipnlocal: fix peerapi printf arg mismatch
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-13 08:56:26 -07:00
Brad Fitzpatrick
a08d978476 cmd/tailscale/cli: make push get peerapi base via localapi, not TSMP ping
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-13 08:50:16 -07:00
Brad Fitzpatrick
1dc2cf4835 cmd/tailscale/cli: add push --targets to list possible targets
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-13 08:36:14 -07:00
Brad Fitzpatrick
1f4cf1a4f4 ipn/localapi: only require read access to list file targets
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-13 08:35:16 -07:00
Brad Fitzpatrick
d17f96b586 cmd/tailscale/cli: restore SIGINT/SIGTERM on context cancel
This fixes Ctrl-C not interrupting "tailscale push".

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-13 08:34:42 -07:00
Brad Fitzpatrick
db5e269463 client/tailscale/apitype: move local API types to new apitype package
They were scattered/duplicated in misc places before.

It can't be in the client package itself for circular dep reasons.

This new package is basically tailcfg but for localhost
communications, instead of to control.

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-13 08:13:46 -07:00
Maisem Ali
1b9d8771dc ipn/ipnlocal,wgengine/router,cmd/tailscale: add flag to allow local lan access when routing traffic via an exit node.
For #1527

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2021-04-12 17:29:01 -07:00
David Anderson
854d5d36a1 net/dns: return error from NewOSManager, use it to initialize NM.
Signed-off-by: David Anderson <danderson@tailscale.com>
2021-04-12 15:51:37 -07:00
Maisem Ali
4d142ebe06 derp: handle net.ErrClosed in TestSendFreeze
Signed-off-by: Maisem Ali <maisem@tailscale.com>
2021-04-12 15:23:36 -07:00
Brad Fitzpatrick
8e75c8504c ipn/ipnlocal: in direct file mode, don't readdir
And don't even allow attempts at Open/Delete.

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-12 15:12:47 -07:00
Brad Fitzpatrick
9972c02b60 cmd/tailscale/cli: don't let up change prefs based on implicit flag values
This changes the behavior of "tailscale up".

Previously "tailscale up" always did a new Start and reset all the settings.

Now "tailscale up" with no flags just brings the world [back] up.
(The opposite of "tailscale down").

But with flags, "tailscale up" now only is allowed to change
preferences if they're explicitly named in the flags. Otherwise it's
an error. Or you need to use --reset to explicitly nuke everything.

RELNOTE=tailscale up change

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-12 14:39:47 -07:00
David Anderson
9aa33b43e6 net/dns: support split and unsplit DNS in NetworkManager.
Signed-off-by: David Anderson <danderson@tailscale.com>
2021-04-12 14:34:08 -07:00
Brad Fitzpatrick
f5742b0647 ipn/ipnlocal: add LocalBackend.SetDirectFileRoot
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-12 14:29:45 -07:00
Brad Fitzpatrick
64c80129f1 types/netmap: add some docs/warning to NetworkMap
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-12 12:49:25 -07:00
Brad Fitzpatrick
ccb322db04 tailcfg, control/controlclient: make nil MapResponse.DNSConfig mean unchanged (mapver15)
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-12 12:46:32 -07:00
Brad Fitzpatrick
a3113a793a ipn: add hostname to Prefs.Pretty output
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-12 10:45:33 -07:00
Brad Fitzpatrick
4c3f7c06fc ipn/ipnlocal: be consistent in not logging when no notify registered
Some paths already didn't. And in the future I hope to shut all the
notify funcs down end-to-end when nothing is connected (as in the
common case in tailscaled).  Then we can save some JSON encoding work.

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-12 10:38:51 -07:00
Brad Fitzpatrick
7c0e58c537 ipn/ipnlocal: remove redundant notify nil check
send does it.

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-12 10:38:51 -07:00
Brad Fitzpatrick
d9ee9a0d3f ipn: set BackendServer's notify earlier; don't require Start
We've been slowly making Start less special and making IPN a
multi-connection "watch" bus of changes, but this Start specialness
had remained.

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-12 10:38:51 -07:00
Brad Fitzpatrick
8e4d1e3f2c ipn: include err in Notify decode fatal path
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-12 10:38:51 -07:00
Brad Fitzpatrick
d5d70ae9ea wgengine/monitor: reduce Linux log spam on down
Fixes #1689

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-12 10:38:51 -07:00
Brad Fitzpatrick
c0befee188 portlist: use windows OpenCurrentProcessToken, not GetCurrentProcessToken
The latter only works on Windows 8+.

Also add a TODO to get do this all more efficiently.

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-12 09:23:52 -07:00
Brad Fitzpatrick
e619296ece portlist: filter out all of 127.0.0.0/8, not just 127.0.0.1/32
Per user private bug report.

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-12 09:17:39 -07:00
Brad Fitzpatrick
f325aa7e38 portlist: exclude services bound to IPv6 loopback address
Fixes #1683

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-12 09:07:16 -07:00
David Anderson
87eb8384f5 net/dns: fix up NetworkManager configurator a bit.
Clear LLMNR and mdns flags, update reasoning for our settings,
and set our override priority harder than before when we want
to be primary resolver.

Signed-off-by: David Anderson <danderson@tailscale.com>
2021-04-11 23:19:50 -07:00
Brad Fitzpatrick
303805a389 ipn/localapi: require write access to PATCH prefs
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-11 21:31:20 -07:00
David Anderson
3d81e6260b net/dns: set resolved DefaultRoute setting according to split-dns mode.
Signed-off-by: David Anderson <danderson@tailscale.com>
2021-04-11 20:53:28 -07:00
David Anderson
cca230cc23 net/dns: fix staticcheck errors.
Signed-off-by: David Anderson <danderson@tailscale.com>
2021-04-11 20:53:28 -07:00
Brad Fitzpatrick
79109f4965 ipn/ipnlocal: use PATCH for EditPrefs, not POST
Addendum to earlier 00d641d9fc.

Reserve POST for SetPrefs in the future.

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-11 20:49:07 -07:00
Brad Fitzpatrick
4b47393e0c net/dns: pacify staticcheck for now
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-11 20:43:15 -07:00
David Anderson
a7340c2015 net/dns: support split DNS in systemd-resolved.
Signed-off-by: David Anderson <danderson@tailscale.com>
2021-04-11 18:14:23 -07:00
Brad Fitzpatrick
00d641d9fc ipn/localapi: move EditPrefs to localapi
Follow-up/revision to recent 53cfff109b which
added EditPrefs.

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-11 16:11:43 -07:00
David Anderson
84430cdfa1 net/dns: improve NetworkManager detection, using more DBus.
Signed-off-by: David Anderson <danderson@tailscale.com>
2021-04-11 15:22:06 -07:00
David Anderson
9a48bac8ad net/dns: rename resolvconf.go to debian_resolvconf.go.
Signed-off-by: David Anderson <danderson@tailscale.com>
2021-04-10 23:31:14 -07:00
David Anderson
9831f1b183 net/dns: also include 'tail' and 'base' files when fixing up resolv.conf.
Signed-off-by: David Anderson <danderson@tailscale.com>
2021-04-10 23:01:11 -07:00
David Anderson
e43afe9140 net/dns: implement prior config reading for debian resolvconf.
Signed-off-by: David Anderson <danderson@tailscale.com>
2021-04-10 22:37:13 -07:00
David Anderson
143e5dd087 net/dns: rename script variable.
Debian resolvconf is not legacy, it's alive and well,
just historically before the other implementations.

Signed-off-by: David Anderson <danderson@tailscale.com>
2021-04-10 21:28:27 -07:00
David Anderson
55b39fa945 net/dns: add documentation to openresolv's config fetch.
Signed-off-by: David Anderson <danderson@tailscale.com>
2021-04-10 20:21:05 -07:00
David Anderson
61b361bac0 net/dns: teach the openresolv manager to read DNS config.
Signed-off-by: David Anderson <danderson@tailscale.com>
2021-04-10 19:37:11 -07:00
David Anderson
19eca34f47 wgengine/router: fix FreeBSD configuration failure on the v6 /48.
On FreeBSD, we add the interface IP as a /48 to work around a kernel
bug, so we mustn't then try to add a /48 route to the Tailscale ULA,
since that will fail as a dupe.

Signed-off-by: David Anderson <danderson@tailscale.com>
2021-04-10 19:36:26 -07:00
David Anderson
58760f7b82 net/dns: split resolvconfManager into a debian and an openresolv manager.
Signed-off-by: David Anderson <danderson@tailscale.com>
2021-04-10 18:55:05 -07:00
David Anderson
5480189313 net/dns: implement a DNS override workaround for legacy resolvconf.
Signed-off-by: David Anderson <danderson@tailscale.com>
2021-04-10 17:58:13 -07:00
David Anderson
1a371b93be util/dnsname: add FQDN type, use throughout codebase.
Signed-off-by: David Anderson <danderson@tailscale.com>
2021-04-10 17:58:13 -07:00
AdamKorcz
7a1813fd24 Added 2 fuzzers
Signed-off-by: AdamKorcz <adam@adalogics.com>
2021-04-10 11:40:10 -07:00
Daniel Chung
5e90037f1a api.md: clarify response behaviour for ACL POST endpoint
Signed-off-by: Daniel Chung <daniel@tailscale.com>
2021-04-10 11:38:12 -07:00
Simon Kirillov
a64b57e2fb control/controlclient: create tls client config with server hostname instead of serverURL.Host
Signed-off-by: Simon Kirillov <svkirillov3@gmail.com>
2021-04-10 11:20:10 -07:00
Brad Fitzpatrick
958782c737 cmd/{tailscale,tailscaled}: use netstack for subnet routing on Synology
Updates #707
Fixes #451
Fixes tailscale/tailscale-synology#52 (just make it work by default)

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-09 18:44:33 -07:00
Brad Fitzpatrick
3b451509dd cmd/tailscale/cli: make advertise-{routes,exit-node} available unconditionally
It was only Linux and BSDs before, but now with netstack mode, it also works on
Windows and darwin. It's not worth limiting it to certain platforms.

Tailscaled itself can complain/fail if it doesn't like the settings
for the mode/OS it's operating under.

Updates #707

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-09 18:37:24 -07:00
Brad Fitzpatrick
83402e2753 cmd/tailscale/cli: show nicer status output when logged out
Also nicer output when running "down".

Fixes #1680

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-09 18:25:22 -07:00
Brad Fitzpatrick
5c5acadb2a portlist: unexport SameInodes method
Signed-off-by: Brad Fitzpatrick <brad@danga.com>
2021-04-09 15:16:36 -07:00
Brad Fitzpatrick
3167e55ddf ipn/{ipnlocal,localapi}, cmd/tailscale: add logout command
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-09 13:26:35 -07:00
Brad Fitzpatrick
11127666b2 ipn/ipnlocal: fix deadlock from 227f73284
Sigh.

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-09 13:23:34 -07:00
Brad Fitzpatrick
227f73284f ipn/ipnlocal: eagerly announce peerapi service(s) on change
We were previously only doing it as a side effect of the port poller,
which doesn't run on e.g. iOS.

Updates tailscale/corp#1559
2021-04-09 12:10:52 -07:00
Brad Fitzpatrick
fe23506471 ipn/ipnlocal: avoid unneeded initPeerAPIListener work if no changes
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-09 11:13:46 -07:00
Brad Fitzpatrick
20e7646b8d ipn/ipnlocal: always set Notify.FilesWaiting, set IncomingFiles non-nil when empty 2021-04-09 07:59:36 -07:00
Brad Fitzpatrick
b0af15ff5c portlist: remove some old TODOs
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-09 07:50:00 -07:00
David Anderson
e638a4d86b net/dns: make directManager support split DNS, and work in sandboxes.
Fixes #1495, #683.

Signed-off-by: David Anderson <danderson@tailscale.com>
2021-04-09 02:52:21 -07:00
David Anderson
2685260ba1 net/dns: add temporary fallback to quad-9 resolver for split-DNS testing.
This allows split-DNS configurations to not break clients on OSes that
haven't yet been ported to understand split DNS, by falling back to quad-9
as a global resolver when handed an "impossible to implement"
split-DNS config.

Part of #953. Needs to be removed before shipping 1.8.

Signed-off-by: David Anderson <danderson@tailscale.com>
2021-04-08 23:26:22 -07:00
David Anderson
b9e194c14b net/dns: add missing FQDN qualification.
Signed-off-by: David Anderson <danderson@tailscale.com>
2021-04-08 23:07:16 -07:00
Brad Fitzpatrick
c50c3f0313 tailcfg: document new RegisterRequest.Expiry behavior
Deployed to control server.

For upcoming "logout" command and fixes.

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-08 22:27:14 -07:00
David Anderson
b74a8994ca net/dns: make FQDN dot style consistent in more places.
Signed-off-by: David Anderson <danderson@tailscale.com>
2021-04-08 22:03:32 -07:00
Brad Fitzpatrick
6d01d3bece ipn/ipnlocal: provide IPN bus updates as files arrive
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-08 20:09:52 -07:00
David Anderson
2f398106e2 ipn/ipnlocal: allow setting MagicDNS without DefaultResolvers.
Signed-off-by: David Anderson <danderson@tailscale.com>
2021-04-08 16:23:33 -07:00
David Anderson
fad21af01c tailcfg: add DNS routes and advanced resolver config.
Signed-off-by: David Anderson <danderson@tailscale.com>
2021-04-08 15:29:08 -07:00
Brad Fitzpatrick
6a7912e37a cmd/tailscale/cli: add debug mode to push slowly for testing
Also set Content-Length when known, and fail explicitly on sending
directories for now.

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-08 15:01:12 -07:00
Brad Fitzpatrick
a9a3d3b4c1 ipn/ipnlocal: don't filter by time in FileTargets
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-08 14:02:07 -07:00
David Anderson
6def647514 net/dns/resolver: don't avoid tailscale routes for DNS forwarding.
Signed-off-by: David Anderson <danderson@tailscale.com>
2021-04-08 12:20:42 -07:00
Brad Fitzpatrick
597c19ff4e control/controlclient: refactor some internals
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-07 21:20:17 -07:00
Brad Fitzpatrick
71432c6449 ipn/ipnlocal: some more variable renames
Missed in earlier commit.

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-07 21:17:33 -07:00
Brad Fitzpatrick
e86b7752ef ipn/ipnlocal: rename some variables to be consistent
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-07 21:12:28 -07:00
David Anderson
4a64d2a603 net/dns: some post-review cleanups.
Signed-off-by: David Anderson <danderson@tailscale.com>
2021-04-07 15:40:31 -07:00
David Anderson
720c1ad0f0 net/dns: insert OS base config when emulating split DNS.
Part of #953.

Signed-off-by: David Anderson <danderson@tailscale.com>
2021-04-07 15:40:31 -07:00
David Anderson
e560be6443 net/dns: sort matchDomains to avoid test flake.
Signed-off-by: David Anderson <danderson@tailscale.com>
2021-04-07 15:40:31 -07:00
David Anderson
68f76e9aa1 net/dns: add GetBaseConfig to OSConfigurator interface.
Part of #953, required to make split DNS work on more basic
platforms.

Signed-off-by: David Anderson <danderson@tailscale.com>
2021-04-07 15:40:31 -07:00
David Anderson
fe9cd61d71 net/dns: add tests for DNS config generation.
Part of #953.

Signed-off-by: David Anderson <danderson@tailscale.com>
2021-04-07 15:40:31 -07:00
David Anderson
0ba6d03768 net/dns/resolver: add a test helper to get at the resolver config.
Signed-off-by: David Anderson <danderson@tailscale.com>
2021-04-07 15:40:31 -07:00
David Anderson
da4cc8bbb4 net/dns: handle all possible translations of high-level DNS config.
With this change, all OSes can sort-of do split DNS, except that the
default upstream is hardcoded to 8.8.8.8 pending further plumbing.
Additionally, Windows 8-10 can do split DNS fully correctly, without
the 8.8.8.8 hack.

Part of #953.

Signed-off-by: David Anderson <danderson@tailscale.com>
2021-04-07 15:40:31 -07:00
Brad Fitzpatrick
939861773d net/tstun: accept peerapi connections through the filter
Fixes tailscale/corp#1545

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-07 12:29:20 -07:00
Brad Fitzpatrick
950fc28887 ipn, paths, cmd/tailscaled: remove LegacyConfigPath, relaynode migration
It is time.

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-07 10:15:45 -07:00
Brad Fitzpatrick
d581ee2536 ipn: remove Options.HTTPTestClient, move to LocalBackend
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-07 09:20:51 -07:00
Brad Fitzpatrick
50b309c1eb ipn/localapi, cmd/tailscale: add API to get prefs, CLI debug command to show
Updates #1436

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-07 08:28:11 -07:00
Brad Fitzpatrick
03be116997 client/tailscale: factor out some helpers to reduce boilerplate
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-07 08:19:36 -07:00
Brad Fitzpatrick
d4b609e138 cmd/tailscale/cli: fix bug in earlier tailscale debug --local-creds addition
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-07 08:01:04 -07:00
Adrian Dewhurst
3f456ba2e7 control/controlclient: return correct certificate
When searching for the matching client identity, the returned
certificate chain was accidentally set to that of the last identity
returned by the certificate store instead of the one corresponding to
the selected identity.

Also, add some extra error checking for invalid certificate chains, just
in case.

Signed-off-by: Adrian Dewhurst <adrian@tailscale.com>
2021-04-07 09:42:13 -04:00
Brad Fitzpatrick
799973a68d ipn: move Options.Notify to its own method
We already had SetNotifyCallback elsewhere on controlclient, so use
that name.

Baby steps towards some CLI refactor work.

Updates tailscale/tailscale#1436
2021-04-06 22:12:40 -07:00
Brad Fitzpatrick
d488678fdc cmd/tailscaled, wgengine{,/netstack}: add netstack hybrid mode, add to Windows
For #707

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-06 21:37:28 -07:00
Brad Fitzpatrick
1f99f889e1 ipn/{ipnlocal,localapi}: add localapi handler to dial/proxy file PUTs
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-06 21:31:50 -07:00
Denton Gentry
3089081349 monitor/polling: reduce Cloud Run polling interval.
Cloud Run's routes never change at runtime. Don't poll it for
route changes very often.

Signed-off-by: Denton Gentry <dgentry@tailscale.com>
2021-04-06 17:21:16 -07:00
Denton Gentry
224e60cef2 hostifo: update LXC, add Cloud Run.
Recent LXC support no longer has "lxc" in /proc/1/cgroup:
    # cat /proc/1/cgroup
    12:freezer:/
    11:rdma:/
    10:cpuset:/
    9:pids:/
    8:blkio:/
    7:devices:/
    6:perf_event:/
    5:net_cls,net_prio:/
    4:memory:/
    3:hugetlb:/
    2:cpu,cpuacct:/
    1:name=systemd:/init.scope
    0::/init.scope

Look for fuse.lxcfs in /proc.mounts in addition:
    # grep lxc /proc/mounts
    lxcfs /proc/cpuinfo fuse.lxcfs ...
    lxcfs /proc/diskstats fuse.lxcfs ...
    lxcfs /proc/loadavg fuse.lxcfs ...
    lxcfs /proc/meminfo fuse.lxcfs ...
    lxcfs /proc/stat fuse.lxcfs ...
    lxcfs /proc/swaps fuse.lxcfs ...
    lxcfs /proc/uptime fuse.lxcfs ...
    lxcfs /sys/devices/system/cpu/online fuse.lxcfs ...

Add Knative detection by looking for the environment variables
which are part of its container contract.

Signed-off-by: Denton Gentry <dgentry@tailscale.com>
2021-04-06 17:21:16 -07:00
Maisem Ali
57756ef673 net/nettest: make nettest.NewConn pass x/net/nettest.TestConn.
Signed-off-by: Maisem Ali <maisem@tailscale.com>
2021-04-06 15:34:29 -07:00
David Anderson
e0e677a8f6 net/dns: split out search domains and match domains in OSConfig.
It seems that all the setups that support split DNS understand
this distinction, and it's an important one when translating
high-level configuration.

Part of #953.

Signed-off-by: David Anderson <danderson@tailscale.com>
2021-04-06 15:27:09 -07:00
David Anderson
a8dcda9c9a net/dns: start of compat hacks for Windows 7.
Correctly reports that Win7 cannot do split DNS, and has a helper to
discover the "base" resolvers for the system.

Part of #953

Signed-off-by: David Anderson <danderson@tailscale.com>
2021-04-06 15:27:09 -07:00
Brad Fitzpatrick
ea9e68280d cmd/tailscale/cli: add debug command to print localapi curl command 2021-04-06 14:05:49 -07:00
Brad Fitzpatrick
d717499ac4 ipn/localapi: add API for getting file targets
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-06 11:01:30 -07:00
David Anderson
3e915ac783 net/dns: implement OS-level split DNS for Windows.
Part of #953.

Signed-off-by: David Anderson <danderson@tailscale.com>
2021-04-05 22:53:10 -07:00
David Anderson
c16a926bf2 net/dns: set OSConfig.Primary.
OS implementations are going to support split DNS soon.
Until they're all in place, hardcode Primary=true to get
the old behavior.

Signed-off-by: David Anderson <danderson@tailscale.com>
2021-04-05 22:53:10 -07:00
David Anderson
bc4381447f net/tstun: return the real interface name at device creation.
This is usually the same as the requested interface, but on some
unixes can vary based on device number allocation, and on Windows
it's the GUID instead of the pretty name, since everything relating
to configuration wants the GUID.

Signed-off-by: David Anderson <danderson@tailscale.com>
2021-04-05 22:53:10 -07:00
David Crawshaw
d2f838c058 ipn/localapi: 404 on bad endpoints
Confused us for a while!

Signed-off-by: David Crawshaw <crawshaw@tailscale.com>
2021-04-05 15:09:54 -07:00
David Anderson
de6dc4c510 net/dns: add a Primary field to OSConfig.
Currently ignored.

Signed-off-by: David Anderson <danderson@tailscale.com>
2021-04-05 13:05:47 -07:00
404 changed files with 33189 additions and 7644 deletions

View File

@@ -2,36 +2,7 @@
name: Bug report
about: Create a bug report
title: ''
labels: ''
labels: 'needs-triage'
assignees: ''
---
<!-- Please note, this template is for definite bugs, not requests for
support. If you need help with Tailscale, please email
support@tailscale.com. We don't provide support via Github issues. -->
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Version information:**
- Device: [e.g. iPhone X, laptop]
- OS: [e.g. Windows, MacOS]
- OS version: [e.g. Windows 10, Ubuntu 18.04]
- Tailscale version: [e.g. 0.95-0]
**Additional context**
Add any other context about the problem here.

View File

@@ -2,25 +2,6 @@
name: Feature request
about: Suggest an idea for this project
title: ''
labels: ''
labels: 'needs-triage'
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always
frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or
features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

View File

@@ -1,48 +0,0 @@
name: Code Coverage
on:
push:
branches:
- main
pull_request:
branches:
- '*'
jobs:
build:
runs-on: ubuntu-latest
if: "!contains(github.event.head_commit.message, '[ci skip]')"
steps:
- name: Set up Go
uses: actions/setup-go@v1
with:
go-version: 1.16
id: go
- name: Check out code into the Go module directory
uses: actions/checkout@v1
# https://markphelps.me/2019/11/speed-up-your-go-builds-with-actions-cache/
- name: Restore Cache
uses: actions/cache@preview
id: cache
with:
path: ~/go/pkg/mod
key: ${{ runner.os }}-${{ hashFiles('**/go.sum') }}
- name: Basic build
run: go build ./cmd/...
- name: Run tests on linux with coverage data
run: go test -race -coverprofile=coverage.txt -bench=. -benchtime=1x ./...
- name: coveralls.io
uses: shogo82148/actions-goveralls@v1
env:
COVERALLS_TOKEN: ${{ secrets.COVERALLS_TOKEN }}
GITHUB_TOKEN: ${{ secrets.COVERALLS_BOT_PUBLIC_REPO_TOKEN }}
with:
path-to-profile: ./coverage.txt

34
.github/workflows/go_generate.yml vendored Normal file
View File

@@ -0,0 +1,34 @@
name: go generate
on:
push:
branches:
- main
- "release-branch/*"
pull_request:
branches:
- "*"
jobs:
check:
runs-on: ubuntu-latest
steps:
- name: Set up Go
uses: actions/setup-go@v1
with:
go-version: 1.16
- name: Check out code
uses: actions/checkout@v2
with:
fetch-depth: 0
- name: check 'go generate' is clean
run: |
mkdir gentools
go build -o gentools/stringer golang.org/x/tools/cmd/stringer
PATH="$PATH:$(pwd)/gentools" go generate ./...
echo
echo
git diff --name-only --exit-code || (echo "The files above need updating. Please run 'go generate'."; exit 1)

48
.github/workflows/linux-race.yml vendored Normal file
View File

@@ -0,0 +1,48 @@
name: Linux race
on:
push:
branches:
- main
pull_request:
branches:
- '*'
jobs:
build:
runs-on: ubuntu-latest
if: "!contains(github.event.head_commit.message, '[ci skip]')"
steps:
- name: Set up Go
uses: actions/setup-go@v1
with:
go-version: 1.16
id: go
- name: Check out code into the Go module directory
uses: actions/checkout@v1
- name: Basic build
run: go build ./cmd/...
- name: Run tests and benchmarks with -race flag on linux
run: go test -race -bench=. -benchtime=1x ./...
- 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'

View File

@@ -29,7 +29,7 @@ jobs:
run: go build ./cmd/...
- name: Run tests on linux
run: go test ./...
run: go test -bench=. -benchtime=1x ./...
- uses: k0kubun/action-slack@v2.0.0
with:

View File

@@ -29,7 +29,7 @@ jobs:
run: GOARCH=386 go build ./cmd/...
- name: Run tests on linux
run: GOARCH=386 go test ./...
run: GOARCH=386 go test -bench=. -benchtime=1x ./...
- uses: k0kubun/action-slack@v2.0.0
with:

View File

@@ -24,11 +24,23 @@ jobs:
- name: Run go vet
run: go vet ./...
- name: Print staticcheck version
run: go run honnef.co/go/tools/cmd/staticcheck -version
- name: Install staticcheck
run: "GOBIN=~/.local/bin go install honnef.co/go/tools/cmd/staticcheck"
- name: Run staticcheck
run: "go run honnef.co/go/tools/cmd/staticcheck -- $(go list ./... | grep -v tempfork)"
- name: Print staticcheck version
run: "staticcheck -version"
- name: Run staticcheck (linux/amd64)
run: "GOOS=linux GOARCH=amd64 staticcheck -- $(go list ./... | grep -v tempfork)"
- name: Run staticcheck (darwin/amd64)
run: "GOOS=darwin GOARCH=amd64 staticcheck -- $(go list ./... | grep -v tempfork)"
- name: Run staticcheck (windows/amd64)
run: "GOOS=windows GOARCH=amd64 staticcheck -- $(go list ./... | grep -v tempfork)"
- name: Run staticcheck (windows/386)
run: "GOOS=windows GOARCH=386 staticcheck -- $(go list ./... | grep -v tempfork)"
- uses: k0kubun/action-slack@v2.0.0
with:

55
.github/workflows/windows-race.yml vendored Normal file
View File

@@ -0,0 +1,55 @@
name: Windows race
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.16.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 with -race flag
# Don't use -bench=. -benchtime=1x.
# Somewhere in the layers (powershell?)
# the equals signs cause great confusion.
run: go test -race -bench . -benchtime 1x ./...
- 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'

View File

@@ -33,7 +33,10 @@ jobs:
${{ runner.os }}-go-
- name: Test
run: go test ./...
# Don't use -bench=. -benchtime=1x.
# Somewhere in the layers (powershell?)
# the equals signs cause great confusion.
run: go test -bench . -benchtime 1x ./...
- uses: k0kubun/action-slack@v2.0.0
with:

View File

@@ -0,0 +1,45 @@
name: "integration-vms"
on:
pull_request:
paths:
- "tstest/integration/vms/**"
release:
types: [ created ]
jobs:
experimental-linux-vm-test:
# To set up a new runner, see tstest/integration/vms/runner.nix
runs-on: [ self-hosted, linux, vm_integration_test ]
if: "!contains(github.event.head_commit.message, '[ci skip]')"
steps:
- name: Checkout Code
uses: actions/checkout@v1
- name: Download VM Images
run: go test ./tstest/integration/vms -run-vm-tests -run=Download -timeout=60m -no-s3
env:
XDG_CACHE_HOME: "/var/lib/ghrunner/cache"
- name: Run VM tests
run: go test ./tstest/integration/vms -v -run-vm-tests
env:
TMPDIR: "/tmp"
XDG_CACHE_HOME: "/var/lib/ghrunner/cache"
- 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'

View File

@@ -42,8 +42,7 @@ FROM golang:1.16-alpine AS build-env
WORKDIR /go/src/tailscale
COPY go.mod .
COPY go.sum .
COPY go.mod go.sum ./
RUN go mod download
COPY . .

View File

@@ -1 +1 @@
1.7.0
1.13.0

46
api.md
View File

@@ -367,10 +367,11 @@ Etag: "e0b2816b418b3f266309d94426ac7668ab3c1fa87798785bf82f1085cc2f6d9c"
#### `POST /api/v2/tailnet/:tailnet/acl` - set ACL for a tailnet
Sets the ACL for the given tailnet. HuJSON and JSON are both accepted inputs. An `If-Match` header can be set to avoid missed updates.
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.
Returns the updated ACL in JSON or HuJSON according to the `Accept` header on success. Otherwise, errors are returned for incorrectly defined ACLs, ACLs with failing tests on attempted updates, and mismatched `If-Match` header and ETag.
##### Parameters
@@ -380,7 +381,17 @@ Returns error if using an `If-Match` header and the ETag does not match.
`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)
The POST body should be a JSON or [HuJSON](https://github.com/tailscale/hujson#hujson---human-json) formatted JSON object.
An ACL policy may contain the following top-level properties:
* `Groups` - Static groups of users which can be used for ACL rules.
* `Hosts` - Hostname aliases to use in place of IP addresses or subnets.
* `ACLs` - Access control lists.
* `TagOwners` - Defines who is allowed to use which tags.
* `Tests` - Run on ACL updates to check correct functionality of defined ACLs.
See https://tailscale.com/kb/1018/acls for more information on those properties.
##### Example
```
@@ -411,7 +422,7 @@ curl 'https://api.tailscale.com/api/v2/tailnet/example.com/acl' \
}'
```
Response
Response:
```
// Example/default ACLs for unrestricted connections.
{
@@ -436,23 +447,40 @@ Response
}
```
Failed test error response:
```
{
"message": "test(s) failed",
"data": [
{
"user": "user1@example.com",
"errors": [
"address \"user2@example.com:400\": want: Accept, got: Drop"
]
}
]
}
```
<a name=tailnet-acl-preview-post></a>
#### `POST /api/v2/tailnet/:tailnet/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.
`type` - can be 'user' or 'ipport'
`previewFor` - if type=user, a user's email. If type=ipport, a IP address + port like "10.0.0.1:80".
The provided ACL is queried with this paramater to determine which rules match.
###### POST Body
ACL JSON or HuJSON (see https://tailscale.com/kb/1018/acls)
##### Example
```
POST /api/v2/tailnet/example.com/acl/preiew
curl 'https://api.tailscale.com/api/v2/tailnet/example.com/acl?user=user1@example.com' \
curl 'https://api.tailscale.com/api/v2/tailnet/example.com/acl/preview?previewFor=user1@example.com&type=user' \
-u "tskey-yourapikey123:" \
--data-binary '// Example/default ACLs for unrestricted connections.
{
@@ -477,7 +505,7 @@ curl 'https://api.tailscale.com/api/v2/tailnet/example.com/acl?user=user1@exampl
}'
```
Response
Response:
```
{"matches":[{"users":["*"],"ports":["*:*"],"lineNumber":19}],"user":"user1@example.com"}
```

View File

@@ -11,6 +11,36 @@
set -eu
eval $(./version/version.sh)
IFS=".$IFS" read -r major minor patch <VERSION.txt
git_hash=$(git rev-parse HEAD)
if ! git diff-index --quiet HEAD; then
git_hash="${git_hash}-dirty"
fi
base_hash=$(git rev-list --max-count=1 HEAD -- VERSION.txt)
change_count=$(git rev-list --count HEAD "^$base_hash")
short_hash=$(echo "$git_hash" | cut -c1-9)
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}" "$@"
if expr "$minor" : "[0-9]*[13579]$" >/dev/null; then
patch="$change_count"
change_suffix=""
elif [ "$change_count" != "0" ]; then
change_suffix="-$change_count"
else
change_suffix=""
fi
long_suffix="$change_suffix-t$short_hash"
SHORT="$major.$minor.$patch"
LONG="${SHORT}$long_suffix"
GIT_HASH="$git_hash"
if [ "$1" = "shellvars" ]; then
cat <<EOF
VERSION_SHORT="$SHORT"
VERSION_LONG="$LONG"
VERSION_GIT_HASH="$GIT_HASH"
EOF
exit 0
fi
exec go build -ldflags "-X tailscale.com/version.Long=${LONG} -X tailscale.com/version.Short=${SHORT} -X tailscale.com/version.GitCommit=${GIT_HASH}" "$@"

View File

@@ -25,7 +25,7 @@
set -eu
eval $(./version/version.sh)
eval $(./build_dist.sh shellvars)
docker build \
--build-arg VERSION_LONG=$VERSION_LONG \

View File

@@ -0,0 +1,29 @@
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package apitype contains types for the Tailscale local API.
package apitype
import "tailscale.com/tailcfg"
// WhoIsResponse is the JSON type returned by tailscaled debug server's /whois?ip=$IP handler.
type WhoIsResponse struct {
Node *tailcfg.Node
UserProfile *tailcfg.UserProfile
}
// FileTarget is a node to which files can be sent, and the PeerAPI
// URL base to do so via.
type FileTarget struct {
Node *tailcfg.Node
// PeerAPI is the http://ip:port URL base of the node's peer API,
// without any path (not even a single slash).
PeerAPIURL string
}
type WaitingFile struct {
Name string
Size int64
}

View File

@@ -6,6 +6,7 @@
package tailscale
import (
"bytes"
"context"
"encoding/json"
"errors"
@@ -18,6 +19,8 @@ import (
"strconv"
"strings"
"tailscale.com/client/tailscale/apitype"
"tailscale.com/ipn"
"tailscale.com/ipn/ipnstate"
"tailscale.com/paths"
"tailscale.com/safesocket"
@@ -64,9 +67,22 @@ func DoLocalRequest(req *http.Request) (*http.Response, error) {
return tsClient.Do(req)
}
// WhoIs returns the owner of the remoteAddr, which must be an IP or IP:port.
func WhoIs(ctx context.Context, remoteAddr string) (*tailcfg.WhoIsResponse, error) {
req, err := http.NewRequestWithContext(ctx, "GET", "http://local-tailscaled.sock/localapi/v0/whois?addr="+url.QueryEscape(remoteAddr), nil)
type errorJSON struct {
Error string
}
// bestError returns either err, or if body contains a valid JSON
// object of type errorJSON, its non-empty error body.
func bestError(err error, body []byte) error {
var j errorJSON
if err := json.Unmarshal(body, &j); err == nil && j.Error != "" {
return errors.New(j.Error)
}
return err
}
func send(ctx context.Context, method, path string, wantStatus int, body io.Reader) ([]byte, error) {
req, err := http.NewRequestWithContext(ctx, method, "http://local-tailscaled.sock"+path, body)
if err != nil {
return nil, err
}
@@ -75,60 +91,48 @@ func WhoIs(ctx context.Context, remoteAddr string) (*tailcfg.WhoIsResponse, erro
return nil, err
}
defer res.Body.Close()
slurp, _ := ioutil.ReadAll(res.Body)
if res.StatusCode != 200 {
return nil, fmt.Errorf("HTTP %s: %s", res.Status, slurp)
slurp, err := ioutil.ReadAll(res.Body)
if err != nil {
return nil, err
}
r := new(tailcfg.WhoIsResponse)
if err := json.Unmarshal(slurp, r); err != nil {
if max := 200; len(slurp) > max {
slurp = slurp[:max]
if res.StatusCode != wantStatus {
err := fmt.Errorf("HTTP %s: %s (expected %v)", res.Status, slurp, wantStatus)
return nil, bestError(err, slurp)
}
return slurp, nil
}
func get200(ctx context.Context, path string) ([]byte, error) {
return send(ctx, "GET", path, 200, nil)
}
// WhoIs returns the owner of the remoteAddr, which must be an IP or IP:port.
func WhoIs(ctx context.Context, remoteAddr string) (*apitype.WhoIsResponse, error) {
body, err := get200(ctx, "/localapi/v0/whois?addr="+url.QueryEscape(remoteAddr))
if err != nil {
return nil, err
}
r := new(apitype.WhoIsResponse)
if err := json.Unmarshal(body, r); err != nil {
if max := 200; len(body) > max {
body = append(body[:max], "..."...)
}
return nil, fmt.Errorf("failed to parse JSON WhoIsResponse from %q", slurp)
return nil, fmt.Errorf("failed to parse JSON WhoIsResponse from %q", body)
}
return r, nil
}
// Goroutines returns a dump of the Tailscale daemon's current goroutines.
func Goroutines(ctx context.Context) ([]byte, error) {
req, err := http.NewRequestWithContext(ctx, "GET", "http://local-tailscaled.sock/localapi/v0/goroutines", nil)
if err != nil {
return nil, err
}
res, err := DoLocalRequest(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
body, err := ioutil.ReadAll(res.Body)
if err != nil {
return nil, err
}
if res.StatusCode != 200 {
return nil, fmt.Errorf("HTTP %s: %s", res.Status, body)
}
return body, nil
return get200(ctx, "/localapi/v0/goroutines")
}
// BugReport logs and returns a log marker that can be shared by the user with support.
func BugReport(ctx context.Context, note string) (string, error) {
u := fmt.Sprintf("http://local-tailscaled.sock/localapi/v0/bugreport?note=%s", url.QueryEscape(note))
req, err := http.NewRequestWithContext(ctx, "POST", u, nil)
body, err := send(ctx, "POST", "/localapi/v0/bugreport?note="+url.QueryEscape(note), 200, nil)
if err != nil {
return "", err
}
res, err := DoLocalRequest(req)
if err != nil {
return "", err
}
defer res.Body.Close()
body, err := ioutil.ReadAll(res.Body)
if err != nil {
return "", err
}
if res.StatusCode != 200 {
return "", fmt.Errorf("HTTP %s: %s", res.Status, body)
}
return strings.TrimSpace(string(body)), nil
}
@@ -143,67 +147,32 @@ func StatusWithoutPeers(ctx context.Context) (*ipnstate.Status, error) {
}
func status(ctx context.Context, queryString string) (*ipnstate.Status, error) {
req, err := http.NewRequestWithContext(ctx, "GET", "http://local-tailscaled.sock/localapi/v0/status"+queryString, nil)
body, err := get200(ctx, "/localapi/v0/status"+queryString)
if err != nil {
return nil, err
}
res, err := DoLocalRequest(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
if res.StatusCode != 200 {
body, _ := ioutil.ReadAll(res.Body)
return nil, fmt.Errorf("HTTP %s: %s", res.Status, body)
}
st := new(ipnstate.Status)
if err := json.NewDecoder(res.Body).Decode(st); err != nil {
if err := json.Unmarshal(body, st); err != nil {
return nil, err
}
return st, nil
}
type WaitingFile struct {
Name string
Size int64
}
func WaitingFiles(ctx context.Context) ([]WaitingFile, error) {
req, err := http.NewRequestWithContext(ctx, "GET", "http://local-tailscaled.sock/localapi/v0/files/", nil)
func WaitingFiles(ctx context.Context) ([]apitype.WaitingFile, error) {
body, err := get200(ctx, "/localapi/v0/files/")
if err != nil {
return nil, err
}
res, err := DoLocalRequest(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
if res.StatusCode != 200 {
body, _ := ioutil.ReadAll(res.Body)
return nil, fmt.Errorf("HTTP %s: %s", res.Status, body)
}
var wfs []WaitingFile
if err := json.NewDecoder(res.Body).Decode(&wfs); err != nil {
var wfs []apitype.WaitingFile
if err := json.Unmarshal(body, &wfs); err != nil {
return nil, err
}
return wfs, nil
}
func DeleteWaitingFile(ctx context.Context, baseName string) error {
req, err := http.NewRequestWithContext(ctx, "DELETE", "http://local-tailscaled.sock/localapi/v0/files/"+url.PathEscape(baseName), nil)
if err != nil {
return err
}
res, err := DoLocalRequest(req)
if err != nil {
return err
}
defer res.Body.Close()
if res.StatusCode != http.StatusNoContent {
body, _ := ioutil.ReadAll(res.Body)
return fmt.Errorf("expected 204 No Content; got HTTP %s: %s", res.Status, body)
}
return nil
_, err := send(ctx, "DELETE", "/localapi/v0/files/"+url.PathEscape(baseName), http.StatusNoContent, nil)
return err
}
func GetWaitingFile(ctx context.Context, baseName string) (rc io.ReadCloser, size int64, err error) {
@@ -227,25 +196,27 @@ func GetWaitingFile(ctx context.Context, baseName string) (rc io.ReadCloser, siz
return res.Body, res.ContentLength, nil
}
func FileTargets(ctx context.Context) ([]apitype.FileTarget, error) {
body, err := get200(ctx, "/localapi/v0/file-targets")
if err != nil {
return nil, err
}
var fts []apitype.FileTarget
if err := json.Unmarshal(body, &fts); err != nil {
return nil, fmt.Errorf("invalid JSON: %w", err)
}
return fts, nil
}
func CheckIPForwarding(ctx context.Context) error {
req, err := http.NewRequestWithContext(ctx, "GET", "http://local-tailscaled.sock/localapi/v0/check-ip-forwarding", nil)
body, err := get200(ctx, "/localapi/v0/check-ip-forwarding")
if err != nil {
return err
}
res, err := DoLocalRequest(req)
if err != nil {
return err
}
defer res.Body.Close()
if res.StatusCode != 200 {
body, _ := ioutil.ReadAll(res.Body)
res.Body.Close()
return fmt.Errorf("HTTP %s: %s", res.Status, body)
}
var jres struct {
Warning string
}
if err := json.NewDecoder(res.Body).Decode(&jres); err != nil {
if err := json.Unmarshal(body, &jres); err != nil {
return fmt.Errorf("invalid JSON from check-ip-forwarding: %w", err)
}
if jres.Warning != "" {
@@ -253,3 +224,72 @@ func CheckIPForwarding(ctx context.Context) error {
}
return nil
}
func GetPrefs(ctx context.Context) (*ipn.Prefs, error) {
body, err := get200(ctx, "/localapi/v0/prefs")
if err != nil {
return nil, err
}
var p ipn.Prefs
if err := json.Unmarshal(body, &p); err != nil {
return nil, fmt.Errorf("invalid prefs JSON: %w", err)
}
return &p, nil
}
func EditPrefs(ctx context.Context, mp *ipn.MaskedPrefs) (*ipn.Prefs, error) {
mpj, err := json.Marshal(mp)
if err != nil {
return nil, err
}
body, err := send(ctx, "PATCH", "/localapi/v0/prefs", http.StatusOK, bytes.NewReader(mpj))
if err != nil {
return nil, err
}
var p ipn.Prefs
if err := json.Unmarshal(body, &p); err != nil {
return nil, fmt.Errorf("invalid prefs JSON: %w", err)
}
return &p, nil
}
func Logout(ctx context.Context) error {
_, err := send(ctx, "POST", "/localapi/v0/logout", http.StatusNoContent, nil)
return err
}
// SetDNS adds a DNS TXT record for the given domain name, containing
// the provided TXT value. The intended use case is answering
// LetsEncrypt/ACME dns-01 challenges.
//
// The control plane will only permit SetDNS requests with very
// specific names and values. The name should be
// "_acme-challenge." + your node's MagicDNS name. It's expected that
// clients cache the certs from LetsEncrypt (or whichever CA is
// providing them) and only request new ones as needed; the control plane
// rate limits SetDNS requests.
//
// This is a low-level interface; it's expected that most Tailscale
// users use a higher level interface to getting/using TLS
// certificates.
func SetDNS(ctx context.Context, name, value string) error {
v := url.Values{}
v.Set("name", name)
v.Set("value", value)
_, err := send(ctx, "POST", "/localapi/v0/set-dns?"+v.Encode(), 200, nil)
return err
}
// CurrentDERPMap returns the current DERPMap that is being used by the local tailscaled.
// It is intended to be used with netcheck to see availability of DERPs.
func CurrentDERPMap(ctx context.Context) (*tailcfg.DERPMap, error) {
var derpMap tailcfg.DERPMap
res, err := send(ctx, "GET", "/localapi/v0/derpmap", 200, nil)
if err != nil {
return nil, err
}
if err = json.Unmarshal(res, &derpMap); err != nil {
return nil, fmt.Errorf("invalid derp map json: %w", err)
}
return &derpMap, nil
}

77
cmd/addlicense/main.go Normal file
View File

@@ -0,0 +1,77 @@
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Program addlicense adds a license header to a file.
// It is intended for use with 'go generate',
// so it has a slightly weird usage.
package main
import (
"flag"
"fmt"
"os"
"os/exec"
)
var (
year = flag.Int("year", 0, "copyright year")
file = flag.String("file", "", "file to modify")
)
func usage() {
fmt.Fprintf(os.Stderr, `
usage: addlicense -year YEAR -file FILE <subcommand args...>
`[1:])
flag.PrintDefaults()
fmt.Fprintf(os.Stderr, `
addlicense adds a Tailscale license to the beginning of file,
using year as the copyright year.
It is intended for use with 'go generate', so it also runs a subcommand,
which presumably creates the file.
Sample usage:
addlicense -year 2021 -file pull_strings.go stringer -type=pull
`[1:])
os.Exit(2)
}
func main() {
flag.Usage = usage
flag.Parse()
if len(flag.Args()) == 0 {
flag.Usage()
}
cmd := exec.Command(flag.Arg(0), flag.Args()[1:]...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
err := cmd.Run()
check(err)
b, err := os.ReadFile(*file)
check(err)
f, err := os.OpenFile(*file, os.O_TRUNC|os.O_WRONLY, 0644)
check(err)
_, err = fmt.Fprintf(f, license, *year)
check(err)
_, err = f.Write(b)
check(err)
err = f.Close()
check(err)
}
func check(err error) {
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
var license = `
// Copyright (c) %d 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.
`[1:]

View File

@@ -246,7 +246,9 @@ func gen(buf *bytes.Buffer, imports map[string]struct{}, name string, typ *types
writef("\t\tdst.%s[k] = append([]%s{}, src.%s[k]...)", fname, n, fname)
writef("\t}")
} else if containsPointers(ft.Elem()) {
writef("\t\t" + `panic("TODO map value pointers")`)
writef("\tfor k, v := range src.%s {", fname)
writef("\t\tdst.%s[k] = v.Clone()", fname)
writef("\t}")
} else {
writef("\tfor k, v := range src.%s {", fname)
writef("\t\tdst.%s[k] = v", fname)

View File

@@ -12,8 +12,6 @@ import (
"errors"
"expvar"
"flag"
"fmt"
"html"
"io"
"io/ioutil"
"log"
@@ -35,7 +33,6 @@ import (
"tailscale.com/tsweb"
"tailscale.com/types/key"
"tailscale.com/types/wgkey"
"tailscale.com/version"
)
var (
@@ -49,6 +46,7 @@ var (
meshPSKFile = flag.String("mesh-psk-file", defaultMeshPSKFile(), "if non-empty, path to file containing the mesh pre-shared key file. It should contain some hex string; whitespace is trimmed.")
meshWith = flag.String("mesh-with", "", "optional comma-separated list of hostnames to mesh with; the server's own hostname can be in the list")
bootstrapDNS = flag.String("bootstrap-dns-names", "", "optional comma-separated list of hostnames to make available at /bootstrap-dns")
verifyClients = flag.Bool("verify-clients", false, "verify clients to this DERP server through a local tailscaled instance.")
)
type config struct {
@@ -60,7 +58,12 @@ func loadConfig() config {
return config{PrivateKey: mustNewKey()}
}
if *configPath == "" {
log.Fatalf("derper: -c <config path> not specified")
if os.Getuid() == 0 {
*configPath = "/var/lib/derper/derper.key"
} else {
log.Fatalf("derper: -c <config path> not specified")
}
log.Printf("no config path specified; using %s", *configPath)
}
b, err := ioutil.ReadFile(*configPath)
switch {
@@ -125,6 +128,7 @@ func main() {
letsEncrypt := tsweb.IsProd443(*addr)
s := derp.NewServer(key.Private(cfg.PrivateKey), log.Printf)
s.SetVerifyClient(*verifyClients)
if *meshPSKFile != "" {
b, err := ioutil.ReadFile(*meshPSKFile)
@@ -143,8 +147,7 @@ func main() {
}
expvar.Publish("derp", s.ExpVar())
// Create our own mux so we don't expose /debug/ stuff to the world.
mux := tsweb.NewMux(debugHandler(s))
mux := http.NewServeMux()
mux.Handle("/derp", derphttp.Handler(s))
go refreshBootstrapDNSLoop()
mux.HandleFunc("/bootstrap-dns", handleBootstrapDNS)
@@ -164,6 +167,18 @@ func main() {
io.WriteString(w, "<p>Debug info at <a href='/debug/'>/debug/</a>.</p>\n")
}
}))
debug := tsweb.Debugger(mux)
debug.KV("TLS hostname", *hostname)
debug.KV("Mesh key", s.HasMeshKey())
debug.Handle("check", "Consistency check", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
err := s.ConsistencyCheck()
if err != nil {
http.Error(w, err.Error(), 500)
} else {
io.WriteString(w, "derp.Server ConsistencyCheck okay")
}
}))
debug.Handle("traffic", "Traffic check", http.HandlerFunc(s.ServeDebugTraffic))
if *runSTUN {
go serveSTUN()
@@ -217,39 +232,6 @@ func main() {
}
}
func debugHandler(s *derp.Server) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.RequestURI == "/debug/check" {
err := s.ConsistencyCheck()
if err != nil {
http.Error(w, err.Error(), 500)
} else {
io.WriteString(w, "derp.Server ConsistencyCheck okay")
}
return
}
f := func(format string, args ...interface{}) { fmt.Fprintf(w, format, args...) }
f(`<html><body>
<h1>DERP debug</h1>
<ul>
`)
f("<li><b>Hostname:</b> %v</li>\n", html.EscapeString(*hostname))
f("<li><b>Uptime:</b> %v</li>\n", tsweb.Uptime())
f("<li><b>Mesh Key:</b> %v</li>\n", s.HasMeshKey())
f("<li><b>Version:</b> %v</li>\n", html.EscapeString(version.Long))
f(`<li><a href="/debug/vars">/debug/vars</a> (Go)</li>
<li><a href="/debug/varz">/debug/varz</a> (Prometheus)</li>
<li><a href="/debug/pprof/">/debug/pprof/</a></li>
<li><a href="/debug/pprof/goroutine?debug=1">/debug/pprof/goroutine</a> (collapsed)</li>
<li><a href="/debug/pprof/goroutine?debug=2">/debug/pprof/goroutine</a> (full)</li>
<li><a href="/debug/check">/debug/check</a> internal consistency check</li>
<ul>
</html>
`)
})
}
func serveSTUN() {
pc, err := net.ListenPacket("udp", ":3478")
if err != nil {

View File

@@ -9,7 +9,9 @@ import (
"errors"
"fmt"
"log"
"net"
"strings"
"time"
"tailscale.com/derp"
"tailscale.com/derp/derphttp"
@@ -39,6 +41,34 @@ func startMeshWithHost(s *derp.Server, host string) error {
return err
}
c.MeshKey = s.MeshKey()
// For meshed peers within a region, connect via VPC addresses.
c.SetURLDialer(func(ctx context.Context, network, addr string) (net.Conn, error) {
host, port, err := net.SplitHostPort(addr)
if err != nil {
return nil, err
}
var d net.Dialer
var r net.Resolver
if port == "443" && strings.HasSuffix(host, ".tailscale.com") {
base := strings.TrimSuffix(host, ".tailscale.com")
subCtx, cancel := context.WithTimeout(ctx, 2*time.Second)
defer cancel()
vpcHost := base + "-vpc.tailscale.com"
ips, _ := r.LookupIP(subCtx, "ip", vpcHost)
if len(ips) > 0 {
vpcAddr := net.JoinHostPort(ips[0].String(), port)
c, err := d.DialContext(subCtx, network, vpcAddr)
if err == nil {
log.Printf("connected to %v (%v) instead of %v", vpcHost, ips[0], base)
return c, nil
}
log.Printf("failed to connect to %v (%v): %v; trying non-VPC route", vpcHost, ips[0], err)
}
}
return d.DialContext(ctx, network, addr)
})
add := func(k key.Public) { s.AddPacketForwarder(k, c) }
remove := func(k key.Public) { s.RemovePacketForwarder(k, c) }
go c.RunWatchConnectionLoop(context.Background(), s.PublicKey(), logf, add, remove)

354
cmd/derpprobe/derpprobe.go Normal file
View File

@@ -0,0 +1,354 @@
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// The derpprobe binary probes derpers.
package main // import "tailscale.com/cmd/derper/derpprobe"
import (
"bytes"
"context"
crand "crypto/rand"
"encoding/json"
"flag"
"fmt"
"html"
"io"
"log"
"net/http"
"sort"
"sync"
"time"
"tailscale.com/derp"
"tailscale.com/derp/derphttp"
"tailscale.com/tailcfg"
"tailscale.com/types/key"
)
var (
derpMapURL = flag.String("derp-map", "https://login.tailscale.com/derpmap/default", "URL to DERP map (https:// or file://)")
listen = flag.String("listen", ":8030", "HTTP listen address")
)
var (
mu sync.Mutex
state = map[nodePair]pairStatus{}
lastDERPMap *tailcfg.DERPMap
lastDERPMapAt time.Time
)
func main() {
flag.Parse()
go probeLoop()
log.Fatal(http.ListenAndServe(*listen, http.HandlerFunc(serve)))
}
type overallStatus struct {
good, bad []string
}
func (st *overallStatus) addBadf(format string, a ...interface{}) {
st.bad = append(st.bad, fmt.Sprintf(format, a...))
}
func (st *overallStatus) addGoodf(format string, a ...interface{}) {
st.good = append(st.good, fmt.Sprintf(format, a...))
}
func getOverallStatus() (o overallStatus) {
mu.Lock()
defer mu.Unlock()
if lastDERPMap == nil {
o.addBadf("no DERP map")
return
}
now := time.Now()
if age := now.Sub(lastDERPMapAt); age > time.Minute {
o.addBadf("DERPMap hasn't been successfully refreshed in %v", age.Round(time.Second))
}
for _, reg := range sortedRegions(lastDERPMap) {
for _, from := range reg.Nodes {
for _, to := range reg.Nodes {
pair := nodePair{from.Name, to.Name}
st, ok := state[pair]
age := now.Sub(st.at).Round(time.Second)
switch {
case !ok:
o.addBadf("no state for %v", pair)
case st.err != nil:
o.addBadf("%v: %v", pair, st.err)
case age > 90*time.Second:
o.addBadf("%v: update is %v old", pair, age)
default:
o.addGoodf("%v: %v, %v ago", pair, st.latency.Round(time.Millisecond), age)
}
}
}
}
return
}
func serve(w http.ResponseWriter, r *http.Request) {
st := getOverallStatus()
summary := "All good"
if len(st.bad) > 0 {
w.WriteHeader(500)
summary = fmt.Sprintf("%d problems", len(st.bad))
}
io.WriteString(w, "<html><head><style>.bad { font-weight: bold; color: #700; }</style></head>\n")
fmt.Fprintf(w, "<body><h1>derp probe</h1>\n%s:<ul>", summary)
for _, s := range st.bad {
fmt.Fprintf(w, "<li class=bad>%s</li>\n", html.EscapeString(s))
}
for _, s := range st.good {
fmt.Fprintf(w, "<li>%s</li>\n", html.EscapeString(s))
}
io.WriteString(w, "</ul></body></html>\n")
}
func sortedRegions(dm *tailcfg.DERPMap) []*tailcfg.DERPRegion {
ret := make([]*tailcfg.DERPRegion, 0, len(dm.Regions))
for _, r := range dm.Regions {
ret = append(ret, r)
}
sort.Slice(ret, func(i, j int) bool { return ret[i].RegionID < ret[j].RegionID })
return ret
}
type nodePair struct {
from, to string // DERPNode.Name
}
func (p nodePair) String() string { return fmt.Sprintf("(%s→%s)", p.from, p.to) }
type pairStatus struct {
err error
latency time.Duration
at time.Time
}
func setDERPMap(dm *tailcfg.DERPMap) {
mu.Lock()
defer mu.Unlock()
lastDERPMap = dm
lastDERPMapAt = time.Now()
}
func setState(p nodePair, latency time.Duration, err error) {
mu.Lock()
defer mu.Unlock()
st := pairStatus{
err: err,
latency: latency,
at: time.Now(),
}
state[p] = st
if err != nil {
log.Printf("%+v error: %v", p, err)
} else {
log.Printf("%+v: %v", p, latency.Round(time.Millisecond))
}
}
func probeLoop() {
ticker := time.NewTicker(15 * time.Second)
for {
err := probe()
if err != nil {
log.Printf("probe: %v", err)
}
<-ticker.C
}
}
func probe() error {
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel()
dm, err := getDERPMap(ctx)
if err != nil {
return err
}
var wg sync.WaitGroup
wg.Add(len(dm.Regions))
for _, reg := range dm.Regions {
reg := reg
go func() {
defer wg.Done()
for _, from := range reg.Nodes {
for _, to := range reg.Nodes {
latency, err := probeNodePair(ctx, dm, from, to)
setState(nodePair{from.Name, to.Name}, latency, err)
}
}
}()
}
wg.Wait()
return ctx.Err()
}
func probeNodePair(ctx context.Context, dm *tailcfg.DERPMap, from, to *tailcfg.DERPNode) (latency time.Duration, err error) {
// The passed in context is a minute for the whole region. The
// idea is that each node pair in the region will be done
// serially and regularly in the future, reusing connections
// (at least in the happy path). For now they don't reuse
// connections and probe at most once every 15 seconds. We
// bound the duration of a single node pair within a region
// so one bad one can't starve others.
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
fromc, err := newConn(ctx, dm, from)
if err != nil {
return 0, err
}
defer fromc.Close()
toc, err := newConn(ctx, dm, to)
if err != nil {
return 0, err
}
defer toc.Close()
// Wait a bit for from's node to hear about to existing on the
// other node in the region, in the case where the two nodes
// are different.
if from.Name != to.Name {
time.Sleep(100 * time.Millisecond) // pretty arbitrary
}
// Make a random packet
pkt := make([]byte, 8)
crand.Read(pkt)
t0 := time.Now()
// Send the random packet.
sendc := make(chan error, 1)
go func() {
sendc <- fromc.Send(toc.SelfPublicKey(), pkt)
}()
select {
case <-ctx.Done():
return 0, fmt.Errorf("timeout sending via %q: %w", from.Name, ctx.Err())
case err := <-sendc:
if err != nil {
return 0, fmt.Errorf("error sending via %q: %w", from.Name, err)
}
}
// Receive the random packet.
recvc := make(chan interface{}, 1) // either derp.ReceivedPacket or error
go func() {
for {
m, err := toc.Recv()
if err != nil {
recvc <- err
return
}
switch v := m.(type) {
case derp.ReceivedPacket:
recvc <- v
default:
log.Printf("%v: ignoring Recv frame type %T", to.Name, v)
// Loop.
}
}
}()
select {
case <-ctx.Done():
return 0, fmt.Errorf("timeout receiving from %q: %w", to.Name, ctx.Err())
case v := <-recvc:
if err, ok := v.(error); ok {
return 0, fmt.Errorf("error receiving from %q: %w", to.Name, err)
}
p := v.(derp.ReceivedPacket)
if p.Source != fromc.SelfPublicKey() {
return 0, fmt.Errorf("got data packet from unexpected source, %v", p.Source)
}
if !bytes.Equal(p.Data, pkt) {
return 0, fmt.Errorf("unexpected data packet %q", p.Data)
}
}
return time.Since(t0), nil
}
func newConn(ctx context.Context, dm *tailcfg.DERPMap, n *tailcfg.DERPNode) (*derphttp.Client, error) {
priv := key.NewPrivate()
dc := derphttp.NewRegionClient(priv, log.Printf, func() *tailcfg.DERPRegion {
rid := n.RegionID
return &tailcfg.DERPRegion{
RegionID: rid,
RegionCode: fmt.Sprintf("%s-%s", dm.Regions[rid].RegionCode, n.Name),
RegionName: dm.Regions[rid].RegionName,
Nodes: []*tailcfg.DERPNode{n},
}
})
dc.IsProber = true
err := dc.Connect(ctx)
if err != nil {
return nil, err
}
errc := make(chan error, 1)
go func() {
m, err := dc.Recv()
if err != nil {
errc <- err
return
}
switch m.(type) {
case derp.ServerInfoMessage:
errc <- nil
default:
errc <- fmt.Errorf("unexpected first message type %T", errc)
}
}()
select {
case err := <-errc:
if err != nil {
go dc.Close()
return nil, err
}
case <-ctx.Done():
go dc.Close()
return nil, fmt.Errorf("timeout waiting for ServerInfoMessage: %w", ctx.Err())
}
return dc, nil
}
var httpOrFileClient = &http.Client{Transport: httpOrFileTransport()}
func httpOrFileTransport() http.RoundTripper {
tr := http.DefaultTransport.(*http.Transport).Clone()
tr.RegisterProtocol("file", http.NewFileTransport(http.Dir("/")))
return tr
}
func getDERPMap(ctx context.Context) (*tailcfg.DERPMap, error) {
req, err := http.NewRequestWithContext(ctx, "GET", *derpMapURL, nil)
if err != nil {
return nil, err
}
res, err := httpOrFileClient.Do(req)
if err != nil {
mu.Lock()
defer mu.Unlock()
if lastDERPMap != nil && time.Since(lastDERPMapAt) < 10*time.Minute {
// Assume that control is restarting and use
// the same one for a bit.
return lastDERPMap, nil
}
return nil, err
}
defer res.Body.Close()
if res.StatusCode != 200 {
return nil, fmt.Errorf("fetching %s: %s", *derpMapURL, res.Status)
}
dm := new(tailcfg.DERPMap)
if err := json.NewDecoder(res.Body).Decode(dm); err != nil {
return nil, fmt.Errorf("decoding %s JSON: %v", *derpMapURL, err)
}
setDERPMap(dm)
return dm, nil
}

View File

@@ -18,7 +18,7 @@ import (
"strings"
"tailscale.com/client/tailscale"
"tailscale.com/tailcfg"
"tailscale.com/client/tailscale/apitype"
)
var (
@@ -107,18 +107,18 @@ type tmplData struct {
IP string // "100.2.3.4"
}
func tailscaleIP(who *tailcfg.WhoIsResponse) string {
func tailscaleIP(who *apitype.WhoIsResponse) string {
if who == nil {
return ""
}
for _, nodeIP := range who.Node.Addresses {
if nodeIP.IP.Is4() && nodeIP.IsSingleIP() {
return nodeIP.IP.String()
if nodeIP.IP().Is4() && nodeIP.IsSingleIP() {
return nodeIP.IP().String()
}
}
for _, nodeIP := range who.Node.Addresses {
if nodeIP.IsSingleIP() {
return nodeIP.IP.String()
return nodeIP.IP().String()
}
}
return ""

View File

@@ -55,9 +55,13 @@ func main() {
log.Fatalf("Couldn't parse URL %q: %v", *goVarsURL, err)
}
mux := tsweb.NewMux(http.HandlerFunc(debugHandler))
mux := http.NewServeMux()
tsweb.Debugger(mux) // registers /debug/*
mux.Handle("/metrics", tsweb.Protected(proxy))
mux.Handle("/varz", tsweb.Protected(tsweb.StdHandler(&goVarsHandler{*goVarsURL}, log.Printf)))
mux.Handle("/varz", tsweb.Protected(tsweb.StdHandler(&goVarsHandler{*goVarsURL}, tsweb.HandlerOptions{
Quiet200s: true,
Logf: log.Printf,
})))
ch := &certHolder{
hostname: *hostname,
@@ -167,23 +171,3 @@ func (c *certHolder) loadLocked() error {
c.loaded = time.Now()
return nil
}
// debugHandler serves a page with links to tsweb-managed debug URLs
// at /debug/.
func debugHandler(w http.ResponseWriter, r *http.Request) {
f := func(format string, args ...interface{}) { fmt.Fprintf(w, format, args...) }
f(`<html><body>
<h1>microproxy debug</h1>
<ul>
`)
f("<li><b>Hostname:</b> %v</li>\n", *hostname)
f("<li><b>Uptime:</b> %v</li>\n", tsweb.Uptime())
f(`<li><a href="/debug/vars">/debug/vars</a> (Go)</li>
<li><a href="/debug/varz">/debug/varz</a> (Prometheus)</li>
<li><a href="/debug/pprof/">/debug/pprof/</a></li>
<li><a href="/debug/pprof/goroutine?debug=1">/debug/pprof/goroutine</a> (collapsed)</li>
<li><a href="/debug/pprof/goroutine?debug=2">/debug/pprof/goroutine</a> (full)</li>
<ul>
</html>
`)
}

View File

@@ -21,6 +21,9 @@ import (
// into a map of filePathOnDisk -> filePathInPackage.
func parseFiles(s string) (map[string]string, error) {
ret := map[string]string{}
if len(s) == 0 {
return ret, nil
}
for _, f := range strings.Split(s, ",") {
fs := strings.Split(f, ":")
if len(fs) != 2 {

121
cmd/speedtest/speedtest.go Normal file
View File

@@ -0,0 +1,121 @@
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Program speedtest provides the speedtest command. The reason to keep it separate from
// the normal tailscale cli is because it is not yet ready to go in the tailscale binary.
// It will be included in the tailscale cli after it has been added to tailscaled.
// Example usage for client command: go run cmd/speedtest -host 127.0.0.1:20333 -t 5s
// This will connect to the server on 127.0.0.1:20333 and start a 5 second download speedtest.
// Example usage for server command: go run cmd/speedtest -s -host :20333
// This will start a speedtest server on port 20333.
package main
import (
"context"
"errors"
"flag"
"fmt"
"net"
"os"
"strconv"
"text/tabwriter"
"time"
"github.com/peterbourgon/ff/v2/ffcli"
"tailscale.com/net/speedtest"
)
// Runs the speedtest command as a commandline program
func main() {
args := os.Args[1:]
if err := speedtestCmd.Parse(args); err != nil {
fmt.Fprintln(os.Stderr, err.Error())
os.Exit(1)
}
err := speedtestCmd.Run(context.Background())
if errors.Is(err, flag.ErrHelp) {
fmt.Fprintln(os.Stderr, speedtestCmd.ShortUsage)
os.Exit(2)
}
if err != nil {
fmt.Fprintln(os.Stderr, err.Error())
os.Exit(1)
}
}
// speedtestCmd is the root command. It runs either the server or client depending on the
// flags passed to it.
var speedtestCmd = &ffcli.Command{
Name: "speedtest",
ShortUsage: "speedtest [-host <host:port>] [-s] [-r] [-t <test duration>]",
ShortHelp: "Run a speed test",
FlagSet: (func() *flag.FlagSet {
fs := flag.NewFlagSet("speedtest", flag.ExitOnError)
fs.StringVar(&speedtestArgs.host, "host", ":20333", "host:port pair to connect to or listen on")
fs.DurationVar(&speedtestArgs.testDuration, "t", speedtest.DefaultDuration, "duration of the speed test")
fs.BoolVar(&speedtestArgs.runServer, "s", false, "run a speedtest server")
fs.BoolVar(&speedtestArgs.reverse, "r", false, "run in reverse mode (server sends, client receives)")
return fs
})(),
Exec: runSpeedtest,
}
var speedtestArgs struct {
host string
testDuration time.Duration
runServer bool
reverse bool
}
func runSpeedtest(ctx context.Context, args []string) error {
if _, _, err := net.SplitHostPort(speedtestArgs.host); err != nil {
var addrErr *net.AddrError
if errors.As(err, &addrErr) && addrErr.Err == "missing port in address" {
// if no port is provided, append the default port
speedtestArgs.host = net.JoinHostPort(speedtestArgs.host, strconv.Itoa(speedtest.DefaultPort))
}
}
if speedtestArgs.runServer {
listener, err := net.Listen("tcp", speedtestArgs.host)
if err != nil {
return err
}
fmt.Printf("listening on %v\n", listener.Addr())
return speedtest.Serve(listener)
}
// Ensure the duration is within the allowed range
if speedtestArgs.testDuration < speedtest.MinDuration || speedtestArgs.testDuration > speedtest.MaxDuration {
return fmt.Errorf("test duration must be within %v and %v", speedtest.MinDuration, speedtest.MaxDuration)
}
dir := speedtest.Download
if speedtestArgs.reverse {
dir = speedtest.Upload
}
fmt.Printf("Starting a %s test with %s\n", dir, speedtestArgs.host)
results, err := speedtest.RunClient(dir, speedtestArgs.testDuration, speedtestArgs.host)
if err != nil {
return err
}
w := tabwriter.NewWriter(os.Stdout, 12, 0, 0, ' ', tabwriter.TabIndent)
fmt.Println("Results:")
fmt.Fprintln(w, "Interval\t\tTransfer\t\tBandwidth\t\t")
for _, r := range results {
if r.Total {
fmt.Fprintln(w, "-------------------------------------------------------------------------")
}
fmt.Fprintf(w, "%.2f-%.2f\tsec\t%.4f\tMBits\t%.4f\tMbits/sec\t\n", r.IntervalStart.Seconds(), r.IntervalEnd.Seconds(), r.MegaBits(), r.MBitsPerSecond())
}
w.Flush()
return nil
}

155
cmd/tailscale/cli/admin.go Normal file
View File

@@ -0,0 +1,155 @@
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package cli
import (
"context"
"errors"
"flag"
"fmt"
"io"
"net/http"
"os"
"strings"
"github.com/dsnet/golib/jsonfmt"
"github.com/peterbourgon/ff/v2/ffcli"
)
const tailscaleAPIURL = "https://api.tailscale.com/api"
var adminCmd = &ffcli.Command{
Name: "admin",
ShortUsage: "admin <subcommand> [command flags]",
ShortHelp: "Administrate a tailnet",
LongHelp: strings.TrimSpace(`
The "tailscale admin" command administrates a tailnet through the CLI.
It is a wrapper over the RESTful API served at ` + tailscaleAPIURL + `.
See https://github.com/tailscale/tailscale/blob/main/api.md for more information
about the API itself.
In order for the "admin" command to call the API, it needs an API key,
which is specified by setting the TAILSCALE_API_KEY environment variable.
Also, to easy usage, the tailnet to administrate can be specified through the
TAILSCALE_NET_NAME environment variable, or specified with the -tailnet flag.
Visit https://login.tailscale.com/admin/settings/authkeys in order to obtain
an API key.
`),
FlagSet: (func() *flag.FlagSet {
fs := flag.NewFlagSet("status", flag.ExitOnError)
// TODO(dsnet): Can we determine the default tailnet from what this
// device is currently part of? Alternatively, when add specific logic
// to handle auth keys, we can always associate a given key with a
// specific tailnet.
fs.StringVar(&adminArgs.tailnet, "tailnet", os.Getenv("TAILSCALE_NET_NAME"), "which tailnet to administrate")
return fs
})(),
// TODO(dsnet): Handle users, groups, dns.
Subcommands: []*ffcli.Command{{
Name: "acl",
ShortUsage: "acl <subcommand> [command flags]",
ShortHelp: "Manage the ACL for a tailnet",
// TODO(dsnet): Handle preview.
Subcommands: []*ffcli.Command{{
Name: "get",
ShortUsage: "get",
ShortHelp: "Downloads the HuJSON ACL file to stdout",
Exec: checkAdminKey(runAdminACLGet),
}, {
Name: "set",
ShortUsage: "set",
ShortHelp: "Uploads the HuJSON ACL file from stdin",
Exec: checkAdminKey(runAdminACLSet),
}},
Exec: runHelp,
}, {
Name: "devices",
ShortUsage: "devices <subcommand> [command flags]",
ShortHelp: "Manage devices in a tailnet",
Subcommands: []*ffcli.Command{{
Name: "list",
ShortUsage: "list",
ShortHelp: "List all devices in a tailnet",
Exec: checkAdminKey(runAdminDevicesList),
}, {
Name: "get",
ShortUsage: "get <id>",
ShortHelp: "Get information about a specific device",
Exec: checkAdminKey(runAdminDevicesGet),
}},
Exec: runHelp,
}},
Exec: runHelp,
}
var adminArgs struct {
tailnet string // which tailnet to operate upon
}
func checkAdminKey(f func(context.Context, string, []string) error) func(context.Context, []string) error {
return func(ctx context.Context, args []string) error {
// TODO(dsnet): We should have a subcommand or flag to manage keys.
// Use of an environment variable is a temporary hack.
key := os.Getenv("TAILSCALE_API_KEY")
if !strings.HasPrefix(key, "tskey-") {
return errors.New("no API key specified")
}
return f(ctx, key, args)
}
}
func runAdminACLGet(ctx context.Context, key string, args []string) error {
if len(args) > 0 {
return flag.ErrHelp
}
return adminCallAPI(ctx, key, http.MethodGet, "/v2/tailnet/"+adminArgs.tailnet+"/acl", nil, os.Stdout)
}
func runAdminACLSet(ctx context.Context, key string, args []string) error {
if len(args) > 0 {
return flag.ErrHelp
}
return adminCallAPI(ctx, key, http.MethodPost, "/v2/tailnet/"+adminArgs.tailnet+"/acl", os.Stdin, os.Stdout)
}
func runAdminDevicesList(ctx context.Context, key string, args []string) error {
if len(args) > 0 {
return flag.ErrHelp
}
return adminCallAPI(ctx, key, http.MethodGet, "/v2/tailnet/"+adminArgs.tailnet+"/devices", nil, os.Stdout)
}
func runAdminDevicesGet(ctx context.Context, key string, args []string) error {
if len(args) != 1 {
return flag.ErrHelp
}
return adminCallAPI(ctx, key, http.MethodGet, "/v2/device/"+args[0], nil, os.Stdout)
}
func adminCallAPI(ctx context.Context, key, method, path string, in io.Reader, out io.Writer) error {
req, err := http.NewRequestWithContext(ctx, method, tailscaleAPIURL+path, in)
req.SetBasicAuth(key, "")
if err != nil {
return fmt.Errorf("failed to create request: %w", err)
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return fmt.Errorf("failed to send HTTP request: %w", err)
}
defer resp.Body.Close()
b, err := io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("failed to receive HTTP response: %w", err)
}
b, err = jsonfmt.Format(b)
if err != nil {
return fmt.Errorf("failed to format JSON response: %w", err)
}
_, err = out.Write(b)
return err
}

View File

@@ -0,0 +1,57 @@
<html>
<head>
<title>Redirecting...</title>
<style>
html,
body {
height: 100%;
}
html {
background-color: rgb(249, 247, 246);
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
line-height: 1.5;
-webkit-text-size-adjust: 100%;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
body {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.spinner {
margin-bottom: 2rem;
border: 4px rgba(112, 110, 109, 0.5) solid;
border-left-color: transparent;
border-radius: 9999px;
width: 4rem;
height: 4rem;
-webkit-animation: spin 700ms linear infinite;
animation: spin 800ms linear infinite;
}
.label {
color: rgb(112, 110, 109);
padding-left: 0.4rem;
}
@-webkit-keyframes spin {
to {
transform: rotate(360deg);
}
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
</style>
</head> <body>
<div class="spinner"></div>
<div class="label">Redirecting...</div>
</body>

View File

@@ -8,13 +8,16 @@ package cli
import (
"context"
"errors"
"flag"
"fmt"
"io"
"log"
"net"
"os"
"os/signal"
"runtime"
"strconv"
"strings"
"syscall"
"text/tabwriter"
@@ -24,24 +27,59 @@ import (
"tailscale.com/ipn"
"tailscale.com/paths"
"tailscale.com/safesocket"
"tailscale.com/syncs"
)
// ActLikeCLI reports whether a GUI application should act like the
// CLI based on os.Args, GOOS, the context the process is running in
// (pty, parent PID), etc.
func ActLikeCLI() bool {
if len(os.Args) < 2 {
// This function is only used on macOS.
if runtime.GOOS != "darwin" {
return false
}
switch os.Args[1] {
case "up", "down", "status", "netcheck", "ping", "version",
"debug",
"-V", "--version", "-h", "--help":
// Escape hatch to let people force running the macOS
// GUI Tailscale binary as the CLI.
if v, _ := strconv.ParseBool(os.Getenv("TAILSCALE_BE_CLI")); v {
return true
}
// If our parent is launchd, we're definitely not
// being run as a CLI.
if os.Getppid() == 1 {
return false
}
// Xcode adds the -NSDocumentRevisionsDebugMode flag on execution.
// If present, we are almost certainly being run as a GUI.
for _, arg := range os.Args {
if arg == "-NSDocumentRevisionsDebugMode" {
return false
}
}
// Looking at the environment of the GUI Tailscale app (ps eww
// $PID), empirically none of these environment variables are
// present. But all or some of these should be present with
// Terminal.all and bash or zsh.
for _, e := range []string{
"SHLVL",
"TERM",
"TERM_PROGRAM",
"PS1",
} {
if os.Getenv(e) != "" {
return true
}
}
return false
}
func runHelp(context.Context, []string) error {
return flag.ErrHelp
}
// Run runs the CLI. The args do not include the binary name.
func Run(args []string) error {
if len(args) == 1 && (args[0] == "-V" || args[0] == "--version") {
@@ -64,17 +102,19 @@ change in the future.
Subcommands: []*ffcli.Command{
upCmd,
downCmd,
logoutCmd,
adminCmd,
netcheckCmd,
ipCmd,
statusCmd,
pingCmd,
versionCmd,
webCmd,
pushCmd,
fileCmd,
bugReportCmd,
},
FlagSet: rootfs,
Exec: func(context.Context, []string) error { return flag.ErrHelp },
Exec: runHelp,
UsageFunc: usageFunc,
}
for _, c := range rootCmd.Subcommands {
@@ -108,6 +148,8 @@ var rootArgs struct {
socket string
}
var gotSignal syncs.AtomicBool
func connect(ctx context.Context) (net.Conn, *ipn.BackendClient, context.Context, context.CancelFunc) {
c, err := safesocket.Connect(rootArgs.socket, 41112)
if err != nil {
@@ -125,7 +167,14 @@ func connect(ctx context.Context) (net.Conn, *ipn.BackendClient, context.Context
go func() {
interrupt := make(chan os.Signal, 1)
signal.Notify(interrupt, syscall.SIGINT, syscall.SIGTERM)
<-interrupt
select {
case <-interrupt:
case <-ctx.Done():
// Context canceled elsewhere.
signal.Reset(syscall.SIGINT, syscall.SIGTERM)
return
}
gotSignal.Set(true)
c.Close()
cancel()
}()
@@ -135,19 +184,22 @@ func connect(ctx context.Context) (net.Conn, *ipn.BackendClient, context.Context
}
// pump receives backend messages on conn and pushes them into bc.
func pump(ctx context.Context, bc *ipn.BackendClient, conn net.Conn) {
func pump(ctx context.Context, bc *ipn.BackendClient, conn net.Conn) error {
defer conn.Close()
for ctx.Err() == nil {
msg, err := ipn.ReadMsg(conn)
if err != nil {
if ctx.Err() != nil {
return
return ctx.Err()
}
log.Printf("ReadMsg: %v\n", err)
break
if errors.Is(err, io.EOF) || errors.Is(err, net.ErrClosed) {
return fmt.Errorf("%w (tailscaled stopped running?)", err)
}
return err
}
bc.GotNotifyMsg(msg)
}
return ctx.Err()
}
func strSliceContains(ss []string, s string) bool {

View File

@@ -0,0 +1,798 @@
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package cli
import (
"bytes"
"encoding/json"
"flag"
"fmt"
"reflect"
"strings"
"testing"
"github.com/google/go-cmp/cmp"
"inet.af/netaddr"
"tailscale.com/ipn"
"tailscale.com/ipn/ipnstate"
"tailscale.com/types/persist"
"tailscale.com/types/preftype"
)
// geese is a collection of gooses. It need not be complete.
// But it should include anything handled specially (e.g. linux, windows)
// and at least one thing that's not (darwin, freebsd).
var geese = []string{"linux", "darwin", "windows", "freebsd"}
// Test that checkForAccidentalSettingReverts's updateMaskedPrefsFromUpFlag can handle
// all flags. This will panic if a new flag creeps in that's unhandled.
//
// Also, issue 1880: advertise-exit-node was being ignored. Verify that all flags cause an edit.
func TestUpdateMaskedPrefsFromUpFlag(t *testing.T) {
for _, goos := range geese {
var upArgs upArgsT
fs := newUpFlagSet(goos, &upArgs)
fs.VisitAll(func(f *flag.Flag) {
mp := new(ipn.MaskedPrefs)
updateMaskedPrefsFromUpFlag(mp, f.Name)
got := mp.Pretty()
wantEmpty := preflessFlag(f.Name)
isEmpty := got == "MaskedPrefs{}"
if isEmpty != wantEmpty {
t.Errorf("flag %q created MaskedPrefs %s; want empty=%v", f.Name, got, wantEmpty)
}
})
}
}
func TestCheckForAccidentalSettingReverts(t *testing.T) {
tests := []struct {
name string
flags []string // argv to be parsed by FlagSet
curPrefs *ipn.Prefs
curExitNodeIP netaddr.IP
curUser string // os.Getenv("USER") on the client side
goos string // empty means "linux"
want string
}{
{
name: "bare_up_means_up",
flags: []string{},
curPrefs: &ipn.Prefs{
ControlURL: ipn.DefaultControlURL,
WantRunning: false,
Hostname: "foo",
},
want: "",
},
{
name: "losing_hostname",
flags: []string{"--accept-dns"},
curPrefs: &ipn.Prefs{
ControlURL: ipn.DefaultControlURL,
WantRunning: false,
Hostname: "foo",
CorpDNS: true,
NetfilterMode: preftype.NetfilterOn,
AllowSingleHosts: true,
},
want: accidentalUpPrefix + " --accept-dns --hostname=foo",
},
{
name: "hostname_changing_explicitly",
flags: []string{"--hostname=bar"},
curPrefs: &ipn.Prefs{
ControlURL: ipn.DefaultControlURL,
CorpDNS: true,
NetfilterMode: preftype.NetfilterOn,
AllowSingleHosts: true,
Hostname: "foo",
},
want: "",
},
{
name: "hostname_changing_empty_explicitly",
flags: []string{"--hostname="},
curPrefs: &ipn.Prefs{
ControlURL: ipn.DefaultControlURL,
CorpDNS: true,
NetfilterMode: preftype.NetfilterOn,
AllowSingleHosts: true,
Hostname: "foo",
},
want: "",
},
{
// Issue 1725: "tailscale up --authkey=..." (or other non-empty flags) works from
// a fresh server's initial prefs.
name: "up_with_default_prefs",
flags: []string{"--authkey=foosdlkfjskdljf"},
curPrefs: ipn.NewPrefs(),
want: "",
},
{
name: "implicit_operator_change",
flags: []string{"--hostname=foo"},
curPrefs: &ipn.Prefs{
ControlURL: ipn.DefaultControlURL,
OperatorUser: "alice",
AllowSingleHosts: true,
CorpDNS: true,
NetfilterMode: preftype.NetfilterOn,
},
curUser: "eve",
want: accidentalUpPrefix + " --hostname=foo --operator=alice",
},
{
name: "implicit_operator_matches_shell_user",
flags: []string{"--hostname=foo"},
curPrefs: &ipn.Prefs{
ControlURL: ipn.DefaultControlURL,
AllowSingleHosts: true,
CorpDNS: true,
NetfilterMode: preftype.NetfilterOn,
OperatorUser: "alice",
},
curUser: "alice",
want: "",
},
{
name: "error_advertised_routes_exit_node_removed",
flags: []string{"--advertise-routes=10.0.42.0/24"},
curPrefs: &ipn.Prefs{
ControlURL: ipn.DefaultControlURL,
AllowSingleHosts: true,
CorpDNS: true,
NetfilterMode: preftype.NetfilterOn,
AdvertiseRoutes: []netaddr.IPPrefix{
netaddr.MustParseIPPrefix("10.0.42.0/24"),
netaddr.MustParseIPPrefix("0.0.0.0/0"),
netaddr.MustParseIPPrefix("::/0"),
},
},
want: accidentalUpPrefix + " --advertise-routes=10.0.42.0/24 --advertise-exit-node",
},
{
name: "advertised_routes_exit_node_removed_explicit",
flags: []string{"--advertise-routes=10.0.42.0/24", "--advertise-exit-node=false"},
curPrefs: &ipn.Prefs{
ControlURL: ipn.DefaultControlURL,
AllowSingleHosts: true,
CorpDNS: true,
NetfilterMode: preftype.NetfilterOn,
AdvertiseRoutes: []netaddr.IPPrefix{
netaddr.MustParseIPPrefix("10.0.42.0/24"),
netaddr.MustParseIPPrefix("0.0.0.0/0"),
netaddr.MustParseIPPrefix("::/0"),
},
},
want: "",
},
{
name: "advertised_routes_includes_the_0_routes", // but no --advertise-exit-node
flags: []string{"--advertise-routes=11.1.43.0/24,0.0.0.0/0,::/0"},
curPrefs: &ipn.Prefs{
ControlURL: ipn.DefaultControlURL,
AllowSingleHosts: true,
CorpDNS: true,
NetfilterMode: preftype.NetfilterOn,
AdvertiseRoutes: []netaddr.IPPrefix{
netaddr.MustParseIPPrefix("10.0.42.0/24"),
netaddr.MustParseIPPrefix("0.0.0.0/0"),
netaddr.MustParseIPPrefix("::/0"),
},
},
want: "",
},
{
name: "advertise_exit_node", // Issue 1859
flags: []string{"--advertise-exit-node"},
curPrefs: &ipn.Prefs{
ControlURL: ipn.DefaultControlURL,
AllowSingleHosts: true,
CorpDNS: true,
NetfilterMode: preftype.NetfilterOn,
},
want: "",
},
{
name: "advertise_exit_node_over_existing_routes",
flags: []string{"--advertise-exit-node"},
curPrefs: &ipn.Prefs{
ControlURL: ipn.DefaultControlURL,
AllowSingleHosts: true,
CorpDNS: true,
NetfilterMode: preftype.NetfilterOn,
AdvertiseRoutes: []netaddr.IPPrefix{
netaddr.MustParseIPPrefix("1.2.0.0/16"),
},
},
want: accidentalUpPrefix + " --advertise-exit-node --advertise-routes=1.2.0.0/16",
},
{
name: "advertise_exit_node_over_existing_routes_and_exit_node",
flags: []string{"--advertise-exit-node"},
curPrefs: &ipn.Prefs{
ControlURL: ipn.DefaultControlURL,
AllowSingleHosts: true,
CorpDNS: true,
NetfilterMode: preftype.NetfilterOn,
AdvertiseRoutes: []netaddr.IPPrefix{
netaddr.MustParseIPPrefix("0.0.0.0/0"),
netaddr.MustParseIPPrefix("::/0"),
netaddr.MustParseIPPrefix("1.2.0.0/16"),
},
},
want: accidentalUpPrefix + " --advertise-exit-node --advertise-routes=1.2.0.0/16",
},
{
name: "exit_node_clearing", // Issue 1777
flags: []string{"--exit-node="},
curPrefs: &ipn.Prefs{
ControlURL: ipn.DefaultControlURL,
AllowSingleHosts: true,
CorpDNS: true,
NetfilterMode: preftype.NetfilterOn,
ExitNodeID: "fooID",
},
want: "",
},
{
name: "remove_all_implicit",
flags: []string{"--force-reauth"},
curPrefs: &ipn.Prefs{
WantRunning: true,
ControlURL: ipn.DefaultControlURL,
RouteAll: true,
AllowSingleHosts: false,
ExitNodeIP: netaddr.MustParseIP("100.64.5.6"),
CorpDNS: false,
ShieldsUp: true,
AdvertiseTags: []string{"tag:foo", "tag:bar"},
Hostname: "myhostname",
ForceDaemon: true,
AdvertiseRoutes: []netaddr.IPPrefix{
netaddr.MustParseIPPrefix("10.0.0.0/16"),
netaddr.MustParseIPPrefix("0.0.0.0/0"),
netaddr.MustParseIPPrefix("::/0"),
},
NetfilterMode: preftype.NetfilterNoDivert,
OperatorUser: "alice",
},
curUser: "eve",
want: accidentalUpPrefix + " --force-reauth --accept-dns=false --accept-routes --advertise-exit-node --advertise-routes=10.0.0.0/16 --advertise-tags=tag:foo,tag:bar --exit-node=100.64.5.6 --host-routes=false --hostname=myhostname --netfilter-mode=nodivert --operator=alice --shields-up",
},
{
name: "remove_all_implicit_except_hostname",
flags: []string{"--hostname=newhostname"},
curPrefs: &ipn.Prefs{
WantRunning: true,
ControlURL: ipn.DefaultControlURL,
RouteAll: true,
AllowSingleHosts: false,
ExitNodeIP: netaddr.MustParseIP("100.64.5.6"),
CorpDNS: false,
ShieldsUp: true,
AdvertiseTags: []string{"tag:foo", "tag:bar"},
Hostname: "myhostname",
ForceDaemon: true,
AdvertiseRoutes: []netaddr.IPPrefix{
netaddr.MustParseIPPrefix("10.0.0.0/16"),
},
NetfilterMode: preftype.NetfilterNoDivert,
OperatorUser: "alice",
},
curUser: "eve",
want: accidentalUpPrefix + " --hostname=newhostname --accept-dns=false --accept-routes --advertise-routes=10.0.0.0/16 --advertise-tags=tag:foo,tag:bar --exit-node=100.64.5.6 --host-routes=false --netfilter-mode=nodivert --operator=alice --shields-up",
},
{
name: "loggedout_is_implicit",
flags: []string{"--hostname=foo"},
curPrefs: &ipn.Prefs{
ControlURL: ipn.DefaultControlURL,
LoggedOut: true,
AllowSingleHosts: true,
CorpDNS: true,
NetfilterMode: preftype.NetfilterOn,
},
want: "", // not an error. LoggedOut is implicit.
},
{
// Test that a pre-1.8 version of Tailscale with bogus NoSNAT pref
// values is able to enable exit nodes without warnings.
name: "make_windows_exit_node",
flags: []string{"--advertise-exit-node"},
curPrefs: &ipn.Prefs{
ControlURL: ipn.DefaultControlURL,
AllowSingleHosts: true,
CorpDNS: true,
// And assume this no-op accidental pre-1.8 value:
NoSNAT: true,
},
goos: "windows",
want: "", // not an error
},
{
name: "ignore_netfilter_change_non_linux",
flags: []string{"--accept-dns"},
curPrefs: &ipn.Prefs{
ControlURL: ipn.DefaultControlURL,
AllowSingleHosts: true,
NetfilterMode: preftype.NetfilterNoDivert, // we never had this bug, but pretend it got set non-zero on Windows somehow
},
goos: "windows",
want: "", // not an error
},
{
name: "operator_losing_routes_step1", // https://twitter.com/EXPbits/status/1390418145047887877
flags: []string{"--operator=expbits"},
curPrefs: &ipn.Prefs{
ControlURL: ipn.DefaultControlURL,
AllowSingleHosts: true,
CorpDNS: true,
NetfilterMode: preftype.NetfilterOn,
AdvertiseRoutes: []netaddr.IPPrefix{
netaddr.MustParseIPPrefix("0.0.0.0/0"),
netaddr.MustParseIPPrefix("::/0"),
netaddr.MustParseIPPrefix("1.2.0.0/16"),
},
},
want: accidentalUpPrefix + " --operator=expbits --advertise-exit-node --advertise-routes=1.2.0.0/16",
},
{
name: "operator_losing_routes_step2", // https://twitter.com/EXPbits/status/1390418145047887877
flags: []string{"--operator=expbits", "--advertise-routes=1.2.0.0/16"},
curPrefs: &ipn.Prefs{
ControlURL: ipn.DefaultControlURL,
AllowSingleHosts: true,
CorpDNS: true,
NetfilterMode: preftype.NetfilterOn,
AdvertiseRoutes: []netaddr.IPPrefix{
netaddr.MustParseIPPrefix("0.0.0.0/0"),
netaddr.MustParseIPPrefix("::/0"),
netaddr.MustParseIPPrefix("1.2.0.0/16"),
},
},
want: accidentalUpPrefix + " --advertise-routes=1.2.0.0/16 --operator=expbits --advertise-exit-node",
},
{
name: "errors_preserve_explicit_flags",
flags: []string{"--reset", "--force-reauth=false", "--authkey=secretrand"},
curPrefs: &ipn.Prefs{
ControlURL: ipn.DefaultControlURL,
WantRunning: false,
CorpDNS: true,
NetfilterMode: preftype.NetfilterOn,
AllowSingleHosts: true,
Hostname: "foo",
},
want: accidentalUpPrefix + " --authkey=secretrand --force-reauth=false --reset --hostname=foo",
},
{
name: "error_exit_node_omit_with_ip_pref",
flags: []string{"--hostname=foo"},
curPrefs: &ipn.Prefs{
ControlURL: ipn.DefaultControlURL,
AllowSingleHosts: true,
CorpDNS: true,
NetfilterMode: preftype.NetfilterOn,
ExitNodeIP: netaddr.MustParseIP("100.64.5.4"),
},
want: accidentalUpPrefix + " --hostname=foo --exit-node=100.64.5.4",
},
{
name: "error_exit_node_omit_with_id_pref",
flags: []string{"--hostname=foo"},
curExitNodeIP: netaddr.MustParseIP("100.64.5.7"),
curPrefs: &ipn.Prefs{
ControlURL: ipn.DefaultControlURL,
AllowSingleHosts: true,
CorpDNS: true,
NetfilterMode: preftype.NetfilterOn,
ExitNodeID: "some_stable_id",
},
want: accidentalUpPrefix + " --hostname=foo --exit-node=100.64.5.7",
},
{
name: "ignore_login_server_synonym",
flags: []string{"--login-server=https://controlplane.tailscale.com"},
curPrefs: &ipn.Prefs{
ControlURL: "https://login.tailscale.com",
AllowSingleHosts: true,
CorpDNS: true,
NetfilterMode: preftype.NetfilterOn,
},
want: "", // not an error
},
{
name: "ignore_login_server_synonym_on_other_change",
flags: []string{"--netfilter-mode=off"},
curPrefs: &ipn.Prefs{
ControlURL: "https://login.tailscale.com",
AllowSingleHosts: true,
CorpDNS: false,
NetfilterMode: preftype.NetfilterOn,
},
want: accidentalUpPrefix + " --netfilter-mode=off --accept-dns=false",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
goos := "linux"
if tt.goos != "" {
goos = tt.goos
}
var upArgs upArgsT
flagSet := newUpFlagSet(goos, &upArgs)
flagSet.Parse(tt.flags)
newPrefs, err := prefsFromUpArgs(upArgs, t.Logf, new(ipnstate.Status), goos)
if err != nil {
t.Fatal(err)
}
applyImplicitPrefs(newPrefs, tt.curPrefs, tt.curUser)
var got string
if err := checkForAccidentalSettingReverts(newPrefs, tt.curPrefs, upCheckEnv{
goos: goos,
flagSet: flagSet,
curExitNodeIP: tt.curExitNodeIP,
}); err != nil {
got = err.Error()
}
if strings.TrimSpace(got) != tt.want {
t.Errorf("unexpected result\n got: %s\nwant: %s\n", got, tt.want)
}
})
}
}
func upArgsFromOSArgs(goos string, flagArgs ...string) (args upArgsT) {
fs := newUpFlagSet(goos, &args)
fs.Parse(flagArgs) // populates args
return
}
func TestPrefsFromUpArgs(t *testing.T) {
tests := []struct {
name string
args upArgsT
goos string // runtime.GOOS; empty means linux
st *ipnstate.Status // or nil
want *ipn.Prefs
wantErr string
wantWarn string
}{
{
name: "default_linux",
goos: "linux",
args: upArgsFromOSArgs("linux"),
want: &ipn.Prefs{
ControlURL: ipn.DefaultControlURL,
WantRunning: true,
NoSNAT: false,
NetfilterMode: preftype.NetfilterOn,
CorpDNS: true,
AllowSingleHosts: true,
},
},
{
name: "default_windows",
goos: "windows",
args: upArgsFromOSArgs("windows"),
want: &ipn.Prefs{
ControlURL: ipn.DefaultControlURL,
WantRunning: true,
CorpDNS: true,
AllowSingleHosts: true,
NetfilterMode: preftype.NetfilterOn,
},
},
{
name: "advertise_default_route",
args: upArgsFromOSArgs("linux", "--advertise-exit-node"),
want: &ipn.Prefs{
ControlURL: ipn.DefaultControlURL,
WantRunning: true,
AllowSingleHosts: true,
CorpDNS: true,
AdvertiseRoutes: []netaddr.IPPrefix{
netaddr.MustParseIPPrefix("0.0.0.0/0"),
netaddr.MustParseIPPrefix("::/0"),
},
NetfilterMode: preftype.NetfilterOn,
},
},
{
name: "error_advertise_route_invalid_ip",
args: upArgsT{
advertiseRoutes: "foo",
},
wantErr: `"foo" is not a valid IP address or CIDR prefix`,
},
{
name: "error_advertise_route_unmasked_bits",
args: upArgsT{
advertiseRoutes: "1.2.3.4/16",
},
wantErr: `1.2.3.4/16 has non-address bits set; expected 1.2.0.0/16`,
},
{
name: "error_exit_node_bad_ip",
args: upArgsT{
exitNodeIP: "foo",
},
wantErr: `invalid IP address "foo" for --exit-node: ParseIP("foo"): unable to parse IP`,
},
{
name: "error_exit_node_allow_lan_without_exit_node",
args: upArgsT{
exitNodeAllowLANAccess: true,
},
wantErr: `--exit-node-allow-lan-access can only be used with --exit-node`,
},
{
name: "error_tag_prefix",
args: upArgsT{
advertiseTags: "foo",
},
wantErr: `tag: "foo": tags must start with 'tag:'`,
},
{
name: "error_long_hostname",
args: upArgsT{
hostname: strings.Repeat("a", 300),
},
wantErr: `hostname too long: 300 bytes (max 256)`,
},
{
name: "error_linux_netfilter_empty",
args: upArgsT{
netfilterMode: "",
},
wantErr: `invalid value --netfilter-mode=""`,
},
{
name: "error_linux_netfilter_bogus",
args: upArgsT{
netfilterMode: "bogus",
},
wantErr: `invalid value --netfilter-mode="bogus"`,
},
{
name: "error_exit_node_ip_is_self_ip",
args: upArgsT{
exitNodeIP: "100.105.106.107",
},
st: &ipnstate.Status{
TailscaleIPs: []netaddr.IP{netaddr.MustParseIP("100.105.106.107")},
},
wantErr: `cannot use 100.105.106.107 as the exit node as it is a local IP address to this machine, did you mean --advertise-exit-node?`,
},
{
name: "warn_linux_netfilter_nodivert",
goos: "linux",
args: upArgsT{
netfilterMode: "nodivert",
},
wantWarn: "netfilter=nodivert; add iptables calls to ts-* chains manually.",
want: &ipn.Prefs{
WantRunning: true,
NetfilterMode: preftype.NetfilterNoDivert,
NoSNAT: true,
},
},
{
name: "warn_linux_netfilter_off",
goos: "linux",
args: upArgsT{
netfilterMode: "off",
},
wantWarn: "netfilter=off; configure iptables yourself.",
want: &ipn.Prefs{
WantRunning: true,
NetfilterMode: preftype.NetfilterOff,
NoSNAT: true,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var warnBuf bytes.Buffer
warnf := func(format string, a ...interface{}) {
fmt.Fprintf(&warnBuf, format, a...)
}
goos := tt.goos
if goos == "" {
goos = "linux"
}
st := tt.st
if st == nil {
st = new(ipnstate.Status)
}
got, err := prefsFromUpArgs(tt.args, warnf, st, goos)
gotErr := fmt.Sprint(err)
if tt.wantErr != "" {
if tt.wantErr != gotErr {
t.Errorf("wrong error.\n got error: %v\nwant error: %v\n", gotErr, tt.wantErr)
}
return
}
if err != nil {
t.Fatal(err)
}
if tt.want == nil {
t.Fatal("tt.want is nil")
}
if !got.Equals(tt.want) {
jgot, _ := json.MarshalIndent(got, "", "\t")
jwant, _ := json.MarshalIndent(tt.want, "", "\t")
if bytes.Equal(jgot, jwant) {
t.Logf("prefs differ only in non-JSON-visible ways (nil/non-nil zero-length arrays)")
}
t.Errorf("wrong prefs\n got: %s\nwant: %s\n\ngot: %s\nwant: %s\n",
got.Pretty(), tt.want.Pretty(),
jgot, jwant,
)
}
})
}
}
func TestPrefFlagMapping(t *testing.T) {
prefHasFlag := map[string]bool{}
for _, pv := range prefsOfFlag {
for _, pref := range pv {
prefHasFlag[pref] = true
}
}
prefType := reflect.TypeOf(ipn.Prefs{})
for i := 0; i < prefType.NumField(); i++ {
prefName := prefType.Field(i).Name
if prefHasFlag[prefName] {
continue
}
switch prefName {
case "WantRunning", "Persist", "LoggedOut":
// All explicitly handled (ignored) by checkForAccidentalSettingReverts.
continue
case "OSVersion", "DeviceModel":
// Only used by Android, which doesn't have a CLI mode anyway, so
// fine to not map.
continue
case "NotepadURLs":
// TODO(bradfitz): https://github.com/tailscale/tailscale/issues/1830
continue
}
t.Errorf("unexpected new ipn.Pref field %q is not handled by up.go (see addPrefFlagMapping and checkForAccidentalSettingReverts)", prefName)
}
}
func TestFlagAppliesToOS(t *testing.T) {
for _, goos := range geese {
var upArgs upArgsT
fs := newUpFlagSet(goos, &upArgs)
fs.VisitAll(func(f *flag.Flag) {
if !flagAppliesToOS(f.Name, goos) {
t.Errorf("flagAppliesToOS(%q, %q) = false but found in %s set", f.Name, goos, goos)
}
})
}
}
func TestUpdatePrefs(t *testing.T) {
tests := []struct {
name string
flags []string // argv to be parsed into env.flagSet and env.upArgs
curPrefs *ipn.Prefs
env upCheckEnv // empty goos means "linux"
wantSimpleUp bool
wantJustEditMP *ipn.MaskedPrefs
wantErrSubtr string
}{
{
name: "bare_up_means_up",
flags: []string{},
curPrefs: &ipn.Prefs{
ControlURL: ipn.DefaultControlURL,
WantRunning: false,
Hostname: "foo",
},
},
{
name: "just_up",
flags: []string{},
curPrefs: &ipn.Prefs{
ControlURL: ipn.DefaultControlURL,
Persist: &persist.Persist{LoginName: "crawshaw.github"},
},
env: upCheckEnv{
backendState: "Stopped",
},
wantSimpleUp: true,
},
{
name: "just_edit",
flags: []string{},
curPrefs: &ipn.Prefs{
ControlURL: ipn.DefaultControlURL,
Persist: &persist.Persist{LoginName: "crawshaw.github"},
},
env: upCheckEnv{backendState: "Running"},
wantSimpleUp: true,
wantJustEditMP: &ipn.MaskedPrefs{WantRunningSet: true},
},
{
name: "control_synonym",
flags: []string{},
curPrefs: &ipn.Prefs{
ControlURL: "https://login.tailscale.com",
Persist: &persist.Persist{LoginName: "crawshaw.github"},
},
env: upCheckEnv{backendState: "Running"},
wantSimpleUp: true,
wantJustEditMP: &ipn.MaskedPrefs{WantRunningSet: true},
},
{
name: "change_login_server",
flags: []string{"--login-server=https://localhost:1000"},
curPrefs: &ipn.Prefs{
ControlURL: "https://login.tailscale.com",
Persist: &persist.Persist{LoginName: "crawshaw.github"},
AllowSingleHosts: true,
CorpDNS: true,
NetfilterMode: preftype.NetfilterOn,
},
env: upCheckEnv{backendState: "Running"},
wantSimpleUp: true,
wantJustEditMP: &ipn.MaskedPrefs{WantRunningSet: true},
wantErrSubtr: "can't change --login-server without --force-reauth",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.env.goos == "" {
tt.env.goos = "linux"
}
tt.env.flagSet = newUpFlagSet(tt.env.goos, &tt.env.upArgs)
tt.env.flagSet.Parse(tt.flags)
newPrefs, err := prefsFromUpArgs(tt.env.upArgs, t.Logf, new(ipnstate.Status), tt.env.goos)
if err != nil {
t.Fatal(err)
}
simpleUp, justEditMP, err := updatePrefs(newPrefs, tt.curPrefs, tt.env)
if err != nil {
if tt.wantErrSubtr != "" {
if !strings.Contains(err.Error(), tt.wantErrSubtr) {
t.Fatalf("want error %q, got: %v", tt.wantErrSubtr, err)
}
return
}
t.Fatal(err)
}
if simpleUp != tt.wantSimpleUp {
t.Fatalf("simpleUp=%v, want %v", simpleUp, tt.wantSimpleUp)
}
if justEditMP != nil {
justEditMP.Prefs = ipn.Prefs{} // uninteresting
}
if !reflect.DeepEqual(justEditMP, tt.wantJustEditMP) {
t.Fatalf("justEditMP: %v", cmp.Diff(justEditMP, tt.wantJustEditMP))
}
})
}
}

View File

@@ -13,11 +13,14 @@ import (
"io"
"log"
"os"
"runtime"
"strings"
"github.com/peterbourgon/ff/v2/ffcli"
"tailscale.com/client/tailscale"
"tailscale.com/ipn"
"tailscale.com/paths"
"tailscale.com/safesocket"
)
var debugCmd = &ffcli.Command{
@@ -27,23 +30,57 @@ var debugCmd = &ffcli.Command{
fs := flag.NewFlagSet("debug", flag.ExitOnError)
fs.BoolVar(&debugArgs.goroutines, "daemon-goroutines", false, "If true, dump the tailscaled daemon's goroutines")
fs.BoolVar(&debugArgs.ipn, "ipn", false, "If true, subscribe to IPN notifications")
fs.BoolVar(&debugArgs.prefs, "prefs", false, "If true, dump active prefs")
fs.BoolVar(&debugArgs.derpMap, "derp", false, "If true, dump DERP map")
fs.BoolVar(&debugArgs.pretty, "pretty", false, "If true, pretty-print output (for --prefs)")
fs.BoolVar(&debugArgs.netMap, "netmap", true, "whether to include netmap in --ipn mode")
fs.BoolVar(&debugArgs.localCreds, "local-creds", false, "print how to connect to local tailscaled")
fs.StringVar(&debugArgs.file, "file", "", "get, delete:NAME, or NAME")
return fs
})(),
}
var debugArgs struct {
localCreds bool
goroutines bool
ipn bool
netMap bool
derpMap bool
file string
prefs bool
pretty bool
}
func runDebug(ctx context.Context, args []string) error {
if len(args) > 0 {
return errors.New("unknown arguments")
}
if debugArgs.localCreds {
port, token, err := safesocket.LocalTCPPortAndToken()
if err == nil {
fmt.Printf("curl -u:%s http://localhost:%d/localapi/v0/status\n", token, port)
return nil
}
if runtime.GOOS == "windows" {
fmt.Printf("curl http://localhost:41112/localapi/v0/status\n")
return nil
}
fmt.Printf("curl --unix-socket %s http://foo/localapi/v0/status\n", paths.DefaultTailscaledSocket())
return nil
}
if debugArgs.prefs {
prefs, err := tailscale.GetPrefs(ctx)
if err != nil {
return err
}
if debugArgs.pretty {
fmt.Println(prefs.Pretty())
} else {
j, _ := json.MarshalIndent(prefs, "", "\t")
fmt.Println(string(j))
}
return nil
}
if debugArgs.goroutines {
goroutines, err := tailscale.Goroutines(ctx)
if err != nil {
@@ -52,6 +89,18 @@ func runDebug(ctx context.Context, args []string) error {
os.Stdout.Write(goroutines)
return nil
}
if debugArgs.derpMap {
dm, err := tailscale.CurrentDERPMap(ctx)
if err != nil {
return fmt.Errorf(
"failed to get local derp map, instead `curl %s/derpmap/default`: %w", ipn.DefaultControlURL, err,
)
}
enc := json.NewEncoder(os.Stdout)
enc.SetIndent("", "\t")
enc.Encode(dm)
return nil
}
if debugArgs.ipn {
c, bc, ctx, cancel := connect(ctx)
defer cancel()

56
cmd/tailscale/cli/diag.go Normal file
View File

@@ -0,0 +1,56 @@
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build linux || windows || darwin
// +build linux windows darwin
package cli
import (
"fmt"
"path/filepath"
"runtime"
"strings"
ps "github.com/mitchellh/go-ps"
)
// fixTailscaledConnectError is called when the local tailscaled has
// been determined unreachable due to the provided origErr value. It
// returns either the same error or a better one to help the user
// understand why tailscaled isn't running for their platform.
func fixTailscaledConnectError(origErr error) error {
procs, err := ps.Processes()
if err != nil {
return fmt.Errorf("failed to connect to local Tailscaled process and failed to enumerate processes while looking for it")
}
found := false
for _, proc := range procs {
base := filepath.Base(proc.Executable())
if base == "tailscaled" {
found = true
break
}
if runtime.GOOS == "darwin" && base == "IPNExtension" {
found = true
break
}
if runtime.GOOS == "windows" && strings.EqualFold(base, "tailscaled.exe") {
found = true
break
}
}
if !found {
switch runtime.GOOS {
case "windows":
return fmt.Errorf("failed to connect to local tailscaled process; is the Tailscale service running?")
case "darwin":
return fmt.Errorf("failed to connect to local Tailscale service; is Tailscale running?")
case "linux":
return fmt.Errorf("failed to connect to local tailscaled; it doesn't appear to be running (sudo systemctl start tailscaled ?)")
}
return fmt.Errorf("failed to connect to local tailscaled process; it doesn't appear to be running")
}
return fmt.Errorf("failed to connect to local tailscaled (which appears to be running). Got error: %w", origErr)
}

View File

@@ -0,0 +1,17 @@
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build !linux && !windows && !darwin
// +build !linux,!windows,!darwin
package cli
import "fmt"
// The github.com/mitchellh/go-ps package doesn't work on all platforms,
// so just don't diagnose connect failures.
func fixTailscaledConnectError(origErr error) error {
return fmt.Errorf("failed to connect to local tailscaled process (is it running?); got: %w", origErr)
}

View File

@@ -8,7 +8,7 @@ import (
"context"
"fmt"
"log"
"time"
"os"
"github.com/peterbourgon/ff/v2/ffcli"
"tailscale.com/client/tailscale"
@@ -33,39 +33,14 @@ func runDown(ctx context.Context, args []string) error {
return fmt.Errorf("error fetching current status: %w", err)
}
if st.BackendState == "Stopped" {
log.Printf("already stopped")
fmt.Fprintf(os.Stderr, "Tailscale was already stopped.\n")
return nil
}
log.Printf("was in state %q", st.BackendState)
c, bc, ctx, cancel := connect(ctx)
defer cancel()
timer := time.AfterFunc(5*time.Second, func() {
log.Fatalf("timeout running stop")
})
defer timer.Stop()
bc.SetNotifyCallback(func(n ipn.Notify) {
if n.ErrMessage != nil {
log.Fatal(*n.ErrMessage)
}
if n.State != nil {
log.Printf("now in state %q", *n.State)
if *n.State == ipn.Stopped {
cancel()
}
return
}
})
bc.EditPrefs(&ipn.MaskedPrefs{
_, err = tailscale.EditPrefs(ctx, &ipn.MaskedPrefs{
Prefs: ipn.Prefs{
WantRunning: false,
},
WantRunningSet: true,
})
pump(ctx, bc, c)
return nil
return err
}

436
cmd/tailscale/cli/file.go Normal file
View File

@@ -0,0 +1,436 @@
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package cli
import (
"bytes"
"context"
"errors"
"flag"
"fmt"
"io"
"io/ioutil"
"log"
"mime"
"net/http"
"net/url"
"os"
"path/filepath"
"strconv"
"strings"
"time"
"unicode/utf8"
"github.com/peterbourgon/ff/v2/ffcli"
"golang.org/x/time/rate"
"inet.af/netaddr"
"tailscale.com/client/tailscale"
"tailscale.com/client/tailscale/apitype"
"tailscale.com/ipn"
"tailscale.com/net/tsaddr"
"tailscale.com/version"
)
var fileCmd = &ffcli.Command{
Name: "file",
ShortUsage: "file <cp|get> ...",
ShortHelp: "Send or receive files",
Subcommands: []*ffcli.Command{
fileCpCmd,
fileGetCmd,
},
Exec: func(context.Context, []string) error {
// TODO(bradfitz): is there a better ffcli way to
// annotate subcommand-required commands that don't
// have an exec body of their own?
return errors.New("file subcommand required; run 'tailscale file -h' for details")
},
}
var fileCpCmd = &ffcli.Command{
Name: "cp",
ShortUsage: "file cp <files...> <target>:",
ShortHelp: "Copy file(s) to a host",
Exec: runCp,
FlagSet: (func() *flag.FlagSet {
fs := flag.NewFlagSet("cp", flag.ExitOnError)
fs.StringVar(&cpArgs.name, "name", "", "alternate filename to use, especially useful when <file> is \"-\" (stdin)")
fs.BoolVar(&cpArgs.verbose, "verbose", false, "verbose output")
fs.BoolVar(&cpArgs.targets, "targets", false, "list possible file cp targets")
return fs
})(),
}
var cpArgs struct {
name string
verbose bool
targets bool
}
func runCp(ctx context.Context, args []string) error {
if cpArgs.targets {
return runCpTargets(ctx, args)
}
if len(args) < 2 {
return errors.New("usage: tailscale file cp <files...> <target>:")
}
files, target := args[:len(args)-1], args[len(args)-1]
if !strings.HasSuffix(target, ":") {
return fmt.Errorf("final argument to 'tailscale file cp' must end in colon")
}
target = strings.TrimSuffix(target, ":")
hadBrackets := false
if strings.HasPrefix(target, "[") && strings.HasSuffix(target, "]") {
hadBrackets = true
target = strings.TrimSuffix(strings.TrimPrefix(target, "["), "]")
}
if ip, err := netaddr.ParseIP(target); err == nil && ip.Is6() && !hadBrackets {
return fmt.Errorf("an IPv6 literal must be written as [%s]", ip)
} else if hadBrackets && (err != nil || !ip.Is6()) {
return errors.New("unexpected brackets around target")
}
ip, err := tailscaleIPFromArg(ctx, target)
if err != nil {
return err
}
peerAPIBase, isOffline, err := discoverPeerAPIBase(ctx, ip)
if err != nil {
return fmt.Errorf("can't send to %s: %v", target, err)
}
if isOffline {
fmt.Fprintf(os.Stderr, "# warning: %s is offline\n", target)
}
if len(files) > 1 {
if cpArgs.name != "" {
return errors.New("can't use --name= with multiple files")
}
for _, fileArg := range files {
if fileArg == "-" {
return errors.New("can't use '-' as STDIN file when providing filename arguments")
}
}
}
for _, fileArg := range files {
var fileContents io.Reader
var name = cpArgs.name
var contentLength int64 = -1
if fileArg == "-" {
fileContents = os.Stdin
if name == "" {
name, fileContents, err = pickStdinFilename()
if err != nil {
return err
}
}
} else {
f, err := os.Open(fileArg)
if err != nil {
if version.IsSandboxedMacOS() {
return errors.New("the GUI version of Tailscale on macOS runs in a macOS sandbox that can't read files")
}
return err
}
defer f.Close()
fi, err := f.Stat()
if err != nil {
return err
}
if fi.IsDir() {
return errors.New("directories not supported")
}
contentLength = fi.Size()
fileContents = io.LimitReader(f, contentLength)
if name == "" {
name = filepath.Base(fileArg)
}
if slow, _ := strconv.ParseBool(os.Getenv("TS_DEBUG_SLOW_PUSH")); slow {
fileContents = &slowReader{r: fileContents}
}
}
dstURL := peerAPIBase + "/v0/put/" + url.PathEscape(name)
req, err := http.NewRequestWithContext(ctx, "PUT", dstURL, fileContents)
if err != nil {
return err
}
req.ContentLength = contentLength
if cpArgs.verbose {
log.Printf("sending to %v ...", dstURL)
}
res, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
if res.StatusCode == 200 {
io.Copy(ioutil.Discard, res.Body)
res.Body.Close()
continue
}
io.Copy(os.Stdout, res.Body)
res.Body.Close()
return errors.New(res.Status)
}
return nil
}
func discoverPeerAPIBase(ctx context.Context, ipStr string) (base string, isOffline bool, err error) {
ip, err := netaddr.ParseIP(ipStr)
if err != nil {
return "", false, err
}
fts, err := tailscale.FileTargets(ctx)
if err != nil {
return "", false, err
}
for _, ft := range fts {
n := ft.Node
for _, a := range n.Addresses {
if a.IP() != ip {
continue
}
isOffline = n.Online != nil && !*n.Online
return ft.PeerAPIURL, isOffline, nil
}
}
return "", false, fileTargetErrorDetail(ctx, ip)
}
// fileTargetErrorDetail returns a non-nil error saying why ip is an
// invalid file sharing target.
func fileTargetErrorDetail(ctx context.Context, ip netaddr.IP) error {
found := false
if st, err := tailscale.Status(ctx); err == nil && st.Self != nil {
for _, peer := range st.Peer {
for _, pip := range peer.TailscaleIPs {
if pip == ip {
found = true
if peer.UserID != st.Self.UserID {
return errors.New("owned by different user; can only send files to your own devices")
}
}
}
}
}
if found {
return errors.New("target seems to be running an old Tailscale version")
}
if !tsaddr.IsTailscaleIP(ip) {
return fmt.Errorf("unknown target; %v is not a Tailscale IP address", ip)
}
return errors.New("unknown target; not in your Tailnet")
}
const maxSniff = 4 << 20
func ext(b []byte) string {
if len(b) < maxSniff && utf8.Valid(b) {
return ".txt"
}
if exts, _ := mime.ExtensionsByType(http.DetectContentType(b)); len(exts) > 0 {
return exts[0]
}
return ""
}
// pickStdinFilename reads a bit of stdin to return a good filename
// for its contents. The returned Reader is the concatenation of the
// read and unread bits.
func pickStdinFilename() (name string, r io.Reader, err error) {
sniff, err := io.ReadAll(io.LimitReader(os.Stdin, maxSniff))
if err != nil {
return "", nil, err
}
return "stdin" + ext(sniff), io.MultiReader(bytes.NewReader(sniff), os.Stdin), nil
}
type slowReader struct {
r io.Reader
rl *rate.Limiter
}
func (r *slowReader) Read(p []byte) (n int, err error) {
const burst = 4 << 10
plen := len(p)
if plen > burst {
plen = burst
}
if r.rl == nil {
r.rl = rate.NewLimiter(rate.Limit(1<<10), burst)
}
n, err = r.r.Read(p[:plen])
r.rl.WaitN(context.Background(), n)
return
}
func runCpTargets(ctx context.Context, args []string) error {
if len(args) > 0 {
return errors.New("invalid arguments with --targets")
}
fts, err := tailscale.FileTargets(ctx)
if err != nil {
return err
}
for _, ft := range fts {
n := ft.Node
var detail string
if n.Online != nil {
if !*n.Online {
detail = "offline"
}
} else {
detail = "unknown-status"
}
if detail != "" && n.LastSeen != nil {
d := time.Since(*n.LastSeen)
detail += fmt.Sprintf("; last seen %v ago", d.Round(time.Minute))
}
if detail != "" {
detail = "\t" + detail
}
fmt.Printf("%s\t%s%s\n", n.Addresses[0].IP(), n.ComputedName, detail)
}
return nil
}
var fileGetCmd = &ffcli.Command{
Name: "get",
ShortUsage: "file get [--wait] [--verbose] <target-directory>",
ShortHelp: "Move files out of the Tailscale file inbox",
Exec: runFileGet,
FlagSet: (func() *flag.FlagSet {
fs := flag.NewFlagSet("get", flag.ExitOnError)
fs.BoolVar(&getArgs.wait, "wait", false, "wait for a file to arrive if inbox is empty")
fs.BoolVar(&getArgs.verbose, "verbose", false, "verbose output")
return fs
})(),
}
var getArgs struct {
wait bool
verbose bool
}
func runFileGet(ctx context.Context, args []string) error {
if len(args) != 1 {
return errors.New("usage: file get <target-directory>")
}
log.SetFlags(0)
dir := args[0]
if dir == "/dev/null" {
return wipeInbox(ctx)
}
if fi, err := os.Stat(dir); err != nil || !fi.IsDir() {
return fmt.Errorf("%q is not a directory", dir)
}
var wfs []apitype.WaitingFile
var err error
for {
wfs, err = tailscale.WaitingFiles(ctx)
if err != nil {
return fmt.Errorf("getting WaitingFiles: %v", err)
}
if len(wfs) != 0 || !getArgs.wait {
break
}
if getArgs.verbose {
log.Printf("waiting for file...")
}
if err := waitForFile(ctx); err != nil {
return err
}
}
deleted := 0
for _, wf := range wfs {
rc, size, err := tailscale.GetWaitingFile(ctx, wf.Name)
if err != nil {
return fmt.Errorf("opening inbox file %q: %v", wf.Name, err)
}
targetFile := filepath.Join(dir, wf.Name)
of, err := os.OpenFile(targetFile, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0644)
if err != nil {
if _, err := os.Stat(targetFile); err == nil {
return fmt.Errorf("refusing to overwrite %v", targetFile)
}
return err
}
_, err = io.Copy(of, rc)
rc.Close()
if err != nil {
return fmt.Errorf("failed to write %v: %v", targetFile, err)
}
if err := of.Close(); err != nil {
return err
}
if getArgs.verbose {
log.Printf("wrote %v (%d bytes)", wf.Name, size)
}
if err := tailscale.DeleteWaitingFile(ctx, wf.Name); err != nil {
return fmt.Errorf("deleting %q from inbox: %v", wf.Name, err)
}
deleted++
}
if getArgs.verbose {
log.Printf("moved %d files", deleted)
}
return nil
}
func wipeInbox(ctx context.Context) error {
if getArgs.wait {
return errors.New("can't use --wait with /dev/null target")
}
wfs, err := tailscale.WaitingFiles(ctx)
if err != nil {
return fmt.Errorf("getting WaitingFiles: %v", err)
}
deleted := 0
for _, wf := range wfs {
if getArgs.verbose {
log.Printf("deleting %v ...", wf.Name)
}
if err := tailscale.DeleteWaitingFile(ctx, wf.Name); err != nil {
return fmt.Errorf("deleting %q: %v", wf.Name, err)
}
deleted++
}
if getArgs.verbose {
log.Printf("deleted %d files", deleted)
}
return nil
}
func waitForFile(ctx context.Context) error {
c, bc, pumpCtx, cancel := connect(ctx)
defer cancel()
fileWaiting := make(chan bool, 1)
bc.SetNotifyCallback(func(n ipn.Notify) {
if n.ErrMessage != nil {
log.Fatal(*n.ErrMessage)
}
if n.FilesWaiting != nil {
select {
case fileWaiting <- true:
default:
}
}
})
go pump(pumpCtx, bc, c)
select {
case <-fileWaiting:
return nil
case <-pumpCtx.Done():
return pumpCtx.Err()
case <-ctx.Done():
return ctx.Err()
}
}

View File

@@ -11,13 +11,16 @@ import (
"fmt"
"github.com/peterbourgon/ff/v2/ffcli"
"inet.af/netaddr"
"tailscale.com/client/tailscale"
"tailscale.com/ipn/ipnstate"
)
var ipCmd = &ffcli.Command{
Name: "ip",
ShortUsage: "ip [-4] [-6]",
ShortHelp: "Show this machine's current Tailscale IP address(es)",
ShortUsage: "ip [-4] [-6] [peername]",
ShortHelp: "Show current Tailscale IP address(es)",
LongHelp: "Shows the Tailscale IP address of the current machine without an argument. With an argument, it shows the IP of a named peer.",
Exec: runIP,
FlagSet: (func() *flag.FlagSet {
fs := flag.NewFlagSet("ip", flag.ExitOnError)
@@ -33,9 +36,14 @@ var ipArgs struct {
}
func runIP(ctx context.Context, args []string) error {
if len(args) > 0 {
if len(args) > 1 {
return errors.New("unknown arguments")
}
var of string
if len(args) == 1 {
of = args[0]
}
v4, v6 := ipArgs.want4, ipArgs.want6
if v4 && v6 {
return errors.New("tailscale up -4 and -6 are mutually exclusive")
@@ -47,11 +55,24 @@ func runIP(ctx context.Context, args []string) error {
if err != nil {
return err
}
if len(st.TailscaleIPs) == 0 {
ips := st.TailscaleIPs
if of != "" {
ip, err := tailscaleIPFromArg(ctx, of)
if err != nil {
return err
}
peer, ok := peerMatchingIP(st, ip)
if !ok {
return fmt.Errorf("no peer found with IP %v", ip)
}
ips = peer.TailscaleIPs
}
if len(ips) == 0 {
return fmt.Errorf("no current Tailscale IPs; state: %v", st.BackendState)
}
match := false
for _, ip := range st.TailscaleIPs {
for _, ip := range ips {
if ip.Is4() && v4 || ip.Is6() && v6 {
match = true
fmt.Println(ip)
@@ -67,3 +88,18 @@ func runIP(ctx context.Context, args []string) error {
}
return nil
}
func peerMatchingIP(st *ipnstate.Status, ipStr string) (ps *ipnstate.PeerStatus, ok bool) {
ip, err := netaddr.ParseIP(ipStr)
if err != nil {
return
}
for _, ps = range st.Peer {
for _, pip := range ps.TailscaleIPs {
if ip == pip {
return ps, true
}
}
}
return nil, false
}

View File

@@ -0,0 +1,34 @@
// 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 cli
import (
"context"
"log"
"strings"
"github.com/peterbourgon/ff/v2/ffcli"
"tailscale.com/client/tailscale"
)
var logoutCmd = &ffcli.Command{
Name: "logout",
ShortUsage: "logout [flags]",
ShortHelp: "Disconnect from Tailscale and expire current node key",
LongHelp: strings.TrimSpace(`
"tailscale logout" brings the network down and invalidates
the current node key, forcing a future use of it to cause
a reauthentication.
`),
Exec: runLogout,
}
func runLogout(ctx context.Context, args []string) error {
if len(args) > 0 {
log.Fatalf("too many non-flag arguments: %q", args)
}
return tailscale.Logout(ctx)
}

View File

@@ -9,14 +9,18 @@ import (
"encoding/json"
"flag"
"fmt"
"io"
"io/ioutil"
"log"
"net/http"
"os"
"sort"
"strings"
"time"
"github.com/peterbourgon/ff/v2/ffcli"
"tailscale.com/derp/derpmap"
"tailscale.com/client/tailscale"
"tailscale.com/ipn"
"tailscale.com/net/netcheck"
"tailscale.com/net/portmapper"
"tailscale.com/tailcfg"
@@ -46,7 +50,7 @@ var netcheckArgs struct {
func runNetcheck(ctx context.Context, args []string) error {
c := &netcheck.Client{
UDPBindAddr: os.Getenv("TS_DEBUG_NETCHECK_UDP_BIND"),
PortMapper: portmapper.NewClient(logger.WithPrefix(log.Printf, "portmap: ")),
PortMapper: portmapper.NewClient(logger.WithPrefix(log.Printf, "portmap: "), nil),
}
if netcheckArgs.verbose {
c.Logf = logger.WithPrefix(log.Printf, "netcheck: ")
@@ -59,7 +63,13 @@ func runNetcheck(ctx context.Context, args []string) error {
fmt.Fprintln(os.Stderr, "# Warning: this JSON format is not yet considered a stable interface")
}
dm := derpmap.Prod()
dm, err := tailscale.CurrentDERPMap(ctx)
if err != nil {
dm, err = prodDERPMap(ctx, http.DefaultClient)
if err != nil {
return err
}
}
for {
t0 := time.Now()
report, err := c.GetReport(ctx, dm)
@@ -176,3 +186,27 @@ func portMapping(r *netcheck.Report) string {
}
return strings.Join(got, ", ")
}
func prodDERPMap(ctx context.Context, httpc *http.Client) (*tailcfg.DERPMap, error) {
req, err := http.NewRequestWithContext(ctx, "GET", ipn.DefaultControlURL+"/derpmap/default", nil)
if err != nil {
return nil, fmt.Errorf("create prodDERPMap request: %w", err)
}
res, err := httpc.Do(req)
if err != nil {
return nil, fmt.Errorf("fetch prodDERPMap failed: %w", err)
}
defer res.Body.Close()
b, err := ioutil.ReadAll(io.LimitReader(res.Body, 1<<20))
if err != nil {
return nil, fmt.Errorf("fetch prodDERPMap failed: %w", err)
}
if res.StatusCode != 200 {
return nil, fmt.Errorf("fetch prodDERPMap: %v: %s", res.Status, b)
}
var derpMap tailcfg.DERPMap
if err = json.Unmarshal(b, &derpMap); err != nil {
return nil, fmt.Errorf("fetch prodDERPMap: %w", err)
}
return &derpMap, nil
}

View File

@@ -80,7 +80,8 @@ func runPing(ctx context.Context, args []string) error {
prc <- pr
}
})
go pump(ctx, bc, c)
pumpErr := make(chan error, 1)
go func() { pumpErr <- pump(ctx, bc, c) }()
hostOrIP := args[0]
ip, err := tailscaleIPFromArg(ctx, hostOrIP)
@@ -101,6 +102,8 @@ func runPing(ctx context.Context, args []string) error {
select {
case <-timer.C:
fmt.Printf("timeout waiting for ping reply\n")
case err := <-pumpErr:
return err
case pr := <-prc:
timer.Stop()
if pr.Err != "" {
@@ -136,6 +139,9 @@ func runPing(ctx context.Context, args []string) error {
if !anyPong {
return errors.New("no reply")
}
if pingArgs.untilDirect {
return errors.New("direct connection not established")
}
return nil
}
}
@@ -154,7 +160,10 @@ func tailscaleIPFromArg(ctx context.Context, hostOrIP string) (ip string, err er
}
for _, ps := range st.Peer {
if hostOrIP == dnsOrQuoteHostname(st, ps) || hostOrIP == ps.DNSName {
return ps.TailAddr, nil
if len(ps.TailscaleIPs) == 0 {
return "", errors.New("node found but lacks an IP")
}
return ps.TailscaleIPs[0].String(), nil
}
}

View File

@@ -1,173 +0,0 @@
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package cli
import (
"bytes"
"context"
"errors"
"flag"
"fmt"
"io"
"log"
"mime"
"net"
"net/http"
"net/url"
"os"
"time"
"unicode/utf8"
"github.com/peterbourgon/ff/v2/ffcli"
"tailscale.com/ipn"
"tailscale.com/ipn/ipnstate"
)
var pushCmd = &ffcli.Command{
Name: "push",
ShortUsage: "push [--flags] <hostname-or-IP> <file>",
ShortHelp: "Push a file to a host",
Exec: runPush,
FlagSet: (func() *flag.FlagSet {
fs := flag.NewFlagSet("push", flag.ExitOnError)
fs.StringVar(&pushArgs.name, "name", "", "alternate filename to use, especially useful when <file> is \"-\" (stdin)")
fs.BoolVar(&pushArgs.verbose, "verbose", false, "verbose output")
return fs
})(),
}
var pushArgs struct {
name string
verbose bool
}
func runPush(ctx context.Context, args []string) error {
if len(args) != 2 || args[0] == "" {
return errors.New("usage: push <hostname-or-IP> <file>")
}
var ip string
hostOrIP, fileArg := args[0], args[1]
ip, err := tailscaleIPFromArg(ctx, hostOrIP)
if err != nil {
return err
}
peerAPIPort, err := discoverPeerAPIPort(ctx, ip)
if err != nil {
return err
}
var fileContents io.Reader
var name = pushArgs.name
if fileArg == "-" {
fileContents = os.Stdin
if name == "" {
name, fileContents, err = pickStdinFilename()
if err != nil {
return err
}
}
} else {
f, err := os.Open(fileArg)
if err != nil {
return err
}
defer f.Close()
fileContents = f
if name == "" {
name = fileArg
}
}
dstURL := "http://" + net.JoinHostPort(ip, fmt.Sprint(peerAPIPort)) + "/v0/put/" + url.PathEscape(name)
req, err := http.NewRequestWithContext(ctx, "PUT", dstURL, fileContents)
if err != nil {
return err
}
if pushArgs.verbose {
log.Printf("sending to %v ...", dstURL)
}
res, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer res.Body.Close()
if res.StatusCode == 200 {
return nil
}
io.Copy(os.Stdout, res.Body)
return errors.New(res.Status)
}
func discoverPeerAPIPort(ctx context.Context, ip string) (port uint16, err error) {
c, bc, ctx, cancel := connect(ctx)
defer cancel()
prc := make(chan *ipnstate.PingResult, 2)
bc.SetNotifyCallback(func(n ipn.Notify) {
if n.ErrMessage != nil {
log.Fatal(*n.ErrMessage)
}
if pr := n.PingResult; pr != nil && pr.IP == ip {
prc <- pr
}
})
go pump(ctx, bc, c)
ticker := time.NewTicker(time.Second)
defer ticker.Stop()
discoPings := 0
timer := time.NewTimer(10 * time.Second)
defer timer.Stop()
sendPings := func() {
bc.Ping(ip, false)
bc.Ping(ip, true)
}
sendPings()
for {
select {
case <-ticker.C:
sendPings()
case <-timer.C:
return 0, fmt.Errorf("timeout contacting %v; it offline?", ip)
case pr := <-prc:
if p := pr.PeerAPIPort; p != 0 {
return p, nil
}
discoPings++
if discoPings == 3 {
return 0, fmt.Errorf("%v is online, but seems to be running an old Tailscale version", ip)
}
case <-ctx.Done():
return 0, ctx.Err()
}
}
}
const maxSniff = 4 << 20
func ext(b []byte) string {
if len(b) < maxSniff && utf8.Valid(b) {
return ".txt"
}
if exts, _ := mime.ExtensionsByType(http.DetectContentType(b)); len(exts) > 0 {
return exts[0]
}
return ""
}
// pickStdinFilename reads a bit of stdin to return a good filename
// for its contents. The returned Reader is the concatenation of the
// read and unread bits.
func pickStdinFilename() (name string, r io.Reader, err error) {
sniff, err := io.ReadAll(io.LimitReader(os.Stdin, maxSniff))
if err != nil {
return "", nil, err
}
return "stdin" + ext(sniff), io.MultiReader(bytes.NewReader(sniff), os.Stdin), nil
}

View File

@@ -14,10 +14,10 @@ import (
"net/http"
"os"
"strings"
"time"
"github.com/peterbourgon/ff/v2/ffcli"
"github.com/toqueteos/webbrowser"
"inet.af/netaddr"
"tailscale.com/client/tailscale"
"tailscale.com/ipn"
"tailscale.com/ipn/ipnstate"
@@ -56,12 +56,12 @@ var statusArgs struct {
func runStatus(ctx context.Context, args []string) error {
st, err := tailscale.Status(ctx)
if err != nil {
return err
return fixTailscaledConnectError(err)
}
if statusArgs.json {
if statusArgs.active {
for peer, ps := range st.Peer {
if !peerActive(ps) {
if !ps.Active {
delete(st.Peer, peer)
}
}
@@ -106,24 +106,38 @@ func runStatus(ctx context.Context, args []string) error {
return err
}
if st.BackendState == ipn.Stopped.String() {
switch st.BackendState {
default:
fmt.Fprintf(os.Stderr, "unexpected state: %s\n", st.BackendState)
os.Exit(1)
case ipn.Stopped.String():
fmt.Println("Tailscale is stopped.")
os.Exit(1)
case ipn.NeedsLogin.String():
fmt.Println("Logged out.")
if st.AuthURL != "" {
fmt.Printf("\nLog in at: %s\n", st.AuthURL)
}
os.Exit(1)
case ipn.NeedsMachineAuth.String():
fmt.Println("Machine is not yet authorized by tailnet admin.")
os.Exit(1)
case ipn.Running.String():
// Run below.
}
var buf bytes.Buffer
f := func(format string, a ...interface{}) { fmt.Fprintf(&buf, format, a...) }
printPS := func(ps *ipnstate.PeerStatus) {
active := peerActive(ps)
f("%-15s %-20s %-12s %-7s ",
ps.TailAddr,
firstIPString(ps.TailscaleIPs),
dnsOrQuoteHostname(st, ps),
ownerLogin(st, ps),
ps.OS,
)
relay := ps.Relay
anyTraffic := ps.TxBytes != 0 || ps.RxBytes != 0
if !active {
if !ps.Active {
if ps.ExitNode {
f("idle; exit node")
} else if anyTraffic {
@@ -162,8 +176,7 @@ func runStatus(ctx context.Context, args []string) error {
}
ipnstate.SortPeers(peers)
for _, ps := range peers {
active := peerActive(ps)
if statusArgs.active && !active {
if statusArgs.active && !ps.Active {
continue
}
printPS(ps)
@@ -173,13 +186,6 @@ func runStatus(ctx context.Context, args []string) error {
return nil
}
// peerActive reports whether ps has recent activity.
//
// TODO: have the server report this bool instead.
func peerActive(ps *ipnstate.PeerStatus) bool {
return !ps.LastWrite.IsZero() && time.Since(ps.LastWrite) < 2*time.Minute
}
func dnsOrQuoteHostname(st *ipnstate.Status, ps *ipnstate.PeerStatus) string {
baseName := dnsname.TrimSuffix(ps.DNSName, st.MagicDNSSuffix)
if baseName != "" {
@@ -201,3 +207,10 @@ func ownerLogin(st *ipnstate.Status, ps *ipnstate.PeerStatus) string {
}
return u.LoginName
}
func firstIPString(v []netaddr.IP) string {
if len(v) == 0 {
return ""
}
return v[0].String()
}

File diff suppressed because it is too large Load Diff

View File

@@ -9,12 +9,16 @@ import (
"context"
_ "embed"
"encoding/json"
"encoding/xml"
"flag"
"fmt"
"html/template"
"io/ioutil"
"log"
"net"
"net/http"
"net/http/cgi"
"net/url"
"os/exec"
"runtime"
"strings"
@@ -24,6 +28,7 @@ import (
"tailscale.com/ipn"
"tailscale.com/tailcfg"
"tailscale.com/types/preftype"
"tailscale.com/util/groupmember"
"tailscale.com/version/distro"
)
@@ -33,6 +38,9 @@ var webHTML string
//go:embed web.css
var webCSS string
//go:embed auth-redirect.html
var authenticationRedirectHTML string
var tmpl *template.Template
func init() {
@@ -53,6 +61,14 @@ var webCmd = &ffcli.Command{
ShortUsage: "web [flags]",
ShortHelp: "Run a web server for controlling Tailscale",
LongHelp: strings.TrimSpace(`
"tailscale web" runs a webserver for controlling the Tailscale daemon.
It's primarily intended for use on Synology, QNAP, and other
NAS devices where a web interface is the natural place to control
Tailscale, as opposed to a CLI or a native app.
`),
FlagSet: (func() *flag.FlagSet {
webf := flag.NewFlagSet("web", flag.ExitOnError)
webf.StringVar(&webArgs.listen, "listen", "localhost:8088", "listen address; use port 0 for automatic")
@@ -73,28 +89,134 @@ func runWeb(ctx context.Context, args []string) error {
}
if webArgs.cgi {
return cgi.Serve(http.HandlerFunc(webHandler))
if err := cgi.Serve(http.HandlerFunc(webHandler)); err != nil {
log.Printf("tailscale.cgi: %v", err)
return err
}
return nil
}
log.Printf("web server running on: %s", urlOfListenAddr(webArgs.listen))
return http.ListenAndServe(webArgs.listen, http.HandlerFunc(webHandler))
}
func auth() (string, error) {
if distro.Get() == distro.Synology {
cmd := exec.Command("/usr/syno/synoman/webman/modules/authenticate.cgi")
out, err := cmd.CombinedOutput()
if err != nil {
return "", fmt.Errorf("auth: %v: %s", err, out)
}
return string(out), nil
// urlOfListenAddr parses a given listen address into a formatted URL
func urlOfListenAddr(addr string) string {
host, port, _ := net.SplitHostPort(addr)
if host == "" {
host = "127.0.0.1"
}
return fmt.Sprintf("http://%s", net.JoinHostPort(host, port))
}
// authorize returns the name of the user accessing the web UI after verifying
// whether the user has access to the web UI. The function will write the
// error to the provided http.ResponseWriter.
// Note: This is different from a tailscale user, and is typically the local
// user on the node.
func authorize(w http.ResponseWriter, r *http.Request) (string, error) {
switch distro.Get() {
case distro.Synology:
user, err := synoAuthn()
if err != nil {
http.Error(w, err.Error(), http.StatusUnauthorized)
return "", err
}
if err := authorizeSynology(user); err != nil {
http.Error(w, err.Error(), http.StatusForbidden)
return "", err
}
return user, nil
case distro.QNAP:
user, resp, err := qnapAuthn(r)
if err != nil {
http.Error(w, err.Error(), http.StatusUnauthorized)
return "", err
}
if resp.IsAdmin == 0 {
http.Error(w, err.Error(), http.StatusForbidden)
return "", err
}
return user, nil
}
return "", nil
}
func synoTokenRedirect(w http.ResponseWriter, r *http.Request) bool {
if distro.Get() != distro.Synology {
return false
// authorizeSynology checks whether the provided user has access to the web UI
// by consulting the membership of the "administrators" group.
func authorizeSynology(name string) error {
yes, err := groupmember.IsMemberOfGroup("administrators", name)
if err != nil {
return err
}
if !yes {
return fmt.Errorf("not a member of administrators group")
}
return nil
}
type qnapAuthResponse struct {
AuthPassed int `xml:"authPassed"`
IsAdmin int `xml:"isAdmin"`
AuthSID string `xml:"authSid"`
ErrorValue int `xml:"errorValue"`
}
func qnapAuthn(r *http.Request) (string, *qnapAuthResponse, error) {
user, err := r.Cookie("NAS_USER")
if err != nil {
return "", nil, err
}
token, err := r.Cookie("qtoken")
if err != nil {
return "", nil, err
}
query := url.Values{
"qtoken": []string{token.Value},
"user": []string{user.Value},
}
u := url.URL{
Scheme: r.URL.Scheme,
Host: r.URL.Host,
Path: "/cgi-bin/authLogin.cgi",
RawQuery: query.Encode(),
}
resp, err := http.Get(u.String())
if err != nil {
return "", nil, err
}
defer resp.Body.Close()
out, err := ioutil.ReadAll(resp.Body)
if err != nil {
return "", nil, err
}
authResp := &qnapAuthResponse{}
if err := xml.Unmarshal(out, authResp); err != nil {
return "", nil, err
}
if authResp.AuthPassed == 0 {
return "", nil, fmt.Errorf("not authenticated")
}
return user.Value, authResp, nil
}
func synoAuthn() (string, error) {
cmd := exec.Command("/usr/syno/synoman/webman/modules/authenticate.cgi")
out, err := cmd.CombinedOutput()
if err != nil {
return "", fmt.Errorf("auth: %v: %s", err, out)
}
return strings.TrimSpace(string(out)), nil
}
func authRedirect(w http.ResponseWriter, r *http.Request) bool {
if distro.Get() == distro.Synology {
return synoTokenRedirect(w, r)
}
return false
}
func synoTokenRedirect(w http.ResponseWriter, r *http.Request) bool {
if r.Header.Get("X-Syno-Token") != "" {
return false
}
@@ -128,75 +250,13 @@ req.send(null);
</body></html>
`
const authenticationRedirectHTML = `
<html>
<head>
<title>Redirecting...</title>
<style>
html,
body {
height: 100%;
}
html {
background-color: rgb(249, 247, 246);
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
line-height: 1.5;
-webkit-text-size-adjust: 100%;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
body {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.spinner {
margin-bottom: 2rem;
border: 4px rgba(112, 110, 109, 0.5) solid;
border-left-color: transparent;
border-radius: 9999px;
width: 4rem;
height: 4rem;
-webkit-animation: spin 700ms linear infinite;
animation: spin 800ms linear infinite;
}
.label {
color: rgb(112, 110, 109);
padding-left: 0.4rem;
}
@-webkit-keyframes spin {
to {
transform: rotate(360deg);
}
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
</style>
</head>
<body>
<div class="spinner"></div>
<div class="label">Redirecting...</div>
</body>
`
func webHandler(w http.ResponseWriter, r *http.Request) {
if synoTokenRedirect(w, r) {
if authRedirect(w, r) {
return
}
user, err := auth()
user, err := authorize(w, r)
if err != nil {
http.Error(w, err.Error(), http.StatusForbidden)
return
}
@@ -208,9 +268,10 @@ func webHandler(w http.ResponseWriter, r *http.Request) {
if r.Method == "POST" {
type mi map[string]interface{}
w.Header().Set("Content-Type", "application/json")
url, err := tailscaleUp(r.Context())
url, err := tailscaleUpForceReauth(r.Context())
if err != nil {
json.NewEncoder(w).Encode(mi{"error": err})
w.WriteHeader(http.StatusInternalServerError)
json.NewEncoder(w).Encode(mi{"error": err.Error()})
return
}
json.NewEncoder(w).Encode(mi{"url": url})
@@ -219,7 +280,7 @@ func webHandler(w http.ResponseWriter, r *http.Request) {
st, err := tailscale.Status(r.Context())
if err != nil {
http.Error(w, err.Error(), 500)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
@@ -237,16 +298,16 @@ func webHandler(w http.ResponseWriter, r *http.Request) {
buf := new(bytes.Buffer)
if err := tmpl.Execute(buf, data); err != nil {
http.Error(w, err.Error(), 500)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Write(buf.Bytes())
}
// TODO(crawshaw): some of this is very similar to the code in 'tailscale up', can we share anything?
func tailscaleUp(ctx context.Context) (authURL string, retErr error) {
func tailscaleUpForceReauth(ctx context.Context) (authURL string, retErr error) {
prefs := ipn.NewPrefs()
prefs.ControlURL = "https://login.tailscale.com"
prefs.ControlURL = ipn.DefaultControlURL
prefs.WantRunning = true
prefs.CorpDNS = true
prefs.AllowSingleHosts = true
@@ -256,36 +317,70 @@ func tailscaleUp(ctx context.Context) (authURL string, retErr error) {
prefs.NetfilterMode = preftype.NetfilterOff
}
c, bc, ctx, cancel := connect(ctx)
st, err := tailscale.Status(ctx)
if err != nil {
return "", fmt.Errorf("can't fetch status: %v", err)
}
origAuthURL := st.AuthURL
// printAuthURL reports whether we should print out the
// provided auth URL from an IPN notify.
printAuthURL := func(url string) bool {
return url != origAuthURL
}
c, bc, pumpCtx, cancel := connect(ctx)
defer cancel()
gotEngineUpdate := make(chan bool, 1) // gets value upon an engine update
go pump(pumpCtx, bc, c)
bc.SetNotifyCallback(func(n ipn.Notify) {
if n.Engine != nil {
select {
case gotEngineUpdate <- true:
default:
}
}
if n.ErrMessage != nil {
msg := *n.ErrMessage
if msg == ipn.ErrMsgPermissionDenied {
switch runtime.GOOS {
case "windows":
msg += " (Tailscale service in use by other user?)"
default:
msg += " (try 'sudo tailscale up [...]')"
}
}
retErr = fmt.Errorf("backend error: %v", msg)
cancel()
} else if url := n.BrowseToURL; url != nil && printAuthURL(*url) {
authURL = *url
cancel()
}
})
// Wait for backend client to be connected so we know
// we're subscribed to updates. Otherwise we can miss
// an update upon its transition to running. Do so by causing some traffic
// back to the bus that we then wait on.
bc.RequestEngineStatus()
select {
case <-gotEngineUpdate:
case <-pumpCtx.Done():
return authURL, pumpCtx.Err()
}
bc.SetPrefs(prefs)
opts := ipn.Options{
bc.Start(ipn.Options{
StateKey: ipn.GlobalDaemonStateKey,
Notify: func(n ipn.Notify) {
if n.ErrMessage != nil {
msg := *n.ErrMessage
if msg == ipn.ErrMsgPermissionDenied {
switch runtime.GOOS {
case "windows":
msg += " (Tailscale service in use by other user?)"
default:
msg += " (try 'sudo tailscale up [...]')"
}
}
retErr = fmt.Errorf("backend error: %v", msg)
cancel()
} else if url := n.BrowseToURL; url != nil {
authURL = *url
cancel()
}
},
}
bc.Start(opts)
})
bc.StartLoginInteractive()
pump(ctx, bc, c)
<-pumpCtx.Done() // wait for authURL or complete failure
if authURL == "" && retErr == nil {
retErr = pumpCtx.Err()
}
if authURL == "" && retErr == nil {
return "", fmt.Errorf("login failed with no backend error message")
}

View File

@@ -11,140 +11,133 @@
</head>
<body class="py-14">
<main class="container max-w-lg mx-auto py-6 px-8 bg-white rounded-md shadow-2xl" style="width: 95%">
<header class="flex justify-between items-center min-width-0 py-2 mb-8">
<svg width="26" height="26" viewBox="0 0 23 23" title="Tailscale" fill="none" xmlns="http://www.w3.org/2000/svg"
class="flex-shrink-0 mr-4">
<circle opacity="0.2" cx="3.4" cy="3.25" r="2.7" fill="currentColor"></circle>
<circle cx="3.4" cy="11.3" r="2.7" fill="currentColor"></circle>
<circle opacity="0.2" cx="3.4" cy="19.5" r="2.7" fill="currentColor"></circle>
<circle cx="11.5" cy="11.3" r="2.7" fill="currentColor"></circle>
<circle cx="11.5" cy="19.5" r="2.7" fill="currentColor"></circle>
<circle opacity="0.2" cx="11.5" cy="3.25" r="2.7" fill="currentColor"></circle>
<circle opacity="0.2" cx="19.5" cy="3.25" r="2.7" fill="currentColor"></circle>
<circle cx="19.5" cy="11.3" r="2.7" fill="currentColor"></circle>
<circle opacity="0.2" cx="19.5" cy="19.5" r="2.7" fill="currentColor"></circle>
</svg>
<div class="flex items-center justify-end space-x-2 w-2/3">
{{ with .Profile.LoginName }}
<div class="text-right truncate leading-4">
<h4 class="truncate">{{.}}</h4>
<a href="#" class="text-xs text-gray-500 hover:text-gray-700 js-loginButton">Switch account</a>
</div>
<main class="container max-w-lg mx-auto py-6 px-8 bg-white rounded-md shadow-2xl" style="width: 95%">
<header class="flex justify-between items-center min-width-0 py-2 mb-8">
<svg width="26" height="26" viewBox="0 0 23 23" title="Tailscale" fill="none" xmlns="http://www.w3.org/2000/svg"
class="flex-shrink-0 mr-4">
<circle opacity="0.2" cx="3.4" cy="3.25" r="2.7" fill="currentColor"></circle>
<circle cx="3.4" cy="11.3" r="2.7" fill="currentColor"></circle>
<circle opacity="0.2" cx="3.4" cy="19.5" r="2.7" fill="currentColor"></circle>
<circle cx="11.5" cy="11.3" r="2.7" fill="currentColor"></circle>
<circle cx="11.5" cy="19.5" r="2.7" fill="currentColor"></circle>
<circle opacity="0.2" cx="11.5" cy="3.25" r="2.7" fill="currentColor"></circle>
<circle opacity="0.2" cx="19.5" cy="3.25" r="2.7" fill="currentColor"></circle>
<circle cx="19.5" cy="11.3" r="2.7" fill="currentColor"></circle>
<circle opacity="0.2" cx="19.5" cy="19.5" r="2.7" fill="currentColor"></circle>
</svg>
<div class="flex items-center justify-end space-x-2 w-2/3">
{{ with .Profile.LoginName }}
<div class="text-right truncate leading-4">
<h4 class="truncate">{{.}}</h4>
<a href="#" class="text-xs text-gray-500 hover:text-gray-700 js-loginButton">Switch account</a>
</div>
{{ end }}
<div class="relative flex-shrink-0 w-8 h-8 rounded-full overflow-hidden">
{{ with .Profile.ProfilePicURL }}
<div class="w-8 h-8 flex pointer-events-none rounded-full bg-gray-200"
style="background-image: url('{{.}}'); background-size: cover;"></div>
{{ else }}
<div class="w-8 h-8 flex pointer-events-none rounded-full border border-gray-400 border-dashed"></div>
{{ end }}
<div class="relative flex-shrink-0 w-8 h-8 rounded-full overflow-hidden">
{{ with .Profile.ProfilePicURL }}
<div class="w-8 h-8 flex pointer-events-none rounded-full bg-gray-200"
style="background-image: url('{{.}}'); background-size: cover;"></div>
{{ else }}
<div class="w-8 h-8 flex pointer-events-none rounded-full border border-gray-400 border-dashed"></div>
{{ end }}
</div>
</div>
</header>
{{ if .IP }}
<div
class="border border-gray-200 bg-gray-0 rounded-lg p-2 pl-3 pr-3 mb-8 width-full flex items-center justify-between">
<div class="flex items-center min-width-0">
<svg class="flex-shrink-0 text-gray-600 mr-3 ml-1" xmlns="http://www.w3.org/2000/svg" width="20" height="20"
viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
stroke-linejoin="round">
<rect x="2" y="2" width="20" height="8" rx="2" ry="2"></rect>
<rect x="2" y="14" width="20" height="8" rx="2" ry="2"></rect>
<line x1="6" y1="6" x2="6.01" y2="6"></line>
<line x1="6" y1="18" x2="6.01" y2="18"></line>
</svg>
<h4 class="font-semibold truncate mr-2">{{.DeviceName}}</h4>
</div>
<h5>{{.IP}}</h5>
</div>
{{ end }}
{{ if or (eq .Status "NeedsLogin") (eq .Status "NoState") }}
{{ if .IP }}
<div class="mb-6">
<p class="text-gray-700">Your device's key has expired. Reauthenticate this device by logging in again, or <a
href="https://tailscale.com/kb/1028/key-expiry" class="link" target="_blank">learn more</a>.</p>
</header>
{{ if .IP }}
<div
class="border border-gray-200 bg-gray-0 rounded-lg p-2 pl-3 pr-3 mb-8 width-full flex items-center justify-between">
<div class="flex items-center min-width-0">
<svg class="flex-shrink-0 text-gray-600 mr-3 ml-1" xmlns="http://www.w3.org/2000/svg" width="20" height="20"
viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
stroke-linejoin="round">
<rect x="2" y="2" width="20" height="8" rx="2" ry="2"></rect>
<rect x="2" y="14" width="20" height="8" rx="2" ry="2"></rect>
<line x1="6" y1="6" x2="6.01" y2="6"></line>
<line x1="6" y1="18" x2="6.01" y2="18"></line>
</svg>
<h4 class="font-semibold truncate mr-2">{{.DeviceName}}</h4>
</div>
<a href="#" class="mb-4 js-loginButton" target="_blank">
<button class="button button-blue w-full">Reauthenticate</button>
</a>
{{ else }}
<div class="mb-6">
<h3 class="text-3xl font-semibold mb-3">Log in</h3>
<p class="text-gray-700">Get started by logging in to your Tailscale network. Or,&nbsp;learn&nbsp;more at <a
href="https://tailscale.com/" class="link" target="_blank">tailscale.com</a>.</p>
</div>
<a href="#" class="mb-4 js-loginButton" target="_blank">
<button class="button button-blue w-full">Log In</button>
</a>
{{ end }}
{{ else if eq .Status "NeedsMachineAuth" }}
<div class="mb-4">
This device is authorized, but needs approval from a network admin before it can connect to the network.
</div>
{{ else }}
<div class="mb-4">
<p>You are connected! Access this device over Tailscale using the device name or IP address above.</p>
</div>
<a href="#" class="mb-4 link font-medium js-loginButton" target="_blank">Reauthenticate</a>
{{ end }}
</main>
<script>
(function () {
let loginButtons = document.querySelectorAll(".js-loginButton");
let fetchingUrl = false;
<h5>{{.IP}}</h5>
</div>
{{ end }}
{{ if or (eq .Status "NeedsLogin") (eq .Status "NoState") }}
{{ if .IP }}
<div class="mb-6">
<p class="text-gray-700">Your device's key has expired. Reauthenticate this device by logging in again, or <a
href="https://tailscale.com/kb/1028/key-expiry" class="link" target="_blank">learn more</a>.</p>
</div>
<a href="#" class="mb-4 js-loginButton" target="_blank">
<button class="button button-blue w-full">Reauthenticate</button>
</a>
{{ else }}
<div class="mb-6">
<h3 class="text-3xl font-semibold mb-3">Log in</h3>
<p class="text-gray-700">Get started by logging in to your Tailscale network. Or,&nbsp;learn&nbsp;more at <a
href="https://tailscale.com/" class="link" target="_blank">tailscale.com</a>.</p>
</div>
<a href="#" class="mb-4 js-loginButton" target="_blank">
<button class="button button-blue w-full">Log In</button>
</a>
{{ end }}
{{ else if eq .Status "NeedsMachineAuth" }}
<div class="mb-4">
This device is authorized, but needs approval from a network admin before it can connect to the network.
</div>
{{ else }}
<div class="mb-4">
<p>You are connected! Access this device over Tailscale using the device name or IP address above.</p>
</div>
<a href="#" class="mb-4 link font-medium js-loginButton" target="_blank">Reauthenticate</a>
{{ end }}
</main>
<script>(function () {
let loginButtons = document.querySelectorAll(".js-loginButton");
let fetchingUrl = false;
function handleClick(e) {
e.preventDefault();
function handleClick(e) {
e.preventDefault();
if (fetchingUrl) {
return;
}
if (fetchingUrl) {
return;
}
fetchingUrl = true;
const urlParams = new URLSearchParams(window.location.search);
const token = urlParams.get("SynoToken");
const nextParams = new URLSearchParams({ up: true });
if (token) {
nextParams.set("SynoToken", token)
}
const nextUrl = new URL(window.location);
nextUrl.search = nextParams.toString()
const url = nextUrl.toString();
fetchingUrl = true;
const urlParams = new URLSearchParams(window.location.search);
const token = urlParams.get("SynoToken");
const nextParams = new URLSearchParams({ up: true });
if (token) {
nextParams.set("SynoToken", token)
}
const nextUrl = new URL(window.location);
nextUrl.search = nextParams.toString()
const url = nextUrl.toString();
const tab = window.open("/redirect", "_blank");
fetch(url, {
method: "POST",
headers: {
"Accept": "application/json",
"Content-Type": "application/json",
}
}).then(res => res.json()).then(res => {
fetchingUrl = false;
const err = res["error"];
if (err) {
throw new Error(err);
}
const url = res["url"];
if (url) {
document.location.href = url;
} else {
location.reload();
}
}).catch(err => {
alert("Failed to log in: " + err.message);
});
}
fetch(url, {
method: "POST",
headers: {
"Accept": "application/json",
"Content-Type": "application/json",
}
}).then(res => res.json()).then(res => {
fetchingUrl = false;
const err = res["error"];
if (err) {
throw new Error(err);
}
const url = res["url"];
if (url) {
authUrl = url;
tab.location = url;
tab.focus();
} else {
location.reload();
}
}).catch(err => {
tab.close();
alert("Failed to log in: " + err.message);
});
}
Array.from(loginButtons).forEach(el => {
el.addEventListener("click", handleClick);
})
})();
</script>
Array.from(loginButtons).forEach(el => {
el.addEventListener("click", handleClick);
})
})();</script>
</body>
</html>

View File

@@ -0,0 +1,44 @@
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package cli
import "testing"
func TestUrlOfListenAddr(t *testing.T) {
tests := []struct {
name string
in, want string
}{
{
name: "TestLocalhost",
in: "localhost:8088",
want: "http://localhost:8088",
},
{
name: "TestNoHost",
in: ":8088",
want: "http://127.0.0.1:8088",
},
{
name: "TestExplicitHost",
in: "127.0.0.2:8088",
want: "http://127.0.0.2:8088",
},
{
name: "TestIPv6",
in: "[::1]:8088",
want: "http://[::1]:8088",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
url := urlOfListenAddr(tt.in)
if url != tt.want {
t.Errorf("expected url: %q, got: %q", tt.want, url)
}
})
}
}

View File

@@ -1,9 +1,19 @@
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 from github.com/alexbrainman/sspi/negotiate+
W github.com/alexbrainman/sspi/internal/common from github.com/alexbrainman/sspi/negotiate
W 💣 github.com/alexbrainman/sspi/negotiate from tailscale.com/net/tshttpproxy
github.com/dsnet/golib/jsonfmt from tailscale.com/cmd/tailscale/cli
github.com/kballard/go-shellquote from tailscale.com/cmd/tailscale/cli
💣 github.com/mitchellh/go-ps from tailscale.com/cmd/tailscale/cli
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/goupnp from github.com/tailscale/goupnp/dcps/internetgateway2+
github.com/tailscale/goupnp/dcps/internetgateway2 from tailscale.com/net/portmapper
github.com/tailscale/goupnp/httpu from github.com/tailscale/goupnp+
github.com/tailscale/goupnp/scpd from github.com/tailscale/goupnp
github.com/tailscale/goupnp/soap from github.com/tailscale/goupnp+
github.com/tailscale/goupnp/ssdp from github.com/tailscale/goupnp
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
@@ -13,13 +23,15 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
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/client/tailscale from tailscale.com/cmd/tailscale/cli
tailscale.com/client/tailscale from tailscale.com/cmd/tailscale/cli+
tailscale.com/client/tailscale/apitype from tailscale.com/client/tailscale+
tailscale.com/cmd/tailscale/cli from tailscale.com/cmd/tailscale
tailscale.com/control/controlknobs from tailscale.com/net/portmapper
tailscale.com/derp from tailscale.com/derp/derphttp
tailscale.com/derp/derphttp from tailscale.com/net/netcheck
tailscale.com/derp/derpmap from tailscale.com/cmd/tailscale/cli
tailscale.com/disco from tailscale.com/derp
tailscale.com/ipn from tailscale.com/cmd/tailscale/cli
tailscale.com/hostinfo from tailscale.com/net/interfaces
tailscale.com/ipn from tailscale.com/cmd/tailscale/cli+
tailscale.com/ipn/ipnstate from tailscale.com/cmd/tailscale/cli+
tailscale.com/metrics from tailscale.com/derp
tailscale.com/net/dnscache from tailscale.com/derp/derphttp
@@ -31,29 +43,33 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
tailscale.com/net/portmapper from tailscale.com/net/netcheck+
tailscale.com/net/stun from tailscale.com/net/netcheck
tailscale.com/net/tlsdial from tailscale.com/derp/derphttp
tailscale.com/net/tsaddr from tailscale.com/net/interfaces
tailscale.com/net/tsaddr from tailscale.com/net/interfaces+
💣 tailscale.com/net/tshttpproxy from tailscale.com/derp/derphttp+
tailscale.com/paths from tailscale.com/cmd/tailscale/cli+
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/tstime/mono from tailscale.com/tstime/rate
tailscale.com/tstime/rate from tailscale.com/wgengine/filter
tailscale.com/types/dnstype from tailscale.com/tailcfg
tailscale.com/types/empty from tailscale.com/ipn
tailscale.com/types/ipproto from tailscale.com/net/flowtrack+
tailscale.com/types/key from tailscale.com/derp+
tailscale.com/types/logger from tailscale.com/cmd/tailscale/cli+
tailscale.com/types/netmap from tailscale.com/ipn
tailscale.com/types/opt from tailscale.com/net/netcheck+
tailscale.com/types/pad32 from tailscale.com/derp
tailscale.com/types/persist from tailscale.com/ipn
tailscale.com/types/preftype from tailscale.com/cmd/tailscale/cli+
tailscale.com/types/strbuilder from tailscale.com/net/packet
tailscale.com/types/structs from tailscale.com/ipn+
tailscale.com/types/wgkey from tailscale.com/types/netmap+
tailscale.com/util/dnsname from tailscale.com/cmd/tailscale/cli+
W tailscale.com/util/endian from tailscale.com/net/netns
L tailscale.com/util/lineread from tailscale.com/net/interfaces
tailscale.com/util/groupmember from tailscale.com/cmd/tailscale/cli
tailscale.com/util/lineread from tailscale.com/net/interfaces+
tailscale.com/version from tailscale.com/cmd/tailscale/cli+
tailscale.com/version/distro from tailscale.com/cmd/tailscale/cli
tailscale.com/version/distro from tailscale.com/cmd/tailscale/cli+
tailscale.com/wgengine/filter from tailscale.com/types/netmap
golang.org/x/crypto/blake2b from golang.org/x/crypto/nacl/box
golang.org/x/crypto/chacha20 from golang.org/x/crypto/chacha20poly1305
@@ -73,7 +89,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
golang.org/x/net/idna from golang.org/x/net/http/httpguts+
golang.org/x/net/proxy from tailscale.com/net/netns
D golang.org/x/net/route from net+
golang.org/x/sync/errgroup from tailscale.com/derp
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 tailscale.com/net/netns+
@@ -83,7 +99,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
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+
golang.org/x/time/rate from tailscale.com/cmd/tailscale/cli+
bufio from compress/flate+
bytes from bufio+
compress/flate from compress/gzip+
@@ -116,13 +132,14 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
debug/macho from rsc.io/goversion/version
debug/pe from rsc.io/goversion/version
embed from tailscale.com/cmd/tailscale/cli
encoding from encoding/json
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+
encoding/xml from tailscale.com/cmd/tailscale/cli+
errors from bufio+
expvar from tailscale.com/derp+
flag from github.com/peterbourgon/ff/v2+
@@ -154,6 +171,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
os from crypto/rand+
os/exec from github.com/toqueteos/webbrowser+
os/signal from tailscale.com/cmd/tailscale/cli
os/user from tailscale.com/util/groupmember
path from debug/dwarf+
path/filepath from crypto/x509+
reflect from crypto/x509+

View File

@@ -11,19 +11,26 @@ import (
"errors"
"flag"
"fmt"
"io"
"io/ioutil"
"log"
"net"
"net/http"
"net/http/httptrace"
"net/url"
"os"
"strings"
"time"
"inet.af/netaddr"
"tailscale.com/derp/derphttp"
"tailscale.com/derp/derpmap"
"tailscale.com/ipn"
"tailscale.com/net/interfaces"
"tailscale.com/net/portmapper"
"tailscale.com/net/tshttpproxy"
"tailscale.com/tailcfg"
"tailscale.com/types/key"
"tailscale.com/types/logger"
"tailscale.com/wgengine/monitor"
)
@@ -31,6 +38,7 @@ var debugArgs struct {
monitor bool
getURL string
derpCheck string
portmap bool
}
var debugModeFunc = debugMode // so it can be addressable
@@ -38,6 +46,7 @@ var debugModeFunc = debugMode // so it can be addressable
func debugMode(args []string) error {
fs := flag.NewFlagSet("debug", flag.ExitOnError)
fs.BoolVar(&debugArgs.monitor, "monitor", false, "If true, run link monitor forever. Precludes all other options.")
fs.BoolVar(&debugArgs.portmap, "portmap", false, "If true, run portmap debugging. Precludes all other options.")
fs.StringVar(&debugArgs.getURL, "get-url", "", "If non-empty, fetch provided URL.")
fs.StringVar(&debugArgs.derpCheck, "derp", "", "if non-empty, test a DERP ping via named region code")
if err := fs.Parse(args); err != nil {
@@ -53,6 +62,9 @@ func debugMode(args []string) error {
if debugArgs.monitor {
return runMonitor(ctx)
}
if debugArgs.portmap {
return debugPortmap(ctx)
}
if debugArgs.getURL != "" {
return getURL(ctx, debugArgs.getURL)
}
@@ -131,7 +143,26 @@ func getURL(ctx context.Context, urlStr string) error {
}
func checkDerp(ctx context.Context, derpRegion string) error {
dmap := derpmap.Prod()
req, err := http.NewRequestWithContext(ctx, "GET", ipn.DefaultControlURL+"/derpmap/default", nil)
if err != nil {
return fmt.Errorf("create derp map request: %w", err)
}
res, err := http.DefaultClient.Do(req)
if err != nil {
return fmt.Errorf("fetch derp map failed: %w", err)
}
defer res.Body.Close()
b, err := ioutil.ReadAll(io.LimitReader(res.Body, 1<<20))
if err != nil {
return fmt.Errorf("fetch derp map failed: %w", err)
}
if res.StatusCode != 200 {
return fmt.Errorf("fetch derp map: %v: %s", res.Status, b)
}
var dmap tailcfg.DERPMap
if err = json.Unmarshal(b, &dmap); err != nil {
return fmt.Errorf("fetch DERP map: %w", err)
}
getRegion := func() *tailcfg.DERPRegion {
for _, r := range dmap.Regions {
if r.RegionCode == derpRegion {
@@ -170,3 +201,97 @@ func checkDerp(ctx context.Context, derpRegion string) error {
log.Printf("ok")
return err
}
func debugPortmap(ctx context.Context) error {
ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
defer cancel()
portmapper.VerboseLogs = true
switch os.Getenv("TS_DEBUG_PORTMAP_TYPE") {
case "":
case "pmp":
portmapper.DisablePCP = true
portmapper.DisableUPnP = true
case "pcp":
portmapper.DisablePMP = true
portmapper.DisableUPnP = true
case "upnp":
portmapper.DisablePCP = true
portmapper.DisablePMP = true
default:
log.Fatalf("TS_DEBUG_PORTMAP_TYPE must be one of pmp,pcp,upnp")
}
done := make(chan bool, 1)
var c *portmapper.Client
logf := log.Printf
c = portmapper.NewClient(logger.WithPrefix(logf, "portmapper: "), func() {
logf("portmapping changed.")
logf("have mapping: %v", c.HaveMapping())
if ext, ok := c.GetCachedMappingOrStartCreatingOne(); ok {
logf("cb: mapping: %v", ext)
select {
case done <- true:
default:
}
return
}
logf("cb: no mapping")
})
linkMon, err := monitor.New(logger.WithPrefix(logf, "monitor: "))
if err != nil {
return err
}
gatewayAndSelfIP := func() (gw, self netaddr.IP, ok bool) {
if v := os.Getenv("TS_DEBUG_GW_SELF"); strings.Contains(v, "/") {
i := strings.Index(v, "/")
gw = netaddr.MustParseIP(v[:i])
self = netaddr.MustParseIP(v[i+1:])
return gw, self, true
}
return linkMon.GatewayAndSelfIP()
}
c.SetGatewayLookupFunc(gatewayAndSelfIP)
gw, selfIP, ok := gatewayAndSelfIP()
if !ok {
logf("no gateway or self IP; %v", linkMon.InterfaceState())
return nil
}
logf("gw=%v; self=%v", gw, selfIP)
uc, err := net.ListenPacket("udp", "0.0.0.0:0")
if err != nil {
return err
}
defer uc.Close()
c.SetLocalPort(uint16(uc.LocalAddr().(*net.UDPAddr).Port))
res, err := c.Probe(ctx)
if err != nil {
return fmt.Errorf("Probe: %v", err)
}
logf("Probe: %+v", res)
if !res.PCP && !res.PMP && !res.UPnP {
logf("no portmapping services available")
return nil
}
if ext, ok := c.GetCachedMappingOrStartCreatingOne(); ok {
logf("mapping: %v", ext)
} else {
logf("no mapping")
}
select {
case <-done:
return nil
case <-ctx.Done():
return ctx.Err()
}
}

View File

@@ -1,43 +1,60 @@
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 from github.com/alexbrainman/sspi/negotiate+
W github.com/alexbrainman/sspi/internal/common from github.com/alexbrainman/sspi/negotiate
W 💣 github.com/alexbrainman/sspi/negotiate from tailscale.com/net/tshttpproxy
L github.com/coreos/go-iptables/iptables from tailscale.com/wgengine/router
W 💣 github.com/github/certstore from tailscale.com/control/controlclient
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/net/dns
github.com/golang/snappy from github.com/klauspost/compress/zstd
github.com/google/btree from inet.af/netstack/tcpip/header+
L github.com/insomniacslk/dhcp/dhcpv4 from tailscale.com/net/tstun
L github.com/insomniacslk/dhcp/iana from github.com/insomniacslk/dhcp/dhcpv4
L github.com/insomniacslk/dhcp/interfaces from github.com/insomniacslk/dhcp/dhcpv4
L github.com/insomniacslk/dhcp/rfc1035label from github.com/insomniacslk/dhcp/dhcpv4
L github.com/josharian/native from github.com/mdlayher/netlink+
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/netlink from tailscale.com/wgengine/monitor+
L 💣 github.com/mdlayher/netlink/nlenc from github.com/mdlayher/netlink+
L github.com/mdlayher/sdnotify from tailscale.com/util/systemd
W github.com/pkg/errors from github.com/github/certstore
💣 github.com/tailscale/wireguard-go/conn from github.com/tailscale/wireguard-go/device+
W 💣 github.com/tailscale/wireguard-go/conn/winrio from github.com/tailscale/wireguard-go/conn
💣 github.com/tailscale/wireguard-go/device from tailscale.com/wgengine+
💣 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+
L 💣 github.com/mdlayher/socket from github.com/mdlayher/netlink
W github.com/pkg/errors from github.com/tailscale/certstore
W 💣 github.com/tailscale/certstore from tailscale.com/control/controlclient
github.com/tailscale/goupnp from github.com/tailscale/goupnp/dcps/internetgateway2+
github.com/tailscale/goupnp/dcps/internetgateway2 from tailscale.com/net/portmapper
github.com/tailscale/goupnp/httpu from github.com/tailscale/goupnp+
github.com/tailscale/goupnp/scpd from github.com/tailscale/goupnp
github.com/tailscale/goupnp/soap from github.com/tailscale/goupnp+
github.com/tailscale/goupnp/ssdp from github.com/tailscale/goupnp
github.com/tcnksm/go-httpstat from tailscale.com/net/netcheck
L github.com/u-root/uio/rand from github.com/insomniacslk/dhcp/dhcpv4
L github.com/u-root/uio/ubinary from github.com/u-root/uio/uio
L github.com/u-root/uio/uio from github.com/insomniacslk/dhcp/dhcpv4+
💣 go4.org/intern from inet.af/netaddr
💣 go4.org/mem from tailscale.com/control/controlclient+
💣 go4.org/mem from tailscale.com/derp+
go4.org/unsafe/assume-no-moving-gc from go4.org/intern
💣 golang.zx2c4.com/wireguard/conn from golang.zx2c4.com/wireguard/device+
W 💣 golang.zx2c4.com/wireguard/conn/winrio from golang.zx2c4.com/wireguard/conn
💣 golang.zx2c4.com/wireguard/device from tailscale.com/net/tstun+
💣 golang.zx2c4.com/wireguard/ipc from golang.zx2c4.com/wireguard/device
W 💣 golang.zx2c4.com/wireguard/ipc/winpipe from golang.zx2c4.com/wireguard/ipc
golang.zx2c4.com/wireguard/ratelimiter from golang.zx2c4.com/wireguard/device
golang.zx2c4.com/wireguard/replay from golang.zx2c4.com/wireguard/device
golang.zx2c4.com/wireguard/rwcancel from golang.zx2c4.com/wireguard/device+
golang.zx2c4.com/wireguard/tai64n from golang.zx2c4.com/wireguard/device+
💣 golang.zx2c4.com/wireguard/tun from golang.zx2c4.com/wireguard/device+
W 💣 golang.zx2c4.com/wireguard/tun/wintun from golang.zx2c4.com/wireguard/tun+
W 💣 golang.zx2c4.com/wireguard/windows/tunnel/winipcfg from tailscale.com/net/interfaces+
inet.af/netaddr from tailscale.com/control/controlclient+
inet.af/netstack/atomicbitops from inet.af/netstack/tcpip+
💣 inet.af/netstack/buffer from inet.af/netstack/tcpip/stack
💣 inet.af/netstack/gohacks from inet.af/netstack/state/wire+
inet.af/netstack/linewriter from inet.af/netstack/log
inet.af/netstack/log from inet.af/netstack/state+
@@ -46,7 +63,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
💣 inet.af/netstack/state from inet.af/netstack/tcpip+
inet.af/netstack/state/wire from inet.af/netstack/state
💣 inet.af/netstack/sync from inet.af/netstack/linewriter+
💣 inet.af/netstack/tcpip from inet.af/netstack/tcpip/adapters/gonet+
inet.af/netstack/tcpip from inet.af/netstack/tcpip/adapters/gonet+
inet.af/netstack/tcpip/adapters/gonet from tailscale.com/wgengine/netstack
💣 inet.af/netstack/tcpip/buffer from inet.af/netstack/tcpip/adapters/gonet+
inet.af/netstack/tcpip/hash/jenkins from inet.af/netstack/tcpip/stack+
@@ -56,7 +73,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
inet.af/netstack/tcpip/network/hash from inet.af/netstack/tcpip/network/ipv4+
inet.af/netstack/tcpip/network/internal/fragmentation from inet.af/netstack/tcpip/network/ipv4+
inet.af/netstack/tcpip/network/internal/ip from inet.af/netstack/tcpip/network/ipv4+
inet.af/netstack/tcpip/network/ipv4 from tailscale.com/wgengine/netstack
inet.af/netstack/tcpip/network/ipv4 from tailscale.com/wgengine/netstack+
inet.af/netstack/tcpip/network/ipv6 from tailscale.com/wgengine/netstack
inet.af/netstack/tcpip/ports from inet.af/netstack/tcpip/stack+
inet.af/netstack/tcpip/seqnum from inet.af/netstack/tcpip/header+
@@ -69,15 +86,18 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
inet.af/netstack/tcpip/transport/udp from inet.af/netstack/tcpip/adapters/gonet+
inet.af/netstack/waiter from inet.af/netstack/tcpip+
inet.af/peercred from tailscale.com/ipn/ipnserver
W 💣 inet.af/wf from tailscale.com/wf
rsc.io/goversion/version from tailscale.com/version
tailscale.com/atomicfile from tailscale.com/ipn+
tailscale.com/client/tailscale from tailscale.com/derp
tailscale.com/client/tailscale/apitype from tailscale.com/ipn/ipnlocal+
tailscale.com/control/controlclient from tailscale.com/ipn/ipnlocal+
tailscale.com/control/controlknobs from tailscale.com/control/controlclient+
tailscale.com/derp from tailscale.com/derp/derphttp+
tailscale.com/derp/derphttp from tailscale.com/net/netcheck+
tailscale.com/derp/derpmap from tailscale.com/cmd/tailscaled+
tailscale.com/disco from tailscale.com/derp+
tailscale.com/health from tailscale.com/control/controlclient+
tailscale.com/internal/deepprint from tailscale.com/ipn/ipnlocal+
tailscale.com/hostinfo from tailscale.com/control/controlclient+
tailscale.com/ipn from tailscale.com/ipn/ipnserver+
tailscale.com/ipn/ipnlocal from tailscale.com/ipn/ipnserver+
tailscale.com/ipn/ipnserver from tailscale.com/cmd/tailscaled
@@ -102,21 +122,24 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
💣 tailscale.com/net/netstat from tailscale.com/ipn/ipnserver
tailscale.com/net/packet from tailscale.com/wgengine+
tailscale.com/net/portmapper from tailscale.com/net/netcheck+
tailscale.com/net/socks5 from tailscale.com/cmd/tailscaled
tailscale.com/net/socks5 from tailscale.com/net/socks5/tssocks
tailscale.com/net/socks5/tssocks from tailscale.com/cmd/tailscaled
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/ipnlocal+
💣 tailscale.com/net/tshttpproxy from tailscale.com/control/controlclient+
tailscale.com/net/tstun from tailscale.com/cmd/tailscaled+
💣 tailscale.com/net/tstun from tailscale.com/cmd/tailscaled+
tailscale.com/paths from tailscale.com/cmd/tailscaled+
tailscale.com/portlist from tailscale.com/ipn/ipnlocal
tailscale.com/safesocket from tailscale.com/ipn/ipnserver
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/tempfork/wireguard-windows/firewall from tailscale.com/cmd/tailscaled
W tailscale.com/tsconst from tailscale.com/net/interfaces
tailscale.com/tstime from tailscale.com/wgengine/magicsock
💣 tailscale.com/tstime/mono from tailscale.com/net/tstun+
tailscale.com/tstime/rate from tailscale.com/wgengine/filter
tailscale.com/types/dnstype from tailscale.com/ipn/ipnlocal+
tailscale.com/types/empty from tailscale.com/control/controlclient+
tailscale.com/types/flagtype from tailscale.com/cmd/tailscaled
tailscale.com/types/ipproto from tailscale.com/net/flowtrack+
@@ -125,32 +148,38 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
tailscale.com/types/netmap from tailscale.com/control/controlclient+
tailscale.com/types/nettype from tailscale.com/wgengine/magicsock
tailscale.com/types/opt from tailscale.com/control/controlclient+
tailscale.com/types/pad32 from tailscale.com/net/tstun+
tailscale.com/types/persist from tailscale.com/control/controlclient+
tailscale.com/types/preftype from tailscale.com/ipn+
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+
L tailscale.com/util/cmpver from tailscale.com/net/dns
💣 tailscale.com/util/deephash from tailscale.com/ipn/ipnlocal+
tailscale.com/util/dnsname from tailscale.com/ipn/ipnstate+
LW tailscale.com/util/endian from tailscale.com/net/netns+
L tailscale.com/util/lineread from tailscale.com/control/controlclient+
tailscale.com/util/groupmember from tailscale.com/ipn/ipnserver
tailscale.com/util/lineread from tailscale.com/control/controlclient+
tailscale.com/util/osshare from tailscale.com/cmd/tailscaled+
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/util/uniq from tailscale.com/wgengine/magicsock
tailscale.com/util/winutil from tailscale.com/logpolicy+
tailscale.com/version from tailscale.com/cmd/tailscaled+
tailscale.com/version/distro from tailscale.com/control/controlclient+
W tailscale.com/wf from tailscale.com/cmd/tailscaled
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/magicsock from tailscale.com/wgengine+
tailscale.com/wgengine/monitor from tailscale.com/wgengine+
tailscale.com/wgengine/netstack from tailscale.com/cmd/tailscaled
tailscale.com/wgengine/netstack from tailscale.com/cmd/tailscaled+
tailscale.com/wgengine/router from tailscale.com/cmd/tailscaled+
tailscale.com/wgengine/wgcfg from tailscale.com/ipn/ipnlocal+
tailscale.com/wgengine/wgcfg/nmcfg from tailscale.com/ipn/ipnlocal
tailscale.com/wgengine/wglog 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/blake2s from golang.zx2c4.com/wireguard/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+
@@ -159,31 +188,32 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
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/poly1305 from golang.org/x/crypto/chacha20poly1305+
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/dns/dnsmessage from net+
golang.org/x/net/http/httpguts from net/http
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/ipv4 from golang.zx2c4.com/wireguard/device
golang.org/x/net/ipv6 from golang.zx2c4.com/wireguard/device+
golang.org/x/net/proxy from tailscale.com/net/netns
D golang.org/x/net/route from net+
golang.org/x/sync/errgroup from tailscale.com/derp
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/tailscale/wireguard-go/conn+
LD golang.org/x/sys/unix from github.com/mdlayher/netlink+
W golang.org/x/sys/windows from github.com/go-ole/go-ole+
W golang.org/x/sys/windows/registry from golang.zx2c4.com/wireguard/windows/tunnel/winipcfg+
W golang.org/x/sys/windows/svc from tailscale.com/cmd/tailscaled
W golang.org/x/sys/windows/svc from tailscale.com/cmd/tailscaled+
W golang.org/x/sys/windows/svc/mgr from tailscale.com/cmd/tailscaled
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+
golang.org/x/time/rate from inet.af/netstack/tcpip/stack+
bufio from compress/flate+
bytes from bufio+
compress/flate from compress/gzip+
@@ -216,6 +246,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
debug/elf from rsc.io/goversion/version
debug/macho from rsc.io/goversion/version
debug/pe from rsc.io/goversion/version
embed from tailscale.com/net/dns+
encoding from encoding/json+
encoding/asn1 from crypto/x509+
encoding/base64 from encoding/json+
@@ -223,6 +254,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
encoding/hex from crypto/x509+
encoding/json from expvar+
encoding/pem from crypto/tls+
encoding/xml from github.com/tailscale/goupnp+
errors from bufio+
expvar from tailscale.com/derp+
flag from tailscale.com/cmd/tailscaled+
@@ -247,7 +279,8 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
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/httputil from tailscale.com/ipn/localapi
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+

View File

@@ -0,0 +1,123 @@
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package main
import (
"context"
"errors"
"fmt"
"os"
"time"
"golang.org/x/sys/windows"
"golang.org/x/sys/windows/svc"
"golang.org/x/sys/windows/svc/mgr"
"tailscale.com/logtail/backoff"
"tailscale.com/types/logger"
"tailscale.com/util/osshare"
)
func init() {
installSystemDaemon = installSystemDaemonWindows
uninstallSystemDaemon = uninstallSystemDaemonWindows
}
func installSystemDaemonWindows(args []string) (err error) {
m, err := mgr.Connect()
if err != nil {
return fmt.Errorf("failed to connect to Windows service manager: %v", err)
}
service, err := m.OpenService(serviceName)
if err == nil {
service.Close()
return fmt.Errorf("service %q is already installed", serviceName)
}
// no such service; proceed to install the service.
exe, err := os.Executable()
if err != nil {
return err
}
c := mgr.Config{
ServiceType: windows.SERVICE_WIN32_OWN_PROCESS,
StartType: mgr.StartAutomatic,
ErrorControl: mgr.ErrorNormal,
DisplayName: serviceName,
Description: "Connects this computer to others on the Tailscale network.",
}
service, err = m.CreateService(serviceName, exe, c)
if err != nil {
return fmt.Errorf("failed to create %q service: %v", serviceName, err)
}
defer service.Close()
// Exponential backoff is often too aggressive, so use (mostly)
// squares instead.
ra := []mgr.RecoveryAction{
{mgr.ServiceRestart, 1 * time.Second},
{mgr.ServiceRestart, 2 * time.Second},
{mgr.ServiceRestart, 4 * time.Second},
{mgr.ServiceRestart, 9 * time.Second},
{mgr.ServiceRestart, 16 * time.Second},
{mgr.ServiceRestart, 25 * time.Second},
{mgr.ServiceRestart, 36 * time.Second},
{mgr.ServiceRestart, 49 * time.Second},
{mgr.ServiceRestart, 64 * time.Second},
}
const resetPeriodSecs = 60
err = service.SetRecoveryActions(ra, resetPeriodSecs)
if err != nil {
return fmt.Errorf("failed to set service recovery actions: %v", err)
}
return nil
}
func uninstallSystemDaemonWindows(args []string) (ret error) {
// Remove file sharing from Windows shell (noop in non-windows)
osshare.SetFileSharingEnabled(false, logger.Discard)
m, err := mgr.Connect()
if err != nil {
return fmt.Errorf("failed to connect to Windows service manager: %v", err)
}
defer m.Disconnect()
service, err := m.OpenService(serviceName)
if err != nil {
return fmt.Errorf("failed to open %q service: %v", serviceName, err)
}
st, err := service.Query()
if err != nil {
service.Close()
return fmt.Errorf("failed to query service state: %v", err)
}
if st.State != svc.Stopped {
service.Control(svc.Stop)
}
err = service.Delete()
service.Close()
if err != nil {
return fmt.Errorf("failed to delete service: %v", err)
}
bo := backoff.NewBackoff("uninstall", logger.Discard, 30*time.Second)
end := time.Now().Add(15 * time.Second)
for time.Until(end) > 0 {
service, err = m.OpenService(serviceName)
if err != nil {
// service is no longer openable; success!
break
}
service.Close()
bo.BackOff(context.Background(), errors.New("service not deleted"))
}
return nil
}

View File

@@ -24,38 +24,28 @@ import (
"runtime/debug"
"strconv"
"strings"
"sync"
"syscall"
"time"
"github.com/go-multierror/multierror"
"tailscale.com/ipn"
"tailscale.com/ipn/ipnserver"
"tailscale.com/logpolicy"
"tailscale.com/net/dns"
"tailscale.com/net/socks5"
"tailscale.com/net/socks5/tssocks"
"tailscale.com/net/tstun"
"tailscale.com/paths"
"tailscale.com/types/flagtype"
"tailscale.com/types/logger"
"tailscale.com/types/netmap"
"tailscale.com/util/osshare"
"tailscale.com/version"
"tailscale.com/version/distro"
"tailscale.com/wgengine"
"tailscale.com/wgengine/magicsock"
"tailscale.com/wgengine/monitor"
"tailscale.com/wgengine/netstack"
"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"
// defaultTunName returns the default tun device name for the platform.
func defaultTunName() string {
switch runtime.GOOS {
@@ -78,9 +68,13 @@ func defaultTunName() string {
}
var args struct {
// tunname is a /dev/net/tun tunnel name ("tailscale0"), the
// string "userspace-networking", "tap:TAPNAME[:BRIDGENAME]"
// or comma-separated list thereof.
tunname string
cleanup bool
debug string
tunname string // tun name, "userspace-networking", or comma-separated list thereof
port uint16
statepath string
socketpath string
@@ -114,7 +108,7 @@ func main() {
flag.StringVar(&args.debug, "debug", "", "listen address ([ip]:port) of optional debug server")
flag.StringVar(&args.socksAddr, "socks5-server", "", `optional [ip]:port to run a SOCK5 server (e.g. "localhost:1080")`)
flag.StringVar(&args.tunname, "tun", defaultTunName(), `tunnel interface name; use "userspace-networking" (beta) to not use TUN`)
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.Var(flagtype.PortValue(&args.port, 0), "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")
@@ -148,7 +142,7 @@ func main() {
os.Exit(0)
}
if runtime.GOOS == "darwin" && os.Getuid() != 0 && !strings.Contains(args.tunname, "userspace-networking") {
if runtime.GOOS == "darwin" && os.Getuid() != 0 && !strings.Contains(args.tunname, "userspace-networking") && !args.cleanup {
log.SetFlags(0)
log.Fatalf("tailscaled requires root; use sudo tailscaled (or use --tun=userspace-networking)")
}
@@ -158,12 +152,39 @@ func main() {
log.Fatalf("--socket is required")
}
if err := run(); err != nil {
err := run()
// Remove file sharing from Windows shell (noop in non-windows)
osshare.SetFileSharingEnabled(false, logger.Discard)
if err != nil {
// No need to log; the func already did
os.Exit(1)
}
}
func ipnServerOpts() (o ipnserver.Options) {
// Allow changing the OS-specific IPN behavior for tests
// so we can e.g. test Windows-specific behaviors on Linux.
goos := os.Getenv("TS_DEBUG_TAILSCALED_IPN_GOOS")
if goos == "" {
goos = runtime.GOOS
}
o.Port = 41112
o.StatePath = args.statepath
o.SocketPath = args.socketpath // even for goos=="windows", for tests
switch goos {
default:
o.SurviveDisconnects = true
o.AutostartStateKey = ipn.GlobalDaemonStateKey
case "windows":
// Not those.
}
return o
}
func run() error {
var err error
@@ -193,6 +214,9 @@ func run() error {
logf = logger.RateLimitedFn(logf, 5*time.Second, 5, 100)
if args.cleanup {
if os.Getenv("TS_PLEASE_PANIC") != "" {
panic("TS_PLEASE_PANIC asked us to panic")
}
dns.Cleanup(logf, args.tunname)
router.Cleanup(logf, args.tunname)
return nil
@@ -221,6 +245,11 @@ func run() error {
if err != nil {
log.Fatalf("SOCKS5 listener: %v", err)
}
if strings.HasSuffix(args.socksAddr, ":0") {
// Log kernel-selected port number so integration tests
// can find it portably.
log.Printf("SOCKS5 listening on %v", socksListener.Addr())
}
}
e, useNetstack, err := createEngine(logf, linkMon)
@@ -230,45 +259,13 @@ func run() error {
}
var ns *netstack.Impl
if useNetstack {
tunDev, magicConn, ok := e.(wgengine.InternalsGetter).GetInternals()
if !ok {
log.Fatalf("%T is not a wgengine.InternalsGetter", e)
}
ns, err = netstack.Create(logf, tunDev, e, magicConn)
if err != nil {
log.Fatalf("netstack.Create: %v", err)
}
if err := ns.Start(); err != nil {
log.Fatalf("failed to start netstack: %v", err)
}
if useNetstack || wrapNetstack {
onlySubnets := wrapNetstack && !useNetstack
ns = mustStartNetstack(logf, e, onlySubnets)
}
if socksListener != nil {
srv := &socks5.Server{
Logf: logger.WithPrefix(logf, "socks5: "),
}
if useNetstack {
srv.Dialer = func(ctx context.Context, network, addr string) (net.Conn, error) {
return ns.DialContextTCP(ctx, addr)
}
} else {
var mu sync.Mutex
var dns netstack.DNSMap
e.AddNetworkMapCallback(func(nm *netmap.NetworkMap) {
mu.Lock()
defer mu.Unlock()
dns = netstack.DNSMapFromNetworkMap(nm)
})
srv.Dialer = func(ctx context.Context, network, addr string) (net.Conn, error) {
ipp, err := dns.Resolve(ctx, addr)
if err != nil {
return nil, err
}
var d net.Dialer
return d.DialContext(ctx, network, ipp.String())
}
}
srv := tssocks.NewServer(logger.WithPrefix(logf, "socks5: "), e, ns)
go func() {
log.Fatalf("SOCKS5 server exited: %v", srv.Serve(socksListener))
}()
@@ -295,15 +292,8 @@ func run() error {
}
}()
opts := ipnserver.Options{
SocketPath: args.socketpath,
Port: 41112,
StatePath: args.statepath,
AutostartStateKey: globalStateKey,
LegacyConfigPath: paths.LegacyConfigPath(),
SurviveDisconnects: true,
DebugMux: debugMux,
}
opts := ipnServerOpts()
opts.DebugMux = debugMux
err = ipnserver.Run(ctx, logf, pol.PublicID.String(), ipnserver.FixedEngine(e), opts)
// Cancelation is not an error: it is the only way to stop ipnserver.
if err != nil && err != context.Canceled {
@@ -331,6 +321,28 @@ func createEngine(logf logger.Logf, linkMon *monitor.Mon) (e wgengine.Engine, us
return nil, false, multierror.New(errs)
}
var wrapNetstack = shouldWrapNetstack()
func shouldWrapNetstack() bool {
if e := os.Getenv("TS_DEBUG_WRAP_NETSTACK"); e != "" {
v, err := strconv.ParseBool(e)
if err != nil {
log.Fatalf("invalid TS_DEBUG_WRAP_NETSTACK value: %v", err)
}
return v
}
if distro.Get() == distro.Synology {
return true
}
switch runtime.GOOS {
case "windows", "darwin":
// Enable on Windows and tailscaled-on-macOS (this doesn't
// affect the GUI clients).
return true
}
return false
}
func tryEngine(logf logger.Logf, linkMon *monitor.Mon, name string) (e wgengine.Engine, useNetstack bool, err error) {
conf := wgengine.Config{
ListenPort: args.port,
@@ -338,25 +350,32 @@ func tryEngine(logf logger.Logf, linkMon *monitor.Mon, name string) (e wgengine.
}
useNetstack = name == "userspace-networking"
if !useNetstack {
dev, err := tstun.New(logf, name)
dev, devName, err := tstun.New(logf, name)
if err != nil {
tstun.Diagnose(logf, name)
return nil, false, err
}
conf.Tun = dev
r, err := router.New(logf, dev)
if strings.HasPrefix(name, "tap:") {
conf.IsTAP = true
e, err := wgengine.NewUserspaceEngine(logf, conf)
return e, false, err
}
r, err := router.New(logf, dev, linkMon)
if err != nil {
dev.Close()
return nil, false, err
}
d, err := dns.NewOSConfigurator(logf, devName)
if err != nil {
return nil, false, err
}
conf.DNS = d
conf.Router = r
tunname, err := dev.Name()
if err != nil {
r.Close()
dev.Close()
return nil, false, err
if wrapNetstack {
conf.Router = netstack.NewSubnetRouterWrapper(conf.Router)
}
conf.DNS = dns.NewOSConfigurator(logf, tunname)
}
e, err = wgengine.NewUserspaceEngine(logf, conf)
if err != nil {
@@ -384,3 +403,18 @@ func runDebugServer(mux *http.ServeMux, addr string) {
log.Fatal(err)
}
}
func mustStartNetstack(logf logger.Logf, e wgengine.Engine, onlySubnets bool) *netstack.Impl {
tunDev, magicConn, ok := e.(wgengine.InternalsGetter).GetInternals()
if !ok {
log.Fatalf("%T is not a wgengine.InternalsGetter", e)
}
ns, err := netstack.Create(logf, tunDev, e, magicConn, onlySubnets)
if err != nil {
log.Fatalf("netstack.Create: %v", err)
}
if err := ns.Start(); err != nil {
log.Fatalf("failed to start netstack: %v", err)
}
return ns
}

View File

@@ -0,0 +1,25 @@
#!/sbin/openrc-run
set -a
source /etc/default/tailscaled
set +a
command="/usr/sbin/tailscaled"
command_args="--state=/var/lib/tailscale/tailscaled.state --port=$PORT --socket=/var/run/tailscale/tailscaled.sock $FLAGS"
command_background=true
pidfile="/run/tailscaled.pid"
start_stop_daemon_args="-1 /var/log/tailscaled.log -2 /var/log/tailscaled.log"
depend() {
need net
}
start_pre() {
mkdir -p /var/run/tailscale
mkdir -p /var/lib/tailscale
$command --cleanup
}
stop_post() {
$command --cleanup
}

View File

@@ -2,7 +2,7 @@
Description=Tailscale node agent
Documentation=https://tailscale.com/kb/
Wants=network-pre.target
After=network-pre.target
After=network-pre.target NetworkManager.service systemd-resolved.service
[Service]
EnvironmentFile=/etc/default/tailscaled

View File

@@ -2,6 +2,7 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build !windows
// +build !windows
package main // import "tailscale.com/cmd/tailscaled"

View File

@@ -19,22 +19,25 @@ package main // import "tailscale.com/cmd/tailscaled"
import (
"context"
"encoding/json"
"fmt"
"log"
"net"
"os"
"time"
"golang.org/x/sys/windows"
"golang.org/x/sys/windows/svc"
"golang.zx2c4.com/wireguard/windows/tunnel/winipcfg"
"inet.af/netaddr"
"tailscale.com/ipn/ipnserver"
"tailscale.com/logpolicy"
"tailscale.com/net/dns"
"tailscale.com/net/tstun"
"tailscale.com/tempfork/wireguard-windows/firewall"
"tailscale.com/types/logger"
"tailscale.com/version"
"tailscale.com/wf"
"tailscale.com/wgengine"
"tailscale.com/wgengine/netstack"
"tailscale.com/wgengine/router"
)
@@ -125,16 +128,6 @@ func beFirewallKillswitch() bool {
log.SetFlags(0)
log.Printf("killswitch subprocess starting, tailscale GUID is %s", os.Args[2])
go func() {
b := make([]byte, 16)
for {
_, err := os.Stdin.Read(b)
if err != nil {
log.Fatalf("parent process died or requested exit, exiting (%v)", err)
}
}
}()
guid, err := windows.GUIDFromString(os.Args[2])
if err != nil {
log.Fatalf("invalid GUID %q: %v", os.Args[2], err)
@@ -142,101 +135,141 @@ func beFirewallKillswitch() bool {
luid, err := winipcfg.LUIDFromGUID(&guid)
if err != nil {
log.Fatalf("no interface with GUID %q", guid)
log.Fatalf("no interface with GUID %q: %v", guid, err)
}
noProtection := false
var dnsIPs []net.IP // unused in called code.
start := time.Now()
firewall.EnableFirewall(uint64(luid), noProtection, dnsIPs)
fw, err := wf.New(uint64(luid))
if err != nil {
log.Fatalf("failed to enable firewall: %v", err)
}
log.Printf("killswitch enabled, took %s", time.Since(start))
// Block until the monitor goroutine shuts us down.
select {}
// Note(maisem): when local lan access toggled, tailscaled needs to
// inform the firewall to let local routes through. The set of routes
// is passed in via stdin encoded in json.
dcd := json.NewDecoder(os.Stdin)
for {
var routes []netaddr.IPPrefix
if err := dcd.Decode(&routes); err != nil {
log.Fatalf("parent process died or requested exit, exiting (%v)", err)
}
if err := fw.UpdatePermittedRoutes(routes); err != nil {
log.Fatalf("failed to update routes (%v)", err)
}
}
}
func startIPNServer(ctx context.Context, logid string) error {
var logf logger.Logf = log.Printf
var eng wgengine.Engine
var err error
getEngine := func() (wgengine.Engine, error) {
dev, err := tstun.New(logf, "Tailscale")
getEngineRaw := func() (wgengine.Engine, error) {
dev, devName, err := tstun.New(logf, "Tailscale")
if err != nil {
return nil, err
return nil, fmt.Errorf("TUN: %w", err)
}
r, err := router.New(logf, dev)
r, err := router.New(logf, dev, nil)
if err != nil {
dev.Close()
return nil, err
return nil, fmt.Errorf("router: %w", err)
}
if wrapNetstack {
r = netstack.NewSubnetRouterWrapper(r)
}
d, err := dns.NewOSConfigurator(logf, devName)
if err != nil {
r.Close()
dev.Close()
return nil, fmt.Errorf("DNS: %w", err)
}
eng, err := wgengine.NewUserspaceEngine(logf, wgengine.Config{
Tun: dev,
Router: r,
DNS: d,
ListenPort: 41641,
})
if err != nil {
r.Close()
dev.Close()
return nil, err
return nil, fmt.Errorf("engine: %w", err)
}
onlySubnets := true
if wrapNetstack {
mustStartNetstack(logf, eng, onlySubnets)
}
return wgengine.NewWatchdog(eng), nil
}
if msg := os.Getenv("TS_DEBUG_WIN_FAIL"); msg != "" {
err = fmt.Errorf("pretending to be a service failure: %v", msg)
} else {
// We have a bunch of bug reports of wgengine.NewUserspaceEngine returning a few different errors,
// all intermittently. A few times I (Brad) have also seen sporadic failures that simply
// restarting fixed. So try a few times.
for try := 1; try <= 5; try++ {
if try > 1 {
// Only sleep a bit. Don't do some massive backoff because
// the frontend GUI has a 30 second timeout on connecting to us,
// but even 5 seconds is too long for them to get any results.
// 5 tries * 1 second each seems fine.
time.Sleep(time.Second)
}
eng, err = getEngine()
type engineOrError struct {
Engine wgengine.Engine
Err error
}
engErrc := make(chan engineOrError)
t0 := time.Now()
go func() {
const ms = time.Millisecond
for try := 1; ; try++ {
logf("tailscaled: getting engine... (try %v)", try)
t1 := time.Now()
eng, err := getEngineRaw()
d, dt := time.Since(t1).Round(ms), time.Since(t1).Round(ms)
if err != nil {
logf("wgengine.NewUserspaceEngine: (try %v) %v", try, err)
logf("tailscaled: engine fetch error (try %v) in %v (total %v, sysUptime %v): %v",
try, d, dt, windowsUptime().Round(time.Second), err)
} else {
if try > 1 {
logf("tailscaled: got engine on try %v in %v (total %v)", try, d, dt)
} else {
logf("tailscaled: got engine in %v", d)
}
}
timer := time.NewTimer(5 * time.Second)
engErrc <- engineOrError{eng, err}
if err == nil {
timer.Stop()
return
}
<-timer.C
}
}()
// getEngine is called by ipnserver to get the engine. It's
// not called concurrently and is not called again once it
// successfully returns an engine.
getEngine := func() (wgengine.Engine, error) {
if msg := os.Getenv("TS_DEBUG_WIN_FAIL"); msg != "" {
return nil, fmt.Errorf("pretending to be a service failure: %v", msg)
}
for {
res := <-engErrc
if res.Engine != nil {
return res.Engine, nil
}
if time.Since(t0) < time.Minute || windowsUptime() < 10*time.Minute {
// Ignore errors during early boot. Windows 10 auto logs in the GUI
// way sooner than the networking stack components start up.
// So the network will fail for a bit (and require a few tries) while
// the GUI is still fine.
continue
}
if try > 1 {
logf("wgengine.NewUserspaceEngine: ended up working on try %v", try)
}
break
// Return nicer errors to users, annotated with logids, which helps
// when they file bugs.
return nil, fmt.Errorf("%w\n\nlogid: %v", res.Err, logid)
}
}
if err != nil {
// Log the error, but don't fatalf. We want to
// propagate the error message to the UI frontend. So
// we continue and tell the ipnserver to return that
// in a Notify message.
logf("wgengine.NewUserspaceEngine: %v", err)
}
opts := ipnserver.Options{
Port: 41112,
SurviveDisconnects: false,
StatePath: args.statepath,
}
if err != nil {
// Return nicer errors to users, annotated with logids, which helps
// when they file bugs.
rawGetEngine := getEngine // raw == without verbose logid-containing error
getEngine = func() (wgengine.Engine, error) {
eng, err := rawGetEngine()
if err != nil {
return nil, fmt.Errorf("wgengine.NewUserspaceEngine: %v\n\nlogid: %v", err, logid)
}
return eng, nil
}
} else {
getEngine = ipnserver.FixedEngine(eng)
}
err = ipnserver.Run(ctx, logf, logid, getEngine, opts)
err := ipnserver.Run(ctx, logf, logid, getEngine, ipnServerOpts())
if err != nil {
logf("ipnserver.Run: %v", err)
}
return err
}
var (
kernel32 = windows.NewLazySystemDLL("kernel32.dll")
getTickCount64Proc = kernel32.NewProc("GetTickCount64")
)
func windowsUptime() time.Duration {
r, _, _ := getTickCount64Proc.Call()
return time.Duration(int64(r)) * time.Millisecond
}

View File

@@ -2,6 +2,7 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build !windows
// +build !windows
// The tsshd binary is an SSH server that accepts connections
@@ -29,8 +30,8 @@ import (
"time"
"unsafe"
"github.com/creack/pty"
"github.com/gliderlabs/ssh"
"github.com/kr/pty"
gossh "golang.org/x/crypto/ssh"
"inet.af/netaddr"
"tailscale.com/net/interfaces"
@@ -59,11 +60,11 @@ func main() {
warned := false
for {
addr, iface, err := interfaces.Tailscale()
addrs, iface, err := interfaces.Tailscale()
if err != nil {
log.Fatalf("listing interfaces: %v", err)
}
if addr == nil {
if len(addrs) == 0 {
if !warned {
log.Printf("no tailscale interface found; polling until one is available")
warned = true
@@ -75,6 +76,13 @@ func main() {
continue
}
warned = false
var addr netaddr.IP
for _, a := range addrs {
if a.Is4() {
addr = a
break
}
}
listen := net.JoinHostPort(addr.String(), fmt.Sprint(*port))
log.Printf("tailscale ssh server listening on %v, %v", iface.Name, listen)
s := &ssh.Server{

View File

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

View File

@@ -2,18 +2,11 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package controlclient implements the client for the Tailscale
// control plane.
//
// It handles authentication, port picking, and collects the local
// network configuration.
package controlclient
import (
"context"
"encoding/json"
"fmt"
"reflect"
"sync"
"time"
@@ -28,87 +21,28 @@ import (
"tailscale.com/types/wgkey"
)
// State is the high-level state of the client. It is used only in
// unit tests for proper sequencing, don't depend on it anywhere else.
// TODO(apenwarr): eliminate 'state', as it's now obsolete.
type State int
const (
StateNew = State(iota)
StateNotAuthenticated
StateAuthenticating
StateURLVisitRequired
StateAuthenticated
StateSynchronized // connected and received map update
)
func (s State) MarshalText() ([]byte, error) {
return []byte(s.String()), nil
}
func (s State) String() string {
switch s {
case StateNew:
return "state:new"
case StateNotAuthenticated:
return "state:not-authenticated"
case StateAuthenticating:
return "state:authenticating"
case StateURLVisitRequired:
return "state:url-visit-required"
case StateAuthenticated:
return "state:authenticated"
case StateSynchronized:
return "state:synchronized"
default:
return fmt.Sprintf("state:unknown:%d", int(s))
}
}
type Status struct {
_ structs.Incomparable
LoginFinished *empty.Message
Err string
URL string
Persist *persist.Persist // locally persisted configuration
NetMap *netmap.NetworkMap // server-pushed configuration
Hostinfo *tailcfg.Hostinfo // current Hostinfo data
State State
}
// Equal reports whether s and s2 are equal.
func (s *Status) Equal(s2 *Status) bool {
if s == nil && s2 == nil {
return true
}
return s != nil && s2 != nil &&
(s.LoginFinished == nil) == (s2.LoginFinished == nil) &&
s.Err == s2.Err &&
s.URL == s2.URL &&
reflect.DeepEqual(s.Persist, s2.Persist) &&
reflect.DeepEqual(s.NetMap, s2.NetMap) &&
reflect.DeepEqual(s.Hostinfo, s2.Hostinfo) &&
s.State == s2.State
}
func (s Status) String() string {
b, err := json.MarshalIndent(s, "", "\t")
if err != nil {
panic(err)
}
return s.State.String() + " " + string(b)
}
type LoginGoal struct {
_ structs.Incomparable
wantLoggedIn bool // true if we *want* to be logged in
token *tailcfg.Oauth2Token // oauth token to use when logging in
flags LoginFlags // flags to use when logging in
url string // auth url that needs to be visited
_ structs.Incomparable
wantLoggedIn bool // true if we *want* to be logged in
token *tailcfg.Oauth2Token // oauth token to use when logging in
flags LoginFlags // flags to use when logging in
url string // auth url that needs to be visited
loggedOutResult chan<- error
}
// Client connects to a tailcontrol server for a node.
type Client struct {
func (g *LoginGoal) sendLogoutError(err error) {
if g.loggedOutResult == nil {
return
}
select {
case g.loggedOutResult <- err:
default:
}
}
// Auto connects to a tailcontrol server for a node.
// It's a concrete implementation of the Client interface.
type Auto struct {
direct *Direct // our interface to the server APIs
timeNow func() time.Time
logf logger.Logf
@@ -141,8 +75,8 @@ type Client struct {
mapDone chan struct{} // when closed, map goroutine is done
}
// New creates and starts a new Client.
func New(opts Options) (*Client, error) {
// New creates and starts a new Auto.
func New(opts Options) (*Auto, error) {
c, err := NewNoStart(opts)
if c != nil {
c.Start()
@@ -150,8 +84,8 @@ func New(opts Options) (*Client, error) {
return c, err
}
// NewNoStart creates a new Client, but without calling Start on it.
func NewNoStart(opts Options) (*Client, error) {
// NewNoStart creates a new Auto, but without calling Start on it.
func NewNoStart(opts Options) (*Auto, error) {
direct, err := NewDirect(opts)
if err != nil {
return nil, err
@@ -162,7 +96,7 @@ func NewNoStart(opts Options) (*Client, error) {
if opts.TimeNow == nil {
opts.TimeNow = time.Now
}
c := &Client{
c := &Auto{
direct: direct,
timeNow: opts.TimeNow,
logf: opts.Logf,
@@ -178,7 +112,7 @@ func NewNoStart(opts Options) (*Client, error) {
}
func (c *Client) onHealthChange(sys health.Subsystem, err error) {
func (c *Auto) onHealthChange(sys health.Subsystem, err error) {
if sys == health.SysOverall {
return
}
@@ -189,12 +123,13 @@ func (c *Client) onHealthChange(sys health.Subsystem, err error) {
// SetPaused controls whether HTTP activity should be paused.
//
// The client can be paused and unpaused repeatedly, unlike Start and Shutdown, which can only be used once.
func (c *Client) SetPaused(paused bool) {
func (c *Auto) SetPaused(paused bool) {
c.mu.Lock()
defer c.mu.Unlock()
if paused == c.paused {
return
}
c.logf("setPaused(%v)", paused)
c.paused = paused
if paused {
// Only cancel the map routine. (The auth routine isn't expensive
@@ -211,7 +146,7 @@ func (c *Client) SetPaused(paused bool) {
// Start starts the client's goroutines.
//
// It should only be called for clients created by NewNoStart.
func (c *Client) Start() {
func (c *Auto) Start() {
go c.authRoutine()
go c.mapRoutine()
}
@@ -221,7 +156,7 @@ func (c *Client) Start() {
// streaming response open), or start a new streaming one if necessary.
//
// It should be called whenever there's something new to tell the server.
func (c *Client) sendNewMapRequest() {
func (c *Auto) sendNewMapRequest() {
c.mu.Lock()
// If we're not already streaming a netmap, or if we're already stuck
@@ -260,7 +195,7 @@ func (c *Client) sendNewMapRequest() {
}()
}
func (c *Client) cancelAuth() {
func (c *Auto) cancelAuth() {
c.mu.Lock()
if c.authCancel != nil {
c.authCancel()
@@ -271,7 +206,7 @@ func (c *Client) cancelAuth() {
c.mu.Unlock()
}
func (c *Client) cancelMapLocked() {
func (c *Auto) cancelMapLocked() {
if c.mapCancel != nil {
c.mapCancel()
}
@@ -280,13 +215,13 @@ func (c *Client) cancelMapLocked() {
}
}
func (c *Client) cancelMapUnsafely() {
func (c *Auto) cancelMapUnsafely() {
c.mu.Lock()
c.cancelMapLocked()
c.mu.Unlock()
}
func (c *Client) cancelMapSafely() {
func (c *Auto) cancelMapSafely() {
c.mu.Lock()
defer c.mu.Unlock()
@@ -322,7 +257,7 @@ func (c *Client) cancelMapSafely() {
}
}
func (c *Client) authRoutine() {
func (c *Auto) authRoutine() {
defer close(c.authDone)
bo := backoff.NewBackoff("authRoutine", c.logf, 30*time.Second)
@@ -333,7 +268,7 @@ func (c *Client) authRoutine() {
if goal != nil {
c.logf("authRoutine: %s; wantLoggedIn=%v", c.state, goal.wantLoggedIn)
} else {
c.logf("authRoutine: %s; goal=nil", c.state)
c.logf("authRoutine: %s; goal=nil paused=%v", c.state, c.paused)
}
c.mu.Unlock()
@@ -363,6 +298,7 @@ func (c *Client) authRoutine() {
if !goal.wantLoggedIn {
err := c.direct.TryLogout(ctx)
goal.sendLogoutError(err)
if err != nil {
report(err, "TryLogout")
bo.BackOff(ctx, err)
@@ -402,7 +338,8 @@ func (c *Client) authRoutine() {
report(err, f)
bo.BackOff(ctx, err)
continue
} else if url != "" {
}
if url != "" {
if goal.url != "" {
err = fmt.Errorf("[unexpected] server required a new URL?")
report(err, "WaitLoginURL")
@@ -439,7 +376,7 @@ func (c *Client) authRoutine() {
// Expiry returns the credential expiration time, or the zero time if
// the expiration time isn't known. Used in tests only.
func (c *Client) Expiry() *time.Time {
func (c *Auto) Expiry() *time.Time {
c.mu.Lock()
defer c.mu.Unlock()
return c.expiry
@@ -447,21 +384,21 @@ func (c *Client) Expiry() *time.Time {
// Direct returns the underlying direct client object. Used in tests
// only.
func (c *Client) Direct() *Direct {
func (c *Auto) Direct() *Direct {
return c.direct
}
// unpausedChanLocked returns a new channel that is closed when the
// current Client pause is unpaused.
// current Auto pause is unpaused.
//
// c.mu must be held
func (c *Client) unpausedChanLocked() <-chan struct{} {
func (c *Auto) unpausedChanLocked() <-chan struct{} {
unpaused := make(chan struct{})
c.unpauseWaiters = append(c.unpauseWaiters, unpaused)
return unpaused
}
func (c *Client) mapRoutine() {
func (c *Auto) mapRoutine() {
defer close(c.mapDone)
bo := backoff.NewBackoff("mapRoutine", c.logf, 30*time.Second)
@@ -583,7 +520,10 @@ func (c *Client) mapRoutine() {
}
}
func (c *Client) AuthCantContinue() bool {
func (c *Auto) AuthCantContinue() bool {
if c == nil {
return true
}
c.mu.Lock()
defer c.mu.Unlock()
@@ -591,13 +531,13 @@ func (c *Client) AuthCantContinue() bool {
}
// SetStatusFunc sets fn as the callback to run on any status change.
func (c *Client) SetStatusFunc(fn func(Status)) {
func (c *Auto) SetStatusFunc(fn func(Status)) {
c.mu.Lock()
c.statusFunc = fn
c.mu.Unlock()
}
func (c *Client) SetHostinfo(hi *tailcfg.Hostinfo) {
func (c *Auto) SetHostinfo(hi *tailcfg.Hostinfo) {
if hi == nil {
panic("nil Hostinfo")
}
@@ -610,7 +550,7 @@ func (c *Client) SetHostinfo(hi *tailcfg.Hostinfo) {
c.sendNewMapRequest()
}
func (c *Client) SetNetInfo(ni *tailcfg.NetInfo) {
func (c *Auto) SetNetInfo(ni *tailcfg.NetInfo) {
if ni == nil {
panic("nil NetInfo")
}
@@ -623,7 +563,7 @@ func (c *Client) SetNetInfo(ni *tailcfg.NetInfo) {
c.sendNewMapRequest()
}
func (c *Client) sendStatus(who string, err error, url string, nm *netmap.NetworkMap) {
func (c *Auto) sendStatus(who string, err error, url string, nm *netmap.NetworkMap) {
c.mu.Lock()
state := c.state
loggedIn := c.loggedIn
@@ -636,9 +576,12 @@ func (c *Client) sendStatus(who string, err error, url string, nm *netmap.Networ
c.logf("[v1] sendStatus: %s: %v", who, state)
var p *persist.Persist
var fin *empty.Message
var loginFin, logoutFin *empty.Message
if state == StateAuthenticated {
fin = new(empty.Message)
loginFin = new(empty.Message)
}
if state == StateNotAuthenticated {
logoutFin = new(empty.Message)
}
if nm != nil && loggedIn && synced {
pp := c.direct.GetPersist()
@@ -649,12 +592,13 @@ func (c *Client) sendStatus(who string, err error, url string, nm *netmap.Networ
nm = nil
}
new := Status{
LoginFinished: fin,
URL: url,
Persist: p,
NetMap: nm,
Hostinfo: hi,
State: state,
LoginFinished: loginFin,
LogoutFinished: logoutFin,
URL: url,
Persist: p,
NetMap: nm,
Hostinfo: hi,
State: state,
}
if err != nil {
new.Err = err.Error()
@@ -668,7 +612,7 @@ func (c *Client) sendStatus(who string, err error, url string, nm *netmap.Networ
c.mu.Unlock()
}
func (c *Client) Login(t *tailcfg.Oauth2Token, flags LoginFlags) {
func (c *Auto) Login(t *tailcfg.Oauth2Token, flags LoginFlags) {
c.logf("client.Login(%v, %v)", t != nil, flags)
c.mu.Lock()
@@ -682,18 +626,42 @@ func (c *Client) Login(t *tailcfg.Oauth2Token, flags LoginFlags) {
c.cancelAuth()
}
func (c *Client) Logout() {
c.logf("client.Logout()")
func (c *Auto) StartLogout() {
c.logf("client.StartLogout()")
c.mu.Lock()
c.loginGoal = &LoginGoal{
wantLoggedIn: false,
}
c.mu.Unlock()
c.cancelAuth()
}
func (c *Auto) Logout(ctx context.Context) error {
c.logf("client.Logout()")
errc := make(chan error, 1)
c.mu.Lock()
c.loginGoal = &LoginGoal{
wantLoggedIn: false,
loggedOutResult: errc,
}
c.mu.Unlock()
c.cancelAuth()
timer := time.NewTimer(10 * time.Second)
defer timer.Stop()
select {
case err := <-errc:
return err
case <-ctx.Done():
return ctx.Err()
case <-timer.C:
return context.DeadlineExceeded
}
}
// UpdateEndpoints sets the client's discovered endpoints and sends
// them to the control server if they've changed.
//
@@ -701,14 +669,14 @@ func (c *Client) Logout() {
//
// The localPort field is unused except for integration tests in
// another repo.
func (c *Client) UpdateEndpoints(localPort uint16, endpoints []string) {
func (c *Auto) UpdateEndpoints(localPort uint16, endpoints []tailcfg.Endpoint) {
changed := c.direct.SetEndpoints(localPort, endpoints)
if changed {
c.sendNewMapRequest()
}
}
func (c *Client) Shutdown() {
func (c *Auto) Shutdown() {
c.logf("client.Shutdown()")
c.mu.Lock()
@@ -734,17 +702,23 @@ func (c *Client) Shutdown() {
// NodePublicKey returns the node public key currently in use. This is
// used exclusively in tests.
func (c *Client) TestOnlyNodePublicKey() wgkey.Key {
func (c *Auto) TestOnlyNodePublicKey() wgkey.Key {
priv := c.direct.GetPersist()
return priv.PrivateNodeKey.Public()
}
func (c *Client) TestOnlySetAuthKey(authkey string) {
func (c *Auto) TestOnlySetAuthKey(authkey string) {
c.direct.mu.Lock()
defer c.direct.mu.Unlock()
c.direct.authKey = authkey
}
func (c *Client) TestOnlyTimeNow() time.Time {
func (c *Auto) TestOnlyTimeNow() time.Time {
return c.timeNow()
}
// SetDNS sends the SetDNSRequest request to the control plane server,
// requesting a DNS record be created or updated.
func (c *Auto) SetDNS(ctx context.Context, req *tailcfg.SetDNSRequest) error {
return c.direct.SetDNS(ctx, req)
}

View File

@@ -0,0 +1,80 @@
// 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 controlclient implements the client for the Tailscale
// control plane.
//
// It handles authentication, port picking, and collects the local
// network configuration.
package controlclient
import (
"context"
"tailscale.com/tailcfg"
)
type LoginFlags int
const (
LoginDefault = LoginFlags(0)
LoginInteractive = LoginFlags(1 << iota) // force user login and key refresh
)
// Client represents a client connection to the control server.
// Currently this is done through a pair of polling https requests in
// the Auto client, but that might change eventually.
type Client interface {
// SetStatusFunc provides a callback to call when control sends us
// a message.
SetStatusFunc(func(Status))
// Shutdown closes this session, which should not be used any further
// afterwards.
Shutdown()
// Login begins an interactive or non-interactive login process.
// Client will eventually call the Status callback with either a
// LoginFinished flag (on success) or an auth URL (if further
// interaction is needed).
Login(*tailcfg.Oauth2Token, LoginFlags)
// StartLogout starts an asynchronous logout process.
// When it finishes, the Status callback will be called while
// AuthCantContinue()==true.
StartLogout()
// Logout starts a synchronous logout process. It doesn't return
// until the logout operation has been completed.
Logout(context.Context) error
// SetPaused pauses or unpauses the controlclient activity as much
// as possible, without losing its internal state, to minimize
// unnecessary network activity.
// TODO: It might be better to simply shutdown the controlclient and
// make a new one when it's time to unpause.
SetPaused(bool)
// AuthCantContinue returns whether authentication is blocked. If it
// is, you either need to visit the auth URL (previously sent in a
// Status callback) or call the Login function appropriately.
// TODO: this probably belongs in the Status itself instead.
AuthCantContinue() bool
// SetHostinfo changes the Hostinfo structure that will be sent in
// subsequent node registration requests.
// TODO: a server-side change would let us simply upload this
// in a separate http request. It has nothing to do with the rest of
// the state machine.
SetHostinfo(*tailcfg.Hostinfo)
// SetNetinfo changes the NetIinfo structure that will be sent in
// subsequent node registration requests.
// TODO: a server-side change would let us simply upload this
// in a separate http request. It has nothing to do with the rest of
// the state machine.
SetNetInfo(*tailcfg.NetInfo)
// UpdateEndpoints changes the Endpoint structure that will be sent
// in subsequent node registration requests.
// TODO: localPort seems to be obsolete, remove it.
// TODO: a server-side change would let us simply upload this
// in a separate http request. It has nothing to do with the rest of
// the state machine.
UpdateEndpoints(localPort uint16, endpoints []tailcfg.Endpoint)
// SetDNS sends the SetDNSRequest request to the control plane server,
// requesting a DNS record be created or updated.
SetDNS(context.Context, *tailcfg.SetDNSRequest) error
}

View File

@@ -22,7 +22,7 @@ func fieldsOf(t reflect.Type) (fields []string) {
func TestStatusEqual(t *testing.T) {
// Verify that the Equal method stays in sync with reality
equalHandles := []string{"LoginFinished", "Err", "URL", "Persist", "NetMap", "Hostinfo", "State"}
equalHandles := []string{"LoginFinished", "LogoutFinished", "Err", "URL", "NetMap", "State", "Persist", "Hostinfo"}
if have := fieldsOf(reflect.TypeOf(Status{})); !reflect.DeepEqual(have, equalHandles) {
t.Errorf("Status.Equal check might be out of sync\nfields: %q\nhandled: %q\n",
have, equalHandles)

View File

@@ -11,7 +11,6 @@ import (
"fmt"
"log"
"net/http"
"regexp"
"runtime"
"strconv"
"time"
@@ -42,28 +41,82 @@ func dumpGoroutinesToURL(c *http.Client, targetURL string) {
}
}
var reHexArgs = regexp.MustCompile(`\b0x[0-9a-f]+\b`)
// scrubbedGoroutineDump returns the list of all current goroutines, but with the actual
// values of arguments scrubbed out, lest it contain some private key material.
func scrubbedGoroutineDump() []byte {
buf := make([]byte, 1<<20)
buf = buf[:runtime.Stack(buf, true)]
var buf []byte
// Grab stacks multiple times into increasingly larger buffer sizes
// to minimize the risk that we blow past our iOS memory limit.
for size := 1 << 10; size <= 1<<20; size += 1 << 10 {
buf = make([]byte, size)
buf = buf[:runtime.Stack(buf, true)]
if len(buf) < size {
// It fit.
break
}
}
return scrubHex(buf)
}
func scrubHex(buf []byte) []byte {
saw := map[string][]byte{} // "0x123" => "v1%3" (unique value 1 and its value mod 8)
return reHexArgs.ReplaceAllFunc(buf, func(in []byte) []byte {
foreachHexAddress(buf, func(in []byte) {
if string(in) == "0x0" {
return in
return
}
if v, ok := saw[string(in)]; ok {
return v
for i := range in {
in[i] = '_'
}
copy(in, v)
return
}
inStr := string(in)
u64, err := strconv.ParseUint(string(in[2:]), 16, 64)
for i := range in {
in[i] = '_'
}
if err != nil {
return []byte("??")
in[0] = '?'
return
}
v := []byte(fmt.Sprintf("v%d%%%d", len(saw)+1, u64%8))
saw[string(in)] = v
return v
saw[inStr] = v
copy(in, v)
})
return buf
}
var ohx = []byte("0x")
// foreachHexAddress calls f with each subslice of b that matches
// regexp `0x[0-9a-f]*`.
func foreachHexAddress(b []byte, f func([]byte)) {
for len(b) > 0 {
i := bytes.Index(b, ohx)
if i == -1 {
return
}
b = b[i:]
hx := hexPrefix(b)
f(hx)
b = b[len(hx):]
}
}
func hexPrefix(b []byte) []byte {
for i, c := range b {
if i < 2 {
continue
}
if !isHexByte(c) {
return b[:i]
}
}
return b
}
func isHexByte(b byte) bool {
return '0' <= b && b <= '9' || 'a' <= b && b <= 'f' || 'A' <= b && b <= 'F'
}

View File

@@ -9,3 +9,22 @@ import "testing"
func TestScrubbedGoroutineDump(t *testing.T) {
t.Logf("Got:\n%s\n", scrubbedGoroutineDump())
}
func TestScrubHex(t *testing.T) {
tests := []struct {
in, want string
}{
{"foo", "foo"},
{"", ""},
{"0x", "?_"},
{"0x001 and same 0x001", "v1%1_ and same v1%1_"},
{"0x008 and same 0x008", "v1%0_ and same v1%0_"},
{"0x001 and diff 0x002", "v1%1_ and diff v2%2_"},
}
for _, tt := range tests {
got := scrubHex([]byte(tt.in))
if string(got) != tt.want {
t.Errorf("for input:\n%s\n\ngot:\n%s\n\nwant:\n%s\n", tt.in, got, tt.want)
}
}
}

View File

@@ -23,7 +23,6 @@ import (
"path/filepath"
"reflect"
"runtime"
"sort"
"strconv"
"strings"
"sync"
@@ -32,7 +31,9 @@ import (
"golang.org/x/crypto/nacl/box"
"inet.af/netaddr"
"tailscale.com/control/controlknobs"
"tailscale.com/health"
"tailscale.com/ipn/ipnstate"
"tailscale.com/log/logheap"
"tailscale.com/net/dnscache"
"tailscale.com/net/dnsfallback"
@@ -46,9 +47,9 @@ import (
"tailscale.com/types/opt"
"tailscale.com/types/persist"
"tailscale.com/types/wgkey"
"tailscale.com/util/dnsname"
"tailscale.com/util/systemd"
"tailscale.com/version"
"tailscale.com/wgengine/filter"
"tailscale.com/wgengine/monitor"
)
@@ -67,6 +68,7 @@ type Direct struct {
debugFlags []string
keepSharerAndUserSplit bool
skipIPForwardingCheck bool
pinger Pinger
mu sync.Mutex // mutex guards the following fields
serverKey wgkey.Key
@@ -76,9 +78,10 @@ type Direct struct {
expiry *time.Time
// hostinfo is mutated in-place while mu is held.
hostinfo *tailcfg.Hostinfo // always non-nil
endpoints []string
endpoints []tailcfg.Endpoint
everEndpoints bool // whether we've ever had non-empty endpoints
localPort uint16 // or zero to mean auto
lastPingURL string // last PingRequest.URL received, for dup suppresion
}
type Options struct {
@@ -104,6 +107,18 @@ type Options struct {
// forwarding works and should not be double-checked by the
// controlclient package.
SkipIPForwardingCheck bool
// Pinger optionally specifies the Pinger to use to satisfy
// MapResponse.PingRequest queries from the control plane.
// If nil, PingRequest queries are not answered.
Pinger Pinger
}
// Pinger is a subset of the wgengine.Engine interface, containing just the Ping method.
type Pinger interface {
// Ping is a request to start a discovery or TSMP ping with the peer handling
// the given IP and then call cb with its ping latency & method.
Ping(ip netaddr.IP, useTSMP bool, cb func(*ipnstate.PingResult))
}
type Decompressor interface {
@@ -144,7 +159,7 @@ func NewDirect(opts Options) (*Direct, error) {
tr := http.DefaultTransport.(*http.Transport).Clone()
tr.Proxy = tshttpproxy.ProxyFromEnvironment
tshttpproxy.SetTransportGetProxyConnectHeader(tr)
tr.TLSClientConfig = tlsdial.Config(serverURL.Host, tr.TLSClientConfig)
tr.TLSClientConfig = tlsdial.Config(serverURL.Hostname(), tr.TLSClientConfig)
tr.DialContext = dnscache.Dialer(dialer.DialContext, dnsCache)
tr.DialTLSContext = dnscache.TLSDialer(dialer.DialContext, dnsCache, tr.TLSClientConfig)
tr.ForceAttemptHTTP2 = true
@@ -166,6 +181,7 @@ func NewDirect(opts Options) (*Direct, error) {
keepSharerAndUserSplit: opts.KeepSharerAndUserSplit,
linkMon: opts.LinkMonitor,
skipIPForwardingCheck: opts.SkipIPForwardingCheck,
pinger: opts.Pinger,
}
if opts.Hostinfo == nil {
c.SetHostinfo(NewHostinfo())
@@ -179,6 +195,7 @@ var osVersion func() string // non-nil on some platforms
func NewHostinfo() *tailcfg.Hostinfo {
hostname, _ := os.Hostname()
hostname = dnsname.FirstLabel(hostname)
var osv string
if osVersion != nil {
osv = osVersion()
@@ -203,6 +220,13 @@ func packageType() string {
// Using tailscaled or IPNExtension?
exe, _ := os.Executable()
return filepath.Base(exe)
case "linux":
// Report whether this is in a snap.
// See https://snapcraft.io/docs/environment-variables
// We just look at two somewhat arbitrarily.
if os.Getenv("SNAP_NAME") != "" && os.Getenv("SNAP") != "" {
return "snap"
}
}
return ""
}
@@ -251,30 +275,22 @@ func (c *Direct) GetPersist() persist.Persist {
return c.persist
}
type LoginFlags int
const (
LoginDefault = LoginFlags(0)
LoginInteractive = LoginFlags(1 << iota) // force user login and key refresh
)
func (c *Direct) TryLogout(ctx context.Context) error {
c.logf("direct.TryLogout()")
c.mu.Lock()
defer c.mu.Unlock()
mustRegen, newURL, err := c.doLogin(ctx, loginOpt{Logout: true})
c.logf("TryLogout control response: mustRegen=%v, newURL=%v, err=%v", mustRegen, newURL, err)
// TODO(crawshaw): Tell the server. This node key should be
// immediately invalidated.
//if !c.persist.PrivateNodeKey.IsZero() {
//}
c.mu.Lock()
c.persist = persist.Persist{}
return nil
c.mu.Unlock()
return err
}
func (c *Direct) TryLogin(ctx context.Context, t *tailcfg.Oauth2Token, flags LoginFlags) (url string, err error) {
c.logf("direct.TryLogin(token=%v, flags=%v)", t != nil, flags)
return c.doLoginOrRegen(ctx, t, flags, false, "")
return c.doLoginOrRegen(ctx, loginOpt{Token: t, Flags: flags})
}
// WaitLoginURL sits in a long poll waiting for the user to authenticate at url.
@@ -282,22 +298,30 @@ func (c *Direct) TryLogin(ctx context.Context, t *tailcfg.Oauth2Token, flags Log
// On success, newURL and err will both be nil.
func (c *Direct) WaitLoginURL(ctx context.Context, url string) (newURL string, err error) {
c.logf("direct.WaitLoginURL")
return c.doLoginOrRegen(ctx, nil, LoginDefault, false, url)
return c.doLoginOrRegen(ctx, loginOpt{URL: url})
}
func (c *Direct) doLoginOrRegen(ctx context.Context, t *tailcfg.Oauth2Token, flags LoginFlags, regen bool, url string) (newURL string, err error) {
mustregen, url, err := c.doLogin(ctx, t, flags, regen, url)
func (c *Direct) doLoginOrRegen(ctx context.Context, opt loginOpt) (newURL string, err error) {
mustRegen, url, err := c.doLogin(ctx, opt)
if err != nil {
return url, err
}
if mustregen {
_, url, err = c.doLogin(ctx, t, flags, true, url)
if mustRegen {
opt.Regen = true
_, url, err = c.doLogin(ctx, opt)
}
return url, err
}
func (c *Direct) doLogin(ctx context.Context, t *tailcfg.Oauth2Token, flags LoginFlags, regen bool, url string) (mustregen bool, newurl string, err error) {
type loginOpt struct {
Token *tailcfg.Oauth2Token
Flags LoginFlags
Regen bool
URL string
Logout bool
}
func (c *Direct) doLogin(ctx context.Context, opt loginOpt) (mustRegen bool, newURL string, err error) {
c.mu.Lock()
persist := c.persist
tryingNewKey := c.tryingNewKey
@@ -316,23 +340,29 @@ func (c *Direct) doLogin(ctx context.Context, t *tailcfg.Oauth2Token, flags Logi
return false, "", errors.New("getMachinePrivKey returned zero 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 {
c.logf("LoginInteractive -> regen=true")
regen = true
regen := opt.Regen
if opt.Logout {
c.logf("logging out...")
} else {
if expired {
c.logf("Old key expired -> regen=true")
systemd.Status("key expired; run 'tailscale up' to authenticate")
regen = true
}
if (opt.Flags & LoginInteractive) != 0 {
c.logf("LoginInteractive -> regen=true")
regen = true
}
}
c.logf("doLogin(regen=%v, hasUrl=%v)", regen, url != "")
c.logf("doLogin(regen=%v, hasUrl=%v)", regen, opt.URL != "")
if serverKey.IsZero() {
var err error
serverKey, err = loadServerKey(ctx, c.httpc, c.serverURL)
if err != nil {
return regen, url, err
return regen, opt.URL, err
}
c.logf("control server key %s from %s", serverKey.HexString(), c.serverURL)
c.mu.Lock()
c.serverKey = serverKey
@@ -340,17 +370,21 @@ func (c *Direct) doLogin(ctx context.Context, t *tailcfg.Oauth2Token, flags Logi
}
var oldNodeKey wgkey.Key
if url != "" {
} else if regen || persist.PrivateNodeKey.IsZero() {
switch {
case opt.Logout:
tryingNewKey = persist.PrivateNodeKey
case opt.URL != "":
// Nothing.
case regen || persist.PrivateNodeKey.IsZero():
c.logf("Generating a new nodekey.")
persist.OldPrivateNodeKey = persist.PrivateNodeKey
key, err := wgkey.NewPrivate()
if err != nil {
c.logf("login keygen: %v", err)
return regen, url, err
return regen, opt.URL, err
}
tryingNewKey = key
} else {
default:
// Try refreshing the current key first
tryingNewKey = persist.PrivateNodeKey
}
@@ -359,11 +393,14 @@ func (c *Direct) doLogin(ctx context.Context, t *tailcfg.Oauth2Token, flags Logi
}
if tryingNewKey.IsZero() {
if opt.Logout {
return false, "", errors.New("no nodekey to log out")
}
log.Fatalf("tryingNewKey is empty, give up")
}
if backendLogID == "" {
err = errors.New("hostinfo: BackendLogID missing")
return regen, url, err
return regen, opt.URL, err
}
now := time.Now().Round(time.Second)
request := tailcfg.RegisterRequest{
@@ -371,13 +408,16 @@ func (c *Direct) doLogin(ctx context.Context, t *tailcfg.Oauth2Token, flags Logi
OldNodeKey: tailcfg.NodeKey(oldNodeKey),
NodeKey: tailcfg.NodeKey(tryingNewKey.Public()),
Hostinfo: hostinfo,
Followup: url,
Followup: opt.URL,
Timestamp: &now,
}
if opt.Logout {
request.Expiry = time.Unix(123, 0) // far in the past
}
c.logf("RegisterReq: onode=%v node=%v fup=%v",
request.OldNodeKey.ShortString(),
request.NodeKey.ShortString(), url != "")
request.Auth.Oauth2Token = t
request.NodeKey.ShortString(), opt.URL != "")
request.Auth.Oauth2Token = opt.Token
request.Auth.Provider = persist.Provider
request.Auth.LoginName = persist.LoginName
request.Auth.AuthKey = authKey
@@ -391,38 +431,48 @@ func (c *Direct) doLogin(ctx context.Context, t *tailcfg.Oauth2Token, flags Logi
// Don't log the common error types. Signatures are not usually enabled,
// so these are expected.
if err != errCertificateNotConfigured && err != errNoCertStore {
if !errors.Is(err, errCertificateNotConfigured) && !errors.Is(err, errNoCertStore) {
c.logf("RegisterReq sign error: %v", err)
}
}
if debugRegister {
j, _ := json.MarshalIndent(request, "", "\t")
c.logf("RegisterRequest: %s", j)
}
bodyData, err := encode(request, &serverKey, &machinePrivKey)
if err != nil {
return regen, url, err
return regen, opt.URL, err
}
body := bytes.NewReader(bodyData)
u := fmt.Sprintf("%s/machine/%s", c.serverURL, machinePrivKey.Public().HexString())
req, err := http.NewRequest("POST", u, body)
if err != nil {
return regen, url, err
return regen, opt.URL, err
}
req = req.WithContext(ctx)
res, err := c.httpc.Do(req)
if err != nil {
return regen, url, fmt.Errorf("register request: %v", err)
return regen, opt.URL, fmt.Errorf("register request: %v", err)
}
if res.StatusCode != 200 {
msg, _ := ioutil.ReadAll(res.Body)
res.Body.Close()
return regen, url, fmt.Errorf("register request: http %d: %.200s",
return regen, opt.URL, fmt.Errorf("register request: http %d: %.200s",
res.StatusCode, strings.TrimSpace(string(msg)))
}
resp := tailcfg.RegisterResponse{}
if err := decode(res, &resp, &serverKey, &machinePrivKey); err != nil {
c.logf("error decoding RegisterResponse with server key %s and machine key %s: %v", serverKey, machinePrivKey.Public(), err)
return regen, url, fmt.Errorf("register request: %v", err)
return regen, opt.URL, fmt.Errorf("register request: %v", err)
}
if debugRegister {
j, _ := json.MarshalIndent(resp, "", "\t")
c.logf("RegisterResponse: %s", j)
}
// Log without PII:
c.logf("RegisterReq: got response; nodeKeyExpired=%v, machineAuthorized=%v; authURL=%v",
resp.NodeKeyExpired, resp.MachineAuthorized, resp.AuthURL != "")
@@ -435,10 +485,10 @@ func (c *Direct) doLogin(ctx context.Context, t *tailcfg.Oauth2Token, flags Logi
request.NodeKey.ShortString())
return true, "", nil
}
if persist.Provider == "" {
if resp.Login.Provider != "" {
persist.Provider = resp.Login.Provider
}
if persist.LoginName == "" {
if resp.Login.LoginName != "" {
persist.LoginName = resp.Login.LoginName
}
@@ -474,7 +524,7 @@ func (c *Direct) doLogin(ctx context.Context, t *tailcfg.Oauth2Token, flags Logi
return false, resp.AuthURL, nil
}
func sameStrings(a, b []string) bool {
func sameEndpoints(a, b []tailcfg.Endpoint) bool {
if len(a) != len(b) {
return false
}
@@ -490,15 +540,19 @@ func sameStrings(a, b []string) bool {
// whether they've changed.
//
// It does not retain the provided slice.
func (c *Direct) newEndpoints(localPort uint16, endpoints []string) (changed bool) {
func (c *Direct) newEndpoints(localPort uint16, endpoints []tailcfg.Endpoint) (changed bool) {
c.mu.Lock()
defer c.mu.Unlock()
// Nothing new?
if c.localPort == localPort && sameStrings(c.endpoints, endpoints) {
if c.localPort == localPort && sameEndpoints(c.endpoints, endpoints) {
return false // unchanged
}
c.logf("client.newEndpoints(%v, %v)", localPort, endpoints)
var epStrs []string
for _, ep := range endpoints {
epStrs = append(epStrs, ep.Addr.String())
}
c.logf("client.newEndpoints(%v, %v)", localPort, epStrs)
c.localPort = localPort
c.endpoints = append(c.endpoints[:0], endpoints...)
if len(endpoints) > 0 {
@@ -510,7 +564,7 @@ func (c *Direct) newEndpoints(localPort uint16, endpoints []string) (changed boo
// SetEndpoints updates the list of locally advertised endpoints.
// It won't be replicated to the server until a *fresh* call to PollNetMap().
// You don't need to restart PollNetMap if we return changed==false.
func (c *Direct) SetEndpoints(localPort uint16, endpoints []string) (changed bool) {
func (c *Direct) SetEndpoints(localPort uint16, endpoints []tailcfg.Endpoint) (changed bool) {
// (no log message on function entry, because it clutters the logs
// if endpoints haven't changed. newEndpoints() will log it.)
return c.newEndpoints(localPort, endpoints)
@@ -534,6 +588,11 @@ func (c *Direct) SendLiteMapUpdate(ctx context.Context) error {
return c.sendMapRequest(ctx, 1, nil)
}
// If we go more than pollTimeout without hearing from the server,
// end the long poll. We should be receiving a keep alive ping
// every minute.
const pollTimeout = 120 * time.Second
// cb nil means to omit peers.
func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, cb func(*netmap.NetworkMap)) error {
c.mu.Lock()
@@ -543,7 +602,12 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, cb func(*netm
hostinfo := c.hostinfo.Clone()
backendLogID := hostinfo.BackendLogID
localPort := c.localPort
ep := append([]string(nil), c.endpoints...)
var epStrs []string
var epTypes []tailcfg.EndpointType
for _, ep := range c.endpoints {
epStrs = append(epStrs, ep.Addr.String())
epTypes = append(epTypes, ep.Type)
}
everEndpoints := c.everEndpoints
c.mu.Unlock()
@@ -563,7 +627,7 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, cb func(*netm
}
allowStream := maxPolls != 1
c.logf("[v1] PollNetMap: stream=%v :%v ep=%v", allowStream, localPort, ep)
c.logf("[v1] PollNetMap: stream=%v :%v ep=%v", allowStream, localPort, epStrs)
vlogf := logger.Discard
if Debug.NetMap {
@@ -573,15 +637,16 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, cb func(*netm
}
request := &tailcfg.MapRequest{
Version: tailcfg.CurrentMapRequestVersion,
KeepAlive: c.keepAlive,
NodeKey: tailcfg.NodeKey(persist.PrivateNodeKey.Public()),
DiscoKey: c.discoPubKey,
Endpoints: ep,
Stream: allowStream,
Hostinfo: hostinfo,
DebugFlags: c.debugFlags,
OmitPeers: cb == nil,
Version: tailcfg.CurrentMapRequestVersion,
KeepAlive: c.keepAlive,
NodeKey: tailcfg.NodeKey(persist.PrivateNodeKey.Public()),
DiscoKey: c.discoPubKey,
Endpoints: epStrs,
EndpointTypes: epTypes,
Stream: allowStream,
Hostinfo: hostinfo,
DebugFlags: c.debugFlags,
OmitPeers: cb == nil,
}
var extraDebugFlags []string
if hostinfo != nil && c.linkMon != nil && !c.skipIPForwardingCheck &&
@@ -609,7 +674,7 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, cb func(*netm
// 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() {
if len(epStrs) == 0 && !everEndpoints && !inTest() {
request.ReadOnly = true
}
@@ -652,10 +717,6 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, cb func(*netm
return nil
}
// If we go more than pollTimeout without hearing from the server,
// end the long poll. We should be receiving a keep alive ping
// every minute.
const pollTimeout = 120 * time.Second
timeout := time.NewTimer(pollTimeout)
timeoutReset := make(chan struct{})
pollDone := make(chan struct{})
@@ -685,10 +746,11 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, cb func(*netm
}
}()
var lastDERPMap *tailcfg.DERPMap
var lastUserProfile = map[tailcfg.UserID]tailcfg.UserProfile{}
var lastParsedPacketFilter []filter.Match
var collectServices bool
sess := newMapSession(persist.PrivateNodeKey)
sess.logf = c.logf
sess.vlogf = vlogf
sess.machinePubKey = machinePubKey
sess.keepSharerAndUserSplit = c.keepSharerAndUserSplit
// If allowStream, then the server will use an HTTP long poll to
// return incremental results. There is always one response right
@@ -697,7 +759,6 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, cb func(*netm
// the same format before just closing the connection.
// We can use this same read loop either way.
var msg []byte
var previousPeers []*tailcfg.Node // for delta-purposes
for i := 0; i < maxPolls || maxPolls < 0; i++ {
vlogf("netmap: starting size read after %v (poll %v)", time.Since(t0).Round(time.Millisecond), i)
var siz [4]byte
@@ -724,7 +785,7 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, cb func(*netm
health.GotStreamedMapResponse()
}
if pr := resp.PingRequest; pr != nil {
if pr := resp.PingRequest; pr != nil && c.isUniquePingRequest(pr) {
go answerPing(c.logf, c.httpc, pr)
}
@@ -744,17 +805,10 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, cb func(*netm
continue
}
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")
lastDERPMap = resp.DERPMap
}
if resp.Debug != nil {
hasDebug := resp.Debug != nil
// being conservative here, if Debug not present set to False
controlknobs.SetDisableUPnP(hasDebug && resp.Debug.DisableUPnP.EqualBool(true))
if hasDebug {
if resp.Debug.LogHeapPprof {
go logheap.LogHeap(resp.Debug.LogHeapURL)
}
@@ -763,18 +817,40 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, cb func(*netm
}
setControlAtomic(&controlUseDERPRoute, resp.Debug.DERPRoute)
setControlAtomic(&controlTrimWGConfig, resp.Debug.TrimWGConfig)
if sleep := time.Duration(resp.Debug.SleepSeconds * float64(time.Second)); sleep > 0 {
if err := sleepAsRequested(ctx, c.logf, timeoutReset, sleep); err != nil {
return err
}
}
}
nm := sess.netmapForResponse(&resp)
if nm.SelfNode == nil {
c.logf("MapResponse lacked node")
return errors.New("MapResponse lacked node")
}
// Temporarily (2020-06-29) support removing all but
// discovery-supporting nodes during development, for
// less noise.
if Debug.OnlyDisco {
filtered := resp.Peers[:0]
for _, p := range resp.Peers {
if !p.DiscoKey.IsZero() {
filtered = append(filtered, p)
anyOld, numDisco := false, 0
for _, p := range nm.Peers {
if p.DiscoKey.IsZero() {
anyOld = true
} else {
numDisco++
}
}
resp.Peers = filtered
if anyOld {
filtered := make([]*tailcfg.Node, 0, numDisco)
for _, p := range nm.Peers {
if !p.DiscoKey.IsZero() {
filtered = append(filtered, p)
}
}
nm.Peers = filtered
}
}
if Debug.StripEndpoints {
for _, p := range resp.Peers {
@@ -784,13 +860,8 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, cb func(*netm
p.Endpoints = []string{"127.9.9.9:456"}
}
}
if pf := resp.PacketFilter; pf != nil {
lastParsedPacketFilter = c.parsePacketFilter(pf)
}
if v, ok := resp.CollectServices.Get(); ok {
collectServices = v
if Debug.StripCaps {
nm.SelfNode.Capabilities = nil
}
// Get latest localPort. This might've changed if
@@ -798,67 +869,9 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, cb func(*netm
// the end-to-end test.
// TODO(bradfitz): remove the NetworkMap.LocalPort field entirely.
c.mu.Lock()
localPort = c.localPort
nm.LocalPort = c.localPort
c.mu.Unlock()
nm := &netmap.NetworkMap{
SelfNode: resp.Node,
NodeKey: tailcfg.NodeKey(persist.PrivateNodeKey.Public()),
PrivateKey: persist.PrivateNodeKey,
MachineKey: machinePubKey,
Expiry: resp.Node.KeyExpiry,
Name: resp.Node.Name,
Addresses: resp.Node.Addresses,
Peers: resp.Peers,
LocalPort: localPort,
User: resp.Node.User,
UserProfiles: make(map[tailcfg.UserID]tailcfg.UserProfile),
Domain: resp.Domain,
DNS: resp.DNSConfig,
Hostinfo: resp.Node.Hostinfo,
PacketFilter: lastParsedPacketFilter,
CollectServices: collectServices,
DERPMap: lastDERPMap,
Debug: resp.Debug,
}
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)
magicDNSSuffix := nm.MagicDNSSuffix()
nm.SelfNode.InitDisplayNames(magicDNSSuffix)
for _, peer := range resp.Peers {
peer.InitDisplayNames(magicDNSSuffix)
if !peer.Sharer.IsZero() {
if c.keepSharerAndUserSplit {
addUserProfile(peer.Sharer)
} else {
peer.User = peer.Sharer
}
}
addUserProfile(peer.User)
}
if resp.Node.MachineAuthorized {
nm.MachineStatus = tailcfg.MachineAuthorized
} else {
nm.MachineStatus = tailcfg.MachineUnauthorized
}
if len(resp.DNS) > 0 {
nm.DNS.Nameservers = resp.DNS
}
if len(resp.SearchPaths) > 0 {
nm.DNS.Domains = resp.SearchPaths
}
if Debug.ProxyDNS {
nm.DNS.Proxied = true
}
// Printing the netmap can be extremely verbose, but is very
// handy for debugging. Let's limit how often we do it.
// Code elsewhere prints netmap diffs every time, so this
@@ -894,7 +907,10 @@ func decode(res *http.Response, v interface{}, serverKey *wgkey.Key, mkey *wgkey
return decodeMsg(msg, v, serverKey, mkey)
}
var debugMap, _ = strconv.ParseBool(os.Getenv("TS_DEBUG_MAP"))
var (
debugMap, _ = strconv.ParseBool(os.Getenv("TS_DEBUG_MAP"))
debugRegister, _ = strconv.ParseBool(os.Getenv("TS_DEBUG_REGISTER"))
)
var jsonEscapedZero = []byte(`\u0000`)
@@ -1021,6 +1037,7 @@ type debug struct {
OnlyDisco bool
Disco bool
StripEndpoints bool // strip endpoints from control (only use disco messages)
StripCaps bool // strip all local node's control-provided capabilities
}
func initDebug() debug {
@@ -1029,6 +1046,7 @@ func initDebug() debug {
NetMap: envBool("TS_DEBUG_NETMAP"),
ProxyDNS: envBool("TS_DEBUG_PROXY_DNS"),
StripEndpoints: envBool("TS_DEBUG_STRIP_ENDPOINTS"),
StripCaps: envBool("TS_DEBUG_STRIP_CAPS"),
OnlyDisco: use == "only",
Disco: use == "only" || use == "" || envBool("TS_DEBUG_USE_DISCO"),
}
@@ -1046,117 +1064,7 @@ func envBool(k string) bool {
return v
}
// undeltaPeers updates mapRes.Peers to be complete based on the provided previous peer list
// and the PeersRemoved and PeersChanged fields in mapRes.
// It then also nils out the delta fields.
func undeltaPeers(mapRes *tailcfg.MapResponse, prev []*tailcfg.Node) {
if len(mapRes.Peers) > 0 {
// Not delta encoded.
if !nodesSorted(mapRes.Peers) {
log.Printf("netmap: undeltaPeers: MapResponse.Peers not sorted; sorting")
sortNodes(mapRes.Peers)
}
return
}
var removed map[tailcfg.NodeID]bool
if pr := mapRes.PeersRemoved; len(pr) > 0 {
removed = make(map[tailcfg.NodeID]bool, len(pr))
for _, id := range pr {
removed[id] = true
}
}
changed := mapRes.PeersChanged
if len(removed) == 0 && len(changed) == 0 {
// No changes fast path.
mapRes.Peers = prev
return
}
if !nodesSorted(changed) {
log.Printf("netmap: undeltaPeers: MapResponse.PeersChanged not sorted; sorting")
sortNodes(changed)
}
if !nodesSorted(prev) {
// Internal error (unrelated to the network) if we get here.
log.Printf("netmap: undeltaPeers: [unexpected] prev not sorted; sorting")
sortNodes(prev)
}
newFull := make([]*tailcfg.Node, 0, len(prev)-len(removed))
for len(prev) > 0 && len(changed) > 0 {
pID := prev[0].ID
cID := changed[0].ID
if removed[pID] {
prev = prev[1:]
continue
}
switch {
case pID < cID:
newFull = append(newFull, prev[0])
prev = prev[1:]
case pID == cID:
newFull = append(newFull, changed[0])
prev, changed = prev[1:], changed[1:]
case cID < pID:
newFull = append(newFull, changed[0])
changed = changed[1:]
}
}
newFull = append(newFull, changed...)
for _, n := range prev {
if !removed[n.ID] {
newFull = append(newFull, n)
}
}
sortNodes(newFull)
if mapRes.PeerSeenChange != nil {
peerByID := make(map[tailcfg.NodeID]*tailcfg.Node, len(newFull))
for _, n := range newFull {
peerByID[n.ID] = n
}
now := time.Now()
for nodeID, seen := range mapRes.PeerSeenChange {
if n, ok := peerByID[nodeID]; ok {
if seen {
n.LastSeen = &now
} else {
n.LastSeen = nil
}
}
}
}
mapRes.Peers = newFull
mapRes.PeersChanged = nil
mapRes.PeersRemoved = nil
}
func nodesSorted(v []*tailcfg.Node) bool {
for i, n := range v {
if i > 0 && n.ID <= v[i-1].ID {
return false
}
}
return true
}
func sortNodes(v []*tailcfg.Node) {
sort.Slice(v, func(i, j int) bool { return v[i].ID < v[j].ID })
}
func cloneNodes(v1 []*tailcfg.Node) []*tailcfg.Node {
if v1 == nil {
return nil
}
v2 := make([]*tailcfg.Node, len(v1))
for i, n := range v1 {
v2[i] = n.Clone()
}
return v2
}
var clockNow = time.Now
// opt.Bool configs from control.
var (
@@ -1211,7 +1119,7 @@ func ipForwardingBroken(routes []netaddr.IPPrefix, state *interfaces.State) bool
localIPs := map[netaddr.IP]bool{}
for _, addrs := range state.InterfaceIPs {
for _, pfx := range addrs {
localIPs[pfx.IP] = true
localIPs[pfx.IP()] = true
}
}
@@ -1220,10 +1128,10 @@ func ipForwardingBroken(routes []netaddr.IPPrefix, state *interfaces.State) bool
// It's possible to advertise a route to one of the local
// machine's local IPs. IP forwarding isn't required for this
// to work, so we shouldn't warn for such exports.
if r.IsSingleIP() && localIPs[r.IP] {
if r.IsSingleIP() && localIPs[r.IP()] {
continue
}
if r.IP.Is4() {
if r.IP().Is4() {
v4Routes = true
} else {
v6Routes = true
@@ -1275,6 +1183,23 @@ func ipForwardingBroken(routes []netaddr.IPPrefix, state *interfaces.State) bool
return false
}
// isUniquePingRequest reports whether pr contains a new PingRequest.URL
// not already handled, noting its value when returning true.
func (c *Direct) isUniquePingRequest(pr *tailcfg.PingRequest) bool {
if pr == nil || pr.URL == "" {
// Bogus.
return false
}
c.mu.Lock()
defer c.mu.Unlock()
if pr.URL == c.lastPingURL {
return false
}
c.lastPingURL = pr.URL
return true
}
func answerPing(logf logger.Logf, c *http.Client, pr *tailcfg.PingRequest) {
if pr.URL == "" {
logf("invalid PingRequest with no URL")
@@ -1300,3 +1225,81 @@ func answerPing(logf logger.Logf, c *http.Client, pr *tailcfg.PingRequest) {
logf("answerPing complete to %v (after %v)", pr.URL, d)
}
}
func sleepAsRequested(ctx context.Context, logf logger.Logf, timeoutReset chan<- struct{}, d time.Duration) error {
const maxSleep = 5 * time.Minute
if d > maxSleep {
logf("sleeping for %v, capped from server-requested %v ...", maxSleep, d)
d = maxSleep
} else {
logf("sleeping for server-requested %v ...", d)
}
ticker := time.NewTicker(pollTimeout / 2)
defer ticker.Stop()
timer := time.NewTimer(d)
defer timer.Stop()
for {
select {
case <-ctx.Done():
return ctx.Err()
case <-timer.C:
return nil
case <-ticker.C:
select {
case timeoutReset <- struct{}{}:
case <-timer.C:
return nil
case <-ctx.Done():
return ctx.Err()
}
}
}
}
// SetDNS sends the SetDNSRequest request to the control plane server,
// requesting a DNS record be created or updated.
func (c *Direct) SetDNS(ctx context.Context, req *tailcfg.SetDNSRequest) error {
c.mu.Lock()
serverKey := c.serverKey
c.mu.Unlock()
if serverKey.IsZero() {
return errors.New("zero serverKey")
}
machinePrivKey, err := c.getMachinePrivKey()
if err != nil {
return fmt.Errorf("getMachinePrivKey: %w", err)
}
if machinePrivKey.IsZero() {
return errors.New("getMachinePrivKey returned zero key")
}
bodyData, err := encode(req, &serverKey, &machinePrivKey)
if err != nil {
return err
}
body := bytes.NewReader(bodyData)
u := fmt.Sprintf("%s/machine/%s/set-dns", c.serverURL, machinePrivKey.Public().HexString())
hreq, err := http.NewRequestWithContext(ctx, "POST", u, body)
if err != nil {
return err
}
res, err := c.httpc.Do(hreq)
if err != nil {
return err
}
defer res.Body.Close()
if res.StatusCode != 200 {
msg, _ := ioutil.ReadAll(res.Body)
return fmt.Errorf("set-dns response: %v, %.200s", res.Status, strings.TrimSpace(string(msg)))
}
var setDNSRes struct{} // no fields yet
if err := decode(res, &setDNSRes, &serverKey, &machinePrivKey); err != nil {
c.logf("error decoding SetDNSResponse with server key %s and machine key %s: %v", serverKey, machinePrivKey.Public(), err)
return fmt.Errorf("set-dns-response: %v", err)
}
return nil
}

View File

@@ -6,94 +6,13 @@ package controlclient
import (
"encoding/json"
"fmt"
"reflect"
"strings"
"testing"
"inet.af/netaddr"
"tailscale.com/tailcfg"
"tailscale.com/types/wgkey"
)
func TestUndeltaPeers(t *testing.T) {
n := func(id tailcfg.NodeID, name string) *tailcfg.Node {
return &tailcfg.Node{ID: id, Name: name}
}
peers := func(nv ...*tailcfg.Node) []*tailcfg.Node { return nv }
tests := []struct {
name string
mapRes *tailcfg.MapResponse
prev []*tailcfg.Node
want []*tailcfg.Node
}{
{
name: "full_peers",
mapRes: &tailcfg.MapResponse{
Peers: peers(n(1, "foo"), n(2, "bar")),
},
want: peers(n(1, "foo"), n(2, "bar")),
},
{
name: "full_peers_ignores_deltas",
mapRes: &tailcfg.MapResponse{
Peers: peers(n(1, "foo"), n(2, "bar")),
PeersRemoved: []tailcfg.NodeID{2},
},
want: peers(n(1, "foo"), n(2, "bar")),
},
{
name: "add_and_update",
prev: peers(n(1, "foo"), n(2, "bar")),
mapRes: &tailcfg.MapResponse{
PeersChanged: peers(n(0, "zero"), n(2, "bar2"), n(3, "three")),
},
want: peers(n(0, "zero"), n(1, "foo"), n(2, "bar2"), n(3, "three")),
},
{
name: "remove",
prev: peers(n(1, "foo"), n(2, "bar")),
mapRes: &tailcfg.MapResponse{
PeersRemoved: []tailcfg.NodeID{1},
},
want: peers(n(2, "bar")),
},
{
name: "add_and_remove",
prev: peers(n(1, "foo"), n(2, "bar")),
mapRes: &tailcfg.MapResponse{
PeersChanged: peers(n(1, "foo2")),
PeersRemoved: []tailcfg.NodeID{2},
},
want: peers(n(1, "foo2")),
},
{
name: "unchanged",
prev: peers(n(1, "foo"), n(2, "bar")),
mapRes: &tailcfg.MapResponse{},
want: peers(n(1, "foo"), n(2, "bar")),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
undeltaPeers(tt.mapRes, tt.prev)
if !reflect.DeepEqual(tt.mapRes.Peers, tt.want) {
t.Errorf("wrong results\n got: %s\nwant: %s", formatNodes(tt.mapRes.Peers), formatNodes(tt.want))
}
})
}
}
func formatNodes(nodes []*tailcfg.Node) string {
var sb strings.Builder
for i, n := range nodes {
if i > 0 {
sb.WriteString(", ")
}
fmt.Fprintf(&sb, "(%d, %q)", n.ID, n.Name)
}
return sb.String()
}
func TestNewDirect(t *testing.T) {
hi := NewHostinfo()
ni := tailcfg.NetInfo{LinkType: "wired"}
@@ -144,7 +63,7 @@ func TestNewDirect(t *testing.T) {
t.Errorf("c.SetHostinfo(hi) want true got %v", changed)
}
endpoints := []string{"1", "2", "3"}
endpoints := fakeEndpoints(1, 2, 3)
changed = c.newEndpoints(12, endpoints)
if !changed {
t.Errorf("c.newEndpoints(12) want true got %v", changed)
@@ -157,13 +76,22 @@ func TestNewDirect(t *testing.T) {
if !changed {
t.Errorf("c.newEndpoints(13) want true got %v", changed)
}
endpoints = []string{"4", "5", "6"}
endpoints = fakeEndpoints(4, 5, 6)
changed = c.newEndpoints(13, endpoints)
if !changed {
t.Errorf("c.newEndpoints(13) want true got %v", changed)
}
}
func fakeEndpoints(ports ...uint16) (ret []tailcfg.Endpoint) {
for _, port := range ports {
ret = append(ret, tailcfg.Endpoint{
Addr: netaddr.IPPortFrom(netaddr.IP{}, port),
})
}
return
}
func TestNewHostinfo(t *testing.T) {
hi := NewHostinfo()
if hi == nil {

View File

@@ -1,20 +0,0 @@
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package controlclient
import (
"tailscale.com/tailcfg"
"tailscale.com/wgengine/filter"
)
// 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)
}
return mm
}

View File

@@ -2,6 +2,7 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build linux && !android
// +build linux,!android
package controlclient
@@ -9,12 +10,11 @@ package controlclient
import (
"bytes"
"fmt"
"io"
"io/ioutil"
"strings"
"syscall"
"go4.org/mem"
"tailscale.com/hostinfo"
"tailscale.com/util/lineread"
"tailscale.com/version/distro"
)
@@ -55,9 +55,12 @@ func osVersionLinux() string {
}
attrBuf.WriteByte(byte(b))
}
if inContainer() {
if hostinfo.InContainer() {
attrBuf.WriteString("; container")
}
if env := hostinfo.GetEnvType(); env != "" {
fmt.Fprintf(&attrBuf, "; env=%s", env)
}
attr := attrBuf.String()
id := m["ID"]
@@ -89,15 +92,3 @@ func osVersionLinux() string {
}
return fmt.Sprintf("Other%s", attr)
}
func inContainer() (ret bool) {
lineread.File("/proc/1/cgroup", func(line []byte) error {
if mem.Contains(mem.B(line), mem.S("/docker/")) ||
mem.Contains(mem.B(line), mem.S("/lxc/")) {
ret = true
return io.EOF // arbitrary non-nil error to stop loop
}
return nil
})
return
}

View File

@@ -0,0 +1,301 @@
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package controlclient
import (
"log"
"os"
"sort"
"strconv"
"inet.af/netaddr"
"tailscale.com/tailcfg"
"tailscale.com/types/logger"
"tailscale.com/types/netmap"
"tailscale.com/types/wgkey"
"tailscale.com/wgengine/filter"
)
// mapSession holds the state over a long-polled "map" request to the
// control plane.
//
// It accepts incremental tailcfg.MapResponse values to
// netMapForResponse and returns fully inflated NetworkMaps, filling
// in the omitted data implicit from prior MapResponse values from
// within the same session (the same long-poll HTTP response to the
// one MapRequest).
type mapSession struct {
// Immutable fields.
privateNodeKey wgkey.Private
logf logger.Logf
vlogf logger.Logf
machinePubKey tailcfg.MachineKey
keepSharerAndUserSplit bool // see Options.KeepSharerAndUserSplit
// Fields storing state over the the coards of multiple MapResponses.
lastNode *tailcfg.Node
lastDNSConfig *tailcfg.DNSConfig
lastDERPMap *tailcfg.DERPMap
lastUserProfile map[tailcfg.UserID]tailcfg.UserProfile
lastParsedPacketFilter []filter.Match
collectServices bool
previousPeers []*tailcfg.Node // for delta-purposes
lastDomain string
// netMapBuilding is non-nil during a netmapForResponse call,
// containing the value to be returned, once fully populated.
netMapBuilding *netmap.NetworkMap
}
func newMapSession(privateNodeKey wgkey.Private) *mapSession {
ms := &mapSession{
privateNodeKey: privateNodeKey,
logf: logger.Discard,
vlogf: logger.Discard,
lastDNSConfig: new(tailcfg.DNSConfig),
lastUserProfile: map[tailcfg.UserID]tailcfg.UserProfile{},
}
return ms
}
func (ms *mapSession) addUserProfile(userID tailcfg.UserID) {
nm := ms.netMapBuilding
if _, dup := nm.UserProfiles[userID]; dup {
// Already populated it from a previous peer.
return
}
if up, ok := ms.lastUserProfile[userID]; ok {
nm.UserProfiles[userID] = up
}
}
// netmapForResponse returns a fully populated NetworkMap from a full
// or incremental MapResponse within the session, filling in omitted
// information from prior MapResponse values.
func (ms *mapSession) netmapForResponse(resp *tailcfg.MapResponse) *netmap.NetworkMap {
undeltaPeers(resp, ms.previousPeers)
ms.previousPeers = cloneNodes(resp.Peers) // defensive/lazy clone, since this escapes to who knows where
for _, up := range resp.UserProfiles {
ms.lastUserProfile[up.ID] = up
}
if resp.DERPMap != nil {
ms.vlogf("netmap: new map contains DERP map")
ms.lastDERPMap = resp.DERPMap
}
if pf := resp.PacketFilter; pf != nil {
var err error
ms.lastParsedPacketFilter, err = filter.MatchesFromFilterRules(pf)
if err != nil {
ms.logf("parsePacketFilter: %v", err)
}
}
if c := resp.DNSConfig; c != nil {
ms.lastDNSConfig = c
}
if v, ok := resp.CollectServices.Get(); ok {
ms.collectServices = v
}
if resp.Domain != "" {
ms.lastDomain = resp.Domain
}
nm := &netmap.NetworkMap{
NodeKey: tailcfg.NodeKey(ms.privateNodeKey.Public()),
PrivateKey: ms.privateNodeKey,
MachineKey: ms.machinePubKey,
Peers: resp.Peers,
UserProfiles: make(map[tailcfg.UserID]tailcfg.UserProfile),
Domain: ms.lastDomain,
DNS: *ms.lastDNSConfig,
PacketFilter: ms.lastParsedPacketFilter,
CollectServices: ms.collectServices,
DERPMap: ms.lastDERPMap,
Debug: resp.Debug,
}
ms.netMapBuilding = nm
if resp.Node != nil {
ms.lastNode = resp.Node
}
if node := ms.lastNode.Clone(); node != nil {
nm.SelfNode = node
nm.Expiry = node.KeyExpiry
nm.Name = node.Name
nm.Addresses = filterSelfAddresses(node.Addresses)
nm.User = node.User
nm.Hostinfo = node.Hostinfo
if node.MachineAuthorized {
nm.MachineStatus = tailcfg.MachineAuthorized
} else {
nm.MachineStatus = tailcfg.MachineUnauthorized
}
}
ms.addUserProfile(nm.User)
magicDNSSuffix := nm.MagicDNSSuffix()
if nm.SelfNode != nil {
nm.SelfNode.InitDisplayNames(magicDNSSuffix)
}
for _, peer := range resp.Peers {
peer.InitDisplayNames(magicDNSSuffix)
if !peer.Sharer.IsZero() {
if ms.keepSharerAndUserSplit {
ms.addUserProfile(peer.Sharer)
} else {
peer.User = peer.Sharer
}
}
ms.addUserProfile(peer.User)
}
if len(resp.DNS) > 0 {
nm.DNS.Nameservers = resp.DNS
}
if len(resp.SearchPaths) > 0 {
nm.DNS.Domains = resp.SearchPaths
}
if Debug.ProxyDNS {
nm.DNS.Proxied = true
}
ms.netMapBuilding = nil
return nm
}
// undeltaPeers updates mapRes.Peers to be complete based on the
// provided previous peer list and the PeersRemoved and PeersChanged
// fields in mapRes, as well as the PeerSeenChange and OnlineChange
// maps.
//
// It then also nils out the delta fields.
func undeltaPeers(mapRes *tailcfg.MapResponse, prev []*tailcfg.Node) {
if len(mapRes.Peers) > 0 {
// Not delta encoded.
if !nodesSorted(mapRes.Peers) {
log.Printf("netmap: undeltaPeers: MapResponse.Peers not sorted; sorting")
sortNodes(mapRes.Peers)
}
return
}
var removed map[tailcfg.NodeID]bool
if pr := mapRes.PeersRemoved; len(pr) > 0 {
removed = make(map[tailcfg.NodeID]bool, len(pr))
for _, id := range pr {
removed[id] = true
}
}
changed := mapRes.PeersChanged
if !nodesSorted(changed) {
log.Printf("netmap: undeltaPeers: MapResponse.PeersChanged not sorted; sorting")
sortNodes(changed)
}
if !nodesSorted(prev) {
// Internal error (unrelated to the network) if we get here.
log.Printf("netmap: undeltaPeers: [unexpected] prev not sorted; sorting")
sortNodes(prev)
}
newFull := prev
if len(removed) > 0 || len(changed) > 0 {
newFull = make([]*tailcfg.Node, 0, len(prev)-len(removed))
for len(prev) > 0 && len(changed) > 0 {
pID := prev[0].ID
cID := changed[0].ID
if removed[pID] {
prev = prev[1:]
continue
}
switch {
case pID < cID:
newFull = append(newFull, prev[0])
prev = prev[1:]
case pID == cID:
newFull = append(newFull, changed[0])
prev, changed = prev[1:], changed[1:]
case cID < pID:
newFull = append(newFull, changed[0])
changed = changed[1:]
}
}
newFull = append(newFull, changed...)
for _, n := range prev {
if !removed[n.ID] {
newFull = append(newFull, n)
}
}
sortNodes(newFull)
}
if len(mapRes.PeerSeenChange) != 0 || len(mapRes.OnlineChange) != 0 {
peerByID := make(map[tailcfg.NodeID]*tailcfg.Node, len(newFull))
for _, n := range newFull {
peerByID[n.ID] = n
}
now := clockNow()
for nodeID, seen := range mapRes.PeerSeenChange {
if n, ok := peerByID[nodeID]; ok {
if seen {
n.LastSeen = &now
} else {
n.LastSeen = nil
}
}
}
for nodeID, online := range mapRes.OnlineChange {
if n, ok := peerByID[nodeID]; ok {
online := online
n.Online = &online
}
}
}
mapRes.Peers = newFull
mapRes.PeersChanged = nil
mapRes.PeersRemoved = nil
}
func nodesSorted(v []*tailcfg.Node) bool {
for i, n := range v {
if i > 0 && n.ID <= v[i-1].ID {
return false
}
}
return true
}
func sortNodes(v []*tailcfg.Node) {
sort.Slice(v, func(i, j int) bool { return v[i].ID < v[j].ID })
}
func cloneNodes(v1 []*tailcfg.Node) []*tailcfg.Node {
if v1 == nil {
return nil
}
v2 := make([]*tailcfg.Node, len(v1))
for i, n := range v1 {
v2[i] = n.Clone()
}
return v2
}
var debugSelfIPv6Only, _ = strconv.ParseBool(os.Getenv("TS_DEBUG_SELF_V6_ONLY"))
func filterSelfAddresses(in []netaddr.IPPrefix) (ret []netaddr.IPPrefix) {
switch {
default:
return in
case debugSelfIPv6Only:
for _, a := range in {
if a.IP().Is6() {
ret = append(ret, a)
}
}
return ret
}
}

View File

@@ -0,0 +1,311 @@
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package controlclient
import (
"encoding/json"
"fmt"
"reflect"
"strings"
"testing"
"time"
"tailscale.com/tailcfg"
"tailscale.com/types/netmap"
"tailscale.com/types/wgkey"
)
func TestUndeltaPeers(t *testing.T) {
defer func(old func() time.Time) { clockNow = old }(clockNow)
var curTime time.Time
clockNow = func() time.Time {
return curTime
}
online := func(v bool) func(*tailcfg.Node) {
return func(n *tailcfg.Node) {
n.Online = &v
}
}
seenAt := func(t time.Time) func(*tailcfg.Node) {
return func(n *tailcfg.Node) {
n.LastSeen = &t
}
}
n := func(id tailcfg.NodeID, name string, mod ...func(*tailcfg.Node)) *tailcfg.Node {
n := &tailcfg.Node{ID: id, Name: name}
for _, f := range mod {
f(n)
}
return n
}
peers := func(nv ...*tailcfg.Node) []*tailcfg.Node { return nv }
tests := []struct {
name string
mapRes *tailcfg.MapResponse
curTime time.Time
prev []*tailcfg.Node
want []*tailcfg.Node
}{
{
name: "full_peers",
mapRes: &tailcfg.MapResponse{
Peers: peers(n(1, "foo"), n(2, "bar")),
},
want: peers(n(1, "foo"), n(2, "bar")),
},
{
name: "full_peers_ignores_deltas",
mapRes: &tailcfg.MapResponse{
Peers: peers(n(1, "foo"), n(2, "bar")),
PeersRemoved: []tailcfg.NodeID{2},
},
want: peers(n(1, "foo"), n(2, "bar")),
},
{
name: "add_and_update",
prev: peers(n(1, "foo"), n(2, "bar")),
mapRes: &tailcfg.MapResponse{
PeersChanged: peers(n(0, "zero"), n(2, "bar2"), n(3, "three")),
},
want: peers(n(0, "zero"), n(1, "foo"), n(2, "bar2"), n(3, "three")),
},
{
name: "remove",
prev: peers(n(1, "foo"), n(2, "bar")),
mapRes: &tailcfg.MapResponse{
PeersRemoved: []tailcfg.NodeID{1},
},
want: peers(n(2, "bar")),
},
{
name: "add_and_remove",
prev: peers(n(1, "foo"), n(2, "bar")),
mapRes: &tailcfg.MapResponse{
PeersChanged: peers(n(1, "foo2")),
PeersRemoved: []tailcfg.NodeID{2},
},
want: peers(n(1, "foo2")),
},
{
name: "unchanged",
prev: peers(n(1, "foo"), n(2, "bar")),
mapRes: &tailcfg.MapResponse{},
want: peers(n(1, "foo"), n(2, "bar")),
},
{
name: "online_change",
prev: peers(n(1, "foo"), n(2, "bar")),
mapRes: &tailcfg.MapResponse{
OnlineChange: map[tailcfg.NodeID]bool{
1: true,
},
},
want: peers(
n(1, "foo", online(true)),
n(2, "bar"),
),
},
{
name: "online_change_offline",
prev: peers(n(1, "foo"), n(2, "bar")),
mapRes: &tailcfg.MapResponse{
OnlineChange: map[tailcfg.NodeID]bool{
1: false,
2: true,
},
},
want: peers(
n(1, "foo", online(false)),
n(2, "bar", online(true)),
),
},
{
name: "peer_seen_at",
prev: peers(n(1, "foo", seenAt(time.Unix(111, 0))), n(2, "bar")),
curTime: time.Unix(123, 0),
mapRes: &tailcfg.MapResponse{
PeerSeenChange: map[tailcfg.NodeID]bool{
1: false,
2: true,
},
},
want: peers(
n(1, "foo"),
n(2, "bar", seenAt(time.Unix(123, 0))),
),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if !tt.curTime.IsZero() {
curTime = tt.curTime
}
undeltaPeers(tt.mapRes, tt.prev)
if !reflect.DeepEqual(tt.mapRes.Peers, tt.want) {
t.Errorf("wrong results\n got: %s\nwant: %s", formatNodes(tt.mapRes.Peers), formatNodes(tt.want))
}
})
}
}
func formatNodes(nodes []*tailcfg.Node) string {
var sb strings.Builder
for i, n := range nodes {
if i > 0 {
sb.WriteString(", ")
}
var extra string
if n.Online != nil {
extra += fmt.Sprintf(", online=%v", *n.Online)
}
if n.LastSeen != nil {
extra += fmt.Sprintf(", lastSeen=%v", n.LastSeen.Unix())
}
fmt.Fprintf(&sb, "(%d, %q%s)", n.ID, n.Name, extra)
}
return sb.String()
}
func newTestMapSession(t *testing.T) *mapSession {
k, err := wgkey.NewPrivate()
if err != nil {
t.Fatal(err)
}
return newMapSession(k)
}
func TestNetmapForResponse(t *testing.T) {
t.Run("implicit_packetfilter", func(t *testing.T) {
somePacketFilter := []tailcfg.FilterRule{
{
SrcIPs: []string{"*"},
DstPorts: []tailcfg.NetPortRange{
{IP: "10.2.3.4", Ports: tailcfg.PortRange{First: 22, Last: 22}},
},
},
}
ms := newTestMapSession(t)
nm1 := ms.netmapForResponse(&tailcfg.MapResponse{
Node: new(tailcfg.Node),
PacketFilter: somePacketFilter,
})
if len(nm1.PacketFilter) == 0 {
t.Fatalf("zero length PacketFilter")
}
nm2 := ms.netmapForResponse(&tailcfg.MapResponse{
Node: new(tailcfg.Node),
PacketFilter: nil, // testing that the server can omit this.
})
if len(nm1.PacketFilter) == 0 {
t.Fatalf("zero length PacketFilter in 2nd netmap")
}
if !reflect.DeepEqual(nm1.PacketFilter, nm2.PacketFilter) {
t.Error("packet filters differ")
}
})
t.Run("implicit_dnsconfig", func(t *testing.T) {
someDNSConfig := &tailcfg.DNSConfig{Domains: []string{"foo", "bar"}}
ms := newTestMapSession(t)
nm1 := ms.netmapForResponse(&tailcfg.MapResponse{
Node: new(tailcfg.Node),
DNSConfig: someDNSConfig,
})
if !reflect.DeepEqual(nm1.DNS, *someDNSConfig) {
t.Fatalf("1st DNS wrong")
}
nm2 := ms.netmapForResponse(&tailcfg.MapResponse{
Node: new(tailcfg.Node),
DNSConfig: nil, // implict
})
if !reflect.DeepEqual(nm2.DNS, *someDNSConfig) {
t.Fatalf("2nd DNS wrong")
}
})
t.Run("collect_services", func(t *testing.T) {
ms := newTestMapSession(t)
var nm *netmap.NetworkMap
wantCollect := func(v bool) {
t.Helper()
if nm.CollectServices != v {
t.Errorf("netmap.CollectServices = %v; want %v", nm.CollectServices, v)
}
}
nm = ms.netmapForResponse(&tailcfg.MapResponse{
Node: new(tailcfg.Node),
})
wantCollect(false)
nm = ms.netmapForResponse(&tailcfg.MapResponse{
Node: new(tailcfg.Node),
CollectServices: "false",
})
wantCollect(false)
nm = ms.netmapForResponse(&tailcfg.MapResponse{
Node: new(tailcfg.Node),
CollectServices: "true",
})
wantCollect(true)
nm = ms.netmapForResponse(&tailcfg.MapResponse{
Node: new(tailcfg.Node),
CollectServices: "",
})
wantCollect(true)
})
t.Run("implicit_domain", func(t *testing.T) {
ms := newTestMapSession(t)
var nm *netmap.NetworkMap
want := func(v string) {
t.Helper()
if nm.Domain != v {
t.Errorf("netmap.Domain = %q; want %q", nm.Domain, v)
}
}
nm = ms.netmapForResponse(&tailcfg.MapResponse{
Node: new(tailcfg.Node),
Domain: "foo.com",
})
want("foo.com")
nm = ms.netmapForResponse(&tailcfg.MapResponse{
Node: new(tailcfg.Node),
})
want("foo.com")
})
t.Run("implicit_node", func(t *testing.T) {
someNode := &tailcfg.Node{
Name: "foo",
}
wantNode := &tailcfg.Node{
Name: "foo",
ComputedName: "foo",
ComputedNameWithHost: "foo",
}
ms := newTestMapSession(t)
nm1 := ms.netmapForResponse(&tailcfg.MapResponse{
Node: someNode,
})
if nm1.SelfNode == nil {
t.Fatal("nil Node in 1st netmap")
}
if !reflect.DeepEqual(nm1.SelfNode, wantNode) {
j, _ := json.Marshal(nm1.SelfNode)
t.Errorf("Node mismatch in 1st netmap; got: %s", j)
}
nm2 := ms.netmapForResponse(&tailcfg.MapResponse{})
if nm2.SelfNode == nil {
t.Fatal("nil Node in 1st netmap")
}
if !reflect.DeepEqual(nm2.SelfNode, wantNode) {
j, _ := json.Marshal(nm2.SelfNode)
t.Errorf("Node mismatch in 2nd netmap; got: %s", j)
}
})
}

View File

@@ -2,6 +2,7 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build windows && cgo
// +build windows,cgo
// darwin,cgo is also supported by certstore but machineCertificateSubject will
@@ -18,7 +19,7 @@ import (
"fmt"
"sync"
"github.com/github/certstore"
"github.com/tailscale/certstore"
"tailscale.com/tailcfg"
"tailscale.com/types/wgkey"
"tailscale.com/util/winutil"
@@ -51,6 +52,46 @@ var (
errBadRequest = errors.New("malformed request")
)
func isSupportedCertificate(cert *x509.Certificate) bool {
return cert.PublicKeyAlgorithm == x509.RSA
}
func isSubjectInChain(subject string, chain []*x509.Certificate) bool {
if len(chain) == 0 || chain[0] == nil {
return false
}
for _, c := range chain {
if c == nil {
continue
}
if c.Subject.String() == subject {
return true
}
}
return false
}
func selectIdentityFromSlice(subject string, ids []certstore.Identity) (certstore.Identity, []*x509.Certificate) {
for _, id := range ids {
chain, err := id.CertificateChain()
if err != nil {
continue
}
if !isSupportedCertificate(chain[0]) {
continue
}
if isSubjectInChain(subject, chain) {
return id, chain
}
}
return nil, nil
}
// findIdentity locates an identity from the Windows or Darwin certificate
// store. It returns the first certificate with a matching Subject anywhere in
// its certificate chain, so it is possible to search for the leaf certificate,
@@ -64,26 +105,7 @@ func findIdentity(subject string, st certstore.Store) (certstore.Identity, []*x5
return nil, nil, err
}
var selected certstore.Identity
var chain []*x509.Certificate
for _, id := range ids {
chain, err = id.CertificateChain()
if err != nil {
continue
}
if chain[0].PublicKeyAlgorithm != x509.RSA {
continue
}
for _, c := range chain {
if c.Subject.String() == subject {
selected = id
break
}
}
}
selected, chain := selectIdentityFromSlice(subject, ids)
for _, id := range ids {
if id != selected {

View File

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

View File

@@ -0,0 +1,105 @@
// 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 controlclient
import (
"encoding/json"
"fmt"
"reflect"
"tailscale.com/tailcfg"
"tailscale.com/types/empty"
"tailscale.com/types/netmap"
"tailscale.com/types/persist"
"tailscale.com/types/structs"
)
// State is the high-level state of the client. It is used only in
// unit tests for proper sequencing, don't depend on it anywhere else.
//
// TODO(apenwarr): eliminate the state, as it's now obsolete.
//
// apenwarr: Historical note: controlclient.Auto was originally
// intended to be the state machine for the whole tailscale client, but that
// turned out to not be the right abstraction layer, and it moved to
// ipn.Backend. Since ipn.Backend now has a state machine, it would be
// much better if controlclient could be a simple stateless API. But the
// current server-side API (two interlocking polling https calls) makes that
// very hard to implement. A server side API change could untangle this and
// remove all the statefulness.
type State int
const (
StateNew = State(iota)
StateNotAuthenticated
StateAuthenticating
StateURLVisitRequired
StateAuthenticated
StateSynchronized // connected and received map update
)
func (s State) MarshalText() ([]byte, error) {
return []byte(s.String()), nil
}
func (s State) String() string {
switch s {
case StateNew:
return "state:new"
case StateNotAuthenticated:
return "state:not-authenticated"
case StateAuthenticating:
return "state:authenticating"
case StateURLVisitRequired:
return "state:url-visit-required"
case StateAuthenticated:
return "state:authenticated"
case StateSynchronized:
return "state:synchronized"
default:
return fmt.Sprintf("state:unknown:%d", int(s))
}
}
type Status struct {
_ structs.Incomparable
LoginFinished *empty.Message // nonempty when login finishes
LogoutFinished *empty.Message // nonempty when logout finishes
Err string
URL string // interactive URL to visit to finish logging in
NetMap *netmap.NetworkMap // server-pushed configuration
// The internal state should not be exposed outside this
// package, but we have some automated tests elsewhere that need to
// use them. Please don't use these fields.
// TODO(apenwarr): Unexport or remove these.
State State
Persist *persist.Persist // locally persisted configuration
Hostinfo *tailcfg.Hostinfo // current Hostinfo data
}
// Equal reports whether s and s2 are equal.
func (s *Status) Equal(s2 *Status) bool {
if s == nil && s2 == nil {
return true
}
return s != nil && s2 != nil &&
(s.LoginFinished == nil) == (s2.LoginFinished == nil) &&
(s.LogoutFinished == nil) == (s2.LogoutFinished == nil) &&
s.Err == s2.Err &&
s.URL == s2.URL &&
reflect.DeepEqual(s.Persist, s2.Persist) &&
reflect.DeepEqual(s.NetMap, s2.NetMap) &&
reflect.DeepEqual(s.Hostinfo, s2.Hostinfo) &&
s.State == s2.State
}
func (s Status) String() string {
b, err := json.MarshalIndent(s, "", "\t")
if err != nil {
panic(err)
}
return s.State.String() + " " + string(b)
}

View File

@@ -0,0 +1,34 @@
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package controlknobs contains client options configurable from control which can be turned on
// or off. The ability to turn options on and off is for incrementally adding features in.
package controlknobs
import (
"os"
"strconv"
"tailscale.com/syncs"
)
// disableUPnP indicates whether to attempt UPnP mapping.
var disableUPnP syncs.AtomicBool
func init() {
v, _ := strconv.ParseBool(os.Getenv("TS_DISABLE_UPNP"))
SetDisableUPnP(v)
}
// DisableUPnP reports the last reported value from control
// whether UPnP portmapping should be disabled.
func DisableUPnP() bool {
return disableUPnP.Get()
}
// SetDisableUPnP sets whether control says that UPnP should be
// disabled.
func SetDisableUPnP(v bool) {
disableUPnP.Set(v)
}

View File

@@ -29,6 +29,7 @@ type Client struct {
br *bufio.Reader
meshKey string
canAckPings bool
isProber bool
wmu sync.Mutex // hold while writing to bw
bw *bufio.Writer
@@ -52,6 +53,7 @@ type clientOpt struct {
MeshKey string
ServerPub key.Public
CanAckPings bool
IsProber bool
}
// MeshKey returns a ClientOpt to pass to the DERP server during connect to get
@@ -60,6 +62,10 @@ type clientOpt struct {
// An empty key means to not use a mesh key.
func MeshKey(key string) ClientOpt { return clientOptFunc(func(o *clientOpt) { o.MeshKey = key }) }
// IsProber returns a ClientOpt to pass to the DERP server during connect to
// declare that this client is a a prober.
func IsProber(v bool) ClientOpt { return clientOptFunc(func(o *clientOpt) { o.IsProber = v }) }
// ServerPublicKey returns a ClientOpt to declare that the server's DERP public key is known.
// If key is the zero value, the returned ClientOpt is a no-op.
func ServerPublicKey(key key.Public) ClientOpt {
@@ -93,6 +99,7 @@ func newClient(privateKey key.Private, nc Conn, brw *bufio.ReadWriter, logf logg
bw: brw.Writer,
meshKey: opt.MeshKey,
canAckPings: opt.CanAckPings,
isProber: opt.IsProber,
}
if opt.ServerPub.IsZero() {
if err := c.recvServerKey(); err != nil {
@@ -160,6 +167,9 @@ type clientInfo struct {
// CanAckPings is whether the client declares it's able to ack
// pings.
CanAckPings bool
// IsProber is whether this client is a prober.
IsProber bool `json:",omitempty"`
}
func (c *Client) sendClientKey() error {
@@ -171,6 +181,7 @@ func (c *Client) sendClientKey() error {
Version: ProtocolVersion,
MeshKey: c.meshKey,
CanAckPings: c.canAckPings,
IsProber: c.isProber,
})
if err != nil {
return err

View File

@@ -20,22 +20,30 @@ import (
"io"
"io/ioutil"
"log"
"math"
"math/big"
"math/rand"
"net/http"
"os"
"os/exec"
"runtime"
"strconv"
"strings"
"sync"
"sync/atomic"
"time"
"go4.org/mem"
"golang.org/x/crypto/nacl/box"
"golang.org/x/sync/errgroup"
"golang.org/x/time/rate"
"inet.af/netaddr"
"tailscale.com/client/tailscale"
"tailscale.com/disco"
"tailscale.com/metrics"
"tailscale.com/types/key"
"tailscale.com/types/logger"
"tailscale.com/types/pad32"
"tailscale.com/version"
)
@@ -69,13 +77,6 @@ const (
writeTimeout = 2 * time.Second
)
const host64bit = (^uint(0) >> 32) & 1 // 1 on 64-bit, 0 on 32-bit
// pad32bit is 4 on 32-bit machines and 0 on 64-bit.
// It exists so the Server struct's atomic fields can be aligned to 8
// byte boundaries. (As tested by GOARCH=386 go test, etc)
const pad32bit = 4 - host64bit*4 // 0 on 64-bit, 4 on 32-bit
// Server is a DERP server.
type Server struct {
// WriteTimeout, if non-zero, specifies how long to wait
@@ -91,42 +92,46 @@ type Server struct {
metaCert []byte // the encoded x509 cert to send after LetsEncrypt cert+intermediate
// Counters:
_ [pad32bit]byte
packetsSent, bytesSent expvar.Int
packetsRecv, bytesRecv expvar.Int
packetsRecvByKind metrics.LabelMap
packetsRecvDisco *expvar.Int
packetsRecvOther *expvar.Int
_ [pad32bit]byte
packetsDropped expvar.Int
packetsDroppedReason metrics.LabelMap
packetsDroppedUnknown *expvar.Int // unknown dst pubkey
packetsDroppedFwdUnknown *expvar.Int // unknown dst pubkey on forward
packetsDroppedGone *expvar.Int // dst conn shutting down
packetsDroppedQueueHead *expvar.Int // queue full, drop head packet
packetsDroppedQueueTail *expvar.Int // queue full, drop tail packet
packetsDroppedWrite *expvar.Int // error writing to dst conn
_ [pad32bit]byte
packetsForwardedOut expvar.Int
packetsForwardedIn expvar.Int
peerGoneFrames expvar.Int // number of peer gone frames sent
accepts expvar.Int
curClients expvar.Int
curHomeClients expvar.Int // ones with preferred
clientsReplaced expvar.Int
unknownFrames expvar.Int
homeMovesIn expvar.Int // established clients announce home server moves in
homeMovesOut expvar.Int // established clients announce home server moves out
multiForwarderCreated expvar.Int
multiForwarderDeleted expvar.Int
removePktForwardOther expvar.Int
_ pad32.Four
packetsSent, bytesSent expvar.Int
packetsRecv, bytesRecv expvar.Int
packetsRecvByKind metrics.LabelMap
packetsRecvDisco *expvar.Int
packetsRecvOther *expvar.Int
_ pad32.Four
packetsDropped expvar.Int
packetsDroppedReason metrics.LabelMap
packetsDroppedReasonCounters []*expvar.Int // indexed by dropReason
packetsDroppedType metrics.LabelMap
packetsDroppedTypeDisco *expvar.Int
packetsDroppedTypeOther *expvar.Int
_ pad32.Four
packetsForwardedOut expvar.Int
packetsForwardedIn expvar.Int
peerGoneFrames expvar.Int // number of peer gone frames sent
accepts expvar.Int
curClients expvar.Int
curHomeClients expvar.Int // ones with preferred
clientsReplaced expvar.Int
clientsReplaceLimited expvar.Int
clientsReplaceSleeping expvar.Int
unknownFrames expvar.Int
homeMovesIn expvar.Int // established clients announce home server moves in
homeMovesOut expvar.Int // established clients announce home server moves out
multiForwarderCreated expvar.Int
multiForwarderDeleted expvar.Int
removePktForwardOther expvar.Int
avgQueueDuration *uint64 // In milliseconds; accessed atomically
mu sync.Mutex
closed bool
netConns map[Conn]chan struct{} // chan is closed when conn closes
clients map[key.Public]*sclient
clientsEver map[key.Public]bool // never deleted from, for stats; fine for now
watchers map[*sclient]bool // mesh peer -> true
// verifyClients only accepts client connections to the DERP server if the clientKey is a
// known peer in the network, as specified by a running tailscaled's client's local api.
verifyClients bool
mu sync.Mutex
closed bool
netConns map[Conn]chan struct{} // chan is closed when conn closes
clients map[key.Public]*sclient
watchers map[*sclient]bool // mesh peer -> true
// clientsMesh tracks all clients in the cluster, both locally
// and to mesh peers. If the value is nil, that means the
// peer is only local (and thus in the clients Map, but not
@@ -138,6 +143,9 @@ type Server struct {
// because it includes intra-region forwarded packets as the
// src.
sentTo map[key.Public]map[key.Public]int64 // src => dst => dst's latest sclient.connNum
// maps from netaddr.IPPort to a client's public key
keyOfAddr map[netaddr.IPPort]key.Public
}
// PacketForwarder is something that can forward packets.
@@ -153,7 +161,7 @@ type PacketForwarder interface {
// Conn is the subset of the underlying net.Conn the DERP Server needs.
// It is a defined type so that non-net connections can be used.
type Conn interface {
io.Closer
io.WriteCloser
// The *Deadline methods follow the semantics of net.Conn.
@@ -175,23 +183,29 @@ func NewServer(privateKey key.Private, logf logger.Logf) *Server {
limitedLogf: logger.RateLimitedFn(logf, 30*time.Second, 5, 100),
packetsRecvByKind: metrics.LabelMap{Label: "kind"},
packetsDroppedReason: metrics.LabelMap{Label: "reason"},
packetsDroppedType: metrics.LabelMap{Label: "type"},
clients: map[key.Public]*sclient{},
clientsEver: map[key.Public]bool{},
clientsMesh: map[key.Public]PacketForwarder{},
netConns: map[Conn]chan struct{}{},
memSys0: ms.Sys,
watchers: map[*sclient]bool{},
sentTo: map[key.Public]map[key.Public]int64{},
avgQueueDuration: new(uint64),
keyOfAddr: map[netaddr.IPPort]key.Public{},
}
s.initMetacert()
s.packetsRecvDisco = s.packetsRecvByKind.Get("disco")
s.packetsRecvOther = s.packetsRecvByKind.Get("other")
s.packetsDroppedUnknown = s.packetsDroppedReason.Get("unknown_dest")
s.packetsDroppedFwdUnknown = s.packetsDroppedReason.Get("unknown_dest_on_fwd")
s.packetsDroppedGone = s.packetsDroppedReason.Get("gone")
s.packetsDroppedQueueHead = s.packetsDroppedReason.Get("queue_head")
s.packetsDroppedQueueTail = s.packetsDroppedReason.Get("queue_tail")
s.packetsDroppedWrite = s.packetsDroppedReason.Get("write_error")
s.packetsDroppedReasonCounters = []*expvar.Int{
s.packetsDroppedReason.Get("unknown_dest"),
s.packetsDroppedReason.Get("unknown_dest_on_fwd"),
s.packetsDroppedReason.Get("gone"),
s.packetsDroppedReason.Get("queue_head"),
s.packetsDroppedReason.Get("queue_tail"),
s.packetsDroppedReason.Get("write_error"),
}
s.packetsDroppedTypeDisco = s.packetsDroppedType.Get("disco")
s.packetsDroppedTypeOther = s.packetsDroppedType.Get("other")
return s
}
@@ -203,6 +217,13 @@ func (s *Server) SetMeshKey(v string) {
s.meshKey = v
}
// SetVerifyClients sets whether this DERP server verifies clients through tailscaled.
//
// It must be called before serving begins.
func (s *Server) SetVerifyClient(v bool) {
s.verifyClients = v
}
// HasMeshKey reports whether the server is configured with a mesh key.
func (s *Server) HasMeshKey() bool { return s.meshKey != "" }
@@ -322,25 +343,40 @@ func (s *Server) initMetacert() {
func (s *Server) MetaCert() []byte { return s.metaCert }
// registerClient notes that client c is now authenticated and ready for packets.
// If c's public key was already connected with a different connection, the prior one is closed.
func (s *Server) registerClient(c *sclient) {
//
// If c's public key was already connected with a different
// connection, the prior one is closed, unless it's fighting rapidly
// with another client with the same key, in which case the returned
// ok is false, and the caller should wait the provided duration
// before trying again.
func (s *Server) registerClient(c *sclient) (ok bool, d time.Duration) {
s.mu.Lock()
defer s.mu.Unlock()
old := s.clients[c.key]
if old == nil {
c.logf("adding connection")
} else {
// Take over the old rate limiter, discarding the one
// our caller just made.
c.replaceLimiter = old.replaceLimiter
if rr := c.replaceLimiter.ReserveN(timeNow(), 1); rr.OK() {
if d := rr.DelayFrom(timeNow()); d > 0 {
s.clientsReplaceLimited.Add(1)
return false, d
}
}
s.clientsReplaced.Add(1)
c.logf("adding connection, replacing %s", old.remoteAddr)
go old.nc.Close()
}
s.clients[c.key] = c
s.clientsEver[c.key] = true
if _, ok := s.clientsMesh[c.key]; !ok {
s.clientsMesh[c.key] = nil // just for varz of total users in cluster
}
s.keyOfAddr[c.remoteIPPort] = c.key
s.curClients.Add(1)
s.broadcastPeerStateChangeLocked(c.key, true)
return true, 0
}
// broadcastPeerStateChangeLocked enqueues a message to all watchers
@@ -373,6 +409,8 @@ func (s *Server) unregisterClient(c *sclient) {
delete(s.watchers, c)
}
delete(s.keyOfAddr, c.remoteIPPort)
s.curClients.Add(-1)
if c.preferred {
s.curHomeClients.Add(-1)
@@ -426,8 +464,9 @@ func (s *Server) addWatcher(c *sclient) {
}
func (s *Server) accept(nc Conn, brw *bufio.ReadWriter, remoteAddr string, connNum int64) error {
br, bw := brw.Reader, brw.Writer
br := brw.Reader
nc.SetDeadline(time.Now().Add(10 * time.Second))
bw := &lazyBufioWriter{w: nc, lbw: brw.Writer}
if err := s.sendServerKey(bw); err != nil {
return fmt.Errorf("send server key: %v", err)
}
@@ -446,21 +485,32 @@ func (s *Server) accept(nc Conn, brw *bufio.ReadWriter, remoteAddr string, connN
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
remoteIPPort, _ := netaddr.ParseIPPort(remoteAddr)
c := &sclient{
connNum: connNum,
s: s,
key: clientKey,
nc: nc,
br: br,
bw: bw,
logf: logger.WithPrefix(s.logf, fmt.Sprintf("derp client %v/%x: ", remoteAddr, clientKey)),
done: ctx.Done(),
remoteAddr: remoteAddr,
connectedAt: time.Now(),
sendQueue: make(chan pkt, perClientSendQueueDepth),
peerGone: make(chan key.Public),
canMesh: clientInfo.MeshKey != "" && clientInfo.MeshKey == s.meshKey,
connNum: connNum,
s: s,
key: clientKey,
nc: nc,
br: br,
bw: bw,
logf: logger.WithPrefix(s.logf, fmt.Sprintf("derp client %v/%x: ", remoteAddr, clientKey)),
done: ctx.Done(),
remoteAddr: remoteAddr,
remoteIPPort: remoteIPPort,
connectedAt: time.Now(),
sendQueue: make(chan pkt, perClientSendQueueDepth),
discoSendQueue: make(chan pkt, perClientSendQueueDepth),
peerGone: make(chan key.Public),
canMesh: clientInfo.MeshKey != "" && clientInfo.MeshKey == s.meshKey,
// Allow kicking out previous connections once a
// minute, with a very high burst of 100. Once a
// minute is less than the client's 2 minute
// inactivity timeout.
replaceLimiter: rate.NewLimiter(rate.Every(time.Minute), 100),
}
if c.canMesh {
c.meshUpdate = make(chan struct{})
}
@@ -468,10 +518,18 @@ func (s *Server) accept(nc Conn, brw *bufio.ReadWriter, remoteAddr string, connN
c.info = *clientInfo
}
s.registerClient(c)
for {
ok, d := s.registerClient(c)
if ok {
break
}
s.clientsReplaceSleeping.Add(1)
timeSleep(d)
s.clientsReplaceSleeping.Add(-1)
}
defer s.unregisterClient(c)
err = s.sendServerInfo(bw, clientKey)
err = s.sendServerInfo(c.bw, clientKey)
if err != nil {
return fmt.Errorf("send server info: %v", err)
}
@@ -479,6 +537,12 @@ func (s *Server) accept(nc Conn, brw *bufio.ReadWriter, remoteAddr string, connN
return c.run(ctx)
}
// for testing
var (
timeSleep = time.Sleep
timeNow = time.Now
)
// run serves the client until there's an error.
// If the client hangs up or the server is closed, run returns nil, otherwise run returns an error.
func (c *sclient) run(ctx context.Context) error {
@@ -602,17 +666,14 @@ func (c *sclient) handleFrameForwardPacket(ft frameType, fl uint32) error {
s.mu.Unlock()
if dst == nil {
s.packetsDropped.Add(1)
s.packetsDroppedFwdUnknown.Add(1)
if debug {
c.logf("dropping forwarded packet for unknown %x", dstKey)
}
s.recordDrop(contents, srcKey, dstKey, dropReasonUnknownDestOnFwd)
return nil
}
return c.sendPkt(dst, pkt{
bs: contents,
src: srcKey,
bs: contents,
enqueuedAt: time.Now(),
src: srcKey,
})
}
@@ -656,21 +717,53 @@ func (c *sclient) handleFrameSendPacket(ft frameType, fl uint32) error {
}
return nil
}
s.packetsDropped.Add(1)
s.packetsDroppedUnknown.Add(1)
if debug {
c.logf("dropping packet for unknown %x", dstKey)
}
s.recordDrop(contents, c.key, dstKey, dropReasonUnknownDest)
return nil
}
p := pkt{
bs: contents,
src: c.key,
bs: contents,
enqueuedAt: time.Now(),
src: c.key,
}
return c.sendPkt(dst, p)
}
// dropReason is why we dropped a DERP frame.
type dropReason int
//go:generate go run tailscale.com/cmd/addlicense -year 2021 -file dropreason_string.go go run golang.org/x/tools/cmd/stringer -type=dropReason -trimprefix=dropReason
const (
dropReasonUnknownDest dropReason = iota // unknown destination pubkey
dropReasonUnknownDestOnFwd // unknown destination pubkey on a derp-forwarded packet
dropReasonGone // destination tailscaled disconnected before we could send
dropReasonQueueHead // destination queue is full, dropped packet at queue head
dropReasonQueueTail // destination queue is full, dropped packet at queue tail
dropReasonWriteError // OS write() failed
)
func (s *Server) recordDrop(packetBytes []byte, srcKey, dstKey key.Public, reason dropReason) {
s.packetsDropped.Add(1)
s.packetsDroppedReasonCounters[reason].Add(1)
if disco.LooksLikeDiscoWrapper(packetBytes) {
s.packetsDroppedTypeDisco.Add(1)
} else {
s.packetsDroppedTypeOther.Add(1)
}
if verboseDropKeys[dstKey] {
// Preformat the log string prior to calling limitedLogf. The
// limiter acts based on the format string, and we want to
// rate-limit per src/dst keys, not on the generic "dropped
// stuff" message.
msg := fmt.Sprintf("drop (%s) %s -> %s", srcKey.ShortString(), reason, dstKey.ShortString())
s.limitedLogf(msg)
}
if debug {
s.logf("dropping packet reason=%s dst=%s disco=%v", reason, dstKey, disco.LooksLikeDiscoWrapper(packetBytes))
}
}
func (c *sclient) sendPkt(dst *sclient, p pkt) error {
s := c.s
dstKey := dst.key
@@ -678,53 +771,34 @@ func (c *sclient) sendPkt(dst *sclient, p pkt) error {
// Attempt to queue for sending up to 3 times. On each attempt, if
// the queue is full, try to drop from queue head to prioritize
// fresher packets.
sendQueue := dst.sendQueue
if disco.LooksLikeDiscoWrapper(p.bs) {
sendQueue = dst.discoSendQueue
}
for attempt := 0; attempt < 3; attempt++ {
select {
case <-dst.done:
s.packetsDropped.Add(1)
s.packetsDroppedGone.Add(1)
if debug {
c.logf("dropping packet for shutdown client %x", dstKey)
}
s.recordDrop(p.bs, c.key, dstKey, dropReasonGone)
return nil
default:
}
select {
case dst.sendQueue <- p:
case sendQueue <- p:
return nil
default:
}
select {
case <-dst.sendQueue:
s.packetsDropped.Add(1)
s.packetsDroppedQueueHead.Add(1)
if verboseDropKeys[dstKey] {
// Generate a full string including src and dst, so
// the limiter kicks in once per src.
msg := fmt.Sprintf("tail drop %s -> %s", p.src.ShortString(), dstKey.ShortString())
c.s.limitedLogf(msg)
}
if debug {
c.logf("dropping packet from client %x queue head", dstKey)
}
case pkt := <-sendQueue:
s.recordDrop(pkt.bs, c.key, dstKey, dropReasonQueueHead)
c.recordQueueTime(pkt.enqueuedAt)
default:
}
}
// Failed to make room for packet. This can happen in a heavily
// contended queue with racing writers. Give up and tail-drop in
// this case to keep reader unblocked.
s.packetsDropped.Add(1)
s.packetsDroppedQueueTail.Add(1)
if verboseDropKeys[dstKey] {
// Generate a full string including src and dst, so
// the limiter kicks in once per src.
msg := fmt.Sprintf("head drop %s -> %s", p.src.ShortString(), dstKey.ShortString())
c.s.limitedLogf(msg)
}
if debug {
c.logf("dropping packet from client %x queue tail", dstKey)
}
s.recordDrop(p.bs, c.key, dstKey, dropReasonQueueTail)
return nil
}
@@ -750,23 +824,37 @@ func (c *sclient) requestMeshUpdate() {
}
func (s *Server) verifyClient(clientKey key.Public, info *clientInfo) error {
// TODO(crawshaw): implement policy constraints on who can use the DERP server
// TODO(bradfitz): ... and at what rate.
if !s.verifyClients {
return nil
}
status, err := tailscale.Status(context.TODO())
if err != nil {
return fmt.Errorf("failed to query local tailscaled status: %w", err)
}
if clientKey == status.Self.PublicKey {
return nil
}
if _, exists := status.Peer[clientKey]; !exists {
return fmt.Errorf("client %v not in set of peers", clientKey)
}
// TODO(bradfitz): add policy for configurable bandwidth rate per client?
return nil
}
func (s *Server) sendServerKey(bw *bufio.Writer) error {
func (s *Server) sendServerKey(lw *lazyBufioWriter) error {
buf := make([]byte, 0, len(magic)+len(s.publicKey))
buf = append(buf, magic...)
buf = append(buf, s.publicKey[:]...)
return writeFrame(bw, frameServerKey, buf)
err := writeFrame(lw.bw(), frameServerKey, buf)
lw.Flush() // redundant (no-op) flush to release bufio.Writer
return err
}
type serverInfo struct {
Version int `json:"version,omitempty"`
}
func (s *Server) sendServerInfo(bw *bufio.Writer, clientKey key.Public) error {
func (s *Server) sendServerInfo(bw *lazyBufioWriter, clientKey key.Public) error {
var nonce [24]byte
if _, err := crand.Read(nonce[:]); err != nil {
return err
@@ -777,7 +865,7 @@ func (s *Server) sendServerInfo(bw *bufio.Writer, clientKey key.Public) error {
}
msgbox := box.Seal(nil, msg, &nonce, clientKey.B32(), s.privateKey.B32())
if err := writeFrameHeader(bw, frameServerInfo, nonceLen+uint32(len(msgbox))); err != nil {
if err := writeFrameHeader(bw.bw(), frameServerInfo, nonceLen+uint32(len(msgbox))); err != nil {
return err
}
if _, err := bw.Write(nonce[:]); err != nil {
@@ -885,18 +973,25 @@ func (s *Server) recvForwardPacket(br *bufio.Reader, frameLen uint32) (srcKey, d
// (The "s" prefix is to more explicitly distinguish it from Client in derp_client.go)
type sclient struct {
// Static after construction.
connNum int64 // process-wide unique counter, incremented each Accept
s *Server
nc Conn
key key.Public
info clientInfo
logf logger.Logf
done <-chan struct{} // closed when connection closes
remoteAddr string // usually ip:port from net.Conn.RemoteAddr().String()
sendQueue chan pkt // packets queued to this client; never closed
peerGone chan key.Public // write request that a previous sender has disconnected (not used by mesh peers)
meshUpdate chan struct{} // write request to write peerStateChange
canMesh bool // clientInfo had correct mesh token for inter-region routing
connNum int64 // process-wide unique counter, incremented each Accept
s *Server
nc Conn
key key.Public
info clientInfo
logf logger.Logf
done <-chan struct{} // closed when connection closes
remoteAddr string // usually ip:port from net.Conn.RemoteAddr().String()
remoteIPPort netaddr.IPPort // zero if remoteAddr is not ip:port.
sendQueue chan pkt // packets queued to this client; never closed
discoSendQueue chan pkt // important packets queued to this client; never closed
peerGone chan key.Public // write request that a previous sender has disconnected (not used by mesh peers)
meshUpdate chan struct{} // write request to write peerStateChange
canMesh bool // clientInfo had correct mesh token for inter-region routing
// replaceLimiter controls how quickly two connections with
// the same client key can kick each other off the server by
// taking over ownership of a key.
replaceLimiter *rate.Limiter
// Owned by run, not thread-safe.
br *bufio.Reader
@@ -904,7 +999,7 @@ type sclient struct {
preferred bool
// Owned by sender, not thread-safe.
bw *bufio.Writer
bw *lazyBufioWriter
// Guarded by s.mu
//
@@ -927,11 +1022,13 @@ type pkt struct {
// src is the who's the sender of the packet.
src key.Public
// enqueuedAt is when a packet was put onto a queue before it was sent,
// and is used for reporting metrics on the duration of packets in the queue.
enqueuedAt time.Time
// bs is the data packet bytes.
// The memory is owned by pkt.
bs []byte
// TODO(danderson): enqueue time, to measure queue latency?
}
func (c *sclient) setPreferred(v bool) {
@@ -959,6 +1056,25 @@ func (c *sclient) setPreferred(v bool) {
}
}
// expMovingAverage returns the new moving average given the previous average,
// a new value, and an alpha decay factor.
// https://en.wikipedia.org/wiki/Moving_average#Exponential_moving_average
func expMovingAverage(prev, newValue, alpha float64) float64 {
return alpha*newValue + (1-alpha)*prev
}
// recordQueueTime updates the average queue duration metric after a packet has been sent.
func (c *sclient) recordQueueTime(enqueuedAt time.Time) {
elapsed := float64(time.Since(enqueuedAt).Milliseconds())
for {
old := atomic.LoadUint64(c.s.avgQueueDuration)
newAvg := expMovingAverage(math.Float64frombits(old), elapsed, 0.1)
if atomic.CompareAndSwapUint64(c.s.avgQueueDuration, old, math.Float64bits(newAvg)) {
break
}
}
}
func (c *sclient) sendLoop(ctx context.Context) error {
defer func() {
// If the sender shuts down unilaterally due to an error, close so
@@ -968,12 +1084,10 @@ func (c *sclient) sendLoop(ctx context.Context) error {
// Drain the send queue to count dropped packets
for {
select {
case <-c.sendQueue:
c.s.packetsDropped.Add(1)
c.s.packetsDroppedGone.Add(1)
if debug {
c.logf("dropping packet for shutdown %x", c.key)
}
case pkt := <-c.sendQueue:
c.s.recordDrop(pkt.bs, pkt.src, c.key, dropReasonGone)
case pkt := <-c.discoSendQueue:
c.s.recordDrop(pkt.bs, pkt.src, c.key, dropReasonGone)
default:
return
}
@@ -1002,6 +1116,11 @@ func (c *sclient) sendLoop(ctx context.Context) error {
continue
case msg := <-c.sendQueue:
werr = c.sendPacket(msg.src, msg.bs)
c.recordQueueTime(msg.enqueuedAt)
continue
case msg := <-c.discoSendQueue:
werr = c.sendPacket(msg.src, msg.bs)
c.recordQueueTime(msg.enqueuedAt)
continue
case <-keepAliveTick.C:
werr = c.sendKeepAlive()
@@ -1025,6 +1144,10 @@ func (c *sclient) sendLoop(ctx context.Context) error {
continue
case msg := <-c.sendQueue:
werr = c.sendPacket(msg.src, msg.bs)
c.recordQueueTime(msg.enqueuedAt)
case msg := <-c.discoSendQueue:
werr = c.sendPacket(msg.src, msg.bs)
c.recordQueueTime(msg.enqueuedAt)
case <-keepAliveTick.C:
werr = c.sendKeepAlive()
}
@@ -1038,14 +1161,14 @@ func (c *sclient) setWriteDeadline() {
// sendKeepAlive sends a keep-alive frame, without flushing.
func (c *sclient) sendKeepAlive() error {
c.setWriteDeadline()
return writeFrameHeader(c.bw, frameKeepAlive, 0)
return writeFrameHeader(c.bw.bw(), frameKeepAlive, 0)
}
// sendPeerGone sends a peerGone frame, without flushing.
func (c *sclient) sendPeerGone(peer key.Public) error {
c.s.peerGoneFrames.Add(1)
c.setWriteDeadline()
if err := writeFrameHeader(c.bw, framePeerGone, keyLen); err != nil {
if err := writeFrameHeader(c.bw.bw(), framePeerGone, keyLen); err != nil {
return err
}
_, err := c.bw.Write(peer[:])
@@ -1055,7 +1178,7 @@ func (c *sclient) sendPeerGone(peer key.Public) error {
// sendPeerPresent sends a peerPresent frame, without flushing.
func (c *sclient) sendPeerPresent(peer key.Public) error {
c.setWriteDeadline()
if err := writeFrameHeader(c.bw, framePeerPresent, keyLen); err != nil {
if err := writeFrameHeader(c.bw.bw(), framePeerPresent, keyLen); err != nil {
return err
}
_, err := c.bw.Write(peer[:])
@@ -1114,11 +1237,7 @@ func (c *sclient) sendPacket(srcKey key.Public, contents []byte) (err error) {
defer func() {
// Stats update.
if err != nil {
c.s.packetsDropped.Add(1)
c.s.packetsDroppedWrite.Add(1)
if debug {
c.logf("dropping packet to %x: %v", c.key, err)
}
c.s.recordDrop(contents, srcKey, c.key, dropReasonWriteError)
} else {
c.s.packetsSent.Add(1)
c.s.bytesSent.Add(int64(len(contents)))
@@ -1132,11 +1251,11 @@ func (c *sclient) sendPacket(srcKey key.Public, contents []byte) (err error) {
if withKey {
pktLen += len(srcKey)
}
if err = writeFrameHeader(c.bw, frameRecvPacket, uint32(pktLen)); err != nil {
if err = writeFrameHeader(c.bw.bw(), frameRecvPacket, uint32(pktLen)); err != nil {
return err
}
if withKey {
err := writePublicKey(c.bw, &srcKey)
err := writePublicKey(c.bw.bw(), &srcKey)
if err != nil {
return err
}
@@ -1264,7 +1383,6 @@ func (s *Server) expVarFunc(f func() interface{}) expvar.Func {
// ExpVar returns an expvar variable suitable for registering with expvar.Publish.
func (s *Server) ExpVar() expvar.Var {
m := new(metrics.Set)
m.Set("counter_unique_clients_ever", s.expVarFunc(func() interface{} { return len(s.clientsEver) }))
m.Set("gauge_memstats_sys0", expvar.Func(func() interface{} { return int64(s.memSys0) }))
m.Set("gauge_watchers", s.expVarFunc(func() interface{} { return len(s.watchers) }))
m.Set("gauge_current_connections", &s.curClients)
@@ -1274,10 +1392,13 @@ func (s *Server) ExpVar() expvar.Var {
m.Set("gauge_clients_remote", expvar.Func(func() interface{} { return len(s.clientsMesh) - len(s.clients) }))
m.Set("accepts", &s.accepts)
m.Set("clients_replaced", &s.clientsReplaced)
m.Set("clients_replace_limited", &s.clientsReplaceLimited)
m.Set("gauge_clients_replace_sleeping", &s.clientsReplaceSleeping)
m.Set("bytes_received", &s.bytesRecv)
m.Set("bytes_sent", &s.bytesSent)
m.Set("packets_dropped", &s.packetsDropped)
m.Set("counter_packets_dropped_reason", &s.packetsDroppedReason)
m.Set("counter_packets_dropped_type", &s.packetsDroppedType)
m.Set("counter_packets_received_kind", &s.packetsRecvByKind)
m.Set("packets_sent", &s.packetsSent)
m.Set("packets_received", &s.packetsRecv)
@@ -1290,6 +1411,9 @@ func (s *Server) ExpVar() expvar.Var {
m.Set("multiforwarder_created", &s.multiForwarderCreated)
m.Set("multiforwarder_deleted", &s.multiForwarderDeleted)
m.Set("packet_forwarder_delete_other_value", &s.removePktForwardOther)
m.Set("average_queue_duration_ms", expvar.Func(func() interface{} {
return math.Float64frombits(atomic.LoadUint64(s.avgQueueDuration))
}))
var expvarVersion expvar.String
expvarVersion.Set(version.Long)
m.Set("version", &expvarVersion)
@@ -1365,3 +1489,126 @@ func writePublicKey(bw *bufio.Writer, key *key.Public) error {
}
return nil
}
const minTimeBetweenLogs = 2 * time.Second
// BytesSentRecv records the number of bytes that have been sent since the last traffic check
// for a given process, as well as the public key of the process sending those bytes.
type BytesSentRecv struct {
Sent uint64
Recv uint64
// Key is the public key of the client which sent/received these bytes.
Key key.Public
}
// parseSSOutput parses the output from the specific call to ss in ServeDebugTraffic.
// Separated out for ease of testing.
func parseSSOutput(raw string) map[netaddr.IPPort]BytesSentRecv {
newState := map[netaddr.IPPort]BytesSentRecv{}
// parse every 2 lines and get src and dst ips, and kv pairs
lines := strings.Split(raw, "\n")
for i := 0; i < len(lines); i += 2 {
ipInfo := strings.Fields(strings.TrimSpace(lines[i]))
if len(ipInfo) < 5 {
continue
}
src, err := netaddr.ParseIPPort(ipInfo[4])
if err != nil {
continue
}
stats := strings.Fields(strings.TrimSpace(lines[i+1]))
stat := BytesSentRecv{}
for _, s := range stats {
if strings.Contains(s, "bytes_sent") {
sent, err := strconv.Atoi(s[strings.Index(s, ":")+1:])
if err == nil {
stat.Sent = uint64(sent)
}
} else if strings.Contains(s, "bytes_received") {
recv, err := strconv.Atoi(s[strings.Index(s, ":")+1:])
if err == nil {
stat.Recv = uint64(recv)
}
}
}
newState[src] = stat
}
return newState
}
func (s *Server) ServeDebugTraffic(w http.ResponseWriter, r *http.Request) {
prevState := map[netaddr.IPPort]BytesSentRecv{}
enc := json.NewEncoder(w)
for r.Context().Err() == nil {
output, err := exec.Command("ss", "-i", "-H", "-t").Output()
if err != nil {
fmt.Fprintf(w, "ss failed: %v", err)
return
}
newState := parseSSOutput(string(output))
s.mu.Lock()
for k, next := range newState {
prev := prevState[k]
if prev.Sent < next.Sent || prev.Recv < next.Recv {
if pkey, ok := s.keyOfAddr[k]; ok {
next.Key = pkey
if err := enc.Encode(next); err != nil {
s.mu.Unlock()
return
}
}
}
}
s.mu.Unlock()
prevState = newState
if _, err := fmt.Fprintln(w); err != nil {
return
}
if f, ok := w.(http.Flusher); ok {
f.Flush()
}
time.Sleep(minTimeBetweenLogs)
}
}
var bufioWriterPool = &sync.Pool{
New: func() interface{} {
return bufio.NewWriterSize(ioutil.Discard, 2<<10)
},
}
// lazyBufioWriter is a bufio.Writer-like wrapping writer that lazily
// allocates its actual bufio.Writer from a sync.Pool, releasing it to
// the pool upon flush.
//
// We do this to reduce memory overhead; most DERP connections are
// idle and the idle bufio.Writers were 30% of overall memory usage.
type lazyBufioWriter struct {
w io.Writer // underlying
lbw *bufio.Writer // lazy; nil means it needs an associated buffer
}
func (w *lazyBufioWriter) bw() *bufio.Writer {
if w.lbw == nil {
w.lbw = bufioWriterPool.Get().(*bufio.Writer)
w.lbw.Reset(w.w)
}
return w.lbw
}
func (w *lazyBufioWriter) Available() int { return w.bw().Available() }
func (w *lazyBufioWriter) Write(p []byte) (int, error) { return w.bw().Write(p) }
func (w *lazyBufioWriter) Flush() error {
if w.lbw == nil {
return nil
}
err := w.lbw.Flush()
w.lbw.Reset(ioutil.Discard)
bufioWriterPool.Put(w.lbw)
w.lbw = nil
return err
}

View File

@@ -23,6 +23,7 @@ import (
"testing"
"time"
"golang.org/x/time/rate"
"tailscale.com/net/nettest"
"tailscale.com/types/key"
"tailscale.com/types/logger"
@@ -409,7 +410,7 @@ func TestSendFreeze(t *testing.T) {
for i := 0; i < cap(errCh); i++ {
err := <-errCh
if err != nil {
if errors.Is(err, io.EOF) {
if errors.Is(err, io.EOF) || errors.Is(err, net.ErrClosed) {
continue
}
t.Error(err)
@@ -849,6 +850,136 @@ func TestClientSendPong(t *testing.T) {
}
func TestServerReplaceClients(t *testing.T) {
defer func() {
timeSleep = time.Sleep
timeNow = time.Now
}()
var (
mu sync.Mutex
now = time.Unix(123, 0)
sleeps int
slept time.Duration
)
timeSleep = func(d time.Duration) {
mu.Lock()
defer mu.Unlock()
sleeps++
slept += d
now = now.Add(d)
}
timeNow = func() time.Time {
mu.Lock()
defer mu.Unlock()
return now
}
serverPrivateKey := newPrivateKey(t)
var logger logger.Logf = logger.Discard
const debug = false
if debug {
logger = t.Logf
}
s := NewServer(serverPrivateKey, logger)
defer s.Close()
priv := newPrivateKey(t)
ln, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
defer ln.Close()
connNum := 0
connect := func() *Client {
connNum++
cout, err := net.Dial("tcp", ln.Addr().String())
if err != nil {
t.Fatal(err)
}
cin, err := ln.Accept()
if err != nil {
t.Fatal(err)
}
brwServer := bufio.NewReadWriter(bufio.NewReader(cin), bufio.NewWriter(cin))
go s.Accept(cin, brwServer, fmt.Sprintf("test-client-%d", connNum))
brw := bufio.NewReadWriter(bufio.NewReader(cout), bufio.NewWriter(cout))
c, err := NewClient(priv, cout, brw, logger)
if err != nil {
t.Fatalf("client %d: %v", connNum, err)
}
return c
}
wantVar := func(v *expvar.Int, want int64) {
t.Helper()
if got := v.Value(); got != want {
t.Errorf("got %d; want %d", got, want)
}
}
wantClosed := func(c *Client) {
t.Helper()
for {
m, err := c.Recv()
if err != nil {
t.Logf("got expected error: %v", err)
return
}
switch m.(type) {
case ServerInfoMessage:
continue
default:
t.Fatalf("client got %T; wanted an error", m)
}
}
}
c1 := connect()
waitConnect(t, c1)
c2 := connect()
waitConnect(t, c2)
wantVar(&s.clientsReplaced, 1)
wantClosed(c1)
for i := 0; i < 100+5; i++ {
c := connect()
defer c.nc.Close()
if s.clientsReplaceLimited.Value() == 0 && i < 90 {
continue
}
t.Logf("for %d: replaced=%d, limited=%d, sleeping=%d", i,
s.clientsReplaced.Value(),
s.clientsReplaceLimited.Value(),
s.clientsReplaceSleeping.Value(),
)
}
mu.Lock()
defer mu.Unlock()
if sleeps == 0 {
t.Errorf("no sleeps")
}
if slept == 0 {
t.Errorf("total sleep duration was 0")
}
}
func TestLimiter(t *testing.T) {
rl := rate.NewLimiter(rate.Every(time.Minute), 100)
for i := 0; i < 200; i++ {
r := rl.Reserve()
d := r.Delay()
t.Logf("i=%d, allow=%v, d=%v", i, r.OK(), d)
}
}
func BenchmarkSendRecv(b *testing.B) {
for _, size := range []int{10, 100, 1000, 10000} {
b.Run(fmt.Sprintf("msgsize=%d", size), func(b *testing.B) { benchmarkSendRecvSize(b, size) })
@@ -948,3 +1079,14 @@ func waitConnect(t testing.TB, c *Client) {
t.Fatalf("client first Recv was unexpected type %T", v)
}
}
func TestParseSSOutput(t *testing.T) {
contents, err := ioutil.ReadFile("testdata/example_ss.txt")
if err != nil {
t.Errorf("ioutil.Readfile(example_ss.txt) failed: %v", err)
}
seen := parseSSOutput(string(contents))
if len(seen) == 0 {
t.Errorf("parseSSOutput expected non-empty map")
}
}

View File

@@ -50,9 +50,11 @@ type Client struct {
TLSConfig *tls.Config // optional; nil means default
DNSCache *dnscache.Resolver // optional; nil means no caching
MeshKey string // optional; for trusted clients
IsProber bool // optional; for probers to optional declare themselves as such
privateKey key.Private
logf logger.Logf
dialer func(ctx context.Context, network, addr string) (net.Conn, error)
// Either url or getRegion is non-nil:
url *url.URL
@@ -130,6 +132,11 @@ func (c *Client) ServerPublicKey() key.Public {
return c.serverPubKey
}
// SelfPublicKey returns our own public key.
func (c *Client) SelfPublicKey() key.Public {
return c.privateKey.Public()
}
func urlPort(u *url.URL) string {
if p := u.Port(); p != "" {
return p
@@ -338,6 +345,7 @@ func (c *Client) connect(ctx context.Context, caller string) (client *derp.Clien
derp.MeshKey(c.MeshKey),
derp.ServerPublicKey(serverPub),
derp.CanAckPings(c.canAckPings),
derp.IsProber(c.IsProber),
)
if err != nil {
return nil, 0, err
@@ -356,14 +364,26 @@ func (c *Client) connect(ctx context.Context, caller string) (client *derp.Clien
return c.client, c.connGen, nil
}
// SetURLDialer sets the dialer to use for dialing URLs.
// This dialer is only use for clients created with NewClient, not NewRegionClient.
// If unset or nil, the default dialer is used.
//
// The primary use for this is the derper mesh mode to connect to each
// other over a VPC network.
func (c *Client) SetURLDialer(dialer func(ctx context.Context, network, addr string) (net.Conn, error)) {
c.dialer = dialer
}
func (c *Client) dialURL(ctx context.Context) (net.Conn, error) {
host := c.url.Hostname()
if c.dialer != nil {
return c.dialer(ctx, "tcp", net.JoinHostPort(host, urlPort(c.url)))
}
hostOrIP := host
dialer := netns.NewDialer()
if c.DNSCache != nil {
ip, _, err := c.DNSCache.LookupIP(ctx, host)
ip, _, _, err := c.DNSCache.LookupIP(ctx, host)
if err == nil {
hostOrIP = ip.String()
}
@@ -410,9 +430,7 @@ func (c *Client) dialRegion(ctx context.Context, reg *tailcfg.DERPRegion) (net.C
func (c *Client) tlsClient(nc net.Conn, node *tailcfg.DERPNode) *tls.Conn {
tlsConf := tlsdial.Config(c.tlsServerName(node), c.TLSConfig)
if node != nil {
if node.DERPTestPort != 0 {
tlsConf.InsecureSkipVerify = true
}
tlsConf.InsecureSkipVerify = node.InsecureForTests
if node.CertName != "" {
tlsdial.SetConfigExpectedCert(tlsConf, node.CertName)
}
@@ -511,8 +529,8 @@ func (c *Client) dialNode(ctx context.Context, n *tailcfg.DERPNode) (net.Conn, e
dst = n.HostName
}
port := "443"
if n.DERPTestPort != 0 {
port = fmt.Sprint(n.DERPTestPort)
if n.DERPPort != 0 {
port = fmt.Sprint(n.DERPPort)
}
c, err := c.dialContext(ctx, proto, net.JoinHostPort(dst, port))
select {

View File

@@ -1,88 +0,0 @@
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package derpmap contains information about Tailscale.com's production DERP nodes.
//
// This package is only used by the "tailscale netcheck" command for debugging.
// In normal operation the Tailscale nodes get this sent to them from the control
// server.
//
// TODO: remove this package and make "tailscale netcheck" get the
// list from the control server too.
package derpmap
import (
"fmt"
"strings"
"tailscale.com/tailcfg"
)
func derpNode(suffix, v4, v6 string) *tailcfg.DERPNode {
return &tailcfg.DERPNode{
Name: suffix, // updated later
RegionID: 0, // updated later
IPv4: v4,
IPv6: v6,
}
}
func derpRegion(id int, code, name string, nodes ...*tailcfg.DERPNode) *tailcfg.DERPRegion {
region := &tailcfg.DERPRegion{
RegionID: id,
RegionName: name,
RegionCode: code,
Nodes: nodes,
}
for _, n := range nodes {
n.Name = fmt.Sprintf("%d%s", id, n.Name)
n.RegionID = id
n.HostName = fmt.Sprintf("derp%s.tailscale.com", strings.TrimSuffix(n.Name, "a"))
}
return region
}
// Prod returns Tailscale's map of relay servers.
//
// This list is only used by cmd/tailscale's netcheck subcommand. In
// normal operation the Tailscale nodes get this sent to them from the
// control server.
//
// This list is subject to change and should not be relied on.
func Prod() *tailcfg.DERPMap {
return &tailcfg.DERPMap{
Regions: map[int]*tailcfg.DERPRegion{
1: derpRegion(1, "nyc", "New York City",
derpNode("a", "159.89.225.99", "2604:a880:400:d1::828:b001"),
),
2: derpRegion(2, "sfo", "San Francisco",
derpNode("a", "167.172.206.31", "2604:a880:2:d1::c5:7001"),
),
3: derpRegion(3, "sin", "Singapore",
derpNode("a", "68.183.179.66", "2400:6180:0:d1::67d:8001"),
),
4: derpRegion(4, "fra", "Frankfurt",
derpNode("a", "167.172.182.26", "2a03:b0c0:3:e0::36e:9001"),
),
5: derpRegion(5, "syd", "Sydney",
derpNode("a", "103.43.75.49", "2001:19f0:5801:10b7:5400:2ff:feaa:284c"),
),
6: derpRegion(6, "blr", "Bangalore",
derpNode("a", "68.183.90.120", "2400:6180:100:d0::982:d001"),
),
7: derpRegion(7, "tok", "Tokyo",
derpNode("a", "167.179.89.145", "2401:c080:1000:467f:5400:2ff:feee:22aa"),
),
8: derpRegion(8, "lhr", "London",
derpNode("a", "167.71.139.179", "2a03:b0c0:1:e0::3cc:e001"),
),
9: derpRegion(9, "dfw", "Dallas",
derpNode("a", "207.148.3.137", "2001:19f0:6401:1d9c:5400:2ff:feef:bb82"),
),
10: derpRegion(10, "sea", "Seattle",
derpNode("a", "137.220.36.168", "2001:19f0:8001:2d9:5400:2ff:feef:bbb1"),
),
},
}
}

32
derp/dropreason_string.go Normal file
View File

@@ -0,0 +1,32 @@
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Code generated by "stringer -type=dropReason -trimprefix=dropReason"; DO NOT EDIT.
package derp
import "strconv"
func _() {
// An "invalid array index" compiler error signifies that the constant values have changed.
// Re-run the stringer command to generate them again.
var x [1]struct{}
_ = x[dropReasonUnknownDest-0]
_ = x[dropReasonUnknownDestOnFwd-1]
_ = x[dropReasonGone-2]
_ = x[dropReasonQueueHead-3]
_ = x[dropReasonQueueTail-4]
_ = x[dropReasonWriteError-5]
}
const _dropReason_name = "UnknownDestUnknownDestOnFwdGoneQueueHeadQueueTailWriteError"
var _dropReason_index = [...]uint8{0, 11, 27, 31, 40, 49, 59}
func (i dropReason) String() string {
if i < 0 || i >= dropReason(len(_dropReason_index)-1) {
return "dropReason(" + strconv.FormatInt(int64(i), 10) + ")"
}
return _dropReason_name[_dropReason_index[i]:_dropReason_index[i+1]]
}

8
derp/testdata/example_ss.txt vendored Normal file
View File

@@ -0,0 +1,8 @@
ESTAB 0 0 10.255.1.11:35238 34.210.105.16:https
cubic wscale:7,7 rto:236 rtt:34.14/3.432 ato:40 mss:1448 pmtu:1500 rcvmss:1448 advmss:1448 cwnd:8 ssthresh:6 bytes_sent:38056577 bytes_retrans:2918 bytes_acked:38053660 bytes_received:6973211 segs_out:165090 segs_in:124227 data_segs_out:78018 data_segs_in:71645 send 2.71Mbps lastsnd:1156 lastrcv:1120 lastack:1120 pacing_rate 3.26Mbps delivery_rate 2.35Mbps delivered:78017 app_limited busy:2586132ms retrans:0/6 dsack_dups:4 reordering:5 reord_seen:15 rcv_rtt:126355 rcv_space:65780 rcv_ssthresh:541928 minrtt:26.632
ESTAB 0 80 100.79.58.14:ssh 100.95.73.104:58145
cubic wscale:6,7 rto:224 rtt:23.051/2.03 ato:172 mss:1228 pmtu:1280 rcvmss:1228 advmss:1228 cwnd:10 ssthresh:94 bytes_sent:1591815 bytes_retrans:944 bytes_acked:1590791 bytes_received:158925 segs_out:8070 segs_in:8858 data_segs_out:7452 data_segs_in:3789 send 4.26Mbps lastsnd:4 lastrcv:4 lastack:4 pacing_rate 8.52Mbps delivery_rate 10.9Mbps delivered:7451 app_limited busy:61656ms unacked:2 retrans:0/10 dsack_dups:10 rcv_rtt:174712 rcv_space:65025 rcv_ssthresh:64296 minrtt:16.186
ESTAB 0 374 10.255.1.11:43254 167.172.206.31:https
cubic wscale:7,7 rto:224 rtt:22.55/1.941 ato:40 mss:1448 pmtu:1500 rcvmss:1448 advmss:1448 cwnd:6 ssthresh:4 bytes_sent:14594668 bytes_retrans:173314 bytes_acked:14420981 bytes_received:4207111 segs_out:80566 segs_in:70310 data_segs_out:24317 data_segs_in:20365 send 3.08Mbps lastsnd:4 lastrcv:4 lastack:4 pacing_rate 3.7Mbps delivery_rate 3.05Mbps delivered:24111 app_limited busy:184820ms unacked:2 retrans:0/185 dsack_dups:1 reord_seen:3 rcv_rtt:651.262 rcv_space:226657 rcv_ssthresh:1557136 minrtt:10.18
ESTAB 0 0 10.255.1.11:33036 3.121.18.47:https
cubic wscale:7,7 rto:372 rtt:168.408/2.044 ato:40 mss:1448 pmtu:1500 rcvmss:1448 advmss:1448 cwnd:10 bytes_sent:27500 bytes_acked:27501 bytes_received:1386524 segs_out:10990 segs_in:11037 data_segs_out:303 data_segs_in:3414 send 688kbps lastsnd:125776 lastrcv:9640 lastack:22760 pacing_rate 1.38Mbps delivery_rate 482kbps delivered:304 app_limited busy:43024ms rcv_rtt:3345.12 rcv_space:62431 rcv_ssthresh:760472 minrtt:168.867

View File

@@ -147,9 +147,9 @@ const epLength = 16 + 2 // 16 byte IP address + 2 byte port
func (m *CallMeMaybe) AppendMarshal(b []byte) []byte {
ret, p := appendMsgHeader(b, TypeCallMeMaybe, v0, epLength*len(m.MyNumber))
for _, ipp := range m.MyNumber {
a := ipp.IP.As16()
a := ipp.IP().As16()
copy(p[:], a[:])
binary.BigEndian.PutUint16(p[16:], ipp.Port)
binary.BigEndian.PutUint16(p[16:], ipp.Port())
p = p[epLength:]
}
return ret
@@ -164,10 +164,9 @@ func parseCallMeMaybe(ver uint8, p []byte) (m *CallMeMaybe, err error) {
for len(p) > 0 {
var a [16]byte
copy(a[:], p)
m.MyNumber = append(m.MyNumber, netaddr.IPPort{
IP: netaddr.IPFrom16(a),
Port: binary.BigEndian.Uint16(p[16:18]),
})
m.MyNumber = append(m.MyNumber, netaddr.IPPortFrom(
netaddr.IPFrom16(a),
binary.BigEndian.Uint16(p[16:18])))
p = p[epLength:]
}
return m, nil
@@ -187,9 +186,9 @@ const pongLen = 12 + 16 + 2
func (m *Pong) AppendMarshal(b []byte) []byte {
ret, d := appendMsgHeader(b, TypePong, v0, pongLen)
d = d[copy(d, m.TxID[:]):]
ip16 := m.Src.IP.As16()
ip16 := m.Src.IP().As16()
d = d[copy(d, ip16[:]):]
binary.BigEndian.PutUint16(d, m.Src.Port)
binary.BigEndian.PutUint16(d, m.Src.Port())
return ret
}
@@ -201,10 +200,10 @@ func parsePong(ver uint8, p []byte) (m *Pong, err error) {
copy(m.TxID[:], p)
p = p[12:]
m.Src.IP, _ = netaddr.FromStdIP(net.IP(p[:16]))
srcIP, _ := netaddr.FromStdIP(net.IP(p[:16]))
p = p[16:]
m.Src.Port = binary.BigEndian.Uint16(p)
port := binary.BigEndian.Uint16(p)
m.Src = netaddr.IPPortFrom(srcIP, port)
return m, nil
}

19
disco/disco_fuzzer.go Normal file
View File

@@ -0,0 +1,19 @@
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build gofuzz
// +build gofuzz
package disco
func Fuzz(data []byte) int {
m, _ := Parse(data)
newBytes := m.AppendMarshal(data)
parsedMarshall, _ := Parse(newBytes)
if m != parsedMarshall {
panic("Parsing error")
}
return 1
}

75
go.mod
View File

@@ -3,45 +3,54 @@ module tailscale.com
go 1.16
require (
github.com/alexbrainman/sspi v0.0.0-20180613141037-e580b900e9f5
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 // indirect
github.com/coreos/go-iptables v0.4.5
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 // indirect
github.com/github/certstore v0.1.0
github.com/gliderlabs/ssh v0.2.2
github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect
github.com/aws/aws-sdk-go v1.38.52
github.com/coreos/go-iptables v0.6.0
github.com/creack/pty v1.1.9
github.com/dave/jennifer v1.4.1
github.com/dsnet/golib/jsonfmt v1.0.0
github.com/frankban/quicktest v1.13.0
github.com/gliderlabs/ssh v0.3.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/google/go-cmp v0.5.4
github.com/goreleaser/nfpm v1.1.10
github.com/jsimonetti/rtnetlink v0.0.0-20210212075122-66c871082f2b
github.com/klauspost/compress v1.10.10
github.com/kr/pty v1.1.8
github.com/mdlayher/netlink v1.3.2
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/go-ole/go-ole v1.2.5
github.com/godbus/dbus/v5 v5.0.4
github.com/google/go-cmp v0.5.6
github.com/google/goexpect v0.0.0-20210430020637-ab937bf7fd6f
github.com/google/uuid v1.1.2
github.com/goreleaser/nfpm v1.10.3
github.com/iancoleman/strcase v0.2.0
github.com/insomniacslk/dhcp v0.0.0-20210621130208-1cac67f12b1e
github.com/jsimonetti/rtnetlink v0.0.0-20210525051524-4cc836578190
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
github.com/klauspost/compress v1.12.2
github.com/mdlayher/netlink v1.4.1
github.com/mdlayher/sdnotify v0.0.0-20210228150836-ea3ec207d697
github.com/miekg/dns v1.1.42
github.com/mitchellh/go-ps v1.0.0
github.com/pborman/getopt v1.1.0
github.com/peterbourgon/ff/v2 v2.0.0
github.com/pkg/errors v0.9.1 // indirect
github.com/pkg/sftp v1.13.0
github.com/tailscale/certstore v0.0.0-20210528134328-066c94b793d3
github.com/tailscale/depaware v0.0.0-20201214215404-77d1e9757027
github.com/tailscale/wireguard-go v0.0.0-20210403171604-17614717a9b5
github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05
github.com/tailscale/hujson v0.0.0-20200924210142-dde312d0d6a2
github.com/tcnksm/go-httpstat v0.2.0
github.com/toqueteos/webbrowser v1.2.0
go4.org/mem v0.0.0-20201119185036-c04c5a6ff174
golang.org/x/crypto v0.0.0-20210317152858-513c2a44f670
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e
golang.org/x/net v0.0.0-20210614182718-04defd469f4e
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57
golang.org/x/term v0.0.0-20210317153231-de623e64d2a6
golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba
golang.org/x/tools v0.0.0-20201211185031-d93e913c1a58
golang.zx2c4.com/wireguard/windows v0.1.2-0.20201113162609-9b85be97fdf8
gopkg.in/yaml.v2 v2.2.8 // indirect
honnef.co/go/tools v0.1.0
inet.af/netaddr v0.0.0-20210222205655-a1ec2b7b8c44
inet.af/netstack v0.0.0-20210317161235-a1bf4e56ef22
inet.af/peercred v0.0.0-20210302202138-56e694897155
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22
golang.org/x/term v0.0.0-20210503060354-a79de5458b56
golang.org/x/time v0.0.0-20210611083556-38a9dc6acbc6
golang.org/x/tools v0.1.2
golang.zx2c4.com/wireguard v0.0.0-20210624150102-15b24b6179e0
golang.zx2c4.com/wireguard/windows v0.3.16
honnef.co/go/tools v0.1.4
inet.af/netaddr v0.0.0-20210721214506-ce7a8ad02cc1
inet.af/netstack v0.0.0-20210622165351-29b14ebc044e
inet.af/peercred v0.0.0-20210318190834-4259e17bb763
inet.af/wf v0.0.0-20210516214145-a5343001b756
rsc.io/goversion v1.2.0
)
replace github.com/github/certstore => github.com/cyolosecurity/certstore v0.0.0-20200922073901-ece7f1d353c2

947
go.sum

File diff suppressed because it is too large Load Diff

View File

@@ -11,6 +11,7 @@ import (
"fmt"
"sort"
"sync"
"sync/atomic"
"time"
"github.com/go-multierror/multierror"
@@ -36,6 +37,7 @@ var (
ipnState string
ipnWantRunning bool
anyInterfaceUp = true // until told otherwise
udp4Unbound bool
)
// Subsystem is the name of a subsystem whose health can be monitored.
@@ -46,7 +48,7 @@ const (
// the system, rather than one particular subsystem.
SysOverall = Subsystem("overall")
// SysRouter is the name the wgengine/router subsystem.
// SysRouter is the name of the wgengine/router subsystem.
SysRouter = Subsystem("router")
// SysDNS is the name of the net/dns subsystem.
@@ -213,9 +215,18 @@ func SetAnyInterfaceUp(up bool) {
selfCheckLocked()
}
// SetUDP4Unbound sets whether the udp4 bind failed completely.
func SetUDP4Unbound(unbound bool) {
mu.Lock()
defer mu.Unlock()
udp4Unbound = unbound
selfCheckLocked()
}
func timerSelfCheck() {
mu.Lock()
defer mu.Unlock()
checkReceiveFuncs()
selfCheckLocked()
if timer != nil {
timer.Reset(time.Minute)
@@ -255,6 +266,9 @@ func overallErrorLocked() error {
if d := now.Sub(derpRegionLastFrame[rid]).Round(time.Second); d > tooIdle {
return fmt.Errorf("haven't heard from home DERP region %v in %v", rid, d)
}
if udp4Unbound {
return errors.New("no udp4 bind")
}
// TODO: use
_ = inMapPollSince
@@ -263,6 +277,11 @@ func overallErrorLocked() error {
_ = lastMapRequestHeard
var errs []error
for _, recv := range receiveFuncs {
if recv.missing {
errs = append(errs, fmt.Errorf("%s is not running", recv.name))
}
}
for sys, err := range sysErr {
if err == nil || sys == SysOverall {
continue
@@ -275,3 +294,58 @@ func overallErrorLocked() error {
})
return multierror.New(errs)
}
var (
ReceiveIPv4 = ReceiveFuncStats{name: "ReceiveIPv4"}
ReceiveIPv6 = ReceiveFuncStats{name: "ReceiveIPv6"}
ReceiveDERP = ReceiveFuncStats{name: "ReceiveDERP"}
receiveFuncs = []*ReceiveFuncStats{&ReceiveIPv4, &ReceiveIPv6, &ReceiveDERP}
)
// ReceiveFuncStats tracks the calls made to a wireguard-go receive func.
type ReceiveFuncStats struct {
// name is the name of the receive func.
name string
// numCalls is the number of times the receive func has ever been called.
// It is required because it is possible for a receive func's wireguard-go goroutine
// to be active even though the receive func isn't.
// The wireguard-go goroutine alternates between calling the receive func and
// processing what the func returned.
numCalls uint64 // accessed atomically
// prevNumCalls is the value of numCalls last time the health check examined it.
prevNumCalls uint64
// inCall indicates whether the receive func is currently running.
inCall uint32 // bool, accessed atomically
// missing indicates whether the receive func is not running.
missing bool
}
func (s *ReceiveFuncStats) Enter() {
atomic.AddUint64(&s.numCalls, 1)
atomic.StoreUint32(&s.inCall, 1)
}
func (s *ReceiveFuncStats) Exit() {
atomic.StoreUint32(&s.inCall, 0)
}
func checkReceiveFuncs() {
for _, recv := range receiveFuncs {
recv.missing = false
prev := recv.prevNumCalls
numCalls := atomic.LoadUint64(&recv.numCalls)
recv.prevNumCalls = numCalls
if numCalls > prev {
// OK: the function has gotten called since last we checked
continue
}
if atomic.LoadUint32(&recv.inCall) == 1 {
// OK: the function is active, probably blocked due to inactivity
continue
}
// Not OK: The function is not active, and not accumulating new calls.
// It is probably MIA.
recv.missing = true
}
}

128
hostinfo/hostinfo.go Normal file
View File

@@ -0,0 +1,128 @@
// 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 hostinfo answers questions about the host environment that Tailscale is
// running on.
//
// TODO(bradfitz): move more of control/controlclient/hostinfo_* into this package.
package hostinfo
import (
"io"
"os"
"runtime"
"sync/atomic"
"go4.org/mem"
"tailscale.com/util/lineread"
)
// EnvType represents a known environment type.
// The empty string, the default, means unknown.
type EnvType string
const (
KNative = EnvType("kn")
AWSLambda = EnvType("lm")
Heroku = EnvType("hr")
AzureAppService = EnvType("az")
AWSFargate = EnvType("fg")
)
var envType atomic.Value // of EnvType
func GetEnvType() EnvType {
if e, ok := envType.Load().(EnvType); ok {
return e
}
e := getEnvType()
envType.Store(e)
return e
}
func getEnvType() EnvType {
if inKnative() {
return KNative
}
if inAWSLambda() {
return AWSLambda
}
if inHerokuDyno() {
return Heroku
}
if inAzureAppService() {
return AzureAppService
}
if inAWSFargate() {
return AWSFargate
}
return ""
}
// InContainer reports whether we're running in a container.
func InContainer() bool {
if runtime.GOOS != "linux" {
return false
}
var ret bool
lineread.File("/proc/1/cgroup", func(line []byte) error {
if mem.Contains(mem.B(line), mem.S("/docker/")) ||
mem.Contains(mem.B(line), mem.S("/lxc/")) {
ret = true
return io.EOF // arbitrary non-nil error to stop loop
}
return nil
})
lineread.File("/proc/mounts", func(line []byte) error {
if mem.Contains(mem.B(line), mem.S("fuse.lxcfs")) {
ret = true
return io.EOF
}
return nil
})
return ret
}
func inKnative() bool {
// https://cloud.google.com/run/docs/reference/container-contract#env-vars
if os.Getenv("K_REVISION") != "" && os.Getenv("K_CONFIGURATION") != "" &&
os.Getenv("K_SERVICE") != "" && os.Getenv("PORT") != "" {
return true
}
return false
}
func inAWSLambda() bool {
// https://docs.aws.amazon.com/lambda/latest/dg/configuration-envvars.html
if os.Getenv("AWS_LAMBDA_FUNCTION_NAME") != "" &&
os.Getenv("AWS_LAMBDA_FUNCTION_VERSION") != "" &&
os.Getenv("AWS_LAMBDA_INITIALIZATION_TYPE") != "" &&
os.Getenv("AWS_LAMBDA_RUNTIME_API") != "" {
return true
}
return false
}
func inHerokuDyno() bool {
// https://devcenter.heroku.com/articles/dynos#local-environment-variables
if os.Getenv("PORT") != "" && os.Getenv("DYNO") != "" {
return true
}
return false
}
func inAzureAppService() bool {
if os.Getenv("APPSVC_RUN_ZIP") != "" && os.Getenv("WEBSITE_STACK") != "" &&
os.Getenv("WEBSITE_AUTH_AUTO_AAD") != "" {
return true
}
return false
}
func inAWSFargate() bool {
if os.Getenv("AWS_EXECUTION_ENV") == "AWS_ECS_FARGATE" {
return true
}
return false
}

View File

@@ -1,103 +0,0 @@
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package deepprint walks a Go value recursively, in a predictable
// order, without looping, and prints each value out to a given
// Writer, which is assumed to be a hash.Hash, as this package doesn't
// format things nicely.
//
// This is intended as a lighter version of go-spew, etc. We don't need its
// features when our writer is just a hash.
package deepprint
import (
"crypto/sha256"
"fmt"
"io"
"reflect"
)
func Hash(v ...interface{}) string {
h := sha256.New()
Print(h, v)
return fmt.Sprintf("%x", h.Sum(nil))
}
// UpdateHash sets last to the hash of v and reports whether its value changed.
func UpdateHash(last *string, v ...interface{}) (changed bool) {
sig := Hash(v)
if *last != sig {
*last = sig
return true
}
return false
}
func Print(w io.Writer, v ...interface{}) {
print(w, reflect.ValueOf(v), make(map[uintptr]bool))
}
func print(w io.Writer, v reflect.Value, visited map[uintptr]bool) {
if !v.IsValid() {
return
}
switch v.Kind() {
default:
panic(fmt.Sprintf("unhandled kind %v for type %v", v.Kind(), v.Type()))
case reflect.Ptr:
ptr := v.Pointer()
if visited[ptr] {
return
}
visited[ptr] = true
print(w, v.Elem(), visited)
return
case reflect.Struct:
fmt.Fprintf(w, "struct{\n")
t := v.Type()
for i, n := 0, v.NumField(); i < n; i++ {
sf := t.Field(i)
fmt.Fprintf(w, "%s: ", sf.Name)
print(w, v.Field(i), visited)
fmt.Fprintf(w, "\n")
}
case reflect.Slice, reflect.Array:
if v.Type().Elem().Kind() == reflect.Uint8 && v.CanInterface() {
fmt.Fprintf(w, "%q", v.Interface())
return
}
fmt.Fprintf(w, "[%d]{\n", v.Len())
for i, ln := 0, v.Len(); i < ln; i++ {
fmt.Fprintf(w, " [%d]: ", i)
print(w, v.Index(i), visited)
fmt.Fprintf(w, "\n")
}
fmt.Fprintf(w, "}\n")
case reflect.Interface:
print(w, v.Elem(), visited)
case reflect.Map:
sm := newSortedMap(v)
fmt.Fprintf(w, "map[%d]{\n", len(sm.Key))
for i, k := range sm.Key {
print(w, k, visited)
fmt.Fprintf(w, ": ")
print(w, sm.Value[i], visited)
fmt.Fprintf(w, "\n")
}
fmt.Fprintf(w, "}\n")
case reflect.String:
fmt.Fprintf(w, "%s", v.String())
case reflect.Bool:
fmt.Fprintf(w, "%v", v.Bool())
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
fmt.Fprintf(w, "%v", v.Int())
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
fmt.Fprintf(w, "%v", v.Uint())
case reflect.Float32, reflect.Float64:
fmt.Fprintf(w, "%v", v.Float())
case reflect.Complex64, reflect.Complex128:
fmt.Fprintf(w, "%v", v.Complex())
}
}

View File

@@ -1,64 +0,0 @@
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package deepprint
import (
"bytes"
"testing"
"inet.af/netaddr"
"tailscale.com/wgengine/router"
"tailscale.com/wgengine/wgcfg"
)
func TestDeepPrint(t *testing.T) {
// v contains the types of values we care about for our current callers.
// Mostly we're just testing that we don't panic on handled types.
v := getVal()
var buf bytes.Buffer
Print(&buf, v)
t.Logf("Got: %s", buf.Bytes())
hash1 := Hash(v)
t.Logf("hash: %v", hash1)
for i := 0; i < 20; i++ {
hash2 := Hash(getVal())
if hash1 != hash2 {
t.Error("second hash didn't match")
}
}
}
func getVal() []interface{} {
return []interface{}{
&wgcfg.Config{
Name: "foo",
Addresses: []netaddr.IPPrefix{{Bits: 5, IP: netaddr.IPFrom16([16]byte{3: 3})}},
Peers: []wgcfg.Peer{
{
Endpoints: "foo:5",
},
},
},
&router.Config{
Routes: []netaddr.IPPrefix{
netaddr.MustParseIPPrefix("1.2.3.0/24"),
netaddr.MustParseIPPrefix("1234::/64"),
},
},
map[string]string{
"key1": "val1",
"key2": "val2",
"key3": "val3",
"key4": "val4",
"key5": "val5",
"key6": "val6",
"key7": "val7",
"key8": "val8",
"key9": "val9",
},
}
}

View File

@@ -1,224 +0,0 @@
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// and
// Copyright 2018 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.
// This is a slightly modified fork of Go's src/internal/fmtsort/sort.go
package deepprint
import (
"reflect"
"sort"
)
// Note: Throughout this package we avoid calling reflect.Value.Interface as
// it is not always legal to do so and it's easier to avoid the issue than to face it.
// sortedMap represents a map's keys and values. The keys and values are
// aligned in index order: Value[i] is the value in the map corresponding to Key[i].
type sortedMap struct {
Key []reflect.Value
Value []reflect.Value
}
func (o *sortedMap) Len() int { return len(o.Key) }
func (o *sortedMap) Less(i, j int) bool { return compare(o.Key[i], o.Key[j]) < 0 }
func (o *sortedMap) Swap(i, j int) {
o.Key[i], o.Key[j] = o.Key[j], o.Key[i]
o.Value[i], o.Value[j] = o.Value[j], o.Value[i]
}
// Sort accepts a map and returns a sortedMap that has the same keys and
// values but in a stable sorted order according to the keys, modulo issues
// raised by unorderable key values such as NaNs.
//
// The ordering rules are more general than with Go's < operator:
//
// - when applicable, nil compares low
// - ints, floats, and strings order by <
// - NaN compares less than non-NaN floats
// - bool compares false before true
// - complex compares real, then imag
// - pointers compare by machine address
// - channel values compare by machine address
// - structs compare each field in turn
// - arrays compare each element in turn.
// Otherwise identical arrays compare by length.
// - interface values compare first by reflect.Type describing the concrete type
// and then by concrete value as described in the previous rules.
//
func newSortedMap(mapValue reflect.Value) *sortedMap {
if mapValue.Type().Kind() != reflect.Map {
return nil
}
// Note: this code is arranged to not panic even in the presence
// of a concurrent map update. The runtime is responsible for
// yelling loudly if that happens. See issue 33275.
n := mapValue.Len()
key := make([]reflect.Value, 0, n)
value := make([]reflect.Value, 0, n)
iter := mapValue.MapRange()
for iter.Next() {
key = append(key, iter.Key())
value = append(value, iter.Value())
}
sorted := &sortedMap{
Key: key,
Value: value,
}
sort.Stable(sorted)
return sorted
}
// compare compares two values of the same type. It returns -1, 0, 1
// according to whether a > b (1), a == b (0), or a < b (-1).
// If the types differ, it returns -1.
// See the comment on Sort for the comparison rules.
func compare(aVal, bVal reflect.Value) int {
aType, bType := aVal.Type(), bVal.Type()
if aType != bType {
return -1 // No good answer possible, but don't return 0: they're not equal.
}
switch aVal.Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
a, b := aVal.Int(), bVal.Int()
switch {
case a < b:
return -1
case a > b:
return 1
default:
return 0
}
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
a, b := aVal.Uint(), bVal.Uint()
switch {
case a < b:
return -1
case a > b:
return 1
default:
return 0
}
case reflect.String:
a, b := aVal.String(), bVal.String()
switch {
case a < b:
return -1
case a > b:
return 1
default:
return 0
}
case reflect.Float32, reflect.Float64:
return floatCompare(aVal.Float(), bVal.Float())
case reflect.Complex64, reflect.Complex128:
a, b := aVal.Complex(), bVal.Complex()
if c := floatCompare(real(a), real(b)); c != 0 {
return c
}
return floatCompare(imag(a), imag(b))
case reflect.Bool:
a, b := aVal.Bool(), bVal.Bool()
switch {
case a == b:
return 0
case a:
return 1
default:
return -1
}
case reflect.Ptr:
a, b := aVal.Pointer(), bVal.Pointer()
switch {
case a < b:
return -1
case a > b:
return 1
default:
return 0
}
case reflect.Chan:
if c, ok := nilCompare(aVal, bVal); ok {
return c
}
ap, bp := aVal.Pointer(), bVal.Pointer()
switch {
case ap < bp:
return -1
case ap > bp:
return 1
default:
return 0
}
case reflect.Struct:
for i := 0; i < aVal.NumField(); i++ {
if c := compare(aVal.Field(i), bVal.Field(i)); c != 0 {
return c
}
}
return 0
case reflect.Array:
for i := 0; i < aVal.Len(); i++ {
if c := compare(aVal.Index(i), bVal.Index(i)); c != 0 {
return c
}
}
return 0
case reflect.Interface:
if c, ok := nilCompare(aVal, bVal); ok {
return c
}
c := compare(reflect.ValueOf(aVal.Elem().Type()), reflect.ValueOf(bVal.Elem().Type()))
if c != 0 {
return c
}
return compare(aVal.Elem(), bVal.Elem())
default:
// Certain types cannot appear as keys (maps, funcs, slices), but be explicit.
panic("bad type in compare: " + aType.String())
}
}
// nilCompare checks whether either value is nil. If not, the boolean is false.
// If either value is nil, the boolean is true and the integer is the comparison
// value. The comparison is defined to be 0 if both are nil, otherwise the one
// nil value compares low. Both arguments must represent a chan, func,
// interface, map, pointer, or slice.
func nilCompare(aVal, bVal reflect.Value) (int, bool) {
if aVal.IsNil() {
if bVal.IsNil() {
return 0, true
}
return -1, true
}
if bVal.IsNil() {
return 1, true
}
return 0, false
}
// floatCompare compares two floating-point values. NaNs compare low.
func floatCompare(a, b float64) int {
switch {
case isNaN(a):
return -1 // No good answer if b is a NaN so don't bother checking.
case isNaN(b):
return 1
case a < b:
return -1
case a > b:
return 1
}
return 0
}
func isNaN(a float64) bool {
return a != a
}

View File

@@ -5,7 +5,8 @@
package ipn
import (
"net/http"
"fmt"
"strings"
"time"
"tailscale.com/ipn/ipnstate"
@@ -56,20 +57,34 @@ type EngineStatus struct {
// that they have not changed.
// They are JSON-encoded on the wire, despite the lack of struct tags.
type Notify struct {
_ structs.Incomparable
Version string // version number of IPN backend
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
NetMap *netmap.NetworkMap // new netmap received
Engine *EngineStatus // wireguard engine stats
BrowseToURL *string // UI should open a browser right now
BackendLogID *string // public logtail id used by backend
PingResult *ipnstate.PingResult
_ structs.Incomparable
Version string // version number of IPN backend
// ErrMessage, if non-nil, contains a critical error message.
// For State InUseOtherUser, ErrMessage is not critical and just contains the details.
ErrMessage *string
LoginFinished *empty.Message // non-nil when/if the login process succeeded
State *State // if non-nil, the new or current IPN state
Prefs *Prefs // if non-nil, the new or current preferences
NetMap *netmap.NetworkMap // if non-nil, the new or current netmap
Engine *EngineStatus // if non-nil, the new or urrent wireguard stats
BrowseToURL *string // if non-nil, UI should open a browser right now
BackendLogID *string // if non-nil, the public logtail ID used by backend
PingResult *ipnstate.PingResult // if non-nil, a ping response arrived
// FilesWaiting if non-nil means that files are buffered in
// the Tailscale daemon and ready for local transfer to the
// user's preferred storage location.
FilesWaiting *empty.Message `json:",omitempty"`
// IncomingFiles, if non-nil, specifies which files are in the
// process of being received. A nil IncomingFiles means this
// Notify should not update the state of file transfers. A non-nil
// but empty IncomingFiles means that no files are in the middle
// of being transferred.
IncomingFiles []PartialFile `json:",omitempty"`
// LocalTCPPort, if non-nil, informs the UI frontend which
// (non-zero) localhost TCP port it's listening on.
// This is currently only used by Tailscale when run in the
@@ -79,6 +94,67 @@ type Notify struct {
// type is mirrored in xcode/Shared/IPN.swift
}
func (n Notify) String() string {
var sb strings.Builder
sb.WriteString("Notify{")
if n.ErrMessage != nil {
fmt.Fprintf(&sb, "err=%q ", *n.ErrMessage)
}
if n.LoginFinished != nil {
sb.WriteString("LoginFinished ")
}
if n.State != nil {
fmt.Fprintf(&sb, "state=%v ", *n.State)
}
if n.Prefs != nil {
fmt.Fprintf(&sb, "%v ", n.Prefs.Pretty())
}
if n.NetMap != nil {
sb.WriteString("NetMap{...} ")
}
if n.Engine != nil {
fmt.Fprintf(&sb, "wg=%v ", *n.Engine)
}
if n.BrowseToURL != nil {
sb.WriteString("URL=<...> ")
}
if n.BackendLogID != nil {
sb.WriteString("BackendLogID ")
}
if n.PingResult != nil {
fmt.Fprintf(&sb, "ping=%v ", *n.PingResult)
}
if n.FilesWaiting != nil {
sb.WriteString("FilesWaiting ")
}
if len(n.IncomingFiles) != 0 {
sb.WriteString("IncomingFiles ")
}
if n.LocalTCPPort != nil {
fmt.Fprintf(&sb, "tcpport=%v ", n.LocalTCPPort)
}
s := sb.String()
return s[0:len(s)-1] + "}"
}
// PartialFile represents an in-progress file transfer.
type PartialFile struct {
Name string // e.g. "foo.jpg"
Started time.Time // time transfer started
DeclaredSize int64 // or -1 if unknown
Received int64 // bytes copied thus far
// PartialPath is set non-empty in "direct" file mode to the
// in-progress '*.partial' file's path when the peerapi isn't
// being used; see LocalBackend.SetDirectFileRoot.
PartialPath string `json:",omitempty"`
// Done is set in "direct" mode when the partial file has been
// closed and is ready for the caller to rename away the
// ".partial" suffix.
Done bool `json:",omitempty"`
}
// StateKey is an opaque identifier for a set of LocalBackend state
// (preferences, private keys, etc.).
//
@@ -109,24 +185,33 @@ type Options struct {
// state and use/update that.
// - StateKey!="" && Prefs!=nil: like the previous case, but do
// an initial overwrite of backend state with Prefs.
//
// NOTE(apenwarr): The above means that this Prefs field does not do
// what you probably think it does. It will overwrite your encryption
// keys. Do not use unless you know what you're doing.
StateKey StateKey
Prefs *Prefs
// UpdatePrefs, if provided, overrides Options.Prefs *and* the Prefs
// already stored in the backend state, *except* for the Persist
// Persist member. If you just want to provide prefs, this is
// probably what you want.
//
// UpdatePrefs.Persist is always ignored. Prefs.Persist will still
// be used even if UpdatePrefs is provided. Other than Persist,
// UpdatePrefs takes precedence over Prefs.
//
// This is intended as a purely temporary workaround for the
// currently unexpected behaviour of Options.Prefs.
//
// TODO(apenwarr): Remove this, or rename Prefs to something else
// and rename this to Prefs. Or, move Prefs.Persist elsewhere
// entirely (as it always should have been), and then we wouldn't
// need two separate fields at all. Or, move the fancy state
// migration stuff out of Start().
UpdatePrefs *Prefs
// AuthKey is an optional node auth key used to authorize a
// new node key without user interaction.
AuthKey string
// LegacyConfigPath optionally specifies the old-style relaynode
// relay.conf location. If both LegacyConfigPath and StateKey are
// specified and the requested state doesn't exist in the backend
// store, the backend migrates the config from LegacyConfigPath.
//
// TODO(danderson): remove some time after the transition to
// tailscaled is done.
LegacyConfigPath string
// Notify is called when backend events happen.
Notify func(Notify) `json:"-"`
// HTTPTestClient is an optional HTTP client to pass to controlclient
// (for tests only).
HTTPTestClient *http.Client
}
// Backend is the interface between Tailscale frontends
@@ -135,6 +220,9 @@ type Options struct {
// (It has nothing to do with the interface between the backends
// and the cloud control plane.)
type Backend interface {
// SetNotifyCallback sets the callback to be called on updates
// from the backend to the client.
SetNotifyCallback(func(Notify))
// Start starts or restarts the backend, typically when a
// frontend client connects.
Start(Options) error
@@ -151,8 +239,6 @@ type Backend interface {
// WantRunning. This may cause the wireguard engine to
// reconfigure or stop.
SetPrefs(*Prefs)
// EditPrefs is like SetPrefs but only sets the specified fields.
EditPrefs(*MaskedPrefs)
// RequestEngineStatus polls for an update from the wireguard
// engine. Only needed if you want to display byte
// counts. Connection events are emitted automatically without

View File

@@ -5,7 +5,6 @@
package ipn
import (
"log"
"time"
"tailscale.com/ipn/ipnstate"
@@ -20,19 +19,29 @@ type FakeBackend struct {
}
func (b *FakeBackend) Start(opts Options) error {
b.serverURL = opts.Prefs.ControlURL
if opts.Notify == nil {
log.Fatalf("FakeBackend.Start: opts.Notify is nil\n")
b.serverURL = opts.Prefs.ControlURLOrDefault()
if b.notify == nil {
panic("FakeBackend.Start: SetNotifyCallback not called")
}
b.notify = opts.Notify
b.notify(Notify{Prefs: opts.Prefs})
nl := NeedsLogin
b.notify(Notify{State: &nl})
if b.notify != nil {
b.notify(Notify{Prefs: opts.Prefs})
b.notify(Notify{State: &nl})
}
return nil
}
func (b *FakeBackend) SetNotifyCallback(notify func(Notify)) {
if notify == nil {
panic("FakeBackend.SetNotifyCallback: notify is nil")
}
b.notify = notify
}
func (b *FakeBackend) newState(s State) {
b.notify(Notify{State: &s})
if b.notify != nil {
b.notify(Notify{State: &s})
}
if s == Running {
b.live = true
} else {
@@ -42,7 +51,9 @@ func (b *FakeBackend) newState(s State) {
func (b *FakeBackend) StartLoginInteractive() {
u := b.serverURL + "/this/is/fake"
b.notify(Notify{BrowseToURL: &u})
if b.notify != nil {
b.notify(Notify{BrowseToURL: &u})
}
b.login()
}
@@ -54,10 +65,14 @@ func (b *FakeBackend) login() {
b.newState(NeedsMachineAuth)
b.newState(Stopped)
// TODO(apenwarr): Fill in a more interesting netmap here.
b.notify(Notify{NetMap: &netmap.NetworkMap{}})
if b.notify != nil {
b.notify(Notify{NetMap: &netmap.NetworkMap{}})
}
b.newState(Starting)
// TODO(apenwarr): Fill in a more interesting status.
b.notify(Notify{Engine: &EngineStatus{}})
if b.notify != nil {
b.notify(Notify{Engine: &EngineStatus{}})
}
b.newState(Running)
}
@@ -70,7 +85,9 @@ func (b *FakeBackend) SetPrefs(new *Prefs) {
panic("FakeBackend.SetPrefs got nil prefs")
}
b.notify(Notify{Prefs: new.Clone()})
if b.notify != nil {
b.notify(Notify{Prefs: new.Clone()})
}
if new.WantRunning && !b.live {
b.newState(Starting)
b.newState(Running)
@@ -79,21 +96,20 @@ func (b *FakeBackend) SetPrefs(new *Prefs) {
}
}
func (b *FakeBackend) EditPrefs(mp *MaskedPrefs) {
// This fake implementation only cares about this one pref.
if mp.WantRunningSet {
b.SetPrefs(&mp.Prefs)
func (b *FakeBackend) RequestEngineStatus() {
if b.notify != nil {
b.notify(Notify{Engine: &EngineStatus{}})
}
}
func (b *FakeBackend) RequestEngineStatus() {
b.notify(Notify{Engine: &EngineStatus{}})
}
func (b *FakeBackend) FakeExpireAfter(x time.Duration) {
b.notify(Notify{NetMap: &netmap.NetworkMap{}})
if b.notify != nil {
b.notify(Notify{NetMap: &netmap.NetworkMap{}})
}
}
func (b *FakeBackend) Ping(ip string, useTSMP bool) {
b.notify(Notify{PingResult: &ipnstate.PingResult{}})
if b.notify != nil {
b.notify(Notify{PingResult: &ipnstate.PingResult{}})
}
}

View File

@@ -15,25 +15,26 @@ import (
)
type Handle struct {
frontendLogID string
b Backend
xnotify func(Notify)
logf logger.Logf
b Backend
logf logger.Logf
// Mutex protects everything below
mu sync.Mutex
xnotify func(Notify)
frontendLogID string
netmapCache *netmap.NetworkMap
engineStatusCache EngineStatus
stateCache State
prefsCache *Prefs
}
func NewHandle(b Backend, logf logger.Logf, opts Options) (*Handle, error) {
func NewHandle(b Backend, logf logger.Logf, notify func(Notify), opts Options) (*Handle, error) {
h := &Handle{
b: b,
logf: logf,
}
h.SetNotifyCallback(notify)
err := h.Start(opts)
if err != nil {
return nil, err
@@ -42,18 +43,25 @@ func NewHandle(b Backend, logf logger.Logf, opts Options) (*Handle, error) {
return h, nil
}
func (h *Handle) SetNotifyCallback(notify func(Notify)) {
h.mu.Lock()
h.xnotify = notify
h.mu.Unlock()
h.b.SetNotifyCallback(h.notify)
}
func (h *Handle) Start(opts Options) error {
h.mu.Lock()
h.frontendLogID = opts.FrontendLogID
h.xnotify = opts.Notify
h.netmapCache = nil
h.engineStatusCache = EngineStatus{}
h.stateCache = NoState
if opts.Prefs != nil {
h.prefsCache = opts.Prefs.Clone()
}
xopts := opts
xopts.Notify = h.notify
return h.b.Start(xopts)
h.mu.Unlock()
return h.b.Start(opts)
}
func (h *Handle) Reset() {
@@ -148,7 +156,7 @@ func (h *Handle) Expiry() time.Time {
}
func (h *Handle) AdminPageURL() string {
return h.prefsCache.ControlURL + "/admin/machines"
return h.prefsCache.AdminPageURL()
}
func (h *Handle) StartLoginInteractive() {

File diff suppressed because it is too large Load Diff

View File

@@ -5,14 +5,20 @@
package ipnlocal
import (
"fmt"
"net/http"
"reflect"
"testing"
"time"
"inet.af/netaddr"
"tailscale.com/ipn"
"tailscale.com/net/interfaces"
"tailscale.com/net/tsaddr"
"tailscale.com/tailcfg"
"tailscale.com/types/logger"
"tailscale.com/types/netmap"
"tailscale.com/wgengine"
"tailscale.com/wgengine/wgcfg"
)
@@ -165,7 +171,7 @@ func TestShrinkDefaultRoute(t *testing.T) {
out: []string{
"fe80::1",
"ff00::1",
tsaddr.TailscaleULARange().IP.String(),
tsaddr.TailscaleULARange().IP().String(),
},
localIPFn: func(ip netaddr.IP) bool { return !inRemove(ip) && ip.Is6() },
},
@@ -291,3 +297,199 @@ func TestPeerRoutes(t *testing.T) {
}
}
func TestPeerAPIBase(t *testing.T) {
tests := []struct {
name string
nm *netmap.NetworkMap
peer *tailcfg.Node
want string
}{
{
name: "nil_netmap",
peer: new(tailcfg.Node),
want: "",
},
{
name: "nil_peer",
nm: new(netmap.NetworkMap),
want: "",
},
{
name: "self_only_4_them_both",
nm: &netmap.NetworkMap{
Addresses: []netaddr.IPPrefix{
netaddr.MustParseIPPrefix("100.64.1.1/32"),
},
},
peer: &tailcfg.Node{
Addresses: []netaddr.IPPrefix{
netaddr.MustParseIPPrefix("100.64.1.2/32"),
netaddr.MustParseIPPrefix("fe70::2/128"),
},
Hostinfo: tailcfg.Hostinfo{
Services: []tailcfg.Service{
{Proto: "peerapi4", Port: 444},
{Proto: "peerapi6", Port: 666},
},
},
},
want: "http://100.64.1.2:444",
},
{
name: "self_only_6_them_both",
nm: &netmap.NetworkMap{
Addresses: []netaddr.IPPrefix{
netaddr.MustParseIPPrefix("fe70::1/128"),
},
},
peer: &tailcfg.Node{
Addresses: []netaddr.IPPrefix{
netaddr.MustParseIPPrefix("100.64.1.2/32"),
netaddr.MustParseIPPrefix("fe70::2/128"),
},
Hostinfo: tailcfg.Hostinfo{
Services: []tailcfg.Service{
{Proto: "peerapi4", Port: 444},
{Proto: "peerapi6", Port: 666},
},
},
},
want: "http://[fe70::2]:666",
},
{
name: "self_both_them_only_4",
nm: &netmap.NetworkMap{
Addresses: []netaddr.IPPrefix{
netaddr.MustParseIPPrefix("100.64.1.1/32"),
netaddr.MustParseIPPrefix("fe70::1/128"),
},
},
peer: &tailcfg.Node{
Addresses: []netaddr.IPPrefix{
netaddr.MustParseIPPrefix("100.64.1.2/32"),
netaddr.MustParseIPPrefix("fe70::2/128"),
},
Hostinfo: tailcfg.Hostinfo{
Services: []tailcfg.Service{
{Proto: "peerapi4", Port: 444},
},
},
},
want: "http://100.64.1.2:444",
},
{
name: "self_both_them_only_6",
nm: &netmap.NetworkMap{
Addresses: []netaddr.IPPrefix{
netaddr.MustParseIPPrefix("100.64.1.1/32"),
netaddr.MustParseIPPrefix("fe70::1/128"),
},
},
peer: &tailcfg.Node{
Addresses: []netaddr.IPPrefix{
netaddr.MustParseIPPrefix("100.64.1.2/32"),
netaddr.MustParseIPPrefix("fe70::2/128"),
},
Hostinfo: tailcfg.Hostinfo{
Services: []tailcfg.Service{
{Proto: "peerapi6", Port: 666},
},
},
},
want: "http://[fe70::2]:666",
},
{
name: "self_both_them_no_peerapi_service",
nm: &netmap.NetworkMap{
Addresses: []netaddr.IPPrefix{
netaddr.MustParseIPPrefix("100.64.1.1/32"),
netaddr.MustParseIPPrefix("fe70::1/128"),
},
},
peer: &tailcfg.Node{
Addresses: []netaddr.IPPrefix{
netaddr.MustParseIPPrefix("100.64.1.2/32"),
netaddr.MustParseIPPrefix("fe70::2/128"),
},
},
want: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := peerAPIBase(tt.nm, tt.peer)
if got != tt.want {
t.Errorf("got %q; want %q", got, tt.want)
}
})
}
}
type panicOnUseTransport struct{}
func (panicOnUseTransport) RoundTrip(*http.Request) (*http.Response, error) {
panic("unexpected HTTP request")
}
// Issue 1573: don't generate a machine key if we don't want to be running.
func TestLazyMachineKeyGeneration(t *testing.T) {
defer func(old bool) { panicOnMachineKeyGeneration = old }(panicOnMachineKeyGeneration)
panicOnMachineKeyGeneration = true
var logf logger.Logf = logger.Discard
store := new(ipn.MemoryStore)
eng, err := wgengine.NewFakeUserspaceEngine(logf, 0)
if err != nil {
t.Fatalf("NewFakeUserspaceEngine: %v", err)
}
lb, err := NewLocalBackend(logf, "logid", store, eng)
if err != nil {
t.Fatalf("NewLocalBackend: %v", err)
}
lb.SetHTTPTestClient(&http.Client{
Transport: panicOnUseTransport{}, // validate we don't send HTTP requests
})
if err := lb.Start(ipn.Options{
StateKey: ipn.GlobalDaemonStateKey,
}); err != nil {
t.Fatalf("Start: %v", err)
}
// Give the controlclient package goroutines (if they're
// accidentally started) extra time to schedule and run (and thus
// hit panicOnUseTransport).
time.Sleep(500 * time.Millisecond)
}
func TestFileTargets(t *testing.T) {
b := new(LocalBackend)
_, err := b.FileTargets()
if got, want := fmt.Sprint(err), "not connected"; got != want {
t.Errorf("before connect: got %q; want %q", got, want)
}
b.netMap = new(netmap.NetworkMap)
_, err = b.FileTargets()
if got, want := fmt.Sprint(err), "not connected"; got != want {
t.Errorf("non-running netmap: got %q; want %q", got, want)
}
b.state = ipn.Running
_, err = b.FileTargets()
if got, want := fmt.Sprint(err), "file sharing not enabled by Tailscale admin"; got != want {
t.Errorf("without cap: got %q; want %q", got, want)
}
b.capFileSharing = true
got, err := b.FileTargets()
if err != nil {
t.Fatal(err)
}
if len(got) != 0 {
t.Fatalf("unexpected %d peers", len(got))
}
// (other cases handled by TestPeerAPIBase above)
}

View File

@@ -15,6 +15,7 @@ import (
"tailscale.com/tailcfg"
"tailscale.com/tstest"
"tailscale.com/types/key"
"tailscale.com/types/logger"
"tailscale.com/types/persist"
"tailscale.com/wgengine"
)
@@ -30,6 +31,12 @@ func TestLocalLogLines(t *testing.T) {
})
defer logListen.Close()
// Put a rate-limiter with a burst of 0 between the components below.
// This instructs the rate-limiter to eliminate all logging that
// isn't explicitly exempt from rate-limiting.
// This lets the logListen tracker verify that the rate-limiter allows these key lines.
logf := logger.RateLimitedFnWithClock(logListen.Logf, 5*time.Second, 0, 10, time.Now)
logid := func(hex byte) logtail.PublicID {
var ret logtail.PublicID
for i := 0; i < len(ret); i++ {
@@ -41,12 +48,12 @@ func TestLocalLogLines(t *testing.T) {
// set up a LocalBackend, super bare bones. No functional data.
store := &ipn.MemoryStore{}
e, err := wgengine.NewFakeUserspaceEngine(logListen.Logf, 0)
e, err := wgengine.NewFakeUserspaceEngine(logf, 0)
if err != nil {
t.Fatal(err)
}
lb, err := NewLocalBackend(logListen.Logf, idA.String(), store, e)
lb, err := NewLocalBackend(logf, idA.String(), store, e)
if err != nil {
t.Fatal(err)
}
@@ -61,6 +68,7 @@ func TestLocalLogLines(t *testing.T) {
testWantRemain := func(wantRemain ...string) func(t *testing.T) {
return func(t *testing.T) {
if remain := logListen.Check(); !reflect.DeepEqual(remain, wantRemain) {
t.Helper()
t.Errorf("remain %q, want %q", remain, wantRemain)
}
}
@@ -75,18 +83,30 @@ func TestLocalLogLines(t *testing.T) {
t.Run("after_prefs", testWantRemain("[v1] peer keys: %s", "[v1] v%v peers: %v"))
// log peers, peer keys
status := &wgengine.Status{
lb.mu.Lock()
lb.parseWgStatusLocked(&wgengine.Status{
Peers: []ipnstate.PeerStatusLite{{
TxBytes: 10,
RxBytes: 10,
LastHandshake: time.Now(),
NodeKey: tailcfg.NodeKey(key.NewPrivate()),
}},
LocalAddrs: []string{"idk an address"},
}
lb.mu.Lock()
lb.parseWgStatusLocked(status)
})
lb.mu.Unlock()
t.Run("after_peers", testWantRemain())
// Log it again with different stats to ensure it's not dup-suppressed.
logListen.Reset()
lb.mu.Lock()
lb.parseWgStatusLocked(&wgengine.Status{
Peers: []ipnstate.PeerStatusLite{{
TxBytes: 11,
RxBytes: 12,
LastHandshake: time.Now(),
NodeKey: tailcfg.NodeKey(key.NewPrivate()),
}},
})
lb.mu.Unlock()
t.Run("after_second_peer_status", testWantRemain("SetPrefs: %v"))
}

View File

@@ -11,6 +11,7 @@ import (
"hash/crc32"
"html"
"io"
"io/fs"
"net"
"net/http"
"net/url"
@@ -18,11 +19,18 @@ import (
"path"
"path/filepath"
"runtime"
"sort"
"strconv"
"strings"
"sync"
"time"
"unicode"
"unicode/utf8"
"inet.af/netaddr"
"tailscale.com/client/tailscale/apitype"
"tailscale.com/ipn"
"tailscale.com/logtail/backoff"
"tailscale.com/net/interfaces"
"tailscale.com/syncs"
"tailscale.com/tailcfg"
@@ -37,25 +45,77 @@ type peerAPIServer struct {
tunName string
selfNode *tailcfg.Node
knownEmpty syncs.AtomicBool
// directFileMode is whether we're writing files directly to a
// download directory (as *.partial files), rather than making
// the frontend retrieve it over localapi HTTP and write it
// somewhere itself. This is used on GUI macOS version.
// In directFileMode, the peerapi doesn't do the final rename
// from "foo.jpg.partial" to "foo.jpg".
directFileMode bool
}
const partialSuffix = ".tspartial"
const (
// partialSuffix is the suffix appened to files while they're
// still in the process of being transferred.
partialSuffix = ".partial"
// deletedSuffix is the suffix for a deleted marker file
// that's placed next to a file (without the suffix) that we
// tried to delete, but Windows wouldn't let us. These are
// only written on Windows (and in tests), but they're not
// permitted to be uploaded directly on any platform, like
// partial files.
deletedSuffix = ".deleted"
)
func validFilenameRune(r rune) bool {
switch r {
case '/':
return false
case '\\', ':', '*', '"', '<', '>', '|':
// Invalid stuff on Windows, but we reject them everywhere
// for now.
// TODO(bradfitz): figure out a better plan. We initially just
// wrote things to disk URL path-escaped, but that's gross
// when debugging, and just moves the problem to callers.
// So now we put the UTF-8 filenames on disk directly as
// sent.
return false
}
return unicode.IsPrint(r)
}
func (s *peerAPIServer) diskPath(baseName string) (fullPath string, ok bool) {
if !utf8.ValidString(baseName) {
return "", false
}
if strings.TrimSpace(baseName) != baseName {
return "", false
}
if len(baseName) > 255 {
return "", false
}
// TODO: validate unicode normalization form too? Varies by platform.
clean := path.Clean(baseName)
if clean != baseName ||
clean == "." ||
strings.ContainsAny(clean, `/\`) ||
clean == "." || clean == ".." ||
strings.HasSuffix(clean, deletedSuffix) ||
strings.HasSuffix(clean, partialSuffix) {
return "", false
}
return filepath.Join(s.rootDir, strings.ReplaceAll(url.PathEscape(baseName), ":", "%3a")), true
for _, r := range baseName {
if !validFilenameRune(r) {
return "", false
}
}
return filepath.Join(s.rootDir, baseName), true
}
// hasFilesWaiting reports whether any files are buffered in the
// tailscaled daemon storage.
func (s *peerAPIServer) hasFilesWaiting() bool {
if s.rootDir == "" {
if s.rootDir == "" || s.directFileMode {
return false
}
if s.knownEmpty.Get() {
@@ -74,11 +134,28 @@ func (s *peerAPIServer) hasFilesWaiting() bool {
for {
des, err := f.ReadDir(10)
for _, de := range des {
if strings.HasSuffix(de.Name(), partialSuffix) {
name := de.Name()
if strings.HasSuffix(name, partialSuffix) {
continue
}
if strings.HasSuffix(name, deletedSuffix) { // for Windows + tests
// After we're done looping over files, then try
// to delete this file. Don't do it proactively,
// as the OS may return "foo.jpg.deleted" before "foo.jpg"
// and we don't want to delete the ".deleted" file before
// enumerating to the "foo.jpg" file.
defer tryDeleteAgain(filepath.Join(s.rootDir, strings.TrimSuffix(name, deletedSuffix)))
continue
}
if de.Type().IsRegular() {
return true
_, err := os.Stat(filepath.Join(s.rootDir, name+deletedSuffix))
if os.IsNotExist(err) {
return true
}
if err == nil {
tryDeleteAgain(filepath.Join(s.rootDir, name))
continue
}
}
}
if err == io.EOF {
@@ -91,22 +168,25 @@ func (s *peerAPIServer) hasFilesWaiting() bool {
return false
}
// WaitingFile is a JSON-marshaled struct sent by the localapi to pick
// up queued files.
type WaitingFile struct {
Name string
Size int64
}
func (s *peerAPIServer) WaitingFiles() (ret []WaitingFile, err error) {
// WaitingFiles returns the list of files that have been sent by a
// peer that are waiting in the buffered "pick up" directory owned by
// the Tailscale daemon.
//
// As a side effect, it also does any lazy deletion of files as
// required by Windows.
func (s *peerAPIServer) WaitingFiles() (ret []apitype.WaitingFile, err error) {
if s.rootDir == "" {
return nil, errors.New("peerapi disabled; no storage configured")
}
if s.directFileMode {
return nil, nil
}
f, err := os.Open(s.rootDir)
if err != nil {
return nil, err
}
defer f.Close()
var deleted map[string]bool // "foo.jpg" => true (if "foo.jpg.deleted" exists)
for {
des, err := f.ReadDir(10)
for _, de := range des {
@@ -114,12 +194,19 @@ func (s *peerAPIServer) WaitingFiles() (ret []WaitingFile, err error) {
if strings.HasSuffix(name, partialSuffix) {
continue
}
if strings.HasSuffix(name, deletedSuffix) { // for Windows + tests
if deleted == nil {
deleted = map[string]bool{}
}
deleted[strings.TrimSuffix(name, deletedSuffix)] = true
continue
}
if de.Type().IsRegular() {
fi, err := de.Info()
if err != nil {
continue
}
ret = append(ret, WaitingFile{
ret = append(ret, apitype.WaitingFile{
Name: filepath.Base(name),
Size: fi.Size(),
})
@@ -132,40 +219,130 @@ func (s *peerAPIServer) WaitingFiles() (ret []WaitingFile, err error) {
return nil, err
}
}
if len(deleted) > 0 {
// Filter out any return values "foo.jpg" where a
// "foo.jpg.deleted" marker file exists on disk.
all := ret
ret = ret[:0]
for _, wf := range all {
if !deleted[wf.Name] {
ret = append(ret, wf)
}
}
// And do some opportunistic deleting while we're here.
// Maybe Windows is done virus scanning the file we tried
// to delete a long time ago and will let us delete it now.
for name := range deleted {
tryDeleteAgain(filepath.Join(s.rootDir, name))
}
}
sort.Slice(ret, func(i, j int) bool { return ret[i].Name < ret[j].Name })
return ret, nil
}
// tryDeleteAgain tries to delete path (and path+deletedSuffix) after
// it failed earlier. This happens on Windows when various anti-virus
// tools hook into filesystem operations and have the file open still
// while we're trying to delete it. In that case we instead mark it as
// deleted (writing a "foo.jpg.deleted" marker file), but then we
// later try to clean them up.
//
// fullPath is the full path to the file without the deleted suffix.
func tryDeleteAgain(fullPath string) {
if err := os.Remove(fullPath); err == nil || os.IsNotExist(err) {
os.Remove(fullPath + deletedSuffix)
}
}
func (s *peerAPIServer) DeleteFile(baseName string) error {
if s.rootDir == "" {
return errors.New("peerapi disabled; no storage configured")
}
if s.directFileMode {
return errors.New("deletes not allowed in direct mode")
}
path, ok := s.diskPath(baseName)
if !ok {
return errors.New("bad filename")
}
err := os.Remove(path)
if err != nil && !os.IsNotExist(err) {
return err
var bo *backoff.Backoff
logf := s.b.logf
t0 := time.Now()
for {
err := os.Remove(path)
if err != nil && !os.IsNotExist(err) {
err = redactErr(err)
// Put a retry loop around deletes on Windows. Windows
// file descriptor closes are effectively asynchronous,
// as a bunch of hooks run on/after close, and we can't
// necessarily delete the file for a while after close,
// as we need to wait for everybody to be done with
// it. (on Windows, unlike Unix, a file can't be deleted
// if it's open anywhere)
// So try a few times but ultimately just leave a
// "foo.jpg.deleted" marker file to note that it's
// deleted and we clean it up later.
if runtime.GOOS == "windows" {
if bo == nil {
bo = backoff.NewBackoff("delete-retry", logf, 1*time.Second)
}
if time.Since(t0) < 5*time.Second {
bo.BackOff(context.Background(), err)
continue
}
if err := touchFile(path + deletedSuffix); err != nil {
logf("peerapi: failed to leave deleted marker: %v", err)
}
}
logf("peerapi: failed to DeleteFile: %v", err)
return err
}
return nil
}
return nil
}
// redacted is a fake path name we use in errors, to avoid
// accidentally logging actual filenames anywhere.
const redacted = "redacted"
func redactErr(err error) error {
if pe, ok := err.(*os.PathError); ok {
pe.Path = redacted
}
return err
}
func touchFile(path string) error {
f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE, 0666)
if err != nil {
return redactErr(err)
}
return f.Close()
}
func (s *peerAPIServer) OpenFile(baseName string) (rc io.ReadCloser, size int64, err error) {
if s.rootDir == "" {
return nil, 0, errors.New("peerapi disabled; no storage configured")
}
if s.directFileMode {
return nil, 0, errors.New("opens not allowed in direct mode")
}
path, ok := s.diskPath(baseName)
if !ok {
return nil, 0, errors.New("bad filename")
}
if fi, err := os.Stat(path + deletedSuffix); err == nil && fi.Mode().IsRegular() {
tryDeleteAgain(path)
return nil, 0, &fs.PathError{Op: "open", Path: redacted, Err: fs.ErrNotExist}
}
f, err := os.Open(path)
if err != nil {
return nil, 0, err
return nil, 0, redactErr(err)
}
fi, err := f.Stat()
if err != nil {
f.Close()
return nil, 0, err
return nil, 0, redactErr(err)
}
return f, fi.Size(), nil
}
@@ -279,7 +456,6 @@ func (pln *peerAPIListener) serve() {
remoteAddr: ipp,
peerNode: peerNode,
peerUser: peerUser,
lb: pln.lb,
}
httpServer := &http.Server{
Handler: h,
@@ -313,7 +489,6 @@ type peerAPIHandler struct {
isSelf bool // whether peerNode is owned by same user as this node
peerNode *tailcfg.Node // peerNode is who's making the request
peerUser tailcfg.UserProfile // profile of peerNode
lb *LocalBackend
}
func (h *peerAPIHandler) logf(format string, a ...interface{}) {
@@ -322,7 +497,11 @@ func (h *peerAPIHandler) logf(format string, a ...interface{}) {
func (h *peerAPIHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if strings.HasPrefix(r.URL.Path, "/v0/put/") {
h.put(w, r)
h.handlePeerPut(w, r)
return
}
if r.URL.Path == "/v0/goroutines" {
h.handleServeGoroutines(w, r)
return
}
who := h.peerUser.DisplayName
@@ -331,65 +510,179 @@ func (h *peerAPIHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
<body>
<h1>Hello, %s (%v)</h1>
This is my Tailscale device. Your device is %v.
`, html.EscapeString(who), h.remoteAddr.IP, html.EscapeString(h.peerNode.ComputedName))
`, html.EscapeString(who), h.remoteAddr.IP(), html.EscapeString(h.peerNode.ComputedName))
if h.isSelf {
fmt.Fprintf(w, "<p>You are the owner of this node.\n")
}
}
func (h *peerAPIHandler) put(w http.ResponseWriter, r *http.Request) {
type incomingFile struct {
name string // "foo.jpg"
started time.Time
size int64 // or -1 if unknown; never 0
w io.Writer // underlying writer
ph *peerAPIHandler
partialPath string // non-empty in direct mode
mu sync.Mutex
copied int64
done bool
lastNotify time.Time
}
func (f *incomingFile) markAndNotifyDone() {
f.mu.Lock()
f.done = true
f.mu.Unlock()
b := f.ph.ps.b
b.sendFileNotify()
}
func (f *incomingFile) Write(p []byte) (n int, err error) {
n, err = f.w.Write(p)
b := f.ph.ps.b
var needNotify bool
defer func() {
if needNotify {
b.sendFileNotify()
}
}()
if n > 0 {
f.mu.Lock()
defer f.mu.Unlock()
f.copied += int64(n)
now := time.Now()
if f.lastNotify.IsZero() || now.Sub(f.lastNotify) > time.Second {
f.lastNotify = now
needNotify = true
}
}
return n, err
}
func (f *incomingFile) PartialFile() ipn.PartialFile {
f.mu.Lock()
defer f.mu.Unlock()
return ipn.PartialFile{
Name: f.name,
Started: f.started,
DeclaredSize: f.size,
Received: f.copied,
PartialPath: f.partialPath,
Done: f.done,
}
}
func (h *peerAPIHandler) handlePeerPut(w http.ResponseWriter, r *http.Request) {
if !h.isSelf {
http.Error(w, "not owner", http.StatusForbidden)
return
}
if !h.ps.b.hasCapFileSharing() {
http.Error(w, "file sharing not enabled by Tailscale admin", http.StatusForbidden)
return
}
if r.Method != "PUT" {
http.Error(w, "not method PUT", http.StatusMethodNotAllowed)
http.Error(w, "expected method PUT", http.StatusMethodNotAllowed)
return
}
if h.ps.rootDir == "" {
http.Error(w, "no rootdir", http.StatusInternalServerError)
return
}
baseName := path.Base(r.URL.Path)
rawPath := r.URL.EscapedPath()
suffix := strings.TrimPrefix(rawPath, "/v0/put/")
if suffix == rawPath {
http.Error(w, "misconfigured internals", 500)
return
}
if suffix == "" {
http.Error(w, "empty filename", 400)
return
}
if strings.Contains(suffix, "/") {
http.Error(w, "directories not supported", 400)
return
}
baseName, err := url.PathUnescape(suffix)
if err != nil {
http.Error(w, "bad path encoding", 400)
return
}
dstFile, ok := h.ps.diskPath(baseName)
if !ok {
http.Error(w, "bad filename", 400)
return
}
f, err := os.Create(dstFile)
t0 := time.Now()
// TODO(bradfitz): prevent same filename being sent by two peers at once
partialFile := dstFile + partialSuffix
f, err := os.Create(partialFile)
if err != nil {
h.logf("put Create error: %v", err)
h.logf("put Create error: %v", redactErr(err))
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
var success bool
defer func() {
if !success {
os.Remove(dstFile)
os.Remove(partialFile)
}
}()
n, err := io.Copy(f, r.Body)
if err != nil {
f.Close()
h.logf("put Copy error: %v", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
var finalSize int64
var inFile *incomingFile
if r.ContentLength != 0 {
inFile = &incomingFile{
name: baseName,
started: time.Now(),
size: r.ContentLength,
w: f,
ph: h,
}
if h.ps.directFileMode {
inFile.partialPath = partialFile
}
h.ps.b.registerIncomingFile(inFile, true)
defer h.ps.b.registerIncomingFile(inFile, false)
n, err := io.Copy(inFile, r.Body)
if err != nil {
err = redactErr(err)
f.Close()
h.logf("put Copy error: %v", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
finalSize = n
}
if err := f.Close(); err != nil {
if err := redactErr(f.Close()); err != nil {
h.logf("put Close error: %v", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if h.ps.directFileMode {
if inFile != nil { // non-zero length; TODO: notify even for zero length
inFile.markAndNotifyDone()
}
} else {
if err := os.Rename(partialFile, dstFile); err != nil {
err = redactErr(err)
h.logf("put final rename: %v", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
h.logf("put of %s from %v/%v", baseName, approxSize(n), h.remoteAddr.IP, h.peerNode.ComputedName)
d := time.Since(t0).Round(time.Second / 10)
h.logf("got put of %s in %v from %v/%v", approxSize(finalSize), d, h.remoteAddr.IP, h.peerNode.ComputedName)
// TODO: set modtime
// TODO: some real response
success = true
io.WriteString(w, "{}\n")
h.ps.knownEmpty.Set(false)
h.ps.b.send(ipn.Notify{}) // it will set FilesWaiting
h.ps.b.sendFileNotify()
}
func approxSize(n int64) string {
@@ -399,5 +692,21 @@ func approxSize(n int64) string {
if n <= 1<<20 {
return "<=1MB"
}
return fmt.Sprintf("~%dMB", n/1<<20)
return fmt.Sprintf("~%dMB", n>>20)
}
func (h *peerAPIHandler) handleServeGoroutines(w http.ResponseWriter, r *http.Request) {
if !h.isSelf {
http.Error(w, "not owner", http.StatusForbidden)
return
}
var buf []byte
for size := 4 << 10; size <= 2<<20; size *= 2 {
buf = make([]byte, size)
buf = buf[:runtime.Stack(buf, true)]
if len(buf) < size {
break
}
}
w.Write(buf)
}

View File

@@ -2,24 +2,25 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build darwin,redo ios,redo
//go:build (darwin && ts_macext) || (ios && ts_macext)
// +build darwin,ts_macext ios,ts_macext
package ipnlocal
import (
"errors"
"fmt"
"log"
"net"
"strings"
"syscall"
"golang.org/x/sys/unix"
"inet.af/netaddr"
"tailscale.com/net/interfaces"
"tailscale.com/net/netns"
)
func init() {
initListenConfig = initListenConfigNetworkExtension
peerDialControlFunc = peerDialControlFuncNetworkExtension
}
// initListenConfigNetworkExtension configures nc for listening on IP
@@ -30,25 +31,26 @@ func initListenConfigNetworkExtension(nc *net.ListenConfig, ip netaddr.IP, st *i
if !ok {
return fmt.Errorf("no interface with name %q", tunIfName)
}
nc.Control = func(network, address string, c syscall.RawConn) error {
var sockErr error
err := c.Control(func(fd uintptr) {
v6 := strings.Contains(address, "]:") || strings.HasSuffix(network, "6") // hacky test for v6
proto := unix.IPPROTO_IP
opt := unix.IP_BOUND_IF
if v6 {
proto = unix.IPPROTO_IPV6
opt = unix.IPV6_BOUND_IF
}
sockErr = unix.SetsockoptInt(int(fd), proto, opt, tunIf.Index)
log.Printf("peerapi: bind(%q, %q) on index %v = %v", network, address, tunIf.Index, sockErr)
})
if err != nil {
return err
}
return sockErr
}
return nil
return netns.SetListenConfigInterfaceIndex(nc, tunIf.Index)
}
func peerDialControlFuncNetworkExtension(b *LocalBackend) func(network, address string, c syscall.RawConn) error {
b.mu.Lock()
defer b.mu.Unlock()
st := b.prevIfState
pas := b.peerAPIServer
index := -1
if st != nil && pas != nil && pas.tunName != "" {
if tunIf, ok := st.Interface[pas.tunName]; ok {
index = tunIf.Index
}
}
var lc net.ListenConfig
netns.SetListenConfigInterfaceIndex(&lc, index)
return func(network, address string, c syscall.RawConn) error {
if index == -1 {
return errors.New("failed to find TUN interface to bind to")
}
return lc.Control(network, address, c)
}
}

View File

@@ -0,0 +1,573 @@
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package ipnlocal
import (
"bytes"
"fmt"
"io"
"io/fs"
"io/ioutil"
"math/rand"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"runtime"
"strings"
"testing"
"tailscale.com/tailcfg"
)
type peerAPITestEnv struct {
ph *peerAPIHandler
rr *httptest.ResponseRecorder
logBuf bytes.Buffer
}
func (e *peerAPITestEnv) logf(format string, a ...interface{}) {
fmt.Fprintf(&e.logBuf, format, a...)
}
type check func(*testing.T, *peerAPITestEnv)
func checks(vv ...check) []check { return vv }
func httpStatus(wantStatus int) check {
return func(t *testing.T, e *peerAPITestEnv) {
if res := e.rr.Result(); res.StatusCode != wantStatus {
t.Errorf("HTTP response code = %v; want %v", res.Status, wantStatus)
}
}
}
func bodyContains(sub string) check {
return func(t *testing.T, e *peerAPITestEnv) {
if body := e.rr.Body.String(); !strings.Contains(body, sub) {
t.Errorf("HTTP response body does not contain %q; got: %s", sub, body)
}
}
}
func bodyNotContains(sub string) check {
return func(t *testing.T, e *peerAPITestEnv) {
if body := e.rr.Body.String(); strings.Contains(body, sub) {
t.Errorf("HTTP response body unexpectedly contains %q; got: %s", sub, body)
}
}
}
func fileHasSize(name string, size int) check {
return func(t *testing.T, e *peerAPITestEnv) {
root := e.ph.ps.rootDir
if root == "" {
t.Errorf("no rootdir; can't check whether %q has size %v", name, size)
return
}
path := filepath.Join(root, name)
if fi, err := os.Stat(path); err != nil {
t.Errorf("fileHasSize(%q, %v): %v", name, size, err)
} else if fi.Size() != int64(size) {
t.Errorf("file %q has size %v; want %v", name, fi.Size(), size)
}
}
}
func fileHasContents(name string, want string) check {
return func(t *testing.T, e *peerAPITestEnv) {
root := e.ph.ps.rootDir
if root == "" {
t.Errorf("no rootdir; can't check contents of %q", name)
return
}
path := filepath.Join(root, name)
got, err := ioutil.ReadFile(path)
if err != nil {
t.Errorf("fileHasContents: %v", err)
return
}
if string(got) != want {
t.Errorf("file contents = %q; want %q", got, want)
}
}
}
func hexAll(v string) string {
var sb strings.Builder
for i := 0; i < len(v); i++ {
fmt.Fprintf(&sb, "%%%02x", v[i])
}
return sb.String()
}
func TestHandlePeerAPI(t *testing.T) {
tests := []struct {
name string
isSelf bool // the peer sending the request is owned by us
capSharing bool // self node has file sharing capabilty
omitRoot bool // don't configure
req *http.Request
checks []check
}{
{
name: "not_peer_api",
isSelf: true,
capSharing: true,
req: httptest.NewRequest("GET", "/", nil),
checks: checks(
httpStatus(200),
bodyContains("This is my Tailscale device."),
bodyContains("You are the owner of this node."),
),
},
{
name: "not_peer_api_not_owner",
isSelf: false,
capSharing: true,
req: httptest.NewRequest("GET", "/", nil),
checks: checks(
httpStatus(200),
bodyContains("This is my Tailscale device."),
bodyNotContains("You are the owner of this node."),
),
},
{
name: "peer_api_goroutines_deny",
isSelf: false,
req: httptest.NewRequest("GET", "/v0/goroutines", nil),
checks: checks(httpStatus(403)),
},
{
name: "peer_api_goroutines",
isSelf: true,
req: httptest.NewRequest("GET", "/v0/goroutines", nil),
checks: checks(
httpStatus(200),
bodyContains("ServeHTTP"),
),
},
{
name: "reject_non_owner_put",
isSelf: false,
capSharing: true,
req: httptest.NewRequest("PUT", "/v0/put/foo", nil),
checks: checks(
httpStatus(http.StatusForbidden),
bodyContains("not owner"),
),
},
{
name: "owner_without_cap",
isSelf: true,
capSharing: false,
req: httptest.NewRequest("PUT", "/v0/put/foo", nil),
checks: checks(
httpStatus(http.StatusForbidden),
bodyContains("file sharing not enabled by Tailscale admin"),
),
},
{
name: "owner_with_cap_no_rootdir",
omitRoot: true,
isSelf: true,
capSharing: true,
req: httptest.NewRequest("PUT", "/v0/put/foo", nil),
checks: checks(
httpStatus(http.StatusInternalServerError),
bodyContains("no rootdir"),
),
},
{
name: "bad_method",
isSelf: true,
capSharing: true,
req: httptest.NewRequest("POST", "/v0/put/foo", nil),
checks: checks(
httpStatus(405),
bodyContains("expected method PUT"),
),
},
{
name: "put_zero_length",
isSelf: true,
capSharing: true,
req: httptest.NewRequest("PUT", "/v0/put/foo", nil),
checks: checks(
httpStatus(200),
bodyContains("{}"),
fileHasSize("foo", 0),
fileHasContents("foo", ""),
),
},
{
name: "put_non_zero_length_content_length",
isSelf: true,
capSharing: true,
req: httptest.NewRequest("PUT", "/v0/put/foo", strings.NewReader("contents")),
checks: checks(
httpStatus(200),
bodyContains("{}"),
fileHasSize("foo", len("contents")),
fileHasContents("foo", "contents"),
),
},
{
name: "put_non_zero_length_chunked",
isSelf: true,
capSharing: true,
req: httptest.NewRequest("PUT", "/v0/put/foo", struct{ io.Reader }{strings.NewReader("contents")}),
checks: checks(
httpStatus(200),
bodyContains("{}"),
fileHasSize("foo", len("contents")),
fileHasContents("foo", "contents"),
),
},
{
name: "bad_filename_partial",
isSelf: true,
capSharing: true,
req: httptest.NewRequest("PUT", "/v0/put/foo.partial", nil),
checks: checks(
httpStatus(400),
bodyContains("bad filename"),
),
},
{
name: "bad_filename_deleted",
isSelf: true,
capSharing: true,
req: httptest.NewRequest("PUT", "/v0/put/foo.deleted", nil),
checks: checks(
httpStatus(400),
bodyContains("bad filename"),
),
},
{
name: "bad_filename_dot",
isSelf: true,
capSharing: true,
req: httptest.NewRequest("PUT", "/v0/put/.", nil),
checks: checks(
httpStatus(400),
bodyContains("bad filename"),
),
},
{
name: "bad_filename_empty",
isSelf: true,
capSharing: true,
req: httptest.NewRequest("PUT", "/v0/put/", nil),
checks: checks(
httpStatus(400),
bodyContains("empty filename"),
),
},
{
name: "bad_filename_slash",
isSelf: true,
capSharing: true,
req: httptest.NewRequest("PUT", "/v0/put/foo/bar", nil),
checks: checks(
httpStatus(400),
bodyContains("directories not supported"),
),
},
{
name: "bad_filename_encoded_dot",
isSelf: true,
capSharing: true,
req: httptest.NewRequest("PUT", "/v0/put/"+hexAll("."), nil),
checks: checks(
httpStatus(400),
bodyContains("bad filename"),
),
},
{
name: "bad_filename_encoded_slash",
isSelf: true,
capSharing: true,
req: httptest.NewRequest("PUT", "/v0/put/"+hexAll("/"), nil),
checks: checks(
httpStatus(400),
bodyContains("bad filename"),
),
},
{
name: "bad_filename_encoded_backslash",
isSelf: true,
capSharing: true,
req: httptest.NewRequest("PUT", "/v0/put/"+hexAll("\\"), nil),
checks: checks(
httpStatus(400),
bodyContains("bad filename"),
),
},
{
name: "bad_filename_encoded_dotdot",
isSelf: true,
capSharing: true,
req: httptest.NewRequest("PUT", "/v0/put/"+hexAll(".."), nil),
checks: checks(
httpStatus(400),
bodyContains("bad filename"),
),
},
{
name: "bad_filename_encoded_dotdot_out",
isSelf: true,
capSharing: true,
req: httptest.NewRequest("PUT", "/v0/put/"+hexAll("foo/../../../../../etc/passwd"), nil),
checks: checks(
httpStatus(400),
bodyContains("bad filename"),
),
},
{
name: "put_spaces_and_caps",
isSelf: true,
capSharing: true,
req: httptest.NewRequest("PUT", "/v0/put/"+hexAll("Foo Bar.dat"), strings.NewReader("baz")),
checks: checks(
httpStatus(200),
bodyContains("{}"),
fileHasContents("Foo Bar.dat", "baz"),
),
},
{
name: "put_unicode",
isSelf: true,
capSharing: true,
req: httptest.NewRequest("PUT", "/v0/put/"+hexAll("Томас и его друзья.mp3"), strings.NewReader("главный озорник")),
checks: checks(
httpStatus(200),
bodyContains("{}"),
fileHasContents("Томас и его друзья.mp3", "главный озорник"),
),
},
{
name: "put_invalid_utf8",
isSelf: true,
capSharing: true,
req: httptest.NewRequest("PUT", "/v0/put/"+(hexAll("😜")[:3]), nil),
checks: checks(
httpStatus(400),
bodyContains("bad filename"),
),
},
{
name: "put_invalid_null",
isSelf: true,
capSharing: true,
req: httptest.NewRequest("PUT", "/v0/put/%00", nil),
checks: checks(
httpStatus(400),
bodyContains("bad filename"),
),
},
{
name: "put_invalid_non_printable",
isSelf: true,
capSharing: true,
req: httptest.NewRequest("PUT", "/v0/put/%01", nil),
checks: checks(
httpStatus(400),
bodyContains("bad filename"),
),
},
{
name: "put_invalid_colon",
isSelf: true,
capSharing: true,
req: httptest.NewRequest("PUT", "/v0/put/"+hexAll("nul:"), nil),
checks: checks(
httpStatus(400),
bodyContains("bad filename"),
),
},
{
name: "put_invalid_surrounding_whitespace",
isSelf: true,
capSharing: true,
req: httptest.NewRequest("PUT", "/v0/put/"+hexAll(" foo "), nil),
checks: checks(
httpStatus(400),
bodyContains("bad filename"),
),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var e peerAPITestEnv
lb := &LocalBackend{
logf: e.logf,
capFileSharing: tt.capSharing,
}
e.ph = &peerAPIHandler{
isSelf: tt.isSelf,
peerNode: &tailcfg.Node{
ComputedName: "some-peer-name",
},
ps: &peerAPIServer{
b: lb,
},
}
var rootDir string
if !tt.omitRoot {
rootDir = t.TempDir()
e.ph.ps.rootDir = rootDir
}
e.rr = httptest.NewRecorder()
e.ph.ServeHTTP(e.rr, tt.req)
for _, f := range tt.checks {
f(t, &e)
}
if t.Failed() && rootDir != "" {
t.Logf("Contents of %s:", rootDir)
des, _ := fs.ReadDir(os.DirFS(rootDir), ".")
for _, de := range des {
fi, err := de.Info()
if err != nil {
t.Log(err)
} else {
t.Logf(" %v %5d %s", fi.Mode(), fi.Size(), de.Name())
}
}
}
})
}
}
// Windows likes to hold on to file descriptors for some indeterminate
// amount of time after you close them and not let you delete them for
// a bit. So test that we work around that sufficiently.
func TestFileDeleteRace(t *testing.T) {
dir := t.TempDir()
ps := &peerAPIServer{
b: &LocalBackend{
logf: t.Logf,
capFileSharing: true,
},
rootDir: dir,
}
ph := &peerAPIHandler{
isSelf: true,
peerNode: &tailcfg.Node{
ComputedName: "some-peer-name",
},
ps: ps,
}
buf := make([]byte, 2<<20)
for i := 0; i < 30; i++ {
rr := httptest.NewRecorder()
ph.ServeHTTP(rr, httptest.NewRequest("PUT", "/v0/put/foo.txt", bytes.NewReader(buf[:rand.Intn(len(buf))])))
if res := rr.Result(); res.StatusCode != 200 {
t.Fatal(res.Status)
}
wfs, err := ps.WaitingFiles()
if err != nil {
t.Fatal(err)
}
if len(wfs) != 1 {
t.Fatalf("waiting files = %d; want 1", len(wfs))
}
if err := ps.DeleteFile("foo.txt"); err != nil {
t.Fatal(err)
}
wfs, err = ps.WaitingFiles()
if err != nil {
t.Fatal(err)
}
if len(wfs) != 0 {
t.Fatalf("waiting files = %d; want 0", len(wfs))
}
}
}
// Tests "foo.jpg.deleted" marks (for Windows).
func TestDeletedMarkers(t *testing.T) {
dir := t.TempDir()
ps := &peerAPIServer{
b: &LocalBackend{
logf: t.Logf,
capFileSharing: true,
},
rootDir: dir,
}
nothingWaiting := func() {
t.Helper()
ps.knownEmpty.Set(false)
if ps.hasFilesWaiting() {
t.Fatal("unexpected files waiting")
}
}
touch := func(base string) {
t.Helper()
if err := touchFile(filepath.Join(dir, base)); err != nil {
t.Fatal(err)
}
}
wantEmptyTempDir := func() {
t.Helper()
if fis, err := ioutil.ReadDir(dir); err != nil {
t.Fatal(err)
} else if len(fis) > 0 && runtime.GOOS != "windows" {
for _, fi := range fis {
t.Errorf("unexpected file in tempdir: %q", fi.Name())
}
}
}
nothingWaiting()
wantEmptyTempDir()
touch("foo.jpg.deleted")
nothingWaiting()
wantEmptyTempDir()
touch("foo.jpg.deleted")
touch("foo.jpg")
nothingWaiting()
wantEmptyTempDir()
touch("foo.jpg.deleted")
touch("foo.jpg")
wf, err := ps.WaitingFiles()
if err != nil {
t.Fatal(err)
}
if len(wf) != 0 {
t.Fatalf("WaitingFiles = %d; want 0", len(wf))
}
wantEmptyTempDir()
touch("foo.jpg.deleted")
touch("foo.jpg")
if rc, _, err := ps.OpenFile("foo.jpg"); err == nil {
rc.Close()
t.Fatal("unexpected foo.jpg open")
}
wantEmptyTempDir()
// And verify basics still work in non-deleted cases.
touch("foo.jpg")
touch("bar.jpg.deleted")
if wf, err := ps.WaitingFiles(); err != nil {
t.Error(err)
} else if len(wf) != 1 {
t.Errorf("WaitingFiles = %d; want 1", len(wf))
} else if wf[0].Name != "foo.jpg" {
t.Errorf("unexpected waiting file %+v", wf[0])
}
if rc, _, err := ps.OpenFile("foo.jpg"); err != nil {
t.Fatal(err)
} else {
rc.Close()
}
}

919
ipn/ipnlocal/state_test.go Normal file
View File

@@ -0,0 +1,919 @@
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package ipnlocal
import (
"context"
"sync"
"testing"
"time"
qt "github.com/frankban/quicktest"
"tailscale.com/control/controlclient"
"tailscale.com/ipn"
"tailscale.com/syncs"
"tailscale.com/tailcfg"
"tailscale.com/types/empty"
"tailscale.com/types/logger"
"tailscale.com/types/netmap"
"tailscale.com/types/persist"
"tailscale.com/types/wgkey"
"tailscale.com/wgengine"
)
// notifyThrottler receives notifications from an ipn.Backend, blocking
// (with eventual timeout and t.Fatal) if there are too many and complaining
// (also with t.Fatal) if they are too few.
type notifyThrottler struct {
t *testing.T
// ch gets replaced frequently. Lock the mutex before getting or
// setting it, but not while waiting on it.
mu sync.Mutex
ch chan ipn.Notify
}
// expect tells the throttler to expect count upcoming notifications.
func (nt *notifyThrottler) expect(count int) {
nt.mu.Lock()
nt.ch = make(chan ipn.Notify, count)
nt.mu.Unlock()
}
// put adds one notification into the throttler's queue.
func (nt *notifyThrottler) put(n ipn.Notify) {
nt.mu.Lock()
ch := nt.ch
nt.mu.Unlock()
select {
case ch <- n:
return
default:
nt.t.Fatalf("put: channel full: %v", n)
}
}
// drain pulls the notifications out of the queue, asserting that there are
// exactly count notifications that have been put so far.
func (nt *notifyThrottler) drain(count int) []ipn.Notify {
nt.mu.Lock()
ch := nt.ch
nt.mu.Unlock()
nn := []ipn.Notify{}
for i := 0; i < count; i++ {
select {
case n := <-ch:
nn = append(nn, n)
case <-time.After(6 * time.Second):
nt.t.Fatalf("drain: channel empty after %d/%d", i, count)
}
}
// no more notifications expected
close(ch)
return nn
}
// mockControl is a mock implementation of controlclient.Client.
// Much of the backend state machine depends on callbacks and state
// in the controlclient.Client, so by controlling it, we can check that
// the state machine works as expected.
type mockControl struct {
opts controlclient.Options
logf logger.Logf
statusFunc func(controlclient.Status)
mu sync.Mutex
calls []string
authBlocked bool
persist persist.Persist
machineKey wgkey.Private
}
func newMockControl() *mockControl {
return &mockControl{
calls: []string{},
authBlocked: true,
}
}
func (cc *mockControl) SetStatusFunc(fn func(controlclient.Status)) {
cc.statusFunc = fn
}
func (cc *mockControl) populateKeys() (newKeys bool) {
cc.mu.Lock()
defer cc.mu.Unlock()
if cc.machineKey.IsZero() {
cc.logf("Copying machineKey.")
cc.machineKey, _ = cc.opts.GetMachinePrivateKey()
newKeys = true
}
if cc.persist.PrivateNodeKey.IsZero() {
cc.logf("Generating a new nodekey.")
cc.persist.OldPrivateNodeKey = cc.persist.PrivateNodeKey
cc.persist.PrivateNodeKey, _ = wgkey.NewPrivate()
newKeys = true
}
return newKeys
}
// send publishes a controlclient.Status notification upstream.
// (In our tests here, upstream is the ipnlocal.Local instance.)
func (cc *mockControl) send(err error, url string, loginFinished bool, nm *netmap.NetworkMap) {
if cc.statusFunc != nil {
s := controlclient.Status{
URL: url,
NetMap: nm,
Persist: &cc.persist,
}
if err != nil {
s.Err = err.Error()
}
if loginFinished {
s.LoginFinished = &empty.Message{}
} else if url == "" && err == nil && nm == nil {
s.LogoutFinished = &empty.Message{}
}
cc.statusFunc(s)
}
}
// called records that a particular function name was called.
func (cc *mockControl) called(s string) {
cc.mu.Lock()
defer cc.mu.Unlock()
cc.calls = append(cc.calls, s)
}
// getCalls returns the list of functions that have been called since the
// last time getCalls was run.
func (cc *mockControl) getCalls() []string {
cc.mu.Lock()
defer cc.mu.Unlock()
r := cc.calls
cc.calls = []string{}
return r
}
// setAuthBlocked changes the return value of AuthCantContinue.
// Auth is blocked if you haven't called Login, the control server hasn't
// provided an auth URL, or it has provided an auth URL and you haven't
// visited it yet.
func (cc *mockControl) setAuthBlocked(blocked bool) {
cc.mu.Lock()
defer cc.mu.Unlock()
cc.authBlocked = blocked
}
// Shutdown disconnects the client.
//
// Note that in a normal controlclient, Shutdown would be the last thing you
// do before discarding the object. In this mock, we don't actually discard
// the object, but if you see a call to Shutdown, you should always see a
// call to New right after it, if the object continues to be used.
// (Note that "New" is the ccGen function here; it means ipn.Backend wanted
// to create an entirely new controlclient.)
func (cc *mockControl) Shutdown() {
cc.logf("Shutdown")
cc.called("Shutdown")
}
// Login starts a login process.
// Note that in this mock, we don't automatically generate notifications
// about the progress of the login operation. You have to call setAuthBlocked()
// and send() as required by the test.
func (cc *mockControl) Login(t *tailcfg.Oauth2Token, flags controlclient.LoginFlags) {
cc.logf("Login token=%v flags=%v", t, flags)
cc.called("Login")
newKeys := cc.populateKeys()
interact := (flags & controlclient.LoginInteractive) != 0
cc.logf("Login: interact=%v newKeys=%v", interact, newKeys)
cc.setAuthBlocked(interact || newKeys)
}
func (cc *mockControl) StartLogout() {
cc.logf("StartLogout")
cc.called("StartLogout")
}
func (cc *mockControl) Logout(ctx context.Context) error {
cc.logf("Logout")
cc.called("Logout")
return nil
}
func (cc *mockControl) SetPaused(paused bool) {
cc.logf("SetPaused=%v", paused)
if paused {
cc.called("pause")
} else {
cc.called("unpause")
}
}
func (cc *mockControl) AuthCantContinue() bool {
cc.mu.Lock()
defer cc.mu.Unlock()
return cc.authBlocked
}
func (cc *mockControl) SetHostinfo(hi *tailcfg.Hostinfo) {
cc.logf("SetHostinfo: %v", *hi)
cc.called("SetHostinfo")
}
func (cc *mockControl) SetNetInfo(ni *tailcfg.NetInfo) {
cc.called("SetNetinfo")
cc.logf("SetNetInfo: %v", *ni)
cc.called("SetNetInfo")
}
func (cc *mockControl) UpdateEndpoints(localPort uint16, endpoints []tailcfg.Endpoint) {
// validate endpoint information here?
cc.logf("UpdateEndpoints: lp=%v ep=%v", localPort, endpoints)
cc.called("UpdateEndpoints")
}
func (*mockControl) SetDNS(context.Context, *tailcfg.SetDNSRequest) error {
panic("unexpected SetDNS call")
}
// A very precise test of the sequence of function calls generated by
// ipnlocal.Local into its controlclient instance, and the events it
// produces upstream into the UI.
//
// [apenwarr] Normally I'm not a fan of "mock" style tests, but the precise
// sequence of this state machine is so important for writing our multiple
// frontends, that it's worth validating it all in one place.
//
// Any changes that affect this test will most likely require carefully
// re-testing all our GUIs (and the CLI) to make sure we didn't break
// anything.
//
// Note also that this test doesn't have any timers, goroutines, or duplicate
// detection. It expects messages to be produced in exactly the right order,
// with no duplicates, without doing network activity (other than through
// controlclient, which we fake, so there's no network activity there either).
//
// TODO: A few messages that depend on magicsock (which actually might have
// network delays) are just ignored for now, which makes the test
// predictable, but maybe a bit less thorough. This is more of an overall
// state machine test than a test of the wgengine+magicsock integration.
func TestStateMachine(t *testing.T) {
c := qt.New(t)
logf := t.Logf
store := new(testStateStorage)
e, err := wgengine.NewFakeUserspaceEngine(logf, 0)
if err != nil {
t.Fatalf("NewFakeUserspaceEngine: %v", err)
}
cc := newMockControl()
b, err := NewLocalBackend(logf, "logid", store, e)
if err != nil {
t.Fatalf("NewLocalBackend: %v", err)
}
b.SetControlClientGetterForTesting(func(opts controlclient.Options) (controlclient.Client, error) {
cc.mu.Lock()
cc.opts = opts
cc.logf = opts.Logf
cc.authBlocked = true
cc.persist = cc.opts.Persist
cc.mu.Unlock()
cc.logf("ccGen: new mockControl.")
cc.called("New")
return cc, nil
})
notifies := &notifyThrottler{t: t}
notifies.expect(0)
b.SetNotifyCallback(func(n ipn.Notify) {
if n.State != nil ||
n.Prefs != nil ||
n.BrowseToURL != nil ||
n.LoginFinished != nil {
logf("\n%v\n\n", n)
notifies.put(n)
} else {
logf("\n(ignored) %v\n\n", n)
}
})
// Check that it hasn't called us right away.
// The state machine should be idle until we call Start().
c.Assert(cc.getCalls(), qt.HasLen, 0)
// Start the state machine.
// Since !WantRunning by default, it'll create a controlclient,
// but not ask it to do anything yet.
t.Logf("\n\nStart")
notifies.expect(2)
c.Assert(b.Start(ipn.Options{StateKey: ipn.GlobalDaemonStateKey}), qt.IsNil)
{
// BUG: strictly, it should pause, not unpause, here, since !WantRunning.
c.Assert([]string{"New", "unpause"}, qt.DeepEquals, cc.getCalls())
nn := notifies.drain(2)
c.Assert(cc.getCalls(), qt.HasLen, 0)
c.Assert(nn[0].Prefs, qt.Not(qt.IsNil))
c.Assert(nn[1].State, qt.Not(qt.IsNil))
prefs := *nn[0].Prefs
// Note: a totally fresh system has Prefs.LoggedOut=false by
// default. We are logged out, but not because the user asked
// for it, so it doesn't count as Prefs.LoggedOut==true.
c.Assert(nn[0].Prefs.LoggedOut, qt.IsFalse)
c.Assert(prefs.WantRunning, qt.IsFalse)
c.Assert(ipn.NeedsLogin, qt.Equals, *nn[1].State)
c.Assert(ipn.NeedsLogin, qt.Equals, b.State())
}
// Restart the state machine.
// It's designed to handle frontends coming and going sporadically.
// Make the sure the restart not only works, but generates the same
// events as the first time, so UIs always know what to expect.
t.Logf("\n\nStart2")
notifies.expect(2)
c.Assert(b.Start(ipn.Options{StateKey: ipn.GlobalDaemonStateKey}), qt.IsNil)
{
// BUG: strictly, it should pause, not unpause, here, since !WantRunning.
c.Assert([]string{"Shutdown", "unpause", "New", "unpause"}, qt.DeepEquals, cc.getCalls())
nn := notifies.drain(2)
c.Assert(cc.getCalls(), qt.HasLen, 0)
c.Assert(nn[0].Prefs, qt.Not(qt.IsNil))
c.Assert(nn[1].State, qt.Not(qt.IsNil))
c.Assert(nn[0].Prefs.LoggedOut, qt.IsFalse)
c.Assert(nn[0].Prefs.WantRunning, qt.IsFalse)
c.Assert(ipn.NeedsLogin, qt.Equals, *nn[1].State)
c.Assert(ipn.NeedsLogin, qt.Equals, b.State())
}
// Start non-interactive login with no token.
// This will ask controlclient to start its own Login() process,
// then wait for us to respond.
t.Logf("\n\nLogin (noninteractive)")
notifies.expect(0)
b.Login(nil)
{
c.Assert(cc.getCalls(), qt.DeepEquals, []string{"Login"})
notifies.drain(0)
// Note: WantRunning isn't true yet. It'll switch to true
// after a successful login finishes.
// (This behaviour is needed so that b.Login() won't
// start connecting to an old account right away, if one
// exists when you launch another login.)
}
// Attempted non-interactive login with no key; indicate that
// the user needs to visit a login URL.
t.Logf("\n\nLogin (url response)")
notifies.expect(1)
url1 := "http://localhost:1/1"
cc.send(nil, url1, false, nil)
{
c.Assert(cc.getCalls(), qt.DeepEquals, []string{"unpause"})
// ...but backend eats that notification, because the user
// didn't explicitly request interactive login yet, and
// we're already in NeedsLogin state.
nn := notifies.drain(1)
c.Assert(nn[0].Prefs, qt.Not(qt.IsNil))
c.Assert(nn[0].Prefs.LoggedOut, qt.IsFalse)
c.Assert(nn[0].Prefs.WantRunning, qt.IsFalse)
}
// Now we'll try an interactive login.
// Since we provided an interactive URL earlier, this shouldn't
// ask control to do anything. Instead backend will emit an event
// indicating that the UI should browse to the given URL.
t.Logf("\n\nLogin (interactive)")
notifies.expect(1)
b.StartLoginInteractive()
{
nn := notifies.drain(1)
// BUG: UpdateEndpoints shouldn't be called yet.
// We're still not logged in so there's nothing we can do
// with it. (And empirically, it's providing an empty list
// of endpoints.)
c.Assert([]string{"UpdateEndpoints", "unpause"}, qt.DeepEquals, cc.getCalls())
c.Assert(nn[0].BrowseToURL, qt.Not(qt.IsNil))
c.Assert(url1, qt.Equals, *nn[0].BrowseToURL)
}
// Sometimes users press the Login button again, in the middle of
// a login sequence. For example, they might have closed their
// browser window without logging in, or they waited too long and
// the login URL expired. If they start another interactive login,
// we must always get a *new* login URL first.
t.Logf("\n\nLogin2 (interactive)")
notifies.expect(0)
b.StartLoginInteractive()
{
notifies.drain(0)
// backend asks control for another login sequence
c.Assert([]string{"Login"}, qt.DeepEquals, cc.getCalls())
}
// Provide a new interactive login URL.
t.Logf("\n\nLogin2 (url response)")
notifies.expect(1)
url2 := "http://localhost:1/2"
cc.send(nil, url2, false, nil)
{
// BUG: UpdateEndpoints again, this is getting silly.
c.Assert([]string{"UpdateEndpoints", "unpause", "unpause"}, qt.DeepEquals, cc.getCalls())
// This time, backend should emit it to the UI right away,
// because the UI is anxiously awaiting a new URL to visit.
nn := notifies.drain(1)
c.Assert(nn[0].BrowseToURL, qt.Not(qt.IsNil))
c.Assert(url2, qt.Equals, *nn[0].BrowseToURL)
}
// Pretend that the interactive login actually happened.
// Controlclient always sends the netmap and LoginFinished at the
// same time.
// The backend should propagate this upward for the UI.
t.Logf("\n\nLoginFinished")
notifies.expect(3)
cc.setAuthBlocked(false)
cc.persist.LoginName = "user1"
cc.send(nil, "", true, &netmap.NetworkMap{})
{
nn := notifies.drain(3)
// BUG: still too soon for UpdateEndpoints.
//
// Arguably it makes sense to unpause now, since the machine
// authorization status is part of the netmap.
//
// BUG: backend unblocks wgengine at this point, even though
// our machine key is not authorized. It probably should
// wait until it gets into Starting.
// TODO: (Currently this test doesn't detect that bug, but
// it's visible in the logs)
c.Assert([]string{"unpause", "unpause", "UpdateEndpoints", "unpause"}, qt.DeepEquals, cc.getCalls())
c.Assert(nn[0].LoginFinished, qt.Not(qt.IsNil))
c.Assert(nn[1].Prefs, qt.Not(qt.IsNil))
c.Assert(nn[2].State, qt.Not(qt.IsNil))
c.Assert(nn[1].Prefs.Persist.LoginName, qt.Equals, "user1")
c.Assert(ipn.NeedsMachineAuth, qt.Equals, *nn[2].State)
}
// Pretend that the administrator has authorized our machine.
t.Logf("\n\nMachineAuthorized")
notifies.expect(1)
// BUG: the real controlclient sends LoginFinished with every
// notification while it's in StateAuthenticated, but not StateSynced.
// It should send it exactly once, or every time we're authenticated,
// but the current code is brittle.
// (ie. I suspect it would be better to change false->true in send()
// below, and do the same in the real controlclient.)
cc.send(nil, "", false, &netmap.NetworkMap{
MachineStatus: tailcfg.MachineAuthorized,
})
{
nn := notifies.drain(1)
c.Assert([]string{"unpause", "unpause", "UpdateEndpoints", "unpause"}, qt.DeepEquals, cc.getCalls())
c.Assert(nn[0].State, qt.Not(qt.IsNil))
c.Assert(ipn.Starting, qt.Equals, *nn[0].State)
}
// TODO: add a fake DERP server to our fake netmap, so we can
// transition to the Running state here.
// TODO: test what happens when the admin forcibly deletes our key.
// (ie. unsolicited logout)
// TODO: test what happens when our key expires, client side.
// (and when it gets close to expiring)
// The user changes their preference to !WantRunning.
t.Logf("\n\nWantRunning -> false")
notifies.expect(2)
b.EditPrefs(&ipn.MaskedPrefs{
WantRunningSet: true,
Prefs: ipn.Prefs{WantRunning: false},
})
{
nn := notifies.drain(2)
c.Assert([]string{"pause"}, qt.DeepEquals, cc.getCalls())
// BUG: I would expect Prefs to change first, and state after.
c.Assert(nn[0].State, qt.Not(qt.IsNil))
c.Assert(nn[1].Prefs, qt.Not(qt.IsNil))
c.Assert(ipn.Stopped, qt.Equals, *nn[0].State)
}
// The user changes their preference to WantRunning after all.
t.Logf("\n\nWantRunning -> true")
store.awaitWrite()
notifies.expect(2)
b.EditPrefs(&ipn.MaskedPrefs{
WantRunningSet: true,
Prefs: ipn.Prefs{WantRunning: true},
})
{
nn := notifies.drain(2)
// BUG: UpdateEndpoints isn't needed here.
// BUG: Login isn't needed here. We never logged out.
c.Assert([]string{"Login", "unpause", "UpdateEndpoints", "unpause"}, qt.DeepEquals, cc.getCalls())
// BUG: I would expect Prefs to change first, and state after.
c.Assert(nn[0].State, qt.Not(qt.IsNil))
c.Assert(nn[1].Prefs, qt.Not(qt.IsNil))
c.Assert(ipn.Starting, qt.Equals, *nn[0].State)
c.Assert(store.sawWrite(), qt.IsTrue)
}
// Test the fast-path frontend reconnection.
// This one is very finicky, so we have to force State==Running
// or it won't use the fast path.
// TODO: actually get to State==Running, rather than cheating.
// That'll require spinning up a fake DERP server and putting it in
// the netmap.
t.Logf("\n\nFastpath Start()")
notifies.expect(1)
b.state = ipn.Running
c.Assert(b.Start(ipn.Options{StateKey: ipn.GlobalDaemonStateKey}), qt.IsNil)
{
nn := notifies.drain(1)
c.Assert(cc.getCalls(), qt.HasLen, 0)
c.Assert(nn[0].State, qt.Not(qt.IsNil))
c.Assert(nn[0].LoginFinished, qt.Not(qt.IsNil))
c.Assert(nn[0].NetMap, qt.Not(qt.IsNil))
c.Assert(nn[0].Prefs, qt.Not(qt.IsNil))
}
// undo the state hack above.
b.state = ipn.Starting
// User wants to logout.
store.awaitWrite()
t.Logf("\n\nLogout (async)")
notifies.expect(2)
b.Logout()
{
nn := notifies.drain(2)
c.Assert([]string{"pause", "StartLogout", "pause"}, qt.DeepEquals, cc.getCalls())
c.Assert(nn[0].State, qt.Not(qt.IsNil))
c.Assert(nn[1].Prefs, qt.Not(qt.IsNil))
c.Assert(ipn.Stopped, qt.Equals, *nn[0].State)
c.Assert(nn[1].Prefs.LoggedOut, qt.IsTrue)
c.Assert(nn[1].Prefs.WantRunning, qt.IsFalse)
c.Assert(ipn.Stopped, qt.Equals, b.State())
c.Assert(store.sawWrite(), qt.IsTrue)
}
// Let's make the logout succeed.
t.Logf("\n\nLogout (async) - succeed")
notifies.expect(1)
cc.setAuthBlocked(true)
cc.send(nil, "", false, nil)
{
nn := notifies.drain(1)
c.Assert([]string{"unpause", "unpause"}, qt.DeepEquals, cc.getCalls())
c.Assert(nn[0].State, qt.Not(qt.IsNil))
c.Assert(ipn.NeedsLogin, qt.Equals, *nn[0].State)
c.Assert(b.Prefs().LoggedOut, qt.IsTrue)
c.Assert(b.Prefs().WantRunning, qt.IsFalse)
c.Assert(ipn.NeedsLogin, qt.Equals, b.State())
}
// A second logout should do nothing, since the prefs haven't changed.
t.Logf("\n\nLogout2 (async)")
notifies.expect(0)
b.Logout()
{
notifies.drain(0)
// BUG: the backend has already called StartLogout, and we're
// still logged out. So it shouldn't call it again.
c.Assert([]string{"StartLogout", "unpause"}, qt.DeepEquals, cc.getCalls())
c.Assert(cc.getCalls(), qt.HasLen, 0)
c.Assert(b.Prefs().LoggedOut, qt.IsTrue)
c.Assert(b.Prefs().WantRunning, qt.IsFalse)
c.Assert(ipn.NeedsLogin, qt.Equals, b.State())
}
// Let's acknowledge the second logout too.
t.Logf("\n\nLogout2 (async) - succeed")
notifies.expect(0)
cc.setAuthBlocked(true)
cc.send(nil, "", false, nil)
{
notifies.drain(0)
c.Assert(cc.getCalls(), qt.DeepEquals, []string{"unpause", "unpause"})
c.Assert(b.Prefs().LoggedOut, qt.IsTrue)
c.Assert(b.Prefs().WantRunning, qt.IsFalse)
c.Assert(ipn.NeedsLogin, qt.Equals, b.State())
}
// Try the synchronous logout feature.
t.Logf("\n\nLogout3 (sync)")
notifies.expect(0)
b.LogoutSync(context.Background())
// NOTE: This returns as soon as cc.Logout() returns, which is okay
// I guess, since that's supposed to be synchronous.
{
notifies.drain(0)
c.Assert([]string{"Logout", "unpause"}, qt.DeepEquals, cc.getCalls())
c.Assert(cc.getCalls(), qt.HasLen, 0)
c.Assert(b.Prefs().LoggedOut, qt.IsTrue)
c.Assert(b.Prefs().WantRunning, qt.IsFalse)
c.Assert(ipn.NeedsLogin, qt.Equals, b.State())
}
// Generate the third logout event.
t.Logf("\n\nLogout3 (sync) - succeed")
notifies.expect(0)
cc.setAuthBlocked(true)
cc.send(nil, "", false, nil)
{
notifies.drain(0)
c.Assert(cc.getCalls(), qt.DeepEquals, []string{"unpause", "unpause"})
c.Assert(b.Prefs().LoggedOut, qt.IsTrue)
c.Assert(b.Prefs().WantRunning, qt.IsFalse)
c.Assert(ipn.NeedsLogin, qt.Equals, b.State())
}
// Shut down the backend.
t.Logf("\n\nShutdown")
notifies.expect(0)
b.Shutdown()
{
notifies.drain(0)
// BUG: I expect a transition to ipn.NoState here.
c.Assert(cc.getCalls(), qt.DeepEquals, []string{"Shutdown"})
}
// Oh, you thought we were done? Ha! Now we have to test what
// happens if the user exits and restarts while logged out.
// Note that it's explicitly okay to call b.Start() over and over
// again, every time the frontend reconnects.
// TODO: test user switching between statekeys.
// The frontend restarts!
t.Logf("\n\nStart3")
notifies.expect(2)
c.Assert(b.Start(ipn.Options{StateKey: ipn.GlobalDaemonStateKey}), qt.IsNil)
{
// BUG: We already called Shutdown(), no need to do it again.
// BUG: Way too soon for UpdateEndpoints.
// BUG: don't unpause because we're not logged in.
c.Assert([]string{"Shutdown", "unpause", "New", "UpdateEndpoints", "unpause"}, qt.DeepEquals, cc.getCalls())
nn := notifies.drain(2)
c.Assert(cc.getCalls(), qt.HasLen, 0)
c.Assert(nn[0].Prefs, qt.Not(qt.IsNil))
c.Assert(nn[1].State, qt.Not(qt.IsNil))
c.Assert(nn[0].Prefs.LoggedOut, qt.IsTrue)
c.Assert(nn[0].Prefs.WantRunning, qt.IsFalse)
c.Assert(ipn.NeedsLogin, qt.Equals, *nn[1].State)
c.Assert(ipn.NeedsLogin, qt.Equals, b.State())
}
// Let's break the rules a little. Our control server accepts
// your invalid login attempt, with no need for an interactive login.
// (This simulates an admin reviving a key that you previously
// disabled.)
t.Logf("\n\nLoginFinished3")
notifies.expect(3)
cc.setAuthBlocked(false)
cc.persist.LoginName = "user2"
cc.send(nil, "", true, &netmap.NetworkMap{
MachineStatus: tailcfg.MachineAuthorized,
})
{
nn := notifies.drain(3)
c.Assert([]string{"unpause", "unpause"}, qt.DeepEquals, cc.getCalls())
c.Assert(nn[0].LoginFinished, qt.Not(qt.IsNil))
c.Assert(nn[1].Prefs, qt.Not(qt.IsNil))
c.Assert(nn[2].State, qt.Not(qt.IsNil))
// Prefs after finishing the login, so LoginName updated.
c.Assert(nn[1].Prefs.Persist.LoginName, qt.Equals, "user2")
c.Assert(nn[1].Prefs.LoggedOut, qt.IsFalse)
c.Assert(nn[1].Prefs.WantRunning, qt.IsTrue)
c.Assert(ipn.Starting, qt.Equals, *nn[2].State)
}
// Now we've logged in successfully. Let's disconnect.
t.Logf("\n\nWantRunning -> false")
notifies.expect(2)
b.EditPrefs(&ipn.MaskedPrefs{
WantRunningSet: true,
Prefs: ipn.Prefs{WantRunning: false},
})
{
nn := notifies.drain(2)
c.Assert([]string{"pause"}, qt.DeepEquals, cc.getCalls())
// BUG: I would expect Prefs to change first, and state after.
c.Assert(nn[0].State, qt.Not(qt.IsNil))
c.Assert(nn[1].Prefs, qt.Not(qt.IsNil))
c.Assert(ipn.Stopped, qt.Equals, *nn[0].State)
c.Assert(nn[1].Prefs.LoggedOut, qt.IsFalse)
}
// One more restart, this time with a valid key, but WantRunning=false.
t.Logf("\n\nStart4")
notifies.expect(2)
c.Assert(b.Start(ipn.Options{StateKey: ipn.GlobalDaemonStateKey}), qt.IsNil)
{
// NOTE: cc.Shutdown() is correct here, since we didn't call
// b.Shutdown() explicitly ourselves.
// BUG: UpdateEndpoints should be called here since we're not WantRunning.
// Note: unpause happens because ipn needs to get at least one netmap
// on startup, otherwise UIs can't show the node list, login
// name, etc when in state ipn.Stopped.
// Arguably they shouldn't try. But they currently do.
nn := notifies.drain(2)
c.Assert([]string{"Shutdown", "unpause", "New", "UpdateEndpoints", "Login", "unpause"}, qt.DeepEquals, cc.getCalls())
c.Assert(cc.getCalls(), qt.HasLen, 0)
c.Assert(nn[0].Prefs, qt.Not(qt.IsNil))
c.Assert(nn[1].State, qt.Not(qt.IsNil))
c.Assert(nn[0].Prefs.WantRunning, qt.IsFalse)
c.Assert(nn[0].Prefs.LoggedOut, qt.IsFalse)
c.Assert(ipn.Stopped, qt.Equals, *nn[1].State)
}
// When logged in but !WantRunning, ipn leaves us unpaused to retrieve
// the first netmap. Simulate that netmap being received, after which
// it should pause us, to avoid wasting CPU retrieving unnecessarily
// additional netmap updates.
//
// TODO: really the various GUIs and prefs should be refactored to
// not require the netmap structure at all when starting while
// !WantRunning. That would remove the need for this (or contacting
// the control server at all when stopped).
t.Logf("\n\nStart4 -> netmap")
notifies.expect(0)
cc.send(nil, "", true, &netmap.NetworkMap{
MachineStatus: tailcfg.MachineAuthorized,
})
{
notifies.drain(0)
c.Assert([]string{"pause", "pause"}, qt.DeepEquals, cc.getCalls())
}
// Request connection.
// The state machine didn't call Login() earlier, so now it needs to.
t.Logf("\n\nWantRunning4 -> true")
notifies.expect(2)
b.EditPrefs(&ipn.MaskedPrefs{
WantRunningSet: true,
Prefs: ipn.Prefs{WantRunning: true},
})
{
nn := notifies.drain(2)
c.Assert([]string{"Login", "unpause"}, qt.DeepEquals, cc.getCalls())
// BUG: I would expect Prefs to change first, and state after.
c.Assert(nn[0].State, qt.Not(qt.IsNil))
c.Assert(nn[1].Prefs, qt.Not(qt.IsNil))
c.Assert(ipn.Starting, qt.Equals, *nn[0].State)
}
// Disconnect.
t.Logf("\n\nStop")
notifies.expect(2)
b.EditPrefs(&ipn.MaskedPrefs{
WantRunningSet: true,
Prefs: ipn.Prefs{WantRunning: false},
})
{
nn := notifies.drain(2)
c.Assert([]string{"pause"}, qt.DeepEquals, cc.getCalls())
// BUG: I would expect Prefs to change first, and state after.
c.Assert(nn[0].State, qt.Not(qt.IsNil))
c.Assert(nn[1].Prefs, qt.Not(qt.IsNil))
c.Assert(ipn.Stopped, qt.Equals, *nn[0].State)
}
// We want to try logging in as a different user, while Stopped.
// First, start the login process (without logging out first).
t.Logf("\n\nLoginDifferent")
notifies.expect(1)
b.StartLoginInteractive()
url3 := "http://localhost:1/3"
cc.send(nil, url3, false, nil)
{
nn := notifies.drain(1)
// It might seem like WantRunning should switch to true here,
// but that would be risky since we already have a valid
// user account. It might try to reconnect to the old account
// before the new one is ready. So no change yet.
//
// Because the login hasn't yet completed, the old login
// is still valid, so it's correct that we stay paused.
c.Assert([]string{"Login", "pause"}, qt.DeepEquals, cc.getCalls())
c.Assert(nn[0].BrowseToURL, qt.Not(qt.IsNil))
c.Assert(*nn[0].BrowseToURL, qt.Equals, url3)
}
// Now, let's complete the interactive login, using a different
// user account than before. WantRunning changes to true after an
// interactive login, so we end up unpaused.
t.Logf("\n\nLoginDifferent URL visited")
notifies.expect(3)
cc.persist.LoginName = "user3"
cc.send(nil, "", true, &netmap.NetworkMap{
MachineStatus: tailcfg.MachineAuthorized,
})
{
nn := notifies.drain(3)
// BUG: pause() being called here is a bad sign.
// It means that either the state machine ran at least once
// with the old netmap, or it ran with the new login+netmap
// and !WantRunning. But since it's a fresh and successful
// new login, WantRunning is true, so there was never a
// reason to pause().
c.Assert([]string{"pause", "unpause"}, qt.DeepEquals, cc.getCalls())
c.Assert(nn[0].LoginFinished, qt.Not(qt.IsNil))
c.Assert(nn[1].Prefs, qt.Not(qt.IsNil))
c.Assert(nn[2].State, qt.Not(qt.IsNil))
// Prefs after finishing the login, so LoginName updated.
c.Assert(nn[1].Prefs.Persist.LoginName, qt.Equals, "user3")
c.Assert(nn[1].Prefs.LoggedOut, qt.IsFalse)
c.Assert(nn[1].Prefs.WantRunning, qt.IsTrue)
c.Assert(ipn.Starting, qt.Equals, *nn[2].State)
}
// The last test case is the most common one: restarting when both
// logged in and WantRunning.
t.Logf("\n\nStart5")
notifies.expect(1)
c.Assert(b.Start(ipn.Options{StateKey: ipn.GlobalDaemonStateKey}), qt.IsNil)
{
// NOTE: cc.Shutdown() is correct here, since we didn't call
// b.Shutdown() ourselves.
c.Assert([]string{"Shutdown", "unpause", "New", "UpdateEndpoints", "Login", "unpause"}, qt.DeepEquals, cc.getCalls())
nn := notifies.drain(1)
c.Assert(cc.getCalls(), qt.HasLen, 0)
c.Assert(nn[0].Prefs, qt.Not(qt.IsNil))
c.Assert(nn[0].Prefs.LoggedOut, qt.IsFalse)
c.Assert(nn[0].Prefs.WantRunning, qt.IsTrue)
c.Assert(ipn.NoState, qt.Equals, b.State())
}
// Control server accepts our valid key from before.
t.Logf("\n\nLoginFinished5")
notifies.expect(1)
cc.setAuthBlocked(false)
cc.send(nil, "", true, &netmap.NetworkMap{
MachineStatus: tailcfg.MachineAuthorized,
})
{
nn := notifies.drain(1)
c.Assert([]string{"unpause", "unpause"}, qt.DeepEquals, cc.getCalls())
// NOTE: No LoginFinished message since no interactive
// login was needed.
c.Assert(nn[0].State, qt.Not(qt.IsNil))
c.Assert(ipn.Starting, qt.Equals, *nn[0].State)
// NOTE: No prefs change this time. WantRunning stays true.
// We were in Starting in the first place, so that doesn't
// change either.
c.Assert(ipn.Starting, qt.Equals, b.State())
}
}
type testStateStorage struct {
mem ipn.MemoryStore
written syncs.AtomicBool
}
func (s *testStateStorage) ReadState(id ipn.StateKey) ([]byte, error) {
return s.mem.ReadState(id)
}
func (s *testStateStorage) WriteState(id ipn.StateKey, bs []byte) error {
s.written.Set(true)
return s.mem.WriteState(id, bs)
}
// awaitWrite clears the "I've seen writes" bit, in prep for a future
// call to sawWrite to see if a write arrived.
func (s *testStateStorage) awaitWrite() { s.written.Set(false) }
// sawWrite reports whether there's been a WriteState call since the most
// recent awaitWrite call.
func (s *testStateStorage) sawWrite() bool {
v := s.written.Get()
s.awaitWrite()
return v
}

View File

@@ -6,7 +6,9 @@ package ipnserver
import (
"bufio"
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
@@ -22,7 +24,6 @@ import (
"strconv"
"strings"
"sync"
"sync/atomic"
"syscall"
"time"
@@ -39,9 +40,11 @@ import (
"tailscale.com/safesocket"
"tailscale.com/smallzstd"
"tailscale.com/types/logger"
"tailscale.com/util/groupmember"
"tailscale.com/util/pidowner"
"tailscale.com/util/systemd"
"tailscale.com/version"
"tailscale.com/version/distro"
"tailscale.com/wgengine"
)
@@ -63,16 +66,6 @@ type Options struct {
// waits for a frontend to start it.
AutostartStateKey ipn.StateKey
// LegacyConfigPath optionally specifies the old-style relaynode
// relay.conf location. If both LegacyConfigPath and
// AutostartStateKey are specified and the requested state doesn't
// exist in the backend store, the backend migrates the config
// from LegacyConfigPath.
//
// TODO(danderson): remove some time after the transition to
// tailscaled is done.
LegacyConfigPath string
// SurviveDisconnects specifies how the server reacts to its
// frontend disconnecting. If true, the server keeps running on
// its existing state, and accepts new frontend connections. If
@@ -155,7 +148,7 @@ func (s *server) getConnIdentity(c net.Conn) (ci connIdentity, err error) {
if err != nil {
return ci, fmt.Errorf("parsing local remote: %w", err)
}
if !la.IP.IsLoopback() || !ra.IP.IsLoopback() {
if !la.IP().IsLoopback() || !ra.IP().IsLoopback() {
return ci, errors.New("non-loopback connection")
}
tab, err := netstat.Get()
@@ -261,8 +254,7 @@ func (s *server) serveConn(ctx context.Context, c net.Conn, logf logger.Logf) {
return
}
defer c.Close()
serverToClient := func(b []byte) { ipn.WriteMsg(c, b) }
bs := ipn.NewBackendServer(logf, nil, serverToClient)
bs := ipn.NewBackendServer(logf, nil, jsonNotifier(c, s.logf))
_, occupied := err.(inUseOtherUserError)
if occupied {
bs.SendInUseOtherUserErrorMessage(err.Error())
@@ -297,7 +289,7 @@ func (s *server) serveConn(ctx context.Context, c net.Conn, logf logger.Logf) {
defer s.removeAndCloseConn(c)
logf("[v1] incoming control connection")
if isReadonlyConn(ci, logf) {
if isReadonlyConn(ci, s.b.OperatorUserID(), logf) {
ctx = ipn.ReadonlyContextOf(ctx)
}
@@ -323,7 +315,7 @@ func (s *server) serveConn(ctx context.Context, c net.Conn, logf logger.Logf) {
}
}
func isReadonlyConn(ci connIdentity, logf logger.Logf) bool {
func isReadonlyConn(ci connIdentity, operatorUID string, logf logger.Logf) bool {
if runtime.GOOS == "windows" {
// Windows doesn't need/use this mechanism, at least yet. It
// has a different last-user-wins auth model.
@@ -352,51 +344,36 @@ func isReadonlyConn(ci connIdentity, logf logger.Logf) bool {
logf("connection from userid %v; connection from non-root user matching daemon has access", uid)
return rw
}
var adminGroupID string
switch runtime.GOOS {
case "darwin":
adminGroupID = darwinAdminGroupID()
default:
logf("connection from userid %v; read-only", uid)
return ro
if operatorUID != "" && uid == operatorUID {
logf("connection from userid %v; is configured operator", uid)
return rw
}
if adminGroupID == "" {
logf("connection from userid %v; no system admin group found, read-only", uid)
if yes, err := isLocalAdmin(uid); err != nil {
logf("connection from userid %v; read-only; %v", uid, err)
return ro
}
u, err := user.LookupId(uid)
if err != nil {
logf("connection from userid %v; failed to look up user; read-only", uid)
return ro
}
gids, err := u.GroupIds()
if err != nil {
logf("connection from userid %v; failed to look up groups; read-only", uid)
return ro
}
for _, gid := range gids {
if gid == adminGroupID {
logf("connection from userid %v; is local admin, has access", uid)
return rw
}
} else if yes {
logf("connection from userid %v; is local admin, has access", uid)
return rw
}
logf("connection from userid %v; read-only", uid)
return ro
}
var darwinAdminGroupIDCache atomic.Value // of string
func darwinAdminGroupID() string {
s, _ := darwinAdminGroupIDCache.Load().(string)
if s != "" {
return s
}
g, err := user.LookupGroup("admin")
func isLocalAdmin(uid string) (bool, error) {
u, err := user.LookupId(uid)
if err != nil {
return ""
return false, err
}
darwinAdminGroupIDCache.Store(g.Gid)
return g.Gid
var adminGroup string
switch {
case runtime.GOOS == "darwin":
adminGroup = "admin"
case distro.Get() == distro.QNAP:
adminGroup = "administrators"
default:
return false, fmt.Errorf("no system admin group found")
}
return groupmember.IsMemberOfGroup(adminGroup, u.Username)
}
// inUseOtherUserError is the error type for when the server is in use
@@ -420,12 +397,10 @@ func (s *server) checkConnIdentityLocked(ci connIdentity) error {
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
@@ -445,7 +420,7 @@ func (s *server) localAPIPermissions(ci connIdentity) (read, write bool) {
return false, false
}
if ci.IsUnixSock {
return true, !isReadonlyConn(ci, logger.Discard)
return true, !isReadonlyConn(ci, s.b.OperatorUserID(), logger.Discard)
}
return false, false
}
@@ -482,9 +457,7 @@ func (s *server) addConn(c net.Conn, isHTTP bool) (ci connIdentity, err error) {
defer func() {
if doReset {
s.logf("identity changed; resetting server")
s.bsMu.Lock()
s.bs.Reset(context.TODO())
s.bsMu.Unlock()
s.b.ResetForClientDisconnect()
}
}()
@@ -534,9 +507,7 @@ func (s *server) removeAndCloseConn(c net.Conn) {
s.logf("client disconnected; staying alive in server mode")
} else {
s.logf("client disconnected; stopping server")
s.bsMu.Lock()
s.bs.Reset(context.TODO())
s.bsMu.Unlock()
s.b.ResetForClientDisconnect()
}
}
c.Close()
@@ -577,7 +548,9 @@ func (s *server) setServerModeUserLocked() {
}
}
func (s *server) writeToClients(b []byte) {
var jsonEscapedZero = []byte(`\u0000`)
func (s *server) writeToClients(n ipn.Notify) {
inServerMode := s.b.InServerMode()
s.mu.Lock()
@@ -594,14 +567,24 @@ func (s *server) writeToClients(b []byte) {
}
}
for c := range s.clients {
ipn.WriteMsg(c, b)
if len(s.clients) == 0 {
// Common case (at least on busy servers): nobody
// connected (no GUI, etc), so return before
// serializing JSON.
return
}
if b, ok := marshalNotify(n, s.logf); ok {
for c := range s.clients {
ipn.WriteMsg(c, b)
}
}
}
// Run runs a Tailscale backend service.
// The getEngine func is called repeatedly, once per connection, until it returns an engine successfully.
func Run(ctx context.Context, logf logger.Logf, logid string, getEngine func() (wgengine.Engine, error), opts Options) error {
getEngine = getEngineUntilItWorksWrapper(getEngine)
runDone := make(chan struct{})
defer close(runDone)
@@ -662,46 +645,6 @@ func Run(ctx context.Context, logf logger.Logf, logid string, getEngine func() (
eng, err := getEngine()
if err != nil {
logf("ipnserver: initial getEngine call: %v", err)
// Issue 1187: on Windows, in unattended mode,
// sometimes we try 5 times and fail to create the
// engine before the system's ready. Hack until the
// bug if fixed properly: if we're running in
// unattended mode on Windows, keep trying forever,
// waiting for the machine to be ready (networking to
// come up?) and then dial our own safesocket TCP
// listener to wake up the usual mechanism that lets
// us surface getEngine errors to UI clients. (We
// don't want to just call getEngine in a loop without
// the listener.Accept, as we do want to handle client
// connections so we can tell them about errors)
bootRaceWaitForEngine, bootRaceWaitForEngineCancel := context.WithTimeout(context.Background(), time.Minute)
if runtime.GOOS == "windows" && opts.AutostartStateKey != "" {
logf("ipnserver: in unattended mode, waiting for engine availability")
getEngine = getEngineUntilItWorksWrapper(getEngine)
// Wait for it to be ready.
go func() {
defer bootRaceWaitForEngineCancel()
t0 := time.Now()
for {
time.Sleep(10 * time.Second)
if _, err := getEngine(); err != nil {
logf("ipnserver: unattended mode engine load: %v", err)
continue
}
c, err := net.Dial("tcp", listen.Addr().String())
logf("ipnserver: engine created after %v; waking up Accept: Dial error: %v", time.Since(t0).Round(time.Second), err)
if err == nil {
c.Close()
}
break
}
}()
} else {
bootRaceWaitForEngineCancel()
}
for i := 1; ctx.Err() == nil; i++ {
c, err := listen.Accept()
if err != nil {
@@ -709,7 +652,6 @@ func Run(ctx context.Context, logf logger.Logf, logid string, getEngine func() (
bo.BackOff(ctx, err)
continue
}
<-bootRaceWaitForEngine.Done()
logf("ipnserver: try%d: trying getEngine again...", i)
eng, err = getEngine()
if err == nil {
@@ -721,8 +663,7 @@ func Run(ctx context.Context, logf logger.Logf, logid string, getEngine func() (
errMsg := err.Error()
go func() {
defer c.Close()
serverToClient := func(b []byte) { ipn.WriteMsg(c, b) }
bs := ipn.NewBackendServer(logf, nil, serverToClient)
bs := ipn.NewBackendServer(logf, nil, jsonNotifier(c, logf))
bs.SendErrorMessage(errMsg)
time.Sleep(time.Second)
}()
@@ -754,10 +695,7 @@ func Run(ctx context.Context, logf logger.Logf, logid string, getEngine func() (
server.bs.GotCommand(context.TODO(), &ipn.Command{
Version: version.Long,
Start: &ipn.StartArgs{
Opts: ipn.Options{
StateKey: opts.AutostartStateKey,
LegacyConfigPath: opts.LegacyConfigPath,
},
Opts: ipn.Options{StateKey: opts.AutostartStateKey},
},
})
}
@@ -1015,3 +953,25 @@ func peerPid(entries []netstat.Entry, la, ra netaddr.IPPort) int {
}
return 0
}
// jsonNotifier returns a notify-writer func that writes ipn.Notify
// messages to w.
func jsonNotifier(w io.Writer, logf logger.Logf) func(ipn.Notify) {
return func(n ipn.Notify) {
if b, ok := marshalNotify(n, logf); ok {
ipn.WriteMsg(w, b)
}
}
}
func marshalNotify(n ipn.Notify, logf logger.Logf) (b []byte, ok bool) {
b, err := json.Marshal(n)
if err != nil {
logf("ipnserver: [unexpected] error serializing JSON: %v", err)
return nil, false
}
if bytes.Contains(b, jsonEscapedZero) {
logf("[unexpected] zero byte in BackendServer.send notify message: %q", b)
}
return b, true
}

View File

@@ -45,6 +45,13 @@ type Status struct {
// has MagicDNS enabled.
MagicDNSSuffix string
// CertDomains are the set of DNS names for which the control
// plane server will assist with provisioning TLS
// certificates. See SetDNSRequest for dns-01 ACME challenges
// for e.g. LetsEncrypt. These names are FQDNs without
// trailing periods, and without any "_acme-challenge." prefix.
CertDomains []string
Peer map[key.Public]*PeerStatus
User map[tailcfg.UserID]tailcfg.UserProfile
}
@@ -65,13 +72,15 @@ type PeerStatusLite struct {
}
type PeerStatus struct {
ID tailcfg.StableNodeID
PublicKey key.Public
HostName string // HostInfo's Hostname (not a DNS name or necessarily unique)
DNSName string
OS string // HostInfo.OS
UserID tailcfg.UserID
TailAddr string // Tailscale IP
TailAddrDeprecated string `json:"TailAddr"` // Tailscale IP
TailscaleIPs []netaddr.IP // Tailscale IP(s) assigned to this node
// Endpoints:
Addrs []string
@@ -87,7 +96,15 @@ type PeerStatus struct {
KeepAlive bool
ExitNode bool // true if this is the currently selected exit node.
PeerAPIURL []string
// Active is whether the node was recently active. The
// definition is somewhat undefined but has historically and
// currently means that there was some packet sent to this
// peer in the past two minutes. That definition is subject to
// change.
Active bool
PeerAPIURL []string
Capabilities []string `json:",omitempty"`
// ShareeNode indicates this node exists in the netmap because
// it's owned by a shared-to user and that node might connect
@@ -201,6 +218,9 @@ func (sb *StatusBuilder) AddPeer(peer key.Public, st *PeerStatus) {
return
}
if v := st.ID; v != "" {
e.ID = v
}
if v := st.HostName; v != "" {
e.HostName = v
}
@@ -213,8 +233,11 @@ func (sb *StatusBuilder) AddPeer(peer key.Public, st *PeerStatus) {
if v := st.UserID; v != 0 {
e.UserID = v
}
if v := st.TailAddr; v != "" {
e.TailAddr = v
if v := st.TailAddrDeprecated; v != "" {
e.TailAddrDeprecated = v
}
if v := st.TailscaleIPs; v != nil {
e.TailscaleIPs = v
}
if v := st.OS; v != "" {
e.OS = st.OS
@@ -261,6 +284,9 @@ func (sb *StatusBuilder) AddPeer(peer key.Public, st *PeerStatus) {
if st.ShareeNode {
e.ShareeNode = true
}
if st.Active {
e.Active = true
}
}
type StatusUpdater interface {
@@ -343,13 +369,17 @@ table tbody tr:nth-child(even) td { background-color: #f5f5f5; }
hostNameHTML = "<br>" + html.EscapeString(hostName)
}
var tailAddr string
if len(ps.TailscaleIPs) > 0 {
tailAddr = ps.TailscaleIPs[0].String()
}
f("<tr><td>%s</td><td class=acenter>%s</td>"+
"<td><b>%s</b>%s<div class=\"tailaddr\">%s</div></td><td class=\"acenter owner\">%s</td><td class=\"aright\">%v</td><td class=\"aright\">%v</td><td class=\"aright\">%v</td>",
ps.PublicKey.ShortString(),
osEmoji(ps.OS),
html.EscapeString(dnsName),
hostNameHTML,
ps.TailAddr,
tailAddr,
html.EscapeString(owner),
ps.RxBytes,
ps.TxBytes,
@@ -357,9 +387,7 @@ table tbody tr:nth-child(even) td { background-color: #f5f5f5; }
)
f("<td>")
// TODO: let server report this active bool instead
active := !ps.LastWrite.IsZero() && time.Since(ps.LastWrite) < 2*time.Minute
if active {
if ps.Active {
if ps.Relay != "" && ps.CurAddr == "" {
f("relay <b>%s</b>", html.EscapeString(ps.Relay))
} else if ps.CurAddr != "" {
@@ -437,5 +465,9 @@ func sortKey(ps *PeerStatus) string {
if ps.HostName != "" {
return ps.HostName
}
return ps.TailAddr
// TODO(bradfitz): add PeerStatus.Less and avoid these allocs in a Less func.
if len(ps.TailscaleIPs) > 0 {
return ps.TailscaleIPs[0].String()
}
return string(ps.PublicKey[:])
}

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